@intutic/sync-daemon 2.0.0 → 2.1.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/dist/agentReporter.d.ts +4 -12
- package/dist/agentReporter.d.ts.map +1 -1
- package/dist/agentReporter.js +25 -4
- package/dist/agentReporter.js.map +1 -1
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +20 -6
- package/dist/configWriter.js.map +1 -1
- package/dist/harness/aiderConfigMerger.d.ts +41 -5
- package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
- package/dist/harness/aiderConfigMerger.js +110 -86
- package/dist/harness/aiderConfigMerger.js.map +1 -1
- package/dist/harness/antigravityHooks.d.ts +13 -8
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +51 -39
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +42 -28
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/clineHooks.d.ts +39 -9
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +139 -60
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexConfigMerger.d.ts +38 -0
- package/dist/harness/codexConfigMerger.d.ts.map +1 -0
- package/dist/harness/codexConfigMerger.js +101 -0
- package/dist/harness/codexConfigMerger.js.map +1 -0
- package/dist/harness/codexHooks.d.ts +4 -2
- package/dist/harness/codexHooks.d.ts.map +1 -1
- package/dist/harness/codexHooks.js +11 -11
- package/dist/harness/codexHooks.js.map +1 -1
- package/dist/harness/continueConfigMerger.d.ts +46 -0
- package/dist/harness/continueConfigMerger.d.ts.map +1 -0
- package/dist/harness/continueConfigMerger.js +90 -0
- package/dist/harness/continueConfigMerger.js.map +1 -0
- package/dist/harness/continueHooks.d.ts +15 -25
- package/dist/harness/continueHooks.d.ts.map +1 -1
- package/dist/harness/continueHooks.js +22 -119
- package/dist/harness/continueHooks.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +33 -16
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +66 -25
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +59 -52
- package/dist/harness/dshHooks.d.ts.map +1 -1
- package/dist/harness/dshHooks.js +145 -147
- package/dist/harness/dshHooks.js.map +1 -1
- package/dist/harness/gateKind.d.ts.map +1 -1
- package/dist/harness/gateKind.js +1 -0
- package/dist/harness/gateKind.js.map +1 -1
- package/dist/harness/githubCopilotHooks.js +1 -1
- package/dist/harness/grokHooks.d.ts.map +1 -1
- package/dist/harness/grokHooks.js +7 -2
- package/dist/harness/grokHooks.js.map +1 -1
- package/dist/harness/hermesHooks.d.ts +9 -0
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +32 -31
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/jsonMergeTarget.d.ts +23 -0
- package/dist/harness/jsonMergeTarget.d.ts.map +1 -0
- package/dist/harness/jsonMergeTarget.js +50 -0
- package/dist/harness/jsonMergeTarget.js.map +1 -0
- package/dist/harness/mcpAutoWrite.d.ts +28 -6
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +258 -32
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +6 -7
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +37 -6
- package/dist/harness/openCodeHooks.d.ts.map +1 -1
- package/dist/harness/openCodeHooks.js +101 -8
- package/dist/harness/openCodeHooks.js.map +1 -1
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +5 -4
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts +13 -0
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +70 -0
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts +17 -0
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +28 -29
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +64 -6
- package/dist/harness/protectedPaths.d.ts.map +1 -1
- package/dist/harness/protectedPaths.js +161 -11
- package/dist/harness/protectedPaths.js.map +1 -1
- package/dist/harness/windsurfHooks.d.ts +3 -1
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +64 -42
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts +9 -6
- package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.js +17 -6
- package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/policySnapshot.d.ts +19 -0
- package/dist/lib/policySnapshot.d.ts.map +1 -1
- package/dist/lib/policySnapshot.js +29 -2
- package/dist/lib/policySnapshot.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +14 -15
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/package.json +6 -6
|
@@ -2,8 +2,26 @@
|
|
|
2
2
|
* aiderConfigMerger.ts — Safe YAML merge for .aider.conf.yml.
|
|
3
3
|
*
|
|
4
4
|
* Reads any existing .aider.conf.yml, STRIPS dangerous auto-exec keys
|
|
5
|
-
* (test-cmd, lint-cmd, auto-test, auto-lint), merges in Intutic
|
|
6
|
-
*
|
|
5
|
+
* (test-cmd, lint-cmd, auto-test, auto-lint), merges in the Intutic proxy
|
|
6
|
+
* routing, and writes back atomically. Everything else in the file — lists,
|
|
7
|
+
* nested values, comments — is kept: the file is edited through the `yaml`
|
|
8
|
+
* library's document model, never re-serialised from a flattened copy.
|
|
9
|
+
*
|
|
10
|
+
* Aider rejects any config key that is not one of its command-line options
|
|
11
|
+
* ("unrecognized arguments"), so only real options are written:
|
|
12
|
+
*
|
|
13
|
+
* - `openai-api-base` — the proxy's OpenAI-style base URL (host + `/v1`).
|
|
14
|
+
* - `set-env: ANTHROPIC_BASE_URL=<host>` — Aider has no Anthropic base-URL
|
|
15
|
+
* option; its model layer (LiteLLM) reads this variable, and wants the bare
|
|
16
|
+
* host because it appends `/v1/messages` itself. Entries the user set for
|
|
17
|
+
* other variables are kept.
|
|
18
|
+
* - `read: .intutic/aider-sops.md` — the SOP text, as a read-only context
|
|
19
|
+
* file (Aider's own mechanism for conventions), written next to the config.
|
|
20
|
+
*
|
|
21
|
+
* Earlier versions wrote `anthropic-api-base` and `extra-instructions`, which
|
|
22
|
+
* are not Aider options and stopped Aider from starting; both are removed.
|
|
23
|
+
*
|
|
24
|
+
* A file that does not parse as YAML, or is not a mapping, is left untouched.
|
|
7
25
|
*
|
|
8
26
|
* Each strip emits a governance_config_sanitized log entry visible in
|
|
9
27
|
* the control plane audit feed.
|
|
@@ -15,125 +33,131 @@
|
|
|
15
33
|
*/
|
|
16
34
|
import * as fs from 'node:fs/promises';
|
|
17
35
|
import * as path from 'node:path';
|
|
36
|
+
import { parseDocument, isMap, isSeq, isScalar, YAMLSeq } from 'yaml';
|
|
18
37
|
import { createLogger } from '@intutic/logger';
|
|
19
|
-
import {
|
|
38
|
+
import { anthropicBaseUrl, openaiBaseUrl } from '@intutic/shared-types';
|
|
20
39
|
const log = createLogger('sync-aider-merger');
|
|
21
40
|
/**
|
|
22
41
|
* Keys that Aider auto-executes on startup without user confirmation.
|
|
23
42
|
* Stripping these prevents supply-chain persistence attacks.
|
|
24
43
|
*/
|
|
25
44
|
const SUPPRESSED_KEYS = ['test-cmd', 'lint-cmd', 'auto-test', 'auto-lint', 'test_cmd', 'lint_cmd'];
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
/** Keys earlier versions wrote that Aider rejects as unrecognized arguments. */
|
|
46
|
+
const INVALID_LEGACY_KEYS = ['anthropic-api-base', 'extra-instructions'];
|
|
47
|
+
/** SOP file, relative to the workspace root (where Aider runs). */
|
|
48
|
+
export const AIDER_SOPS_FILE = '.intutic/aider-sops.md';
|
|
49
|
+
/** Header written at the top of the file. Kept short: it sits above the
|
|
50
|
+
* user's own content, and is replaced, not stacked, on every sync. */
|
|
51
|
+
const HEADER = [
|
|
52
|
+
'# Intutic: openai-api-base, the ANTHROPIC_BASE_URL set-env entry and the',
|
|
53
|
+
`# ${AIDER_SOPS_FILE} read entry are managed by intutic connect. test-cmd,`,
|
|
54
|
+
'# lint-cmd, auto-test and auto-lint are removed on every sync.',
|
|
55
|
+
];
|
|
56
|
+
/** Leading lines this product wrote (the header above, or the one earlier
|
|
57
|
+
* versions wrote), removed before the file is parsed. */
|
|
58
|
+
const OWN_HEADER_LINE = /^# (Intutic: |Intutic Governance Rules \(auto-generated|Last sync: |WARNING: test-cmd and lint-cmd keys are suppressed|\.intutic\/aider-sops\.md read entry|lint-cmd, auto-test and auto-lint are removed)/;
|
|
59
|
+
function stripOwnHeader(raw) {
|
|
33
60
|
const lines = raw.split('\n');
|
|
34
61
|
let i = 0;
|
|
35
|
-
while (i < lines.length)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) {
|
|
54
|
-
blockLines.push(lines[i].startsWith(' ') ? lines[i].slice(2) : '');
|
|
55
|
-
i++;
|
|
56
|
-
}
|
|
57
|
-
result[key] = blockLines.join('\n').trimEnd();
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// Strip inline quotes
|
|
61
|
-
result[key] = rest.replace(/^["']|["']$/g, '');
|
|
62
|
-
i++;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return result;
|
|
62
|
+
while (i < lines.length && OWN_HEADER_LINE.test(lines[i]))
|
|
63
|
+
i++;
|
|
64
|
+
if (i === 0)
|
|
65
|
+
return raw;
|
|
66
|
+
while (i < lines.length && lines[i].trim() === '')
|
|
67
|
+
i++;
|
|
68
|
+
return lines.slice(i).join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** Turn `key`'s value into a sequence (a scalar becomes a one-item list). */
|
|
71
|
+
function ensureSeq(doc, key) {
|
|
72
|
+
const current = doc.get(key, true);
|
|
73
|
+
if (isSeq(current))
|
|
74
|
+
return current;
|
|
75
|
+
const seq = new YAMLSeq();
|
|
76
|
+
if (isScalar(current) && current.value !== null && current.value !== '')
|
|
77
|
+
seq.add(current.value);
|
|
78
|
+
doc.set(key, seq);
|
|
79
|
+
return seq;
|
|
66
80
|
}
|
|
67
81
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
82
|
+
* Merge the Intutic keys into the text of an `.aider.conf.yml`.
|
|
83
|
+
*
|
|
84
|
+
* @param raw - Current file content ('' when the file does not exist).
|
|
85
|
+
* @param proxyUrl - The proxy host (a trailing `/v1` is tolerated).
|
|
86
|
+
* @param hasSops - Whether the SOP file is to be listed under `read`.
|
|
70
87
|
*/
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
export function mergeAiderYaml(raw, proxyUrl, hasSops) {
|
|
89
|
+
const doc = parseDocument(stripOwnHeader(raw));
|
|
90
|
+
if (doc.errors.length > 0)
|
|
91
|
+
return { content: null, stripped: [] };
|
|
92
|
+
// A missing, empty or comment-only file starts as an empty mapping (its
|
|
93
|
+
// comments stay on the document); anything else that is not a mapping (a
|
|
94
|
+
// list, a bare scalar) is not an Aider config to merge into.
|
|
95
|
+
if (doc.contents === null)
|
|
96
|
+
doc.contents = doc.createNode({});
|
|
97
|
+
if (!isMap(doc.contents))
|
|
98
|
+
return { content: null, stripped: [] };
|
|
99
|
+
const stripped = SUPPRESSED_KEYS.filter((key) => doc.has(key));
|
|
100
|
+
for (const key of [...stripped, ...INVALID_LEGACY_KEYS])
|
|
101
|
+
doc.delete(key);
|
|
102
|
+
doc.set('openai-api-base', openaiBaseUrl(proxyUrl));
|
|
103
|
+
const setEnv = ensureSeq(doc, 'set-env');
|
|
104
|
+
setEnv.items = setEnv.items.filter((item) => {
|
|
105
|
+
const value = isScalar(item) ? item.value : item;
|
|
106
|
+
return !(typeof value === 'string' && value.startsWith('ANTHROPIC_BASE_URL='));
|
|
107
|
+
});
|
|
108
|
+
setEnv.add(`ANTHROPIC_BASE_URL=${anthropicBaseUrl(proxyUrl)}`);
|
|
109
|
+
const read = ensureSeq(doc, 'read');
|
|
110
|
+
read.items = read.items.filter((item) => (isScalar(item) ? item.value : item) !== AIDER_SOPS_FILE);
|
|
111
|
+
if (hasSops)
|
|
112
|
+
read.add(AIDER_SOPS_FILE);
|
|
113
|
+
if (read.items.length === 0)
|
|
114
|
+
doc.delete('read');
|
|
115
|
+
return { content: `${HEADER.join('\n')}\n\n${doc.toString()}`, stripped };
|
|
88
116
|
}
|
|
89
117
|
/**
|
|
90
118
|
* Merge Intutic governance settings into .aider.conf.yml.
|
|
91
119
|
*
|
|
92
120
|
* Preserves all user keys EXCEPT the suppressed auto-exec keys.
|
|
93
|
-
* Adds/updates openai-api-base and anthropic-api-base.
|
|
94
121
|
*
|
|
95
122
|
* @param configPath - Absolute path to .aider.conf.yml
|
|
96
123
|
* @param proxyUrl - Intutic proxy URL
|
|
97
|
-
* @param sopsText - Optional SOP instructions
|
|
124
|
+
* @param sopsText - Optional SOP instructions, written to the `read` file
|
|
125
|
+
* @returns `true` when the config was written, `false` when it was left
|
|
126
|
+
* untouched because it is not a YAML mapping (a warning is logged).
|
|
98
127
|
*/
|
|
99
128
|
export async function mergeAiderConfig(configPath, proxyUrl, sopsText) {
|
|
100
|
-
|
|
101
|
-
let existing = {};
|
|
129
|
+
let raw = '';
|
|
102
130
|
try {
|
|
103
|
-
|
|
104
|
-
existing = parseAiderYaml(raw);
|
|
131
|
+
raw = await fs.readFile(configPath, 'utf-8');
|
|
105
132
|
}
|
|
106
|
-
catch {
|
|
107
|
-
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err.code !== 'ENOENT')
|
|
135
|
+
throw err;
|
|
108
136
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
stripped.push(key);
|
|
114
|
-
delete existing[key];
|
|
115
|
-
}
|
|
137
|
+
const { content, stripped } = mergeAiderYaml(raw, proxyUrl, Boolean(sopsText));
|
|
138
|
+
if (content === null) {
|
|
139
|
+
log.warn({ action: 'aider_config_merge_skipped', path: configPath }, `${configPath} is not a YAML mapping — left untouched; fix it and the next sync will add the Intutic keys`);
|
|
140
|
+
return false;
|
|
116
141
|
}
|
|
117
142
|
if (stripped.length > 0) {
|
|
118
143
|
log.warn({ action: 'aider_keys_stripped', keys: stripped, path: configPath }, `Stripped dangerous auto-exec keys from .aider.conf.yml: ${stripped.join(', ')}`);
|
|
119
144
|
}
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
existing['anthropic-api-base'] = proxyUrl;
|
|
145
|
+
// The SOP file lives next to the config, under the workspace's .intutic/.
|
|
146
|
+
const sopsPath = path.join(path.dirname(configPath), AIDER_SOPS_FILE);
|
|
123
147
|
if (sopsText) {
|
|
124
|
-
|
|
148
|
+
await fs.mkdir(path.dirname(sopsPath), { recursive: true });
|
|
149
|
+
const tmpSops = sopsPath + '.intutic-tmp';
|
|
150
|
+
await fs.writeFile(tmpSops, sopsText + '\n', 'utf-8');
|
|
151
|
+
await fs.rename(tmpSops, sopsPath);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
await fs.rm(sopsPath, { force: true });
|
|
125
155
|
}
|
|
126
|
-
const header = [
|
|
127
|
-
'# Intutic Governance Rules (auto-generated — do not edit proxy keys)',
|
|
128
|
-
`# Last sync: ${newIso()}`,
|
|
129
|
-
`# WARNING: test-cmd and lint-cmd keys are suppressed by Intutic governance`,
|
|
130
|
-
'',
|
|
131
|
-
].join('\n');
|
|
132
|
-
const content = header + serializeAiderYaml(existing) + '\n';
|
|
133
156
|
const tmpPath = configPath + '.intutic-tmp';
|
|
134
157
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
135
158
|
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
136
159
|
await fs.rename(tmpPath, configPath);
|
|
137
160
|
log.info({ action: 'aider_config_written', path: configPath, stripped }, 'Aider config merged with proxy URL and dangerous keys stripped');
|
|
161
|
+
return true;
|
|
138
162
|
}
|
|
139
163
|
//# sourceMappingURL=aiderConfigMerger.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aiderConfigMerger.js","sourceRoot":"","sources":["../../src/harness/aiderConfigMerger.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"aiderConfigMerger.js","sourceRoot":"","sources":["../../src/harness/aiderConfigMerger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* antigravityHooks.ts — Gemini CLI (Antigravity) governance hook injection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Gemini CLI runs command hooks registered under `hooks.BeforeTool` in
|
|
5
|
+
* `~/.gemini/settings.json`. Each entry carries a `matcher` regex over tool
|
|
6
|
+
* names and a list of `{type: "command", command}` hooks; the command receives
|
|
7
|
+
* `{tool_name, tool_input, ...}` on stdin and blocks the call with exit code 2
|
|
8
|
+
* (stderr is the reason). This module:
|
|
7
9
|
*
|
|
8
|
-
* 1. Writes
|
|
10
|
+
* 1. Writes the gate script to `.intutic/hooks/antigravity-check.sh`
|
|
9
11
|
* (same pattern as Goose / OpenHands bash hooks).
|
|
10
|
-
* 2. Merges
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* 2. Merges a catch-all `BeforeTool` entry for it into
|
|
13
|
+
* `~/.gemini/settings.json`, keeping every other setting and hook. A
|
|
14
|
+
* settings file that is not a plain JSON object is left alone.
|
|
15
|
+
*
|
|
16
|
+
* Earlier versions registered the script under `hooks.preTool` and wrote an
|
|
17
|
+
* `ANTIGRAVITY_PRE_TOOL_HOOK` env snippet. Neither is a Gemini CLI setting, so
|
|
18
|
+
* that gate never ran; the stale `preTool` key is removed on the next write.
|
|
14
19
|
*
|
|
15
20
|
* Why bash and not Node.js?
|
|
16
21
|
* The Gemini CLI (antigravity) is a standalone binary; when it invokes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"antigravityHooks.d.ts","sourceRoot":"","sources":["../../src/harness/antigravityHooks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"antigravityHooks.d.ts","sourceRoot":"","sources":["../../src/harness/antigravityHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAwFH;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CA0Ef"}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* antigravityHooks.ts — Gemini CLI (Antigravity) governance hook injection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Gemini CLI runs command hooks registered under `hooks.BeforeTool` in
|
|
5
|
+
* `~/.gemini/settings.json`. Each entry carries a `matcher` regex over tool
|
|
6
|
+
* names and a list of `{type: "command", command}` hooks; the command receives
|
|
7
|
+
* `{tool_name, tool_input, ...}` on stdin and blocks the call with exit code 2
|
|
8
|
+
* (stderr is the reason). This module:
|
|
7
9
|
*
|
|
8
|
-
* 1. Writes
|
|
10
|
+
* 1. Writes the gate script to `.intutic/hooks/antigravity-check.sh`
|
|
9
11
|
* (same pattern as Goose / OpenHands bash hooks).
|
|
10
|
-
* 2. Merges
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* 2. Merges a catch-all `BeforeTool` entry for it into
|
|
13
|
+
* `~/.gemini/settings.json`, keeping every other setting and hook. A
|
|
14
|
+
* settings file that is not a plain JSON object is left alone.
|
|
15
|
+
*
|
|
16
|
+
* Earlier versions registered the script under `hooks.preTool` and wrote an
|
|
17
|
+
* `ANTIGRAVITY_PRE_TOOL_HOOK` env snippet. Neither is a Gemini CLI setting, so
|
|
18
|
+
* that gate never ran; the stale `preTool` key is removed on the next write.
|
|
14
19
|
*
|
|
15
20
|
* Why bash and not Node.js?
|
|
16
21
|
* The Gemini CLI (antigravity) is a standalone binary; when it invokes
|
|
@@ -34,9 +39,8 @@ import * as os from 'node:os';
|
|
|
34
39
|
import { createLogger } from '@intutic/logger';
|
|
35
40
|
import { newIso } from '@intutic/id';
|
|
36
41
|
import { emitShellGate, SHELL_EXTRACT, SHELL_FAIL_CLOSED } from './gateBody.js';
|
|
42
|
+
import { readJsonObjectForMerge } from './jsonMergeTarget.js';
|
|
37
43
|
const log = createLogger('sync-antigravity-hooks');
|
|
38
|
-
/** Default path to the Gemini CLI global settings file. */
|
|
39
|
-
const GEMINI_SETTINGS = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
40
44
|
/** Governance-sensitive paths that the hook gate protects. */
|
|
41
45
|
// ─── Bash hook script template ────────────────────────────────────────────────
|
|
42
46
|
function buildAntigravityCheckScript(proxyUrl, workspaceRoot, workspaceId) {
|
|
@@ -120,12 +124,12 @@ export async function writeAntigravityHooks(workspaceRoot, proxyUrl, workspaceId
|
|
|
120
124
|
// ── 1. Ensure directories ──────────────────────────────────────────────────
|
|
121
125
|
const hookScriptDir = path.join(workspaceRoot, '.intutic', 'hooks');
|
|
122
126
|
const hookEventsDir = path.join(workspaceRoot, '.intutic', 'events');
|
|
123
|
-
|
|
127
|
+
// homedir() read at call time so tests that move HOME are honoured.
|
|
128
|
+
const geminiSettings = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
124
129
|
await Promise.all([
|
|
125
130
|
fs.mkdir(hookScriptDir, { recursive: true }),
|
|
126
131
|
fs.mkdir(hookEventsDir, { recursive: true }),
|
|
127
|
-
fs.mkdir(
|
|
128
|
-
fs.mkdir(path.join(os.homedir(), '.gemini'), { recursive: true }),
|
|
132
|
+
fs.mkdir(path.dirname(geminiSettings), { recursive: true }),
|
|
129
133
|
]);
|
|
130
134
|
// ── 2. Write hook script ───────────────────────────────────────────────────
|
|
131
135
|
const hookScriptPath = path.join(hookScriptDir, 'antigravity-check.sh');
|
|
@@ -135,37 +139,45 @@ export async function writeAntigravityHooks(workspaceRoot, proxyUrl, workspaceId
|
|
|
135
139
|
await fs.chmod(hookScriptPath, 0o755);
|
|
136
140
|
log.info({ action: 'antigravity_hook_script_written', path: hookScriptPath }, 'Antigravity hook script written');
|
|
137
141
|
// ── 3. Merge into ~/.gemini/settings.json ─────────────────────────────────
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
const existingSettings = await readJsonObjectForMerge(geminiSettings);
|
|
143
|
+
if (existingSettings === null)
|
|
144
|
+
return;
|
|
145
|
+
const existingHooks = (typeof existingSettings.hooks === 'object' && existingSettings.hooks !== null
|
|
146
|
+
? existingSettings.hooks
|
|
147
|
+
: {});
|
|
148
|
+
// `preTool` is where earlier versions registered the gate; it is not a
|
|
149
|
+
// Gemini CLI hook event, so it is dropped rather than carried forward.
|
|
150
|
+
const keptHooks = { ...existingHooks };
|
|
151
|
+
delete keptHooks.preTool;
|
|
152
|
+
const existingBeforeTool = Array.isArray(keptHooks.BeforeTool) ? keptHooks.BeforeTool : [];
|
|
153
|
+
// De-duplicated by command, so repeated syncs replace this entry rather than
|
|
154
|
+
// stacking copies, and every hook the user registered is kept.
|
|
155
|
+
const intuticCmd = `bash ${JSON.stringify(hookScriptPath)}`;
|
|
156
|
+
const filtered = existingBeforeTool.filter((entry) => {
|
|
157
|
+
if (typeof entry !== 'object' || entry === null)
|
|
158
|
+
return true;
|
|
159
|
+
const inner = entry.hooks ?? [];
|
|
160
|
+
return !(inner.length > 0 &&
|
|
161
|
+
inner.every((h) => typeof h === 'object' && h !== null && h.command === intuticCmd));
|
|
162
|
+
});
|
|
146
163
|
const mergedSettings = {
|
|
147
164
|
...existingSettings,
|
|
148
165
|
hooks: {
|
|
149
|
-
...
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
...keptHooks,
|
|
167
|
+
BeforeTool: [
|
|
168
|
+
...filtered,
|
|
169
|
+
{
|
|
170
|
+
// Catch-all: the gate decides from the arguments, so a tool name
|
|
171
|
+
// nobody anticipated is still evaluated.
|
|
172
|
+
matcher: '.*',
|
|
173
|
+
hooks: [{ name: 'intutic-governance', type: 'command', command: intuticCmd }],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
152
176
|
},
|
|
153
177
|
};
|
|
154
|
-
const tmpSettings =
|
|
178
|
+
const tmpSettings = geminiSettings + '.intutic-tmp';
|
|
155
179
|
await fs.writeFile(tmpSettings, JSON.stringify(mergedSettings, null, 2) + '\n', 'utf-8');
|
|
156
|
-
await fs.rename(tmpSettings,
|
|
157
|
-
log.info({ action: 'antigravity_settings_written', path:
|
|
158
|
-
// ── 4. Write env snippet (optional — for shell profiles / IDE env) ─────────
|
|
159
|
-
const envSnippet = [
|
|
160
|
-
`# Intutic Antigravity governance hook — auto-generated ${newIso()}`,
|
|
161
|
-
`# Source this file in ~/.bashrc or ~/.zshrc to apply governance to Gemini CLI.`,
|
|
162
|
-
`export ANTIGRAVITY_PRE_TOOL_HOOK=${JSON.stringify(hookScriptPath)}`,
|
|
163
|
-
proxyUrl ? `export ANTIGRAVITY_PROXY_URL=${JSON.stringify(proxyUrl)}` : '',
|
|
164
|
-
].filter(Boolean).join('\n') + '\n';
|
|
165
|
-
const envFilePath = path.join(envDir, 'antigravity.env');
|
|
166
|
-
const tmpEnv = envFilePath + '.intutic-tmp';
|
|
167
|
-
await fs.writeFile(tmpEnv, envSnippet, 'utf-8');
|
|
168
|
-
await fs.rename(tmpEnv, envFilePath);
|
|
169
|
-
log.info({ action: 'antigravity_env_written', path: envFilePath }, 'Antigravity env snippet written');
|
|
180
|
+
await fs.rename(tmpSettings, geminiSettings);
|
|
181
|
+
log.info({ action: 'antigravity_settings_written', path: geminiSettings }, 'Merged Intutic BeforeTool hook into ~/.gemini/settings.json');
|
|
170
182
|
}
|
|
171
183
|
//# sourceMappingURL=antigravityHooks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"antigravityHooks.js","sourceRoot":"","sources":["../../src/harness/antigravityHooks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"antigravityHooks.js","sourceRoot":"","sources":["../../src/harness/antigravityHooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claudeCodeHooks.d.ts","sourceRoot":"","sources":["../../src/harness/claudeCodeHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"claudeCodeHooks.d.ts","sourceRoot":"","sources":["../../src/harness/claudeCodeHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAyBzD,gFAAgF;AAChF,eAAO,MAAM,eAAe,sCAAsC,CAAA;AAElE;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,0BAAgC,CAAA;AACrE,eAAO,MAAM,mBAAmB,0CAA2B,CAAA;AAC3D,eAAO,MAAM,sBAAsB,IAA8B,CAAA;AAmBjE,eAAO,MAAM,wBAAwB;;;IAGnC;;;;;;;;;;OAUG;;;;;;;;;;EAEH,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAsCzE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,uBAAuB,EAAE,iBAAiB,GACrF,kBAAkB,CAgHpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,YAAY,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,uBAAuB,EAAE,iBAAiB,EACtF,eAAe,CAAC,EAAE,MAAM,EACxB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,SAAgB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAuTf;AAiKD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAQjB"}
|
|
@@ -17,6 +17,7 @@ import { z } from 'zod';
|
|
|
17
17
|
import { createLogger } from '@intutic/logger';
|
|
18
18
|
import { emitJsGate, emitJsFailClosedPrelude, emitPreImageCapture, REVIEW_REQUESTS_BASENAME as GATE_REVIEW_REQUESTS_BASENAME, REVIEW_REQUESTS_LOG as GATE_REVIEW_REQUESTS_LOG, REVIEW_REQUEST_VERSION as GATE_REVIEW_REQUEST_VERSION, } from './gateBody.js';
|
|
19
19
|
import { emitRedactor } from './holdRedaction.js';
|
|
20
|
+
import { readJsonObjectForMerge } from './jsonMergeTarget.js';
|
|
20
21
|
const log = createLogger('sync-claude-hooks');
|
|
21
22
|
/**
|
|
22
23
|
* Coerces an unvalidated settings value to a string array, dropping anything else.
|
|
@@ -454,26 +455,23 @@ process.stdin.on('end', () => {
|
|
|
454
455
|
const localClaudeDir = node_path.join(workspaceRoot, '.claude');
|
|
455
456
|
await node_fs.mkdir(localClaudeDir, { recursive: true });
|
|
456
457
|
const localSettingsPath = node_path.join(localClaudeDir, 'settings.json');
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
458
|
+
// A settings file that is not a plain JSON object is left untouched (and
|
|
459
|
+
// reported) rather than replaced with only the Intutic keys.
|
|
460
|
+
const existingLocal = await readJsonObjectForMerge(localSettingsPath);
|
|
461
|
+
if (existingLocal !== null) {
|
|
462
|
+
const mergedLocal = {
|
|
463
|
+
...existingLocal,
|
|
464
|
+
permissions: {
|
|
465
|
+
...existingLocal.permissions,
|
|
466
|
+
deny: denyRules,
|
|
467
|
+
},
|
|
468
|
+
hooks: {
|
|
469
|
+
...existingLocal.hooks,
|
|
470
|
+
PreToolUse: mergePreToolUse(existingLocal.hooks?.['PreToolUse'], newSettings.hooks.PreToolUse),
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
await node_fs.writeFile(localSettingsPath, JSON.stringify(mergedLocal, null, 2) + '\n', 'utf-8');
|
|
464
474
|
}
|
|
465
|
-
const mergedLocal = {
|
|
466
|
-
...existingLocal,
|
|
467
|
-
permissions: {
|
|
468
|
-
...existingLocal.permissions,
|
|
469
|
-
deny: denyRules,
|
|
470
|
-
},
|
|
471
|
-
hooks: {
|
|
472
|
-
...existingLocal.hooks,
|
|
473
|
-
PreToolUse: newSettings.hooks.PreToolUse,
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
await node_fs.writeFile(localSettingsPath, JSON.stringify(mergedLocal, null, 2) + '\n', 'utf-8');
|
|
477
475
|
// Write global settings config ~/.claude/settings.json
|
|
478
476
|
//
|
|
479
477
|
// GUARD (bugfix): never touch the user's real global settings from a test
|
|
@@ -490,14 +488,9 @@ process.stdin.on('end', () => {
|
|
|
490
488
|
const globalClaudeDir = node_path.join(node_os.homedir(), '.claude');
|
|
491
489
|
await node_fs.mkdir(globalClaudeDir, { recursive: true });
|
|
492
490
|
const globalSettingsPath = node_path.join(globalClaudeDir, 'settings.json');
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
existingGlobal = JSON.parse(raw);
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
// Ignore
|
|
500
|
-
}
|
|
491
|
+
const existingGlobal = await readJsonObjectForMerge(globalSettingsPath);
|
|
492
|
+
if (existingGlobal === null)
|
|
493
|
+
return;
|
|
501
494
|
// Merge, prioritizing global user settings but updating permissions/hooks
|
|
502
495
|
const mergedGlobal = {
|
|
503
496
|
...existingGlobal,
|
|
@@ -511,12 +504,33 @@ process.stdin.on('end', () => {
|
|
|
511
504
|
},
|
|
512
505
|
hooks: {
|
|
513
506
|
...existingGlobal.hooks,
|
|
514
|
-
PreToolUse: newSettings.hooks.PreToolUse,
|
|
507
|
+
PreToolUse: mergePreToolUse(existingGlobal.hooks?.['PreToolUse'], newSettings.hooks.PreToolUse),
|
|
515
508
|
},
|
|
516
509
|
};
|
|
517
510
|
await node_fs.writeFile(globalSettingsPath, JSON.stringify(mergedGlobal, null, 2) + '\n', 'utf-8');
|
|
518
511
|
log.info({ action: 'hooks_written' }, 'Successfully updated settings.json hooks globally and locally');
|
|
519
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* The PreToolUse list to write: the user's own entries, then the Intutic
|
|
515
|
+
* ones. An entry is Intutic's when every hook in it runs a gate this module
|
|
516
|
+
* wrote (`claude-code-check.js`, or `pre-tool-check.js` from earlier
|
|
517
|
+
* versions), so re-running replaces those instead of stacking copies, and a
|
|
518
|
+
* hook the user registered survives every sync.
|
|
519
|
+
*/
|
|
520
|
+
function mergePreToolUse(existing, intutic) {
|
|
521
|
+
const isOurs = (entry) => {
|
|
522
|
+
if (typeof entry !== 'object' || entry === null)
|
|
523
|
+
return false;
|
|
524
|
+
const inner = entry['hooks'];
|
|
525
|
+
return Array.isArray(inner) && inner.length > 0 && inner.every((h) => {
|
|
526
|
+
const command = typeof h === 'object' && h !== null ? h['command'] : undefined;
|
|
527
|
+
return typeof command === 'string' &&
|
|
528
|
+
(command.includes('.intutic/hooks/claude-code-check.js') || command.includes('.intutic/hooks/pre-tool-check.js'));
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
const kept = Array.isArray(existing) ? existing.filter((e) => !isOurs(e)) : [];
|
|
532
|
+
return [...kept, ...intutic];
|
|
533
|
+
}
|
|
520
534
|
// ─── Event Drain Helper ──────────────────────────────────────────────────────
|
|
521
535
|
/**
|
|
522
536
|
* Drains the local hook-events log file and POSTs all accumulated governance
|