@intutic/sync-daemon 1.10.0 → 2.0.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/README.md +2 -2
- package/dist/agentReporter.d.ts +205 -0
- package/dist/agentReporter.d.ts.map +1 -0
- package/dist/agentReporter.js +394 -0
- package/dist/agentReporter.js.map +1 -0
- package/dist/configReader.d.ts +39 -2
- package/dist/configReader.d.ts.map +1 -1
- package/dist/configReader.js +56 -2
- package/dist/configReader.js.map +1 -1
- package/dist/configWriter.d.ts +51 -1
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +490 -63
- package/dist/configWriter.js.map +1 -1
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +33 -55
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/claudeCodeHooks.d.ts +47 -13
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +316 -54
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/claudeDesktopHooks.d.ts.map +1 -1
- package/dist/harness/claudeDesktopHooks.js +35 -48
- package/dist/harness/claudeDesktopHooks.js.map +1 -1
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +42 -34
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexHooks.d.ts +36 -0
- package/dist/harness/codexHooks.d.ts.map +1 -0
- package/dist/harness/codexHooks.js +227 -0
- package/dist/harness/codexHooks.js.map +1 -0
- package/dist/harness/continueHooks.d.ts +45 -5
- package/dist/harness/continueHooks.d.ts.map +1 -1
- package/dist/harness/continueHooks.js +225 -5
- package/dist/harness/continueHooks.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +31 -0
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +78 -49
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +198 -0
- package/dist/harness/dshHooks.d.ts.map +1 -0
- package/dist/harness/dshHooks.js +569 -0
- package/dist/harness/dshHooks.js.map +1 -0
- package/dist/harness/gateBody.d.ts +422 -0
- package/dist/harness/gateBody.d.ts.map +1 -0
- package/dist/harness/gateBody.js +1816 -0
- package/dist/harness/gateBody.js.map +1 -0
- package/dist/harness/gateKind.d.ts +164 -0
- package/dist/harness/gateKind.d.ts.map +1 -0
- package/dist/harness/gateKind.js +143 -0
- package/dist/harness/gateKind.js.map +1 -0
- package/dist/harness/githubCopilotHooks.d.ts +54 -0
- package/dist/harness/githubCopilotHooks.d.ts.map +1 -0
- package/dist/harness/githubCopilotHooks.js +218 -0
- package/dist/harness/githubCopilotHooks.js.map +1 -0
- package/dist/harness/gooseHooks.d.ts.map +1 -1
- package/dist/harness/gooseHooks.js +67 -37
- package/dist/harness/gooseHooks.js.map +1 -1
- package/dist/harness/grokHooks.d.ts +77 -0
- package/dist/harness/grokHooks.d.ts.map +1 -0
- package/dist/harness/grokHooks.js +350 -0
- package/dist/harness/grokHooks.js.map +1 -0
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +26 -55
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/holdRedaction.d.ts +69 -0
- package/dist/harness/holdRedaction.d.ts.map +1 -0
- package/dist/harness/holdRedaction.js +211 -0
- package/dist/harness/holdRedaction.js.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts +51 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.js +151 -0
- package/dist/harness/jetbrainsXmlConfig.js.map +1 -0
- package/dist/harness/mcpAutoWrite.d.ts +100 -3
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +712 -90
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/museHooks.d.ts +53 -0
- package/dist/harness/museHooks.d.ts.map +1 -0
- package/dist/harness/museHooks.js +273 -0
- package/dist/harness/museHooks.js.map +1 -0
- package/dist/harness/n8nHooks.d.ts +25 -9
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +174 -9
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +93 -0
- package/dist/harness/openCodeHooks.d.ts.map +1 -0
- package/dist/harness/openCodeHooks.js +248 -0
- package/dist/harness/openCodeHooks.js.map +1 -0
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +56 -11
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openclawHooks.d.ts.map +1 -1
- package/dist/harness/openclawHooks.js +30 -54
- package/dist/harness/openclawHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +35 -41
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +31 -58
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +353 -0
- package/dist/harness/protectedPaths.d.ts.map +1 -0
- package/dist/harness/protectedPaths.js +805 -0
- package/dist/harness/protectedPaths.js.map +1 -0
- package/dist/harness/rooCodeHooks.d.ts.map +1 -1
- package/dist/harness/rooCodeHooks.js +34 -48
- package/dist/harness/rooCodeHooks.js.map +1 -1
- package/dist/harness/windsurfHooks.d.ts +72 -0
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +172 -58
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts +78 -0
- package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -0
- package/dist/harness/windsurfJetBrainsProxy.js +159 -0
- package/dist/harness/windsurfJetBrainsProxy.js.map +1 -0
- package/dist/hashReporter.js +1 -1
- package/dist/hashReporter.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/integrityStore.d.ts +0 -8
- package/dist/integrityStore.d.ts.map +1 -1
- package/dist/integrityStore.js +6 -29
- package/dist/integrityStore.js.map +1 -1
- package/dist/lib/approvedBypasses.d.ts +76 -0
- package/dist/lib/approvedBypasses.d.ts.map +1 -0
- package/dist/lib/approvedBypasses.js +140 -0
- package/dist/lib/approvedBypasses.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts.map +1 -1
- package/dist/lib/complianceProbes.js +5 -0
- package/dist/lib/complianceProbes.js.map +1 -1
- package/dist/lib/decisionsDigest.d.ts +99 -0
- package/dist/lib/decisionsDigest.d.ts.map +1 -0
- package/dist/lib/decisionsDigest.js +199 -0
- package/dist/lib/decisionsDigest.js.map +1 -0
- package/dist/lib/egressPolicy.d.ts +69 -0
- package/dist/lib/egressPolicy.d.ts.map +1 -0
- package/dist/lib/egressPolicy.js +126 -0
- package/dist/lib/egressPolicy.js.map +1 -0
- package/dist/lib/gitWorktrees.d.ts +140 -0
- package/dist/lib/gitWorktrees.d.ts.map +1 -0
- package/dist/lib/gitWorktrees.js +191 -0
- package/dist/lib/gitWorktrees.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +224 -0
- package/dist/lib/policySnapshot.d.ts.map +1 -0
- package/dist/lib/policySnapshot.js +637 -0
- package/dist/lib/policySnapshot.js.map +1 -0
- package/dist/lib/processPoller.d.ts +44 -0
- package/dist/lib/processPoller.d.ts.map +1 -1
- package/dist/lib/processPoller.js +176 -1
- package/dist/lib/processPoller.js.map +1 -1
- package/dist/lib/runtimeEnv.d.ts +8 -0
- package/dist/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/lib/runtimeEnv.js +3 -0
- package/dist/lib/runtimeEnv.js.map +1 -1
- package/dist/localTrajectoryHeuristics.d.ts +51 -0
- package/dist/localTrajectoryHeuristics.d.ts.map +1 -0
- package/dist/localTrajectoryHeuristics.js +90 -0
- package/dist/localTrajectoryHeuristics.js.map +1 -0
- package/dist/sessionReporter.d.ts +70 -0
- package/dist/sessionReporter.d.ts.map +1 -0
- package/dist/sessionReporter.js +140 -0
- package/dist/sessionReporter.js.map +1 -0
- package/dist/skillWriter.d.ts +1 -1
- package/dist/skillWriter.d.ts.map +1 -1
- package/dist/skillWriter.js +5 -2
- package/dist/skillWriter.js.map +1 -1
- package/dist/syncLoop.d.ts +105 -3
- package/dist/syncLoop.d.ts.map +1 -1
- package/dist/syncLoop.js +397 -65
- package/dist/syncLoop.js.map +1 -1
- package/dist/trajectoryMonitor.d.ts +2 -0
- package/dist/trajectoryMonitor.d.ts.map +1 -1
- package/dist/trajectoryMonitor.js +57 -0
- package/dist/trajectoryMonitor.js.map +1 -1
- package/dist/watcher/driftWatcher.d.ts.map +1 -1
- package/dist/watcher/driftWatcher.js +12 -3
- package/dist/watcher/driftWatcher.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts +23 -0
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +202 -17
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/package.json +37 -6
- package/dist/brainIndexer.d.ts +0 -51
- package/dist/brainIndexer.d.ts.map +0 -1
- package/dist/brainIndexer.js +0 -148
- package/dist/brainIndexer.js.map +0 -1
- package/dist/harness/cursorHooksJson.d.ts +0 -29
- package/dist/harness/cursorHooksJson.d.ts.map +0 -1
- package/dist/harness/cursorHooksJson.js +0 -130
- package/dist/harness/cursorHooksJson.js.map +0 -1
- package/dist/harness/gooseHooksWriter.d.ts +0 -27
- package/dist/harness/gooseHooksWriter.d.ts.map +0 -1
- package/dist/harness/gooseHooksWriter.js +0 -147
- package/dist/harness/gooseHooksWriter.js.map +0 -1
- package/dist/harness/openhandsHooksWriter.d.ts +0 -23
- package/dist/harness/openhandsHooksWriter.d.ts.map +0 -1
- package/dist/harness/openhandsHooksWriter.js +0 -146
- package/dist/harness/openhandsHooksWriter.js.map +0 -1
|
@@ -21,24 +21,11 @@ import * as path from 'node:path';
|
|
|
21
21
|
import * as os from 'node:os';
|
|
22
22
|
import { createLogger } from '@intutic/logger';
|
|
23
23
|
import { newIso } from '@intutic/id';
|
|
24
|
+
import { emitShellGate, SHELL_EXTRACT, SHELL_FAIL_CLOSED } from './gateBody.js';
|
|
24
25
|
const log = createLogger('sync-hermes-hooks');
|
|
25
26
|
/** Path to the Hermes global config file. */
|
|
26
27
|
const HERMES_CONFIG = path.join(os.homedir(), '.hermes', 'config.yaml');
|
|
27
28
|
/** Governance-sensitive paths that the hook gate protects. */
|
|
28
|
-
const PROTECTED_PATHS = [
|
|
29
|
-
'.gemini/settings.json',
|
|
30
|
-
'.intutic/hooks',
|
|
31
|
-
'.intutic/integrity.json',
|
|
32
|
-
'.intutic/events',
|
|
33
|
-
'.claude/settings.json',
|
|
34
|
-
'.cursor/hooks.json',
|
|
35
|
-
'.codeium/windsurf/hooks.json',
|
|
36
|
-
'.cline/hooks',
|
|
37
|
-
'.agents/plugins/intutic-governance',
|
|
38
|
-
'.openhands/hooks.json',
|
|
39
|
-
'.hermes/config.yaml',
|
|
40
|
-
'.hermes/skills/intutic-governance',
|
|
41
|
-
];
|
|
42
29
|
// ─── Bash hook script template ────────────────────────────────────────────────
|
|
43
30
|
function buildHermesCheckScript(proxyUrl, workspaceRoot, workspaceId) {
|
|
44
31
|
const hookEventsLog = path.join(workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
|
|
@@ -50,7 +37,7 @@ function buildHermesCheckScript(proxyUrl, workspaceRoot, workspaceId) {
|
|
|
50
37
|
# Generated: ${newIso()}
|
|
51
38
|
# Workspace: ${workspaceId}
|
|
52
39
|
set -euo pipefail
|
|
53
|
-
|
|
40
|
+
${SHELL_FAIL_CLOSED}
|
|
54
41
|
# Source runtime credentials (never embedded in this file)
|
|
55
42
|
if [ -f "${runtimeEnv}" ]; then
|
|
56
43
|
# shellcheck disable=SC1090
|
|
@@ -62,18 +49,35 @@ INTUTIC_API_KEY="\${INTUTIC_API_KEY:-}"
|
|
|
62
49
|
INTUTIC_WORKSPACE_ID="\${INTUTIC_WORKSPACE_ID:-${workspaceId}}"
|
|
63
50
|
|
|
64
51
|
INPUT="$(cat)"
|
|
65
|
-
|
|
66
|
-
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)"
|
|
67
|
-
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)"
|
|
52
|
+
${SHELL_EXTRACT}
|
|
68
53
|
|
|
69
54
|
HOOK_EVENTS_LOG=${JSON.stringify(hookEventsLog)}
|
|
70
55
|
|
|
71
56
|
log_event() {
|
|
72
57
|
local verdict="\$1" tool="\$2" reason="\$3"
|
|
58
|
+
# \`tool\` and \`reason\` carry agent-controlled text — the tool name, and the
|
|
59
|
+
# target path quoted into the block reason — and both are spliced into the
|
|
60
|
+
# JSON string below. Escape first: one unescaped quote yields a line
|
|
61
|
+
# JSON.parse rejects, and drainHookEvents drops malformed lines then wipes the
|
|
62
|
+
# log on a successful drain, so the record of a block is destroyed rather than
|
|
63
|
+
# delayed. A crafted path could otherwise close the string and forge fields.
|
|
64
|
+
tool="\${tool//\\\\/\\\\\\\\}"; tool="\${tool//\\"/\\\\\\"}"
|
|
65
|
+
reason="\${reason//\\\\/\\\\\\\\}"; reason="\${reason//\\"/\\\\\\"}"
|
|
66
|
+
# Every C0 control character, not just newline and carriage return. JSON
|
|
67
|
+
# forbids all of U+0000-U+001F unescaped, so a plain TAB in a filename — no
|
|
68
|
+
# adversary required — was still enough to produce a line JSON.parse rejects.
|
|
69
|
+
# (NUL cannot appear in a bash variable, so the range starts at \\x01.)
|
|
70
|
+
tool="\${tool//[\$'\\x01'-\$'\\x1f']/ }"; reason="\${reason//[\$'\\x01'-\$'\\x1f']/ }"
|
|
71
|
+
# The workspace id is spliced into the same JSON string. It comes from
|
|
72
|
+
# runtime.env on disk rather than from the agent, so it is lower risk — but a
|
|
73
|
+
# value with a quote in it breaks every audit line the same way, and "lower
|
|
74
|
+
# risk" is not "cannot happen" for a file the operator edits by hand.
|
|
75
|
+
local ws="\${INTUTIC_WORKSPACE_ID}"
|
|
76
|
+
ws="\${ws//\\\\/\\\\\\\\}"; ws="\${ws//\\"/\\\\\\"}"; ws="\${ws//[\$'\\x01'-\$'\\x1f']/ }"
|
|
73
77
|
local ts; ts="\$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
74
78
|
# incidentId = sha1(timestamp + toolName + workspaceId).slice(0,16)
|
|
75
79
|
local incident_id; incident_id="\$(echo -n \"\${ts}\${tool}\${INTUTIC_WORKSPACE_ID}\" | sha1sum | cut -c1-16)"
|
|
76
|
-
local entry="{\\\"event\\\":\\\"
|
|
80
|
+
local entry="{\\\"event\\\":\\\"\${verdict}\\\",\\\"toolName\\\":\\\"\${tool}\\\",\\\"reason\\\":\\\"\${reason}\\\",\\\"workspaceId\\\":\\\"\${INTUTIC_WORKSPACE_ID}\\\",\\\"harnessType\\\":\\\"hermes\\\",\\\"incidentId\\\":\\\"\${incident_id}\\\",\\\"timestamp\\\":\\\"\${ts}\\\"}"
|
|
77
81
|
# Path B: reliable file append (sync-daemon drains on FSEvents change)
|
|
78
82
|
printf '%s\\n' "\$entry" >> "\$HOOK_EVENTS_LOG" 2>/dev/null || true
|
|
79
83
|
# Path A: fire-and-forget HTTP POST (near-real-time dashboard, non-blocking)
|
|
@@ -86,40 +90,7 @@ log_event() {
|
|
|
86
90
|
fi
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
${PROTECTED_PATHS.map((p) => ` "${p}"`).join('\n')}
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# 1. Protected-path guard
|
|
94
|
-
for p in "\${PROTECTED_PATHS[@]}"; do
|
|
95
|
-
if echo "$TARGET_PATH" | grep -qF "$p"; then
|
|
96
|
-
REASON="Attempt to modify governance-protected path \\"$TARGET_PATH\\""
|
|
97
|
-
echo "[Intutic Governance] BLOCKED: $REASON" >&2
|
|
98
|
-
log_event "tool_blocked" "$TOOL_NAME" "$REASON"
|
|
99
|
-
exit 2
|
|
100
|
-
fi
|
|
101
|
-
done
|
|
102
|
-
|
|
103
|
-
# 2. Bash/shell command guard — block governance bypass patterns
|
|
104
|
-
if [ -n "$COMMAND" ]; then
|
|
105
|
-
BYPASS_PATTERNS=(
|
|
106
|
-
"chflags nouchg"
|
|
107
|
-
"chattr -i"
|
|
108
|
-
"rm -rf .intutic"
|
|
109
|
-
"rm -rf .hermes/config"
|
|
110
|
-
"HERMES_PRE_TOOL_HOOK="
|
|
111
|
-
"rm -rf .gemini/settings"
|
|
112
|
-
)
|
|
113
|
-
for pattern in "\${BYPASS_PATTERNS[@]}"; do
|
|
114
|
-
if echo "$COMMAND" | grep -qF "$pattern"; then
|
|
115
|
-
REASON="Shell command matches governance bypass pattern: \\"$pattern\\""
|
|
116
|
-
echo "[Intutic Governance] BLOCKED: $REASON" >&2
|
|
117
|
-
log_event "tool_blocked" "$TOOL_NAME" "$REASON"
|
|
118
|
-
exit 2
|
|
119
|
-
fi
|
|
120
|
-
done
|
|
121
|
-
fi
|
|
122
|
-
|
|
93
|
+
${emitShellGate({ harness: "hermes" })}
|
|
123
94
|
log_event "tool_allowed" "$TOOL_NAME" ""
|
|
124
95
|
exit 0
|
|
125
96
|
`;
|
|
@@ -233,8 +204,8 @@ export async function writeHermesHooks(workspaceRoot, proxyUrl, workspaceId = ''
|
|
|
233
204
|
// ── 5. Write env snippet ───────────────────────────────────────────────────
|
|
234
205
|
const envSnippet = [
|
|
235
206
|
`# Intutic Hermes governance env`,
|
|
236
|
-
`
|
|
237
|
-
`
|
|
207
|
+
`INTUTIC_HERMES_HOOK=~/.intutic/hooks/hermes-check.sh`,
|
|
208
|
+
`INTUTIC_HERMES_CONFIG=~/.hermes/config.yaml`,
|
|
238
209
|
].join('\n') + '\n';
|
|
239
210
|
const envFilePath = path.join(envDir, 'hermes.env');
|
|
240
211
|
const tmpEnv = envFilePath + '.intutic-tmp';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermesHooks.js","sourceRoot":"","sources":["../../src/harness/hermesHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"hermesHooks.js","sourceRoot":"","sources":["../../src/harness/hermesHooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for the request context captured when a review hold fires.
|
|
3
|
+
*
|
|
4
|
+
* A hold snapshot is the one place in the product where raw tool *arguments*
|
|
5
|
+
* leave the developer's machine. `Write` carries file contents; `Bash` carries
|
|
6
|
+
* whole command lines; both routinely contain credentials that the developer
|
|
7
|
+
* never intended to publish. The proxy's DLP scanner does not help here — it
|
|
8
|
+
* sits on the HTTP path, and this runs inside the harness before any request
|
|
9
|
+
* exists.
|
|
10
|
+
*
|
|
11
|
+
* So the snapshot is redacted and capped **in the hook, before it touches
|
|
12
|
+
* disk** — not in the daemon on the way out. If it were done on the way out,
|
|
13
|
+
* the plaintext would already be sitting in `.intutic/events/`, and a file that
|
|
14
|
+
* exists is a file that gets copied into a bug report.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this is emitted rather than imported
|
|
17
|
+
*
|
|
18
|
+
* The generated hook is a standalone script with no module resolution — it runs
|
|
19
|
+
* as whatever `node` the harness invokes, in whatever directory. So the function
|
|
20
|
+
* below is serialised into it with `String()`. That is also why it must be
|
|
21
|
+
* **entirely self-contained**: no imports, no closure captures, no references to
|
|
22
|
+
* anything outside its own body — `emitRedactor` declares every module-level
|
|
23
|
+
* name these two reach, and a test evaluates the emitted text to prove it.
|
|
24
|
+
*
|
|
25
|
+
* Serialising the function instead of pasting a source string is deliberate.
|
|
26
|
+
* Four separate bugs in this file's history came from backticks and `${`
|
|
27
|
+
* sequences inside emitted code terminating the enclosing template literal.
|
|
28
|
+
* An interpolated *value* is never re-parsed, so that whole class is gone — and
|
|
29
|
+
* the function gets ordinary unit tests instead of being tested through a
|
|
30
|
+
* generated artifact.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
/** Longest string kept verbatim. Past this the middle is dropped, not the end. */
|
|
35
|
+
export declare const MAX_STRING = 2000;
|
|
36
|
+
/** Deepest object nesting walked. Below this, values become a marker. */
|
|
37
|
+
export declare const MAX_DEPTH = 6;
|
|
38
|
+
/** Most array elements kept. */
|
|
39
|
+
export declare const MAX_ARRAY = 50;
|
|
40
|
+
/** Hard ceiling on the serialised snapshot. */
|
|
41
|
+
export declare const MAX_SNAPSHOT_BYTES = 32768;
|
|
42
|
+
/**
|
|
43
|
+
* Returns a redacted, size-capped copy of `value`.
|
|
44
|
+
*
|
|
45
|
+
* Self-contained by contract — see the module docstring. Do not add imports or
|
|
46
|
+
* outer-scope references to this function; `emitRedactor()` asserts it stays
|
|
47
|
+
* closed over nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function redactSecrets(value: unknown, depth?: number): unknown;
|
|
50
|
+
/**
|
|
51
|
+
* Emits both functions as standalone JavaScript for the generated hook.
|
|
52
|
+
*
|
|
53
|
+
* The caps are declared ahead of the functions rather than substituted into
|
|
54
|
+
* their bodies, so one definition serves the emitted hook and the unit tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function emitRedactor(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Builds the snapshot the hook writes, redacted and under the byte ceiling.
|
|
59
|
+
*
|
|
60
|
+
* Serialised into the hook by `emitRedactor` and imported directly by the
|
|
61
|
+
* tests, so what the tests exercise is what runs.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildContextSnapshot(input: {
|
|
64
|
+
tool: string;
|
|
65
|
+
toolInput: unknown;
|
|
66
|
+
cwd: string;
|
|
67
|
+
reason: string;
|
|
68
|
+
}): Record<string, unknown>;
|
|
69
|
+
//# sourceMappingURL=holdRedaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"holdRedaction.d.ts","sourceRoot":"","sources":["../../src/harness/holdRedaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,kFAAkF;AAClF,eAAO,MAAM,UAAU,OAAQ,CAAA;AAC/B,yEAAyE;AACzE,eAAO,MAAM,SAAS,IAAI,CAAA;AAC1B,gCAAgC;AAChC,eAAO,MAAM,SAAS,KAAK,CAAA;AAC3B,+CAA+C;AAC/C,eAAO,MAAM,kBAAkB,QAAS,CAAA;AAExC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,OAAO,CAwGhE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAsBrC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkB1B"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for the request context captured when a review hold fires.
|
|
3
|
+
*
|
|
4
|
+
* A hold snapshot is the one place in the product where raw tool *arguments*
|
|
5
|
+
* leave the developer's machine. `Write` carries file contents; `Bash` carries
|
|
6
|
+
* whole command lines; both routinely contain credentials that the developer
|
|
7
|
+
* never intended to publish. The proxy's DLP scanner does not help here — it
|
|
8
|
+
* sits on the HTTP path, and this runs inside the harness before any request
|
|
9
|
+
* exists.
|
|
10
|
+
*
|
|
11
|
+
* So the snapshot is redacted and capped **in the hook, before it touches
|
|
12
|
+
* disk** — not in the daemon on the way out. If it were done on the way out,
|
|
13
|
+
* the plaintext would already be sitting in `.intutic/events/`, and a file that
|
|
14
|
+
* exists is a file that gets copied into a bug report.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this is emitted rather than imported
|
|
17
|
+
*
|
|
18
|
+
* The generated hook is a standalone script with no module resolution — it runs
|
|
19
|
+
* as whatever `node` the harness invokes, in whatever directory. So the function
|
|
20
|
+
* below is serialised into it with `String()`. That is also why it must be
|
|
21
|
+
* **entirely self-contained**: no imports, no closure captures, no references to
|
|
22
|
+
* anything outside its own body — `emitRedactor` declares every module-level
|
|
23
|
+
* name these two reach, and a test evaluates the emitted text to prove it.
|
|
24
|
+
*
|
|
25
|
+
* Serialising the function instead of pasting a source string is deliberate.
|
|
26
|
+
* Four separate bugs in this file's history came from backticks and `${`
|
|
27
|
+
* sequences inside emitted code terminating the enclosing template literal.
|
|
28
|
+
* An interpolated *value* is never re-parsed, so that whole class is gone — and
|
|
29
|
+
* the function gets ordinary unit tests instead of being tested through a
|
|
30
|
+
* generated artifact.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
/** Longest string kept verbatim. Past this the middle is dropped, not the end. */
|
|
35
|
+
export const MAX_STRING = 2_000;
|
|
36
|
+
/** Deepest object nesting walked. Below this, values become a marker. */
|
|
37
|
+
export const MAX_DEPTH = 6;
|
|
38
|
+
/** Most array elements kept. */
|
|
39
|
+
export const MAX_ARRAY = 50;
|
|
40
|
+
/** Hard ceiling on the serialised snapshot. */
|
|
41
|
+
export const MAX_SNAPSHOT_BYTES = 32_768;
|
|
42
|
+
/**
|
|
43
|
+
* Returns a redacted, size-capped copy of `value`.
|
|
44
|
+
*
|
|
45
|
+
* Self-contained by contract — see the module docstring. Do not add imports or
|
|
46
|
+
* outer-scope references to this function; `emitRedactor()` asserts it stays
|
|
47
|
+
* closed over nothing.
|
|
48
|
+
*/
|
|
49
|
+
export function redactSecrets(value, depth = 0) {
|
|
50
|
+
// Keys whose value is a credential regardless of what it looks like. Matched
|
|
51
|
+
// loosely on purpose: `apiKey`, `api_key`, `X-Api-Key` and `apikey` all occur.
|
|
52
|
+
const SECRET_KEY = /(pass(word|phrase)?|secret|token|api[-_ ]?key|auth(orization)?|credential|private[-_ ]?key|session[-_ ]?id|cookie)/i;
|
|
53
|
+
// Credential *shapes*, for values that arrive under an innocent key — the
|
|
54
|
+
// common case, since most of these appear inside a shell command string.
|
|
55
|
+
const SECRET_VALUE = [
|
|
56
|
+
/AKIA[0-9A-Z]{16}/g, // AWS access key id
|
|
57
|
+
/ASIA[0-9A-Z]{16}/g, // AWS temporary access key id
|
|
58
|
+
/sk-ant-[A-Za-z0-9_-]{16,}/g, // Anthropic
|
|
59
|
+
/sk-[A-Za-z0-9]{32,}/g, // OpenAI and lookalikes
|
|
60
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub
|
|
61
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
62
|
+
/xox[abprs]-[A-Za-z0-9-]{10,}/g, // Slack
|
|
63
|
+
/AIza[0-9A-Za-z_-]{30,}/g, // Google
|
|
64
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, // JWT
|
|
65
|
+
/-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,
|
|
66
|
+
// Bare basic-auth in a URL.
|
|
67
|
+
/\/\/[^/\s:@]+:[^/\s:@]+@/g,
|
|
68
|
+
];
|
|
69
|
+
// Assignment forms — `--token=…`, `PASSWORD=…`, `"secret": "…"`,
|
|
70
|
+
// `Authorization: Bearer …`. Matched in two steps, name first and value
|
|
71
|
+
// second, for two independent reasons.
|
|
72
|
+
//
|
|
73
|
+
// **Cost.** The single regex this replaces put two unbounded runs either side
|
|
74
|
+
// of the keyword alternation — `[A-Za-z0-9_-]*(token|…)[A-Za-z0-9_-]*` — so
|
|
75
|
+
// every start offset enumerated every split point of the identifier and the
|
|
76
|
+
// whole thing went quadratic. Measured on `'key'.repeat(n)`: 59 KB took 566 ms
|
|
77
|
+
// and 469 KB took 38 *seconds*, synchronously, inside a `PreToolUse` hook. The
|
|
78
|
+
// size caps do not bound it: `scrub` redacts the whole string and truncates to
|
|
79
|
+
// MAX_STRING only afterwards, and MAX_SNAPSHOT_BYTES applies later still. A
|
|
80
|
+
// `Write` carrying a few hundred KB of file content was enough. Matching the
|
|
81
|
+
// name alone is bounded by the identifier run, and the value is walked once,
|
|
82
|
+
// by hand, only after the name has been found interesting — 1.2 MB in ~25 ms.
|
|
83
|
+
//
|
|
84
|
+
// **Reach.** That same mandatory `[A-Za-z_]` before the alternation meant the
|
|
85
|
+
// keyword could only ever be an *infix* of the identifier, never the whole of
|
|
86
|
+
// it. So `MY_API_KEY=` redacted and `PASSWORD=`, `--token=`, `--credential=`,
|
|
87
|
+
// `passphrase=` and `"secret":` did not — all three of the forms the old
|
|
88
|
+
// comment offered as its examples passed through to disk in plaintext.
|
|
89
|
+
const NAME = /\b([A-Za-z_][A-Za-z0-9_-]*)(["']?\s*[:=]\s*["']?(?:Bearer\s+|Basic\s+)?)/g;
|
|
90
|
+
const NAME_IS_SECRET = /pass(word|phrase)?|secret|token|key|credential|auth(orization)?/i;
|
|
91
|
+
const VALUE_END = /[\s"',;]/;
|
|
92
|
+
const redactAssignments = (s) => {
|
|
93
|
+
let out = '';
|
|
94
|
+
let copied = 0;
|
|
95
|
+
let m;
|
|
96
|
+
NAME.lastIndex = 0;
|
|
97
|
+
while ((m = NAME.exec(s)) !== null) {
|
|
98
|
+
// Declining leaves `lastIndex` just past the separator rather than past a
|
|
99
|
+
// consumed value, so a boring name cannot shadow a secret packed into the
|
|
100
|
+
// same unbroken run — `--set=PGPASSWORD=…` still redacts.
|
|
101
|
+
if (!NAME_IS_SECRET.test(m[1]))
|
|
102
|
+
continue;
|
|
103
|
+
let end = NAME.lastIndex;
|
|
104
|
+
while (end < s.length && !VALUE_END.test(s[end]))
|
|
105
|
+
end += 1;
|
|
106
|
+
// Too short to be a credential, and short enough that redacting it would
|
|
107
|
+
// cost more context than it protects.
|
|
108
|
+
if (end - NAME.lastIndex < 8)
|
|
109
|
+
continue;
|
|
110
|
+
out += s.slice(copied, NAME.lastIndex) + '[redacted]';
|
|
111
|
+
copied = end;
|
|
112
|
+
NAME.lastIndex = end;
|
|
113
|
+
}
|
|
114
|
+
// The name and separator survive. A snapshot exists to be reviewed, and
|
|
115
|
+
// `--token=[redacted]` tells the reviewer what was held; a bare
|
|
116
|
+
// `[redacted]` swallowing the key name does not.
|
|
117
|
+
return copied === 0 ? s : out + s.slice(copied);
|
|
118
|
+
};
|
|
119
|
+
const scrub = (s) => {
|
|
120
|
+
let out = s;
|
|
121
|
+
for (const re of SECRET_VALUE)
|
|
122
|
+
out = out.replace(re, '[redacted]');
|
|
123
|
+
out = redactAssignments(out);
|
|
124
|
+
if (out.length > MAX_STRING) {
|
|
125
|
+
// Keep both ends. A command's verb is at the front and its target is at
|
|
126
|
+
// the back; truncating the tail throws away the half that identifies what
|
|
127
|
+
// the hold was about.
|
|
128
|
+
const half = Math.floor(MAX_STRING / 2);
|
|
129
|
+
out = `${out.slice(0, half)}…[${out.length - MAX_STRING} chars elided]…${out.slice(-half)}`;
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
};
|
|
133
|
+
if (depth > MAX_DEPTH)
|
|
134
|
+
return '[depth limit]';
|
|
135
|
+
if (value === null || value === undefined)
|
|
136
|
+
return value ?? null;
|
|
137
|
+
if (typeof value === 'string')
|
|
138
|
+
return scrub(value);
|
|
139
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
140
|
+
return value;
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
const kept = value.slice(0, MAX_ARRAY).map((v) => redactSecrets(v, depth + 1));
|
|
143
|
+
if (value.length > MAX_ARRAY)
|
|
144
|
+
kept.push(`[${value.length - MAX_ARRAY} more elided]`);
|
|
145
|
+
return kept;
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === 'object') {
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const [k, v] of Object.entries(value)) {
|
|
150
|
+
out[k] = SECRET_KEY.test(k) ? '[redacted]' : redactSecrets(v, depth + 1);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
// Functions, symbols, bigints — nothing a tool input should carry.
|
|
155
|
+
return `[${typeof value}]`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Emits both functions as standalone JavaScript for the generated hook.
|
|
159
|
+
*
|
|
160
|
+
* The caps are declared ahead of the functions rather than substituted into
|
|
161
|
+
* their bodies, so one definition serves the emitted hook and the unit tests.
|
|
162
|
+
*/
|
|
163
|
+
export function emitRedactor() {
|
|
164
|
+
// Bound under their SOURCE names first, then aliased.
|
|
165
|
+
//
|
|
166
|
+
// `buildContextSnapshot` calls `redactSecrets` by name, and a named function
|
|
167
|
+
// expression only supplies a binding for its own recursion — not for its
|
|
168
|
+
// siblings. Emitting solely as `__intuticSnapshot` produced a body calling a
|
|
169
|
+
// `redactSecrets` that did not exist in the hook, which threw inside the
|
|
170
|
+
// hold-write catch and silently produced no snapshot at all. The structural
|
|
171
|
+
// assertions did not catch it; evaluating the emitted text did, which is why
|
|
172
|
+
// that test exists.
|
|
173
|
+
return [
|
|
174
|
+
'// ── Intutic hold redaction ───────────────────────────────────────────',
|
|
175
|
+
'// Serialised from harness/holdRedaction.ts. Do not edit here.',
|
|
176
|
+
`const MAX_STRING = ${MAX_STRING};`,
|
|
177
|
+
`const MAX_DEPTH = ${MAX_DEPTH};`,
|
|
178
|
+
`const MAX_ARRAY = ${MAX_ARRAY};`,
|
|
179
|
+
`const MAX_SNAPSHOT_BYTES = ${MAX_SNAPSHOT_BYTES};`,
|
|
180
|
+
`const redactSecrets = ${String(redactSecrets)};`,
|
|
181
|
+
`const buildContextSnapshot = ${String(buildContextSnapshot)};`,
|
|
182
|
+
'const __intuticRedact = redactSecrets;',
|
|
183
|
+
'const __intuticSnapshot = buildContextSnapshot;',
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Builds the snapshot the hook writes, redacted and under the byte ceiling.
|
|
188
|
+
*
|
|
189
|
+
* Serialised into the hook by `emitRedactor` and imported directly by the
|
|
190
|
+
* tests, so what the tests exercise is what runs.
|
|
191
|
+
*/
|
|
192
|
+
export function buildContextSnapshot(input) {
|
|
193
|
+
const snapshot = {
|
|
194
|
+
tool: input.tool,
|
|
195
|
+
reason: input.reason,
|
|
196
|
+
cwd: redactSecrets(input.cwd),
|
|
197
|
+
toolInput: redactSecrets(input.toolInput),
|
|
198
|
+
};
|
|
199
|
+
const serialised = JSON.stringify(snapshot);
|
|
200
|
+
if (serialised.length > MAX_SNAPSHOT_BYTES) {
|
|
201
|
+
// Drop the arguments rather than truncating the JSON into something that
|
|
202
|
+
// will not parse. A snapshot that says why it is incomplete is usable; a
|
|
203
|
+
// snapshot that is silently half a document is not.
|
|
204
|
+
//
|
|
205
|
+
// No re-serialise: the replacement is a short string, so the result is
|
|
206
|
+
// necessarily under the cap, and nothing downstream reads a byte count.
|
|
207
|
+
snapshot['toolInput'] = `[elided: ${serialised.length} bytes over the ${MAX_SNAPSHOT_BYTES} cap]`;
|
|
208
|
+
}
|
|
209
|
+
return snapshot;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=holdRedaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"holdRedaction.js","sourceRoot":"","sources":["../../src/harness/holdRedaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jetbrainsXmlConfig.ts — narrow, dependency-free reader/writer for the
|
|
3
|
+
* JetBrains Platform's `<application><component name="X">...</component>
|
|
4
|
+
* </application>` settings-file shape.
|
|
5
|
+
*
|
|
6
|
+
* # Why this exists, and why it is not a general XML library
|
|
7
|
+
*
|
|
8
|
+
* `windsurfHooks.ts` needs to write into TWO files that live under a
|
|
9
|
+
* JetBrains IDE's `options/` directory — `proxy.settings.xml` (the IDE
|
|
10
|
+
* platform's own HTTP proxy setting) and `CodeiumSettings.xml` (the
|
|
11
|
+
* Windsurf plugin's own settings, including its `detectProxy` toggle) —
|
|
12
|
+
* and BOTH already exist on a real install, carrying settings this
|
|
13
|
+
* codebase has no business touching (a user's proxy exceptions list, their
|
|
14
|
+
* indexing path, dozens of unrelated plugin toggles, some of which — like
|
|
15
|
+
* `customTrackedWorkspaces` — serialize as a NESTED element, not a simple
|
|
16
|
+
* `<option name value/>` pair). A wholesale overwrite — the pattern
|
|
17
|
+
* `atomicWriteJson` uses for files this codebase fully owns, like
|
|
18
|
+
* `hooks.json` — would silently destroy those. This module instead
|
|
19
|
+
* MERGES at two levels:
|
|
20
|
+
*
|
|
21
|
+
* - Every OTHER `<component>` in the file is passed through completely
|
|
22
|
+
* unchanged — this module never parses their contents.
|
|
23
|
+
* - Within the ONE component the caller names, every simple self-closing
|
|
24
|
+
* `<option name="K" value="V" />` this module recognizes is
|
|
25
|
+
* added/overwritten by key; anything else inside that same component
|
|
26
|
+
* (a nested `<option name="X"><list>...</list></option>`, a comment,
|
|
27
|
+
* whatever) is preserved VERBATIM, appended after the merged simple
|
|
28
|
+
* options — never dropped, never guessed at.
|
|
29
|
+
*
|
|
30
|
+
* The only thing this module refuses outright (writes nothing, returns
|
|
31
|
+
* `false`) is content it cannot even locate an `<application>` root and
|
|
32
|
+
* `<component>` children in at all — a file this foreign is not this
|
|
33
|
+
* module's to touch.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Merges `desiredOptions` into `filePath`'s `<component name=
|
|
39
|
+
* componentName">` block as simple `<option name value/>` entries,
|
|
40
|
+
* creating the file/component fresh if absent. Every other component,
|
|
41
|
+
* and anything inside the target component this module does not
|
|
42
|
+
* recognize as a simple option, is preserved unchanged (see module doc
|
|
43
|
+
* comment). `desiredOptions` values are stringified as `"true"`/`"false"`
|
|
44
|
+
* for booleans, matching JetBrains' own XML serialization convention.
|
|
45
|
+
*
|
|
46
|
+
* Returns `false` (and writes nothing) when the existing file does not
|
|
47
|
+
* match the `<application><component>...` shape this module understands
|
|
48
|
+
* at all — refusal, not a guess.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mergeXmlComponentOptions(filePath: string, componentName: string, desiredOptions: Readonly<Record<string, string | boolean>>, readFile: (path: string) => Promise<string | null>, writeFileAtomic: (path: string, content: string) => Promise<void>): Promise<boolean>;
|
|
51
|
+
//# sourceMappingURL=jetbrainsXmlConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jetbrainsXmlConfig.d.ts","sourceRoot":"","sources":["../../src/harness/jetbrainsXmlConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAwGH;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,EAC1D,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,EAClD,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAChE,OAAO,CAAC,OAAO,CAAC,CA0BlB"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jetbrainsXmlConfig.ts — narrow, dependency-free reader/writer for the
|
|
3
|
+
* JetBrains Platform's `<application><component name="X">...</component>
|
|
4
|
+
* </application>` settings-file shape.
|
|
5
|
+
*
|
|
6
|
+
* # Why this exists, and why it is not a general XML library
|
|
7
|
+
*
|
|
8
|
+
* `windsurfHooks.ts` needs to write into TWO files that live under a
|
|
9
|
+
* JetBrains IDE's `options/` directory — `proxy.settings.xml` (the IDE
|
|
10
|
+
* platform's own HTTP proxy setting) and `CodeiumSettings.xml` (the
|
|
11
|
+
* Windsurf plugin's own settings, including its `detectProxy` toggle) —
|
|
12
|
+
* and BOTH already exist on a real install, carrying settings this
|
|
13
|
+
* codebase has no business touching (a user's proxy exceptions list, their
|
|
14
|
+
* indexing path, dozens of unrelated plugin toggles, some of which — like
|
|
15
|
+
* `customTrackedWorkspaces` — serialize as a NESTED element, not a simple
|
|
16
|
+
* `<option name value/>` pair). A wholesale overwrite — the pattern
|
|
17
|
+
* `atomicWriteJson` uses for files this codebase fully owns, like
|
|
18
|
+
* `hooks.json` — would silently destroy those. This module instead
|
|
19
|
+
* MERGES at two levels:
|
|
20
|
+
*
|
|
21
|
+
* - Every OTHER `<component>` in the file is passed through completely
|
|
22
|
+
* unchanged — this module never parses their contents.
|
|
23
|
+
* - Within the ONE component the caller names, every simple self-closing
|
|
24
|
+
* `<option name="K" value="V" />` this module recognizes is
|
|
25
|
+
* added/overwritten by key; anything else inside that same component
|
|
26
|
+
* (a nested `<option name="X"><list>...</list></option>`, a comment,
|
|
27
|
+
* whatever) is preserved VERBATIM, appended after the merged simple
|
|
28
|
+
* options — never dropped, never guessed at.
|
|
29
|
+
*
|
|
30
|
+
* The only thing this module refuses outright (writes nothing, returns
|
|
31
|
+
* `false`) is content it cannot even locate an `<application>` root and
|
|
32
|
+
* `<component>` children in at all — a file this foreign is not this
|
|
33
|
+
* module's to touch.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
const COMPONENT_RE = /<component\s+name="([^"]*)">([\s\S]*?)<\/component>/g;
|
|
38
|
+
const SIMPLE_OPTION_RE = /<option\s+name="([^"]*)"\s+value="([^"]*)"\s*\/>/g;
|
|
39
|
+
function escapeXmlAttr(value) {
|
|
40
|
+
return value
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/"/g, '"')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>');
|
|
45
|
+
}
|
|
46
|
+
function parseComponentInner(inner) {
|
|
47
|
+
const simpleOptions = new Map();
|
|
48
|
+
let extra = inner;
|
|
49
|
+
SIMPLE_OPTION_RE.lastIndex = 0;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = SIMPLE_OPTION_RE.exec(inner)) !== null) {
|
|
52
|
+
const [full, key, value] = match;
|
|
53
|
+
if (key === undefined || value === undefined || full === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
simpleOptions.set(key, value);
|
|
56
|
+
extra = extra.replace(full, '');
|
|
57
|
+
}
|
|
58
|
+
// Normalized to one trimmed line per non-empty line, deliberately
|
|
59
|
+
// dropping the ORIGINAL indentation depth: re-applying a fixed indent
|
|
60
|
+
// on top of already-indented text (rather than a normalized base) would
|
|
61
|
+
// make the file's whitespace grow by one indent level on every sync
|
|
62
|
+
// cycle this module runs in. XML semantics never depend on this
|
|
63
|
+
// whitespace, so normalizing costs nothing real and keeps repeated
|
|
64
|
+
// merges idempotent byte-for-byte.
|
|
65
|
+
const normalizedExtra = extra
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter((line) => line.length > 0)
|
|
69
|
+
.join('\n');
|
|
70
|
+
return { simpleOptions, extra: normalizedExtra };
|
|
71
|
+
}
|
|
72
|
+
/** Parses `raw` (the file's current content, or `null`/empty if it does
|
|
73
|
+
* not exist yet) looking for the `<application>` shape this module
|
|
74
|
+
* understands. An absent/empty file is `ok: true` with no target —
|
|
75
|
+
* "create fresh," not a parse failure. */
|
|
76
|
+
function parseApplicationXml(raw, targetComponentName) {
|
|
77
|
+
const otherComponents = new Map();
|
|
78
|
+
if (raw === null || raw.trim().length === 0) {
|
|
79
|
+
return { ok: true, otherComponents, target: undefined };
|
|
80
|
+
}
|
|
81
|
+
const appMatch = /^\s*(?:<\?xml[^>]*\?>\s*)?<application>([\s\S]*)<\/application>\s*$/.exec(raw);
|
|
82
|
+
if (!appMatch) {
|
|
83
|
+
return { ok: false, otherComponents, target: undefined };
|
|
84
|
+
}
|
|
85
|
+
const body = appMatch[1] ?? '';
|
|
86
|
+
let target;
|
|
87
|
+
let match;
|
|
88
|
+
COMPONENT_RE.lastIndex = 0;
|
|
89
|
+
while ((match = COMPONENT_RE.exec(body)) !== null) {
|
|
90
|
+
const [full, name, inner] = match;
|
|
91
|
+
if (name === undefined || inner === undefined || full === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
if (name === targetComponentName) {
|
|
94
|
+
target = parseComponentInner(inner);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
otherComponents.set(name, full);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Anything at the <application> level that is not whitespace and not one
|
|
101
|
+
// of the <component> blocks just consumed is content this module does
|
|
102
|
+
// not understand — refuse the whole file rather than silently drop it.
|
|
103
|
+
// (Length-based, not substring-removal: two components could otherwise
|
|
104
|
+
// have byte-identical bodies and make removal ambiguous.)
|
|
105
|
+
const nonWhitespaceLength = body.replace(/\s/g, '').length;
|
|
106
|
+
const consumedNonWhitespaceLength = [...body.matchAll(COMPONENT_RE)]
|
|
107
|
+
.map((m) => (m[0] ?? '').replace(/\s/g, '').length)
|
|
108
|
+
.reduce((a, b) => a + b, 0);
|
|
109
|
+
if (nonWhitespaceLength !== consumedNonWhitespaceLength) {
|
|
110
|
+
return { ok: false, otherComponents, target: undefined };
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, otherComponents, target };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Merges `desiredOptions` into `filePath`'s `<component name=
|
|
116
|
+
* componentName">` block as simple `<option name value/>` entries,
|
|
117
|
+
* creating the file/component fresh if absent. Every other component,
|
|
118
|
+
* and anything inside the target component this module does not
|
|
119
|
+
* recognize as a simple option, is preserved unchanged (see module doc
|
|
120
|
+
* comment). `desiredOptions` values are stringified as `"true"`/`"false"`
|
|
121
|
+
* for booleans, matching JetBrains' own XML serialization convention.
|
|
122
|
+
*
|
|
123
|
+
* Returns `false` (and writes nothing) when the existing file does not
|
|
124
|
+
* match the `<application><component>...` shape this module understands
|
|
125
|
+
* at all — refusal, not a guess.
|
|
126
|
+
*/
|
|
127
|
+
export async function mergeXmlComponentOptions(filePath, componentName, desiredOptions, readFile, writeFileAtomic) {
|
|
128
|
+
const existing = await readFile(filePath);
|
|
129
|
+
const parsed = parseApplicationXml(existing, componentName);
|
|
130
|
+
if (!parsed.ok)
|
|
131
|
+
return false;
|
|
132
|
+
const options = parsed.target?.simpleOptions ?? new Map();
|
|
133
|
+
for (const [key, value] of Object.entries(desiredOptions)) {
|
|
134
|
+
options.set(key, typeof value === 'boolean' ? String(value) : value);
|
|
135
|
+
}
|
|
136
|
+
const optionLines = [...options.entries()]
|
|
137
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
138
|
+
.map(([key, value]) => ` <option name="${escapeXmlAttr(key)}" value="${escapeXmlAttr(value)}" />`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
const extra = parsed.target?.extra;
|
|
141
|
+
const componentBody = extra ? `${optionLines}\n${extra.split('\n').map((l) => ` ${l}`).join('\n')}` : optionLines;
|
|
142
|
+
const componentBlock = ` <component name="${escapeXmlAttr(componentName)}">\n${componentBody}\n </component>`;
|
|
143
|
+
const otherBlocks = [...parsed.otherComponents.entries()]
|
|
144
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
145
|
+
.map(([, block]) => ` ${block}`);
|
|
146
|
+
const body = [componentBlock, ...otherBlocks].join('\n');
|
|
147
|
+
const serialized = `<application>\n${body}\n</application>\n`;
|
|
148
|
+
await writeFileAtomic(filePath, serialized);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=jetbrainsXmlConfig.js.map
|