@intutic/sync-daemon 1.6.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/brainIndexer.d.ts +51 -0
  4. package/dist/brainIndexer.d.ts.map +1 -0
  5. package/dist/brainIndexer.js +148 -0
  6. package/dist/brainIndexer.js.map +1 -0
  7. package/dist/configReader.d.ts +46 -0
  8. package/dist/configReader.d.ts.map +1 -0
  9. package/dist/configReader.js +133 -0
  10. package/dist/configReader.js.map +1 -0
  11. package/dist/configWriter.d.ts +53 -0
  12. package/dist/configWriter.d.ts.map +1 -0
  13. package/dist/configWriter.js +503 -0
  14. package/dist/configWriter.js.map +1 -0
  15. package/dist/harness/aiderConfigMerger.d.ts +27 -0
  16. package/dist/harness/aiderConfigMerger.d.ts.map +1 -0
  17. package/dist/harness/aiderConfigMerger.js +139 -0
  18. package/dist/harness/aiderConfigMerger.js.map +1 -0
  19. package/dist/harness/antigravityHooks.d.ts +43 -0
  20. package/dist/harness/antigravityHooks.d.ts.map +1 -0
  21. package/dist/harness/antigravityHooks.js +193 -0
  22. package/dist/harness/antigravityHooks.js.map +1 -0
  23. package/dist/harness/claudeCodeHooks.d.ts +59 -0
  24. package/dist/harness/claudeCodeHooks.d.ts.map +1 -0
  25. package/dist/harness/claudeCodeHooks.js +414 -0
  26. package/dist/harness/claudeCodeHooks.js.map +1 -0
  27. package/dist/harness/claudeDesktopHooks.d.ts +30 -0
  28. package/dist/harness/claudeDesktopHooks.d.ts.map +1 -0
  29. package/dist/harness/claudeDesktopHooks.js +267 -0
  30. package/dist/harness/claudeDesktopHooks.js.map +1 -0
  31. package/dist/harness/clineHooks.d.ts +23 -0
  32. package/dist/harness/clineHooks.d.ts.map +1 -0
  33. package/dist/harness/clineHooks.js +169 -0
  34. package/dist/harness/clineHooks.js.map +1 -0
  35. package/dist/harness/continueHooks.d.ts +33 -0
  36. package/dist/harness/continueHooks.d.ts.map +1 -0
  37. package/dist/harness/continueHooks.js +124 -0
  38. package/dist/harness/continueHooks.js.map +1 -0
  39. package/dist/harness/cursorHooks.d.ts +27 -0
  40. package/dist/harness/cursorHooks.d.ts.map +1 -0
  41. package/dist/harness/cursorHooks.js +221 -0
  42. package/dist/harness/cursorHooks.js.map +1 -0
  43. package/dist/harness/cursorHooksJson.d.ts +29 -0
  44. package/dist/harness/cursorHooksJson.d.ts.map +1 -0
  45. package/dist/harness/cursorHooksJson.js +130 -0
  46. package/dist/harness/cursorHooksJson.js.map +1 -0
  47. package/dist/harness/gooseHardener.d.ts +39 -0
  48. package/dist/harness/gooseHardener.d.ts.map +1 -0
  49. package/dist/harness/gooseHardener.js +152 -0
  50. package/dist/harness/gooseHardener.js.map +1 -0
  51. package/dist/harness/gooseHooks.d.ts +24 -0
  52. package/dist/harness/gooseHooks.d.ts.map +1 -0
  53. package/dist/harness/gooseHooks.js +175 -0
  54. package/dist/harness/gooseHooks.js.map +1 -0
  55. package/dist/harness/gooseHooksWriter.d.ts +27 -0
  56. package/dist/harness/gooseHooksWriter.d.ts.map +1 -0
  57. package/dist/harness/gooseHooksWriter.js +147 -0
  58. package/dist/harness/gooseHooksWriter.js.map +1 -0
  59. package/dist/harness/hermesHooks.d.ts +31 -0
  60. package/dist/harness/hermesHooks.d.ts.map +1 -0
  61. package/dist/harness/hermesHooks.js +245 -0
  62. package/dist/harness/hermesHooks.js.map +1 -0
  63. package/dist/harness/mcpAutoWrite.d.ts +35 -0
  64. package/dist/harness/mcpAutoWrite.d.ts.map +1 -0
  65. package/dist/harness/mcpAutoWrite.js +334 -0
  66. package/dist/harness/mcpAutoWrite.js.map +1 -0
  67. package/dist/harness/n8nHooks.d.ts +28 -0
  68. package/dist/harness/n8nHooks.d.ts.map +1 -0
  69. package/dist/harness/n8nHooks.js +205 -0
  70. package/dist/harness/n8nHooks.js.map +1 -0
  71. package/dist/harness/openWebuiHooks.d.ts +31 -0
  72. package/dist/harness/openWebuiHooks.d.ts.map +1 -0
  73. package/dist/harness/openWebuiHooks.js +166 -0
  74. package/dist/harness/openWebuiHooks.js.map +1 -0
  75. package/dist/harness/openclawHooks.d.ts +34 -0
  76. package/dist/harness/openclawHooks.d.ts.map +1 -0
  77. package/dist/harness/openclawHooks.js +296 -0
  78. package/dist/harness/openclawHooks.js.map +1 -0
  79. package/dist/harness/openhandsHooks.d.ts +24 -0
  80. package/dist/harness/openhandsHooks.d.ts.map +1 -0
  81. package/dist/harness/openhandsHooks.js +178 -0
  82. package/dist/harness/openhandsHooks.js.map +1 -0
  83. package/dist/harness/openhandsHooksWriter.d.ts +23 -0
  84. package/dist/harness/openhandsHooksWriter.d.ts.map +1 -0
  85. package/dist/harness/openhandsHooksWriter.js +146 -0
  86. package/dist/harness/openhandsHooksWriter.js.map +1 -0
  87. package/dist/harness/piHooks.d.ts +32 -0
  88. package/dist/harness/piHooks.d.ts.map +1 -0
  89. package/dist/harness/piHooks.js +257 -0
  90. package/dist/harness/piHooks.js.map +1 -0
  91. package/dist/harness/rooCodeHooks.d.ts +31 -0
  92. package/dist/harness/rooCodeHooks.d.ts.map +1 -0
  93. package/dist/harness/rooCodeHooks.js +300 -0
  94. package/dist/harness/rooCodeHooks.js.map +1 -0
  95. package/dist/harness/windsurfHooks.d.ts +27 -0
  96. package/dist/harness/windsurfHooks.d.ts.map +1 -0
  97. package/dist/harness/windsurfHooks.js +202 -0
  98. package/dist/harness/windsurfHooks.js.map +1 -0
  99. package/dist/hashReporter.d.ts +33 -0
  100. package/dist/hashReporter.d.ts.map +1 -0
  101. package/dist/hashReporter.js +69 -0
  102. package/dist/hashReporter.js.map +1 -0
  103. package/dist/index.d.ts +43 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +43 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/integrityStore.d.ts +43 -0
  108. package/dist/integrityStore.d.ts.map +1 -0
  109. package/dist/integrityStore.js +94 -0
  110. package/dist/integrityStore.js.map +1 -0
  111. package/dist/lib/complianceProbes.d.ts +39 -0
  112. package/dist/lib/complianceProbes.d.ts.map +1 -0
  113. package/dist/lib/complianceProbes.js +115 -0
  114. package/dist/lib/complianceProbes.js.map +1 -0
  115. package/dist/lib/processPoller.d.ts +2 -0
  116. package/dist/lib/processPoller.d.ts.map +1 -0
  117. package/dist/lib/processPoller.js +87 -0
  118. package/dist/lib/processPoller.js.map +1 -0
  119. package/dist/lib/runtimeEnv.d.ts +49 -0
  120. package/dist/lib/runtimeEnv.d.ts.map +1 -0
  121. package/dist/lib/runtimeEnv.js +70 -0
  122. package/dist/lib/runtimeEnv.js.map +1 -0
  123. package/dist/skillWriter.d.ts +24 -0
  124. package/dist/skillWriter.d.ts.map +1 -0
  125. package/dist/skillWriter.js +112 -0
  126. package/dist/skillWriter.js.map +1 -0
  127. package/dist/syncLoop.d.ts +70 -0
  128. package/dist/syncLoop.d.ts.map +1 -0
  129. package/dist/syncLoop.js +546 -0
  130. package/dist/syncLoop.js.map +1 -0
  131. package/dist/trajectoryMonitor.d.ts +126 -0
  132. package/dist/trajectoryMonitor.d.ts.map +1 -0
  133. package/dist/trajectoryMonitor.js +291 -0
  134. package/dist/trajectoryMonitor.js.map +1 -0
  135. package/dist/watcher/driftWatcher.d.ts +25 -0
  136. package/dist/watcher/driftWatcher.d.ts.map +1 -0
  137. package/dist/watcher/driftWatcher.js +72 -0
  138. package/dist/watcher/driftWatcher.js.map +1 -0
  139. package/dist/watcher/settingsGuard.d.ts +32 -0
  140. package/dist/watcher/settingsGuard.d.ts.map +1 -0
  141. package/dist/watcher/settingsGuard.js +250 -0
  142. package/dist/watcher/settingsGuard.js.map +1 -0
  143. package/dist/wsClient.d.ts +49 -0
  144. package/dist/wsClient.d.ts.map +1 -0
  145. package/dist/wsClient.js +149 -0
  146. package/dist/wsClient.js.map +1 -0
  147. package/package.json +96 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * openhandsHooks.ts — OpenHands .openhands/hooks.json injection
3
+ * and llm.base_url proxy write.
4
+ *
5
+ * OpenHands supports PreToolUse, Stop, SessionStart hooks via
6
+ * a JSON file at .openhands/hooks.json in the repository root.
7
+ * The hooks file lives within the agent's sandbox working directory,
8
+ * so we also register it with the drift watcher.
9
+ *
10
+ * LLD #14 — Phase 3 cross-harness defence
11
+ * HLD §3.14 — Three-Tier Defense Cascade
12
+ *
13
+ * @module
14
+ */
15
+ import * as fs from 'node:fs/promises';
16
+ import * as path from 'node:path';
17
+ import { createLogger } from '@intutic/logger';
18
+ import { newIso } from '@intutic/id';
19
+ const log = createLogger('sync-openhands-hooks');
20
+ const PROTECTED_PATHS = [
21
+ '.openhands/hooks.json',
22
+ '.intutic/hooks',
23
+ '.intutic/integrity.json',
24
+ '.claude/settings.json',
25
+ '.cursor/hooks.json',
26
+ '.codeium/windsurf/hooks.json',
27
+ '.cline/hooks',
28
+ '.agents/plugins/intutic-governance',
29
+ ];
30
+ /**
31
+ * Write .openhands/hooks.json and the pre-tool-check shell script.
32
+ * Also merges llm.base_url into config.toml if it exists.
33
+ *
34
+ * @param workspaceRoot - Absolute workspace root.
35
+ * @param proxyUrl - Intutic proxy URL.
36
+ * @param workspaceId - Workspace ID embedded in hook event payloads.
37
+ */
38
+ export async function writeOpenHandsHooks(workspaceRoot, proxyUrl, workspaceId = '') {
39
+ const openhandsDir = path.join(workspaceRoot, '.openhands');
40
+ await fs.mkdir(openhandsDir, { recursive: true });
41
+ const hookScriptDir = path.join(workspaceRoot, '.intutic', 'hooks');
42
+ await fs.mkdir(hookScriptDir, { recursive: true });
43
+ const hookScriptPath = path.join(hookScriptDir, 'openhands-check.sh');
44
+ // Ensure hook events log directory exists
45
+ const hookEventsLog = path.join(workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
46
+ await fs.mkdir(path.join(workspaceRoot, '.intutic', 'events'), { recursive: true });
47
+ // ── Shell hook script ──────────────────────────────────────────────
48
+ // OpenHands passes tool name and input as env vars + JSON body.
49
+ // Exits 2 to block, 0 to allow.
50
+ const hookScript = `#!/usr/bin/env bash
51
+ # Intutic OpenHands PreToolUse governance gate.
52
+ # Auto-generated by intutic sync-daemon. DO NOT EDIT.
53
+ # Proxy: ${proxyUrl}
54
+ # Generated: ${newIso()}
55
+ set -euo pipefail
56
+
57
+ INPUT="$(cat)"
58
+ TOOL_NAME="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || true)"
59
+ TARGET_PATH="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); i=d.get('tool_input',{}); print(i.get('path',i.get('file_path','')))" 2>/dev/null || true)"
60
+
61
+ HOOK_EVENTS_LOG=${JSON.stringify(hookEventsLog)}
62
+
63
+ # ── Runtime credentials (sourced from ~/.intutic/env/runtime.env) ────────────────────────
64
+ RUNTIME_ENV="$HOME/.intutic/env/runtime.env"
65
+ INTUITIC_HOST="https://api.intutic.ai"
66
+ INTUITIC_API_KEY=""
67
+ INTUITIC_WORKSPACE_ID=${JSON.stringify(workspaceId)}
68
+ [ -f "$RUNTIME_ENV" ] && source "$RUNTIME_ENV" 2>/dev/null || true
69
+
70
+ log_event() {
71
+ local verdict="\$1" tool="\$2" reason="\$3"
72
+ local ts; ts="\$(date -u +%Y-%m-%dT%H:%M:%SZ)"
73
+ local incident_id; incident_id="\$(printf '%s' "\${ts}\${tool}\${INTUTIC_WORKSPACE_ID}" | sha1sum 2>/dev/null | cut -c1-16 || echo \"\$(date +%s)\")"
74
+ local entry="{\"event\":\"\${verdict}\",\"toolName\":\"\${tool}\",\"reason\":\"\${reason}\",\"workspaceId\":\"\${INTUTIC_WORKSPACE_ID}\",\"harnessType\":\"openhands\",\"timestamp\":\"\${ts}\",\"incidentId\":\"\${incident_id}\"}"
75
+ # Path B: reliable file append
76
+ printf '%s\\n' "\$entry" >> "\$HOOK_EVENTS_LOG" 2>/dev/null || true
77
+ # Path A: fire-and-forget HTTP (non-blocking, near-real-time)
78
+ if [ -n "\$INTUTIC_API_KEY" ]; then
79
+ curl -s -o /dev/null --max-time 3 -X POST \\
80
+ -H "Content-Type: application/json" \\
81
+ -H "Authorization: Bearer \$INTUTIC_API_KEY" \\
82
+ -d "{\"events\":[\$entry]}" \\
83
+ "\${INTUTIC_HOST}/api/v1/hook-events" & disown 2>/dev/null || true
84
+ fi
85
+ }
86
+
87
+ PROTECTED_PATHS=(
88
+ ".openhands/hooks.json"
89
+ ".intutic/hooks"
90
+ ".intutic/integrity.json"
91
+ ".claude/settings.json"
92
+ ".cursor/hooks.json"
93
+ ".codeium/windsurf/hooks.json"
94
+ ".cline/hooks"
95
+ ".agents/plugins/intutic-governance"
96
+ )
97
+
98
+ # Protected-path guard
99
+ for p in "\${PROTECTED_PATHS[@]}"; do
100
+ if echo "$TARGET_PATH" | grep -qF "$p"; then
101
+ REASON="Attempt to modify governance-protected path \\"$TARGET_PATH\\""
102
+ echo "[Intutic Governance] BLOCKED: $REASON" >&2
103
+ log_event "tool_blocked" "\$TOOL_NAME" "\$REASON"
104
+ exit 2
105
+ fi
106
+ done
107
+
108
+ log_event "tool_allowed" "\$TOOL_NAME" ""
109
+ exit 0
110
+ `;
111
+ const tmpScript = hookScriptPath + '.oh-tmp';
112
+ await fs.writeFile(tmpScript, hookScript, 'utf-8');
113
+ await fs.rename(tmpScript, hookScriptPath);
114
+ await fs.chmod(hookScriptPath, 0o755);
115
+ // ── .openhands/hooks.json ──────────────────────────────────────────
116
+ const hooksConfig = {
117
+ _comment: 'Intutic governance hooks — auto-generated. DO NOT EDIT.',
118
+ _lastSync: newIso(),
119
+ hooks: [
120
+ {
121
+ type: 'PreToolUse',
122
+ command: hookScriptPath,
123
+ failClosed: true,
124
+ },
125
+ {
126
+ type: 'Stop',
127
+ command: `${hookScriptPath} --event stop`,
128
+ failClosed: false,
129
+ },
130
+ ],
131
+ };
132
+ const hooksPath = path.join(openhandsDir, 'hooks.json');
133
+ const tmpHooks = hooksPath + '.intutic-tmp';
134
+ await fs.writeFile(tmpHooks, JSON.stringify(hooksConfig, null, 2) + '\n', 'utf-8');
135
+ await fs.rename(tmpHooks, hooksPath);
136
+ log.info({ action: 'openhands_hooks_written', path: hooksPath }, 'OpenHands hooks written');
137
+ // ── config.toml llm.base_url injection ────────────────────────────
138
+ await mergeOpenHandsConfig(workspaceRoot, proxyUrl);
139
+ }
140
+ /**
141
+ * Merge llm.base_url into OpenHands config.toml.
142
+ * Looks for the file in the workspace root and the user config directory.
143
+ */
144
+ async function mergeOpenHandsConfig(workspaceRoot, proxyUrl) {
145
+ const candidates = [
146
+ path.join(workspaceRoot, 'config.toml'),
147
+ path.join(process.env.HOME ?? '', '.openhands', 'config.toml'),
148
+ ];
149
+ for (const configPath of candidates) {
150
+ let existing = '';
151
+ try {
152
+ existing = await fs.readFile(configPath, 'utf-8');
153
+ }
154
+ catch {
155
+ // File doesn't exist — create minimal config
156
+ existing = '';
157
+ }
158
+ // Inject or update [llm] section with base_url
159
+ if (existing.includes('[llm]')) {
160
+ // Update existing [llm] section
161
+ if (existing.includes('base_url')) {
162
+ existing = existing.replace(/^base_url\s*=\s*.*/m, `base_url = "${proxyUrl}"`);
163
+ }
164
+ else {
165
+ existing = existing.replace(/\[llm\]/, `[llm]\nbase_url = "${proxyUrl}"`);
166
+ }
167
+ }
168
+ else {
169
+ existing += `\n[llm]\nbase_url = "${proxyUrl}"\n`;
170
+ }
171
+ const tmpConfig = configPath + '.intutic-tmp';
172
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
173
+ await fs.writeFile(tmpConfig, existing, 'utf-8');
174
+ await fs.rename(tmpConfig, configPath);
175
+ log.info({ action: 'openhands_config_written', path: configPath }, 'OpenHands config.toml updated with proxy base_url');
176
+ }
177
+ }
178
+ //# sourceMappingURL=openhandsHooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openhandsHooks.js","sourceRoot":"","sources":["../../src/harness/openhandsHooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * openhandsHooksWriter.ts — OpenHands PreToolUse native hook writer.
3
+ *
4
+ * Writes two artefacts:
5
+ * 1. `~/.intutic/hooks/pre-tool-check.sh` — shared bash gate script that
6
+ * sources runtime credentials, POSTs to the Intutic hook-gate endpoint,
7
+ * exits 2 on block (fail-open on curl error). Sets HARNESS=openhands in
8
+ * the request body so the control plane can route correctly.
9
+ * 2. `<workspaceRoot>/.openhands/hooks.json` — registers the script as the
10
+ * PreToolUse handler with a 10-second timeout.
11
+ *
12
+ * LLD #14 — Phase 4 WS-4C native hook script writers
13
+ * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
14
+ *
15
+ * @module
16
+ */
17
+ /**
18
+ * Write OpenHands PreToolUse hook script and hooks.json.
19
+ *
20
+ * @param workspaceRoot - Absolute path to the workspace root.
21
+ */
22
+ export declare function writeOpenHandsHooks(workspaceRoot: string): Promise<void>;
23
+ //# sourceMappingURL=openhandsHooksWriter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openhandsHooksWriter.d.ts","sourceRoot":"","sources":["../../src/harness/openhandsHooksWriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA+GH;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B9E"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * openhandsHooksWriter.ts — OpenHands PreToolUse native hook writer.
3
+ *
4
+ * Writes two artefacts:
5
+ * 1. `~/.intutic/hooks/pre-tool-check.sh` — shared bash gate script that
6
+ * sources runtime credentials, POSTs to the Intutic hook-gate endpoint,
7
+ * exits 2 on block (fail-open on curl error). Sets HARNESS=openhands in
8
+ * the request body so the control plane can route correctly.
9
+ * 2. `<workspaceRoot>/.openhands/hooks.json` — registers the script as the
10
+ * PreToolUse handler with a 10-second timeout.
11
+ *
12
+ * LLD #14 — Phase 4 WS-4C native hook script writers
13
+ * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
14
+ *
15
+ * @module
16
+ */
17
+ import * as node_fs from 'node:fs/promises';
18
+ import * as node_path from 'node:path';
19
+ import * as node_os from 'node:os';
20
+ import { createLogger } from '@intutic/logger';
21
+ const log = createLogger('sync-openhands-hooks-writer');
22
+ const HOOKS_DIR = node_path.join(node_os.homedir(), '.intutic', 'hooks');
23
+ const EVENTS_DIR = node_path.join(node_os.homedir(), '.intutic', 'events');
24
+ const HOOK_SCRIPT = node_path.join(HOOKS_DIR, 'pre-tool-check.sh');
25
+ const HOOK_EVENTS_LOG = node_path.join(EVENTS_DIR, 'hook-events.jsonl');
26
+ /**
27
+ * Build the shared bash gate script.
28
+ *
29
+ * The script is designed to be shared across harnesses that invoke a bash
30
+ * pre-tool-use hook. It sets HARNESS=openhands in the POST body so the
31
+ * control-plane can distinguish the origin.
32
+ *
33
+ * Logic:
34
+ * 1. Source ~/.intutic/env/runtime.env for credentials.
35
+ * 2. Read full stdin JSON payload.
36
+ * 3. Merge HARNESS=openhands into the payload.
37
+ * 4. POST to $INTUTIC_CONTROL_PLANE_URL/api/v1/hook-gate (10 s timeout).
38
+ * 5. Fail-open if curl returns an error or no API key is set.
39
+ * 6. Exit 2 + append JSONL event if `"allowed":false`.
40
+ * 7. Exit 0 otherwise.
41
+ */
42
+ function buildPreToolCheckScript() {
43
+ return `#!/usr/bin/env bash
44
+ # Intutic shared pre-tool-check governance gate.
45
+ # Auto-generated by intutic sync-daemon. DO NOT EDIT.
46
+ # Used by: OpenHands (and any other bash-based hook harness).
47
+ set -euo pipefail
48
+
49
+ HARNESS="openhands"
50
+ RUNTIME_ENV="$HOME/.intutic/env/runtime.env"
51
+ HOOK_EVENTS_LOG=${JSON.stringify(HOOK_EVENTS_LOG)}
52
+ INTUTIC_CONTROL_PLANE_URL="https://api.intutic.ai"
53
+ INTUTIC_API_KEY=""
54
+ INTUTIC_WORKSPACE_ID=""
55
+
56
+ # Source runtime credentials (key rotation is automatic on next tool call)
57
+ [ -f "$RUNTIME_ENV" ] && source "$RUNTIME_ENV" 2>/dev/null || true
58
+
59
+ INPUT="$(cat)"
60
+ TOOL_NAME="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || true)"
61
+
62
+ # Augment body with harness identifier
63
+ BODY="$(echo "$INPUT" | python3 -c "
64
+ import sys, json
65
+ d = json.load(sys.stdin)
66
+ d['harness'] = '$HARNESS'
67
+ d['workspaceId'] = '$INTUTIC_WORKSPACE_ID'
68
+ print(json.dumps(d))
69
+ " 2>/dev/null || echo "$INPUT")"
70
+
71
+ # ── POST to hook-gate ─────────────────────────────────────────────────
72
+ GATE_URL="\${INTUTIC_CONTROL_PLANE_URL}/api/v1/hook-gate"
73
+ RESPONSE=""
74
+ if [ -n "$INTUTIC_API_KEY" ]; then
75
+ RESPONSE="$(curl -s --max-time 10 -X POST \\
76
+ -H "Content-Type: application/json" \\
77
+ -H "Authorization: Bearer $INTUTIC_API_KEY" \\
78
+ -d "$BODY" \\
79
+ "$GATE_URL" 2>/dev/null)" || true
80
+ fi
81
+
82
+ # Fail-open: empty response (curl error / no key) → allow
83
+ if [ -z "$RESPONSE" ]; then
84
+ exit 0
85
+ fi
86
+
87
+ # Parse decision
88
+ ALLOWED="$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d.get('allowed', True)).lower())" 2>/dev/null || echo "true")"
89
+
90
+ if [ "$ALLOWED" = "false" ]; then
91
+ REASON="$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('reason',''))" 2>/dev/null || true)"
92
+ TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
93
+ INCIDENT_ID="$(printf '%s' "\${TS}\${TOOL_NAME}\${INTUTIC_WORKSPACE_ID}" | sha1sum 2>/dev/null | cut -c1-16 || echo "$(date +%s)")"
94
+ ENTRY="{\\"event\\":\\"tool_blocked\\",\\"toolName\\":\\"\${TOOL_NAME}\\",\\"reason\\":\\"\${REASON}\\",\\"workspaceId\\":\\"\${INTUTIC_WORKSPACE_ID}\\",\\"harnessType\\":\\"\${HARNESS}\\",\\"timestamp\\":\\"\${TS}\\",\\"incidentId\\":\\"\${INCIDENT_ID}\\"}"
95
+ mkdir -p "$(dirname "$HOOK_EVENTS_LOG")"
96
+ printf '%s\\n' "$ENTRY" >> "$HOOK_EVENTS_LOG" 2>/dev/null || true
97
+ echo "[Intutic Governance] BLOCKED: $REASON" >&2
98
+ exit 2
99
+ fi
100
+
101
+ exit 0
102
+ `;
103
+ }
104
+ /**
105
+ * Build the .openhands/hooks.json document that registers the PreToolUse hook.
106
+ *
107
+ * @param hookScriptPath - Absolute path to the bash gate script.
108
+ */
109
+ function buildOpenHandsHooksJson(hookScriptPath) {
110
+ return {
111
+ _comment: 'Intutic governance hooks — auto-generated. DO NOT EDIT.',
112
+ hooks: [
113
+ {
114
+ type: 'PreToolUse',
115
+ command: hookScriptPath,
116
+ timeout: 10,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ /**
122
+ * Write OpenHands PreToolUse hook script and hooks.json.
123
+ *
124
+ * @param workspaceRoot - Absolute path to the workspace root.
125
+ */
126
+ export async function writeOpenHandsHooks(workspaceRoot) {
127
+ // ── Ensure directories exist ─────────────────────────────────────────
128
+ await node_fs.mkdir(HOOKS_DIR, { recursive: true });
129
+ await node_fs.mkdir(EVENTS_DIR, { recursive: true });
130
+ // ── 1. Write shared bash gate script ────────────────────────────────
131
+ const tmpScript = HOOK_SCRIPT + '.intutic-tmp';
132
+ await node_fs.writeFile(tmpScript, buildPreToolCheckScript(), 'utf-8');
133
+ await node_fs.rename(tmpScript, HOOK_SCRIPT);
134
+ await node_fs.chmod(HOOK_SCRIPT, 0o755);
135
+ log.info({ action: 'openhands_hook_script_written', path: HOOK_SCRIPT }, 'Shared pre-tool-check.sh written');
136
+ // ── 2. Write .openhands/hooks.json ──────────────────────────────────
137
+ const openhandsDir = node_path.join(workspaceRoot, '.openhands');
138
+ await node_fs.mkdir(openhandsDir, { recursive: true });
139
+ const hooksJsonPath = node_path.join(openhandsDir, 'hooks.json');
140
+ const hooksDoc = buildOpenHandsHooksJson(HOOK_SCRIPT);
141
+ const tmpHooks = hooksJsonPath + '.intutic-tmp';
142
+ await node_fs.writeFile(tmpHooks, JSON.stringify(hooksDoc, null, 2) + '\n', 'utf-8');
143
+ await node_fs.rename(tmpHooks, hooksJsonPath);
144
+ log.info({ action: 'openhands_hooks_json_written', path: hooksJsonPath }, 'OpenHands hooks.json written');
145
+ }
146
+ //# sourceMappingURL=openhandsHooksWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openhandsHooksWriter.js","sourceRoot":"","sources":["../../src/harness/openhandsHooksWriter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * piHooks.ts — Pi (earendil-works/pi) coding agent governance hook injection.
3
+ *
4
+ * Pi uses:
5
+ * - `~/.pi/hooks.json` for PreToolUse hook matchers (stdin JSON, exit 2 = block)
6
+ * - `~/.pi/models.json` for per-provider base URL routing
7
+ *
8
+ * This module:
9
+ * 1. Writes/merges `~/.pi/hooks.json` with intutic-governance hook entries.
10
+ * 2. Writes/merges `~/.pi/models.json` to redirect LLM providers via proxyUrl.
11
+ * 3. Writes `~/.intutic/hooks/pi-check.sh` — dual-path bash governance hook.
12
+ * 4. Writes `.intutic/env/pi.env` — env snippet for IDE/shell integration.
13
+ *
14
+ * WS-C3 — Pi harness (Phase 3 cross-harness defence)
15
+ * LLD #14 — Phase 3 cross-harness defence (Gap 3)
16
+ * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * Write the Pi governance hook script, merge hooks.json and models.json,
22
+ * and write the env snippet.
23
+ *
24
+ * Safe to call repeatedly — uses atomic rename so partial writes never
25
+ * leave a corrupt state.
26
+ *
27
+ * @param workspaceRoot - Absolute workspace root path.
28
+ * @param proxyUrl - Intutic proxy URL for LLM provider routing.
29
+ * @param workspaceId - Workspace ID embedded in every hook event payload.
30
+ */
31
+ export declare function writePiHooks(workspaceRoot: string, proxyUrl: string, workspaceId?: string): Promise<void>;
32
+ //# sourceMappingURL=piHooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piHooks.d.ts","sourceRoot":"","sources":["../../src/harness/piHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA2NH;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CAuFf"}
@@ -0,0 +1,257 @@
1
+ /**
2
+ * piHooks.ts — Pi (earendil-works/pi) coding agent governance hook injection.
3
+ *
4
+ * Pi uses:
5
+ * - `~/.pi/hooks.json` for PreToolUse hook matchers (stdin JSON, exit 2 = block)
6
+ * - `~/.pi/models.json` for per-provider base URL routing
7
+ *
8
+ * This module:
9
+ * 1. Writes/merges `~/.pi/hooks.json` with intutic-governance hook entries.
10
+ * 2. Writes/merges `~/.pi/models.json` to redirect LLM providers via proxyUrl.
11
+ * 3. Writes `~/.intutic/hooks/pi-check.sh` — dual-path bash governance hook.
12
+ * 4. Writes `.intutic/env/pi.env` — env snippet for IDE/shell integration.
13
+ *
14
+ * WS-C3 — Pi harness (Phase 3 cross-harness defence)
15
+ * LLD #14 — Phase 3 cross-harness defence (Gap 3)
16
+ * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
17
+ *
18
+ * @module
19
+ */
20
+ import * as fs from 'node:fs/promises';
21
+ import * as path from 'node:path';
22
+ import * as os from 'node:os';
23
+ import { createLogger } from '@intutic/logger';
24
+ import { newIso } from '@intutic/id';
25
+ const log = createLogger('sync-pi-hooks');
26
+ /** Path to the Pi hooks config. */
27
+ const PI_HOOKS_CONFIG = path.join(os.homedir(), '.pi', 'hooks.json');
28
+ /** Path to the Pi models config. */
29
+ const PI_MODELS_CONFIG = path.join(os.homedir(), '.pi', 'models.json');
30
+ /** Governance-sensitive paths that the hook gate protects. */
31
+ const PROTECTED_PATHS = [
32
+ '.gemini/settings.json',
33
+ '.intutic/hooks',
34
+ '.intutic/integrity.json',
35
+ '.intutic/events',
36
+ '.claude/settings.json',
37
+ '.cursor/hooks.json',
38
+ '.codeium/windsurf/hooks.json',
39
+ '.cline/hooks',
40
+ '.agents/plugins/intutic-governance',
41
+ '.openhands/hooks.json',
42
+ '.pi/hooks.json',
43
+ '.pi/models.json',
44
+ ];
45
+ // ─── Bash hook script template ────────────────────────────────────────────────
46
+ function buildPiCheckScript(proxyUrl, workspaceRoot, workspaceId) {
47
+ const hookEventsLog = path.join(workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
48
+ const runtimeEnv = path.join(os.homedir(), '.intutic', 'env', 'runtime.env');
49
+ return `#!/usr/bin/env bash
50
+ # Intutic Pi (earendil-works/pi) PreToolUse governance gate.
51
+ # Auto-generated by intutic sync-daemon. DO NOT EDIT.
52
+ # Proxy: ${proxyUrl}
53
+ # Generated: ${newIso()}
54
+ # Workspace: ${workspaceId}
55
+ set -euo pipefail
56
+
57
+ # Source runtime credentials (never embedded in this file)
58
+ if [ -f "${runtimeEnv}" ]; then
59
+ # shellcheck disable=SC1090
60
+ source "${runtimeEnv}"
61
+ fi
62
+
63
+ INTUTIC_HOST="\${INTUTIC_HOST:-https://api.intutic.ai}"
64
+ INTUTIC_API_KEY="\${INTUTIC_API_KEY:-}"
65
+ INTUTIC_WORKSPACE_ID="\${INTUTIC_WORKSPACE_ID:-${workspaceId}}"
66
+
67
+ INPUT="$(cat)"
68
+ TOOL_NAME="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || true)"
69
+ TARGET_PATH="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); i=d.get('tool_input',{}); print(i.get('path',i.get('file_path','')))" 2>/dev/null || true)"
70
+ COMMAND="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); i=d.get('tool_input',{}); print(i.get('command',i.get('cmd','')))" 2>/dev/null || true)"
71
+
72
+ HOOK_EVENTS_LOG="${hookEventsLog}"
73
+
74
+ log_event() {
75
+ local verdict="$1" tool="$2" reason="$3"
76
+ local ts; ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
77
+ # incidentId = sha1(timestamp + toolName + workspaceId).slice(0,16)
78
+ local incident_id; incident_id="$(printf '%s' "\${ts}\${tool}\${INTUTIC_WORKSPACE_ID}" | sha1sum 2>/dev/null | cut -c1-16 || echo "$(date +%s)")"
79
+ local entry="{\\"event\\":\\"toolGovernance\\",\\"verdict\\":\\"\${verdict}\\",\\"toolName\\":\\"\${tool}\\",\\"reason\\":\\"\${reason}\\",\\"workspaceId\\":\\"\${INTUTIC_WORKSPACE_ID}\\",\\"harnessType\\":\\"pi\\",\\"incidentId\\":\\"\${incident_id}\\",\\"timestamp\\":\\"\${ts}\\"}"
80
+ # Path B: reliable file append (sync-daemon drains on FSEvents change)
81
+ printf '%s\\n' "\$entry" >> "\$HOOK_EVENTS_LOG" 2>/dev/null || true
82
+ # Path A: fire-and-forget HTTP POST (near-real-time dashboard, non-blocking)
83
+ if [ -n "\$INTUTIC_API_KEY" ]; then
84
+ curl -s -o /dev/null --max-time 3 -X POST \\
85
+ -H "Content-Type: application/json" \\
86
+ -H "Authorization: Bearer \$INTUTIC_API_KEY" \\
87
+ -d "{\\"events\\":[\$entry]}" \\
88
+ "\${INTUTIC_HOST}/api/v1/hook-events" & disown 2>/dev/null || true
89
+ fi
90
+ }
91
+
92
+ PROTECTED_PATHS=(
93
+ ${PROTECTED_PATHS.map((p) => ` "${p}"`).join('\n')}
94
+ )
95
+
96
+ # 1. Protected-path guard
97
+ for p in "\${PROTECTED_PATHS[@]}"; do
98
+ if echo "$TARGET_PATH" | grep -qF "$p"; then
99
+ REASON="Attempt to modify governance-protected path \\"$TARGET_PATH\\""
100
+ echo "[Intutic Governance] BLOCKED: $REASON" >&2
101
+ log_event "tool_blocked" "$TOOL_NAME" "$REASON"
102
+ exit 2
103
+ fi
104
+ done
105
+
106
+ # 2. Bash/shell command guard — block governance bypass patterns
107
+ if [ -n "$COMMAND" ]; then
108
+ BYPASS_PATTERNS=(
109
+ "chflags nouchg"
110
+ "chattr -i"
111
+ "rm -rf .intutic"
112
+ "rm -rf .pi/hooks"
113
+ "rm -rf .pi/models"
114
+ "PI_PRE_TOOL_HOOK="
115
+ "rm -rf .gemini/settings"
116
+ )
117
+ for pattern in "\${BYPASS_PATTERNS[@]}"; do
118
+ if echo "$COMMAND" | grep -qF "$pattern"; then
119
+ REASON="Shell command matches governance bypass pattern: \\"$pattern\\""
120
+ echo "[Intutic Governance] BLOCKED: $REASON" >&2
121
+ log_event "tool_blocked" "$TOOL_NAME" "$REASON"
122
+ exit 2
123
+ fi
124
+ done
125
+ fi
126
+
127
+ log_event "tool_allowed" "$TOOL_NAME" ""
128
+ exit 0
129
+ `;
130
+ }
131
+ // ─── Pi hooks.json merge ──────────────────────────────────────────────────────
132
+ /**
133
+ * Build the 4 canonical Pi PreToolUse hook matchers pointing at the given
134
+ * hook script path. The wildcard `.*` matcher catches all tools not matched
135
+ * by the more specific matchers above it.
136
+ */
137
+ function buildPiHookMatchers(hookScriptPath) {
138
+ return [
139
+ { matcher: 'Bash', hooks: [{ type: 'command', command: hookScriptPath }] },
140
+ { matcher: 'Edit', hooks: [{ type: 'command', command: hookScriptPath }] },
141
+ { matcher: 'Write', hooks: [{ type: 'command', command: hookScriptPath }] },
142
+ { matcher: '.*', hooks: [{ type: 'command', command: hookScriptPath }] },
143
+ ];
144
+ }
145
+ /**
146
+ * Merge Pi hooks.json — upsert intutic-governance entries under
147
+ * `hooks.PreToolUse`. Existing entries with our command path are filtered out
148
+ * then re-added so there are no duplicates.
149
+ */
150
+ function mergePiHooks(existing, hookScriptPath) {
151
+ const preToolUse = existing.hooks?.PreToolUse ?? [];
152
+ // Filter out any existing entries that reference our hook script
153
+ const filtered = preToolUse.filter((m) => !m.hooks.some((h) => h.command === hookScriptPath));
154
+ return {
155
+ ...existing,
156
+ hooks: {
157
+ ...(existing.hooks ?? {}),
158
+ PreToolUse: [...filtered, ...buildPiHookMatchers(hookScriptPath)],
159
+ },
160
+ };
161
+ }
162
+ // ─── Pi models.json merge ─────────────────────────────────────────────────────
163
+ /**
164
+ * Merge Pi models.json — update existing providers' baseUrl and add
165
+ * missing providers. The three canonical providers are anthropic, openai,
166
+ * and google.
167
+ */
168
+ function mergePiModels(existing, proxyUrl) {
169
+ const providers = existing.providers ?? {};
170
+ const targetProviders = ['anthropic', 'openai', 'google'];
171
+ const mergedProviders = { ...providers };
172
+ for (const provider of targetProviders) {
173
+ mergedProviders[provider] = {
174
+ ...(providers[provider] ?? {}),
175
+ baseUrl: proxyUrl,
176
+ };
177
+ }
178
+ return {
179
+ ...existing,
180
+ providers: mergedProviders,
181
+ };
182
+ }
183
+ // ─── Main export ──────────────────────────────────────────────────────────────
184
+ /**
185
+ * Write the Pi governance hook script, merge hooks.json and models.json,
186
+ * and write the env snippet.
187
+ *
188
+ * Safe to call repeatedly — uses atomic rename so partial writes never
189
+ * leave a corrupt state.
190
+ *
191
+ * @param workspaceRoot - Absolute workspace root path.
192
+ * @param proxyUrl - Intutic proxy URL for LLM provider routing.
193
+ * @param workspaceId - Workspace ID embedded in every hook event payload.
194
+ */
195
+ export async function writePiHooks(workspaceRoot, proxyUrl, workspaceId = '') {
196
+ // ── 1. Ensure directories ──────────────────────────────────────────────────
197
+ const globalHookDir = path.join(os.homedir(), '.intutic', 'hooks');
198
+ const hookEventsDir = path.join(workspaceRoot, '.intutic', 'events');
199
+ const envDir = path.join(workspaceRoot, '.intutic', 'env');
200
+ const piDir = path.join(os.homedir(), '.pi');
201
+ await Promise.all([
202
+ fs.mkdir(globalHookDir, { recursive: true }),
203
+ fs.mkdir(hookEventsDir, { recursive: true }),
204
+ fs.mkdir(envDir, { recursive: true }),
205
+ fs.mkdir(piDir, { recursive: true }),
206
+ ]);
207
+ // ── 2. Write bash hook script ──────────────────────────────────────────────
208
+ const hookScriptPath = path.join(globalHookDir, 'pi-check.sh');
209
+ const tmpScript = hookScriptPath + '.intutic-tmp';
210
+ await fs.writeFile(tmpScript, buildPiCheckScript(proxyUrl, workspaceRoot, workspaceId), 'utf-8');
211
+ await fs.rename(tmpScript, hookScriptPath);
212
+ await fs.chmod(hookScriptPath, 0o755);
213
+ log.info({ action: 'pi_hook_script_written', path: hookScriptPath }, 'Pi hook script written');
214
+ // ── 3. Write/merge ~/.pi/hooks.json ───────────────────────────────────────
215
+ let existingHooks = {};
216
+ try {
217
+ const raw = await fs.readFile(PI_HOOKS_CONFIG, 'utf-8');
218
+ existingHooks = JSON.parse(raw);
219
+ }
220
+ catch {
221
+ // File doesn't exist or is unparseable — start fresh
222
+ }
223
+ const mergedHooks = mergePiHooks(existingHooks, hookScriptPath);
224
+ const tmpHooks = PI_HOOKS_CONFIG + '.intutic-tmp';
225
+ await fs.writeFile(tmpHooks, JSON.stringify(mergedHooks, null, 2) + '\n', 'utf-8');
226
+ await fs.rename(tmpHooks, PI_HOOKS_CONFIG);
227
+ log.info({ action: 'pi_hooks_written', path: PI_HOOKS_CONFIG }, 'Pi hooks.json written');
228
+ // ── 4. Write/merge ~/.pi/models.json ──────────────────────────────────────
229
+ let existingModels = {};
230
+ try {
231
+ const raw = await fs.readFile(PI_MODELS_CONFIG, 'utf-8');
232
+ existingModels = JSON.parse(raw);
233
+ }
234
+ catch {
235
+ // File doesn't exist or is unparseable — start fresh
236
+ }
237
+ const mergedModels = mergePiModels(existingModels, proxyUrl);
238
+ const tmpModels = PI_MODELS_CONFIG + '.intutic-tmp';
239
+ await fs.writeFile(tmpModels, JSON.stringify(mergedModels, null, 2) + '\n', 'utf-8');
240
+ await fs.rename(tmpModels, PI_MODELS_CONFIG);
241
+ log.info({ action: 'pi_models_written', path: PI_MODELS_CONFIG }, 'Pi models.json written');
242
+ // ── 5. Write env snippet ───────────────────────────────────────────────────
243
+ const envSnippet = [
244
+ `# Intutic pi governance env`,
245
+ `INTUITIC_PI_HOOK=~/.intutic/hooks/pi-check.sh`,
246
+ `INTUITIC_PI_HOOKS_CONFIG=~/.pi/hooks.json`,
247
+ `INTUITIC_PI_MODELS_CONFIG=~/.pi/models.json`,
248
+ `OPENAI_BASE_URL=${proxyUrl}`,
249
+ `ANTHROPIC_BASE_URL=${proxyUrl}`,
250
+ ].join('\n') + '\n';
251
+ const envFilePath = path.join(envDir, 'pi.env');
252
+ const tmpEnv = envFilePath + '.intutic-tmp';
253
+ await fs.writeFile(tmpEnv, envSnippet, 'utf-8');
254
+ await fs.rename(tmpEnv, envFilePath);
255
+ log.info({ action: 'pi_env_written', path: envFilePath }, 'Pi env snippet written');
256
+ }
257
+ //# sourceMappingURL=piHooks.js.map