@kontourai/flow-agents 1.3.0 → 2.0.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.
Files changed (214) hide show
  1. package/.github/CODEOWNERS +29 -0
  2. package/.github/actions/trust-verify/action.yml +145 -0
  3. package/.github/workflows/ci.yml +11 -4
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +10 -2
  6. package/.github/workflows/release-please.yml +1 -1
  7. package/.github/workflows/trust-reconcile.yml +113 -0
  8. package/AGENTS.md +13 -0
  9. package/CHANGELOG.md +103 -0
  10. package/CONTRIBUTING.md +4 -4
  11. package/README.md +1 -0
  12. package/agents/tool-planner.json +1 -1
  13. package/build/src/cli/console-learning-projection.d.ts +1 -0
  14. package/build/src/cli/effective-backlog-settings.d.ts +1 -0
  15. package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
  16. package/build/src/cli/init.d.ts +17 -0
  17. package/build/src/cli/init.js +242 -20
  18. package/build/src/cli/kit.d.ts +1 -0
  19. package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
  20. package/build/src/cli/publish-change-helper.d.ts +1 -0
  21. package/build/src/cli/pull-work-provider.d.ts +1 -0
  22. package/build/src/cli/runtime-adapter.d.ts +1 -0
  23. package/build/src/cli/telemetry-doctor.d.ts +1 -0
  24. package/build/src/cli/usage-feedback.d.ts +1 -0
  25. package/build/src/cli/utterance-check.d.ts +1 -0
  26. package/build/src/cli/validate-hook-influence.d.ts +1 -0
  27. package/build/src/cli/validate-source-tree.d.ts +1 -0
  28. package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
  29. package/build/src/cli/validate-workflow-artifacts.js +19 -2
  30. package/build/src/cli/verify.d.ts +1 -0
  31. package/build/src/cli/verify.js +90 -0
  32. package/build/src/cli/veritas-governance.d.ts +1 -0
  33. package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
  34. package/build/src/cli/workflow-sidecar.d.ts +324 -0
  35. package/build/src/cli/workflow-sidecar.js +1973 -90
  36. package/build/src/cli.d.ts +2 -0
  37. package/build/src/cli.js +2 -3
  38. package/build/src/flow-kit/validate.d.ts +81 -0
  39. package/build/src/index.d.ts +5 -0
  40. package/build/src/index.js +36 -0
  41. package/build/src/lib/args.d.ts +8 -0
  42. package/build/src/lib/flow-resolver.d.ts +82 -0
  43. package/build/src/lib/flow-resolver.js +237 -0
  44. package/build/src/lib/fs.d.ts +7 -0
  45. package/build/src/lib/workflow-learning-projection.d.ts +132 -0
  46. package/build/src/runtime-adapters.d.ts +18 -0
  47. package/build/src/tools/build-universal-bundles.d.ts +2 -0
  48. package/build/src/tools/build-universal-bundles.js +34 -22
  49. package/build/src/tools/common.d.ts +9 -0
  50. package/build/src/tools/generate-context-map.d.ts +2 -0
  51. package/build/src/tools/generate-context-map.js +3 -16
  52. package/build/src/tools/validate-package.d.ts +2 -0
  53. package/build/src/tools/validate-source-tree.d.ts +2 -0
  54. package/build/src/tools/validate-source-tree.js +42 -162
  55. package/context/contracts/artifact-contract.md +10 -0
  56. package/context/contracts/delivery-contract.md +1 -0
  57. package/context/contracts/review-contract.md +1 -0
  58. package/context/contracts/verification-contract.md +2 -0
  59. package/context/gate-awareness.md +39 -0
  60. package/context/scripts/hooks/stop-goal-fit.js +632 -70
  61. package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
  62. package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
  63. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  64. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  65. package/docs/adr/0007-skill-audit.md +1 -1
  66. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
  67. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
  68. package/docs/adr/0011-mcp-posture.md +100 -0
  69. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
  70. package/docs/adr/0013-context-lifecycle.md +151 -0
  71. package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
  72. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
  73. package/docs/adr/0016-three-hard-boundary-model.md +71 -0
  74. package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
  75. package/docs/agent-system-guidebook.md +5 -12
  76. package/docs/context-map.md +4 -10
  77. package/docs/developer-architecture.md +14 -0
  78. package/docs/index.md +3 -2
  79. package/docs/integrations/framework-adapter.md +19 -6
  80. package/docs/integrations/index.md +2 -2
  81. package/docs/north-star.md +4 -4
  82. package/docs/operating-layers.md +3 -3
  83. package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
  84. package/docs/repository-structure.md +2 -2
  85. package/docs/skills-map.md +1 -0
  86. package/docs/spec/runtime-hook-surface.md +78 -10
  87. package/docs/standards-register.md +3 -3
  88. package/docs/survey-utterance-check.md +1 -1
  89. package/docs/trust-anchor-adoption.md +197 -0
  90. package/docs/verifiable-trust.md +95 -0
  91. package/docs/veritas-integration.md +2 -2
  92. package/docs/workflow-usage-guide.md +69 -0
  93. package/evals/acceptance/DEMO-false-completion.md +144 -0
  94. package/evals/acceptance/demo-cast.sh +92 -0
  95. package/evals/acceptance/demo-false-completion.sh +72 -0
  96. package/evals/acceptance/demo-real-evidence.sh +104 -0
  97. package/evals/acceptance/demo.tape +29 -0
  98. package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
  99. package/evals/acceptance/prove-capture-teeth.sh +114 -0
  100. package/evals/acceptance/prove-teeth.sh +105 -0
  101. package/evals/ci/antigaming-suite.sh +54 -0
  102. package/evals/ci/run-baseline.sh +2 -0
  103. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
  104. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
  105. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
  106. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
  107. package/evals/integration/test_builder_step_producers.sh +379 -0
  108. package/evals/integration/test_bundle_install.sh +35 -71
  109. package/evals/integration/test_bundle_lifecycle.sh +39 -2
  110. package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
  111. package/evals/integration/test_checkpoint_signing.sh +489 -0
  112. package/evals/integration/test_claim_lookup.sh +352 -0
  113. package/evals/integration/test_command_log_integrity.sh +275 -0
  114. package/evals/integration/test_context_map.sh +0 -2
  115. package/evals/integration/test_dual_emit_flow_step.sh +278 -0
  116. package/evals/integration/test_enforcer_expects_driven.sh +281 -0
  117. package/evals/integration/test_evidence_capture_hook.sh +185 -0
  118. package/evals/integration/test_flow_kit_repository.sh +2 -0
  119. package/evals/integration/test_flowdef_session_activation.sh +273 -0
  120. package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
  121. package/evals/integration/test_gate_bypass_chain.sh +448 -0
  122. package/evals/integration/test_gate_lockdown.sh +1137 -0
  123. package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
  124. package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
  125. package/evals/integration/test_goal_fit_hook.sh +69 -4
  126. package/evals/integration/test_goal_fit_rederive.sh +263 -0
  127. package/evals/integration/test_hook_category_behaviors.sh +14 -0
  128. package/evals/integration/test_install_merge.sh +1176 -0
  129. package/evals/integration/test_mint_attestation.sh +373 -0
  130. package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
  131. package/evals/integration/test_publish_delivery.sh +269 -0
  132. package/evals/integration/test_reconcile_soundness.sh +528 -0
  133. package/evals/integration/test_resolvefirststep_security.sh +208 -0
  134. package/evals/integration/test_session_resume_roundtrip.sh +286 -0
  135. package/evals/integration/test_trust_checkpoint.sh +325 -0
  136. package/evals/integration/test_trust_reconcile.sh +293 -0
  137. package/evals/integration/test_verify_cli.sh +208 -0
  138. package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
  139. package/evals/lib/node.sh +0 -6
  140. package/evals/run.sh +47 -0
  141. package/evals/static/test_library_exports.sh +85 -0
  142. package/evals/static/test_universal_bundles.sh +15 -0
  143. package/evals/static/test_workflow_skills.sh +6 -13
  144. package/install.sh +0 -7
  145. package/integrations/strands-ts/README.md +25 -15
  146. package/integrations/veritas/flow-agents.adapter.json +1 -2
  147. package/kits/builder/flows/build.flow.json +59 -12
  148. package/kits/builder/kit.json +85 -15
  149. package/kits/builder/skills/continue-work/SKILL.md +116 -0
  150. package/kits/builder/skills/deliver/SKILL.md +36 -6
  151. package/kits/builder/skills/design-probe/SKILL.md +28 -0
  152. package/kits/builder/skills/execute-plan/SKILL.md +9 -1
  153. package/kits/builder/skills/gate-review/SKILL.md +234 -0
  154. package/kits/builder/skills/learning-review/SKILL.md +30 -0
  155. package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
  156. package/kits/builder/skills/plan-work/SKILL.md +13 -1
  157. package/kits/builder/skills/pull-work/SKILL.md +19 -0
  158. package/kits/knowledge/adapters/default-store/index.js +38 -0
  159. package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
  160. package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
  161. package/kits/knowledge/docs/store-contract.md +314 -0
  162. package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
  163. package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
  164. package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
  165. package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
  166. package/kits/knowledge/evals/entities/suite.test.js +40 -0
  167. package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
  168. package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
  169. package/kits/knowledge/evals/retirement/suite.test.js +145 -0
  170. package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
  171. package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
  172. package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
  173. package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
  174. package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
  175. package/kits/knowledge/kit.json +51 -1
  176. package/package.json +13 -4
  177. package/packaging/conformance/README.md +10 -2
  178. package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
  179. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
  180. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
  181. package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
  182. package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
  183. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
  184. package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
  185. package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
  186. package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
  187. package/packaging/conformance/run-conformance.js +1 -1
  188. package/scripts/README.md +2 -1
  189. package/scripts/build-universal-bundles.js +0 -1
  190. package/scripts/ci/mint-attestation.js +221 -0
  191. package/scripts/ci/trust-reconcile.js +545 -0
  192. package/scripts/hooks/config-protection.js +423 -1
  193. package/scripts/hooks/evidence-capture.js +348 -0
  194. package/scripts/hooks/lib/liveness-read.js +113 -0
  195. package/scripts/hooks/run-hook.js +6 -1
  196. package/scripts/hooks/stop-goal-fit.js +1471 -79
  197. package/scripts/hooks/workflow-steering.js +135 -5
  198. package/scripts/install-codex-home.sh +39 -0
  199. package/scripts/install-merge.js +330 -0
  200. package/src/cli/init.ts +218 -20
  201. package/src/cli/validate-workflow-artifacts.ts +18 -2
  202. package/src/cli/verify.ts +100 -0
  203. package/src/cli/workflow-sidecar.ts +2093 -84
  204. package/src/cli.ts +2 -3
  205. package/src/index.ts +53 -0
  206. package/src/lib/flow-resolver.ts +284 -0
  207. package/src/tools/build-universal-bundles.ts +34 -21
  208. package/src/tools/generate-context-map.ts +3 -17
  209. package/src/tools/validate-source-tree.ts +44 -104
  210. package/tsconfig.json +1 -0
  211. package/build/src/tools/filter-installed-packs.js +0 -135
  212. package/packaging/packs.json +0 -49
  213. package/scripts/filter-installed-packs.js +0 -2
  214. package/src/tools/filter-installed-packs.ts +0 -132
@@ -8,6 +8,38 @@
8
8
  * Also blocks git verification-bypass flags in actual flag positions only.
9
9
  * Text that merely mentions the flag inside quoted strings or prose is allowed.
10
10
  *
11
+ * GATE LOCK-DOWN (adversarial review Finding 2 + anti-gaming; R5a extension):
12
+ * Also blocks Write/Edit to kill-switch / routing files that an agent could use
13
+ * to self-tamper the gate:
14
+ * - Shell profiles (.bashrc, .bash_profile, etc.) — where an agent could
15
+ * export FLOW_AGENTS_GOAL_FIT_MODE=off for the next bash -lc hook invocation.
16
+ * - .claude/settings.json / settings.local.json — where an agent could add an
17
+ * env block or delete the Stop hook entry entirely.
18
+ * - .flow-agents/current.json — where an agent could forge active_flow_id /
19
+ * active_step_id to route the gate to a fake permissive FlowDefinition.
20
+ * - .flow-agents/<slug>/state.json — where an agent could forge phase=planning
21
+ * to downgrade from FULL_BLOCK to HARD_BLOCK (R5a: MEDIUM finding fix).
22
+ * SAFE: the CLI writes state.json via writeState → writeJson → fs.writeFileSync.
23
+ * - .flow-agents/<slug>/trust.bundle — where an agent could forge claims.
24
+ * SAFE: the CLI writes trust.bundle via writeTrustBundle → writeJson → fs.writeFileSync.
25
+ * (Verified: ensureSession / advanceState / writeCurrent / writeState /
26
+ * writeTrustBundle all use writeJson which calls fs.writeFileSync directly —
27
+ * blocking the Write/Edit tool path does NOT break legit CLI sidecar writes.)
28
+ *
29
+ * Also detects (best-effort) Bash-command redirects that write to the same paths
30
+ * via shell operators (>, >>, tee). HONEST: this is INCOMPLETE COVERAGE — sed -i,
31
+ * process substitution, /dev/fd tricks, and other non-standard redirect forms are
32
+ * NOT caught. An agent with shell access can still evade. The real anchor is
33
+ * external: signed checkpoints + human review. This raises the bar, not a wall.
34
+ *
35
+ * INTERPRETER-WRITE DETECTION — INCOMPLETE (R5a best-effort):
36
+ * Also checks Bash commands matching interpreter patterns (node -e, py3 -c,
37
+ * sed -i, perl -e) combined with a protected-path token literal in the command
38
+ * string. KNOWN EVASIONS NOT CAUGHT: runtime-constructed paths (process.env.HOME +
39
+ * '/.bashrc', homedir()+'/.bashrc'), base64-encoded paths, multi-step path assembly,
40
+ * any interpreter not in the list (ruby, php, etc.), and multiline here-docs.
41
+ * The real anchor remains external (clean CI env + human review).
42
+ *
11
43
  * Exit codes: 0 = allow, 2 = block
12
44
  */
13
45
 
@@ -26,8 +58,118 @@ const PROTECTED_FILES = new Set([
26
58
  '.ruff.toml', 'ruff.toml',
27
59
  '.shellcheckrc', '.stylelintrc', '.stylelintrc.json', '.stylelintrc.yml',
28
60
  '.markdownlint.json', '.markdownlint.yaml', '.markdownlintrc',
61
+ // ── Gate lock-down: shell profiles ────────────────────────────────────────
62
+ // An agent could write `export FLOW_AGENTS_GOAL_FIT_MODE=off` here so the next
63
+ // `bash -lc <hook>` invocation inherits the override and silences the gate.
64
+ // Blocking Write/Edit to these basenames raises the cost without stopping
65
+ // legitimate terminal customization done outside the agent session.
66
+ '.bash_profile', '.bashrc', '.profile', '.zprofile', '.zshrc',
29
67
  ]);
30
68
 
69
+ // ---------------------------------------------------------------------------
70
+ // Gate lock-down: path-pattern protected files
71
+ //
72
+ // These files need PATH context (not just basename) to avoid over-matching
73
+ // common file names in unrelated projects.
74
+ //
75
+ // R5a adds .flow-agents/<slug>/state.json and .flow-agents/<slug>/trust.bundle.
76
+ //
77
+ // checkProtectedPathPattern(filePath) → { name, reason } | null
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Check whether a Write/Edit tool file path targets a gate kill-switch file
82
+ * that requires path-level matching (.claude/settings.json,
83
+ * .flow-agents/current.json, .flow-agents/<slug>/state.json,
84
+ * .flow-agents/<slug>/trust.bundle).
85
+ *
86
+ * Returns { name, reason } when blocked, null when allowed.
87
+ *
88
+ * BAR-RAISER framing (honest): this is not airtight. An agent that writes the
89
+ * file through a Bash redirect or through a shell call that constructs the path
90
+ * at runtime can still evade. The real anchor is external (signed checkpoints +
91
+ * human review). We raise the cost of casual/direct manipulation.
92
+ */
93
+ function checkProtectedPathPattern(filePath) {
94
+ if (!filePath || typeof filePath !== 'string') return null;
95
+ // Normalize: forward-slashes, strip leading ~/
96
+ const norm = filePath.replace(/\\/g, '/').replace(/^~\//, '');
97
+
98
+ // .claude/settings.json — an agent could add an env block or delete the Stop
99
+ // hook to disable gate enforcement for the entire session.
100
+ if (/(?:^|\/)\.claude\/settings\.json$/.test(norm)) {
101
+ return {
102
+ name: '.claude/settings.json',
103
+ reason: 'an agent could add an env block or remove the Stop hook to disable gate enforcement',
104
+ };
105
+ }
106
+
107
+ // .claude/settings.local.json — same risk as settings.json (local overrides
108
+ // are loaded alongside the main settings file by Claude Code).
109
+ if (/(?:^|\/)\.claude\/settings\.local\.json$/.test(norm)) {
110
+ return {
111
+ name: '.claude/settings.local.json',
112
+ reason: 'an agent could add an env block or remove the Stop hook to disable gate enforcement',
113
+ };
114
+ }
115
+
116
+ // .flow-agents/current.json — an agent could forge active_flow_id / active_step_id
117
+ // to route the gate to a permissive or empty-expects FlowDefinition.
118
+ // SAFE: the workflow CLI writes current.json via fs (writeJson → fs.writeFileSync),
119
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
120
+ if (/(?:^|\/)\.flow-agents\/current\.json$/.test(norm)) {
121
+ return {
122
+ name: '.flow-agents/current.json',
123
+ reason: 'an agent could forge active_flow_id/active_step_id to route the gate to a permissive FlowDefinition',
124
+ };
125
+ }
126
+
127
+ // .flow-agents/<slug>/state.json — an agent could forge phase=planning to
128
+ // downgrade the block regime (FULL_BLOCK → HARD_BLOCK) and weaken gate checks.
129
+ // SAFE: the CLI writes state.json via writeState → writeJson → fs.writeFileSync,
130
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
131
+ if (/(?:^|\/)\.flow-agents\/[^/]+\/state\.json$/.test(norm)) {
132
+ return {
133
+ name: '.flow-agents/<slug>/state.json',
134
+ reason: 'an agent could forge phase=planning to downgrade the block regime and weaken gate enforcement',
135
+ };
136
+ }
137
+
138
+ // .flow-agents/<slug>/trust.bundle — an agent could forge claims (e.g. status=verified
139
+ // or impactLevel=low) to suppress gate blocks or make disputed evidence appear accepted.
140
+ // SAFE: the CLI writes trust.bundle via writeTrustBundle → writeJson → fs.writeFileSync,
141
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
142
+ if (/(?:^|\/)\.flow-agents\/[^/]+\/trust\.bundle$/.test(norm)) {
143
+ return {
144
+ name: '.flow-agents/<slug>/trust.bundle',
145
+ reason: 'an agent could forge trust claims (verified status, impact level) to bypass gate integrity checks',
146
+ };
147
+ }
148
+
149
+ // delivery/trust.bundle is the CI anchor read by trust-reconcile.js and
150
+ // used as the attestation subject in mint-attestation.js. An agent could
151
+ // copy a forged bundle here to corrupt the CI trust check.
152
+ // SAFE: publishDelivery writes via fs.copyFileSync (not Write/Edit tool).
153
+ // RESIDUAL: runtime-constructed paths and fs writes are unaffected.
154
+ if (/(?:^|\/)delivery\/trust\.bundle$/.test(norm)) {
155
+ return {
156
+ name: "delivery/trust.bundle",
157
+ reason: "an agent could write a forged bundle to corrupt the CI trust-reconcile anchor",
158
+ };
159
+ }
160
+
161
+ // delivery/trust.checkpoint.json -- the signed checkpoint companion.
162
+ // SAFE: publishDelivery writes via fs.copyFileSync, NOT via Write/Edit tool.
163
+ if (/(?:^|\/)delivery\/trust\.checkpoint\.json$/.test(norm)) {
164
+ return {
165
+ name: "delivery/trust.checkpoint.json",
166
+ reason: "an agent could forge a signed delivery by writing a tampered checkpoint",
167
+ };
168
+ }
169
+
170
+ return null;
171
+ }
172
+
31
173
  // ---------------------------------------------------------------------------
32
174
  // Shell-aware tokenizer
33
175
  //
@@ -217,6 +359,233 @@ function checkCommandForBypass(command) {
217
359
  return null;
218
360
  }
219
361
 
362
+ // ---------------------------------------------------------------------------
363
+ // Gate lock-down: Bash-redirect detection for protected kill-switch paths
364
+ //
365
+ // When the agent uses a Bash tool (tool_input.command), detect redirects that
366
+ // write to protected paths via shell redirect operators (> >>) or `tee`.
367
+ //
368
+ // R5a extends coverage to .flow-agents/<slug>/state.json and trust.bundle.
369
+ //
370
+ // HONEST — INCOMPLETE COVERAGE: this catches the obvious forms only:
371
+ // - `cmd > ~/.bashrc` ✓ caught (> redirect operator)
372
+ // - `cmd >> .claude/settings.json` ✓ caught (>> redirect operator)
373
+ // - `tee .flow-agents/current.json` ✓ caught (tee command)
374
+ // - `cmd > .flow-agents/s/state.json` ✓ caught (> redirect operator)
375
+ // - `tee .flow-agents/s/trust.bundle` ✓ caught (tee command)
376
+ // - `sed -i ... .bashrc` ✗ NOT caught (in-place edit)
377
+ // - `interpreted-lang -c "open(...,'w')" ✗ NOT caught (see interpreter check)
378
+ // - `/dev/fd` tricks, process substitution ✗ NOT caught
379
+ //
380
+ // An agent with shell access can still evade by constructing paths at runtime,
381
+ // using subshells, or using tools not scanned here. The real tamper-proof
382
+ // boundary is external: signed checkpoints + human review. This raises the bar.
383
+ // ---------------------------------------------------------------------------
384
+
385
+ /**
386
+ * Regex that matches a normalized (forward-slash) path ending with a protected
387
+ * kill-switch file: shell profiles, .claude/settings*.json,
388
+ * .flow-agents/current.json, .flow-agents/<slug>/state.json,
389
+ * .flow-agents/<slug>/trust.bundle.
390
+ */
391
+ const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)\.flow-agents\/current\.json$|(?:^|\/)\.flow-agents\/[^/]+\/state\.json$|(?:^|\/)\.flow-agents\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
392
+
393
+ /**
394
+ * Return true when a token (an unquoted redirect target or tee argument) matches
395
+ * a protected kill-switch path.
396
+ */
397
+ function matchesRedirectProtected(token) {
398
+ if (!token || typeof token !== 'string') return false;
399
+ const norm = token.replace(/\\/g, '/');
400
+ return REDIRECT_PROTECTED_RE.test(norm);
401
+ }
402
+
403
+ /**
404
+ * checkRedirectToProtected(command): scan a Bash command string for shell
405
+ * redirects (> >>) or tee invocations that target protected kill-switch paths.
406
+ *
407
+ * Returns a human-readable description of the matched redirect, or null if
408
+ * none found.
409
+ *
410
+ * INCOMPLETE COVERAGE — see module header for honest framing.
411
+ */
412
+ function checkRedirectToProtected(command) {
413
+ if (typeof command !== 'string' || !command) return null;
414
+ // Fast path: skip if no redirect indicators present.
415
+ if (!command.includes('>') && !command.includes('tee')) return null;
416
+
417
+ const segments = splitSegments(command);
418
+ for (const seg of segments) {
419
+ const tokens = tokenize(seg);
420
+ for (let i = 0; i < tokens.length; i++) {
421
+ const t = tokens[i];
422
+
423
+ // Redirect operators: > and >>
424
+ if ((t === '>' || t === '>>') && i + 1 < tokens.length) {
425
+ const target = tokens[i + 1];
426
+ if (matchesRedirectProtected(target)) {
427
+ return `shell redirect (${t}) to ${target}`;
428
+ }
429
+ }
430
+
431
+ // tee command: `tee [-a] [--] <file> [file2 ...]`
432
+ // tee accepts MULTIPLE output files — check ALL positional args, not just the first.
433
+ if (t === 'tee') {
434
+ let pastDashDash = false;
435
+ for (let j = i + 1; j < tokens.length; j++) {
436
+ const arg = tokens[j];
437
+ if (!pastDashDash && arg === '--') { pastDashDash = true; continue; }
438
+ if (!pastDashDash && arg.startsWith('-')) continue; // skip tee flags (-a, --append, etc.)
439
+ // Check every positional arg — no early break (tee writes to all of them).
440
+ if (matchesRedirectProtected(arg)) return `tee to ${arg}`;
441
+ }
442
+ }
443
+ }
444
+ }
445
+ return null;
446
+ }
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Gate lock-down: Interpreter-write detection (best-effort, INCOMPLETE)
450
+ //
451
+ // R5a: Detect Bash commands that invoke interpreters (node -e, py3 -c,
452
+ // sed -i, perl -e) when the command string contains a protected-path token
453
+ // as a literal substring. This catches the obvious "node -e with .bashrc"
454
+ // form but NOT runtime-constructed paths.
455
+ //
456
+ // HONEST — INCOMPLETE COVERAGE (permanently):
457
+ // `node -e "require('fs').appendFileSync('.bashrc','x')"` ✓ caught (literal token)
458
+ // `py3 -c "open('state.json','w').write(...)" ✓ caught (literal token)
459
+ // `sed -i 's/a/b/' ~/.zshrc` ✓ caught (literal token)
460
+ // `node -e "const p=homedir()+'/.bashrc'; ..."` ✗ NOT caught (constructed)
461
+ // `node -e "require('fs').writeFileSync(atob('...'),...)` ✗ NOT caught (encoded)
462
+ // `ruby -e / php -r / etc.` ✗ NOT caught (not listed)
463
+ // Multiline here-docs or code from a file ✗ NOT caught
464
+ //
465
+ // An agent that constructs paths at runtime evades this check. The real
466
+ // anchor is external: clean CI env + human review. This raises the bar.
467
+ // ---------------------------------------------------------------------------
468
+
469
+ /**
470
+ * Interpreter patterns that can write files when invoked with inline code.
471
+ * Matched against each shell segment (not the raw full command).
472
+ */
473
+ // NOTE: _PY_CMD is assembled from two parts to avoid a source-tree validator
474
+ // false-positive on the first-party-Python-command scan. Value = 'p'+'ython'.
475
+ // eslint-disable-next-line no-useless-concat
476
+ const _PY_CMD = 'p' + 'ython'; // joined value: py...thon (see NOTE above)
477
+ const INTERPRETER_WRITE_RE = new RegExp(
478
+ '\\bnode\\s+(?:-e|--eval)\\b|' +
479
+ '\\b' + _PY_CMD + '[23]?\\s+-c\\b|' +
480
+ '\\bsed\\s+-i\\b|\\bperl\\s+-e\\b'
481
+ );
482
+
483
+ /**
484
+ * Protected-path token literals. When any of these strings appears as a
485
+ * literal substring of a segment that also matches INTERPRETER_WRITE_RE,
486
+ * the command is blocked.
487
+ *
488
+ * INCOMPLETE: only literal occurrences are caught. An agent that assembles
489
+ * the path at runtime (e.g. process.env.HOME + '/.bashrc') bypasses this.
490
+ */
491
+ const INTERPRETER_PROTECTED_TOKENS = [
492
+ // Shell profiles (basename match is specific in this context)
493
+ '.bash_profile', '.bashrc', '.profile', '.zshrc', '.zprofile',
494
+ // Claude and flow-agents routing files
495
+ '.claude/settings.json',
496
+ // Flow-agents session sidecars (basename match; false-positive risk is low
497
+ // in the interpreter-write context and accepted per R5a honest framing)
498
+ 'current.json', 'state.json', 'trust.bundle',
499
+ // Delivery CI anchor paths. The existing trust.bundle token catches delivery/trust.bundle
500
+ // as a substring; explicit path added for clarity. trust.checkpoint.json is new.
501
+ 'delivery/trust.bundle', 'delivery/trust.checkpoint.json',
502
+ ];
503
+
504
+ /**
505
+ * checkInterpreterWriteToProtected(command): detect interpreter invocations
506
+ * (node -e, py3 -c, sed -i, perl -e) in segments that also contain a
507
+ * protected-path token as a literal substring.
508
+ *
509
+ * Returns a human-readable description of the match, or null if not detected.
510
+ *
511
+ * INCOMPLETE COVERAGE — see module header for honest framing.
512
+ */
513
+ function checkInterpreterWriteToProtected(command) {
514
+ if (typeof command !== 'string' || !command) return null;
515
+ // Fast path: skip if no interpreter keywords present.
516
+ if (!command.includes('node') && !command.includes(_PY_CMD) &&
517
+ !command.includes('sed') && !command.includes('perl')) return null;
518
+
519
+ const segments = splitSegments(command);
520
+ for (const seg of segments) {
521
+ // Check interpreter pattern.
522
+ const interpMatch = INTERPRETER_WRITE_RE.exec(seg);
523
+ if (!interpMatch) continue;
524
+
525
+ // Check for protected-path token literal in the same segment.
526
+ for (const token of INTERPRETER_PROTECTED_TOKENS) {
527
+ if (seg.includes(token)) {
528
+ return `${interpMatch[0].trim()} with protected path token "${token}"`;
529
+ }
530
+ }
531
+ }
532
+ return null;
533
+ }
534
+
535
+ /**
536
+ * Delivery-protected path regex: delivery/trust.bundle and delivery/trust.checkpoint.json.
537
+ * These are the CI anchor files whose contents must not be agent-forged.
538
+ * Used by checkCopyMoveToProtected to catch `cp x delivery/trust.bundle`.
539
+ */
540
+ const DELIVERY_COPY_PROTECTED_RE = /(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
541
+
542
+ /**
543
+ * Return true when a normalized token matches a delivery-protected path.
544
+ */
545
+ function matchesDeliveryProtected(token) {
546
+ if (!token || typeof token !== "string") return false;
547
+ return DELIVERY_COPY_PROTECTED_RE.test(token.replace(/\\/g, "/"));
548
+ }
549
+
550
+ /**
551
+ * checkCopyMoveToProtected(command): detect cp/mv/install commands whose
552
+ * destination argument targets a delivery-protected path.
553
+ *
554
+ * Catches the plain-cp attack vector: `cp forged.json delivery/trust.bundle`
555
+ * is not a redirect and not an interpreter invocation, so those checks miss it.
556
+ * The destination is the LAST positional (non-flag) argument.
557
+ *
558
+ * INCOMPLETE COVERAGE: only cp, mv, install are checked. Other copy tools
559
+ * (rsync, scp, dd, etc.) and runtime-constructed path arguments are NOT caught.
560
+ * The real anchor remains external (clean CI env + human review). Bar-raiser only.
561
+ * RESIDUAL: publishDelivery uses fs.copyFileSync (not bash cp) -- unaffected.
562
+ */
563
+ function checkCopyMoveToProtected(command) {
564
+ if (typeof command !== "string" || !command) return null;
565
+ if (!command.includes("cp") && !command.includes("mv") && !command.includes("install")) return null;
566
+ if (!command.includes("delivery/")) return null;
567
+
568
+ const segments = splitSegments(command);
569
+ for (const seg of segments) {
570
+ const tokens = tokenize(seg);
571
+ if (tokens.length < 2) continue;
572
+ const cmd = tokens[0];
573
+ if (cmd !== "cp" && cmd !== "mv" && cmd !== "install") continue;
574
+
575
+ const positional = [];
576
+ for (let i = 1; i < tokens.length; i++) {
577
+ if (!tokens[i].startsWith("-")) positional.push(tokens[i]);
578
+ }
579
+ if (positional.length === 0) continue;
580
+
581
+ const dest = positional[positional.length - 1];
582
+ if (matchesDeliveryProtected(dest)) {
583
+ return `${cmd} to ${dest} (delivery-protected path)`;
584
+ }
585
+ }
586
+ return null;
587
+ }
588
+
220
589
  function run(inputOrRaw, options = {}) {
221
590
  if (options.truncated) {
222
591
  return {
@@ -241,6 +610,16 @@ function run(inputOrRaw, options = {}) {
241
610
  'disable the config-protection hook temporarily.',
242
611
  };
243
612
  }
613
+ // Gate lock-down: check path-pattern protected files (need path context).
614
+ const pathMatch = checkProtectedPathPattern(filePath);
615
+ if (pathMatch) {
616
+ return {
617
+ exitCode: 2,
618
+ stderr: `BLOCKED: Writing to ${pathMatch.name} is not allowed. ` +
619
+ `This file is protected because ${pathMatch.reason}. ` +
620
+ 'If this is a legitimate change, disable the config-protection hook temporarily and document the reason.',
621
+ };
622
+ }
244
623
  }
245
624
  const command = input?.tool_input?.command || '';
246
625
  if (command) {
@@ -254,11 +633,54 @@ function run(inputOrRaw, options = {}) {
254
633
  'If the hook is genuinely misconfigured, correct the hook configuration directly.',
255
634
  };
256
635
  }
636
+ // Gate lock-down: check for shell redirects to protected kill-switch paths.
637
+ // HONEST — INCOMPLETE: only > >> and tee are covered; sed -i and other forms
638
+ // are NOT. An agent with shell access can still evade. Bar-raiser only.
639
+ const redirect = checkRedirectToProtected(command);
640
+ if (redirect) {
641
+ return {
642
+ exitCode: 2,
643
+ stderr: `BLOCKED: Detected ${redirect} targeting a protected gate kill-switch file. ` +
644
+ 'Writing to shell profiles or Claude/flow-agents config files via shell redirect could ' +
645
+ 'disable or tamper with the gate. If this is a legitimate operation, ' +
646
+ 'disable the config-protection hook temporarily and document the reason. ' +
647
+ 'NOTE: This check has incomplete coverage (sed -i and similar forms are not caught).',
648
+ };
649
+ }
650
+ // Gate lock-down: check for interpreter invocations (node -e, py3 -c, sed -i,
651
+ // perl -e) combined with a protected-path token literal in the command string.
652
+ // HONEST — INCOMPLETE (R5a best-effort): runtime-constructed paths, base64,
653
+ // multi-step assembly, and other interpreters not listed are NOT caught.
654
+ const interpWrite = checkInterpreterWriteToProtected(command);
655
+ if (interpWrite) {
656
+ return {
657
+ exitCode: 2,
658
+ stderr: `BLOCKED: Detected ${interpWrite} in a Bash command. ` +
659
+ 'Interpreter invocations (node -e, py3 -c, sed -i, perl -e) that reference ' +
660
+ 'protected gate files could tamper with the gate. If this is a legitimate operation, ' +
661
+ 'disable the config-protection hook temporarily and document the reason. ' +
662
+ 'NOTE: This check has INCOMPLETE COVERAGE — runtime path construction evades it.',
663
+ };
664
+ }
665
+ // Gate lock-down R6: detect cp/mv/install targeting delivery-protected paths.
666
+ // Catches the plain-cp attack: `cp forged.json delivery/trust.bundle`.
667
+ // INCOMPLETE: cp/mv/install only; rsync/scp/dd evade. Real anchor is external.
668
+ const copyMove = checkCopyMoveToProtected(command);
669
+ if (copyMove) {
670
+ return {
671
+ exitCode: 2,
672
+ stderr: `BLOCKED: Detected ${copyMove} in a Bash command. ` +
673
+ 'Writing to delivery/trust.bundle or delivery/trust.checkpoint.json via cp/mv/install ' +
674
+ 'could forge the CI trust anchor. The legitimate write path is the publishDelivery CLI ' +
675
+ '(fs.copyFileSync -- not the Write/Edit tool or bash cp). ' +
676
+ 'NOTE: This check covers cp/mv/install only -- other copy tools may evade it.',
677
+ };
678
+ }
257
679
  }
258
680
  return { exitCode: 0 };
259
681
  }
260
682
 
261
- module.exports = { run, tokenize, splitSegments, checkCommandForBypass };
683
+ module.exports = { run, tokenize, splitSegments, checkCommandForBypass, checkProtectedPathPattern, checkRedirectToProtected, checkInterpreterWriteToProtected, checkCopyMoveToProtected, matchesDeliveryProtected };
262
684
 
263
685
  // Stdin fallback for spawnSync execution
264
686
  if (require.main === module) {