@fitlab-ai/agent-infra 0.11.13 → 0.11.15

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.
Files changed (115) hide show
  1. package/dist/bin/internal-cli.js +2 -7
  2. package/dist/lib/agent-clients/adapter.js +0 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  7. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  8. package/dist/lib/internal/cli-route-inventory.js +0 -1
  9. package/dist/lib/internal/task-finalization.js +7 -5
  10. package/dist/lib/internal/task-lifecycle.js +3 -1
  11. package/dist/lib/internal/task-operation-registry.js +1 -3
  12. package/dist/lib/internal/task-validate.js +14 -124
  13. package/dist/lib/platform/pr-change-report.js +10 -3
  14. package/dist/lib/platform/release-notes.js +2 -2
  15. package/dist/lib/process-data/index.js +15 -4
  16. package/dist/lib/sandbox/commands/control-status.js +0 -1
  17. package/dist/lib/sandbox/commands/create.js +5 -4
  18. package/dist/lib/sandbox/config.js +12 -0
  19. package/dist/lib/sandbox/control/audit.js +1 -2
  20. package/dist/lib/sandbox/control/client.js +12 -47
  21. package/dist/lib/sandbox/control/executor.js +6 -10
  22. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  23. package/dist/lib/sandbox/control/lifecycle.js +22 -37
  24. package/dist/lib/sandbox/control/protocol.js +8 -36
  25. package/dist/lib/sandbox/control/server.js +2 -21
  26. package/dist/lib/sandbox/environment.js +7 -8
  27. package/dist/lib/sandbox/mounts.js +71 -59
  28. package/dist/lib/sandbox/recovery.js +2 -3
  29. package/dist/lib/sandbox/removal.js +19 -4
  30. package/dist/lib/sandbox/workspace-identity.js +30 -15
  31. package/dist/lib/sandbox/workspace-view.js +41 -4
  32. package/dist/lib/task/control-authority.js +39 -17
  33. package/dist/lib/task/control-recovery.js +1 -2
  34. package/dist/lib/task/finalization.js +13 -2
  35. package/dist/lib/task/lifecycle.js +122 -35
  36. package/dist/lib/task/short-id.js +2 -2
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapter.ts +0 -27
  39. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  40. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  41. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  42. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  43. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
  44. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  45. package/lib/agent-clients/adapters/codex.ts +0 -2
  46. package/lib/internal/cli-route-inventory.ts +0 -1
  47. package/lib/internal/task-finalization.ts +8 -6
  48. package/lib/internal/task-lifecycle.ts +3 -1
  49. package/lib/internal/task-operation-registry.ts +1 -3
  50. package/lib/internal/task-validate.ts +13 -127
  51. package/lib/platform/pr-change-report.ts +9 -3
  52. package/lib/platform/release-notes.ts +2 -2
  53. package/lib/process-data/index.ts +15 -4
  54. package/lib/sandbox/commands/control-status.ts +0 -1
  55. package/lib/sandbox/commands/create.ts +5 -4
  56. package/lib/sandbox/config.ts +13 -0
  57. package/lib/sandbox/control/audit.ts +1 -2
  58. package/lib/sandbox/control/client.ts +13 -61
  59. package/lib/sandbox/control/executor.ts +7 -13
  60. package/lib/sandbox/control/host-recovery.ts +94 -21
  61. package/lib/sandbox/control/lifecycle.ts +22 -37
  62. package/lib/sandbox/control/protocol.ts +9 -45
  63. package/lib/sandbox/control/server.ts +2 -17
  64. package/lib/sandbox/environment.ts +7 -8
  65. package/lib/sandbox/mounts.ts +92 -75
  66. package/lib/sandbox/recovery.ts +2 -3
  67. package/lib/sandbox/removal.ts +16 -4
  68. package/lib/sandbox/workspace-identity.ts +36 -13
  69. package/lib/sandbox/workspace-view.ts +40 -5
  70. package/lib/task/control-authority.ts +40 -26
  71. package/lib/task/control-recovery.ts +2 -3
  72. package/lib/task/finalization.ts +15 -2
  73. package/lib/task/lifecycle.ts +135 -33
  74. package/lib/task/short-id.ts +5 -2
  75. package/package.json +1 -1
  76. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  77. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  78. package/templates/.agents/rules/task-short-id.en.md +18 -12
  79. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  80. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
  81. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
  82. package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
  83. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
  84. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  85. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  86. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
  87. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
  88. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
  89. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
  90. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  91. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  92. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  93. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  94. package/templates/.agents/skills/watch-pr/SKILL.en.md +19 -2
  95. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
  96. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
  97. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
  98. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  99. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  100. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  101. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  102. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  103. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  104. package/dist/lib/runtime/agent-runtime.js +0 -33
  105. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  106. package/dist/lib/task/runtime-paths.js +0 -10
  107. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  108. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  109. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  110. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  111. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  112. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  113. package/lib/runtime/agent-runtime.ts +0 -41
  114. package/lib/sandbox/control/agent-operation.ts +0 -49
  115. package/lib/task/runtime-paths.ts +0 -12
@@ -1,163 +0,0 @@
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 './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
- }
@@ -1,151 +0,0 @@
1
- import type { ProcessIdentity } from '../../../server/process-state.ts';
2
- import {
3
- closeCodexControllerRegistration,
4
- CodexControllerRegistrationError,
5
- openCodexControllerRegistration,
6
- readCodexControllerRegistration,
7
- resolveCodexControllerBinding,
8
- type CodexControllerLeaseProofV1
9
- } from './controller-registration.ts';
10
- import { computeLifecycleBuildIdentity } from './build-identity.ts';
11
-
12
- function validProcess(value: unknown): value is ProcessIdentity {
13
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
14
- const process = value as Record<string, unknown>;
15
- return Object.keys(process).sort().join(',') === 'pid,startTime'
16
- && Number.isSafeInteger(process.pid) && (process.pid as number) > 0
17
- && Number.isSafeInteger(process.startTime) && (process.startTime as number) >= 0;
18
- }
19
-
20
- function validProof(value: unknown): value is CodexControllerLeaseProofV1 {
21
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
22
- const proof = value as Record<string, unknown>;
23
- return Object.keys(proof).sort().join(',') === 'controllerProcess,leaseId,leaseSecret,version'
24
- && proof.version === 1
25
- && typeof proof.leaseId === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseId)
26
- && typeof proof.leaseSecret === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseSecret)
27
- && validProcess(proof.controllerProcess);
28
- }
29
-
30
- function validate(params: Readonly<{
31
- operation: string;
32
- payload: Record<string, unknown>;
33
- manifest: { mode: string; taskId: string | null };
34
- }>): void {
35
- const { operation, payload, manifest } = params;
36
- const expectedKeys = operation === 'controller.open' ? ['controllerProcess'] : ['proof'];
37
- if (!['controller.open', 'controller.close', 'controller.verify'].includes(operation)
38
- || Object.keys(payload).sort().join(',') !== expectedKeys.sort().join(',')
39
- || (operation === 'controller.open'
40
- ? !validProcess(payload.controllerProcess)
41
- : !validProof(payload.proof))) {
42
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: controller operation payload is invalid');
43
- }
44
- if (manifest.mode !== 'task-bound' || !manifest.taskId) {
45
- throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: controller operations require a task-bound sandbox');
46
- }
47
- }
48
-
49
- async function execute(params: Readonly<{
50
- operation: string;
51
- payload: Record<string, unknown>;
52
- manifest: Parameters<typeof openCodexControllerRegistration>[0]['manifest'];
53
- manifestPath: string;
54
- }>): Promise<Readonly<{ exitCode: number; stdout: string; stderr: string }>> {
55
- const { operation, payload, manifest, manifestPath } = params;
56
- try {
57
- const result = operation === 'controller.open'
58
- ? openCodexControllerRegistration({
59
- manifest,
60
- manifestPath,
61
- controllerProcess: payload.controllerProcess as ProcessIdentity,
62
- buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot)
63
- })
64
- : operation === 'controller.close'
65
- ? closeCodexControllerRegistration({
66
- manifest,
67
- manifestPath,
68
- proof: payload.proof as CodexControllerLeaseProofV1
69
- })
70
- : (() => {
71
- const binding = resolveCodexControllerBinding({
72
- manifest,
73
- manifestPath,
74
- proof: payload.proof as CodexControllerLeaseProofV1,
75
- buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot)
76
- });
77
- return {
78
- version: 1,
79
- status: 'verified',
80
- changed: false,
81
- lease: null,
82
- binding: {
83
- taskId: manifest.taskId,
84
- controlGeneration: binding.controlGeneration,
85
- controllerInstanceDigest: binding.instanceDigest
86
- },
87
- error: null
88
- };
89
- })();
90
- return { exitCode: 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
91
- } catch (error) {
92
- const message = error instanceof Error ? error.message : String(error);
93
- const code = error instanceof CodexControllerRegistrationError
94
- ? error.code
95
- : /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
96
- return {
97
- exitCode: 1,
98
- stdout: `${JSON.stringify({
99
- version: 1,
100
- status: 'failed',
101
- changed: false,
102
- lease: null,
103
- error: { code, message, retryable: false }
104
- })}\n`,
105
- stderr: ''
106
- };
107
- }
108
- }
109
-
110
- function recover(params: Readonly<{
111
- operation: string;
112
- manifest: Parameters<typeof openCodexControllerRegistration>[0]['manifest'];
113
- manifestPath: string;
114
- stdout: string | null;
115
- }>): Readonly<Record<string, unknown>> {
116
- if (params.stdout === null) return { consistent: false };
117
- let output: Record<string, unknown>;
118
- try {
119
- const parsed: unknown = JSON.parse(params.stdout);
120
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return { consistent: false };
121
- output = parsed as Record<string, unknown>;
122
- } catch {
123
- return { consistent: false };
124
- }
125
- if (output.error !== null || typeof output.status !== 'string') return { consistent: false };
126
- try {
127
- const registration = readCodexControllerRegistration(params.manifestPath);
128
- if (params.operation === 'controller.open') {
129
- const lease = output.lease as Record<string, unknown> | null;
130
- return { consistent: output.status === 'opened' && output.changed === true
131
- && lease?.controlGeneration === registration.controlGeneration
132
- && lease?.controllerInstanceDigest === registration.controllerInstanceDigest
133
- && registration.controlGeneration === params.manifest.generation };
134
- }
135
- if (params.operation === 'controller.verify') {
136
- const binding = output.binding as Record<string, unknown> | null;
137
- return { consistent: output.status === 'verified' && output.changed === false
138
- && binding?.controlGeneration === registration.controlGeneration
139
- && binding?.controllerInstanceDigest === registration.controllerInstanceDigest };
140
- }
141
- return { consistent: false };
142
- } catch (error) {
143
- return { consistent: params.operation === 'controller.close'
144
- && (error as { code?: string }).code === 'CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING'
145
- && output.status === 'closed' && typeof output.changed === 'boolean' };
146
- }
147
- }
148
-
149
- const codexControllerOperation = Object.freeze({ validate, execute, recover });
150
-
151
- export { codexControllerOperation };
@@ -1,352 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- import type { LifecycleBuildIdentity } from './build-identity.ts';
6
- import { parseLinuxProcessStat, type ProcessIdentity, type ProcessIdentityState } from '../../../server/process-state.ts';
7
- import { commandForEngine, runProbe } from '../../../sandbox/shell.ts';
8
- import type { SandboxControlManifest } from '../../../sandbox/control/protocol.ts';
9
-
10
- const CONTROLLER_TTL_MS = 4 * 60 * 60 * 1_000;
11
- const HEX_256 = /^[a-f0-9]{64}$/u;
12
- const REGISTRATION_KEYS = [
13
- 'buildIdentity', 'containerId', 'controlGeneration', 'controllerInstanceDigest',
14
- 'controllerProcess', 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecretHash',
15
- 'taskId', 'version'
16
- ].sort().join(',');
17
-
18
- export type CodexControllerLeaseProofV1 = Readonly<{
19
- version: 1;
20
- leaseId: string;
21
- leaseSecret: string;
22
- controllerProcess: ProcessIdentity;
23
- }>;
24
-
25
- export type CodexControllerRegistrationV1 = Readonly<{
26
- version: 1;
27
- taskId: string;
28
- controlGeneration: string;
29
- containerId: string;
30
- leaseId: string;
31
- leaseSecretHash: string;
32
- controllerInstanceDigest: string;
33
- controllerProcess: ProcessIdentity;
34
- buildIdentity: LifecycleBuildIdentity;
35
- issuedAt: number;
36
- expiresAt: number;
37
- }>;
38
-
39
- export type CodexControllerLeaseV1 = Readonly<{
40
- version: 1;
41
- leaseId: string;
42
- leaseSecret: string;
43
- taskId: string;
44
- controlGeneration: string;
45
- controllerInstanceDigest: string;
46
- controllerProcess: ProcessIdentity;
47
- buildIdentity: LifecycleBuildIdentity;
48
- issuedAt: number;
49
- expiresAt: number;
50
- }>;
51
-
52
- export type CodexControllerOpened = Readonly<{
53
- version: 1;
54
- status: 'opened';
55
- changed: true;
56
- lease: CodexControllerLeaseV1;
57
- error: null;
58
- }>;
59
-
60
- type RegistrationOptions = Readonly<{
61
- now?: () => number;
62
- probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
63
- randomHex?: () => string;
64
- beforeCommit?: () => void;
65
- }>;
66
-
67
- export class CodexControllerRegistrationError extends Error {
68
- readonly code: string;
69
- constructor(code: string, message: string) {
70
- super(`${code}: ${message}`);
71
- this.name = code;
72
- this.code = code;
73
- }
74
- }
75
-
76
- function fail(code: string, message: string): never {
77
- throw new CodexControllerRegistrationError(code, message);
78
- }
79
-
80
- function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
81
- return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
82
- }
83
-
84
- function validProcess(value: unknown): value is ProcessIdentity {
85
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
86
- const processValue = value as Record<string, unknown>;
87
- return exactKeys(processValue, ['pid', 'startTime'])
88
- && Number.isSafeInteger(processValue.pid) && (processValue.pid as number) > 0
89
- && Number.isSafeInteger(processValue.startTime) && (processValue.startTime as number) >= 0;
90
- }
91
-
92
- function validBuild(value: unknown): value is LifecycleBuildIdentity {
93
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
94
- const build = value as Record<string, unknown>;
95
- return exactKeys(build, [
96
- 'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
97
- ])
98
- && build.protocolVersion === 3
99
- && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
100
- && typeof build.internalExecutableBuildHash === 'string' && HEX_256.test(build.internalExecutableBuildHash)
101
- && typeof build.lifecycleContractHash === 'string' && HEX_256.test(build.lifecycleContractHash);
102
- }
103
-
104
- function parseRegistration(raw: string): CodexControllerRegistrationV1 {
105
- let value: unknown;
106
- try {
107
- value = JSON.parse(raw);
108
- } catch {
109
- fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is malformed');
110
- }
111
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
112
- fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration is invalid');
113
- }
114
- const registration = value as Record<string, unknown>;
115
- if (Object.keys(registration).sort().join(',') !== REGISTRATION_KEYS
116
- || registration.version !== 1
117
- || typeof registration.taskId !== 'string'
118
- || typeof registration.controlGeneration !== 'string'
119
- || typeof registration.containerId !== 'string'
120
- || typeof registration.leaseId !== 'string' || !HEX_256.test(registration.leaseId)
121
- || typeof registration.leaseSecretHash !== 'string' || !HEX_256.test(registration.leaseSecretHash)
122
- || typeof registration.controllerInstanceDigest !== 'string' || !HEX_256.test(registration.controllerInstanceDigest)
123
- || !validProcess(registration.controllerProcess)
124
- || !validBuild(registration.buildIdentity)
125
- || !Number.isSafeInteger(registration.issuedAt)
126
- || !Number.isSafeInteger(registration.expiresAt)
127
- || (registration.expiresAt as number) <= (registration.issuedAt as number)) {
128
- fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration schema is invalid');
129
- }
130
- return registration as CodexControllerRegistrationV1;
131
- }
132
-
133
- function registrationPath(manifestPath: string): string {
134
- return path.join(path.dirname(path.resolve(manifestPath)), 'codex-controller.json');
135
- }
136
-
137
- function readRaw(file: string): { raw: string; stat: fs.Stats } | null {
138
- try {
139
- const stat = fs.lstatSync(file);
140
- if (!stat.isFile() || stat.isSymbolicLink()
141
- || (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600)) {
142
- fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration file is unsafe');
143
- }
144
- return { raw: fs.readFileSync(file, 'utf8'), stat };
145
- } catch (error) {
146
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
147
- throw error;
148
- }
149
- }
150
-
151
- function sameFile(left: fs.Stats, right: fs.Stats): boolean {
152
- return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode
153
- && left.size === right.size && left.mtimeMs === right.mtimeMs;
154
- }
155
-
156
- function secretHash(secret: string): string {
157
- return crypto.createHash('sha256')
158
- .update('agent-infra/codex-controller-lease/v1\0')
159
- .update(secret)
160
- .digest('hex');
161
- }
162
-
163
- function safeEqual(left: string, right: string): boolean {
164
- if (!HEX_256.test(left) || !HEX_256.test(right)) return false;
165
- return crypto.timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'));
166
- }
167
-
168
- function defaultProbe(manifest: SandboxControlManifest, identity: ProcessIdentity): ProcessIdentityState {
169
- const command = commandForEngine(manifest.engine, 'docker', [
170
- 'exec', manifest.containerIdentity.id, 'cat', `/proc/${identity.pid}/stat`
171
- ]);
172
- const result = runProbe(command.cmd, command.args, { timeout: 2_000 });
173
- if (result.status !== 0) {
174
- const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
175
- return /no such (?:file|container)|not found|does not exist/iu.test(stderr) ? 'dead' : 'unknown';
176
- }
177
- const stdout = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
178
- const stat = parseLinuxProcessStat(stdout);
179
- if (!stat) return 'unknown';
180
- if (stat.state === 'Z' || stat.startTime !== identity.startTime) return 'dead';
181
- return 'alive';
182
- }
183
-
184
- function assertRegistrationBinding(
185
- registration: CodexControllerRegistrationV1,
186
- manifest: SandboxControlManifest,
187
- buildIdentity: LifecycleBuildIdentity
188
- ): void {
189
- if (registration.taskId !== manifest.taskId
190
- || registration.controlGeneration !== manifest.generation
191
- || registration.containerId !== manifest.containerIdentity.id) {
192
- fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration binding is invalid');
193
- }
194
- if (registration.buildIdentity.protocolVersion !== buildIdentity.protocolVersion) {
195
- fail('CODEX_LIFECYCLE_PROTOCOL_MISMATCH', 'Codex lifecycle protocol version does not match');
196
- }
197
- }
198
-
199
- function atomicWrite(file: string, value: CodexControllerRegistrationV1): void {
200
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
201
- try {
202
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
203
- fs.chmodSync(temporary, 0o600);
204
- fs.renameSync(temporary, file);
205
- } finally {
206
- fs.rmSync(temporary, { force: true });
207
- }
208
- }
209
-
210
- export function readCodexControllerRegistration(manifestPath: string): CodexControllerRegistrationV1 {
211
- const existing = readRaw(registrationPath(manifestPath));
212
- if (!existing) fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING', 'controller registration is missing');
213
- return parseRegistration(existing.raw);
214
- }
215
-
216
- export function openCodexControllerRegistration(params: Readonly<{
217
- manifest: SandboxControlManifest;
218
- manifestPath: string;
219
- controllerProcess: ProcessIdentity;
220
- buildIdentity: LifecycleBuildIdentity;
221
- }>, options: RegistrationOptions = {}): CodexControllerOpened {
222
- if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
223
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
224
- }
225
- if (!validProcess(params.controllerProcess)) {
226
- fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is invalid');
227
- }
228
- const file = registrationPath(params.manifestPath);
229
- const now = (options.now ?? Date.now)();
230
- const initial = readRaw(file);
231
- if (initial) {
232
- const existing = parseRegistration(initial.raw);
233
- assertRegistrationBinding(existing, params.manifest, params.buildIdentity);
234
- if (existing.expiresAt > now) {
235
- const oldState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(existing.controllerProcess);
236
- if (oldState === 'alive') fail('CODEX_SANDBOX_CONTROLLER_BUSY', 'an active controller registration already exists');
237
- if (oldState === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'existing controller process state is unknown');
238
- }
239
- }
240
- const requestedState = (options.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(params.controllerProcess);
241
- if (requestedState === 'dead') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'requested controller process is not alive');
242
- if (requestedState === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'requested controller process state is unknown');
243
-
244
- const randomHex = options.randomHex ?? (() => crypto.randomBytes(32).toString('hex'));
245
- const leaseId = randomHex();
246
- const leaseSecret = randomHex();
247
- const controllerInstanceDigest = randomHex();
248
- if (![leaseId, leaseSecret, controllerInstanceDigest].every((value) => HEX_256.test(value))) {
249
- fail('CODEX_SANDBOX_CONTROLLER_CREDENTIAL_INVALID', 'generated controller credential is invalid');
250
- }
251
- const registration: CodexControllerRegistrationV1 = Object.freeze({
252
- version: 1,
253
- taskId: params.manifest.taskId!,
254
- controlGeneration: params.manifest.generation,
255
- containerId: params.manifest.containerIdentity.id,
256
- leaseId,
257
- leaseSecretHash: secretHash(leaseSecret),
258
- controllerInstanceDigest,
259
- controllerProcess: params.controllerProcess,
260
- buildIdentity: params.buildIdentity,
261
- issuedAt: now,
262
- expiresAt: now + CONTROLLER_TTL_MS
263
- });
264
- options.beforeCommit?.();
265
- const current = readRaw(file);
266
- if ((!initial && current)
267
- || (initial && (!current || current.raw !== initial.raw || !sameFile(current.stat, initial.stat)))) {
268
- fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before commit');
269
- }
270
- atomicWrite(file, registration);
271
- return Object.freeze({
272
- version: 1,
273
- status: 'opened',
274
- changed: true,
275
- lease: Object.freeze({
276
- version: 1,
277
- leaseId,
278
- leaseSecret,
279
- taskId: registration.taskId,
280
- controlGeneration: registration.controlGeneration,
281
- controllerInstanceDigest,
282
- controllerProcess: registration.controllerProcess,
283
- buildIdentity: registration.buildIdentity,
284
- issuedAt: registration.issuedAt,
285
- expiresAt: registration.expiresAt
286
- }),
287
- error: null
288
- });
289
- }
290
-
291
- export function closeCodexControllerRegistration(params: Readonly<{
292
- manifest: SandboxControlManifest;
293
- manifestPath: string;
294
- proof: CodexControllerLeaseProofV1;
295
- }>): Readonly<{ version: 1; status: 'closed'; changed: boolean; lease: null; error: null }> {
296
- if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
297
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot close a Codex controller registration');
298
- }
299
- const file = registrationPath(params.manifestPath);
300
- const existingRaw = readRaw(file);
301
- if (!existingRaw) return Object.freeze({ version: 1, status: 'closed', changed: false, lease: null, error: null });
302
- const existing = parseRegistration(existingRaw.raw);
303
- if (existing.taskId !== params.manifest.taskId
304
- || existing.controlGeneration !== params.manifest.generation
305
- || existing.containerId !== params.manifest.containerIdentity.id
306
- || params.proof.version !== 1
307
- || existing.leaseId !== params.proof.leaseId
308
- || !safeEqual(existing.leaseSecretHash, secretHash(params.proof.leaseSecret))
309
- || JSON.stringify(existing.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
310
- fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller close proof is invalid');
311
- }
312
- const current = readRaw(file);
313
- if (!current || current.raw !== existingRaw.raw || !sameFile(current.stat, existingRaw.stat)) {
314
- fail('CODEX_SANDBOX_CONTROLLER_OWNERSHIP_LOST', 'controller registration changed before close');
315
- }
316
- fs.unlinkSync(file);
317
- return Object.freeze({ version: 1, status: 'closed', changed: true, lease: null, error: null });
318
- }
319
-
320
- export function resolveCodexControllerBinding(params: Readonly<{
321
- manifest: SandboxControlManifest;
322
- manifestPath: string;
323
- proof: CodexControllerLeaseProofV1;
324
- buildIdentity: LifecycleBuildIdentity;
325
- now?: number;
326
- probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
327
- }>): Readonly<{
328
- instanceDigest: string;
329
- controlGeneration: string;
330
- }> {
331
- if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
332
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot resolve a Codex controller registration');
333
- }
334
- const registration = readCodexControllerRegistration(params.manifestPath);
335
- assertRegistrationBinding(registration, params.manifest, params.buildIdentity);
336
- if (registration.expiresAt <= (params.now ?? Date.now())) {
337
- fail('CODEX_SANDBOX_CONTROLLER_LEASE_EXPIRED', 'controller lease is expired');
338
- }
339
- if (params.proof.version !== 1
340
- || registration.leaseId !== params.proof.leaseId
341
- || !safeEqual(registration.leaseSecretHash, secretHash(params.proof.leaseSecret))
342
- || JSON.stringify(registration.controllerProcess) !== JSON.stringify(params.proof.controllerProcess)) {
343
- fail('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'controller proof is invalid');
344
- }
345
- const state = (params.probeProcess ?? ((identity) => defaultProbe(params.manifest, identity)))(registration.controllerProcess);
346
- if (state === 'dead') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_INACTIVE', 'controller process is not alive');
347
- if (state === 'unknown') fail('CODEX_SANDBOX_CONTROLLER_PROCESS_UNKNOWN', 'controller process state is unknown');
348
- return Object.freeze({
349
- instanceDigest: registration.controllerInstanceDigest,
350
- controlGeneration: registration.controlGeneration
351
- });
352
- }