@kontourai/flow-agents 1.4.0 → 2.0.1
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/.github/CODEOWNERS +29 -0
- package/.github/actions/trust-verify/action.yml +145 -0
- package/.github/workflows/ci.yml +11 -4
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +10 -2
- package/.github/workflows/release-please.yml +1 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/.github/workflows/trust-reconcile.yml +113 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +103 -0
- package/CONTRIBUTING.md +4 -4
- package/README.md +1 -0
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/validate-workflow-artifacts.js +19 -2
- package/build/src/cli/verify.d.ts +1 -0
- package/build/src/cli/verify.js +90 -0
- package/build/src/cli/workflow-sidecar.d.ts +316 -8
- package/build/src/cli/workflow-sidecar.js +1996 -91
- package/build/src/cli.js +2 -3
- package/build/src/lib/flow-resolver.d.ts +111 -0
- package/build/src/lib/flow-resolver.js +308 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-source-tree.d.ts +1 -1
- package/build/src/tools/validate-source-tree.js +42 -162
- package/context/contracts/artifact-contract.md +10 -0
- package/context/contracts/delivery-contract.md +1 -0
- package/context/contracts/review-contract.md +1 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/gate-awareness.md +39 -0
- package/context/scripts/hooks/stop-goal-fit.js +632 -70
- package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
- package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0007-skill-audit.md +1 -1
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
- package/docs/adr/0011-mcp-posture.md +100 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
- package/docs/adr/0013-context-lifecycle.md +151 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
- package/docs/adr/0016-three-hard-boundary-model.md +71 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
- package/docs/agent-system-guidebook.md +5 -12
- package/docs/context-map.md +4 -10
- package/docs/index.md +3 -2
- package/docs/integrations/framework-adapter.md +19 -6
- package/docs/integrations/index.md +2 -2
- package/docs/north-star.md +4 -4
- package/docs/operating-layers.md +3 -3
- package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
- package/docs/repository-structure.md +2 -2
- package/docs/skills-map.md +1 -0
- package/docs/spec/runtime-hook-surface.md +62 -9
- package/docs/standards-register.md +3 -3
- package/docs/survey-utterance-check.md +1 -1
- package/docs/trust-anchor-adoption.md +197 -0
- package/docs/verifiable-trust.md +95 -0
- package/docs/veritas-integration.md +2 -2
- package/docs/workflow-usage-guide.md +69 -0
- package/evals/acceptance/DEMO-false-completion.md +144 -0
- package/evals/acceptance/demo-cast.sh +92 -0
- package/evals/acceptance/demo-false-completion.sh +72 -0
- package/evals/acceptance/demo-real-evidence.sh +104 -0
- package/evals/acceptance/demo.tape +29 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
- package/evals/acceptance/prove-capture-teeth.sh +114 -0
- package/evals/acceptance/prove-teeth.sh +105 -0
- package/evals/ci/antigaming-suite.sh +55 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
- package/evals/integration/test_builder_step_producers.sh +379 -0
- package/evals/integration/test_bundle_install.sh +35 -71
- package/evals/integration/test_bundle_lifecycle.sh +39 -2
- package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
- package/evals/integration/test_checkpoint_signing.sh +489 -0
- package/evals/integration/test_claim_lookup.sh +352 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_command_log_integrity.sh +275 -0
- package/evals/integration/test_context_map.sh +0 -2
- package/evals/integration/test_dual_emit_flow_step.sh +278 -0
- package/evals/integration/test_enforcer_expects_driven.sh +281 -0
- package/evals/integration/test_evidence_capture_hook.sh +185 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +273 -0
- package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
- package/evals/integration/test_gate_bypass_chain.sh +448 -0
- package/evals/integration/test_gate_lockdown.sh +1137 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
- package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
- package/evals/integration/test_goal_fit_hook.sh +69 -4
- package/evals/integration/test_goal_fit_rederive.sh +263 -0
- package/evals/integration/test_install_merge.sh +1176 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_mint_attestation.sh +373 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
- package/evals/integration/test_publish_delivery.sh +269 -0
- package/evals/integration/test_reconcile_soundness.sh +528 -0
- package/evals/integration/test_resolvefirststep_security.sh +208 -0
- package/evals/integration/test_session_resume_roundtrip.sh +286 -0
- package/evals/integration/test_trust_checkpoint.sh +325 -0
- package/evals/integration/test_trust_reconcile.sh +293 -0
- package/evals/integration/test_verify_cli.sh +208 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
- package/evals/lib/node.sh +0 -6
- package/evals/run.sh +47 -0
- package/evals/static/test_workflow_skills.sh +6 -13
- package/install.sh +0 -7
- package/integrations/strands-ts/README.md +25 -15
- package/integrations/veritas/flow-agents.adapter.json +1 -2
- package/kits/builder/flows/build.flow.json +59 -12
- package/kits/builder/kit.json +85 -15
- package/kits/builder/skills/continue-work/SKILL.md +116 -0
- package/kits/builder/skills/deliver/SKILL.md +36 -6
- package/kits/builder/skills/design-probe/SKILL.md +28 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -1
- package/kits/builder/skills/gate-review/SKILL.md +234 -0
- package/kits/builder/skills/learning-review/SKILL.md +30 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
- package/kits/builder/skills/plan-work/SKILL.md +13 -1
- package/kits/builder/skills/pull-work/SKILL.md +19 -0
- package/kits/knowledge/adapters/default-store/index.js +38 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
- package/kits/knowledge/docs/store-contract.md +314 -0
- package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
- package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
- package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
- package/kits/knowledge/evals/entities/suite.test.js +40 -0
- package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
- package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
- package/kits/knowledge/evals/retirement/suite.test.js +145 -0
- package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
- package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
- package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
- package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
- package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
- package/kits/knowledge/kit.json +51 -1
- package/package.json +6 -6
- package/packaging/conformance/README.md +10 -2
- package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
- package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
- package/packaging/conformance/run-conformance.js +1 -1
- package/scripts/README.md +2 -1
- package/scripts/build-universal-bundles.js +0 -1
- package/scripts/ci/mint-attestation.js +221 -0
- package/scripts/ci/trust-reconcile.js +545 -0
- package/scripts/hooks/config-protection.js +423 -1
- package/scripts/hooks/evidence-capture.js +348 -0
- package/scripts/hooks/lib/liveness-read.js +113 -0
- package/scripts/hooks/run-hook.js +6 -1
- package/scripts/hooks/stop-goal-fit.js +1524 -79
- package/scripts/hooks/workflow-steering.js +135 -5
- package/scripts/install-codex-home.sh +39 -0
- package/scripts/install-merge.js +330 -0
- package/scripts/repair-command-log.js +115 -0
- package/src/cli/init.ts +218 -20
- package/src/cli/validate-workflow-artifacts.ts +18 -2
- package/src/cli/verify.ts +100 -0
- package/src/cli/workflow-sidecar.ts +2127 -84
- package/src/cli.ts +2 -3
- package/src/lib/flow-resolver.ts +369 -0
- package/src/tools/build-universal-bundles.ts +34 -21
- package/src/tools/generate-context-map.ts +3 -17
- package/src/tools/validate-source-tree.ts +44 -104
- package/build/src/tools/filter-installed-packs.d.ts +0 -2
- package/build/src/tools/filter-installed-packs.js +0 -135
- package/packaging/packs.json +0 -49
- package/scripts/filter-installed-packs.js +0 -2
- 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) {
|