@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/brainIndexer.d.ts +51 -0
- package/dist/brainIndexer.d.ts.map +1 -0
- package/dist/brainIndexer.js +148 -0
- package/dist/brainIndexer.js.map +1 -0
- package/dist/configReader.d.ts +46 -0
- package/dist/configReader.d.ts.map +1 -0
- package/dist/configReader.js +133 -0
- package/dist/configReader.js.map +1 -0
- package/dist/configWriter.d.ts +53 -0
- package/dist/configWriter.d.ts.map +1 -0
- package/dist/configWriter.js +503 -0
- package/dist/configWriter.js.map +1 -0
- package/dist/harness/aiderConfigMerger.d.ts +27 -0
- package/dist/harness/aiderConfigMerger.d.ts.map +1 -0
- package/dist/harness/aiderConfigMerger.js +139 -0
- package/dist/harness/aiderConfigMerger.js.map +1 -0
- package/dist/harness/antigravityHooks.d.ts +43 -0
- package/dist/harness/antigravityHooks.d.ts.map +1 -0
- package/dist/harness/antigravityHooks.js +193 -0
- package/dist/harness/antigravityHooks.js.map +1 -0
- package/dist/harness/claudeCodeHooks.d.ts +59 -0
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -0
- package/dist/harness/claudeCodeHooks.js +414 -0
- package/dist/harness/claudeCodeHooks.js.map +1 -0
- package/dist/harness/claudeDesktopHooks.d.ts +30 -0
- package/dist/harness/claudeDesktopHooks.d.ts.map +1 -0
- package/dist/harness/claudeDesktopHooks.js +267 -0
- package/dist/harness/claudeDesktopHooks.js.map +1 -0
- package/dist/harness/clineHooks.d.ts +23 -0
- package/dist/harness/clineHooks.d.ts.map +1 -0
- package/dist/harness/clineHooks.js +169 -0
- package/dist/harness/clineHooks.js.map +1 -0
- package/dist/harness/continueHooks.d.ts +33 -0
- package/dist/harness/continueHooks.d.ts.map +1 -0
- package/dist/harness/continueHooks.js +124 -0
- package/dist/harness/continueHooks.js.map +1 -0
- package/dist/harness/cursorHooks.d.ts +27 -0
- package/dist/harness/cursorHooks.d.ts.map +1 -0
- package/dist/harness/cursorHooks.js +221 -0
- package/dist/harness/cursorHooks.js.map +1 -0
- package/dist/harness/cursorHooksJson.d.ts +29 -0
- package/dist/harness/cursorHooksJson.d.ts.map +1 -0
- package/dist/harness/cursorHooksJson.js +130 -0
- package/dist/harness/cursorHooksJson.js.map +1 -0
- package/dist/harness/gooseHardener.d.ts +39 -0
- package/dist/harness/gooseHardener.d.ts.map +1 -0
- package/dist/harness/gooseHardener.js +152 -0
- package/dist/harness/gooseHardener.js.map +1 -0
- package/dist/harness/gooseHooks.d.ts +24 -0
- package/dist/harness/gooseHooks.d.ts.map +1 -0
- package/dist/harness/gooseHooks.js +175 -0
- package/dist/harness/gooseHooks.js.map +1 -0
- package/dist/harness/gooseHooksWriter.d.ts +27 -0
- package/dist/harness/gooseHooksWriter.d.ts.map +1 -0
- package/dist/harness/gooseHooksWriter.js +147 -0
- package/dist/harness/gooseHooksWriter.js.map +1 -0
- package/dist/harness/hermesHooks.d.ts +31 -0
- package/dist/harness/hermesHooks.d.ts.map +1 -0
- package/dist/harness/hermesHooks.js +245 -0
- package/dist/harness/hermesHooks.js.map +1 -0
- package/dist/harness/mcpAutoWrite.d.ts +35 -0
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -0
- package/dist/harness/mcpAutoWrite.js +334 -0
- package/dist/harness/mcpAutoWrite.js.map +1 -0
- package/dist/harness/n8nHooks.d.ts +28 -0
- package/dist/harness/n8nHooks.d.ts.map +1 -0
- package/dist/harness/n8nHooks.js +205 -0
- package/dist/harness/n8nHooks.js.map +1 -0
- package/dist/harness/openWebuiHooks.d.ts +31 -0
- package/dist/harness/openWebuiHooks.d.ts.map +1 -0
- package/dist/harness/openWebuiHooks.js +166 -0
- package/dist/harness/openWebuiHooks.js.map +1 -0
- package/dist/harness/openclawHooks.d.ts +34 -0
- package/dist/harness/openclawHooks.d.ts.map +1 -0
- package/dist/harness/openclawHooks.js +296 -0
- package/dist/harness/openclawHooks.js.map +1 -0
- package/dist/harness/openhandsHooks.d.ts +24 -0
- package/dist/harness/openhandsHooks.d.ts.map +1 -0
- package/dist/harness/openhandsHooks.js +178 -0
- package/dist/harness/openhandsHooks.js.map +1 -0
- package/dist/harness/openhandsHooksWriter.d.ts +23 -0
- package/dist/harness/openhandsHooksWriter.d.ts.map +1 -0
- package/dist/harness/openhandsHooksWriter.js +146 -0
- package/dist/harness/openhandsHooksWriter.js.map +1 -0
- package/dist/harness/piHooks.d.ts +32 -0
- package/dist/harness/piHooks.d.ts.map +1 -0
- package/dist/harness/piHooks.js +257 -0
- package/dist/harness/piHooks.js.map +1 -0
- package/dist/harness/rooCodeHooks.d.ts +31 -0
- package/dist/harness/rooCodeHooks.d.ts.map +1 -0
- package/dist/harness/rooCodeHooks.js +300 -0
- package/dist/harness/rooCodeHooks.js.map +1 -0
- package/dist/harness/windsurfHooks.d.ts +27 -0
- package/dist/harness/windsurfHooks.d.ts.map +1 -0
- package/dist/harness/windsurfHooks.js +202 -0
- package/dist/harness/windsurfHooks.js.map +1 -0
- package/dist/hashReporter.d.ts +33 -0
- package/dist/hashReporter.d.ts.map +1 -0
- package/dist/hashReporter.js +69 -0
- package/dist/hashReporter.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/integrityStore.d.ts +43 -0
- package/dist/integrityStore.d.ts.map +1 -0
- package/dist/integrityStore.js +94 -0
- package/dist/integrityStore.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts +39 -0
- package/dist/lib/complianceProbes.d.ts.map +1 -0
- package/dist/lib/complianceProbes.js +115 -0
- package/dist/lib/complianceProbes.js.map +1 -0
- package/dist/lib/processPoller.d.ts +2 -0
- package/dist/lib/processPoller.d.ts.map +1 -0
- package/dist/lib/processPoller.js +87 -0
- package/dist/lib/processPoller.js.map +1 -0
- package/dist/lib/runtimeEnv.d.ts +49 -0
- package/dist/lib/runtimeEnv.d.ts.map +1 -0
- package/dist/lib/runtimeEnv.js +70 -0
- package/dist/lib/runtimeEnv.js.map +1 -0
- package/dist/skillWriter.d.ts +24 -0
- package/dist/skillWriter.d.ts.map +1 -0
- package/dist/skillWriter.js +112 -0
- package/dist/skillWriter.js.map +1 -0
- package/dist/syncLoop.d.ts +70 -0
- package/dist/syncLoop.d.ts.map +1 -0
- package/dist/syncLoop.js +546 -0
- package/dist/syncLoop.js.map +1 -0
- package/dist/trajectoryMonitor.d.ts +126 -0
- package/dist/trajectoryMonitor.d.ts.map +1 -0
- package/dist/trajectoryMonitor.js +291 -0
- package/dist/trajectoryMonitor.js.map +1 -0
- package/dist/watcher/driftWatcher.d.ts +25 -0
- package/dist/watcher/driftWatcher.d.ts.map +1 -0
- package/dist/watcher/driftWatcher.js +72 -0
- package/dist/watcher/driftWatcher.js.map +1 -0
- package/dist/watcher/settingsGuard.d.ts +32 -0
- package/dist/watcher/settingsGuard.d.ts.map +1 -0
- package/dist/watcher/settingsGuard.js +250 -0
- package/dist/watcher/settingsGuard.js.map +1 -0
- package/dist/wsClient.d.ts +49 -0
- package/dist/wsClient.d.ts.map +1 -0
- package/dist/wsClient.js +149 -0
- package/dist/wsClient.js.map +1 -0
- 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
|