@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
package/src/lab/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export * from "./constants";
|
|
2
|
+
export * from "./digest";
|
|
3
|
+
export * from "./paths";
|
|
4
|
+
export * from "./events/types";
|
|
5
|
+
export * from "./events/validate";
|
|
6
|
+
export * from "./artifacts/secure-fs";
|
|
7
|
+
export * from "./artifacts/store";
|
|
8
|
+
export * from "./artifacts/sanitize";
|
|
9
|
+
export * from "./ledger/store";
|
|
10
|
+
export * from "./ledger/invalidation";
|
|
11
|
+
export * from "./ledger/purge";
|
|
12
|
+
export * from "./projection/schema";
|
|
13
|
+
export * from "./projection/verdicts";
|
|
14
|
+
export * from "./projection/rebuild";
|
|
15
|
+
export * from "./events/limits";
|
|
16
|
+
export * from "./conformance/suite-manifest";
|
|
17
|
+
export * from "./ledger/artifact-refs";
|
|
18
|
+
export * from "./projection/verification";
|
|
19
|
+
export * from "./observe/from-conformance";
|
|
20
|
+
export * from "./observe/from-live";
|
|
21
|
+
export * from "./live/manifest";
|
|
22
|
+
export * from "./live/runner";
|
|
23
|
+
export * from "./live/executor";
|
|
24
|
+
export * from "./live/types";
|
|
25
|
+
export * from "./live/destination";
|
|
26
|
+
export * from "./live/sandbox";
|
|
27
|
+
export * from "./live/transport";
|
|
28
|
+
export * from "./live/credential-lease";
|
|
29
|
+
export * from "./live/inert-tools";
|
|
30
|
+
export * from "./live/mcp-loopback";
|
|
31
|
+
export * from "./live/suite-manifest";
|
|
32
|
+
export * from "./subject/route-subject";
|
|
33
|
+
export * from "./subject/behavior-fingerprint";
|
|
34
|
+
export * from "./subject/installation-salt";
|
|
35
|
+
export { CL03_LIVE_SUITES } from "./conformance/types";
|
|
36
|
+
export * from "./query";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ClaimSnapshotEvent, LabEvent, ObservationEvent } from "../events/types";
|
|
2
|
+
import type { InvalidationIndex } from "./invalidation";
|
|
3
|
+
|
|
4
|
+
/** Collect artifacts that the frozen CL-00 retention rules still require. */
|
|
5
|
+
export function collectReferencedArtifactDigests(
|
|
6
|
+
events: LabEvent[],
|
|
7
|
+
index: InvalidationIndex,
|
|
8
|
+
opts: { excludeEventIds?: Set<string> } = {},
|
|
9
|
+
): Set<string> {
|
|
10
|
+
const refs = new Set<string>();
|
|
11
|
+
const exclude = opts.excludeEventIds ?? new Set<string>();
|
|
12
|
+
|
|
13
|
+
for (const event of events) {
|
|
14
|
+
if (exclude.has(event.eventId)) continue;
|
|
15
|
+
if (index.purgedEventIds.has(event.eventId)) continue;
|
|
16
|
+
|
|
17
|
+
if (event.eventKind === "observation") {
|
|
18
|
+
// CL-00 releases observation artifacts after invalidation or purge.
|
|
19
|
+
if (index.invalidatedBy.has(event.eventId)) continue;
|
|
20
|
+
addObservationArtifacts(event, refs);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (event.eventKind === "claim_snapshot") {
|
|
24
|
+
// CL-00 retains claim source manifests while any non-purged claim references them.
|
|
25
|
+
refs.add(event.sourceManifestDigest);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return refs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addObservationArtifacts(obs: ObservationEvent, refs: Set<string>): void {
|
|
32
|
+
refs.add(obs.scenarioManifestDigest);
|
|
33
|
+
refs.add(obs.suiteManifestDigest);
|
|
34
|
+
for (const digest of obs.fixtureDigests) refs.add(digest);
|
|
35
|
+
for (const ref of obs.artifactRefs) refs.add(ref.digest);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function eventReferencesArtifactDigest(event: LabEvent, digest: string): boolean {
|
|
39
|
+
if (event.eventKind === "observation") {
|
|
40
|
+
if (event.scenarioManifestDigest === digest) return true;
|
|
41
|
+
if (event.suiteManifestDigest === digest) return true;
|
|
42
|
+
if (event.fixtureDigests.includes(digest)) return true;
|
|
43
|
+
return event.artifactRefs.some((ref) => ref.digest === digest);
|
|
44
|
+
}
|
|
45
|
+
if (event.eventKind === "claim_snapshot") {
|
|
46
|
+
return event.sourceManifestDigest === digest;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Expand purge targets so explicitly sensitive artifact digests cannot survive
|
|
53
|
+
* while any retained ledger evidence line still references them.
|
|
54
|
+
*/
|
|
55
|
+
export function expandSensitiveArtifactEventTargets(
|
|
56
|
+
events: LabEvent[],
|
|
57
|
+
index: InvalidationIndex,
|
|
58
|
+
targetEventIds: Set<string>,
|
|
59
|
+
explicitArtifactDigests: Set<string>,
|
|
60
|
+
): Set<string> {
|
|
61
|
+
const expanded = new Set(targetEventIds);
|
|
62
|
+
if (explicitArtifactDigests.size === 0) return expanded;
|
|
63
|
+
|
|
64
|
+
for (const event of events) {
|
|
65
|
+
if (expanded.has(event.eventId)) continue;
|
|
66
|
+
if (index.purgedEventIds.has(event.eventId)) continue;
|
|
67
|
+
for (const digest of explicitArtifactDigests) {
|
|
68
|
+
if (eventReferencesArtifactDigest(event, digest)) {
|
|
69
|
+
expanded.add(event.eventId);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return expanded;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Artifacts still required by surviving evidence after excluding purge targets. */
|
|
78
|
+
export function artifactsStillRequired(
|
|
79
|
+
events: LabEvent[],
|
|
80
|
+
index: InvalidationIndex,
|
|
81
|
+
excludeEventIds: Set<string>,
|
|
82
|
+
): Set<string> {
|
|
83
|
+
return collectReferencedArtifactDigests(events, index, { excludeEventIds });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ArtifactDeletionPlan {
|
|
87
|
+
deletable: string[];
|
|
88
|
+
/** Explicitly sensitive digests that remain pinned by surviving evidence. */
|
|
89
|
+
retainedExplicit: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Plan physical artifact deletion for a purge without silently retaining explicit targets. */
|
|
93
|
+
export function artifactDeletionPlan(
|
|
94
|
+
events: LabEvent[],
|
|
95
|
+
index: InvalidationIndex,
|
|
96
|
+
targetEventIds: Set<string>,
|
|
97
|
+
explicitArtifactDigests: string[],
|
|
98
|
+
): ArtifactDeletionPlan {
|
|
99
|
+
const stillRequired = artifactsStillRequired(events, index, targetEventIds);
|
|
100
|
+
const candidates = new Set<string>(explicitArtifactDigests);
|
|
101
|
+
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
if (!targetEventIds.has(event.eventId)) continue;
|
|
104
|
+
if (event.eventKind === "observation") {
|
|
105
|
+
const scratch = new Set<string>();
|
|
106
|
+
addObservationArtifacts(event, scratch);
|
|
107
|
+
for (const digest of scratch) candidates.add(digest);
|
|
108
|
+
} else if (event.eventKind === "claim_snapshot") {
|
|
109
|
+
candidates.add(event.sourceManifestDigest);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
deletable: [...candidates].filter((digest) => !stillRequired.has(digest)).sort(),
|
|
115
|
+
retainedExplicit: explicitArtifactDigests.filter((digest) => stillRequired.has(digest)).sort(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function observationArtifactDigests(obs: ObservationEvent): string[] {
|
|
120
|
+
const out = new Set<string>();
|
|
121
|
+
addObservationArtifacts(obs, out);
|
|
122
|
+
return [...out].sort();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function claimArtifactDigests(claim: ClaimSnapshotEvent): string[] {
|
|
126
|
+
return [claim.sourceManifestDigest];
|
|
127
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClaimSnapshotEvent,
|
|
3
|
+
InvalidationEvent,
|
|
4
|
+
LabEvent,
|
|
5
|
+
LedgerCorruption,
|
|
6
|
+
ObservationEvent,
|
|
7
|
+
PurgeTombstoneEvent,
|
|
8
|
+
} from "../events/types";
|
|
9
|
+
import { LabValidationError } from "../events/validate";
|
|
10
|
+
|
|
11
|
+
export interface InvalidationIndex {
|
|
12
|
+
/** eventId -> invalidation eventIds that targeted it */
|
|
13
|
+
invalidatedBy: Map<string, string[]>;
|
|
14
|
+
purgedEventIds: Set<string>;
|
|
15
|
+
purgedArtifactDigests: Set<string>;
|
|
16
|
+
corruptions: LedgerCorruption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type EventPosition = { kind: LabEvent["eventKind"]; index: number };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply purge tombstones before ordinary invalidations.
|
|
23
|
+
* Invalidation target lists are atomic: any bad target rejects the whole event.
|
|
24
|
+
*/
|
|
25
|
+
export function buildInvalidationIndex(events: LabEvent[]): InvalidationIndex {
|
|
26
|
+
const purgedEventIds = new Set<string>();
|
|
27
|
+
const purgedArtifactDigests = new Set<string>();
|
|
28
|
+
const invalidatedBy = new Map<string, string[]>();
|
|
29
|
+
const corruptions: LedgerCorruption[] = [];
|
|
30
|
+
|
|
31
|
+
const validEvidenceIds = new Map<string, { kind: "observation" | "claim_snapshot"; index: number }>();
|
|
32
|
+
const allEventIds = new Map<string, EventPosition>();
|
|
33
|
+
|
|
34
|
+
// First pass: record event positions and evidence positions; apply purges as encountered.
|
|
35
|
+
for (let i = 0; i < events.length; i++) {
|
|
36
|
+
const event = events[i]!;
|
|
37
|
+
allEventIds.set(event.eventId, { kind: event.eventKind, index: i });
|
|
38
|
+
if (event.eventKind === "observation" || event.eventKind === "claim_snapshot") {
|
|
39
|
+
validEvidenceIds.set(event.eventId, { kind: event.eventKind, index: i });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (event.eventKind === "purge_tombstone") {
|
|
43
|
+
applyPurge(event, purgedEventIds, purgedArtifactDigests);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Second pass: validate and apply invalidations against earlier evidence.
|
|
48
|
+
for (let i = 0; i < events.length; i++) {
|
|
49
|
+
const event = events[i]!;
|
|
50
|
+
if (event.eventKind !== "invalidation") continue;
|
|
51
|
+
try {
|
|
52
|
+
validateInvalidationTargets(event, i, validEvidenceIds, allEventIds);
|
|
53
|
+
for (const target of event.targetEventIds) {
|
|
54
|
+
const list = invalidatedBy.get(target) ?? [];
|
|
55
|
+
list.push(event.eventId);
|
|
56
|
+
invalidatedBy.set(target, list);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
corruptions.push({
|
|
60
|
+
kind: "invalid_reference",
|
|
61
|
+
eventId: event.eventId,
|
|
62
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { invalidatedBy, purgedEventIds, purgedArtifactDigests, corruptions };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyPurge(
|
|
71
|
+
event: PurgeTombstoneEvent,
|
|
72
|
+
purgedEventIds: Set<string>,
|
|
73
|
+
purgedArtifactDigests: Set<string>,
|
|
74
|
+
): void {
|
|
75
|
+
for (const id of event.targetEventIds) purgedEventIds.add(id);
|
|
76
|
+
for (const digest of event.targetArtifactDigests) purgedArtifactDigests.add(digest);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateInvalidationTargets(
|
|
80
|
+
event: InvalidationEvent,
|
|
81
|
+
index: number,
|
|
82
|
+
validEvidenceIds: Map<string, { kind: "observation" | "claim_snapshot"; index: number }>,
|
|
83
|
+
allEventIds: Map<string, EventPosition>,
|
|
84
|
+
): void {
|
|
85
|
+
for (const target of event.targetEventIds) {
|
|
86
|
+
if (target === event.eventId) {
|
|
87
|
+
throw new LabValidationError("self_target", "invalidation cannot target itself");
|
|
88
|
+
}
|
|
89
|
+
const meta = validEvidenceIds.get(target);
|
|
90
|
+
if (!meta) {
|
|
91
|
+
const other = allEventIds.get(target);
|
|
92
|
+
if (
|
|
93
|
+
other &&
|
|
94
|
+
other.index < index &&
|
|
95
|
+
(other.kind === "invalidation" || other.kind === "purge_tombstone")
|
|
96
|
+
) {
|
|
97
|
+
throw new LabValidationError("bad_target_kind", `cannot invalidate ${other.kind}`);
|
|
98
|
+
}
|
|
99
|
+
throw new LabValidationError("unknown_target", `unknown target ${target}`);
|
|
100
|
+
}
|
|
101
|
+
if (meta.index >= index) {
|
|
102
|
+
throw new LabValidationError("future_target", `target ${target} is not earlier`);
|
|
103
|
+
}
|
|
104
|
+
// Purged targets may no longer have a ledger line. A previously valid evidence
|
|
105
|
+
// ID remains addressable because validEvidenceIds records its original position.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isEventExcluded(
|
|
110
|
+
eventId: string,
|
|
111
|
+
index: InvalidationIndex,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (index.purgedEventIds.has(eventId)) return true;
|
|
114
|
+
if (index.invalidatedBy.has(eventId)) return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function usableObservations(
|
|
119
|
+
events: LabEvent[],
|
|
120
|
+
index: InvalidationIndex,
|
|
121
|
+
): ObservationEvent[] {
|
|
122
|
+
return events.filter(
|
|
123
|
+
(e): e is ObservationEvent =>
|
|
124
|
+
e.eventKind === "observation" && !isEventExcluded(e.eventId, index),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function usableClaims(
|
|
129
|
+
events: LabEvent[],
|
|
130
|
+
index: InvalidationIndex,
|
|
131
|
+
): ClaimSnapshotEvent[] {
|
|
132
|
+
return events.filter(
|
|
133
|
+
(e): e is ClaimSnapshotEvent =>
|
|
134
|
+
e.eventKind === "claim_snapshot" && !isEventExcluded(e.eventId, index),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeTrustedArtifactDir,
|
|
3
|
+
deleteArtifactBytes,
|
|
4
|
+
openTrustedArtifactDir,
|
|
5
|
+
type TrustedArtifactDir,
|
|
6
|
+
} from "../artifacts/secure-fs";
|
|
7
|
+
import { ArtifactFsError } from "../artifacts/secure-fs";
|
|
8
|
+
import {
|
|
9
|
+
LAB_EVENT_SCHEMA_VERSION,
|
|
10
|
+
LAB_PRODUCER,
|
|
11
|
+
LAB_PRODUCER_VERSION,
|
|
12
|
+
PURGE_ACTIONS,
|
|
13
|
+
} from "../constants";
|
|
14
|
+
import type { LabEvent, PurgeTombstoneEvent } from "../events/types";
|
|
15
|
+
import { assignEventId, validateLabEvent } from "../events/validate";
|
|
16
|
+
import {
|
|
17
|
+
artifactDeletionPlan,
|
|
18
|
+
expandSensitiveArtifactEventTargets,
|
|
19
|
+
} from "./artifact-refs";
|
|
20
|
+
import { buildInvalidationIndex } from "./invalidation";
|
|
21
|
+
import { appendLabEvent, replayLabLedger } from "./store";
|
|
22
|
+
import { ensureLabDirs } from "../paths";
|
|
23
|
+
import { rebuildLabProjection } from "../projection/rebuild";
|
|
24
|
+
import { jcsStringify } from "../digest";
|
|
25
|
+
import {
|
|
26
|
+
closeSync,
|
|
27
|
+
existsSync,
|
|
28
|
+
fsyncSync,
|
|
29
|
+
openSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
renameSync,
|
|
32
|
+
rmSync,
|
|
33
|
+
unlinkSync,
|
|
34
|
+
writeSync,
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
|
|
38
|
+
export class PurgeError extends Error {
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly completedActions: string[];
|
|
41
|
+
constructor(code: string, message: string, completedActions: string[] = []) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "PurgeError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.completedActions = [...completedActions];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SensitivePurgeRequest {
|
|
50
|
+
configDir?: string;
|
|
51
|
+
targetEventIds?: string[];
|
|
52
|
+
targetArtifactDigests?: string[];
|
|
53
|
+
purgeActions?: Array<(typeof PURGE_ACTIONS)[number]>;
|
|
54
|
+
recordedAt?: number;
|
|
55
|
+
producerVersion?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeAll(fd: number, bytes: Uint8Array): void {
|
|
59
|
+
let offset = 0;
|
|
60
|
+
while (offset < bytes.byteLength) {
|
|
61
|
+
const n = writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
62
|
+
if (n <= 0) throw new PurgeError("short_write", "ledger rewrite short write");
|
|
63
|
+
offset += n;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
68
|
+
const body = events.map((e) => jcsStringify(e)).join("\n") + (events.length ? "\n" : "");
|
|
69
|
+
const bytes = new TextEncoder().encode(body);
|
|
70
|
+
const parent = dirname(ledgerPath);
|
|
71
|
+
const tmpPath = join(parent, `.purge-${process.pid}-${Date.now()}.jsonl.tmp`);
|
|
72
|
+
let renamed = false;
|
|
73
|
+
try {
|
|
74
|
+
const fd = openSync(tmpPath, "wx", 0o600);
|
|
75
|
+
try {
|
|
76
|
+
writeAll(fd, bytes);
|
|
77
|
+
fsyncSync(fd);
|
|
78
|
+
} finally {
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
}
|
|
81
|
+
renameSync(tmpPath, ledgerPath);
|
|
82
|
+
renamed = true;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (!renamed) {
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(tmpPath);
|
|
87
|
+
} catch {
|
|
88
|
+
// Preserve the original failure.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.platform !== "win32") {
|
|
95
|
+
try {
|
|
96
|
+
const dirFd = openSync(parent, "r");
|
|
97
|
+
try {
|
|
98
|
+
fsyncSync(dirFd);
|
|
99
|
+
} finally {
|
|
100
|
+
closeSync(dirFd);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// The rename is already committed and visible. Report durability failure
|
|
104
|
+
// without pretending the ledger action can be rolled back.
|
|
105
|
+
throw new PurgeError(
|
|
106
|
+
"ledger_durability_failed",
|
|
107
|
+
`ledger rewrite committed but directory fsync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
["ledger"],
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function deleteArtifactsFailClosed(dir: TrustedArtifactDir, digests: string[]): void {
|
|
115
|
+
const errors: string[] = [];
|
|
116
|
+
for (const digest of digests) {
|
|
117
|
+
try {
|
|
118
|
+
deleteArtifactBytes(dir, digest);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err instanceof ArtifactFsError && err.code === "artifact_missing") continue;
|
|
121
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (errors.length > 0) {
|
|
125
|
+
throw new PurgeError("artifact_delete_failed", errors.join("; "));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function purgeBoundedDirectory(dirPath: string): void {
|
|
130
|
+
if (!existsSync(dirPath)) return;
|
|
131
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const full = join(dirPath, entry.name);
|
|
134
|
+
try {
|
|
135
|
+
rmSync(full, { recursive: entry.isDirectory(), force: true });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new PurgeError(
|
|
138
|
+
"scratch_export_delete_failed",
|
|
139
|
+
err instanceof Error ? err.message : String(err),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Exceptional sensitive-evidence purge:
|
|
147
|
+
* physically remove targeted JSONL lines and artifacts, append purge_tombstone,
|
|
148
|
+
* rebuild SQLite. Fails closed when required sensitive bytes cannot be removed.
|
|
149
|
+
*/
|
|
150
|
+
export function purgeSensitiveEvidence(req: SensitivePurgeRequest): PurgeTombstoneEvent {
|
|
151
|
+
const paths = ensureLabDirs(req.configDir);
|
|
152
|
+
const targetEventIds = [...(req.targetEventIds ?? [])].sort();
|
|
153
|
+
const targetArtifactDigests = [...(req.targetArtifactDigests ?? [])].sort();
|
|
154
|
+
const purgeActions = [...(req.purgeActions ?? PURGE_ACTIONS)].sort();
|
|
155
|
+
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
|
+
const completed: string[] = [];
|
|
192
|
+
try {
|
|
193
|
+
if (purgeActions.includes("scratch")) {
|
|
194
|
+
purgeBoundedDirectory(paths.scratchDir);
|
|
195
|
+
completed.push("scratch");
|
|
196
|
+
}
|
|
197
|
+
if (purgeActions.includes("export")) {
|
|
198
|
+
purgeBoundedDirectory(paths.exportDir);
|
|
199
|
+
completed.push("export");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (purgeActions.includes("artifact")) {
|
|
203
|
+
if (deletionPlan.deletable.length > 0) {
|
|
204
|
+
dir = openTrustedArtifactDir(paths.artifactsDir);
|
|
205
|
+
deleteArtifactsFailClosed(dir, deletionPlan.deletable);
|
|
206
|
+
}
|
|
207
|
+
completed.push("artifact");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (purgeActions.includes("ledger")) {
|
|
211
|
+
const kept: LabEvent[] = [];
|
|
212
|
+
for (const event of replay.events) {
|
|
213
|
+
if (removeIds.has(event.eventId)) continue;
|
|
214
|
+
kept.push(event);
|
|
215
|
+
}
|
|
216
|
+
kept.push(tombstone);
|
|
217
|
+
atomicRewriteLedger(paths.ledgerPath, kept);
|
|
218
|
+
} else {
|
|
219
|
+
appendLabEvent(paths.ledgerPath, tombstone);
|
|
220
|
+
}
|
|
221
|
+
completed.push("ledger");
|
|
222
|
+
|
|
223
|
+
if (purgeActions.includes("sqlite")) {
|
|
224
|
+
rebuildLabProjection(req.configDir);
|
|
225
|
+
completed.push("sqlite");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return tombstone;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err instanceof PurgeError) {
|
|
231
|
+
throw new PurgeError(err.code, err.message, [...completed, ...err.completedActions]);
|
|
232
|
+
}
|
|
233
|
+
throw new PurgeError(
|
|
234
|
+
"purge_failed",
|
|
235
|
+
err instanceof Error ? err.message : String(err),
|
|
236
|
+
completed,
|
|
237
|
+
);
|
|
238
|
+
} finally {
|
|
239
|
+
if (dir) closeTrustedArtifactDir(dir);
|
|
240
|
+
}
|
|
241
|
+
}
|