@event4u/agent-config 14.19.0 → 14.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +108 -156
- package/dist/agent-src/commands/agent-handoff.md +42 -41
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +7 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-C6IPZRDA.js} +79 -12
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-NUMQHLS5.js} +21 -4
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_conformance.js +73 -109
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +41 -11
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +9 -9
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +7257 -6497
- package/dist/install/install.mjs +393 -168
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +497 -118
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/gate-coverage.yml +55 -0
- package/src/config/gate-violation-baselines.json +15 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/install.ts +4 -4
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/skill_scout.ts +69 -0
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -55,9 +55,19 @@ const _TOKEN = TOKEN_RE;
|
|
|
55
55
|
export const _classify_token = classifyToken;
|
|
56
56
|
|
|
57
57
|
export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// A BOUND pragma suppresses only the matches it fingerprints, so the scan
|
|
59
|
+
// runs and its output is filtered at the end. An UNBOUND one keeps the old
|
|
60
|
+
// whole-file bail and reports itself.
|
|
61
|
+
const pragmaForm = sf.pragma_form(CHECK);
|
|
62
|
+
if (pragmaForm === 'legacy') {
|
|
63
|
+
return [sl.legacy_pragma_finding(sf, CHECK)];
|
|
60
64
|
}
|
|
65
|
+
// EVERY exit from this function goes through `finish`. Wrapping only the
|
|
66
|
+
// terminal `return` is the defect that shipped for one commit here: this
|
|
67
|
+
// scan has an early return for an artifact with no `execution:` block, and a
|
|
68
|
+
// bound pragma silently did not apply to exactly those artifacts.
|
|
69
|
+
const finish = (hits: sl.Finding[]): sl.Finding[] =>
|
|
70
|
+
pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
|
|
61
71
|
const out: sl.Finding[] = [];
|
|
62
72
|
for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true })) {
|
|
63
73
|
const tokens = text.match(_TOKEN);
|
|
@@ -78,15 +88,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
|
-
return out;
|
|
91
|
+
return finish(out);
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
interface Args {
|
|
95
|
+
/** Scan base for the bounded root mode; absent = the package root. */
|
|
96
|
+
root?: string;
|
|
85
97
|
json: boolean;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function _argError(msg: string): never {
|
|
89
|
-
process.stderr.write('usage: lint_confusables [-h] [--json]\n');
|
|
101
|
+
process.stderr.write('usage: lint_confusables [-h] [--json] [--root DIR]\n');
|
|
90
102
|
process.stderr.write(`lint_confusables: error: ${msg}\n`);
|
|
91
103
|
process.exit(2);
|
|
92
104
|
}
|
|
@@ -94,12 +106,28 @@ function _argError(msg: string): never {
|
|
|
94
106
|
function parse_args(argv: readonly string[]): Args {
|
|
95
107
|
const out: Args = { json: false };
|
|
96
108
|
const extra: string[] = [];
|
|
109
|
+
let rootPending = false;
|
|
97
110
|
for (const a of argv) {
|
|
111
|
+
if (rootPending) {
|
|
112
|
+
out.root = a;
|
|
113
|
+
rootPending = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
98
116
|
if (a === '-h' || a === '--help') {
|
|
99
|
-
process.stdout.write('usage: lint_confusables [-h] [--json]\n');
|
|
117
|
+
process.stdout.write('usage: lint_confusables [-h] [--json] [--root DIR]\n');
|
|
100
118
|
process.exit(0);
|
|
101
119
|
} else if (a === '--json') {
|
|
102
120
|
out.json = true;
|
|
121
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
122
|
+
// Bounded root mode. Absent, this linter's own default roots stand;
|
|
123
|
+
// present, the same relative roots resolve against DIR instead. Used by
|
|
124
|
+
// the scout to scan a quarantined candidate, and by --self-test fixtures.
|
|
125
|
+
const eq = a.indexOf('=');
|
|
126
|
+
if (eq !== -1) {
|
|
127
|
+
out.root = a.slice(eq + 1);
|
|
128
|
+
} else {
|
|
129
|
+
rootPending = true;
|
|
130
|
+
}
|
|
103
131
|
} else {
|
|
104
132
|
extra.push(a);
|
|
105
133
|
}
|
|
@@ -112,10 +140,11 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
112
140
|
|
|
113
141
|
export function main(argv: readonly string[] | null = null): number {
|
|
114
142
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
143
|
+
const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
|
|
115
144
|
|
|
116
145
|
const findings: sl.Finding[] = [];
|
|
117
146
|
let scanned = 0;
|
|
118
|
-
for (const sf of sl.iter_corpus()) {
|
|
147
|
+
for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
|
|
119
148
|
// Counted on the corpus walk, not on `_scan`'s return: `iter_corpus`
|
|
120
149
|
// skips a root that does not exist, so every root moving at once yields
|
|
121
150
|
// zero files and the report line still says "clean (scanned <roots>)".
|
|
@@ -143,6 +172,13 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
143
172
|
throw exc;
|
|
144
173
|
}
|
|
145
174
|
|
|
175
|
+
// Publish what this child inspected so the umbrella can aggregate a real
|
|
176
|
+
// corpus size rather than a count of children. Same number the assertion
|
|
177
|
+
// above just accepted, so the published figure cannot drift from it.
|
|
178
|
+
if (args.json) {
|
|
179
|
+
sl.report_child_scanned(scanned);
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
if (args.json) {
|
|
147
183
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
148
184
|
process.stdout.write(payload + '\n');
|
|
@@ -821,119 +821,6 @@ function _pyRepr(s: string): string {
|
|
|
821
821
|
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
-
// ── HANDOFF.md artifact validation (workflow-contracts Phase 2) ─────────
|
|
825
|
-
// SEPARATE concern from the cross-wing skill-handoff lint above: this
|
|
826
|
-
// validates the OPTIONAL standing workflow-resume artifact
|
|
827
|
-
// (agents/runtime/state/HANDOFF.md, written by /agent-handoff --file).
|
|
828
|
-
// Same file on purpose — one linter owns everything named "handoff";
|
|
829
|
-
// the naming reconciliation is this comment.
|
|
830
|
-
|
|
831
|
-
export const HANDOFF_ARTIFACT_REQUIRED = [
|
|
832
|
-
'Mode',
|
|
833
|
-
'Contract received',
|
|
834
|
-
'Contract owed',
|
|
835
|
-
'Decisions',
|
|
836
|
-
'Open questions',
|
|
837
|
-
'Next command',
|
|
838
|
-
] as const;
|
|
839
|
-
|
|
840
|
-
/** Returns missing required section headings ([] = valid). */
|
|
841
|
-
export function validate_handoff_artifact(text: string): string[] {
|
|
842
|
-
const missing: string[] = [];
|
|
843
|
-
for (const heading of HANDOFF_ARTIFACT_REQUIRED) {
|
|
844
|
-
const re = new RegExp(`^##\\s+${heading}\\s*$`, 'mi');
|
|
845
|
-
if (!re.test(text)) {
|
|
846
|
-
missing.push(heading);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return missing;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* The body of a `## <heading>` section, up to the next heading of the same or a
|
|
854
|
-
* higher level. Fenced blocks are stripped first so a `##` inside a code fence
|
|
855
|
-
* cannot end the section early — a resume artefact routinely quotes commands.
|
|
856
|
-
*/
|
|
857
|
-
export function handoff_section_body(text: string, heading: string): string | null {
|
|
858
|
-
const withoutFences = text.replace(/^```[\s\S]*?^```\s*$/gm, '');
|
|
859
|
-
const start = new RegExp(`^##\\s+${heading}\\s*$`, 'mi').exec(withoutFences);
|
|
860
|
-
if (start === null) {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
const rest = withoutFences.slice(start.index + start[0].length);
|
|
864
|
-
const next = /^#{1,2}\s+\S/m.exec(rest);
|
|
865
|
-
return next === null ? rest : rest.slice(0, next.index);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// An explicit "there are none" answer. Kept as a closed list rather than a
|
|
869
|
-
// heuristic so the escape cannot be widened by accident: a section saying
|
|
870
|
-
// nothing and a section saying "none" are different facts, and only the first is
|
|
871
|
-
// a defect.
|
|
872
|
-
const _NO_OPEN_QUESTIONS = new Set(['none', 'keine', 'n/a', 'na', 'no open questions', 'nothing']);
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Strip list markers, checkbox glyphs, emphasis and trailing sentence
|
|
876
|
-
* punctuation. The trailing strip is skipped when it would empty the line: a
|
|
877
|
-
* line that is ONLY punctuation (`...`) is a placeholder to name, not an empty
|
|
878
|
-
* section to report as blank, and the two findings say different things.
|
|
879
|
-
*/
|
|
880
|
-
function _bareLine(line: string): string {
|
|
881
|
-
const stripped = line
|
|
882
|
-
.trim()
|
|
883
|
-
.replace(/^[-*+]\s*/, '')
|
|
884
|
-
.replace(/^\[[ x~-]\]\s*/i, '')
|
|
885
|
-
.replace(/[*_`]/g, '')
|
|
886
|
-
.trim()
|
|
887
|
-
.toLowerCase();
|
|
888
|
-
const trimmed = stripped.replace(/[.!]+$/, '').trim();
|
|
889
|
-
return trimmed.length > 0 ? trimmed : stripped;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Shape check on the one required section whose emptiness is the defect rather
|
|
894
|
-
* than a style nit. `validate_handoff_artifact` only tests that the heading
|
|
895
|
-
* exists, so `## Open questions` followed by nothing passed while carrying
|
|
896
|
-
* nothing — and a resume artefact whose open questions are blank hands the next
|
|
897
|
-
* session a false all-clear.
|
|
898
|
-
*
|
|
899
|
-
* **The step this implements claimed the false-positive class was empty because
|
|
900
|
-
* one `?`-terminated line is required. It is not, and the counterexample ships
|
|
901
|
-
* in this repo:** `tests/scripts/lint_handoffs_artifact.test.ts` carries
|
|
902
|
-
* `## Open questions` / `- none`, which is an honest answer and would be forced
|
|
903
|
-
* to grow a fake question. So the check accepts EITHER a real question OR an
|
|
904
|
-
* explicit none-marker, and fires only on a section that answers neither —
|
|
905
|
-
* blank, or a bare placeholder. That keeps the defect the step names without
|
|
906
|
-
* turning a truthful "none" into a lint error.
|
|
907
|
-
*
|
|
908
|
-
* Returns a finding string, or null when the shape holds. A MISSING section is
|
|
909
|
-
* not this check's business — `validate_handoff_artifact` already reports it.
|
|
910
|
-
*/
|
|
911
|
-
export function validate_handoff_open_questions(text: string): string | null {
|
|
912
|
-
const body = handoff_section_body(text, 'Open questions');
|
|
913
|
-
if (body === null) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
const lines = body
|
|
917
|
-
.split('\n')
|
|
918
|
-
.map((l) => l.trim())
|
|
919
|
-
.filter((l) => l.length > 0);
|
|
920
|
-
if (lines.some((l) => l.length > 1 && l.endsWith('?'))) {
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
const bare = lines.map(_bareLine).filter((l) => l.length > 0);
|
|
924
|
-
if (bare.some((l) => _NO_OPEN_QUESTIONS.has(l))) {
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
if (bare.length === 0) {
|
|
928
|
-
return 'section `## Open questions` is empty — carry a `?`-terminated question, or state `none`';
|
|
929
|
-
}
|
|
930
|
-
const placeholder = new Set(['tbd', 'todo', 'wip', '...', '…', '?']);
|
|
931
|
-
if (bare.every((l) => placeholder.has(l))) {
|
|
932
|
-
return 'section `## Open questions` carries only a placeholder — carry a `?`-terminated question, or state `none`';
|
|
933
|
-
}
|
|
934
|
-
return null;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
824
|
/** `git rev-parse HEAD`, or null outside a repository. */
|
|
938
825
|
function _headCommit(): string | null {
|
|
939
826
|
try {
|
|
@@ -978,25 +865,6 @@ export function main(argv?: readonly string[]): number {
|
|
|
978
865
|
process.stdout.write(JSON.stringify(census, null, 2) + '\n');
|
|
979
866
|
return 0;
|
|
980
867
|
}
|
|
981
|
-
if (args.length > 0 && String(args[0]).endsWith('HANDOFF.md')) {
|
|
982
|
-
// artifact mode — validate the workflow-resume file's required fields
|
|
983
|
-
const text = fs.readFileSync(_resolve(args[0] as string), 'utf-8');
|
|
984
|
-
const missing = validate_handoff_artifact(text);
|
|
985
|
-
const shape = validate_handoff_open_questions(text);
|
|
986
|
-
if (missing.length === 0 && shape === null) {
|
|
987
|
-
if (!QUIET) process.stdout.write('✅ HANDOFF artifact: all required fields present\n');
|
|
988
|
-
return 0;
|
|
989
|
-
}
|
|
990
|
-
for (const m of missing) {
|
|
991
|
-
process.stdout.write(`${args[0]}:1:handoff-artifact-missing-field: missing required section \`## ${m}\`\n`);
|
|
992
|
-
}
|
|
993
|
-
if (shape !== null) {
|
|
994
|
-
process.stdout.write(`${args[0]}:1:handoff-artifact-empty-section: ${shape}\n`);
|
|
995
|
-
}
|
|
996
|
-
const problems = missing.length + (shape === null ? 0 : 1);
|
|
997
|
-
process.stderr.write(`❌ ${problems} HANDOFF artifact problem(s)\n`);
|
|
998
|
-
return 1;
|
|
999
|
-
}
|
|
1000
868
|
if (args.length > 0) {
|
|
1001
869
|
skills_dir = _resolve(args[0] as string);
|
|
1002
870
|
}
|
|
@@ -220,9 +220,19 @@ function _cpName(cp: number): string {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// A BOUND pragma suppresses only the matches it fingerprints, so the scan
|
|
224
|
+
// runs and its output is filtered at the end. An UNBOUND one keeps the old
|
|
225
|
+
// whole-file bail and reports itself.
|
|
226
|
+
const pragmaForm = sf.pragma_form(CHECK);
|
|
227
|
+
if (pragmaForm === 'legacy') {
|
|
228
|
+
return [sl.legacy_pragma_finding(sf, CHECK)];
|
|
225
229
|
}
|
|
230
|
+
// EVERY exit from this function goes through `finish`. Wrapping only the
|
|
231
|
+
// terminal `return` is the defect that shipped for one commit here: this
|
|
232
|
+
// scan has an early return for an artifact with no `execution:` block, and a
|
|
233
|
+
// bound pragma silently did not apply to exactly those artifacts.
|
|
234
|
+
const finish = (hits: sl.Finding[]): sl.Finding[] =>
|
|
235
|
+
pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
|
|
226
236
|
const out: sl.Finding[] = [];
|
|
227
237
|
for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true })) {
|
|
228
238
|
let vs_run = 0;
|
|
@@ -260,7 +270,7 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
260
270
|
);
|
|
261
271
|
}
|
|
262
272
|
}
|
|
263
|
-
return out;
|
|
273
|
+
return finish(out);
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
// ---------------------------------------------------------------------------
|
|
@@ -464,12 +474,14 @@ export function _sanitize(filePath: string): string {
|
|
|
464
474
|
}
|
|
465
475
|
|
|
466
476
|
interface Args {
|
|
477
|
+
/** Scan base for the bounded root mode; absent = the package root. */
|
|
478
|
+
root?: string;
|
|
467
479
|
json: boolean;
|
|
468
480
|
fix: boolean;
|
|
469
481
|
}
|
|
470
482
|
|
|
471
483
|
function _argError(msg: string): never {
|
|
472
|
-
process.stderr.write('usage: lint_hidden_unicode [-h] [--json] [--fix]\n');
|
|
484
|
+
process.stderr.write('usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n');
|
|
473
485
|
process.stderr.write(`lint_hidden_unicode: error: ${msg}\n`);
|
|
474
486
|
process.exit(2);
|
|
475
487
|
}
|
|
@@ -477,14 +489,30 @@ function _argError(msg: string): never {
|
|
|
477
489
|
function parse_args(argv: readonly string[]): Args {
|
|
478
490
|
const out: Args = { json: false, fix: false };
|
|
479
491
|
const extra: string[] = [];
|
|
492
|
+
let rootPending = false;
|
|
480
493
|
for (const a of argv) {
|
|
494
|
+
if (rootPending) {
|
|
495
|
+
out.root = a;
|
|
496
|
+
rootPending = false;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
481
499
|
if (a === '-h' || a === '--help') {
|
|
482
|
-
process.stdout.write('usage: lint_hidden_unicode [-h] [--json] [--fix]\n');
|
|
500
|
+
process.stdout.write('usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n');
|
|
483
501
|
process.exit(0);
|
|
484
502
|
} else if (a === '--json') {
|
|
485
503
|
out.json = true;
|
|
486
504
|
} else if (a === '--fix') {
|
|
487
505
|
out.fix = true;
|
|
506
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
507
|
+
// Bounded root mode. Absent, this linter's own default roots stand;
|
|
508
|
+
// present, the same relative roots resolve against DIR instead. Used by
|
|
509
|
+
// the scout to scan a quarantined candidate, and by --self-test fixtures.
|
|
510
|
+
const eq = a.indexOf('=');
|
|
511
|
+
if (eq !== -1) {
|
|
512
|
+
out.root = a.slice(eq + 1);
|
|
513
|
+
} else {
|
|
514
|
+
rootPending = true;
|
|
515
|
+
}
|
|
488
516
|
} else {
|
|
489
517
|
extra.push(a);
|
|
490
518
|
}
|
|
@@ -497,11 +525,12 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
497
525
|
|
|
498
526
|
export function main(argv: readonly string[] | null = null): number {
|
|
499
527
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
528
|
+
const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
|
|
500
529
|
|
|
501
530
|
const findings: sl.Finding[] = [];
|
|
502
531
|
const flagged = new Set<string>();
|
|
503
532
|
let corpusFiles = 0;
|
|
504
|
-
for (const sf of sl.iter_corpus()) {
|
|
533
|
+
for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
|
|
505
534
|
corpusFiles += 1;
|
|
506
535
|
const hits = _scan(sf);
|
|
507
536
|
for (const h of hits) {
|
|
@@ -540,7 +569,12 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
540
569
|
// corpus) and a narrower codepoint set (C0 only). `--fix` does not touch
|
|
541
570
|
// these — rewriting a source file's bytes is the author's call, and the
|
|
542
571
|
// finding names the escape to use.
|
|
543
|
-
|
|
572
|
+
// The source pass enumerates TRACKED files via `git ls-files`, which is a
|
|
573
|
+
// property of the repository and not of the scan base. Under `--root` it
|
|
574
|
+
// would import this repository's own files into a scan of somebody else's
|
|
575
|
+
// directory and attribute the findings to it, so bounded mode skips it and
|
|
576
|
+
// the `.md` corpus under DIR is the whole scan.
|
|
577
|
+
const sourceFiles = args.root === undefined ? _eligibleSourceFiles() : [];
|
|
544
578
|
for (const h of _scanSourceControlBytes(sourceFiles)) {
|
|
545
579
|
findings.push(h);
|
|
546
580
|
}
|
|
@@ -566,6 +600,14 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
566
600
|
}
|
|
567
601
|
}
|
|
568
602
|
|
|
603
|
+
// Publish what this child inspected so the umbrella can aggregate a real
|
|
604
|
+
// corpus size rather than a count of children. BOTH passes count: the `.md`
|
|
605
|
+
// corpus and the raw-control-byte pass over tracked source files, which is a
|
|
606
|
+
// separate scope this linter genuinely reads.
|
|
607
|
+
if (args.json) {
|
|
608
|
+
sl.report_child_scanned(corpusFiles + (sourceFiles === null ? 0 : sourceFiles.length));
|
|
609
|
+
}
|
|
610
|
+
|
|
569
611
|
if (args.json) {
|
|
570
612
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
571
613
|
process.stdout.write(payload + '\n');
|
|
@@ -71,9 +71,19 @@ function _strip_inline_code(text: string): string {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// A BOUND pragma suppresses only the matches it fingerprints, so the scan
|
|
75
|
+
// runs and its output is filtered at the end. An UNBOUND one keeps the old
|
|
76
|
+
// whole-file bail and reports itself.
|
|
77
|
+
const pragmaForm = sf.pragma_form(CHECK);
|
|
78
|
+
if (pragmaForm === 'legacy') {
|
|
79
|
+
return [sl.legacy_pragma_finding(sf, CHECK)];
|
|
76
80
|
}
|
|
81
|
+
// EVERY exit from this function goes through `finish`. Wrapping only the
|
|
82
|
+
// terminal `return` is the defect that shipped for one commit here: this
|
|
83
|
+
// scan has an early return for an artifact with no `execution:` block, and a
|
|
84
|
+
// bound pragma silently did not apply to exactly those artifacts.
|
|
85
|
+
const finish = (hits: sl.Finding[]): sl.Finding[] =>
|
|
86
|
+
pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
|
|
77
87
|
const out: sl.Finding[] = [];
|
|
78
88
|
// prose = lines outside ANY fence; inline-code spans blanked.
|
|
79
89
|
for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true, skip_any_fence: true })) {
|
|
@@ -117,15 +127,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
|
-
return out;
|
|
130
|
+
return finish(out);
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
interface Args {
|
|
134
|
+
/** Scan base for the bounded root mode; absent = the package root. */
|
|
135
|
+
root?: string;
|
|
124
136
|
json: boolean;
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
function _argError(msg: string): never {
|
|
128
|
-
process.stderr.write('usage: lint_instruction_smuggling [-h] [--json]\n');
|
|
140
|
+
process.stderr.write('usage: lint_instruction_smuggling [-h] [--json] [--root DIR]\n');
|
|
129
141
|
process.stderr.write(`lint_instruction_smuggling: error: ${msg}\n`);
|
|
130
142
|
process.exit(2);
|
|
131
143
|
}
|
|
@@ -133,12 +145,28 @@ function _argError(msg: string): never {
|
|
|
133
145
|
function parse_args(argv: readonly string[]): Args {
|
|
134
146
|
const out: Args = { json: false };
|
|
135
147
|
const extra: string[] = [];
|
|
148
|
+
let rootPending = false;
|
|
136
149
|
for (const a of argv) {
|
|
150
|
+
if (rootPending) {
|
|
151
|
+
out.root = a;
|
|
152
|
+
rootPending = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
137
155
|
if (a === '-h' || a === '--help') {
|
|
138
|
-
process.stdout.write('usage: lint_instruction_smuggling [-h] [--json]\n');
|
|
156
|
+
process.stdout.write('usage: lint_instruction_smuggling [-h] [--json] [--root DIR]\n');
|
|
139
157
|
process.exit(0);
|
|
140
158
|
} else if (a === '--json') {
|
|
141
159
|
out.json = true;
|
|
160
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
161
|
+
// Bounded root mode. Absent, this linter's own default roots stand;
|
|
162
|
+
// present, the same relative roots resolve against DIR instead. Used by
|
|
163
|
+
// the scout to scan a quarantined candidate, and by --self-test fixtures.
|
|
164
|
+
const eq = a.indexOf('=');
|
|
165
|
+
if (eq !== -1) {
|
|
166
|
+
out.root = a.slice(eq + 1);
|
|
167
|
+
} else {
|
|
168
|
+
rootPending = true;
|
|
169
|
+
}
|
|
142
170
|
} else {
|
|
143
171
|
extra.push(a);
|
|
144
172
|
}
|
|
@@ -151,10 +179,11 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
151
179
|
|
|
152
180
|
export function main(argv: readonly string[] | null = null): number {
|
|
153
181
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
182
|
+
const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
|
|
154
183
|
|
|
155
184
|
const findings: sl.Finding[] = [];
|
|
156
185
|
let corpusFiles = 0;
|
|
157
|
-
for (const sf of sl.iter_corpus()) {
|
|
186
|
+
for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
|
|
158
187
|
corpusFiles += 1;
|
|
159
188
|
for (const h of _scan(sf)) {
|
|
160
189
|
findings.push(h);
|
|
@@ -181,6 +210,13 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
181
210
|
throw exc;
|
|
182
211
|
}
|
|
183
212
|
|
|
213
|
+
// Publish what this child inspected so the umbrella can aggregate a real
|
|
214
|
+
// corpus size rather than a count of children. Same number the assertion
|
|
215
|
+
// above just accepted, so the published figure cannot drift from it.
|
|
216
|
+
if (args.json) {
|
|
217
|
+
sl.report_child_scanned(corpusFiles);
|
|
218
|
+
}
|
|
219
|
+
|
|
184
220
|
if (args.json) {
|
|
185
221
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
186
222
|
process.stdout.write(payload + '\n');
|
|
@@ -119,9 +119,19 @@ function _suffix(p: string): string {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// A BOUND pragma suppresses only the matches it fingerprints, so the scan
|
|
123
|
+
// runs and its output is filtered at the end. An UNBOUND one keeps the old
|
|
124
|
+
// whole-file bail and reports itself.
|
|
125
|
+
const pragmaForm = sf.pragma_form(CHECK);
|
|
126
|
+
if (pragmaForm === 'legacy') {
|
|
127
|
+
return [sl.legacy_pragma_finding(sf, CHECK)];
|
|
124
128
|
}
|
|
129
|
+
// EVERY exit from this function goes through `finish`. Wrapping only the
|
|
130
|
+
// terminal `return` is the defect that shipped for one commit here: this
|
|
131
|
+
// scan has an early return for an artifact with no `execution:` block, and a
|
|
132
|
+
// bound pragma silently did not apply to exactly those artifacts.
|
|
133
|
+
const finish = (hits: sl.Finding[]): sl.Finding[] =>
|
|
134
|
+
pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
|
|
125
135
|
const out: sl.Finding[] = [];
|
|
126
136
|
for (const [start, numbered] of _candidate_chunks(sf)) {
|
|
127
137
|
for (const [lineno, text] of numbered) {
|
|
@@ -166,15 +176,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
166
176
|
);
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
|
-
return out;
|
|
179
|
+
return finish(out);
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
interface Args {
|
|
183
|
+
/** Scan base for the bounded root mode; absent = the package root. */
|
|
184
|
+
root?: string;
|
|
173
185
|
json: boolean;
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
function _argError(msg: string): never {
|
|
177
|
-
process.stderr.write('usage: lint_mcp_config_security [-h] [--json]\n');
|
|
189
|
+
process.stderr.write('usage: lint_mcp_config_security [-h] [--json] [--root DIR]\n');
|
|
178
190
|
process.stderr.write(`lint_mcp_config_security: error: ${msg}\n`);
|
|
179
191
|
process.exit(2);
|
|
180
192
|
}
|
|
@@ -182,12 +194,28 @@ function _argError(msg: string): never {
|
|
|
182
194
|
function parse_args(argv: readonly string[]): Args {
|
|
183
195
|
const out: Args = { json: false };
|
|
184
196
|
const extra: string[] = [];
|
|
197
|
+
let rootPending = false;
|
|
185
198
|
for (const a of argv) {
|
|
199
|
+
if (rootPending) {
|
|
200
|
+
out.root = a;
|
|
201
|
+
rootPending = false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
186
204
|
if (a === '-h' || a === '--help') {
|
|
187
|
-
process.stdout.write('usage: lint_mcp_config_security [-h] [--json]\n');
|
|
205
|
+
process.stdout.write('usage: lint_mcp_config_security [-h] [--json] [--root DIR]\n');
|
|
188
206
|
process.exit(0);
|
|
189
207
|
} else if (a === '--json') {
|
|
190
208
|
out.json = true;
|
|
209
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
210
|
+
// Bounded root mode. Absent, this linter's own default roots stand;
|
|
211
|
+
// present, the same relative roots resolve against DIR instead. Used by
|
|
212
|
+
// the scout to scan a quarantined candidate, and by --self-test fixtures.
|
|
213
|
+
const eq = a.indexOf('=');
|
|
214
|
+
if (eq !== -1) {
|
|
215
|
+
out.root = a.slice(eq + 1);
|
|
216
|
+
} else {
|
|
217
|
+
rootPending = true;
|
|
218
|
+
}
|
|
191
219
|
} else {
|
|
192
220
|
extra.push(a);
|
|
193
221
|
}
|
|
@@ -200,13 +228,14 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
200
228
|
|
|
201
229
|
export function main(argv: readonly string[] | null = null): number {
|
|
202
230
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
231
|
+
const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
|
|
203
232
|
|
|
204
233
|
const findings: sl.Finding[] = [];
|
|
205
234
|
// scan .md (fenced examples) under the default roots PLUS named MCP configs
|
|
206
235
|
// under src/templates (where the shipped claude_desktop_config template lives).
|
|
207
236
|
const roots = [...sl.DEFAULT_SCAN_ROOTS, 'src/templates'];
|
|
208
237
|
let corpusFiles = 0;
|
|
209
|
-
for (const sf of sl.iter_corpus(roots, ['.md', '.json', '.template'])) {
|
|
238
|
+
for (const sf of sl.iter_corpus(roots, ['.md', '.json', '.template'], scanBase)) {
|
|
210
239
|
corpusFiles += 1;
|
|
211
240
|
for (const h of _scan(sf)) {
|
|
212
241
|
findings.push(h);
|
|
@@ -234,6 +263,13 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
234
263
|
throw exc;
|
|
235
264
|
}
|
|
236
265
|
|
|
266
|
+
// Publish what this child inspected so the umbrella can aggregate a real
|
|
267
|
+
// corpus size rather than a count of children. Same number the assertion
|
|
268
|
+
// above just accepted, so the published figure cannot drift from it.
|
|
269
|
+
if (args.json) {
|
|
270
|
+
sl.report_child_scanned(corpusFiles);
|
|
271
|
+
}
|
|
272
|
+
|
|
237
273
|
if (args.json) {
|
|
238
274
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
239
275
|
process.stdout.write(payload + '\n');
|