agent-embassy 1.4.1 → 1.6.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/CHANGELOG.md +40 -1
- package/CONTRIBUTING.md +69 -6
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/SECURITY.md +152 -27
- package/dist/src/gateway/claude-peer.d.ts +7 -4
- package/dist/src/gateway/claude-peer.js +80 -36
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +4 -0
- package/dist/src/gateway/claude-runtime.js +50 -39
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.js +1 -1
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +1 -1
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +6 -7
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +44 -2
- package/dist/src/gateway/codex-app-server.js +536 -2
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -6
- package/dist/src/gateway/codex-local-transport.js +101 -56
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +42 -3
- package/dist/src/gateway/compatibility.js +190 -37
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/control.js +28 -30
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +42 -17
- package/dist/src/gateway/dashboard-copy.en.js +56 -31
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +42 -17
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +42 -17
- package/dist/src/gateway/dashboard-copy.zh-CN.js +56 -31
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +199 -11
- package/dist/src/gateway/dashboard-model.js +478 -75
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +3 -0
- package/dist/src/gateway/dashboard.js +62 -137
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +13 -0
- package/dist/src/gateway/deepseek-detect.js +125 -0
- package/dist/src/gateway/deepseek-detect.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +158 -256
- package/dist/src/gateway/live-dashboard-assets.js +2 -1
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-stream.js +2 -2
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
- package/dist/src/gateway/progress-watch-machine.js +42 -133
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +2 -0
- package/dist/src/gateway/provenance-envelope.js +13 -3
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +80 -16
- package/dist/src/gateway/providers.js +782 -115
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +10 -5
- package/dist/src/gateway/server.js +100 -13
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +46 -3
- package/dist/src/gateway/service.js +363 -101
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +23 -28
- package/dist/src/gateway/store.js +479 -467
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +40 -12
- package/dist/src/gateway/types.js +145 -4
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +8 -8
- package/docs/CONFIGURATION.zh-CN.md +8 -8
- package/docs/DASHBOARD.md +35 -6
- package/docs/DASHBOARD.zh-CN.md +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +173 -61
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +2 -2
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { chmod, lstat, mkdir, mkdtemp, readdir, realpath, rmdir, } from "node:fs/promises";
|
|
3
3
|
import { BridgeError } from "../errors.js";
|
|
4
4
|
import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
|
|
5
|
-
import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, sharesCompatibilityMajor, } from "./compatibility.js";
|
|
6
|
-
import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
|
|
5
|
+
import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, isCompatibilityAttestation, isCompatibilityVersion, sharesCompatibilityMajor, UNKNOWN_COMPATIBILITY_VERSION, } from "./compatibility.js";
|
|
6
|
+
import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, codexWriteProbeFailure, } from "./codex-app-server.js";
|
|
7
7
|
import { LocalCodexTransportError } from "./codex-local-transport.js";
|
|
8
8
|
import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
|
|
9
9
|
import { isDashboardLocale } from "./locale.js";
|
|
10
10
|
import { composeProvenanceEnvelope } from "./provenance-envelope.js";
|
|
11
11
|
const LOCAL_HOST = "this-mac";
|
|
12
|
+
const STABLE_CLAUDE_ENDPOINT_GENERATION = "claude_local_endpoint";
|
|
12
13
|
const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
13
14
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
14
15
|
const OPAQUE_ROUTE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
@@ -24,12 +25,39 @@ const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
|
|
|
24
25
|
const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
|
|
25
26
|
const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
|
|
26
27
|
const COMPATIBILITY_PROBE_THREAD_ID = "00000000-0000-7000-8000-000000000000";
|
|
28
|
+
const CODEX_WRITE_PROBE_DIRECTORY_PREFIX = ".codex-write-probe-";
|
|
29
|
+
const CODEX_WRITE_PROBE_ROOT = "write-probes";
|
|
30
|
+
const MAX_CODEX_WRITE_PROBE_ATTEMPTS = 16;
|
|
31
|
+
const CODEX_WRITE_PROBE_TRANSIENT_ERRNOS = new Set([
|
|
32
|
+
"EAGAIN", "EIO", "EMFILE", "ENFILE",
|
|
33
|
+
]);
|
|
27
34
|
const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
|
|
28
35
|
"CLAUDE_PEER_TARGET_UNKNOWN",
|
|
29
36
|
"CLAUDE_PEER_TARGET_STALE",
|
|
30
37
|
"CLAUDE_PEER_TARGET_CHANGED",
|
|
31
38
|
"CLAUDE_PEER_WORKSPACE_UNATTESTED",
|
|
32
39
|
]);
|
|
40
|
+
const CODEX_TRUST_FAILURE_CODES = new Set([
|
|
41
|
+
"HOME_INVALID",
|
|
42
|
+
"MANAGED_CODEX_INVALID",
|
|
43
|
+
"LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
|
|
44
|
+
"APP_SERVER_VERSION_MISMATCH",
|
|
45
|
+
"ENDPOINT_GENERATION_CHANGED",
|
|
46
|
+
"CLEANUP_FAILED",
|
|
47
|
+
"FACTORY_CLOSED",
|
|
48
|
+
"INVALID_CONFIGURATION",
|
|
49
|
+
"CODEX_ROUTE_CLEANUP_FAILED",
|
|
50
|
+
"CODEX_PROVIDER_CLEANUP_FAILED",
|
|
51
|
+
"CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
|
|
52
|
+
"CODEX_PROVIDER_CLEANUP_TIMEOUT",
|
|
53
|
+
]);
|
|
54
|
+
function isCodexTrustFailure(error) {
|
|
55
|
+
return (typeof error === "object" &&
|
|
56
|
+
error !== null &&
|
|
57
|
+
"code" in error &&
|
|
58
|
+
typeof error.code === "string" &&
|
|
59
|
+
CODEX_TRUST_FAILURE_CODES.has(error.code));
|
|
60
|
+
}
|
|
33
61
|
function claudeCleanPrewriteResult(error) {
|
|
34
62
|
if (error instanceof BridgeError &&
|
|
35
63
|
error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
|
|
@@ -64,6 +92,99 @@ function passedProbe(name) {
|
|
|
64
92
|
function failedProbe(name, safeErrorCode) {
|
|
65
93
|
return { name, outcome: "fail", safeErrorCode };
|
|
66
94
|
}
|
|
95
|
+
function systemErrorCode(error) {
|
|
96
|
+
return typeof error === "object" &&
|
|
97
|
+
error !== null &&
|
|
98
|
+
"code" in error &&
|
|
99
|
+
typeof error.code === "string"
|
|
100
|
+
? error.code
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
function transientCodexWriteProbeError(error) {
|
|
104
|
+
return CODEX_WRITE_PROBE_TRANSIENT_ERRNOS.has(systemErrorCode(error) ?? "");
|
|
105
|
+
}
|
|
106
|
+
function safeCodexWriteProbeFilesystemFailure(error) {
|
|
107
|
+
return (transientCodexWriteProbeError(error) ||
|
|
108
|
+
systemErrorCode(error) === "ENOTEMPTY" ||
|
|
109
|
+
(error instanceof BridgeError &&
|
|
110
|
+
error.code === "CODEX_WRITE_PROBE_THREAD_SETUP_FAILED"));
|
|
111
|
+
}
|
|
112
|
+
async function attestCodexWriteProbeDirectory(directory) {
|
|
113
|
+
try {
|
|
114
|
+
const resolved = path.resolve(directory);
|
|
115
|
+
const [metadata, canonical, entries] = await Promise.all([
|
|
116
|
+
lstat(resolved, { bigint: true }),
|
|
117
|
+
realpath(resolved),
|
|
118
|
+
readdir(resolved),
|
|
119
|
+
]);
|
|
120
|
+
const uid = process.getuid?.();
|
|
121
|
+
if (uid === undefined ||
|
|
122
|
+
!metadata.isDirectory() ||
|
|
123
|
+
canonical !== resolved ||
|
|
124
|
+
metadata.uid !== BigInt(uid) ||
|
|
125
|
+
(metadata.mode & 511n) !== 448n) {
|
|
126
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The disposable Codex probe directory crossed its ownership, mode, or canonical-path boundary.");
|
|
127
|
+
}
|
|
128
|
+
if (entries.length !== 0) {
|
|
129
|
+
throw new BridgeError("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED", "The disposable Codex probe directory was not empty.");
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
dev: metadata.dev,
|
|
133
|
+
ino: metadata.ino,
|
|
134
|
+
mode: metadata.mode & 511n,
|
|
135
|
+
mtimeNs: metadata.mtimeNs,
|
|
136
|
+
uid: metadata.uid,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof BridgeError)
|
|
141
|
+
throw error;
|
|
142
|
+
if (transientCodexWriteProbeError(error)) {
|
|
143
|
+
throw new BridgeError("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED", "A transient filesystem error prevented disposable probe attestation.");
|
|
144
|
+
}
|
|
145
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The disposable Codex probe directory could not be attested safely.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function attestCodexWriteProbeRoot(root) {
|
|
149
|
+
try {
|
|
150
|
+
const resolved = path.resolve(root);
|
|
151
|
+
const [metadata, canonical] = await Promise.all([
|
|
152
|
+
lstat(resolved, { bigint: true }),
|
|
153
|
+
realpath(resolved),
|
|
154
|
+
]);
|
|
155
|
+
const uid = process.getuid?.();
|
|
156
|
+
if (uid === undefined ||
|
|
157
|
+
!metadata.isDirectory() ||
|
|
158
|
+
canonical !== resolved ||
|
|
159
|
+
metadata.uid !== BigInt(uid) ||
|
|
160
|
+
(metadata.mode & 511n) !== 448n) {
|
|
161
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "A controller-owned Codex probe root changed outside its exact owned-directory boundary.");
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
dev: metadata.dev,
|
|
165
|
+
ino: metadata.ino,
|
|
166
|
+
mode: metadata.mode & 511n,
|
|
167
|
+
uid: metadata.uid,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error instanceof BridgeError)
|
|
172
|
+
throw error;
|
|
173
|
+
if (transientCodexWriteProbeError(error)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "A controller-owned Codex probe root could not be attested.");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function sameCodexWriteProbeDirectory(before, after) {
|
|
180
|
+
return sameCodexWriteProbeRoot(before, after) && before.mtimeNs === after.mtimeNs;
|
|
181
|
+
}
|
|
182
|
+
function sameCodexWriteProbeRoot(before, after) {
|
|
183
|
+
return (before.dev === after.dev &&
|
|
184
|
+
before.ino === after.ino &&
|
|
185
|
+
before.mode === after.mode &&
|
|
186
|
+
before.uid === after.uid);
|
|
187
|
+
}
|
|
67
188
|
function exactLocalHost(hostId) {
|
|
68
189
|
if ((hostId ?? LOCAL_HOST) !== LOCAL_HOST) {
|
|
69
190
|
throw new BridgeError("GATEWAY_REMOTE_PROVIDER_DISABLED", "This provider is restricted to the exact local host boundary.");
|
|
@@ -105,33 +226,184 @@ function invokeCallback(operation) {
|
|
|
105
226
|
}
|
|
106
227
|
}
|
|
107
228
|
function validateAttestedClaudeRuntime(runtime) {
|
|
108
|
-
if (runtime.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
229
|
+
if (runtime.versionEvidenceFailure !== undefined) {
|
|
230
|
+
throw new BridgeError(runtime.versionEvidenceFailure, "The local Claude provider requires bounded, internally consistent runtime version evidence.");
|
|
231
|
+
}
|
|
232
|
+
const majorEvidence = runtime.claudeCodeVersion === UNKNOWN_COMPATIBILITY_VERSION
|
|
233
|
+
? runtime.launcherVersionEvidence
|
|
234
|
+
: runtime.claudeCodeVersion;
|
|
235
|
+
if (majorEvidence === undefined ||
|
|
236
|
+
!sharesCompatibilityMajor(majorEvidence, CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion)) {
|
|
237
|
+
throw new BridgeError(majorEvidence === undefined
|
|
238
|
+
? "CLAUDE_VERSION_UNPARSEABLE"
|
|
239
|
+
: "CLAUDE_PEER_VERSION_UNSUPPORTED", "The local Claude provider requires a positively observed supported runtime major.");
|
|
240
|
+
}
|
|
241
|
+
validateClaudeRuntimePathEvidence(runtime);
|
|
119
242
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.slice(0, 32)}`;
|
|
243
|
+
function validateClaudeRuntimePathEvidence(runtime) {
|
|
244
|
+
if ([
|
|
245
|
+
runtime.claudeExecutable,
|
|
246
|
+
runtime.sessionsDir,
|
|
247
|
+
runtime.socketDir,
|
|
248
|
+
].some((value) => !path.isAbsolute(value) ||
|
|
249
|
+
path.normalize(value) !== value ||
|
|
250
|
+
value.includes("\0"))) {
|
|
251
|
+
throw new BridgeError("CLAUDE_RUNTIME_ATTESTATION_INVALID", "The local Claude provider requires bounded absolute runtime evidence.");
|
|
252
|
+
}
|
|
131
253
|
}
|
|
132
254
|
function claudeRouteState(peer) {
|
|
133
255
|
return peer.status === "idle" ? "idle" : "busy";
|
|
134
256
|
}
|
|
257
|
+
function claudeRegistryObservation(discovery) {
|
|
258
|
+
const rejected = Object.entries(discovery.rejected)
|
|
259
|
+
.filter((entry) => Number.isSafeInteger(entry[1]) && entry[1] > 0)
|
|
260
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
261
|
+
.map(([safeErrorCode, count]) => Object.freeze({ safeErrorCode, count }));
|
|
262
|
+
return Object.freeze({
|
|
263
|
+
entriesScanned: discovery.entriesScanned,
|
|
264
|
+
parseableRecords: discovery.parseableRecords,
|
|
265
|
+
rejected: Object.freeze(rejected),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function unavailableClaudeRegistryObservation() {
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
entriesScanned: 0,
|
|
271
|
+
parseableRecords: 0,
|
|
272
|
+
rejected: Object.freeze([
|
|
273
|
+
Object.freeze({
|
|
274
|
+
safeErrorCode: "CLAUDE_REGISTRY_UNAVAILABLE",
|
|
275
|
+
count: 1,
|
|
276
|
+
}),
|
|
277
|
+
]),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function isClaudeRegistryAvailabilityError(error) {
|
|
281
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
return (error.code === "ENOENT" ||
|
|
285
|
+
error.code === "EACCES" ||
|
|
286
|
+
error.code === "EPERM" ||
|
|
287
|
+
error.code === "UNSAFE_PEER_DIRECTORY");
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* A bounded observation-only surface for an installed provider generation
|
|
291
|
+
* whose protocol cannot be used safely. It deliberately performs no provider
|
|
292
|
+
* discovery, listener construction, route selection, or message write.
|
|
293
|
+
*/
|
|
294
|
+
export class IncompatibleGatewayProvider {
|
|
295
|
+
identity;
|
|
296
|
+
protocol = "not-attested";
|
|
297
|
+
protocolVersion = "unknown";
|
|
298
|
+
version;
|
|
299
|
+
probes;
|
|
300
|
+
safeErrorCode;
|
|
301
|
+
initialized = false;
|
|
302
|
+
closed = false;
|
|
303
|
+
constructor(options) {
|
|
304
|
+
const attestation = evaluateCompatibilityAttestation({
|
|
305
|
+
surface: options.identity.provider,
|
|
306
|
+
version: options.version,
|
|
307
|
+
checkedAt: "2000-01-01T00:00:00.000Z",
|
|
308
|
+
certifiedVersions: certifiedCompatibilityVersions[options.identity.provider],
|
|
309
|
+
probes: options.probes,
|
|
310
|
+
});
|
|
311
|
+
if (attestation.tier !== "incompatible" ||
|
|
312
|
+
attestation.safeErrorCode !== options.safeErrorCode) {
|
|
313
|
+
throw new BridgeError("INCOMPATIBLE_PROVIDER_EVIDENCE_INVALID", "A disabled provider requires exact bounded incompatibility evidence.");
|
|
314
|
+
}
|
|
315
|
+
exactLocalHost(options.identity.hostId);
|
|
316
|
+
this.identity = Object.freeze({ ...options.identity });
|
|
317
|
+
this.version = options.version;
|
|
318
|
+
this.probes = Object.freeze(options.probes.map((probe) => Object.freeze({ ...probe })));
|
|
319
|
+
this.safeErrorCode = options.safeErrorCode;
|
|
320
|
+
}
|
|
321
|
+
compatibilitySurface() {
|
|
322
|
+
return { surface: this.identity.provider, version: this.version };
|
|
323
|
+
}
|
|
324
|
+
async runCompatibilityProbes() {
|
|
325
|
+
return this.probes;
|
|
326
|
+
}
|
|
327
|
+
acceptCompatibilityAttestation(attestation) {
|
|
328
|
+
if (!isCompatibilityAttestation(attestation) ||
|
|
329
|
+
attestation.surface !== this.identity.provider ||
|
|
330
|
+
attestation.version !== this.version ||
|
|
331
|
+
attestation.tier !== "incompatible" ||
|
|
332
|
+
attestation.safeErrorCode !== this.safeErrorCode ||
|
|
333
|
+
attestation.probes.length !== this.probes.length ||
|
|
334
|
+
attestation.probes.some((probe, index) => probe.name !== this.probes[index]?.name ||
|
|
335
|
+
probe.outcome !== this.probes[index]?.outcome ||
|
|
336
|
+
probe.safeErrorCode !== this.probes[index]?.safeErrorCode)) {
|
|
337
|
+
throw new BridgeError("INCOMPATIBLE_PROVIDER_ATTESTATION_MISMATCH", "The compatibility attestation does not match this disabled provider surface.");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async initialize(_callbacks) {
|
|
341
|
+
if (this.closed || this.initialized) {
|
|
342
|
+
throw new BridgeError("INCOMPATIBLE_PROVIDER_UNAVAILABLE", "The disabled provider surface cannot be initialized again.");
|
|
343
|
+
}
|
|
344
|
+
this.initialized = true;
|
|
345
|
+
return {
|
|
346
|
+
health: "degraded",
|
|
347
|
+
compatibility: "incompatible",
|
|
348
|
+
safeErrorCode: this.safeErrorCode,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async discoverClaudePeers() {
|
|
352
|
+
throw this.unavailable();
|
|
353
|
+
}
|
|
354
|
+
async selectRoute(_input) {
|
|
355
|
+
throw this.unavailable();
|
|
356
|
+
}
|
|
357
|
+
async assertWorkspaceDisjoint(_routeHandle, _stateRoot) {
|
|
358
|
+
throw this.unavailable();
|
|
359
|
+
}
|
|
360
|
+
async resolveReplyAddress(_address) {
|
|
361
|
+
throw this.unavailable();
|
|
362
|
+
}
|
|
363
|
+
async advertiseNativeCodexPeer(_input) {
|
|
364
|
+
throw this.unavailable();
|
|
365
|
+
}
|
|
366
|
+
async dispatch(_input) {
|
|
367
|
+
throw this.unavailable();
|
|
368
|
+
}
|
|
369
|
+
async releaseRoute(_routeHandle) { }
|
|
370
|
+
async close() {
|
|
371
|
+
this.closed = true;
|
|
372
|
+
}
|
|
373
|
+
unavailable() {
|
|
374
|
+
return new BridgeError(this.safeErrorCode, "This provider surface is observation-only because its compatibility boundary did not pass.");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
export function createIncompatibleClaudeGatewayProvider(options) {
|
|
378
|
+
validateClaudeRuntimePathEvidence(options.runtime);
|
|
379
|
+
const [launcher, version, ...unobserved] = compatibilityProbeNames.claude;
|
|
380
|
+
return new IncompatibleGatewayProvider({
|
|
381
|
+
identity: {
|
|
382
|
+
provider: "claude",
|
|
383
|
+
hostId: LOCAL_HOST,
|
|
384
|
+
endpointGeneration: STABLE_CLAUDE_ENDPOINT_GENERATION,
|
|
385
|
+
},
|
|
386
|
+
version: options.version,
|
|
387
|
+
probes: [
|
|
388
|
+
passedProbe(launcher),
|
|
389
|
+
failedProbe(version, options.safeErrorCode),
|
|
390
|
+
...unobserved.map((name) => failedProbe(name, options.safeErrorCode)),
|
|
391
|
+
],
|
|
392
|
+
safeErrorCode: options.safeErrorCode,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
export function createIncompatibleCodexGatewayProvider(options) {
|
|
396
|
+
const [installation, ...unobserved] = compatibilityProbeNames.codex;
|
|
397
|
+
return new IncompatibleGatewayProvider({
|
|
398
|
+
identity: options.identity,
|
|
399
|
+
version: options.version,
|
|
400
|
+
probes: [
|
|
401
|
+
passedProbe(installation),
|
|
402
|
+
...unobserved.map((name) => failedProbe(name, options.safeErrorCode)),
|
|
403
|
+
],
|
|
404
|
+
safeErrorCode: options.safeErrorCode,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
135
407
|
export class LocalClaudeGatewayProvider {
|
|
136
408
|
identity;
|
|
137
409
|
protocol = "claude-peer";
|
|
@@ -165,6 +437,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
165
437
|
/** Provider-owned terminal rejections that never enter the service queue. */
|
|
166
438
|
rejectedNativeInbound = new Map();
|
|
167
439
|
discoveryInFlight;
|
|
440
|
+
registryObservation;
|
|
168
441
|
monitorTimer;
|
|
169
442
|
/** Invocation-ordered registry mutations; the tail itself never rejects. */
|
|
170
443
|
nativeCodexRegistrationTail = Promise.resolve();
|
|
@@ -175,10 +448,25 @@ export class LocalClaudeGatewayProvider {
|
|
|
175
448
|
retiredNativeCodexListener;
|
|
176
449
|
/** Fences discovery callbacks and monitor restarts across listener rotation. */
|
|
177
450
|
nativeCodexSuccessionFreeze;
|
|
451
|
+
/** A completed failed probe set fences every authority-bearing operation. */
|
|
452
|
+
compatibilityBlocked = false;
|
|
453
|
+
compatibilityFailureCode;
|
|
178
454
|
initialized = false;
|
|
179
455
|
closed = false;
|
|
180
456
|
constructor(options) {
|
|
181
457
|
validateAttestedClaudeRuntime(options.runtime);
|
|
458
|
+
const compatibilityVersion = options.compatibilityVersion ??
|
|
459
|
+
(options.runtime.claudeCodeVersion === UNKNOWN_COMPATIBILITY_VERSION
|
|
460
|
+
? options.runtime.launcherVersionEvidence
|
|
461
|
+
: options.runtime.claudeCodeVersion);
|
|
462
|
+
if (compatibilityVersion === undefined ||
|
|
463
|
+
!isCompatibilityVersion(compatibilityVersion) ||
|
|
464
|
+
(compatibilityVersion !== options.runtime.claudeCodeVersion &&
|
|
465
|
+
!(options.runtime.claudeCodeVersion ===
|
|
466
|
+
UNKNOWN_COMPATIBILITY_VERSION &&
|
|
467
|
+
options.runtime.launcherVersionEvidence === compatibilityVersion))) {
|
|
468
|
+
throw new BridgeError("CLAUDE_RUNTIME_ATTESTATION_INVALID", "Claude compatibility classification requires exact bounded launcher evidence.");
|
|
469
|
+
}
|
|
182
470
|
if (options.locale !== undefined && !isDashboardLocale(options.locale)) {
|
|
183
471
|
throw new BridgeError("DASHBOARD_LOCALE_UNSUPPORTED", "The Claude provider notice locale is unsupported.");
|
|
184
472
|
}
|
|
@@ -186,12 +474,20 @@ export class LocalClaudeGatewayProvider {
|
|
|
186
474
|
this.identity = {
|
|
187
475
|
provider: "claude",
|
|
188
476
|
hostId,
|
|
189
|
-
endpointGeneration:
|
|
477
|
+
endpointGeneration: STABLE_CLAUDE_ENDPOINT_GENERATION,
|
|
190
478
|
};
|
|
191
|
-
this.runtimeVersion =
|
|
479
|
+
this.runtimeVersion = compatibilityVersion;
|
|
192
480
|
this.maxPending = positiveBounded(options.maxPendingMessages, MAX_CLAUDE_PENDING, 4_096);
|
|
193
481
|
this.discoveryPollMs = positiveBounded(options.discoveryPollMs, DEFAULT_CLAUDE_DISCOVERY_POLL_MS, 30_000);
|
|
194
482
|
this.now = options.now ?? Date.now;
|
|
483
|
+
const protocolRuntime = {
|
|
484
|
+
claudeExecutable: options.runtime.claudeExecutable,
|
|
485
|
+
claudeCodeVersion: options.runtime.claudeCodeVersion === UNKNOWN_COMPATIBILITY_VERSION
|
|
486
|
+
? options.runtime.launcherVersionEvidence
|
|
487
|
+
: options.runtime.claudeCodeVersion,
|
|
488
|
+
sessionsDir: options.runtime.sessionsDir,
|
|
489
|
+
socketDir: options.runtime.socketDir,
|
|
490
|
+
};
|
|
195
491
|
this.peer = (options.peerFactory ??
|
|
196
492
|
((runtime, locale, deliveryNotices) => new ClaudePeerAdapter({
|
|
197
493
|
sessionsDir: runtime.sessionsDir,
|
|
@@ -199,18 +495,18 @@ export class LocalClaudeGatewayProvider {
|
|
|
199
495
|
attestedClaudeCodeVersion: runtime.claudeCodeVersion,
|
|
200
496
|
locale,
|
|
201
497
|
deliveryNotices,
|
|
202
|
-
})))(
|
|
498
|
+
})))(protocolRuntime, options.locale ?? "en", options.deliveryNotices ?? "merged");
|
|
203
499
|
this.nativeHelpers =
|
|
204
500
|
options.nativeHelpers === undefined
|
|
205
501
|
? undefined
|
|
206
502
|
: new ClaudeNativeHelperSupervisor({
|
|
207
503
|
identity: this.identity,
|
|
208
|
-
runtime:
|
|
504
|
+
runtime: protocolRuntime,
|
|
209
505
|
locale: options.locale ?? "en",
|
|
210
506
|
deliveryNotices: options.deliveryNotices ?? "merged",
|
|
211
507
|
maxPendingMessages: this.maxPending,
|
|
212
508
|
maxHelpers: positiveBounded(options.nativeHelpers.maxHelpers, 256, 128),
|
|
213
|
-
callbacks: () => this.callbacks,
|
|
509
|
+
callbacks: () => this.compatibilityBlocked ? undefined : this.callbacks,
|
|
214
510
|
...(options.nativeHelpers.factory === undefined
|
|
215
511
|
? {}
|
|
216
512
|
: { factory: options.nativeHelpers.factory }),
|
|
@@ -223,33 +519,49 @@ export class LocalClaudeGatewayProvider {
|
|
|
223
519
|
const [launcher, version, registry, socket, protocol] = compatibilityProbeNames.claude;
|
|
224
520
|
try {
|
|
225
521
|
const discovery = await this.peer.discover();
|
|
226
|
-
|
|
227
|
-
// dead, unsafe, or non-routable records are omitted rather than allowed
|
|
228
|
-
// to poison the whole registry. Startup compatibility must use the same
|
|
229
|
-
// boundary. Fail only when the bounded scan itself is incomplete, or a
|
|
230
|
-
// nonempty registry contains no record with a parseable closed schema.
|
|
231
|
-
const registryUnusable = discovery.truncated ||
|
|
232
|
-
(discovery.entriesScanned > 0 && discovery.parseableRecords === 0);
|
|
522
|
+
this.registryObservation = claudeRegistryObservation(discovery);
|
|
233
523
|
return [
|
|
234
524
|
passedProbe(launcher),
|
|
235
525
|
passedProbe(version),
|
|
236
|
-
|
|
237
|
-
? failedProbe(registry, "CLAUDE_REGISTRY_SCHEMA_REJECTED")
|
|
238
|
-
: passedProbe(registry),
|
|
526
|
+
passedProbe(registry),
|
|
239
527
|
passedProbe(socket),
|
|
240
528
|
passedProbe(protocol),
|
|
241
529
|
];
|
|
242
530
|
}
|
|
243
|
-
catch {
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (!isClaudeRegistryAvailabilityError(error))
|
|
533
|
+
throw error;
|
|
534
|
+
this.registryObservation = unavailableClaudeRegistryObservation();
|
|
535
|
+
// A missing or unreadable registry is provider-local incompatibility:
|
|
536
|
+
// keep the broker and the other surface alive, but fence this provider
|
|
537
|
+
// before listener construction can turn the same missing path into a
|
|
538
|
+
// broker-wide startup failure.
|
|
244
539
|
return [
|
|
245
540
|
passedProbe(launcher),
|
|
246
541
|
passedProbe(version),
|
|
247
542
|
failedProbe(registry, "CLAUDE_REGISTRY_UNAVAILABLE"),
|
|
248
|
-
failedProbe(socket, "
|
|
543
|
+
failedProbe(socket, "CLAUDE_REGISTRY_UNAVAILABLE"),
|
|
249
544
|
passedProbe(protocol),
|
|
250
545
|
];
|
|
251
546
|
}
|
|
252
547
|
}
|
|
548
|
+
latestRegistryObservation() {
|
|
549
|
+
return this.registryObservation;
|
|
550
|
+
}
|
|
551
|
+
acceptCompatibilityAttestation(attestation) {
|
|
552
|
+
if (!isCompatibilityAttestation(attestation) ||
|
|
553
|
+
attestation.surface !== "claude" ||
|
|
554
|
+
attestation.version !== this.runtimeVersion) {
|
|
555
|
+
throw new BridgeError("CLAUDE_COMPATIBILITY_ATTESTATION_MISMATCH", "The Claude compatibility attestation does not match this runtime.");
|
|
556
|
+
}
|
|
557
|
+
this.compatibilityBlocked = attestation.tier === "incompatible";
|
|
558
|
+
this.compatibilityFailureCode = attestation.safeErrorCode;
|
|
559
|
+
if (this.compatibilityBlocked) {
|
|
560
|
+
if (this.monitorTimer !== undefined)
|
|
561
|
+
clearTimeout(this.monitorTimer);
|
|
562
|
+
this.monitorTimer = undefined;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
253
565
|
async initialize(callbacks) {
|
|
254
566
|
if (this.closed) {
|
|
255
567
|
throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider is closed.");
|
|
@@ -258,6 +570,14 @@ export class LocalClaudeGatewayProvider {
|
|
|
258
570
|
throw new BridgeError("CLAUDE_PROVIDER_ALREADY_INITIALIZED", "The local Claude provider can be initialized only once.");
|
|
259
571
|
}
|
|
260
572
|
this.callbacks = callbacks;
|
|
573
|
+
if (this.compatibilityBlocked) {
|
|
574
|
+
this.initialized = true;
|
|
575
|
+
return {
|
|
576
|
+
health: "degraded",
|
|
577
|
+
compatibility: "incompatible",
|
|
578
|
+
safeErrorCode: this.compatibilityFailureCode ?? "CLAUDE_COMPATIBILITY_CHECK_FAILED",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
261
581
|
if (this.nativeHelpers !== undefined) {
|
|
262
582
|
this.initialized = true;
|
|
263
583
|
return { health: "healthy", compatibility: "compatible" };
|
|
@@ -297,8 +617,24 @@ export class LocalClaudeGatewayProvider {
|
|
|
297
617
|
this.initialized = true;
|
|
298
618
|
return { health: "healthy", compatibility: "compatible" };
|
|
299
619
|
}
|
|
300
|
-
catch {
|
|
620
|
+
catch (error) {
|
|
621
|
+
if (error instanceof BridgeError &&
|
|
622
|
+
error.code === "UNSAFE_PEER_DIRECTORY") {
|
|
623
|
+
this.compatibilityBlocked = true;
|
|
624
|
+
this.compatibilityFailureCode = "CLAUDE_REGISTRY_UNAVAILABLE";
|
|
625
|
+
this.registryObservation = unavailableClaudeRegistryObservation();
|
|
626
|
+
this.initialized = true;
|
|
627
|
+
return {
|
|
628
|
+
health: "degraded",
|
|
629
|
+
compatibility: "incompatible",
|
|
630
|
+
safeErrorCode: "CLAUDE_REGISTRY_UNAVAILABLE",
|
|
631
|
+
};
|
|
632
|
+
}
|
|
301
633
|
this.callbacks = undefined;
|
|
634
|
+
if (error instanceof BridgeError &&
|
|
635
|
+
error.code === "CLAUDE_PEER_CALLBACK_UNSAFE") {
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
302
638
|
throw new BridgeError("CLAUDE_CALLBACK_UNAVAILABLE", "The private Claude callback listener could not be established.");
|
|
303
639
|
}
|
|
304
640
|
}
|
|
@@ -769,6 +1105,12 @@ export class LocalClaudeGatewayProvider {
|
|
|
769
1105
|
active.inboundQuiesced = true;
|
|
770
1106
|
}
|
|
771
1107
|
async dispatch(input) {
|
|
1108
|
+
if (this.compatibilityBlocked) {
|
|
1109
|
+
return {
|
|
1110
|
+
state: "failed",
|
|
1111
|
+
safeErrorCode: this.compatibilityFailureCode ?? "CLAUDE_COMPATIBILITY_CHECK_FAILED",
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
772
1114
|
if (this.nativeHelpers !== undefined) {
|
|
773
1115
|
if (input.sourceAlias === undefined ||
|
|
774
1116
|
!this.initialized ||
|
|
@@ -1050,7 +1392,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
1050
1392
|
}
|
|
1051
1393
|
}
|
|
1052
1394
|
assertReady() {
|
|
1053
|
-
if (!this.initialized || this.closed) {
|
|
1395
|
+
if (!this.initialized || this.closed || this.compatibilityBlocked) {
|
|
1054
1396
|
throw new BridgeError("CLAUDE_PROVIDER_UNAVAILABLE", "The local Claude provider is not available.", true);
|
|
1055
1397
|
}
|
|
1056
1398
|
}
|
|
@@ -1109,7 +1451,8 @@ export class LocalClaudeGatewayProvider {
|
|
|
1109
1451
|
await this.rejectNativeInboundReceipt(listenerGeneration, message.receiptHandle, diagnosticCode);
|
|
1110
1452
|
}
|
|
1111
1453
|
};
|
|
1112
|
-
if (this.
|
|
1454
|
+
if (this.compatibilityBlocked ||
|
|
1455
|
+
this.closed ||
|
|
1113
1456
|
listenerGeneration.listener.closed ||
|
|
1114
1457
|
!this.ownsNativeCodexListener(listenerGeneration)) {
|
|
1115
1458
|
try {
|
|
@@ -1433,13 +1776,21 @@ export class LocalClaudeGatewayProvider {
|
|
|
1433
1776
|
dispatchEpochAtStart.set(routeHandle, this.selectedDispatchEpoch.get(routeHandle) ?? 0);
|
|
1434
1777
|
}
|
|
1435
1778
|
const pendingAtStart = new Set(this.pendingTargetByGatewayId.values());
|
|
1436
|
-
|
|
1779
|
+
let discovery;
|
|
1780
|
+
try {
|
|
1781
|
+
discovery = await this.peer.discover();
|
|
1782
|
+
}
|
|
1783
|
+
catch (error) {
|
|
1784
|
+
this.registryObservation = unavailableClaudeRegistryObservation();
|
|
1785
|
+
throw error;
|
|
1786
|
+
}
|
|
1787
|
+
const registry = claudeRegistryObservation(discovery);
|
|
1788
|
+
this.registryObservation = registry;
|
|
1437
1789
|
this.discovered.clear();
|
|
1438
1790
|
const rows = [];
|
|
1439
1791
|
for (const peer of discovery.peers) {
|
|
1440
1792
|
if (peer.kind !== "interactive" ||
|
|
1441
|
-
!NATIVE_CLAUDE_NAME.test(peer.alias)
|
|
1442
|
-
peer.alias.startsWith("codex-")) {
|
|
1793
|
+
!NATIVE_CLAUDE_NAME.test(peer.alias)) {
|
|
1443
1794
|
continue;
|
|
1444
1795
|
}
|
|
1445
1796
|
const alias = `${peer.alias}@${this.identity.hostId}`;
|
|
@@ -1480,7 +1831,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
1480
1831
|
this.emitClaudeRouteObservation(routeHandle, row.state, undefined, true);
|
|
1481
1832
|
}
|
|
1482
1833
|
}
|
|
1483
|
-
return { peers: rows, complete: !discovery.truncated };
|
|
1834
|
+
return { peers: rows, complete: !discovery.truncated, registry };
|
|
1484
1835
|
}
|
|
1485
1836
|
emitClaudeRouteObservation(routeHandle, state, safeErrorCode, authoritative = false) {
|
|
1486
1837
|
if (this.nativeCodexSuccessionFreeze !== undefined)
|
|
@@ -1540,33 +1891,52 @@ export class LocalClaudeGatewayProvider {
|
|
|
1540
1891
|
export function createLocalClaudeGatewayProvider(options) {
|
|
1541
1892
|
return new LocalClaudeGatewayProvider(options);
|
|
1542
1893
|
}
|
|
1543
|
-
function
|
|
1894
|
+
function codexVersionFailureCode(version) {
|
|
1895
|
+
if (version === UNKNOWN_COMPATIBILITY_VERSION) {
|
|
1896
|
+
return "CODEX_APP_SERVER_VERSION_UNPARSEABLE";
|
|
1897
|
+
}
|
|
1898
|
+
if (!isCompatibilityVersion(version)) {
|
|
1899
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The local Codex factory version evidence is malformed.");
|
|
1900
|
+
}
|
|
1901
|
+
return CODEX_APP_SERVER_WRITABLE_VERSIONS.some((certifiedVersion) => sharesCompatibilityMajor(certifiedVersion, version))
|
|
1902
|
+
? undefined
|
|
1903
|
+
: "CODEX_APP_SERVER_VERSION_UNSUPPORTED";
|
|
1904
|
+
}
|
|
1905
|
+
function validateCodexFactory(factory, options = {}) {
|
|
1544
1906
|
const schema = factory.schemaCompatibility;
|
|
1545
1907
|
const write = factory.writeCompatibility;
|
|
1908
|
+
const versionFailure = codexVersionFailureCode(factory.appServerVersion);
|
|
1546
1909
|
const certified = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(factory.appServerVersion);
|
|
1547
1910
|
const schemaCandidate = schema.observedSchemaCandidate === true &&
|
|
1548
1911
|
CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, factory.appServerVersion));
|
|
1912
|
+
const unsupportedVersionInvalid = versionFailure !== undefined &&
|
|
1913
|
+
(options.allowUnsupportedVersion !== true ||
|
|
1914
|
+
write !== null ||
|
|
1915
|
+
factory.writableReady);
|
|
1916
|
+
const supportedSchemaInvalid = versionFailure === undefined &&
|
|
1917
|
+
((!certified &&
|
|
1918
|
+
(!schemaCandidate || write !== null || factory.writableReady)) ||
|
|
1919
|
+
schema.protocol !== "app-server-v2-stable" ||
|
|
1920
|
+
schema.steering?.method !== "turn/steer" ||
|
|
1921
|
+
schema.steering.requestSchema !== "expected-turn-id-text-v1" ||
|
|
1922
|
+
schema.steering.deliveryBoundary !== "next-tool-call-boundary" ||
|
|
1923
|
+
(write !== null &&
|
|
1924
|
+
(write.appServerVersion !== schema.appServerVersion ||
|
|
1925
|
+
write.endpointGeneration !== schema.endpointGeneration ||
|
|
1926
|
+
write.protocol !== schema.protocol ||
|
|
1927
|
+
write.observedSchemaCandidate !== schema.observedSchemaCandidate ||
|
|
1928
|
+
write.steering?.method !== schema.steering.method ||
|
|
1929
|
+
write.steering.requestSchema !== schema.steering.requestSchema ||
|
|
1930
|
+
write.steering.deliveryBoundary !==
|
|
1931
|
+
schema.steering.deliveryBoundary)) ||
|
|
1932
|
+
factory.writableReady !== (write !== null));
|
|
1549
1933
|
if (factory.hostId !== LOCAL_HOST ||
|
|
1550
1934
|
factory.protocol !== "codex-app-server" ||
|
|
1551
1935
|
factory.protocolVersion !== factory.appServerVersion ||
|
|
1552
|
-
(!certified &&
|
|
1553
|
-
(!schemaCandidate || write !== null || factory.writableReady)) ||
|
|
1554
1936
|
schema.appServerVersion !== factory.appServerVersion ||
|
|
1555
1937
|
schema.endpointGeneration !== factory.endpointGeneration ||
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
schema.steering.requestSchema !== "expected-turn-id-text-v1" ||
|
|
1559
|
-
schema.steering.deliveryBoundary !== "next-tool-call-boundary" ||
|
|
1560
|
-
(write !== null &&
|
|
1561
|
-
(write.appServerVersion !== schema.appServerVersion ||
|
|
1562
|
-
write.endpointGeneration !== schema.endpointGeneration ||
|
|
1563
|
-
write.protocol !== schema.protocol ||
|
|
1564
|
-
write.observedSchemaCandidate !== schema.observedSchemaCandidate ||
|
|
1565
|
-
write.steering?.method !== schema.steering.method ||
|
|
1566
|
-
write.steering.requestSchema !== schema.steering.requestSchema ||
|
|
1567
|
-
write.steering.deliveryBoundary !==
|
|
1568
|
-
schema.steering.deliveryBoundary)) ||
|
|
1569
|
-
factory.writableReady !== (write !== null)) {
|
|
1938
|
+
unsupportedVersionInvalid ||
|
|
1939
|
+
supportedSchemaInvalid) {
|
|
1570
1940
|
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The local Codex provider requires exact schema and write attestations.");
|
|
1571
1941
|
}
|
|
1572
1942
|
}
|
|
@@ -1627,10 +1997,15 @@ export class LocalCodexGatewayProvider {
|
|
|
1627
1997
|
endpointActivationRetry;
|
|
1628
1998
|
endpointActivationRetryTimer;
|
|
1629
1999
|
callbackDrainFrozen = false;
|
|
2000
|
+
writeProbeObservationsByGeneration = new Map();
|
|
2001
|
+
writeProbeResults = new Map();
|
|
2002
|
+
writeProbeOverflow;
|
|
1630
2003
|
compatibilityAttested;
|
|
2004
|
+
compatibilityFailureCode;
|
|
1631
2005
|
endpointUnavailable = false;
|
|
1632
2006
|
pendingEndpointAttestation;
|
|
1633
2007
|
pendingEndpointRefreshEvent;
|
|
2008
|
+
pendingEndpointRefreshSelectorClaimed = false;
|
|
1634
2009
|
initialized = false;
|
|
1635
2010
|
closing = false;
|
|
1636
2011
|
closed = false;
|
|
@@ -1673,31 +2048,82 @@ export class LocalCodexGatewayProvider {
|
|
|
1673
2048
|
}
|
|
1674
2049
|
acceptCompatibilityAttestation(attestation) {
|
|
1675
2050
|
const pending = this.pendingEndpointAttestation;
|
|
1676
|
-
|
|
2051
|
+
const pendingMatches = pending === undefined ||
|
|
2052
|
+
(attestation.checkedAt === pending.checkedAt &&
|
|
2053
|
+
attestation.tier === pending.tier &&
|
|
2054
|
+
attestation.safeErrorCode === pending.safeErrorCode &&
|
|
2055
|
+
attestation.probes.length === pending.probes.length &&
|
|
2056
|
+
attestation.probes.every((probe, index) => probe.name === pending.probes[index]?.name &&
|
|
2057
|
+
probe.outcome === pending.probes[index]?.outcome &&
|
|
2058
|
+
probe.safeErrorCode === pending.probes[index]?.safeErrorCode));
|
|
2059
|
+
if (!isCompatibilityAttestation(attestation) ||
|
|
2060
|
+
attestation.surface !== "codex" ||
|
|
1677
2061
|
attestation.version !== this.factory.appServerVersion ||
|
|
1678
|
-
|
|
1679
|
-
|
|
2062
|
+
!pendingMatches) {
|
|
2063
|
+
throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
|
|
2064
|
+
}
|
|
2065
|
+
if (attestation.tier === "incompatible") {
|
|
2066
|
+
this.pendingEndpointAttestation = undefined;
|
|
2067
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2068
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2069
|
+
this.clearEndpointActivationRetry();
|
|
2070
|
+
this.compatibilityAttested = false;
|
|
2071
|
+
this.endpointUnavailable = true;
|
|
2072
|
+
this.compatibilityFailureCode =
|
|
2073
|
+
attestation.safeErrorCode ?? "CODEX_COMPATIBILITY_CHECK_FAILED";
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const writableCertified = attestation.tier === "certified" &&
|
|
2077
|
+
CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(attestation.version) &&
|
|
2078
|
+
this.factory.writableReady &&
|
|
2079
|
+
this.factory.writeCompatibility !== null;
|
|
2080
|
+
const schemaObserved = attestation.tier === "schema_attested" &&
|
|
2081
|
+
!this.factory.writableReady &&
|
|
2082
|
+
this.factory.writeCompatibility === null;
|
|
2083
|
+
if ((!writableCertified && !schemaObserved) ||
|
|
1680
2084
|
(this.endpointUnavailable && pending === undefined) ||
|
|
1681
|
-
|
|
1682
|
-
(attestation.checkedAt !== pending.checkedAt ||
|
|
1683
|
-
attestation.tier !== pending.tier ||
|
|
1684
|
-
attestation.safeErrorCode !== pending.safeErrorCode ||
|
|
1685
|
-
attestation.probes.length !== pending.probes.length ||
|
|
1686
|
-
attestation.probes.some((probe, index) => probe.name !== pending.probes[index]?.name ||
|
|
1687
|
-
probe.outcome !== pending.probes[index]?.outcome ||
|
|
1688
|
-
probe.safeErrorCode !== pending.probes[index]?.safeErrorCode)))) {
|
|
2085
|
+
!pendingMatches) {
|
|
1689
2086
|
throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
|
|
1690
2087
|
}
|
|
1691
2088
|
this.pendingEndpointAttestation = undefined;
|
|
1692
2089
|
this.pendingEndpointRefreshEvent = undefined;
|
|
2090
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
1693
2091
|
this.clearEndpointActivationRetry();
|
|
1694
|
-
this.compatibilityAttested =
|
|
1695
|
-
this.endpointUnavailable =
|
|
2092
|
+
this.compatibilityAttested = writableCertified;
|
|
2093
|
+
this.endpointUnavailable = !writableCertified;
|
|
2094
|
+
this.compatibilityFailureCode = undefined;
|
|
1696
2095
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
2096
|
+
releaseEndpointRefreshSelectorClaim(endpointGeneration) {
|
|
2097
|
+
const event = this.pendingEndpointRefreshEvent;
|
|
2098
|
+
if (event?.current.endpointGeneration === endpointGeneration &&
|
|
2099
|
+
this.pendingEndpointAttestation !== undefined) {
|
|
2100
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
rearmEndpointRefreshActivation(endpointGeneration) {
|
|
2104
|
+
const event = this.pendingEndpointRefreshEvent;
|
|
2105
|
+
if (event?.current.endpointGeneration === endpointGeneration &&
|
|
2106
|
+
this.pendingEndpointAttestation !== undefined &&
|
|
2107
|
+
!this.closing &&
|
|
2108
|
+
!this.closed) {
|
|
2109
|
+
this.publishEndpointRefresh(event);
|
|
2110
|
+
}
|
|
1699
2111
|
}
|
|
1700
|
-
|
|
2112
|
+
latestWriteCompatibilityProbeObservation(endpointGeneration) {
|
|
2113
|
+
return (this.writeProbeObservationsByGeneration.get(endpointGeneration) ??
|
|
2114
|
+
(this.writeProbeOverflow?.endpointGeneration === endpointGeneration
|
|
2115
|
+
? this.writeProbeOverflow.observation
|
|
2116
|
+
: undefined));
|
|
2117
|
+
}
|
|
2118
|
+
async runCompatibilityProbes(context) {
|
|
2119
|
+
return await this.runCompatibilityProbesFor(this.factory, context === undefined
|
|
2120
|
+
? undefined
|
|
2121
|
+
: {
|
|
2122
|
+
stateRoot: path.resolve(context.stateRoot),
|
|
2123
|
+
forbiddenCodexThreadIds: [...context.forbiddenCodexThreadIds],
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
async runCompatibilityProbesFor(factory, context) {
|
|
1701
2127
|
const [installation, control, initialize, threadList] = compatibilityProbeNames.codex;
|
|
1702
2128
|
let transport;
|
|
1703
2129
|
let connector;
|
|
@@ -1707,6 +2133,9 @@ export class LocalCodexGatewayProvider {
|
|
|
1707
2133
|
stage = "initialize";
|
|
1708
2134
|
connector = await CodexAppServerConnector.connect({
|
|
1709
2135
|
compatibility: factory.schemaCompatibility,
|
|
2136
|
+
...(context === undefined
|
|
2137
|
+
? {}
|
|
2138
|
+
: { writeCompatibilityProbe: true }),
|
|
1710
2139
|
writesEnabled: false,
|
|
1711
2140
|
route: {
|
|
1712
2141
|
endpointGeneration: factory.endpointGeneration,
|
|
@@ -1720,30 +2149,46 @@ export class LocalCodexGatewayProvider {
|
|
|
1720
2149
|
});
|
|
1721
2150
|
stage = "thread_list";
|
|
1722
2151
|
await connector.observeLoadedThread(connector.guard());
|
|
1723
|
-
|
|
2152
|
+
const requiredProbes = [
|
|
1724
2153
|
passedProbe(installation),
|
|
1725
2154
|
passedProbe(control),
|
|
1726
2155
|
passedProbe(initialize),
|
|
1727
2156
|
passedProbe(threadList),
|
|
1728
2157
|
];
|
|
2158
|
+
if (context === undefined)
|
|
2159
|
+
return requiredProbes;
|
|
2160
|
+
const result = await this.runCodexWriteProbe(factory, connector, context);
|
|
2161
|
+
return result.outcome === "pass"
|
|
2162
|
+
? [...requiredProbes, passedProbe("write_attestation")]
|
|
2163
|
+
: requiredProbes;
|
|
1729
2164
|
}
|
|
1730
2165
|
catch (error) {
|
|
1731
|
-
if (this.isEndpointGenerationChanged(error)
|
|
2166
|
+
if (this.isEndpointGenerationChanged(error) ||
|
|
2167
|
+
(error instanceof BridgeError &&
|
|
2168
|
+
error.code === "CODEX_FACTORY_ATTESTATION_INVALID")) {
|
|
1732
2169
|
throw error;
|
|
2170
|
+
}
|
|
1733
2171
|
if (stage === "transport") {
|
|
1734
|
-
const
|
|
2172
|
+
const availabilityFailure = error instanceof LocalCodexTransportError &&
|
|
1735
2173
|
[
|
|
2174
|
+
"MANAGED_CODEX_UNAVAILABLE",
|
|
1736
2175
|
"LOCAL_APP_SERVER_NOT_RUNNING",
|
|
1737
|
-
"
|
|
2176
|
+
"SPAWN_FAILED",
|
|
2177
|
+
"SPAWN_TIMEOUT",
|
|
2178
|
+
"PROXY_STDERR_LIMIT",
|
|
1738
2179
|
"TRANSPORT_CONNECT_FAILED",
|
|
1739
2180
|
].includes(error.code);
|
|
2181
|
+
if (!availabilityFailure)
|
|
2182
|
+
throw error;
|
|
2183
|
+
const installationUnavailable = error instanceof LocalCodexTransportError &&
|
|
2184
|
+
error.code === "MANAGED_CODEX_UNAVAILABLE";
|
|
1740
2185
|
return [
|
|
1741
|
-
|
|
1742
|
-
?
|
|
1743
|
-
:
|
|
1744
|
-
failedProbe(control,
|
|
1745
|
-
? "
|
|
1746
|
-
: "
|
|
2186
|
+
installationUnavailable
|
|
2187
|
+
? failedProbe(installation, "CODEX_INSTALLATION_INVALID")
|
|
2188
|
+
: passedProbe(installation),
|
|
2189
|
+
failedProbe(control, installationUnavailable
|
|
2190
|
+
? "CODEX_COMPAT_PROBE_BLOCKED"
|
|
2191
|
+
: "CODEX_CONTROL_SOCKET_UNAVAILABLE"),
|
|
1747
2192
|
failedProbe(initialize, "CODEX_COMPAT_PROBE_BLOCKED"),
|
|
1748
2193
|
failedProbe(threadList, "CODEX_COMPAT_PROBE_BLOCKED"),
|
|
1749
2194
|
];
|
|
@@ -1759,19 +2204,168 @@ export class LocalCodexGatewayProvider {
|
|
|
1759
2204
|
}
|
|
1760
2205
|
finally {
|
|
1761
2206
|
if (connector !== undefined) {
|
|
1762
|
-
await connector.close()
|
|
2207
|
+
await connector.close();
|
|
1763
2208
|
}
|
|
1764
2209
|
else if (transport !== undefined) {
|
|
1765
|
-
await transport.close()
|
|
2210
|
+
await transport.close();
|
|
1766
2211
|
}
|
|
1767
2212
|
}
|
|
1768
2213
|
}
|
|
2214
|
+
async runCodexWriteProbe(factory, connector, context) {
|
|
2215
|
+
const key = `${factory.appServerVersion}\0${factory.endpointGeneration}`;
|
|
2216
|
+
const cached = this.writeProbeResults.get(key);
|
|
2217
|
+
if (cached !== undefined)
|
|
2218
|
+
return await cached;
|
|
2219
|
+
if (this.writeProbeOverflow?.key === key) {
|
|
2220
|
+
return this.writeProbeOverflow.observation;
|
|
2221
|
+
}
|
|
2222
|
+
if (this.writeProbeResults.size >= MAX_CODEX_WRITE_PROBE_ATTEMPTS) {
|
|
2223
|
+
const observation = Object.freeze(codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED"));
|
|
2224
|
+
this.writeProbeOverflow = Object.freeze({
|
|
2225
|
+
endpointGeneration: factory.endpointGeneration,
|
|
2226
|
+
key,
|
|
2227
|
+
observation,
|
|
2228
|
+
});
|
|
2229
|
+
if (this.callbacks !== undefined) {
|
|
2230
|
+
invokeCallback(() => this.callbacks?.onProtocolNotice?.({
|
|
2231
|
+
code: observation.safeErrorCode,
|
|
2232
|
+
}));
|
|
2233
|
+
}
|
|
2234
|
+
return observation;
|
|
2235
|
+
}
|
|
2236
|
+
const pending = this.executeCodexWriteProbe(connector, context).then((result) => {
|
|
2237
|
+
const observation = Object.freeze({ ...result });
|
|
2238
|
+
this.writeProbeObservationsByGeneration.set(factory.endpointGeneration, observation);
|
|
2239
|
+
if (observation.outcome === "fail" && this.callbacks !== undefined) {
|
|
2240
|
+
invokeCallback(() => this.callbacks?.onProtocolNotice?.({
|
|
2241
|
+
code: observation.safeErrorCode,
|
|
2242
|
+
}));
|
|
2243
|
+
}
|
|
2244
|
+
return observation;
|
|
2245
|
+
});
|
|
2246
|
+
this.writeProbeResults.set(key, pending);
|
|
2247
|
+
return await pending;
|
|
2248
|
+
}
|
|
2249
|
+
async executeCodexWriteProbe(connector, context) {
|
|
2250
|
+
const stateRoot = path.resolve(context.stateRoot);
|
|
2251
|
+
const rootBefore = await attestCodexWriteProbeRoot(stateRoot);
|
|
2252
|
+
if (rootBefore === null) {
|
|
2253
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
2254
|
+
}
|
|
2255
|
+
const probeRoot = path.join(stateRoot, CODEX_WRITE_PROBE_ROOT);
|
|
2256
|
+
let probeRootBefore;
|
|
2257
|
+
try {
|
|
2258
|
+
let created = false;
|
|
2259
|
+
try {
|
|
2260
|
+
await mkdir(probeRoot, { mode: 0o700 });
|
|
2261
|
+
created = true;
|
|
2262
|
+
}
|
|
2263
|
+
catch (error) {
|
|
2264
|
+
if (systemErrorCode(error) !== "EEXIST")
|
|
2265
|
+
throw error;
|
|
2266
|
+
}
|
|
2267
|
+
if (created)
|
|
2268
|
+
await chmod(probeRoot, 0o700);
|
|
2269
|
+
const evidence = await attestCodexWriteProbeRoot(probeRoot);
|
|
2270
|
+
if (evidence === null) {
|
|
2271
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
2272
|
+
}
|
|
2273
|
+
probeRootBefore = evidence;
|
|
2274
|
+
}
|
|
2275
|
+
catch (error) {
|
|
2276
|
+
if (error instanceof BridgeError &&
|
|
2277
|
+
error.code === "CODEX_FACTORY_ATTESTATION_INVALID") {
|
|
2278
|
+
throw error;
|
|
2279
|
+
}
|
|
2280
|
+
if (transientCodexWriteProbeError(error)) {
|
|
2281
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
2282
|
+
}
|
|
2283
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The controller-owned Codex probe root could not be established safely.");
|
|
2284
|
+
}
|
|
2285
|
+
let cwd;
|
|
2286
|
+
let before;
|
|
2287
|
+
let result = codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
2288
|
+
try {
|
|
2289
|
+
cwd = await mkdtemp(path.join(probeRoot, CODEX_WRITE_PROBE_DIRECTORY_PREFIX));
|
|
2290
|
+
if (path.dirname(cwd) !== probeRoot) {
|
|
2291
|
+
throw new Error("probe directory escaped its dedicated root");
|
|
2292
|
+
}
|
|
2293
|
+
await chmod(cwd, 0o700);
|
|
2294
|
+
before = await attestCodexWriteProbeDirectory(cwd);
|
|
2295
|
+
result = await connector.runWriteCompatibilityProbe({
|
|
2296
|
+
cwd,
|
|
2297
|
+
forbiddenThreadIds: [
|
|
2298
|
+
...new Set([
|
|
2299
|
+
COMPATIBILITY_PROBE_THREAD_ID,
|
|
2300
|
+
...context.forbiddenCodexThreadIds,
|
|
2301
|
+
...this.trackedRoutes,
|
|
2302
|
+
]),
|
|
2303
|
+
],
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
if (!safeCodexWriteProbeFilesystemFailure(error)) {
|
|
2308
|
+
if (error instanceof BridgeError)
|
|
2309
|
+
throw error;
|
|
2310
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The disposable Codex probe directory could not be established safely.");
|
|
2311
|
+
}
|
|
2312
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED");
|
|
2313
|
+
}
|
|
2314
|
+
let unchanged = false;
|
|
2315
|
+
let removed = false;
|
|
2316
|
+
if (cwd !== undefined && before !== undefined) {
|
|
2317
|
+
try {
|
|
2318
|
+
const after = await attestCodexWriteProbeDirectory(cwd);
|
|
2319
|
+
unchanged = sameCodexWriteProbeDirectory(before, after);
|
|
2320
|
+
await rmdir(cwd);
|
|
2321
|
+
try {
|
|
2322
|
+
await lstat(cwd);
|
|
2323
|
+
}
|
|
2324
|
+
catch (error) {
|
|
2325
|
+
removed = systemErrorCode(error) === "ENOENT";
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
catch (error) {
|
|
2329
|
+
if (!safeCodexWriteProbeFilesystemFailure(error)) {
|
|
2330
|
+
if (error instanceof BridgeError)
|
|
2331
|
+
throw error;
|
|
2332
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The disposable Codex probe directory could not be re-attested safely.");
|
|
2333
|
+
}
|
|
2334
|
+
// Never remove a path after its exact ownership evidence changed.
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
if (cwd !== undefined && !removed) {
|
|
2338
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED", result);
|
|
2339
|
+
}
|
|
2340
|
+
else if (cwd !== undefined && !unchanged) {
|
|
2341
|
+
result = codexWriteProbeFailure("CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED", result);
|
|
2342
|
+
}
|
|
2343
|
+
const [probeRootAfter, rootAfter] = await Promise.all([
|
|
2344
|
+
attestCodexWriteProbeRoot(probeRoot),
|
|
2345
|
+
attestCodexWriteProbeRoot(stateRoot),
|
|
2346
|
+
]);
|
|
2347
|
+
if (probeRootAfter === null || rootAfter === null) {
|
|
2348
|
+
return codexWriteProbeFailure("CODEX_WRITE_PROBE_THREAD_SETUP_FAILED", result);
|
|
2349
|
+
}
|
|
2350
|
+
if (!sameCodexWriteProbeRoot(probeRootBefore, probeRootAfter) ||
|
|
2351
|
+
!sameCodexWriteProbeRoot(rootBefore, rootAfter)) {
|
|
2352
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "A controller-owned Codex probe root changed while the disposable probe ran.");
|
|
2353
|
+
}
|
|
2354
|
+
return result;
|
|
2355
|
+
}
|
|
1769
2356
|
async initialize(callbacks) {
|
|
1770
2357
|
if (this.closed || this.initialized) {
|
|
1771
2358
|
throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The local Codex provider cannot be initialized in this state.");
|
|
1772
2359
|
}
|
|
1773
2360
|
this.callbacks = callbacks;
|
|
1774
2361
|
this.initialized = true;
|
|
2362
|
+
if (this.compatibilityFailureCode !== undefined) {
|
|
2363
|
+
return {
|
|
2364
|
+
health: "degraded",
|
|
2365
|
+
compatibility: "incompatible",
|
|
2366
|
+
safeErrorCode: this.compatibilityFailureCode,
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
1775
2369
|
return this.factory.writeCompatibility === null
|
|
1776
2370
|
? {
|
|
1777
2371
|
health: "degraded",
|
|
@@ -1817,9 +2411,13 @@ export class LocalCodexGatewayProvider {
|
|
|
1817
2411
|
throw selectionError;
|
|
1818
2412
|
}
|
|
1819
2413
|
};
|
|
2414
|
+
const retainedEndpointRefresh = this.retainedEndpointRefreshResult();
|
|
1820
2415
|
if (this.endpointRefresh !== undefined) {
|
|
1821
2416
|
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1822
2417
|
}
|
|
2418
|
+
else if (retainedEndpointRefresh !== undefined) {
|
|
2419
|
+
route = await stageRefreshedRoute(retainedEndpointRefresh);
|
|
2420
|
+
}
|
|
1823
2421
|
else if (this.endpointUnavailable &&
|
|
1824
2422
|
this.pendingEndpointAttestation === undefined) {
|
|
1825
2423
|
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
@@ -1977,6 +2575,9 @@ export class LocalCodexGatewayProvider {
|
|
|
1977
2575
|
...(input.progressWatchActive === true
|
|
1978
2576
|
? { progressWatchActive: true }
|
|
1979
2577
|
: {}),
|
|
2578
|
+
...(input.steer === true && input.queuedAhead !== undefined
|
|
2579
|
+
? { queuedAhead: input.queuedAhead }
|
|
2580
|
+
: {}),
|
|
1980
2581
|
});
|
|
1981
2582
|
}
|
|
1982
2583
|
catch (error) {
|
|
@@ -2075,6 +2676,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2075
2676
|
this.routesRequiringRecovery.clear();
|
|
2076
2677
|
this.clearEndpointActivationRetry();
|
|
2077
2678
|
this.pendingEndpointRefreshEvent = undefined;
|
|
2679
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2078
2680
|
this.pendingEndpointAttestation = undefined;
|
|
2079
2681
|
const refreshResult = this.endpointRefresh === undefined
|
|
2080
2682
|
? []
|
|
@@ -2103,6 +2705,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2103
2705
|
this.callbackQueue.length = 0;
|
|
2104
2706
|
this.pendingEndpointAttestation = undefined;
|
|
2105
2707
|
this.pendingEndpointRefreshEvent = undefined;
|
|
2708
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2106
2709
|
this.trackedRoutes.clear();
|
|
2107
2710
|
this.routeAliases.clear();
|
|
2108
2711
|
this.expectsReply.clear();
|
|
@@ -2214,13 +2817,23 @@ export class LocalCodexGatewayProvider {
|
|
|
2214
2817
|
return route;
|
|
2215
2818
|
}
|
|
2216
2819
|
catch (error) {
|
|
2820
|
+
let cleanupFailure;
|
|
2217
2821
|
if (connector !== undefined) {
|
|
2218
|
-
await connector.close().catch(() =>
|
|
2822
|
+
await connector.close().catch((closeError) => {
|
|
2823
|
+
cleanupFailure = closeError;
|
|
2824
|
+
});
|
|
2219
2825
|
}
|
|
2220
2826
|
else if (transport !== undefined) {
|
|
2221
|
-
await transport.close().catch(() =>
|
|
2827
|
+
await transport.close().catch((closeError) => {
|
|
2828
|
+
cleanupFailure = closeError;
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
if (cleanupFailure !== undefined) {
|
|
2832
|
+
if (isCodexTrustFailure(cleanupFailure))
|
|
2833
|
+
throw cleanupFailure;
|
|
2834
|
+
throw new BridgeError("CODEX_ROUTE_CLEANUP_FAILED", "Codex route setup could not prove exact provider-process cleanup.");
|
|
2222
2835
|
}
|
|
2223
|
-
if (
|
|
2836
|
+
if (isCodexTrustFailure(error))
|
|
2224
2837
|
throw error;
|
|
2225
2838
|
if (error instanceof BridgeError)
|
|
2226
2839
|
throw error;
|
|
@@ -2343,6 +2956,21 @@ export class LocalCodexGatewayProvider {
|
|
|
2343
2956
|
event.current.endpointGeneration === this.factory.endpointGeneration &&
|
|
2344
2957
|
event.routes.some((route) => route.routeHandle === routeHandle));
|
|
2345
2958
|
}
|
|
2959
|
+
retainedEndpointRefreshResult() {
|
|
2960
|
+
const event = this.pendingEndpointRefreshEvent;
|
|
2961
|
+
if (event === undefined ||
|
|
2962
|
+
this.pendingEndpointRefreshSelectorClaimed ||
|
|
2963
|
+
this.pendingEndpointAttestation === undefined ||
|
|
2964
|
+
event.current.endpointGeneration !== this.factory.endpointGeneration) {
|
|
2965
|
+
return undefined;
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
event,
|
|
2969
|
+
delivery: "selector",
|
|
2970
|
+
selectorClaimed: false,
|
|
2971
|
+
published: true,
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2346
2974
|
async refreshEndpoint(delivery) {
|
|
2347
2975
|
if (this.refreshFactory === undefined || this.closing || this.closed) {
|
|
2348
2976
|
return undefined;
|
|
@@ -2387,6 +3015,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2387
3015
|
this.compatibilityAttested = false;
|
|
2388
3016
|
this.pendingEndpointAttestation = undefined;
|
|
2389
3017
|
this.pendingEndpointRefreshEvent = undefined;
|
|
3018
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2390
3019
|
for (const routeHandle of this.trackedRoutes) {
|
|
2391
3020
|
this.routesRequiringRecovery.add(routeHandle);
|
|
2392
3021
|
}
|
|
@@ -2401,7 +3030,9 @@ export class LocalCodexGatewayProvider {
|
|
|
2401
3030
|
},
|
|
2402
3031
|
delivery: "callback",
|
|
2403
3032
|
selectorClaimed: true,
|
|
3033
|
+
published: false,
|
|
2404
3034
|
};
|
|
3035
|
+
result.published = true;
|
|
2405
3036
|
this.emitEndpointRefresh(result.event);
|
|
2406
3037
|
for (const routeHandle of this.routesRequiringRecovery) {
|
|
2407
3038
|
if (this.trackedRoutes.has(routeHandle)) {
|
|
@@ -2415,18 +3046,14 @@ export class LocalCodexGatewayProvider {
|
|
|
2415
3046
|
candidate = undefined;
|
|
2416
3047
|
return undefined;
|
|
2417
3048
|
}
|
|
2418
|
-
|
|
2419
|
-
candidate = undefined;
|
|
2420
|
-
this.retiredEndpointGeneration = undefined;
|
|
2421
|
-
this.compatibilityAttested = false;
|
|
2422
|
-
this.endpointUnavailable = true;
|
|
2423
|
-
this.pendingEndpointAttestation = attestation;
|
|
3049
|
+
const replacementFactory = candidate;
|
|
2424
3050
|
const refreshedRoutes = [];
|
|
3051
|
+
const stagedRoutes = [];
|
|
2425
3052
|
for (const routeHandle of [...this.trackedRoutes]) {
|
|
2426
3053
|
if (this.closing || this.closed)
|
|
2427
3054
|
break;
|
|
2428
3055
|
try {
|
|
2429
|
-
const route = await this.createRoute(routeHandle,
|
|
3056
|
+
const route = await this.createRoute(routeHandle, replacementFactory, false);
|
|
2430
3057
|
if (this.closing || this.closed) {
|
|
2431
3058
|
await this.closeRoute(route);
|
|
2432
3059
|
break;
|
|
@@ -2436,17 +3063,34 @@ export class LocalCodexGatewayProvider {
|
|
|
2436
3063
|
await this.closeRoute(route);
|
|
2437
3064
|
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
|
|
2438
3065
|
}
|
|
2439
|
-
|
|
2440
|
-
this.routesRequiringRecovery.delete(routeHandle);
|
|
2441
|
-
this.routeRecoveryAttempts.delete(routeHandle);
|
|
2442
|
-
refreshedRoutes.push({ routeHandle, state });
|
|
3066
|
+
stagedRoutes.push({ routeHandle, route, state });
|
|
2443
3067
|
}
|
|
2444
|
-
catch {
|
|
3068
|
+
catch (error) {
|
|
2445
3069
|
this.routesRequiringRecovery.add(routeHandle);
|
|
3070
|
+
if (isCodexTrustFailure(error))
|
|
3071
|
+
throw error;
|
|
2446
3072
|
}
|
|
2447
3073
|
}
|
|
2448
|
-
if (this.closing || this.closed)
|
|
3074
|
+
if (this.closing || this.closed) {
|
|
3075
|
+
await replacementFactory.close();
|
|
3076
|
+
candidate = undefined;
|
|
2449
3077
|
return undefined;
|
|
3078
|
+
}
|
|
3079
|
+
this.factory = replacementFactory;
|
|
3080
|
+
candidate = undefined;
|
|
3081
|
+
this.retiredEndpointGeneration = undefined;
|
|
3082
|
+
this.compatibilityAttested = false;
|
|
3083
|
+
this.endpointUnavailable = true;
|
|
3084
|
+
this.pendingEndpointAttestation = attestation;
|
|
3085
|
+
for (const staged of stagedRoutes) {
|
|
3086
|
+
this.routes.set(staged.routeHandle, staged.route);
|
|
3087
|
+
this.routesRequiringRecovery.delete(staged.routeHandle);
|
|
3088
|
+
this.routeRecoveryAttempts.delete(staged.routeHandle);
|
|
3089
|
+
refreshedRoutes.push({
|
|
3090
|
+
routeHandle: staged.routeHandle,
|
|
3091
|
+
state: staged.state,
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
2450
3094
|
const result = {
|
|
2451
3095
|
event: {
|
|
2452
3096
|
outcome: "compatible",
|
|
@@ -2457,12 +3101,15 @@ export class LocalCodexGatewayProvider {
|
|
|
2457
3101
|
},
|
|
2458
3102
|
delivery: this.endpointRefreshDelivery ?? "callback",
|
|
2459
3103
|
selectorClaimed: false,
|
|
3104
|
+
published: false,
|
|
2460
3105
|
};
|
|
2461
3106
|
if (result.event.outcome === "compatible") {
|
|
2462
3107
|
this.pendingEndpointRefreshEvent = result.event;
|
|
3108
|
+
this.pendingEndpointRefreshSelectorClaimed = false;
|
|
2463
3109
|
}
|
|
2464
3110
|
if (result.delivery === "callback") {
|
|
2465
3111
|
result.selectorClaimed = true;
|
|
3112
|
+
result.published = true;
|
|
2466
3113
|
this.publishEndpointRefresh(result.event);
|
|
2467
3114
|
}
|
|
2468
3115
|
for (const routeHandle of this.routesRequiringRecovery) {
|
|
@@ -2474,7 +3121,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2474
3121
|
}
|
|
2475
3122
|
catch (error) {
|
|
2476
3123
|
if (candidate !== undefined && candidate !== previousFactory) {
|
|
2477
|
-
await candidate.close()
|
|
3124
|
+
await candidate.close();
|
|
2478
3125
|
}
|
|
2479
3126
|
throw error;
|
|
2480
3127
|
}
|
|
@@ -2490,7 +3137,10 @@ export class LocalCodexGatewayProvider {
|
|
|
2490
3137
|
let candidate;
|
|
2491
3138
|
try {
|
|
2492
3139
|
candidate = await refreshFactory();
|
|
2493
|
-
|
|
3140
|
+
const versionFailure = codexVersionFailureCode(candidate.appServerVersion);
|
|
3141
|
+
validateCodexFactory(candidate, {
|
|
3142
|
+
allowUnsupportedVersion: versionFailure !== undefined,
|
|
3143
|
+
});
|
|
2494
3144
|
if (candidate.hostId !== previous.hostId ||
|
|
2495
3145
|
candidate.protocol !== previousFactory.protocol) {
|
|
2496
3146
|
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
|
|
@@ -2500,14 +3150,25 @@ export class LocalCodexGatewayProvider {
|
|
|
2500
3150
|
if (candidate !== previousFactory)
|
|
2501
3151
|
await candidate.close();
|
|
2502
3152
|
candidate = undefined;
|
|
3153
|
+
if (this.endpointUnavailable &&
|
|
3154
|
+
this.compatibilityFailureCode === undefined &&
|
|
3155
|
+
!this.factory.writableReady &&
|
|
3156
|
+
this.factory.writeCompatibility === null) {
|
|
3157
|
+
throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server remains monitor-only until a write-attested generation is available.");
|
|
3158
|
+
}
|
|
2503
3159
|
if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
|
|
2504
3160
|
continue;
|
|
2505
3161
|
}
|
|
2506
3162
|
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2507
3163
|
}
|
|
2508
|
-
// Endpoint generations never inherit cached compatibility.
|
|
2509
|
-
//
|
|
2510
|
-
|
|
3164
|
+
// Endpoint generations never inherit cached compatibility. A version
|
|
3165
|
+
// outside the tested major is classified from OS-attested factory
|
|
3166
|
+
// evidence alone; no connector or transport may touch that protocol.
|
|
3167
|
+
const probes = versionFailure === undefined
|
|
3168
|
+
? await this.runCompatibilityProbesFor(candidate)
|
|
3169
|
+
: compatibilityProbeNames.codex.map((name, index) => index === 0
|
|
3170
|
+
? passedProbe(name)
|
|
3171
|
+
: failedProbe(name, versionFailure));
|
|
2511
3172
|
const attestation = evaluateCompatibilityAttestation({
|
|
2512
3173
|
surface: "codex",
|
|
2513
3174
|
version: candidate.appServerVersion,
|
|
@@ -2567,11 +3228,16 @@ export class LocalCodexGatewayProvider {
|
|
|
2567
3228
|
return false;
|
|
2568
3229
|
}
|
|
2569
3230
|
result.selectorClaimed = true;
|
|
3231
|
+
if (this.pendingEndpointRefreshEvent === result.event) {
|
|
3232
|
+
this.pendingEndpointRefreshSelectorClaimed = true;
|
|
3233
|
+
}
|
|
2570
3234
|
return true;
|
|
2571
3235
|
}
|
|
2572
3236
|
emitSelectorRefreshFallback(result, staleRouteHandle) {
|
|
2573
3237
|
if (result.delivery !== "selector" || !result.selectorClaimed)
|
|
2574
3238
|
return;
|
|
3239
|
+
if (result.published)
|
|
3240
|
+
return;
|
|
2575
3241
|
let event = result.event;
|
|
2576
3242
|
if (staleRouteHandle !== undefined &&
|
|
2577
3243
|
event.outcome === "compatible" &&
|
|
@@ -2583,6 +3249,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2583
3249
|
result.event = event;
|
|
2584
3250
|
this.pendingEndpointRefreshEvent = event;
|
|
2585
3251
|
}
|
|
3252
|
+
result.published = true;
|
|
2586
3253
|
this.publishEndpointRefresh(event);
|
|
2587
3254
|
}
|
|
2588
3255
|
publishEndpointRefresh(event) {
|