@bitkyc08/opencodex 2.11.1 → 2.12.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/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { evaluateAssertions } from "../conformance/assertion";
|
|
2
|
+
import { emptyObservation, finalizeObservation, setClientResponse } from "../conformance/observation";
|
|
3
|
+
import { normalizeSseBytes } from "../conformance/sse-normalize";
|
|
4
|
+
import type { CaseAuthority, CaseRecord, FailureClassification, FailureRule, NormalizedEvent, NormalizedObservation } from "../conformance/types";
|
|
5
|
+
import { domainHash, jcsStringify, scenarioManifestDigest, subjectIdForSubject, suiteManifestDigest } from "../digest";
|
|
6
|
+
import type { RouteSubjectV1 } from "../events/types";
|
|
7
|
+
import { isTrustedLabRouteExecutor } from "../../lib/lab-live-execution-authority";
|
|
8
|
+
import { buildRouteSubjectV1 } from "../subject/route-subject";
|
|
9
|
+
import { createLabDestination, LabDestinationError } from "./destination";
|
|
10
|
+
import { expandLiveScenario, loadLiveCaseAuthority } from "./manifest";
|
|
11
|
+
import { createSandboxResourceState, enforceSandboxLimits, LabSandboxError, prepareLiveSandbox } from "./sandbox";
|
|
12
|
+
import { liveSuiteManifestObjectForCase } from "./suite-manifest";
|
|
13
|
+
import { classifyTransportError, TransportError } from "./transport";
|
|
14
|
+
import type { DnsResolver, LabRouteContext, LabTransport, LiveExecutionAuthority, LiveRunConfig, LiveScenarioRunResult, TrustedLabRouteExecutor } from "./types";
|
|
15
|
+
|
|
16
|
+
export interface LiveExecutorOptions {
|
|
17
|
+
/** Host-issued exact-route capability for evidence-eligible production execution. */
|
|
18
|
+
routeExecutor?: TrustedLabRouteExecutor;
|
|
19
|
+
/** Test-only byte transport. Results are deliberately not evidence-eligible. */
|
|
20
|
+
transport?: LabTransport;
|
|
21
|
+
resolve?: DnsResolver;
|
|
22
|
+
configDir?: string;
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TrustedLiveResultReceipt {
|
|
27
|
+
authorityDigest: string;
|
|
28
|
+
scenarioId: string;
|
|
29
|
+
suiteId: string;
|
|
30
|
+
scenarioManifestDigest: string;
|
|
31
|
+
suiteManifestDigest: string;
|
|
32
|
+
routeSubjectId: string;
|
|
33
|
+
resultDigest: string;
|
|
34
|
+
retryPolicy: FailureRule["retry"] | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TRUSTED_RESULT_RECEIPTS = new WeakMap<object, TrustedLiveResultReceipt>();
|
|
38
|
+
const LIVE_AUTHORITY_DOMAIN = "ocx-lab:live-authority:v1";
|
|
39
|
+
const TRUSTED_LIVE_RESULT_DOMAIN = "ocx-lab:trusted-live-result:v1";
|
|
40
|
+
|
|
41
|
+
function trustedResultDigest(result: LiveScenarioRunResult): string {
|
|
42
|
+
const payload = {
|
|
43
|
+
scenarioId: result.scenarioId,
|
|
44
|
+
suite: result.suite,
|
|
45
|
+
startedAt: result.startedAt,
|
|
46
|
+
completedAt: result.completedAt,
|
|
47
|
+
passed: result.passed,
|
|
48
|
+
classification: result.classification,
|
|
49
|
+
secondaryCode: result.secondaryCode ?? null,
|
|
50
|
+
assertionResults: result.assertionResults.map((row) => ({
|
|
51
|
+
id: row.id,
|
|
52
|
+
operator: row.operator,
|
|
53
|
+
required: row.required,
|
|
54
|
+
passed: row.passed,
|
|
55
|
+
observedSummary: row.observedSummary,
|
|
56
|
+
reason: row.reason ?? null,
|
|
57
|
+
})),
|
|
58
|
+
diagnostics: [...result.diagnostics],
|
|
59
|
+
routeSubject: result.routeSubject ?? null,
|
|
60
|
+
transportError: result.transportError ?? null,
|
|
61
|
+
executionAuthority: result.executionAuthority,
|
|
62
|
+
};
|
|
63
|
+
return domainHash(TRUSTED_LIVE_RESULT_DOMAIN, jcsStringify(payload));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function receiptFor(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority, retryPolicy: FailureRule["retry"] | null): TrustedLiveResultReceipt {
|
|
67
|
+
if (!result.routeSubject) throw new Error("trusted live result has no route subject");
|
|
68
|
+
return {
|
|
69
|
+
authorityDigest: domainHash(LIVE_AUTHORITY_DOMAIN, jcsStringify(authority)),
|
|
70
|
+
scenarioId: caseRecord.id,
|
|
71
|
+
suiteId: caseRecord.suite,
|
|
72
|
+
scenarioManifestDigest: scenarioManifestDigest(expandLiveScenario(caseRecord, authority)),
|
|
73
|
+
suiteManifestDigest: suiteManifestDigest(liveSuiteManifestObjectForCase(caseRecord, authority)),
|
|
74
|
+
routeSubjectId: subjectIdForSubject(result.routeSubject),
|
|
75
|
+
resultDigest: trustedResultDigest(result),
|
|
76
|
+
retryPolicy,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sealTrustedLiveResult(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority, retryPolicy: FailureRule["retry"] | null): void {
|
|
81
|
+
TRUSTED_RESULT_RECEIPTS.set(result, receiptFor(result, caseRecord, authority, retryPolicy));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Verify the module-private execution receipt before any live result enters persistence. */
|
|
85
|
+
export function assertTrustedLiveResultReceipt(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority): void {
|
|
86
|
+
const actual = TRUSTED_RESULT_RECEIPTS.get(result);
|
|
87
|
+
if (!actual || result.executionAuthority !== "trusted_route") {
|
|
88
|
+
throw new Error("live result lacks trusted execution receipt");
|
|
89
|
+
}
|
|
90
|
+
const expected = receiptFor(result, caseRecord, authority, actual.retryPolicy);
|
|
91
|
+
for (const key of Object.keys(expected) as Array<keyof TrustedLiveResultReceipt>) {
|
|
92
|
+
if (actual[key] !== expected[key]) throw new Error("live result trusted execution receipt mismatch");
|
|
93
|
+
}
|
|
94
|
+
if (result.scenarioId !== caseRecord.id || result.suite !== caseRecord.suite) {
|
|
95
|
+
throw new Error("live result scenario identity mismatch");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Return retryability sealed by the executor after validating the trusted receipt. */
|
|
100
|
+
export function trustedLiveResultRetryable(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority): boolean {
|
|
101
|
+
assertTrustedLiveResultReceipt(result, caseRecord, authority);
|
|
102
|
+
return TRUSTED_RESULT_RECEIPTS.get(result)?.retryPolicy === "bounded";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function liveLimitsFromAuthority(authorityLimits: Record<string, number>): LiveRunConfig {
|
|
106
|
+
return {
|
|
107
|
+
totalTimeoutMs: authorityLimits.totalTimeoutMs ?? 120_000, connectTimeoutMs: authorityLimits.connectTimeoutMs ?? 10_000,
|
|
108
|
+
firstByteTimeoutMs: authorityLimits.firstByteTimeoutMs ?? 30_000, inactivityTimeoutMs: authorityLimits.inactivityTimeoutMs ?? 30_000,
|
|
109
|
+
maxRequests: authorityLimits.maxRequests ?? 16, maxInputBytes: authorityLimits.maxInputBytes ?? 8 * 1024 * 1024,
|
|
110
|
+
maxOutputBytes: authorityLimits.maxOutputBytes ?? 16 * 1024 * 1024, maxOutputTokens: authorityLimits.maxOutputTokens ?? 32_768,
|
|
111
|
+
maxToolCalls: authorityLimits.maxToolCalls ?? 32, maxMemoryBytes: authorityLimits.maxMemoryBytes ?? 512 * 1024 * 1024,
|
|
112
|
+
maxChildProcesses: authorityLimits.maxChildProcesses ?? 0, maxArtifacts: authorityLimits.maxArtifacts ?? 16,
|
|
113
|
+
perArtifactBytes: authorityLimits.perArtifactBytes ?? 256 * 1024, aggregateArtifactBytes: authorityLimits.aggregateArtifactBytes ?? 1024 * 1024,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isLiveCaseApplicableToRoute(caseRecord: CaseRecord, route: LabRouteContext): boolean {
|
|
118
|
+
const req = caseRecord.requirements;
|
|
119
|
+
if (!req.inboundProtocols.includes(route.inboundProtocol) || !req.upstreamProtocols.includes(route.upstreamProtocol) || !req.surfaces.includes(route.surface)) return false;
|
|
120
|
+
if (req.platforms.length > 0 && !req.platforms.includes(process.platform)) return false;
|
|
121
|
+
if (!req.requiredClaims.every((claim) => (route.requiredClaims ?? []).includes(claim))) return false;
|
|
122
|
+
if (!req.requiredHarnessFeatures.every((feature) => (route.availableHarnessFeatures ?? []).includes(feature))) return false;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function routePreconditionFailure(route: LabRouteContext, caseRecord: CaseRecord): string | null {
|
|
127
|
+
for (const precondition of caseRecord.requirements.routePreconditions) {
|
|
128
|
+
if (precondition === "lab_run_approval" && route.labRunApproval !== true) return "route_precondition_unmet:lab_run_approval";
|
|
129
|
+
if (precondition === "allow_private_network" && route.allowPrivateNetwork !== true) return "route_precondition_unmet:allow_private_network";
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function classifyWithFailureRules(rules: FailureRule[], signal: string): { classification: FailureClassification; secondaryCode: string; retryPolicy: FailureRule["retry"] | null } {
|
|
135
|
+
const rule = rules.find((row) => row.match.includes(signal));
|
|
136
|
+
return rule ? { classification: rule.classification, secondaryCode: rule.secondaryCode ?? signal, retryPolicy: rule.retry }
|
|
137
|
+
: { classification: "inconclusive", secondaryCode: "unclassified", retryPolicy: null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pathForProtocol(protocol: string): string {
|
|
141
|
+
if (protocol === "openai-chat") return "/chat/completions";
|
|
142
|
+
if (protocol === "anthropic-messages") return "/messages";
|
|
143
|
+
return "/responses";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function chatObservation(body: string, status: number): NormalizedObservation {
|
|
147
|
+
const observation = emptyObservation();
|
|
148
|
+
const output: unknown[] = [];
|
|
149
|
+
let text = "";
|
|
150
|
+
let terminal = false;
|
|
151
|
+
const trimmed = body.trimStart();
|
|
152
|
+
if (trimmed.startsWith("data:")) {
|
|
153
|
+
const toolParts = new Map<number, { id: string; name: string; arguments: string }>();
|
|
154
|
+
for (const frame of body.split(/\r?\n\r?\n/)) {
|
|
155
|
+
const line = frame.split(/\r?\n/).find((row) => row.startsWith("data:"));
|
|
156
|
+
if (!line) continue;
|
|
157
|
+
const payload = line.slice(5).trim();
|
|
158
|
+
if (!payload || payload === "[DONE]") { if (payload === "[DONE]") terminal = true; continue; }
|
|
159
|
+
let json: any;
|
|
160
|
+
try { json = JSON.parse(payload); } catch { continue; }
|
|
161
|
+
for (const choice of Array.isArray(json.choices) ? json.choices : []) {
|
|
162
|
+
if (typeof choice?.delta?.content === "string") text += choice.delta.content;
|
|
163
|
+
if (choice?.finish_reason != null) terminal = true;
|
|
164
|
+
for (const call of Array.isArray(choice?.delta?.tool_calls) ? choice.delta.tool_calls : []) {
|
|
165
|
+
const idx = Number.isInteger(call?.index) ? call.index : toolParts.size;
|
|
166
|
+
const prior = toolParts.get(idx) ?? { id: "", name: "", arguments: "" };
|
|
167
|
+
if (typeof call?.id === "string") prior.id = call.id;
|
|
168
|
+
if (typeof call?.function?.name === "string") prior.name = call.function.name;
|
|
169
|
+
if (typeof call?.function?.arguments === "string") prior.arguments += call.function.arguments;
|
|
170
|
+
toolParts.set(idx, prior);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const row of [...toolParts.entries()].sort((a, b) => a[0] - b[0]).map(([, value]) => value)) {
|
|
175
|
+
output.push({ type: "function_call", call_id: row.id, name: row.name, arguments: row.arguments });
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const json = JSON.parse(body) as any;
|
|
179
|
+
const choice = Array.isArray(json.choices) ? json.choices[0] : undefined;
|
|
180
|
+
if (typeof choice?.message?.content === "string") text = choice.message.content;
|
|
181
|
+
terminal = choice?.finish_reason != null;
|
|
182
|
+
for (const call of Array.isArray(choice?.message?.tool_calls) ? choice.message.tool_calls : []) {
|
|
183
|
+
output.push({ type: "function_call", call_id: call.id, name: call.function?.name, arguments: call.function?.arguments });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (text) output.unshift({ type: "message", content: [{ type: "output_text", text }] });
|
|
187
|
+
const json = { status: terminal ? "completed" : "incomplete", output };
|
|
188
|
+
const events: NormalizedEvent[] = terminal ? [{ event: "response.completed", data: { response: json }, ordinal: 0 }] : [];
|
|
189
|
+
finalizeObservation(observation, events, json, status);
|
|
190
|
+
return observation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function responsesObservation(body: string, status: number): NormalizedObservation {
|
|
194
|
+
const observation = emptyObservation();
|
|
195
|
+
const trimmed = body.trimStart();
|
|
196
|
+
if (trimmed.startsWith("data:") || trimmed.startsWith("event:")) {
|
|
197
|
+
const events = normalizeSseBytes(new TextEncoder().encode(body), "openai-responses");
|
|
198
|
+
finalizeObservation(observation, events, null, status);
|
|
199
|
+
return observation;
|
|
200
|
+
}
|
|
201
|
+
const json = JSON.parse(body) as Record<string, unknown>;
|
|
202
|
+
finalizeObservation(observation, [], json, status);
|
|
203
|
+
const state = typeof json.status === "string" ? json.status : undefined;
|
|
204
|
+
if (state === "completed" || state === "failed" || state === "incomplete") {
|
|
205
|
+
setClientResponse(observation, { terminal: state === "completed" ? "completed" : state });
|
|
206
|
+
}
|
|
207
|
+
return observation;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeTransportObservation(route: LabRouteContext, response: { status: number; body: string }): NormalizedObservation {
|
|
211
|
+
const observation = route.upstreamProtocol === "openai-chat" ? chatObservation(response.body, response.status) : responsesObservation(response.body, response.status);
|
|
212
|
+
if (route.surface.startsWith("anthropic-") && observation.client.response.terminal === "completed") {
|
|
213
|
+
setClientResponse(observation, { terminal: "message_stop" });
|
|
214
|
+
}
|
|
215
|
+
return observation;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function withTotalTimeout<T>(timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const timer = setTimeout(() => controller.abort(new TransportError("total_timeout", "live scenario total timeout")), timeoutMs);
|
|
221
|
+
try {
|
|
222
|
+
return await Promise.race([
|
|
223
|
+
run(controller.signal),
|
|
224
|
+
new Promise<T>((_, reject) => controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true })),
|
|
225
|
+
]);
|
|
226
|
+
} finally { clearTimeout(timer); }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function runLiveScenario(caseRecord: CaseRecord, routeContext: LabRouteContext, opts: LiveExecutorOptions = {}): Promise<LiveScenarioRunResult> {
|
|
230
|
+
const diagnostics: string[] = [];
|
|
231
|
+
const startedAt = Date.now();
|
|
232
|
+
let routeSubject: RouteSubjectV1 | undefined;
|
|
233
|
+
let executionAuthority: LiveExecutionAuthority = "none";
|
|
234
|
+
let authority: CaseAuthority | undefined;
|
|
235
|
+
let activeCase = caseRecord;
|
|
236
|
+
let trustedExecutionStarted = false;
|
|
237
|
+
let failureRules: FailureRule[] = [];
|
|
238
|
+
const complete = (partial: Omit<LiveScenarioRunResult, "startedAt" | "completedAt" | "executionAuthority">, retryPolicy: FailureRule["retry"] | null = null): LiveScenarioRunResult => {
|
|
239
|
+
const result: LiveScenarioRunResult = { ...partial, executionAuthority, startedAt, completedAt: Math.max(startedAt, Date.now()) };
|
|
240
|
+
if (trustedExecutionStarted && authority && result.routeSubject) sealTrustedLiveResult(result, activeCase, authority, retryPolicy);
|
|
241
|
+
return result;
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
const environment = prepareLiveSandbox(opts.env);
|
|
245
|
+
authority = loadLiveCaseAuthority();
|
|
246
|
+
const canonicalCase = authority.cases.find((row) => row.id === caseRecord.id);
|
|
247
|
+
if (!canonicalCase || jcsStringify(canonicalCase) !== jcsStringify(caseRecord)) {
|
|
248
|
+
throw new TransportError("harness_failure", "live scenario does not match canonical authority");
|
|
249
|
+
}
|
|
250
|
+
activeCase = canonicalCase;
|
|
251
|
+
if (!isLiveCaseApplicableToRoute(activeCase, routeContext)) {
|
|
252
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed: false, classification: "inconclusive", secondaryCode: "scenario_inapplicable", assertionResults: [], diagnostics, routeSubject });
|
|
253
|
+
}
|
|
254
|
+
const preconditionFailure = routePreconditionFailure(routeContext, activeCase);
|
|
255
|
+
if (preconditionFailure) {
|
|
256
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed: false, classification: "inconclusive", secondaryCode: preconditionFailure, assertionResults: [], diagnostics: [preconditionFailure], routeSubject });
|
|
257
|
+
}
|
|
258
|
+
const expanded = expandLiveScenario(activeCase, authority);
|
|
259
|
+
failureRules = expanded.failureRules as FailureRule[];
|
|
260
|
+
const limits = liveLimitsFromAuthority(expanded.executionLimits as Record<string, number>);
|
|
261
|
+
const destination = await createLabDestination({ baseUrl: routeContext.baseUrl, allowPrivateNetwork: routeContext.allowPrivateNetwork, labRunApproval: routeContext.labRunApproval, resolve: opts.resolve, configDir: opts.configDir });
|
|
262
|
+
routeSubject = buildRouteSubjectV1(routeContext, destination, opts.configDir);
|
|
263
|
+
const state = createSandboxResourceState();
|
|
264
|
+
let observation: NormalizedObservation;
|
|
265
|
+
if (opts.routeExecutor) {
|
|
266
|
+
if (!isTrustedLabRouteExecutor(opts.routeExecutor)) throw new TransportError("untrusted_route_executor", "untrusted route executor capability");
|
|
267
|
+
executionAuthority = "trusted_route";
|
|
268
|
+
trustedExecutionStarted = true;
|
|
269
|
+
observation = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.routeExecutor!.execute({ routeContext, destination, routeSubject: routeSubject!, scenarioId: activeCase.id, initiatingRequest: activeCase.initiatingRequest?.bytesUtf8, limits, signal, environment }));
|
|
270
|
+
} else if (opts.transport && activeCase.initiatingRequest) {
|
|
271
|
+
executionAuthority = "test_transport";
|
|
272
|
+
const body = activeCase.initiatingRequest.bytesUtf8;
|
|
273
|
+
const inputBytes = new TextEncoder().encode(body).byteLength;
|
|
274
|
+
enforceSandboxLimits(state, limits, { requests: 1, inputBytes });
|
|
275
|
+
const response = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.transport!.request({ method: "POST", path: pathForProtocol(routeContext.upstreamProtocol), body, signal }));
|
|
276
|
+
if (response.status === 401 || response.status === 403) throw new TransportError("auth_blocked", `HTTP ${response.status}`);
|
|
277
|
+
if (response.status === 429) throw new TransportError("quota_blocked", "HTTP 429");
|
|
278
|
+
if (response.status === 451) throw new TransportError("region_blocked", "HTTP 451");
|
|
279
|
+
if (response.status >= 500) throw new TransportError("provider_transient", `HTTP ${response.status}`);
|
|
280
|
+
const outputBytes = new TextEncoder().encode(response.body).byteLength;
|
|
281
|
+
enforceSandboxLimits(state, limits, { outputBytes, outputTokens: Math.ceil(outputBytes / 4) });
|
|
282
|
+
observation = normalizeTransportObservation(routeContext, response);
|
|
283
|
+
} else {
|
|
284
|
+
throw new TransportError("live_transport_required", activeCase.initiatingRequest ? "live transport or trusted route executor required" : "trusted route executor required");
|
|
285
|
+
}
|
|
286
|
+
const assertionResults = evaluateAssertions(activeCase.assertions, observation);
|
|
287
|
+
const requiredFailures = assertionResults.filter((row) => row.required && !row.passed);
|
|
288
|
+
if (activeCase.expectedFailure) {
|
|
289
|
+
const matched = activeCase.expectedFailure.assertionIds.every((id) => assertionResults.find((row) => row.id === id)?.passed === true) && requiredFailures.length === 0;
|
|
290
|
+
if (matched) {
|
|
291
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed: true, classification: activeCase.expectedFailure.expectedClass, secondaryCode: activeCase.expectedFailure.expectedCode, assertionResults, diagnostics, routeSubject });
|
|
292
|
+
}
|
|
293
|
+
const mismatch = classifyWithFailureRules(failureRules, "required_assertion_failed");
|
|
294
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed: false, classification: mismatch.classification, secondaryCode: mismatch.secondaryCode, assertionResults, diagnostics, routeSubject }, mismatch.retryPolicy);
|
|
295
|
+
}
|
|
296
|
+
const passed = requiredFailures.length === 0;
|
|
297
|
+
const classified = passed ? { classification: "inconclusive" as FailureClassification, secondaryCode: "pass", retryPolicy: null } : classifyWithFailureRules(failureRules, "required_assertion_failed");
|
|
298
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed, classification: passed ? "inconclusive" : classified.classification, secondaryCode: passed ? undefined : classified.secondaryCode, assertionResults, diagnostics, routeSubject }, passed ? null : classified.retryPolicy);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const diagnosticCode = error instanceof TransportError || error instanceof LabSandboxError || error instanceof LabDestinationError
|
|
301
|
+
? error.code
|
|
302
|
+
: "execution_error";
|
|
303
|
+
diagnostics.push(diagnosticCode);
|
|
304
|
+
let classified = classifyTransportError(error);
|
|
305
|
+
if (error instanceof LabSandboxError) classified = { classification: error.code === "harness_failure" ? "harness_failure" : "budget_exhausted", secondaryCode: error.code };
|
|
306
|
+
if (error instanceof LabDestinationError) classified = { classification: error.code === "network_blocked" ? "network_failure" : "harness_failure", secondaryCode: error.code };
|
|
307
|
+
const failureSignal = error instanceof TransportError || error instanceof LabSandboxError || error instanceof LabDestinationError ? error.code : "harness_failure";
|
|
308
|
+
const retryPolicy = failureRules.find((rule) => rule.match.includes(failureSignal))?.retry ?? null;
|
|
309
|
+
return complete({ scenarioId: activeCase.id, suite: activeCase.suite, passed: false, classification: classified.classification, secondaryCode: classified.secondaryCode, assertionResults: [], diagnostics, routeSubject, transportError: error instanceof TransportError ? error.code : undefined }, retryPolicy);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface InertToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: "function" | "custom";
|
|
4
|
+
description?: string;
|
|
5
|
+
parameters?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InertToolResult {
|
|
9
|
+
output: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LAB_TOOLS: Record<string, InertToolDefinition & { staticResult: InertToolResult }> = {
|
|
13
|
+
lookup: {
|
|
14
|
+
name: "lookup",
|
|
15
|
+
kind: "function",
|
|
16
|
+
description: "Lab inert lookup tool",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: { q: { type: "string" } },
|
|
20
|
+
required: ["q"],
|
|
21
|
+
},
|
|
22
|
+
staticResult: { output: "RESULT" },
|
|
23
|
+
},
|
|
24
|
+
apply_patch: {
|
|
25
|
+
name: "apply_patch",
|
|
26
|
+
kind: "custom",
|
|
27
|
+
description: "Lab inert patch tool",
|
|
28
|
+
staticResult: { output: "PATCHED" },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function listInertToolDefinitions(): InertToolDefinition[] {
|
|
33
|
+
return Object.values(LAB_TOOLS).map(({ staticResult: _r, ...tool }) => tool);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateInertToolArgs(name: string, args: unknown): void {
|
|
37
|
+
const tool = LAB_TOOLS[name];
|
|
38
|
+
if (!tool) throw new Error(`unknown inert tool: ${name}`);
|
|
39
|
+
if (tool.kind === "function") {
|
|
40
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
41
|
+
throw new Error("invalid function tool arguments");
|
|
42
|
+
}
|
|
43
|
+
const record = args as Record<string, unknown>;
|
|
44
|
+
if (typeof record.q !== "string") {
|
|
45
|
+
throw new Error("lookup requires string q");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Execute an inert lab tool — validate args and return static results only. */
|
|
51
|
+
export function executeInertTool(name: string, args: unknown): InertToolResult {
|
|
52
|
+
validateInertToolArgs(name, args);
|
|
53
|
+
const tool = LAB_TOOLS[name];
|
|
54
|
+
if (!tool) throw new Error(`unknown inert tool: ${name}`);
|
|
55
|
+
return { ...tool.staticResult };
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fixtureDigest } from "../digest";
|
|
5
|
+
import type { CaseAuthority, CaseRecord, FailureClassification, FailureRule } from "../conformance/types";
|
|
6
|
+
import { CL03_LIVE_SUITES, SYNTHETIC_MARKER } from "../conformance/types";
|
|
7
|
+
|
|
8
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const AUTHORITY_FILE = "024_live_v1_cases.json";
|
|
10
|
+
const RUNTIME_AUTHORITY = join(MODULE_DIR, "..", "conformance", "fixtures", "live-v1-cases.json");
|
|
11
|
+
const REPO_AUTHORITY = join(MODULE_DIR, "..", "..", "..", "devlog", "_plan", "260807_compatibility_lab", AUTHORITY_FILE);
|
|
12
|
+
|
|
13
|
+
function readAuthority(path: string): CaseAuthority { return JSON.parse(readFileSync(path, "utf8")) as CaseAuthority; }
|
|
14
|
+
|
|
15
|
+
/** During source-tree execution, fail closed unless the runtime copy is byte-identical to normative 024. */
|
|
16
|
+
export function assertLiveAuthorityMirror(): void {
|
|
17
|
+
if (!existsSync(REPO_AUTHORITY)) return;
|
|
18
|
+
const runtimeBytes = readFileSync(RUNTIME_AUTHORITY);
|
|
19
|
+
const normativeBytes = readFileSync(REPO_AUTHORITY);
|
|
20
|
+
if (!runtimeBytes.equals(normativeBytes)) throw new Error("harness_failure: live authority runtime copy drift");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadLiveCaseAuthority(): CaseAuthority {
|
|
24
|
+
assertLiveAuthorityMirror();
|
|
25
|
+
const raw = readAuthority(RUNTIME_AUTHORITY);
|
|
26
|
+
validateLiveAuthority(raw);
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function discoverLiveScenarios(authority: CaseAuthority, suites: readonly string[] = CL03_LIVE_SUITES): CaseRecord[] {
|
|
31
|
+
return authority.cases.filter((row) => suites.includes(row.suite));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function expandLiveScenario(caseRecord: CaseRecord, authority: CaseAuthority): Record<string, unknown> {
|
|
35
|
+
const defaults = authority.manifestDefaults;
|
|
36
|
+
const fixtures = caseRecord.initiatingRequest ? [fixtureRef(caseRecord.initiatingRequest, authority), fixtureRef(caseRecord.fixture, authority)] : [fixtureRef(caseRecord.fixture, authority)];
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: authority.schemaVersion, id: caseRecord.id, version: defaults.version,
|
|
39
|
+
suite: { id: caseRecord.suite, version: defaults.suiteVersion, evidenceLayer: defaults.evidenceLayer },
|
|
40
|
+
evidenceLayer: defaults.evidenceLayer, capability: caseRecord.capability,
|
|
41
|
+
verificationRole: caseRecord.verificationRole ?? defaults.verificationRole, requirements: caseRecord.requirements,
|
|
42
|
+
fixtures, executionLimits: defaults.executionLimits, executionMode: defaults.executionMode, assertions: caseRecord.assertions,
|
|
43
|
+
...(caseRecord.expectedFailure ? { expectedFailure: caseRecord.expectedFailure } : {}),
|
|
44
|
+
failureRules: expandLiveFailureRules(caseRecord, authority), artifactPolicy: defaults.artifactPolicy, freshness: defaults.freshness,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fixtureRef(fixture: CaseRecord["fixture"], authority: CaseAuthority): Record<string, unknown> {
|
|
49
|
+
const bytes = new TextEncoder().encode(fixture.bytesUtf8);
|
|
50
|
+
return { id: fixture.id, role: fixture.role, mediaType: fixture.mediaType, digest: fixture.digest, byteLength: bytes.byteLength,
|
|
51
|
+
syntheticMarker: SYNTHETIC_MARKER, provenance: { kind: "lab_authored", authority: AUTHORITY_FILE, sourceCommit: authority.sourceCommit } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function expandLiveFailureRules(caseRecord: CaseRecord, authority: CaseAuthority): FailureRule[] {
|
|
55
|
+
const setName = authority.manifestDefaults.failureRuleSet;
|
|
56
|
+
const ruleSet = authority.failureRuleSets[setName];
|
|
57
|
+
if (!Array.isArray(ruleSet)) throw new Error(`harness_failure: contract_integrity unknown failureRuleSet ${setName}`);
|
|
58
|
+
const base = [...ruleSet];
|
|
59
|
+
if (!caseRecord.expectedFailure) return base;
|
|
60
|
+
const template = authority.expectedFailureRuleTemplate;
|
|
61
|
+
const controlRule: FailureRule = { id: template.id, match: [...template.match], classification: caseRecord.expectedFailure.expectedClass as FailureClassification,
|
|
62
|
+
secondaryCode: caseRecord.expectedFailure.expectedCode, verdictEffect: caseRecord.expectedFailure.onMatch === "unsupported" ? "unsupported" : "none",
|
|
63
|
+
retry: template.retry, expected: template.expected };
|
|
64
|
+
const idx = base.findIndex((row) => row.id === "required-assertion");
|
|
65
|
+
if (idx >= 0) base.splice(idx, 0, controlRule); else base.push(controlRule);
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateLiveAuthority(authority: CaseAuthority): void {
|
|
70
|
+
if (authority.schemaVersion !== 1 || authority.manifestDefaults.evidenceLayer !== "live_route_compatibility") throw new Error("invalid live authority");
|
|
71
|
+
if (!authority.sourceCommit || !Array.isArray(authority.cases) || authority.cases.length !== 10) throw new Error("invalid live authority case set");
|
|
72
|
+
const ids = new Set<string>();
|
|
73
|
+
for (const caseRecord of authority.cases) {
|
|
74
|
+
if (ids.has(caseRecord.id)) throw new Error(`duplicate live scenario ${caseRecord.id}`);
|
|
75
|
+
ids.add(caseRecord.id);
|
|
76
|
+
if (!caseRecord.id.split(".").includes("live")) throw new Error(`non-live scenario in CL-03 authority: ${caseRecord.id}`);
|
|
77
|
+
const bytes = new TextEncoder().encode(caseRecord.fixture.bytesUtf8);
|
|
78
|
+
if (fixtureDigest(bytes) !== caseRecord.fixture.digest) throw new Error(`${caseRecord.id}: fixture digest mismatch`);
|
|
79
|
+
if (caseRecord.initiatingRequest) {
|
|
80
|
+
const initBytes = new TextEncoder().encode(caseRecord.initiatingRequest.bytesUtf8);
|
|
81
|
+
if (fixtureDigest(initBytes) !== caseRecord.initiatingRequest.digest) throw new Error(`${caseRecord.id}: initiatingRequest digest mismatch`);
|
|
82
|
+
}
|
|
83
|
+
expandLiveScenario(caseRecord, authority);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface McpStubTool {
|
|
2
|
+
namespace: string;
|
|
3
|
+
name: string;
|
|
4
|
+
inputSchema?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface McpStubCall {
|
|
8
|
+
namespace: string;
|
|
9
|
+
name: string;
|
|
10
|
+
arguments: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface McpStubResult {
|
|
14
|
+
content: Array<{ type: "text"; text: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tools = new Map<string, McpStubTool>();
|
|
18
|
+
const invocations: McpStubCall[] = [];
|
|
19
|
+
|
|
20
|
+
function toolKey(namespace: string, name: string): string {
|
|
21
|
+
return `${namespace}::${name}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Register a lab-owned MCP stub tool (in-memory only). */
|
|
25
|
+
export function registerMcpStubTool(tool: McpStubTool): void {
|
|
26
|
+
tools.set(toolKey(tool.namespace, tool.name), { ...tool });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearMcpStub(): void {
|
|
30
|
+
tools.clear();
|
|
31
|
+
invocations.length = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function listMcpStubTools(): McpStubTool[] {
|
|
35
|
+
return [...tools.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Invoke the in-memory MCP stub with fixed pure results. */
|
|
39
|
+
export function invokeMcpStub(
|
|
40
|
+
namespace: string,
|
|
41
|
+
name: string,
|
|
42
|
+
args: Record<string, unknown>,
|
|
43
|
+
result: McpStubResult,
|
|
44
|
+
): McpStubResult {
|
|
45
|
+
const key = toolKey(namespace, name);
|
|
46
|
+
if (!tools.has(key)) throw new Error(`MCP stub tool not registered: ${namespace}/${name}`);
|
|
47
|
+
invocations.push({ namespace, name, arguments: { ...args } });
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function mcpStubInvocations(): readonly McpStubCall[] {
|
|
52
|
+
return invocations;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function wireName(namespace: string, name: string): string {
|
|
56
|
+
return `mcp__${namespace}__${name}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CL03_LIVE_SUITES } from "../conformance/types";
|
|
2
|
+
import { isLiveCaseApplicableToRoute, runLiveScenario, type LiveExecutorOptions } from "./executor";
|
|
3
|
+
import { discoverLiveScenarios, loadLiveCaseAuthority } from "./manifest";
|
|
4
|
+
import type { LabRouteContext, LiveScenarioRunResult } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface LiveRunSummary { total: number; passed: number; failed: number; results: LiveScenarioRunResult[] }
|
|
7
|
+
|
|
8
|
+
export async function runLiveSuite(routeContext: LabRouteContext, suites: readonly string[] = CL03_LIVE_SUITES, opts: LiveExecutorOptions = {}): Promise<LiveRunSummary> {
|
|
9
|
+
const authority = loadLiveCaseAuthority();
|
|
10
|
+
const scenarios = discoverLiveScenarios(authority, suites).filter((scenario) => isLiveCaseApplicableToRoute(scenario, routeContext));
|
|
11
|
+
const results: LiveScenarioRunResult[] = [];
|
|
12
|
+
for (const scenario of scenarios) results.push(await runLiveScenario(scenario, routeContext, opts));
|
|
13
|
+
const passed = results.filter((row) => row.passed).length;
|
|
14
|
+
return { total: results.length, passed, failed: results.length - passed, results };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function listLiveScenarioIds(suites: readonly string[] = CL03_LIVE_SUITES): string[] {
|
|
18
|
+
return discoverLiveScenarios(loadLiveCaseAuthority(), suites).map((scenario) => scenario.id);
|
|
19
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LiveRunConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
const PROXY_ENV_VARS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"] as const;
|
|
4
|
+
const ALLOWED_ENV = { TZ: "UTC", NO_COLOR: "1" } as const;
|
|
5
|
+
|
|
6
|
+
export class LabSandboxError extends Error {
|
|
7
|
+
override readonly name = "LabSandboxError";
|
|
8
|
+
constructor(message: string, readonly code: string) { super(message); }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function rejectProxyEnvironment(env: NodeJS.ProcessEnv = process.env): void {
|
|
12
|
+
for (const name of PROXY_ENV_VARS) {
|
|
13
|
+
const value = env[name];
|
|
14
|
+
if (value !== undefined && value !== "") throw new LabSandboxError(`proxy environment variable ${name} is forbidden`, "harness_failure");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function labSandboxEnvironment(): Readonly<Record<string, string>> { return Object.freeze({ ...ALLOWED_ENV }); }
|
|
19
|
+
|
|
20
|
+
export interface SandboxResourceState {
|
|
21
|
+
requests: number;
|
|
22
|
+
inputBytes: number;
|
|
23
|
+
outputBytes: number;
|
|
24
|
+
outputTokens: number;
|
|
25
|
+
toolCalls: number;
|
|
26
|
+
artifacts: number;
|
|
27
|
+
artifactBytes: number;
|
|
28
|
+
childProcesses: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSandboxResourceState(): SandboxResourceState {
|
|
32
|
+
return { requests: 0, inputBytes: 0, outputBytes: 0, outputTokens: 0, toolCalls: 0, artifacts: 0, artifactBytes: 0, childProcesses: 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Counter-based ceilings enforce the resources observable in this process. The hard resident-memory
|
|
37
|
+
* boundary is NOT inferred from the hosting Bun test/server process RSS; a trusted exact-route
|
|
38
|
+
* executor must attest/enforce that isolated boundary before its result is evidence-eligible.
|
|
39
|
+
*/
|
|
40
|
+
export function enforceSandboxLimits(state: SandboxResourceState, limits: LiveRunConfig, delta: Partial<SandboxResourceState> = {}): void {
|
|
41
|
+
const next: SandboxResourceState = {
|
|
42
|
+
requests: state.requests + (delta.requests ?? 0), inputBytes: state.inputBytes + (delta.inputBytes ?? 0),
|
|
43
|
+
outputBytes: state.outputBytes + (delta.outputBytes ?? 0), outputTokens: state.outputTokens + (delta.outputTokens ?? 0),
|
|
44
|
+
toolCalls: state.toolCalls + (delta.toolCalls ?? 0), artifacts: state.artifacts + (delta.artifacts ?? 0),
|
|
45
|
+
artifactBytes: state.artifactBytes + (delta.artifactBytes ?? 0), childProcesses: state.childProcesses + (delta.childProcesses ?? 0),
|
|
46
|
+
};
|
|
47
|
+
if (next.childProcesses > limits.maxChildProcesses) throw new LabSandboxError("child process limit exceeded", "child_process_limit");
|
|
48
|
+
if (next.requests > limits.maxRequests) throw new LabSandboxError("request limit exceeded", "request_limit");
|
|
49
|
+
if (next.inputBytes > limits.maxInputBytes) throw new LabSandboxError("input byte limit exceeded", "input_byte_limit");
|
|
50
|
+
if (next.outputBytes > limits.maxOutputBytes) throw new LabSandboxError("output byte limit exceeded", "output_byte_limit");
|
|
51
|
+
if (next.outputTokens > limits.maxOutputTokens) throw new LabSandboxError("output token limit exceeded", "output_token_limit");
|
|
52
|
+
if (next.toolCalls > limits.maxToolCalls) throw new LabSandboxError("tool call limit exceeded", "tool_call_limit");
|
|
53
|
+
if (next.artifacts > limits.maxArtifacts) throw new LabSandboxError("artifact limit exceeded", "artifact_byte_limit");
|
|
54
|
+
if (next.artifactBytes > limits.aggregateArtifactBytes) throw new LabSandboxError("aggregate artifact limit exceeded", "artifact_byte_limit");
|
|
55
|
+
Object.assign(state, next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function prepareLiveSandbox(env: NodeJS.ProcessEnv = process.env): Readonly<Record<string, string>> {
|
|
59
|
+
rejectProxyEnvironment(env);
|
|
60
|
+
return labSandboxEnvironment();
|
|
61
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { scenarioManifestDigest, suiteManifestDigest } from "../digest";
|
|
2
|
+
import type { CaseAuthority, CaseRecord, VerificationRole } from "../conformance/types";
|
|
3
|
+
import { expandLiveScenario } from "./manifest";
|
|
4
|
+
|
|
5
|
+
export interface LiveSuiteScenarioRefV1 { id: string; version: string; role: VerificationRole; manifestDigest: string }
|
|
6
|
+
export interface LiveSuiteManifestV1 {
|
|
7
|
+
schemaVersion: 1; id: string; version: string; evidenceLayer: string; capability: string;
|
|
8
|
+
assertionDslVersion: string; evidenceSchemaVersion: string; freshness: { maxAgeMs: number | null };
|
|
9
|
+
contradictionRule: string; scenarios: LiveSuiteScenarioRefV1[]; verificationRule: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function casesForSuiteManifest(suiteId: string, authority: CaseAuthority): { own: CaseRecord[]; referenced: CaseRecord[] } {
|
|
13
|
+
const own = authority.cases.filter((row) => row.suite === suiteId);
|
|
14
|
+
if (own.length === 0) throw new Error(`unknown live suite ${suiteId}`);
|
|
15
|
+
if (suiteId !== "codex-core") return { own, referenced: own };
|
|
16
|
+
const prerequisite = authority.cases.find((row) => row.id === "responses-core.live.basic-turn");
|
|
17
|
+
if (!prerequisite) throw new Error("harness_failure: codex live prerequisite missing");
|
|
18
|
+
return { own, referenced: [...own, prerequisite] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function expandLiveSuiteManifest(suiteId: string, authority: CaseAuthority): LiveSuiteManifestV1 {
|
|
22
|
+
const { own, referenced } = casesForSuiteManifest(suiteId, authority);
|
|
23
|
+
const defaults = authority.manifestDefaults;
|
|
24
|
+
const scenarios = referenced.map((caseRecord) => ({
|
|
25
|
+
id: caseRecord.id, version: String(defaults.version), role: caseRecord.verificationRole ?? defaults.verificationRole,
|
|
26
|
+
manifestDigest: scenarioManifestDigest(expandLiveScenario(caseRecord, authority)),
|
|
27
|
+
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
28
|
+
return {
|
|
29
|
+
schemaVersion: 1, id: suiteId, version: String(defaults.suiteVersion), evidenceLayer: defaults.evidenceLayer,
|
|
30
|
+
capability: own[0]!.capability, assertionDslVersion: authority.assertionDslVersion, evidenceSchemaVersion: authority.evidenceSchemaVersion,
|
|
31
|
+
freshness: defaults.freshness ?? { maxAgeMs: null }, contradictionRule: "newest-required-observation-v1", scenarios,
|
|
32
|
+
verificationRule: "all-applicable-required-pass-v1",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function liveSuiteManifestObjectForCase(caseRecord: CaseRecord, authority: CaseAuthority): Record<string, unknown> {
|
|
37
|
+
return expandLiveSuiteManifest(caseRecord.suite, authority) as unknown as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export function liveSuiteManifestDigestForCase(caseRecord: CaseRecord, authority: CaseAuthority): string {
|
|
40
|
+
return suiteManifestDigest(expandLiveSuiteManifest(caseRecord.suite, authority) as unknown as Record<string, unknown>);
|
|
41
|
+
}
|