@bitkyc08/opencodex 2.18.2 → 2.20.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/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +37 -8
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/shim.ts +100 -5
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +239 -87
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/errors.ts +27 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +9 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -34
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +278 -48
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/service-manager-probe.ts +2 -3
- package/src/types.ts +76 -6
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
- package/gui/dist/assets/index-CXI1262_.js +0 -76
- package/gui/dist/assets/index-DUCH59lJ.css +0 -1
package/src/images/loop.ts
CHANGED
|
@@ -247,6 +247,8 @@ export interface ImageBridgeDeps {
|
|
|
247
247
|
stallTimeoutSec?: number;
|
|
248
248
|
/** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
|
|
249
249
|
fetchImpl?: typeof globalThis.fetch;
|
|
250
|
+
/** Reserve the routed provider's next request-start slot before each adapter dispatch. */
|
|
251
|
+
waitForRequestSlot?: (signal?: AbortSignal) => Promise<void>;
|
|
250
252
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
251
253
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
252
254
|
/**
|
|
@@ -380,6 +382,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
380
382
|
// expose buildRequest/fetchResponse/parseStream to the bridge, so collect their events through
|
|
381
383
|
// an AdapterEventQueue and wrap them in a pseudo-response whose parseStream replays them.
|
|
382
384
|
if (adapter.runTurn) {
|
|
385
|
+
await deps.waitForRequestSlot?.(signal);
|
|
383
386
|
const queue = createAdapterEventQueue({
|
|
384
387
|
onBacklogExceeded: () => internalAbort.abort("runTurn backlog exceeded"),
|
|
385
388
|
});
|
|
@@ -457,6 +460,11 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
457
460
|
}
|
|
458
461
|
|
|
459
462
|
let headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
463
|
+
const paceThenResetHeaderDeadline = async (): Promise<void> => {
|
|
464
|
+
headerDeadline.clear();
|
|
465
|
+
await deps.waitForRequestSlot?.(signal);
|
|
466
|
+
headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
467
|
+
};
|
|
460
468
|
try {
|
|
461
469
|
/**
|
|
462
470
|
* Build and fetch one image-bridge iteration on the given adapter, under the iteration
|
|
@@ -487,6 +495,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
487
495
|
let response: Response;
|
|
488
496
|
try {
|
|
489
497
|
if (requestAdapter.fetchResponse) {
|
|
498
|
+
await paceThenResetHeaderDeadline();
|
|
490
499
|
deps.onAttemptSend?.(recovery);
|
|
491
500
|
response = await requestAdapter.fetchResponse(request, {
|
|
492
501
|
abortSignal: headerDeadline.signal,
|
|
@@ -496,7 +505,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
496
505
|
});
|
|
497
506
|
} else {
|
|
498
507
|
response = await fetchWithResetRetry(
|
|
499
|
-
(retryRecovery) => {
|
|
508
|
+
async (retryRecovery) => {
|
|
509
|
+
await paceThenResetHeaderDeadline();
|
|
500
510
|
// Record every helper-driven send (the callback runs for the first attempt and
|
|
501
511
|
// each connection-reset replay); preserve the caller's recovery kind
|
|
502
512
|
// (rate-limit-429 / key-429) when the retry layer supplies none.
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
hermesHomeDir,
|
|
21
21
|
kimiConfigPath,
|
|
22
22
|
kimiHomeDir,
|
|
23
|
+
mcodeConfigPath,
|
|
24
|
+
mcodeHomeDir,
|
|
23
25
|
ompAgentDir,
|
|
24
26
|
ompModelsConfigPath,
|
|
25
27
|
opencodeGlobalConfigPath,
|
|
@@ -112,6 +114,11 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
112
114
|
sourcePreservingYaml: { path: ["llm-pi-ai", "providers", "opencodex"] },
|
|
113
115
|
writerLock: { suffix: ".lock" },
|
|
114
116
|
},
|
|
117
|
+
mcode: {
|
|
118
|
+
id: "mcode",
|
|
119
|
+
configPath: (env = process.env, home = homedir()) => mcodeConfigPath(env, home),
|
|
120
|
+
detectDir: (env = process.env, home = homedir()) => mcodeHomeDir(env, home),
|
|
121
|
+
},
|
|
115
122
|
};
|
|
116
123
|
|
|
117
124
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
/** RFC 8785 JSON Canonicalization Scheme (JCS) for deterministic equality. */
|
|
2
2
|
|
|
3
|
+
function assertValidUnicodeScalarString(value: string): void {
|
|
4
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
5
|
+
const code = value.charCodeAt(index);
|
|
6
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
7
|
+
const next = value.charCodeAt(index + 1);
|
|
8
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
9
|
+
throw new TypeError("jcsStringify: lone UTF-16 surrogate is not valid Unicode");
|
|
10
|
+
}
|
|
11
|
+
index += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
15
|
+
throw new TypeError("jcsStringify: lone UTF-16 surrogate is not valid Unicode");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stringifyJcsString(value: string): string {
|
|
21
|
+
assertValidUnicodeScalarString(value);
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertDenseJsonArray(value: readonly unknown[]): void {
|
|
26
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
27
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
28
|
+
throw new TypeError("jcsStringify: sparse arrays / array holes are not representable in JCS");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const key of Object.keys(value)) {
|
|
32
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
|
|
33
|
+
throw new TypeError("jcsStringify: arrays with extra enumerable properties are not representable in JCS");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
3
38
|
export function jcsStringify(value: unknown): string {
|
|
4
39
|
if (value === undefined) throw new TypeError("jcsStringify: undefined is not representable in JCS");
|
|
5
40
|
if (value === null || typeof value === "boolean") return JSON.stringify(value);
|
|
@@ -7,14 +42,19 @@ export function jcsStringify(value: unknown): string {
|
|
|
7
42
|
if (!Number.isFinite(value)) throw new TypeError("jcsStringify: non-finite numbers are not representable in JCS");
|
|
8
43
|
return JSON.stringify(value);
|
|
9
44
|
}
|
|
10
|
-
if (typeof value === "string") return
|
|
45
|
+
if (typeof value === "string") return stringifyJcsString(value);
|
|
11
46
|
if (Array.isArray(value)) {
|
|
47
|
+
assertDenseJsonArray(value);
|
|
12
48
|
return `[${value.map(jcsStringify).join(",")}]`;
|
|
13
49
|
}
|
|
14
50
|
if (typeof value === "object") {
|
|
51
|
+
const prototype = Object.getPrototypeOf(value);
|
|
52
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
53
|
+
throw new TypeError("jcsStringify: only plain JSON objects are representable in JCS");
|
|
54
|
+
}
|
|
15
55
|
const obj = value as Record<string, unknown>;
|
|
16
56
|
const keys = Object.keys(obj).sort();
|
|
17
|
-
return `{${keys.map((
|
|
57
|
+
return `{${keys.map((key) => `${stringifyJcsString(key)}:${jcsStringify(obj[key])}`).join(",")}}`;
|
|
18
58
|
}
|
|
19
59
|
throw new TypeError(`jcsStringify: unsupported value type ${typeof value}`);
|
|
20
60
|
}
|
|
@@ -50,11 +50,15 @@ export const NEGATIVE_CONTROL_FIXTURES: Array<{
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
id: "negative.tool-result-order",
|
|
53
|
-
defect: "invalid tool-result
|
|
53
|
+
defect: "invalid tool-result correlation after chat-history repair",
|
|
54
54
|
mutate: (c) => ({
|
|
55
55
|
...c,
|
|
56
56
|
id: "negative.tool-result-order",
|
|
57
|
-
|
|
57
|
+
// Chat history hardening closes the unmatched call with a synthetic result, then
|
|
58
|
+
// re-emits the orphan result behind a synthetic assistant call. Inspect the actual
|
|
59
|
+
// supplied result at the end of that repaired pair rather than the synthetic close,
|
|
60
|
+
// otherwise the negative control accidentally validates the repair and passes.
|
|
61
|
+
assertions: [{ id: "result", operator: "tool_result_correlates", selector: "/upstream/requests", expected: { call: "/client/response/toolCalls/0/id", result: "/upstream/requests/1/json/messages/3/tool_call_id" }, required: true }],
|
|
58
62
|
fixture: {
|
|
59
63
|
...c.fixture,
|
|
60
64
|
bytesUtf8: JSON.stringify({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { discoverScenarios, loadCaseAuthority } from "./manifest";
|
|
2
2
|
import { runScenario } from "./executor";
|
|
3
|
-
import { buildNegativeControls } from "./negative-controls";
|
|
4
|
-
import type { ScenarioRunResult } from "./types";
|
|
3
|
+
import { baseCaseForNegativeControl, buildNegativeControls } from "./negative-controls";
|
|
4
|
+
import type { CaseRecord, ScenarioRunResult } from "./types";
|
|
5
5
|
import { CL01_SUITES } from "./types";
|
|
6
6
|
|
|
7
7
|
export interface ConformanceRunSummary {
|
|
@@ -19,6 +19,8 @@ export interface NegativeControlRunSummary extends ConformanceRunSummary {
|
|
|
19
19
|
rejected: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
type ScenarioRunner = (caseRecord: CaseRecord) => Promise<ScenarioRunResult>;
|
|
23
|
+
|
|
22
24
|
export async function runConformanceSuite(
|
|
23
25
|
suites: readonly string[] = CL01_SUITES,
|
|
24
26
|
): Promise<ConformanceRunSummary> {
|
|
@@ -33,15 +35,24 @@ export async function runConformanceSuite(
|
|
|
33
35
|
return { total: results.length, passed, failed: results.length - passed, results };
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export async function runNegativeControls(
|
|
38
|
+
export async function runNegativeControls(
|
|
39
|
+
execute: ScenarioRunner = runScenario,
|
|
40
|
+
): Promise<NegativeControlRunSummary> {
|
|
37
41
|
const authority = loadCaseAuthority();
|
|
38
42
|
const scenarios = buildNegativeControls(discoverScenarios(authority));
|
|
39
43
|
if (scenarios.length === 0) throw new Error("harness_failure: no negative controls discovered");
|
|
40
44
|
const results: ScenarioRunResult[] = [];
|
|
41
45
|
for (const scenario of scenarios) {
|
|
42
|
-
|
|
46
|
+
const baseCase = baseCaseForNegativeControl(scenario.id, authority.cases);
|
|
47
|
+
const executionScenario = baseCase ? { ...scenario, id: baseCase.id } : scenario;
|
|
48
|
+
const result = await execute(executionScenario);
|
|
49
|
+
results.push({ ...result, scenarioId: scenario.id });
|
|
43
50
|
}
|
|
44
|
-
const rejected = results.filter((r) =>
|
|
51
|
+
const rejected = results.filter((r) => (
|
|
52
|
+
!r.passed
|
|
53
|
+
&& r.classification === "protocol_failure"
|
|
54
|
+
&& r.secondaryCode === "deterministic_assertion"
|
|
55
|
+
)).length;
|
|
45
56
|
return {
|
|
46
57
|
total: results.length,
|
|
47
58
|
passed: rejected,
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
OUTCOMES,
|
|
9
9
|
} from "../constants";
|
|
10
10
|
import { FAILURE_CLASSIFICATIONS } from "../conformance/types";
|
|
11
|
-
import { fixtureDigest, isSha256Hex, jcsStringify } from "../digest";
|
|
11
|
+
import { fixtureDigest, isSha256Hex, jcsStringify, subjectIdForSubject } from "../digest";
|
|
12
12
|
import type { ObservationEvent, RouteSubjectV1, TaskSubjectV1 } from "../events/types";
|
|
13
13
|
import { LabValidationError } from "../events/errors";
|
|
14
14
|
import { assignEventId, validateSubject } from "../events/validate";
|
|
15
|
-
import {
|
|
15
|
+
import { withLedgerMutation } from "../ledger/store";
|
|
16
16
|
import { ensureLabDirs } from "../paths";
|
|
17
17
|
import {
|
|
18
18
|
FABRIC_EVIDENCE_LAYER,
|
|
@@ -172,6 +172,11 @@ function validateFabricVerifier(raw: Record<string, unknown>): void {
|
|
|
172
172
|
|
|
173
173
|
/** Validate non-negative usage counters on a producer outcome. */
|
|
174
174
|
function validateFabricUsage(raw: Record<string, unknown>): void {
|
|
175
|
+
for (const key of Object.keys(raw)) {
|
|
176
|
+
if (!(USAGE_KEYS as readonly string[]).includes(key)) {
|
|
177
|
+
throw new FabricTaskError(`unknown usage field ${key}`, "malformed_producer_outcome", "harness");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
175
180
|
for (const key of USAGE_KEYS) {
|
|
176
181
|
assertNonNegativeIntegerField(raw, key);
|
|
177
182
|
}
|
|
@@ -179,6 +184,11 @@ function validateFabricUsage(raw: Record<string, unknown>): void {
|
|
|
179
184
|
|
|
180
185
|
/** Validate non-negative limit fields on a producer outcome. */
|
|
181
186
|
function validateFabricLimits(raw: Record<string, unknown>): void {
|
|
187
|
+
for (const key of Object.keys(raw)) {
|
|
188
|
+
if (!(LIMIT_KEYS as readonly string[]).includes(key)) {
|
|
189
|
+
throw new FabricTaskError(`unknown limit field ${key}`, "malformed_producer_outcome", "harness");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
182
192
|
for (const key of LIMIT_KEYS) {
|
|
183
193
|
assertNonNegativeIntegerField(raw, key);
|
|
184
194
|
}
|
|
@@ -262,12 +272,18 @@ export function assertFabricOutcomeV1(raw: unknown): FabricTaskOutcomeV1 {
|
|
|
262
272
|
} catch (error) {
|
|
263
273
|
wrapValidationError(error);
|
|
264
274
|
}
|
|
275
|
+
if (jcsStringify(taskSubjectObj) !== jcsStringify(taskSubject)) {
|
|
276
|
+
throw new FabricTaskError("taskSubject contains undeclared fields", "malformed_producer_outcome", "harness");
|
|
277
|
+
}
|
|
278
|
+
if (jcsStringify(routeSubjectObj) !== jcsStringify(routeSubject)) {
|
|
279
|
+
throw new FabricTaskError("routeSubject contains undeclared fields", "malformed_producer_outcome", "harness");
|
|
280
|
+
}
|
|
265
281
|
if (!routeSubjectsMatch(routeSubject, taskSubject.routeSubject)) {
|
|
266
282
|
throw new FabricTaskError("contradictory route subjects", "layer_subject_mismatch", "harness");
|
|
267
283
|
}
|
|
268
284
|
|
|
269
|
-
assertStringField(obj, "taskClassId");
|
|
270
|
-
assertStringField(obj, "taskClassVersion");
|
|
285
|
+
const taskClassId = assertStringField(obj, "taskClassId");
|
|
286
|
+
const taskClassVersion = assertStringField(obj, "taskClassVersion");
|
|
271
287
|
const subjectId = assertStringField(obj, "subjectId");
|
|
272
288
|
if (!isSha256Hex(subjectId)) {
|
|
273
289
|
throw new FabricTaskError("malformed producer outcome: subjectId", "malformed_producer_outcome", "harness");
|
|
@@ -277,16 +293,36 @@ export function assertFabricOutcomeV1(raw: unknown): FabricTaskOutcomeV1 {
|
|
|
277
293
|
if (!isSha256Hex(taskFixtureDigest) || !isSha256Hex(verifierManifestDigest)) {
|
|
278
294
|
throw new FabricTaskError("malformed producer outcome: digest field", "malformed_producer_outcome", "harness");
|
|
279
295
|
}
|
|
280
|
-
assertStringField(obj, "fabricCompatibilityVersion");
|
|
296
|
+
const fabricCompatibilityVersion = assertStringField(obj, "fabricCompatibilityVersion");
|
|
281
297
|
const sandboxProfileDigest = assertStringField(obj, "sandboxProfileDigest");
|
|
282
298
|
if (!isSha256Hex(sandboxProfileDigest)) {
|
|
283
299
|
throw new FabricTaskError("malformed producer outcome: sandboxProfileDigest", "malformed_producer_outcome", "harness");
|
|
284
300
|
}
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
if (subjectId !== subjectIdForSubject(taskSubject)) {
|
|
302
|
+
throw new FabricTaskError("subjectId does not match taskSubject", "malformed_producer_outcome", "harness");
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
taskClassId !== taskSubject.taskClassId
|
|
306
|
+
|| taskClassVersion !== taskSubject.taskClassVersion
|
|
307
|
+
|| taskFixtureDigest !== taskSubject.taskFixtureDigest
|
|
308
|
+
|| verifierManifestDigest !== taskSubject.verifierManifestDigest
|
|
309
|
+
|| fabricCompatibilityVersion !== taskSubject.fabricCompatibilityVersion
|
|
310
|
+
|| sandboxProfileDigest !== taskSubject.sandboxProfileDigest
|
|
311
|
+
) {
|
|
312
|
+
throw new FabricTaskError("task identity fields do not match taskSubject", "malformed_producer_outcome", "harness");
|
|
313
|
+
}
|
|
314
|
+
const startedAt = assertNonNegativeIntegerField(obj, "startedAt");
|
|
315
|
+
const completedAt = assertNonNegativeIntegerField(obj, "completedAt");
|
|
316
|
+
if (completedAt < startedAt) {
|
|
317
|
+
throw new FabricTaskError("invalid execution timestamps", "malformed_producer_outcome", "harness");
|
|
318
|
+
}
|
|
287
319
|
validateFabricLimits(assertPlainObject(obj.limits, "limits"));
|
|
288
320
|
validateFabricUsage(assertPlainObject(obj.usage, "usage"));
|
|
289
|
-
|
|
321
|
+
const verifier = assertPlainObject(obj.verifier, "verifier");
|
|
322
|
+
validateFabricVerifier(verifier);
|
|
323
|
+
if (verifier.manifestDigest !== verifierManifestDigest) {
|
|
324
|
+
throw new FabricTaskError("verifier manifest digest does not match outcome", "malformed_producer_outcome", "harness");
|
|
325
|
+
}
|
|
290
326
|
if (!OUTCOMES.includes(obj.outcome as typeof OUTCOMES[number])) {
|
|
291
327
|
throw new FabricTaskError("malformed producer outcome: outcome", "malformed_producer_outcome", "harness");
|
|
292
328
|
}
|
|
@@ -324,9 +360,6 @@ export function observationFromFabricOutcome(
|
|
|
324
360
|
if (routeSubject.subjectKind !== "route") {
|
|
325
361
|
throw new FabricTaskError("nested route subject required", "layer_subject_mismatch", "harness");
|
|
326
362
|
}
|
|
327
|
-
if (!Number.isInteger(outcome.startedAt) || !Number.isInteger(outcome.completedAt) || outcome.completedAt < outcome.startedAt) {
|
|
328
|
-
throw new FabricTaskError("invalid execution timestamps", "malformed_producer_outcome", "harness");
|
|
329
|
-
}
|
|
330
363
|
|
|
331
364
|
const paths = ensureLabDirs(opts.configDir);
|
|
332
365
|
const ownsStore = !opts.artifactStore;
|
|
@@ -430,9 +463,11 @@ function persistFabricOutcome(
|
|
|
430
463
|
const ownsStore = !opts.artifactStore;
|
|
431
464
|
const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
|
|
432
465
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
466
|
+
return withLedgerMutation(paths.ledgerPath, (ledger) => {
|
|
467
|
+
const { event } = observationFromFabricOutcome(outcome, { ...opts, artifactStore: store });
|
|
468
|
+
ledger.appendIfAbsent(event);
|
|
469
|
+
return { event, ledgerPath: paths.ledgerPath };
|
|
470
|
+
});
|
|
436
471
|
} finally {
|
|
437
472
|
if (ownsStore) store.close();
|
|
438
473
|
}
|
package/src/lab/index.ts
CHANGED
|
@@ -36,3 +36,19 @@ export * from "./subject/installation-salt";
|
|
|
36
36
|
export { CL03_LIVE_SUITES } from "./conformance/types";
|
|
37
37
|
export * from "./query";
|
|
38
38
|
export * from "./automation";
|
|
39
|
+
export * from "./public/types";
|
|
40
|
+
export {
|
|
41
|
+
exportLocalPublicEvidence,
|
|
42
|
+
importCommunityEvidenceFile,
|
|
43
|
+
importCommunityEvidenceValue,
|
|
44
|
+
listCommunityEvidenceContext,
|
|
45
|
+
previewLocalPublicEvidence,
|
|
46
|
+
summarizePublicEvidenceVerification,
|
|
47
|
+
verifyPublicEvidenceFile,
|
|
48
|
+
type LocalPublicExportV1,
|
|
49
|
+
type LocalPublicPreviewV1,
|
|
50
|
+
type PublicOperatorExclusionReason,
|
|
51
|
+
type PublicOperatorExclusionV1,
|
|
52
|
+
type PublicVerificationSummaryV1,
|
|
53
|
+
} from "./public/operator";
|
|
54
|
+
export { PublicEvidenceValidationError } from "./public/validate";
|
package/src/lab/ledger/purge.ts
CHANGED
|
@@ -18,10 +18,11 @@ import {
|
|
|
18
18
|
expandSensitiveArtifactEventTargets,
|
|
19
19
|
} from "./artifact-refs";
|
|
20
20
|
import { buildInvalidationIndex } from "./invalidation";
|
|
21
|
-
import {
|
|
21
|
+
import { withLedgerMutation } from "./store";
|
|
22
22
|
import { ensureLabDirs } from "../paths";
|
|
23
23
|
import { rebuildLabProjection } from "../projection/rebuild";
|
|
24
24
|
import { jcsStringify } from "../digest";
|
|
25
|
+
import { purgeLocalPublicEvidenceCopies } from "../public/purge";
|
|
25
26
|
import {
|
|
26
27
|
closeSync,
|
|
27
28
|
existsSync,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
writeSync,
|
|
35
36
|
} from "node:fs";
|
|
36
37
|
import { dirname, join } from "node:path";
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
export class PurgeError extends Error {
|
|
39
40
|
readonly code: string;
|
|
40
41
|
readonly completedActions: string[];
|
|
@@ -45,7 +46,7 @@ export class PurgeError extends Error {
|
|
|
45
46
|
this.completedActions = [...completedActions];
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
export interface SensitivePurgeRequest {
|
|
50
51
|
configDir?: string;
|
|
51
52
|
targetEventIds?: string[];
|
|
@@ -54,7 +55,7 @@ export interface SensitivePurgeRequest {
|
|
|
54
55
|
recordedAt?: number;
|
|
55
56
|
producerVersion?: string;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
function writeAll(fd: number, bytes: Uint8Array): void {
|
|
59
60
|
let offset = 0;
|
|
60
61
|
while (offset < bytes.byteLength) {
|
|
@@ -63,7 +64,7 @@ function writeAll(fd: number, bytes: Uint8Array): void {
|
|
|
63
64
|
offset += n;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
|
|
67
68
|
function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
68
69
|
const body = events.map((e) => jcsStringify(e)).join("\n") + (events.length ? "\n" : "");
|
|
69
70
|
const bytes = new TextEncoder().encode(body);
|
|
@@ -90,7 +91,7 @@ function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
|
90
91
|
}
|
|
91
92
|
throw err;
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
+
|
|
94
95
|
if (process.platform !== "win32") {
|
|
95
96
|
try {
|
|
96
97
|
const dirFd = openSync(parent, "r");
|
|
@@ -100,8 +101,6 @@ function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
|
100
101
|
closeSync(dirFd);
|
|
101
102
|
}
|
|
102
103
|
} catch (err) {
|
|
103
|
-
// The rename is already committed and visible. Report durability failure
|
|
104
|
-
// without pretending the ledger action can be rolled back.
|
|
105
104
|
throw new PurgeError(
|
|
106
105
|
"ledger_durability_failed",
|
|
107
106
|
`ledger rewrite committed but directory fsync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -110,7 +109,7 @@ function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
function deleteArtifactsFailClosed(dir: TrustedArtifactDir, digests: string[]): void {
|
|
115
114
|
const errors: string[] = [];
|
|
116
115
|
for (const digest of digests) {
|
|
@@ -125,7 +124,7 @@ function deleteArtifactsFailClosed(dir: TrustedArtifactDir, digests: string[]):
|
|
|
125
124
|
throw new PurgeError("artifact_delete_failed", errors.join("; "));
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
|
-
|
|
127
|
+
|
|
129
128
|
function purgeBoundedDirectory(dirPath: string): void {
|
|
130
129
|
if (!existsSync(dirPath)) return;
|
|
131
130
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
@@ -141,7 +140,41 @@ function purgeBoundedDirectory(dirPath: string): void {
|
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
}
|
|
144
|
-
|
|
143
|
+
|
|
144
|
+
function normalizePurgeError(err: unknown, completed: readonly string[]): PurgeError {
|
|
145
|
+
if (err instanceof PurgeError) {
|
|
146
|
+
return new PurgeError(
|
|
147
|
+
err.code,
|
|
148
|
+
err.message,
|
|
149
|
+
[...new Set([...completed, ...err.completedActions])],
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return new PurgeError(
|
|
153
|
+
"purge_failed",
|
|
154
|
+
err instanceof Error ? err.message : String(err),
|
|
155
|
+
[...completed],
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildPurgeTombstone(
|
|
160
|
+
req: SensitivePurgeRequest,
|
|
161
|
+
removeIds: ReadonlySet<string>,
|
|
162
|
+
targetArtifactDigests: string[],
|
|
163
|
+
purgeActions: Array<(typeof PURGE_ACTIONS)[number]>,
|
|
164
|
+
): PurgeTombstoneEvent {
|
|
165
|
+
return validateLabEvent(assignEventId({
|
|
166
|
+
schemaVersion: LAB_EVENT_SCHEMA_VERSION,
|
|
167
|
+
eventKind: "purge_tombstone" as const,
|
|
168
|
+
recordedAt: req.recordedAt ?? Date.now(),
|
|
169
|
+
producer: LAB_PRODUCER,
|
|
170
|
+
producerVersion: req.producerVersion ?? LAB_PRODUCER_VERSION,
|
|
171
|
+
targetEventIds: [...removeIds].sort(),
|
|
172
|
+
targetArtifactDigests,
|
|
173
|
+
reason: "sensitive_evidence" as const,
|
|
174
|
+
purgeActions,
|
|
175
|
+
})) as PurgeTombstoneEvent;
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
/**
|
|
146
179
|
* Exceptional sensitive-evidence purge:
|
|
147
180
|
* physically remove targeted JSONL lines and artifacts, append purge_tombstone,
|
|
@@ -153,89 +186,125 @@ export function purgeSensitiveEvidence(req: SensitivePurgeRequest): PurgeTombsto
|
|
|
153
186
|
const targetArtifactDigests = [...(req.targetArtifactDigests ?? [])].sort();
|
|
154
187
|
const purgeActions = [...(req.purgeActions ?? PURGE_ACTIONS)].sort();
|
|
155
188
|
const explicitSensitive = new Set(targetArtifactDigests);
|
|
156
|
-
|
|
157
|
-
const replay = replayLabLedger(paths.ledgerPath);
|
|
158
|
-
const index = buildInvalidationIndex(replay.events);
|
|
159
|
-
const removeIds = expandSensitiveArtifactEventTargets(
|
|
160
|
-
replay.events,
|
|
161
|
-
index,
|
|
162
|
-
new Set(targetEventIds),
|
|
163
|
-
explicitSensitive,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const tombstonePayload = {
|
|
167
|
-
schemaVersion: LAB_EVENT_SCHEMA_VERSION,
|
|
168
|
-
eventKind: "purge_tombstone" as const,
|
|
169
|
-
recordedAt: req.recordedAt ?? Date.now(),
|
|
170
|
-
producer: LAB_PRODUCER,
|
|
171
|
-
producerVersion: req.producerVersion ?? LAB_PRODUCER_VERSION,
|
|
172
|
-
targetEventIds: [...removeIds].sort(),
|
|
173
|
-
targetArtifactDigests,
|
|
174
|
-
reason: "sensitive_evidence" as const,
|
|
175
|
-
purgeActions,
|
|
176
|
-
};
|
|
177
|
-
const tombstone = validateLabEvent(assignEventId(tombstonePayload)) as PurgeTombstoneEvent;
|
|
178
|
-
|
|
179
|
-
const deletionPlan = purgeActions.includes("artifact")
|
|
180
|
-
? artifactDeletionPlan(replay.events, index, removeIds, targetArtifactDigests)
|
|
181
|
-
: { deletable: [], retainedExplicit: [] };
|
|
182
|
-
|
|
183
|
-
if (deletionPlan.retainedExplicit.length > 0) {
|
|
184
|
-
throw new PurgeError(
|
|
185
|
-
"sensitive_bytes_retained",
|
|
186
|
-
`explicit sensitive artifacts remain required: ${deletionPlan.retainedExplicit.join(",")}`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let dir: TrustedArtifactDir | null = null;
|
|
191
189
|
const completed: string[] = [];
|
|
190
|
+
// Cell object: the mutation callback assigns through the property, which keeps
|
|
191
|
+
// the post-mutation read at the declared type (a closure-captured let would
|
|
192
|
+
// narrow to null and break the combined-error report below).
|
|
193
|
+
const deferredExport: { error: PurgeError | null } = { error: null };
|
|
194
|
+
let operationError: PurgeError | null = null;
|
|
195
|
+
let tombstone: PurgeTombstoneEvent | null = null;
|
|
196
|
+
|
|
192
197
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
tombstone = withLedgerMutation(paths.ledgerPath, (ledger) => {
|
|
199
|
+
// Replay and plan under the same lock as every append. Otherwise an event
|
|
200
|
+
// appended after this snapshot can be lost by the atomic rename or can
|
|
201
|
+
// start referencing an artifact after the deletion plan was calculated.
|
|
202
|
+
const replay = ledger.replay();
|
|
203
|
+
const index = buildInvalidationIndex(replay.events);
|
|
204
|
+
const removeIds = expandSensitiveArtifactEventTargets(
|
|
205
|
+
replay.events,
|
|
206
|
+
index,
|
|
207
|
+
new Set(targetEventIds),
|
|
208
|
+
explicitSensitive,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const deletionPlan = purgeActions.includes("artifact")
|
|
212
|
+
? artifactDeletionPlan(replay.events, index, removeIds, targetArtifactDigests)
|
|
213
|
+
: { deletable: [], retainedExplicit: [] };
|
|
214
|
+
|
|
215
|
+
if (deletionPlan.retainedExplicit.length > 0) {
|
|
216
|
+
throw new PurgeError(
|
|
217
|
+
"sensitive_bytes_retained",
|
|
218
|
+
`explicit sensitive artifacts remain required: ${deletionPlan.retainedExplicit.join(",")}`,
|
|
219
|
+
);
|
|
206
220
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const kept: LabEvent[] = [];
|
|
212
|
-
for (const event of replay.events) {
|
|
213
|
-
if (removeIds.has(event.eventId)) continue;
|
|
214
|
-
kept.push(event);
|
|
221
|
+
|
|
222
|
+
if (purgeActions.includes("scratch")) {
|
|
223
|
+
purgeBoundedDirectory(paths.scratchDir);
|
|
224
|
+
completed.push("scratch");
|
|
215
225
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
if (purgeActions.includes("export")) {
|
|
227
|
+
try {
|
|
228
|
+
purgeLocalPublicEvidenceCopies(req.configDir);
|
|
229
|
+
completed.push("export");
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// Export deletion is independent from artifact/ledger/sqlite deletion. Keep
|
|
232
|
+
// deleting every other requested sensitive copy, then report this failure.
|
|
233
|
+
deferredExport.error = normalizePurgeError(err, completed);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let dir: TrustedArtifactDir | null = null;
|
|
238
|
+
try {
|
|
239
|
+
if (purgeActions.includes("artifact")) {
|
|
240
|
+
if (deletionPlan.deletable.length > 0) {
|
|
241
|
+
dir = openTrustedArtifactDir(paths.artifactsDir);
|
|
242
|
+
deleteArtifactsFailClosed(dir, deletionPlan.deletable);
|
|
243
|
+
}
|
|
244
|
+
completed.push("artifact");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Never persist a tombstone claiming that export completed when the export
|
|
248
|
+
// purge failed. Other independent actions remain recordable and continue.
|
|
249
|
+
const tombstoneActions = deferredExport.error
|
|
250
|
+
? purgeActions.filter((action) => action !== "export")
|
|
251
|
+
: purgeActions;
|
|
252
|
+
const hasTombstoneTarget = removeIds.size > 0
|
|
253
|
+
|| targetArtifactDigests.length > 0
|
|
254
|
+
|| tombstoneActions.includes("scratch")
|
|
255
|
+
|| tombstoneActions.includes("export");
|
|
256
|
+
|
|
257
|
+
if (tombstoneActions.length > 0 && (hasTombstoneTarget || !deferredExport.error)) {
|
|
258
|
+
const mutationTombstone = buildPurgeTombstone(req, removeIds, targetArtifactDigests, tombstoneActions);
|
|
259
|
+
if (purgeActions.includes("ledger")) {
|
|
260
|
+
const kept: LabEvent[] = [];
|
|
261
|
+
for (const event of replay.events) {
|
|
262
|
+
if (removeIds.has(event.eventId)) continue;
|
|
263
|
+
kept.push(event);
|
|
264
|
+
}
|
|
265
|
+
kept.push(mutationTombstone);
|
|
266
|
+
atomicRewriteLedger(paths.ledgerPath, kept);
|
|
267
|
+
completed.push("ledger");
|
|
268
|
+
} else {
|
|
269
|
+
ledger.append(mutationTombstone);
|
|
270
|
+
}
|
|
271
|
+
return mutationTombstone;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
} finally {
|
|
275
|
+
if (dir) closeTrustedArtifactDir(dir);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// SQLite is disposable and rebuildLabProjection replays the canonical
|
|
280
|
+
// ledger again, so it does not need to extend the mutation lock duration.
|
|
223
281
|
if (purgeActions.includes("sqlite")) {
|
|
224
282
|
rebuildLabProjection(req.configDir);
|
|
225
283
|
completed.push("sqlite");
|
|
226
284
|
}
|
|
227
|
-
|
|
228
|
-
return tombstone;
|
|
229
285
|
} catch (err) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
286
|
+
operationError = normalizePurgeError(err, completed);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const deferredExportError = deferredExport.error;
|
|
290
|
+
if (operationError && deferredExportError) {
|
|
233
291
|
throw new PurgeError(
|
|
234
292
|
"purge_failed",
|
|
235
|
-
|
|
236
|
-
completed,
|
|
293
|
+
`export purge failed: ${deferredExportError.message}; subsequent purge failure (${operationError.code}): ${operationError.message}`,
|
|
294
|
+
[...new Set([...completed, ...deferredExportError.completedActions, ...operationError.completedActions])],
|
|
237
295
|
);
|
|
238
|
-
} finally {
|
|
239
|
-
if (dir) closeTrustedArtifactDir(dir);
|
|
240
296
|
}
|
|
297
|
+
if (operationError) throw operationError;
|
|
298
|
+
if (deferredExportError) {
|
|
299
|
+
throw new PurgeError(
|
|
300
|
+
deferredExportError.code,
|
|
301
|
+
deferredExportError.message,
|
|
302
|
+
[...new Set([...completed, ...deferredExportError.completedActions])],
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (!tombstone) {
|
|
306
|
+
throw new PurgeError("purge_failed", "purge completed without a durable tombstone", completed);
|
|
307
|
+
}
|
|
308
|
+
return tombstone;
|
|
241
309
|
}
|
|
310
|
+
|