@bitkyc08/opencodex 2.12.0 → 2.13.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/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
FABRIC_LIMITS,
|
|
4
|
+
FABRIC_VERIFIER_ID,
|
|
5
|
+
SYNTHETIC_AFTER_UTF8,
|
|
6
|
+
SYNTHETIC_BEFORE_UTF8,
|
|
7
|
+
SYNTHETIC_VALUE_PATH,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
import { readScratchFileUtf8, walkScratchFiles } from "./scratch";
|
|
10
|
+
import { verifierManifestDigest } from "./subject";
|
|
11
|
+
import type { ExactTreeDiffResultV1 } from "./types";
|
|
12
|
+
import { FabricTaskError } from "./types";
|
|
13
|
+
|
|
14
|
+
/** SHA-256 hex digest of a UTF-8 string for verifier tree comparisons. */
|
|
15
|
+
function sha256Utf8(value: string): string {
|
|
16
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic exact-tree-diff-v1 verifier. No LLM. Walks only the scratch tree.
|
|
21
|
+
*/
|
|
22
|
+
export function verifyExactTreeDiffV1(scratchRoot: string): ExactTreeDiffResultV1 {
|
|
23
|
+
const manifestDigest = verifierManifestDigest();
|
|
24
|
+
const files = walkScratchFiles(scratchRoot);
|
|
25
|
+
let aggregate = 0;
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
aggregate += file.byteLength;
|
|
28
|
+
if (aggregate > FABRIC_LIMITS.maxAggregateIoBytes) {
|
|
29
|
+
throw new FabricTaskError("tree exceeds io budget", "budget_exhausted", "environment");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (files.length !== 1 || files[0]!.relativePosix !== SYNTHETIC_VALUE_PATH) {
|
|
33
|
+
const unexpected = files
|
|
34
|
+
.filter((row) => row.relativePosix !== SYNTHETIC_VALUE_PATH)
|
|
35
|
+
.map((row) => row.relativePosix);
|
|
36
|
+
return {
|
|
37
|
+
verifierId: FABRIC_VERIFIER_ID,
|
|
38
|
+
manifestDigest,
|
|
39
|
+
passed: false,
|
|
40
|
+
pathSummaries: [
|
|
41
|
+
...unexpected.map((path) => ({
|
|
42
|
+
path,
|
|
43
|
+
kind: "added" as const,
|
|
44
|
+
reason: "unexpected_file",
|
|
45
|
+
})),
|
|
46
|
+
...(files.some((row) => row.relativePosix === SYNTHETIC_VALUE_PATH)
|
|
47
|
+
? []
|
|
48
|
+
: [{ path: SYNTHETIC_VALUE_PATH, kind: "deleted" as const, reason: "missing_required_file" }]),
|
|
49
|
+
],
|
|
50
|
+
reason: files.length === 0 ? "deleted_required_file" : "unexpected_tree_shape",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const after = readScratchFileUtf8(scratchRoot, SYNTHETIC_VALUE_PATH, FABRIC_LIMITS.maxAggregateIoBytes);
|
|
54
|
+
const beforeDigest = sha256Utf8(SYNTHETIC_BEFORE_UTF8);
|
|
55
|
+
const afterDigest = sha256Utf8(after);
|
|
56
|
+
const expectedDigest = sha256Utf8(SYNTHETIC_AFTER_UTF8);
|
|
57
|
+
if (after === SYNTHETIC_BEFORE_UTF8) {
|
|
58
|
+
return {
|
|
59
|
+
verifierId: FABRIC_VERIFIER_ID,
|
|
60
|
+
manifestDigest,
|
|
61
|
+
passed: false,
|
|
62
|
+
pathSummaries: [{
|
|
63
|
+
path: SYNTHETIC_VALUE_PATH,
|
|
64
|
+
kind: "unchanged",
|
|
65
|
+
beforeDigest,
|
|
66
|
+
afterDigest,
|
|
67
|
+
reason: "unchanged",
|
|
68
|
+
}],
|
|
69
|
+
reason: "unchanged_file",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (after !== SYNTHETIC_AFTER_UTF8) {
|
|
73
|
+
return {
|
|
74
|
+
verifierId: FABRIC_VERIFIER_ID,
|
|
75
|
+
manifestDigest,
|
|
76
|
+
passed: false,
|
|
77
|
+
pathSummaries: [{
|
|
78
|
+
path: SYNTHETIC_VALUE_PATH,
|
|
79
|
+
kind: "modified",
|
|
80
|
+
beforeDigest,
|
|
81
|
+
afterDigest,
|
|
82
|
+
reason: "wrong_final_bytes",
|
|
83
|
+
}],
|
|
84
|
+
reason: `expected_digest_${expectedDigest}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
verifierId: FABRIC_VERIFIER_ID,
|
|
89
|
+
manifestDigest,
|
|
90
|
+
passed: true,
|
|
91
|
+
pathSummaries: [{
|
|
92
|
+
path: SYNTHETIC_VALUE_PATH,
|
|
93
|
+
kind: "modified",
|
|
94
|
+
beforeDigest,
|
|
95
|
+
afterDigest,
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/lab/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./ledger/artifact-refs";
|
|
|
18
18
|
export * from "./projection/verification";
|
|
19
19
|
export * from "./observe/from-conformance";
|
|
20
20
|
export * from "./observe/from-live";
|
|
21
|
+
export * from "./fabric";
|
|
21
22
|
export * from "./live/manifest";
|
|
22
23
|
export * from "./live/runner";
|
|
23
24
|
export * from "./live/executor";
|
|
@@ -34,3 +35,4 @@ export * from "./subject/behavior-fingerprint";
|
|
|
34
35
|
export * from "./subject/installation-salt";
|
|
35
36
|
export { CL03_LIVE_SUITES } from "./conformance/types";
|
|
36
37
|
export * from "./query";
|
|
38
|
+
export * from "./automation";
|
package/src/lab/ledger/store.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
closeSync,
|
|
3
|
+
constants as fsConstants,
|
|
3
4
|
existsSync,
|
|
4
5
|
fsyncSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
openSync,
|
|
8
|
+
readFileSync,
|
|
7
9
|
readSync,
|
|
8
10
|
statSync,
|
|
11
|
+
unlinkSync,
|
|
9
12
|
writeSync,
|
|
10
13
|
} from "node:fs";
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
11
15
|
import { dirname } from "node:path";
|
|
12
16
|
import { jcsStringify } from "../digest";
|
|
13
17
|
import { MAX_SERIALIZED_EVENT_BYTES } from "../constants";
|
|
@@ -21,6 +25,153 @@ export interface LedgerStore {
|
|
|
21
25
|
replay(): ReplayResult;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
const eventIdIndexByLedger = new Map<string, Set<string>>();
|
|
29
|
+
const LEDGER_LOCK_STALE_MS = 60_000;
|
|
30
|
+
const LEDGER_LOCK_WAIT_MS = 5_000;
|
|
31
|
+
|
|
32
|
+
interface LedgerLockMeta {
|
|
33
|
+
pid: number;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
token: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Block synchronously for the given duration (ledger lock retry only). */
|
|
39
|
+
function sleepSyncMs(ms: number): void {
|
|
40
|
+
const end = Date.now() + ms;
|
|
41
|
+
while (Date.now() < end) {
|
|
42
|
+
/* spin */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read pid, createdAt, and token metadata from a ledger lock file, if well-formed. */
|
|
47
|
+
function readLedgerLockMeta(lockPath: string): LedgerLockMeta | null {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(lockPath, "utf8")) as LedgerLockMeta;
|
|
50
|
+
if (
|
|
51
|
+
typeof parsed.pid === "number"
|
|
52
|
+
&& typeof parsed.createdAt === "number"
|
|
53
|
+
&& typeof parsed.token === "string"
|
|
54
|
+
&& parsed.token.length > 0
|
|
55
|
+
) {
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* ignore */
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Return true when the lock holder process is still running. */
|
|
65
|
+
function isLockHolderAlive(pid: number): boolean {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
71
|
+
if (code === "ESRCH") return false;
|
|
72
|
+
if (code === "EPERM") return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Return true when a ledger lock file has dead metadata and can be recovered. */
|
|
78
|
+
function isLedgerLockStale(lockPath: string): boolean {
|
|
79
|
+
const meta = readLedgerLockMeta(lockPath);
|
|
80
|
+
if (!meta) {
|
|
81
|
+
try {
|
|
82
|
+
return Date.now() - statSync(lockPath).mtimeMs > LEDGER_LOCK_STALE_MS;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (isLockHolderAlive(meta.pid)) return false;
|
|
88
|
+
return Date.now() - meta.createdAt > LEDGER_LOCK_STALE_MS;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create a ledger lock file exclusively, recovering stale locks when needed. */
|
|
92
|
+
function tryAcquireLedgerLock(lockPath: string, deadline: number): { fd: number; token: string } {
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const token = randomBytes(16).toString("hex");
|
|
95
|
+
let fd: number;
|
|
96
|
+
try {
|
|
97
|
+
fd = openSync(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (existsSync(lockPath) && isLedgerLockStale(lockPath)) {
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(lockPath);
|
|
102
|
+
} catch (unlinkError) {
|
|
103
|
+
if (Date.now() >= deadline) throw unlinkError;
|
|
104
|
+
sleepSyncMs(10);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (Date.now() >= deadline) throw error;
|
|
109
|
+
sleepSyncMs(10);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const metadataBytes = Buffer.from(JSON.stringify({ pid: process.pid, createdAt: Date.now(), token }), "utf8");
|
|
114
|
+
const written = writeSync(fd, metadataBytes);
|
|
115
|
+
if (written !== metadataBytes.byteLength) {
|
|
116
|
+
throw new LabValidationError("short_write", "ledger lock metadata write incomplete");
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
closeSync(fd);
|
|
120
|
+
try {
|
|
121
|
+
unlinkSync(lockPath);
|
|
122
|
+
} catch {
|
|
123
|
+
/* best-effort */
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return { fd, token };
|
|
128
|
+
}
|
|
129
|
+
throw new Error("ledger lock acquisition timed out");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Release a ledger lock only when the token still matches the lock file. */
|
|
133
|
+
function releaseLedgerLock(lockPath: string, lockFd: number, token: string): void {
|
|
134
|
+
try {
|
|
135
|
+
closeSync(lockFd);
|
|
136
|
+
} catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const meta = readLedgerLockMeta(lockPath);
|
|
141
|
+
if (meta?.token === token) unlinkSync(lockPath);
|
|
142
|
+
} catch {
|
|
143
|
+
/* best-effort */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Run a ledger mutation while holding the compatibility ledger lock file. */
|
|
148
|
+
function withLedgerLock<T>(ledgerPath: string, fn: () => T): T {
|
|
149
|
+
const lockPath = `${ledgerPath}.lock`;
|
|
150
|
+
mkdirSync(dirname(ledgerPath), { recursive: true, mode: 0o700 });
|
|
151
|
+
const deadline = Date.now() + LEDGER_LOCK_WAIT_MS;
|
|
152
|
+
const { fd, token } = tryAcquireLedgerLock(lockPath, deadline);
|
|
153
|
+
try {
|
|
154
|
+
return fn();
|
|
155
|
+
} finally {
|
|
156
|
+
releaseLedgerLock(lockPath, fd, token);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Load or build the in-memory event-id index for a ledger path. */
|
|
161
|
+
function loadEventIdIndex(ledgerPath: string): Set<string> {
|
|
162
|
+
let index = eventIdIndexByLedger.get(ledgerPath);
|
|
163
|
+
if (!index) {
|
|
164
|
+
index = new Set();
|
|
165
|
+
if (existsSync(ledgerPath)) {
|
|
166
|
+
for (const event of replayLabLedger(ledgerPath).events) {
|
|
167
|
+
index.add(event.eventId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
eventIdIndexByLedger.set(ledgerPath, index);
|
|
171
|
+
}
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
|
|
24
175
|
/** Durable append of one validated event as a single JSONL line + fsync. */
|
|
25
176
|
export function appendLabEvent(ledgerPath: string, event: LabEvent): void {
|
|
26
177
|
const validated = validateLabEvent(event);
|
|
@@ -41,6 +192,28 @@ export function appendLabEvent(ledgerPath: string, event: LabEvent): void {
|
|
|
41
192
|
} finally {
|
|
42
193
|
closeSync(fd);
|
|
43
194
|
}
|
|
195
|
+
loadEventIdIndex(ledgerPath).add(validated.eventId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Append only when eventId is absent. Uses an exclusive lock file plus a
|
|
200
|
+
* process-local event-id index refreshed under the lock.
|
|
201
|
+
*/
|
|
202
|
+
export function appendLabEventIfAbsent(ledgerPath: string, event: LabEvent): boolean {
|
|
203
|
+
const validated = validateLabEvent(event);
|
|
204
|
+
return withLedgerLock(ledgerPath, () => {
|
|
205
|
+
// Refresh from disk under the lock so concurrent writers are visible.
|
|
206
|
+
const fresh = new Set<string>();
|
|
207
|
+
if (existsSync(ledgerPath)) {
|
|
208
|
+
for (const row of replayLabLedger(ledgerPath).events) {
|
|
209
|
+
fresh.add(row.eventId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
eventIdIndexByLedger.set(ledgerPath, fresh);
|
|
213
|
+
if (fresh.has(validated.eventId)) return false;
|
|
214
|
+
appendLabEvent(ledgerPath, validated);
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
44
217
|
}
|
|
45
218
|
|
|
46
219
|
function processLine(
|
package/src/lab/live/executor.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface LiveExecutorOptions {
|
|
|
21
21
|
resolve?: DnsResolver;
|
|
22
22
|
configDir?: string;
|
|
23
23
|
env?: NodeJS.ProcessEnv;
|
|
24
|
+
/** Orchestration cancellation — must not produce route incompatibility evidence. */
|
|
25
|
+
cancelSignal?: AbortSignal;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface TrustedLiveResultReceipt {
|
|
@@ -143,6 +145,11 @@ function pathForProtocol(protocol: string): string {
|
|
|
143
145
|
return "/responses";
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/** Upstream HTTP path for a trusted live-route request (CL-03 / CL-08 production dispatch). */
|
|
149
|
+
export function liveUpstreamRequestPath(protocol: string): string {
|
|
150
|
+
return pathForProtocol(protocol);
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
function chatObservation(body: string, status: number): NormalizedObservation {
|
|
147
154
|
const observation = emptyObservation();
|
|
148
155
|
const output: unknown[] = [];
|
|
@@ -215,15 +222,33 @@ function normalizeTransportObservation(route: LabRouteContext, response: { statu
|
|
|
215
222
|
return observation;
|
|
216
223
|
}
|
|
217
224
|
|
|
218
|
-
|
|
225
|
+
/** Normalize a pinned provider response into a Lab observation (trusted-route production path). */
|
|
226
|
+
export function normalizeLabLiveTransportObservation(
|
|
227
|
+
route: LabRouteContext,
|
|
228
|
+
response: { status: number; body: string },
|
|
229
|
+
): NormalizedObservation {
|
|
230
|
+
return normalizeTransportObservation(route, response);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function withTotalTimeout<T>(
|
|
234
|
+
timeoutMs: number,
|
|
235
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
236
|
+
cancelSignal?: AbortSignal,
|
|
237
|
+
): Promise<T> {
|
|
238
|
+
if (cancelSignal?.aborted) throw cancelSignal.reason ?? new TransportError("harness_failure", "cancelled");
|
|
219
239
|
const controller = new AbortController();
|
|
240
|
+
const abortFromCancel = () => controller.abort(cancelSignal?.reason ?? new TransportError("harness_failure", "cancelled"));
|
|
241
|
+
if (cancelSignal) cancelSignal.addEventListener("abort", abortFromCancel, { once: true });
|
|
220
242
|
const timer = setTimeout(() => controller.abort(new TransportError("total_timeout", "live scenario total timeout")), timeoutMs);
|
|
221
243
|
try {
|
|
222
244
|
return await Promise.race([
|
|
223
245
|
run(controller.signal),
|
|
224
246
|
new Promise<T>((_, reject) => controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true })),
|
|
225
247
|
]);
|
|
226
|
-
} finally {
|
|
248
|
+
} finally {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
if (cancelSignal) cancelSignal.removeEventListener("abort", abortFromCancel);
|
|
251
|
+
}
|
|
227
252
|
}
|
|
228
253
|
|
|
229
254
|
export async function runLiveScenario(caseRecord: CaseRecord, routeContext: LabRouteContext, opts: LiveExecutorOptions = {}): Promise<LiveScenarioRunResult> {
|
|
@@ -266,13 +291,13 @@ export async function runLiveScenario(caseRecord: CaseRecord, routeContext: LabR
|
|
|
266
291
|
if (!isTrustedLabRouteExecutor(opts.routeExecutor)) throw new TransportError("untrusted_route_executor", "untrusted route executor capability");
|
|
267
292
|
executionAuthority = "trusted_route";
|
|
268
293
|
trustedExecutionStarted = true;
|
|
269
|
-
observation = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.routeExecutor!.execute({ routeContext, destination, routeSubject: routeSubject!, scenarioId: activeCase.id, initiatingRequest: activeCase.initiatingRequest?.bytesUtf8, limits, signal, environment }));
|
|
294
|
+
observation = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.routeExecutor!.execute({ routeContext, destination, routeSubject: routeSubject!, scenarioId: activeCase.id, initiatingRequest: activeCase.initiatingRequest?.bytesUtf8, limits, signal, environment }), opts.cancelSignal);
|
|
270
295
|
} else if (opts.transport && activeCase.initiatingRequest) {
|
|
271
296
|
executionAuthority = "test_transport";
|
|
272
297
|
const body = activeCase.initiatingRequest.bytesUtf8;
|
|
273
298
|
const inputBytes = new TextEncoder().encode(body).byteLength;
|
|
274
299
|
enforceSandboxLimits(state, limits, { requests: 1, inputBytes });
|
|
275
|
-
const response = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.transport!.request({ method: "POST", path: pathForProtocol(routeContext.upstreamProtocol), body, signal }));
|
|
300
|
+
const response = await withTotalTimeout(limits.totalTimeoutMs, (signal) => opts.transport!.request({ method: "POST", path: pathForProtocol(routeContext.upstreamProtocol), body, signal }), opts.cancelSignal);
|
|
276
301
|
if (response.status === 401 || response.status === 403) throw new TransportError("auth_blocked", `HTTP ${response.status}`);
|
|
277
302
|
if (response.status === 429) throw new TransportError("quota_blocked", "HTTP 429");
|
|
278
303
|
if (response.status === 451) throw new TransportError("region_blocked", "HTTP 451");
|
|
@@ -104,8 +104,10 @@ export function classifyTransportError(error: unknown): { classification: Failur
|
|
|
104
104
|
case "network_blocked": return { classification: "network_failure", secondaryCode: code };
|
|
105
105
|
case "region_blocked": return { classification: "region_blocked", secondaryCode: code };
|
|
106
106
|
case "provider_transient": return { classification: "provider_transient", secondaryCode: code };
|
|
107
|
-
case "connect_timeout": case "first_byte_timeout": case "
|
|
107
|
+
case "connect_timeout": case "first_byte_timeout": case "total_timeout":
|
|
108
108
|
return { classification: "timeout", secondaryCode: code };
|
|
109
|
+
case "inactivity_timeout":
|
|
110
|
+
return { classification: "inactivity_timeout", secondaryCode: code };
|
|
109
111
|
case "request_limit": case "input_byte_limit": case "output_byte_limit": case "output_token_limit":
|
|
110
112
|
case "tool_call_limit": case "artifact_byte_limit": case "memory_limit": case "child_process_limit":
|
|
111
113
|
return { classification: "budget_exhausted", secondaryCode: code };
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Persistence seam: transform CL-01 conformance results into valid observation events.
|
|
3
3
|
* Deterministic harness execution stays separate from ledger persistence.
|
|
4
4
|
*/
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
5
|
import { createArtifactStore, type ArtifactStore } from "../artifacts/store";
|
|
7
6
|
import { sanitizeDiagnostic, truncateUtf8 } from "../artifacts/sanitize";
|
|
8
7
|
import {
|
|
@@ -12,12 +11,11 @@ import {
|
|
|
12
11
|
type ObservationOutcome,
|
|
13
12
|
} from "../constants";
|
|
14
13
|
import {
|
|
15
|
-
jcsStringify,
|
|
16
14
|
scenarioManifestDigest,
|
|
17
15
|
subjectIdForSubject,
|
|
18
16
|
suiteManifestDigest,
|
|
19
17
|
} from "../digest";
|
|
20
|
-
import type { ObservationEvent
|
|
18
|
+
import type { ObservationEvent } from "../events/types";
|
|
21
19
|
import { assignEventId } from "../events/validate";
|
|
22
20
|
import { appendLabEvent } from "../ledger/store";
|
|
23
21
|
import { ensureLabDirs } from "../paths";
|
|
@@ -31,8 +29,10 @@ import { expandScenario } from "../conformance/manifest";
|
|
|
31
29
|
import { suiteManifestObjectForCase } from "../conformance/suite-manifest";
|
|
32
30
|
import { fixtureDigest } from "../conformance/digest";
|
|
33
31
|
import { resolveProtocolExecutionContext } from "../conformance/executor";
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
import {
|
|
33
|
+
buildProtocolSubjectV1,
|
|
34
|
+
protocolBehaviorFingerprintV1,
|
|
35
|
+
} from "../subject/protocol-subject";
|
|
36
36
|
|
|
37
37
|
export interface PersistConformanceOptions {
|
|
38
38
|
configDir?: string;
|
|
@@ -75,71 +75,24 @@ function behaviorFingerprintForCase(
|
|
|
75
75
|
caseRecord,
|
|
76
76
|
executionContext ?? resolveProtocolExecutionContext(caseRecord),
|
|
77
77
|
);
|
|
78
|
-
|
|
79
|
-
const values = {
|
|
80
|
-
schemaVersion: 1,
|
|
81
|
-
resolverVersion: 1,
|
|
82
|
-
values: {
|
|
83
|
-
"wire.adapter": {
|
|
84
|
-
source: "lab_forced",
|
|
85
|
-
value: adapter,
|
|
86
|
-
},
|
|
87
|
-
"wire.upstreamProtocol": {
|
|
88
|
-
source: "lab_forced",
|
|
89
|
-
value: ctx.upstreamProtocol,
|
|
90
|
-
},
|
|
91
|
-
"runtime.arch": {
|
|
92
|
-
source: "lab_forced",
|
|
93
|
-
value: process.arch,
|
|
94
|
-
},
|
|
95
|
-
"runtime.bunVersion": {
|
|
96
|
-
source: "lab_forced",
|
|
97
|
-
value: process.versions.bun ?? Bun.version,
|
|
98
|
-
},
|
|
99
|
-
"runtime.platform": {
|
|
100
|
-
source: "lab_forced",
|
|
101
|
-
value: process.platform,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
return createHash("sha256").update(jcsStringify(values)).digest("hex");
|
|
78
|
+
return protocolBehaviorFingerprintV1(ctx);
|
|
106
79
|
}
|
|
107
80
|
|
|
108
|
-
function protocolSubject(caseRecord: CaseRecord, result: ScenarioRunResult)
|
|
81
|
+
function protocolSubject(caseRecord: CaseRecord, result: ScenarioRunResult) {
|
|
109
82
|
const ctx = validateExecutionContext(
|
|
110
83
|
caseRecord,
|
|
111
84
|
result.executionContext ?? resolveProtocolExecutionContext(caseRecord),
|
|
112
85
|
);
|
|
113
|
-
return
|
|
114
|
-
subjectSchemaVersion: 1,
|
|
115
|
-
subjectKind: "protocol",
|
|
116
|
-
opencodexCompatibilityVersion: COMPAT_VERSION,
|
|
117
|
-
effectiveAdapter: upstreamAdapter(ctx.upstreamProtocol),
|
|
118
|
-
inboundProtocol: ctx.inboundProtocol,
|
|
119
|
-
upstreamProtocol: ctx.upstreamProtocol,
|
|
120
|
-
surface: ctx.surface,
|
|
121
|
-
behaviorFingerprint: behaviorFingerprintForCase(caseRecord, ctx),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function upstreamAdapter(protocol: string): string {
|
|
126
|
-
switch (protocol) {
|
|
127
|
-
case "openai-responses":
|
|
128
|
-
return "openai-responses";
|
|
129
|
-
case "anthropic-messages":
|
|
130
|
-
return "anthropic";
|
|
131
|
-
case "openai-chat":
|
|
132
|
-
return "openai-chat";
|
|
133
|
-
default:
|
|
134
|
-
throw new Error(`unsupported protocol identity: ${protocol}`);
|
|
135
|
-
}
|
|
86
|
+
return buildProtocolSubjectV1(ctx);
|
|
136
87
|
}
|
|
137
88
|
|
|
138
89
|
function outcomeFromResult(result: ScenarioRunResult): ObservationOutcome {
|
|
139
90
|
if (result.passed) return "pass";
|
|
140
91
|
switch (result.classification) {
|
|
141
92
|
case "timeout":
|
|
93
|
+
case "inactivity_timeout":
|
|
142
94
|
case "budget_exhausted":
|
|
95
|
+
case "sandbox_violation":
|
|
143
96
|
case "authentication_blocked":
|
|
144
97
|
case "quota_blocked":
|
|
145
98
|
case "region_blocked":
|
|
@@ -148,6 +101,8 @@ function outcomeFromResult(result: ScenarioRunResult): ObservationOutcome {
|
|
|
148
101
|
return "blocked";
|
|
149
102
|
case "inconclusive":
|
|
150
103
|
case "harness_failure":
|
|
104
|
+
case "malformed_producer_outcome":
|
|
105
|
+
case "layer_subject_mismatch":
|
|
151
106
|
return "inconclusive";
|
|
152
107
|
case "protocol_failure":
|
|
153
108
|
case "capability_failure":
|
|
@@ -20,7 +20,9 @@ function outcomeFromLiveResult(result: LiveScenarioRunResult): ObservationOutcom
|
|
|
20
20
|
if (result.passed) return "pass";
|
|
21
21
|
switch (result.classification) {
|
|
22
22
|
case "timeout":
|
|
23
|
+
case "inactivity_timeout":
|
|
23
24
|
case "budget_exhausted":
|
|
25
|
+
case "sandbox_violation":
|
|
24
26
|
case "authentication_blocked":
|
|
25
27
|
case "quota_blocked":
|
|
26
28
|
case "region_blocked":
|
|
@@ -33,7 +35,13 @@ function outcomeFromLiveResult(result: LiveScenarioRunResult): ObservationOutcom
|
|
|
33
35
|
return "fail";
|
|
34
36
|
case "inconclusive":
|
|
35
37
|
case "harness_failure":
|
|
38
|
+
case "malformed_producer_outcome":
|
|
39
|
+
case "layer_subject_mismatch":
|
|
36
40
|
return "inconclusive";
|
|
41
|
+
default: {
|
|
42
|
+
const _never: never = result.classification;
|
|
43
|
+
throw new Error(`unmapped failure classification: ${String(_never)}`);
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -41,7 +49,7 @@ function failureFromLiveResult(result: LiveScenarioRunResult, retryable: boolean
|
|
|
41
49
|
if (result.passed || result.classification === "inconclusive") return undefined;
|
|
42
50
|
const attribution: FailureRecordV1["attribution"] = result.classification === "harness_failure"
|
|
43
51
|
? "harness"
|
|
44
|
-
: ["authentication_blocked", "quota_blocked", "region_blocked", "network_failure", "provider_transient", "timeout", "budget_exhausted"].includes(result.classification)
|
|
52
|
+
: ["authentication_blocked", "quota_blocked", "region_blocked", "network_failure", "provider_transient", "timeout", "inactivity_timeout", "budget_exhausted"].includes(result.classification)
|
|
45
53
|
? "environment"
|
|
46
54
|
: "route";
|
|
47
55
|
return {
|
package/src/lab/paths.ts
CHANGED
|
@@ -1,12 +1,63 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync,
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
5
|
+
/** Return true when path is the Lab root or a descendant path component. */
|
|
6
|
+
function isAtOrBelowLabBoundary(path: string, labRootBoundary: string): boolean {
|
|
7
|
+
const normalizedPath = resolve(path);
|
|
8
|
+
const normalizedBoundary = resolve(labRootBoundary);
|
|
9
|
+
if (normalizedPath === normalizedBoundary) return true;
|
|
10
|
+
const prefix = normalizedBoundary + sep;
|
|
11
|
+
return normalizedPath.startsWith(prefix) && normalizedPath.length > normalizedBoundary.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Reject symlink, non-directory, and reparse-point substitutions for a Lab-owned component. */
|
|
15
|
+
function assertRestrictedDirectoryComponent(path: string): void {
|
|
16
|
+
const componentStats = lstatSync(path);
|
|
17
|
+
if (componentStats.isSymbolicLink()) {
|
|
18
|
+
throw new Error(`restricted directory component is a symbolic link: ${path}`);
|
|
19
|
+
}
|
|
20
|
+
if (!componentStats.isDirectory()) {
|
|
21
|
+
throw new Error(`restricted path component is not a directory: ${path}`);
|
|
22
|
+
}
|
|
23
|
+
const canonical = resolve(realpathSync.native(path));
|
|
24
|
+
const canonicalParent = resolve(realpathSync.native(dirname(path)));
|
|
25
|
+
const expectedCanonical = resolve(join(canonicalParent, basename(path)));
|
|
26
|
+
if (canonical !== expectedCanonical) {
|
|
27
|
+
throw new Error(`restricted directory component contains a link or reparse-point substitution: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create (or harden) a directory to mode 0o700 without following symlinks on Lab-owned
|
|
33
|
+
* components. Ancestors above labRootBoundary are treated as infrastructure and are not
|
|
34
|
+
* inspected for symlinks (for example macOS /var → /private/var).
|
|
35
|
+
*/
|
|
36
|
+
export function ensureRestrictedDir(dir: string, labRootBoundary: string): void {
|
|
37
|
+
const abs = resolve(dir);
|
|
38
|
+
const boundary = resolve(labRootBoundary);
|
|
39
|
+
const parts = abs.split(sep);
|
|
40
|
+
let current = parts[0] === "" ? sep : parts[0]!;
|
|
41
|
+
for (const part of parts.slice(1)) {
|
|
42
|
+
if (part === "") continue;
|
|
43
|
+
current = join(current, part);
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(current, { mode: 0o700 });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
48
|
+
if (code !== "EEXIST") throw error;
|
|
49
|
+
}
|
|
50
|
+
if (isAtOrBelowLabBoundary(current, boundary)) {
|
|
51
|
+
assertRestrictedDirectoryComponent(current);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!existsSync(abs)) return;
|
|
55
|
+
if (!isAtOrBelowLabBoundary(abs, boundary)) return;
|
|
56
|
+
assertRestrictedDirectoryComponent(abs);
|
|
57
|
+
if (process.platform !== "win32") {
|
|
58
|
+
const mode = lstatSync(abs).mode & 0o777;
|
|
59
|
+
if (mode !== 0o700) chmodSync(abs, 0o700);
|
|
60
|
+
}
|
|
10
61
|
}
|
|
11
62
|
|
|
12
63
|
/** Canonical Compatibility Lab state root under the OpenCodex config dir. */
|
|
@@ -39,6 +90,18 @@ export function labInstallationSaltPath(configDir = getConfigDir()): string {
|
|
|
39
90
|
return join(labRoot(configDir), "installation-salt.bin");
|
|
40
91
|
}
|
|
41
92
|
|
|
93
|
+
export function labAutomationPolicyPath(configDir = getConfigDir()): string {
|
|
94
|
+
return join(labRoot(configDir), "automation-policy.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function labAutomationStatePath(configDir = getConfigDir()): string {
|
|
98
|
+
return join(labRoot(configDir), "automation-state.json");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function labAutomationRoutesPath(configDir = getConfigDir()): string {
|
|
102
|
+
return join(labRoot(configDir), "automation-routes.json");
|
|
103
|
+
}
|
|
104
|
+
|
|
42
105
|
/** Ensure lab directories exist with restrictive permissions where the platform allows. */
|
|
43
106
|
export function ensureLabDirs(configDir = getConfigDir()): {
|
|
44
107
|
root: string;
|
|
@@ -52,8 +115,10 @@ export function ensureLabDirs(configDir = getConfigDir()): {
|
|
|
52
115
|
const artifactsDir = labArtifactsDir(configDir);
|
|
53
116
|
const scratchDir = labScratchDir(configDir);
|
|
54
117
|
const exportDir = labExportDir(configDir);
|
|
55
|
-
ensureRestrictedDir(root);
|
|
56
|
-
ensureRestrictedDir(artifactsDir);
|
|
118
|
+
ensureRestrictedDir(root, root);
|
|
119
|
+
ensureRestrictedDir(artifactsDir, root);
|
|
120
|
+
ensureRestrictedDir(scratchDir, root);
|
|
121
|
+
ensureRestrictedDir(exportDir, root);
|
|
57
122
|
return {
|
|
58
123
|
root,
|
|
59
124
|
ledgerPath: labLedgerPath(configDir),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Disposable SQLite projection schema for Compatibility Lab (CL-02).
|
|
3
3
|
* Not canonical storage — rebuildable from JSONL + content-addressed artifacts.
|
|
4
4
|
*/
|
|
5
|
-
export const LAB_SQLITE_SCHEMA_VERSION =
|
|
5
|
+
export const LAB_SQLITE_SCHEMA_VERSION = 3;
|
|
6
6
|
|
|
7
7
|
export const LAB_SQLITE_DDL = `
|
|
8
8
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
@@ -50,6 +50,19 @@ CREATE INDEX IF NOT EXISTS idx_observations_proj ON observations(
|
|
|
50
50
|
subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest, completed_at
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_observations_exact_identity ON observations(
|
|
54
|
+
evidence_layer,
|
|
55
|
+
subject_id,
|
|
56
|
+
suite_id,
|
|
57
|
+
suite_version,
|
|
58
|
+
suite_manifest_digest,
|
|
59
|
+
scenario_id,
|
|
60
|
+
scenario_version,
|
|
61
|
+
scenario_manifest_digest,
|
|
62
|
+
completed_at DESC,
|
|
63
|
+
event_id DESC
|
|
64
|
+
);
|
|
65
|
+
|
|
53
66
|
CREATE TABLE IF NOT EXISTS claims (
|
|
54
67
|
event_id TEXT PRIMARY KEY,
|
|
55
68
|
subject_id TEXT NOT NULL,
|