@fitlab-ai/agent-infra 0.9.8 → 0.9.9
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/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +137 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/delegation-receipts.js +243 -2
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +113 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +163 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/delegation-receipts.ts +258 -1
- package/lib/task/finalization.ts +719 -0
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +217 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +118 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -2,7 +2,12 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
isCanonicalLifecyclePackageVersion,
|
|
7
|
+
isLifecycleProtocolVersion,
|
|
8
|
+
LIFECYCLE_PROTOCOL_VERSION,
|
|
9
|
+
verifyLifecycleBuildIdentity
|
|
10
|
+
} from './build-identity.ts';
|
|
6
11
|
import type { LifecycleBuildIdentity } from './build-identity.ts';
|
|
7
12
|
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
8
13
|
|
|
@@ -11,6 +16,36 @@ type CodexControllerBinding = Readonly<{
|
|
|
11
16
|
instanceDigest: string;
|
|
12
17
|
controlGeneration: string;
|
|
13
18
|
}>;
|
|
19
|
+
type CodexCapabilityProjection = Readonly<
|
|
20
|
+
| { kind: 'absent' }
|
|
21
|
+
| { kind: 'protocol-version'; value: typeof LIFECYCLE_PROTOCOL_VERSION }
|
|
22
|
+
| { kind: 'semver'; value: string }
|
|
23
|
+
| { kind: 'digest-prefix'; value: string }
|
|
24
|
+
| { kind: 'presence'; present: boolean }
|
|
25
|
+
>;
|
|
26
|
+
type CodexCapabilityProvenanceField = Readonly<{
|
|
27
|
+
matches: boolean;
|
|
28
|
+
expected: CodexCapabilityProjection;
|
|
29
|
+
actual: CodexCapabilityProjection;
|
|
30
|
+
}>;
|
|
31
|
+
type CodexCapabilityProvenanceDetail = Readonly<{
|
|
32
|
+
kind: 'codex-capability-provenance-mismatch';
|
|
33
|
+
version: 1;
|
|
34
|
+
fields: Readonly<{
|
|
35
|
+
taskId: CodexCapabilityProvenanceField;
|
|
36
|
+
hookDefinitionHash: CodexCapabilityProvenanceField;
|
|
37
|
+
buildIdentity: Readonly<{
|
|
38
|
+
protocolVersion: CodexCapabilityProvenanceField;
|
|
39
|
+
packageVersion: CodexCapabilityProvenanceField;
|
|
40
|
+
internalExecutableBuildHash: CodexCapabilityProvenanceField;
|
|
41
|
+
lifecycleContractHash: CodexCapabilityProvenanceField;
|
|
42
|
+
}>;
|
|
43
|
+
controller: Readonly<{
|
|
44
|
+
instanceDigest: CodexCapabilityProvenanceField;
|
|
45
|
+
controlGeneration: CodexCapabilityProvenanceField;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
14
49
|
type CodexCapabilityRecord = Readonly<{
|
|
15
50
|
schemaVersion: 1;
|
|
16
51
|
revision: number;
|
|
@@ -45,23 +80,222 @@ const DEFAULT_MAX_RECORDS = 128;
|
|
|
45
80
|
const LOCK_RETRY_MS = 10;
|
|
46
81
|
const LOCK_TIMEOUT_MS = 1_000;
|
|
47
82
|
const LOCK_STALE_MS = 30_000;
|
|
83
|
+
const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
|
|
84
|
+
const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
|
|
85
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
86
|
+
const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
|
|
48
87
|
|
|
49
88
|
function digest(value: string): string {
|
|
50
89
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
function capabilityError(code: string, message: string): Error {
|
|
92
|
+
function capabilityError(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): Error {
|
|
54
93
|
const error = new Error(`${code}: ${message}`);
|
|
55
94
|
error.name = code;
|
|
95
|
+
if (detail) (error as Error & { detail: CodexCapabilityProvenanceDetail }).detail = detail;
|
|
56
96
|
return error;
|
|
57
97
|
}
|
|
58
98
|
|
|
99
|
+
function objectValue(value: unknown, key: string): unknown {
|
|
100
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
101
|
+
return (value as Record<string, unknown>)[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function digestPrefix(fieldName: string, value: string): CodexCapabilityProjection {
|
|
105
|
+
return {
|
|
106
|
+
kind: 'digest-prefix',
|
|
107
|
+
value: `sha256:${crypto.createHash('sha256')
|
|
108
|
+
.update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
|
|
109
|
+
.digest('hex')
|
|
110
|
+
.slice(0, 16)}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function projectDigest(fieldName: string, value: unknown, pattern: RegExp): CodexCapabilityProjection {
|
|
115
|
+
return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function projectTaskId(value: unknown): CodexCapabilityProjection {
|
|
119
|
+
return projectDigest('taskId', value, TASK_ID_PATTERN);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function projectHash(fieldName: string, value: unknown): CodexCapabilityProjection {
|
|
123
|
+
return projectDigest(fieldName, value, SHA256_PATTERN);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function projectProtocol(value: unknown): CodexCapabilityProjection {
|
|
127
|
+
return isLifecycleProtocolVersion(value)
|
|
128
|
+
? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
|
|
129
|
+
: { kind: 'absent' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function projectPackageVersion(value: unknown): CodexCapabilityProjection {
|
|
133
|
+
return isCanonicalLifecyclePackageVersion(value)
|
|
134
|
+
? { kind: 'semver', value }
|
|
135
|
+
: { kind: 'absent' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function projectPresence(value: unknown): CodexCapabilityProjection {
|
|
139
|
+
return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function field(matches: boolean, expected: CodexCapabilityProjection, actual: CodexCapabilityProjection): CodexCapabilityProvenanceField {
|
|
143
|
+
return { matches, expected, actual };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function provenanceDetail(
|
|
147
|
+
record: CodexCapabilityRecord,
|
|
148
|
+
expected: Readonly<{
|
|
149
|
+
taskId: string;
|
|
150
|
+
hookDefinitionHash: string;
|
|
151
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
152
|
+
controller?: CodexControllerBinding;
|
|
153
|
+
}>
|
|
154
|
+
): CodexCapabilityProvenanceDetail {
|
|
155
|
+
const actualBuild = record.buildIdentity;
|
|
156
|
+
const expectedBuild = expected.buildIdentity;
|
|
157
|
+
const actualController = record.controller;
|
|
158
|
+
const expectedController = expected.controller ?? null;
|
|
159
|
+
return {
|
|
160
|
+
kind: 'codex-capability-provenance-mismatch',
|
|
161
|
+
version: 1,
|
|
162
|
+
fields: {
|
|
163
|
+
taskId: field(
|
|
164
|
+
record.taskId === expected.taskId,
|
|
165
|
+
projectTaskId(expected.taskId),
|
|
166
|
+
projectTaskId(record.taskId)
|
|
167
|
+
),
|
|
168
|
+
hookDefinitionHash: field(
|
|
169
|
+
record.hookDefinitionHash === expected.hookDefinitionHash,
|
|
170
|
+
projectHash('hookDefinitionHash', expected.hookDefinitionHash),
|
|
171
|
+
projectHash('hookDefinitionHash', record.hookDefinitionHash)
|
|
172
|
+
),
|
|
173
|
+
buildIdentity: {
|
|
174
|
+
protocolVersion: field(
|
|
175
|
+
objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'),
|
|
176
|
+
projectProtocol(objectValue(expectedBuild, 'protocolVersion')),
|
|
177
|
+
projectProtocol(objectValue(actualBuild, 'protocolVersion'))
|
|
178
|
+
),
|
|
179
|
+
packageVersion: field(
|
|
180
|
+
objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'),
|
|
181
|
+
projectPackageVersion(objectValue(expectedBuild, 'packageVersion')),
|
|
182
|
+
projectPackageVersion(objectValue(actualBuild, 'packageVersion'))
|
|
183
|
+
),
|
|
184
|
+
internalExecutableBuildHash: field(
|
|
185
|
+
objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'),
|
|
186
|
+
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')),
|
|
187
|
+
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))
|
|
188
|
+
),
|
|
189
|
+
lifecycleContractHash: field(
|
|
190
|
+
objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'),
|
|
191
|
+
projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')),
|
|
192
|
+
projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash'))
|
|
193
|
+
)
|
|
194
|
+
},
|
|
195
|
+
controller: {
|
|
196
|
+
instanceDigest: field(
|
|
197
|
+
sameControllerField(actualController, expectedController, 'instanceDigest'),
|
|
198
|
+
projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController),
|
|
199
|
+
projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)
|
|
200
|
+
),
|
|
201
|
+
controlGeneration: field(
|
|
202
|
+
sameControllerField(actualController, expectedController, 'controlGeneration'),
|
|
203
|
+
projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController),
|
|
204
|
+
projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController)
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
212
|
+
return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isCodexControllerBinding(value: unknown): value is CodexControllerBinding {
|
|
216
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
217
|
+
const record = value as Record<string, unknown>;
|
|
218
|
+
return exactKeys(record, ['instanceDigest', 'controlGeneration'])
|
|
219
|
+
&& typeof record.instanceDigest === 'string'
|
|
220
|
+
&& typeof record.controlGeneration === 'string';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isProjection(value: unknown): value is CodexCapabilityProjection {
|
|
224
|
+
if (!value || typeof value !== 'object') return false;
|
|
225
|
+
const record = value as Record<string, unknown>;
|
|
226
|
+
if (record.kind === 'absent') return exactKeys(record, ['kind']);
|
|
227
|
+
if (record.kind === 'protocol-version') {
|
|
228
|
+
return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
|
|
229
|
+
}
|
|
230
|
+
if (record.kind === 'semver') {
|
|
231
|
+
return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
|
|
232
|
+
}
|
|
233
|
+
if (record.kind === 'digest-prefix') {
|
|
234
|
+
return exactKeys(record, ['kind', 'value'])
|
|
235
|
+
&& typeof record.value === 'string'
|
|
236
|
+
&& DIGEST_PREFIX_PATTERN.test(record.value);
|
|
237
|
+
}
|
|
238
|
+
if (record.kind === 'presence') {
|
|
239
|
+
return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isProvenanceField(value: unknown): value is CodexCapabilityProvenanceField {
|
|
245
|
+
if (!value || typeof value !== 'object') return false;
|
|
246
|
+
const record = value as Record<string, unknown>;
|
|
247
|
+
return exactKeys(record, ['matches', 'expected', 'actual'])
|
|
248
|
+
&& typeof record.matches === 'boolean'
|
|
249
|
+
&& isProjection(record.expected)
|
|
250
|
+
&& isProjection(record.actual);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isCodexCapabilityProvenanceDetail(value: unknown): value is CodexCapabilityProvenanceDetail {
|
|
254
|
+
if (!value || typeof value !== 'object') return false;
|
|
255
|
+
const record = value as Record<string, unknown>;
|
|
256
|
+
if (!exactKeys(record, ['kind', 'version', 'fields'])
|
|
257
|
+
|| record.kind !== 'codex-capability-provenance-mismatch'
|
|
258
|
+
|| record.version !== 1
|
|
259
|
+
|| !record.fields || typeof record.fields !== 'object') return false;
|
|
260
|
+
const fields = record.fields as Record<string, unknown>;
|
|
261
|
+
if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
|
|
262
|
+
|| !isProvenanceField(fields.taskId)
|
|
263
|
+
|| !isProvenanceField(fields.hookDefinitionHash)) return false;
|
|
264
|
+
const buildIdentity = fields.buildIdentity;
|
|
265
|
+
const controller = fields.controller;
|
|
266
|
+
if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object') return false;
|
|
267
|
+
const build = buildIdentity as Record<string, unknown>;
|
|
268
|
+
const binding = controller as Record<string, unknown>;
|
|
269
|
+
return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
|
|
270
|
+
&& exactKeys(binding, ['instanceDigest', 'controlGeneration'])
|
|
271
|
+
&& isProvenanceField(build.protocolVersion)
|
|
272
|
+
&& isProvenanceField(build.packageVersion)
|
|
273
|
+
&& isProvenanceField(build.internalExecutableBuildHash)
|
|
274
|
+
&& isProvenanceField(build.lifecycleContractHash)
|
|
275
|
+
&& isProvenanceField(binding.instanceDigest)
|
|
276
|
+
&& isProvenanceField(binding.controlGeneration);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function sameControllerField(
|
|
280
|
+
left: unknown,
|
|
281
|
+
right: unknown,
|
|
282
|
+
key: keyof CodexControllerBinding
|
|
283
|
+
): boolean {
|
|
284
|
+
if (left === null || right === null) return left === null && right === null;
|
|
285
|
+
return isCodexControllerBinding(left)
|
|
286
|
+
&& isCodexControllerBinding(right)
|
|
287
|
+
&& left[key] === right[key];
|
|
288
|
+
}
|
|
289
|
+
|
|
59
290
|
function sameController(
|
|
60
|
-
left:
|
|
61
|
-
right:
|
|
291
|
+
left: unknown,
|
|
292
|
+
right: unknown
|
|
62
293
|
): boolean {
|
|
63
|
-
return left
|
|
64
|
-
|
|
294
|
+
if (left === null || right === null) return left === null && right === null;
|
|
295
|
+
return isCodexControllerBinding(left)
|
|
296
|
+
&& isCodexControllerBinding(right)
|
|
297
|
+
&& left.instanceDigest === right.instanceDigest
|
|
298
|
+
&& left.controlGeneration === right.controlGeneration;
|
|
65
299
|
}
|
|
66
300
|
|
|
67
301
|
function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
@@ -275,27 +509,54 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
|
275
509
|
return next;
|
|
276
510
|
}
|
|
277
511
|
|
|
278
|
-
function
|
|
512
|
+
function validateRecord(record: CodexCapabilityRecord, expected: Readonly<{
|
|
279
513
|
taskId: string;
|
|
280
514
|
hookDefinitionHash: string;
|
|
281
515
|
buildIdentity: LifecycleBuildIdentity;
|
|
282
516
|
controller?: CodexControllerBinding;
|
|
283
517
|
}>): CodexCapabilityRecord {
|
|
284
|
-
sweep();
|
|
285
|
-
const { file, record } = loadActive(token);
|
|
286
518
|
if (record.status === 'consumed') {
|
|
287
519
|
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
|
|
288
520
|
}
|
|
289
521
|
if (record.status !== 'attested') {
|
|
290
522
|
throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
|
|
291
523
|
}
|
|
292
|
-
const identity =
|
|
524
|
+
const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
|
|
525
|
+
? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
|
|
526
|
+
: { ok: false };
|
|
293
527
|
if (!identity.ok
|
|
294
528
|
|| record.taskId !== expected.taskId
|
|
295
529
|
|| record.hookDefinitionHash !== expected.hookDefinitionHash
|
|
296
530
|
|| !sameController(record.controller, expected.controller ?? null)) {
|
|
297
|
-
throw capabilityError(
|
|
531
|
+
throw capabilityError(
|
|
532
|
+
'CODEX_CAPABILITY_PROVENANCE_MISMATCH',
|
|
533
|
+
'capability consumption identity does not match',
|
|
534
|
+
provenanceDetail(record, expected)
|
|
535
|
+
);
|
|
298
536
|
}
|
|
537
|
+
return record;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function validate(token: string, expected: Readonly<{
|
|
541
|
+
taskId: string;
|
|
542
|
+
hookDefinitionHash: string;
|
|
543
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
544
|
+
controller?: CodexControllerBinding;
|
|
545
|
+
}>): CodexCapabilityRecord {
|
|
546
|
+
sweep();
|
|
547
|
+
const { record } = loadActive(token);
|
|
548
|
+
return validateRecord(record, expected);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function consume(token: string, expected: Readonly<{
|
|
552
|
+
taskId: string;
|
|
553
|
+
hookDefinitionHash: string;
|
|
554
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
555
|
+
controller?: CodexControllerBinding;
|
|
556
|
+
}>): CodexCapabilityRecord {
|
|
557
|
+
sweep();
|
|
558
|
+
const { file, record } = loadActive(token);
|
|
559
|
+
validateRecord(record, expected);
|
|
299
560
|
const next: CodexCapabilityRecord = Object.freeze({
|
|
300
561
|
...record,
|
|
301
562
|
revision: record.revision + 1,
|
|
@@ -310,13 +571,16 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
|
310
571
|
return loadActive(token).record;
|
|
311
572
|
}
|
|
312
573
|
|
|
313
|
-
return Object.freeze({ arm, attest, consume, inspect, sweep });
|
|
574
|
+
return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
|
|
314
575
|
}
|
|
315
576
|
|
|
316
577
|
export { createCodexCapabilityStore };
|
|
317
578
|
export type {
|
|
318
579
|
CapabilityStatus,
|
|
580
|
+
CodexCapabilityProjection,
|
|
581
|
+
CodexCapabilityProvenanceDetail,
|
|
319
582
|
CodexCapabilityRecord,
|
|
320
583
|
CodexCapabilityStoreOptions,
|
|
321
584
|
CodexControllerBinding
|
|
322
585
|
};
|
|
586
|
+
export { isCodexCapabilityProvenanceDetail };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getProcessIdentityState, type ProcessIdentity } from '../../../server/process-state.ts';
|
|
6
|
+
import type { CodexControllerLeaseProofV1, CodexControllerLeaseV1 } from '../../../sandbox/control/controller-registration.ts';
|
|
7
|
+
import { computeLifecycleBuildIdentity, type LifecycleBuildIdentity } from './build-identity.ts';
|
|
8
|
+
|
|
9
|
+
const HEX_256 = /^[a-f0-9]{64}$/u;
|
|
10
|
+
|
|
11
|
+
export type CodexSandboxControllerContextV2 = Readonly<{
|
|
12
|
+
version: 2;
|
|
13
|
+
taskId: string;
|
|
14
|
+
controlGeneration: string;
|
|
15
|
+
controllerInstanceDigest: string;
|
|
16
|
+
controllerProcess: ProcessIdentity;
|
|
17
|
+
controllerLease: Readonly<{ version: 1; leaseId: string; leaseSecret: string }>;
|
|
18
|
+
issuedAt: number;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
21
|
+
hookDefinitionHash: string;
|
|
22
|
+
lifecycleProfilesHash: string;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
26
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function digestProfiles(repoRoot: string): string {
|
|
30
|
+
const files = [
|
|
31
|
+
path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml'),
|
|
32
|
+
path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml')
|
|
33
|
+
];
|
|
34
|
+
const hash = crypto.createHash('sha256');
|
|
35
|
+
for (const file of files.sort()) {
|
|
36
|
+
const stat = fs.lstatSync(file);
|
|
37
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
38
|
+
hash.update(path.basename(file));
|
|
39
|
+
hash.update('\0');
|
|
40
|
+
hash.update(fs.readFileSync(file));
|
|
41
|
+
hash.update('\0');
|
|
42
|
+
}
|
|
43
|
+
return hash.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validProcess(value: unknown): value is ProcessIdentity {
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
48
|
+
const identity = value as Record<string, unknown>;
|
|
49
|
+
return exactKeys(identity, ['pid', 'startTime'])
|
|
50
|
+
&& Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
|
|
51
|
+
&& Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validBuildIdentity(value: unknown): value is LifecycleBuildIdentity {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
56
|
+
const identity = value as Record<string, unknown>;
|
|
57
|
+
return exactKeys(identity, [
|
|
58
|
+
'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
|
|
59
|
+
])
|
|
60
|
+
&& identity.protocolVersion === 3
|
|
61
|
+
&& typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
|
|
62
|
+
&& typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
|
|
63
|
+
&& typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateContext(value: unknown): CodexSandboxControllerContextV2 {
|
|
67
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
68
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
69
|
+
}
|
|
70
|
+
const context = value as Record<string, unknown>;
|
|
71
|
+
const lease = context.controllerLease as Record<string, unknown> | null;
|
|
72
|
+
if (!exactKeys(context, [
|
|
73
|
+
'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
|
|
74
|
+
'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
|
|
75
|
+
'lifecycleProfilesHash', 'taskId', 'version'
|
|
76
|
+
])
|
|
77
|
+
|| context.version !== 2
|
|
78
|
+
|| typeof context.taskId !== 'string' || context.taskId.length === 0
|
|
79
|
+
|| typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
|
|
80
|
+
|| typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
|
|
81
|
+
|| !validProcess(context.controllerProcess)
|
|
82
|
+
|| !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
|
|
83
|
+
|| lease.version !== 1
|
|
84
|
+
|| typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
|
|
85
|
+
|| typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
|
|
86
|
+
|| !Number.isSafeInteger(context.issuedAt)
|
|
87
|
+
|| !Number.isSafeInteger(context.expiresAt)
|
|
88
|
+
|| (context.expiresAt as number) <= (context.issuedAt as number)
|
|
89
|
+
|| !validBuildIdentity(context.buildIdentity)
|
|
90
|
+
|| typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)
|
|
91
|
+
|| typeof context.lifecycleProfilesHash !== 'string' || !HEX_256.test(context.lifecycleProfilesHash)) {
|
|
92
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
93
|
+
}
|
|
94
|
+
return context as unknown as CodexSandboxControllerContextV2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function contextFromControllerLease(
|
|
98
|
+
lease: CodexControllerLeaseV1,
|
|
99
|
+
hashes: Readonly<{ hookDefinitionHash: string; lifecycleProfilesHash: string }>
|
|
100
|
+
): CodexSandboxControllerContextV2 {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
version: 2,
|
|
103
|
+
taskId: lease.taskId,
|
|
104
|
+
controlGeneration: lease.controlGeneration,
|
|
105
|
+
controllerInstanceDigest: lease.controllerInstanceDigest,
|
|
106
|
+
controllerProcess: lease.controllerProcess,
|
|
107
|
+
controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
|
|
108
|
+
issuedAt: lease.issuedAt,
|
|
109
|
+
expiresAt: lease.expiresAt,
|
|
110
|
+
buildIdentity: lease.buildIdentity,
|
|
111
|
+
hookDefinitionHash: hashes.hookDefinitionHash,
|
|
112
|
+
lifecycleProfilesHash: hashes.lifecycleProfilesHash
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function writeCodexSandboxControllerContext(
|
|
117
|
+
contextPath: string,
|
|
118
|
+
context: CodexSandboxControllerContextV2
|
|
119
|
+
): void {
|
|
120
|
+
validateContext(context);
|
|
121
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
|
|
122
|
+
const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
125
|
+
fs.chmodSync(temporary, 0o600);
|
|
126
|
+
fs.renameSync(temporary, contextPath);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(temporary, { force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function verifyCodexSandboxControllerContext(
|
|
133
|
+
contextPath: string,
|
|
134
|
+
options: Readonly<{
|
|
135
|
+
repoRoot?: string;
|
|
136
|
+
now?: number;
|
|
137
|
+
generation?: string;
|
|
138
|
+
probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
|
|
139
|
+
}> = {}
|
|
140
|
+
): CodexSandboxControllerContextV2 {
|
|
141
|
+
let stat: fs.Stats;
|
|
142
|
+
try {
|
|
143
|
+
stat = fs.lstatSync(contextPath);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
146
|
+
}
|
|
147
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
148
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
149
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
150
|
+
}
|
|
151
|
+
let context: CodexSandboxControllerContextV2;
|
|
152
|
+
try {
|
|
153
|
+
context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
|
|
154
|
+
} catch {
|
|
155
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
156
|
+
}
|
|
157
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
158
|
+
const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
159
|
+
const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
|
|
160
|
+
if (context.controlGeneration !== generation
|
|
161
|
+
|| context.expiresAt <= (options.now ?? Date.now())
|
|
162
|
+
|| state !== 'alive') {
|
|
163
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
164
|
+
}
|
|
165
|
+
if (JSON.stringify(computeLifecycleBuildIdentity(repoRoot)) !== JSON.stringify(context.buildIdentity)
|
|
166
|
+
|| crypto.createHash('sha256').update(fs.readFileSync(path.join(repoRoot, '.codex', 'hooks.json'))).digest('hex') !== context.hookDefinitionHash
|
|
167
|
+
|| digestProfiles(repoRoot) !== context.lifecycleProfilesHash) {
|
|
168
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze(context);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function controllerProofFromContext(context: CodexSandboxControllerContextV2): CodexControllerLeaseProofV1 {
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
version: 1,
|
|
176
|
+
leaseId: context.controllerLease.leaseId,
|
|
177
|
+
leaseSecret: context.controllerLease.leaseSecret,
|
|
178
|
+
controllerProcess: context.controllerProcess
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -147,6 +147,16 @@ function same(left: unknown, right: unknown): boolean {
|
|
|
147
147
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function sameChildCorrelation(
|
|
151
|
+
left: Extract<CodexLifecycleEvent, { type: 'hook-child' }>,
|
|
152
|
+
right: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
|
|
153
|
+
): boolean {
|
|
154
|
+
return left.sessionId === right.sessionId
|
|
155
|
+
&& left.childThreadId === right.childThreadId
|
|
156
|
+
&& left.parentThreadId === right.parentThreadId
|
|
157
|
+
&& left.nativeAgent === right.nativeAgent;
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
function requiredStrings(event: CodexLifecycleEvent): boolean {
|
|
151
161
|
return Object.entries(event).every(([key, value]) =>
|
|
152
162
|
key === 'forkedFromId'
|
|
@@ -286,6 +296,15 @@ function reduceCodexLifecycleEvent(
|
|
|
286
296
|
const existing = state[key];
|
|
287
297
|
if (existing) {
|
|
288
298
|
if (same(existing, event)) return state;
|
|
299
|
+
if (
|
|
300
|
+
existing.type === 'hook-child'
|
|
301
|
+
&& event.type === 'hook-child'
|
|
302
|
+
&& sameChildCorrelation(existing, event)
|
|
303
|
+
&& (existing.source === 'parent-rollout') !== (event.source === 'parent-rollout')
|
|
304
|
+
) {
|
|
305
|
+
if (existing.source !== 'parent-rollout') return state;
|
|
306
|
+
return derive(Object.freeze({ ...state, child: Object.freeze({ ...event }) }));
|
|
307
|
+
}
|
|
289
308
|
return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
|
|
290
309
|
}
|
|
291
310
|
return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"bin/internal-cli.ts",
|
|
4
4
|
"lib/internal/codex-lifecycle.ts",
|
|
5
5
|
"lib/internal/codex-sandbox-controller.ts",
|
|
6
|
+
"lib/internal/sandbox-control.ts",
|
|
6
7
|
"lib/internal/task-orchestration.ts",
|
|
7
8
|
"lib/task/codex-orchestration.ts",
|
|
8
9
|
"lib/task/delegation-receipts.ts",
|
|
@@ -10,10 +11,17 @@
|
|
|
10
11
|
"lib/agent-clients/adapters/codex-lifecycle/build-identity.ts",
|
|
11
12
|
"lib/agent-clients/adapters/codex-lifecycle/capability-store.ts",
|
|
12
13
|
"lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts",
|
|
14
|
+
"lib/agent-clients/adapters/codex-lifecycle/controller-context.ts",
|
|
13
15
|
"lib/agent-clients/adapters/codex-lifecycle/app-server.ts",
|
|
14
16
|
"lib/agent-clients/adapters/codex-lifecycle/evidence.ts",
|
|
15
17
|
"lib/agent-clients/adapters/codex-lifecycle/store.ts",
|
|
16
|
-
"lib/agent-clients/adapters/codex-lifecycle/manifest-files.json"
|
|
18
|
+
"lib/agent-clients/adapters/codex-lifecycle/manifest-files.json",
|
|
19
|
+
"lib/sandbox/control/protocol.ts",
|
|
20
|
+
"lib/sandbox/control/client.ts",
|
|
21
|
+
"lib/sandbox/control/executor.ts",
|
|
22
|
+
"lib/sandbox/control/controller-registration.ts",
|
|
23
|
+
"lib/server/process-state.ts",
|
|
24
|
+
"lib/sandbox/shell.ts"
|
|
17
25
|
],
|
|
18
26
|
"contractFiles": [
|
|
19
27
|
".codex/hooks.json",
|