@event4u/agent-config 14.18.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 +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- 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/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- 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-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- 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_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- 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 +42 -12
- 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 +2 -2
- 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 +34 -33
- 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 +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.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/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +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/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -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/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -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/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* category_a — which `pre_tool_use` calls this package is willing to hand the
|
|
3
|
+
* host an explicit `permissionDecision: allow` for.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `host_semantics` built exactly one envelope shape, so a
|
|
6
|
+
* call this package does not gate at all still cost the user a host
|
|
7
|
+
* confirmation. `permissionDecision` is offered by the host contract and was
|
|
8
|
+
* never emitted, so the prompt is ours to remove — but only for calls where
|
|
9
|
+
* "we do not gate this" is a fact about the call rather than about our
|
|
10
|
+
* coverage. That is what category A names: reads, navigation, build, test and
|
|
11
|
+
* lint inside the working tree with no consequence operation attached.
|
|
12
|
+
*
|
|
13
|
+
* THE CLASSIFIER IS AN ALLOWLIST AND MUST STAY ONE. A denylist answers "is
|
|
14
|
+
* this one of the bad shapes I thought of", so every shape nobody thought of
|
|
15
|
+
* would be auto-allowed. Everything below answers the opposite question, and an
|
|
16
|
+
* unrecognised call is not category A. The failure direction of a bug here is
|
|
17
|
+
* then a prompt that did not go away, never a confirmation that was skipped.
|
|
18
|
+
*
|
|
19
|
+
* WHAT AN ALLOW DOES NOT DO. It never overrides a verdict from this package:
|
|
20
|
+
* composition lives in `host_semantics.composePermissionDecision`, where one
|
|
21
|
+
* `ask` or `deny` beats every `allow`, and the dispatcher reaches the allow
|
|
22
|
+
* path only when the reduced severity is already `allow`. It is also not a
|
|
23
|
+
* grant — the host's own strings record `permissionDecision=allow ignored: a
|
|
24
|
+
* confined session takes grants only from its command line`.
|
|
25
|
+
*/
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
|
|
28
|
+
/** A JSON-ish tool input as it arrives in the dispatcher envelope. */
|
|
29
|
+
export type ToolInput = Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tools whose every documented effect is a read. `WebFetch` and `WebSearch`
|
|
33
|
+
* are deliberately absent — they reach the network, which is the egress leg of
|
|
34
|
+
* the lethal trifecta and not something to auto-allow.
|
|
35
|
+
*/
|
|
36
|
+
const READ_ONLY_TOOLS: ReadonlySet<string> = new Set([
|
|
37
|
+
"Read",
|
|
38
|
+
"Glob",
|
|
39
|
+
"Grep",
|
|
40
|
+
"NotebookRead",
|
|
41
|
+
"LS",
|
|
42
|
+
"TodoRead",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Per-tool input keys that carry a filesystem path needing confinement. */
|
|
46
|
+
const PATH_KEYS: readonly string[] = [
|
|
47
|
+
"file_path",
|
|
48
|
+
"notebook_path",
|
|
49
|
+
"path",
|
|
50
|
+
"directory",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Any of these in a Bash command string disqualifies it before the argv is
|
|
55
|
+
* looked at. Chaining, substitution, redirection and backgrounding all let a
|
|
56
|
+
* safe head token carry an arbitrary second command, and a classifier that
|
|
57
|
+
* tries to parse past them is a shell parser with a security boundary attached.
|
|
58
|
+
* Refusing the whole shape is both smaller and correct.
|
|
59
|
+
*/
|
|
60
|
+
const SHELL_METACHARACTERS = /[;&|`$><\n\r(){}\\]/;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Words naming a consequence operation, matched as whole tokens anywhere in
|
|
64
|
+
* the command. Defence in depth: the metacharacter rule already prevents a
|
|
65
|
+
* second command, so a hit here means the FIRST command is consequential.
|
|
66
|
+
*/
|
|
67
|
+
const CONSEQUENCE_WORDS: ReadonlySet<string> = new Set([
|
|
68
|
+
"push", "publish", "deploy", "release", "merge", "rebase", "reset",
|
|
69
|
+
"revert", "restore", "checkout", "commit", "tag", "fetch", "pull", "clone",
|
|
70
|
+
"rm", "rmdir", "unlink", "mv", "cp", "chmod", "chown", "kill", "sudo",
|
|
71
|
+
"curl", "wget", "ssh", "scp", "rsync", "nc", "telnet",
|
|
72
|
+
"drop", "truncate", "delete", "insert", "migrate", "seed", "restore-db",
|
|
73
|
+
"install", "uninstall", "upgrade", "prune", "clean", "apply", "destroy",
|
|
74
|
+
"terraform", "kubectl", "helm", "aws", "gcloud", "az", "docker", "podman",
|
|
75
|
+
"systemctl", "launchctl", "crontab", "mail", "sendmail",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** Read-only `git` subcommands; every consequential one is absent by design. */
|
|
79
|
+
const GIT_READ_SUBCOMMANDS: ReadonlySet<string> = new Set([
|
|
80
|
+
"status", "log", "diff", "show", "branch", "blame", "describe",
|
|
81
|
+
"shortlog", "rev-parse", "ls-files", "ls-tree", "cat-file", "whatchanged",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Head tokens that cannot write outside their arguments and whose named
|
|
86
|
+
* operation is a read, a navigation, a build, a test or a lint.
|
|
87
|
+
*
|
|
88
|
+
* `node`, `python`, `ruby`, `php` and `sh` are deliberately absent: their named
|
|
89
|
+
* operation is "run this program", which establishes nothing about what the
|
|
90
|
+
* program does. A test runner is admitted by name because the name IS the
|
|
91
|
+
* operation.
|
|
92
|
+
*/
|
|
93
|
+
const SAFE_HEADS: ReadonlySet<string> = new Set([
|
|
94
|
+
"ls", "pwd", "cat", "head", "tail", "wc", "file", "stat", "find",
|
|
95
|
+
"grep", "rg", "tree", "du", "df", "which", "basename", "dirname",
|
|
96
|
+
"realpath", "sort", "uniq", "cut", "diff", "cmp", "date", "echo",
|
|
97
|
+
"tsc", "eslint", "prettier", "biome", "stylelint",
|
|
98
|
+
"vitest", "jest", "mocha", "pytest", "mypy", "ruff", "flake8", "black",
|
|
99
|
+
"phpstan", "phpunit", "psalm", "pest", "rspec", "rubocop",
|
|
100
|
+
"golangci-lint", "shellcheck", "markdownlint", "hadolint",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Multi-purpose runners: the head token decides nothing, so the subcommand
|
|
105
|
+
* must be declared. `npm run <script>` executes whatever the manifest says,
|
|
106
|
+
* which is why the script name is checked separately below.
|
|
107
|
+
*/
|
|
108
|
+
const RUNNER_SUBCOMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
109
|
+
npm: new Set(["test", "run", "ls", "list", "why", "outdated", "view"]),
|
|
110
|
+
pnpm: new Set(["test", "run", "ls", "list", "why", "outdated"]),
|
|
111
|
+
yarn: new Set(["test", "run", "list", "why", "info"]),
|
|
112
|
+
npx: new Set(["tsc", "eslint", "prettier", "vitest", "jest"]),
|
|
113
|
+
composer: new Set(["test", "run-script", "show", "validate", "diagnose"]),
|
|
114
|
+
cargo: new Set(["test", "build", "check", "clippy", "fmt", "tree"]),
|
|
115
|
+
go: new Set(["test", "build", "vet", "list", "version"]),
|
|
116
|
+
make: new Set(["test", "build", "lint", "check", "typecheck"]),
|
|
117
|
+
task: new Set(["test", "build", "lint", "check", "typecheck", "--list"]),
|
|
118
|
+
just: new Set(["test", "build", "lint", "check"]),
|
|
119
|
+
bundle: new Set(["exec"]),
|
|
120
|
+
dotnet: new Set(["test", "build"]),
|
|
121
|
+
mvn: new Set(["test", "compile", "verify"]),
|
|
122
|
+
gradle: new Set(["test", "build", "check"]),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Script names a `run`-style subcommand may name and stay category A. */
|
|
126
|
+
const SAFE_SCRIPT_NAMES: ReadonlySet<string> = new Set([
|
|
127
|
+
"test", "tests", "lint", "typecheck", "types", "build", "check",
|
|
128
|
+
"format", "fmt", "unit", "e2e", "coverage", "compile",
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
/** Subcommands that take a script name rather than acting themselves. */
|
|
132
|
+
const SCRIPT_TAKING_SUBCOMMANDS: ReadonlySet<string> = new Set([
|
|
133
|
+
"run", "run-script", "exec",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Is `candidate` inside `root`?
|
|
138
|
+
*
|
|
139
|
+
* Pure `path` arithmetic, no `fs`: the classifier must be decidable from the
|
|
140
|
+
* envelope alone, and a stat call would make it depend on what happens to
|
|
141
|
+
* exist on the machine that runs the hook.
|
|
142
|
+
*/
|
|
143
|
+
export function isInsideWorkingTree(candidate: string, root: string): boolean {
|
|
144
|
+
if (!candidate.trim() || !root.trim()) return false;
|
|
145
|
+
const abs = path.resolve(root, candidate);
|
|
146
|
+
const rel = path.relative(path.resolve(root), abs);
|
|
147
|
+
if (rel === "") return true;
|
|
148
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Split a command on whitespace; the caller has already refused metacharacters. */
|
|
152
|
+
function _argv(command: string): string[] {
|
|
153
|
+
return command.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Strip a leading `-`/`--` so a flag is never mistaken for a subcommand. */
|
|
157
|
+
function _isFlag(token: string): boolean {
|
|
158
|
+
return token.startsWith("-");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** First non-flag token after the head, or `""`. */
|
|
162
|
+
function _firstSubcommand(argv: readonly string[]): string {
|
|
163
|
+
for (const token of argv.slice(1)) {
|
|
164
|
+
if (!_isFlag(token)) return token;
|
|
165
|
+
}
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `git` global options that consume the NEXT token as their value.
|
|
171
|
+
*
|
|
172
|
+
* Without this, `git -C sub status` resolves its subcommand to `sub` and the
|
|
173
|
+
* whole directory-flag form — the shape the canon now teaches in place of
|
|
174
|
+
* `cd X && …` — falls out of category A. That defect was found by the friction
|
|
175
|
+
* corpus on its first run, which is the reason the corpus counts confirmations
|
|
176
|
+
* rather than gates: a gate count would have looked identical.
|
|
177
|
+
*
|
|
178
|
+
* The `--flag=value` spelling consumes no extra token and needs no entry.
|
|
179
|
+
*/
|
|
180
|
+
const GIT_VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
181
|
+
"-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path",
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The `git` subcommand, with global options skipped.
|
|
186
|
+
*
|
|
187
|
+
* Returns `""` when a global option's VALUE escapes the working tree — an
|
|
188
|
+
* absolute path or a `..` traversal. The check is textual rather than resolved
|
|
189
|
+
* against a root because this function is pure and receives no root; what it
|
|
190
|
+
* can decide is exactly the property that matters, and a relative path with no
|
|
191
|
+
* `..` cannot escape.
|
|
192
|
+
*/
|
|
193
|
+
function _gitSubcommand(argv: readonly string[]): string {
|
|
194
|
+
for (let i = 1; i < argv.length; i++) {
|
|
195
|
+
const token = argv[i] as string;
|
|
196
|
+
if (!_isFlag(token)) return token;
|
|
197
|
+
const [flag, inlineValue] = token.includes("=")
|
|
198
|
+
? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)]
|
|
199
|
+
: [token, null];
|
|
200
|
+
const value = inlineValue ?? (GIT_VALUE_FLAGS.has(flag) ? argv[++i] : undefined);
|
|
201
|
+
if (value !== undefined && (path.isAbsolute(value) || value.split("/").includes(".."))) {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Non-flag token after the subcommand, or `""`. */
|
|
209
|
+
function _secondSubcommand(argv: readonly string[]): string {
|
|
210
|
+
const nonFlags = argv.slice(1).filter((t) => !_isFlag(t));
|
|
211
|
+
return nonFlags[1] ?? "";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Does any token in the command name a consequence operation?
|
|
216
|
+
*
|
|
217
|
+
* Tokenised rather than substring-matched: a substring test would refuse
|
|
218
|
+
* `git log --format=…` for containing `mat`, and a classifier that refuses
|
|
219
|
+
* everything is indistinguishable from one that was never wired.
|
|
220
|
+
*/
|
|
221
|
+
export function namesConsequenceOperation(command: string): boolean {
|
|
222
|
+
for (const raw of _argv(command)) {
|
|
223
|
+
const token = raw.replace(/^-+/, "").toLowerCase();
|
|
224
|
+
if (CONSEQUENCE_WORDS.has(token)) return true;
|
|
225
|
+
// `npm:publish`, `db:seed`, `deploy:prod` — a namespaced script name
|
|
226
|
+
// hides the consequential half behind a separator.
|
|
227
|
+
for (const part of token.split(/[:/=,]/)) {
|
|
228
|
+
if (part && CONSEQUENCE_WORDS.has(part)) return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Is this Bash command string category A? */
|
|
235
|
+
export function isCategoryABashCommand(command: string): boolean {
|
|
236
|
+
if (typeof command !== "string" || !command.trim()) return false;
|
|
237
|
+
if (SHELL_METACHARACTERS.test(command)) return false;
|
|
238
|
+
if (namesConsequenceOperation(command)) return false;
|
|
239
|
+
|
|
240
|
+
const argv = _argv(command);
|
|
241
|
+
const head = (argv[0] ?? "").toLowerCase();
|
|
242
|
+
if (!head) return false;
|
|
243
|
+
|
|
244
|
+
if (SAFE_HEADS.has(head)) return true;
|
|
245
|
+
|
|
246
|
+
if (head === "git") {
|
|
247
|
+
return GIT_READ_SUBCOMMANDS.has(_gitSubcommand(argv));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const allowed = RUNNER_SUBCOMMANDS[head];
|
|
251
|
+
if (!allowed) return false;
|
|
252
|
+
const sub = _firstSubcommand(argv);
|
|
253
|
+
if (!sub || !allowed.has(sub)) return false;
|
|
254
|
+
if (SCRIPT_TAKING_SUBCOMMANDS.has(sub)) {
|
|
255
|
+
const script = _secondSubcommand(argv);
|
|
256
|
+
return script !== "" && SAFE_SCRIPT_NAMES.has(script.toLowerCase());
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Is this tool call category A?
|
|
263
|
+
*
|
|
264
|
+
* `workingTree` is the root every path argument must resolve inside. An empty
|
|
265
|
+
* root means the dispatcher could not establish one, and an unestablished
|
|
266
|
+
* boundary is not a boundary — the call is not category A.
|
|
267
|
+
*/
|
|
268
|
+
export function isCategoryA(
|
|
269
|
+
toolName: string,
|
|
270
|
+
toolInput: ToolInput,
|
|
271
|
+
workingTree: string,
|
|
272
|
+
): boolean {
|
|
273
|
+
if (!workingTree.trim()) return false;
|
|
274
|
+
if (typeof toolName !== "string" || !toolName) return false;
|
|
275
|
+
|
|
276
|
+
if (READ_ONLY_TOOLS.has(toolName)) {
|
|
277
|
+
for (const key of PATH_KEYS) {
|
|
278
|
+
const value = toolInput?.[key];
|
|
279
|
+
if (typeof value !== "string") continue;
|
|
280
|
+
if (!isInsideWorkingTree(value, workingTree)) return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (toolName === "Bash") {
|
|
286
|
+
const command = toolInput?.["command"];
|
|
287
|
+
return typeof command === "string" && isCategoryABashCommand(command);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Routes user-scope `~/Documents/Cline/Hooks/<HookName>` events into
|
|
5
5
|
# the active workspace's `./agent-config dispatch:hook`. Project-scope
|
|
6
6
|
# `.clinerules/hooks/<HookName>` does NOT need this trampoline —
|
|
7
|
-
#
|
|
7
|
+
# the installer's `ensure_cline_bridge()` writes per-event scripts there
|
|
8
8
|
# that dispatch directly because Cline fires project hooks with the
|
|
9
9
|
# workspace as cwd.
|
|
10
10
|
#
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Routes user-scope `~/.cursor/hooks.json` events into the active
|
|
5
5
|
# workspace's `./agent-config dispatch:hook`. Project-scope
|
|
6
|
-
# `.cursor/hooks.json` does NOT need this trampoline —
|
|
6
|
+
# `.cursor/hooks.json` does NOT need this trampoline — the installer
|
|
7
7
|
# `ensure_cursor_bridge()` writes direct dispatch:hook commands there
|
|
8
8
|
# because the project hooks fire with the workspace as cwd.
|
|
9
9
|
#
|
|
@@ -20,10 +20,16 @@
|
|
|
20
20
|
# - Re-pipe the original JSON into
|
|
21
21
|
# ./agent-config dispatch:hook --platform cursor \
|
|
22
22
|
# --event $1 --native-event $2
|
|
23
|
-
# - Always exit 0 — Cursor's pre-hooks can block via exit code
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
23
|
+
# - Always exit 0 — Cursor's pre-hooks can block via exit code. The
|
|
24
|
+
# manifest declares 7 concerns at `severity: blocking`, and this host
|
|
25
|
+
# binds none of them: cursor's platform row carries session_start,
|
|
26
|
+
# session_end, stop, user_prompt_submit and post_tool_use, and every
|
|
27
|
+
# blocking concern sits in a slot or platform row outside that set.
|
|
28
|
+
# The count is a YAML parse of the concerns map, never `grep -c
|
|
29
|
+
# 'severity: blocking'`, which returns 8 by also counting the prose
|
|
30
|
+
# line that quotes the key. What IS bound here — chat-history /
|
|
31
|
+
# roadmap-progress / context-hygiene — is observe-only, and
|
|
32
|
+
# onboarding-gate writes state without denying sessionStart.
|
|
27
33
|
|
|
28
34
|
set -u
|
|
29
35
|
|
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
* stdout/stderr split, and exit codes. Helper names keep snake_case for
|
|
10
10
|
* fidelity (concern authors rely on the documented surface).
|
|
11
11
|
*
|
|
12
|
-
* Per `docs/contracts/hook-architecture-v1.md`. Reads
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (0=allow, 1=block, 2=warn, ≥3=error → fail-open unless concern is
|
|
17
|
-
* fail_closed).
|
|
12
|
+
* Per `docs/contracts/hook-architecture-v1.md`. Reads `src/scripts/hook_manifest.yaml`,
|
|
13
|
+
* resolves which concerns fire on the given (platform, event) tuple, and runs each
|
|
14
|
+
* sequentially with the stdin envelope contract. Reduces concern exit codes per the
|
|
15
|
+
* spec (0=allow, 1=block, 2=warn, ≥3=error → fail-open unless concern is fail_closed).
|
|
18
16
|
*
|
|
19
17
|
* Invocation:
|
|
20
18
|
*
|
|
@@ -50,13 +48,16 @@ import {
|
|
|
50
48
|
setHookStdinOverride,
|
|
51
49
|
clearHookStdinOverride,
|
|
52
50
|
} from "./hook_stdin.js";
|
|
53
|
-
import { emitFor, type Severity } from "./host_semantics.js";
|
|
51
|
+
import { emitFor, type PermissionDecision, type Severity } from "./host_semantics.js";
|
|
52
|
+
import { _concern_permission_verdict, _payload_tool_name, _permission_for } from "./permission_gate.js";
|
|
53
|
+
export { _concern_permission_verdict, _payload_tool_input, _payload_tool_name, _permission_for, _working_tree_root } from "./permission_gate.js";
|
|
54
54
|
import { _concern_body_classes, planPayloadShapes } from "./payload_stub.js";
|
|
55
55
|
import { resolveSessionRole, type SessionRole } from "../_lib/session_role.js";
|
|
56
56
|
import { stdinReadFailure, denyOnStdinFailure } from './stdin_failure_policy.js';
|
|
57
57
|
export { stdinReadFailure, denyOnStdinFailure, _is_fail_closed_blocking } from './stdin_failure_policy.js';
|
|
58
58
|
import { _py_json_dumps } from './py_json_dumps.js';
|
|
59
59
|
import { _fallback_yaml } from './fallback_yaml.js';
|
|
60
|
+
import { detectSurface } from '../_lib/surface.js';
|
|
60
61
|
import { recordCapture, recordOpportunity } from "../_lib/collector_denominator.js";
|
|
61
62
|
export { _fallback_yaml } from './fallback_yaml.js';
|
|
62
63
|
|
|
@@ -255,20 +256,6 @@ interface ConcernDef extends JsonObject {
|
|
|
255
256
|
name: string;
|
|
256
257
|
}
|
|
257
258
|
|
|
258
|
-
/**
|
|
259
|
-
* The tool name the host reported for this event, or `""`.
|
|
260
|
-
*
|
|
261
|
-
* Only tool events carry one. Read defensively: the payload is host-shaped and
|
|
262
|
-
* a missing / non-string field must degrade to "unknown tool", never throw.
|
|
263
|
-
*/
|
|
264
|
-
export function _payload_tool_name(envelope: JsonObject): string {
|
|
265
|
-
const payload = envelope["payload"];
|
|
266
|
-
if (!_isObject(payload)) {
|
|
267
|
-
return "";
|
|
268
|
-
}
|
|
269
|
-
const raw = payload["tool_name"];
|
|
270
|
-
return typeof raw === "string" ? raw : "";
|
|
271
|
-
}
|
|
272
259
|
|
|
273
260
|
/**
|
|
274
261
|
* Per-concern tool filter — the `tools:` key.
|
|
@@ -290,7 +277,7 @@ export function _payload_tool_name(envelope: JsonObject): string {
|
|
|
290
277
|
* - A host matcher would only help the two hosts that support one (Claude,
|
|
291
278
|
* Gemini). The in-process skip helps all eight platforms in the manifest.
|
|
292
279
|
*
|
|
293
|
-
*
|
|
280
|
+
* What this does NOT claim:
|
|
294
281
|
*
|
|
295
282
|
* It does not claim a latency win. The measured hook cost that was fixed was
|
|
296
283
|
* the *invocation path* (~370 of ~450–500 ms was eager CLI imports); nothing in
|
|
@@ -509,6 +496,7 @@ export function _build_envelope(args: Args, payload_text: string): JsonObject {
|
|
|
509
496
|
process.env["AGENT_SESSION_ID"] ||
|
|
510
497
|
"",
|
|
511
498
|
workspace_root: process.cwd(),
|
|
499
|
+
surface: detectSurface(payloadObj),
|
|
512
500
|
payload: payloadObj,
|
|
513
501
|
settings: {},
|
|
514
502
|
};
|
|
@@ -1280,6 +1268,11 @@ export function main(argv?: string[]): number {
|
|
|
1280
1268
|
// fixed-field schema (PII-exclusion-by-construction), and a concern's raw
|
|
1281
1269
|
// stderr is free-form content. This array is in-memory only.
|
|
1282
1270
|
const concern_messages: ConcernMessage[] = [];
|
|
1271
|
+
// Per-concern permission verdicts for the host's `permissionDecision` field.
|
|
1272
|
+
// Separate from `rcs` because the two ladders are not the same: an advisory
|
|
1273
|
+
// concern can ask for a confirmation without blocking, and a reduced `allow`
|
|
1274
|
+
// must not become a grant when one of them did.
|
|
1275
|
+
const permission_verdicts: PermissionDecision[] = [];
|
|
1283
1276
|
// Per-concern `tools:` filter (see _concern_matches_tool). Applied here so it
|
|
1284
1277
|
// is one place, after the envelope exists and before any concern is spawned.
|
|
1285
1278
|
const tool_name = _payload_tool_name(envelope);
|
|
@@ -1339,6 +1332,7 @@ export function main(argv?: string[]): number {
|
|
|
1339
1332
|
}
|
|
1340
1333
|
rcs.push(rc);
|
|
1341
1334
|
const reply = _parse_concern_stdout(stdout_text);
|
|
1335
|
+
permission_verdicts.push(_concern_permission_verdict(rc, reply));
|
|
1342
1336
|
// A concern states its reason either as JSON {"reason": …} on stdout
|
|
1343
1337
|
// (advisory concerns) or as a formatted stderr line (the block guards, e.g.
|
|
1344
1338
|
// `block-no-verify: BLOCKED — …`). Both are captured here so the emission
|
|
@@ -1423,6 +1417,7 @@ export function main(argv?: string[]): number {
|
|
|
1423
1417
|
_severity_for(final_rc) as Severity,
|
|
1424
1418
|
decidingReasons,
|
|
1425
1419
|
final_rc,
|
|
1420
|
+
_permission_for(args.event, envelope, final_rc, permission_verdicts),
|
|
1426
1421
|
);
|
|
1427
1422
|
if (emission.stdout) {
|
|
1428
1423
|
process.stdout.write(emission.stdout);
|
|
@@ -116,6 +116,22 @@ export const BLOCK_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
|
|
|
116
116
|
"worktree-remove",
|
|
117
117
|
/** `-x` is the flag that takes `.env` and local certificates. */
|
|
118
118
|
"clean-ignored",
|
|
119
|
+
|
|
120
|
+
// ── The consequence vocabulary, added 2026-09-06. Every one is already
|
|
121
|
+
// never-autonomous in the `non-destructive-by-default` prose.
|
|
122
|
+
//
|
|
123
|
+
// MEMBERSHIP HERE IS A COUNTING DECISION, NOT A REFUSAL. The set's name
|
|
124
|
+
// predates ADR-254, which deleted the reader that blocked: the one consumer
|
|
125
|
+
// that acts on it now is `conformance_scan`'s after-the-fact tally, so
|
|
126
|
+
// "blocked" describes what the Hard Floor requires of the model, never what
|
|
127
|
+
// this package does at tool-call time.
|
|
128
|
+
"prod-deploy",
|
|
129
|
+
"prod-data-destroy",
|
|
130
|
+
"prod-infra-change",
|
|
131
|
+
"external-send",
|
|
132
|
+
"money-movement",
|
|
133
|
+
"trunk-force-push",
|
|
134
|
+
"out-of-scope-destruction",
|
|
119
135
|
]);
|
|
120
136
|
|
|
121
137
|
/** Recoverable operations — warned without a this-turn authorization. */
|
|
@@ -154,6 +170,17 @@ const G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
|
|
|
154
170
|
/** Optional absolute/relative path in front of the tool word: `/usr/bin/npm`. */
|
|
155
171
|
const P = "^(?:\\S*\\/)?";
|
|
156
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Tool words the consequence rows classify, as a head anchor.
|
|
175
|
+
*
|
|
176
|
+
* Kept as one constant rather than folded into each row: `commandOp` filters
|
|
177
|
+
* segments by head BEFORE consulting the table, so a row whose tool word is
|
|
178
|
+
* missing here never runs — a silent no-op that looks exactly like a pattern
|
|
179
|
+
* that did not match.
|
|
180
|
+
*/
|
|
181
|
+
const CONSEQUENCE_TOOL_HEAD =
|
|
182
|
+
/^(?:\S*\/)?(terraform|pulumi|kubectl|helm|aws|gcloud|az|vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku|cap|psql|mysql|mariadb|sqlite3|mongosh|mongo|redis-cli|sendmail|msmtp|mail|mailx|curl|stripe|braintree|paypal|rm)\b/i;
|
|
183
|
+
|
|
157
184
|
/**
|
|
158
185
|
* A `gh api` call that WRITES. Read-only `gh api` is the standard way to inspect
|
|
159
186
|
* GitHub state and must never be refused.
|
|
@@ -267,6 +294,13 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
267
294
|
// Above `push`, and both spellings: `--force` and `--force-with-lease`. The
|
|
268
295
|
// "safe force" is in the same class here because what it is safe against is a
|
|
269
296
|
// stale local view, not a collaborator who pushed after your fetch.
|
|
297
|
+
{
|
|
298
|
+
op: "trunk-force-push",
|
|
299
|
+
re: new RegExp(
|
|
300
|
+
`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)[^\\n;|&]*\\b(main|master|trunk|production|prod)\\b`,
|
|
301
|
+
"i",
|
|
302
|
+
),
|
|
303
|
+
},
|
|
270
304
|
{
|
|
271
305
|
op: "force-push",
|
|
272
306
|
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i"),
|
|
@@ -296,6 +330,73 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
296
330
|
},
|
|
297
331
|
{ op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
|
|
298
332
|
|
|
333
|
+
// ── Consequence operations, added 2026-09-06
|
|
334
|
+
// (`road-to-authorization-that-reaches-further` 2.1).
|
|
335
|
+
//
|
|
336
|
+
// ORDER: above the generic `push`/`commit` fallbacks, because
|
|
337
|
+
// `trunk-force-push` is a `git push` and the most specific pattern must win —
|
|
338
|
+
// the rule this table states at its head.
|
|
339
|
+
//
|
|
340
|
+
// These classify commands the count could not see before. They add NOTHING to
|
|
341
|
+
// any refusal path: `commandOp` and `BLOCK_OPS` have exactly one consumer
|
|
342
|
+
// that acts on them, the after-the-fact tally in `conformance_scan`, and this
|
|
343
|
+
// change does not add a second.
|
|
344
|
+
{
|
|
345
|
+
op: "prod-data-destroy",
|
|
346
|
+
re: new RegExp(
|
|
347
|
+
`${P}(psql|mysql|mariadb|sqlite3|mongosh?)\\b[^\\n]*\\b(drop\\s+(table|database|schema)|truncate|delete\\s+from)\\b` +
|
|
348
|
+
`|${P}aws\\s+s3\\s+(rm|rb)\\b` +
|
|
349
|
+
`|${P}aws\\s+(dynamodb|rds)\\s+delete-\\S+` +
|
|
350
|
+
`|${P}redis-cli\\b[^\\n]*\\bflush(all|db)\\b`,
|
|
351
|
+
"i",
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
op: "prod-infra-change",
|
|
356
|
+
re: new RegExp(
|
|
357
|
+
`${P}terraform\\s+(apply|destroy)\\b` +
|
|
358
|
+
`|${P}pulumi\\s+(up|destroy)\\b` +
|
|
359
|
+
`|${P}kubectl\\s+(apply|delete|scale|drain)\\b` +
|
|
360
|
+
`|${P}helm\\s+(install|upgrade|uninstall|rollback)\\b` +
|
|
361
|
+
`|${P}aws\\s+(iam|ec2|route53|elbv2)\\s+(create|delete|put|update|modify|attach|detach)-\\S+` +
|
|
362
|
+
`|${P}(gcloud|az)\\s+\\S+\\s+(create|delete|update)\\b`,
|
|
363
|
+
"i",
|
|
364
|
+
),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
op: "prod-deploy",
|
|
368
|
+
re: new RegExp(
|
|
369
|
+
`${P}(vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku)\\s+${G}deploy\\b` +
|
|
370
|
+
`|${P}(vercel|netlify)\\b[^\\n]*--prod\\b` +
|
|
371
|
+
`|${P}(npm|pnpm|yarn)\\s+run\\s+deploy\\b` +
|
|
372
|
+
`|${P}cap\\s+production\\s+deploy\\b`,
|
|
373
|
+
"i",
|
|
374
|
+
),
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
op: "external-send",
|
|
378
|
+
re: new RegExp(
|
|
379
|
+
`${P}(sendmail|msmtp|mailx?)\\b` +
|
|
380
|
+
`|${P}curl\\b(?=[^\\n]*(?:-X|--request)\\s+(?:POST|PUT|PATCH)\\b)(?=[^\\n]*https?:\\/\\/)`,
|
|
381
|
+
"i",
|
|
382
|
+
),
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
op: "money-movement",
|
|
386
|
+
re: new RegExp(
|
|
387
|
+
`${P}(stripe|braintree|paypal)\\s+\\S*(charge|refund|payout|transfer|payment)\\S*\\b`,
|
|
388
|
+
"i",
|
|
389
|
+
),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
// `rm -rf` on an ABSOLUTE path or a home-relative one. Deliberately not
|
|
393
|
+
// "outside the working tree" — this function receives a command string and
|
|
394
|
+
// no cwd, so that reading is not decidable here and a classifier that
|
|
395
|
+
// guesses is worse than one that under-reports.
|
|
396
|
+
op: "out-of-scope-destruction",
|
|
397
|
+
re: new RegExp(`${P}rm\\s+(-\\S+\\s+)*-\\S*[rR]\\S*\\s+(-\\S+\\s+)*(~|/)\\S*`, "i"),
|
|
398
|
+
},
|
|
399
|
+
|
|
299
400
|
{ op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
|
|
300
401
|
{ op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
|
|
301
402
|
{ op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") },
|
|
@@ -592,7 +693,14 @@ export function commandOp(command: string): GitOp | null {
|
|
|
592
693
|
// unchecked once `publish` was authorized.
|
|
593
694
|
let found: GitOp | null = null;
|
|
594
695
|
for (const seg of invokedSegments(command)) {
|
|
595
|
-
|
|
696
|
+
// Widened 2026-09-06 for the consequence vocabulary. Before this the gate
|
|
697
|
+
// admitted five tool words, so `terraform apply` and `aws s3 rm` were not
|
|
698
|
+
// merely unclassified — they could not reach the table at all, and the
|
|
699
|
+
// count read them as nothing rather than as unmeasured.
|
|
700
|
+
if (
|
|
701
|
+
!/^(?:\S*\/)?(git|gh|npm|pnpm|yarn)\b/.test(seg) &&
|
|
702
|
+
!CONSEQUENCE_TOOL_HEAD.test(seg)
|
|
703
|
+
) {
|
|
596
704
|
continue;
|
|
597
705
|
}
|
|
598
706
|
for (const { op, re } of COMMAND_OPS) {
|
|
@@ -634,6 +742,53 @@ export function mergeTargetOf(command: string): number | null {
|
|
|
634
742
|
return api ? _boundedTarget(api[1] as string) : null;
|
|
635
743
|
}
|
|
636
744
|
|
|
745
|
+
/**
|
|
746
|
+
* The object a consequence command acts on, or `null` when the command does not
|
|
747
|
+
* name one.
|
|
748
|
+
*
|
|
749
|
+
* The sibling of `mergeTargetOf`, generalised past pull-request numbers, and it
|
|
750
|
+
* inherits that function's fail-closed stance verbatim: a command that names no
|
|
751
|
+
* object matches no record, so the caller reports nothing rather than guessing.
|
|
752
|
+
* Under-reporting is the safe direction for a measurement; a guessed object
|
|
753
|
+
* would manufacture a violation out of a parse failure.
|
|
754
|
+
*
|
|
755
|
+
* Normalised to lower case with quotes stripped, so it compares against the
|
|
756
|
+
* prose side's `extractObjects` without either end re-deriving the rule.
|
|
757
|
+
*/
|
|
758
|
+
export function commandObject(op: GitOp, command: string): string | null {
|
|
759
|
+
const patterns = COMMAND_OBJECTS[op];
|
|
760
|
+
if (!patterns) return null;
|
|
761
|
+
for (const re of patterns) {
|
|
762
|
+
const m = new RegExp(re.source, re.flags.replace("g", "")).exec(command);
|
|
763
|
+
const raw = m?.[1];
|
|
764
|
+
if (raw) {
|
|
765
|
+
const value = raw.trim().replace(/^["'`]+|["'`]+$/g, "").replace(/[.,;:]+$/, "").toLowerCase();
|
|
766
|
+
if (value) return value;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Per-operation object patterns on the COMMAND side. */
|
|
773
|
+
const COMMAND_OBJECTS: Readonly<Record<string, readonly RegExp[]>> = {
|
|
774
|
+
"prod-data-destroy": [
|
|
775
|
+
/\bdrop\s+(?:table|database|schema|collection)\s+(?:if\s+exists\s+)?["'`]?([\w.$-]+)/i,
|
|
776
|
+
/\btruncate\s+(?:table\s+)?["'`]?([\w.$-]+)/i,
|
|
777
|
+
/\bdelete\s+from\s+["'`]?([\w.$-]+)/i,
|
|
778
|
+
/\bs3:\/\/([\w.-]+)/i,
|
|
779
|
+
/--table-name[= ]["'`]?([\w.-]+)/i,
|
|
780
|
+
],
|
|
781
|
+
"external-send": [
|
|
782
|
+
/\bhttps?:\/\/([^\s/"'`]+)/i,
|
|
783
|
+
/([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/,
|
|
784
|
+
],
|
|
785
|
+
"money-movement": [
|
|
786
|
+
/--customer[= ]["'`]?([\w.-]+)/i,
|
|
787
|
+
/--amount[= ]["'`]?([\d.,]+)/i,
|
|
788
|
+
/\b(\d[\d.,]*)\s*(?:eur|usd)\b/i,
|
|
789
|
+
],
|
|
790
|
+
};
|
|
791
|
+
|
|
637
792
|
/** A target only counts inside the API's own range; anything else is no target. */
|
|
638
793
|
function _boundedTarget(digits: string): number | null {
|
|
639
794
|
const n = Number.parseInt(digits, 10);
|