@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,24 @@
|
|
|
1
|
+
/** RFC 8785 JSON Canonicalization Scheme (JCS) for deterministic equality. */
|
|
2
|
+
|
|
3
|
+
export function jcsStringify(value: unknown): string {
|
|
4
|
+
if (value === undefined) throw new TypeError("jcsStringify: undefined is not representable in JCS");
|
|
5
|
+
if (value === null || typeof value === "boolean") return JSON.stringify(value);
|
|
6
|
+
if (typeof value === "number") {
|
|
7
|
+
if (!Number.isFinite(value)) throw new TypeError("jcsStringify: non-finite numbers are not representable in JCS");
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return `[${value.map(jcsStringify).join(",")}]`;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "object") {
|
|
15
|
+
const obj = value as Record<string, unknown>;
|
|
16
|
+
const keys = Object.keys(obj).sort();
|
|
17
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${jcsStringify(obj[k])}`).join(",")}}`;
|
|
18
|
+
}
|
|
19
|
+
throw new TypeError(`jcsStringify: unsupported value type ${typeof value}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function jcsEqual(a: unknown, b: unknown): boolean {
|
|
23
|
+
return jcsStringify(a) === jcsStringify(b);
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** RFC 6901 JSON Pointer resolution for assertion selectors. */
|
|
2
|
+
|
|
3
|
+
export type PointerResult =
|
|
4
|
+
| { ok: true; value: unknown }
|
|
5
|
+
| { ok: false; reason: "selector_missing" | "selector_type_mismatch" };
|
|
6
|
+
|
|
7
|
+
function decodeToken(token: string): string {
|
|
8
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveJsonPointer(root: unknown, pointer: string): PointerResult {
|
|
12
|
+
if (!pointer.startsWith("/")) return { ok: false, reason: "selector_missing" };
|
|
13
|
+
if (pointer === "/") return { ok: true, value: root };
|
|
14
|
+
const tokens = pointer.slice(1).split("/").map(decodeToken);
|
|
15
|
+
let current: unknown = root;
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
if (token === "-") return { ok: false, reason: "selector_missing" };
|
|
18
|
+
if (Array.isArray(current)) {
|
|
19
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) return { ok: false, reason: "selector_missing" };
|
|
20
|
+
const index = Number(token);
|
|
21
|
+
if (index >= current.length) return { ok: false, reason: "selector_missing" };
|
|
22
|
+
current = current[index];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (current === null || typeof current !== "object") {
|
|
26
|
+
return { ok: false, reason: "selector_missing" };
|
|
27
|
+
}
|
|
28
|
+
const obj = current as Record<string, unknown>;
|
|
29
|
+
if (!Object.prototype.hasOwnProperty.call(obj, token)) {
|
|
30
|
+
return { ok: false, reason: "selector_missing" };
|
|
31
|
+
}
|
|
32
|
+
current = obj[token];
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, value: current };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pointerExists(root: unknown, pointer: string): boolean {
|
|
38
|
+
return resolveJsonPointer(root, pointer).ok;
|
|
39
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fixtureDigest } from "./digest";
|
|
5
|
+
import { MCP_ACTION_TOKENS } from "./mcp-stub";
|
|
6
|
+
import type {
|
|
7
|
+
CaseAuthority,
|
|
8
|
+
CaseRecord,
|
|
9
|
+
FailureClassification,
|
|
10
|
+
FailureRule,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { CL01_SUITES, SYNTHETIC_MARKER } from "./types";
|
|
13
|
+
|
|
14
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
// Provenance is the normative CL-00 authority name, not the runtime copy's basename.
|
|
16
|
+
const AUTHORITY_FILE = "022_protocol_v1_cases.json";
|
|
17
|
+
|
|
18
|
+
export function loadCaseAuthority(): CaseAuthority {
|
|
19
|
+
const path = join(MODULE_DIR, "fixtures", "protocol-v1-cases.json");
|
|
20
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as CaseAuthority;
|
|
21
|
+
validateAuthority(raw);
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function discoverScenarios(
|
|
26
|
+
authority: CaseAuthority,
|
|
27
|
+
suites: readonly string[] = CL01_SUITES,
|
|
28
|
+
): CaseRecord[] {
|
|
29
|
+
return authority.cases.filter((c) => suites.includes(c.suite));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function expandScenario(caseRecord: CaseRecord, authority: CaseAuthority): Record<string, unknown> {
|
|
33
|
+
const defaults = authority.manifestDefaults;
|
|
34
|
+
const fixtures = caseRecord.initiatingRequest
|
|
35
|
+
? [fixtureRef(caseRecord.initiatingRequest, authority), fixtureRef(caseRecord.fixture, authority)]
|
|
36
|
+
: [fixtureRef(caseRecord.fixture, authority)];
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: authority.schemaVersion,
|
|
39
|
+
id: caseRecord.id,
|
|
40
|
+
version: defaults.version,
|
|
41
|
+
suite: {
|
|
42
|
+
id: caseRecord.suite,
|
|
43
|
+
version: defaults.suiteVersion,
|
|
44
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
45
|
+
},
|
|
46
|
+
evidenceLayer: defaults.evidenceLayer,
|
|
47
|
+
capability: caseRecord.capability,
|
|
48
|
+
verificationRole: caseRecord.verificationRole ?? defaults.verificationRole,
|
|
49
|
+
requirements: caseRecord.requirements,
|
|
50
|
+
fixtures,
|
|
51
|
+
executionLimits: defaults.executionLimits,
|
|
52
|
+
assertions: caseRecord.assertions,
|
|
53
|
+
...(caseRecord.expectedFailure ? { expectedFailure: caseRecord.expectedFailure } : {}),
|
|
54
|
+
failureRules: expandFailureRules(caseRecord, authority),
|
|
55
|
+
artifactPolicy: defaults.artifactPolicy,
|
|
56
|
+
freshness: defaults.freshness,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fixtureRef(fixture: CaseRecord["fixture"], authority: CaseAuthority): Record<string, unknown> {
|
|
61
|
+
const bytes = new TextEncoder().encode(fixture.bytesUtf8);
|
|
62
|
+
return {
|
|
63
|
+
id: fixture.id,
|
|
64
|
+
role: fixture.role,
|
|
65
|
+
mediaType: fixture.mediaType,
|
|
66
|
+
digest: fixture.digest,
|
|
67
|
+
byteLength: bytes.byteLength,
|
|
68
|
+
syntheticMarker: SYNTHETIC_MARKER,
|
|
69
|
+
provenance: {
|
|
70
|
+
kind: "lab_authored",
|
|
71
|
+
authority: AUTHORITY_FILE,
|
|
72
|
+
sourceCommit: authority.sourceCommit,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function expandFailureRules(caseRecord: CaseRecord, authority: CaseAuthority): FailureRule[] {
|
|
78
|
+
const setName = authority.manifestDefaults.failureRuleSet;
|
|
79
|
+
const ruleSet = authority.failureRuleSets[setName];
|
|
80
|
+
if (!Array.isArray(ruleSet)) {
|
|
81
|
+
throw new Error(`harness_failure: contract_integrity unknown failureRuleSet ${setName}`);
|
|
82
|
+
}
|
|
83
|
+
const base = [...ruleSet];
|
|
84
|
+
if (!caseRecord.expectedFailure) return base;
|
|
85
|
+
const template = authority.expectedFailureRuleTemplate;
|
|
86
|
+
const controlRule: FailureRule = {
|
|
87
|
+
id: template.id,
|
|
88
|
+
match: [...template.match],
|
|
89
|
+
classification: caseRecord.expectedFailure.expectedClass as FailureClassification,
|
|
90
|
+
secondaryCode: caseRecord.expectedFailure.expectedCode,
|
|
91
|
+
verdictEffect: caseRecord.expectedFailure.onMatch === "unsupported" ? "unsupported" : "none",
|
|
92
|
+
retry: template.retry,
|
|
93
|
+
expected: template.expected,
|
|
94
|
+
};
|
|
95
|
+
const idx = base.findIndex((r) => r.id === "required-assertion");
|
|
96
|
+
if (idx >= 0) base.splice(idx, 0, controlRule);
|
|
97
|
+
else base.push(controlRule);
|
|
98
|
+
return base;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function validateFixtureDigests(caseRecord: CaseRecord): string[] {
|
|
102
|
+
const errors: string[] = [];
|
|
103
|
+
const check = (fixture: CaseRecord["fixture"], label: string) => {
|
|
104
|
+
const bytes = new TextEncoder().encode(fixture.bytesUtf8);
|
|
105
|
+
const digest = fixtureDigest(bytes);
|
|
106
|
+
if (digest !== fixture.digest) {
|
|
107
|
+
errors.push(`${label} digest mismatch: expected ${fixture.digest}, got ${digest}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
check(caseRecord.fixture, caseRecord.fixture.id);
|
|
111
|
+
if (caseRecord.initiatingRequest) check(caseRecord.initiatingRequest, caseRecord.initiatingRequest.id);
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function validateExpandedFixtureRef(
|
|
116
|
+
ref: Record<string, unknown>,
|
|
117
|
+
authority: CaseAuthority,
|
|
118
|
+
fixtureBytes: string,
|
|
119
|
+
): string[] {
|
|
120
|
+
const errors: string[] = [];
|
|
121
|
+
const bytes = new TextEncoder().encode(fixtureBytes);
|
|
122
|
+
if (ref.syntheticMarker !== SYNTHETIC_MARKER) {
|
|
123
|
+
errors.push(`invalid syntheticMarker: ${String(ref.syntheticMarker)}`);
|
|
124
|
+
}
|
|
125
|
+
const provenance = ref.provenance as Record<string, unknown> | undefined;
|
|
126
|
+
if (!provenance || provenance.kind !== "lab_authored") {
|
|
127
|
+
errors.push("invalid provenance kind");
|
|
128
|
+
} else if (provenance.authority !== AUTHORITY_FILE) {
|
|
129
|
+
errors.push(`invalid provenance authority: ${String(provenance.authority)}`);
|
|
130
|
+
} else if (provenance.sourceCommit !== authority.sourceCommit) {
|
|
131
|
+
errors.push(`invalid provenance sourceCommit: ${String(provenance.sourceCommit)}`);
|
|
132
|
+
}
|
|
133
|
+
if (ref.digest !== fixtureDigest(bytes)) {
|
|
134
|
+
errors.push("fixture digest mismatch in expanded ref");
|
|
135
|
+
}
|
|
136
|
+
if (ref.byteLength !== bytes.byteLength) {
|
|
137
|
+
errors.push("fixture byteLength mismatch in expanded ref");
|
|
138
|
+
}
|
|
139
|
+
return errors;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateMcpHarnessFeatures(caseRecord: CaseRecord): string[] {
|
|
143
|
+
if (caseRecord.suite !== "mcp-core") return [];
|
|
144
|
+
const tokens = caseRecord.requirements.requiredHarnessFeatures.filter(
|
|
145
|
+
(f) => MCP_ACTION_TOKENS.includes(f as typeof MCP_ACTION_TOKENS[number]),
|
|
146
|
+
);
|
|
147
|
+
if (tokens.length !== 1) {
|
|
148
|
+
return [`${caseRecord.id}: invalid_manifest MCP action token count ${tokens.length}`];
|
|
149
|
+
}
|
|
150
|
+
if (caseRecord.fixture.role !== "synthetic_tool") {
|
|
151
|
+
return [`${caseRecord.id}: MCP cases require synthetic_tool fixture role`];
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateAuthority(authority: CaseAuthority): void {
|
|
157
|
+
if (authority.schemaVersion !== 1) throw new Error("unsupported schemaVersion");
|
|
158
|
+
if (!authority.sourceCommit || typeof authority.sourceCommit !== "string") {
|
|
159
|
+
throw new Error("missing sourceCommit");
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(authority.cases) || authority.cases.length === 0) throw new Error("no cases");
|
|
162
|
+
for (const caseRecord of authority.cases) {
|
|
163
|
+
const errors = [
|
|
164
|
+
...validateFixtureDigests(caseRecord),
|
|
165
|
+
...validateMcpHarnessFeatures(caseRecord),
|
|
166
|
+
];
|
|
167
|
+
const expanded = expandScenario(caseRecord, authority);
|
|
168
|
+
const fixtures = expanded.fixtures as Array<Record<string, unknown>>;
|
|
169
|
+
for (let i = 0; i < fixtures.length; i++) {
|
|
170
|
+
const fixtureSource = i === 0 && caseRecord.initiatingRequest
|
|
171
|
+
? caseRecord.initiatingRequest.bytesUtf8
|
|
172
|
+
: caseRecord.fixture.bytesUtf8;
|
|
173
|
+
errors.push(...validateExpandedFixtureRef(fixtures[i], authority, fixtureSource));
|
|
174
|
+
}
|
|
175
|
+
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
176
|
+
if (caseRecord.fixture.role === "upstream_response" && !caseRecord.initiatingRequest) {
|
|
177
|
+
throw new Error(`${caseRecord.id}: upstream_response without initiatingRequest`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { CaseRecord, NormalizedObservation } from "./types";
|
|
2
|
+
import { emptyObservation, projectMcpCalls, setClientResponse } from "./observation";
|
|
3
|
+
|
|
4
|
+
export const MCP_ACTION_TOKENS = [
|
|
5
|
+
"mcp_namespace_round_trip_v1",
|
|
6
|
+
"mcp_schema_bounds_v1",
|
|
7
|
+
"mcp_call_result_v1",
|
|
8
|
+
"mcp_resource_round_trip_v1",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type McpActionToken = typeof MCP_ACTION_TOKENS[number];
|
|
12
|
+
|
|
13
|
+
export function mcpActionToken(caseRecord: CaseRecord): McpActionToken | null {
|
|
14
|
+
const features = caseRecord.requirements.requiredHarnessFeatures;
|
|
15
|
+
const tokens = features.filter((f) => MCP_ACTION_TOKENS.includes(f as McpActionToken));
|
|
16
|
+
if (tokens.length !== 1) return null;
|
|
17
|
+
return tokens[0] as McpActionToken;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function executeMcpSyntheticAction(caseRecord: CaseRecord): NormalizedObservation {
|
|
21
|
+
const token = mcpActionToken(caseRecord);
|
|
22
|
+
if (!token) throw new Error(`invalid_manifest: missing or ambiguous MCP action token for ${caseRecord.id}`);
|
|
23
|
+
const decoded = JSON.parse(caseRecord.fixture.bytesUtf8) as Record<string, unknown>;
|
|
24
|
+
switch (token) {
|
|
25
|
+
case "mcp_namespace_round_trip_v1":
|
|
26
|
+
return runNamespaceRoundTrip(decoded);
|
|
27
|
+
case "mcp_schema_bounds_v1":
|
|
28
|
+
return runSchemaBounds(decoded);
|
|
29
|
+
case "mcp_call_result_v1":
|
|
30
|
+
return runCallResult(decoded);
|
|
31
|
+
case "mcp_resource_round_trip_v1":
|
|
32
|
+
return runResourceRoundTrip(decoded);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nonEmptyString(value: unknown): value is string {
|
|
37
|
+
return typeof value === "string" && value.length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runNamespaceRoundTrip(decoded: Record<string, unknown>): NormalizedObservation {
|
|
41
|
+
if (!nonEmptyString(decoded.namespace) || !nonEmptyString(decoded.name)) {
|
|
42
|
+
throw new Error("invalid_manifest: MCP namespace/name must be non-empty strings");
|
|
43
|
+
}
|
|
44
|
+
const namespace = decoded.namespace;
|
|
45
|
+
const name = decoded.name;
|
|
46
|
+
const wireName = `${namespace}__${name}`;
|
|
47
|
+
const observation = emptyObservation();
|
|
48
|
+
observation.upstream.requests.push({
|
|
49
|
+
status: 0,
|
|
50
|
+
headers: {},
|
|
51
|
+
json: {
|
|
52
|
+
model: "fixture-model",
|
|
53
|
+
tools: [{
|
|
54
|
+
name: wireName,
|
|
55
|
+
description: decoded.description,
|
|
56
|
+
inputSchema: decoded.inputSchema,
|
|
57
|
+
}],
|
|
58
|
+
},
|
|
59
|
+
rawBytes: 0,
|
|
60
|
+
});
|
|
61
|
+
const toolCalls = [{
|
|
62
|
+
id: "call_fixture",
|
|
63
|
+
name: wireName,
|
|
64
|
+
arguments: {},
|
|
65
|
+
kind: "function" as const,
|
|
66
|
+
ordinal: 0,
|
|
67
|
+
}];
|
|
68
|
+
setClientResponse(observation, {
|
|
69
|
+
toolCalls,
|
|
70
|
+
mcpCalls: projectMcpCalls(toolCalls),
|
|
71
|
+
status: 200,
|
|
72
|
+
});
|
|
73
|
+
return observation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function utf8ByteLength(value: string): number {
|
|
77
|
+
return new TextEncoder().encode(value).byteLength;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parsesJson(value: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
JSON.parse(value);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runSchemaBounds(decoded: Record<string, unknown>): NormalizedObservation {
|
|
90
|
+
const observation = emptyObservation();
|
|
91
|
+
const limitBytes = decoded.limitBytes;
|
|
92
|
+
const exactSchema = decoded.exactSchema;
|
|
93
|
+
const overSchema = decoded.overSchema;
|
|
94
|
+
if (!Number.isInteger(limitBytes) || (limitBytes as number) <= 0
|
|
95
|
+
|| typeof exactSchema !== "string" || typeof overSchema !== "string") {
|
|
96
|
+
observation.verifiers = {
|
|
97
|
+
exact_bound: "fail",
|
|
98
|
+
one_over_rejected: "fail",
|
|
99
|
+
partial_commit: false,
|
|
100
|
+
};
|
|
101
|
+
return observation;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const limit = limitBytes as number;
|
|
105
|
+
const exactValid = utf8ByteLength(exactSchema) === limit && parsesJson(exactSchema);
|
|
106
|
+
// The inert stub models two isolated catalogue transactions. The over-bound transaction
|
|
107
|
+
// is rejected before commit; its validity matters so this tests the byte ceiling rather
|
|
108
|
+
// than malformed JSON.
|
|
109
|
+
const overRejected = utf8ByteLength(overSchema) === limit + 1 && parsesJson(overSchema);
|
|
110
|
+
observation.verifiers = {
|
|
111
|
+
exact_bound: exactValid ? "pass" : "fail",
|
|
112
|
+
one_over_rejected: overRejected ? "pass" : "fail",
|
|
113
|
+
partial_commit: false,
|
|
114
|
+
};
|
|
115
|
+
return observation;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runCallResult(decoded: Record<string, unknown>): NormalizedObservation {
|
|
119
|
+
if (!nonEmptyString(decoded.namespace) || !nonEmptyString(decoded.name)) {
|
|
120
|
+
throw new Error("invalid_manifest: MCP namespace/name must be non-empty strings");
|
|
121
|
+
}
|
|
122
|
+
const namespace = decoded.namespace;
|
|
123
|
+
const name = decoded.name;
|
|
124
|
+
const argumentsValue = decoded.arguments ?? {};
|
|
125
|
+
const result = decoded.result;
|
|
126
|
+
const wireName = `${namespace}__${name}`;
|
|
127
|
+
const observation = emptyObservation();
|
|
128
|
+
const toolCalls = [{
|
|
129
|
+
id: "call_fixture",
|
|
130
|
+
name: wireName,
|
|
131
|
+
arguments: argumentsValue,
|
|
132
|
+
kind: "function" as const,
|
|
133
|
+
ordinal: 0,
|
|
134
|
+
}];
|
|
135
|
+
setClientResponse(observation, {
|
|
136
|
+
toolCalls,
|
|
137
|
+
mcpCalls: projectMcpCalls(toolCalls),
|
|
138
|
+
json: result,
|
|
139
|
+
status: 200,
|
|
140
|
+
});
|
|
141
|
+
observation.verifiers = {
|
|
142
|
+
stub_received: { namespace, name, arguments: argumentsValue },
|
|
143
|
+
};
|
|
144
|
+
return observation;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runResourceRoundTrip(decoded: Record<string, unknown>): NormalizedObservation {
|
|
148
|
+
if (!Array.isArray(decoded.resources) || !decoded.read || typeof decoded.read !== "object") {
|
|
149
|
+
throw new Error("invalid_manifest: invalid MCP resource fixture");
|
|
150
|
+
}
|
|
151
|
+
const read = decoded.read as { uri?: unknown; contents?: unknown[] };
|
|
152
|
+
if (!nonEmptyString(read.uri) || !Array.isArray(read.contents)) {
|
|
153
|
+
throw new Error("invalid_manifest: invalid MCP resource read fixture");
|
|
154
|
+
}
|
|
155
|
+
const matching = decoded.resources.filter((resource) =>
|
|
156
|
+
resource && typeof resource === "object" && (resource as { uri?: unknown }).uri === read.uri
|
|
157
|
+
);
|
|
158
|
+
if (matching.length !== 1) throw new Error("invalid_manifest: MCP resource URI must resolve exactly once");
|
|
159
|
+
const observation = emptyObservation();
|
|
160
|
+
setClientResponse(observation, {
|
|
161
|
+
json: {
|
|
162
|
+
resources: decoded.resources,
|
|
163
|
+
contents: read.contents,
|
|
164
|
+
},
|
|
165
|
+
status: 200,
|
|
166
|
+
});
|
|
167
|
+
return observation;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function attachMcpVerifiers(observation: NormalizedObservation, caseRecord: CaseRecord): void {
|
|
171
|
+
if (caseRecord.id === "mcp-core.protocol.namespace-mapping") {
|
|
172
|
+
const toolCalls = observation.client.response.toolCalls;
|
|
173
|
+
if (toolCalls.length === 1) {
|
|
174
|
+
observation.client.response.mcpCalls = projectMcpCalls(toolCalls);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// runCallResult records the literal one-invocation receipt. Do not reconstruct it here:
|
|
178
|
+
// assertions must inspect the action result that actually ran.
|
|
179
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CaseRecord } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Deliberately broken variants proving the harness rejects known defects. */
|
|
4
|
+
export const NEGATIVE_CONTROL_FIXTURES: Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
defect: string;
|
|
7
|
+
mutate: (caseRecord: CaseRecord) => CaseRecord;
|
|
8
|
+
}> = [
|
|
9
|
+
{
|
|
10
|
+
id: "negative.malformed-sse",
|
|
11
|
+
defect: "malformed SSE JSON",
|
|
12
|
+
mutate: (c) => ({
|
|
13
|
+
...c,
|
|
14
|
+
id: "negative.malformed-sse",
|
|
15
|
+
assertions: [
|
|
16
|
+
{ id: "events", operator: "sse_event_sequence", selector: "/client/response/events", expected: ["response.completed"], required: true },
|
|
17
|
+
{ id: "terminal", operator: "terminal_signal_equals", selector: "/client/response/terminal", expected: "completed", required: true },
|
|
18
|
+
],
|
|
19
|
+
fixture: {
|
|
20
|
+
...c.fixture,
|
|
21
|
+
bytesUtf8: "data: {not-json}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n",
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "negative.missing-terminal",
|
|
27
|
+
defect: "missing terminal event",
|
|
28
|
+
mutate: (c) => ({
|
|
29
|
+
...c,
|
|
30
|
+
id: "negative.missing-terminal",
|
|
31
|
+
assertions: [{ id: "terminal", operator: "terminal_signal_equals", selector: "/client/response/terminal", expected: "completed", required: true }],
|
|
32
|
+
fixture: {
|
|
33
|
+
...c.fixture,
|
|
34
|
+
bytesUtf8: "data: {\"type\":\"response.output_text.delta\",\"delta\":\"A\"}\n\n",
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "negative.corrupted-tool-id",
|
|
40
|
+
defect: "corrupted tool IDs",
|
|
41
|
+
mutate: (c) => ({
|
|
42
|
+
...c,
|
|
43
|
+
id: "negative.corrupted-tool-id",
|
|
44
|
+
assertions: [{ id: "alpha", operator: "tool_call_equals", selector: "/client/response/toolCalls/0", expected: { id: "call_a", name: "alpha", arguments: { x: 1 }, kind: "function", ordinal: 0 }, required: true }],
|
|
45
|
+
fixture: {
|
|
46
|
+
...c.fixture,
|
|
47
|
+
bytesUtf8: "data:{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"WRONG\",\"function\":{\"name\":\"alpha\",\"arguments\":\"{\\\"x\\\":1}\"}}]}}]}\n\ndata:{\"choices\":[{\"finish_reason\":\"tool_calls\"}]}\n\ndata:[DONE]\n\n",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "negative.tool-result-order",
|
|
53
|
+
defect: "invalid tool-result ordering",
|
|
54
|
+
mutate: (c) => ({
|
|
55
|
+
...c,
|
|
56
|
+
id: "negative.tool-result-order",
|
|
57
|
+
assertions: [{ id: "result", operator: "tool_result_correlates", selector: "/upstream/requests", expected: { call: "/client/response/toolCalls/0/id", result: "/upstream/requests/1/json/messages/1/tool_call_id" }, required: true }],
|
|
58
|
+
fixture: {
|
|
59
|
+
...c.fixture,
|
|
60
|
+
bytesUtf8: JSON.stringify({
|
|
61
|
+
tools: [{ name: "lookup", parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] } }],
|
|
62
|
+
upstreamToolCall: { id: "call_fixture", name: "lookup", arguments: "{\"q\":\"x\"}" },
|
|
63
|
+
toolResult: { toolCallId: "wrong_id", content: "RESULT" },
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "negative.truncated-tool-args",
|
|
70
|
+
defect: "truncated tool arguments",
|
|
71
|
+
mutate: (c) => ({
|
|
72
|
+
...c,
|
|
73
|
+
id: "negative.truncated-tool-args",
|
|
74
|
+
assertions: [{ id: "alpha", operator: "tool_call_equals", selector: "/client/response/toolCalls/0", expected: { id: "call_a", name: "alpha", arguments: { x: 1 }, kind: "function", ordinal: 0 }, required: true }],
|
|
75
|
+
fixture: {
|
|
76
|
+
...c.fixture,
|
|
77
|
+
bytesUtf8: `data: ${JSON.stringify({ choices: [{ delta: { tool_calls: [{ index: 0, id: "call_a", function: { name: "alpha", arguments: "{\"x\":" } }] } }] })}\n\ndata: ${JSON.stringify({ choices: [{ finish_reason: "tool_calls" }] })}\n\ndata: [DONE]\n\n`,
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "negative.parallel-tool-fragments",
|
|
83
|
+
defect: "duplicate parallel-tool fragments",
|
|
84
|
+
mutate: (c) => ({
|
|
85
|
+
...c,
|
|
86
|
+
id: "negative.parallel-tool-fragments",
|
|
87
|
+
assertions: [{ id: "order", operator: "json_path_equals", selector: "/verifiers/nonoverlap_order", expected: ["call_a", "call_b"], required: true }],
|
|
88
|
+
fixture: {
|
|
89
|
+
...c.fixture,
|
|
90
|
+
bytesUtf8: "data:{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_a\",\"function\":{\"name\":\"a\",\"arguments\":\"{}\"}},{\"index\":0,\"id\":\"call_a\",\"function\":{\"name\":\"a\",\"arguments\":\"{}\"}}]}}]}\n\ndata:{\"choices\":[{\"finish_reason\":\"tool_calls\"}]}\n\ndata:[DONE]\n\n",
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "negative.custom-tool-failure",
|
|
96
|
+
defect: "custom/freeform tool translation failure",
|
|
97
|
+
mutate: (c) => ({
|
|
98
|
+
...c,
|
|
99
|
+
id: "negative.custom-tool-failure",
|
|
100
|
+
assertions: [{ id: "call", operator: "tool_call_equals", selector: "/client/response/toolCalls/0", expected: { id: "call_patch", name: "apply_patch", arguments: "*** Begin Patch\n*** End Patch\n", kind: "custom", ordinal: 0 }, required: true }],
|
|
101
|
+
fixture: {
|
|
102
|
+
...c.fixture,
|
|
103
|
+
bytesUtf8: JSON.stringify({
|
|
104
|
+
tool: { type: "custom", name: "apply_patch", format: { type: "grammar", syntax: "lark", definition: "start: /[\\s\\S]+/" } },
|
|
105
|
+
call: { id: "call_patch", name: "apply_patch", input: "WRONG PATCH" },
|
|
106
|
+
output: { call_id: "call_patch", output: "Done" },
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "negative.continuation-semantics",
|
|
113
|
+
defect: "invalid continuation semantics",
|
|
114
|
+
mutate: (c) => ({
|
|
115
|
+
...c,
|
|
116
|
+
id: "negative.continuation-semantics",
|
|
117
|
+
assertions: [{ id: "order", operator: "json_path_equals", selector: "/verifiers/call_result_order", expected: "pass", required: true }],
|
|
118
|
+
fixture: {
|
|
119
|
+
...c.fixture,
|
|
120
|
+
bytesUtf8: JSON.stringify({
|
|
121
|
+
turn1: { output: [{ type: "function_call", id: "fc_fixture", call_id: "call_fixture", name: "lookup", arguments: "{}" }] },
|
|
122
|
+
turn2: { input: [{ type: "function_call_output", call_id: "wrong_call", output: "RESULT" }] },
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export function baseCaseForNegativeControl(controlId: string, cases: CaseRecord[]): CaseRecord | undefined {
|
|
130
|
+
switch (controlId) {
|
|
131
|
+
case "negative.malformed-sse":
|
|
132
|
+
case "negative.missing-terminal":
|
|
133
|
+
return cases.find((c) => c.id === "responses-core.protocol.sse-framing");
|
|
134
|
+
case "negative.corrupted-tool-id":
|
|
135
|
+
case "negative.truncated-tool-args":
|
|
136
|
+
return cases.find((c) => c.id === "chat-core.protocol.stream-assembly");
|
|
137
|
+
case "negative.tool-result-order":
|
|
138
|
+
return cases.find((c) => c.id === "tools-core.protocol.function-round-trip");
|
|
139
|
+
case "negative.parallel-tool-fragments":
|
|
140
|
+
return cases.find((c) => c.id === "tools-core.protocol.parallel-correlation");
|
|
141
|
+
case "negative.custom-tool-failure":
|
|
142
|
+
return cases.find((c) => c.id === "tools-core.protocol.custom-freeform-round-trip");
|
|
143
|
+
case "negative.continuation-semantics":
|
|
144
|
+
return cases.find((c) => c.id === "codex-core.protocol.tool-continuation");
|
|
145
|
+
default:
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildNegativeControls(cases: CaseRecord[]): CaseRecord[] {
|
|
151
|
+
const built: CaseRecord[] = [];
|
|
152
|
+
for (const control of NEGATIVE_CONTROL_FIXTURES) {
|
|
153
|
+
const base = baseCaseForNegativeControl(control.id, cases);
|
|
154
|
+
if (!base) continue;
|
|
155
|
+
const cloned = structuredClone(base);
|
|
156
|
+
delete cloned.expectedFailure;
|
|
157
|
+
built.push(control.mutate(cloned));
|
|
158
|
+
}
|
|
159
|
+
return built;
|
|
160
|
+
}
|