@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,310 @@
|
|
|
1
|
+
import { MAX_BYTES_PER_ARTIFACT, MAX_AGGREGATE_ARTIFACT_BYTES, MAX_ARTIFACTS_PER_RUN, ARTIFACT_FILENAME_EXT, type ArtifactClass, type ContractArtifactClass } from "../constants";
|
|
2
|
+
import {
|
|
3
|
+
artifactBytesDigest,
|
|
4
|
+
claimSourceManifestDigest,
|
|
5
|
+
fixtureDigest,
|
|
6
|
+
isSha256Hex,
|
|
7
|
+
jcsStringify,
|
|
8
|
+
scenarioManifestDigest,
|
|
9
|
+
suiteManifestDigest,
|
|
10
|
+
} from "../digest";
|
|
11
|
+
import type { ArtifactRefV1, ClaimSourceManifestV1 } from "../events/types";
|
|
12
|
+
import { artifactClassMediaType, validateClaimSourceManifest } from "../events/validate";
|
|
13
|
+
import {
|
|
14
|
+
closeTrustedArtifactDir,
|
|
15
|
+
ArtifactFsError,
|
|
16
|
+
deleteArtifactBytes,
|
|
17
|
+
openTrustedArtifactDir,
|
|
18
|
+
putArtifactBytes,
|
|
19
|
+
putNamedDigestBytes,
|
|
20
|
+
readArtifactBytes,
|
|
21
|
+
type TrustedArtifactDir,
|
|
22
|
+
} from "./secure-fs";
|
|
23
|
+
import { redactForArtifact, sanitizeDiagnostic } from "./sanitize";
|
|
24
|
+
|
|
25
|
+
export { ArtifactFsError, openTrustedArtifactDir };
|
|
26
|
+
export type { TrustedArtifactDir };
|
|
27
|
+
|
|
28
|
+
export interface PutArtifactInput {
|
|
29
|
+
artifactClass: ArtifactClass;
|
|
30
|
+
/** Pre-redaction payload; sanitizer runs before hash/write. */
|
|
31
|
+
payload: Uint8Array | string | unknown;
|
|
32
|
+
mediaType?: string;
|
|
33
|
+
redactionPolicy?: string;
|
|
34
|
+
/** For contract artifacts whose digest uses a domain other than artifact-bytes. */
|
|
35
|
+
expectedDigest?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ArtifactReadOptions {
|
|
39
|
+
expectedByteCount?: number;
|
|
40
|
+
artifactClass?: ArtifactClass;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ArtifactStore {
|
|
44
|
+
dir: TrustedArtifactDir;
|
|
45
|
+
put(input: PutArtifactInput): ArtifactRefV1;
|
|
46
|
+
get(digest: string, expectedByteCountOrOpts?: number | ArtifactReadOptions): Uint8Array;
|
|
47
|
+
getVerified(
|
|
48
|
+
digest: string,
|
|
49
|
+
expectedByteCountOrOpts?: number | ArtifactReadOptions,
|
|
50
|
+
): { bytes: Uint8Array; digest: string };
|
|
51
|
+
remove(digest: string): void;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toBytes(payload: Uint8Array | string | unknown): Uint8Array {
|
|
56
|
+
if (payload instanceof Uint8Array) return payload;
|
|
57
|
+
if (typeof payload === "string") return new TextEncoder().encode(payload);
|
|
58
|
+
return new TextEncoder().encode(jcsStringify(payload));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeReadOptions(value?: number | ArtifactReadOptions): ArtifactReadOptions {
|
|
62
|
+
return typeof value === "number" ? { expectedByteCount: value } : value ?? {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseContractJson(bytes: Uint8Array): unknown {
|
|
66
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function jsonDigest(
|
|
70
|
+
digest: (value: Record<string, unknown>) => string,
|
|
71
|
+
): (bytes: Uint8Array) => string {
|
|
72
|
+
return (bytes) => {
|
|
73
|
+
try {
|
|
74
|
+
return digest(parseContractJson(bytes) as Record<string, unknown>);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof ArtifactFsError) throw err;
|
|
77
|
+
throw new ArtifactFsError("artifact_mismatch", "artifact content is not valid contract JSON");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function digestForArtifactClass(artifactClass: ArtifactClass): (bytes: Uint8Array) => string {
|
|
83
|
+
switch (artifactClass) {
|
|
84
|
+
case "fixture":
|
|
85
|
+
return fixtureDigest;
|
|
86
|
+
case "scenario_manifest":
|
|
87
|
+
return jsonDigest(scenarioManifestDigest);
|
|
88
|
+
case "suite_manifest":
|
|
89
|
+
return jsonDigest(suiteManifestDigest);
|
|
90
|
+
case "claim_source_manifest":
|
|
91
|
+
return (bytes) => {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = parseContractJson(bytes);
|
|
94
|
+
return claimSourceManifestDigest(validateClaimSourceManifest(parsed).manifest);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof ArtifactFsError) throw err;
|
|
97
|
+
throw new ArtifactFsError("artifact_mismatch", "claim-source artifact failed validation");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
default:
|
|
101
|
+
return artifactBytesDigest;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createArtifactStore(artifactsDir: string): ArtifactStore {
|
|
106
|
+
const dir = openTrustedArtifactDir(artifactsDir);
|
|
107
|
+
let aggregateBytes = 0;
|
|
108
|
+
let putCount = 0;
|
|
109
|
+
|
|
110
|
+
const getVerified = (
|
|
111
|
+
digest: string,
|
|
112
|
+
expectedByteCountOrOpts?: number | ArtifactReadOptions,
|
|
113
|
+
): { bytes: Uint8Array; digest: string } => {
|
|
114
|
+
const opts = normalizeReadOptions(expectedByteCountOrOpts);
|
|
115
|
+
const candidates = opts.artifactClass
|
|
116
|
+
? [digestForArtifactClass(opts.artifactClass)]
|
|
117
|
+
: [
|
|
118
|
+
artifactBytesDigest,
|
|
119
|
+
fixtureDigest,
|
|
120
|
+
jsonDigest(scenarioManifestDigest),
|
|
121
|
+
jsonDigest(suiteManifestDigest),
|
|
122
|
+
digestForArtifactClass("claim_source_manifest"),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
let lastErr: unknown;
|
|
126
|
+
for (const contentDigest of candidates) {
|
|
127
|
+
try {
|
|
128
|
+
const got = readArtifactBytes(dir, digest, {
|
|
129
|
+
expectedByteCount: opts.expectedByteCount,
|
|
130
|
+
contentDigest,
|
|
131
|
+
});
|
|
132
|
+
return { bytes: got.bytes, digest: got.digest };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
lastErr = err;
|
|
135
|
+
if (
|
|
136
|
+
err instanceof ArtifactFsError &&
|
|
137
|
+
err.code !== "artifact_mismatch" &&
|
|
138
|
+
!err.message.includes("mismatch")
|
|
139
|
+
) {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw lastErr instanceof Error
|
|
145
|
+
? lastErr
|
|
146
|
+
: new ArtifactFsError("artifact_mismatch", "artifact digest verification failed");
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
dir,
|
|
151
|
+
put(input: PutArtifactInput): ArtifactRefV1 {
|
|
152
|
+
if (putCount >= MAX_ARTIFACTS_PER_RUN) {
|
|
153
|
+
throw new ArtifactFsError("budget_exhausted", "maximum artifacts per run exceeded");
|
|
154
|
+
}
|
|
155
|
+
if (input.payload instanceof Uint8Array && input.payload.byteLength > MAX_BYTES_PER_ARTIFACT) {
|
|
156
|
+
throw new ArtifactFsError("budget_exhausted", `artifact exceeds ${MAX_BYTES_PER_ARTIFACT} bytes`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof input.payload === "string" && new TextEncoder().encode(input.payload).byteLength > MAX_BYTES_PER_ARTIFACT) {
|
|
159
|
+
throw new ArtifactFsError("budget_exhausted", `artifact exceeds ${MAX_BYTES_PER_ARTIFACT} bytes`);
|
|
160
|
+
}
|
|
161
|
+
const redacted = redactForArtifact(input.artifactClass, input.payload);
|
|
162
|
+
const bytes = toBytes(redacted);
|
|
163
|
+
if (bytes.byteLength > MAX_BYTES_PER_ARTIFACT) {
|
|
164
|
+
throw new ArtifactFsError("budget_exhausted", `artifact exceeds ${MAX_BYTES_PER_ARTIFACT} bytes`);
|
|
165
|
+
}
|
|
166
|
+
if (aggregateBytes + bytes.byteLength > MAX_AGGREGATE_ARTIFACT_BYTES) {
|
|
167
|
+
throw new ArtifactFsError("budget_exhausted", "aggregate artifact ceiling exceeded");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let stored;
|
|
171
|
+
if (isContractClass(input.artifactClass)) {
|
|
172
|
+
const contractClass = input.artifactClass;
|
|
173
|
+
let computedDigest: string;
|
|
174
|
+
try {
|
|
175
|
+
computedDigest = computeContractDigest(contractClass, bytes, redacted);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof ArtifactFsError) throw err;
|
|
178
|
+
throw new ArtifactFsError(
|
|
179
|
+
"artifact_mismatch",
|
|
180
|
+
err instanceof Error ? err.message : "contract artifact failed validation",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (input.expectedDigest !== undefined && computedDigest !== input.expectedDigest) {
|
|
184
|
+
throw new ArtifactFsError("artifact_mismatch", "contract artifact digest mismatch");
|
|
185
|
+
}
|
|
186
|
+
stored = putNamedDigestBytes(
|
|
187
|
+
dir,
|
|
188
|
+
computedDigest,
|
|
189
|
+
bytes,
|
|
190
|
+
digestForArtifactClass(contractClass),
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
stored = putArtifactBytes(dir, bytes, input.expectedDigest);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
putCount += 1;
|
|
197
|
+
aggregateBytes += stored.byteCount;
|
|
198
|
+
return {
|
|
199
|
+
digest: stored.digest,
|
|
200
|
+
mediaType: input.mediaType ?? artifactClassMediaType(input.artifactClass),
|
|
201
|
+
byteCount: stored.byteCount,
|
|
202
|
+
redactionPolicy: input.redactionPolicy ?? defaultRedactionPolicy(input.artifactClass),
|
|
203
|
+
relativePath: `${stored.digest}${ARTIFACT_FILENAME_EXT}`,
|
|
204
|
+
artifactClass: input.artifactClass,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
get(digest: string, expectedByteCountOrOpts?: number | ArtifactReadOptions): Uint8Array {
|
|
208
|
+
return getVerified(digest, expectedByteCountOrOpts).bytes;
|
|
209
|
+
},
|
|
210
|
+
getVerified,
|
|
211
|
+
remove(digest: string): void {
|
|
212
|
+
deleteArtifactBytes(dir, digest);
|
|
213
|
+
},
|
|
214
|
+
close(): void {
|
|
215
|
+
closeTrustedArtifactDir(dir);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isContractClass(c: ArtifactClass): c is ContractArtifactClass {
|
|
221
|
+
return (
|
|
222
|
+
c === "scenario_manifest" ||
|
|
223
|
+
c === "suite_manifest" ||
|
|
224
|
+
c === "fixture" ||
|
|
225
|
+
c === "claim_source_manifest"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function computeContractDigest(
|
|
230
|
+
artifactClass: ContractArtifactClass,
|
|
231
|
+
bytes: Uint8Array,
|
|
232
|
+
redacted: unknown,
|
|
233
|
+
): string {
|
|
234
|
+
switch (artifactClass) {
|
|
235
|
+
case "fixture":
|
|
236
|
+
return fixtureDigest(bytes);
|
|
237
|
+
case "scenario_manifest":
|
|
238
|
+
return scenarioManifestDigest(
|
|
239
|
+
typeof redacted === "object" && redacted ? (redacted as Record<string, unknown>) : parseContractJson(bytes) as Record<string, unknown>,
|
|
240
|
+
);
|
|
241
|
+
case "suite_manifest":
|
|
242
|
+
return suiteManifestDigest(
|
|
243
|
+
typeof redacted === "object" && redacted ? (redacted as Record<string, unknown>) : parseContractJson(bytes) as Record<string, unknown>,
|
|
244
|
+
);
|
|
245
|
+
case "claim_source_manifest": {
|
|
246
|
+
const parsed = typeof redacted === "object" && redacted
|
|
247
|
+
? redacted
|
|
248
|
+
: parseContractJson(bytes);
|
|
249
|
+
return claimSourceManifestDigest(validateClaimSourceManifest(parsed).manifest);
|
|
250
|
+
}
|
|
251
|
+
default: {
|
|
252
|
+
const _never: never = artifactClass;
|
|
253
|
+
return _never;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function defaultRedactionPolicy(artifactClass: ArtifactClass): string {
|
|
259
|
+
switch (artifactClass) {
|
|
260
|
+
case "scenario_manifest":
|
|
261
|
+
case "suite_manifest":
|
|
262
|
+
case "fixture":
|
|
263
|
+
case "claim_source_manifest":
|
|
264
|
+
return "contract_canonical_v1";
|
|
265
|
+
default:
|
|
266
|
+
// v2 marks the sanitizer that also redacts network and account
|
|
267
|
+
// identifiers. The field records which algorithm produced the bytes, so
|
|
268
|
+
// it moves whenever those semantics change.
|
|
269
|
+
return "sanitized_evidence_v2";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function putClaimSourceManifest(
|
|
274
|
+
store: ArtifactStore,
|
|
275
|
+
manifest: ClaimSourceManifestV1,
|
|
276
|
+
): ArtifactRefV1 {
|
|
277
|
+
const { manifest: validated, digest } = validateClaimSourceManifest(manifest);
|
|
278
|
+
return store.put({
|
|
279
|
+
artifactClass: "claim_source_manifest",
|
|
280
|
+
payload: validated,
|
|
281
|
+
expectedDigest: digest,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export type LoadClaimSourceManifestResult =
|
|
286
|
+
| { ok: true; manifest: ClaimSourceManifestV1; corruption?: undefined }
|
|
287
|
+
| { ok: false; manifest: ClaimSourceManifestV1 | null; corruption: string };
|
|
288
|
+
|
|
289
|
+
export function loadClaimSourceManifest(
|
|
290
|
+
store: ArtifactStore,
|
|
291
|
+
digest: string,
|
|
292
|
+
expected: { subjectId: string; capability: string },
|
|
293
|
+
): LoadClaimSourceManifestResult {
|
|
294
|
+
if (!isSha256Hex(digest)) return { ok: false, manifest: null, corruption: "invalid digest" };
|
|
295
|
+
try {
|
|
296
|
+
const bytes = store.get(digest, { artifactClass: "claim_source_manifest" });
|
|
297
|
+
const parsed = parseContractJson(bytes);
|
|
298
|
+
const { manifest, digest: recomputed } = validateClaimSourceManifest(parsed);
|
|
299
|
+
if (recomputed !== digest) return { ok: false, manifest, corruption: "claim-source digest mismatch" };
|
|
300
|
+
if (manifest.subjectId !== expected.subjectId) return { ok: false, manifest, corruption: "claim-source subjectId mismatch" };
|
|
301
|
+
if (manifest.capability !== expected.capability) return { ok: false, manifest, corruption: "claim-source capability mismatch" };
|
|
302
|
+
return { ok: true, manifest };
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
manifest: null,
|
|
307
|
+
corruption: sanitizeDiagnostic(err),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { jcsEqual } from "./jcs";
|
|
2
|
+
import { pointerExists, resolveJsonPointer } from "./json-pointer";
|
|
3
|
+
import type { AssertionResult, AssertionSpec, NormalizedObservation } from "./types";
|
|
4
|
+
|
|
5
|
+
const ID_GRAMMARS: Record<string, RegExp> = {
|
|
6
|
+
responses_message: /^msg_[A-Za-z0-9_-]{1,128}$/,
|
|
7
|
+
responses_reasoning: /^rs_[A-Za-z0-9_-]{1,128}$/,
|
|
8
|
+
responses_call: /^call_[A-Za-z0-9_-]{1,128}$/,
|
|
9
|
+
nonempty_128: /^[^\s]{1,128}$/,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function evaluateAssertion(
|
|
13
|
+
assertion: AssertionSpec,
|
|
14
|
+
observation: NormalizedObservation,
|
|
15
|
+
): AssertionResult {
|
|
16
|
+
const base: AssertionResult = {
|
|
17
|
+
id: assertion.id,
|
|
18
|
+
operator: assertion.operator,
|
|
19
|
+
required: assertion.required,
|
|
20
|
+
passed: false,
|
|
21
|
+
observedSummary: "",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
switch (assertion.operator) {
|
|
26
|
+
case "http_status_equals":
|
|
27
|
+
return evaluateEquals(assertion, observation, (obs) => obs.client.response.status);
|
|
28
|
+
case "json_path_equals":
|
|
29
|
+
return evaluateJsonPathEquals(assertion, observation);
|
|
30
|
+
case "json_path_present":
|
|
31
|
+
return evaluatePresence(assertion, observation, true);
|
|
32
|
+
case "json_path_absent":
|
|
33
|
+
return evaluatePresence(assertion, observation, false);
|
|
34
|
+
case "sse_event_sequence":
|
|
35
|
+
return evaluateEventSequence(assertion, observation);
|
|
36
|
+
case "sse_event_count":
|
|
37
|
+
return evaluateEventCount(assertion, observation);
|
|
38
|
+
case "terminal_signal_equals":
|
|
39
|
+
return evaluateEquals(assertion, observation, (obs) => obs.client.response.terminal);
|
|
40
|
+
case "id_matches":
|
|
41
|
+
return evaluateIdMatches(assertion, observation);
|
|
42
|
+
case "id_stable_across_events":
|
|
43
|
+
return evaluateIdStable(assertion, observation);
|
|
44
|
+
case "id_correlates":
|
|
45
|
+
return evaluateIdCorrelates(assertion, observation);
|
|
46
|
+
case "tool_call_equals":
|
|
47
|
+
return evaluateJsonPathEquals(assertion, observation);
|
|
48
|
+
case "tool_result_correlates":
|
|
49
|
+
return evaluateToolResultCorrelates(assertion, observation);
|
|
50
|
+
case "normalized_text_equals":
|
|
51
|
+
return evaluateEquals(assertion, observation, (obs) => obs.client.response.normalizedText);
|
|
52
|
+
case "verifier_result_equals":
|
|
53
|
+
return evaluateJsonPathEquals(assertion, observation);
|
|
54
|
+
default:
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
passed: false,
|
|
58
|
+
observedSummary: `unknown operator ${assertion.operator}`,
|
|
59
|
+
reason: "unknown_operator",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
passed: false,
|
|
66
|
+
observedSummary: String(error),
|
|
67
|
+
reason: "evaluation_error",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function evaluateEquals(
|
|
73
|
+
assertion: AssertionSpec,
|
|
74
|
+
observation: NormalizedObservation,
|
|
75
|
+
pick: (obs: NormalizedObservation) => unknown,
|
|
76
|
+
): AssertionResult {
|
|
77
|
+
const observed = pick(observation);
|
|
78
|
+
const passed = jcsEqual(observed, assertion.expected);
|
|
79
|
+
return {
|
|
80
|
+
id: assertion.id,
|
|
81
|
+
operator: assertion.operator,
|
|
82
|
+
required: assertion.required,
|
|
83
|
+
passed,
|
|
84
|
+
observedSummary: summarize(observed),
|
|
85
|
+
reason: passed ? undefined : "value_mismatch",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function evaluateJsonPathEquals(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
90
|
+
const resolved = resolveJsonPointer(observation, assertion.selector);
|
|
91
|
+
if (!resolved.ok) {
|
|
92
|
+
return {
|
|
93
|
+
id: assertion.id,
|
|
94
|
+
operator: assertion.operator,
|
|
95
|
+
required: assertion.required,
|
|
96
|
+
passed: false,
|
|
97
|
+
observedSummary: resolved.reason,
|
|
98
|
+
reason: resolved.reason,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const passed = jcsEqual(resolved.value, assertion.expected);
|
|
102
|
+
return {
|
|
103
|
+
id: assertion.id,
|
|
104
|
+
operator: assertion.operator,
|
|
105
|
+
required: assertion.required,
|
|
106
|
+
passed,
|
|
107
|
+
observedSummary: summarize(resolved.value),
|
|
108
|
+
reason: passed ? undefined : "value_mismatch",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function evaluatePresence(
|
|
113
|
+
assertion: AssertionSpec,
|
|
114
|
+
observation: NormalizedObservation,
|
|
115
|
+
shouldExist: boolean,
|
|
116
|
+
): AssertionResult {
|
|
117
|
+
const exists = pointerExists(observation, assertion.selector);
|
|
118
|
+
const passed = shouldExist ? exists : !exists;
|
|
119
|
+
return {
|
|
120
|
+
id: assertion.id,
|
|
121
|
+
operator: assertion.operator,
|
|
122
|
+
required: assertion.required,
|
|
123
|
+
passed,
|
|
124
|
+
observedSummary: exists ? "present" : "absent",
|
|
125
|
+
reason: passed ? undefined : shouldExist ? "selector_missing" : "selector_present",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function evaluateEventSequence(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
130
|
+
const events = observation.client.response.events.map((e) => e.event);
|
|
131
|
+
const expected = assertion.expected as string[];
|
|
132
|
+
const passed = Array.isArray(expected)
|
|
133
|
+
&& events.length === expected.length
|
|
134
|
+
&& events.every((e, i) => e === expected[i]);
|
|
135
|
+
return {
|
|
136
|
+
id: assertion.id,
|
|
137
|
+
operator: assertion.operator,
|
|
138
|
+
required: assertion.required,
|
|
139
|
+
passed,
|
|
140
|
+
observedSummary: summarize(events),
|
|
141
|
+
reason: passed ? undefined : "event_sequence_mismatch",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function evaluateEventCount(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
146
|
+
const spec = assertion.expected as { event: string; count: number };
|
|
147
|
+
const count = observation.client.response.events.filter((e) => e.event === spec.event).length;
|
|
148
|
+
const passed = count === spec.count;
|
|
149
|
+
return {
|
|
150
|
+
id: assertion.id,
|
|
151
|
+
operator: assertion.operator,
|
|
152
|
+
required: assertion.required,
|
|
153
|
+
passed,
|
|
154
|
+
observedSummary: String(count),
|
|
155
|
+
reason: passed ? undefined : "event_count_mismatch",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function evaluateIdMatches(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
160
|
+
const resolved = resolveJsonPointer(observation, assertion.selector);
|
|
161
|
+
if (!resolved.ok) {
|
|
162
|
+
return {
|
|
163
|
+
id: assertion.id,
|
|
164
|
+
operator: assertion.operator,
|
|
165
|
+
required: assertion.required,
|
|
166
|
+
passed: false,
|
|
167
|
+
observedSummary: resolved.reason,
|
|
168
|
+
reason: resolved.reason,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const grammar = ID_GRAMMARS[String(assertion.expected)];
|
|
172
|
+
const value = typeof resolved.value === "string" ? resolved.value : "";
|
|
173
|
+
const passed = grammar ? grammar.test(value) : false;
|
|
174
|
+
return {
|
|
175
|
+
id: assertion.id,
|
|
176
|
+
operator: assertion.operator,
|
|
177
|
+
required: assertion.required,
|
|
178
|
+
passed,
|
|
179
|
+
observedSummary: value,
|
|
180
|
+
reason: passed ? undefined : "id_grammar_mismatch",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function evaluateIdStable(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
185
|
+
const pointers = assertion.expected;
|
|
186
|
+
if (!Array.isArray(pointers) || pointers.length < 2 || !pointers.every((p) => typeof p === "string")) {
|
|
187
|
+
return {
|
|
188
|
+
id: assertion.id,
|
|
189
|
+
operator: assertion.operator,
|
|
190
|
+
required: assertion.required,
|
|
191
|
+
passed: false,
|
|
192
|
+
observedSummary: "expected at least two pointers",
|
|
193
|
+
reason: "invalid_expected",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const values: string[] = [];
|
|
197
|
+
for (const pointer of pointers) {
|
|
198
|
+
const resolved = resolveJsonPointer(observation, pointer);
|
|
199
|
+
if (!resolved.ok) {
|
|
200
|
+
return {
|
|
201
|
+
id: assertion.id,
|
|
202
|
+
operator: assertion.operator,
|
|
203
|
+
required: assertion.required,
|
|
204
|
+
passed: false,
|
|
205
|
+
observedSummary: resolved.reason,
|
|
206
|
+
reason: resolved.reason,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (typeof resolved.value !== "string") {
|
|
210
|
+
return {
|
|
211
|
+
id: assertion.id,
|
|
212
|
+
operator: assertion.operator,
|
|
213
|
+
required: assertion.required,
|
|
214
|
+
passed: false,
|
|
215
|
+
observedSummary: "identifier must be a string",
|
|
216
|
+
reason: "selector_type_mismatch",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
values.push(resolved.value);
|
|
220
|
+
}
|
|
221
|
+
const passed = values.every((v) => v === values[0]);
|
|
222
|
+
return {
|
|
223
|
+
id: assertion.id,
|
|
224
|
+
operator: assertion.operator,
|
|
225
|
+
required: assertion.required,
|
|
226
|
+
passed,
|
|
227
|
+
observedSummary: summarize(values),
|
|
228
|
+
reason: passed ? undefined : "id_not_stable",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function correlatedIds(left: unknown, right: unknown): boolean {
|
|
233
|
+
return typeof left === "string"
|
|
234
|
+
&& typeof right === "string"
|
|
235
|
+
&& left.length > 0
|
|
236
|
+
&& right.length > 0
|
|
237
|
+
&& left === right;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function evaluateIdCorrelates(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
241
|
+
const pointers = assertion.expected;
|
|
242
|
+
if (!Array.isArray(pointers) || pointers.length !== 2 || !pointers.every((p) => typeof p === "string")) {
|
|
243
|
+
return {
|
|
244
|
+
id: assertion.id,
|
|
245
|
+
operator: assertion.operator,
|
|
246
|
+
required: assertion.required,
|
|
247
|
+
passed: false,
|
|
248
|
+
observedSummary: "expected two pointers",
|
|
249
|
+
reason: "invalid_expected",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const left = resolveJsonPointer(observation, pointers[0]);
|
|
253
|
+
const right = resolveJsonPointer(observation, pointers[1]);
|
|
254
|
+
if (!left.ok || !right.ok) {
|
|
255
|
+
return {
|
|
256
|
+
id: assertion.id,
|
|
257
|
+
operator: assertion.operator,
|
|
258
|
+
required: assertion.required,
|
|
259
|
+
passed: false,
|
|
260
|
+
observedSummary: left.ok ? (right as { reason: string }).reason : (left as { reason: string }).reason,
|
|
261
|
+
reason: "selector_missing",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const passed = correlatedIds(left.value, right.value);
|
|
265
|
+
return {
|
|
266
|
+
id: assertion.id,
|
|
267
|
+
operator: assertion.operator,
|
|
268
|
+
required: assertion.required,
|
|
269
|
+
passed,
|
|
270
|
+
observedSummary: `${summarize(left.value)} vs ${summarize(right.value)}`,
|
|
271
|
+
reason: passed ? undefined : "id_correlation_mismatch",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function evaluateToolResultCorrelates(assertion: AssertionSpec, observation: NormalizedObservation): AssertionResult {
|
|
276
|
+
const spec = assertion.expected as { call?: unknown; result?: unknown };
|
|
277
|
+
if (!spec || typeof spec.call !== "string" || typeof spec.result !== "string") {
|
|
278
|
+
return {
|
|
279
|
+
id: assertion.id,
|
|
280
|
+
operator: assertion.operator,
|
|
281
|
+
required: assertion.required,
|
|
282
|
+
passed: false,
|
|
283
|
+
observedSummary: "expected call/result pointers",
|
|
284
|
+
reason: "invalid_expected",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const call = resolveJsonPointer(observation, spec.call);
|
|
288
|
+
const result = resolveJsonPointer(observation, spec.result);
|
|
289
|
+
if (!call.ok || !result.ok) {
|
|
290
|
+
return {
|
|
291
|
+
id: assertion.id,
|
|
292
|
+
operator: assertion.operator,
|
|
293
|
+
required: assertion.required,
|
|
294
|
+
passed: false,
|
|
295
|
+
observedSummary: call.ok ? (result as { reason: string }).reason : (call as { reason: string }).reason,
|
|
296
|
+
reason: "selector_missing",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const passed = correlatedIds(call.value, result.value);
|
|
300
|
+
return {
|
|
301
|
+
id: assertion.id,
|
|
302
|
+
operator: assertion.operator,
|
|
303
|
+
required: assertion.required,
|
|
304
|
+
passed,
|
|
305
|
+
observedSummary: `${summarize(call.value)} -> ${summarize(result.value)}`,
|
|
306
|
+
reason: passed ? undefined : "tool_result_correlation_mismatch",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function summarize(value: unknown): string {
|
|
311
|
+
if (typeof value === "string") return value.length > 120 ? value.slice(0, 120) + "…" : value;
|
|
312
|
+
try {
|
|
313
|
+
const text = JSON.stringify(value);
|
|
314
|
+
return text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
315
|
+
} catch {
|
|
316
|
+
return String(value);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function evaluateAssertions(
|
|
321
|
+
assertions: AssertionSpec[],
|
|
322
|
+
observation: NormalizedObservation,
|
|
323
|
+
): AssertionResult[] {
|
|
324
|
+
return assertions.map((a) => evaluateAssertion(a, observation));
|
|
325
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { jcsStringify } from "./jcs";
|
|
3
|
+
|
|
4
|
+
function domainHash(domain: string, payload: Uint8Array | string): string {
|
|
5
|
+
const hash = createHash("sha256");
|
|
6
|
+
hash.update(new TextEncoder().encode(`${domain}\0`));
|
|
7
|
+
if (typeof payload === "string") hash.update(new TextEncoder().encode(payload));
|
|
8
|
+
else hash.update(payload);
|
|
9
|
+
return hash.digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fixtureDigest(bytes: Uint8Array): string {
|
|
13
|
+
return domainHash("ocx-lab:fixture:v1", bytes);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function scenarioManifestDigest(expandedScenario: Record<string, unknown>): string {
|
|
17
|
+
return domainHash("ocx-lab:scenario-manifest:v1", jcsStringify(expandedScenario));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function suiteManifestDigest(expandedSuite: Record<string, unknown>): string {
|
|
21
|
+
return domainHash("ocx-lab:suite-manifest:v1", jcsStringify(expandedSuite));
|
|
22
|
+
}
|