@fitlab-ai/agent-infra 0.10.0 → 0.11.1

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 (154) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/manual-validation.js +6 -2
  7. package/dist/lib/internal/platform-comment.js +60 -2
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-finalization.js +2 -2
  10. package/dist/lib/internal/task-lifecycle.js +2 -2
  11. package/dist/lib/internal/task-operation-registry.js +22 -12
  12. package/dist/lib/internal/task-orchestration.js +2 -2
  13. package/dist/lib/internal/task-review.js +10 -5
  14. package/dist/lib/platform/activity-recovery.js +159 -0
  15. package/dist/lib/platform/issue-comments.js +84 -32
  16. package/dist/lib/platform/issues.js +3 -4
  17. package/dist/lib/platform/task-comment-projection.js +70 -0
  18. package/dist/lib/platform/task-recovery.js +55 -0
  19. package/dist/lib/platform/verification-sync.js +184 -162
  20. package/dist/lib/sandbox/control/executor.js +4 -2
  21. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  22. package/dist/lib/sandbox/control/workflow-executor.js +90 -22
  23. package/dist/lib/server/process-state.js +17 -5
  24. package/dist/lib/task/artifact-command.js +6 -3
  25. package/dist/lib/task/artifact-lifecycle.js +15 -52
  26. package/dist/lib/task/artifact-operations.js +28 -2
  27. package/dist/lib/task/artifact-recovery.js +114 -68
  28. package/dist/lib/task/artifact-repair-intent.js +25 -8
  29. package/dist/lib/task/capabilities.js +0 -2
  30. package/dist/lib/task/control-authority.js +1 -1
  31. package/dist/lib/task/events.js +66 -49
  32. package/dist/lib/task/gate-policy.js +94 -0
  33. package/dist/lib/task/intermediate-cleanup.js +28 -3
  34. package/dist/lib/task/local-artifact-finalization.js +85 -18
  35. package/dist/lib/task/orchestration.js +8 -6
  36. package/dist/lib/task/review-artifacts.js +12 -6
  37. package/dist/lib/task/review-command.js +2 -2
  38. package/dist/lib/task/review-finalization.js +198 -11
  39. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  40. package/dist/lib/task/verification-engine.js +28 -17
  41. package/dist/lib/task/verification.js +25 -9
  42. package/dist/lib/task/workflow-command.js +2 -0
  43. package/dist/lib/task/workflow-dispatch.js +161 -0
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/internal/cli-route-inventory.ts +3 -4
  46. package/lib/internal/manual-validation.ts +6 -2
  47. package/lib/internal/platform-comment.ts +53 -2
  48. package/lib/internal/task-artifact.ts +1 -1
  49. package/lib/internal/task-finalization.ts +1 -1
  50. package/lib/internal/task-lifecycle.ts +1 -1
  51. package/lib/internal/task-operation-registry.ts +22 -12
  52. package/lib/internal/task-orchestration.ts +1 -1
  53. package/lib/internal/task-review.ts +9 -5
  54. package/lib/platform/activity-recovery.ts +179 -0
  55. package/lib/platform/issue-comments.ts +98 -31
  56. package/lib/platform/issues.ts +3 -4
  57. package/lib/platform/task-comment-projection.ts +79 -0
  58. package/lib/platform/task-recovery.ts +54 -0
  59. package/lib/platform/verification-sync.ts +203 -188
  60. package/lib/sandbox/control/executor.ts +4 -2
  61. package/lib/sandbox/control/protocol.ts +1 -1
  62. package/lib/sandbox/control/task-workflow.ts +1 -1
  63. package/lib/sandbox/control/workflow-executor.ts +113 -22
  64. package/lib/server/process-state.ts +26 -5
  65. package/lib/task/artifact-command.ts +6 -4
  66. package/lib/task/artifact-lifecycle.ts +12 -49
  67. package/lib/task/artifact-operations.ts +28 -2
  68. package/lib/task/artifact-recovery.ts +120 -67
  69. package/lib/task/artifact-repair-intent.ts +38 -11
  70. package/lib/task/capabilities.ts +0 -1
  71. package/lib/task/control-authority.ts +1 -1
  72. package/lib/task/events.ts +55 -38
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/intermediate-cleanup.ts +18 -2
  75. package/lib/task/local-artifact-finalization.ts +91 -15
  76. package/lib/task/orchestration.ts +12 -7
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +1 -1
  79. package/lib/task/review-finalization.ts +198 -9
  80. package/lib/{host-control → task}/secure-fs.ts +10 -32
  81. package/lib/task/verification-engine.ts +24 -14
  82. package/lib/task/verification-types.ts +7 -0
  83. package/lib/task/verification.ts +25 -9
  84. package/lib/task/workflow-command.ts +2 -0
  85. package/lib/task/workflow-dispatch.ts +171 -0
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/issue-sync.en.md +11 -1
  88. package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
  89. package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
  90. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
  91. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  94. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  95. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  96. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  97. package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
  98. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
  99. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  100. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  101. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  102. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  104. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  105. package/templates/.agents/skills/commit/config/verify.json +1 -5
  106. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  108. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  109. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  110. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  111. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  112. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  116. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  117. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  118. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
  119. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  120. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  121. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  122. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  129. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  130. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  131. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  134. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  135. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  136. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  138. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  139. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  140. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  141. package/dist/lib/host-control/audit.js +0 -19
  142. package/dist/lib/host-control/client.js +0 -142
  143. package/dist/lib/host-control/command.js +0 -49
  144. package/dist/lib/host-control/path.js +0 -160
  145. package/dist/lib/host-control/server.js +0 -159
  146. package/dist/lib/host-control/service.js +0 -112
  147. package/dist/lib/internal/host-control.js +0 -28
  148. package/lib/host-control/audit.ts +0 -22
  149. package/lib/host-control/client.ts +0 -172
  150. package/lib/host-control/command.ts +0 -60
  151. package/lib/host-control/path.ts +0 -167
  152. package/lib/host-control/server.ts +0 -180
  153. package/lib/host-control/service.ts +0 -133
  154. package/lib/internal/host-control.ts +0 -28
@@ -1,159 +0,0 @@
1
- import fs from 'node:fs';
2
- import net from 'node:net';
3
- import { createHash } from 'node:crypto';
4
- import { HOST_CONTROL_DIRECTORY_MODE, HOST_CONTROL_SOCKET_MODE, ensureHostControlWorkerToken, inspectHostControlEndpoint, prepareHostControlDirectory, removeHostControlWorkerToken, resolveHostControlEndpoint } from './path.js';
5
- import { HOST_CONTROL_MAX_REQUEST_BYTES, validateHostControlRequest } from './client.js';
6
- import { acquireFileLock } from '../fs/file-lock.js';
7
- import { appendHostControlAudit } from './audit.js';
8
- function auditFor(request, phase, outcome) {
9
- return {
10
- requestId: request.id,
11
- taskId: request.taskId,
12
- operation: request.operation,
13
- scope: request.scope,
14
- phase,
15
- outcome,
16
- authorityKind: 'host-broker',
17
- transportKind: 'host-fixed-transport',
18
- identityDigest: createHash('sha256').update(`${request.taskId ?? '-'}\0${request.generation ?? '-'}`, 'utf8').digest('hex')
19
- };
20
- }
21
- function failedResponse(id, status, code, message) {
22
- const error = { code, message };
23
- const stdout = `${JSON.stringify({ status: 'failed', changed: status === 'rejected' ? false : null, error })}\n`;
24
- return { version: 1, id, status, exitCode: 1, stdout, stderr: '', error };
25
- }
26
- async function handleConnection(socket, options) {
27
- await new Promise((resolve) => {
28
- let input = '';
29
- let completed = false;
30
- let processing = false;
31
- socket.setEncoding('utf8');
32
- // A disconnected caller does not cancel an already dispatched domain operation.
33
- socket.on('error', () => socket.destroy());
34
- socket.on('close', () => { if (!processing)
35
- resolve(); });
36
- socket.on('end', () => { if (!processing)
37
- socket.end(); });
38
- const finish = (value) => {
39
- if (completed || socket.destroyed)
40
- return;
41
- completed = true;
42
- socket.end(`${JSON.stringify(value)}\n`);
43
- };
44
- socket.on('data', async (chunk) => {
45
- if (completed || processing)
46
- return;
47
- input += chunk;
48
- if (Buffer.byteLength(input, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) {
49
- finish(failedResponse('invalid', 'rejected', 'HOST_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit'));
50
- return;
51
- }
52
- const newline = input.indexOf('\n');
53
- if (newline < 0)
54
- return;
55
- const raw = input.slice(0, newline);
56
- let request;
57
- try {
58
- request = validateHostControlRequest(JSON.parse(raw));
59
- }
60
- catch (error) {
61
- finish(failedResponse('invalid', 'rejected', 'HOST_CONTROL_REQUEST_INVALID', error instanceof Error ? error.message : String(error)));
62
- return;
63
- }
64
- processing = true;
65
- let started = false;
66
- try {
67
- options.audit?.(auditFor(request, 'accepted', 'in-progress'));
68
- started = true;
69
- const result = await options.dispatch(request);
70
- options.audit?.(auditFor(request, 'completed', result.exitCode === 0 ? 'success' : 'failure'));
71
- finish({ version: 1, id: request.id, status: 'completed', ...result, error: null });
72
- }
73
- catch (error) {
74
- const message = error instanceof Error ? error.message : String(error);
75
- const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'HOST_CONTROL_DISPATCH_FAILED';
76
- const status = started ? 'unknown' : 'rejected';
77
- try {
78
- options.audit?.(auditFor(request, started ? 'completed' : 'rejected', status));
79
- }
80
- catch { /* Return the uncertainty even when the audit sink is unavailable. */ }
81
- finish(failedResponse(request.id, status, code, message));
82
- }
83
- finally {
84
- resolve();
85
- }
86
- });
87
- });
88
- }
89
- export async function startHostControlServer(options) {
90
- const endpoint = options.endpoint ?? resolveHostControlEndpoint();
91
- const audit = options.audit ?? ((entry) => appendHostControlAudit(endpoint, entry));
92
- prepareHostControlDirectory(endpoint);
93
- const lock = acquireFileLock(`${endpoint}.lock`);
94
- let server;
95
- try {
96
- ensureHostControlWorkerToken(endpoint);
97
- try {
98
- const existing = fs.lstatSync(endpoint);
99
- if (existing.isSymbolicLink() || !existing.isSocket())
100
- throw new Error('HOST_CONTROL_ENDPOINT_INVALID');
101
- fs.unlinkSync(endpoint);
102
- }
103
- catch (error) {
104
- if (error.code !== 'ENOENT')
105
- throw error;
106
- }
107
- const connections = new Set();
108
- server = net.createServer({ allowHalfOpen: true }, (socket) => {
109
- const connection = handleConnection(socket, { ...options, audit });
110
- connections.add(connection);
111
- void connection.finally(() => connections.delete(connection));
112
- });
113
- const listeningServer = server;
114
- await new Promise((resolve, reject) => {
115
- listeningServer.once('error', reject);
116
- listeningServer.listen(endpoint, () => { listeningServer.off('error', reject); resolve(); });
117
- });
118
- fs.chmodSync(endpoint, HOST_CONTROL_SOCKET_MODE);
119
- const inspection = inspectHostControlEndpoint(endpoint, { uid: process.getuid?.() });
120
- if (!inspection.ok) {
121
- await new Promise((resolve) => listeningServer.close(() => resolve()));
122
- removeHostControlWorkerToken(endpoint);
123
- throw new Error(inspection.code ?? 'HOST_CONTROL_ENDPOINT_INVALID');
124
- }
125
- let closing;
126
- return {
127
- endpoint,
128
- server,
129
- close: () => closing ??= (async () => {
130
- try {
131
- await new Promise((resolve) => listeningServer.close(() => resolve()));
132
- await Promise.all(connections);
133
- await fs.promises.rm(endpoint, { force: true });
134
- removeHostControlWorkerToken(endpoint);
135
- }
136
- finally {
137
- lock.release();
138
- }
139
- })()
140
- };
141
- }
142
- catch (error) {
143
- if (server?.listening)
144
- await new Promise((resolve) => server.close(() => resolve()));
145
- lock.release();
146
- throw error;
147
- }
148
- }
149
- export async function serveHostControl(options, signal) {
150
- const running = await startHostControlServer(options);
151
- if (!signal)
152
- return;
153
- if (signal.aborted) {
154
- await running.close();
155
- return;
156
- }
157
- await new Promise((resolve) => signal.addEventListener('abort', () => { void running.close().then(resolve); }, { once: true }));
158
- }
159
- //# sourceMappingURL=server.js.map
@@ -1,112 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { inspectHostControlEndpoint, resolveHostControlEndpoint } from './path.js';
6
- import { serveHostControl, startHostControlServer } from './server.js';
7
- function servicePlatform() {
8
- if (process.platform === 'linux' || process.platform === 'darwin')
9
- return process.platform;
10
- return 'unsupported';
11
- }
12
- function serviceEntryPoint() {
13
- const compiled = fileURLToPath(new URL('../../bin/internal-cli.js', import.meta.url));
14
- if (fs.existsSync(compiled))
15
- return fs.realpathSync.native(compiled);
16
- const candidate = process.argv[1];
17
- if (candidate && fs.existsSync(candidate))
18
- return fs.realpathSync.native(candidate);
19
- return fs.realpathSync.native(fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url)));
20
- }
21
- function xml(value) {
22
- return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
23
- }
24
- function systemdArgument(value) {
25
- const escaped = value.replace(/[\\"]/g, '\\$&')
26
- .replace(/[\x00-\x1f\x7f]/g, (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, '0')}`)
27
- .replaceAll('%', '%%');
28
- return `"${escaped}"`;
29
- }
30
- function servicePaths(platform = servicePlatform()) {
31
- if (platform === 'linux')
32
- return { unit: path.join(os.homedir(), '.config', 'systemd', 'user', 'agent-infra-host-control.service') };
33
- if (platform === 'darwin')
34
- return { plist: path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.fitlab-ai.agent-infra.host-control.plist') };
35
- throw new Error('HOST_CONTROL_UNSUPPORTED_PLATFORM');
36
- }
37
- export function installHostControlService(platform = servicePlatform()) {
38
- const paths = servicePaths(platform);
39
- const entry = serviceEntryPoint();
40
- if (paths.unit) {
41
- // mkdir only applies mode to directories it creates, and this one usually
42
- // already exists, so restate the intended mode explicitly.
43
- fs.mkdirSync(path.dirname(paths.unit), { recursive: true, mode: 0o700 });
44
- fs.chmodSync(path.dirname(paths.unit), 0o700);
45
- fs.writeFileSync(paths.unit, [
46
- '[Unit]',
47
- 'Description=agent-infra host-control service',
48
- '',
49
- '[Service]',
50
- // The ':' executable prefix disables environment expansion for literal paths.
51
- `ExecStart=${systemdArgument(`:${process.execPath}`)} ${systemdArgument(entry)} host-control serve`,
52
- 'Restart=on-failure',
53
- 'NoNewPrivileges=true',
54
- // Workers receive host paths, including working directories and inputs under /tmp.
55
- 'PrivateTmp=false',
56
- '',
57
- '[Install]',
58
- 'WantedBy=default.target',
59
- ''
60
- ].join('\n'), { mode: 0o600 });
61
- fs.chmodSync(paths.unit, 0o600);
62
- return paths.unit;
63
- }
64
- const plist = paths.plist;
65
- fs.mkdirSync(path.dirname(plist), { recursive: true, mode: 0o700 });
66
- fs.chmodSync(path.dirname(plist), 0o700);
67
- const argumentsXml = [process.execPath, entry, 'host-control', 'serve']
68
- .map((argument) => ` <string>${xml(argument)}</string>`)
69
- .join('\n');
70
- fs.writeFileSync(plist, [
71
- '<?xml version="1.0" encoding="UTF-8"?>',
72
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
73
- '<plist version="1.0">',
74
- '<dict>',
75
- ' <key>Label</key>',
76
- ' <string>com.fitlab-ai.agent-infra.host-control</string>',
77
- ' <key>ProgramArguments</key>',
78
- ' <array>',
79
- argumentsXml,
80
- ' </array>',
81
- ' <key>RunAtLoad</key>',
82
- ' <true/>',
83
- ' <key>KeepAlive</key>',
84
- ' <true/>',
85
- '</dict>',
86
- '</plist>',
87
- ''
88
- ].join('\n'), { mode: 0o600 });
89
- fs.chmodSync(plist, 0o600);
90
- return plist;
91
- }
92
- export function uninstallHostControlService(platform = servicePlatform()) {
93
- const paths = servicePaths(platform);
94
- const target = paths.unit ?? paths.plist;
95
- fs.rmSync(target, { force: true });
96
- return target;
97
- }
98
- export function hostControlServiceEndpoint(options = {}) {
99
- return options.endpoint ?? resolveHostControlEndpoint(options);
100
- }
101
- export async function startHostControlService(options) {
102
- return startHostControlServer({ endpoint: hostControlServiceEndpoint(options), dispatch: options.dispatch, ...(options.audit ? { audit: options.audit } : {}) });
103
- }
104
- export async function serveHostControlService(options, signal) {
105
- await serveHostControl({ endpoint: hostControlServiceEndpoint(options), dispatch: options.dispatch, ...(options.audit ? { audit: options.audit } : {}) }, signal);
106
- }
107
- export function hostControlServiceStatus(options = {}) {
108
- const endpoint = hostControlServiceEndpoint(options);
109
- const result = inspectHostControlEndpoint(endpoint, { uid: options.uid ?? process.getuid?.() });
110
- return { endpoint, available: result.ok, code: result.code };
111
- }
112
- //# sourceMappingURL=service.js.map
@@ -1,28 +0,0 @@
1
- import { dispatchHostControlCommand } from '../host-control/command.js';
2
- import { hostControlServiceStatus, installHostControlService, serveHostControlService, uninstallHostControlService } from '../host-control/service.js';
3
- import { internalHandlerRoute } from './cli-route-inventory.js';
4
- export async function hostControl(args) {
5
- const action = args[0] ?? '';
6
- if (internalHandlerRoute('host-control', 'serve', action)) {
7
- const controller = new AbortController();
8
- process.once('SIGINT', () => controller.abort());
9
- process.once('SIGTERM', () => controller.abort());
10
- await serveHostControlService({ dispatch: dispatchHostControlCommand }, controller.signal);
11
- return;
12
- }
13
- if (internalHandlerRoute('host-control', 'status', action)) {
14
- process.stdout.write(`${JSON.stringify(hostControlServiceStatus())}\n`);
15
- return;
16
- }
17
- if (internalHandlerRoute('host-control', 'install', action)) {
18
- process.stdout.write(`${JSON.stringify({ status: 'completed', path: installHostControlService() })}\n`);
19
- return;
20
- }
21
- if (internalHandlerRoute('host-control', 'uninstall', action)) {
22
- process.stdout.write(`${JSON.stringify({ status: 'completed', path: uninstallHostControlService() })}\n`);
23
- return;
24
- }
25
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'HOST_CONTROL_COMMAND_INVALID', message: 'usage: host-control <serve|status|install|uninstall>' } })}\n`);
26
- process.exitCode = 1;
27
- }
28
- //# sourceMappingURL=host-control.js.map
@@ -1,22 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import type { HostControlAudit } from './server.ts';
5
-
6
- export const HOST_CONTROL_AUDIT_FILE = 'audit.ndjson';
7
-
8
- export function hostControlAuditPath(endpoint: string): string {
9
- return path.join(path.dirname(endpoint), HOST_CONTROL_AUDIT_FILE);
10
- }
11
-
12
- export function appendHostControlAudit(endpoint: string, entry: HostControlAudit, at = Date.now()): void {
13
- const filePath = hostControlAuditPath(endpoint);
14
- const descriptor = fs.openSync(filePath, 'a', 0o600);
15
- try {
16
- fs.fchmodSync(descriptor, 0o600);
17
- fs.writeFileSync(descriptor, `${JSON.stringify({ version: 1, at, ...entry })}\n`, 'utf8');
18
- fs.fsyncSync(descriptor);
19
- } finally {
20
- fs.closeSync(descriptor);
21
- }
22
- }
@@ -1,172 +0,0 @@
1
- import net from 'node:net';
2
- import { randomUUID } from 'node:crypto';
3
-
4
- import {
5
- inspectHostControlEndpoint,
6
- resolveHostControlEndpoint
7
- } from './path.ts';
8
-
9
- export const HOST_CONTROL_MAX_REQUEST_BYTES = 64 * 1024;
10
-
11
- export const HOST_CONTROL_COMMANDS = Object.freeze([
12
- 'task-lifecycle', 'task-orchestration', 'task-finalization',
13
- 'task-artifact', 'task-review', 'task-event', 'task-ledger',
14
- 'task-invalidation', 'task-warning'
15
- ] as const);
16
- export type HostControlCommand = typeof HOST_CONTROL_COMMANDS[number];
17
-
18
- export type HostControlCommandPayload = Readonly<{
19
- workingDirectory: string;
20
- args: readonly string[];
21
- environment?: Readonly<Record<string, string>>;
22
- }>;
23
-
24
- export const HOST_CONTROL_TEST_ENVIRONMENT_KEYS = Object.freeze([
25
- 'TZ', 'AGENT_INFRA_GH_BIN', 'AGENT_INFRA_GH_ARGS_JSON', 'GH_FAKE_PR_PATH', 'GH_FAKE_ARGS_PATH'
26
- ] as const);
27
-
28
- export type HostControlRequest = Readonly<{
29
- version: 1;
30
- id: string;
31
- taskId: string | null;
32
- generation: string | null;
33
- operation: HostControlCommand;
34
- scope: 'host-command';
35
- payload: HostControlCommandPayload;
36
- }>;
37
-
38
- export type HostControlResponse = Readonly<{
39
- version: 1;
40
- id: string;
41
- status: 'completed' | 'rejected' | 'unknown';
42
- exitCode: number;
43
- stdout: string;
44
- stderr: string;
45
- error: Readonly<{ code: string; message: string }> | null;
46
- }>;
47
-
48
- export class HostControlClientError extends Error {
49
- readonly code: string;
50
- readonly retryable: boolean;
51
- readonly changed: false | null;
52
- constructor(code: string, message: string, retryable = false, submitted = false) {
53
- super(`${code}: ${message}`);
54
- this.name = 'HostControlClientError';
55
- this.code = code;
56
- this.retryable = retryable && !submitted;
57
- this.changed = submitted ? null : false;
58
- }
59
- }
60
-
61
- function reject(message: string): never {
62
- throw new HostControlClientError('HOST_CONTROL_REQUEST_INVALID', message);
63
- }
64
-
65
- export function validateHostControlRequest(value: unknown): HostControlRequest {
66
- if (!value || typeof value !== 'object' || Array.isArray(value)) reject('request must be an object');
67
- const request = value as HostControlRequest;
68
- if (Object.keys(request).sort().join(',') !== 'generation,id,operation,payload,scope,taskId,version'
69
- || request.version !== 1 || typeof request.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.id)
70
- || (request.taskId !== null && (typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)))
71
- || (request.generation !== null && (typeof request.generation !== 'string' || !request.generation))
72
- || request.scope !== 'host-command' || !HOST_CONTROL_COMMANDS.includes(request.operation as HostControlCommand)) reject('request schema is invalid');
73
- const payload = request.payload;
74
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)
75
- || !['args,environment,workingDirectory', 'args,workingDirectory'].includes(Object.keys(payload).sort().join(','))
76
- || !Array.isArray(payload.args) || payload.args.length > 128
77
- || !payload.args.every((arg) => typeof arg === 'string' && !/[\r\n]/u.test(arg))
78
- || typeof payload.workingDirectory !== 'string' || !payload.workingDirectory.startsWith('/')) reject('host-command payload is invalid');
79
- if (payload.environment !== undefined) {
80
- if (!payload.environment || typeof payload.environment !== 'object' || Array.isArray(payload.environment)) reject('host-command environment is invalid');
81
- for (const [key, value] of Object.entries(payload.environment)) {
82
- if (!HOST_CONTROL_TEST_ENVIRONMENT_KEYS.includes(key as typeof HOST_CONTROL_TEST_ENVIRONMENT_KEYS[number])
83
- || typeof value !== 'string' || /[\r\n]/u.test(value)) reject('host-command environment is invalid');
84
- }
85
- }
86
- return request;
87
- }
88
-
89
- export function hostControlRequestForCommand(
90
- command: HostControlCommand,
91
- args: readonly string[],
92
- workingDirectory: string,
93
- environment: Readonly<Record<string, string>> = {}
94
- ): HostControlRequest {
95
- if (!HOST_CONTROL_COMMANDS.includes(command)) reject('host-command is unsupported');
96
- const taskArg = args[0];
97
- const taskId = taskArg && /^TASK-\d{8}-\d{6}$/u.test(taskArg) ? taskArg : null;
98
- return validateHostControlRequest({
99
- version: 1,
100
- id: randomUUID(),
101
- taskId,
102
- generation: null,
103
- operation: command,
104
- scope: 'host-command',
105
- payload: {
106
- workingDirectory,
107
- args: [...args],
108
- ...(Object.keys(environment).length > 0 ? { environment } : {})
109
- }
110
- });
111
- }
112
-
113
- function parseResponse(value: unknown, id: string): HostControlResponse {
114
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new HostControlClientError('HOST_CONTROL_RESPONSE_INVALID', 'response must be an object');
115
- const response = value as Record<string, unknown>;
116
- if (Object.keys(response).sort().join(',') !== 'error,exitCode,id,status,stderr,stdout,version'
117
- || response.version !== 1 || response.id !== id
118
- || !['completed', 'rejected', 'unknown'].includes(response.status as string)
119
- || !Number.isSafeInteger(response.exitCode)
120
- || typeof response.stdout !== 'string' || typeof response.stderr !== 'string'
121
- || (response.error !== null && (!response.error || typeof response.error !== 'object'))) {
122
- throw new HostControlClientError('HOST_CONTROL_RESPONSE_INVALID', 'response schema is invalid');
123
- }
124
- return response as HostControlResponse;
125
- }
126
-
127
- export async function requestHostControl(params: Readonly<{
128
- request: HostControlRequest;
129
- endpoint?: string;
130
- timeoutMs?: number;
131
- }>): Promise<HostControlResponse> {
132
- const request = validateHostControlRequest(params.request);
133
- const endpoint = params.endpoint ?? resolveHostControlEndpoint();
134
- const inspection = inspectHostControlEndpoint(endpoint, { uid: process.getuid?.() });
135
- if (!inspection.ok) throw new HostControlClientError(inspection.code ?? 'HOST_CONTROL_UNAVAILABLE', 'fixed host-control endpoint is unavailable', true);
136
- const encoded = `${JSON.stringify(request)}\n`;
137
- if (Buffer.byteLength(encoded, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) throw new HostControlClientError('HOST_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
138
- const timeoutMs = params.timeoutMs ?? 30_000;
139
- return await new Promise<HostControlResponse>((resolve, rejectPromise) => {
140
- const socket = net.createConnection(endpoint);
141
- let output = '';
142
- let settled = false;
143
- let submitted = false;
144
- const failure = (code: string, message: string, retryable = false) =>
145
- new HostControlClientError(code, message, retryable, submitted);
146
- const finish = (error?: Error, response?: HostControlResponse): void => {
147
- if (settled) return;
148
- settled = true;
149
- clearTimeout(timer);
150
- socket.destroy();
151
- if (error) rejectPromise(error); else resolve(response!);
152
- };
153
- const timer = setTimeout(() => finish(failure('HOST_CONTROL_TIMEOUT', 'host-control did not respond; inspect task state before retrying', true)), timeoutMs);
154
- socket.setEncoding('utf8');
155
- socket.on('connect', () => { submitted = true; socket.end(encoded); });
156
- socket.on('data', (chunk: string) => {
157
- output += chunk;
158
- if (Buffer.byteLength(output, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) {
159
- finish(failure('HOST_CONTROL_RESPONSE_TOO_LARGE', 'host-control response exceeds the control limit'));
160
- return;
161
- }
162
- const newline = output.indexOf('\n');
163
- if (newline < 0) return;
164
- try { finish(undefined, parseResponse(JSON.parse(output.slice(0, newline)), request.id)); }
165
- catch (error) { finish(failure('HOST_CONTROL_RESPONSE_INVALID', String(error))); }
166
- });
167
- socket.on('error', (error) => finish(failure('HOST_CONTROL_UNAVAILABLE', error.message, true)));
168
- socket.on('close', () => {
169
- if (!settled) finish(failure('HOST_CONTROL_RESPONSE_INVALID', 'host-control closed without a response'));
170
- });
171
- });
172
- }
@@ -1,60 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- import type { HostControlRequest } from './client.ts';
6
- import { readHostControlWorkerToken, resolveHostControlEndpoint } from './path.ts';
7
-
8
- type HostCommandResult = Readonly<{
9
- stdout: string;
10
- stderr: string;
11
- exitCode: number;
12
- }>;
13
-
14
- export async function dispatchHostControlCommand(
15
- request: Pick<HostControlRequest, 'operation' | 'payload'>
16
- ): Promise<HostCommandResult> {
17
- const { payload } = request;
18
- const compiledEntry = fileURLToPath(new URL('../../bin/internal-cli.js', import.meta.url));
19
- const sourceEntry = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
20
- const entry = fs.existsSync(compiledEntry) ? compiledEntry : sourceEntry;
21
- const payloadEnvironment = payload.environment ?? {};
22
- const env = { ...process.env, ...payloadEnvironment };
23
- for (const key of Object.keys(env)) {
24
- if (key.startsWith('AGENT_INFRA_CONTROL_')
25
- || key === 'AGENT_INFRA_TASK_ID'
26
- || key === 'AGENT_INFRA_RUNTIME_DIR'
27
- || key === 'AGENT_INFRA_EXECUTOR_MANIFEST'
28
- || key === 'AGENT_INFRA_HOST_CONTROL_SOCKET') {
29
- delete env[key];
30
- }
31
- }
32
- delete env.NODE_OPTIONS;
33
- delete env.NODE_PATH;
34
- delete env.AGENT_INFRA_HOST_CONTROL_WORKER;
35
- delete env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN;
36
- env.AGENT_INFRA_HOST_CONTROL_WORKER = '1';
37
- env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN = readHostControlWorkerToken(
38
- process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint()
39
- );
40
-
41
- return await new Promise<HostCommandResult>((resolve, reject) => {
42
- const runtimeArgs = entry.endsWith('.ts') ? ['--experimental-strip-types', '--no-warnings', entry] : [entry];
43
- const child = spawn(process.execPath, [...runtimeArgs, request.operation, ...payload.args], {
44
- cwd: payload.workingDirectory,
45
- env,
46
- stdio: ['ignore', 'pipe', 'pipe']
47
- });
48
- let stdout = '';
49
- let stderr = '';
50
- child.stdout.setEncoding('utf8');
51
- child.stderr.setEncoding('utf8');
52
- child.stdout.on('data', (chunk: string) => { stdout += chunk; });
53
- child.stderr.on('data', (chunk: string) => { stderr += chunk; });
54
- child.once('error', reject);
55
- child.once('close', (code, signal) => {
56
- if (signal) reject(new Error(`HOST_CONTROL_WORKER_TERMINATED: ${signal}`));
57
- else resolve({ stdout, stderr, exitCode: code ?? 1 });
58
- });
59
- });
60
- }