@event4u/agent-config 9.26.0 → 9.28.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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +12 -11
- package/CHANGELOG.md +134 -207
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +43 -0
- package/dist/agent-src/commands/roadmap/next.md +65 -3
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
- package/dist/agent-src/rules/artifact-engagement-recording.md +6 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +3 -1
- package/dist/agent-src/rules/brand-consistency.md +2 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +35 -6
- package/dist/agent-src/rules/broken-access-control.md +2 -0
- package/dist/agent-src/rules/cli-output-handling.md +2 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/code-provenance.md +2 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +2 -0
- package/dist/agent-src/rules/content-quoting-floor.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +13 -1
- package/dist/agent-src/rules/copilot-routing.md +2 -0
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +5 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +31 -5
- package/dist/agent-src/rules/design-fidelity.md +5 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +4 -0
- package/dist/agent-src/rules/devcontainer-routing.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +2 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
- package/dist/agent-src/rules/domain-safety-pii.md +2 -0
- package/dist/agent-src/rules/domain-safety-retention.md +2 -0
- package/dist/agent-src/rules/downstream-changes.md +2 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +14 -3
- package/dist/agent-src/rules/external-code-graph-interop.md +4 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
- package/dist/agent-src/rules/finance-safety-floor.md +3 -1
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +3 -1
- package/dist/agent-src/rules/guidelines.md +2 -0
- package/dist/agent-src/rules/history-discipline.md +5 -1
- package/dist/agent-src/rules/icon-consistency.md +2 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
- package/dist/agent-src/rules/improve-before-implement.md +2 -0
- package/dist/agent-src/rules/invite-challenge.md +2 -0
- package/dist/agent-src/rules/laravel-routing.md +2 -0
- package/dist/agent-src/rules/laravel-translations.md +2 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -1
- package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/missing-tool-handling.md +2 -0
- package/dist/agent-src/rules/model-recommendation.md +2 -0
- package/dist/agent-src/rules/no-attribution-footers.md +2 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +5 -1
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/onboarding-gate.md +5 -1
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/package-ci-checks.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +2 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
- package/dist/agent-src/rules/question-not-instruction.md +2 -0
- package/dist/agent-src/rules/reviewer-awareness.md +2 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +6 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/role-mode-adherence.md +3 -1
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/runtime-safety.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +5 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
- package/dist/agent-src/rules/session-canary.md +62 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +4 -0
- package/dist/agent-src/rules/size-enforcement.md +2 -0
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-discovery-gate.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +3 -1
- package/dist/agent-src/rules/symfony-routing.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +2 -0
- package/dist/agent-src/rules/token-budget-discipline.md +5 -1
- package/dist/agent-src/rules/token-efficiency.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +7 -1
- package/dist/agent-src/rules/ui-audit-gate.md +4 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/upstream-proposal.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +2 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +6 -10
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +9 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
- package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-2BE5I2MN.js} +156 -141
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
- package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-PQSAQKHW.js} +467 -305
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +7 -7
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +5 -5
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +5 -5
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +272 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
- package/dist/cli-delegate/cmd_settings_set.js +7 -2967
- package/dist/cli-delegate/cmd_sync.js +5 -5
- package/dist/cli-delegate/cmd_uninstall.js +5 -5
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +5 -5
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +184 -124
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +9080 -5701
- package/dist/install/install.mjs +698 -508
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +698 -437
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +80 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +33 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/io/yamlIO.js +85 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/settings.js +1 -12
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/wizard.js +52 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.js.map +1 -0
- package/dist/shared/settingsSurface.js +7 -1
- package/dist/shared/settingsSurface.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
- package/docs/contracts/settings-classes.md +29 -8
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +6 -10
- package/src/config/budgets.yml +29 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +75 -0
- package/src/scripts/_lib/agent_settings.ts +82 -16
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/git_common_dir.ts +128 -0
- package/src/scripts/_lib/host_capability.ts +84 -1
- package/src/scripts/_lib/obligation_frequency.ts +391 -0
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +342 -0
- package/src/scripts/_lib/self_repair_store.ts +100 -0
- package/src/scripts/_lib/session_register.ts +361 -0
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +307 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +126 -7
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/hook_manifest.yaml +155 -33
- package/src/scripts/hooks/block_no_verify.ts +95 -3
- package/src/scripts/hooks/block_unauthorized_git.ts +217 -2
- package/src/scripts/hooks/concern_registry.ts +12 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +49 -6
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/release.ts +142 -36
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +77 -6
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/self_repair_cli.ts +240 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/session_canary_hook.ts +35 -3
- package/src/scripts/session_register_hook.ts +288 -0
- package/src/scripts/sessions_cli.ts +140 -0
- package/src/scripts/validate_frontmatter.ts +68 -0
- package/src/server/io/yamlIO.ts +87 -0
- package/src/shared/settingsCarveOut.ts +143 -0
- package/src/shared/settingsSurface.ts +7 -1
|
@@ -298,34 +298,146 @@ concerns:
|
|
|
298
298
|
args: []
|
|
299
299
|
fail_closed: false
|
|
300
300
|
severity: blocking
|
|
301
|
-
#
|
|
301
|
+
# Canary injection — deterministic carrier for the
|
|
302
302
|
# session-canary rule (greet-by-name at task start + reply-close markers
|
|
303
303
|
# as a user-observable context-degradation signal). Gated on
|
|
304
304
|
# `personal.canary_name` in .agent-settings.yml (empty = clean no-op),
|
|
305
305
|
# so unconfigured consumers pay nothing. Emits a `context` block the
|
|
306
306
|
# dispatcher forwards on session_start (hot-context mechanism).
|
|
307
|
+
# Bound in TWO slots on purpose: session_start carries the full contract once,
|
|
308
|
+
# user_prompt_submit carries a one-line beat. The obligation is per TASK and no
|
|
309
|
+
# host has a per-task slot (Cline maps TaskStart/TaskResume onto session_start;
|
|
310
|
+
# Claude Code has no task event), so per-turn — a strict superset — is the
|
|
311
|
+
# reachable covering period. Session scope alone measured a ~13-of-15 miss rate
|
|
312
|
+
# on task starts. Augment has no user_prompt_submit and is a DECLARED gap: its
|
|
313
|
+
# `stop` fires after the reply, so injecting there could not shape the reply the
|
|
314
|
+
# reminder is for, and counting it would be exactly the over-credit this pairing
|
|
315
|
+
# was added to remove.
|
|
307
316
|
# Never blocks. fail_closed: false.
|
|
308
317
|
session-canary:
|
|
309
318
|
script: src/scripts/session_canary_hook.ts
|
|
310
319
|
args: []
|
|
311
320
|
fail_closed: false
|
|
312
321
|
severity: advisory
|
|
322
|
+
# Council-availability fact. Carries whether a council is configured and from
|
|
323
|
+
# where, because the always-loaded layer refers to the council conditionally
|
|
324
|
+
# and never says how to check. Measured 2026-08-08: an agent announced "kein
|
|
325
|
+
# Council konfiguriert (keine .agent-settings.yml)" and substituted a weaker
|
|
326
|
+
# path; the council was configured user-globally the whole time, and the
|
|
327
|
+
# project tree is never searched for it (ADR-104). Advisory: supplies a fact,
|
|
328
|
+
# never blocks.
|
|
329
|
+
council-availability:
|
|
330
|
+
script: src/scripts/council_availability_hook.ts
|
|
331
|
+
args: []
|
|
332
|
+
fail_closed: false
|
|
333
|
+
severity: advisory
|
|
334
|
+
# Self-repair intake (road-to-self-repair Phase 1). Two slots, one queue:
|
|
335
|
+
# user_prompt_submit opens a record when the user states the agent worked
|
|
336
|
+
# wrongly and injects the open-queue line; stop runs the deterministic
|
|
337
|
+
# detectors over the finished turn and records silently (a reminder fired
|
|
338
|
+
# after the reply cannot shape it). ADVISORY by construction — it records,
|
|
339
|
+
# it never gates; the outward step is a separate, user-run CLI command.
|
|
340
|
+
self-repair:
|
|
341
|
+
script: src/scripts/self_repair_hook.ts
|
|
342
|
+
args: []
|
|
343
|
+
fail_closed: false
|
|
344
|
+
severity: advisory
|
|
345
|
+
# Shared per-session register (road-to-parallel-session-coordination) — writes
|
|
346
|
+
# this session into <git-common-dir>/agent-sessions/<session_id>.json so every
|
|
347
|
+
# other worktree of the repo can see it, and on session_start injects the live
|
|
348
|
+
# foreign sessions as a `context` block (hot-context mechanism).
|
|
349
|
+
#
|
|
350
|
+
# Bound in FOUR slots, with deliberately asymmetric meanings:
|
|
351
|
+
# session_start → register + prune expired + inject foreign sessions
|
|
352
|
+
# user_prompt_submit → heartbeat (pre-reply carrier)
|
|
353
|
+
# stop → heartbeat (post-reply carrier), NOT deregistration
|
|
354
|
+
# session_end → deregister
|
|
355
|
+
#
|
|
356
|
+
# `stop` is per-turn, not session end: on Claude Code the native Stop fires
|
|
357
|
+
# after every assistant reply (see _lib/obligation_frequency.ts), so
|
|
358
|
+
# deregistering there would kill a live session's own record after its first
|
|
359
|
+
# reply. The ONE exception is Cline, whose stop is TaskCancel and whose
|
|
360
|
+
# session_end is TaskComplete — a cancelled Cline task reaches neither, so it
|
|
361
|
+
# would leak its claim until TTL. That exception is an explicit platform
|
|
362
|
+
# allow-list in _lib/session_register.ts, never a condition computed from the
|
|
363
|
+
# frequency lattice: a future platform must be added by a human, deliberately.
|
|
364
|
+
#
|
|
365
|
+
# Correctness rests on heartbeat + TTL alone; deregistration only frees a claim
|
|
366
|
+
# sooner. So Windsurf's missing session_end costs latency, not correctness.
|
|
367
|
+
# Declared non-carriers: copilot (no hook surface), cursor CLI (per-turn slots
|
|
368
|
+
# are IDE-only), cowork (slots wired, lifecycle events inert) — a session there
|
|
369
|
+
# registers but expires by TTL rather than heartbeating, and the concern says so
|
|
370
|
+
# on stderr rather than letting the coverage join read green.
|
|
371
|
+
# Never blocks. fail_closed: false.
|
|
372
|
+
session-register:
|
|
373
|
+
script: src/scripts/session_register_hook.ts
|
|
374
|
+
args: []
|
|
375
|
+
fail_closed: false
|
|
376
|
+
severity: advisory
|
|
377
|
+
# road-to-orchestrator-discipline-carriers Phase 3 (F6) — deterministic
|
|
378
|
+
# PostToolUse orchestration-telemetry capture. Fires only on an Agent/Task
|
|
379
|
+
# tool-use completion (the two observed subagent-dispatch tool names) and
|
|
380
|
+
# appends one `orchestration_record` line — no model step, replacing the
|
|
381
|
+
# measured 0.27% model-carried capture rate for future dispatches. Silent
|
|
382
|
+
# on every path (never writes to stdout); exit 0 always, including a
|
|
383
|
+
# malformed envelope or a write failure. fail_closed: false.
|
|
384
|
+
orchestration-record:
|
|
385
|
+
script: src/scripts/hooks/orchestration_record_hook.ts
|
|
386
|
+
args: []
|
|
387
|
+
fail_closed: false
|
|
388
|
+
severity: advisory
|
|
389
|
+
tools: [Agent, Task]
|
|
390
|
+
# road-to-orchestrator-discipline-carriers Phase 4 (F3-lite) — conditional
|
|
391
|
+
# delegation nudge. Extracts cheap structural signals from the SUBMITTED
|
|
392
|
+
# prompt (enumerated file lists, "for each"/"alle …" shapes, explicit slice
|
|
393
|
+
# counts, ordered-plan markers, multi-deliverable conjunctions), classifies
|
|
394
|
+
# via `classifyTask`, and — only on a positive do-in-parallel/do-in-steps
|
|
395
|
+
# verdict — reports `{decision:"warn", reason, additional_context}` at exit
|
|
396
|
+
# 2, mirroring `language-mirror`'s shipped delivery pattern so the verdict
|
|
397
|
+
# line actually reaches `hookSpecificOutput.additionalContext` on the
|
|
398
|
+
# verified `claude` platform. Silence is the anti-canary default: no
|
|
399
|
+
# enumerated signal → no injection at all. Never blocks the turn — see the
|
|
400
|
+
# hook's file header for the `host_semantics.emitFor` proof. fail_closed:
|
|
401
|
+
# false.
|
|
402
|
+
delegation-nudge:
|
|
403
|
+
script: src/scripts/hooks/delegation_nudge_hook.ts
|
|
404
|
+
args: []
|
|
405
|
+
fail_closed: false
|
|
406
|
+
severity: advisory
|
|
407
|
+
# road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
|
|
408
|
+
# nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
|
|
409
|
+
# files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
|
|
410
|
+
# post-review, F6) AND no reviewer ran this session (scanned from the
|
|
411
|
+
# Stop payload's `transcript_path`) AND this session has not already
|
|
412
|
+
# fired once (F2 — a once-per-session gate; state under
|
|
413
|
+
# agents/runtime/state/end-review-nudge/, gitignored). On fire, reports
|
|
414
|
+
# `{decision:"warn", reason, additional_context}` at exit 2 (same delivery
|
|
415
|
+
# pattern as `language-mirror`/`delegation-nudge` above) and appends one
|
|
416
|
+
# `review_skipped` telemetry line — hook-carried, not model-carried — so a
|
|
417
|
+
# future blocking decision has a measured distribution to cite. Never
|
|
418
|
+
# blocks the turn — see the hook's file header for the `host_semantics.
|
|
419
|
+
# emitFor` proof. fail_closed: false.
|
|
420
|
+
end-review-nudge:
|
|
421
|
+
script: src/scripts/hooks/end_review_nudge_hook.ts
|
|
422
|
+
args: []
|
|
423
|
+
fail_closed: false
|
|
424
|
+
severity: advisory
|
|
313
425
|
|
|
314
426
|
platforms:
|
|
315
427
|
augment:
|
|
316
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
317
|
-
session_end: [chat-history, memory-learn]
|
|
318
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
428
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
429
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
430
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
319
431
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
320
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
432
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
321
433
|
|
|
322
434
|
claude:
|
|
323
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
324
|
-
session_end: [chat-history, memory-learn]
|
|
325
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
|
|
326
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
435
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
436
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
437
|
+
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, self-repair, session-register]
|
|
438
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
327
439
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
328
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
440
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
329
441
|
|
|
330
442
|
# Cowork — the Claude desktop app's local-agent-mode runtime, built
|
|
331
443
|
# on top of the Claude Code CLI. Same lifecycle vocabulary, same
|
|
@@ -342,12 +454,22 @@ platforms:
|
|
|
342
454
|
# Decision matrix + upstream blockers tracked in
|
|
343
455
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
344
456
|
cowork:
|
|
345
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
346
|
-
session_end: [chat-history, memory-learn]
|
|
347
|
-
|
|
348
|
-
|
|
457
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
458
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
459
|
+
# end-review-nudge and delegation-nudge are DELIBERATELY NOT bound on
|
|
460
|
+
# this platform (F10, review) — cowork-dispatcher.sh's own header states
|
|
461
|
+
# it "always exit[s] 0" and its dispatch line discards the dispatcher's
|
|
462
|
+
# exit code AND stdout unconditionally (`>/dev/null 2>&1 || true`). Both
|
|
463
|
+
# concerns' delivery depends entirely on `additional_context` reaching
|
|
464
|
+
# the model via stdout at the verified exit-2 warn path — a delivery this
|
|
465
|
+
# trampoline makes structurally impossible regardless of which platform
|
|
466
|
+
# is later verified to forward it. Binding them here would be dead
|
|
467
|
+
# weight: the concern still runs (git diff, transcript scan, classifier),
|
|
468
|
+
# still costs the dispatch, and its entire output is thrown away.
|
|
469
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
470
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
349
471
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
350
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
472
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
351
473
|
|
|
352
474
|
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
353
475
|
# IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
|
|
@@ -357,11 +479,11 @@ platforms:
|
|
|
357
479
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
358
480
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
359
481
|
cursor:
|
|
360
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
361
|
-
session_end: [chat-history, memory-learn]
|
|
362
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
363
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
364
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
482
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
483
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
484
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
485
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
486
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
365
487
|
|
|
366
488
|
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
367
489
|
# (project, no file extension, must be executable per Cline docs) or
|
|
@@ -372,11 +494,11 @@ platforms:
|
|
|
372
494
|
# both map to session_start. TaskCancel maps to stop because the
|
|
373
495
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
374
496
|
cline:
|
|
375
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
376
|
-
session_end: [chat-history, memory-learn]
|
|
377
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
378
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
379
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
497
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
498
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
499
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
500
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
501
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
380
502
|
|
|
381
503
|
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
382
504
|
# (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
|
|
@@ -391,11 +513,11 @@ platforms:
|
|
|
391
513
|
# surface to record verification commands; documented limitation).
|
|
392
514
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
393
515
|
windsurf:
|
|
394
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
|
|
395
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
516
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
517
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
396
518
|
# language-mirror IS bound here: unlike minimal-safe-diff it needs only the
|
|
397
519
|
# user_prompt_submit slot, which Windsurf has.
|
|
398
|
-
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization]
|
|
520
|
+
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
399
521
|
|
|
400
522
|
# Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
|
|
401
523
|
# (project) or `~/.gemini/settings.json` (user). Per Gemini docs
|
|
@@ -408,11 +530,11 @@ platforms:
|
|
|
408
530
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
409
531
|
# — this is our `stop` slot.
|
|
410
532
|
gemini:
|
|
411
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
412
|
-
session_end: [chat-history, memory-learn]
|
|
413
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
414
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
415
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
533
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
534
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
535
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
536
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
537
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
416
538
|
|
|
417
539
|
# Phase 7.9 — Copilot has no hook surface. Concerns route through
|
|
418
540
|
# rule-only fallback; the dispatcher silently no-ops on --platform copilot.
|
|
@@ -20,12 +20,21 @@
|
|
|
20
20
|
* 2 — warn (not used by this guard)
|
|
21
21
|
*
|
|
22
22
|
* No ALLOW_NO_VERIFY-style env bypass is provided. See src/rules/git-history-discipline.md
|
|
23
|
+
*
|
|
24
|
+
* WHAT THIS GUARD DOES NOT SEE. Command substitutions ARE now checked — see
|
|
25
|
+
* `_check_command` for why they were not, and why the well-formed case was the
|
|
26
|
+
* unprotected one. What remains uncovered is an invocation whose command word
|
|
27
|
+
* only exists at runtime (`P=commit; git $P --no-verify` via indirection, or a
|
|
28
|
+
* word composed by `xargs`). Those execute under bash and are not classified
|
|
29
|
+
* here. Stated rather than left implied: this file's own round-5 defect was a
|
|
30
|
+
* docstring that described coverage it did not have.
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
import * as fs from 'node:fs';
|
|
26
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
35
|
import * as path from 'node:path';
|
|
28
36
|
import { readHookStdin } from './hook_stdin.js';
|
|
37
|
+
import { substitutionPayloads } from './block_unauthorized_git.js';
|
|
29
38
|
|
|
30
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
31
40
|
|
|
@@ -329,14 +338,97 @@ function _is_blocked(git_tokens: string[]): [boolean, string] {
|
|
|
329
338
|
return [false, ''];
|
|
330
339
|
}
|
|
331
340
|
|
|
332
|
-
/**
|
|
333
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Does an unparseable command plausibly INVOKE git?
|
|
343
|
+
*
|
|
344
|
+
* The fail-closed branch below used to test `/\bgit\b/` against the raw command
|
|
345
|
+
* string. That is a mention, not an invocation, and the difference is not
|
|
346
|
+
* academic: measured in the round-5 audit (2026-08-07), a
|
|
347
|
+
* `mkdir -p … && cat > notes.md <<HEREDOC … HEREDOC` with no git anywhere in
|
|
348
|
+
* command position was refused, because the heredoc body named rules such as
|
|
349
|
+
* `git-history-discipline` and contained apostrophes — so shlex failed and the
|
|
350
|
+
* substring matched. The message's own remedy (`git commit -F <file>`) could not
|
|
351
|
+
* apply, because there was no commit.
|
|
352
|
+
*
|
|
353
|
+
* Fail-closed is still the right posture for the case it was written for: a
|
|
354
|
+
* genuinely unparseable command that DOES invoke git. So the test moves from
|
|
355
|
+
* "the string contains git" to "some command position is git", with heredoc
|
|
356
|
+
* bodies removed first — the same data-not-command distinction
|
|
357
|
+
* `block_unauthorized_git` already makes.
|
|
358
|
+
*
|
|
359
|
+
* Deliberately regex-shaped rather than tokenised: shlex has already failed by
|
|
360
|
+
* the time this runs, so no reliable token stream exists. It therefore stays
|
|
361
|
+
* conservative — a `git` at the head of any line or after any separator counts,
|
|
362
|
+
* quoted or not.
|
|
363
|
+
*/
|
|
364
|
+
export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
|
|
365
|
+
if (depth > 3) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const withoutHeredocs = cmd.replace(
|
|
369
|
+
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
370
|
+
'<<HEREDOC',
|
|
371
|
+
);
|
|
372
|
+
for (const rawSeg of withoutHeredocs.split(/\n|;|&&|\|\||\||&|\$\(|`/)) {
|
|
373
|
+
// Drop leading env assignments: `GIT_DIR=x git status`.
|
|
374
|
+
const seg = rawSeg
|
|
375
|
+
.trim()
|
|
376
|
+
.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, '');
|
|
377
|
+
// A `sh -c '<payload>'` payload IS command context — recurse, or the
|
|
378
|
+
// narrowing above would open a bypass this guard did not have before.
|
|
379
|
+
// Caught by the adversarial case, not by review.
|
|
380
|
+
const wrapped =
|
|
381
|
+
/^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
|
|
382
|
+
if (wrapped !== null) {
|
|
383
|
+
if (_looks_like_git_invocation(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? '', depth + 1)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// Command position: optional path prefix, then `git` as its own word.
|
|
389
|
+
if (/^(?:\S*\/)?git(?:\s|$)/.test(seg)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Return (blocked, reason). Fail-closed on parse error for git commands.
|
|
398
|
+
*
|
|
399
|
+
* COMMAND SUBSTITUTIONS ARE CHECKED SEPARATELY, and the reason they were not is
|
|
400
|
+
* the shape of the hole: this file does split on `$(` and backtick, but only
|
|
401
|
+
* inside `_looks_like_git_invocation`, which runs exclusively in the
|
|
402
|
+
* shlex-failure branch. A well-formed `echo "$(git commit --no-verify)"` parses
|
|
403
|
+
* cleanly, so shlex succeeds, the substitution stays inside one token, and
|
|
404
|
+
* `_git_base` never sees git. The more parseable the command, the less it was
|
|
405
|
+
* protected. Classification inside the payload is by command position, so a
|
|
406
|
+
* substitution that INVOKES git is checked and one that merely names it in an
|
|
407
|
+
* argument is not — matching what `block_unauthorized_git` does one level up.
|
|
408
|
+
*
|
|
409
|
+
* Heredoc bodies are removed before extraction: a backtick or `$(` inside a
|
|
410
|
+
* commit message being written to a file is data, and feeding it back through
|
|
411
|
+
* this function is exactly the false positive the round-5 repair closed.
|
|
412
|
+
*/
|
|
413
|
+
function _check_command(cmd: string, depth = 0): [boolean, string] {
|
|
414
|
+
if (depth <= 3) {
|
|
415
|
+
const withoutHeredocs = cmd.replace(
|
|
416
|
+
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
417
|
+
'<<HEREDOC',
|
|
418
|
+
);
|
|
419
|
+
for (const payload of substitutionPayloads(withoutHeredocs)) {
|
|
420
|
+
const [blocked, reason] = _check_command(payload, depth + 1);
|
|
421
|
+
if (blocked) {
|
|
422
|
+
return [true, reason];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
334
426
|
let tokens: string[];
|
|
335
427
|
try {
|
|
336
428
|
tokens = shlexSplit(cmd);
|
|
337
429
|
} catch (e) {
|
|
338
430
|
if (e instanceof ShlexError) {
|
|
339
|
-
if (
|
|
431
|
+
if (_looks_like_git_invocation(cmd)) {
|
|
340
432
|
return [
|
|
341
433
|
true,
|
|
342
434
|
'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
|
|
@@ -38,6 +38,23 @@
|
|
|
38
38
|
* Exit codes (dispatcher contract): 0 allow · 2 block (stderr carries the
|
|
39
39
|
* reason on block-capable events) — the dispatcher's `host_semantics`
|
|
40
40
|
* translation owns the per-platform mapping.
|
|
41
|
+
*
|
|
42
|
+
* WHAT THIS GUARD DOES NOT SEE — stated because the previous version of this
|
|
43
|
+
* header stated only what it covers, and a coverage claim that omits its
|
|
44
|
+
* residue reads as completeness. The classifier reads the command TEXT; it does
|
|
45
|
+
* not interpret the shell. So an invocation whose command word is assembled at
|
|
46
|
+
* runtime is not classified, measured and still open:
|
|
47
|
+
*
|
|
48
|
+
* P=publish; npm $P — variable indirection
|
|
49
|
+
* echo publish | xargs npm — the command word is composed by xargs
|
|
50
|
+
*
|
|
51
|
+
* Both execute under bash. Closing them needs a different mechanism than a
|
|
52
|
+
* longer pattern, and `Measured, deferred, and why` in the round-6 roadmap
|
|
53
|
+
* draws that line deliberately. Covered, by contrast, and each pinned by a
|
|
54
|
+
* vector whose ground truth came from running bash rather than from reading
|
|
55
|
+
* this file: ANSI-C and locale quoting, unbalanced quotes, command and process
|
|
56
|
+
* substitution in every position tried, `sh -c` and `eval` payloads, env
|
|
57
|
+
* assignment prefixes, and quotes inside the command word.
|
|
41
58
|
*/
|
|
42
59
|
import fs from "node:fs";
|
|
43
60
|
import path from "node:path";
|
|
@@ -143,6 +160,174 @@ export function extractCommand(envelope: JsonObject): string | null {
|
|
|
143
160
|
return null;
|
|
144
161
|
}
|
|
145
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Split on shell separators that are OUTSIDE quotes.
|
|
165
|
+
*
|
|
166
|
+
* Separators recognised, matching the raw regex this replaces: newline, `;`,
|
|
167
|
+
* `&&`, `||`, `|`, `&`. A separator inside `'…'` or `"…"` is a literal, so it
|
|
168
|
+
* does not split. Backslash escapes the next character outside single quotes,
|
|
169
|
+
* per POSIX.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately NOT a full tokeniser. `shlexSplit` in `block_no_verify` throws on
|
|
172
|
+
* an unbalanced quote, which is the right behaviour for a guard that fail-closes;
|
|
173
|
+
* this guard must classify every command it sees, including a malformed one.
|
|
174
|
+
*
|
|
175
|
+
* THE UNTERMINATED-QUOTE POSTURE IS FAIL-CLOSED, AND THE ARGUMENT FOR THE
|
|
176
|
+
* PREVIOUS ONE WAS INVERTED. Round 5 shipped this docstring: "an unterminated
|
|
177
|
+
* quote here degrades to 'the rest of the string is quoted', which yields one
|
|
178
|
+
* segment starting at the real command word — the conservative outcome". It is
|
|
179
|
+
* the opposite. In `echo 'oops && npm publish` the surviving segment's command
|
|
180
|
+
* word is `echo`, so everything after `&&` is swallowed and the publish is never
|
|
181
|
+
* seen. Measured, not reasoned: that command was blocked before #1208 and
|
|
182
|
+
* allowed after it. So an unbalanced quote now re-splits the trailing segment
|
|
183
|
+
* WITHOUT quote awareness and classifies every piece. A false positive on input
|
|
184
|
+
* bash would itself refuse to run is acceptable; a false negative on input bash
|
|
185
|
+
* does run is the failure this guard exists for.
|
|
186
|
+
*
|
|
187
|
+
* ANSI-C quoting (`$'…'`) is a distinct opener, and treating its `$` as ordinary
|
|
188
|
+
* text was the second half of the same regress. In `$'don\'t'` the `\'` does NOT
|
|
189
|
+
* close the quote — C escape semantics apply inside — so a plain single-quote
|
|
190
|
+
* reading closes early, re-opens on the trailing `'`, and swallows the tail.
|
|
191
|
+
* `$"…"` is locale translation and behaves as `"…"`.
|
|
192
|
+
*/
|
|
193
|
+
export function splitOutsideQuotes(command: string): string[] {
|
|
194
|
+
const parts: string[] = [];
|
|
195
|
+
let cur = "";
|
|
196
|
+
// `$'` is tracked distinctly from `'` because only it honours `\` escapes.
|
|
197
|
+
let quote: '"' | "'" | "$'" | null = null;
|
|
198
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
199
|
+
const ch = command[i] as string;
|
|
200
|
+
if (quote !== null) {
|
|
201
|
+
if (quote === "$'" && ch === "\\" && i + 1 < command.length) {
|
|
202
|
+
// C-style escape: `\'` is a literal quote, not a terminator.
|
|
203
|
+
cur += ch + (command[i + 1] as string);
|
|
204
|
+
i += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
cur += ch;
|
|
208
|
+
if (ch === (quote === "$'" ? "'" : quote)) {
|
|
209
|
+
quote = null;
|
|
210
|
+
} else if (ch === "\\" && quote === '"' && i + 1 < command.length) {
|
|
211
|
+
cur += command[i + 1] as string;
|
|
212
|
+
i += 1;
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// `$'…'` / `$"…"` — the `$` belongs to the opener, not to the payload.
|
|
217
|
+
if (ch === "$" && (command[i + 1] === "'" || command[i + 1] === '"')) {
|
|
218
|
+
quote = command[i + 1] === "'" ? "$'" : '"';
|
|
219
|
+
cur += ch + (command[i + 1] as string);
|
|
220
|
+
i += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (ch === '"' || ch === "'") {
|
|
224
|
+
quote = ch;
|
|
225
|
+
cur += ch;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
229
|
+
cur += ch + (command[i + 1] as string);
|
|
230
|
+
i += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
|
|
234
|
+
// `&&` and `||` are two-character separators; consume the pair.
|
|
235
|
+
if ((ch === "|" || ch === "&") && command[i + 1] === ch) {
|
|
236
|
+
i += 1;
|
|
237
|
+
}
|
|
238
|
+
parts.push(cur);
|
|
239
|
+
cur = "";
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
cur += ch;
|
|
243
|
+
}
|
|
244
|
+
if (quote !== null) {
|
|
245
|
+
// Unbalanced: the trailing segment carries an opener that never closed, so
|
|
246
|
+
// its separators were read as data. Re-read them as separators. Segments
|
|
247
|
+
// completed BEFORE the opener keep their quote-aware split — the round-5
|
|
248
|
+
// false positive (a `grep -E` alternation) balances its quotes and never
|
|
249
|
+
// reaches this branch.
|
|
250
|
+
parts.push(...splitIgnoringQuotes(cur));
|
|
251
|
+
return parts;
|
|
252
|
+
}
|
|
253
|
+
parts.push(cur);
|
|
254
|
+
return parts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Separator split with no quote awareness at all — the fail-closed fallback for
|
|
259
|
+
* a command whose quotes do not balance. Backslash still escapes, because a
|
|
260
|
+
* `\|` is a literal pipe under every reading.
|
|
261
|
+
*/
|
|
262
|
+
function splitIgnoringQuotes(fragment: string): string[] {
|
|
263
|
+
const parts: string[] = [];
|
|
264
|
+
let cur = "";
|
|
265
|
+
for (let i = 0; i < fragment.length; i += 1) {
|
|
266
|
+
const ch = fragment[i] as string;
|
|
267
|
+
if (ch === "\\" && i + 1 < fragment.length) {
|
|
268
|
+
cur += ch + (fragment[i + 1] as string);
|
|
269
|
+
i += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
|
|
273
|
+
if ((ch === "|" || ch === "&") && fragment[i + 1] === ch) {
|
|
274
|
+
i += 1;
|
|
275
|
+
}
|
|
276
|
+
parts.push(cur);
|
|
277
|
+
cur = "";
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
cur += ch;
|
|
281
|
+
}
|
|
282
|
+
parts.push(cur);
|
|
283
|
+
return parts;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Extract the payload of every command substitution in a segment — `$(…)` with
|
|
288
|
+
* balanced parentheses, and backtick form.
|
|
289
|
+
*
|
|
290
|
+
* An unterminated substitution yields everything to the end of the segment
|
|
291
|
+
* rather than nothing: the same fail-closed direction the quote posture takes.
|
|
292
|
+
*/
|
|
293
|
+
export function substitutionPayloads(segment: string): string[] {
|
|
294
|
+
const out: string[] = [];
|
|
295
|
+
for (let i = 0; i < segment.length; i += 1) {
|
|
296
|
+
const ch = segment[i];
|
|
297
|
+
if (ch === "\\") {
|
|
298
|
+
i += 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// `$(…)` command substitution and `<(…)` / `>(…)` process substitution:
|
|
302
|
+
// different syntax, same fact — the parenthesised payload is a command that
|
|
303
|
+
// runs. Measured: `cat <(npm publish)` executes the publish.
|
|
304
|
+
if ((ch === "$" || ch === "<" || ch === ">") && segment[i + 1] === "(") {
|
|
305
|
+
let open = 1;
|
|
306
|
+
let j = i + 2;
|
|
307
|
+
const start = j;
|
|
308
|
+
for (; j < segment.length && open > 0; j += 1) {
|
|
309
|
+
const c = segment[j];
|
|
310
|
+
if (c === "\\") {
|
|
311
|
+
j += 1;
|
|
312
|
+
} else if (c === "(") {
|
|
313
|
+
open += 1;
|
|
314
|
+
} else if (c === ")") {
|
|
315
|
+
open -= 1;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
out.push(segment.slice(start, open === 0 ? j - 1 : segment.length));
|
|
319
|
+
i = j - 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (ch === "`") {
|
|
323
|
+
const end = segment.indexOf("`", i + 1);
|
|
324
|
+
out.push(segment.slice(i + 1, end === -1 ? segment.length : end));
|
|
325
|
+
i = end === -1 ? segment.length : end;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
146
331
|
/**
|
|
147
332
|
* Split a shell command into the segments that are actually INVOKED.
|
|
148
333
|
*
|
|
@@ -159,6 +344,16 @@ export function extractCommand(envelope: JsonObject): string | null {
|
|
|
159
344
|
* Quoted payloads are NOT simply discarded — `sh -c "npm publish"` really does
|
|
160
345
|
* publish. Those are unwrapped and re-split, so closing the false positive does
|
|
161
346
|
* not open a bypass.
|
|
347
|
+
*
|
|
348
|
+
* The separator split is QUOTE-AWARE, and that is not a refinement — it is the
|
|
349
|
+
* same false-positive class the paragraph above describes, surviving in the one
|
|
350
|
+
* place the round-2 fix did not reach. Measured in the round-5 audit
|
|
351
|
+
* (2026-08-07): a read-only `grep -n -E "…|npm publish|…"` was refused with
|
|
352
|
+
* "Blocked: `publish` with no authorization in this turn's prompt", because a
|
|
353
|
+
* raw `split` on `|` cut the quoted alternation into a segment beginning with
|
|
354
|
+
* `npm`. A `|` inside quotes is data, exactly like a heredoc body; only an
|
|
355
|
+
* unquoted one is a pipe. Splitting outside quotes closes that without
|
|
356
|
+
* discarding quoted payloads, so `sh -c "npm publish"` still unwraps below.
|
|
162
357
|
*/
|
|
163
358
|
export function invokedSegments(command: string, depth = 0): string[] {
|
|
164
359
|
if (depth > 3) {
|
|
@@ -172,20 +367,40 @@ export function invokedSegments(command: string, depth = 0): string[] {
|
|
|
172
367
|
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
173
368
|
"<<HEREDOC",
|
|
174
369
|
);
|
|
175
|
-
for (const raw of withoutHeredocs
|
|
370
|
+
for (const raw of splitOutsideQuotes(withoutHeredocs)) {
|
|
176
371
|
let seg = raw.trim();
|
|
177
372
|
if (!seg) {
|
|
178
373
|
continue;
|
|
179
374
|
}
|
|
180
375
|
// Drop leading env assignments: `FOO=bar BAZ=1 npm publish`.
|
|
181
376
|
seg = seg.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, "");
|
|
377
|
+
// A command substitution is an invocation in its own right, and it runs
|
|
378
|
+
// whether or not the enclosing segment's command word is harmless:
|
|
379
|
+
// `echo "$(npm publish)"` publishes. Classify INSIDE the substitution by
|
|
380
|
+
// command position, so the substitution that *invokes* a blocked op is
|
|
381
|
+
// refused and the one that merely *mentions* it in an argument
|
|
382
|
+
// (`echo "$(grep -c 'npm publish' f)"`) is not — the same rule `commandOp`
|
|
383
|
+
// already applies one level up, applied one level deeper.
|
|
384
|
+
for (const payload of substitutionPayloads(seg)) {
|
|
385
|
+
out.push(...invokedSegments(payload, depth + 1));
|
|
386
|
+
}
|
|
182
387
|
// Unwrap `sh -c '<payload>'` / `bash -c "<payload>"` and recurse.
|
|
183
388
|
const wrapped = /^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
|
|
184
389
|
if (wrapped) {
|
|
185
390
|
out.push(...invokedSegments(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? "", depth + 1));
|
|
186
391
|
continue;
|
|
187
392
|
}
|
|
188
|
-
|
|
393
|
+
// `eval` is the same shape as `sh -c`: its argument is command context, and
|
|
394
|
+
// it takes the rest of the segment rather than one word.
|
|
395
|
+
const evaled = /^eval\s+(?:"([^"]*)"|'([^']*)'|(.+))$/.exec(seg);
|
|
396
|
+
if (evaled) {
|
|
397
|
+
out.push(...invokedSegments(evaled[1] ?? evaled[2] ?? evaled[3] ?? "", depth + 1));
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
// Quotes inside the command WORD are removed by the shell before it looks
|
|
401
|
+
// the command up, so `np''m publish` invokes npm. Strip them from the head
|
|
402
|
+
// only — an argument's quotes are data and stay.
|
|
403
|
+
out.push(seg.replace(/^(\S+)/, (w) => w.replace(/['"]/g, "")));
|
|
189
404
|
}
|
|
190
405
|
return out;
|
|
191
406
|
}
|