@intutic/sync-daemon 2.1.0 → 2.3.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 (288) hide show
  1. package/README.md +1 -1
  2. package/dist/agentReporter.d.ts +24 -11
  3. package/dist/agentReporter.d.ts.map +1 -1
  4. package/dist/agentReporter.js +20 -13
  5. package/dist/agentReporter.js.map +1 -1
  6. package/dist/configReader.d.ts +45 -25
  7. package/dist/configReader.d.ts.map +1 -1
  8. package/dist/configReader.js +70 -46
  9. package/dist/configReader.js.map +1 -1
  10. package/dist/configWriter.d.ts +32 -45
  11. package/dist/configWriter.d.ts.map +1 -1
  12. package/dist/configWriter.js +52 -686
  13. package/dist/configWriter.js.map +1 -1
  14. package/dist/disconnect/harnesses.d.ts +29 -0
  15. package/dist/disconnect/harnesses.d.ts.map +1 -0
  16. package/dist/disconnect/harnesses.js +805 -0
  17. package/dist/disconnect/harnesses.js.map +1 -0
  18. package/dist/disconnect/index.d.ts +25 -0
  19. package/dist/disconnect/index.d.ts.map +1 -0
  20. package/dist/disconnect/index.js +92 -0
  21. package/dist/disconnect/index.js.map +1 -0
  22. package/dist/disconnect/mcp.d.ts +81 -0
  23. package/dist/disconnect/mcp.d.ts.map +1 -0
  24. package/dist/disconnect/mcp.js +354 -0
  25. package/dist/disconnect/mcp.js.map +1 -0
  26. package/dist/disconnect/originals.d.ts +88 -0
  27. package/dist/disconnect/originals.d.ts.map +1 -0
  28. package/dist/disconnect/originals.js +257 -0
  29. package/dist/disconnect/originals.js.map +1 -0
  30. package/dist/disconnect/plan.d.ts +175 -0
  31. package/dist/disconnect/plan.d.ts.map +1 -0
  32. package/dist/disconnect/plan.js +580 -0
  33. package/dist/disconnect/plan.js.map +1 -0
  34. package/dist/disconnect/recognise.d.ts +43 -0
  35. package/dist/disconnect/recognise.d.ts.map +1 -0
  36. package/dist/disconnect/recognise.js +108 -0
  37. package/dist/disconnect/recognise.js.map +1 -0
  38. package/dist/disconnect/tomlLines.d.ts +39 -0
  39. package/dist/disconnect/tomlLines.d.ts.map +1 -0
  40. package/dist/disconnect/tomlLines.js +105 -0
  41. package/dist/disconnect/tomlLines.js.map +1 -0
  42. package/dist/harness/aiderConfigMerger.d.ts +29 -8
  43. package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
  44. package/dist/harness/aiderConfigMerger.js +63 -12
  45. package/dist/harness/aiderConfigMerger.js.map +1 -1
  46. package/dist/harness/antigravityCliHooks.d.ts +61 -0
  47. package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
  48. package/dist/harness/antigravityCliHooks.js +207 -0
  49. package/dist/harness/antigravityCliHooks.js.map +1 -0
  50. package/dist/harness/antigravityHooks.d.ts +12 -2
  51. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  52. package/dist/harness/antigravityHooks.js +38 -16
  53. package/dist/harness/antigravityHooks.js.map +1 -1
  54. package/dist/harness/antigravityProducts.d.ts +40 -0
  55. package/dist/harness/antigravityProducts.d.ts.map +1 -0
  56. package/dist/harness/antigravityProducts.js +109 -0
  57. package/dist/harness/antigravityProducts.js.map +1 -0
  58. package/dist/harness/claudeAgentsMd.d.ts +29 -0
  59. package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
  60. package/dist/harness/claudeAgentsMd.js +80 -0
  61. package/dist/harness/claudeAgentsMd.js.map +1 -0
  62. package/dist/harness/claudeCodeHooks.d.ts +6 -6
  63. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  64. package/dist/harness/claudeCodeHooks.js +115 -28
  65. package/dist/harness/claudeCodeHooks.js.map +1 -1
  66. package/dist/harness/claudeProjectApproval.d.ts +53 -0
  67. package/dist/harness/claudeProjectApproval.d.ts.map +1 -0
  68. package/dist/harness/claudeProjectApproval.js +159 -0
  69. package/dist/harness/claudeProjectApproval.js.map +1 -0
  70. package/dist/harness/clineHooks.d.ts +0 -1
  71. package/dist/harness/clineHooks.d.ts.map +1 -1
  72. package/dist/harness/clineHooks.js +13 -4
  73. package/dist/harness/clineHooks.js.map +1 -1
  74. package/dist/harness/codexConfigMerger.d.ts.map +1 -1
  75. package/dist/harness/codexConfigMerger.js +4 -0
  76. package/dist/harness/codexConfigMerger.js.map +1 -1
  77. package/dist/harness/codexHooks.d.ts +2 -0
  78. package/dist/harness/codexHooks.d.ts.map +1 -1
  79. package/dist/harness/codexHooks.js +15 -4
  80. package/dist/harness/codexHooks.js.map +1 -1
  81. package/dist/harness/continueConfigMerger.d.ts.map +1 -1
  82. package/dist/harness/continueConfigMerger.js +4 -0
  83. package/dist/harness/continueConfigMerger.js.map +1 -1
  84. package/dist/harness/cursorHooks.d.ts +5 -2
  85. package/dist/harness/cursorHooks.d.ts.map +1 -1
  86. package/dist/harness/cursorHooks.js +12 -3
  87. package/dist/harness/cursorHooks.js.map +1 -1
  88. package/dist/harness/dshHooks.d.ts +5 -5
  89. package/dist/harness/dshHooks.d.ts.map +1 -1
  90. package/dist/harness/dshHooks.js +12 -5
  91. package/dist/harness/dshHooks.js.map +1 -1
  92. package/dist/harness/gateArtifacts.d.ts +35 -0
  93. package/dist/harness/gateArtifacts.d.ts.map +1 -0
  94. package/dist/harness/gateArtifacts.js +88 -0
  95. package/dist/harness/gateArtifacts.js.map +1 -0
  96. package/dist/harness/gateBody.d.ts +195 -69
  97. package/dist/harness/gateBody.d.ts.map +1 -1
  98. package/dist/harness/gateBody.js +847 -182
  99. package/dist/harness/gateBody.js.map +1 -1
  100. package/dist/harness/gateSightings.d.ts +47 -0
  101. package/dist/harness/gateSightings.d.ts.map +1 -0
  102. package/dist/harness/gateSightings.js +159 -0
  103. package/dist/harness/gateSightings.js.map +1 -0
  104. package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
  105. package/dist/harness/githubCopilotHooks.js +11 -10
  106. package/dist/harness/githubCopilotHooks.js.map +1 -1
  107. package/dist/harness/gooseHardener.d.ts +0 -1
  108. package/dist/harness/gooseHardener.d.ts.map +1 -1
  109. package/dist/harness/gooseHardener.js +1 -1
  110. package/dist/harness/gooseHardener.js.map +1 -1
  111. package/dist/harness/gooseHooks.d.ts +29 -1
  112. package/dist/harness/gooseHooks.d.ts.map +1 -1
  113. package/dist/harness/gooseHooks.js +107 -69
  114. package/dist/harness/gooseHooks.js.map +1 -1
  115. package/dist/harness/grokHooks.d.ts +2 -2
  116. package/dist/harness/grokHooks.d.ts.map +1 -1
  117. package/dist/harness/grokHooks.js +20 -18
  118. package/dist/harness/grokHooks.js.map +1 -1
  119. package/dist/harness/hermesHooks.d.ts +18 -8
  120. package/dist/harness/hermesHooks.d.ts.map +1 -1
  121. package/dist/harness/hermesHooks.js +55 -19
  122. package/dist/harness/hermesHooks.js.map +1 -1
  123. package/dist/harness/holdRedaction.d.ts +4 -1
  124. package/dist/harness/holdRedaction.d.ts.map +1 -1
  125. package/dist/harness/holdRedaction.js +32 -8
  126. package/dist/harness/holdRedaction.js.map +1 -1
  127. package/dist/harness/jetbrainsXmlConfig.d.ts +20 -0
  128. package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -1
  129. package/dist/harness/jetbrainsXmlConfig.js +39 -18
  130. package/dist/harness/jetbrainsXmlConfig.js.map +1 -1
  131. package/dist/harness/mcpAutoWrite.d.ts +62 -25
  132. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  133. package/dist/harness/mcpAutoWrite.js +406 -95
  134. package/dist/harness/mcpAutoWrite.js.map +1 -1
  135. package/dist/harness/museHooks.d.ts +3 -3
  136. package/dist/harness/museHooks.d.ts.map +1 -1
  137. package/dist/harness/museHooks.js +15 -7
  138. package/dist/harness/museHooks.js.map +1 -1
  139. package/dist/harness/n8nHooks.d.ts +0 -1
  140. package/dist/harness/n8nHooks.d.ts.map +1 -1
  141. package/dist/harness/n8nHooks.js +4 -1
  142. package/dist/harness/n8nHooks.js.map +1 -1
  143. package/dist/harness/openCodeHooks.d.ts +2 -2
  144. package/dist/harness/openCodeHooks.d.ts.map +1 -1
  145. package/dist/harness/openCodeHooks.js +9 -8
  146. package/dist/harness/openCodeHooks.js.map +1 -1
  147. package/dist/harness/openWebuiHooks.d.ts +0 -1
  148. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  149. package/dist/harness/openWebuiHooks.js +11 -3
  150. package/dist/harness/openWebuiHooks.js.map +1 -1
  151. package/dist/harness/openclawHooks.d.ts +75 -18
  152. package/dist/harness/openclawHooks.d.ts.map +1 -1
  153. package/dist/harness/openclawHooks.js +214 -160
  154. package/dist/harness/openclawHooks.js.map +1 -1
  155. package/dist/harness/openhandsHooks.d.ts +37 -11
  156. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  157. package/dist/harness/openhandsHooks.js +148 -97
  158. package/dist/harness/openhandsHooks.js.map +1 -1
  159. package/dist/harness/piHooks.d.ts +52 -17
  160. package/dist/harness/piHooks.d.ts.map +1 -1
  161. package/dist/harness/piHooks.js +177 -163
  162. package/dist/harness/piHooks.js.map +1 -1
  163. package/dist/harness/protectedPaths.d.ts +107 -25
  164. package/dist/harness/protectedPaths.d.ts.map +1 -1
  165. package/dist/harness/protectedPaths.js +342 -48
  166. package/dist/harness/protectedPaths.js.map +1 -1
  167. package/dist/harness/rulesSection.d.ts +62 -0
  168. package/dist/harness/rulesSection.d.ts.map +1 -0
  169. package/dist/harness/rulesSection.js +145 -0
  170. package/dist/harness/rulesSection.js.map +1 -0
  171. package/dist/harness/windsurfHooks.d.ts +18 -3
  172. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  173. package/dist/harness/windsurfHooks.js +41 -8
  174. package/dist/harness/windsurfHooks.js.map +1 -1
  175. package/dist/harness/windsurfJetBrainsProxy.d.ts +1 -1
  176. package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
  177. package/dist/harness/windsurfJetBrainsProxy.js +5 -1
  178. package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
  179. package/dist/index.d.ts +21 -20
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +19 -19
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory.d.ts +53 -0
  184. package/dist/inventory.d.ts.map +1 -0
  185. package/dist/inventory.js +133 -0
  186. package/dist/inventory.js.map +1 -0
  187. package/dist/lib/complianceProbes.d.ts +2 -1
  188. package/dist/lib/complianceProbes.d.ts.map +1 -1
  189. package/dist/lib/complianceProbes.js +1 -1
  190. package/dist/lib/complianceProbes.js.map +1 -1
  191. package/dist/lib/decisionsDigest.d.ts +35 -17
  192. package/dist/lib/decisionsDigest.d.ts.map +1 -1
  193. package/dist/lib/decisionsDigest.js +106 -47
  194. package/dist/lib/decisionsDigest.js.map +1 -1
  195. package/dist/lib/egressPolicy.d.ts +2 -2
  196. package/dist/lib/egressPolicy.js +2 -2
  197. package/dist/lib/mcpRegistryPy.d.ts +11 -0
  198. package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
  199. package/dist/lib/mcpRegistryPy.js +173 -0
  200. package/dist/lib/mcpRegistryPy.js.map +1 -0
  201. package/dist/lib/phrasesPy.d.ts +11 -0
  202. package/dist/lib/phrasesPy.d.ts.map +1 -0
  203. package/dist/lib/phrasesPy.js +351 -0
  204. package/dist/lib/phrasesPy.js.map +1 -0
  205. package/dist/lib/policySnapshot.d.ts +63 -9
  206. package/dist/lib/policySnapshot.d.ts.map +1 -1
  207. package/dist/lib/policySnapshot.js +269 -45
  208. package/dist/lib/policySnapshot.js.map +1 -1
  209. package/dist/lib/processPoller.d.ts.map +1 -1
  210. package/dist/lib/processPoller.js +9 -2
  211. package/dist/lib/processPoller.js.map +1 -1
  212. package/dist/lib/regexLinearity.d.ts +43 -0
  213. package/dist/lib/regexLinearity.d.ts.map +1 -0
  214. package/dist/lib/regexLinearity.js +275 -0
  215. package/dist/lib/regexLinearity.js.map +1 -0
  216. package/dist/lib/runtimeEnv.d.ts +1 -3
  217. package/dist/lib/runtimeEnv.d.ts.map +1 -1
  218. package/dist/lib/runtimeEnv.js +0 -2
  219. package/dist/lib/runtimeEnv.js.map +1 -1
  220. package/dist/lib/sequencePy.d.ts +10 -0
  221. package/dist/lib/sequencePy.d.ts.map +1 -0
  222. package/dist/lib/sequencePy.js +102 -0
  223. package/dist/lib/sequencePy.js.map +1 -0
  224. package/dist/sessionReporter.d.ts +18 -12
  225. package/dist/sessionReporter.d.ts.map +1 -1
  226. package/dist/sessionReporter.js +37 -24
  227. package/dist/sessionReporter.js.map +1 -1
  228. package/dist/skillWriter.d.ts +23 -5
  229. package/dist/skillWriter.d.ts.map +1 -1
  230. package/dist/skillWriter.js +159 -16
  231. package/dist/skillWriter.js.map +1 -1
  232. package/dist/syncCycle.d.ts +142 -0
  233. package/dist/syncCycle.d.ts.map +1 -0
  234. package/dist/syncCycle.js +327 -0
  235. package/dist/syncCycle.js.map +1 -0
  236. package/dist/trajectoryMonitor.d.ts +2 -4
  237. package/dist/trajectoryMonitor.d.ts.map +1 -1
  238. package/dist/trajectoryMonitor.js +2 -5
  239. package/dist/trajectoryMonitor.js.map +1 -1
  240. package/dist/watcher/driftWatcher.d.ts +0 -1
  241. package/dist/watcher/driftWatcher.d.ts.map +1 -1
  242. package/dist/watcher/driftWatcher.js +1 -2
  243. package/dist/watcher/driftWatcher.js.map +1 -1
  244. package/dist/watcher/settingsGuard.d.ts +36 -7
  245. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  246. package/dist/watcher/settingsGuard.js +397 -60
  247. package/dist/watcher/settingsGuard.js.map +1 -1
  248. package/dist/watcher/vscodeHookSettings.d.ts +30 -0
  249. package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
  250. package/dist/watcher/vscodeHookSettings.js +170 -0
  251. package/dist/watcher/vscodeHookSettings.js.map +1 -0
  252. package/dist/wsClient.d.ts +0 -1
  253. package/dist/wsClient.d.ts.map +1 -1
  254. package/dist/wsClient.js +0 -1
  255. package/dist/wsClient.js.map +1 -1
  256. package/package.json +5 -5
  257. package/dist/harness/claudeDesktopHooks.d.ts +0 -30
  258. package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
  259. package/dist/harness/claudeDesktopHooks.js +0 -254
  260. package/dist/harness/claudeDesktopHooks.js.map +0 -1
  261. package/dist/harness/continueHooks.d.ts +0 -63
  262. package/dist/harness/continueHooks.d.ts.map +0 -1
  263. package/dist/harness/continueHooks.js +0 -247
  264. package/dist/harness/continueHooks.js.map +0 -1
  265. package/dist/harness/gateKind.d.ts +0 -164
  266. package/dist/harness/gateKind.d.ts.map +0 -1
  267. package/dist/harness/gateKind.js +0 -144
  268. package/dist/harness/gateKind.js.map +0 -1
  269. package/dist/harness/rooCodeHooks.d.ts +0 -31
  270. package/dist/harness/rooCodeHooks.d.ts.map +0 -1
  271. package/dist/harness/rooCodeHooks.js +0 -286
  272. package/dist/harness/rooCodeHooks.js.map +0 -1
  273. package/dist/hashReporter.d.ts +0 -33
  274. package/dist/hashReporter.d.ts.map +0 -1
  275. package/dist/hashReporter.js +0 -69
  276. package/dist/hashReporter.js.map +0 -1
  277. package/dist/integrityStore.d.ts +0 -35
  278. package/dist/integrityStore.d.ts.map +0 -1
  279. package/dist/integrityStore.js +0 -71
  280. package/dist/integrityStore.js.map +0 -1
  281. package/dist/lib/gitWorktrees.d.ts +0 -140
  282. package/dist/lib/gitWorktrees.d.ts.map +0 -1
  283. package/dist/lib/gitWorktrees.js +0 -191
  284. package/dist/lib/gitWorktrees.js.map +0 -1
  285. package/dist/syncLoop.d.ts +0 -172
  286. package/dist/syncLoop.d.ts.map +0 -1
  287. package/dist/syncLoop.js +0 -878
  288. package/dist/syncLoop.js.map +0 -1
@@ -1,34 +1,91 @@
1
1
  /**
2
- * openclawHooks.ts — Openclaw governance hook injection.
2
+ * openclawHooks.ts — OpenClaw plugin gate.
3
3
  *
4
- * Openclaw uses `~/.openclaw/openclaw.json` (JSON5 format — may have comments
5
- * and trailing commas). The hook entry is registered under
6
- * `hooks.internal.entries["intutic-governance"]`. This module:
4
+ * OpenClaw has two hook systems, and only one of them can stop a tool call.
5
+ * Its *internal* hooks (`hooks.internal.entries` in `~/.openclaw/openclaw.json`,
6
+ * `HOOK.md` handlers) react to command, session and Gateway events; the
7
+ * dispatcher catches a handler's error and carries on, and tool calls are not
8
+ * among their events at all. Earlier versions of this writer registered the
9
+ * gate there, as `{ event: 'PreToolUse', command }`, a shape OpenClaw keeps
10
+ * (hook entries are open-ended) and never runs. Tool calls are intercepted by
11
+ * a *plugin* hook, `before_tool_call`, registered with `api.on(...)`:
12
+ * https://github.com/openclaw/openclaw/blob/59309cc7777e07d52ca1be94bf8c25fd685919bc/docs/automation/hooks.md
13
+ * ("Choose the right surface": intercepting tools is a plugin hook).
7
14
  *
8
- * 1. JSON5-safe-merges `~/.openclaw/openclaw.json` with the hook entry.
9
- * 2. Writes `~/.intutic/hooks/openclaw-check.js` — dual-path Node.js hook.
10
- * 3. Writes `.intutic/env/openclaw.env` — env snippet.
11
- * 4. Optionally validates with `openclaw hooks check` (non-fatal).
15
+ * ## The plugin
12
16
  *
13
- * Hook protocol: stdin JSON `{tool_name, tool_input, transcript_path}`,
14
- * exit 0 = allow, exit 2 = block (stderr shown to agent).
17
+ * A standalone plugin file needs no manifest when it is listed in
18
+ * `plugins.load.paths`; its id is the file's basename, so the file is
19
+ * `intutic-governance.cjs` and exports `{ id: 'intutic-governance', register }`
20
+ * (`docs/plugins/hooks/tool-policy.md` "Sender-aware policy in one file";
21
+ * `src/plugins/discovery.ts` `discoverFromPath`; the loader test "loads
22
+ * multiple manifestless standalone files"). `.cjs`, so the module format does
23
+ * not depend on any `package.json` above it. A plugin from a configured path
24
+ * is enabled by default unless a non-empty `plugins.allow` leaves it out
25
+ * (`src/plugins/config-activation-shared.ts`), so the id is added to an
26
+ * existing allowlist and nothing is written when there is none.
15
27
  *
16
- * WS-C2 — Openclaw harness (Phase 3 cross-harness defence)
17
- * LLD #14 — Phase 3 cross-harness defence (Gap 3)
18
- * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
28
+ * `before_tool_call` receives `{ toolName, params, toolCallId?, runId?,
29
+ * derivedPaths? }` and `ctx.sessionId`, and refuses with `{ block: true,
30
+ * blockReason }`, which is terminal (`docs/plugins/hooks/tool-policy.md`,
31
+ * `src/plugins/hook-types.ts`). The global hook runner runs it fail-closed: a
32
+ * handler that throws or outlives its timeout blocks the call
33
+ * (`src/plugins/hook-runner-global.ts`: `before_tool_call: "fail-closed"`;
34
+ * default 15 s, `docs/plugins/hooks/reference.md`). That timeout cannot stop
35
+ * synchronous code, so the hook registers `timeoutMs: HOOK_TIMEOUT_SECONDS *
36
+ * 1000` and the gate runs under its own deadline inside it
37
+ * (`gateDeadlineMs('openclaw')`).
38
+ *
39
+ * The plugin embeds the shared `emitJsGate` body (`'throw'` contract) and
40
+ * returns OpenClaw's block result for the refusal it throws. Any other fault
41
+ * is a block too. MCP tools reach the hook as `<server>__<tool>`
42
+ * (`src/agents/agent-bundle-mcp-names.ts`, `TOOL_NAME_SEPARATOR`), which the
43
+ * plugin composes into `mcp__<server>__<tool>` for the allowlist.
44
+ *
45
+ * The file lives under `~/.intutic/hooks/`, a governance-protected path, so an
46
+ * agent cannot edit it under any gated harness.
19
47
  *
20
48
  * @module
21
49
  */
50
+ /** The plugin id, which OpenClaw takes from the standalone file's basename. */
51
+ export declare const OPENCLAW_PLUGIN_ID = "intutic-governance";
52
+ /** The plugin file, relative to the home directory. */
53
+ export declare const OPENCLAW_PLUGIN_FILE: string;
54
+ /** The first line of the plugin's header, which disconnect recognises it by. */
55
+ export declare const OPENCLAW_PLUGIN_MARKER = "Intutic OpenClaw governance plugin.";
56
+ /** The absolute path of the plugin file for the current home directory. */
57
+ export declare function openclawPluginPath(): string;
58
+ export declare function buildOpenclawPlugin(proxyUrl: string, workspaceRoot: string, workspaceId: string): string;
59
+ /**
60
+ * Attempt to parse a JSON5-like string by progressively stripping
61
+ * non-standard syntax. Returns a plain JS object or throws if unparseable.
62
+ */
63
+ export declare function parseJson5Like(raw: string): Record<string, unknown>;
22
64
  /**
23
- * Write the Openclaw governance hook script and merge hook configuration
24
- * into `~/.openclaw/openclaw.json`.
65
+ * Adds the plugin to `plugins.load.paths`, and its id to `plugins.allow` when
66
+ * that list is non-empty (an empty or absent one allows every plugin).
67
+ * Everything else is kept, and a second run adds nothing.
68
+ */
69
+ export declare function mergeOpenclawConfig(existing: Record<string, unknown>, pluginPath: string): Record<string, unknown>;
70
+ /**
71
+ * Write the OpenClaw governance plugin and list it in
72
+ * `~/.openclaw/openclaw.json`.
25
73
  *
26
- * Safe to call repeatedly — uses atomic rename so partial writes never
27
- * leave a corrupt state.
74
+ * Safe to call repeatedly: the plugin is replaced by atomic rename and the
75
+ * config merge is idempotent. A config file that does not parse is left as
76
+ * it is, and reported.
28
77
  *
29
78
  * @param workspaceRoot - Absolute workspace root path.
30
- * @param proxyUrl - Intutic proxy URL written into hook headers.
79
+ * @param proxyUrl - Intutic proxy URL written into the plugin header.
31
80
  * @param workspaceId - Workspace ID embedded in every hook event payload.
32
81
  */
33
82
  export declare function writeOpenclawHooks(workspaceRoot: string, proxyUrl: string, workspaceId?: string): Promise<void>;
83
+ /**
84
+ * OpenClaw's agent workspace, where it loads `AGENTS.md` and its other
85
+ * bootstrap files every session — never from a project:
86
+ * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`, else
87
+ * `OPENCLAW_WORKSPACE_DIR`, else `~/.openclaw/workspace`
88
+ * (https://github.com/openclaw/openclaw/blob/98457908cf3e4fbbd0354b530e0aa0d15b560158/docs/gateway/config-agents/workspace-and-bootstrap.md).
89
+ */
90
+ export declare function openclawAgentWorkspace(): Promise<string>;
34
91
  //# sourceMappingURL=openclawHooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"openclawHooks.d.ts","sourceRoot":"","sources":["../../src/harness/openclawHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2MH;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CA+Ef"}
1
+ {"version":3,"file":"openclawHooks.d.ts","sourceRoot":"","sources":["../../src/harness/openclawHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAaH,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AAEtD,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,QAA0E,CAAA;AAE3G,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,wCAAwC,CAAA;AAM3E,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAID,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAyHxG;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBnE;AAKD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAalH;AAaD;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAc9D"}
@@ -1,56 +1,96 @@
1
1
  /**
2
- * openclawHooks.ts — Openclaw governance hook injection.
2
+ * openclawHooks.ts — OpenClaw plugin gate.
3
3
  *
4
- * Openclaw uses `~/.openclaw/openclaw.json` (JSON5 format — may have comments
5
- * and trailing commas). The hook entry is registered under
6
- * `hooks.internal.entries["intutic-governance"]`. This module:
4
+ * OpenClaw has two hook systems, and only one of them can stop a tool call.
5
+ * Its *internal* hooks (`hooks.internal.entries` in `~/.openclaw/openclaw.json`,
6
+ * `HOOK.md` handlers) react to command, session and Gateway events; the
7
+ * dispatcher catches a handler's error and carries on, and tool calls are not
8
+ * among their events at all. Earlier versions of this writer registered the
9
+ * gate there, as `{ event: 'PreToolUse', command }`, a shape OpenClaw keeps
10
+ * (hook entries are open-ended) and never runs. Tool calls are intercepted by
11
+ * a *plugin* hook, `before_tool_call`, registered with `api.on(...)`:
12
+ * https://github.com/openclaw/openclaw/blob/59309cc7777e07d52ca1be94bf8c25fd685919bc/docs/automation/hooks.md
13
+ * ("Choose the right surface": intercepting tools is a plugin hook).
7
14
  *
8
- * 1. JSON5-safe-merges `~/.openclaw/openclaw.json` with the hook entry.
9
- * 2. Writes `~/.intutic/hooks/openclaw-check.js` — dual-path Node.js hook.
10
- * 3. Writes `.intutic/env/openclaw.env` — env snippet.
11
- * 4. Optionally validates with `openclaw hooks check` (non-fatal).
15
+ * ## The plugin
12
16
  *
13
- * Hook protocol: stdin JSON `{tool_name, tool_input, transcript_path}`,
14
- * exit 0 = allow, exit 2 = block (stderr shown to agent).
17
+ * A standalone plugin file needs no manifest when it is listed in
18
+ * `plugins.load.paths`; its id is the file's basename, so the file is
19
+ * `intutic-governance.cjs` and exports `{ id: 'intutic-governance', register }`
20
+ * (`docs/plugins/hooks/tool-policy.md` "Sender-aware policy in one file";
21
+ * `src/plugins/discovery.ts` `discoverFromPath`; the loader test "loads
22
+ * multiple manifestless standalone files"). `.cjs`, so the module format does
23
+ * not depend on any `package.json` above it. A plugin from a configured path
24
+ * is enabled by default unless a non-empty `plugins.allow` leaves it out
25
+ * (`src/plugins/config-activation-shared.ts`), so the id is added to an
26
+ * existing allowlist and nothing is written when there is none.
15
27
  *
16
- * WS-C2 — Openclaw harness (Phase 3 cross-harness defence)
17
- * LLD #14 — Phase 3 cross-harness defence (Gap 3)
18
- * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
28
+ * `before_tool_call` receives `{ toolName, params, toolCallId?, runId?,
29
+ * derivedPaths? }` and `ctx.sessionId`, and refuses with `{ block: true,
30
+ * blockReason }`, which is terminal (`docs/plugins/hooks/tool-policy.md`,
31
+ * `src/plugins/hook-types.ts`). The global hook runner runs it fail-closed: a
32
+ * handler that throws or outlives its timeout blocks the call
33
+ * (`src/plugins/hook-runner-global.ts`: `before_tool_call: "fail-closed"`;
34
+ * default 15 s, `docs/plugins/hooks/reference.md`). That timeout cannot stop
35
+ * synchronous code, so the hook registers `timeoutMs: HOOK_TIMEOUT_SECONDS *
36
+ * 1000` and the gate runs under its own deadline inside it
37
+ * (`gateDeadlineMs('openclaw')`).
38
+ *
39
+ * The plugin embeds the shared `emitJsGate` body (`'throw'` contract) and
40
+ * returns OpenClaw's block result for the refusal it throws. Any other fault
41
+ * is a block too. MCP tools reach the hook as `<server>__<tool>`
42
+ * (`src/agents/agent-bundle-mcp-names.ts`, `TOOL_NAME_SEPARATOR`), which the
43
+ * plugin composes into `mcp__<server>__<tool>` for the allowlist.
44
+ *
45
+ * The file lives under `~/.intutic/hooks/`, a governance-protected path, so an
46
+ * agent cannot edit it under any gated harness.
19
47
  *
20
48
  * @module
21
49
  */
22
50
  import * as fs from 'node:fs/promises';
23
51
  import * as path from 'node:path';
24
52
  import * as os from 'node:os';
25
- import { exec } from 'node:child_process';
26
- import { promisify } from 'node:util';
27
53
  import { createLogger } from '@intutic/logger';
54
+ import { keepOriginal, writeOwnedFile } from '../disconnect/originals.js';
28
55
  import { newIso } from '@intutic/id';
29
- import { emitJsGate, emitJsFailClosedPrelude } from './gateBody.js';
56
+ import { HOOK_TIMEOUT_SECONDS } from '@intutic/shared-types';
57
+ import { emitJsGate, REVIEW_REQUESTS_BASENAME } from './gateBody.js';
30
58
  const log = createLogger('sync-openclaw-hooks');
31
- const execAsync = promisify(exec);
32
- /** Path to the Openclaw global config file. */
33
- const OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
34
- /** Governance-sensitive paths that the hook gate protects. */
35
- // ─── Node.js hook script template ────────────────────────────────────────────
36
- function buildOpenclawCheckScript(hookEventsLog, workspaceId, proxyUrl) {
37
- return `#!/usr/bin/env node
38
- /**
39
- * Intutic Openclaw PreToolUse execution gate.
59
+ /** The plugin id, which OpenClaw takes from the standalone file's basename. */
60
+ export const OPENCLAW_PLUGIN_ID = 'intutic-governance';
61
+ /** The plugin file, relative to the home directory. */
62
+ export const OPENCLAW_PLUGIN_FILE = path.join('.intutic', 'hooks', 'openclaw', `${OPENCLAW_PLUGIN_ID}.cjs`);
63
+ /** The first line of the plugin's header, which disconnect recognises it by. */
64
+ export const OPENCLAW_PLUGIN_MARKER = 'Intutic OpenClaw governance plugin.';
65
+ function openclawConfigPath() {
66
+ return path.join(os.homedir(), '.openclaw', 'openclaw.json');
67
+ }
68
+ /** The absolute path of the plugin file for the current home directory. */
69
+ export function openclawPluginPath() {
70
+ return path.join(os.homedir(), OPENCLAW_PLUGIN_FILE);
71
+ }
72
+ // ─── The plugin ──────────────────────────────────────────────────────────────
73
+ export function buildOpenclawPlugin(proxyUrl, workspaceRoot, workspaceId) {
74
+ const hookEventsLog = path.join(workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
75
+ return `/**
76
+ * ${OPENCLAW_PLUGIN_MARKER}
40
77
  * Auto-generated by intutic sync-daemon. DO NOT EDIT.
41
78
  * Proxy: ${proxyUrl}
42
79
  * Generated: ${newIso()}
43
- * Workspace: ${workspaceId}
80
+ *
81
+ * A before_tool_call plugin hook, loaded through plugins.load.paths in
82
+ * ~/.openclaw/openclaw.json. Refuses a tool call by returning
83
+ * { block: true, blockReason }. See
84
+ * services/sync-daemon/src/harness/openclawHooks.ts.
44
85
  */
45
86
  'use strict';
46
- ${emitJsFailClosedPrelude({ harness: 'openclaw', contract: 'exit2' })}
47
87
  const path = require('path');
48
88
  const os = require('os');
49
89
  const fs = require('fs');
50
90
  const crypto = require('crypto');
51
91
  const https = require('https');
52
92
 
53
- // ── Runtime credentials (sourced from ~/.intutic/env/runtime.env at invocation time) ──
93
+ // Runtime credentials, read when OpenClaw loads the plugin.
54
94
  const _runtimeEnvPath = path.join(os.homedir(), '.intutic', 'env', 'runtime.env');
55
95
  let _intuticHost = 'https://api.intutic.ai', _intuticKey = '', _intuticWsId = ${JSON.stringify(workspaceId)};
56
96
  try {
@@ -61,101 +101,103 @@ try {
61
101
  if (k === 'INTUTIC_API_KEY' && v) _intuticKey = v;
62
102
  if (k === 'INTUTIC_WORKSPACE_ID' && v) _intuticWsId = v;
63
103
  });
64
- } catch { /* runtime.env not yet written — use defaults */ }
104
+ } catch {}
65
105
 
66
- ${emitJsGate({ harness: 'openclaw', contract: 'exit2' })}
106
+ ${emitJsGate({ harness: 'openclaw', contract: 'throw', reviewRequestFile: path.join(workspaceRoot, '.intutic', 'events', REVIEW_REQUESTS_BASENAME) })}
67
107
 
68
- /**
69
- * Appends a governance event to the local hook-events log (Path B)
70
- * and fires a non-blocking HTTP POST to the control plane (Path A).
71
- */
72
108
  let _intuticSessionId = '';
73
109
  function logEvent(verdict, toolName, reason) {
74
110
  try {
75
111
  const ts = new Date().toISOString();
76
- // incidentId = sha1(timestamp + toolName + workspaceId).slice(0,16)
77
112
  const incidentId = crypto.createHash('sha1').update(ts + toolName + _intuticWsId).digest('hex').slice(0, 16);
113
+ // The event's id: random, made once here, and resent with the line it is
114
+ // written into, so the control plane processes the event once.
115
+ const eventId = crypto.randomBytes(16).toString('hex');
78
116
  const entry = JSON.stringify({
79
- // Passed through, not collapsed to two values: the advisory tier emits
80
- // 'tool_flagged', and a ternary here silently recorded it as an allow.
81
117
  event: verdict,
82
- toolName,
83
- reason: reason || '',
118
+ toolName, reason: reason || '',
84
119
  workspaceId: _intuticWsId,
85
120
  harnessType: 'openclaw',
86
121
  timestamp: ts,
87
122
  incidentId,
123
+ eventId,
88
124
  ...(_intuticSessionId ? { sessionId: _intuticSessionId } : {}),
89
125
  }) + '\\n';
90
- // Path B: reliable file append (sync-daemon drains on FSEvents change)
91
- fs.appendFileSync(${JSON.stringify(hookEventsLog)}, entry, { flag: 'a' });
92
- // Path A: fire-and-forget HTTP POST (near-real-time dashboard, non-blocking)
126
+ const logPath = ${JSON.stringify(hookEventsLog)};
127
+ try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch {}
128
+ fs.appendFileSync(logPath, entry, { flag: 'a' });
93
129
  if (_intuticKey) {
94
130
  try {
95
131
  const body = JSON.stringify({ events: [JSON.parse(entry)] });
96
132
  const urlObj = new URL('/api/v1/hook-events', _intuticHost);
97
133
  const isHttps = urlObj.protocol === 'https:';
98
134
  const mod = isHttps ? https : require('http');
99
- const req = mod.request({
100
- hostname: urlObj.hostname,
101
- port: urlObj.port || (isHttps ? 443 : 80),
102
- path: urlObj.pathname,
103
- method: 'POST',
104
- headers: {
105
- 'Content-Type': 'application/json',
106
- 'Content-Length': Buffer.byteLength(body),
107
- 'Authorization': 'Bearer ' + _intuticKey,
108
- },
109
- });
110
- req.on('error', () => { /* fire-and-forget — ignore errors */ });
111
- req.write(body);
112
- req.end();
135
+ const req = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Authorization': 'Bearer ' + _intuticKey } });
136
+ req.on('error', () => { /* fire-and-forget */ });
137
+ req.write(body); req.end();
113
138
  } catch { /* never crash the hook */ }
114
139
  }
115
140
  } catch { /* never crash the hook */ }
116
141
  }
117
142
 
118
- // Read stdin containing Openclaw's tool context
119
- let inputData = '';
120
- process.stdin.on('data', (chunk) => { inputData += chunk; });
121
- process.stdin.on('end', () => {
122
- try {
123
- const ctx = JSON.parse(inputData);
124
- _intuticSessionId = ctx.session_id || ctx.sessionId || ctx.conversation_id || ctx.conversationId || ctx.task_id || ctx.taskId || '';
125
- // An envelope carrying none of the tool fields extracts to empty strings,
126
- // which match no rule — an allow. Refused instead; see the guard itself.
127
- intuticGuardEnvelope(ctx, ['tool_name', 'toolName', 'tool_input', 'toolInput'], logEvent);
128
- const toolName = (ctx.tool_name || ctx.toolName || '').toLowerCase()
129
- // Case preserved for the gate. A BLOCK: SOP compiles to a tool-name
130
- // pattern the operator wrote as they see it (Bash, Write) and this
131
- // harness lowercases the tool for its own matching. Handing the gate the
132
- // lowercased form means every SOP tool rule silently matches nothing here
133
- // while appearing active everywhere else.
134
- const rawToolName = ctx.tool_name || ctx.toolName || '';
135
- const toolInput = ctx.tool_input || ctx.toolInput || {};
136
-
137
- const targetPath = toolInput.path || toolInput.file_path || toolInput.filePath ||
138
- toolInput.new_path || toolInput.target || toolInput.notebook_path || '';
139
- const command = toolInput.command || toolInput.cmd || toolInput.script || '';
140
- intuticGate(rawToolName, targetPath, command, logEvent, _intuticWsId, toolInput);
143
+ /**
144
+ * One tool call. Returns to allow; throws '[Intutic Governance] …' to refuse.
145
+ * Params that are not an object are refused outright: the gate reads paths
146
+ * and commands out of them, and a shape it cannot read is not one it can
147
+ * allow. apply_patch names its files in derivedPaths, and each is checked.
148
+ */
149
+ function intuticEvaluate(event, ctx) {
150
+ _intuticSessionId = String((ctx && (ctx.sessionId || ctx.sessionKey)) || '');
151
+ let tool = String((event && event.toolName) || 'tool');
152
+ // OpenClaw names an MCP tool <server>__<tool>; the allowlist reads
153
+ // mcp__<server>__<tool>. No built-in tool id has a double underscore.
154
+ if (tool.indexOf('__') > 0 && tool.indexOf('mcp__') !== 0) tool = 'mcp__' + tool;
155
+ const params = event && event.params;
156
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
157
+ const reason = '[Intutic Governance] BLOCKED: tool "' + tool + '" was called with parameters the gate cannot read (' +
158
+ (params === null ? 'null' : typeof params) + '); refusing rather than allowing a call it cannot evaluate.';
159
+ try { logEvent('tool_blocked', tool, reason); } catch (e) {}
160
+ throw new Error(reason);
161
+ }
162
+ const command = params.command || params.cmd || params.script || '';
163
+ const direct = params.path || params.file_path || params.filePath || params.target || '';
164
+ const derived = event && Array.isArray(event.derivedPaths) ? event.derivedPaths.filter((p) => typeof p === 'string' && p) : [];
165
+ const targets = derived.length > 0 ? (direct ? [direct].concat(derived) : derived) : [direct];
166
+ for (const target of targets) intuticGate(tool, target, command, logEvent, _intuticWsId, params, _intuticSessionId);
167
+ logEvent('tool_allowed', tool, '');
168
+ }
141
169
 
142
- logEvent('tool_allowed', toolName, '');
143
- process.exit(0);
170
+ /** Fail closed: a fault inside the gate is a refusal, never an allow. */
171
+ function beforeToolCall(event, ctx) {
172
+ try {
173
+ intuticEvaluate(event, ctx);
174
+ return undefined;
144
175
  } catch (err) {
145
- // Fail CLOSED — any hook execution error blocks the tool call.
146
- console.error('[Intutic Governance] Hook error (blocking for safety):', err);
147
- logEvent('tool_blocked', 'unknown', String(err));
148
- process.exit(2);
176
+ let reason = String((err && err.message) || err);
177
+ if (reason.indexOf('[Intutic Governance]') !== 0) {
178
+ reason = '[Intutic Governance] BLOCKED: gate fault (failing closed): ' + reason;
179
+ try { logEvent('tool_blocked', String((event && event.toolName) || 'unknown'), reason); } catch (e) {}
180
+ }
181
+ return { block: true, blockReason: reason };
149
182
  }
150
- });
183
+ }
184
+
185
+ module.exports = {
186
+ id: ${JSON.stringify(OPENCLAW_PLUGIN_ID)},
187
+ name: 'Intutic governance',
188
+ description: 'Refuses tool calls that break the workspace policy before they run.',
189
+ register(api) {
190
+ api.on('before_tool_call', beforeToolCall, { timeoutMs: ${HOOK_TIMEOUT_SECONDS * 1000} });
191
+ },
192
+ };
151
193
  `;
152
194
  }
153
- // ─── JSON5 merge helpers ──────────────────────────────────────────────────────
195
+ // ─── ~/.openclaw/openclaw.json ───────────────────────────────────────────────
154
196
  /**
155
197
  * Attempt to parse a JSON5-like string by progressively stripping
156
198
  * non-standard syntax. Returns a plain JS object or throws if unparseable.
157
199
  */
158
- function parseJson5Like(raw) {
200
+ export function parseJson5Like(raw) {
159
201
  // Attempt 1: vanilla JSON
160
202
  try {
161
203
  return JSON.parse(raw);
@@ -175,98 +217,110 @@ function parseJson5Like(raw) {
175
217
  stripped = stripped.replace(/,(\s*[}\]])/g, '$1');
176
218
  return JSON.parse(stripped);
177
219
  }
220
+ const asObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) ? v : {};
178
221
  /**
179
- * Deep-merge `hooks.internal.entries["intutic-governance"]` into the parsed
180
- * openclaw config object and return the merged result.
222
+ * Adds the plugin to `plugins.load.paths`, and its id to `plugins.allow` when
223
+ * that list is non-empty (an empty or absent one allows every plugin).
224
+ * Everything else is kept, and a second run adds nothing.
181
225
  */
182
- function mergeOpenclawConfig(existing, hookScriptPath) {
183
- const hooks = existing.hooks ?? {};
184
- const internal = hooks.internal ?? {};
185
- const entries = internal.entries ?? {};
226
+ export function mergeOpenclawConfig(existing, pluginPath) {
227
+ const plugins = asObject(existing.plugins);
228
+ const load = asObject(plugins.load);
229
+ const paths = Array.isArray(load.paths) ? load.paths : [];
230
+ const allow = Array.isArray(plugins.allow) ? plugins.allow : undefined;
186
231
  return {
187
232
  ...existing,
188
- hooks: {
189
- ...hooks,
190
- internal: {
191
- ...internal,
192
- entries: {
193
- ...entries,
194
- 'intutic-governance': {
195
- enabled: true,
196
- event: 'PreToolUse',
197
- command: hookScriptPath,
198
- },
199
- },
200
- },
233
+ plugins: {
234
+ ...plugins,
235
+ ...(allow && allow.length > 0 && !allow.includes(OPENCLAW_PLUGIN_ID) ? { allow: [...allow, OPENCLAW_PLUGIN_ID] } : {}),
236
+ load: { ...load, paths: paths.includes(pluginPath) ? paths : [...paths, pluginPath] },
201
237
  },
202
238
  };
203
239
  }
240
+ /** Why the plugin will not load even once it is listed, when the config says so. */
241
+ function configBlocksPlugin(config) {
242
+ const plugins = asObject(config.plugins);
243
+ if (plugins.enabled === false)
244
+ return 'plugins.enabled is false';
245
+ if (Array.isArray(plugins.deny) && plugins.deny.includes(OPENCLAW_PLUGIN_ID))
246
+ return `plugins.deny lists ${OPENCLAW_PLUGIN_ID}`;
247
+ if (asObject(asObject(plugins.entries)[OPENCLAW_PLUGIN_ID]).enabled === false)
248
+ return `plugins.entries.${OPENCLAW_PLUGIN_ID}.enabled is false`;
249
+ return null;
250
+ }
204
251
  // ─── Main export ──────────────────────────────────────────────────────────────
205
252
  /**
206
- * Write the Openclaw governance hook script and merge hook configuration
207
- * into `~/.openclaw/openclaw.json`.
253
+ * Write the OpenClaw governance plugin and list it in
254
+ * `~/.openclaw/openclaw.json`.
208
255
  *
209
- * Safe to call repeatedly — uses atomic rename so partial writes never
210
- * leave a corrupt state.
256
+ * Safe to call repeatedly: the plugin is replaced by atomic rename and the
257
+ * config merge is idempotent. A config file that does not parse is left as
258
+ * it is, and reported.
211
259
  *
212
260
  * @param workspaceRoot - Absolute workspace root path.
213
- * @param proxyUrl - Intutic proxy URL written into hook headers.
261
+ * @param proxyUrl - Intutic proxy URL written into the plugin header.
214
262
  * @param workspaceId - Workspace ID embedded in every hook event payload.
215
263
  */
216
264
  export async function writeOpenclawHooks(workspaceRoot, proxyUrl, workspaceId = '') {
217
- // ── 1. Ensure directories ──────────────────────────────────────────────────
218
- const globalHookDir = path.join(os.homedir(), '.intutic', 'hooks');
219
- const hookEventsDir = path.join(workspaceRoot, '.intutic', 'events');
220
- const envDir = path.join(workspaceRoot, '.intutic', 'env');
221
- const openclawDir = path.join(os.homedir(), '.openclaw');
222
- await Promise.all([
223
- fs.mkdir(globalHookDir, { recursive: true }),
224
- fs.mkdir(hookEventsDir, { recursive: true }),
225
- fs.mkdir(envDir, { recursive: true }),
226
- fs.mkdir(openclawDir, { recursive: true }),
227
- ]);
228
- // ── 2. Write Node.js hook script ──────────────────────────────────────────
229
- const hookScriptPath = path.join(globalHookDir, 'openclaw-check.js');
230
- const hookEventsLog = path.join(hookEventsDir, 'hook-events.jsonl');
231
- const tmpScript = hookScriptPath + '.intutic-tmp';
232
- await fs.writeFile(tmpScript, buildOpenclawCheckScript(hookEventsLog, workspaceId, proxyUrl), 'utf-8');
233
- await fs.rename(tmpScript, hookScriptPath);
234
- await fs.chmod(hookScriptPath, 0o755);
235
- log.info({ action: 'openclaw_hook_script_written', path: hookScriptPath }, 'Openclaw hook script written');
236
- // ── 3. JSON5-safe merge into ~/.openclaw/openclaw.json ────────────────────
237
- let existingConfig = {};
265
+ await fs.mkdir(path.join(workspaceRoot, '.intutic', 'events'), { recursive: true });
266
+ const pluginPath = openclawPluginPath();
267
+ await writeOwnedFile(pluginPath, workspaceRoot, buildOpenclawPlugin(proxyUrl, workspaceRoot, workspaceId));
268
+ log.info({ action: 'openclaw_plugin_written', path: pluginPath }, 'OpenClaw governance plugin written');
269
+ const configPath = openclawConfigPath();
270
+ let raw = null;
238
271
  try {
239
- const raw = await fs.readFile(OPENCLAW_CONFIG, 'utf-8');
240
- existingConfig = parseJson5Like(raw);
272
+ raw = await fs.readFile(configPath, 'utf-8');
241
273
  }
242
- catch {
243
- // File doesn't exist or is unparseable — start fresh
274
+ catch (err) {
275
+ if (err.code !== 'ENOENT') {
276
+ log.warn({ action: 'config_merge_skipped', path: configPath, error: String(err) }, `Could not read ${configPath} — left untouched`);
277
+ return;
278
+ }
244
279
  }
245
- const mergedConfig = mergeOpenclawConfig(existingConfig, hookScriptPath);
246
- const tmpConfig = OPENCLAW_CONFIG + '.intutic-tmp';
247
- await fs.writeFile(tmpConfig, JSON.stringify(mergedConfig, null, 2) + '\n', 'utf-8');
248
- await fs.rename(tmpConfig, OPENCLAW_CONFIG);
249
- log.info({ action: 'openclaw_config_merged', path: OPENCLAW_CONFIG }, 'Merged Intutic hook into ~/.openclaw/openclaw.json');
250
- // ── 4. Write env snippet ───────────────────────────────────────────────────
251
- const envSnippet = [
252
- `# Intutic openclaw governance env`,
253
- `INTUTIC_OPENCLAW_HOOK=~/.intutic/hooks/openclaw-check.js`,
254
- `INTUTIC_OPENCLAW_CONFIG=~/.openclaw/openclaw.json`,
255
- `# Validate: openclaw hooks check`,
256
- ].join('\n') + '\n';
257
- const envFilePath = path.join(envDir, 'openclaw.env');
258
- const tmpEnv = envFilePath + '.intutic-tmp';
259
- await fs.writeFile(tmpEnv, envSnippet, 'utf-8');
260
- await fs.rename(tmpEnv, envFilePath);
261
- log.info({ action: 'openclaw_env_written', path: envFilePath }, 'Openclaw env snippet written');
262
- // ── 5. Optional validation (non-fatal) ────────────────────────────────────
280
+ let existing = {};
281
+ if (raw !== null && raw.trim() !== '') {
282
+ try {
283
+ existing = asObject(parseJson5Like(raw));
284
+ }
285
+ catch {
286
+ log.warn({ action: 'config_merge_skipped', path: configPath }, `${configPath} does not parse — left untouched; fix it and the next sync will list the Intutic plugin`);
287
+ return;
288
+ }
289
+ }
290
+ const blocked = configBlocksPlugin(existing);
291
+ if (blocked) {
292
+ log.warn({ action: 'openclaw_plugin_disabled', path: configPath, reason: blocked }, `OpenClaw will not load the Intutic plugin: ${blocked}`);
293
+ }
294
+ await keepOriginal(configPath, workspaceRoot);
295
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
296
+ const tmp = configPath + '.intutic-tmp';
297
+ await fs.writeFile(tmp, JSON.stringify(mergeOpenclawConfig(existing, pluginPath), null, 2) + '\n', 'utf-8');
298
+ await fs.rename(tmp, configPath);
299
+ log.info({ action: 'openclaw_config_merged', path: configPath }, 'Listed the Intutic plugin in ~/.openclaw/openclaw.json');
300
+ }
301
+ /**
302
+ * OpenClaw's agent workspace, where it loads `AGENTS.md` and its other
303
+ * bootstrap files every session — never from a project:
304
+ * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`, else
305
+ * `OPENCLAW_WORKSPACE_DIR`, else `~/.openclaw/workspace`
306
+ * (https://github.com/openclaw/openclaw/blob/98457908cf3e4fbbd0354b530e0aa0d15b560158/docs/gateway/config-agents/workspace-and-bootstrap.md).
307
+ */
308
+ export async function openclawAgentWorkspace() {
309
+ const home = os.homedir();
310
+ const expand = (p) => (p === '~' || p.startsWith('~/') ? path.join(home, p.slice(1)) : path.resolve(p));
263
311
  try {
264
- const { stdout, stderr } = await execAsync('openclaw hooks check', { timeout: 5000 });
265
- log.info({ action: 'openclaw_hooks_check', stdout: stdout.trim(), stderr: stderr.trim() }, 'openclaw hooks check passed');
312
+ const doc = parseJson5Like(await fs.readFile(path.join(home, '.openclaw', 'openclaw.json'), 'utf-8'));
313
+ const agents = doc.agents;
314
+ const configured = agents?.defaults?.workspace;
315
+ if (typeof configured === 'string' && configured.trim() !== '')
316
+ return expand(configured.trim());
266
317
  }
267
- catch (err) {
268
- // Not installed or check failed — non-fatal
269
- log.debug({ action: 'openclaw_hooks_check_skipped', err }, 'openclaw hooks check skipped (binary not in PATH or check failed)');
318
+ catch {
319
+ // No config, or one that does not parse: the defaults apply.
270
320
  }
321
+ const fromEnv = process.env.OPENCLAW_WORKSPACE_DIR;
322
+ if (fromEnv)
323
+ return expand(fromEnv);
324
+ return path.join(home, '.openclaw', 'workspace');
271
325
  }
272
326
  //# sourceMappingURL=openclawHooks.js.map