@fitlab-ai/agent-infra 0.9.8 → 0.9.10
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 +11 -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 +25 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
- package/dist/lib/agent-clients/adapters/index.js +3 -1
- package/dist/lib/agent-clients/adapters/traecli.js +66 -0
- package/dist/lib/agent-clients/tokens.js +2 -2
- package/dist/lib/agent-clients/types.js +2 -1
- package/dist/lib/defaults.json +5 -0
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +20 -6
- package/dist/lib/internal/codex-sandbox-controller.js +9 -15
- 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/{task/commands/validate.js → internal/task-validate.js} +75 -42
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/run/tui.js +5 -2
- 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 +275 -0
- package/dist/lib/sandbox/control/executor.js +140 -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 +161 -39
- 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 +248 -6
- package/dist/lib/task/finalization.js +593 -0
- package/dist/lib/task/index.js +0 -9
- 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 +110 -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 +37 -10
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -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 +91 -118
- package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
- package/lib/agent-clients/adapters/index.ts +3 -1
- package/lib/agent-clients/adapters/traecli.ts +67 -0
- package/lib/agent-clients/tokens.ts +2 -2
- package/lib/agent-clients/types.ts +2 -1
- package/lib/defaults.json +5 -0
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +19 -6
- package/lib/internal/codex-sandbox-controller.ts +9 -14
- 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/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/run/tui.ts +5 -3
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +352 -0
- package/lib/sandbox/control/executor.ts +169 -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 +168 -40
- 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 +263 -4
- package/lib/task/finalization.ts +761 -0
- package/lib/task/index.ts +0 -8
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +218 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +115 -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 +3 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/compatibility-policy.en.md +39 -0
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +2 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
- package/templates/.agents/rules/review-method.en.md +1 -0
- package/templates/.agents/rules/review-method.zh-CN.md +1 -0
- 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/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- 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/code-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- 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 +17 -9
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
- 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/plan-task/SKILL.en.md +2 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
- 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/templates/.trae/skills/update-agent-infra.en.md +8 -0
- package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
- 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
|
@@ -3,6 +3,8 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
|
|
6
8
|
const LIFECYCLE_PROTOCOL_VERSION = 3;
|
|
7
9
|
|
|
8
10
|
type LifecycleBuildIdentity = Readonly<{
|
|
@@ -26,12 +28,27 @@ type IdentityVerification = Readonly<{
|
|
|
26
28
|
ok: boolean;
|
|
27
29
|
code: string | null;
|
|
28
30
|
message: string | null;
|
|
31
|
+
warnings: readonly LifecycleIdentityWarning[];
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
type LifecycleIdentityWarning = Readonly<{
|
|
35
|
+
code: 'CODEX_LIFECYCLE_BUILD_MISMATCH' | 'CODEX_LIFECYCLE_CONTRACT_MISMATCH';
|
|
36
|
+
message: string;
|
|
37
|
+
action: 'rebuild-sandbox';
|
|
29
38
|
}>;
|
|
30
39
|
|
|
31
40
|
function exactText(value: unknown): value is string {
|
|
32
41
|
return typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
function isCanonicalLifecyclePackageVersion(value: unknown): value is string {
|
|
45
|
+
return typeof value === 'string' && semver.valid(value) === value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isLifecycleProtocolVersion(value: unknown): value is typeof LIFECYCLE_PROTOCOL_VERSION {
|
|
49
|
+
return value === LIFECYCLE_PROTOCOL_VERSION;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
function normalizeRelativePath(value: string): string {
|
|
36
53
|
return value.replaceAll('\\', '/');
|
|
37
54
|
}
|
|
@@ -191,36 +208,46 @@ function verifyLifecycleBuildIdentity(
|
|
|
191
208
|
return {
|
|
192
209
|
ok: false,
|
|
193
210
|
code: 'CODEX_LIFECYCLE_PROTOCOL_MISMATCH',
|
|
194
|
-
message: 'Codex lifecycle protocol version does not match'
|
|
211
|
+
message: 'Codex lifecycle protocol version does not match',
|
|
212
|
+
warnings: []
|
|
195
213
|
};
|
|
196
214
|
}
|
|
215
|
+
const warnings: LifecycleIdentityWarning[] = [];
|
|
197
216
|
if (actual.packageVersion !== expected.packageVersion
|
|
198
217
|
|| actual.internalExecutableBuildHash !== expected.internalExecutableBuildHash) {
|
|
199
|
-
|
|
200
|
-
ok: false,
|
|
218
|
+
warnings.push({
|
|
201
219
|
code: 'CODEX_LIFECYCLE_BUILD_MISMATCH',
|
|
202
|
-
message: 'Codex lifecycle executable build identity
|
|
203
|
-
|
|
220
|
+
message: 'Codex lifecycle executable build identity differs; rebuild the sandbox if the runtime is stale',
|
|
221
|
+
action: 'rebuild-sandbox'
|
|
222
|
+
});
|
|
204
223
|
}
|
|
205
224
|
if (actual.lifecycleContractHash !== expected.lifecycleContractHash) {
|
|
206
|
-
|
|
207
|
-
ok: false,
|
|
225
|
+
warnings.push({
|
|
208
226
|
code: 'CODEX_LIFECYCLE_CONTRACT_MISMATCH',
|
|
209
|
-
message: 'Codex lifecycle contract identity
|
|
210
|
-
|
|
227
|
+
message: 'Codex lifecycle contract identity differs; rebuild the sandbox if the runtime is stale',
|
|
228
|
+
action: 'rebuild-sandbox'
|
|
229
|
+
});
|
|
211
230
|
}
|
|
212
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
code: null,
|
|
234
|
+
message: null,
|
|
235
|
+
warnings: Object.freeze(warnings)
|
|
236
|
+
};
|
|
213
237
|
}
|
|
214
238
|
|
|
215
239
|
export {
|
|
216
240
|
LIFECYCLE_PROTOCOL_VERSION,
|
|
217
241
|
computeLifecycleBuildIdentity,
|
|
242
|
+
isCanonicalLifecyclePackageVersion,
|
|
243
|
+
isLifecycleProtocolVersion,
|
|
218
244
|
readLifecycleManifestFiles,
|
|
219
245
|
resolveLifecycleExecutableFiles,
|
|
220
246
|
verifyLifecycleBuildIdentity
|
|
221
247
|
};
|
|
222
248
|
export type {
|
|
223
249
|
IdentityVerification,
|
|
250
|
+
LifecycleIdentityWarning,
|
|
224
251
|
LifecycleBuildIdentity,
|
|
225
252
|
LifecycleManifestFiles,
|
|
226
253
|
LifecycleBuildIdentityOptions
|
|
@@ -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,163 @@
|
|
|
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 {
|
|
8
|
+
computeLifecycleBuildIdentity,
|
|
9
|
+
verifyLifecycleBuildIdentity,
|
|
10
|
+
type LifecycleBuildIdentity,
|
|
11
|
+
type LifecycleIdentityWarning
|
|
12
|
+
} from './build-identity.ts';
|
|
13
|
+
|
|
14
|
+
const HEX_256 = /^[a-f0-9]{64}$/u;
|
|
15
|
+
|
|
16
|
+
export type CodexSandboxControllerContextV2 = Readonly<{
|
|
17
|
+
version: 2;
|
|
18
|
+
taskId: string;
|
|
19
|
+
controlGeneration: string;
|
|
20
|
+
controllerInstanceDigest: string;
|
|
21
|
+
controllerProcess: ProcessIdentity;
|
|
22
|
+
controllerLease: Readonly<{ version: 1; leaseId: string; leaseSecret: string }>;
|
|
23
|
+
issuedAt: number;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
26
|
+
hookDefinitionHash: string;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
30
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validProcess(value: unknown): value is ProcessIdentity {
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
35
|
+
const identity = value as Record<string, unknown>;
|
|
36
|
+
return exactKeys(identity, ['pid', 'startTime'])
|
|
37
|
+
&& Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
|
|
38
|
+
&& Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validBuildIdentity(value: unknown): value is LifecycleBuildIdentity {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
43
|
+
const identity = value as Record<string, unknown>;
|
|
44
|
+
return exactKeys(identity, [
|
|
45
|
+
'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
|
|
46
|
+
])
|
|
47
|
+
&& identity.protocolVersion === 3
|
|
48
|
+
&& typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
|
|
49
|
+
&& typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
|
|
50
|
+
&& typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateContext(value: unknown): CodexSandboxControllerContextV2 {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
55
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
56
|
+
}
|
|
57
|
+
const context = value as Record<string, unknown>;
|
|
58
|
+
const lease = context.controllerLease as Record<string, unknown> | null;
|
|
59
|
+
if (!exactKeys(context, [
|
|
60
|
+
'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
|
|
61
|
+
'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
|
|
62
|
+
'taskId', 'version'
|
|
63
|
+
])
|
|
64
|
+
|| context.version !== 2
|
|
65
|
+
|| typeof context.taskId !== 'string' || context.taskId.length === 0
|
|
66
|
+
|| typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
|
|
67
|
+
|| typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
|
|
68
|
+
|| !validProcess(context.controllerProcess)
|
|
69
|
+
|| !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
|
|
70
|
+
|| lease.version !== 1
|
|
71
|
+
|| typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
|
|
72
|
+
|| typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
|
|
73
|
+
|| !Number.isSafeInteger(context.issuedAt)
|
|
74
|
+
|| !Number.isSafeInteger(context.expiresAt)
|
|
75
|
+
|| (context.expiresAt as number) <= (context.issuedAt as number)
|
|
76
|
+
|| !validBuildIdentity(context.buildIdentity)
|
|
77
|
+
|| typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)) {
|
|
78
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
79
|
+
}
|
|
80
|
+
return context as unknown as CodexSandboxControllerContextV2;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function contextFromControllerLease(
|
|
84
|
+
lease: CodexControllerLeaseV1,
|
|
85
|
+
input: Readonly<{ hookDefinitionHash: string }>
|
|
86
|
+
): CodexSandboxControllerContextV2 {
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
version: 2,
|
|
89
|
+
taskId: lease.taskId,
|
|
90
|
+
controlGeneration: lease.controlGeneration,
|
|
91
|
+
controllerInstanceDigest: lease.controllerInstanceDigest,
|
|
92
|
+
controllerProcess: lease.controllerProcess,
|
|
93
|
+
controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
|
|
94
|
+
issuedAt: lease.issuedAt,
|
|
95
|
+
expiresAt: lease.expiresAt,
|
|
96
|
+
buildIdentity: lease.buildIdentity,
|
|
97
|
+
hookDefinitionHash: input.hookDefinitionHash
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeCodexSandboxControllerContext(
|
|
102
|
+
contextPath: string,
|
|
103
|
+
context: CodexSandboxControllerContextV2
|
|
104
|
+
): void {
|
|
105
|
+
validateContext(context);
|
|
106
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
|
|
107
|
+
const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
108
|
+
try {
|
|
109
|
+
fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
110
|
+
fs.chmodSync(temporary, 0o600);
|
|
111
|
+
fs.renameSync(temporary, contextPath);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(temporary, { force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function verifyCodexSandboxControllerContextWithWarnings(
|
|
118
|
+
contextPath: string,
|
|
119
|
+
options: Readonly<{
|
|
120
|
+
repoRoot?: string;
|
|
121
|
+
now?: number;
|
|
122
|
+
generation?: string;
|
|
123
|
+
probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
|
|
124
|
+
}> = {}
|
|
125
|
+
): Readonly<{ context: CodexSandboxControllerContextV2; warnings: readonly LifecycleIdentityWarning[] }> {
|
|
126
|
+
let stat: fs.Stats;
|
|
127
|
+
try {
|
|
128
|
+
stat = fs.lstatSync(contextPath);
|
|
129
|
+
} catch {
|
|
130
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
131
|
+
}
|
|
132
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
133
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
134
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
135
|
+
}
|
|
136
|
+
let context: CodexSandboxControllerContextV2;
|
|
137
|
+
try {
|
|
138
|
+
context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
141
|
+
}
|
|
142
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
143
|
+
const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
144
|
+
const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
|
|
145
|
+
if (context.controlGeneration !== generation
|
|
146
|
+
|| context.expiresAt <= (options.now ?? Date.now())
|
|
147
|
+
|| state !== 'alive') {
|
|
148
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
149
|
+
}
|
|
150
|
+
const currentBuildIdentity = computeLifecycleBuildIdentity(repoRoot);
|
|
151
|
+
const identity = verifyLifecycleBuildIdentity(context.buildIdentity, currentBuildIdentity);
|
|
152
|
+
if (!identity.ok) throw new Error(`${identity.code}: ${identity.message}`);
|
|
153
|
+
return Object.freeze({ context: Object.freeze(context), warnings: identity.warnings });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function controllerProofFromContext(context: CodexSandboxControllerContextV2): CodexControllerLeaseProofV1 {
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
version: 1,
|
|
159
|
+
leaseId: context.controllerLease.leaseId,
|
|
160
|
+
leaseSecret: context.controllerLease.leaseSecret,
|
|
161
|
+
controllerProcess: context.controllerProcess
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -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",
|