@fitlab-ai/agent-infra 0.9.5 → 0.9.6

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 (106) hide show
  1. package/dist/bin/internal-cli.js +3 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
  5. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  6. package/dist/lib/git/workflow.js +0 -1
  7. package/dist/lib/internal/codex-lifecycle.js +100 -2
  8. package/dist/lib/internal/release-workflow.js +108 -11
  9. package/dist/lib/internal/sandbox-control.js +43 -6
  10. package/dist/lib/internal/task-create.js +12 -4
  11. package/dist/lib/internal/task-orchestration.js +5 -4
  12. package/dist/lib/platform/issue-comments.js +2 -1
  13. package/dist/lib/release/workflow.js +29 -21
  14. package/dist/lib/sandbox/commands/create.js +15 -16
  15. package/dist/lib/sandbox/commands/prune.js +40 -16
  16. package/dist/lib/sandbox/commands/rm.js +318 -89
  17. package/dist/lib/sandbox/control/client.js +138 -22
  18. package/dist/lib/sandbox/control/executor.js +136 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  20. package/dist/lib/sandbox/control/protocol.js +62 -26
  21. package/dist/lib/sandbox/control/server.js +222 -102
  22. package/dist/lib/sandbox/control/state.js +181 -0
  23. package/dist/lib/sandbox/index.js +2 -2
  24. package/dist/lib/sandbox/managed-fs.js +14 -2
  25. package/dist/lib/sandbox/mounts.js +35 -24
  26. package/dist/lib/sandbox/recovery.js +262 -45
  27. package/dist/lib/sandbox/workspace-view.js +73 -10
  28. package/dist/lib/sandbox/worktree-safety.js +196 -0
  29. package/dist/lib/server/process-control.js +6 -0
  30. package/dist/lib/task/artifact-lifecycle.js +5 -4
  31. package/dist/lib/task/codex-orchestration.js +161 -0
  32. package/dist/lib/task/commands/status.js +3 -0
  33. package/dist/lib/task/commands/validate.js +289 -0
  34. package/dist/lib/task/commit-finalization.js +36 -9
  35. package/dist/lib/task/delegation-receipts.js +25 -0
  36. package/dist/lib/task/events.js +10 -4
  37. package/dist/lib/task/finalization-artifacts.js +2 -1
  38. package/dist/lib/task/implementation-inputs.js +1 -7
  39. package/dist/lib/task/index.js +9 -0
  40. package/dist/lib/task/orchestration.js +133 -4
  41. package/dist/lib/task/verification-engine.js +38 -6
  42. package/dist/lib/task/verification.js +1 -0
  43. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
  44. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  45. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
  46. package/lib/agent-clients/adapters/codex.ts +1 -1
  47. package/lib/git/workflow.ts +0 -1
  48. package/lib/internal/codex-lifecycle.ts +107 -3
  49. package/lib/internal/release-workflow.ts +107 -11
  50. package/lib/internal/sandbox-control.ts +38 -6
  51. package/lib/internal/task-create.ts +16 -4
  52. package/lib/internal/task-orchestration.ts +4 -4
  53. package/lib/platform/issue-comments.ts +2 -1
  54. package/lib/release/workflow.ts +69 -20
  55. package/lib/sandbox/commands/create.ts +17 -21
  56. package/lib/sandbox/commands/prune.ts +53 -15
  57. package/lib/sandbox/commands/rm.ts +368 -103
  58. package/lib/sandbox/control/client.ts +152 -28
  59. package/lib/sandbox/control/executor.ts +163 -0
  60. package/lib/sandbox/control/lifecycle.ts +342 -0
  61. package/lib/sandbox/control/protocol.ts +96 -69
  62. package/lib/sandbox/control/server.ts +239 -110
  63. package/lib/sandbox/control/state.ts +196 -0
  64. package/lib/sandbox/index.ts +2 -2
  65. package/lib/sandbox/managed-fs.ts +17 -2
  66. package/lib/sandbox/mounts.ts +31 -13
  67. package/lib/sandbox/recovery.ts +301 -60
  68. package/lib/sandbox/workspace-view.ts +81 -11
  69. package/lib/sandbox/worktree-safety.ts +234 -0
  70. package/lib/server/process-control.ts +11 -0
  71. package/lib/task/artifact-lifecycle.ts +5 -4
  72. package/lib/task/codex-orchestration.ts +233 -0
  73. package/lib/task/commands/status.ts +2 -0
  74. package/lib/task/commands/validate.ts +282 -0
  75. package/lib/task/commit-finalization.ts +52 -9
  76. package/lib/task/delegation-receipts.ts +52 -3
  77. package/lib/task/events.ts +9 -4
  78. package/lib/task/finalization-artifacts.ts +2 -1
  79. package/lib/task/implementation-inputs.ts +1 -5
  80. package/lib/task/index.ts +8 -0
  81. package/lib/task/orchestration.ts +199 -4
  82. package/lib/task/verification-engine.ts +42 -8
  83. package/lib/task/verification.ts +2 -1
  84. package/package.json +1 -1
  85. package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
  86. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  87. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  88. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  89. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  90. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  91. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  92. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  93. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  94. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  95. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  96. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  97. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  98. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  99. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  100. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  101. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  102. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  103. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  104. package/templates/.codex/hooks.json +8 -8
  105. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  106. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -1,54 +1,90 @@
1
1
  import { validateTaskCreateCandidate } from '../../task/create.js';
2
2
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
3
+ export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
4
+ export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
5
+ export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
6
+ export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
3
7
  export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
8
+ export class SandboxControlProtocolError extends Error {
9
+ code;
10
+ retryable;
11
+ constructor(code, message, retryable = false) {
12
+ super(`${code}: ${message}`);
13
+ this.name = 'SandboxControlProtocolError';
14
+ this.code = code;
15
+ this.retryable = retryable;
16
+ }
17
+ }
18
+ function fail(code, message, retryable = false) {
19
+ throw new SandboxControlProtocolError(code, message, retryable);
20
+ }
4
21
  export function isSandboxControlFamily(value) {
5
22
  return SANDBOX_CONTROL_FAMILIES.includes(value);
6
23
  }
7
- export function validateSandboxControlRequest(value, manifest) {
24
+ export function validateSandboxControlRequest(value, manifest, options = {}) {
8
25
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
9
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
26
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
10
27
  }
11
28
  const request = value;
12
- if (request.version !== 1
13
- || typeof request.id !== 'string'
14
- || !/^[a-f0-9-]{16,64}$/.test(request.id)
15
- || request.token !== manifest.token
16
- || typeof request.family !== 'string'
17
- || !isSandboxControlFamily(request.family)) {
18
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
29
+ if (request.version !== 2 || typeof request.id !== 'string'
30
+ || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
31
+ || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
32
+ || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
33
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
34
+ if (request.generation !== manifest.generation) {
35
+ fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
19
36
  }
37
+ const issuedAt = request.issuedAt;
38
+ const expiresAt = request.expiresAt;
39
+ const now = options.now ?? Date.now();
40
+ if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
41
+ fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
42
+ }
43
+ if (now > expiresAt)
44
+ fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
20
45
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
21
- throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
46
+ fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
22
47
  }
23
48
  if (request.family === 'task-create') {
24
- const keys = Object.keys(request).sort().join(',');
25
- if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
26
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
49
+ const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
50
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
51
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
27
52
  }
28
53
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
29
54
  }
30
- const keys = Object.keys(request).sort().join(',');
31
- if (keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
32
- || !Array.isArray(request.args)
33
- || !request.args.every((arg) => typeof arg === 'string')) {
34
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
55
+ const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
56
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
57
+ || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
58
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
35
59
  }
36
60
  if (manifest.mode !== 'task-bound' || !manifest.taskId) {
37
- throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
61
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', "branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'");
38
62
  }
39
63
  if (request.family === 'task-orchestration'
40
64
  && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
41
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
65
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
42
66
  }
43
67
  return request;
44
68
  }
45
69
  export function bindSandboxControlTask(request, taskId) {
46
- if (request.family === 'task-create') {
47
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
48
- }
49
- if (request.args.length === 0) {
50
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
51
- }
70
+ if (request.family === 'task-create')
71
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create requests do not bind a current task');
72
+ if (request.args.length === 0)
73
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
52
74
  return [taskId, ...request.args.slice(1)];
53
75
  }
76
+ export function controlError(error) {
77
+ if (error instanceof SandboxControlProtocolError) {
78
+ return { code: error.code, message: error.message, retryable: error.retryable };
79
+ }
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'SANDBOX_CONTROL_INTERNAL_ERROR';
82
+ const retryable = new Set([
83
+ 'SANDBOX_CONTROL_BUSY',
84
+ 'SANDBOX_CONTROL_HANDOFF_ACTIVE',
85
+ 'SANDBOX_CONTROL_REQUEST_EXPIRED',
86
+ 'SANDBOX_WORKTREE_BINDING_LOST'
87
+ ]).has(code);
88
+ return { code, message, retryable };
89
+ }
54
90
  //# sourceMappingURL=protocol.js.map
@@ -1,28 +1,12 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { getProcessStartTime } from '../../server/process-state.js';
5
- import { createTask } from '../../task/create-service.js';
6
4
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
7
- import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
8
- function readManifest(manifestPath) {
9
- const stat = fs.lstatSync(manifestPath);
10
- if (!stat.isFile() || stat.isSymbolicLink())
11
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
12
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
13
- if (manifest.version !== 2) {
14
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
15
- }
16
- if (typeof manifest.repoRoot !== 'string'
17
- || typeof manifest.worktreeRoot !== 'string'
18
- || typeof manifest.channelDir !== 'string'
19
- || typeof manifest.token !== 'string')
20
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
21
- if (path.resolve(manifest.channelDir) !== path.join(path.dirname(path.resolve(manifestPath)), 'channel')) {
22
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
23
- }
24
- return manifest;
25
- }
5
+ import { controlError, SANDBOX_CONTROL_STATUS_INTERVAL_MS } from './protocol.js';
6
+ import { prepareSandboxControlExecution } from './executor.js';
7
+ import { appendSandboxControlAudit, atomicWriteJson, cleanupStaleSandboxControlLease, executionPath, readActiveLease, readExecution, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
8
+ import { validateSandboxControlRequest } from './protocol.js';
9
+ import { acquireSandboxControlBrokerStartup, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
26
10
  function assertRealDirectory(directory, parent) {
27
11
  const stat = fs.lstatSync(directory);
28
12
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -31,114 +15,250 @@ function assertRealDirectory(directory, parent) {
31
15
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
32
16
  }
33
17
  }
34
- function writeResponse(directory, response) {
35
- const temporary = path.join(directory, `.${response.id}.tmp`);
36
- fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
37
- fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
18
+ function responsePath(manifest, id) {
19
+ return path.join(manifest.channelDir, 'responses', `${id}.json`);
38
20
  }
39
- export function sandboxControlSafeEnv(env = process.env) {
40
- return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
21
+ function writeResponse(manifest, response) {
22
+ atomicWriteJson(responsePath(manifest, response.id), response);
23
+ }
24
+ function rejected(id, error) {
25
+ const detail = controlError(error);
26
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${detail.message}\n`, error: detail };
27
+ }
28
+ function unknown(id) {
29
+ const error = {
30
+ code: 'SANDBOX_CONTROL_RESULT_UNKNOWN',
31
+ message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: accepted execution ended without a provable result; inspect domain state before retrying',
32
+ retryable: false
33
+ };
34
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
35
+ }
36
+ function notExecuted(id) {
37
+ const error = {
38
+ code: 'SANDBOX_CONTROL_NOT_EXECUTED',
39
+ message: 'SANDBOX_CONTROL_NOT_EXECUTED: request was claimed but never accepted; retry with a new request id',
40
+ retryable: true
41
+ };
42
+ return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
41
43
  }
42
44
  function consumeRequest(directory, id) {
43
45
  try {
44
46
  fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
45
47
  }
46
48
  catch (error) {
47
- if (error.code === 'EEXIST') {
49
+ if (error.code === 'EEXIST')
48
50
  throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
49
- }
50
51
  throw error;
51
52
  }
52
53
  }
53
- export function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
54
- const manifest = readManifest(manifestPath);
55
- assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
56
- const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
54
+ function claimRequest(manifest, requestPath, id) {
55
+ const directory = path.join(manifest.processingDir, id);
56
+ fs.mkdirSync(directory, { mode: 0o700 });
57
+ const claimed = path.join(directory, 'request.json');
58
+ try {
59
+ fs.renameSync(requestPath, claimed);
60
+ return claimed;
61
+ }
62
+ catch (error) {
63
+ fs.rmSync(directory, { recursive: true, force: true });
64
+ throw error;
65
+ }
66
+ }
67
+ function bindingReason(manifest) {
68
+ try {
69
+ assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
70
+ return null;
71
+ }
72
+ catch {
73
+ return 'SANDBOX_WORKTREE_BINDING_LOST';
74
+ }
75
+ }
76
+ function recoverProcessing(manifest) {
77
+ for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
78
+ if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name))
79
+ continue;
80
+ const descriptor = executionPath(manifest, entry.name);
81
+ const existingResponse = responsePath(manifest, entry.name);
82
+ if (fs.existsSync(descriptor)) {
83
+ const execution = readExecution(descriptor);
84
+ if (!terminateSandboxControlExecution(execution)) {
85
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
86
+ }
87
+ appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
88
+ let terminal = false;
89
+ if (fs.existsSync(existingResponse)) {
90
+ try {
91
+ const response = JSON.parse(fs.readFileSync(existingResponse, 'utf8'));
92
+ terminal = response.version === 2 && response.id === entry.name
93
+ && (response.phase === 'completed' || response.phase === 'rejected');
94
+ }
95
+ catch {
96
+ terminal = false;
97
+ }
98
+ }
99
+ if (!terminal)
100
+ writeResponse(manifest, unknown(entry.name));
101
+ }
102
+ else {
103
+ writeResponse(manifest, notExecuted(entry.name));
104
+ }
105
+ fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
106
+ }
107
+ }
108
+ function delay(ms) {
109
+ return new Promise((resolve) => setTimeout(resolve, ms));
110
+ }
111
+ export function sandboxControlSafeEnv(env = process.env) {
112
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
113
+ }
114
+ export async function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
115
+ const manifest = readSandboxControlManifest(manifestPath);
116
+ const root = path.dirname(manifestPath);
57
117
  const startTime = getProcessStartTime(process.pid);
58
118
  if (!startTime)
59
119
  throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
60
- fs.writeFileSync(brokerPath, `${JSON.stringify({
61
- version: 1,
62
- pid: process.pid,
63
- startTime,
64
- token: manifest.token
65
- })}\n`, { mode: 0o600, flag: 'wx' });
120
+ const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime });
66
121
  const requestsDir = path.join(manifest.channelDir, 'requests');
67
122
  const responsesDir = path.join(manifest.channelDir, 'responses');
68
- const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
69
- assertRealDirectory(manifest.channelDir);
70
- fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
71
- fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
72
- fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
73
- assertRealDirectory(requestsDir, manifest.channelDir);
74
- assertRealDirectory(responsesDir, manifest.channelDir);
75
- assertRealDirectory(consumedDir, path.dirname(manifestPath));
76
- while (!signal.aborted) {
77
- const current = readManifest(manifestPath);
78
- if (current.token !== manifest.token)
79
- return;
80
- assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
123
+ const consumedDir = path.join(root, 'consumed');
124
+ const broker = { pid: process.pid, startTime };
125
+ const brokerPath = path.join(root, 'broker.json');
126
+ const brokerRecord = `${JSON.stringify({ version: 2, ...broker, token: manifest.token, generation: manifest.generation })}\n`;
127
+ try {
128
+ for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
129
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
130
+ }
131
+ assertRealDirectory(manifest.channelDir);
81
132
  assertRealDirectory(requestsDir, manifest.channelDir);
82
133
  assertRealDirectory(responsesDir, manifest.channelDir);
83
- for (const name of fs.readdirSync(requestsDir).sort()) {
84
- if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
85
- continue;
86
- const requestPath = path.join(requestsDir, name);
87
- let id = name.slice(0, -5);
88
- let response;
134
+ assertRealDirectory(consumedDir, root);
135
+ assertRealDirectory(manifest.publicStatusDir, root);
136
+ assertRealDirectory(manifest.processingDir, root);
137
+ fs.writeFileSync(brokerPath, brokerRecord, { mode: 0o600, flag: 'wx' });
138
+ if (isSandboxControlRootQuiescing(root)) {
139
+ if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
140
+ fs.unlinkSync(brokerPath);
141
+ return;
142
+ }
143
+ }
144
+ finally {
145
+ releaseStartup();
146
+ }
147
+ let active = null;
148
+ let lastState = '';
149
+ let lastStatusAt = 0;
150
+ try {
151
+ writeSandboxControlStatus(manifest, broker, 'starting', null, null);
152
+ appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
153
+ recoverProcessing(manifest);
154
+ while (!signal.aborted) {
155
+ let current;
89
156
  try {
90
- const stat = fs.lstatSync(requestPath);
91
- if (!stat.isFile() || stat.isSymbolicLink())
92
- throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
93
- const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(requestPath, 'utf8')), manifest);
94
- id = request.id;
95
- consumeRequest(consumedDir, id);
96
- if (request.family === 'task-create') {
97
- const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
98
- response = {
99
- version: 1, id,
100
- exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
101
- stdout: `${JSON.stringify(result)}\n`, stderr: ''
102
- };
157
+ current = readSandboxControlManifest(manifestPath);
158
+ }
159
+ catch {
160
+ break;
161
+ }
162
+ if (current.token !== manifest.token || current.generation !== manifest.generation)
163
+ break;
164
+ if (active?.settled) {
165
+ if (active.result) {
166
+ writeResponse(manifest, {
167
+ version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
168
+ stdout: active.result.stdout, stderr: active.result.stderr, error: null
169
+ });
103
170
  }
104
171
  else {
105
- const boundArgs = bindSandboxControlTask(request, manifest.taskId);
106
- if (request.family === 'task-orchestration') {
107
- boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
108
- }
109
- const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...boundArgs], {
110
- cwd: manifest.repoRoot,
111
- encoding: 'utf8',
112
- env: sandboxControlSafeEnv()
113
- });
114
- response = {
115
- version: 1,
116
- id,
117
- exitCode: result.status ?? 1,
118
- stdout: result.stdout ?? '',
119
- stderr: result.stderr ?? ''
120
- };
172
+ writeResponse(manifest, unknown(active.request.id));
121
173
  }
174
+ fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
175
+ active = null;
122
176
  }
123
- catch (error) {
124
- response = {
125
- version: 1,
126
- id,
127
- exitCode: 1,
128
- stdout: '',
129
- stderr: `${error instanceof Error ? error.message : String(error)}\n`
130
- };
131
- }
177
+ let reasonCode = null;
132
178
  try {
133
- fs.unlinkSync(requestPath);
134
- writeResponse(responsesDir, response);
179
+ if (cleanupStaleSandboxControlLease(manifest)) {
180
+ appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
181
+ }
182
+ if (readActiveLease(manifest))
183
+ reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
184
+ }
185
+ catch {
186
+ reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
187
+ }
188
+ reasonCode ??= bindingReason(manifest);
189
+ const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
190
+ const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
191
+ const now = Date.now();
192
+ if (stateKey !== lastState || now - lastStatusAt >= SANDBOX_CONTROL_STATUS_INTERVAL_MS) {
193
+ writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
194
+ lastStatusAt = now;
195
+ }
196
+ if (stateKey !== lastState) {
197
+ appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
198
+ lastState = stateKey;
135
199
  }
136
- catch (error) {
137
- if (error.code !== 'ENOENT')
138
- throw error;
200
+ for (const name of fs.readdirSync(requestsDir).sort()) {
201
+ if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
202
+ continue;
203
+ const id = name.slice(0, -5);
204
+ const source = path.join(requestsDir, name);
205
+ let claimed = null;
206
+ try {
207
+ claimed = claimRequest(manifest, source, id);
208
+ consumeRequest(consumedDir, id);
209
+ if (reasonCode)
210
+ throw new Error(reasonCode);
211
+ if (active)
212
+ throw new Error('SANDBOX_CONTROL_BUSY');
213
+ const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
214
+ if (bindingReason(manifest))
215
+ throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
216
+ if (readActiveLease(manifest))
217
+ throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
218
+ const prepared = await prepareSandboxControlExecution({
219
+ manifest, manifestPath, request, requestPath: claimed, internalCliPath: process.argv[1]
220
+ });
221
+ const execution = { request, prepared, result: null, failure: null, settled: false };
222
+ active = execution;
223
+ prepared.completion.then((result) => { execution.result = result; execution.settled = true; }, (error) => { execution.failure = error; execution.settled = true; });
224
+ writeResponse(manifest, {
225
+ version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
226
+ });
227
+ try {
228
+ prepared.start();
229
+ }
230
+ catch (error) {
231
+ prepared.terminate();
232
+ writeResponse(manifest, unknown(id));
233
+ fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
234
+ active = null;
235
+ claimed = null;
236
+ appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
237
+ continue;
238
+ }
239
+ }
240
+ catch (error) {
241
+ writeResponse(manifest, rejected(id, error));
242
+ if (claimed)
243
+ fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
244
+ }
139
245
  }
246
+ await delay(25);
247
+ }
248
+ }
249
+ finally {
250
+ if (active) {
251
+ active.prepared.terminate();
252
+ writeResponse(manifest, unknown(active.request.id));
253
+ }
254
+ appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid });
255
+ try {
256
+ if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
257
+ fs.unlinkSync(brokerPath);
258
+ }
259
+ catch {
260
+ // A newer owner or recovery path owns the record.
140
261
  }
141
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
142
262
  }
143
263
  }
144
264
  //# sourceMappingURL=server.js.map