skalpel 3.1.9 → 3.2.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/INSTALL.md +23 -0
- package/package.json +7 -7
- package/postinstall/index.js +57 -5
- package/postinstall/lib/ca-install.js +188 -0
- package/postinstall/lib/ca-install.test.js +228 -0
- package/postinstall/lib/integrations.js +66 -50
- package/postinstall/lib/rc-edit.test.js +9 -4
- package/postinstall/lib/run-tests.js +59 -0
- package/postinstall/lib/service-register.js +12 -1
package/INSTALL.md
CHANGED
|
@@ -134,3 +134,26 @@ A small set of bundling questions are recorded here for the build phase. Each wi
|
|
|
134
134
|
- `SPEC.md` — Skalpel TUI design dockets: §01 (the daemon non-supervisor stance), §03 Journey 1 (first-launch fresh-authentication flow that this document's first-run flow corresponds to), §05 (privacy stance referenced in the install-time telemetry open question).
|
|
135
135
|
- `../../design-dockets/cross-surface-contract.md` — Cross-surface contract: the Auth handoff section (machine-client identity, the `npx skalpel` install wizard arriving in the same transaction as the daemon), the ownership matrix's Post-signup install row.
|
|
136
136
|
- `../../Skalpel_Infrastructure/INFRASTRUCTURE.md` — CLI Distribution section (npm, GitHub Releases, Sigstore signing, the `cli/latest` auto-update endpoint, emergency-release fallback path).
|
|
137
|
+
|
|
138
|
+
## Codex setup (macOS)
|
|
139
|
+
|
|
140
|
+
During `npm install -g skalpel` on macOS you will see a **one-time Touch ID / password prompt** from the system. This prompt is the macOS keychain asking you to trust the daemon's local intercept CA in your **login keychain**. The trust is needed because Codex CLI is Rust and validates TLS against the OS keychain — without the trust, `codex` would reject the daemon's MITM leaf certs and skalpel would never see Codex traffic.
|
|
141
|
+
|
|
142
|
+
**Scope of trust:**
|
|
143
|
+
|
|
144
|
+
- Per-user (your `login.keychain-db`), not system-wide.
|
|
145
|
+
- Trusts only the daemon-minted local CA labelled `Skalpel Local Intercept CA`.
|
|
146
|
+
- Used exclusively for `chatgpt.com` / `api.openai.com` traffic the daemon intercepts; every other host is blind-tunnelled untouched.
|
|
147
|
+
|
|
148
|
+
**If you missed the prompt** or declined it, run `skalpel login` or invoke `codex` once — the postinstall step writes a deferred-install sentinel and the next entry point retries the keychain install.
|
|
149
|
+
|
|
150
|
+
**To revoke trust:**
|
|
151
|
+
|
|
152
|
+
1. Open **Keychain Access**.
|
|
153
|
+
2. Select the `login` keychain.
|
|
154
|
+
3. Search for `Skalpel Local Intercept CA`.
|
|
155
|
+
4. Right-click → Delete.
|
|
156
|
+
|
|
157
|
+
After deletion, Codex traffic falls back to bare TLS (daemon won't see it). Re-install the CA via `skalpel login` if you change your mind.
|
|
158
|
+
|
|
159
|
+
**Linux / Windows status:** the trust-store install path is **not yet shipped** on Linux or Windows. Codex MITM works on macOS only in this release. On other platforms the postinstall step skips the keychain step with a printed notice; the user can configure trust manually if they need Codex visibility.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"preinstall": "node postinstall/preinstall.js",
|
|
32
32
|
"postinstall": "node postinstall/index.js",
|
|
33
33
|
"preuninstall": "node postinstall/uninstall.js",
|
|
34
|
-
"test": "
|
|
34
|
+
"test": "node postinstall/lib/run-tests.js",
|
|
35
35
|
"test:rc-edit": "node postinstall/lib/rc-edit.test.js",
|
|
36
36
|
"test:postinstall": "node --test postinstall/index.test.js",
|
|
37
37
|
"test:preinstall": "node --test postinstall/preinstall.test.js"
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"x64"
|
|
58
58
|
],
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@skalpelai/skalpel-darwin-arm64": "3.
|
|
61
|
-
"@skalpelai/skalpel-darwin-x64": "3.
|
|
62
|
-
"@skalpelai/skalpel-linux-arm64": "3.
|
|
63
|
-
"@skalpelai/skalpel-linux-x64": "3.
|
|
64
|
-
"@skalpelai/skalpel-win32-x64": "3.
|
|
60
|
+
"@skalpelai/skalpel-darwin-arm64": "3.2.0",
|
|
61
|
+
"@skalpelai/skalpel-darwin-x64": "3.2.0",
|
|
62
|
+
"@skalpelai/skalpel-linux-arm64": "3.2.0",
|
|
63
|
+
"@skalpelai/skalpel-linux-x64": "3.2.0",
|
|
64
|
+
"@skalpelai/skalpel-win32-x64": "3.2.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/postinstall/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const signIn = require('./lib/sign-in');
|
|
|
27
27
|
const serviceRegister = require('./lib/service-register');
|
|
28
28
|
const envInject = require('./lib/env-inject');
|
|
29
29
|
const launch = require('./lib/launch');
|
|
30
|
+
const caInstall = require('./lib/ca-install');
|
|
30
31
|
|
|
31
32
|
function printStyledSuccess() {
|
|
32
33
|
if (!process.stdout.isTTY) return;
|
|
@@ -93,7 +94,35 @@ function helpText() {
|
|
|
93
94
|
].join('\n');
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
function
|
|
97
|
+
async function runCAInstallStep(opts) {
|
|
98
|
+
// Resolve CA path by spawning the just-installed `skalpel ca-path`.
|
|
99
|
+
// (The Go binary is on PATH by the time postinstall runs; see
|
|
100
|
+
// npm-bin/skalpel.js for the resolver.)
|
|
101
|
+
const cp = require('child_process');
|
|
102
|
+
let caPath = '';
|
|
103
|
+
try {
|
|
104
|
+
const out = cp.spawnSync('skalpel', ['ca-path'], {
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
timeout: 5_000,
|
|
107
|
+
});
|
|
108
|
+
if (out.status === 0) {
|
|
109
|
+
caPath = String(out.stdout || '').trim();
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {
|
|
112
|
+
// ignored — fall through to deferred-no-ca branch
|
|
113
|
+
}
|
|
114
|
+
if (!caPath) {
|
|
115
|
+
log.warn('ca-install: could not resolve `skalpel ca-path`; deferring keychain trust to first login');
|
|
116
|
+
return { ok: false, reason: 'no-binary' };
|
|
117
|
+
}
|
|
118
|
+
if (opts.dryRun) {
|
|
119
|
+
log.info(`ca-install: dry-run; would install ${caPath} into login keychain`);
|
|
120
|
+
return { ok: true, skipped: true, reason: 'dry-run' };
|
|
121
|
+
}
|
|
122
|
+
return caInstall.installMacOSCA(caPath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(argv) {
|
|
97
126
|
const opts = parseArgs(argv);
|
|
98
127
|
if (opts.help) {
|
|
99
128
|
process.stdout.write(`${helpText()}\n`);
|
|
@@ -118,7 +147,7 @@ function main(argv) {
|
|
|
118
147
|
return 0;
|
|
119
148
|
}
|
|
120
149
|
|
|
121
|
-
const total =
|
|
150
|
+
const total = 6;
|
|
122
151
|
const mode = opts.dryRun ? 'dry-run' : 'live';
|
|
123
152
|
log.info(`postinstall wizard starting (${mode} install) on ${process.platform}`);
|
|
124
153
|
|
|
@@ -155,12 +184,28 @@ function main(argv) {
|
|
|
155
184
|
allWarnings.push(...sr.warnings);
|
|
156
185
|
}
|
|
157
186
|
|
|
158
|
-
log.step(4, total, '
|
|
187
|
+
log.step(4, total, 'ca-install', `keychain trust on ${process.platform}`);
|
|
188
|
+
// ca-install is NON-CRITICAL: a declined Touch ID prompt must NOT
|
|
189
|
+
// fail postinstall. The deferred-install sentinel ensures first
|
|
190
|
+
// `skalpel login` / `codex` invocation retries the trust. On
|
|
191
|
+
// non-darwin we log + skip; Linux/Windows trust-store install is
|
|
192
|
+
// deferred to a later bundle.
|
|
193
|
+
try {
|
|
194
|
+
const caRes = await runCAInstallStep(opts);
|
|
195
|
+
if (caRes && !caRes.ok && caRes.reason) {
|
|
196
|
+
allWarnings.push(`ca-install: ${caRes.reason}`);
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log.warn(`ca-install: ${err.message}; continuing`);
|
|
200
|
+
allWarnings.push(`ca-install: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
log.step(5, total, 'env-inject', 'managed-block edit per shell');
|
|
159
204
|
// env-inject is critical: missing managed block means agents
|
|
160
205
|
// never see the proxy URL. Errors propagate.
|
|
161
206
|
envInject.run({ dryRun: opts.dryRun });
|
|
162
207
|
|
|
163
|
-
log.step(
|
|
208
|
+
log.step(6, total, 'launch', 'next-step hint');
|
|
164
209
|
launch.run({ dryRun: opts.dryRun });
|
|
165
210
|
} catch (err) {
|
|
166
211
|
critical = err;
|
|
@@ -187,7 +232,14 @@ function main(argv) {
|
|
|
187
232
|
}
|
|
188
233
|
|
|
189
234
|
if (require.main === module) {
|
|
190
|
-
|
|
235
|
+
Promise.resolve(main(process.argv)).then(
|
|
236
|
+
(code) => process.exit(code || 0),
|
|
237
|
+
(err) => {
|
|
238
|
+
log.error(`postinstall crashed: ${err && err.stack ? err.stack : err}`);
|
|
239
|
+
// Wizard contract: exit 0 even on crash so npm install never aborts.
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
191
243
|
}
|
|
192
244
|
|
|
193
245
|
module.exports = { main, parseArgs };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// macOS keychain CA install for the daemon's MITM CA.
|
|
4
|
+
//
|
|
5
|
+
// Codex CLI is Rust and consults the OS keychain (Security Framework on
|
|
6
|
+
// macOS) for trust. To make `skalpel codex-exec` work end-to-end on a
|
|
7
|
+
// fresh `npm install -g skalpel`, the postinstall flow trusts the
|
|
8
|
+
// daemon's local CA in the user's login keychain. This module owns that
|
|
9
|
+
// step.
|
|
10
|
+
//
|
|
11
|
+
// Contract:
|
|
12
|
+
// installMacOSCA(caPath, opts) returns
|
|
13
|
+
// { ok: true } — installed (or already trusted)
|
|
14
|
+
// { ok: true, skipped: true, reason: '...' } — non-darwin or idempotent skip
|
|
15
|
+
// { ok: false, reason: '...' } — user declined / timeout /
|
|
16
|
+
// deferred (CA not yet on disk)
|
|
17
|
+
//
|
|
18
|
+
// Failure is NEVER fatal to postinstall. The user falls back to a
|
|
19
|
+
// printed-instruction path and the next `skalpel codex-exec` /
|
|
20
|
+
// `skalpel login` retries via the Go-side `internal/cainstall`
|
|
21
|
+
// RetrySentinel helper.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const childProcess = require('child_process');
|
|
27
|
+
|
|
28
|
+
const KEYCHAIN_LABEL = 'Skalpel Local Intercept CA';
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
|
+
const SENTINEL_BASENAME = '.ca-install-pending';
|
|
31
|
+
|
|
32
|
+
// loginKeychainPath returns the path to the user's login keychain on
|
|
33
|
+
// macOS. macOS Catalina+ uses .keychain-db.
|
|
34
|
+
function loginKeychainPath(homedir) {
|
|
35
|
+
return path.join(homedir, 'Library', 'Keychains', 'login.keychain-db');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logMsg(stream, msg) {
|
|
39
|
+
if (stream && typeof stream.write === 'function') {
|
|
40
|
+
stream.write(`${msg}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// installMacOSCA installs the daemon's MITM CA into the user's login
|
|
45
|
+
// keychain, prompting the user for Touch ID / password. Idempotent —
|
|
46
|
+
// re-running after a successful install is a no-op.
|
|
47
|
+
//
|
|
48
|
+
// opts:
|
|
49
|
+
// spawn: optional injected child_process.spawn for tests
|
|
50
|
+
// stderr: optional Writable for status messages (defaults to process.stderr)
|
|
51
|
+
// homedir: optional homedir for tests (defaults to os.homedir())
|
|
52
|
+
// timeoutMs: optional timeout override (defaults to 60s)
|
|
53
|
+
// sentinelDir: optional dir where the deferred-install sentinel is written
|
|
54
|
+
// when caPath does not exist (defaults to dirname(caPath))
|
|
55
|
+
async function installMacOSCA(caPath, opts) {
|
|
56
|
+
const o = opts || {};
|
|
57
|
+
const spawn = o.spawn || childProcess.spawn;
|
|
58
|
+
const stderr = o.stderr || process.stderr;
|
|
59
|
+
const homedir = o.homedir || os.homedir();
|
|
60
|
+
const timeoutMs = o.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
61
|
+
const platform = o.platform || process.platform;
|
|
62
|
+
|
|
63
|
+
if (platform !== 'darwin') {
|
|
64
|
+
logMsg(
|
|
65
|
+
stderr,
|
|
66
|
+
`Skalpel: Codex MITM CA install skipped on ${platform}; Linux/Windows ` +
|
|
67
|
+
`trust-store install not yet implemented. Codex will fall back to bare ` +
|
|
68
|
+
`TLS — daemon won't see Codex traffic until trust is configured manually.`
|
|
69
|
+
);
|
|
70
|
+
return { ok: true, skipped: true, reason: 'platform' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(caPath)) {
|
|
74
|
+
// Daemon hasn't generated the CA yet. Drop a sentinel and let the
|
|
75
|
+
// Go-side retry path pick it up on next `skalpel login` /
|
|
76
|
+
// `skalpel codex-exec`.
|
|
77
|
+
const sentinelDir = o.sentinelDir || path.dirname(caPath);
|
|
78
|
+
const sentinelPath = path.join(sentinelDir, SENTINEL_BASENAME);
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(sentinelDir, { recursive: true });
|
|
81
|
+
fs.writeFileSync(sentinelPath, '');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logMsg(stderr, `Skalpel: could not write ca-install sentinel at ${sentinelPath}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
logMsg(
|
|
86
|
+
stderr,
|
|
87
|
+
'Skalpel: daemon CA not yet generated — will install on first `skalpel login` or `codex` invocation.'
|
|
88
|
+
);
|
|
89
|
+
return { ok: false, reason: 'deferred-no-ca', sentinelPath };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const loginKC = loginKeychainPath(homedir);
|
|
93
|
+
|
|
94
|
+
// Idempotency: probe first via `security find-certificate -c <label>
|
|
95
|
+
// <keychain>`. Exit 0 → already trusted; nothing to do.
|
|
96
|
+
if (await findCertificate(spawn, loginKC, timeoutMs)) {
|
|
97
|
+
return { ok: true, skipped: true, reason: 'already-installed' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await runAddTrustedCert(spawn, stderr, caPath, loginKC, timeoutMs);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// findCertificate runs `security find-certificate -c <label> <kc>` and
|
|
104
|
+
// resolves to true iff the certificate is already trusted in the user's
|
|
105
|
+
// login keychain.
|
|
106
|
+
function findCertificate(spawn, loginKC, timeoutMs) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const child = spawn('security', ['find-certificate', '-c', KEYCHAIN_LABEL, loginKC], {
|
|
109
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
110
|
+
});
|
|
111
|
+
let settled = false;
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
if (settled) return;
|
|
114
|
+
settled = true;
|
|
115
|
+
try { child.kill('SIGTERM'); } catch (_) { /* noop */ }
|
|
116
|
+
resolve(false);
|
|
117
|
+
}, timeoutMs);
|
|
118
|
+
child.on('error', () => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(false);
|
|
123
|
+
});
|
|
124
|
+
child.on('exit', (code) => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
resolve(code === 0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// runAddTrustedCert spawns the install command, branching on its
|
|
134
|
+
// outcome:
|
|
135
|
+
// exit 0 → installed
|
|
136
|
+
// exit !=0 within timeout → user declined or system rejected
|
|
137
|
+
// timeout → kill child and report timeout
|
|
138
|
+
function runAddTrustedCert(spawn, stderr, caPath, loginKC, timeoutMs) {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
const child = spawn(
|
|
141
|
+
'security',
|
|
142
|
+
['add-trusted-cert', '-r', 'trustRoot', '-k', loginKC, caPath],
|
|
143
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
144
|
+
);
|
|
145
|
+
let settled = false;
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
settled = true;
|
|
149
|
+
try { child.kill('SIGTERM'); } catch (_) { /* noop */ }
|
|
150
|
+
logMsg(stderr, 'Skalpel: user did not respond to keychain prompt within 60s — skipped.');
|
|
151
|
+
resolve({ ok: false, reason: 'timeout' });
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
|
|
154
|
+
child.on('error', (err) => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
logMsg(stderr, `Skalpel: keychain install failed to start: ${err.message}`);
|
|
159
|
+
resolve({ ok: false, reason: 'spawn-error', error: err.message });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
child.on('exit', (code) => {
|
|
163
|
+
if (settled) return;
|
|
164
|
+
settled = true;
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
if (code === 0) {
|
|
167
|
+
logMsg(stderr, 'Skalpel: daemon CA trusted in login keychain.');
|
|
168
|
+
resolve({ ok: true });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
logMsg(
|
|
172
|
+
stderr,
|
|
173
|
+
'Skalpel: Codex MITM CA not trusted. Codex traffic will bypass skalpel until ' +
|
|
174
|
+
'you re-run the prompt (next `skalpel login` or `codex` invocation) or ' +
|
|
175
|
+
'manually trust ~/.skalpel/mitm-ca.pem.'
|
|
176
|
+
);
|
|
177
|
+
resolve({ ok: false, reason: 'declined-or-failed', exitCode: code });
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
installMacOSCA,
|
|
184
|
+
KEYCHAIN_LABEL,
|
|
185
|
+
SENTINEL_BASENAME,
|
|
186
|
+
// Exported for tests:
|
|
187
|
+
loginKeychainPath,
|
|
188
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ca-install tests.
|
|
3
|
+
//
|
|
4
|
+
// Stdlib only: assert + fs + os + path + events. No mocha / no jest.
|
|
5
|
+
// Matches the testing style of rc-edit.test.js. spawn is injected so
|
|
6
|
+
// the tests never touch the real `security` binary or the user's
|
|
7
|
+
// login keychain.
|
|
8
|
+
//
|
|
9
|
+
// Test cases (the promise.txt names each one explicitly):
|
|
10
|
+
// - non-darwin — platform != 'darwin' short-circuits
|
|
11
|
+
// - already-installed — find-certificate returns 0 → skip
|
|
12
|
+
// - success — add-trusted-cert exits 0
|
|
13
|
+
// - declined — add-trusted-cert exits non-zero
|
|
14
|
+
// - timeout — add-trusted-cert never exits; killed
|
|
15
|
+
// - deferred — CA file missing → sentinel written
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { EventEmitter } = require('events');
|
|
23
|
+
const assert = require('assert');
|
|
24
|
+
|
|
25
|
+
const ca = require('./ca-install');
|
|
26
|
+
|
|
27
|
+
let pass = 0;
|
|
28
|
+
let fail = 0;
|
|
29
|
+
|
|
30
|
+
function test(name, fn) {
|
|
31
|
+
return Promise.resolve()
|
|
32
|
+
.then(fn)
|
|
33
|
+
.then(() => {
|
|
34
|
+
process.stdout.write(` PASS ${name}\n`);
|
|
35
|
+
pass += 1;
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
process.stderr.write(` FAIL ${name}\n ${err && err.stack ? err.stack : err}\n`);
|
|
39
|
+
fail += 1;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// fakeChild implements the subset of ChildProcess that installMacOSCA
|
|
44
|
+
// consumes: an event emitter with `kill()`. Plug into makeSpawn().
|
|
45
|
+
function fakeChild() {
|
|
46
|
+
const ee = new EventEmitter();
|
|
47
|
+
ee.killed = false;
|
|
48
|
+
ee.kill = function () { ee.killed = true; };
|
|
49
|
+
return ee;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// makeSpawn returns a fake spawn that scripts each successive
|
|
53
|
+
// invocation. Each script entry is either:
|
|
54
|
+
// { cmd: 'find-certificate', exit: 0|N }
|
|
55
|
+
// { cmd: 'add-trusted-cert', exit: 0|N, async?: true (delay exit), error?: Error }
|
|
56
|
+
// If `async` is set, the child does NOT emit `exit` — the caller is
|
|
57
|
+
// responsible for triggering the timeout path.
|
|
58
|
+
function makeSpawn(script) {
|
|
59
|
+
const calls = [];
|
|
60
|
+
let i = 0;
|
|
61
|
+
function spawn(bin, args) {
|
|
62
|
+
const verb = args[0];
|
|
63
|
+
const step = script[i] || {};
|
|
64
|
+
i += 1;
|
|
65
|
+
calls.push({ bin, args, verb });
|
|
66
|
+
const child = fakeChild();
|
|
67
|
+
if (step.error) {
|
|
68
|
+
process.nextTick(() => child.emit('error', step.error));
|
|
69
|
+
return child;
|
|
70
|
+
}
|
|
71
|
+
if (step.async) {
|
|
72
|
+
// never emits exit — caller's setTimeout drives the timeout
|
|
73
|
+
return child;
|
|
74
|
+
}
|
|
75
|
+
const exitCode = typeof step.exit === 'number' ? step.exit : 0;
|
|
76
|
+
process.nextTick(() => child.emit('exit', exitCode));
|
|
77
|
+
return child;
|
|
78
|
+
}
|
|
79
|
+
spawn.calls = calls;
|
|
80
|
+
return spawn;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function captureStderr() {
|
|
84
|
+
const buf = [];
|
|
85
|
+
return {
|
|
86
|
+
write(s) { buf.push(s); },
|
|
87
|
+
text() { return buf.join(''); },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tmpdir() {
|
|
92
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ca-install-test-'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function run() {
|
|
96
|
+
process.stdout.write('ca-install tests:\n');
|
|
97
|
+
|
|
98
|
+
await test('non-darwin skip', async () => {
|
|
99
|
+
const stderr = captureStderr();
|
|
100
|
+
const spawn = makeSpawn([]);
|
|
101
|
+
const result = await ca.installMacOSCA('/anything', {
|
|
102
|
+
spawn,
|
|
103
|
+
stderr,
|
|
104
|
+
platform: 'linux',
|
|
105
|
+
});
|
|
106
|
+
assert.strictEqual(result.ok, true);
|
|
107
|
+
assert.strictEqual(result.skipped, true);
|
|
108
|
+
assert.strictEqual(result.reason, 'platform');
|
|
109
|
+
assert.strictEqual(spawn.calls.length, 0, 'must not invoke security on non-darwin');
|
|
110
|
+
assert.ok(/linux/.test(stderr.text()), 'should mention current platform');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await test('already-installed idempotency', async () => {
|
|
114
|
+
const stderr = captureStderr();
|
|
115
|
+
const dir = tmpdir();
|
|
116
|
+
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
117
|
+
fs.writeFileSync(caPath, 'dummy');
|
|
118
|
+
// find-certificate exits 0 → already trusted; no add-trusted-cert call.
|
|
119
|
+
const spawn = makeSpawn([{ cmd: 'find-certificate', exit: 0 }]);
|
|
120
|
+
const result = await ca.installMacOSCA(caPath, {
|
|
121
|
+
spawn,
|
|
122
|
+
stderr,
|
|
123
|
+
platform: 'darwin',
|
|
124
|
+
homedir: dir,
|
|
125
|
+
});
|
|
126
|
+
assert.strictEqual(result.ok, true);
|
|
127
|
+
assert.strictEqual(result.skipped, true);
|
|
128
|
+
assert.strictEqual(result.reason, 'already-installed');
|
|
129
|
+
assert.strictEqual(spawn.calls.length, 1);
|
|
130
|
+
assert.strictEqual(spawn.calls[0].verb, 'find-certificate');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('success path', async () => {
|
|
134
|
+
const stderr = captureStderr();
|
|
135
|
+
const dir = tmpdir();
|
|
136
|
+
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
137
|
+
fs.writeFileSync(caPath, 'dummy');
|
|
138
|
+
const spawn = makeSpawn([
|
|
139
|
+
{ cmd: 'find-certificate', exit: 1 },
|
|
140
|
+
{ cmd: 'add-trusted-cert', exit: 0 },
|
|
141
|
+
]);
|
|
142
|
+
const result = await ca.installMacOSCA(caPath, {
|
|
143
|
+
spawn,
|
|
144
|
+
stderr,
|
|
145
|
+
platform: 'darwin',
|
|
146
|
+
homedir: dir,
|
|
147
|
+
});
|
|
148
|
+
assert.strictEqual(result.ok, true);
|
|
149
|
+
assert.strictEqual(result.skipped, undefined);
|
|
150
|
+
assert.strictEqual(spawn.calls.length, 2);
|
|
151
|
+
assert.strictEqual(spawn.calls[1].verb, 'add-trusted-cert');
|
|
152
|
+
assert.deepStrictEqual(
|
|
153
|
+
spawn.calls[1].args.slice(0, 4),
|
|
154
|
+
['add-trusted-cert', '-r', 'trustRoot', '-k']
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await test('declined path', async () => {
|
|
159
|
+
const stderr = captureStderr();
|
|
160
|
+
const dir = tmpdir();
|
|
161
|
+
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
162
|
+
fs.writeFileSync(caPath, 'dummy');
|
|
163
|
+
const spawn = makeSpawn([
|
|
164
|
+
{ cmd: 'find-certificate', exit: 1 },
|
|
165
|
+
{ cmd: 'add-trusted-cert', exit: 128 },
|
|
166
|
+
]);
|
|
167
|
+
const result = await ca.installMacOSCA(caPath, {
|
|
168
|
+
spawn,
|
|
169
|
+
stderr,
|
|
170
|
+
platform: 'darwin',
|
|
171
|
+
homedir: dir,
|
|
172
|
+
});
|
|
173
|
+
assert.strictEqual(result.ok, false);
|
|
174
|
+
assert.strictEqual(result.reason, 'declined-or-failed');
|
|
175
|
+
assert.strictEqual(result.exitCode, 128);
|
|
176
|
+
assert.ok(/CA not trusted|bypass skalpel/.test(stderr.text()), 'should warn user');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('timeout path', async () => {
|
|
180
|
+
const stderr = captureStderr();
|
|
181
|
+
const dir = tmpdir();
|
|
182
|
+
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
183
|
+
fs.writeFileSync(caPath, 'dummy');
|
|
184
|
+
const spawn = makeSpawn([
|
|
185
|
+
{ cmd: 'find-certificate', exit: 1 },
|
|
186
|
+
{ cmd: 'add-trusted-cert', async: true }, // never exits
|
|
187
|
+
]);
|
|
188
|
+
const result = await ca.installMacOSCA(caPath, {
|
|
189
|
+
spawn,
|
|
190
|
+
stderr,
|
|
191
|
+
platform: 'darwin',
|
|
192
|
+
homedir: dir,
|
|
193
|
+
timeoutMs: 30, // small so test is fast
|
|
194
|
+
});
|
|
195
|
+
assert.strictEqual(result.ok, false);
|
|
196
|
+
assert.strictEqual(result.reason, 'timeout');
|
|
197
|
+
assert.ok(/did not respond|skipped/.test(stderr.text()), 'should explain skip');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await test('deferred (no CA file yet) writes sentinel', async () => {
|
|
201
|
+
const stderr = captureStderr();
|
|
202
|
+
const dir = tmpdir();
|
|
203
|
+
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
204
|
+
// Do NOT create caPath — simulate daemon hasn't booted yet.
|
|
205
|
+
const spawn = makeSpawn([]);
|
|
206
|
+
const result = await ca.installMacOSCA(caPath, {
|
|
207
|
+
spawn,
|
|
208
|
+
stderr,
|
|
209
|
+
platform: 'darwin',
|
|
210
|
+
homedir: dir,
|
|
211
|
+
sentinelDir: dir,
|
|
212
|
+
});
|
|
213
|
+
assert.strictEqual(result.ok, false);
|
|
214
|
+
assert.strictEqual(result.reason, 'deferred-no-ca');
|
|
215
|
+
assert.strictEqual(spawn.calls.length, 0, 'no security spawn on deferred path');
|
|
216
|
+
const sentinel = path.join(dir, ca.SENTINEL_BASENAME);
|
|
217
|
+
assert.ok(fs.existsSync(sentinel), `sentinel should exist at ${sentinel}`);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
process.stdout.write(`\n pass=${pass} fail=${fail}\n`);
|
|
221
|
+
return fail === 0 ? 0 : 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (require.main === module) {
|
|
225
|
+
run().then((code) => process.exit(code));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { run };
|
|
@@ -1,5 +1,10 @@
|
|
|
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
8
|
function envBlockValues(port) {
|
|
4
9
|
const p = String(port || 7878);
|
|
5
10
|
const root = `http://127.0.0.1:${p}`;
|
|
@@ -9,8 +14,6 @@ function envBlockValues(port) {
|
|
|
9
14
|
OPENAI_BASE_URL: `${root}/v1`,
|
|
10
15
|
OPENAI_API_BASE: `${root}/v1`,
|
|
11
16
|
SKALPEL_PROXY_URL: root,
|
|
12
|
-
SKALPEL_CODEX_OPENAI_BASE_URL: `${root}/v1`,
|
|
13
|
-
SKALPEL_CODEX_CHATGPT_BASE_URL: `${root}/backend-api`,
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -28,7 +31,7 @@ if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias claude >/dev/null 2>&1 && com
|
|
|
28
31
|
# exec'ing the real claude binary.
|
|
29
32
|
command skalpel claude-exec "$@"
|
|
30
33
|
else
|
|
31
|
-
ANTHROPIC_API_URL= ANTHROPIC_BASE_URL= OPENAI_BASE_URL= OPENAI_API_BASE= SKALPEL_PROXY_URL=
|
|
34
|
+
ANTHROPIC_API_URL= ANTHROPIC_BASE_URL= OPENAI_BASE_URL= OPENAI_API_BASE= SKALPEL_PROXY_URL= command claude "$@"
|
|
32
35
|
fi
|
|
33
36
|
}
|
|
34
37
|
fi`,
|
|
@@ -40,7 +43,7 @@ if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q claude; and command -q
|
|
|
40
43
|
if skalpel status 1>&2
|
|
41
44
|
command skalpel claude-exec $argv
|
|
42
45
|
else
|
|
43
|
-
env -u ANTHROPIC_API_URL -u ANTHROPIC_BASE_URL -u OPENAI_BASE_URL -u OPENAI_API_BASE -u SKALPEL_PROXY_URL
|
|
46
|
+
env -u ANTHROPIC_API_URL -u ANTHROPIC_BASE_URL -u OPENAI_BASE_URL -u OPENAI_API_BASE -u SKALPEL_PROXY_URL command claude $argv
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
end`,
|
|
@@ -60,15 +63,11 @@ if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatu
|
|
|
60
63
|
$_savedOpenAiBase = $env:OPENAI_BASE_URL
|
|
61
64
|
$_savedOpenAiApiBase = $env:OPENAI_API_BASE
|
|
62
65
|
$_savedSkalpelProxy = $env:SKALPEL_PROXY_URL
|
|
63
|
-
$_savedCodexOpenAi = $env:SKALPEL_CODEX_OPENAI_BASE_URL
|
|
64
|
-
$_savedCodexChatGPT = $env:SKALPEL_CODEX_CHATGPT_BASE_URL
|
|
65
66
|
$env:ANTHROPIC_API_URL = ''
|
|
66
67
|
$env:ANTHROPIC_BASE_URL = ''
|
|
67
68
|
$env:OPENAI_BASE_URL = ''
|
|
68
69
|
$env:OPENAI_API_BASE = ''
|
|
69
70
|
$env:SKALPEL_PROXY_URL = ''
|
|
70
|
-
$env:SKALPEL_CODEX_OPENAI_BASE_URL = ''
|
|
71
|
-
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = ''
|
|
72
71
|
try { & $script:_skalpelOrigClaude.Source @args }
|
|
73
72
|
finally {
|
|
74
73
|
$env:ANTHROPIC_API_URL = $_savedAnthropicApi
|
|
@@ -76,8 +75,6 @@ if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatu
|
|
|
76
75
|
$env:OPENAI_BASE_URL = $_savedOpenAiBase
|
|
77
76
|
$env:OPENAI_API_BASE = $_savedOpenAiApiBase
|
|
78
77
|
$env:SKALPEL_PROXY_URL = $_savedSkalpelProxy
|
|
79
|
-
$env:SKALPEL_CODEX_OPENAI_BASE_URL = $_savedCodexOpenAi
|
|
80
|
-
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = $_savedCodexChatGPT
|
|
81
78
|
}
|
|
82
79
|
}
|
|
83
80
|
}
|
|
@@ -85,54 +82,59 @@ if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatu
|
|
|
85
82
|
},
|
|
86
83
|
};
|
|
87
84
|
|
|
85
|
+
// Codex integration — MITM Option A.
|
|
86
|
+
//
|
|
87
|
+
// Under the prior base-URL regime the wrapper sniffed Codex's auth_mode
|
|
88
|
+
// out of ~/.codex/auth.json and injected `codex -c openai_base_url ...`
|
|
89
|
+
// or `codex -c chatgpt_base_url ...`. That bypassed the daemon's TLS
|
|
90
|
+
// pipeline for chatgpt.com and made attribution impossible.
|
|
91
|
+
//
|
|
92
|
+
// The MITM regime drops all base-URL knobs. Each wrapper now simply
|
|
93
|
+
// delegates to `skalpel codex-exec`, which sets HTTPS_PROXY at exec
|
|
94
|
+
// time so Codex CLI CONNECTs to chatgpt.com:443 through the daemon and
|
|
95
|
+
// the daemon TLS-terminates with a leaf minted from its local CA. The
|
|
96
|
+
// trust chain is installed into the OS keychain by
|
|
97
|
+
// postinstall/lib/ca-install.js (macOS today; Linux/Windows deferred).
|
|
98
|
+
//
|
|
99
|
+
// Fail-open contract: if `skalpel status` reports the daemon down, the
|
|
100
|
+
// wrapper falls through to bare `codex` so the user is never blocked
|
|
101
|
+
// from running Codex by a skalpel outage.
|
|
88
102
|
const codex = {
|
|
89
103
|
name: 'codex',
|
|
90
104
|
wrappers: {
|
|
91
105
|
posix: `
|
|
92
|
-
# Codex CLI
|
|
106
|
+
# Codex CLI integration — MITM regime (skalpel codex-exec).
|
|
93
107
|
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
94
108
|
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias codex >/dev/null 2>&1 && command -v codex >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
95
109
|
codex() {
|
|
96
110
|
if skalpel status >&2; then
|
|
97
|
-
|
|
98
|
-
_skalpel_codex_auth_mode="$(awk -F'"' '/"auth_mode"/ { print $4; exit }' "\${_skalpel_codex_home}/auth.json" 2>/dev/null)"
|
|
99
|
-
case "\${_skalpel_codex_auth_mode}" in
|
|
100
|
-
apikey|api_key|api)
|
|
101
|
-
command codex -c "openai_base_url='\${SKALPEL_CODEX_OPENAI_BASE_URL}'" "$@"
|
|
102
|
-
;;
|
|
103
|
-
*)
|
|
104
|
-
command codex -c "chatgpt_base_url='\${SKALPEL_CODEX_CHATGPT_BASE_URL}'" "$@"
|
|
105
|
-
;;
|
|
106
|
-
esac
|
|
111
|
+
command skalpel codex-exec "$@"
|
|
107
112
|
else
|
|
108
|
-
|
|
113
|
+
# Fail-open: daemon down. Clear every proxy var the rc-block set —
|
|
114
|
+
# Codex respects OPENAI_BASE_URL / OPENAI_API_BASE and would
|
|
115
|
+
# otherwise dial the dead loopback URL. Mirrors the claude
|
|
116
|
+
# wrapper's fail-open above.
|
|
117
|
+
ANTHROPIC_API_URL= ANTHROPIC_BASE_URL= OPENAI_BASE_URL= OPENAI_API_BASE= SKALPEL_PROXY_URL= SKALPEL_CODEX_OPENAI_BASE_URL= SKALPEL_CODEX_CHATGPT_BASE_URL= command codex "$@"
|
|
109
118
|
fi
|
|
110
119
|
}
|
|
111
120
|
fi`,
|
|
112
121
|
fish: `
|
|
113
|
-
# Codex CLI
|
|
122
|
+
# Codex CLI integration — MITM regime (skalpel codex-exec).
|
|
114
123
|
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
115
124
|
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q codex; and command -q codex; and command -q skalpel
|
|
116
125
|
function codex
|
|
117
126
|
if skalpel status 1>&2
|
|
118
|
-
|
|
119
|
-
if set -q CODEX_HOME
|
|
120
|
-
set _skalpel_codex_home "$CODEX_HOME"
|
|
121
|
-
end
|
|
122
|
-
set -l _skalpel_codex_auth_mode (awk -F'"' '/"auth_mode"/ { print $4; exit }' "$_skalpel_codex_home/auth.json" 2>/dev/null)
|
|
123
|
-
switch "$_skalpel_codex_auth_mode"
|
|
124
|
-
case apikey api_key api
|
|
125
|
-
command codex -c "openai_base_url='$SKALPEL_CODEX_OPENAI_BASE_URL'" $argv
|
|
126
|
-
case '*'
|
|
127
|
-
command codex -c "chatgpt_base_url='$SKALPEL_CODEX_CHATGPT_BASE_URL'" $argv
|
|
128
|
-
end
|
|
127
|
+
command skalpel codex-exec $argv
|
|
129
128
|
else
|
|
130
|
-
|
|
129
|
+
# Fail-open: daemon down. Clear every proxy var the rc-block set
|
|
130
|
+
# — Codex respects OPENAI_BASE_URL / OPENAI_API_BASE and would
|
|
131
|
+
# otherwise dial the dead loopback URL.
|
|
132
|
+
env -u ANTHROPIC_API_URL -u ANTHROPIC_BASE_URL -u OPENAI_BASE_URL -u OPENAI_API_BASE -u SKALPEL_PROXY_URL -u SKALPEL_CODEX_OPENAI_BASE_URL -u SKALPEL_CODEX_CHATGPT_BASE_URL command codex $argv
|
|
131
133
|
end
|
|
132
134
|
end
|
|
133
135
|
end`,
|
|
134
136
|
powershell: `
|
|
135
|
-
# Codex CLI
|
|
137
|
+
# Codex CLI integration — MITM regime (skalpel codex-exec).
|
|
136
138
|
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
137
139
|
$_skalpelOrigCodex = Get-Command codex -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
138
140
|
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
@@ -140,21 +142,35 @@ if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigCodex -and $_skalpelStatus
|
|
|
140
142
|
function global:codex {
|
|
141
143
|
& $script:_skalpelStatusBin.Source status | Out-Host
|
|
142
144
|
if ($LASTEXITCODE -eq 0) {
|
|
143
|
-
|
|
144
|
-
$_skalpelCodexAuthMode = ''
|
|
145
|
-
try {
|
|
146
|
-
$_skalpelCodexAuthPath = Join-Path $_skalpelCodexHome 'auth.json'
|
|
147
|
-
if (Test-Path $_skalpelCodexAuthPath) {
|
|
148
|
-
$_skalpelCodexAuthMode = (Get-Content -Raw $_skalpelCodexAuthPath | ConvertFrom-Json).auth_mode
|
|
149
|
-
}
|
|
150
|
-
} catch {}
|
|
151
|
-
if ($_skalpelCodexAuthMode -in @('apikey', 'api_key', 'api')) {
|
|
152
|
-
& $script:_skalpelOrigCodex.Source -c "openai_base_url='$env:SKALPEL_CODEX_OPENAI_BASE_URL'" @args
|
|
153
|
-
} else {
|
|
154
|
-
& $script:_skalpelOrigCodex.Source -c "chatgpt_base_url='$env:SKALPEL_CODEX_CHATGPT_BASE_URL'" @args
|
|
155
|
-
}
|
|
145
|
+
& $script:_skalpelStatusBin.Source codex-exec @args
|
|
156
146
|
} else {
|
|
157
|
-
|
|
147
|
+
# Fail-open: daemon down. Save + clear every proxy var the
|
|
148
|
+
# rc-block set so Codex doesn't dial the dead loopback URL,
|
|
149
|
+
# then restore them so the parent shell is unaffected.
|
|
150
|
+
$_savedAnthropicApi = $env:ANTHROPIC_API_URL
|
|
151
|
+
$_savedAnthropicBase = $env:ANTHROPIC_BASE_URL
|
|
152
|
+
$_savedOpenAiBase = $env:OPENAI_BASE_URL
|
|
153
|
+
$_savedOpenAiApiBase = $env:OPENAI_API_BASE
|
|
154
|
+
$_savedSkalpelProxy = $env:SKALPEL_PROXY_URL
|
|
155
|
+
$_savedCodexOpenAi = $env:SKALPEL_CODEX_OPENAI_BASE_URL
|
|
156
|
+
$_savedCodexChatGPT = $env:SKALPEL_CODEX_CHATGPT_BASE_URL
|
|
157
|
+
$env:ANTHROPIC_API_URL = ''
|
|
158
|
+
$env:ANTHROPIC_BASE_URL = ''
|
|
159
|
+
$env:OPENAI_BASE_URL = ''
|
|
160
|
+
$env:OPENAI_API_BASE = ''
|
|
161
|
+
$env:SKALPEL_PROXY_URL = ''
|
|
162
|
+
$env:SKALPEL_CODEX_OPENAI_BASE_URL = ''
|
|
163
|
+
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = ''
|
|
164
|
+
try { & $script:_skalpelOrigCodex.Source @args }
|
|
165
|
+
finally {
|
|
166
|
+
$env:ANTHROPIC_API_URL = $_savedAnthropicApi
|
|
167
|
+
$env:ANTHROPIC_BASE_URL = $_savedAnthropicBase
|
|
168
|
+
$env:OPENAI_BASE_URL = $_savedOpenAiBase
|
|
169
|
+
$env:OPENAI_API_BASE = $_savedOpenAiApiBase
|
|
170
|
+
$env:SKALPEL_PROXY_URL = $_savedSkalpelProxy
|
|
171
|
+
$env:SKALPEL_CODEX_OPENAI_BASE_URL = $_savedCodexOpenAi
|
|
172
|
+
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = $_savedCodexChatGPT
|
|
173
|
+
}
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
}`,
|
|
@@ -188,13 +188,18 @@ function run() {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
test('TestRcEdit_Body_Has_Codex_Wrapper', () => {
|
|
191
|
+
// Codex MITM regime (Option A): wrapper delegates to
|
|
192
|
+
// `skalpel codex-exec`, which sets HTTPS_PROXY at exec time. The
|
|
193
|
+
// prior `auth_mode` / `openai_base_url` / `chatgpt_base_url`
|
|
194
|
+
// sniffing is gone — the daemon TLS-terminates chatgpt.com:443
|
|
195
|
+
// directly and Codex CLI's native upstream resolver does the rest.
|
|
191
196
|
const env = rc.envBlockValues(7878);
|
|
192
197
|
const block = rc.buildBlock('bash', env);
|
|
193
198
|
assert.ok(block.includes('codex()'), 'missing codex wrapper');
|
|
194
|
-
assert.ok(block.includes('
|
|
195
|
-
assert.ok(block.includes('
|
|
196
|
-
assert.ok(block.includes('
|
|
197
|
-
assert.ok(block.includes('
|
|
199
|
+
assert.ok(block.includes('skalpel codex-exec'), 'codex wrapper should delegate to skalpel codex-exec');
|
|
200
|
+
assert.ok(!block.includes('openai_base_url'), 'codex wrapper should not pin openai_base_url (MITM regime drops base-URL knobs)');
|
|
201
|
+
assert.ok(!block.includes('chatgpt_base_url'), 'codex wrapper should not pin chatgpt_base_url (MITM regime drops base-URL knobs)');
|
|
202
|
+
assert.ok(!block.includes('auth_mode'), 'codex wrapper should not sniff auth_mode (MITM regime is auth-mode-agnostic)');
|
|
198
203
|
assert.ok(!block.includes('network.proxy_url'), 'Codex wrapper should not generic-proxy metadata routes');
|
|
199
204
|
});
|
|
200
205
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tiny test driver for postinstall/lib/*.test.js
|
|
3
|
+
//
|
|
4
|
+
// Why: each *.test.js is a self-contained module exporting `run()` that
|
|
5
|
+
// returns 0|1 (rc-edit pattern). This driver lets `npm test` run them
|
|
6
|
+
// all or filter to a single one — required so the promise's two probes
|
|
7
|
+
// (`npm test` AND `npm test -- ca-install.test.js`) both work.
|
|
8
|
+
//
|
|
9
|
+
// Filter argument is the test file's basename, e.g. `ca-install.test.js`
|
|
10
|
+
// or `rc-edit.test.js`. Anything not matching a known test is ignored.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const HERE = __dirname;
|
|
18
|
+
|
|
19
|
+
function discoverTests() {
|
|
20
|
+
return fs
|
|
21
|
+
.readdirSync(HERE)
|
|
22
|
+
.filter((f) => f.endsWith('.test.js'))
|
|
23
|
+
.sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function filterFromArgv(argv) {
|
|
27
|
+
// npm test -- ca-install.test.js → process.argv = [node, run-tests.js, 'ca-install.test.js']
|
|
28
|
+
return argv.slice(2).filter((a) => a && !a.startsWith('-'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const filters = filterFromArgv(process.argv);
|
|
33
|
+
const all = discoverTests();
|
|
34
|
+
const selected = filters.length
|
|
35
|
+
? all.filter((f) => filters.includes(f))
|
|
36
|
+
: all;
|
|
37
|
+
if (selected.length === 0) {
|
|
38
|
+
process.stderr.write(`no tests matched filter(s): ${filters.join(', ')}\n`);
|
|
39
|
+
process.stderr.write(`available: ${all.join(', ')}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const f of selected) {
|
|
44
|
+
process.stdout.write(`\n== ${f} ==\n`);
|
|
45
|
+
const mod = require(path.join(HERE, f));
|
|
46
|
+
if (typeof mod.run !== 'function') {
|
|
47
|
+
process.stderr.write(` ${f}: module does not export run(); skipping\n`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const code = await mod.run();
|
|
51
|
+
total += code || 0;
|
|
52
|
+
}
|
|
53
|
+
process.exit(total === 0 ? 0 : 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main().catch((err) => {
|
|
57
|
+
process.stderr.write(`run-tests crashed: ${err.stack || err}\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
@@ -102,10 +102,21 @@ function loadCommands() {
|
|
|
102
102
|
];
|
|
103
103
|
}
|
|
104
104
|
default:
|
|
105
|
+
// BUG-0038 (cont.19 stale-daemon fix): use `restart` instead of
|
|
106
|
+
// `start`. systemd `start` is a no-op when the unit is already
|
|
107
|
+
// active — so on every `npm i -g skalpel` UPGRADE, the running
|
|
108
|
+
// daemon stays on its in-memory copy of the OLD binary (Linux's
|
|
109
|
+
// kernel keeps the deleted-inode file mapped) and the postinstall
|
|
110
|
+
// silently does nothing. The user thinks they upgraded but the
|
|
111
|
+
// running code is still the prior version. `restart` instead does
|
|
112
|
+
// a graceful stop+start: SIGTERM → daemon drains → exits →
|
|
113
|
+
// releases the deleted inode → systemd brings up a new instance
|
|
114
|
+
// that exec()s the NEW binary now on disk. Idempotent vs fresh
|
|
115
|
+
// install (restart of an inactive unit just starts it).
|
|
105
116
|
return [
|
|
106
117
|
['systemctl', ['--user', 'daemon-reload']],
|
|
107
118
|
['systemctl', ['--user', 'enable', 'skalpel-daemon.service']],
|
|
108
|
-
['systemctl', ['--user', '
|
|
119
|
+
['systemctl', ['--user', 'restart', 'skalpel-daemon.service']],
|
|
109
120
|
];
|
|
110
121
|
}
|
|
111
122
|
}
|