@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,355 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CaseRecord,
|
|
3
|
+
NormalizedEvent,
|
|
4
|
+
NormalizedObservation,
|
|
5
|
+
ToolCallProjection,
|
|
6
|
+
} from "./types";
|
|
7
|
+
import { normalizeSseBytes } from "./sse-normalize";
|
|
8
|
+
|
|
9
|
+
export function emptyObservation(): NormalizedObservation {
|
|
10
|
+
return {
|
|
11
|
+
client: {
|
|
12
|
+
request: { status: 0, headers: {}, json: null, rawBytes: 0 },
|
|
13
|
+
response: {
|
|
14
|
+
status: 0,
|
|
15
|
+
headers: {},
|
|
16
|
+
json: null,
|
|
17
|
+
events: [],
|
|
18
|
+
toolCalls: [],
|
|
19
|
+
mcpCalls: [],
|
|
20
|
+
terminal: null,
|
|
21
|
+
normalizedText: "",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
upstream: { requests: [], responses: [] },
|
|
25
|
+
process: { exitCode: null },
|
|
26
|
+
verifiers: {},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function recordUpstreamRequest(
|
|
31
|
+
observation: NormalizedObservation,
|
|
32
|
+
json: unknown,
|
|
33
|
+
status = 0,
|
|
34
|
+
): void {
|
|
35
|
+
const body = JSON.stringify(json ?? null);
|
|
36
|
+
observation.upstream.requests.push({
|
|
37
|
+
status,
|
|
38
|
+
headers: {},
|
|
39
|
+
json,
|
|
40
|
+
rawBytes: new TextEncoder().encode(body).byteLength,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setClientResponse(
|
|
45
|
+
observation: NormalizedObservation,
|
|
46
|
+
patch: Partial<NormalizedObservation["client"]["response"]>,
|
|
47
|
+
): void {
|
|
48
|
+
observation.client.response = { ...observation.client.response, ...patch };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseToolArguments(raw: unknown, kind: "function" | "custom"): unknown {
|
|
52
|
+
if (kind === "custom") return typeof raw === "string" ? raw : "";
|
|
53
|
+
if (typeof raw === "string") {
|
|
54
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ToolCallProjectionResult {
|
|
60
|
+
calls: ToolCallProjection[];
|
|
61
|
+
sawCallItems: boolean;
|
|
62
|
+
duplicateIds: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function projectToolCallsDetailed(output: unknown[]): ToolCallProjectionResult {
|
|
66
|
+
const calls: ToolCallProjection[] = [];
|
|
67
|
+
let ordinal = 0;
|
|
68
|
+
let sawCallItems = false;
|
|
69
|
+
for (const item of output) {
|
|
70
|
+
if (!item || typeof item !== "object") continue;
|
|
71
|
+
const rec = item as Record<string, unknown>;
|
|
72
|
+
if (rec.type === "function_call") {
|
|
73
|
+
sawCallItems = true;
|
|
74
|
+
const id = rec.call_id ?? rec.id;
|
|
75
|
+
const name = rec.name;
|
|
76
|
+
if (typeof id !== "string" || id.length === 0 || typeof name !== "string" || name.length === 0) continue;
|
|
77
|
+
const args = parseToolArguments(rec.arguments, "function");
|
|
78
|
+
if (args === null) continue;
|
|
79
|
+
calls.push({ id, name, arguments: args, kind: "function", ordinal: ordinal++ });
|
|
80
|
+
} else if (rec.type === "custom_tool_call") {
|
|
81
|
+
sawCallItems = true;
|
|
82
|
+
const id = rec.call_id ?? rec.id;
|
|
83
|
+
const name = rec.name;
|
|
84
|
+
if (typeof id !== "string" || id.length === 0 || typeof name !== "string" || name.length === 0) continue;
|
|
85
|
+
calls.push({
|
|
86
|
+
id,
|
|
87
|
+
name,
|
|
88
|
+
arguments: parseToolArguments(rec.input, "custom"),
|
|
89
|
+
kind: "custom",
|
|
90
|
+
ordinal: ordinal++,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ids = calls.map((call) => call.id);
|
|
95
|
+
const duplicateIds = new Set(ids).size !== ids.length;
|
|
96
|
+
return { calls: duplicateIds ? [] : calls, sawCallItems, duplicateIds };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Build toolCalls projection from Responses output items or SSE events (manifest §5). */
|
|
100
|
+
export function projectToolCallsFromOutput(output: unknown[]): ToolCallProjection[] {
|
|
101
|
+
return projectToolCallsDetailed(output).calls;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function projectToolCallsFromEventsDetailed(events: NormalizedEvent[]): ToolCallProjectionResult {
|
|
105
|
+
const output: unknown[] = [];
|
|
106
|
+
for (const ev of events) {
|
|
107
|
+
if (ev.event === "response.output_item.done" && ev.data && typeof ev.data === "object") {
|
|
108
|
+
const data = ev.data as Record<string, unknown>;
|
|
109
|
+
const item = data.item;
|
|
110
|
+
if (item && typeof item === "object") output.push(item);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return projectToolCallsDetailed(output);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function projectToolCallsFromEvents(events: NormalizedEvent[]): ToolCallProjection[] {
|
|
117
|
+
return projectToolCallsFromEventsDetailed(events).calls;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function projectMcpCalls(toolCalls: ToolCallProjection[]): Array<{ namespace: string; name: string }> {
|
|
121
|
+
const out: Array<{ namespace: string; name: string }> = [];
|
|
122
|
+
for (const call of toolCalls) {
|
|
123
|
+
if (!call.name.startsWith("mcp__")) continue;
|
|
124
|
+
const idx = call.name.lastIndexOf("__");
|
|
125
|
+
if (idx <= 0 || idx >= call.name.length - 2) continue;
|
|
126
|
+
const namespace = call.name.slice(0, idx);
|
|
127
|
+
const name = call.name.slice(idx + 2);
|
|
128
|
+
if (!namespace || !name) continue;
|
|
129
|
+
if (new TextEncoder().encode(namespace).byteLength > 64 || new TextEncoder().encode(name).byteLength > 64) continue;
|
|
130
|
+
out.push({ namespace, name });
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function filterAnthropicEvents(events: ReturnType<typeof normalizeSseBytes>): ReturnType<typeof normalizeSseBytes> {
|
|
136
|
+
return events.filter((e) => e.event !== "ping");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deriveTerminal(events: NormalizedEvent[]): string | null {
|
|
140
|
+
if (events.some((e) => e.event === "error")) return "failed";
|
|
141
|
+
if (events.some((e) => e.event === "response.failed")) return "failed";
|
|
142
|
+
if (events.some((e) => e.event === "response.completed")) return "completed";
|
|
143
|
+
if (events.some((e) => e.event === "message_stop")) return "message_stop";
|
|
144
|
+
if (events.some((e) => e.event === "response.incomplete")) return "incomplete";
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractOutputText(json: Record<string, unknown>): string {
|
|
149
|
+
let text = "";
|
|
150
|
+
const output = json.output;
|
|
151
|
+
if (!Array.isArray(output)) return text;
|
|
152
|
+
for (const item of output) {
|
|
153
|
+
if (!item || typeof item !== "object") continue;
|
|
154
|
+
const content = (item as { content?: unknown[] }).content;
|
|
155
|
+
if (!Array.isArray(content)) continue;
|
|
156
|
+
for (const part of content) {
|
|
157
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "output_text") {
|
|
158
|
+
text += String((part as { text?: string }).text ?? "");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return text;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function deriveNormalizedText(events: NormalizedEvent[], json: unknown): string {
|
|
166
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
167
|
+
const text = extractOutputText(json as Record<string, unknown>);
|
|
168
|
+
if (text) return text;
|
|
169
|
+
}
|
|
170
|
+
let text = "";
|
|
171
|
+
for (const ev of events) {
|
|
172
|
+
if (ev.event === "response.output_text.delta" && ev.data && typeof ev.data === "object") {
|
|
173
|
+
text += String((ev.data as { delta?: string }).delta ?? "");
|
|
174
|
+
}
|
|
175
|
+
if (ev.event === "content_block_delta" && ev.data && typeof ev.data === "object") {
|
|
176
|
+
const delta = (ev.data as { delta?: { text?: string } }).delta;
|
|
177
|
+
if (delta && typeof delta.text === "string") text += delta.text;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return text;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function finalizeObservation(
|
|
184
|
+
observation: NormalizedObservation,
|
|
185
|
+
events: NormalizedEvent[],
|
|
186
|
+
json: unknown = null,
|
|
187
|
+
status = 200,
|
|
188
|
+
): void {
|
|
189
|
+
const eventProjection = projectToolCallsFromEventsDetailed(events);
|
|
190
|
+
const jsonProjection = projectToolCallsDetailed(
|
|
191
|
+
json && typeof json === "object" && !Array.isArray(json)
|
|
192
|
+
? ((json as { output?: unknown[] }).output ?? [])
|
|
193
|
+
: [],
|
|
194
|
+
);
|
|
195
|
+
const selectedProjection = eventProjection.sawCallItems ? eventProjection : jsonProjection;
|
|
196
|
+
const resolvedToolCalls = selectedProjection.calls;
|
|
197
|
+
const terminal = deriveTerminal(events);
|
|
198
|
+
setClientResponse(observation, {
|
|
199
|
+
events,
|
|
200
|
+
toolCalls: resolvedToolCalls,
|
|
201
|
+
mcpCalls: projectMcpCalls(resolvedToolCalls),
|
|
202
|
+
terminal,
|
|
203
|
+
normalizedText: deriveNormalizedText(events, json),
|
|
204
|
+
json,
|
|
205
|
+
status,
|
|
206
|
+
});
|
|
207
|
+
observation.verifiers.duplicate_tool_call_ids = selectedProjection.duplicateIds;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function attachVerifiers(observation: NormalizedObservation, caseRecord: CaseRecord): void {
|
|
211
|
+
observation.verifiers = { ...observation.verifiers, ...buildVerifiers(observation, caseRecord) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildVerifiers(observation: NormalizedObservation, caseRecord: CaseRecord): Record<string, unknown> {
|
|
215
|
+
const verifiers: Record<string, unknown> = {};
|
|
216
|
+
const toolCalls = observation.client.response.toolCalls;
|
|
217
|
+
|
|
218
|
+
verifiers.nonoverlap_order = (() => {
|
|
219
|
+
const ids: string[] = [];
|
|
220
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
221
|
+
const call = toolCalls[i];
|
|
222
|
+
if (!call.id || call.arguments === null) return [];
|
|
223
|
+
if (call.ordinal !== i) return [];
|
|
224
|
+
ids.push(call.id);
|
|
225
|
+
}
|
|
226
|
+
const unique = new Set(ids);
|
|
227
|
+
return unique.size === ids.length ? ids : [];
|
|
228
|
+
})();
|
|
229
|
+
|
|
230
|
+
verifiers.call_result_order = evaluateCallResultOrder(observation);
|
|
231
|
+
|
|
232
|
+
if (caseRecord.id === "codex-core.protocol.compaction-and-special-items") {
|
|
233
|
+
verifiers.compaction_replayed = evaluateCompactionReplayed(caseRecord);
|
|
234
|
+
verifiers.local_shell_correlated = evaluateLocalShellCorrelated(caseRecord);
|
|
235
|
+
verifiers.tool_search_error = evaluateToolSearchError(caseRecord);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (caseRecord.id === "responses-core.protocol.json-sse-equivalence") {
|
|
239
|
+
verifiers.json_sse_equivalence = evaluateJsonSseEquivalence(caseRecord);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (caseRecord.id === "vision-core.protocol.modality-gate") {
|
|
243
|
+
verifiers.modality_path = evaluateModalityPath(caseRecord);
|
|
244
|
+
verifiers.silent_image_drop = evaluateSilentImageDrop(caseRecord);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return verifiers;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function evaluateCallResultOrder(observation: NormalizedObservation): string {
|
|
251
|
+
const request = observation.upstream.requests[0]?.json as { input?: unknown[] } | undefined;
|
|
252
|
+
const input = request?.input;
|
|
253
|
+
if (!Array.isArray(input)) return "fail";
|
|
254
|
+
const pendingCallIds = new Set<string>();
|
|
255
|
+
let callCount = 0;
|
|
256
|
+
let resultCount = 0;
|
|
257
|
+
for (const item of input) {
|
|
258
|
+
if (!item || typeof item !== "object") continue;
|
|
259
|
+
const record = item as { type?: string; call_id?: unknown };
|
|
260
|
+
if (record.type === "function_call") {
|
|
261
|
+
if (typeof record.call_id !== "string" || record.call_id.length === 0) return "fail";
|
|
262
|
+
if (pendingCallIds.has(record.call_id)) return "fail";
|
|
263
|
+
pendingCallIds.add(record.call_id);
|
|
264
|
+
callCount++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (record.type === "function_call_output") {
|
|
268
|
+
if (typeof record.call_id !== "string" || record.call_id.length === 0) return "fail";
|
|
269
|
+
if (!pendingCallIds.delete(record.call_id)) return "fail";
|
|
270
|
+
resultCount++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return callCount > 0 && resultCount === callCount && pendingCallIds.size === 0 ? "pass" : "fail";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function evaluateCompactionReplayed(caseRecord: CaseRecord): boolean {
|
|
277
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as { input?: unknown[] };
|
|
278
|
+
const input = vector.input;
|
|
279
|
+
if (!Array.isArray(input)) return false;
|
|
280
|
+
const compaction = input.find((i) => i && typeof i === "object" && (i as { type?: string }).type === "context_compaction");
|
|
281
|
+
if (!compaction) return false;
|
|
282
|
+
return typeof (compaction as { encrypted_content?: string }).encrypted_content === "string";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function evaluateLocalShellCorrelated(caseRecord: CaseRecord): boolean {
|
|
286
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as { input?: unknown[] };
|
|
287
|
+
const input = vector.input;
|
|
288
|
+
if (!Array.isArray(input)) return false;
|
|
289
|
+
let shellId: string | undefined;
|
|
290
|
+
for (const item of input) {
|
|
291
|
+
if (!item || typeof item !== "object") continue;
|
|
292
|
+
const type = (item as { type?: string }).type;
|
|
293
|
+
if (type === "local_shell_call") {
|
|
294
|
+
shellId = String((item as { call_id?: string }).call_id ?? "");
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (type === "function_call_output" && shellId) {
|
|
298
|
+
return String((item as { call_id?: string }).call_id ?? "") === shellId;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function evaluateToolSearchError(caseRecord: CaseRecord): string | null {
|
|
305
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as { input?: unknown[] };
|
|
306
|
+
const input = vector.input;
|
|
307
|
+
if (!Array.isArray(input)) return null;
|
|
308
|
+
const failed = input.filter((i) => i && typeof i === "object" && (i as { type?: string }).type === "tool_search_output"
|
|
309
|
+
&& (i as { status?: string }).status === "failed");
|
|
310
|
+
if (failed.length !== 1) return null;
|
|
311
|
+
return String((failed[0] as { error?: string }).error ?? "");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function evaluateModalityPath(caseRecord: CaseRecord): string {
|
|
315
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as Record<string, unknown>;
|
|
316
|
+
const requestHasImage = Boolean(vector.requestHasImage);
|
|
317
|
+
const modalities = vector.modelInputModalities as string[] | undefined;
|
|
318
|
+
const sidecar = vector.visionSidecar as { enabled?: boolean } | undefined;
|
|
319
|
+
if (requestHasImage && Array.isArray(modalities) && modalities.includes("image")) return "native";
|
|
320
|
+
if (sidecar?.enabled) return "sidecar";
|
|
321
|
+
return "unsupported";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function evaluateSilentImageDrop(caseRecord: CaseRecord): boolean {
|
|
325
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as Record<string, unknown>;
|
|
326
|
+
if (!Boolean(vector.requestHasImage)) return false;
|
|
327
|
+
// Protocol V1's closed modality-gate vector treats the explicit `unsupported` path as a
|
|
328
|
+
// typed rejection, not as an omitted image. Native/sidecar paths likewise preserve it.
|
|
329
|
+
return !["native", "sidecar", "unsupported"].includes(evaluateModalityPath(caseRecord));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function evaluateJsonSseEquivalence(caseRecord: CaseRecord): string {
|
|
333
|
+
const vector = JSON.parse(caseRecord.fixture.bytesUtf8) as { json?: Record<string, unknown>; sse?: string };
|
|
334
|
+
const json = vector.json;
|
|
335
|
+
const sse = vector.sse ?? "";
|
|
336
|
+
if (!json) return "fail";
|
|
337
|
+
const jsonProjection = {
|
|
338
|
+
text: extractOutputText(json),
|
|
339
|
+
terminal: String(json.status ?? ""),
|
|
340
|
+
};
|
|
341
|
+
const events = normalizeSseBytes(new TextEncoder().encode(sse), "openai-responses");
|
|
342
|
+
let sseText = "";
|
|
343
|
+
let sseTerminal = "";
|
|
344
|
+
for (const ev of events) {
|
|
345
|
+
if (ev.event === "response.output_text.delta" && ev.data && typeof ev.data === "object") {
|
|
346
|
+
sseText += String((ev.data as { delta?: string }).delta ?? "");
|
|
347
|
+
}
|
|
348
|
+
if (ev.event === "response.completed" && ev.data && typeof ev.data === "object") {
|
|
349
|
+
const response = (ev.data as { response?: { status?: string } }).response;
|
|
350
|
+
sseTerminal = String(response?.status ?? "");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const sseProjection = { text: sseText, terminal: sseTerminal };
|
|
354
|
+
return JSON.stringify(jsonProjection) === JSON.stringify(sseProjection) ? "pass" : "fail";
|
|
355
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { discoverScenarios, loadCaseAuthority } from "./manifest";
|
|
2
|
+
import { runScenario } from "./executor";
|
|
3
|
+
import { buildNegativeControls } from "./negative-controls";
|
|
4
|
+
import type { ScenarioRunResult } from "./types";
|
|
5
|
+
import { CL01_SUITES } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface ConformanceRunSummary {
|
|
8
|
+
/** Number of scenarios executed. */
|
|
9
|
+
total: number;
|
|
10
|
+
/** Number of scenarios that met their expected outcome. */
|
|
11
|
+
passed: number;
|
|
12
|
+
/** Number of scenarios that did not meet their expected outcome. */
|
|
13
|
+
failed: number;
|
|
14
|
+
results: ScenarioRunResult[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NegativeControlRunSummary extends ConformanceRunSummary {
|
|
18
|
+
/** Number of deliberately defective controls correctly rejected by the harness. */
|
|
19
|
+
rejected: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runConformanceSuite(
|
|
23
|
+
suites: readonly string[] = CL01_SUITES,
|
|
24
|
+
): Promise<ConformanceRunSummary> {
|
|
25
|
+
const authority = loadCaseAuthority();
|
|
26
|
+
const scenarios = discoverScenarios(authority, suites);
|
|
27
|
+
if (scenarios.length === 0) throw new Error("harness_failure: no CL-01 scenarios discovered");
|
|
28
|
+
const results: ScenarioRunResult[] = [];
|
|
29
|
+
for (const scenario of scenarios) {
|
|
30
|
+
results.push(await runScenario(scenario));
|
|
31
|
+
}
|
|
32
|
+
const passed = results.filter((r) => r.passed).length;
|
|
33
|
+
return { total: results.length, passed, failed: results.length - passed, results };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runNegativeControls(): Promise<NegativeControlRunSummary> {
|
|
37
|
+
const authority = loadCaseAuthority();
|
|
38
|
+
const scenarios = buildNegativeControls(discoverScenarios(authority));
|
|
39
|
+
if (scenarios.length === 0) throw new Error("harness_failure: no negative controls discovered");
|
|
40
|
+
const results: ScenarioRunResult[] = [];
|
|
41
|
+
for (const scenario of scenarios) {
|
|
42
|
+
results.push(await runScenario(scenario));
|
|
43
|
+
}
|
|
44
|
+
const rejected = results.filter((r) => !r.passed).length;
|
|
45
|
+
return {
|
|
46
|
+
total: results.length,
|
|
47
|
+
passed: rejected,
|
|
48
|
+
rejected,
|
|
49
|
+
failed: results.length - rejected,
|
|
50
|
+
results,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listScenarioIds(suites: readonly string[] = CL01_SUITES): string[] {
|
|
55
|
+
const authority = loadCaseAuthority();
|
|
56
|
+
return discoverScenarios(authority, suites).map((s) => s.id);
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { sseFieldValue } from "../../lib/sse-decoder";
|
|
2
|
+
import type { NormalizedEvent } from "./types";
|
|
3
|
+
|
|
4
|
+
/** CL-00 §5 SSE normalization for assertion observations. */
|
|
5
|
+
export function normalizeSseBytes(bytes: Uint8Array, sourceProtocol: string): NormalizedEvent[] {
|
|
6
|
+
let text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
7
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
8
|
+
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
9
|
+
|
|
10
|
+
const events: NormalizedEvent[] = [];
|
|
11
|
+
let ordinal = 0;
|
|
12
|
+
const frames = text.split("\n\n");
|
|
13
|
+
for (const rawFrame of frames) {
|
|
14
|
+
if (!rawFrame.trim()) continue;
|
|
15
|
+
const lines = rawFrame.split("\n");
|
|
16
|
+
const dataLines: string[] = [];
|
|
17
|
+
let eventName: string | undefined;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
if (line.startsWith(":")) continue;
|
|
20
|
+
const eventValue = sseFieldValue(line, "event");
|
|
21
|
+
if (eventValue !== null) {
|
|
22
|
+
eventName = eventValue;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const dataValue = sseFieldValue(line, "data");
|
|
26
|
+
if (dataValue !== null) dataLines.push(dataValue);
|
|
27
|
+
}
|
|
28
|
+
if (dataLines.length === 0) continue;
|
|
29
|
+
const joined = dataLines.join("\n");
|
|
30
|
+
if (sourceProtocol === "openai-chat" && joined === "[DONE]") {
|
|
31
|
+
events.push({ event: "[DONE]", data: "[DONE]", ordinal: ordinal++ });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let parsed: unknown;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(joined);
|
|
37
|
+
} catch {
|
|
38
|
+
events.push({ event: eventName ?? "malformed", data: joined, ordinal: ordinal++ });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Protocol V1 treats null, scalar, and array data values as padding.
|
|
42
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
43
|
+
const inferred = eventName ?? (typeof (parsed as { type?: unknown }).type === "string"
|
|
44
|
+
? (parsed as { type: string }).type
|
|
45
|
+
: "message");
|
|
46
|
+
events.push({ event: inferred, data: parsed, ordinal: ordinal++ });
|
|
47
|
+
}
|
|
48
|
+
return events;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function eventsFromBridgeFrames(
|
|
52
|
+
frames: Array<{ event?: string; data: Record<string, unknown> }>,
|
|
53
|
+
): NormalizedEvent[] {
|
|
54
|
+
return frames.map((frame, ordinal) => ({
|
|
55
|
+
event: frame.event ?? (typeof frame.data.type === "string" ? frame.data.type : "message"),
|
|
56
|
+
data: frame.data,
|
|
57
|
+
ordinal,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { scenarioManifestDigest, suiteManifestDigest } from "../digest";
|
|
2
|
+
import type { CaseAuthority, CaseRecord, VerificationRole } from "./types";
|
|
3
|
+
import { expandScenario } from "./manifest";
|
|
4
|
+
|
|
5
|
+
export interface SuiteScenarioRefV1 {
|
|
6
|
+
id: string;
|
|
7
|
+
version: string;
|
|
8
|
+
role: VerificationRole;
|
|
9
|
+
manifestDigest: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SuiteManifestV1 {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
id: string;
|
|
15
|
+
version: string;
|
|
16
|
+
evidenceLayer: string;
|
|
17
|
+
capability: string;
|
|
18
|
+
assertionDslVersion: string;
|
|
19
|
+
evidenceSchemaVersion: string;
|
|
20
|
+
freshness: { maxAgeMs: number | null };
|
|
21
|
+
contradictionRule: string;
|
|
22
|
+
scenarios: SuiteScenarioRefV1[];
|
|
23
|
+
verificationRule: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Expand the canonical suite manifest for one suite ID from CL-01 authority. */
|
|
27
|
+
export function expandSuiteManifest(
|
|
28
|
+
suiteId: string,
|
|
29
|
+
authority: CaseAuthority,
|
|
30
|
+
): SuiteManifestV1 {
|
|
31
|
+
const cases = authority.cases.filter((c) => c.suite === suiteId);
|
|
32
|
+
if (cases.length === 0) {
|
|
33
|
+
throw new Error(`unknown suite ${suiteId}`);
|
|
34
|
+
}
|
|
35
|
+
const defaults = authority.manifestDefaults;
|
|
36
|
+
const capability = cases[0]!.capability;
|
|
37
|
+
if (cases.some((caseRecord) => caseRecord.capability !== capability)) {
|
|
38
|
+
throw new Error(`suite ${suiteId} declares mixed capabilities`);
|
|
39
|
+
}
|
|
40
|
+
const scenarios: SuiteScenarioRefV1[] = cases
|
|
41
|
+
.map((caseRecord) => suiteScenarioRef(caseRecord, authority))
|
|
42
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
id: suiteId,
|
|
47
|
+
version: String(defaults.suiteVersion),
|
|
48
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
49
|
+
capability,
|
|
50
|
+
assertionDslVersion: authority.assertionDslVersion,
|
|
51
|
+
evidenceSchemaVersion: authority.evidenceSchemaVersion,
|
|
52
|
+
freshness: defaults.freshness ?? { maxAgeMs: null },
|
|
53
|
+
contradictionRule: "newest-required-observation-v1",
|
|
54
|
+
scenarios,
|
|
55
|
+
verificationRule: "all-applicable-required-pass-v1",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function suiteScenarioRef(caseRecord: CaseRecord, authority: CaseAuthority): SuiteScenarioRefV1 {
|
|
60
|
+
const expanded = expandScenario(caseRecord, authority);
|
|
61
|
+
return {
|
|
62
|
+
id: caseRecord.id,
|
|
63
|
+
version: String(authority.manifestDefaults.version),
|
|
64
|
+
role: caseRecord.verificationRole ?? authority.manifestDefaults.verificationRole,
|
|
65
|
+
manifestDigest: scenarioManifestDigest(expanded),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function suiteManifestObjectForCase(
|
|
70
|
+
caseRecord: CaseRecord,
|
|
71
|
+
authority: CaseAuthority,
|
|
72
|
+
): Record<string, unknown> {
|
|
73
|
+
return expandSuiteManifest(caseRecord.suite, authority) as unknown as Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function suiteManifestDigestForCase(caseRecord: CaseRecord, authority: CaseAuthority): string {
|
|
77
|
+
return suiteManifestDigest(expandSuiteManifest(caseRecord.suite, authority) as unknown as Record<string, unknown>);
|
|
78
|
+
}
|