@fitlab-ai/agent-infra 0.9.7 → 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 +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- 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 +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -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 +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- 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/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- 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 +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- 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 +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- 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 +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -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/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- 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 +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- 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/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- 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
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ The most-used lifecycle commands, in delivery order. The command prefix varies b
|
|
|
182
182
|
| `code-task` → `review-code` | Implement and test, then run a structured code review |
|
|
183
183
|
| `commit` → `create-pr` → `complete-task` | Commit, open a PR, and archive the task |
|
|
184
184
|
|
|
185
|
-
Start with one atomic role policy, for example: `$run-task 42 --executor-model <id> --executor-reasoning-effort <value> --reviewer-model <id> --reviewer-reasoning-effort <value>`. Both roles may use the same model. With no explicit policy, `run-task` reads the current Agent Client's optional `agentClients[].orchestration`; if neither source is complete, it shows the host model-selection guidance before creating any run. Re-entry uses the persisted
|
|
185
|
+
Start with one atomic role policy, for example: `$run-task 42 --executor-model <id> --executor-reasoning-effort <value> --reviewer-model <id> --reviewer-reasoning-effort <value>`. Both roles may use the same model. With no explicit policy, `run-task` reads the current Agent Client's optional `agentClients[].orchestration`; if neither source is complete, it shows the host model-selection guidance before creating any run. Re-entry uses the persisted run policy. Finish or clear active runs before upgrading agent-infra; unsupported disk state fails closed without rewriting.
|
|
186
186
|
|
|
187
187
|
See the full catalog — task status, release, security, and project-maintenance skills — in [Built-in AI Skills](./docs/en/skills.md).
|
|
188
188
|
|
package/README.zh-CN.md
CHANGED
|
@@ -181,7 +181,7 @@ ai agent-client configure
|
|
|
181
181
|
| `code-task` → `review-code` | 实现并测试,再执行结构化代码审查 |
|
|
182
182
|
| `commit` → `create-pr` → `complete-task` | 提交、创建 PR、归档任务 |
|
|
183
183
|
|
|
184
|
-
启动时可提供原子角色策略,例如:`$run-task 42 --executor-model <id> --executor-reasoning-effort <值> --reviewer-model <id> --reviewer-reasoning-effort <值>`。两个角色可以使用同一模型。完全没有显式策略时,`run-task` 读取当前 Agent Client 可选的 `agentClients[].orchestration`;两个来源都不完整时,会在创建任何 run 前展示宿主模型选择指引。重入使用已持久化的
|
|
184
|
+
启动时可提供原子角色策略,例如:`$run-task 42 --executor-model <id> --executor-reasoning-effort <值> --reviewer-model <id> --reviewer-reasoning-effort <值>`。两个角色可以使用同一模型。完全没有显式策略时,`run-task` 读取当前 Agent Client 可选的 `agentClients[].orchestration`;两个来源都不完整时,会在创建任何 run 前展示宿主模型选择指引。重入使用已持久化的 run 策略。升级 agent-infra 前必须完成或清空 active run;不受支持的磁盘状态会失败关闭且不改写。
|
|
185
185
|
|
|
186
186
|
完整清单(任务状态、发布、安全、项目维护等 skill)见 [内置 AI Skills](./docs/zh-CN/skills.md)。
|
|
187
187
|
|
package/dist/bin/internal-cli.js
CHANGED
|
@@ -6,7 +6,7 @@ if (major < 22 || (major === 22 && minor < 9)) {
|
|
|
6
6
|
}
|
|
7
7
|
const command = process.argv[2] || '';
|
|
8
8
|
if (process.env.AGENT_INFRA_CONTROL_TOKEN
|
|
9
|
-
&& (command === 'task-lifecycle' || command === 'task-orchestration')) {
|
|
9
|
+
&& (command === 'task-lifecycle' || command === 'task-orchestration' || command === 'task-finalization')) {
|
|
10
10
|
const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
|
|
11
11
|
await sandboxControl(['client', command, ...process.argv.slice(3)]);
|
|
12
12
|
}
|
|
@@ -132,6 +132,16 @@ else
|
|
|
132
132
|
taskLifecycle(process.argv.slice(3));
|
|
133
133
|
break;
|
|
134
134
|
}
|
|
135
|
+
case 'task-finalization': {
|
|
136
|
+
const { taskFinalization } = await import('../lib/internal/task-finalization.js');
|
|
137
|
+
taskFinalization(process.argv.slice(3));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'task-override': {
|
|
141
|
+
const { taskOverride } = await import('../lib/internal/task-override.js');
|
|
142
|
+
taskOverride(process.argv.slice(3));
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
135
145
|
case 'task-short-id': {
|
|
136
146
|
const { taskShortId } = await import('../lib/internal/task-short-id.js');
|
|
137
147
|
taskShortId(process.argv.slice(3));
|
|
@@ -11,7 +11,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
|
|
|
11
11
|
commands: { level: 'integrated' },
|
|
12
12
|
hooks: { level: 'integrated' },
|
|
13
13
|
subagents: { level: 'experimental' },
|
|
14
|
-
orchestration: { level: '
|
|
14
|
+
orchestration: { level: 'experimental' },
|
|
15
15
|
sandbox: { level: 'integrated' },
|
|
16
16
|
verification: { level: 'compatible' }
|
|
17
17
|
},
|
|
@@ -21,8 +21,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
|
|
|
21
21
|
guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
|
|
22
22
|
},
|
|
23
23
|
delegationEvidence: {
|
|
24
|
-
actualModel: '
|
|
25
|
-
actualReasoningEffort: '
|
|
24
|
+
actualModel: 'host-event',
|
|
25
|
+
actualReasoningEffort: 'spawn-ack'
|
|
26
26
|
},
|
|
27
27
|
project: {
|
|
28
28
|
ownedPathPrefixes: ['.claude/'],
|
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import semver from 'semver';
|
|
7
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
|
|
7
8
|
const LIFECYCLE_HOOKS = Object.freeze([
|
|
8
9
|
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
9
10
|
Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
|
|
@@ -12,6 +13,10 @@ const LIFECYCLE_HOOKS = Object.freeze([
|
|
|
12
13
|
]);
|
|
13
14
|
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
14
15
|
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
16
|
+
const DEFAULT_THREAD_READ_ATTEMPTS = 21;
|
|
17
|
+
const DEFAULT_THREAD_RETRY_MS = 100;
|
|
18
|
+
const DEFAULT_TERMINAL_READ_ATTEMPTS = 21;
|
|
19
|
+
const DEFAULT_TERMINAL_RETRY_MS = 100;
|
|
15
20
|
function object(value) {
|
|
16
21
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
17
22
|
? value
|
|
@@ -61,16 +66,20 @@ function resolveCodexSpawnedChild(transcriptPath, expected) {
|
|
|
61
66
|
throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
62
67
|
const activities = records.filter((record) => {
|
|
63
68
|
const payload = object(record?.payload);
|
|
69
|
+
const item = object(payload?.item);
|
|
64
70
|
return record?.type === 'event_msg'
|
|
65
|
-
&& payload?.type === '
|
|
66
|
-
&& payload.
|
|
67
|
-
&& payload.
|
|
68
|
-
&&
|
|
69
|
-
&&
|
|
71
|
+
&& payload?.type === 'item_completed'
|
|
72
|
+
&& payload.thread_id === expected.sessionId
|
|
73
|
+
&& payload.turn_id === expected.turnId
|
|
74
|
+
&& item?.type === 'SubAgentActivity'
|
|
75
|
+
&& item.id === expected.toolUseId
|
|
76
|
+
&& item.kind === 'started'
|
|
77
|
+
&& item.agent_path === `/root/${expected.taskName}`
|
|
78
|
+
&& nonEmpty(item.agent_thread_id);
|
|
70
79
|
});
|
|
71
80
|
if (activities.length !== 1)
|
|
72
81
|
throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
73
|
-
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id);
|
|
82
|
+
return nonEmpty(object(object(activities[0]?.payload)?.item)?.agent_thread_id);
|
|
74
83
|
}
|
|
75
84
|
function validateCodexLifecycleHookConfig(value) {
|
|
76
85
|
const hooks = object(object(value)?.hooks);
|
|
@@ -85,7 +94,7 @@ function validateCodexLifecycleHookConfig(value) {
|
|
|
85
94
|
return entry.hooks.some((hookValue) => {
|
|
86
95
|
const hook = object(hookValue);
|
|
87
96
|
return hook?.type === 'command'
|
|
88
|
-
&& hook.timeout ===
|
|
97
|
+
&& hook.timeout === 30
|
|
89
98
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
90
99
|
});
|
|
91
100
|
});
|
|
@@ -409,14 +418,26 @@ async function resolveCodexThread(childThreadId, options = {}) {
|
|
|
409
418
|
const readThread = object(readResult?.thread);
|
|
410
419
|
if (nonEmpty(readThread?.id) !== childThreadId)
|
|
411
420
|
throw new Error('Codex App Server returned the wrong child thread');
|
|
412
|
-
let
|
|
421
|
+
let rolloutRecords;
|
|
413
422
|
try {
|
|
414
|
-
|
|
415
|
-
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
423
|
+
rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
|
|
416
424
|
}
|
|
417
425
|
catch {
|
|
418
426
|
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
419
427
|
}
|
|
428
|
+
let resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
429
|
+
const threadReadAttempts = options.threadReadAttempts ?? DEFAULT_THREAD_READ_ATTEMPTS;
|
|
430
|
+
for (let attempt = 1; resolution.thread.forkedFromId !== null && attempt < threadReadAttempts; attempt += 1) {
|
|
431
|
+
await new Promise((resolve) => setTimeout(resolve, options.threadRetryMs ?? DEFAULT_THREAD_RETRY_MS));
|
|
432
|
+
const refreshed = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
433
|
+
if (nonEmpty(object(refreshed?.thread)?.id) !== childThreadId) {
|
|
434
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
435
|
+
}
|
|
436
|
+
resolution = parseCodexThreadResolution(refreshed, rolloutRecords);
|
|
437
|
+
}
|
|
438
|
+
if (resolution.thread.forkedFromId !== null) {
|
|
439
|
+
throw new Error('Codex App Server fresh thread metadata is unavailable');
|
|
440
|
+
}
|
|
420
441
|
const reroutes = transport.notifications
|
|
421
442
|
.filter((entry) => entry.method === 'model/rerouted')
|
|
422
443
|
.map((entry) => parseCodexModelReroute(entry.params))
|
|
@@ -433,21 +454,38 @@ async function resolveCodexThread(childThreadId, options = {}) {
|
|
|
433
454
|
transport.close();
|
|
434
455
|
}
|
|
435
456
|
}
|
|
436
|
-
async function resolveCodexTerminal(childThreadId,
|
|
457
|
+
async function resolveCodexTerminal(childThreadId, expectedTurnIdOrOptions = {}, terminalOptions = {}) {
|
|
458
|
+
const expectedTurnId = typeof expectedTurnIdOrOptions === 'string' ? expectedTurnIdOrOptions : null;
|
|
459
|
+
const options = typeof expectedTurnIdOrOptions === 'string' ? terminalOptions : expectedTurnIdOrOptions;
|
|
437
460
|
const transport = new CodexAppServerTransport(options);
|
|
438
461
|
transport.start();
|
|
439
462
|
try {
|
|
440
463
|
await transport.initialize();
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
464
|
+
const attempts = expectedTurnId ? options.terminalReadAttempts ?? DEFAULT_TERMINAL_READ_ATTEMPTS : 1;
|
|
465
|
+
let abnormal = null;
|
|
466
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
467
|
+
const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
|
|
468
|
+
const thread = object(result?.thread);
|
|
469
|
+
if (nonEmpty(thread?.id) !== childThreadId)
|
|
470
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
471
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
472
|
+
const turn = expectedTurnId
|
|
473
|
+
? turns.find((candidate) => object(candidate)?.id === expectedTurnId)
|
|
474
|
+
: turns.at(-1);
|
|
475
|
+
const turnValue = object(turn);
|
|
476
|
+
if (turnValue && turnValue.status !== 'inProgress') {
|
|
477
|
+
const terminal = parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
478
|
+
if (terminal.status === 'completed')
|
|
479
|
+
return terminal;
|
|
480
|
+
abnormal = terminal;
|
|
481
|
+
}
|
|
482
|
+
if (attempt + 1 < attempts) {
|
|
483
|
+
await new Promise((resolve) => setTimeout(resolve, options.terminalRetryMs ?? DEFAULT_TERMINAL_RETRY_MS));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (abnormal)
|
|
487
|
+
return abnormal;
|
|
488
|
+
throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
451
489
|
}
|
|
452
490
|
finally {
|
|
453
491
|
transport.close();
|
|
@@ -482,7 +520,7 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
|
|
|
482
520
|
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
483
521
|
}
|
|
484
522
|
const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
|
|
485
|
-
const runtimeRoot =
|
|
523
|
+
const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
|
|
486
524
|
const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
|
|
487
525
|
const transport = new CodexAppServerTransport();
|
|
488
526
|
transport.start();
|
|
@@ -2,10 +2,17 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import semver from 'semver';
|
|
5
6
|
const LIFECYCLE_PROTOCOL_VERSION = 3;
|
|
6
7
|
function exactText(value) {
|
|
7
8
|
return typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
8
9
|
}
|
|
10
|
+
function isCanonicalLifecyclePackageVersion(value) {
|
|
11
|
+
return typeof value === 'string' && semver.valid(value) === value;
|
|
12
|
+
}
|
|
13
|
+
function isLifecycleProtocolVersion(value) {
|
|
14
|
+
return value === LIFECYCLE_PROTOCOL_VERSION;
|
|
15
|
+
}
|
|
9
16
|
function normalizeRelativePath(value) {
|
|
10
17
|
return value.replaceAll('\\', '/');
|
|
11
18
|
}
|
|
@@ -158,5 +165,5 @@ function verifyLifecycleBuildIdentity(expected, actual) {
|
|
|
158
165
|
}
|
|
159
166
|
return { ok: true, code: null, message: null };
|
|
160
167
|
}
|
|
161
|
-
export { LIFECYCLE_PROTOCOL_VERSION, computeLifecycleBuildIdentity, readLifecycleManifestFiles, resolveLifecycleExecutableFiles, verifyLifecycleBuildIdentity };
|
|
168
|
+
export { LIFECYCLE_PROTOCOL_VERSION, computeLifecycleBuildIdentity, isCanonicalLifecyclePackageVersion, isLifecycleProtocolVersion, readLifecycleManifestFiles, resolveLifecycleExecutableFiles, verifyLifecycleBuildIdentity };
|
|
162
169
|
//# sourceMappingURL=build-identity.js.map
|
|
@@ -1,28 +1,180 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { verifyLifecycleBuildIdentity } from './build-identity.js';
|
|
4
|
+
import { isCanonicalLifecyclePackageVersion, isLifecycleProtocolVersion, LIFECYCLE_PROTOCOL_VERSION, verifyLifecycleBuildIdentity } from './build-identity.js';
|
|
5
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
|
|
5
6
|
const DEFAULT_TTL_MS = 30_000;
|
|
6
7
|
const DEFAULT_TOMBSTONE_MS = 86_400_000;
|
|
7
8
|
const DEFAULT_MAX_RECORDS = 128;
|
|
8
9
|
const LOCK_RETRY_MS = 10;
|
|
9
10
|
const LOCK_TIMEOUT_MS = 1_000;
|
|
10
11
|
const LOCK_STALE_MS = 30_000;
|
|
12
|
+
const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
|
|
13
|
+
const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
|
|
14
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
15
|
+
const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
|
|
11
16
|
function digest(value) {
|
|
12
17
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
13
18
|
}
|
|
14
|
-
function capabilityError(code, message) {
|
|
19
|
+
function capabilityError(code, message, detail) {
|
|
15
20
|
const error = new Error(`${code}: ${message}`);
|
|
16
21
|
error.name = code;
|
|
22
|
+
if (detail)
|
|
23
|
+
error.detail = detail;
|
|
17
24
|
return error;
|
|
18
25
|
}
|
|
26
|
+
function objectValue(value, key) {
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return undefined;
|
|
29
|
+
return value[key];
|
|
30
|
+
}
|
|
31
|
+
function digestPrefix(fieldName, value) {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'digest-prefix',
|
|
34
|
+
value: `sha256:${crypto.createHash('sha256')
|
|
35
|
+
.update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
|
|
36
|
+
.digest('hex')
|
|
37
|
+
.slice(0, 16)}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function projectDigest(fieldName, value, pattern) {
|
|
41
|
+
return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
|
|
42
|
+
}
|
|
43
|
+
function projectTaskId(value) {
|
|
44
|
+
return projectDigest('taskId', value, TASK_ID_PATTERN);
|
|
45
|
+
}
|
|
46
|
+
function projectHash(fieldName, value) {
|
|
47
|
+
return projectDigest(fieldName, value, SHA256_PATTERN);
|
|
48
|
+
}
|
|
49
|
+
function projectProtocol(value) {
|
|
50
|
+
return isLifecycleProtocolVersion(value)
|
|
51
|
+
? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
|
|
52
|
+
: { kind: 'absent' };
|
|
53
|
+
}
|
|
54
|
+
function projectPackageVersion(value) {
|
|
55
|
+
return isCanonicalLifecyclePackageVersion(value)
|
|
56
|
+
? { kind: 'semver', value }
|
|
57
|
+
: { kind: 'absent' };
|
|
58
|
+
}
|
|
59
|
+
function projectPresence(value) {
|
|
60
|
+
return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
|
|
61
|
+
}
|
|
62
|
+
function field(matches, expected, actual) {
|
|
63
|
+
return { matches, expected, actual };
|
|
64
|
+
}
|
|
65
|
+
function provenanceDetail(record, expected) {
|
|
66
|
+
const actualBuild = record.buildIdentity;
|
|
67
|
+
const expectedBuild = expected.buildIdentity;
|
|
68
|
+
const actualController = record.controller;
|
|
69
|
+
const expectedController = expected.controller ?? null;
|
|
70
|
+
return {
|
|
71
|
+
kind: 'codex-capability-provenance-mismatch',
|
|
72
|
+
version: 1,
|
|
73
|
+
fields: {
|
|
74
|
+
taskId: field(record.taskId === expected.taskId, projectTaskId(expected.taskId), projectTaskId(record.taskId)),
|
|
75
|
+
hookDefinitionHash: field(record.hookDefinitionHash === expected.hookDefinitionHash, projectHash('hookDefinitionHash', expected.hookDefinitionHash), projectHash('hookDefinitionHash', record.hookDefinitionHash)),
|
|
76
|
+
buildIdentity: {
|
|
77
|
+
protocolVersion: field(objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'), projectProtocol(objectValue(expectedBuild, 'protocolVersion')), projectProtocol(objectValue(actualBuild, 'protocolVersion'))),
|
|
78
|
+
packageVersion: field(objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'), projectPackageVersion(objectValue(expectedBuild, 'packageVersion')), projectPackageVersion(objectValue(actualBuild, 'packageVersion'))),
|
|
79
|
+
internalExecutableBuildHash: field(objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'), projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')), projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))),
|
|
80
|
+
lifecycleContractHash: field(objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'), projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')), projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash')))
|
|
81
|
+
},
|
|
82
|
+
controller: {
|
|
83
|
+
instanceDigest: field(sameControllerField(actualController, expectedController, 'instanceDigest'), projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController), projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)),
|
|
84
|
+
controlGeneration: field(sameControllerField(actualController, expectedController, 'controlGeneration'), projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController), projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function exactKeys(value, keys) {
|
|
90
|
+
return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
91
|
+
}
|
|
92
|
+
function isCodexControllerBinding(value) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
94
|
+
return false;
|
|
95
|
+
const record = value;
|
|
96
|
+
return exactKeys(record, ['instanceDigest', 'controlGeneration'])
|
|
97
|
+
&& typeof record.instanceDigest === 'string'
|
|
98
|
+
&& typeof record.controlGeneration === 'string';
|
|
99
|
+
}
|
|
100
|
+
function isProjection(value) {
|
|
101
|
+
if (!value || typeof value !== 'object')
|
|
102
|
+
return false;
|
|
103
|
+
const record = value;
|
|
104
|
+
if (record.kind === 'absent')
|
|
105
|
+
return exactKeys(record, ['kind']);
|
|
106
|
+
if (record.kind === 'protocol-version') {
|
|
107
|
+
return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
|
|
108
|
+
}
|
|
109
|
+
if (record.kind === 'semver') {
|
|
110
|
+
return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
|
|
111
|
+
}
|
|
112
|
+
if (record.kind === 'digest-prefix') {
|
|
113
|
+
return exactKeys(record, ['kind', 'value'])
|
|
114
|
+
&& typeof record.value === 'string'
|
|
115
|
+
&& DIGEST_PREFIX_PATTERN.test(record.value);
|
|
116
|
+
}
|
|
117
|
+
if (record.kind === 'presence') {
|
|
118
|
+
return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function isProvenanceField(value) {
|
|
123
|
+
if (!value || typeof value !== 'object')
|
|
124
|
+
return false;
|
|
125
|
+
const record = value;
|
|
126
|
+
return exactKeys(record, ['matches', 'expected', 'actual'])
|
|
127
|
+
&& typeof record.matches === 'boolean'
|
|
128
|
+
&& isProjection(record.expected)
|
|
129
|
+
&& isProjection(record.actual);
|
|
130
|
+
}
|
|
131
|
+
function isCodexCapabilityProvenanceDetail(value) {
|
|
132
|
+
if (!value || typeof value !== 'object')
|
|
133
|
+
return false;
|
|
134
|
+
const record = value;
|
|
135
|
+
if (!exactKeys(record, ['kind', 'version', 'fields'])
|
|
136
|
+
|| record.kind !== 'codex-capability-provenance-mismatch'
|
|
137
|
+
|| record.version !== 1
|
|
138
|
+
|| !record.fields || typeof record.fields !== 'object')
|
|
139
|
+
return false;
|
|
140
|
+
const fields = record.fields;
|
|
141
|
+
if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
|
|
142
|
+
|| !isProvenanceField(fields.taskId)
|
|
143
|
+
|| !isProvenanceField(fields.hookDefinitionHash))
|
|
144
|
+
return false;
|
|
145
|
+
const buildIdentity = fields.buildIdentity;
|
|
146
|
+
const controller = fields.controller;
|
|
147
|
+
if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object')
|
|
148
|
+
return false;
|
|
149
|
+
const build = buildIdentity;
|
|
150
|
+
const binding = controller;
|
|
151
|
+
return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
|
|
152
|
+
&& exactKeys(binding, ['instanceDigest', 'controlGeneration'])
|
|
153
|
+
&& isProvenanceField(build.protocolVersion)
|
|
154
|
+
&& isProvenanceField(build.packageVersion)
|
|
155
|
+
&& isProvenanceField(build.internalExecutableBuildHash)
|
|
156
|
+
&& isProvenanceField(build.lifecycleContractHash)
|
|
157
|
+
&& isProvenanceField(binding.instanceDigest)
|
|
158
|
+
&& isProvenanceField(binding.controlGeneration);
|
|
159
|
+
}
|
|
160
|
+
function sameControllerField(left, right, key) {
|
|
161
|
+
if (left === null || right === null)
|
|
162
|
+
return left === null && right === null;
|
|
163
|
+
return isCodexControllerBinding(left)
|
|
164
|
+
&& isCodexControllerBinding(right)
|
|
165
|
+
&& left[key] === right[key];
|
|
166
|
+
}
|
|
19
167
|
function sameController(left, right) {
|
|
20
|
-
|
|
21
|
-
|
|
168
|
+
if (left === null || right === null)
|
|
169
|
+
return left === null && right === null;
|
|
170
|
+
return isCodexControllerBinding(left)
|
|
171
|
+
&& isCodexControllerBinding(right)
|
|
172
|
+
&& left.instanceDigest === right.instanceDigest
|
|
173
|
+
&& left.controlGeneration === right.controlGeneration;
|
|
22
174
|
}
|
|
23
175
|
function createCodexCapabilityStore(options = {}) {
|
|
24
176
|
const root = options.root
|
|
25
|
-
??
|
|
177
|
+
?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
|
|
26
178
|
const now = options.now ?? Date.now;
|
|
27
179
|
const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
|
|
28
180
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
@@ -210,22 +362,33 @@ function createCodexCapabilityStore(options = {}) {
|
|
|
210
362
|
write(file, next, record.revision);
|
|
211
363
|
return next;
|
|
212
364
|
}
|
|
213
|
-
function
|
|
214
|
-
sweep();
|
|
215
|
-
const { file, record } = loadActive(token);
|
|
365
|
+
function validateRecord(record, expected) {
|
|
216
366
|
if (record.status === 'consumed') {
|
|
217
367
|
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
|
|
218
368
|
}
|
|
219
369
|
if (record.status !== 'attested') {
|
|
220
370
|
throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
|
|
221
371
|
}
|
|
222
|
-
const identity =
|
|
372
|
+
const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
|
|
373
|
+
? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
|
|
374
|
+
: { ok: false };
|
|
223
375
|
if (!identity.ok
|
|
224
376
|
|| record.taskId !== expected.taskId
|
|
225
377
|
|| record.hookDefinitionHash !== expected.hookDefinitionHash
|
|
226
378
|
|| !sameController(record.controller, expected.controller ?? null)) {
|
|
227
|
-
throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
|
|
379
|
+
throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match', provenanceDetail(record, expected));
|
|
228
380
|
}
|
|
381
|
+
return record;
|
|
382
|
+
}
|
|
383
|
+
function validate(token, expected) {
|
|
384
|
+
sweep();
|
|
385
|
+
const { record } = loadActive(token);
|
|
386
|
+
return validateRecord(record, expected);
|
|
387
|
+
}
|
|
388
|
+
function consume(token, expected) {
|
|
389
|
+
sweep();
|
|
390
|
+
const { file, record } = loadActive(token);
|
|
391
|
+
validateRecord(record, expected);
|
|
229
392
|
const next = Object.freeze({
|
|
230
393
|
...record,
|
|
231
394
|
revision: record.revision + 1,
|
|
@@ -238,7 +401,8 @@ function createCodexCapabilityStore(options = {}) {
|
|
|
238
401
|
function inspect(token) {
|
|
239
402
|
return loadActive(token).record;
|
|
240
403
|
}
|
|
241
|
-
return Object.freeze({ arm, attest, consume, inspect, sweep });
|
|
404
|
+
return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
|
|
242
405
|
}
|
|
243
406
|
export { createCodexCapabilityStore };
|
|
407
|
+
export { isCodexCapabilityProvenanceDetail };
|
|
244
408
|
//# sourceMappingURL=capability-store.js.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getProcessIdentityState } from '../../../server/process-state.js';
|
|
5
|
+
import { computeLifecycleBuildIdentity } from './build-identity.js';
|
|
6
|
+
const HEX_256 = /^[a-f0-9]{64}$/u;
|
|
7
|
+
function exactKeys(value, keys) {
|
|
8
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
9
|
+
}
|
|
10
|
+
function digestProfiles(repoRoot) {
|
|
11
|
+
const files = [
|
|
12
|
+
path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-executor.toml'),
|
|
13
|
+
path.join(repoRoot, '.codex', 'agents', 'agent-infra-lifecycle-reviewer.toml')
|
|
14
|
+
];
|
|
15
|
+
const hash = crypto.createHash('sha256');
|
|
16
|
+
for (const file of files.sort()) {
|
|
17
|
+
const stat = fs.lstatSync(file);
|
|
18
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
19
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
20
|
+
hash.update(path.basename(file));
|
|
21
|
+
hash.update('\0');
|
|
22
|
+
hash.update(fs.readFileSync(file));
|
|
23
|
+
hash.update('\0');
|
|
24
|
+
}
|
|
25
|
+
return hash.digest('hex');
|
|
26
|
+
}
|
|
27
|
+
function validProcess(value) {
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
29
|
+
return false;
|
|
30
|
+
const identity = value;
|
|
31
|
+
return exactKeys(identity, ['pid', 'startTime'])
|
|
32
|
+
&& Number.isSafeInteger(identity.pid) && identity.pid > 0
|
|
33
|
+
&& Number.isSafeInteger(identity.startTime) && identity.startTime >= 0;
|
|
34
|
+
}
|
|
35
|
+
function validBuildIdentity(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return false;
|
|
38
|
+
const identity = value;
|
|
39
|
+
return exactKeys(identity, [
|
|
40
|
+
'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
|
|
41
|
+
])
|
|
42
|
+
&& identity.protocolVersion === 3
|
|
43
|
+
&& typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
|
|
44
|
+
&& typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
|
|
45
|
+
&& typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
|
|
46
|
+
}
|
|
47
|
+
function validateContext(value) {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
49
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
50
|
+
}
|
|
51
|
+
const context = value;
|
|
52
|
+
const lease = context.controllerLease;
|
|
53
|
+
if (!exactKeys(context, [
|
|
54
|
+
'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
|
|
55
|
+
'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
|
|
56
|
+
'lifecycleProfilesHash', 'taskId', 'version'
|
|
57
|
+
])
|
|
58
|
+
|| context.version !== 2
|
|
59
|
+
|| typeof context.taskId !== 'string' || context.taskId.length === 0
|
|
60
|
+
|| typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
|
|
61
|
+
|| typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
|
|
62
|
+
|| !validProcess(context.controllerProcess)
|
|
63
|
+
|| !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
|
|
64
|
+
|| lease.version !== 1
|
|
65
|
+
|| typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
|
|
66
|
+
|| typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
|
|
67
|
+
|| !Number.isSafeInteger(context.issuedAt)
|
|
68
|
+
|| !Number.isSafeInteger(context.expiresAt)
|
|
69
|
+
|| context.expiresAt <= context.issuedAt
|
|
70
|
+
|| !validBuildIdentity(context.buildIdentity)
|
|
71
|
+
|| typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)
|
|
72
|
+
|| typeof context.lifecycleProfilesHash !== 'string' || !HEX_256.test(context.lifecycleProfilesHash)) {
|
|
73
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
74
|
+
}
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
export function contextFromControllerLease(lease, hashes) {
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
version: 2,
|
|
80
|
+
taskId: lease.taskId,
|
|
81
|
+
controlGeneration: lease.controlGeneration,
|
|
82
|
+
controllerInstanceDigest: lease.controllerInstanceDigest,
|
|
83
|
+
controllerProcess: lease.controllerProcess,
|
|
84
|
+
controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
|
|
85
|
+
issuedAt: lease.issuedAt,
|
|
86
|
+
expiresAt: lease.expiresAt,
|
|
87
|
+
buildIdentity: lease.buildIdentity,
|
|
88
|
+
hookDefinitionHash: hashes.hookDefinitionHash,
|
|
89
|
+
lifecycleProfilesHash: hashes.lifecycleProfilesHash
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function writeCodexSandboxControllerContext(contextPath, context) {
|
|
93
|
+
validateContext(context);
|
|
94
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
|
|
95
|
+
const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
98
|
+
fs.chmodSync(temporary, 0o600);
|
|
99
|
+
fs.renameSync(temporary, contextPath);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
fs.rmSync(temporary, { force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function verifyCodexSandboxControllerContext(contextPath, options = {}) {
|
|
106
|
+
let stat;
|
|
107
|
+
try {
|
|
108
|
+
stat = fs.lstatSync(contextPath);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
112
|
+
}
|
|
113
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
114
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
115
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
116
|
+
}
|
|
117
|
+
let context;
|
|
118
|
+
try {
|
|
119
|
+
context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
123
|
+
}
|
|
124
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
125
|
+
const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
126
|
+
const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
|
|
127
|
+
if (context.controlGeneration !== generation
|
|
128
|
+
|| context.expiresAt <= (options.now ?? Date.now())
|
|
129
|
+
|| state !== 'alive') {
|
|
130
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
131
|
+
}
|
|
132
|
+
if (JSON.stringify(computeLifecycleBuildIdentity(repoRoot)) !== JSON.stringify(context.buildIdentity)
|
|
133
|
+
|| crypto.createHash('sha256').update(fs.readFileSync(path.join(repoRoot, '.codex', 'hooks.json'))).digest('hex') !== context.hookDefinitionHash
|
|
134
|
+
|| digestProfiles(repoRoot) !== context.lifecycleProfilesHash) {
|
|
135
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
|
|
136
|
+
}
|
|
137
|
+
return Object.freeze(context);
|
|
138
|
+
}
|
|
139
|
+
export function controllerProofFromContext(context) {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
version: 1,
|
|
142
|
+
leaseId: context.controllerLease.leaseId,
|
|
143
|
+
leaseSecret: context.controllerLease.leaseSecret,
|
|
144
|
+
controllerProcess: context.controllerProcess
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=controller-context.js.map
|
|
@@ -29,6 +29,12 @@ function invalid(state, code, message) {
|
|
|
29
29
|
function same(left, right) {
|
|
30
30
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
31
31
|
}
|
|
32
|
+
function sameChildCorrelation(left, right) {
|
|
33
|
+
return left.sessionId === right.sessionId
|
|
34
|
+
&& left.childThreadId === right.childThreadId
|
|
35
|
+
&& left.parentThreadId === right.parentThreadId
|
|
36
|
+
&& left.nativeAgent === right.nativeAgent;
|
|
37
|
+
}
|
|
32
38
|
function requiredStrings(event) {
|
|
33
39
|
return Object.entries(event).every(([key, value]) => key === 'forkedFromId'
|
|
34
40
|
|| (typeof value !== 'string' || value.trim() !== ''));
|
|
@@ -152,6 +158,14 @@ function reduceCodexLifecycleEvent(state, event) {
|
|
|
152
158
|
if (existing) {
|
|
153
159
|
if (same(existing, event))
|
|
154
160
|
return state;
|
|
161
|
+
if (existing.type === 'hook-child'
|
|
162
|
+
&& event.type === 'hook-child'
|
|
163
|
+
&& sameChildCorrelation(existing, event)
|
|
164
|
+
&& (existing.source === 'parent-rollout') !== (event.source === 'parent-rollout')) {
|
|
165
|
+
if (existing.source !== 'parent-rollout')
|
|
166
|
+
return state;
|
|
167
|
+
return derive(Object.freeze({ ...state, child: Object.freeze({ ...event }) }));
|
|
168
|
+
}
|
|
155
169
|
return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
|
|
156
170
|
}
|
|
157
171
|
return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
|