@fitlab-ai/agent-infra 0.11.11 → 0.11.13
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/dist/bin/internal-cli.js +2 -2
- package/dist/lib/agent-clients/adapter.js +7 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
- package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
- package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
- package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
- package/dist/lib/agent-clients/adapters/codex.js +40 -1
- package/dist/lib/agent-clients/adapters/index.js +2 -10
- package/dist/lib/agent-clients/tokens.js +3 -2
- package/dist/lib/internal/agent-client.js +132 -72
- package/dist/lib/internal/cli-route-inventory.js +2 -2
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -1
- package/dist/lib/internal/task-validate.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/issues.js +18 -6
- package/dist/lib/runtime/agent-runtime.js +6 -29
- package/dist/lib/sandbox/commands/control-status.js +1 -0
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/agent-operation.js +34 -0
- package/dist/lib/sandbox/control/client.js +14 -201
- package/dist/lib/sandbox/control/executor.js +12 -125
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +15 -54
- package/dist/lib/sandbox/control/server.js +216 -281
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +9 -1
- package/dist/lib/sandbox/recovery.js +31 -10
- package/dist/lib/sandbox/removal.js +28 -5
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +56 -19
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +56 -264
- package/dist/lib/task/control-recovery.js +2 -3
- package/dist/lib/task/create.js +2 -2
- package/dist/lib/task/delegation-receipts.js +74 -161
- package/dist/lib/task/events.js +88 -44
- package/dist/lib/task/finalization.js +35 -6
- package/dist/lib/task/lifecycle.js +274 -115
- package/dist/lib/task/orchestration.js +201 -194
- package/dist/lib/task/runtime-paths.js +10 -0
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +32 -26
- package/dist/lib/task/workspace-snapshot.js +1 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +39 -0
- package/lib/agent-clients/adapters/claude-code.ts +5 -1
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
- package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
- package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
- package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
- package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
- package/lib/agent-clients/adapters/codex.ts +44 -1
- package/lib/agent-clients/adapters/index.ts +7 -20
- package/lib/agent-clients/tokens.ts +4 -3
- package/lib/internal/agent-client.ts +142 -73
- package/lib/internal/cli-route-inventory.ts +2 -2
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -1
- package/lib/internal/task-validate.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/issues.ts +18 -6
- package/lib/runtime/agent-runtime.ts +6 -41
- package/lib/sandbox/commands/control-status.ts +1 -0
- package/lib/sandbox/commands/create.ts +7 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/agent-operation.ts +49 -0
- package/lib/sandbox/control/client.ts +20 -254
- package/lib/sandbox/control/executor.ts +13 -136
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +22 -66
- package/lib/sandbox/control/server.ts +21 -95
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +10 -1
- package/lib/sandbox/recovery.ts +38 -17
- package/lib/sandbox/removal.ts +33 -7
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +50 -18
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +66 -378
- package/lib/task/control-recovery.ts +3 -4
- package/lib/task/create.ts +3 -3
- package/lib/task/delegation-receipts.ts +87 -231
- package/lib/task/events.ts +100 -42
- package/lib/task/finalization.ts +37 -7
- package/lib/task/lifecycle.ts +257 -109
- package/lib/task/orchestration.ts +260 -237
- package/lib/task/runtime-paths.ts +12 -0
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +30 -25
- package/lib/task/workspace-snapshot.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/next-step-output.en.md +6 -19
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
- package/dist/lib/internal/codex-lifecycle.js +0 -385
- package/dist/lib/task/codex-orchestration.js +0 -206
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
- package/lib/internal/codex-lifecycle.ts +0 -390
- package/lib/task/codex-orchestration.ts +0 -276
|
@@ -1,776 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
isCanonicalLifecyclePackageVersion,
|
|
7
|
-
isLifecycleProtocolVersion,
|
|
8
|
-
LIFECYCLE_PROTOCOL_VERSION,
|
|
9
|
-
verifyLifecycleBuildIdentity
|
|
10
|
-
} from './build-identity.ts';
|
|
11
|
-
import type { LifecycleBuildIdentity } from './build-identity.ts';
|
|
12
|
-
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
13
|
-
|
|
14
|
-
type CapabilityStatus = 'armed' | 'attested' | 'consumed' | 'expired';
|
|
15
|
-
type RecoveryState = 'unreserved' | 'reserved' | 'consumed';
|
|
16
|
-
type RecoveryPhase = 'orchestration.prepare' | 'artifact.finalize-local' | 'task-event.completed';
|
|
17
|
-
type RecoveryPhaseState = 'issued' | 'consumed';
|
|
18
|
-
type CodexCapabilityRecoveryPhase = Readonly<{
|
|
19
|
-
phase: RecoveryPhase;
|
|
20
|
-
requestId: string;
|
|
21
|
-
state: RecoveryPhaseState;
|
|
22
|
-
}>;
|
|
23
|
-
type CodexControllerBinding = Readonly<{
|
|
24
|
-
instanceDigest: string;
|
|
25
|
-
controlGeneration: string;
|
|
26
|
-
}>;
|
|
27
|
-
type CodexCapabilityProjection = Readonly<
|
|
28
|
-
| { kind: 'absent' }
|
|
29
|
-
| { kind: 'protocol-version'; value: typeof LIFECYCLE_PROTOCOL_VERSION }
|
|
30
|
-
| { kind: 'semver'; value: string }
|
|
31
|
-
| { kind: 'digest-prefix'; value: string }
|
|
32
|
-
| { kind: 'presence'; present: boolean }
|
|
33
|
-
>;
|
|
34
|
-
type CodexCapabilityProvenanceField = Readonly<{
|
|
35
|
-
matches: boolean;
|
|
36
|
-
expected: CodexCapabilityProjection;
|
|
37
|
-
actual: CodexCapabilityProjection;
|
|
38
|
-
}>;
|
|
39
|
-
type CodexCapabilityProvenanceDetail = Readonly<{
|
|
40
|
-
kind: 'codex-capability-provenance-mismatch';
|
|
41
|
-
version: 1;
|
|
42
|
-
fields: Readonly<{
|
|
43
|
-
taskId: CodexCapabilityProvenanceField;
|
|
44
|
-
hookDefinitionHash: CodexCapabilityProvenanceField;
|
|
45
|
-
buildIdentity: Readonly<{
|
|
46
|
-
protocolVersion: CodexCapabilityProvenanceField;
|
|
47
|
-
packageVersion: CodexCapabilityProvenanceField;
|
|
48
|
-
internalExecutableBuildHash: CodexCapabilityProvenanceField;
|
|
49
|
-
lifecycleContractHash: CodexCapabilityProvenanceField;
|
|
50
|
-
}>;
|
|
51
|
-
controller: Readonly<{
|
|
52
|
-
instanceDigest: CodexCapabilityProvenanceField;
|
|
53
|
-
controlGeneration: CodexCapabilityProvenanceField;
|
|
54
|
-
}>;
|
|
55
|
-
}>;
|
|
56
|
-
}>;
|
|
57
|
-
type CodexCapabilityRecord = Readonly<{
|
|
58
|
-
schemaVersion: 2;
|
|
59
|
-
revision: number;
|
|
60
|
-
capabilityRefDigest: string;
|
|
61
|
-
taskId: string;
|
|
62
|
-
status: CapabilityStatus;
|
|
63
|
-
recoveryOperationId: string | null;
|
|
64
|
-
recoveryState: RecoveryState;
|
|
65
|
-
reservedAt: number | null;
|
|
66
|
-
recoveryPhases: readonly CodexCapabilityRecoveryPhase[];
|
|
67
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
68
|
-
controller: CodexControllerBinding | null;
|
|
69
|
-
sessionId: string | null;
|
|
70
|
-
turnId: string | null;
|
|
71
|
-
toolUseId: string | null;
|
|
72
|
-
hookDefinitionHash: string | null;
|
|
73
|
-
armedAt: number;
|
|
74
|
-
expiresAt: number;
|
|
75
|
-
attestedAt: number | null;
|
|
76
|
-
consumedAt: number | null;
|
|
77
|
-
}>;
|
|
78
|
-
|
|
79
|
-
type CodexCapabilityStoreOptions = Readonly<{
|
|
80
|
-
root?: string;
|
|
81
|
-
now?: () => number;
|
|
82
|
-
reference?: () => string;
|
|
83
|
-
ttlMs?: number;
|
|
84
|
-
tombstoneMs?: number;
|
|
85
|
-
maxRecords?: number;
|
|
86
|
-
beforeCompareAndSwap?: (input: Readonly<{ path: string; expectedRevision: number }>) => void;
|
|
87
|
-
}>;
|
|
88
|
-
|
|
89
|
-
const DEFAULT_TTL_MS = 30_000;
|
|
90
|
-
const DEFAULT_TOMBSTONE_MS = 86_400_000;
|
|
91
|
-
const DEFAULT_MAX_RECORDS = 128;
|
|
92
|
-
const LOCK_RETRY_MS = 10;
|
|
93
|
-
const LOCK_TIMEOUT_MS = 1_000;
|
|
94
|
-
const LOCK_STALE_MS = 30_000;
|
|
95
|
-
const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
|
|
96
|
-
const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
|
|
97
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
98
|
-
const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
|
|
99
|
-
|
|
100
|
-
function digest(value: string): string {
|
|
101
|
-
return crypto.createHash('sha256').update(value).digest('hex');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function capabilityError(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): Error {
|
|
105
|
-
const error = new Error(`${code}: ${message}`);
|
|
106
|
-
error.name = code;
|
|
107
|
-
if (detail) (error as Error & { detail: CodexCapabilityProvenanceDetail }).detail = detail;
|
|
108
|
-
return error;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function objectValue(value: unknown, key: string): unknown {
|
|
112
|
-
if (!value || typeof value !== 'object') return undefined;
|
|
113
|
-
return (value as Record<string, unknown>)[key];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function digestPrefix(fieldName: string, value: string): CodexCapabilityProjection {
|
|
117
|
-
return {
|
|
118
|
-
kind: 'digest-prefix',
|
|
119
|
-
value: `sha256:${crypto.createHash('sha256')
|
|
120
|
-
.update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
|
|
121
|
-
.digest('hex')
|
|
122
|
-
.slice(0, 16)}`
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function projectDigest(fieldName: string, value: unknown, pattern: RegExp): CodexCapabilityProjection {
|
|
127
|
-
return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function projectTaskId(value: unknown): CodexCapabilityProjection {
|
|
131
|
-
return projectDigest('taskId', value, TASK_ID_PATTERN);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function projectHash(fieldName: string, value: unknown): CodexCapabilityProjection {
|
|
135
|
-
return projectDigest(fieldName, value, SHA256_PATTERN);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function projectProtocol(value: unknown): CodexCapabilityProjection {
|
|
139
|
-
return isLifecycleProtocolVersion(value)
|
|
140
|
-
? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
|
|
141
|
-
: { kind: 'absent' };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function projectPackageVersion(value: unknown): CodexCapabilityProjection {
|
|
145
|
-
return isCanonicalLifecyclePackageVersion(value)
|
|
146
|
-
? { kind: 'semver', value }
|
|
147
|
-
: { kind: 'absent' };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function projectPresence(value: unknown): CodexCapabilityProjection {
|
|
151
|
-
return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function field(matches: boolean, expected: CodexCapabilityProjection, actual: CodexCapabilityProjection): CodexCapabilityProvenanceField {
|
|
155
|
-
return { matches, expected, actual };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function provenanceDetail(
|
|
159
|
-
record: CodexCapabilityRecord,
|
|
160
|
-
expected: Readonly<{
|
|
161
|
-
taskId: string;
|
|
162
|
-
hookDefinitionHash: string;
|
|
163
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
164
|
-
controller?: CodexControllerBinding;
|
|
165
|
-
}>
|
|
166
|
-
): CodexCapabilityProvenanceDetail {
|
|
167
|
-
const actualBuild = record.buildIdentity;
|
|
168
|
-
const expectedBuild = expected.buildIdentity;
|
|
169
|
-
const actualController = record.controller;
|
|
170
|
-
const expectedController = expected.controller ?? null;
|
|
171
|
-
return {
|
|
172
|
-
kind: 'codex-capability-provenance-mismatch',
|
|
173
|
-
version: 1,
|
|
174
|
-
fields: {
|
|
175
|
-
taskId: field(
|
|
176
|
-
record.taskId === expected.taskId,
|
|
177
|
-
projectTaskId(expected.taskId),
|
|
178
|
-
projectTaskId(record.taskId)
|
|
179
|
-
),
|
|
180
|
-
hookDefinitionHash: field(
|
|
181
|
-
record.hookDefinitionHash === expected.hookDefinitionHash,
|
|
182
|
-
projectHash('hookDefinitionHash', expected.hookDefinitionHash),
|
|
183
|
-
projectHash('hookDefinitionHash', record.hookDefinitionHash)
|
|
184
|
-
),
|
|
185
|
-
buildIdentity: {
|
|
186
|
-
protocolVersion: field(
|
|
187
|
-
objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'),
|
|
188
|
-
projectProtocol(objectValue(expectedBuild, 'protocolVersion')),
|
|
189
|
-
projectProtocol(objectValue(actualBuild, 'protocolVersion'))
|
|
190
|
-
),
|
|
191
|
-
packageVersion: field(
|
|
192
|
-
objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'),
|
|
193
|
-
projectPackageVersion(objectValue(expectedBuild, 'packageVersion')),
|
|
194
|
-
projectPackageVersion(objectValue(actualBuild, 'packageVersion'))
|
|
195
|
-
),
|
|
196
|
-
internalExecutableBuildHash: field(
|
|
197
|
-
objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'),
|
|
198
|
-
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')),
|
|
199
|
-
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))
|
|
200
|
-
),
|
|
201
|
-
lifecycleContractHash: field(
|
|
202
|
-
objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'),
|
|
203
|
-
projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')),
|
|
204
|
-
projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash'))
|
|
205
|
-
)
|
|
206
|
-
},
|
|
207
|
-
controller: {
|
|
208
|
-
instanceDigest: field(
|
|
209
|
-
sameControllerField(actualController, expectedController, 'instanceDigest'),
|
|
210
|
-
projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController),
|
|
211
|
-
projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)
|
|
212
|
-
),
|
|
213
|
-
controlGeneration: field(
|
|
214
|
-
sameControllerField(actualController, expectedController, 'controlGeneration'),
|
|
215
|
-
projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController),
|
|
216
|
-
projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController)
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
224
|
-
return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function isRecoveryPhase(value: unknown): value is RecoveryPhase {
|
|
228
|
-
return value === 'orchestration.prepare'
|
|
229
|
-
|| value === 'artifact.finalize-local'
|
|
230
|
-
|| value === 'task-event.completed';
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isRecoveryPhaseRecord(value: unknown): value is CodexCapabilityRecoveryPhase {
|
|
234
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
235
|
-
const phase = value as Record<string, unknown>;
|
|
236
|
-
return exactKeys(phase, ['phase', 'requestId', 'state'])
|
|
237
|
-
&& isRecoveryPhase(phase.phase)
|
|
238
|
-
&& typeof phase.requestId === 'string'
|
|
239
|
-
&& /^[a-f0-9-]{16,64}$/u.test(phase.requestId)
|
|
240
|
-
&& (phase.state === 'issued' || phase.state === 'consumed');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function isCodexControllerBinding(value: unknown): value is CodexControllerBinding {
|
|
244
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
245
|
-
const record = value as Record<string, unknown>;
|
|
246
|
-
return exactKeys(record, ['instanceDigest', 'controlGeneration'])
|
|
247
|
-
&& typeof record.instanceDigest === 'string'
|
|
248
|
-
&& typeof record.controlGeneration === 'string';
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function isProjection(value: unknown): value is CodexCapabilityProjection {
|
|
252
|
-
if (!value || typeof value !== 'object') return false;
|
|
253
|
-
const record = value as Record<string, unknown>;
|
|
254
|
-
if (record.kind === 'absent') return exactKeys(record, ['kind']);
|
|
255
|
-
if (record.kind === 'protocol-version') {
|
|
256
|
-
return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
|
|
257
|
-
}
|
|
258
|
-
if (record.kind === 'semver') {
|
|
259
|
-
return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
|
|
260
|
-
}
|
|
261
|
-
if (record.kind === 'digest-prefix') {
|
|
262
|
-
return exactKeys(record, ['kind', 'value'])
|
|
263
|
-
&& typeof record.value === 'string'
|
|
264
|
-
&& DIGEST_PREFIX_PATTERN.test(record.value);
|
|
265
|
-
}
|
|
266
|
-
if (record.kind === 'presence') {
|
|
267
|
-
return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
|
|
268
|
-
}
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function isProvenanceField(value: unknown): value is CodexCapabilityProvenanceField {
|
|
273
|
-
if (!value || typeof value !== 'object') return false;
|
|
274
|
-
const record = value as Record<string, unknown>;
|
|
275
|
-
return exactKeys(record, ['matches', 'expected', 'actual'])
|
|
276
|
-
&& typeof record.matches === 'boolean'
|
|
277
|
-
&& isProjection(record.expected)
|
|
278
|
-
&& isProjection(record.actual);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function isCodexCapabilityProvenanceDetail(value: unknown): value is CodexCapabilityProvenanceDetail {
|
|
282
|
-
if (!value || typeof value !== 'object') return false;
|
|
283
|
-
const record = value as Record<string, unknown>;
|
|
284
|
-
if (!exactKeys(record, ['kind', 'version', 'fields'])
|
|
285
|
-
|| record.kind !== 'codex-capability-provenance-mismatch'
|
|
286
|
-
|| record.version !== 1
|
|
287
|
-
|| !record.fields || typeof record.fields !== 'object') return false;
|
|
288
|
-
const fields = record.fields as Record<string, unknown>;
|
|
289
|
-
if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
|
|
290
|
-
|| !isProvenanceField(fields.taskId)
|
|
291
|
-
|| !isProvenanceField(fields.hookDefinitionHash)) return false;
|
|
292
|
-
const buildIdentity = fields.buildIdentity;
|
|
293
|
-
const controller = fields.controller;
|
|
294
|
-
if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object') return false;
|
|
295
|
-
const build = buildIdentity as Record<string, unknown>;
|
|
296
|
-
const binding = controller as Record<string, unknown>;
|
|
297
|
-
return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
|
|
298
|
-
&& exactKeys(binding, ['instanceDigest', 'controlGeneration'])
|
|
299
|
-
&& isProvenanceField(build.protocolVersion)
|
|
300
|
-
&& isProvenanceField(build.packageVersion)
|
|
301
|
-
&& isProvenanceField(build.internalExecutableBuildHash)
|
|
302
|
-
&& isProvenanceField(build.lifecycleContractHash)
|
|
303
|
-
&& isProvenanceField(binding.instanceDigest)
|
|
304
|
-
&& isProvenanceField(binding.controlGeneration);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function sameControllerField(
|
|
308
|
-
left: unknown,
|
|
309
|
-
right: unknown,
|
|
310
|
-
key: keyof CodexControllerBinding
|
|
311
|
-
): boolean {
|
|
312
|
-
if (left === null || right === null) return left === null && right === null;
|
|
313
|
-
return isCodexControllerBinding(left)
|
|
314
|
-
&& isCodexControllerBinding(right)
|
|
315
|
-
&& left[key] === right[key];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function sameController(
|
|
319
|
-
left: unknown,
|
|
320
|
-
right: unknown
|
|
321
|
-
): boolean {
|
|
322
|
-
if (left === null || right === null) return left === null && right === null;
|
|
323
|
-
return isCodexControllerBinding(left)
|
|
324
|
-
&& isCodexControllerBinding(right)
|
|
325
|
-
&& left.instanceDigest === right.instanceDigest
|
|
326
|
-
&& left.controlGeneration === right.controlGeneration;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
330
|
-
const root = options.root
|
|
331
|
-
?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
|
|
332
|
-
const now = options.now ?? Date.now;
|
|
333
|
-
const createReference = options.reference ?? (() => crypto.randomBytes(32).toString('base64url'));
|
|
334
|
-
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
335
|
-
const tombstoneMs = options.tombstoneMs ?? DEFAULT_TOMBSTONE_MS;
|
|
336
|
-
const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
337
|
-
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
338
|
-
fs.chmodSync(root, 0o700);
|
|
339
|
-
|
|
340
|
-
function fileFor(capabilityRef: string): string {
|
|
341
|
-
return path.join(root, `${digest(capabilityRef)}.json`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function read(file: string): CodexCapabilityRecord {
|
|
345
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8')) as CodexCapabilityRecord;
|
|
346
|
-
const keys = Object.keys(value as unknown as Record<string, unknown>).sort().join(',');
|
|
347
|
-
if (keys !== 'armedAt,attestedAt,buildIdentity,capabilityRefDigest,consumedAt,controller,expiresAt,hookDefinitionHash,recoveryOperationId,recoveryPhases,recoveryState,reservedAt,revision,schemaVersion,sessionId,status,taskId,toolUseId,turnId'
|
|
348
|
-
|| value.schemaVersion !== 2 || !Number.isSafeInteger(value.revision) || value.revision < 1
|
|
349
|
-
|| typeof value.capabilityRefDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.capabilityRefDigest)
|
|
350
|
-
|| !['unreserved', 'reserved', 'consumed'].includes(value.recoveryState)
|
|
351
|
-
|| (value.recoveryOperationId !== null && typeof value.recoveryOperationId !== 'string')
|
|
352
|
-
|| (value.reservedAt !== null && !Number.isSafeInteger(value.reservedAt))
|
|
353
|
-
|| !Array.isArray(value.recoveryPhases)
|
|
354
|
-
|| !value.recoveryPhases.every((phase) => isRecoveryPhaseRecord(phase))) {
|
|
355
|
-
throw capabilityError('CODEX_CAPABILITY_STATE_INVALID', 'capability record schema is invalid');
|
|
356
|
-
}
|
|
357
|
-
return value;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function withWriteLock<T>(operation: () => T): T {
|
|
361
|
-
const lock = path.join(root, '.write.lock');
|
|
362
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
363
|
-
let descriptor: number | null = null;
|
|
364
|
-
while (descriptor === null) {
|
|
365
|
-
try {
|
|
366
|
-
descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
369
|
-
try {
|
|
370
|
-
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
371
|
-
fs.unlinkSync(lock);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
} catch (lockError) {
|
|
375
|
-
if ((lockError as NodeJS.ErrnoException).code === 'ENOENT') continue;
|
|
376
|
-
throw lockError;
|
|
377
|
-
}
|
|
378
|
-
if (Date.now() >= deadline) {
|
|
379
|
-
throw capabilityError('CODEX_CAPABILITY_STORE_BUSY', 'capability store is busy');
|
|
380
|
-
}
|
|
381
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
return operation();
|
|
386
|
-
} finally {
|
|
387
|
-
fs.closeSync(descriptor);
|
|
388
|
-
fs.unlinkSync(lock);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function write(file: string, value: CodexCapabilityRecord, expectedRevision: number): void {
|
|
393
|
-
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
394
|
-
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
395
|
-
encoding: 'utf8',
|
|
396
|
-
flag: 'wx',
|
|
397
|
-
mode: 0o600
|
|
398
|
-
});
|
|
399
|
-
try {
|
|
400
|
-
withWriteLock(() => {
|
|
401
|
-
options.beforeCompareAndSwap?.({ path: file, expectedRevision });
|
|
402
|
-
const actualRevision = fs.existsSync(file) ? read(file).revision : 0;
|
|
403
|
-
if (actualRevision !== expectedRevision) {
|
|
404
|
-
throw capabilityError(
|
|
405
|
-
'CODEX_CAPABILITY_REVISION_CONFLICT',
|
|
406
|
-
`capability revision changed from ${expectedRevision} to ${actualRevision}`
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
fs.renameSync(temporary, file);
|
|
410
|
-
fs.chmodSync(file, 0o600);
|
|
411
|
-
});
|
|
412
|
-
} finally {
|
|
413
|
-
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function files(): string[] {
|
|
418
|
-
return fs.readdirSync(root)
|
|
419
|
-
.filter((name) => /^[a-f0-9]{64}\.json$/u.test(name))
|
|
420
|
-
.map((name) => path.join(root, name));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function expire(record: CodexCapabilityRecord, file: string, timestamp: number): CodexCapabilityRecord {
|
|
424
|
-
if ((record.status === 'armed' || record.status === 'attested') && timestamp > record.expiresAt) {
|
|
425
|
-
const expired = Object.freeze({
|
|
426
|
-
...record,
|
|
427
|
-
revision: record.revision + 1,
|
|
428
|
-
status: 'expired' as const
|
|
429
|
-
});
|
|
430
|
-
write(file, expired, record.revision);
|
|
431
|
-
return expired;
|
|
432
|
-
}
|
|
433
|
-
return record;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function sweep(): void {
|
|
437
|
-
const timestamp = now();
|
|
438
|
-
const records = files().map((file) => ({ file, record: expire(read(file), file, timestamp) }));
|
|
439
|
-
for (const entry of records) {
|
|
440
|
-
const terminalAt = entry.record.consumedAt ?? entry.record.expiresAt;
|
|
441
|
-
if (['consumed', 'expired'].includes(entry.record.status)
|
|
442
|
-
&& timestamp - terminalAt > tombstoneMs) {
|
|
443
|
-
fs.unlinkSync(entry.file);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
const remaining = files().map((file) => ({ file, record: read(file) }));
|
|
447
|
-
if (remaining.length <= maxRecords) return;
|
|
448
|
-
const removable = remaining
|
|
449
|
-
.filter(({ record }) => ['consumed', 'expired'].includes(record.status))
|
|
450
|
-
.sort((left, right) =>
|
|
451
|
-
(left.record.consumedAt ?? left.record.expiresAt)
|
|
452
|
-
- (right.record.consumedAt ?? right.record.expiresAt));
|
|
453
|
-
while (files().length > maxRecords && removable.length > 0) {
|
|
454
|
-
fs.unlinkSync(removable.shift()!.file);
|
|
455
|
-
}
|
|
456
|
-
if (files().length > maxRecords) {
|
|
457
|
-
throw capabilityError('CODEX_CAPABILITY_CAPACITY_EXCEEDED', 'capability store has too many active records');
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function arm(input: Readonly<{
|
|
462
|
-
taskId: string;
|
|
463
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
464
|
-
controller?: CodexControllerBinding;
|
|
465
|
-
}>): CodexCapabilityRecord & Readonly<{ capabilityRef: string; path: string; marker: string }> {
|
|
466
|
-
sweep();
|
|
467
|
-
const capabilityRef = createReference();
|
|
468
|
-
const file = fileFor(capabilityRef);
|
|
469
|
-
if (fs.existsSync(file)) throw capabilityError('CODEX_CAPABILITY_TOKEN_COLLISION', 'capability token already exists');
|
|
470
|
-
const timestamp = now();
|
|
471
|
-
const record: CodexCapabilityRecord = Object.freeze({
|
|
472
|
-
schemaVersion: 2,
|
|
473
|
-
revision: 1,
|
|
474
|
-
capabilityRefDigest: digest(capabilityRef),
|
|
475
|
-
taskId: input.taskId,
|
|
476
|
-
status: 'armed',
|
|
477
|
-
recoveryOperationId: null,
|
|
478
|
-
recoveryState: 'unreserved',
|
|
479
|
-
reservedAt: null,
|
|
480
|
-
recoveryPhases: Object.freeze([]),
|
|
481
|
-
buildIdentity: input.buildIdentity,
|
|
482
|
-
controller: input.controller ?? null,
|
|
483
|
-
sessionId: null,
|
|
484
|
-
turnId: null,
|
|
485
|
-
toolUseId: null,
|
|
486
|
-
hookDefinitionHash: null,
|
|
487
|
-
armedAt: timestamp,
|
|
488
|
-
expiresAt: timestamp + ttlMs,
|
|
489
|
-
attestedAt: null,
|
|
490
|
-
consumedAt: null
|
|
491
|
-
});
|
|
492
|
-
write(file, record, 0);
|
|
493
|
-
const result = {
|
|
494
|
-
...record,
|
|
495
|
-
capabilityRef,
|
|
496
|
-
path: file,
|
|
497
|
-
marker: `agent-infra-codex-capability:${capabilityRef}`
|
|
498
|
-
};
|
|
499
|
-
return Object.freeze(result) as CodexCapabilityRecord & Readonly<{ capabilityRef: string; path: string; marker: string }>;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function loadActive(
|
|
503
|
-
capabilityRef: string,
|
|
504
|
-
options: Readonly<{ allowExpiredReserved?: boolean }> = {}
|
|
505
|
-
): Readonly<{ file: string; record: CodexCapabilityRecord }> {
|
|
506
|
-
const file = fileFor(capabilityRef);
|
|
507
|
-
if (!fs.existsSync(file)) {
|
|
508
|
-
throw capabilityError('CODEX_CAPABILITY_MISSING', 'capability token was not found');
|
|
509
|
-
}
|
|
510
|
-
const record = expire(read(file), file, now());
|
|
511
|
-
if (record.status === 'expired' && !options.allowExpiredReserved) {
|
|
512
|
-
throw capabilityError('CODEX_CAPABILITY_EXPIRED', 'capability token expired');
|
|
513
|
-
}
|
|
514
|
-
return { file, record };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function attestByReference(input: Readonly<{
|
|
518
|
-
capabilityRef: string;
|
|
519
|
-
sessionId: string;
|
|
520
|
-
turnId: string;
|
|
521
|
-
toolUseId: string;
|
|
522
|
-
hookDefinitionHash: string;
|
|
523
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
524
|
-
controller?: CodexControllerBinding;
|
|
525
|
-
}>): CodexCapabilityRecord {
|
|
526
|
-
const { file, record } = loadActive(input.capabilityRef);
|
|
527
|
-
if (record.status !== 'armed') {
|
|
528
|
-
throw capabilityError(
|
|
529
|
-
record.status === 'consumed' ? 'CODEX_CAPABILITY_REPLAY' : 'CODEX_CAPABILITY_STATE_INVALID',
|
|
530
|
-
`capability is ${record.status}, expected armed`
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
const identity = verifyLifecycleBuildIdentity(record.buildIdentity, input.buildIdentity);
|
|
534
|
-
if (!identity.ok || !sameController(record.controller, input.controller ?? null)) {
|
|
535
|
-
throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability build or controller identity does not match');
|
|
536
|
-
}
|
|
537
|
-
if (![input.sessionId, input.turnId, input.toolUseId, input.hookDefinitionHash]
|
|
538
|
-
.every((value) => value.trim().length > 0)) {
|
|
539
|
-
throw capabilityError('CODEX_CAPABILITY_IDENTITY_INVALID', 'hook identity is incomplete');
|
|
540
|
-
}
|
|
541
|
-
const timestamp = now();
|
|
542
|
-
const next: CodexCapabilityRecord = Object.freeze({
|
|
543
|
-
...record,
|
|
544
|
-
revision: record.revision + 1,
|
|
545
|
-
status: 'attested',
|
|
546
|
-
sessionId: input.sessionId,
|
|
547
|
-
turnId: input.turnId,
|
|
548
|
-
toolUseId: input.toolUseId,
|
|
549
|
-
hookDefinitionHash: input.hookDefinitionHash,
|
|
550
|
-
attestedAt: timestamp
|
|
551
|
-
});
|
|
552
|
-
write(file, next, record.revision);
|
|
553
|
-
return next;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function validateRecord(record: CodexCapabilityRecord, expected: Readonly<{
|
|
557
|
-
taskId: string;
|
|
558
|
-
hookDefinitionHash: string;
|
|
559
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
560
|
-
controller?: CodexControllerBinding;
|
|
561
|
-
}>, options: Readonly<{ allowExpiredReserved?: boolean }> = {}): CodexCapabilityRecord {
|
|
562
|
-
if (record.status === 'consumed') {
|
|
563
|
-
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
|
|
564
|
-
}
|
|
565
|
-
const expiredReserved = options.allowExpiredReserved
|
|
566
|
-
&& record.status === 'expired'
|
|
567
|
-
&& record.recoveryState === 'reserved';
|
|
568
|
-
if (record.status !== 'attested' && !expiredReserved) {
|
|
569
|
-
throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
|
|
570
|
-
}
|
|
571
|
-
const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
|
|
572
|
-
? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
|
|
573
|
-
: { ok: false };
|
|
574
|
-
if (!identity.ok
|
|
575
|
-
|| record.taskId !== expected.taskId
|
|
576
|
-
|| record.hookDefinitionHash !== expected.hookDefinitionHash
|
|
577
|
-
|| !sameController(record.controller, expected.controller ?? null)) {
|
|
578
|
-
throw capabilityError(
|
|
579
|
-
'CODEX_CAPABILITY_PROVENANCE_MISMATCH',
|
|
580
|
-
'capability consumption identity does not match',
|
|
581
|
-
provenanceDetail(record, expected)
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
return record;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function validateReference(capabilityRef: string, expected: Readonly<{
|
|
588
|
-
taskId: string;
|
|
589
|
-
hookDefinitionHash: string;
|
|
590
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
591
|
-
controller?: CodexControllerBinding;
|
|
592
|
-
}>): CodexCapabilityRecord {
|
|
593
|
-
sweep();
|
|
594
|
-
const { record } = loadActive(capabilityRef);
|
|
595
|
-
return validateRecord(record, expected);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function consumeReference(capabilityRef: string, operationId: string, expected: Readonly<{
|
|
599
|
-
taskId: string;
|
|
600
|
-
hookDefinitionHash: string;
|
|
601
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
602
|
-
controller?: CodexControllerBinding;
|
|
603
|
-
}>, options: Readonly<{ allowExpiredReserved?: boolean }> = {}): CodexCapabilityRecord {
|
|
604
|
-
sweep();
|
|
605
|
-
if (!operationId || /[\r\n]/u.test(operationId)) {
|
|
606
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
|
|
607
|
-
}
|
|
608
|
-
const { file, record } = loadActive(capabilityRef, options);
|
|
609
|
-
if (record.status === 'consumed' && record.recoveryState === 'consumed' && record.recoveryOperationId === operationId) {
|
|
610
|
-
return record;
|
|
611
|
-
}
|
|
612
|
-
validateRecord(record, expected, options);
|
|
613
|
-
if (record.recoveryState === 'consumed') {
|
|
614
|
-
if (record.recoveryOperationId === operationId) return record;
|
|
615
|
-
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation was already consumed');
|
|
616
|
-
}
|
|
617
|
-
if (record.recoveryState !== 'reserved' || record.recoveryOperationId !== operationId) {
|
|
618
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability must be reserved by this operation before consumption');
|
|
619
|
-
}
|
|
620
|
-
const next: CodexCapabilityRecord = Object.freeze({
|
|
621
|
-
...record,
|
|
622
|
-
revision: record.revision + 1,
|
|
623
|
-
status: 'consumed',
|
|
624
|
-
recoveryState: 'consumed',
|
|
625
|
-
consumedAt: now()
|
|
626
|
-
});
|
|
627
|
-
write(file, next, record.revision);
|
|
628
|
-
return next;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function reserveRecoveryPhase(
|
|
632
|
-
capabilityRef: string,
|
|
633
|
-
operationId: string,
|
|
634
|
-
phase: RecoveryPhase,
|
|
635
|
-
requestId: string,
|
|
636
|
-
expected: Readonly<{
|
|
637
|
-
taskId: string;
|
|
638
|
-
hookDefinitionHash: string;
|
|
639
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
640
|
-
controller?: CodexControllerBinding;
|
|
641
|
-
}>
|
|
642
|
-
): CodexCapabilityRecord {
|
|
643
|
-
if (!operationId || /[\r\n]/u.test(operationId) || !isRecoveryPhase(phase)
|
|
644
|
-
|| !/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
645
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery phase identity is invalid');
|
|
646
|
-
}
|
|
647
|
-
sweep();
|
|
648
|
-
const { file, record } = loadActive(capabilityRef);
|
|
649
|
-
validateRecord(record, expected);
|
|
650
|
-
if (record.recoveryState !== 'reserved' || record.recoveryOperationId !== operationId) {
|
|
651
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability must be reserved by this operation before phase reservation');
|
|
652
|
-
}
|
|
653
|
-
if (record.recoveryPhases.some((entry) => entry.phase === phase)) {
|
|
654
|
-
throw capabilityError('CODEX_CAPABILITY_PHASE_REPLAY', 'recovery phase was already reserved');
|
|
655
|
-
}
|
|
656
|
-
const next: CodexCapabilityRecord = Object.freeze({
|
|
657
|
-
...record,
|
|
658
|
-
revision: record.revision + 1,
|
|
659
|
-
recoveryPhases: Object.freeze([
|
|
660
|
-
...record.recoveryPhases,
|
|
661
|
-
Object.freeze({ phase, requestId, state: 'issued' as const })
|
|
662
|
-
])
|
|
663
|
-
});
|
|
664
|
-
write(file, next, record.revision);
|
|
665
|
-
return next;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function consumeRecoveryPhase(
|
|
669
|
-
capabilityRef: string,
|
|
670
|
-
operationId: string,
|
|
671
|
-
phase: RecoveryPhase,
|
|
672
|
-
requestId: string,
|
|
673
|
-
expected: Readonly<{
|
|
674
|
-
taskId: string;
|
|
675
|
-
hookDefinitionHash: string;
|
|
676
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
677
|
-
controller?: CodexControllerBinding;
|
|
678
|
-
}>,
|
|
679
|
-
options: Readonly<{ allowExpiredReserved?: boolean }> = {}
|
|
680
|
-
): CodexCapabilityRecord {
|
|
681
|
-
if (!operationId || /[\r\n]/u.test(operationId) || !isRecoveryPhase(phase)
|
|
682
|
-
|| !/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
683
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery phase identity is invalid');
|
|
684
|
-
}
|
|
685
|
-
sweep();
|
|
686
|
-
const { file, record } = loadActive(capabilityRef, options);
|
|
687
|
-
validateRecord(record, expected, options);
|
|
688
|
-
if (record.recoveryState !== 'reserved' && record.recoveryState !== 'consumed') {
|
|
689
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability has no reserved recovery operation');
|
|
690
|
-
}
|
|
691
|
-
if (record.recoveryOperationId !== operationId) {
|
|
692
|
-
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation does not match');
|
|
693
|
-
}
|
|
694
|
-
const current = record.recoveryPhases.find((entry) => entry.phase === phase);
|
|
695
|
-
if (!current || current.requestId !== requestId) {
|
|
696
|
-
throw capabilityError('CODEX_CAPABILITY_PHASE_REPLAY', 'recovery phase identity does not match');
|
|
697
|
-
}
|
|
698
|
-
if (current.state === 'consumed') return record;
|
|
699
|
-
const next: CodexCapabilityRecord = Object.freeze({
|
|
700
|
-
...record,
|
|
701
|
-
revision: record.revision + 1,
|
|
702
|
-
recoveryPhases: Object.freeze(record.recoveryPhases.map((entry) =>
|
|
703
|
-
entry.phase === phase ? Object.freeze({ ...entry, state: 'consumed' as const }) : entry
|
|
704
|
-
))
|
|
705
|
-
});
|
|
706
|
-
write(file, next, record.revision);
|
|
707
|
-
return next;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function reserveReference(
|
|
711
|
-
capabilityRef: string,
|
|
712
|
-
operationId: string,
|
|
713
|
-
expected: Readonly<{
|
|
714
|
-
taskId: string;
|
|
715
|
-
hookDefinitionHash: string;
|
|
716
|
-
buildIdentity: LifecycleBuildIdentity;
|
|
717
|
-
controller?: CodexControllerBinding;
|
|
718
|
-
}>
|
|
719
|
-
): CodexCapabilityRecord {
|
|
720
|
-
if (!operationId || /[\r\n]/u.test(operationId)) {
|
|
721
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
|
|
722
|
-
}
|
|
723
|
-
sweep();
|
|
724
|
-
const { file, record } = loadActive(capabilityRef);
|
|
725
|
-
validateRecord(record, expected);
|
|
726
|
-
if (record.recoveryState === 'reserved') {
|
|
727
|
-
if (record.recoveryOperationId === operationId) return record;
|
|
728
|
-
throw capabilityError('CODEX_CAPABILITY_BUSY', 'capability is reserved by another recovery operation');
|
|
729
|
-
}
|
|
730
|
-
if (record.recoveryState === 'consumed') {
|
|
731
|
-
if (record.recoveryOperationId === operationId) return record;
|
|
732
|
-
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation was already consumed');
|
|
733
|
-
}
|
|
734
|
-
const next: CodexCapabilityRecord = Object.freeze({
|
|
735
|
-
...record,
|
|
736
|
-
revision: record.revision + 1,
|
|
737
|
-
recoveryOperationId: operationId,
|
|
738
|
-
recoveryState: 'reserved',
|
|
739
|
-
reservedAt: now()
|
|
740
|
-
});
|
|
741
|
-
write(file, next, record.revision);
|
|
742
|
-
return next;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function inspectReference(capabilityRef: string): CodexCapabilityRecord {
|
|
746
|
-
return loadActive(capabilityRef).record;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function findByRecoveryOperation(operationId: string): readonly CodexCapabilityRecord[] {
|
|
750
|
-
if (!operationId || /[\r\n]/u.test(operationId)) {
|
|
751
|
-
throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
|
|
752
|
-
}
|
|
753
|
-
return Object.freeze(files()
|
|
754
|
-
.map((file) => read(file))
|
|
755
|
-
.filter((record) => record.recoveryOperationId === operationId));
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return Object.freeze({
|
|
759
|
-
arm, attestByReference, reserveReference, reserveRecoveryPhase, consumeRecoveryPhase,
|
|
760
|
-
consumeReference, inspectReference,
|
|
761
|
-
findByRecoveryOperation, validateReference, sweep
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
export { createCodexCapabilityStore };
|
|
766
|
-
export type {
|
|
767
|
-
CapabilityStatus,
|
|
768
|
-
RecoveryState,
|
|
769
|
-
CodexCapabilityProjection,
|
|
770
|
-
CodexCapabilityProvenanceDetail,
|
|
771
|
-
CodexCapabilityRecord,
|
|
772
|
-
CodexCapabilityRecoveryPhase,
|
|
773
|
-
CodexCapabilityStoreOptions,
|
|
774
|
-
CodexControllerBinding
|
|
775
|
-
};
|
|
776
|
-
export { isCodexCapabilityProvenanceDetail };
|