skalpel 3.3.2 → 3.3.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
4
4
|
"description": "Skalpel — local proxy and TUI for coding agents (skalpel + skalpeld bundle).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://skalpel.ai",
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"x64"
|
|
59
59
|
],
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@skalpelai/skalpel-darwin-arm64": "3.3.
|
|
62
|
-
"@skalpelai/skalpel-darwin-x64": "3.3.
|
|
63
|
-
"@skalpelai/skalpel-linux-arm64": "3.3.
|
|
64
|
-
"@skalpelai/skalpel-linux-x64": "3.3.
|
|
65
|
-
"@skalpelai/skalpel-win32-x64": "3.3.
|
|
61
|
+
"@skalpelai/skalpel-darwin-arm64": "3.3.4",
|
|
62
|
+
"@skalpelai/skalpel-darwin-x64": "3.3.4",
|
|
63
|
+
"@skalpelai/skalpel-linux-arm64": "3.3.4",
|
|
64
|
+
"@skalpelai/skalpel-linux-x64": "3.3.4",
|
|
65
|
+
"@skalpelai/skalpel-win32-x64": "3.3.4"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/postinstall/index.js
CHANGED
|
@@ -198,10 +198,23 @@ async function main(argv) {
|
|
|
198
198
|
allWarnings.push(`ca-install: ${err.message}`);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
201
|
+
// Step 5 — shell activation is OPT-IN (incident 2026-07-05). A global
|
|
202
|
+
// `npm install -g skalpel` must NOT modify the user's shell rc files. That
|
|
203
|
+
// was the blast radius of the incident: the managed block exported
|
|
204
|
+
// ANTHROPIC_BASE_URL into every shell and broke Claude Code, and even a
|
|
205
|
+
// poison-free wrapper is too invasive to write into a user's shell without
|
|
206
|
+
// consent for an agent harness. The daemon registered in step 3 sits IDLE
|
|
207
|
+
// and intercepts nothing until the user explicitly opts in with
|
|
208
|
+
// `skalpel setup`, which wraps the detected agents (setupCore -> the same
|
|
209
|
+
// managed block, now poison-free). Set SKALPEL_ACTIVATE_SHELL=1 to restore
|
|
210
|
+
// the old auto-wrap-on-install behavior.
|
|
211
|
+
if (process.env.SKALPEL_ACTIVATE_SHELL === '1') {
|
|
212
|
+
log.step(5, total, 'env-inject', 'managed-block edit per shell (SKALPEL_ACTIVATE_SHELL=1)');
|
|
213
|
+
envInject.run({ dryRun: opts.dryRun });
|
|
214
|
+
} else {
|
|
215
|
+
log.step(5, total, 'shell-activation', 'deferred — opt-in via `skalpel setup`');
|
|
216
|
+
log.info('Shell integration is opt-in. Run `skalpel setup` to route Claude/Codex through skalpel.');
|
|
217
|
+
}
|
|
205
218
|
|
|
206
219
|
log.step(6, total, 'launch', 'next-step hint');
|
|
207
220
|
launch.run({ dryRun: opts.dryRun });
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// envBlockValues
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// envBlockValues returns the env vars the managed rc-block should export
|
|
4
|
+
// GLOBALLY (into every shell). Under the MITM regime this is now EMPTY, and
|
|
5
|
+
// that is load-bearing, not an oversight:
|
|
6
|
+
//
|
|
7
|
+
// The old base-URL regime exported ANTHROPIC_BASE_URL/_API_URL (+ OpenAI
|
|
8
|
+
// base vars) = http://127.0.0.1:<port> into every shell so agents routed
|
|
9
|
+
// through the loopback proxy. That globally rewired Claude/Codex's provider
|
|
10
|
+
// endpoint. When the daemon was down/unhealthy/removed, Claude hit a non-
|
|
11
|
+
// first-party host -> Connection error -> synthetic "Not logged in", and any
|
|
12
|
+
// Claude process that inherited the var WITHOUT going through the claude()
|
|
13
|
+
// shell wrapper (a subprocess, an IDE, `command claude`) could not self-heal.
|
|
14
|
+
// That broke real Claude Code sessions (incident 2026-07-05).
|
|
15
|
+
//
|
|
16
|
+
// MITM does NOT need any of these. Interception is per-launch: `skalpel
|
|
17
|
+
// claude-exec` / `codex-exec` set HTTPS_PROXY + NODE_EXTRA_CA_CERTS at exec
|
|
18
|
+
// time and explicitly UNSET ANTHROPIC_BASE_URL/_API_URL (see cmd/skalpel/
|
|
19
|
+
// claude_exec.go). So NOTHING routing-related belongs in the global rc block.
|
|
20
|
+
// Returning {} means the managed block carries only the fail-open wrappers,
|
|
21
|
+
// and a bare/subprocess Claude is never poisoned — it talks to Anthropic
|
|
22
|
+
// directly, which is the correct default.
|
|
23
|
+
//
|
|
24
|
+
// `port` is retained in the signature for call-site compatibility.
|
|
8
25
|
function envBlockValues(port) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
ANTHROPIC_API_URL: root,
|
|
13
|
-
ANTHROPIC_BASE_URL: root,
|
|
14
|
-
OPENAI_BASE_URL: `${root}/v1`,
|
|
15
|
-
OPENAI_API_BASE: `${root}/v1`,
|
|
16
|
-
SKALPEL_PROXY_URL: root,
|
|
17
|
-
};
|
|
26
|
+
void port;
|
|
27
|
+
return {};
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
const claudeCode = {
|
|
@@ -72,8 +72,12 @@ function run() {
|
|
|
72
72
|
1,
|
|
73
73
|
'managed block end marker must appear exactly once'
|
|
74
74
|
);
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// MITM regime (incident 2026-07-05): the managed block must NEVER
|
|
76
|
+
// globally export a base-URL / proxy var. Interception is per-launch via
|
|
77
|
+
// claude-exec/codex-exec; the block carries only the fail-open wrappers.
|
|
78
|
+
assert.ok(!after.includes('127.0.0.1:7878'), 'no loopback proxy URL is exported globally');
|
|
79
|
+
assert.ok(!after.includes('export ANTHROPIC_BASE_URL'), 'ANTHROPIC_BASE_URL is never globally exported');
|
|
80
|
+
assert.ok(after.includes('claude-exec'), 'the fail-open claude wrapper is still installed');
|
|
77
81
|
assert.ok(after.includes('# user content'), 'user content preserved');
|
|
78
82
|
assert.ok(after.includes('export FOO=bar'), 'user export preserved');
|
|
79
83
|
} finally {
|
|
@@ -106,7 +110,8 @@ function run() {
|
|
|
106
110
|
rc.applyBlock({ shell: 'fish', file, env });
|
|
107
111
|
const after = fs.readFileSync(file, 'utf8');
|
|
108
112
|
assert.strictEqual(countMatches(after, rc.FENCE_BEGIN_POSIX), 1);
|
|
109
|
-
assert.ok(after.includes('set -gx ANTHROPIC_API_URL'));
|
|
113
|
+
assert.ok(!after.includes('set -gx ANTHROPIC_API_URL'), 'MITM: no global base-URL export in fish');
|
|
114
|
+
assert.ok(!after.includes('127.0.0.1:7878'), 'MITM: no loopback proxy URL exported in fish');
|
|
110
115
|
assert.ok(after.includes('# fish prelude'), 'fish prelude preserved');
|
|
111
116
|
} finally {
|
|
112
117
|
fs.unlinkSync(file);
|
|
@@ -123,23 +128,27 @@ function run() {
|
|
|
123
128
|
const after = fs.readFileSync(file, 'utf8');
|
|
124
129
|
assert.strictEqual(countMatches(after, rc.FENCE_BEGIN_PS), 1);
|
|
125
130
|
assert.strictEqual(countMatches(after, rc.FENCE_END_PS), 1);
|
|
126
|
-
assert.ok(after.includes("
|
|
131
|
+
assert.ok(!after.includes("127.0.0.1:7878"), 'MITM: no base-URL export in the powershell block');
|
|
127
132
|
} finally {
|
|
128
133
|
fs.unlinkSync(file);
|
|
129
134
|
}
|
|
130
135
|
});
|
|
131
136
|
|
|
132
|
-
test('
|
|
137
|
+
test('TestRcEdit_Block_Is_Port_Independent_Under_MITM', () => {
|
|
138
|
+
// Under MITM the managed block exports NO base-URL var, so the proxy port
|
|
139
|
+
// never appears in it — the port lives in the daemon config, not the shell
|
|
140
|
+
// env. Changing the port is therefore a no-op for the rc block. (This test
|
|
141
|
+
// replaces the old TestRcEdit_Replaces_When_Port_Changes, whose premise —
|
|
142
|
+
// the port being baked into a global export — was the 2026-07-05 poison.)
|
|
133
143
|
const file = tmpFile('rcedit-port');
|
|
134
144
|
fs.writeFileSync(file, '');
|
|
135
145
|
try {
|
|
136
146
|
rc.applyBlock({ shell: 'bash', file, env: rc.envBlockValues(7878) });
|
|
137
147
|
const r2 = rc.applyBlock({ shell: 'bash', file, env: rc.envBlockValues(9999) });
|
|
138
|
-
assert.strictEqual(r2.changed,
|
|
148
|
+
assert.strictEqual(r2.changed, false, 'block is port-independent (no URL is exported)');
|
|
139
149
|
const after = fs.readFileSync(file, 'utf8');
|
|
140
150
|
assert.strictEqual(countMatches(after, rc.FENCE_BEGIN_POSIX), 1);
|
|
141
|
-
assert.ok(after.includes('
|
|
142
|
-
assert.ok(!after.includes('http://127.0.0.1:7878'));
|
|
151
|
+
assert.ok(!after.includes('127.0.0.1'), 'no loopback URL anywhere in the managed block');
|
|
143
152
|
} finally {
|
|
144
153
|
fs.unlinkSync(file);
|
|
145
154
|
}
|