@bitkyc08/opencodex 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import net, { type Socket } from "node:net";
|
|
2
|
+
|
|
3
|
+
const DIRECT_LOCAL_HTTP_MAX_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
const DIRECT_LOCAL_HTTP_TIMEOUT_MS = 10_000;
|
|
5
|
+
|
|
6
|
+
type DirectLocalHttpIo = {
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
connect?: (hostname: string, port: number) => Socket;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function abortReason(signal: AbortSignal): Error {
|
|
12
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
13
|
+
const error = new Error("direct local HTTP request aborted");
|
|
14
|
+
error.name = "AbortError";
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function headerBoundary(bytes: Buffer): number {
|
|
19
|
+
return bytes.indexOf("\r\n\r\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateTrailerLines(bytes: Buffer, start: number, end: number): void {
|
|
23
|
+
if (end === start) return;
|
|
24
|
+
for (const line of bytes.subarray(start, end).toString("latin1").split("\r\n")) {
|
|
25
|
+
if (line.indexOf(":") <= 0) {
|
|
26
|
+
throw new Error("direct local HTTP response has an invalid chunk trailer");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function decodeChunkedBody(body: Buffer): Buffer {
|
|
32
|
+
const chunks: Buffer[] = [];
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (;;) {
|
|
35
|
+
const lineEnd = body.indexOf("\r\n", offset);
|
|
36
|
+
if (lineEnd < 0) throw new Error("direct local HTTP response has a truncated chunk header");
|
|
37
|
+
const rawSize = body.subarray(offset, lineEnd).toString("ascii").split(";", 1)[0]?.trim() ?? "";
|
|
38
|
+
if (!/^[0-9a-f]+$/i.test(rawSize)) throw new Error("direct local HTTP response has an invalid chunk size");
|
|
39
|
+
const size = Number.parseInt(rawSize, 16);
|
|
40
|
+
offset = lineEnd + 2;
|
|
41
|
+
if (size === 0) {
|
|
42
|
+
if (body.length < offset + 2) throw new Error("direct local HTTP response has a truncated chunk trailer");
|
|
43
|
+
if (body[offset] !== 13 || body[offset + 1] !== 10) {
|
|
44
|
+
const trailerEnd = body.indexOf("\r\n\r\n", offset);
|
|
45
|
+
if (trailerEnd < 0) {
|
|
46
|
+
throw new Error("direct local HTTP response has a truncated chunk trailer");
|
|
47
|
+
}
|
|
48
|
+
validateTrailerLines(body, offset, trailerEnd);
|
|
49
|
+
if (trailerEnd + 4 !== body.length) {
|
|
50
|
+
throw new Error("direct local HTTP response has trailing bytes");
|
|
51
|
+
}
|
|
52
|
+
} else if (offset + 2 !== body.length) {
|
|
53
|
+
throw new Error("direct local HTTP response has trailing bytes");
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat(chunks);
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isSafeInteger(size) || offset + size + 2 > body.length) {
|
|
58
|
+
throw new Error("direct local HTTP response has a truncated chunk body");
|
|
59
|
+
}
|
|
60
|
+
chunks.push(body.subarray(offset, offset + size));
|
|
61
|
+
offset += size;
|
|
62
|
+
if (body[offset] !== 13 || body[offset + 1] !== 10) {
|
|
63
|
+
throw new Error("direct local HTTP response has an invalid chunk terminator");
|
|
64
|
+
}
|
|
65
|
+
offset += 2;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ResponseFraming =
|
|
70
|
+
| { kind: "head"; searchFrom: number }
|
|
71
|
+
| { kind: "content-length"; totalBytes: number }
|
|
72
|
+
| { kind: "chunk-size"; offset: number; searchFrom: number }
|
|
73
|
+
| { kind: "chunk-body"; offset: number; size: number }
|
|
74
|
+
| { kind: "trailers"; offset: number; searchFrom: number }
|
|
75
|
+
| { kind: "close" }
|
|
76
|
+
| { kind: "complete" };
|
|
77
|
+
|
|
78
|
+
function parseResponseHead(bytes: Buffer, boundary: number): {
|
|
79
|
+
status: number;
|
|
80
|
+
statusText: string;
|
|
81
|
+
headers: Headers;
|
|
82
|
+
} {
|
|
83
|
+
const lines = bytes.subarray(0, boundary).toString("latin1").split("\r\n");
|
|
84
|
+
const statusLine = lines.shift() ?? "";
|
|
85
|
+
const match = /^HTTP\/1\.[01] ([0-9]{3})(?: (.*))?$/.exec(statusLine);
|
|
86
|
+
if (!match) throw new Error("direct local HTTP response has an invalid status line");
|
|
87
|
+
const status = Number(match[1]);
|
|
88
|
+
if (status < 200 || status > 599) throw new Error("direct local HTTP response has an unsupported status");
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const colon = line.indexOf(":");
|
|
92
|
+
if (colon <= 0) throw new Error("direct local HTTP response has an invalid header");
|
|
93
|
+
headers.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
|
|
94
|
+
}
|
|
95
|
+
return { status, statusText: match[2] ?? "", headers };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function advanceResponseFraming(bytes: Buffer, initial: ResponseFraming): ResponseFraming {
|
|
99
|
+
let framing = initial;
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (framing.kind === "complete" || framing.kind === "close") return framing;
|
|
102
|
+
if (framing.kind === "head") {
|
|
103
|
+
const boundary = bytes.indexOf("\r\n\r\n", framing.searchFrom);
|
|
104
|
+
if (boundary < 0) {
|
|
105
|
+
if (bytes.length > 64 * 1024) throw new Error("direct local HTTP response headers exceed the byte cap");
|
|
106
|
+
return { kind: "head", searchFrom: Math.max(0, bytes.length - 3) };
|
|
107
|
+
}
|
|
108
|
+
const { status, headers } = parseResponseHead(bytes, boundary);
|
|
109
|
+
const bodyStart = boundary + 4;
|
|
110
|
+
if (status === 204 || status === 205 || status === 304) {
|
|
111
|
+
if (bytes.length > bodyStart) {
|
|
112
|
+
throw new Error("direct local HTTP response has a body for a bodyless status");
|
|
113
|
+
}
|
|
114
|
+
return { kind: "complete" };
|
|
115
|
+
}
|
|
116
|
+
if (/\bchunked\b/i.test(headers.get("transfer-encoding") ?? "")) {
|
|
117
|
+
framing = { kind: "chunk-size", offset: bodyStart, searchFrom: bodyStart };
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const rawLength = headers.get("content-length");
|
|
121
|
+
if (rawLength === null) return { kind: "close" };
|
|
122
|
+
if (!/^[0-9]+$/.test(rawLength)) throw new Error("direct local HTTP response has an invalid content length");
|
|
123
|
+
const length = Number(rawLength);
|
|
124
|
+
const totalBytes = bodyStart + length;
|
|
125
|
+
if (!Number.isSafeInteger(length) || totalBytes > DIRECT_LOCAL_HTTP_MAX_BYTES) {
|
|
126
|
+
throw new Error("direct local HTTP response exceeds the byte cap");
|
|
127
|
+
}
|
|
128
|
+
framing = { kind: "content-length", totalBytes };
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (framing.kind === "content-length") {
|
|
132
|
+
if (bytes.length > framing.totalBytes) {
|
|
133
|
+
throw new Error("direct local HTTP response has trailing bytes");
|
|
134
|
+
}
|
|
135
|
+
return bytes.length === framing.totalBytes ? { kind: "complete" } : framing;
|
|
136
|
+
}
|
|
137
|
+
if (framing.kind === "chunk-size") {
|
|
138
|
+
const lineEnd = bytes.indexOf("\r\n", framing.searchFrom);
|
|
139
|
+
if (lineEnd < 0) {
|
|
140
|
+
if (bytes.length - framing.offset > 8 * 1024) {
|
|
141
|
+
throw new Error("direct local HTTP response chunk header exceeds the byte cap");
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
...framing,
|
|
145
|
+
searchFrom: Math.max(framing.offset, bytes.length - 1),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const rawSize = bytes.subarray(framing.offset, lineEnd).toString("ascii").split(";", 1)[0]?.trim() ?? "";
|
|
149
|
+
if (!/^[0-9a-f]+$/i.test(rawSize)) throw new Error("direct local HTTP response has an invalid chunk size");
|
|
150
|
+
const size = Number.parseInt(rawSize, 16);
|
|
151
|
+
if (!Number.isSafeInteger(size) || size > DIRECT_LOCAL_HTTP_MAX_BYTES) {
|
|
152
|
+
throw new Error("direct local HTTP response chunk exceeds the byte cap");
|
|
153
|
+
}
|
|
154
|
+
const offset = lineEnd + 2;
|
|
155
|
+
framing = size === 0
|
|
156
|
+
? { kind: "trailers", offset, searchFrom: offset }
|
|
157
|
+
: { kind: "chunk-body", offset, size };
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (framing.kind === "chunk-body") {
|
|
161
|
+
const terminator = framing.offset + framing.size;
|
|
162
|
+
if (terminator + 2 > bytes.length) return framing;
|
|
163
|
+
if (bytes[terminator] !== 13 || bytes[terminator + 1] !== 10) {
|
|
164
|
+
throw new Error("direct local HTTP response has an invalid chunk terminator");
|
|
165
|
+
}
|
|
166
|
+
framing = { kind: "chunk-size", offset: terminator + 2, searchFrom: terminator + 2 };
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (bytes.length < framing.offset + 2) return framing;
|
|
170
|
+
if (bytes[framing.offset] === 13 && bytes[framing.offset + 1] === 10) {
|
|
171
|
+
if (bytes.length > framing.offset + 2) {
|
|
172
|
+
throw new Error("direct local HTTP response has trailing bytes");
|
|
173
|
+
}
|
|
174
|
+
return { kind: "complete" };
|
|
175
|
+
}
|
|
176
|
+
const trailerEnd = bytes.indexOf("\r\n\r\n", framing.searchFrom);
|
|
177
|
+
if (trailerEnd >= 0) {
|
|
178
|
+
validateTrailerLines(bytes, framing.offset, trailerEnd);
|
|
179
|
+
if (bytes.length > trailerEnd + 4) {
|
|
180
|
+
throw new Error("direct local HTTP response has trailing bytes");
|
|
181
|
+
}
|
|
182
|
+
return { kind: "complete" };
|
|
183
|
+
}
|
|
184
|
+
if (bytes.length - framing.offset > 64 * 1024) {
|
|
185
|
+
throw new Error("direct local HTTP response trailers exceed the byte cap");
|
|
186
|
+
}
|
|
187
|
+
return { ...framing, searchFrom: Math.max(framing.offset, bytes.length - 3) };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseResponse(bytes: Buffer): Response {
|
|
192
|
+
const boundary = headerBoundary(bytes);
|
|
193
|
+
if (boundary < 0) throw new Error("direct local HTTP response has no header boundary");
|
|
194
|
+
const { status, statusText, headers } = parseResponseHead(bytes, boundary);
|
|
195
|
+
|
|
196
|
+
let body = bytes.subarray(boundary + 4);
|
|
197
|
+
if (/\bchunked\b/i.test(headers.get("transfer-encoding") ?? "")) {
|
|
198
|
+
body = decodeChunkedBody(body);
|
|
199
|
+
headers.delete("transfer-encoding");
|
|
200
|
+
headers.delete("content-length");
|
|
201
|
+
} else {
|
|
202
|
+
const rawLength = headers.get("content-length");
|
|
203
|
+
if (rawLength !== null) {
|
|
204
|
+
if (!/^[0-9]+$/.test(rawLength)) throw new Error("direct local HTTP response has an invalid content length");
|
|
205
|
+
const length = Number(rawLength);
|
|
206
|
+
if (!Number.isSafeInteger(length) || body.byteLength < length) {
|
|
207
|
+
throw new Error("direct local HTTP response body is truncated");
|
|
208
|
+
}
|
|
209
|
+
body = body.subarray(0, length);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const bodyless = status === 204 || status === 205 || status === 304;
|
|
214
|
+
return new Response(bodyless ? null : new Uint8Array(body), {
|
|
215
|
+
status,
|
|
216
|
+
statusText,
|
|
217
|
+
headers,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fetch one local HTTP GET over a direct TCP connection.
|
|
223
|
+
*
|
|
224
|
+
* Bun's global fetch and Bun 1.3's node:http compatibility layer can honor
|
|
225
|
+
* HTTP(S)_PROXY. Local identity and capability probes must not expose headers
|
|
226
|
+
* to, or accept a fabricated response from, such a proxy. node:net connects to
|
|
227
|
+
* the selected listener without consulting proxy environment variables. Each
|
|
228
|
+
* caller retains an injected fetch seam for deterministic unit tests.
|
|
229
|
+
*/
|
|
230
|
+
export async function directLocalHttpFetch(
|
|
231
|
+
input: string | URL | Request,
|
|
232
|
+
init: RequestInit = {},
|
|
233
|
+
io: DirectLocalHttpIo = {},
|
|
234
|
+
): Promise<Response> {
|
|
235
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
236
|
+
const method = (init.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
237
|
+
const signal = init.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
238
|
+
const body = init.body ?? (input instanceof Request ? input.body : null);
|
|
239
|
+
|
|
240
|
+
if (url.protocol !== "http:") throw new Error("direct local request must use HTTP");
|
|
241
|
+
if (url.username || url.password) throw new Error("direct local request URL must not contain credentials");
|
|
242
|
+
if (method !== "GET" || body !== null) throw new Error("direct local request must be a bodyless GET");
|
|
243
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
244
|
+
|
|
245
|
+
const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
246
|
+
headers.delete("proxy-authorization");
|
|
247
|
+
headers.delete("proxy-connection");
|
|
248
|
+
headers.set("host", url.host);
|
|
249
|
+
headers.set("connection", "close");
|
|
250
|
+
const headerLines: string[] = [];
|
|
251
|
+
headers.forEach((value, key) => { headerLines.push(`${key}: ${value}`); });
|
|
252
|
+
const requestBytes = Buffer.from(
|
|
253
|
+
`GET ${url.pathname}${url.search} HTTP/1.1\r\n${headerLines.join("\r\n")}\r\n\r\n`,
|
|
254
|
+
"latin1",
|
|
255
|
+
);
|
|
256
|
+
const parsedHostname = url.hostname.startsWith("[") && url.hostname.endsWith("]")
|
|
257
|
+
? url.hostname.slice(1, -1)
|
|
258
|
+
: url.hostname;
|
|
259
|
+
const hostname = parsedHostname;
|
|
260
|
+
const port = url.port ? Number(url.port) : 80;
|
|
261
|
+
const timeoutMs = io.timeoutMs ?? DIRECT_LOCAL_HTTP_TIMEOUT_MS;
|
|
262
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
263
|
+
throw new Error("direct local HTTP timeout must be positive");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return await new Promise<Response>((resolve, reject) => {
|
|
267
|
+
let socket: Socket | undefined;
|
|
268
|
+
let settled = false;
|
|
269
|
+
let receivedBytes = 0;
|
|
270
|
+
let responseBytes = Buffer.allocUnsafe(4 * 1024);
|
|
271
|
+
let framing: ResponseFraming = { kind: "head", searchFrom: 0 };
|
|
272
|
+
const cleanup = () => {
|
|
273
|
+
signal?.removeEventListener("abort", onAbort);
|
|
274
|
+
socket?.setTimeout(0);
|
|
275
|
+
};
|
|
276
|
+
const finish = (error?: Error) => {
|
|
277
|
+
if (settled) return;
|
|
278
|
+
settled = true;
|
|
279
|
+
cleanup();
|
|
280
|
+
try { socket?.destroy(); } catch { /* ignore */ }
|
|
281
|
+
if (error) {
|
|
282
|
+
reject(error);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
resolve(parseResponse(responseBytes.subarray(0, receivedBytes)));
|
|
287
|
+
} catch (parseError) {
|
|
288
|
+
reject(parseError instanceof Error ? parseError : new Error(String(parseError)));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const onAbort = () => {
|
|
292
|
+
const error = signal ? abortReason(signal) : new Error("direct local HTTP request aborted");
|
|
293
|
+
finish(error);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
socket = (io.connect ?? ((host, selectedPort) => net.createConnection({
|
|
297
|
+
host,
|
|
298
|
+
port: selectedPort,
|
|
299
|
+
autoSelectFamily: true,
|
|
300
|
+
})))(hostname, port);
|
|
301
|
+
socket.setTimeout(timeoutMs, () => {
|
|
302
|
+
const error = new Error("direct local HTTP request timed out");
|
|
303
|
+
error.name = "TimeoutError";
|
|
304
|
+
finish(error);
|
|
305
|
+
});
|
|
306
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
307
|
+
if (signal?.aborted) {
|
|
308
|
+
onAbort();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
socket.on("connect", () => {
|
|
312
|
+
try { socket?.write(requestBytes); } catch (error) {
|
|
313
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
socket.on("data", chunk => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
const bytes = Buffer.from(chunk);
|
|
319
|
+
receivedBytes += bytes.byteLength;
|
|
320
|
+
if (receivedBytes > DIRECT_LOCAL_HTTP_MAX_BYTES) {
|
|
321
|
+
finish(new Error("direct local HTTP response exceeds the byte cap"));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (receivedBytes > responseBytes.byteLength) {
|
|
325
|
+
let capacity = responseBytes.byteLength;
|
|
326
|
+
while (capacity < receivedBytes) capacity = Math.min(DIRECT_LOCAL_HTTP_MAX_BYTES, capacity * 2);
|
|
327
|
+
const grown = Buffer.allocUnsafe(capacity);
|
|
328
|
+
responseBytes.copy(grown);
|
|
329
|
+
responseBytes = grown;
|
|
330
|
+
}
|
|
331
|
+
bytes.copy(responseBytes, receivedBytes - bytes.byteLength);
|
|
332
|
+
try {
|
|
333
|
+
framing = advanceResponseFraming(responseBytes.subarray(0, receivedBytes), framing);
|
|
334
|
+
if (framing.kind === "complete") finish();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
socket.once("end", () => finish());
|
|
340
|
+
socket.once("error", error => finish(error));
|
|
341
|
+
socket.once("close", () => finish());
|
|
342
|
+
});
|
|
343
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
armClaudeCodeBaseline,
|
|
17
17
|
loadConfig,
|
|
18
18
|
saveConfig,
|
|
19
|
+
getConfigDir,
|
|
19
20
|
websocketsEnabled,
|
|
20
21
|
} from "../config";
|
|
21
22
|
import { reconcileOAuthProviders } from "../oauth";
|
|
@@ -23,7 +24,10 @@ import { withCatalogWriteSerialization } from "../codex/catalog-write-serializat
|
|
|
23
24
|
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
24
25
|
import { getCodexHome } from "../codex/paths";
|
|
25
26
|
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
inspectNativeCodexOwnership,
|
|
29
|
+
type OwnershipInspection,
|
|
30
|
+
} from "../integrations/native/ownership-preflight";
|
|
27
31
|
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
28
32
|
import {
|
|
29
33
|
reconcileLiveStateStores,
|
|
@@ -41,9 +45,16 @@ import {
|
|
|
41
45
|
registerDefaultAppOwnedObservedBuffers,
|
|
42
46
|
} from "../lib/app-owned-memory-stores";
|
|
43
47
|
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
48
|
+
import {
|
|
49
|
+
setLabAutomationDispatchDeps,
|
|
50
|
+
startLabAutomationScheduler,
|
|
51
|
+
} from "../lab/automation/orchestrator";
|
|
52
|
+
import { loadLabAutomationPolicy } from "../lab/automation/persistence";
|
|
53
|
+
import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
|
|
44
54
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
45
55
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
46
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
56
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
57
|
+
import { providerContextCap } from "../providers/context-cap";
|
|
47
58
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
48
59
|
import type { StorageCleanupPolicy } from "../types";
|
|
49
60
|
import {
|
|
@@ -89,7 +100,6 @@ import {
|
|
|
89
100
|
addFinalRequestLog,
|
|
90
101
|
hydrateRequestLogsFromDisk,
|
|
91
102
|
httpStatusForRequestLogTerminal,
|
|
92
|
-
httpStatusForTerminalStatus,
|
|
93
103
|
inspectResponseLogSsePayload,
|
|
94
104
|
nextRequestLogId,
|
|
95
105
|
recordFirstOutput,
|
|
@@ -161,6 +171,7 @@ import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
|
161
171
|
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
162
172
|
import {
|
|
163
173
|
bindNativeMainStartupLifecycle,
|
|
174
|
+
blockNativeMainStartupForUnownedServiceHome,
|
|
164
175
|
releaseNativeMainStartupLifecycle,
|
|
165
176
|
startNativeMainStartupLifecycle,
|
|
166
177
|
type NativeMainStartupGateDeps,
|
|
@@ -425,6 +436,8 @@ export interface StartServerDeps {
|
|
|
425
436
|
managementApi?: ManagementApiDeps;
|
|
426
437
|
/** Test-only native-main recovery dependencies; production constructs the normal manager. */
|
|
427
438
|
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
439
|
+
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
440
|
+
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
428
441
|
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
429
442
|
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
430
443
|
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
@@ -433,6 +446,17 @@ export interface StartServerDeps {
|
|
|
433
446
|
readinessGate?: ReadinessGate;
|
|
434
447
|
}
|
|
435
448
|
|
|
449
|
+
function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
|
|
450
|
+
try {
|
|
451
|
+
return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
|
|
452
|
+
} catch {
|
|
453
|
+
return {
|
|
454
|
+
ownership: "unknown",
|
|
455
|
+
reason: "service-home ownership inspection failed",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
436
460
|
/*
|
|
437
461
|
* #1046. `startServer` rewrites the Codex models cache during boot, and an
|
|
438
462
|
* app-server that started earlier keeps its own in-memory model list. The stale
|
|
@@ -497,21 +521,28 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
497
521
|
if (migrated) saveConfig(config);
|
|
498
522
|
}
|
|
499
523
|
}
|
|
524
|
+
// Resolve unattended service-home authority before any Codex lock, cache, owner,
|
|
525
|
+
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
526
|
+
// ownership state are non-authority.
|
|
527
|
+
startupCacheInvalidationWrote = false;
|
|
528
|
+
const startupCacheOwnership = inspectStartupOwnership(deps);
|
|
500
529
|
// Startup cache invalidation is best-effort and must never block the server from
|
|
501
530
|
// serving. It now takes K so it cannot race a convergence commit, but both the
|
|
502
531
|
// home resolution and the acquisition can fail on a machine with no Codex home —
|
|
503
532
|
// `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
|
|
504
533
|
// otherwise turn "no Codex installed" into "proxy will not start".
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
534
|
+
if (startupCacheOwnership.ownership === "owned") {
|
|
535
|
+
try {
|
|
536
|
+
const startupCodexHome = getCodexHome();
|
|
537
|
+
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
538
|
+
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
539
|
+
// here instead would read a catalog mtime the sync is about to move.
|
|
540
|
+
const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
|
|
541
|
+
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
|
|
542
|
+
// A refused permit is not a write; only a completed run that returned true is.
|
|
543
|
+
startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
|
|
544
|
+
} catch { /* no readable Codex home: nothing to invalidate */ }
|
|
545
|
+
}
|
|
515
546
|
// Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
|
|
516
547
|
// the server can serve a request, and AFTER the startup migrations above — those run
|
|
517
548
|
// against a config nobody else holds and are the documented exception to the save
|
|
@@ -650,10 +681,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
650
681
|
// CODEX_HOME. When the user has disabled the Codex integration, starting the
|
|
651
682
|
// proxy must not manufacture those Codex artifacts merely to serve other
|
|
652
683
|
// clients; no Codex request can use this lifecycle in that state.
|
|
653
|
-
|
|
684
|
+
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
685
|
+
// between the two sites must not widen the service-install race.
|
|
686
|
+
const nativeOwnership = inspectStartupOwnership(deps);
|
|
654
687
|
const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
|
|
655
|
-
|
|
656
|
-
|
|
688
|
+
? nativeOwnership.ownership === "owned"
|
|
689
|
+
? startNativeMainStartupLifecycle(deps.nativeMainStartup)
|
|
690
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
691
|
+
nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
|
|
692
|
+
)
|
|
657
693
|
: {
|
|
658
694
|
homeId: null,
|
|
659
695
|
settled: Promise.resolve({ status: "ready", homeId: null }),
|
|
@@ -911,7 +947,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
911
947
|
const catalogNativeSlugs = accountSelectors.length > 0
|
|
912
948
|
? NATIVE_OPENAI_MODELS
|
|
913
949
|
: nativeSlugs;
|
|
914
|
-
const entries = buildCatalogEntries(
|
|
950
|
+
const entries = buildCatalogEntries(
|
|
951
|
+
loadCatalogTemplate(),
|
|
952
|
+
catalogNativeSlugs,
|
|
953
|
+
goOrdered,
|
|
954
|
+
config.subagentModels,
|
|
955
|
+
websocketsEnabled(config),
|
|
956
|
+
maMode as "v1" | "default" | "v2",
|
|
957
|
+
exactComboCatalogSlugs(config),
|
|
958
|
+
accountSelectors,
|
|
959
|
+
suppressedBareNativeSlugs,
|
|
960
|
+
new Set(),
|
|
961
|
+
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
962
|
+
);
|
|
915
963
|
return jsonResponse({
|
|
916
964
|
models: applyNativeVisibility(
|
|
917
965
|
entries,
|
|
@@ -1136,7 +1184,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1136
1184
|
abortSignal: req.signal,
|
|
1137
1185
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1138
1186
|
onNativePassthroughTerminal: status => {
|
|
1139
|
-
finalizeNativePassthroughLog(
|
|
1187
|
+
finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
1140
1188
|
terminalStatus: status,
|
|
1141
1189
|
closeReason: "terminal",
|
|
1142
1190
|
});
|
|
@@ -1646,5 +1694,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1646
1694
|
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1647
1695
|
backgroundLifecycle.scheduleStartupRun();
|
|
1648
1696
|
|
|
1697
|
+
const labConfigDir = getConfigDir();
|
|
1698
|
+
const productionLabRouteExecutor = createProductionLabRouteExecutor({
|
|
1699
|
+
configDir: labConfigDir,
|
|
1700
|
+
loadConfig: () => config,
|
|
1701
|
+
});
|
|
1702
|
+
setLabAutomationDispatchDeps({
|
|
1703
|
+
configDir: labConfigDir,
|
|
1704
|
+
loadConfig: () => config,
|
|
1705
|
+
routeExecutor: productionLabRouteExecutor,
|
|
1706
|
+
});
|
|
1707
|
+
if (loadLabAutomationPolicy(labConfigDir).enabled) {
|
|
1708
|
+
startLabAutomationScheduler(labConfigDir);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1649
1711
|
return server;
|
|
1650
1712
|
}
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { flushAntigravityReplay } from "../adapters/google-antigravity-replay";
|
|
1
2
|
import { flushResponseState } from "../responses/state";
|
|
2
3
|
import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
|
|
3
4
|
import {
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
} from "../storage/policy-job";
|
|
7
8
|
import { abortRestoreTrashJobAsync } from "../storage/restore-job";
|
|
8
9
|
import { stopStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
10
|
+
import { stopLabAutomationScheduler, requestLabAutomationShutdown } from "../lab/automation/orchestrator";
|
|
9
11
|
import { stopStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
10
12
|
import {
|
|
11
13
|
cancelQueuedStorageWorkerSpawns,
|
|
@@ -433,12 +435,16 @@ export async function drainAndShutdown(
|
|
|
433
435
|
console.warn("[cursor] background shell drain incomplete", shellResult.value);
|
|
434
436
|
}
|
|
435
437
|
|
|
436
|
-
// Debounced replay-state
|
|
437
|
-
// previous_response_id chain
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
// Debounced replay-state snapshots may still be pending; flush so the last completed turn's
|
|
439
|
+
// previous_response_id chain and antigravity thought signatures survive the restart this
|
|
440
|
+
// shutdown is usually part of.
|
|
441
|
+
const stateFlush = await Promise.allSettled([flushResponseState(), flushAntigravityReplay()]);
|
|
442
|
+
if (stateFlush[0]?.status === "rejected") {
|
|
440
443
|
console.warn("[responses] state flush during shutdown failed");
|
|
441
444
|
}
|
|
445
|
+
if (stateFlush[1]?.status === "rejected") {
|
|
446
|
+
console.warn("[antigravity] replay flush during shutdown failed");
|
|
447
|
+
}
|
|
442
448
|
|
|
443
449
|
// Tear down opt-in storage policy timers / worker / live-config sink so they cannot fire after stop.
|
|
444
450
|
// Await worker thread exit: on Windows, a still-exiting Bun Worker under
|
|
@@ -446,6 +452,8 @@ export async function drainAndShutdown(
|
|
|
446
452
|
// Abort each job independently so one wedged join cannot skip the other,
|
|
447
453
|
// then drain leftovers; failures must not prevent `server.stop`.
|
|
448
454
|
stopStorageCleanupScheduler();
|
|
455
|
+
requestLabAutomationShutdown();
|
|
456
|
+
stopLabAutomationScheduler();
|
|
449
457
|
stopStateStoreSweeper();
|
|
450
458
|
// The overlay reconciler is owner-scoped: the startServer stop override
|
|
451
459
|
// releases THIS server's lease through runListenerShutdown →
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readRuntimePort, type RuntimePortState } from "../config";
|
|
2
|
+
import { createLocalAttestationChallenge } from "../lib/local-management-attestation";
|
|
3
|
+
import {
|
|
4
|
+
LOCAL_MANAGEMENT_CAPABILITY_HEADER,
|
|
5
|
+
LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
|
|
6
|
+
LOCAL_MANAGEMENT_CAPABILITY_TTL_MS,
|
|
7
|
+
LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
|
|
8
|
+
LOCAL_MANAGEMENT_NONCE_HEADER,
|
|
9
|
+
createLocalManagementReadCapability,
|
|
10
|
+
type LocalManagementReadPath,
|
|
11
|
+
} from "../lib/local-management-capability";
|
|
12
|
+
import { directLocalHttpFetch } from "./direct-local-http";
|
|
13
|
+
import { probeHostname, type LiveProxy } from "./proxy-liveness";
|
|
14
|
+
|
|
15
|
+
export type LocalManagementReadResult =
|
|
16
|
+
| { kind: "response"; response: Response; targetPid: number }
|
|
17
|
+
| { kind: "unavailable"; reason: "unattested-target" | "runtime-mismatch" | "capability-unavailable" | "transport" };
|
|
18
|
+
|
|
19
|
+
export interface LocalManagementReadDeps {
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
readRuntime?: (pid: number) => RuntimePortState | null;
|
|
22
|
+
createNonce?: () => string;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LocalManagementReadRequestDeps extends LocalManagementReadDeps {
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read one exact local management endpoint without sending a reusable admin credential.
|
|
32
|
+
*
|
|
33
|
+
* The runtime record is the protected source of the per-process key. The server proves
|
|
34
|
+
* it owns that key by accepting a single-use capability bound to this GET, path, PID,
|
|
35
|
+
* port, and short expiry.
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchBoundLocalManagementRead(
|
|
38
|
+
target: LiveProxy,
|
|
39
|
+
path: LocalManagementReadPath,
|
|
40
|
+
deps: LocalManagementReadRequestDeps = {},
|
|
41
|
+
): Promise<LocalManagementReadResult> {
|
|
42
|
+
if (
|
|
43
|
+
target.source !== "runtime"
|
|
44
|
+
|| target.pid === null
|
|
45
|
+
|| !Number.isSafeInteger(target.pid)
|
|
46
|
+
|| target.pid <= 0
|
|
47
|
+
) {
|
|
48
|
+
return { kind: "unavailable", reason: "unattested-target" };
|
|
49
|
+
}
|
|
50
|
+
const readRuntime = deps.readRuntime ?? readRuntimePort;
|
|
51
|
+
const runtime = readRuntime(target.pid);
|
|
52
|
+
if (
|
|
53
|
+
!runtime?.attestationSecret
|
|
54
|
+
|| runtime.pid !== target.pid
|
|
55
|
+
|| runtime.port !== target.port
|
|
56
|
+
) {
|
|
57
|
+
return { kind: "unavailable", reason: "runtime-mismatch" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const nonce = (deps.createNonce ?? createLocalAttestationChallenge)();
|
|
61
|
+
const expiresAt = (deps.now ?? Date.now)() + LOCAL_MANAGEMENT_CAPABILITY_TTL_MS;
|
|
62
|
+
const capability = createLocalManagementReadCapability(
|
|
63
|
+
runtime.attestationSecret,
|
|
64
|
+
nonce,
|
|
65
|
+
"GET",
|
|
66
|
+
path,
|
|
67
|
+
target.pid,
|
|
68
|
+
target.port,
|
|
69
|
+
expiresAt,
|
|
70
|
+
);
|
|
71
|
+
if (!capability) return { kind: "unavailable", reason: "capability-unavailable" };
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await (deps.fetchImpl ?? directLocalHttpFetch)(
|
|
75
|
+
`http://${probeHostname(target.hostname)}:${target.port}${path}`,
|
|
76
|
+
{
|
|
77
|
+
headers: {
|
|
78
|
+
[LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
|
|
79
|
+
[LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
|
|
80
|
+
[LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER]: String(expiresAt),
|
|
81
|
+
[LOCAL_MANAGEMENT_CAPABILITY_HEADER]: capability,
|
|
82
|
+
},
|
|
83
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? 4_000),
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
return { kind: "response", response, targetPid: target.pid };
|
|
87
|
+
} catch {
|
|
88
|
+
return { kind: "unavailable", reason: "transport" };
|
|
89
|
+
}
|
|
90
|
+
}
|