@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,167 +0,0 @@
1
- import fs from 'node:fs';
2
- import { randomBytes } from 'node:crypto';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- export const HOST_CONTROL_DIRECTORY_MODE = 0o700;
7
- export const HOST_CONTROL_SOCKET_MODE = 0o600;
8
- export const HOST_CONTROL_WORKER_TOKEN_MODE = 0o600;
9
-
10
- export type HostControlPlatform = 'linux' | 'darwin' | 'unsupported';
11
-
12
- export type HostControlEndpointOptions = Readonly<{
13
- platform?: HostControlPlatform;
14
- uid?: number;
15
- username?: string;
16
- }>;
17
-
18
- function currentPlatform(): HostControlPlatform {
19
- if (process.platform === 'linux' || process.platform === 'darwin') return process.platform;
20
- return 'unsupported';
21
- }
22
-
23
- function validUid(uid: number | undefined): number {
24
- const resolved = uid ?? process.getuid?.();
25
- if (!Number.isInteger(resolved) || resolved === undefined || resolved < 0) {
26
- throw new Error('HOST_CONTROL_ENDPOINT_INVALID: uid is unavailable');
27
- }
28
- return resolved;
29
- }
30
-
31
- function validUsername(username: string | undefined): string {
32
- const resolved = username ?? os.userInfo({ encoding: 'utf8' }).username;
33
- if (!/^[A-Za-z0-9._-]+$/u.test(resolved)) {
34
- throw new Error('HOST_CONTROL_ENDPOINT_INVALID: username is invalid');
35
- }
36
- return resolved;
37
- }
38
-
39
- export function resolveHostControlEndpoint(options: HostControlEndpointOptions = {}): string {
40
- const platform = options.platform ?? currentPlatform();
41
- if (platform === 'linux') return `/run/user/${validUid(options.uid)}/agent-infra/host-control.sock`;
42
- if (platform === 'darwin') return path.posix.join(
43
- '/Users',
44
- validUsername(options.username),
45
- 'Library',
46
- 'Application Support',
47
- 'agent-infra',
48
- 'run',
49
- 'host-control.sock'
50
- );
51
- throw new Error('HOST_CONTROL_UNSUPPORTED_PLATFORM');
52
- }
53
-
54
- export function hostControlWorkerTokenPath(endpoint: string): string {
55
- return `${endpoint}.worker-token`;
56
- }
57
-
58
- function assertWorkerTokenFile(tokenPath: string, uid = process.getuid?.()): string {
59
- const stat = fs.lstatSync(tokenPath);
60
- if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('HOST_CONTROL_WORKER_TOKEN_INVALID');
61
- if ((stat.mode & 0o777) !== HOST_CONTROL_WORKER_TOKEN_MODE) throw new Error('HOST_CONTROL_WORKER_TOKEN_MODE_INVALID');
62
- if (uid !== undefined && stat.uid !== uid) throw new Error('HOST_CONTROL_WORKER_TOKEN_OWNER_INVALID');
63
- const token = fs.readFileSync(tokenPath, 'utf8').trim();
64
- if (!/^[a-f0-9]{64}$/u.test(token)) throw new Error('HOST_CONTROL_WORKER_TOKEN_INVALID');
65
- return token;
66
- }
67
-
68
- export function ensureHostControlWorkerToken(endpoint: string, uid = process.getuid?.()): string {
69
- const tokenPath = hostControlWorkerTokenPath(endpoint);
70
- try {
71
- return assertWorkerTokenFile(tokenPath, uid);
72
- } catch (error) {
73
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
74
- }
75
- const token = randomBytes(32).toString('hex');
76
- try {
77
- const fd = fs.openSync(tokenPath, 'wx', HOST_CONTROL_WORKER_TOKEN_MODE);
78
- try {
79
- fs.writeFileSync(fd, `${token}\n`, 'utf8');
80
- fs.fchmodSync(fd, HOST_CONTROL_WORKER_TOKEN_MODE);
81
- if (uid !== undefined && process.getuid?.() === uid) fs.fchownSync(fd, uid, process.getgid?.() ?? -1);
82
- } finally {
83
- fs.closeSync(fd);
84
- }
85
- } catch (error) {
86
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
87
- }
88
- return assertWorkerTokenFile(tokenPath, uid);
89
- }
90
-
91
- export function readHostControlWorkerToken(endpoint: string, uid = process.getuid?.()): string {
92
- return assertWorkerTokenFile(hostControlWorkerTokenPath(endpoint), uid);
93
- }
94
-
95
- export function removeHostControlWorkerToken(endpoint: string): void {
96
- try {
97
- fs.unlinkSync(hostControlWorkerTokenPath(endpoint));
98
- } catch (error) {
99
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
100
- }
101
- }
102
-
103
- export type HostControlEndpointInspection = Readonly<{
104
- ok: boolean;
105
- code: string | null;
106
- ownerUid?: number;
107
- mode?: number;
108
- parentMode?: number;
109
- }>;
110
-
111
- export function inspectHostControlEndpoint(
112
- endpoint: string,
113
- expected: Readonly<{ uid?: number }> = {}
114
- ): HostControlEndpointInspection {
115
- let endpointStat: fs.Stats;
116
- let parentStat: fs.Stats;
117
- try {
118
- endpointStat = fs.lstatSync(endpoint);
119
- parentStat = fs.lstatSync(path.dirname(endpoint));
120
- } catch {
121
- return { ok: false, code: 'HOST_CONTROL_ENDPOINT_MISSING' };
122
- }
123
- if (endpointStat.isSymbolicLink()) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_SYMLINK' };
124
- if (!endpointStat.isSocket()) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_NOT_SOCKET' };
125
- if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
126
- return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' };
127
- }
128
- let ancestor = path.dirname(endpoint);
129
- while (true) {
130
- let stat: fs.Stats;
131
- try { stat = fs.lstatSync(ancestor); } catch { return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' }; }
132
- if (!stat.isDirectory() || stat.isSymbolicLink()) return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' };
133
- const next = path.dirname(ancestor);
134
- if (next === ancestor) break;
135
- ancestor = next;
136
- }
137
- const mode = endpointStat.mode & 0o777;
138
- const parentMode = parentStat.mode & 0o777;
139
- if (mode !== HOST_CONTROL_SOCKET_MODE) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_MODE_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
140
- if (parentMode !== HOST_CONTROL_DIRECTORY_MODE) return { ok: false, code: 'HOST_CONTROL_PARENT_MODE_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
141
- if (expected.uid !== undefined && endpointStat.uid !== expected.uid) {
142
- return { ok: false, code: 'HOST_CONTROL_ENDPOINT_OWNER_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
143
- }
144
- if (expected.uid !== undefined && parentStat.uid !== expected.uid) {
145
- return { ok: false, code: 'HOST_CONTROL_PARENT_OWNER_INVALID', ownerUid: parentStat.uid, mode, parentMode };
146
- }
147
- return { ok: true, code: null, ownerUid: endpointStat.uid, mode, parentMode };
148
- }
149
-
150
- export function prepareHostControlDirectory(endpoint: string, uid = process.getuid?.()): string {
151
- const directory = path.dirname(endpoint);
152
- const segments = path.resolve(directory).split(path.sep);
153
- let current = path.isAbsolute(directory) ? path.sep : '';
154
- for (const segment of segments.filter(Boolean)) {
155
- current = path.join(current, segment);
156
- try {
157
- const stat = fs.lstatSync(current);
158
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('HOST_CONTROL_PARENT_INVALID');
159
- } catch (error) {
160
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
161
- }
162
- }
163
- fs.mkdirSync(directory, { recursive: true, mode: HOST_CONTROL_DIRECTORY_MODE });
164
- fs.chmodSync(directory, HOST_CONTROL_DIRECTORY_MODE);
165
- if (uid !== undefined && process.getuid?.() === uid) fs.chownSync(directory, uid, process.getgid?.() ?? -1);
166
- return directory;
167
- }
@@ -1,180 +0,0 @@
1
- import fs from 'node:fs';
2
- import net from 'node:net';
3
- import { createHash } from 'node:crypto';
4
-
5
- import {
6
- HOST_CONTROL_DIRECTORY_MODE,
7
- HOST_CONTROL_SOCKET_MODE,
8
- ensureHostControlWorkerToken,
9
- inspectHostControlEndpoint,
10
- prepareHostControlDirectory,
11
- removeHostControlWorkerToken,
12
- resolveHostControlEndpoint
13
- } from './path.ts';
14
- import {
15
- HOST_CONTROL_MAX_REQUEST_BYTES,
16
- validateHostControlRequest,
17
- type HostControlRequest,
18
- type HostControlResponse
19
- } from './client.ts';
20
- import { acquireFileLock } from '../fs/file-lock.ts';
21
- import { appendHostControlAudit } from './audit.ts';
22
-
23
- export type HostControlDispatch = (request: HostControlRequest) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
24
-
25
- export type HostControlAudit = Readonly<{
26
- requestId: string;
27
- taskId: string | null;
28
- operation: string;
29
- scope: string;
30
- phase: 'accepted' | 'completed' | 'rejected';
31
- outcome: string;
32
- authorityKind: 'host-broker';
33
- transportKind: 'host-fixed-transport';
34
- identityDigest: string;
35
- }>;
36
-
37
- export type HostControlServerOptions = Readonly<{
38
- endpoint?: string;
39
- dispatch: HostControlDispatch;
40
- audit?: (entry: HostControlAudit) => void;
41
- }>;
42
-
43
- function auditFor(request: HostControlRequest, phase: HostControlAudit['phase'], outcome: string): HostControlAudit {
44
- return {
45
- requestId: request.id,
46
- taskId: request.taskId,
47
- operation: request.operation,
48
- scope: request.scope,
49
- phase,
50
- outcome,
51
- authorityKind: 'host-broker',
52
- transportKind: 'host-fixed-transport',
53
- identityDigest: createHash('sha256').update(`${request.taskId ?? '-'}\0${request.generation ?? '-'}`, 'utf8').digest('hex')
54
- };
55
- }
56
-
57
- function failedResponse(id: string, status: 'rejected' | 'unknown', code: string, message: string): HostControlResponse {
58
- const error = { code, message };
59
- const stdout = `${JSON.stringify({ status: 'failed', changed: status === 'rejected' ? false : null, error })}\n`;
60
- return { version: 1, id, status, exitCode: 1, stdout, stderr: '', error };
61
- }
62
-
63
- async function handleConnection(socket: net.Socket, options: HostControlServerOptions): Promise<void> {
64
- await new Promise<void>((resolve) => {
65
- let input = '';
66
- let completed = false;
67
- let processing = false;
68
- socket.setEncoding('utf8');
69
- // A disconnected caller does not cancel an already dispatched domain operation.
70
- socket.on('error', () => socket.destroy());
71
- socket.on('close', () => { if (!processing) resolve(); });
72
- socket.on('end', () => { if (!processing) socket.end(); });
73
- const finish = (value: HostControlResponse): void => {
74
- if (completed || socket.destroyed) return;
75
- completed = true;
76
- socket.end(`${JSON.stringify(value)}\n`);
77
- };
78
- socket.on('data', async (chunk: string) => {
79
- if (completed || processing) return;
80
- input += chunk;
81
- if (Buffer.byteLength(input, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) {
82
- finish(failedResponse('invalid', 'rejected', 'HOST_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit'));
83
- return;
84
- }
85
- const newline = input.indexOf('\n');
86
- if (newline < 0) return;
87
- const raw = input.slice(0, newline);
88
- let request: HostControlRequest;
89
- try { request = validateHostControlRequest(JSON.parse(raw)); }
90
- catch (error) {
91
- finish(failedResponse('invalid', 'rejected', 'HOST_CONTROL_REQUEST_INVALID', error instanceof Error ? error.message : String(error)));
92
- return;
93
- }
94
- processing = true;
95
- let started = false;
96
- try {
97
- options.audit?.(auditFor(request, 'accepted', 'in-progress'));
98
- started = true;
99
- const result = await options.dispatch(request);
100
- options.audit?.(auditFor(request, 'completed', result.exitCode === 0 ? 'success' : 'failure'));
101
- finish({ version: 1, id: request.id, status: 'completed', ...result, error: null });
102
- } catch (error) {
103
- const message = error instanceof Error ? error.message : String(error);
104
- const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'HOST_CONTROL_DISPATCH_FAILED';
105
- const status = started ? 'unknown' : 'rejected';
106
- try { options.audit?.(auditFor(request, started ? 'completed' : 'rejected', status)); }
107
- catch { /* Return the uncertainty even when the audit sink is unavailable. */ }
108
- finish(failedResponse(request.id, status, code, message));
109
- } finally {
110
- resolve();
111
- }
112
- });
113
- });
114
- }
115
-
116
- export type RunningHostControlServer = Readonly<{
117
- endpoint: string;
118
- server: net.Server;
119
- close: () => Promise<void>;
120
- }>;
121
-
122
- export async function startHostControlServer(options: HostControlServerOptions): Promise<RunningHostControlServer> {
123
- const endpoint = options.endpoint ?? resolveHostControlEndpoint();
124
- const audit = options.audit ?? ((entry: HostControlAudit) => appendHostControlAudit(endpoint, entry));
125
- prepareHostControlDirectory(endpoint);
126
- const lock = acquireFileLock(`${endpoint}.lock`);
127
- let server: net.Server | undefined;
128
- try {
129
- ensureHostControlWorkerToken(endpoint);
130
- try {
131
- const existing = fs.lstatSync(endpoint);
132
- if (existing.isSymbolicLink() || !existing.isSocket()) throw new Error('HOST_CONTROL_ENDPOINT_INVALID');
133
- fs.unlinkSync(endpoint);
134
- } catch (error) {
135
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
136
- }
137
- const connections = new Set<Promise<void>>();
138
- server = net.createServer({ allowHalfOpen: true }, (socket) => {
139
- const connection = handleConnection(socket, { ...options, audit });
140
- connections.add(connection);
141
- void connection.finally(() => connections.delete(connection));
142
- });
143
- const listeningServer = server;
144
- await new Promise<void>((resolve, reject) => {
145
- listeningServer.once('error', reject);
146
- listeningServer.listen(endpoint, () => { listeningServer.off('error', reject); resolve(); });
147
- });
148
- fs.chmodSync(endpoint, HOST_CONTROL_SOCKET_MODE);
149
- const inspection = inspectHostControlEndpoint(endpoint, { uid: process.getuid?.() });
150
- if (!inspection.ok) {
151
- await new Promise<void>((resolve) => listeningServer.close(() => resolve()));
152
- removeHostControlWorkerToken(endpoint);
153
- throw new Error(inspection.code ?? 'HOST_CONTROL_ENDPOINT_INVALID');
154
- }
155
- let closing: Promise<void> | undefined;
156
- return {
157
- endpoint,
158
- server,
159
- close: () => closing ??= (async () => {
160
- try {
161
- await new Promise<void>((resolve) => listeningServer.close(() => resolve()));
162
- await Promise.all(connections);
163
- await fs.promises.rm(endpoint, { force: true });
164
- removeHostControlWorkerToken(endpoint);
165
- } finally { lock.release(); }
166
- })()
167
- };
168
- } catch (error) {
169
- if (server?.listening) await new Promise<void>((resolve) => server!.close(() => resolve()));
170
- lock.release();
171
- throw error;
172
- }
173
- }
174
-
175
- export async function serveHostControl(options: HostControlServerOptions, signal?: AbortSignal): Promise<void> {
176
- const running = await startHostControlServer(options);
177
- if (!signal) return;
178
- if (signal.aborted) { await running.close(); return; }
179
- await new Promise<void>((resolve) => signal.addEventListener('abort', () => { void running.close().then(resolve); }, { once: true }));
180
- }
@@ -1,133 +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
-
6
- import {
7
- inspectHostControlEndpoint,
8
- resolveHostControlEndpoint,
9
- type HostControlPlatform
10
- } from './path.ts';
11
- import { serveHostControl, startHostControlServer, type HostControlDispatch, type RunningHostControlServer } from './server.ts';
12
-
13
- export type HostControlServiceOptions = Readonly<{
14
- platform?: HostControlPlatform;
15
- uid?: number;
16
- username?: string;
17
- endpoint?: string;
18
- dispatch: HostControlDispatch;
19
- audit?: Parameters<typeof startHostControlServer>[0]['audit'];
20
- }>;
21
-
22
- export type HostControlServicePlatform = 'linux' | 'darwin' | 'unsupported';
23
-
24
- function servicePlatform(): HostControlServicePlatform {
25
- if (process.platform === 'linux' || process.platform === 'darwin') return process.platform;
26
- return 'unsupported';
27
- }
28
-
29
- function serviceEntryPoint(): string {
30
- const compiled = fileURLToPath(new URL('../../bin/internal-cli.js', import.meta.url));
31
- if (fs.existsSync(compiled)) return fs.realpathSync.native(compiled);
32
- const candidate = process.argv[1];
33
- if (candidate && fs.existsSync(candidate)) return fs.realpathSync.native(candidate);
34
- return fs.realpathSync.native(fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url)));
35
- }
36
-
37
- function xml(value: string): string {
38
- return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
39
- }
40
-
41
- function systemdArgument(value: string): string {
42
- const escaped = value.replace(/[\\"]/g, '\\$&')
43
- .replace(/[\x00-\x1f\x7f]/g, (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, '0')}`)
44
- .replaceAll('%', '%%');
45
- return `"${escaped}"`;
46
- }
47
-
48
- function servicePaths(platform: HostControlServicePlatform = servicePlatform()): Readonly<{ unit?: string; plist?: string }> {
49
- if (platform === 'linux') return { unit: path.join(os.homedir(), '.config', 'systemd', 'user', 'agent-infra-host-control.service') };
50
- if (platform === 'darwin') return { plist: path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.fitlab-ai.agent-infra.host-control.plist') };
51
- throw new Error('HOST_CONTROL_UNSUPPORTED_PLATFORM');
52
- }
53
-
54
- export function installHostControlService(platform: HostControlServicePlatform = servicePlatform()): string {
55
- const paths = servicePaths(platform);
56
- const entry = serviceEntryPoint();
57
- if (paths.unit) {
58
- // mkdir only applies mode to directories it creates, and this one usually
59
- // already exists, so restate the intended mode explicitly.
60
- fs.mkdirSync(path.dirname(paths.unit), { recursive: true, mode: 0o700 });
61
- fs.chmodSync(path.dirname(paths.unit), 0o700);
62
- fs.writeFileSync(paths.unit, [
63
- '[Unit]',
64
- 'Description=agent-infra host-control service',
65
- '',
66
- '[Service]',
67
- // The ':' executable prefix disables environment expansion for literal paths.
68
- `ExecStart=${systemdArgument(`:${process.execPath}`)} ${systemdArgument(entry)} host-control serve`,
69
- 'Restart=on-failure',
70
- 'NoNewPrivileges=true',
71
- // Workers receive host paths, including working directories and inputs under /tmp.
72
- 'PrivateTmp=false',
73
- '',
74
- '[Install]',
75
- 'WantedBy=default.target',
76
- ''
77
- ].join('\n'), { mode: 0o600 });
78
- fs.chmodSync(paths.unit, 0o600);
79
- return paths.unit;
80
- }
81
- const plist = paths.plist!;
82
- fs.mkdirSync(path.dirname(plist), { recursive: true, mode: 0o700 });
83
- fs.chmodSync(path.dirname(plist), 0o700);
84
- const argumentsXml = [process.execPath, entry, 'host-control', 'serve']
85
- .map((argument) => ` <string>${xml(argument)}</string>`)
86
- .join('\n');
87
- fs.writeFileSync(plist, [
88
- '<?xml version="1.0" encoding="UTF-8"?>',
89
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
90
- '<plist version="1.0">',
91
- '<dict>',
92
- ' <key>Label</key>',
93
- ' <string>com.fitlab-ai.agent-infra.host-control</string>',
94
- ' <key>ProgramArguments</key>',
95
- ' <array>',
96
- argumentsXml,
97
- ' </array>',
98
- ' <key>RunAtLoad</key>',
99
- ' <true/>',
100
- ' <key>KeepAlive</key>',
101
- ' <true/>',
102
- '</dict>',
103
- '</plist>',
104
- ''
105
- ].join('\n'), { mode: 0o600 });
106
- fs.chmodSync(plist, 0o600);
107
- return plist;
108
- }
109
-
110
- export function uninstallHostControlService(platform: HostControlServicePlatform = servicePlatform()): string {
111
- const paths = servicePaths(platform);
112
- const target = paths.unit ?? paths.plist!;
113
- fs.rmSync(target, { force: true });
114
- return target;
115
- }
116
-
117
- export function hostControlServiceEndpoint(options: Pick<HostControlServiceOptions, 'platform' | 'uid' | 'username' | 'endpoint'> = {}): string {
118
- return options.endpoint ?? resolveHostControlEndpoint(options);
119
- }
120
-
121
- export async function startHostControlService(options: HostControlServiceOptions): Promise<RunningHostControlServer> {
122
- return startHostControlServer({ endpoint: hostControlServiceEndpoint(options), dispatch: options.dispatch, ...(options.audit ? { audit: options.audit } : {}) });
123
- }
124
-
125
- export async function serveHostControlService(options: HostControlServiceOptions, signal?: AbortSignal): Promise<void> {
126
- await serveHostControl({ endpoint: hostControlServiceEndpoint(options), dispatch: options.dispatch, ...(options.audit ? { audit: options.audit } : {}) }, signal);
127
- }
128
-
129
- export function hostControlServiceStatus(options: Pick<HostControlServiceOptions, 'platform' | 'uid' | 'username' | 'endpoint'> = {}): Readonly<{ endpoint: string; available: boolean; code: string | null }> {
130
- const endpoint = hostControlServiceEndpoint(options);
131
- const result = inspectHostControlEndpoint(endpoint, { uid: options.uid ?? process.getuid?.() });
132
- return { endpoint, available: result.ok, code: result.code };
133
- }
@@ -1,28 +0,0 @@
1
- import { dispatchHostControlCommand } from '../host-control/command.ts';
2
- import { hostControlServiceStatus, installHostControlService, serveHostControlService, uninstallHostControlService } from '../host-control/service.ts';
3
- import { internalHandlerRoute } from './cli-route-inventory.ts';
4
-
5
- export async function hostControl(args: readonly string[]): Promise<void> {
6
- const action = args[0] ?? '';
7
- if (internalHandlerRoute('host-control', 'serve', action)) {
8
- const controller = new AbortController();
9
- process.once('SIGINT', () => controller.abort());
10
- process.once('SIGTERM', () => controller.abort());
11
- await serveHostControlService({ dispatch: dispatchHostControlCommand }, controller.signal);
12
- return;
13
- }
14
- if (internalHandlerRoute('host-control', 'status', action)) {
15
- process.stdout.write(`${JSON.stringify(hostControlServiceStatus())}\n`);
16
- return;
17
- }
18
- if (internalHandlerRoute('host-control', 'install', action)) {
19
- process.stdout.write(`${JSON.stringify({ status: 'completed', path: installHostControlService() })}\n`);
20
- return;
21
- }
22
- if (internalHandlerRoute('host-control', 'uninstall', action)) {
23
- process.stdout.write(`${JSON.stringify({ status: 'completed', path: uninstallHostControlService() })}\n`);
24
- return;
25
- }
26
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'HOST_CONTROL_COMMAND_INVALID', message: 'usage: host-control <serve|status|install|uninstall>' } })}\n`);
27
- process.exitCode = 1;
28
- }