@fitlab-ai/agent-infra 0.11.10 → 0.11.11

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 (132) hide show
  1. package/dist/lib/agent-clients/adapter.js +20 -0
  2. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  3. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  4. package/dist/lib/internal/manual-validation.js +2 -2
  5. package/dist/lib/internal/platform-pr.js +4 -5
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-create.js +15 -1
  8. package/dist/lib/internal/task-finalization.js +1 -3
  9. package/dist/lib/internal/task-operation-registry.js +4 -138
  10. package/dist/lib/platform/pr-summary.js +45 -8
  11. package/dist/lib/platform/pull-requests.js +79 -14
  12. package/dist/lib/platform/verification-sync.js +17 -14
  13. package/dist/lib/sandbox/commands/create.js +18 -2
  14. package/dist/lib/sandbox/commands/enter.js +47 -33
  15. package/dist/lib/sandbox/control/client.js +46 -12
  16. package/dist/lib/sandbox/control/executor.js +4 -11
  17. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  18. package/dist/lib/sandbox/control/protocol.js +2 -3
  19. package/dist/lib/sandbox/control/server.js +42 -2
  20. package/dist/lib/sandbox/environment.js +150 -0
  21. package/dist/lib/sandbox/recovery.js +8 -0
  22. package/dist/lib/sandbox/workspace-identity.js +0 -14
  23. package/dist/lib/sandbox/workspace-view.js +33 -6
  24. package/dist/lib/task/artifact-command.js +7 -2
  25. package/dist/lib/task/artifact-lifecycle.js +2 -1
  26. package/dist/lib/task/capabilities.js +4 -1
  27. package/dist/lib/task/control-authority.js +0 -2
  28. package/dist/lib/task/events.js +1 -1
  29. package/dist/lib/task/finalization.js +37 -54
  30. package/dist/lib/task/frontmatter.js +24 -1
  31. package/dist/lib/task/ledger-intents.js +2 -1
  32. package/dist/lib/task/platform-operation-journal.js +12 -0
  33. package/dist/lib/task/platform-operation-recovery.js +14 -7
  34. package/dist/lib/task/resolve-ref.js +8 -3
  35. package/dist/lifecycle-build-manifest.json +3 -3
  36. package/lib/agent-clients/adapter.ts +22 -0
  37. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  38. package/lib/agent-clients/adapters/codex.ts +4 -0
  39. package/lib/internal/manual-validation.ts +2 -2
  40. package/lib/internal/platform-pr.ts +3 -4
  41. package/lib/internal/task-artifact.ts +1 -1
  42. package/lib/internal/task-create.ts +15 -2
  43. package/lib/internal/task-finalization.ts +1 -3
  44. package/lib/internal/task-operation-registry.ts +4 -150
  45. package/lib/platform/pr-summary.ts +43 -7
  46. package/lib/platform/pull-requests.ts +74 -13
  47. package/lib/platform/verification-sync.ts +18 -14
  48. package/lib/sandbox/commands/create.ts +24 -2
  49. package/lib/sandbox/commands/enter.ts +42 -39
  50. package/lib/sandbox/control/client.ts +52 -15
  51. package/lib/sandbox/control/executor.ts +5 -12
  52. package/lib/sandbox/control/host-recovery.ts +195 -0
  53. package/lib/sandbox/control/protocol.ts +4 -5
  54. package/lib/sandbox/control/server.ts +42 -1
  55. package/lib/sandbox/environment.ts +162 -0
  56. package/lib/sandbox/recovery.ts +10 -0
  57. package/lib/sandbox/tool-types.ts +1 -0
  58. package/lib/sandbox/workspace-identity.ts +0 -26
  59. package/lib/sandbox/workspace-view.ts +33 -8
  60. package/lib/task/artifact-command.ts +9 -2
  61. package/lib/task/artifact-lifecycle.ts +2 -1
  62. package/lib/task/capabilities.ts +5 -2
  63. package/lib/task/control-authority.ts +0 -4
  64. package/lib/task/events.ts +1 -1
  65. package/lib/task/finalization.ts +34 -51
  66. package/lib/task/frontmatter.ts +25 -1
  67. package/lib/task/ledger-intents.ts +2 -1
  68. package/lib/task/platform-operation-journal.ts +29 -2
  69. package/lib/task/platform-operation-recovery.ts +12 -6
  70. package/lib/task/resolve-ref.ts +8 -3
  71. package/package.json +1 -1
  72. package/templates/.agents/rules/README.en.md +1 -1
  73. package/templates/.agents/rules/README.zh-CN.md +1 -1
  74. package/templates/.agents/rules/issue-sync.en.md +1 -1
  75. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  76. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  77. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  78. package/templates/.agents/rules/review-handshake.en.md +3 -0
  79. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  80. package/templates/.agents/rules/task-management.en.md +1 -1
  81. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  82. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  83. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  84. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  85. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  86. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  87. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  88. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  89. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  90. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  91. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  92. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  93. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  94. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  95. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  96. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  97. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
  98. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
  99. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  100. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  101. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  102. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  103. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  104. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  105. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  106. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  107. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  108. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  109. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  110. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  111. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  112. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  116. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  117. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  118. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  119. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  120. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  121. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  122. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  123. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
  124. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
  125. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  126. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  127. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  128. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  129. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  130. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  131. package/dist/lib/task/finalization-handoff.js +0 -93
  132. package/lib/task/finalization-handoff.ts +0 -105
@@ -0,0 +1,218 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { readSandboxControlManifest } from './lifecycle.js';
5
+ import { identityDigest } from './identity-sentinel.js';
6
+ import { readSandboxControlPayload, readSandboxControlResultEvidence } from './state.js';
7
+ import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
8
+ const REQUEST_ID = /^[a-f0-9-]{16,64}$/u;
9
+ function unknown(requestId) {
10
+ return {
11
+ version: 2, id: requestId, phase: 'rejected', exitCode: null, stdout: '', stderr: '',
12
+ error: { code: 'SANDBOX_CONTROL_RESULT_UNKNOWN', message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: recovery evidence is unavailable', retryable: false },
13
+ outputState: 'unavailable', payload: null
14
+ };
15
+ }
16
+ function manifestDigest(manifest) {
17
+ return identityDigest({
18
+ version: 1, mode: manifest.mode, taskId: manifest.taskId,
19
+ generation: manifest.generation, controlRootId: manifest.controlRootId
20
+ });
21
+ }
22
+ function managedManifestPaths(managedRoot) {
23
+ if (!fs.existsSync(managedRoot))
24
+ return [];
25
+ const rootStat = fs.lstatSync(managedRoot);
26
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
27
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
28
+ const result = [];
29
+ for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
30
+ if (!container.isDirectory() || container.isSymbolicLink())
31
+ continue;
32
+ const containerPath = path.join(managedRoot, container.name);
33
+ for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
34
+ if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
35
+ continue;
36
+ result.push(path.join(containerPath, identity.name, 'manifest.json'));
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ function assertDirectory(directory) {
42
+ const stat = fs.lstatSync(directory);
43
+ if (!stat.isDirectory() || stat.isSymbolicLink())
44
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
45
+ }
46
+ function readAudit(manifest) {
47
+ const root = path.dirname(path.resolve(manifest.publicStatusDir));
48
+ const records = [];
49
+ for (const file of [path.join(root, 'audit.ndjson.1'), path.join(root, 'audit.ndjson')]) {
50
+ if (!fs.existsSync(file))
51
+ continue;
52
+ const stat = fs.lstatSync(file);
53
+ if (!stat.isFile() || stat.isSymbolicLink())
54
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
55
+ for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(line);
59
+ }
60
+ catch {
61
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
62
+ }
63
+ if (!value || typeof value !== 'object' || Array.isArray(value))
64
+ throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
65
+ records.push(value);
66
+ }
67
+ }
68
+ return records;
69
+ }
70
+ function auditProof(manifest, requestId) {
71
+ const digest = manifestDigest(manifest);
72
+ const related = readAudit(manifest).filter((record) => record.requestId === requestId);
73
+ for (const record of related) {
74
+ if (record.generation !== manifest.generation || record.identityDigest !== digest) {
75
+ throw new Error('SANDBOX_CONTROL_AUDIT_IDENTITY_MISMATCH');
76
+ }
77
+ }
78
+ if (!related.some((record) => record.event === 'accepted-authorized'
79
+ && record.version === 2 && record.phase === 'accepted-authorized'))
80
+ return [];
81
+ return related;
82
+ }
83
+ function publishedEvidence(records, generation) {
84
+ const publications = records.filter((record) => record.event === 'executor-result-published');
85
+ let selected = null;
86
+ let selectedSummary = null;
87
+ for (const record of publications) {
88
+ if (record.version !== 2 || record.requestGeneration !== generation
89
+ || !Number.isSafeInteger(record.exitCode)
90
+ || !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
91
+ || typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
92
+ || typeof record.errorDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.errorDigest)) {
93
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
94
+ }
95
+ const summary = JSON.stringify([
96
+ record.requestFamily ?? null, record.requestGeneration, record.exitCode,
97
+ record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
98
+ ]);
99
+ if (selectedSummary !== null && summary !== selectedSummary) {
100
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_CONFLICT');
101
+ }
102
+ selected ??= record;
103
+ selectedSummary ??= summary;
104
+ }
105
+ return selected;
106
+ }
107
+ function verifiedResponse(manifest, requestId, records) {
108
+ assertDirectory(manifest.channelDir);
109
+ const responsesDir = path.join(manifest.channelDir, 'responses');
110
+ assertDirectory(responsesDir);
111
+ const responsePath = path.join(responsesDir, `${requestId}.json`);
112
+ if (!fs.existsSync(responsePath))
113
+ return null;
114
+ const responseStat = fs.lstatSync(responsePath);
115
+ if (!responseStat.isFile() || responseStat.isSymbolicLink())
116
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
117
+ const raw = fs.readFileSync(responsePath, 'utf8');
118
+ let response;
119
+ try {
120
+ response = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
124
+ }
125
+ if (!response || response.version !== 2 || response.id !== requestId)
126
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
127
+ if (response.phase === 'accepted')
128
+ return null;
129
+ if (!['completed', 'rejected'].includes(response.phase))
130
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
131
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
132
+ return unknown(requestId);
133
+ const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
134
+ const published = publishedEvidence(records, manifest.generation);
135
+ if (!published)
136
+ return unknown(requestId);
137
+ let evidence = null;
138
+ if (fs.existsSync(evidencePath)) {
139
+ evidence = readSandboxControlResultEvidence(evidencePath);
140
+ if (evidence.id !== requestId || evidence.generation !== manifest.generation
141
+ || evidence.exitCode !== published.exitCode || evidence.stdoutBytes !== published.outputBytes
142
+ || evidence.stderrBytes !== published.errorBytes || evidence.stdoutSha256 !== published.outputDigest
143
+ || evidence.stderrSha256 !== published.errorDigest)
144
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
145
+ }
146
+ if (response.phase !== 'completed' || response.exitCode !== published.exitCode || response.error !== null) {
147
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
148
+ }
149
+ if (response.outputState === undefined) {
150
+ const stdout = response.stdout;
151
+ const stderr = response.stderr;
152
+ if (response.payload !== undefined
153
+ || Buffer.byteLength(stdout, 'utf8') !== published.outputBytes
154
+ || Buffer.byteLength(stderr, 'utf8') !== published.errorBytes
155
+ || createHash('sha256').update(stdout, 'utf8').digest('hex') !== published.outputDigest
156
+ || createHash('sha256').update(stderr, 'utf8').digest('hex') !== published.errorDigest) {
157
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
158
+ }
159
+ return response;
160
+ }
161
+ if (response.outputState === 'available') {
162
+ const payload = readSandboxControlPayload(path.join(manifest.channelDir, 'responses', `${requestId}.payload.json`));
163
+ if (payload.id !== requestId || payload.generation !== manifest.generation
164
+ || payload.stdoutBytes !== published.outputBytes || payload.stderrBytes !== published.errorBytes
165
+ || payload.stdoutSha256 !== published.outputDigest || payload.stderrSha256 !== published.errorDigest
166
+ || response.payload?.id !== payload.id || response.payload?.generation !== payload.generation
167
+ || response.payload?.stdoutBytes !== payload.stdoutBytes || response.payload?.stderrBytes !== payload.stderrBytes
168
+ || response.payload?.stdoutSha256 !== payload.stdoutSha256 || response.payload?.stderrSha256 !== payload.stderrSha256) {
169
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
170
+ }
171
+ return { ...response, stdout: payload.stdout, stderr: payload.stderr };
172
+ }
173
+ const expectedStdout = published.requestFamily === 'task-create'
174
+ ? `${JSON.stringify(taskCreateOutputUnavailableResult(requestId))}\n`
175
+ : '';
176
+ if (response.outputState !== 'unavailable' || response.payload !== null
177
+ || response.stdout !== expectedStdout || response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n'
178
+ && response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n') {
179
+ throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
180
+ }
181
+ return response;
182
+ }
183
+ export function recoverSandboxControlFromHost(requestId, params) {
184
+ if (!REQUEST_ID.test(requestId))
185
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
186
+ const deadline = Date.now() + (params.timeoutMs ?? 30_000);
187
+ let candidates = [];
188
+ while (Date.now() < deadline) {
189
+ candidates = [];
190
+ for (const manifestPath of managedManifestPaths(params.managedRoot)) {
191
+ let manifest;
192
+ try {
193
+ manifest = readSandboxControlManifest(manifestPath);
194
+ }
195
+ catch {
196
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
197
+ }
198
+ if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
199
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
200
+ }
201
+ const records = auditProof(manifest, requestId);
202
+ if (records.length)
203
+ candidates.push({ manifest, records });
204
+ }
205
+ if (candidates.length > 1)
206
+ throw new Error('SANDBOX_CONTROL_RECOVERY_AMBIGUOUS');
207
+ if (candidates.length === 1) {
208
+ const response = verifiedResponse(candidates[0].manifest, requestId, candidates[0].records);
209
+ if (response)
210
+ return response;
211
+ }
212
+ const remainingMs = deadline - Date.now();
213
+ if (remainingMs > 0)
214
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(25, remainingMs));
215
+ }
216
+ return unknown(requestId);
217
+ }
218
+ //# sourceMappingURL=host-recovery.js.map
@@ -111,7 +111,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
111
111
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
112
112
  }
113
113
  const request = value;
114
- if (request.version !== 3 || typeof request.id !== 'string'
114
+ if (request.version !== 4 || typeof request.id !== 'string'
115
115
  || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
116
116
  || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
117
117
  || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
@@ -158,12 +158,11 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
158
158
  return request;
159
159
  }
160
160
  if (request.family === 'task-finalization') {
161
- const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
161
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
162
162
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
163
163
  || request.operation !== 'complete'
164
164
  || !Array.isArray(request.args) || request.args.length !== 0
165
165
  || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
166
- || typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
167
166
  || request.controllerProcess !== null || request.controllerProof !== null) {
168
167
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
169
168
  }
@@ -20,8 +20,35 @@ import { nextSandboxControlBackoff } from './timing.js';
20
20
  import { readTaskFinalizationReceipt } from '../../task/finalization.js';
21
21
  import { readCodexControllerRegistration } from './controller-registration.js';
22
22
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
23
- import { mergeSandboxTaskView, taskViewForManifest } from './task-view.js';
23
+ import { mergeSandboxTaskView, taskViewAfterFinalization, taskViewForManifest } from './task-view.js';
24
24
  import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
25
+ async function taskViewForFinalizationResult(manifest, request, result) {
26
+ if (request.family !== 'task-finalization' || result?.exitCode !== 0 || !manifest.taskId)
27
+ return null;
28
+ let receipt = null;
29
+ for (let attempt = 0; attempt < 100 && !receipt; attempt += 1) {
30
+ try {
31
+ receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
32
+ }
33
+ catch {
34
+ // A missing or temporarily moving task directory is retried before publishing completion.
35
+ }
36
+ if (!receipt)
37
+ await new Promise((resolve) => setTimeout(resolve, 10));
38
+ }
39
+ const finalized = taskViewAfterFinalization({
40
+ taskId: manifest.taskId,
41
+ generation: manifest.generation,
42
+ requestId: request.id,
43
+ receipt
44
+ });
45
+ if (finalized.state === 'unknown')
46
+ return finalized;
47
+ const canonical = taskViewForManifest({ ...manifest, receipt });
48
+ return canonical.state === 'current' && canonical.observedSource === 'completed'
49
+ ? canonical
50
+ : finalized;
51
+ }
25
52
  function appendBrokerAudit(manifest, event, fields = {}) {
26
53
  appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
27
54
  }
@@ -722,6 +749,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
722
749
  // A fresh control root has no previous task-view projection.
723
750
  }
724
751
  let lastState = '';
752
+ let lastTaskView = '';
725
753
  let lastStatusAt = 0;
726
754
  let nextBindingCheckAt = 0;
727
755
  let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
@@ -838,6 +866,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
838
866
  settledExecution = active;
839
867
  active = null;
840
868
  }
869
+ const finalizationView = settledExecution
870
+ ? await taskViewForFinalizationResult(manifest, settledExecution.request, settledExecution.result)
871
+ : null;
872
+ if (finalizationView)
873
+ taskView = finalizationView;
841
874
  let reasonCode = null;
842
875
  try {
843
876
  if (!brokerOwns())
@@ -881,9 +914,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
881
914
  }
882
915
  const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
883
916
  const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
884
- if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
917
+ const taskViewKey = JSON.stringify(taskView);
918
+ if (brokerOwns() && (stateKey !== lastState || taskViewKey !== lastTaskView || now - lastStatusAt >= timing.controlTickMs)) {
885
919
  writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now, taskView);
886
920
  lastStatusAt = now;
921
+ lastTaskView = taskViewKey;
887
922
  }
888
923
  if (brokerOwns() && stateKey !== lastState) {
889
924
  appendBrokerAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
@@ -1156,6 +1191,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
1156
1191
  }
1157
1192
  }
1158
1193
  if (owned && active.result && active.resultEvidenceWritten) {
1194
+ const finalizationView = await taskViewForFinalizationResult(manifest, active.request, active.result);
1195
+ if (finalizationView) {
1196
+ taskView = finalizationView;
1197
+ writeSandboxControlStatus(manifest, broker, 'healthy', null, null, Date.now(), taskView);
1198
+ }
1159
1199
  if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
1160
1200
  if (brokerOwns()) {
1161
1201
  removeAcceptedResponse(manifest, active.request.id);
@@ -0,0 +1,150 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
4
+ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
5
+ const TASK_MARKER_KEYS = [
6
+ 'AGENT_INFRA_SANDBOX',
7
+ 'AGENT_INFRA_TASK_ID',
8
+ 'AGENT_INFRA_CONTROL_TOKEN',
9
+ 'AGENT_INFRA_CONTROL_GENERATION',
10
+ 'AGENT_INFRA_CONTROL_DIR',
11
+ 'AGENT_INFRA_CONTROL_STATUS_DIR',
12
+ 'AGENT_INFRA_RUNTIME_DIR'
13
+ ];
14
+ const TASK_CONTROL_MARKER_KEYS = [
15
+ 'AGENT_INFRA_CONTROL_TOKEN',
16
+ 'AGENT_INFRA_CONTROL_GENERATION',
17
+ 'AGENT_INFRA_CONTROL_DIR',
18
+ 'AGENT_INFRA_CONTROL_STATUS_DIR'
19
+ ];
20
+ const TASK_CONTROL_CONFIG_KEYS = [
21
+ ...TASK_CONTROL_MARKER_KEYS,
22
+ 'AGENT_INFRA_CONTROL_ROOT_ID'
23
+ ];
24
+ function nativeDirectoryProbe(candidate) {
25
+ if (process.platform === 'linux') {
26
+ try {
27
+ // Linux sandboxes use this fixed mount as a host/sandbox trust anchor.
28
+ // Do not let a preload replace the ordinary node:fs probe and select host routing.
29
+ const binding = process.binding('fs');
30
+ if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
31
+ return 'unknown';
32
+ const result = binding.internalModuleStat(candidate);
33
+ if (result === 1)
34
+ return 'present';
35
+ if (result === -2)
36
+ return 'absent';
37
+ return 'unknown';
38
+ }
39
+ catch {
40
+ return 'unknown';
41
+ }
42
+ }
43
+ try {
44
+ const stat = fs.lstatSync(candidate);
45
+ return stat.isDirectory() && !stat.isSymbolicLink() ? 'present' : 'unknown';
46
+ }
47
+ catch (error) {
48
+ if (error && typeof error === 'object' && 'code' in error
49
+ && (error.code === 'ENOENT' || error.code === 'ENOTDIR'))
50
+ return 'absent';
51
+ return 'unknown';
52
+ }
53
+ }
54
+ export function sandboxTaskMarkerState(env) {
55
+ const present = (key) => Boolean(env[key]);
56
+ const sandbox = env.AGENT_INFRA_SANDBOX === '1';
57
+ const taskId = present('AGENT_INFRA_TASK_ID');
58
+ const controls = TASK_CONTROL_MARKER_KEYS.every(present);
59
+ const runtime = present('AGENT_INFRA_RUNTIME_DIR');
60
+ const any = TASK_MARKER_KEYS.some(present);
61
+ if (!any)
62
+ return 'none';
63
+ if (!sandbox || !controls)
64
+ return 'incomplete';
65
+ if (taskId && runtime)
66
+ return 'task-bound';
67
+ if (!taskId && !runtime)
68
+ return 'branch-only';
69
+ return 'incomplete';
70
+ }
71
+ export function hasTaskBoundMarker(env = process.env) {
72
+ return sandboxTaskMarkerState(env) === 'task-bound';
73
+ }
74
+ export function hasSandboxControlMarker(env = process.env) {
75
+ return TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
76
+ }
77
+ export function resolveSandboxControlTransport(env = process.env, options = {}) {
78
+ const markerState = sandboxTaskMarkerState(env);
79
+ const hasAnyMarker = markerState !== 'none'
80
+ || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
81
+ || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
82
+ const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
83
+ const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
84
+ ? nativeDirectoryProbe(fixedStatusDir) : 'absent';
85
+ if (fixedStatusProbe === 'unknown') {
86
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
87
+ }
88
+ const fixedStatusMounted = fixedStatusProbe === 'present';
89
+ if (!hasAnyMarker) {
90
+ return fixedStatusMounted
91
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
92
+ : { kind: 'direct-host', reasonCode: null };
93
+ }
94
+ if (markerState === 'incomplete') {
95
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
96
+ }
97
+ const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
98
+ if (!hasCompleteConfig) {
99
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
100
+ }
101
+ const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
102
+ const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
103
+ ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
104
+ if (configuredStatusProbe === 'unknown') {
105
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
106
+ }
107
+ const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
108
+ ? configuredStatusProbe === 'present' ? configuredStatusDir : null
109
+ : fixedStatusMounted ? fixedStatusDir : null;
110
+ const statusMounted = statusDir !== null;
111
+ if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
112
+ return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
113
+ }
114
+ const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
115
+ const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
116
+ if (statusMounted) {
117
+ let sentinel;
118
+ try {
119
+ sentinel = readSandboxControlIdentitySentinel(statusDir);
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : '';
123
+ return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
124
+ ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
125
+ : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
126
+ }
127
+ if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
128
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
129
+ }
130
+ if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
131
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
132
+ }
133
+ if (sentinel.mode === 'task-bound'
134
+ && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
135
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
136
+ }
137
+ if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
138
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
139
+ }
140
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
141
+ }
142
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' };
143
+ }
144
+ export function isSandbox(env = process.env, options = {}) {
145
+ const decision = resolveSandboxControlTransport(env, options);
146
+ if (decision.kind === 'fail-closed')
147
+ throw new Error(decision.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID');
148
+ return decision.kind !== 'direct-host';
149
+ }
150
+ //# sourceMappingURL=environment.js.map
@@ -567,6 +567,13 @@ function expectedMounts(params) {
567
567
  hostPaths: toolConfigDirCandidates(tool, config.project, branch),
568
568
  expectedRW: true
569
569
  }));
570
+ const hostState = tools.flatMap((tool) => (tool.hostStateMounts ?? []).map(({ hostSubdir, containerSubpath }) => ({
571
+ path: path.posix.join(tool.containerMount, containerSubpath),
572
+ expectedType: 'bind',
573
+ hostPaths: toolConfigDirCandidates(tool, config.project, branch)
574
+ .map((candidate) => path.join(candidate, hostSubdir)),
575
+ expectedRW: true
576
+ })));
570
577
  const staging = tools.flatMap((tool) => (tool.tmpfs?.seed ?? []).flatMap((seedEntry, index) => {
571
578
  const stagingPath = `/run/agent-infra/tmpfs-seeds/${tool.id}/${index}`;
572
579
  if (!actualMounts.has(stagingPath))
@@ -583,6 +590,7 @@ function expectedMounts(params) {
583
590
  ...core,
584
591
  ...persistentTools,
585
592
  ...live,
593
+ ...hostState,
586
594
  ...staging,
587
595
  ...tools
588
596
  .filter((tool) => tool.tmpfs)
@@ -79,20 +79,6 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
79
79
  }
80
80
  return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
81
81
  }
82
- export function resolveSandboxReentryContext(params) {
83
- if (params.target.workspace.mode !== 'branch-only'
84
- || params.containerWorkspace.mode !== 'task-bound') {
85
- return { workspace: params.target.workspace, reentry: 'standard' };
86
- }
87
- const task = resolveTaskWorkspace(params.containerWorkspace.taskId, params.repoRoot);
88
- if (task.state !== 'completed' || task.branch !== params.target.branch) {
89
- return { workspace: params.target.workspace, reentry: 'standard' };
90
- }
91
- return {
92
- workspace: { mode: 'task-bound', taskId: task.taskId },
93
- reentry: 'completed'
94
- };
95
- }
96
82
  export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
97
83
  if (isRemovedHashShortIdInput(requestedRef)) {
98
84
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { resolveTaskWorkspace } from './task-resolver.js';
4
5
  import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
5
6
  export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
6
7
  'active',
@@ -73,15 +74,38 @@ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot)
73
74
  export function assertSandboxTaskSource(repoRoot, taskId) {
74
75
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
75
76
  throw new Error('SANDBOX_TASK_SOURCE_INVALID');
76
- const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
77
- const source = path.join(activeRoot, taskId);
77
+ const activeSource = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
78
+ try {
79
+ const stat = fs.lstatSync(activeSource);
80
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
81
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} must be a real directory`);
82
+ }
83
+ const canonical = fs.realpathSync.native(activeSource);
84
+ const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
85
+ const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
86
+ const relative = path.relative(workspaceRoot, canonical);
87
+ if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
88
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
89
+ }
90
+ return canonical;
91
+ }
92
+ catch (error) {
93
+ if (error.code !== 'ENOENT')
94
+ throw error;
95
+ }
96
+ const task = resolveTaskWorkspace(taskId, repoRoot);
97
+ const source = task.state === 'archive'
98
+ ? path.dirname(path.dirname(task.taskMd))
99
+ : path.dirname(task.taskMd);
78
100
  const stat = fs.lstatSync(source);
79
101
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
80
102
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
81
103
  }
82
104
  const canonical = fs.realpathSync.native(source);
83
- if (path.dirname(canonical) !== activeRoot) {
84
- throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the active workspace`);
105
+ const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
106
+ const relative = path.relative(workspaceRoot, canonical);
107
+ if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
108
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
85
109
  }
86
110
  return canonical;
87
111
  }
@@ -114,10 +138,13 @@ export function materializeSandboxWorkspaceView(params) {
114
138
  fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
115
139
  }
116
140
  const active = path.join(root, 'active');
141
+ const shortId = params.identity.mode === 'task-bound' && 'shortId' in params.identity
142
+ ? params.identity.shortId
143
+ : null;
117
144
  const registry = {
118
145
  version: 1,
119
- ids: params.identity.mode === 'task-bound'
120
- ? { [params.identity.shortId]: params.identity.taskId }
146
+ ids: shortId !== null && params.identity.mode === 'task-bound'
147
+ ? { [shortId]: params.identity.taskId }
121
148
  : {}
122
149
  };
123
150
  fs.writeFileSync(path.join(active, '.short-ids.json'), `${JSON.stringify(registry)}\n`, {
@@ -14,7 +14,7 @@ export function parseArtifactCommand(args) {
14
14
  }
15
15
  const fields = {};
16
16
  const allowed = {
17
- inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
17
+ inspect: ['--family', '--reason-code', '--source-finding', '--source-artifact', '--source-sha256'],
18
18
  init: ['--family', '--artifact', '--locale'],
19
19
  'preflight': ['--family', '--artifact'],
20
20
  'finalize-local': ['--family', '--artifact']
@@ -37,9 +37,13 @@ export function parseArtifactCommand(args) {
37
37
  const locale = fields['--locale'];
38
38
  if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en')
39
39
  throw new Error("option '--locale' must be 'zh-CN' or 'en'");
40
+ const reasonCode = fields['--reason-code'];
41
+ if (reasonCode !== undefined && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'].includes(reasonCode)) {
42
+ throw new Error("option '--reason-code' is invalid");
43
+ }
40
44
  return {
41
45
  taskRef, operation: operation, family: fields['--family'] ?? '',
42
- artifact: fields['--artifact'] ?? '', locale,
46
+ artifact: fields['--artifact'] ?? '', locale, reasonCode: reasonCode,
43
47
  sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
44
48
  sourceSha256: fields['--source-sha256']
45
49
  };
@@ -51,6 +55,7 @@ export function executeArtifactCommand(command, options = {}) {
51
55
  if (operation === 'inspect') {
52
56
  const result = resolveArtifactContext(taskRef, family, {
53
57
  repoRoot: options.repoRoot,
58
+ reasonCode: command.reasonCode,
54
59
  sourceFinding: command.sourceFinding,
55
60
  sourceArtifact: command.sourceArtifact,
56
61
  sourceSha256: command.sourceSha256
@@ -292,7 +292,8 @@ const OPTIONAL_CONTEXT = {
292
292
  function hasSelectionEvidence(content, family, options, context, pathState) {
293
293
  if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
294
294
  return true;
295
- if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt')
295
+ if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
296
+ || family === 'review-code' && options.reasonCode === 'manual-review-supplement')
296
297
  return true;
297
298
  if ((family === 'plan' || family === 'code') && context.latest) {
298
299
  const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
@@ -17,7 +17,7 @@ import { inspectArtifactDirectory } from './artifact-lifecycle.js';
17
17
  import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.js';
18
18
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
19
19
  const REASONS = new Set([
20
- 'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
20
+ 'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'
21
21
  ]);
22
22
  function deny(reasonCode, ...evidence) {
23
23
  return { allowed: false, reasonCode, evidence };
@@ -40,6 +40,9 @@ function canStart(action, facts, trigger) {
40
40
  return deny('TRIGGER_REQUEST_ID_REQUIRED');
41
41
  if (!REASONS.has(trigger.reasonCode))
42
42
  return deny('TRIGGER_REASON_INVALID');
43
+ if (trigger.reasonCode === 'manual-review-supplement' && (action !== 'review-code' || trigger.initiator !== 'human')) {
44
+ return deny('MANUAL_REVIEW_SUPPLEMENT_REQUIRES_HUMAN_REVIEW_CODE');
45
+ }
43
46
  if (trigger.sourceArtifact && !trigger.sourceSha256)
44
47
  return deny('SOURCE_ARTIFACT_HASH_REQUIRED');
45
48
  if (trigger.sourceSha256 && !trigger.sourceArtifact)