@intutic/sync-daemon 1.10.0 → 1.10.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.
Files changed (200) hide show
  1. package/README.md +2 -2
  2. package/dist/agentReporter.d.ts +205 -0
  3. package/dist/agentReporter.d.ts.map +1 -0
  4. package/dist/agentReporter.js +394 -0
  5. package/dist/agentReporter.js.map +1 -0
  6. package/dist/configReader.d.ts +39 -2
  7. package/dist/configReader.d.ts.map +1 -1
  8. package/dist/configReader.js +56 -2
  9. package/dist/configReader.js.map +1 -1
  10. package/dist/configWriter.d.ts +51 -1
  11. package/dist/configWriter.d.ts.map +1 -1
  12. package/dist/configWriter.js +490 -63
  13. package/dist/configWriter.js.map +1 -1
  14. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  15. package/dist/harness/antigravityHooks.js +33 -55
  16. package/dist/harness/antigravityHooks.js.map +1 -1
  17. package/dist/harness/claudeCodeHooks.d.ts +47 -13
  18. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  19. package/dist/harness/claudeCodeHooks.js +316 -54
  20. package/dist/harness/claudeCodeHooks.js.map +1 -1
  21. package/dist/harness/claudeDesktopHooks.d.ts.map +1 -1
  22. package/dist/harness/claudeDesktopHooks.js +35 -48
  23. package/dist/harness/claudeDesktopHooks.js.map +1 -1
  24. package/dist/harness/clineHooks.d.ts.map +1 -1
  25. package/dist/harness/clineHooks.js +42 -34
  26. package/dist/harness/clineHooks.js.map +1 -1
  27. package/dist/harness/codexHooks.d.ts +36 -0
  28. package/dist/harness/codexHooks.d.ts.map +1 -0
  29. package/dist/harness/codexHooks.js +227 -0
  30. package/dist/harness/codexHooks.js.map +1 -0
  31. package/dist/harness/continueHooks.d.ts +45 -5
  32. package/dist/harness/continueHooks.d.ts.map +1 -1
  33. package/dist/harness/continueHooks.js +225 -5
  34. package/dist/harness/continueHooks.js.map +1 -1
  35. package/dist/harness/cursorHooks.d.ts +31 -0
  36. package/dist/harness/cursorHooks.d.ts.map +1 -1
  37. package/dist/harness/cursorHooks.js +78 -49
  38. package/dist/harness/cursorHooks.js.map +1 -1
  39. package/dist/harness/dshHooks.d.ts +198 -0
  40. package/dist/harness/dshHooks.d.ts.map +1 -0
  41. package/dist/harness/dshHooks.js +569 -0
  42. package/dist/harness/dshHooks.js.map +1 -0
  43. package/dist/harness/gateBody.d.ts +422 -0
  44. package/dist/harness/gateBody.d.ts.map +1 -0
  45. package/dist/harness/gateBody.js +1816 -0
  46. package/dist/harness/gateBody.js.map +1 -0
  47. package/dist/harness/gateKind.d.ts +164 -0
  48. package/dist/harness/gateKind.d.ts.map +1 -0
  49. package/dist/harness/gateKind.js +143 -0
  50. package/dist/harness/gateKind.js.map +1 -0
  51. package/dist/harness/githubCopilotHooks.d.ts +54 -0
  52. package/dist/harness/githubCopilotHooks.d.ts.map +1 -0
  53. package/dist/harness/githubCopilotHooks.js +218 -0
  54. package/dist/harness/githubCopilotHooks.js.map +1 -0
  55. package/dist/harness/gooseHooks.d.ts.map +1 -1
  56. package/dist/harness/gooseHooks.js +67 -37
  57. package/dist/harness/gooseHooks.js.map +1 -1
  58. package/dist/harness/grokHooks.d.ts +77 -0
  59. package/dist/harness/grokHooks.d.ts.map +1 -0
  60. package/dist/harness/grokHooks.js +350 -0
  61. package/dist/harness/grokHooks.js.map +1 -0
  62. package/dist/harness/hermesHooks.d.ts.map +1 -1
  63. package/dist/harness/hermesHooks.js +26 -55
  64. package/dist/harness/hermesHooks.js.map +1 -1
  65. package/dist/harness/holdRedaction.d.ts +69 -0
  66. package/dist/harness/holdRedaction.d.ts.map +1 -0
  67. package/dist/harness/holdRedaction.js +211 -0
  68. package/dist/harness/holdRedaction.js.map +1 -0
  69. package/dist/harness/jetbrainsXmlConfig.d.ts +51 -0
  70. package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -0
  71. package/dist/harness/jetbrainsXmlConfig.js +151 -0
  72. package/dist/harness/jetbrainsXmlConfig.js.map +1 -0
  73. package/dist/harness/mcpAutoWrite.d.ts +100 -3
  74. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  75. package/dist/harness/mcpAutoWrite.js +712 -90
  76. package/dist/harness/mcpAutoWrite.js.map +1 -1
  77. package/dist/harness/museHooks.d.ts +53 -0
  78. package/dist/harness/museHooks.d.ts.map +1 -0
  79. package/dist/harness/museHooks.js +273 -0
  80. package/dist/harness/museHooks.js.map +1 -0
  81. package/dist/harness/n8nHooks.d.ts +25 -9
  82. package/dist/harness/n8nHooks.d.ts.map +1 -1
  83. package/dist/harness/n8nHooks.js +174 -9
  84. package/dist/harness/n8nHooks.js.map +1 -1
  85. package/dist/harness/openCodeHooks.d.ts +93 -0
  86. package/dist/harness/openCodeHooks.d.ts.map +1 -0
  87. package/dist/harness/openCodeHooks.js +248 -0
  88. package/dist/harness/openCodeHooks.js.map +1 -0
  89. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  90. package/dist/harness/openWebuiHooks.js +56 -11
  91. package/dist/harness/openWebuiHooks.js.map +1 -1
  92. package/dist/harness/openclawHooks.d.ts.map +1 -1
  93. package/dist/harness/openclawHooks.js +30 -54
  94. package/dist/harness/openclawHooks.js.map +1 -1
  95. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  96. package/dist/harness/openhandsHooks.js +35 -41
  97. package/dist/harness/openhandsHooks.js.map +1 -1
  98. package/dist/harness/piHooks.d.ts.map +1 -1
  99. package/dist/harness/piHooks.js +31 -58
  100. package/dist/harness/piHooks.js.map +1 -1
  101. package/dist/harness/protectedPaths.d.ts +353 -0
  102. package/dist/harness/protectedPaths.d.ts.map +1 -0
  103. package/dist/harness/protectedPaths.js +805 -0
  104. package/dist/harness/protectedPaths.js.map +1 -0
  105. package/dist/harness/rooCodeHooks.d.ts.map +1 -1
  106. package/dist/harness/rooCodeHooks.js +34 -48
  107. package/dist/harness/rooCodeHooks.js.map +1 -1
  108. package/dist/harness/windsurfHooks.d.ts +72 -0
  109. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  110. package/dist/harness/windsurfHooks.js +172 -58
  111. package/dist/harness/windsurfHooks.js.map +1 -1
  112. package/dist/harness/windsurfJetBrainsProxy.d.ts +78 -0
  113. package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -0
  114. package/dist/harness/windsurfJetBrainsProxy.js +159 -0
  115. package/dist/harness/windsurfJetBrainsProxy.js.map +1 -0
  116. package/dist/hashReporter.js +1 -1
  117. package/dist/hashReporter.js.map +1 -1
  118. package/dist/index.d.ts +12 -6
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +14 -6
  121. package/dist/index.js.map +1 -1
  122. package/dist/integrityStore.d.ts +0 -8
  123. package/dist/integrityStore.d.ts.map +1 -1
  124. package/dist/integrityStore.js +6 -29
  125. package/dist/integrityStore.js.map +1 -1
  126. package/dist/lib/approvedBypasses.d.ts +76 -0
  127. package/dist/lib/approvedBypasses.d.ts.map +1 -0
  128. package/dist/lib/approvedBypasses.js +140 -0
  129. package/dist/lib/approvedBypasses.js.map +1 -0
  130. package/dist/lib/complianceProbes.d.ts.map +1 -1
  131. package/dist/lib/complianceProbes.js +5 -0
  132. package/dist/lib/complianceProbes.js.map +1 -1
  133. package/dist/lib/decisionsDigest.d.ts +99 -0
  134. package/dist/lib/decisionsDigest.d.ts.map +1 -0
  135. package/dist/lib/decisionsDigest.js +199 -0
  136. package/dist/lib/decisionsDigest.js.map +1 -0
  137. package/dist/lib/egressPolicy.d.ts +69 -0
  138. package/dist/lib/egressPolicy.d.ts.map +1 -0
  139. package/dist/lib/egressPolicy.js +126 -0
  140. package/dist/lib/egressPolicy.js.map +1 -0
  141. package/dist/lib/gitWorktrees.d.ts +140 -0
  142. package/dist/lib/gitWorktrees.d.ts.map +1 -0
  143. package/dist/lib/gitWorktrees.js +191 -0
  144. package/dist/lib/gitWorktrees.js.map +1 -0
  145. package/dist/lib/policySnapshot.d.ts +224 -0
  146. package/dist/lib/policySnapshot.d.ts.map +1 -0
  147. package/dist/lib/policySnapshot.js +637 -0
  148. package/dist/lib/policySnapshot.js.map +1 -0
  149. package/dist/lib/processPoller.d.ts +44 -0
  150. package/dist/lib/processPoller.d.ts.map +1 -1
  151. package/dist/lib/processPoller.js +176 -1
  152. package/dist/lib/processPoller.js.map +1 -1
  153. package/dist/lib/runtimeEnv.d.ts +8 -0
  154. package/dist/lib/runtimeEnv.d.ts.map +1 -1
  155. package/dist/lib/runtimeEnv.js +3 -0
  156. package/dist/lib/runtimeEnv.js.map +1 -1
  157. package/dist/localTrajectoryHeuristics.d.ts +51 -0
  158. package/dist/localTrajectoryHeuristics.d.ts.map +1 -0
  159. package/dist/localTrajectoryHeuristics.js +90 -0
  160. package/dist/localTrajectoryHeuristics.js.map +1 -0
  161. package/dist/sessionReporter.d.ts +70 -0
  162. package/dist/sessionReporter.d.ts.map +1 -0
  163. package/dist/sessionReporter.js +140 -0
  164. package/dist/sessionReporter.js.map +1 -0
  165. package/dist/skillWriter.d.ts +1 -1
  166. package/dist/skillWriter.d.ts.map +1 -1
  167. package/dist/skillWriter.js +5 -2
  168. package/dist/skillWriter.js.map +1 -1
  169. package/dist/syncLoop.d.ts +105 -3
  170. package/dist/syncLoop.d.ts.map +1 -1
  171. package/dist/syncLoop.js +397 -65
  172. package/dist/syncLoop.js.map +1 -1
  173. package/dist/trajectoryMonitor.d.ts +2 -0
  174. package/dist/trajectoryMonitor.d.ts.map +1 -1
  175. package/dist/trajectoryMonitor.js +57 -0
  176. package/dist/trajectoryMonitor.js.map +1 -1
  177. package/dist/watcher/driftWatcher.d.ts.map +1 -1
  178. package/dist/watcher/driftWatcher.js +12 -3
  179. package/dist/watcher/driftWatcher.js.map +1 -1
  180. package/dist/watcher/settingsGuard.d.ts +23 -0
  181. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  182. package/dist/watcher/settingsGuard.js +202 -17
  183. package/dist/watcher/settingsGuard.js.map +1 -1
  184. package/package.json +37 -6
  185. package/dist/brainIndexer.d.ts +0 -51
  186. package/dist/brainIndexer.d.ts.map +0 -1
  187. package/dist/brainIndexer.js +0 -148
  188. package/dist/brainIndexer.js.map +0 -1
  189. package/dist/harness/cursorHooksJson.d.ts +0 -29
  190. package/dist/harness/cursorHooksJson.d.ts.map +0 -1
  191. package/dist/harness/cursorHooksJson.js +0 -130
  192. package/dist/harness/cursorHooksJson.js.map +0 -1
  193. package/dist/harness/gooseHooksWriter.d.ts +0 -27
  194. package/dist/harness/gooseHooksWriter.d.ts.map +0 -1
  195. package/dist/harness/gooseHooksWriter.js +0 -147
  196. package/dist/harness/gooseHooksWriter.js.map +0 -1
  197. package/dist/harness/openhandsHooksWriter.d.ts +0 -23
  198. package/dist/harness/openhandsHooksWriter.d.ts.map +0 -1
  199. package/dist/harness/openhandsHooksWriter.js +0 -146
  200. package/dist/harness/openhandsHooksWriter.js.map +0 -1
@@ -0,0 +1,422 @@
1
+ /**
2
+ * The gate evaluator, emitted once per language instead of written thirteen
3
+ * times.
4
+ *
5
+ * # Why this file exists
6
+ *
7
+ * Every harness writer used to hand-author its own copy of the decision logic
8
+ * inside a TypeScript template literal. Four production defects came directly
9
+ * out of that arrangement, and each one silently removed enforcement while every
10
+ * test stayed green:
11
+ *
12
+ * - `claudeCodeHooks` emitted `...UNIVERSAL_PROTECTED_PATHS,` as *literal source
13
+ * text* into a script that never imported it. Node threw `ReferenceError`
14
+ * before the gate decided anything — and a Claude Code PreToolUse hook exiting
15
+ * 1 is a hook *error*, not a block, so the call proceeded.
16
+ * - `INTUITIC_` for `INTUTIC_`: the gate died on an unbound variable under
17
+ * `set -u`, exiting 1 rather than 2 — again, not a block.
18
+ * - Five bash writers wrote `\"` where `\\"` was needed. Bash ate the quotes,
19
+ * the audit line stopped being parseable JSON, and `drainHookEvents` drops
20
+ * malformed lines and then *wipes the log* on a successful drain. The record
21
+ * of a block was destroyed rather than delayed.
22
+ * - `gooseHooks`/`openhandsHooks` never extracted a command at all, so neither
23
+ * had a shell-bypass guard — for their entire life.
24
+ *
25
+ * The common cause is not carelessness. It is that shell and JavaScript embedded
26
+ * in a template literal have three levels of escaping and no compiler checking
27
+ * any of them. Emitting from one place does not make the escaping easier; it
28
+ * makes there be one of it, with one set of tests.
29
+ *
30
+ * # The contract with a writer
31
+ *
32
+ * # One editing hazard, since it bit four times
33
+ *
34
+ * A backtick inside a comment in an emitted body ends the TypeScript template
35
+ * literal it lives in, and the rest of the gate becomes TypeScript. `tsc` does
36
+ * catch it — but as a syntax error dozens of lines away, which reads as
37
+ * unrelated damage. Use plain quotes in comments inside the emitted strings.
38
+ *
39
+ * A lint for this was tried and removed: a line-based scan cannot cheaply tell
40
+ * a comment inside a template literal from one outside it, and it produced 26
41
+ * false positives on this file's own prose. The compiler is the right detector;
42
+ * this note is the right documentation.
43
+ *
44
+ * A writer supplies the harness-specific parts — how to read the tool call, how
45
+ * to record an event, how to refuse — and interpolates the bodies below. See
46
+ * {@link ShellGateOptions} and {@link JsGateOptions} for exactly what must be in
47
+ * scope.
48
+ *
49
+ * @module
50
+ */
51
+ import { DESTRUCTIVE_COMMAND_PATTERNS, type GuardPattern } from './protectedPaths.js';
52
+ /**
53
+ * Bumped when the emitted evaluator changes shape.
54
+ *
55
+ * Stamped into the header of every emitted gate and into the policy snapshot, so
56
+ * the version a machine is actually running can be read off the artifact rather
57
+ * than inferred. The previous docstring claimed it "rides on every audit line",
58
+ * which was simply untrue — nothing emitted it anywhere, and a constant whose
59
+ * comment describes behaviour it does not have is worse than no constant.
60
+ *
61
+ * v4: the `.rules` projection grew an `argPatternB64` column and every emitted
62
+ * evaluator learned to condition a matched rule on the serialized tool input
63
+ * (` WHERE ` SOP rules). Nothing *triggers* on this number: the daemon
64
+ * regenerates every gate unconditionally each sync cycle, the snapshot writer
65
+ * stamps it into `policy-snapshot.json` for support conversations, and
66
+ * `gateLivenessService` records it off events when a gate reports one. A v3
67
+ * gate reading a v4 snapshot and a v4 gate reading a v3 snapshot both degrade
68
+ * to v3 behaviour — see the encoding note on {@link RULES_COLUMNS}.
69
+ *
70
+ * v5: the residual fail-open edges were closed. Every bash gate now converts
71
+ * an accidental crash (exit != 0 and != 2 under `set -euo pipefail`) into
72
+ * exit 2 via {@link SHELL_FAIL_CLOSED}; every bash gate refuses a stdin
73
+ * payload from which neither a tool name nor a tool_input could be extracted
74
+ * (the envelope posture githubCopilotHooks established, now uniform); every
75
+ * JS gate installs `uncaughtException`/`unhandledRejection` handlers as its
76
+ * first statements ({@link emitJsFailClosedPrelude}) so a crash outside the
77
+ * stdin handler refuses through the harness's own contract instead of exiting
78
+ * 1 (which every exit-code harness reads as "hook errored — allow"); and the
79
+ * malformed-envelope refusal is shared across the JS writers via
80
+ * `intuticGuardEnvelope`. The `.rules` format is unchanged — v4 and v5 gates
81
+ * read each other's snapshots byte-identically.
82
+ *
83
+ * v6 (M3): `#mcpservers` header parsing + `mcp__<server>__*` allowlist
84
+ * refusal + Cline `use_mcp_tool` envelope normalization; rule-line format
85
+ * itself is unchanged. This IS a `.rules` shape change, unlike v5 (which only
86
+ * closed fail-open edges) — a new header line means:
87
+ *
88
+ * - a v5 gate reading a v6 snapshot ignores the unknown `#mcpservers` header
89
+ * (every existing header-parsing loop already falls through unrecognised
90
+ * `#`-prefixed lines to the comment case) and degrades to v5 behaviour: no
91
+ * allowlist enforcement, but no crash and no false-block either;
92
+ * - a v6 gate reading a v5 snapshot (no `#mcpservers` header present) also
93
+ * behaves as unrestricted — the empty-list default — because "header
94
+ * absent" and "header present with an empty list" are deliberately the same
95
+ * observable state (`writePolicySnapshot` omits the line rather than
96
+ * emitting it empty, precisely so a v6 gate cannot tell "no snapshot
97
+ * configured a list yet" from "this workspace wants zero servers", and
98
+ * picks the fail-open reading of that ambiguity rather than blocking every
99
+ * MCP call the first time a v6 gate meets an old snapshot).
100
+ *
101
+ * v8 (TD-474 item 4): the `hold` severity and the `action` subject. A hold
102
+ * rule — a `require_approval` SOP rule the control plane resolves, or a local
103
+ * `review_before:` token the daemon compiles as `sop.local.review_before.*` —
104
+ * refuses the call through the harness's own contract, appends the v1 hold
105
+ * record `drainReviewRequests` already reads, prints the `intutic decision
106
+ * approve <holdId>` hint, and lets the call through instead when the
107
+ * daemon's approved-bypass cache holds an exact, unexpired match for it. One
108
+ * mechanism in the shared bodies; the Claude Code writer's bespoke copy is
109
+ * gone. The `.rules` line format is unchanged — `hold` is just a new value in
110
+ * the severity column — so a v7 JS or bash gate reading a v8 snapshot falls
111
+ * through its severity switch to BLOCK (fail closed, no hold record, no
112
+ * bypass). The v7 Python filter is the one exception: its mapping sent every
113
+ * unrecognised severity to `flags`, i.e. ALLOWED, which is why the v8 filter
114
+ * maps unknown to block and why the daemon regenerates every gate each cycle —
115
+ * the skew window is one sync interval.
116
+ */
117
+ export declare const GATE_VERSION = 8;
118
+ /**
119
+ * The coarse command → action-token classification the hold tier keys on:
120
+ * `review_before: action:deploy` holds a shell command that contains any of
121
+ * the deploy needles. Deliberately minimal — a gate that tries to be clever
122
+ * about shell commands is a gate that blocks real work — and it mirrors the
123
+ * proxy's `actions.rs` in both directions: `hookActionParity.test.ts` reads
124
+ * THIS file as text and fails if either side knows a needle the other does
125
+ * not. Keep each entry on one line in this exact `['action:x', [...]]` shape;
126
+ * that is what the test's regex finds.
127
+ */
128
+ export declare const ACTION_NEEDLES: ReadonlyArray<readonly [string, readonly string[]]>;
129
+ /** The (lower-cased) tool names whose `command` the classifier reads. */
130
+ export declare const ACTION_TOOL_NAMES: readonly ["bash", "shell", "run_command", "terminal", "execute"];
131
+ /** Where a gate appends a hold, relative to the workspace root; the daemon's
132
+ * `drainReviewRequests` reads the same file. */
133
+ export declare const REVIEW_REQUESTS_BASENAME = "review-requests.jsonl";
134
+ export declare const REVIEW_REQUESTS_LOG = ".intutic/events/review-requests.jsonl";
135
+ /** Bumped when a hold record's shape changes; the control plane drops any
136
+ * other version at ingest. */
137
+ export declare const REVIEW_REQUEST_VERSION = 1;
138
+ /**
139
+ * How old a snapshot may be before a gate reports it as stale.
140
+ *
141
+ * Alerting only — a stale snapshot is still enforced, deliberately. If rules
142
+ * expired into permissiveness, "stop the daemon and wait" would be a supported
143
+ * way to disarm governance. Seven days is far longer than the sync interval, so
144
+ * this fires on "the daemon has been dead for a week", not on "it is Sunday".
145
+ */
146
+ export declare const SNAPSHOT_STALE_AFTER_DAYS = 7;
147
+ /**
148
+ * How a gate refuses a call.
149
+ *
150
+ * The three values are not stylistic. Claude Code, Cursor and the bash
151
+ * harnesses read the **exit code** (2 = deny; 1 is an error and lets the call
152
+ * through). Cline and Roo Code ignore the exit code and read a `{"cancel":
153
+ * true}` object on stdout. Grok Build ALSO ignores the exit code, but its
154
+ * confirmed verdict shape is a *different* stdout object —
155
+ * `{"decision":"deny","reason":"..."}` — not `{"cancel":true}`. The two
156
+ * stdout contracts are kept as distinct union members rather than folded into
157
+ * one "stdout-cancel" bucket precisely because a gate that emits the wrong
158
+ * field name looks identical in a code review and enforces nothing: Grok
159
+ * Build does not recognise `cancel`, and Cline/Roo Code do not recognise
160
+ * `decision`. A gate that uses the wrong one is silently inert.
161
+ */
162
+ /**
163
+ * `'throw'` is for a gate that runs INSIDE the harness process (OpenCode's
164
+ * plugin hook): the refusal is a thrown Error carrying the BLOCKED message,
165
+ * which the host turns into the tool-error the model reads. No exit code, no
166
+ * stdout — the same in-process posture the n8n workflow gate takes.
167
+ */
168
+ export type BlockContract = 'exit2' | 'stdout-cancel' | 'stdout-decision-deny' | 'throw';
169
+ /**
170
+ * The `.rules` snapshot record layout, defined once so the writer and every
171
+ * reader cannot disagree about column order.
172
+ *
173
+ * `source` may contain any punctuation EXCEPT a tab — `assertPortableEre`
174
+ * rejects literal tabs precisely because this projection is tab-separated —
175
+ * so it is safe mid-record.
176
+ *
177
+ * `argPatternB64` is the ` WHERE ` clause of a SOP rule, **base64-encoded**
178
+ * (standard alphabet, UTF-8 source), and the encoding is not a stylistic
179
+ * choice. An argPattern is an arbitrary JS regex: unlike `source` it is not
180
+ * run through `assertPortableEre`, so it may legally contain tabs, and any
181
+ * escaping convention would be a second dialect every one of the four readers
182
+ * (bash, JS, the Open WebUI filter, and the LangGraph port) had to get
183
+ * byte-identically right. Base64's alphabet ([A-Za-z0-9+/=]) cannot collide
184
+ * with the separator, full stop.
185
+ *
186
+ * The column is OMITTED (not emitted empty) when a rule has no argPattern, so
187
+ * every rule that exists today serialises byte-identically to the v3 layout.
188
+ * Compatibility in both directions:
189
+ * - a v4 gate reading a v3 file sees six fields and treats every rule as
190
+ * name-only — exactly v3 behaviour;
191
+ * - a v3 gate reading a v4 file: the JS and Python readers index `f[5]` and
192
+ * ignore the extra field; the bash reader's final `read` variable absorbs
193
+ * `source<TAB>base64`, so that one rule matches nothing rather than
194
+ * crashing the gate. That last degradation (a WHERE rule inert under a
195
+ * stale bash gate for one sync cycle, until the daemon — which writes both
196
+ * the snapshot and the gates — regenerates them) is the cost of never
197
+ * shifting the six columns every deployed reader already indexes.
198
+ */
199
+ export declare const RULES_COLUMNS: readonly ["id", "severity", "flags", "subject", "reason", "source", "argPatternB64"];
200
+ /**
201
+ * Serialises one rule into a `.rules` line.
202
+ *
203
+ * The reason travels with the rule rather than being generated at block time.
204
+ * `hookEvents.resolveSeverity` classifies an incident by looking for the literal
205
+ * substrings "governance-protected" and "bypass pattern" in the reason text — so
206
+ * a gate that emits a generic message files a governance-config tamper as MEDIUM
207
+ * instead of CRITICAL, and whatever routes on CRITICAL never sees it. That
208
+ * coupling is asserted in `harnessProtectedPaths.test.ts`.
209
+ */
210
+ export declare function toRulesLine(p: GuardPattern): string;
211
+ /**
212
+ * The bash fail-closed prelude. Interpolated by every bash writer immediately
213
+ * after `set -euo pipefail`, before anything that can fail has run.
214
+ *
215
+ * The edge it closes: under `set -euo pipefail` any script bug — an unbound
216
+ * variable, a `read` that hits EOF outside its guard, a corrupted snapshot
217
+ * tripping an unguarded command — ends the script with the failing command's
218
+ * status, usually 1. Every harness that runs these gates treats a non-2
219
+ * non-zero exit as "the hook errored" and ALLOWS the call, so an accidental
220
+ * crash was an allow that looked like enforcement.
221
+ *
222
+ * An EXIT trap that inspects the final status, deliberately not `trap ... ERR`:
223
+ *
224
+ * - ERR does not fire on `set -u` unbound-variable deaths or on every `set -e`
225
+ * exit path, and its firing rules differ between bash versions and whatever
226
+ * /bin/sh a harness substitutes. EXIT always runs, in every POSIX shell.
227
+ * - An ERR trap also fires on every benign non-zero intermediate — a grep
228
+ * no-match inside a condition, an arithmetic result of 0 — and turning those
229
+ * into blocks would make the gate refuse every call, which is exactly as
230
+ * broken as allowing every call. Inspecting only the FINAL exit status
231
+ * sidesteps that class entirely: `set -e` semantics are untouched, and a
232
+ * status the script deliberately produced (0 = allow, 2 = block, including
233
+ * the python3-missing refusal in SHELL_EXTRACT) passes through unchanged.
234
+ * Under `set -e` a script bug can never reach the final `exit 0`, so
235
+ * "status not in {0, 2}" is exactly "the gate crashed".
236
+ *
237
+ * The trap body runs under the script's own `set -eu`, so it disarms both
238
+ * first — a trap that itself dies on an unset variable would exit with that
239
+ * failure's status and reopen the hole it exists to close. `log_event` is
240
+ * defined by the writer AFTER this prelude, so the trap probes for it before
241
+ * calling: a crash earlier than the definition still blocks, just without an
242
+ * audit line (stderr carries the reason either way).
243
+ */
244
+ export declare const SHELL_FAIL_CLOSED = "\n# \u2500\u2500 Intutic fail-closed prelude v8 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Any exit status other than the two deliberate verdicts (0 = allow, 2 = block)\n# means this gate crashed rather than decided. The harness would read exit 1 as\n# \"hook errored\" and run the tool anyway, so a crash is converted into a block.\n# EXIT rather than ERR: ERR is unreliable across shells for set -u deaths, and\n# it fires on benign non-zero intermediates; the final status cannot lie.\nintutic_fail_closed() {\n _intutic_exit_rc=$?\n # The trap must not itself die on an unset variable or a failed command \u2014\n # that would replace the exit status it exists to correct.\n set +eu\n trap - EXIT\n if [ \"$_intutic_exit_rc\" = \"0\" ] || [ \"$_intutic_exit_rc\" = \"2\" ]; then\n exit \"$_intutic_exit_rc\"\n fi\n echo \"[Intutic Governance] BLOCKED: gate crashed (exit ${_intutic_exit_rc}) \u2014 failing closed rather than allowing an unevaluated call.\" >&2\n if command -v log_event >/dev/null 2>&1; then\n log_event \"tool_blocked\" \"${TOOL:-unknown}\" \"gate crashed (exit ${_intutic_exit_rc}) \u2014 failing closed\" || true\n fi\n exit 2\n}\ntrap intutic_fail_closed EXIT\n";
245
+ export interface ShellGateOptions {
246
+ /** Harness id, as it appears in audit lines. */
247
+ harness: string;
248
+ /**
249
+ * Name of the shell function the writer defined to record an event.
250
+ * Called as `<fn> <verdict> <tool> <reason>`.
251
+ */
252
+ logFn?: string;
253
+ /**
254
+ * Absolute path of the hold record file. Defaults to
255
+ * `<script dir>/../events/review-requests.jsonl`, which is the workspace's
256
+ * `.intutic/events/` for every gate that lives in `.intutic/hooks/`; a
257
+ * writer whose artifact lives elsewhere passes it.
258
+ */
259
+ reviewRequestFile?: string;
260
+ }
261
+ export declare function emitShellGate(opts: ShellGateOptions): string;
262
+ export interface JsGateOptions {
263
+ harness: string;
264
+ /** How this harness refuses. */
265
+ contract: BlockContract;
266
+ /** See {@link ShellGateOptions.reviewRequestFile}. Writers whose artifact
267
+ * does not live in `.intutic/hooks/` (cline, opencode) pass it. */
268
+ reviewRequestFile?: string;
269
+ }
270
+ /**
271
+ * The JavaScript gate body.
272
+ *
273
+ * Emits a self-contained `intuticGate()` that the writer calls. Refuses through
274
+ * whichever mechanism {@link JsGateOptions.contract} names, so a harness that
275
+ * reads stdout rather than the exit code cannot be given the wrong one by
276
+ * accident — it is a parameter, not a copy-paste.
277
+ */
278
+ export declare function emitJsGate(opts: JsGateOptions): string;
279
+ /**
280
+ * Pre-image capture — the storage half of the rollback enforcement rung (TD-328).
281
+ *
282
+ * ## Why this exists and why it is here
283
+ *
284
+ * The enforcement ladder jumps from Reask (weak) to Kill (destructive). The
285
+ * missing rung is **Revert**: undo a flagged action's effect instead of ending
286
+ * the session over it. Reverting needs a pre-image, and nothing in the system
287
+ * had one — `ChangeEntry` records `tool`, `op`, `target` and deliberately
288
+ * **never the content being written**, so a manifest can say *what* changed
289
+ * and not *to what*.
290
+ *
291
+ * It belongs in the PreToolUse gate and nowhere else. The proxy sees tool calls
292
+ * the model *proposed*, before the harness runs them; only the hook fires in
293
+ * the instant between "this call was flagged and allowed" and "the file
294
+ * changed". That instant is the only place the prior bytes still exist.
295
+ *
296
+ * ## What is captured, and what is refused
297
+ *
298
+ * Only the `warn` tier — a call a guard flagged and let proceed. Blocked calls
299
+ * changed nothing, so there is nothing to restore; clean calls were never
300
+ * interesting. That keeps the volume proportional to flags rather than to
301
+ * edits.
302
+ *
303
+ * **Off unless asked for, and that is not this codebase's usual default.**
304
+ * Every other guard here is on by default because being wrong costs a false
305
+ * positive. This one stores the *contents* of the user's files on their disk,
306
+ * so being wrong costs a copy of their work nobody consented to. It follows
307
+ * the `mirror_sample_rate: 0.0` precedent: a capability with a real external
308
+ * cost is opted into, not defaulted on. `.intutic/rollback/` is gitignored by
309
+ * the same writer that creates it.
310
+ *
311
+ * Three ceilings, each refusing rather than truncating — a partial pre-image
312
+ * restores a file to a state it was never in, which is worse than no rollback:
313
+ * * `MAX_PREIMAGE_BYTES` per file
314
+ * * `MAX_PREIMAGE_ENTRIES` retained, oldest evicted
315
+ * * only paths inside the workspace root, resolved — a capture of
316
+ * `../../.ssh/id_rsa` would be an exfiltration primitive wearing a
317
+ * governance badge.
318
+ */
319
+ export declare function emitPreImageCapture(): string;
320
+ /**
321
+ * The JS fail-closed prelude — the FIRST statements of every emitted JS gate.
322
+ *
323
+ * The edge it closes: each writer's stdin handler wraps its work in a
324
+ * try/catch that fails closed, but a crash OUTSIDE that handler — a
325
+ * module-load failure, an unhandled promise rejection from an async path —
326
+ * ended the process with Node's default exit 1. Every exit-code harness reads
327
+ * a non-2 non-zero exit as "the hook errored" and ALLOWS the call, and the
328
+ * stdout-cancel harnesses saw no cancel object, which is also an allow. So a
329
+ * gate that crashed was indistinguishable from a gate that approved.
330
+ *
331
+ * Interpolated by the writer as the first executable code in the script —
332
+ * before any `require` (after `'use strict'` where the writer has one, since a
333
+ * directive must lead the file to take effect) — so everything that runs is
334
+ * covered. The one thing it cannot cover is a SyntaxError in the emitted file
335
+ * itself: Node rejects the whole module before any statement executes,
336
+ * handlers included. That residue is pinned by the behaviour tests running
337
+ * every emitted gate for real.
338
+ *
339
+ * Refuses through the harness's contract, like everything else: exit 2 means
340
+ * nothing to cline or roo-code, whose harnesses only read stdout — a crashed
341
+ * stdout-cancel gate must still print its cancel object.
342
+ */
343
+ export declare function emitJsFailClosedPrelude(opts: JsGateOptions): string;
344
+ /**
345
+ * The single `python3` invocation that replaces the three or four each bash gate
346
+ * used to spawn.
347
+ *
348
+ * Process startup dominated the old gates: three `python3` spawns is 75–200ms on
349
+ * the tool path, more than rule evaluation will ever cost. This does the whole
350
+ * extraction in one, and collapses whitespace while it is there — which also
351
+ * makes the three values safe to read back as three lines.
352
+ *
353
+ * **Fails closed if there is no parser.** Without one the gate cannot know what
354
+ * it is being asked to allow, and the old behaviour — empty tool, empty target,
355
+ * allow — was a silent hole. The message names the fix, because a developer who
356
+ * hits this needs to act on it rather than file a bug.
357
+ */
358
+ export declare const SHELL_EXTRACT = "\nif ! command -v python3 >/dev/null 2>&1; then\n echo \"[Intutic Governance] BLOCKED: python3 is required to evaluate tool calls and was not found on PATH.\" >&2\n echo \"[Intutic Governance] Install python3, or unset the Intutic hooks to run ungoverned.\" >&2\n exit 2\nfi\n\nINTUTIC_FIELDS=\"$(printf '%s' \"$INPUT\" | python3 -c '\nimport sys, json\ndef clean(v):\n return \" \".join(str(v).split()) if isinstance(v, str) else \"\"\nstate = \"ok\"\ntry:\n d = json.load(sys.stdin)\nexcept Exception:\n d = {}\n state = \"malformed\"\nif not isinstance(d, dict):\n d = {}\n state = \"malformed\"\n# The envelope check githubCopilotHooks established, applied to the bash\n# family: a payload carrying none of the tool fields in any known spelling is\n# one the gate cannot evaluate, and extracting empty fields from it would\n# match no rule \u2014 an allow. Reported as a STATE and decided in the gate body,\n# because log_event is not defined yet at extraction time.\nif state == \"ok\" and not any(\n k in d for k in (\"tool_name\", \"toolName\", \"tool_input\", \"toolInput\")\n):\n state = \"malformed\"\ni = d.get(\"tool_input\") or {}\nif not isinstance(i, dict):\n i = {}\ndef first(*keys):\n for k in keys:\n v = i.get(k)\n if isinstance(v, str) and v:\n return clean(v)\n return \"\"\n# The state line comes FIRST: the lines after it may legitimately be empty,\n# and command substitution strips trailing newlines, so the last line is the\n# only position an empty value cannot survive in.\nprint(state)\nprint(clean(d.get(\"tool_name\", \"\")))\nprint(first(\"path\", \"file_path\", \"notebook_path\", \"filePath\"))\nprint(first(\"command\", \"cmd\", \"script\", \"shell_command\"))\nprint(clean(d.get(\"session_id\", d.get(\"sessionId\", \"\"))))\n# The FULL tool_input, serialized to the exact shape argPattern rules are\n# matched against everywhere: JSON.stringify(tool_input) \u2014 compact separators,\n# insertion order, non-ASCII intact. The selected fields above are whitespace-\n# collapsed for token matching; this one must NOT be, or a WHERE clause that\n# spans a key/value boundary matches here and not in the JS gates. json.dumps\n# escapes every newline, so it is still exactly one line to read back.\nprint(json.dumps(i, separators=(\",\", \":\"), ensure_ascii=False))\n' 2>/dev/null || printf '\\n\\n\\n\\n\\n\\n')\"\n\n# Each read is `|| true` because command substitution strips trailing newlines:\n# a tool call with no command argument yields fewer lines than reads, so a late\n# read hits EOF and returns non-zero. Under `set -e` that killed the gate with\n# **exit 1** \u2014 which every harness reads as a hook error and lets the call\n# through. A guard that fails open on the most ordinary input there is (a Write\n# with no shell command) is worse than no guard, because it looks present.\n{ IFS= read -r INTUTIC_EXTRACT_STATE || true; IFS= read -r TOOL || true; IFS= read -r TARGET || true; IFS= read -r COMMAND || true; IFS= read -r SESSION_ID || true; IFS= read -r TOOL_INPUT_JSON || true; } <<EOF_INTUTIC_FIELDS\n$INTUTIC_FIELDS\nEOF_INTUTIC_FIELDS\nTOOL=\"${TOOL:-}\"; TARGET=\"${TARGET:-}\"; COMMAND=\"${COMMAND:-}\"; SESSION_ID=\"${SESSION_ID:-}\"\n# Empty means the extractor itself died (the fallback printf above): treat it\n# exactly like a payload the parser rejected. The gate body refuses on any\n# value other than \"ok\" \u2014 see the envelope refusal in emitShellGate.\nINTUTIC_EXTRACT_STATE=\"${INTUTIC_EXTRACT_STATE:-}\"\n[ -n \"$INTUTIC_EXTRACT_STATE\" ] || INTUTIC_EXTRACT_STATE=\"malformed\"\n# Empty only when the extractor failed wholesale (malformed stdin). Defaulting\n# to the empty OBJECT rather than the empty string keeps the argPattern match\n# well-defined either way.\nTOOL_INPUT_JSON=\"${TOOL_INPUT_JSON:-}\"\n[ -n \"$TOOL_INPUT_JSON\" ] || TOOL_INPUT_JSON=\"{}\"\n# Aliases for the names the older writers used. Kept so this extractor is a\n# drop-in for every bash gate; the canonical names are the unsuffixed ones.\nTOOL_NAME=\"$TOOL\"; TARGET_PATH=\"$TARGET\"\n";
359
+ /**
360
+ * The Python gate, for Open WebUI.
361
+ *
362
+ * Open WebUI filters see a **prompt**, not a tool call — there is no tool name,
363
+ * no path argument and no shell command. So this deliberately does less than the
364
+ * other two emitters, and the difference is the point:
365
+ *
366
+ * - It **can** refuse. `inlet()` used to hardcode `"event": "tool_allowed"` and
367
+ * return the body unconditionally; it was notification-only, and nothing said
368
+ * so. Raising from `inlet()` is how an Open WebUI filter blocks, and that path
369
+ * now exists.
370
+ * - It **does not** block on the compiled floor. A prompt that mentions
371
+ * `.claude/settings.json` is someone asking about a file, not an agent writing
372
+ * it, and refusing to discuss a path is the kind of false positive that gets a
373
+ * governance tool switched off. Those rules flag instead.
374
+ * - It **does** refuse on a snapshot rule whose severity is `block`, so a
375
+ * workspace that wants a hard stop here can have one without a release.
376
+ *
377
+ * A control that claims more than it delivers is worse than one that claims
378
+ * less, so `enforces` for this harness in `gateRegistry.ts` says exactly this.
379
+ */
380
+ export declare function emitPythonGate(): string;
381
+ /**
382
+ * The n8n workflow gate.
383
+ *
384
+ * A different unit of evaluation than every other JS gate, and the difference
385
+ * is the whole design. n8n's only blocking mechanism is deployment-level:
386
+ * `EXTERNAL_HOOK_FILES=/path/to/hook.js` names a module whose
387
+ * `workflow.preExecute` functions run once per WORKFLOW execution and receive
388
+ * the full Workflow object — every node with its parameters — not one tool
389
+ * call. A hook that throws aborts the execution; that throw is the refusal.
390
+ *
391
+ * So the emitted evaluator walks the workflow's nodes and maps each onto the
392
+ * shape the shared rules were written for:
393
+ *
394
+ * - the node's **type** (`n8n-nodes-base.executeCommand`, …) stands in for the
395
+ * tool name — `subject: 'tool'` rules match it;
396
+ * - the node's **serialized parameters** (compact `JSON.stringify`, the same
397
+ * shape `matchSopRule` pins) stand in for the command/target — `command`,
398
+ * `target` and `any` rules match the normalised serialization, and a WHERE
399
+ * rule's argPattern matches the raw serialization, exactly as it matches
400
+ * `JSON.stringify(tool_input)` in the per-tool gates.
401
+ *
402
+ * Granularity is honest here: one refusal aborts the whole execution, not one
403
+ * node, because that is all preExecute can do. The BLOCKED message names the
404
+ * offending node and rule so the operator knows which node to fix.
405
+ *
406
+ * Refuses by THROWING — there is no exit code and no stdout contract in-process.
407
+ * An internal fault also throws, which n8n treats the same way: fail closed.
408
+ *
409
+ * Verified during the v5 fail-open audit and deliberately left alone: the
410
+ * writer's preExecute wrapper has no try/catch around this gate (see the
411
+ * comment at the module.exports site in n8nHooks.ts), so a fault inside the
412
+ * evaluator aborts the execution exactly as a BLOCKED throw does. This is the
413
+ * one family whose crash posture was already closed; installing process-level
414
+ * exit handlers here would be actively wrong, since the gate runs inside the
415
+ * n8n server process, not a per-call subprocess. Pinned by the "internal
416
+ * fault" case in the n8n block of generatedGateFailClosed.test.ts.
417
+ */
418
+ export declare function emitN8nWorkflowGate(): string;
419
+ /** The destructive tier, for the snapshot writer to ship. Not compiled into any
420
+ * gate — see the note on {@link DESTRUCTIVE_COMMAND_PATTERNS}. */
421
+ export { DESTRUCTIVE_COMMAND_PATTERNS };
422
+ //# sourceMappingURL=gateBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateBody.d.ts","sourceRoot":"","sources":["../../src/harness/gateBody.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,OAAO,EACL,4BAA4B,EAG5B,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAA;AAE5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAK9E,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,kEAAmE,CAAA;AAEjG;iDACiD;AACjD,eAAO,MAAM,wBAAwB,0BAA0B,CAAA;AAC/D,eAAO,MAAM,mBAAmB,0CAAgD,CAAA;AAChF;+BAC+B;AAC/B,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAkBvC;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;GAcG;AACH;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,eAAe,GAAG,sBAAsB,GAAG,OAAO,CAAA;AAmCxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,aAAa,sFAAuF,CAAA;AAEjH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAMnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,iBAAiB,g0CAuB7B,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AA6ID,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CA6Y5D;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,QAAQ,EAAE,aAAa,CAAA;IACvB;wEACoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAiGD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAmPtD;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA8H5C;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAgCnE;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,4gIA8EzB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAyHvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuJ5C;AAED;mEACmE;AACnE,OAAO,EAAE,4BAA4B,EAAE,CAAA"}