@skrr-ai/cli 0.1.50 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-command.d.ts +7 -0
- package/dist/base-command.js +95 -8
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +8 -4
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +32 -0
- package/dist/commands/agents/update.js +81 -9
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +15 -1
- package/dist/commands/commitments/next-tick.js +12 -2
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +53 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +6 -0
- package/dist/commands/tasks/create.js +31 -6
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +29 -1
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +14 -10
- package/dist/lib/agentic-stream.d.ts +88 -1
- package/dist/lib/agentic-stream.js +330 -9
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +68 -0
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +342 -11
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +25 -1
- package/dist/lib/commitments.js +46 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/daemonBroker.d.ts +37 -1
- package/dist/lib/daemonBroker.js +146 -22
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +33 -2
- package/dist/lib/dedicated-machines.js +50 -1
- package/dist/lib/delegated-cli.js +20 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.js +10 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +228 -39
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +68 -1
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +67 -21
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +87 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
- package/oclif.manifest.json +28591 -28438
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Upper bound on a buffered response body. Loopback control-plane bodies are small. */
|
|
2
|
+
export declare const LOOPBACK_DEFAULT_MAX_BODY_BYTES: number;
|
|
3
|
+
/** Thrown (synchronously surfaced as a rejection) for a URL that is not loopback HTTP. */
|
|
4
|
+
export declare class LoopbackTargetRefusedError extends Error {
|
|
5
|
+
readonly code = "LOOPBACK_TARGET_REFUSED";
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** True for an IP literal in 127.0.0.0/8, `::1`, or an IPv4-mapped 127/8 address. */
|
|
9
|
+
export declare function isLoopbackAddress(address: string): boolean;
|
|
10
|
+
/** True for a host name or address this client accepts as "this machine". */
|
|
11
|
+
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Parse and validate a loopback HTTP URL. Throws {@link LoopbackTargetRefusedError}
|
|
14
|
+
* for anything that is not `http:` to a loopback host.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertLoopbackHttpUrl(input: string | URL): URL;
|
|
17
|
+
/**
|
|
18
|
+
* `RequestInit`, so a `fetch` call site can switch to {@link loopbackFetch}
|
|
19
|
+
* unchanged. Only `method`, `headers`, `signal` and a string/bytes `body` are
|
|
20
|
+
* honoured; a stream, Blob or FormData body is rejected rather than guessed at.
|
|
21
|
+
*/
|
|
22
|
+
export interface LoopbackFetchInit extends RequestInit {
|
|
23
|
+
/** Cap on the buffered response body; exceeding it rejects. */
|
|
24
|
+
maxBodyBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
/** A `fetch`-shaped signature, for callers that keep a `fetchImpl` seam. */
|
|
27
|
+
export type LoopbackFetch = (input: string | URL, init?: LoopbackFetchInit) => Promise<Response>;
|
|
28
|
+
/**
|
|
29
|
+
* `fetch` for a loopback URL, over `node:http`, never through a proxy.
|
|
30
|
+
* Refuses (rejects with {@link LoopbackTargetRefusedError}) any other target.
|
|
31
|
+
*/
|
|
32
|
+
export declare const loopbackFetch: LoopbackFetch;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSK-12134 — the ONE HTTP client for talking to a process on this machine.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this is not `fetch` ────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The compiled daemon runs on Bun, and Bun's `fetch` routes EVERY URL through
|
|
7
|
+
* `HTTP_PROXY` / `HTTPS_PROXY` unless `NO_PROXY` names it — a `127.0.0.1`
|
|
8
|
+
* target included. Node's `fetch` does the same once `NODE_USE_ENV_PROXY=1` is
|
|
9
|
+
* set (Node 22.21+/24). On a laptop with a local proxy exported in the shell
|
|
10
|
+
* (Clash, Surge, a corporate agent, …) every loopback call the daemon, a
|
|
11
|
+
* wrapper, or the CLI made with `fetch` was delivered to that proxy instead —
|
|
12
|
+
* WITH its credential: the daemon's local secret, the CLI-handoff descriptor
|
|
13
|
+
* secret, the wrapper fan-out bearer, the browser bridge token, the extension
|
|
14
|
+
* relay token. OSK-12087 found this on the `/health` probe and moved that one
|
|
15
|
+
* caller to `node:http`; this module is that fix made the only way to do it.
|
|
16
|
+
*
|
|
17
|
+
* `node:http` never consults proxy environment variables, on Node or on Bun, so
|
|
18
|
+
* a request made through it cannot be routed off the machine by environment.
|
|
19
|
+
*
|
|
20
|
+
* ── What it refuses ────────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* Only `http:` URLs whose host is a loopback name or address are accepted
|
|
23
|
+
* (`127.0.0.0/8`, `::1`, `localhost`). Anything else throws BEFORE a socket is
|
|
24
|
+
* opened: a caller that builds its URL from a descriptor or an environment
|
|
25
|
+
* variable must not be turned into an SSRF primitive by a hostile value, and a
|
|
26
|
+
* credential that is only meant for this machine must not be sent anywhere
|
|
27
|
+
* else. `localhost` is resolved through a lookup that rejects any non-loopback
|
|
28
|
+
* answer, so a rewritten hosts file cannot redirect it either.
|
|
29
|
+
*
|
|
30
|
+
* ── Shape ──────────────────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* `loopbackFetch` is a drop-in for the subset of `fetch` these callers use —
|
|
33
|
+
* method, headers, a string/bytes body, an `AbortSignal` — and returns a real
|
|
34
|
+
* `Response`, so existing callers (and their `fetchImpl` test seams) change one
|
|
35
|
+
* identifier. Transport failures reject with `TypeError('fetch failed')` whose
|
|
36
|
+
* `cause` carries the socket error (`ECONNREFUSED`, …), and aborts reject with
|
|
37
|
+
* the signal's reason, exactly as `fetch` does, so existing error handling keeps
|
|
38
|
+
* working. The response body is buffered (bounded by `maxBodyBytes`); nothing
|
|
39
|
+
* on loopback streams through this client.
|
|
40
|
+
*/
|
|
41
|
+
import dns from 'node:dns';
|
|
42
|
+
import http from 'node:http';
|
|
43
|
+
import net from 'node:net';
|
|
44
|
+
/** Loopback host names this client will connect to, besides 127/8 and ::1 literals. */
|
|
45
|
+
const LOOPBACK_NAMES = new Set(['localhost']);
|
|
46
|
+
/** Upper bound on a buffered response body. Loopback control-plane bodies are small. */
|
|
47
|
+
export const LOOPBACK_DEFAULT_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
48
|
+
/** Statuses a `Response` must be constructed with a null body for. */
|
|
49
|
+
const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
50
|
+
/** Thrown (synchronously surfaced as a rejection) for a URL that is not loopback HTTP. */
|
|
51
|
+
export class LoopbackTargetRefusedError extends Error {
|
|
52
|
+
code = 'LOOPBACK_TARGET_REFUSED';
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'LoopbackTargetRefusedError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function stripBrackets(hostname) {
|
|
59
|
+
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
60
|
+
}
|
|
61
|
+
/** True for an IP literal in 127.0.0.0/8, `::1`, or an IPv4-mapped 127/8 address. */
|
|
62
|
+
export function isLoopbackAddress(address) {
|
|
63
|
+
const value = stripBrackets(address.trim().toLowerCase());
|
|
64
|
+
if (net.isIPv4(value))
|
|
65
|
+
return value.split('.')[0] === '127';
|
|
66
|
+
if (net.isIPv6(value)) {
|
|
67
|
+
if (value === '::1' || value === '0:0:0:0:0:0:0:1')
|
|
68
|
+
return true;
|
|
69
|
+
// IPv4-mapped (::ffff:127.0.0.1, which WHATWG URL normalizes to ::ffff:7f00:1).
|
|
70
|
+
if (value.startsWith('::ffff:')) {
|
|
71
|
+
const rest = value.slice('::ffff:'.length);
|
|
72
|
+
if (net.isIPv4(rest))
|
|
73
|
+
return isLoopbackAddress(rest);
|
|
74
|
+
const high = /^([0-9a-f]{1,4}):[0-9a-f]{1,4}$/.exec(rest);
|
|
75
|
+
return high ? parseInt(high[1], 16) >> 8 === 127 : false;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/** True for a host name or address this client accepts as "this machine". */
|
|
82
|
+
export function isLoopbackHostname(hostname) {
|
|
83
|
+
const value = stripBrackets(hostname.trim().toLowerCase());
|
|
84
|
+
return LOOPBACK_NAMES.has(value) || isLoopbackAddress(value);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse and validate a loopback HTTP URL. Throws {@link LoopbackTargetRefusedError}
|
|
88
|
+
* for anything that is not `http:` to a loopback host.
|
|
89
|
+
*/
|
|
90
|
+
export function assertLoopbackHttpUrl(input) {
|
|
91
|
+
let url;
|
|
92
|
+
try {
|
|
93
|
+
url = new URL(String(input));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new LoopbackTargetRefusedError('Loopback request refused: not a valid URL.');
|
|
97
|
+
}
|
|
98
|
+
if (url.protocol !== 'http:') {
|
|
99
|
+
throw new LoopbackTargetRefusedError(`Loopback request refused: only http: is allowed on loopback (got ${url.protocol}).`);
|
|
100
|
+
}
|
|
101
|
+
if (!isLoopbackHostname(url.hostname)) {
|
|
102
|
+
throw new LoopbackTargetRefusedError(`Loopback request refused: ${url.hostname} is not a loopback host.`);
|
|
103
|
+
}
|
|
104
|
+
if (url.username || url.password) {
|
|
105
|
+
throw new LoopbackTargetRefusedError('Loopback request refused: URL must not carry userinfo.');
|
|
106
|
+
}
|
|
107
|
+
return url;
|
|
108
|
+
}
|
|
109
|
+
/** A `dns.lookup` that only ever answers with a loopback address. */
|
|
110
|
+
const loopbackOnlyLookup = (hostname, options, callback) => {
|
|
111
|
+
dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
112
|
+
if (err) {
|
|
113
|
+
callback(err, '', 0);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const list = (Array.isArray(addresses) ? addresses : []);
|
|
117
|
+
const loopback = list.filter((entry) => isLoopbackAddress(entry.address));
|
|
118
|
+
if (loopback.length === 0) {
|
|
119
|
+
const refused = Object.assign(new Error(`${hostname} did not resolve to a loopback address`), { code: 'LOOPBACK_TARGET_REFUSED' });
|
|
120
|
+
callback(refused, '', 0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (options?.all) {
|
|
124
|
+
callback(null, loopback);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
callback(null, loopback[0].address, loopback[0].family);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
function abortReason(signal) {
|
|
131
|
+
return signal.reason !== undefined
|
|
132
|
+
? signal.reason
|
|
133
|
+
: new DOMException('This operation was aborted', 'AbortError');
|
|
134
|
+
}
|
|
135
|
+
function fetchFailed(cause) {
|
|
136
|
+
return new TypeError('fetch failed', { cause });
|
|
137
|
+
}
|
|
138
|
+
function toBodyBuffer(body) {
|
|
139
|
+
if (body === undefined || body === null)
|
|
140
|
+
return null;
|
|
141
|
+
if (typeof body === 'string')
|
|
142
|
+
return Buffer.from(body, 'utf8');
|
|
143
|
+
if (body instanceof ArrayBuffer)
|
|
144
|
+
return Buffer.from(body);
|
|
145
|
+
if (ArrayBuffer.isView(body))
|
|
146
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
147
|
+
throw new TypeError('loopbackFetch supports only string or byte bodies');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `fetch` for a loopback URL, over `node:http`, never through a proxy.
|
|
151
|
+
* Refuses (rejects with {@link LoopbackTargetRefusedError}) any other target.
|
|
152
|
+
*/
|
|
153
|
+
export const loopbackFetch = (input, init = {}) => {
|
|
154
|
+
let url;
|
|
155
|
+
let payload;
|
|
156
|
+
try {
|
|
157
|
+
url = assertLoopbackHttpUrl(input);
|
|
158
|
+
payload = toBodyBuffer(init.body);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return Promise.reject(err);
|
|
162
|
+
}
|
|
163
|
+
const signal = init.signal ?? undefined;
|
|
164
|
+
if (signal?.aborted)
|
|
165
|
+
return Promise.reject(abortReason(signal));
|
|
166
|
+
const maxBodyBytes = init.maxBodyBytes ?? LOOPBACK_DEFAULT_MAX_BODY_BYTES;
|
|
167
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
168
|
+
const headers = {};
|
|
169
|
+
new Headers(init.headers ?? undefined).forEach((value, name) => {
|
|
170
|
+
headers[name] = value;
|
|
171
|
+
});
|
|
172
|
+
if (payload && headers['content-length'] === undefined) {
|
|
173
|
+
headers['content-length'] = String(payload.length);
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
let settled = false;
|
|
177
|
+
let req;
|
|
178
|
+
const onAbort = () => {
|
|
179
|
+
if (settled)
|
|
180
|
+
return;
|
|
181
|
+
settled = true;
|
|
182
|
+
req?.destroy();
|
|
183
|
+
reject(abortReason(signal));
|
|
184
|
+
};
|
|
185
|
+
const finish = (fn) => {
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
settled = true;
|
|
189
|
+
signal?.removeEventListener('abort', onAbort);
|
|
190
|
+
fn();
|
|
191
|
+
};
|
|
192
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
193
|
+
try {
|
|
194
|
+
req = http.request({
|
|
195
|
+
protocol: 'http:',
|
|
196
|
+
host: stripBrackets(url.hostname),
|
|
197
|
+
port: url.port ? Number(url.port) : 80,
|
|
198
|
+
path: `${url.pathname}${url.search}`,
|
|
199
|
+
method,
|
|
200
|
+
headers,
|
|
201
|
+
// No pooled agent: a short-lived command must be able to exit as soon
|
|
202
|
+
// as it has its answer, and no socket outlives its request.
|
|
203
|
+
agent: false,
|
|
204
|
+
lookup: loopbackOnlyLookup,
|
|
205
|
+
}, (res) => {
|
|
206
|
+
const chunks = [];
|
|
207
|
+
let size = 0;
|
|
208
|
+
res.on('data', (chunk) => {
|
|
209
|
+
size += chunk.length;
|
|
210
|
+
if (size > maxBodyBytes) {
|
|
211
|
+
res.destroy();
|
|
212
|
+
finish(() => reject(fetchFailed(new Error(`response larger than ${maxBodyBytes} bytes`))));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
chunks.push(chunk);
|
|
216
|
+
});
|
|
217
|
+
res.on('error', (err) => finish(() => reject(fetchFailed(err))));
|
|
218
|
+
res.on('aborted', () => finish(() => reject(fetchFailed(new Error('response aborted')))));
|
|
219
|
+
res.on('end', () => {
|
|
220
|
+
finish(() => {
|
|
221
|
+
const status = res.statusCode ?? 0;
|
|
222
|
+
const responseHeaders = new Headers();
|
|
223
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
for (const v of value)
|
|
228
|
+
responseHeaders.append(name, v);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
responseHeaders.set(name, String(value));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const body = NULL_BODY_STATUSES.has(status) ? null : Buffer.concat(chunks);
|
|
235
|
+
try {
|
|
236
|
+
resolve(new Response(body, {
|
|
237
|
+
status,
|
|
238
|
+
statusText: res.statusMessage ?? '',
|
|
239
|
+
headers: responseHeaders,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
reject(fetchFailed(err));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
req.on('error', (err) => finish(() => reject(fetchFailed(err))));
|
|
249
|
+
if (payload)
|
|
250
|
+
req.write(payload);
|
|
251
|
+
req.end();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
finish(() => reject(fetchFailed(err)));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
};
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* with a real UUIDv7 downstream.
|
|
11
11
|
*
|
|
12
12
|
* The caller may pass `machineUuidFallback` to override the default
|
|
13
|
-
* hostname+platform fingerprint with a richer detector (e.g.
|
|
14
|
-
*
|
|
13
|
+
* hostname+platform fingerprint with a richer detector (e.g.
|
|
14
|
+
* `getMachineUuid()` from `machineUuid.ts`).
|
|
15
15
|
*/
|
|
16
16
|
export declare function ensureMachineDaemonId(machineUuidFallback?: () => string): string;
|
|
17
17
|
/**
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
* directory survives), and a user whose machineUuid changes can
|
|
16
16
|
* still be re-anchored via the legacy-id list.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* This id is NOT the `machineUuid` the server keys Daemon rows on. That
|
|
19
|
+
* is the hardware id from `getMachineUuid()` (`machineUuid.ts`), and the
|
|
20
|
+
* two must never be substituted for each other on the wire: OSK-12063
|
|
21
|
+
* was the CLI sending this id as `machineUuid` on device-key enrollment,
|
|
22
|
+
* which matched no Daemon row and 404'd forever. The fallback used when
|
|
23
|
+
* this file cannot be persisted is supplied by the consumer via the
|
|
24
|
+
* `machineUuidFallback` parameter (the daemon passes `getMachineUuid`).
|
|
23
25
|
*/
|
|
24
26
|
import crypto from 'node:crypto';
|
|
25
27
|
import fs from 'node:fs';
|
|
@@ -100,9 +102,8 @@ function writeDaemonIdFile(target, id) {
|
|
|
100
102
|
* SHA-256 of `os.hostname()` + `os.platform()` — better than just
|
|
101
103
|
* hostname (includes platform) but worse than a true hardware UUID.
|
|
102
104
|
*
|
|
103
|
-
* The daemon overrides this with
|
|
104
|
-
* (`
|
|
105
|
-
* no native deps for OS UUIDs and so accepts this default.
|
|
105
|
+
* The daemon overrides this with the hardware detector
|
|
106
|
+
* (`getMachineUuid()`, `machineUuid.ts`).
|
|
106
107
|
*/
|
|
107
108
|
function defaultMachineUuidFallback() {
|
|
108
109
|
const host = os.hostname() || 'unknown-host';
|
|
@@ -121,8 +122,8 @@ function defaultMachineUuidFallback() {
|
|
|
121
122
|
* with a real UUIDv7 downstream.
|
|
122
123
|
*
|
|
123
124
|
* The caller may pass `machineUuidFallback` to override the default
|
|
124
|
-
* hostname+platform fingerprint with a richer detector (e.g.
|
|
125
|
-
*
|
|
125
|
+
* hostname+platform fingerprint with a richer detector (e.g.
|
|
126
|
+
* `getMachineUuid()` from `machineUuid.ts`).
|
|
126
127
|
*/
|
|
127
128
|
export function ensureMachineDaemonId(machineUuidFallback) {
|
|
128
129
|
if (cachedStableDaemonId !== null)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon sources whose identity is ISSUED by the control plane rather than
|
|
3
|
+
* derived from the host: the provisioner mints the daemon id and machine uuid
|
|
4
|
+
* before the daemon exists, and every binary on the runtime must use exactly
|
|
5
|
+
* those values.
|
|
6
|
+
*
|
|
7
|
+
* `dedicated-runtime` is here because the server's generation fence parses the
|
|
8
|
+
* lease generation out of the machine uuid (`dedicated-runtime:<lease>:<gen>`).
|
|
9
|
+
* A guest that fell back to `/etc/machine-id` would register under a uuid the
|
|
10
|
+
* fence cannot read, and every dispatch to it would be refused.
|
|
11
|
+
*/
|
|
12
|
+
export declare const HOSTED_RUNTIME_MACHINE_SOURCES: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* The control-plane-issued value of `name` (e.g. `OVERSKY_MACHINE_UUID`), or
|
|
15
|
+
* null when this process is not on a hosted runtime or the value is absent /
|
|
16
|
+
* malformed. Only honoured under a hosted `OVERSKY_DAEMON_SOURCE`, so a stray
|
|
17
|
+
* variable on a laptop cannot rename the machine.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hostedRuntimeIdOverride(name: string, env?: NodeJS.ProcessEnv): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Return a stable, machine-scoped UUID. Cached for the process lifetime.
|
|
22
|
+
*
|
|
23
|
+
* Callers should treat this as opaque: the format differs per platform
|
|
24
|
+
* (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid).
|
|
25
|
+
* The guarantee is only: same machine -> same value across reboots, and the
|
|
26
|
+
* same value from every binary that calls this function.
|
|
27
|
+
*
|
|
28
|
+
* On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
|
|
29
|
+
*
|
|
30
|
+
* Falls back to a deterministic hostname+username hash if the platform
|
|
31
|
+
* probe fails. This preserves server-side record continuity even on
|
|
32
|
+
* locked-down machines that refuse ioreg / registry access.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getMachineUuid(): string;
|
|
35
|
+
/** @internal test seam — forget the cached value so the next call re-probes. */
|
|
36
|
+
export declare function __resetMachineUuidCacheForTest(): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* machineUuid.ts — the ONE hardware machine identity sent to the server as
|
|
3
|
+
* `machineUuid`, shared by the daemon and the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Server Daemon rows are keyed on this value (daemon register, device-key
|
|
6
|
+
* enrollment, token mint, pairing), so every binary on one machine must
|
|
7
|
+
* compute it identically. Until OSK-12063 the CLI sent its own UUIDv7 from
|
|
8
|
+
* `~/.skrr/daemon.id` (`ensureMachineDaemonId()`, a DIFFERENT identity) as
|
|
9
|
+
* `machineUuid` on device-key enrollment while the daemon sent the hardware
|
|
10
|
+
* UUID computed here, so every CLI enrollment matched no Daemon row, got
|
|
11
|
+
* 404 DAEMON_NOT_FOUND and backed off forever. Two implementations of one
|
|
12
|
+
* identity can only agree by being edited together; this file is the one
|
|
13
|
+
* implementation, and `daemon/src/registry-lite.ts` re-exports it.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately dependency-free (node builtins only) and exposed as the
|
|
16
|
+
* `@skrr-ai/auth-core/machine-uuid` subpath: the packaged desktop imports the
|
|
17
|
+
* daemon's `registry-lite.js` directly, and loading the full auth-core index
|
|
18
|
+
* there would drag in the credential stack for a one-function need.
|
|
19
|
+
*
|
|
20
|
+
* Not to be confused with `ensureMachineDaemonId()` (`machineId.ts`), the
|
|
21
|
+
* minted per-home-dir UUIDv7 that anchors the daemon's `stableDaemonId`. That
|
|
22
|
+
* one survives an OS reinstall; this one survives a home-dir wipe. They are
|
|
23
|
+
* different facts and neither may stand in for the other on the wire.
|
|
24
|
+
*/
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
/**
|
|
30
|
+
* Daemon sources whose identity is ISSUED by the control plane rather than
|
|
31
|
+
* derived from the host: the provisioner mints the daemon id and machine uuid
|
|
32
|
+
* before the daemon exists, and every binary on the runtime must use exactly
|
|
33
|
+
* those values.
|
|
34
|
+
*
|
|
35
|
+
* `dedicated-runtime` is here because the server's generation fence parses the
|
|
36
|
+
* lease generation out of the machine uuid (`dedicated-runtime:<lease>:<gen>`).
|
|
37
|
+
* A guest that fell back to `/etc/machine-id` would register under a uuid the
|
|
38
|
+
* fence cannot read, and every dispatch to it would be refused.
|
|
39
|
+
*/
|
|
40
|
+
export const HOSTED_RUNTIME_MACHINE_SOURCES = new Set([
|
|
41
|
+
'hosted-machine',
|
|
42
|
+
'cloud-coding',
|
|
43
|
+
'dedicated-runtime',
|
|
44
|
+
]);
|
|
45
|
+
const HOSTED_RUNTIME_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/;
|
|
46
|
+
/**
|
|
47
|
+
* The control-plane-issued value of `name` (e.g. `OVERSKY_MACHINE_UUID`), or
|
|
48
|
+
* null when this process is not on a hosted runtime or the value is absent /
|
|
49
|
+
* malformed. Only honoured under a hosted `OVERSKY_DAEMON_SOURCE`, so a stray
|
|
50
|
+
* variable on a laptop cannot rename the machine.
|
|
51
|
+
*/
|
|
52
|
+
export function hostedRuntimeIdOverride(name, env = process.env) {
|
|
53
|
+
if (!HOSTED_RUNTIME_MACHINE_SOURCES.has(env.OVERSKY_DAEMON_SOURCE ?? '')) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const value = env[name]?.trim();
|
|
57
|
+
if (!value || !HOSTED_RUNTIME_ID_RE.test(value))
|
|
58
|
+
return null;
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
let cachedMachineUuid = null;
|
|
62
|
+
function readMachineUuidDarwin() {
|
|
63
|
+
try {
|
|
64
|
+
const out = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
timeout: 2_000,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
});
|
|
69
|
+
const match = out.match(/IOPlatformUUID"?\s*=\s*"?([0-9A-F-]{36})/i);
|
|
70
|
+
return match ? match[1].toLowerCase() : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readMachineUuidLinux() {
|
|
77
|
+
for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(p, 'utf-8').trim();
|
|
80
|
+
if (raw)
|
|
81
|
+
return raw.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Try the next candidate.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function readMachineUuidWindows() {
|
|
90
|
+
try {
|
|
91
|
+
const out = execFileSync('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], { encoding: 'utf-8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
92
|
+
const match = out.match(/MachineGuid\s+REG_SZ\s+([0-9A-Fa-f-]{36})/);
|
|
93
|
+
return match ? match[1].toLowerCase() : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Return a stable, machine-scoped UUID. Cached for the process lifetime.
|
|
101
|
+
*
|
|
102
|
+
* Callers should treat this as opaque: the format differs per platform
|
|
103
|
+
* (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid).
|
|
104
|
+
* The guarantee is only: same machine -> same value across reboots, and the
|
|
105
|
+
* same value from every binary that calls this function.
|
|
106
|
+
*
|
|
107
|
+
* On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
|
|
108
|
+
*
|
|
109
|
+
* Falls back to a deterministic hostname+username hash if the platform
|
|
110
|
+
* probe fails. This preserves server-side record continuity even on
|
|
111
|
+
* locked-down machines that refuse ioreg / registry access.
|
|
112
|
+
*/
|
|
113
|
+
export function getMachineUuid() {
|
|
114
|
+
const hostedMachineUuid = hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID');
|
|
115
|
+
if (hostedMachineUuid)
|
|
116
|
+
return hostedMachineUuid;
|
|
117
|
+
if (cachedMachineUuid !== null)
|
|
118
|
+
return cachedMachineUuid;
|
|
119
|
+
let detected = null;
|
|
120
|
+
switch (process.platform) {
|
|
121
|
+
case 'darwin':
|
|
122
|
+
detected = readMachineUuidDarwin();
|
|
123
|
+
break;
|
|
124
|
+
case 'linux':
|
|
125
|
+
detected = readMachineUuidLinux();
|
|
126
|
+
break;
|
|
127
|
+
case 'win32':
|
|
128
|
+
detected = readMachineUuidWindows();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (detected) {
|
|
132
|
+
cachedMachineUuid = detected;
|
|
133
|
+
return detected;
|
|
134
|
+
}
|
|
135
|
+
const fallback = 'fallback-' +
|
|
136
|
+
crypto
|
|
137
|
+
.createHash('sha256')
|
|
138
|
+
.update(`${os.hostname()}:${os.userInfo().username}`)
|
|
139
|
+
.digest('hex')
|
|
140
|
+
.slice(0, 32);
|
|
141
|
+
cachedMachineUuid = fallback;
|
|
142
|
+
return fallback;
|
|
143
|
+
}
|
|
144
|
+
/** @internal test seam — forget the cached value so the next call re-probes. */
|
|
145
|
+
export function __resetMachineUuidCacheForTest() {
|
|
146
|
+
cachedMachineUuid = null;
|
|
147
|
+
}
|
|
@@ -274,6 +274,14 @@ export declare function persistReauthState(state: ReauthState): void;
|
|
|
274
274
|
export declare function needsReauthState(): {
|
|
275
275
|
needed: boolean;
|
|
276
276
|
reason?: string;
|
|
277
|
+
/**
|
|
278
|
+
* The server's own words for the refusal (`Refresh token bound to a different
|
|
279
|
+
* daemon`). The code alone collapses several distinct causes into one
|
|
280
|
+
* (`REFRESH_INVALID` is also "not recognized" and "issued without a
|
|
281
|
+
* daemonId"), and a surface that names only the code cannot tell the reader
|
|
282
|
+
* which one they hit (OSK-12116).
|
|
283
|
+
*/
|
|
284
|
+
message?: string;
|
|
277
285
|
since?: number;
|
|
278
286
|
};
|
|
279
287
|
export declare function clearReauthState(): void;
|
|
@@ -953,7 +953,14 @@ export function needsReauthState() {
|
|
|
953
953
|
return { needed: false };
|
|
954
954
|
const raw = fs.readFileSync(flagPath, 'utf-8');
|
|
955
955
|
const state = JSON.parse(raw);
|
|
956
|
-
return {
|
|
956
|
+
return {
|
|
957
|
+
needed: true,
|
|
958
|
+
reason: state.code,
|
|
959
|
+
...(typeof state.message === 'string' && state.message.length > 0
|
|
960
|
+
? { message: state.message }
|
|
961
|
+
: {}),
|
|
962
|
+
since: state.at,
|
|
963
|
+
};
|
|
957
964
|
}
|
|
958
965
|
catch {
|
|
959
966
|
return { needed: false };
|
|
@@ -84,3 +84,25 @@ export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
|
|
|
84
84
|
* etc.) so legitimate tools keep working.
|
|
85
85
|
*/
|
|
86
86
|
export declare function sanitizeSpawnEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
87
|
+
/**
|
|
88
|
+
* OSK-12134 — loopback names a spawned child must reach directly, never through
|
|
89
|
+
* a proxy. A harness talks to this machine constantly: the daemon's local
|
|
90
|
+
* bridges (browser, delegated CLI, message intents), the inference broker, and
|
|
91
|
+
* the engine's own loopback services (skrr Code's `session/new` reaches a local
|
|
92
|
+
* directory service and failed with "service failure" behind a shell proxy).
|
|
93
|
+
*/
|
|
94
|
+
export declare const LOOPBACK_NO_PROXY_HOSTS: readonly string[];
|
|
95
|
+
/** True when `env` names a proxy some client in the child might honour. */
|
|
96
|
+
export declare function envHasProxy(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
|
|
99
|
+
* `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
|
|
100
|
+
* spelling they read and whether they fall back to the other, so an unset
|
|
101
|
+
* spelling is seeded from the other one before the hosts are appended —
|
|
102
|
+
* seeding only the hosts would silently drop every existing exemption for a
|
|
103
|
+
* tool that prefers the unset spelling. Existing entries keep their order.
|
|
104
|
+
*
|
|
105
|
+
* Exported for a parent that layers a proxy onto the child's env AFTER
|
|
106
|
+
* {@link sanitizeSpawnEnv}: call it again on the final map.
|
|
107
|
+
*/
|
|
108
|
+
export declare function withLoopbackNoProxy<T extends Record<string, string | undefined>>(env: T): T;
|
|
@@ -183,5 +183,62 @@ export function sanitizeSpawnEnv(env) {
|
|
|
183
183
|
continue;
|
|
184
184
|
out[key] = value;
|
|
185
185
|
}
|
|
186
|
-
return out;
|
|
186
|
+
return withLoopbackNoProxy(out);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* OSK-12134 — loopback names a spawned child must reach directly, never through
|
|
190
|
+
* a proxy. A harness talks to this machine constantly: the daemon's local
|
|
191
|
+
* bridges (browser, delegated CLI, message intents), the inference broker, and
|
|
192
|
+
* the engine's own loopback services (skrr Code's `session/new` reaches a local
|
|
193
|
+
* directory service and failed with "service failure" behind a shell proxy).
|
|
194
|
+
*/
|
|
195
|
+
export const LOOPBACK_NO_PROXY_HOSTS = ['127.0.0.1', 'localhost', '::1'];
|
|
196
|
+
const PROXY_ENV_VARS = [
|
|
197
|
+
'HTTP_PROXY',
|
|
198
|
+
'http_proxy',
|
|
199
|
+
'HTTPS_PROXY',
|
|
200
|
+
'https_proxy',
|
|
201
|
+
'ALL_PROXY',
|
|
202
|
+
'all_proxy',
|
|
203
|
+
];
|
|
204
|
+
/** True when `env` names a proxy some client in the child might honour. */
|
|
205
|
+
export function envHasProxy(env) {
|
|
206
|
+
return PROXY_ENV_VARS.some((name) => typeof env[name] === 'string' && env[name].trim() !== '');
|
|
207
|
+
}
|
|
208
|
+
function appendNoProxyHosts(list, hosts) {
|
|
209
|
+
const entries = list
|
|
210
|
+
.split(',')
|
|
211
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
// `*` already exempts everything, and some clients honour it only as the
|
|
214
|
+
// entire value, so appending to it would break them.
|
|
215
|
+
if (entries.includes('*'))
|
|
216
|
+
return list;
|
|
217
|
+
const missing = hosts.filter((host) => !entries.includes(host));
|
|
218
|
+
if (missing.length === 0)
|
|
219
|
+
return list;
|
|
220
|
+
const kept = list.replace(/[\s,]+$/, '');
|
|
221
|
+
return kept ? `${kept},${missing.join(',')}` : missing.join(',');
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
|
|
225
|
+
* `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
|
|
226
|
+
* spelling they read and whether they fall back to the other, so an unset
|
|
227
|
+
* spelling is seeded from the other one before the hosts are appended —
|
|
228
|
+
* seeding only the hosts would silently drop every existing exemption for a
|
|
229
|
+
* tool that prefers the unset spelling. Existing entries keep their order.
|
|
230
|
+
*
|
|
231
|
+
* Exported for a parent that layers a proxy onto the child's env AFTER
|
|
232
|
+
* {@link sanitizeSpawnEnv}: call it again on the final map.
|
|
233
|
+
*/
|
|
234
|
+
export function withLoopbackNoProxy(env) {
|
|
235
|
+
if (!envHasProxy(env))
|
|
236
|
+
return env;
|
|
237
|
+
const upper = env.NO_PROXY?.trim() ? env.NO_PROXY : undefined;
|
|
238
|
+
const lower = env.no_proxy?.trim() ? env.no_proxy : undefined;
|
|
239
|
+
return {
|
|
240
|
+
...env,
|
|
241
|
+
NO_PROXY: appendNoProxyHosts(upper ?? lower ?? '', LOOPBACK_NO_PROXY_HOSTS),
|
|
242
|
+
no_proxy: appendNoProxyHosts(lower ?? upper ?? '', LOOPBACK_NO_PROXY_HOSTS),
|
|
243
|
+
};
|
|
187
244
|
}
|