@fitlab-ai/agent-infra 0.9.14 → 0.9.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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -0,0 +1,253 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import type { SandboxControlManifest, SandboxControlFamily, SandboxControlRequest } from './protocol.ts';
5
+ import { identityDigest as digestIdentity } from './identity-sentinel.ts';
6
+ import { acquireSandboxResourceLock } from './native-file-lock.ts';
7
+ import { fsyncDirectory, writeDurableFile } from '../../fs/durable-write.ts';
8
+
9
+ export const SANDBOX_CONTROL_AUDIT_PHASES = Object.freeze([
10
+ 'validated', 'gated', 'reserved', 'prepared', 'accepted-authorized', 'accepted-committed',
11
+ 'start-authorized', 'started-committed', 'completed', 'evidence-written',
12
+ 'publish-authorized', 'published-committed', 'rejected', 'recovered'
13
+ ] as const);
14
+ export type SandboxControlAuditPhase = typeof SANDBOX_CONTROL_AUDIT_PHASES[number];
15
+ export type SandboxControlAuditOutcome = 'not-executed' | 'in-progress' | 'success' | 'failure' | 'unknown' | 'rejected';
16
+
17
+ export type SandboxControlAuditContext = Readonly<{
18
+ requestId: string;
19
+ taskId: string | null;
20
+ family: SandboxControlFamily | null;
21
+ operation: string | null;
22
+ phase: SandboxControlAuditPhase;
23
+ outcome: SandboxControlAuditOutcome;
24
+ generation: string;
25
+ identityDigest: string;
26
+ }>;
27
+
28
+ export type SandboxControlTransition = Readonly<{
29
+ version: 1;
30
+ requestId: string;
31
+ generation: string;
32
+ phase: SandboxControlAuditPhase;
33
+ at: number;
34
+ identityDigest: string;
35
+ reference: string | null;
36
+ }>;
37
+
38
+ const REQUEST_ID = /^[a-f0-9-]{16,64}$/u;
39
+ const SAFE_OPERATION = /^[a-z][a-z0-9.-]{0,96}$/u;
40
+ const AUDIT_MAX_BYTES = 1024 * 1024;
41
+
42
+ export function requestAuditFields(manifest: SandboxControlManifest, request: SandboxControlRequest) {
43
+ return {
44
+ requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId,
45
+ requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
46
+ };
47
+ }
48
+
49
+ export function resultAuditFields(result: Readonly<{ exitCode: number; stdout: string; stderr: string }>) {
50
+ return {
51
+ exitCode: result.exitCode,
52
+ outputBytes: Buffer.byteLength(result.stdout, 'utf8'), errorBytes: Buffer.byteLength(result.stderr, 'utf8'),
53
+ outputDigest: createHash('sha256').update(result.stdout, 'utf8').digest('hex'),
54
+ errorDigest: createHash('sha256').update(result.stderr, 'utf8').digest('hex')
55
+ };
56
+ }
57
+
58
+ function auditRoot(manifest: SandboxControlManifest): string {
59
+ return path.dirname(path.resolve(manifest.publicStatusDir));
60
+ }
61
+
62
+ function auditPath(manifest: SandboxControlManifest): string {
63
+ return path.join(auditRoot(manifest), 'audit.ndjson');
64
+ }
65
+
66
+ function manifestIdentityDigest(manifest: SandboxControlManifest): string {
67
+ return digestIdentity({
68
+ version: 1,
69
+ mode: manifest.mode,
70
+ taskId: manifest.taskId,
71
+ generation: manifest.generation,
72
+ controlRootId: manifest.controlRootId
73
+ });
74
+ }
75
+
76
+ export function createSandboxControlAuditContext(
77
+ manifest: SandboxControlManifest,
78
+ params: Readonly<{
79
+ requestId: string;
80
+ family: SandboxControlFamily | null;
81
+ operation?: string | null;
82
+ phase: SandboxControlAuditPhase;
83
+ outcome: SandboxControlAuditOutcome;
84
+ }>
85
+ ): SandboxControlAuditContext {
86
+ if (!REQUEST_ID.test(params.requestId)) throw new Error('SANDBOX_CONTROL_AUDIT_REQUEST_INVALID');
87
+ if (params.operation !== null && params.operation !== undefined && !SAFE_OPERATION.test(params.operation)) {
88
+ throw new Error('SANDBOX_CONTROL_AUDIT_OPERATION_INVALID');
89
+ }
90
+ if (!SANDBOX_CONTROL_AUDIT_PHASES.includes(params.phase)) throw new Error('SANDBOX_CONTROL_AUDIT_PHASE_INVALID');
91
+ return {
92
+ requestId: params.requestId,
93
+ taskId: manifest.taskId,
94
+ family: params.family,
95
+ operation: params.operation ?? null,
96
+ phase: params.phase,
97
+ outcome: params.outcome,
98
+ generation: manifest.generation,
99
+ identityDigest: manifestIdentityDigest(manifest)
100
+ };
101
+ }
102
+
103
+ function safeFields(fields: Readonly<Record<string, string | number | boolean | null>>): Record<string, string | number | boolean | null> {
104
+ const result: Record<string, string | number | boolean | null> = {};
105
+ for (const [key, value] of Object.entries(fields)) {
106
+ if (!/^[a-z][a-zA-Z0-9]{0,63}$/u.test(key)) continue;
107
+ if (/(token|secret|credential|proof|password|path|root|cwd|stdout|stderr|args|command|taskref)/iu.test(key)) continue;
108
+ if (typeof value === 'string' && value.length > 256) continue;
109
+ result[key] = value;
110
+ }
111
+ return result;
112
+ }
113
+
114
+ function requestHasTerminalTransition(directory: string): boolean {
115
+ const transitions = path.join(directory, 'transitions');
116
+ if (!fs.existsSync(transitions)) return false;
117
+ return ['published-committed', 'recovered'].some((phase) => fs.existsSync(path.join(transitions, `${phase}.json`)));
118
+ }
119
+
120
+ function hasActiveRequest(manifest: SandboxControlManifest): boolean {
121
+ if (!fs.existsSync(manifest.processingDir)) return false;
122
+ return fs.readdirSync(manifest.processingDir, { withFileTypes: true })
123
+ .some((entry) => entry.isDirectory() && REQUEST_ID.test(entry.name)
124
+ && !requestHasTerminalTransition(path.join(manifest.processingDir, entry.name)));
125
+ }
126
+
127
+ function rotateAuditUnderLock(manifest: SandboxControlManifest, filePath: string): void {
128
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).size < AUDIT_MAX_BYTES) return;
129
+ if (hasActiveRequest(manifest)) return;
130
+ fs.rmSync(`${filePath}.1`, { force: true });
131
+ fs.renameSync(filePath, `${filePath}.1`);
132
+ fsyncDirectory(auditRoot(manifest));
133
+ const descriptor = fs.openSync(filePath, 'a', 0o600);
134
+ try {
135
+ fs.fsyncSync(descriptor);
136
+ } finally {
137
+ fs.closeSync(descriptor);
138
+ }
139
+ }
140
+
141
+ function appendAuditRecord(
142
+ manifest: SandboxControlManifest,
143
+ record: Readonly<Record<string, unknown>>,
144
+ critical: boolean
145
+ ): void {
146
+ const root = auditRoot(manifest);
147
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
148
+ const lock = acquireSandboxResourceLock(`sandbox-control-audit:${root}`);
149
+ try {
150
+ const filePath = auditPath(manifest);
151
+ rotateAuditUnderLock(manifest, filePath);
152
+ const descriptor = fs.openSync(filePath, 'a', 0o600);
153
+ try {
154
+ fs.writeSync(descriptor, `${JSON.stringify(record)}\n`, undefined, 'utf8');
155
+ if (critical) fs.fsyncSync(descriptor);
156
+ } finally {
157
+ fs.closeSync(descriptor);
158
+ }
159
+ } catch (error) {
160
+ if (critical) throw new Error(`SANDBOX_CONTROL_AUDIT_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
161
+ } finally {
162
+ lock.release();
163
+ }
164
+ }
165
+
166
+ export function appendCriticalAudit(
167
+ manifest: SandboxControlManifest,
168
+ context: SandboxControlAuditContext,
169
+ fields: Readonly<Record<string, string | number | boolean | null>> = {},
170
+ now = Date.now()
171
+ ): void {
172
+ appendAuditRecord(manifest, {
173
+ version: 2,
174
+ at: now,
175
+ event: context.phase,
176
+ ...context,
177
+ ...safeFields(fields)
178
+ }, true);
179
+ }
180
+
181
+ export function appendDiagnosticAudit(
182
+ manifest: SandboxControlManifest,
183
+ event: string,
184
+ fields: Readonly<Record<string, string | number | boolean | null>> = {},
185
+ now = Date.now()
186
+ ): void {
187
+ try {
188
+ appendAuditRecord(manifest, {
189
+ version: 2,
190
+ at: now,
191
+ event,
192
+ generation: manifest.generation,
193
+ identityDigest: manifestIdentityDigest(manifest),
194
+ ...safeFields(fields)
195
+ }, false);
196
+ } catch {
197
+ // Diagnostics are deliberately best effort.
198
+ }
199
+ }
200
+
201
+ function transitionPath(manifest: SandboxControlManifest, requestId: string, phase: SandboxControlAuditPhase): string {
202
+ if (!REQUEST_ID.test(requestId)) throw new Error('SANDBOX_CONTROL_TRANSITION_REQUEST_INVALID');
203
+ return path.join(manifest.processingDir, requestId, 'transitions', `${phase}.json`);
204
+ }
205
+
206
+ export function writeSandboxControlTransition(
207
+ manifest: SandboxControlManifest,
208
+ params: Readonly<{
209
+ requestId: string;
210
+ phase: SandboxControlAuditPhase;
211
+ reference?: string | null;
212
+ now?: number;
213
+ }>
214
+ ): SandboxControlTransition {
215
+ const transition: SandboxControlTransition = {
216
+ version: 1,
217
+ requestId: params.requestId,
218
+ generation: manifest.generation,
219
+ phase: params.phase,
220
+ at: params.now ?? Date.now(),
221
+ identityDigest: manifestIdentityDigest(manifest),
222
+ reference: params.reference ?? null
223
+ };
224
+ const target = transitionPath(manifest, params.requestId, params.phase);
225
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
226
+ try {
227
+ writeDurableFile(target, `${JSON.stringify(transition)}\n`, { mode: 0o600, replace: false });
228
+ } catch (error) {
229
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
230
+ const existing = readSandboxControlTransition(manifest, params.requestId, params.phase);
231
+ if (JSON.stringify(existing) !== JSON.stringify(transition)) throw new Error('SANDBOX_CONTROL_TRANSITION_CONFLICT');
232
+ }
233
+ return transition;
234
+ }
235
+
236
+ export function readSandboxControlTransition(
237
+ manifest: SandboxControlManifest,
238
+ requestId: string,
239
+ phase: SandboxControlAuditPhase
240
+ ): SandboxControlTransition {
241
+ const filePath = transitionPath(manifest, requestId, phase);
242
+ if (!fs.existsSync(filePath)) throw new Error('SANDBOX_CONTROL_TRANSITION_MISSING');
243
+ let value: unknown;
244
+ try { value = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { throw new Error('SANDBOX_CONTROL_TRANSITION_INVALID'); }
245
+ const candidate = value as Partial<SandboxControlTransition> | null;
246
+ if (!candidate || candidate.version !== 1 || candidate.requestId !== requestId
247
+ || candidate.generation !== manifest.generation || candidate.phase !== phase
248
+ || !Number.isSafeInteger(candidate.at) || typeof candidate.identityDigest !== 'string'
249
+ || (candidate.reference !== null && typeof candidate.reference !== 'string')) {
250
+ throw new Error('SANDBOX_CONTROL_TRANSITION_INVALID');
251
+ }
252
+ return candidate as SandboxControlTransition;
253
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { hasTaskBoundMarker } from '../../internal/task-operation-registry.ts';
2
3
  import path from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import {
@@ -12,7 +13,8 @@ import {
12
13
  type SandboxTaskCreateRequest,
13
14
  type SandboxTaskCommandRequest,
14
15
  type SandboxTaskFinalizationRequest,
15
- type SandboxCodexControllerRequest
16
+ type SandboxCodexControllerRequest,
17
+ type SandboxTaskWorkflowRequest
16
18
  } from './protocol.ts';
17
19
  import type {
18
20
  CodexControllerLeaseProofV1,
@@ -23,65 +25,13 @@ import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
23
25
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
24
26
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
25
27
  import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
28
+ import { readSandboxControlIdentitySentinel } from './identity-sentinel.ts';
29
+ import type { TaskWorkflowRequest } from './task-workflow.ts';
30
+ import type { LifecycleAuthorityRequestV1 } from '../../task/control-authority.ts';
31
+ import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.ts';
26
32
 
27
33
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
28
34
 
29
- type SandboxControlEnvironmentClassification = Readonly<{
30
- kind: 'direct' | 'controlled' | 'invalid';
31
- mode?: 'task-bound' | 'branch-only';
32
- code?: 'TASK_CONTROL_TRANSPORT_INVALID';
33
- message?: string;
34
- }>;
35
-
36
- export function classifySandboxControlEnvironment(
37
- env: NodeJS.ProcessEnv = process.env
38
- ): SandboxControlEnvironmentClassification {
39
- const controlKeys = [
40
- 'AGENT_INFRA_CONTROL_TOKEN',
41
- 'AGENT_INFRA_CONTROL_GENERATION',
42
- 'AGENT_INFRA_CONTROL_DIR',
43
- 'AGENT_INFRA_CONTROL_STATUS_DIR'
44
- ] as const;
45
- const hasControlMarker = controlKeys.some((key) => Boolean(env[key]));
46
- const hasTaskIdentity = Boolean(env.AGENT_INFRA_TASK_ID);
47
- const hasRuntime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
48
- const hasExecutorMarker = Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
49
- const hasControllerBinding = Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING);
50
- const hasAnyMarker = hasControlMarker || hasTaskIdentity || hasRuntime || hasExecutorMarker || hasControllerBinding;
51
- const hasCompleteControl = controlKeys.every((key) => Boolean(env[key]));
52
-
53
- if (!hasAnyMarker) return { kind: 'direct' };
54
- if (hasExecutorMarker) {
55
- return {
56
- kind: 'invalid',
57
- code: 'TASK_CONTROL_TRANSPORT_INVALID',
58
- message: 'executor context is only valid for sandbox-control execute'
59
- };
60
- }
61
- if (hasControllerBinding) {
62
- return {
63
- kind: 'invalid',
64
- code: 'TASK_CONTROL_TRANSPORT_INVALID',
65
- message: 'sandbox client control configuration is incomplete or conflicting'
66
- };
67
- }
68
- if (!hasCompleteControl) {
69
- return {
70
- kind: 'invalid',
71
- code: 'TASK_CONTROL_TRANSPORT_INVALID',
72
- message: 'sandbox client control configuration is incomplete or conflicting'
73
- };
74
- }
75
- if (hasTaskIdentity !== hasRuntime) {
76
- return {
77
- kind: 'invalid',
78
- code: 'TASK_CONTROL_TRANSPORT_INVALID',
79
- message: 'sandbox task identity and runtime binding must be provided together'
80
- };
81
- }
82
- return { kind: 'controlled', mode: hasTaskIdentity ? 'task-bound' : 'branch-only' };
83
- }
84
-
85
35
  export class SandboxControlClientError extends Error {
86
36
  readonly detail: SandboxControlError;
87
37
  readonly accepted: boolean;
@@ -109,12 +59,6 @@ function clientError(
109
59
  throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
110
60
  }
111
61
 
112
- function hasTaskBoundMarker(env: NodeJS.ProcessEnv = process.env): boolean {
113
- return Boolean(env.AGENT_INFRA_TASK_ID)
114
- && Boolean(env.AGENT_INFRA_CONTROL_TOKEN || env.AGENT_INFRA_CONTROL_GENERATION
115
- || env.AGENT_INFRA_CONTROL_STATUS_DIR || env.AGENT_INFRA_RUNTIME_DIR || env.AGENT_INFRA_CONTROL_DIR);
116
- }
117
-
118
62
  function preflight(
119
63
  statusDir: string,
120
64
  generation: string,
@@ -122,6 +66,23 @@ function preflight(
122
66
  now = Date.now(),
123
67
  env: NodeJS.ProcessEnv = process.env
124
68
  ): void {
69
+ if (fs.existsSync(statusDir)) {
70
+ let identity;
71
+ try {
72
+ identity = readSandboxControlIdentitySentinel(statusDir);
73
+ } catch (error) {
74
+ const code = error instanceof Error && error.message.endsWith('MISSING')
75
+ ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
76
+ : 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
77
+ clientError(code, 'sandbox control identity is missing or invalid', false);
78
+ }
79
+ if (identity.generation !== generation) {
80
+ clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
81
+ }
82
+ if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
83
+ clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
84
+ }
85
+ }
125
86
  let status;
126
87
  try {
127
88
  status = readSandboxControlStatus(statusDir);
@@ -159,6 +120,7 @@ function preflight(
159
120
  function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
160
121
  if (request.family === 'task-lifecycle' || request.family === 'task-finalization') return 'progress';
161
122
  if (request.family === 'task-orchestration') return request.args[1] === 'status' ? 'diagnostic' : 'progress';
123
+ if (request.family === 'task-workflow') return request.workflow.operation === 'artifact-inspect' || request.workflow.operation === 'decision-next-id' ? 'diagnostic' : 'progress';
162
124
  return null;
163
125
  }
164
126
 
@@ -204,6 +166,44 @@ function cancelPendingRequest(requestPath: string): boolean {
204
166
  }
205
167
  }
206
168
 
169
+ const CONTROL_TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
170
+ const CONTROL_SHORT_ID_RE = /^\d+$/;
171
+
172
+ type CanonicalRequestTask =
173
+ | Readonly<{ kind: 'none' }>
174
+ | Readonly<{ kind: 'task'; taskId: string }>
175
+ | Readonly<{ kind: 'unresolved' }>;
176
+
177
+ // Use the current short-id contract for the first visible active registry.
178
+ export function resolveVisibleActiveShortId(ref: string): string | null {
179
+ let dir = process.cwd();
180
+ for (let depth = 0; depth < 64; depth += 1) {
181
+ const registryPath = path.join(dir, '.agents', 'workspace', 'active', '.short-ids.json');
182
+ if (fs.existsSync(registryPath)) {
183
+ const resolved = resolveShortIdReadOnly(ref, dir, { shortIdLength: configuredShortIdLength(dir) });
184
+ return resolved.ok ? resolved.taskId : null;
185
+ }
186
+ const parent = path.dirname(dir);
187
+ if (parent === dir) break;
188
+ dir = parent;
189
+ }
190
+ return null;
191
+ }
192
+
193
+ // A request's leading argument is only a task reference for some families and
194
+ // may be a documented short id. Compare canonical ids so a bound sandbox keeps
195
+ // refusing other tasks without rejecting its own task's short id.
196
+ function canonicalRequestTask(request: SandboxControlRequest): CanonicalRequestTask {
197
+ const ref = request.family === 'task-workflow'
198
+ ? request.workflow.taskId
199
+ : 'args' in request ? request.args[0] ?? null : null;
200
+ if (!ref) return { kind: 'none' };
201
+ if (CONTROL_TASK_ID_RE.test(ref)) return { kind: 'task', taskId: ref };
202
+ if (!CONTROL_SHORT_ID_RE.test(ref)) return { kind: 'none' };
203
+ const resolved = resolveVisibleActiveShortId(ref);
204
+ return resolved ? { kind: 'task', taskId: resolved } : { kind: 'unresolved' };
205
+ }
206
+
207
207
  function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly<{
208
208
  channelDir?: string;
209
209
  statusDir?: string;
@@ -213,6 +213,20 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
213
213
  const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
214
214
  const taskViewEffect = taskViewEffectForRequest(request);
215
215
  preflight(statusDir, request.generation, taskViewEffect);
216
+ const identityPath = path.join(statusDir, 'identity.json');
217
+ if (fs.existsSync(statusDir)) {
218
+ const identity = readSandboxControlIdentitySentinel(statusDir);
219
+ const requestTask = canonicalRequestTask(request);
220
+ if (identity.mode === 'task-bound'
221
+ && ((process.env.AGENT_INFRA_TASK_ID && process.env.AGENT_INFRA_TASK_ID !== identity.taskId)
222
+ || requestTask.kind === 'unresolved'
223
+ || (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
224
+ clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
225
+ }
226
+ if (identity.mode === 'branch-only' && (request.family === 'task-finalization' || request.family === 'task-workflow')) {
227
+ clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
228
+ }
229
+ }
216
230
  const encoded = `${JSON.stringify(request)}\n`;
217
231
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
218
232
  clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false, false, request.id);
@@ -370,7 +384,7 @@ export function requestSandboxControl(params: Readonly<{
370
384
  token?: string; generation?: string; timeoutMs?: number;
371
385
  }>): SandboxControlResponse {
372
386
  if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
373
- if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
387
+ if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization' || params.family === 'task-workflow') {
374
388
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
375
389
  }
376
390
  const auth = authority(params);
@@ -389,6 +403,7 @@ export function requestSandboxTaskControl(params: Readonly<{
389
403
  family: 'task-lifecycle' | 'task-orchestration';
390
404
  args: string[];
391
405
  controllerProof: CodexControllerLeaseProofV1 | null;
406
+ authority?: LifecycleAuthorityRequestV1;
392
407
  channelDir?: string;
393
408
  statusDir?: string;
394
409
  token?: string;
@@ -399,14 +414,15 @@ export function requestSandboxTaskControl(params: Readonly<{
399
414
  const issuedAt = Date.now();
400
415
  const request: SandboxTaskCommandRequest = {
401
416
  version: 3,
402
- id: randomUUID(),
417
+ id: params.authority?.requestId ?? randomUUID(),
403
418
  ...auth,
404
419
  issuedAt,
405
420
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
406
421
  family: params.family,
407
422
  args: params.args,
408
423
  controllerProcess: null,
409
- controllerProof: params.controllerProof
424
+ controllerProof: params.authority ? null : params.controllerProof,
425
+ ...(params.authority ? { authority: params.authority } : {})
410
426
  };
411
427
  return exchangeSandboxControl(request, params);
412
428
  }
@@ -439,6 +455,33 @@ export function requestSandboxTaskFinalization(params: Readonly<{
439
455
  return exchangeSandboxControl(request, params);
440
456
  }
441
457
 
458
+ export function requestSandboxTaskWorkflow(params: Readonly<{
459
+ workflow: TaskWorkflowRequest;
460
+ channelDir?: string;
461
+ statusDir?: string;
462
+ token?: string;
463
+ generation?: string;
464
+ timeoutMs?: number;
465
+ authority?: LifecycleAuthorityRequestV1;
466
+ }>): SandboxControlResponse {
467
+ const auth = authority(params);
468
+ const issuedAt = Date.now();
469
+ const request: SandboxTaskWorkflowRequest = {
470
+ version: 3,
471
+ id: params.workflow.id,
472
+ ...auth,
473
+ issuedAt,
474
+ expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
475
+ family: 'task-workflow',
476
+ args: [],
477
+ controllerProcess: null,
478
+ controllerProof: null,
479
+ workflow: params.workflow,
480
+ ...(params.authority ? { authority: params.authority } : {})
481
+ };
482
+ return exchangeSandboxControl(request, params);
483
+ }
484
+
442
485
  type CodexControllerClosed = Readonly<{
443
486
  version: 1;
444
487
  status: 'closed';