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.2",
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.2",
62
- "@skalpelai/skalpel-darwin-x64": "3.3.2",
63
- "@skalpelai/skalpel-linux-arm64": "3.3.2",
64
- "@skalpelai/skalpel-linux-x64": "3.3.2",
65
- "@skalpelai/skalpel-win32-x64": "3.3.2"
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
  }
@@ -198,10 +198,23 @@ async function main(argv) {
198
198
  allWarnings.push(`ca-install: ${err.message}`);
199
199
  }
200
200
 
201
- log.step(5, total, 'env-inject', 'managed-block edit per shell');
202
- // env-inject is critical: missing managed block means agents
203
- // never see the proxy URL. Errors propagate.
204
- envInject.run({ dryRun: opts.dryRun });
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 defines the five vars the managed rc-block exports so
4
- // agents see the daemon's loopback proxy. Under the MITM Codex regime
5
- // (Option A) the Codex wrapper no longer needs SKALPEL_CODEX_* base-URL
6
- // vars — `skalpel codex-exec` sets HTTPS_PROXY at exec time and the
7
- // daemon TLS-terminates chatgpt.com:443 directly.
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
- const p = String(port || 7878);
10
- const root = `http://127.0.0.1:${p}`;
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
- assert.ok(after.includes('export ANTHROPIC_API_URL="http://127.0.0.1:7878"'));
76
- assert.ok(after.includes('export OPENAI_BASE_URL="http://127.0.0.1:7878/v1"'));
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("$env:ANTHROPIC_API_URL = 'http://127.0.0.1:7878'"));
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('TestRcEdit_Replaces_When_Port_Changes', () => {
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, true, 'changing port should rewrite');
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('http://127.0.0.1:9999'));
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
  }