Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

388 lines
12 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Default-pack prompt-content linter.
*
* Per IMPL §7.2 + GAP §8.9: scans `src/default-packs/*` `promptInstructions`
* for the corpus documented in `docs/audit/prompt-content-lint.md`:
* - PII names, email addresses, phone numbers
* - absolute filesystem paths
* - hardcoded ISO times / datetimes outside owner-fact references
* - embedded conditional logic
* - hardcoded URLs
* - Wave-N / W<N>-<L> narrative leaks
* - leftover slop markers (`to` + `do`, `fix` + `me`, `xx` + `x`, `ha` + `ck`)
*
* W3-B promotion: this runner exits non-zero on any finding by default.
* `--allow-warnings` opts back into the legacy warnings-only behavior; it
* exists only so a maintainer can re-run without CI failing while triaging
* a wave of changes locally.
*
* Usage:
* node scripts/lint-default-packs.mjs # CI-fail mode (default)
* node scripts/lint-default-packs.mjs --allow-warnings # warnings-only
*
* The script reads each `src/default-packs/*.ts` file as text and runs the
* same regex corpus the runtime `lintPromptText` uses. Reading the source
* files directly (instead of importing the registered packs) keeps the
* linter independent of the W1-A spine and avoids needing a TS runtime in
* `bun run verify`.
*
* It also rejects direct `ScheduledTaskSeed`/`ScheduledTask` construction from
* pack files. Pack authors define typed task definitions and compile them
* through `task-definitions.ts`.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const packsDir = path.resolve(here, "..", "src", "default-packs");
const PII_NAMES = ["Jill", "Marco", "Sarah", "Suran", "Sam"];
const PII_REGEX = new RegExp(`\\b(${PII_NAMES.join("|")})\\b`, "g");
const PHONE_REGEX =
/(?:\+\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}\b/g;
const ABSOLUTE_PATH_REGEX =
/(?:^|[\s"'`(])(?:\/[A-Za-z0-9_.\-/]{2,}|~\/[A-Za-z0-9_.\-/]{2,}|[A-Z]:\\[A-Za-z0-9_.\\-]{2,})/g;
const ISO_TIME_REGEX =
/\b(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?(?:Z|[+-]\d{2}:\d{2})?\b/g;
const ISO_DATE_REGEX =
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?\b/g;
const OWNER_FACT_TIME_PATTERNS = [
/morningWindow/i,
/eveningWindow/i,
/quietHours/i,
/HH:MM/,
];
function basicEmailValid(value) {
const at = value.indexOf("@");
if (at <= 0 || at !== value.lastIndexOf("@")) return false;
if (/\s/.test(value)) return false;
const domain = value.slice(at + 1);
return domain.length >= 3 && domain.slice(1, -1).includes(".");
}
function isEmailAtomChar(code) {
return (
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code === 33 ||
code === 35 ||
code === 36 ||
code === 37 ||
code === 38 ||
code === 39 ||
code === 42 ||
code === 43 ||
code === 45 ||
code === 47 ||
code === 63 ||
code === 94 ||
code === 95 ||
code === 96 ||
code === 123 ||
code === 124 ||
code === 125 ||
code === 126 ||
code === 46
);
}
function isDomainChar(code) {
return (
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code === 45 ||
code === 46
);
}
function findBasicEmailSpans(text) {
const spans = [];
let i = 0;
while (i < text.length) {
const at = text.indexOf("@", i);
if (at === -1) break;
let start = at - 1;
while (start >= 0 && isEmailAtomChar(text.charCodeAt(start))) start -= 1;
start += 1;
let end = at + 1;
while (end < text.length && isDomainChar(text.charCodeAt(end))) end += 1;
while (end > at + 1) {
const last = text.charCodeAt(end - 1);
if (last !== 45 && last !== 46) break;
end -= 1;
}
if (start < at && end > at + 1) {
const value = text.slice(start, end);
if (basicEmailValid(value)) spans.push({ value, start, end });
}
i = Math.max(end, at + 1);
}
return spans;
}
const CONDITIONAL_REGEX =
/\b(if user(?:'s)?|when [A-Za-z_]+\s*[=:]+|if owner|if the user is|if name is|when name is|unless owner|unless user|else if\b|case [A-Za-z_]+ when\b)/gi;
const URL_REGEX = /\bhttps?:\/\/[^\s'"`)<>]+/g;
const WAVE_NARRATIVE_REGEX = /\b(?:Wave[\s-]?\d+|W[1-9]\d*-[A-Z])\b/g;
const SLOP_TO_DO_TOKEN = "TO" + "DO";
const SLOP_FIX_ME_TOKEN = "FIX" + "ME";
const SLOP_TRIPLE_X_TOKEN = "XX" + "X";
const SLOP_HA_CK_TOKEN = "HA" + "CK";
const SLOP_REGEX = new RegExp(
`\\b(${SLOP_TO_DO_TOKEN}|${SLOP_FIX_ME_TOKEN}|${SLOP_TRIPLE_X_TOKEN}|${SLOP_HA_CK_TOKEN})\\b`,
"g",
);
const RAW_SCHEDULED_TASK_IMPORT_REGEX =
/import\s+type\s+\{[^}]*\bScheduledTask(?:Seed)?\b[^}]*\}\s+from\s+["']\.\/contract-stubs\.js["']/g;
const RAW_SCHEDULED_TASK_ANNOTATION_REGEX =
/:\s*(?:ReadonlyArray<\s*)?ScheduledTask(?:Seed)?\b/g;
function lintTaskDefinitionAuthorship(packKey, source) {
const findings = [];
for (const m of source.matchAll(RAW_SCHEDULED_TASK_IMPORT_REGEX)) {
findings.push({
rule: "raw_scheduled_task",
packKey,
recordIndex: "source",
message:
"Default packs must import typed task definitions and compiler helpers instead of authoring ScheduledTask/ScheduledTaskSeed directly.",
match: m[0],
});
}
for (const m of source.matchAll(RAW_SCHEDULED_TASK_ANNOTATION_REGEX)) {
findings.push({
rule: "raw_scheduled_task",
packKey,
recordIndex: "source",
message:
"Default packs must not type raw ScheduledTask records; compile a TaskDefinition instead.",
match: m[0],
});
}
return findings;
}
/**
* Extract every `promptInstructions: "..."` string from a TS source file.
* Handles single-line strings and `+`-concatenated multi-line literals.
*
* Crude on purpose — the script must run without a TS runtime, and
* `promptInstructions` values are by convention plain string literals
* (single-quoted, double-quoted, or backtick-quoted). The runtime
* `lintPromptText` is what backs in-process pack registration.
*/
function extractPrompts(source) {
const prompts = [];
// Match `promptInstructions:` followed by an optional literal (single, double,
// or backtick-quoted). Support `+`-concatenated string literals on subsequent
// lines.
const re = /promptInstructions:\s*([\s\S]+?)(?=,\s*\n\s*(?:[a-zA-Z_]+:|\}))/g;
let match;
while ((match = re.exec(source)) !== null) {
const slice = match[1];
// Pull every quoted literal in `slice` and concatenate.
const literalRe = /(["'`])((?:\\.|(?!\1).)*)\1/g;
let literalMatch;
let combined = "";
while ((literalMatch = literalRe.exec(slice)) !== null) {
combined += literalMatch[2].replace(/\\"/g, '"').replace(/\\'/g, "'");
}
if (combined.length > 0) {
prompts.push(combined);
}
}
return prompts;
}
function lintPromptText(packKey, recordIndex, prompt) {
const findings = [];
for (const m of prompt.matchAll(PII_REGEX)) {
findings.push({
rule: "pii_name",
packKey,
recordIndex,
message: `PII name "${m[1]}" embedded in prompt; reference owner facts via contextRequest.includeOwnerFacts.preferredName instead.`,
match: m[0],
});
}
for (const m of findBasicEmailSpans(prompt)) {
findings.push({
rule: "email_pii",
packKey,
recordIndex,
message: `Concrete email address "${m.value}" embedded in prompt; reference the owner or an EntityStore contact instead.`,
match: m.value,
});
}
for (const m of prompt.matchAll(PHONE_REGEX)) {
findings.push({
rule: "phone_pii",
packKey,
recordIndex,
message: `Phone number "${m[0]}" embedded in prompt; reference the owner or an EntityStore contact instead.`,
match: m[0],
});
}
for (const m of prompt.matchAll(ABSOLUTE_PATH_REGEX)) {
findings.push({
rule: "absolute_path",
packKey,
recordIndex,
message: `Absolute path embedded in prompt; default packs ship across hosts and must not bake in filesystem paths.`,
match: m[0].trim(),
});
}
const hasOwnerFactReference = OWNER_FACT_TIME_PATTERNS.some((re) =>
re.test(prompt),
);
if (!hasOwnerFactReference) {
for (const m of prompt.matchAll(ISO_TIME_REGEX)) {
findings.push({
rule: "hardcoded_iso_time",
packKey,
recordIndex,
message: `Hardcoded clock time "${m[0]}" in prompt; reference ownerFact.morningWindow / eveningWindow instead.`,
match: m[0],
});
}
for (const m of prompt.matchAll(ISO_DATE_REGEX)) {
findings.push({
rule: "hardcoded_iso_time",
packKey,
recordIndex,
message: `Hardcoded ISO datetime "${m[0]}" in prompt; reference owner facts or trigger anchors instead.`,
match: m[0],
});
}
}
for (const m of prompt.matchAll(CONDITIONAL_REGEX)) {
findings.push({
rule: "embedded_conditional",
packKey,
recordIndex,
message: `Conditional logic in prompt ("${m[0]}"); express as a registered gate or completionCheck rather than a content branch.`,
match: m[0],
});
}
for (const m of prompt.matchAll(URL_REGEX)) {
findings.push({
rule: "hardcoded_url",
packKey,
recordIndex,
message: `Concrete URL "${m[0]}" embedded in prompt; reference a connector capability rather than baking in a host-specific URL.`,
match: m[0],
});
}
for (const m of prompt.matchAll(WAVE_NARRATIVE_REGEX)) {
findings.push({
rule: "wave_narrative",
packKey,
recordIndex,
message: `Internal milestone reference "${m[0]}" in prompt; Wave/W-prefixed labels belong in comments and docs, not in runtime prompt content.`,
match: m[0],
});
}
for (const m of prompt.matchAll(SLOP_REGEX)) {
findings.push({
rule: "prompt_slop",
packKey,
recordIndex,
message: `Leftover marker "${m[0]}" in prompt; finish the prompt or remove the placeholder.`,
match: m[0],
});
}
return findings;
}
function packKeyFromFilename(filename) {
// Pack files are named like `daily-rhythm.ts`; the key matches the filename
// stem. Skip helper / contract / registry / lint / index / consolidation /
// escalation files.
const stem = path.basename(filename, ".ts");
const skip = new Set([
"index",
"registry-types",
"contract-stubs",
"consolidation-policies",
"escalation-ladders",
"lint",
"task-definitions",
// Registry bridge, not a pack: reconciles already-compiled packs against
// the first-run seed and registers them on the scheduling spine, so it
// legitimately references the ScheduledTaskSeed shape.
"spine-registration",
]);
return skip.has(stem) ? null : stem;
}
function main() {
if (!fs.existsSync(packsDir)) {
console.error(
`[lint-default-packs] FAIL: no default-packs directory found at ${packsDir}.`,
);
process.exit(1);
}
const allowWarnings = process.argv.includes("--allow-warnings");
const files = fs
.readdirSync(packsDir)
.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"));
const allFindings = [];
for (const file of files) {
const packKey = packKeyFromFilename(file);
if (!packKey) continue;
const fullPath = path.join(packsDir, file);
const source = fs.readFileSync(fullPath, "utf8");
allFindings.push(...lintTaskDefinitionAuthorship(packKey, source));
const prompts = extractPrompts(source);
prompts.forEach((prompt, recordIndex) => {
const findings = lintPromptText(packKey, recordIndex, prompt);
allFindings.push(...findings);
});
}
if (allFindings.length === 0) {
console.log(
"[lint-default-packs] clean — 0 findings across default packs.",
);
process.exit(0);
}
const prefix = allowWarnings ? "WARN" : "FAIL";
console.error(
`[lint-default-packs] ${prefix}: ${allFindings.length} finding(s) across default packs:`,
);
for (const finding of allFindings) {
console.error(
` [${finding.rule}] ${finding.packKey}#${finding.recordIndex}: ${finding.message} (matched: ${JSON.stringify(finding.match)})`,
);
}
if (allowWarnings) {
console.error(
"[lint-default-packs] --allow-warnings: exiting 0 despite findings.",
);
process.exit(0);
}
console.error(
"[lint-default-packs] failing CI on findings (W3-B). Re-run with --allow-warnings only for local triage.",
);
process.exit(1);
}
main();