mecord-connect 1.0.0

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 (82) hide show
  1. package/DISCLOSURE +47 -0
  2. package/LICENSE +64 -0
  3. package/README.md +44 -0
  4. package/bin/operator.mjs +10 -0
  5. package/package.json +58 -0
  6. package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
  7. package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
  8. package/runtime/app/apps/local-agent/src/cli.js +237 -0
  9. package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
  10. package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
  11. package/runtime/app/apps/local-agent/src/main.js +311 -0
  12. package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
  13. package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
  14. package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
  15. package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
  16. package/runtime/app/apps/local-agent/src/remote.js +130 -0
  17. package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
  18. package/runtime/app/apps/local-agent/src/server.js +578 -0
  19. package/runtime/app/package.json +4 -0
  20. package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
  21. package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
  22. package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
  23. package/runtime/app/src/capabilities/browser-cdp.js +384 -0
  24. package/runtime/app/src/capabilities/browser-managed.js +345 -0
  25. package/runtime/app/src/capabilities/docker.js +448 -0
  26. package/runtime/app/src/capabilities/filesystem.js +264 -0
  27. package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
  28. package/runtime/app/src/capabilities/git-write.js +321 -0
  29. package/runtime/app/src/capabilities/git.js +252 -0
  30. package/runtime/app/src/capabilities/index.js +16 -0
  31. package/runtime/app/src/capabilities/path-scope.js +86 -0
  32. package/runtime/app/src/capabilities/postgres.js +566 -0
  33. package/runtime/app/src/capabilities/process.js +258 -0
  34. package/runtime/app/src/capabilities/project-command.js +599 -0
  35. package/runtime/app/src/capabilities/project-transaction.js +215 -0
  36. package/runtime/app/src/capabilities/project.js +67 -0
  37. package/runtime/app/src/capabilities/system.js +40 -0
  38. package/runtime/app/src/capabilities/vscode.js +312 -0
  39. package/runtime/app/src/capabilities/windows-uia.js +331 -0
  40. package/runtime/app/src/core/account-device-registry.js +604 -0
  41. package/runtime/app/src/core/action-identity.js +44 -0
  42. package/runtime/app/src/core/audit.js +295 -0
  43. package/runtime/app/src/core/capability-policy.js +58 -0
  44. package/runtime/app/src/core/child-environment.js +29 -0
  45. package/runtime/app/src/core/device-enrollment.js +417 -0
  46. package/runtime/app/src/core/device-identity.js +436 -0
  47. package/runtime/app/src/core/device-registry.js +477 -0
  48. package/runtime/app/src/core/device-reset.js +178 -0
  49. package/runtime/app/src/core/device-routing.js +305 -0
  50. package/runtime/app/src/core/durable-state.js +306 -0
  51. package/runtime/app/src/core/errors.js +20 -0
  52. package/runtime/app/src/core/evidence.js +9 -0
  53. package/runtime/app/src/core/index.js +17 -0
  54. package/runtime/app/src/core/machine-state.js +160 -0
  55. package/runtime/app/src/core/network-authority.js +20 -0
  56. package/runtime/app/src/core/policy.js +67 -0
  57. package/runtime/app/src/core/product-identity.js +7 -0
  58. package/runtime/app/src/core/provenance.js +18 -0
  59. package/runtime/app/src/core/provider-learning.js +274 -0
  60. package/runtime/app/src/core/public-plugin-surface.js +25 -0
  61. package/runtime/app/src/core/public-restricted-data.js +105 -0
  62. package/runtime/app/src/core/rate-limit.js +118 -0
  63. package/runtime/app/src/core/relay-client.js +568 -0
  64. package/runtime/app/src/core/relay-delivery-store.js +512 -0
  65. package/runtime/app/src/core/relay-result-store.js +321 -0
  66. package/runtime/app/src/core/release-update.js +253 -0
  67. package/runtime/app/src/core/router.js +96 -0
  68. package/runtime/app/src/core/runtime.js +136 -0
  69. package/runtime/app/src/core/scoped-path-syntax.js +16 -0
  70. package/runtime/app/src/core/session-token.js +498 -0
  71. package/runtime/app/src/core/task-orchestrator.js +1148 -0
  72. package/runtime/app/src/core/task-store.js +517 -0
  73. package/runtime/app/src/core/task.js +142 -0
  74. package/runtime/app/src/core/trusted-executable.js +124 -0
  75. package/runtime/app/src/core/types.js +83 -0
  76. package/runtime/app/src/core/windows-path-lease.js +138 -0
  77. package/runtime/native/operator-windows-dpapi.exe +0 -0
  78. package/runtime/native/operator-windows-path-lease.exe +0 -0
  79. package/runtime/native/operator-windows-uia.exe +0 -0
  80. package/runtime/runtime-manifest.json +380 -0
  81. package/src/cli.mjs +345 -0
  82. package/src/publish-check.mjs +22 -0
@@ -0,0 +1,578 @@
1
+ import http from 'node:http';
2
+ import crypto from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ import { applyBoundedHttpServerPolicy, requireLiteralLoopbackBindHost } from '../../../src/core/network-authority.js';
6
+ import { PRODUCT_VERSION } from '../../../src/core/product-identity.js';
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+ const MAX_BODY_BYTES = 1024 * 1024;
20
+
21
+
22
+
23
+ function timingSafeTokenMatch(actual , expected ) {
24
+ if (!actual?.startsWith('Bearer ')) return false;
25
+ return timingSafeSecretMatch(actual.slice('Bearer '.length), expected);
26
+ }
27
+
28
+ function timingSafeSecretMatch(actual , expected ) {
29
+ if (typeof actual !== 'string') return false;
30
+ const supplied = Buffer.from(actual);
31
+ const wanted = Buffer.from(expected);
32
+ return supplied.length === wanted.length && crypto.timingSafeEqual(supplied, wanted);
33
+ }
34
+
35
+ async function readJson(req ) {
36
+ const chunks = [];
37
+ let bytes = 0;
38
+ for await (const chunk of req) {
39
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40
+ bytes += buffer.byteLength;
41
+ if (bytes > MAX_BODY_BYTES) throw new Error('REQUEST_TOO_LARGE');
42
+ chunks.push(buffer);
43
+ }
44
+ if (chunks.length === 0) return {};
45
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
46
+ }
47
+
48
+ const ACTION_RISKS = new Set(['read', 'write', 'external', 'system', 'destructive']);
49
+ const PROVENANCE_KINDS = new Set(['user', 'chatgpt', 'trusted_policy', 'runtime', 'website', 'file', 'application', 'terminal']);
50
+
51
+ function validateActionEnvelope(value ) {
52
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('action must be a JSON object.');
53
+ const raw = value ;
54
+ const id = boundedString(raw.id, 'action.id', 256);
55
+ const capability = boundedString(raw.capability, 'action.capability', 128);
56
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(capability)) throw new Error('action.capability contains unsupported characters.');
57
+ if (typeof raw.risk !== 'string' || !ACTION_RISKS.has(raw.risk)) throw new Error('action.risk is invalid.');
58
+ if (!raw.input || typeof raw.input !== 'object' || Array.isArray(raw.input)) throw new Error('action.input must be a JSON object.');
59
+ if (!raw.provenance || typeof raw.provenance !== 'object' || Array.isArray(raw.provenance)) throw new Error('action.provenance must be a JSON object.');
60
+ const provenanceRaw = raw.provenance ;
61
+ if (typeof provenanceRaw.kind !== 'string' || !PROVENANCE_KINDS.has(provenanceRaw.kind)) throw new Error('action.provenance.kind is invalid.');
62
+ const source = provenanceRaw.source === undefined ? undefined : boundedString(provenanceRaw.source, 'action.provenance.source', 512);
63
+ const taskId = raw.taskId === undefined ? undefined : boundedString(raw.taskId, 'action.taskId', 256);
64
+ const target = raw.target === undefined ? undefined : boundedString(raw.target, 'action.target', 4096);
65
+ return {
66
+ id,
67
+ capability,
68
+ risk: raw.risk ,
69
+ input: raw.input ,
70
+ provenance: { kind: provenanceRaw.kind , source },
71
+ taskId,
72
+ target
73
+ };
74
+ }
75
+
76
+ function validateApprovalAuthority(input ) {
77
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('approvalAuthority must be an object.');
78
+ const raw = input ;
79
+ const accountId = String(raw.accountId ?? '');
80
+ const deviceId = String(raw.deviceId ?? '');
81
+ const generation = Number(raw.generation);
82
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
83
+ if (!uuid.test(accountId) || !uuid.test(deviceId) || !Number.isSafeInteger(generation) || generation < 1) {
84
+ throw new Error('approvalAuthority is invalid.');
85
+ }
86
+ return { accountId: accountId.toLowerCase(), deviceId: deviceId.toLowerCase(), generation };
87
+ }
88
+
89
+ function boundedString(value , field , maxLength ) {
90
+ if (typeof value !== 'string' || value.length === 0 || value.length > maxLength || value.includes('\0')) {
91
+ throw new Error(`${field} must be a non-empty string of at most ${maxLength} characters without NUL bytes.`);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ function send(res , status , payload ) {
97
+ const body = JSON.stringify(payload);
98
+ res.writeHead(status, {
99
+ 'content-type': 'application/json; charset=utf-8',
100
+ 'content-length': Buffer.byteLength(body),
101
+ 'cache-control': 'no-store',
102
+ 'x-content-type-options': 'nosniff'
103
+ });
104
+ res.end(body);
105
+ }
106
+
107
+ export function createLocalAgentServer(options
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+ ) {
125
+ if (options.token.length < 32) throw new Error('Agent token must be at least 32 characters.');
126
+ if (options.recoveryToken !== undefined && options.recoveryToken.length < 32) throw new Error('Recovery token must be at least 32 characters.');
127
+
128
+ const server = http.createServer(async (req, res) => {
129
+ const requestUrl = new URL(req.url ?? '/', 'http://operator.local');
130
+ const pathname = requestUrl.pathname;
131
+
132
+ if (pathname === '/health' && req.method === 'GET') {
133
+ const emergencyStopped = options.emergencyStop ? (await options.emergencyStop.status()).engaged : false;
134
+ send(res, 200, { ok: true, service: 'operator-local-agent', version: PRODUCT_VERSION, emergencyStopped });
135
+ return;
136
+ }
137
+
138
+ if (!timingSafeTokenMatch(req.headers.authorization, options.token)) {
139
+ send(res, 401, { ok: false, error: { code: 'UNAUTHORIZED', message: 'Valid agent bearer token required.' } });
140
+ return;
141
+ }
142
+
143
+ if (pathname === '/v1/activity' && req.method === 'GET') {
144
+ const requested = Number(requestUrl.searchParams.get('limit') ?? 100);
145
+ const limit = Number.isInteger(requested) ? Math.min(Math.max(requested, 1), 500) : 100;
146
+ send(res, 200, { ok: true, events: options.audit ? await options.audit.tail(limit) : [], configured: Boolean(options.audit) });
147
+ return;
148
+ }
149
+
150
+ if (pathname === '/v1/tasks' && req.method === 'GET') {
151
+ const requested = Number(requestUrl.searchParams.get('limit') ?? 100);
152
+ const limit = Number.isInteger(requested) ? Math.min(Math.max(requested, 1), 500) : 100;
153
+ send(res, 200, { ok: true, tasks: options.tasks ? await options.tasks.list(limit) : [], configured: Boolean(options.tasks) });
154
+ return;
155
+ }
156
+
157
+ if (pathname === '/v1/tasks' && req.method === 'POST') {
158
+ if (!options.taskOrchestrator) {
159
+ send(res, 503, { ok: false, error: { code: 'TASK_EXECUTOR_NOT_CONFIGURED', message: 'Task execution is not configured.' } });
160
+ return;
161
+ }
162
+ try {
163
+ const body = await readJson(req) ;
164
+ const goal = body.goal ;
165
+ const authorizedScope = taskAuthorizedScope(goal, options.permissions.allowedRoots);
166
+ if (!authorizedScope) {
167
+ send(res, 403, { ok: false, error: { code: 'TASK_SCOPE_DENIED', message: 'Task goal root is outside the authorized roots.' } });
168
+ return;
169
+ }
170
+ if (body.run === true && options.emergencyStop && (await options.emergencyStop.status()).engaged) {
171
+ send(res, 423, { ok: false, error: { code: 'EMERGENCY_STOPPED', message: 'Operator task execution is disabled by the local emergency stop.' } });
172
+ return;
173
+ }
174
+ const submitted = await options.taskOrchestrator.submit({
175
+ requestId: body.requestId,
176
+ objective: body.objective,
177
+ authorizedScope,
178
+ prohibitedScope: Array.isArray(body.prohibitedScope) ? body.prohibitedScope : [],
179
+ successConditions: body.successConditions,
180
+ goal,
181
+ maxSteps: body.maxSteps,
182
+ maxAttemptsPerStep: body.maxAttemptsPerStep,
183
+ timeoutMs: body.timeoutMs
184
+ } );
185
+ const task = body.run === true ? await options.taskOrchestrator.run(submitted.id) : submitted;
186
+ send(res, body.run === true ? 200 : 202, { ok: true, task });
187
+ } catch (error) {
188
+ const code = typeof (error )?.code === 'string' ? (error ).code : 'TASK_SUBMISSION_INVALID';
189
+ send(res, 400, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
190
+ }
191
+ return;
192
+ }
193
+
194
+ const taskRoute = /^\/v1\/tasks\/([0-9a-f-]{36})(?:\/(run|pause|resume|cancel))?$/i.exec(pathname);
195
+ if (taskRoute && req.method === 'GET' && !taskRoute[2]) {
196
+ if (!options.tasks) {
197
+ send(res, 503, { ok: false, error: { code: 'TASK_STORE_NOT_CONFIGURED', message: 'Task state is not configured.' } });
198
+ return;
199
+ }
200
+ try { send(res, 200, { ok: true, task: await options.tasks.get(taskRoute[1] ) }); }
201
+ catch (error) {
202
+ const code = typeof (error )?.code === 'string' ? (error ).code : 'TASK_READ_FAILED';
203
+ send(res, code === 'TASK_NOT_FOUND' ? 404 : 409, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
204
+ }
205
+ return;
206
+ }
207
+
208
+ if (taskRoute && req.method === 'POST' && taskRoute[2]) {
209
+ if (!options.taskOrchestrator) {
210
+ send(res, 503, { ok: false, error: { code: 'TASK_EXECUTOR_NOT_CONFIGURED', message: 'Task execution is not configured.' } });
211
+ return;
212
+ }
213
+ try {
214
+ const taskId = taskRoute[1] ;
215
+ const operation = taskRoute[2] ;
216
+ if ((operation === 'run' || operation === 'resume') && options.emergencyStop && (await options.emergencyStop.status()).engaged) {
217
+ send(res, 423, { ok: false, error: { code: 'EMERGENCY_STOPPED', message: 'Operator task execution is disabled by the local emergency stop.' } });
218
+ return;
219
+ }
220
+ let task;
221
+ if (operation === 'run') task = await options.taskOrchestrator.run(taskId);
222
+ else if (operation === 'pause') task = await options.taskOrchestrator.pause(taskId);
223
+ else if (operation === 'cancel') task = await options.taskOrchestrator.cancel(taskId);
224
+ else {
225
+ const body = await readJson(req) ;
226
+ const approvedActionId = body.approvedActionId === undefined ? undefined : boundedString(body.approvedActionId, 'approvedActionId', 256);
227
+ if (approvedActionId) {
228
+ if (!options.recoveryToken) {
229
+ send(res, 503, { ok: false, error: { code: 'TASK_APPROVAL_NOT_CONFIGURED', message: 'Task approval requires a separate recovery token.' } });
230
+ return;
231
+ }
232
+ const supplied = Array.isArray(req.headers['x-operator-recovery-token']) ? req.headers['x-operator-recovery-token'][0] : req.headers['x-operator-recovery-token'];
233
+ if (!timingSafeSecretMatch(supplied, options.recoveryToken)) {
234
+ send(res, 401, { ok: false, error: { code: 'RECOVERY_UNAUTHORIZED', message: 'Valid recovery token required.' } });
235
+ return;
236
+ }
237
+ const current = options.tasks ? await options.tasks.get(taskId) : null;
238
+ const blocked = current?.execution?.records.find((record) => record.state === 'BLOCKED');
239
+ if (!blocked || blocked.actionId !== approvedActionId) {
240
+ send(res, 409, { ok: false, error: { code: 'TASK_APPROVAL_MISMATCH', message: 'Approval must match the task current blocked action.' } });
241
+ return;
242
+ }
243
+ }
244
+ task = await options.taskOrchestrator.resume(taskId, approvedActionId ? [approvedActionId] : []);
245
+ }
246
+ send(res, 200, { ok: true, task });
247
+ } catch (error) {
248
+ const code = typeof (error )?.code === 'string' ? (error ).code : 'TASK_CONTROL_FAILED';
249
+ send(res, 409, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
250
+ }
251
+ return;
252
+ }
253
+
254
+ if (pathname === '/v1/devices' && req.method === 'GET') {
255
+ const local = options.deviceIdentity ? await options.deviceIdentity.loadExisting() : null;
256
+ const peers = options.deviceRegistry ? await options.deviceRegistry.listDevices() : [];
257
+ send(res, 200, {
258
+ ok: true,
259
+ local: local ? {
260
+ deviceId: local.deviceId,
261
+ deviceName: local.deviceName,
262
+ createdAt: local.createdAt,
263
+ fingerprint: local.fingerprint
264
+ } : null,
265
+ peers: peers.map((device) => ({
266
+ deviceId: device.deviceId,
267
+ deviceName: device.deviceName,
268
+ fingerprint: device.fingerprint,
269
+ status: device.status,
270
+ pairedAt: device.pairedAt,
271
+ revokedAt: device.revokedAt
272
+ })),
273
+ configured: Boolean(options.deviceIdentity)
274
+ });
275
+ return;
276
+ }
277
+
278
+ if (pathname === '/v1/settings' && req.method === 'GET') {
279
+ send(res, 200, { ok: true, settings: { ...(options.settings ?? {}) } });
280
+ return;
281
+ }
282
+
283
+ if (pathname === '/v1/privacy' && req.method === 'GET') {
284
+ send(res, 200, { ok: true, categories: options.privacy ? await options.privacy.inventory() : [], configured: Boolean(options.privacy) });
285
+ return;
286
+ }
287
+
288
+ if (pathname.startsWith('/v1/privacy/') && req.method === 'DELETE') {
289
+ if (!options.privacy || !options.recoveryToken) {
290
+ send(res, 503, { ok: false, error: { code: 'PRIVACY_CONTROLS_NOT_CONFIGURED', message: 'Privacy deletion requires local privacy state and a separate recovery token.' } });
291
+ return;
292
+ }
293
+ const supplied = Array.isArray(req.headers['x-operator-recovery-token']) ? req.headers['x-operator-recovery-token'][0] : req.headers['x-operator-recovery-token'];
294
+ if (!timingSafeSecretMatch(supplied, options.recoveryToken)) {
295
+ send(res, 401, { ok: false, error: { code: 'RECOVERY_UNAUTHORIZED', message: 'Valid recovery token required.' } });
296
+ return;
297
+ }
298
+ const category = decodeURIComponent(pathname.slice('/v1/privacy/'.length));
299
+ if (!['activity', 'tasks', 'session-state'].includes(category)) {
300
+ send(res, 400, { ok: false, error: { code: 'PRIVACY_CATEGORY_INVALID', message: 'Only activity, tasks, and session-state can be deleted through the generic privacy API.' } });
301
+ return;
302
+ }
303
+ try {
304
+ const removed = await options.privacy.purge(category );
305
+ send(res, 200, { ok: true, removed, categories: await options.privacy.inventory() });
306
+ } catch (error) {
307
+ send(res, 409, { ok: false, error: { code: 'PRIVACY_PURGE_FAILED', message: error instanceof Error ? error.message : String(error) } });
308
+ }
309
+ return;
310
+ }
311
+
312
+ if (pathname === '/v1/device/reset' && req.method === 'POST') {
313
+ if (!options.deviceReset || !options.recoveryToken) {
314
+ send(res, 503, { ok: false, error: { code: 'DEVICE_RESET_NOT_CONFIGURED', message: 'Device reset requires local recovery authority.' } });
315
+ return;
316
+ }
317
+ const supplied = Array.isArray(req.headers['x-operator-recovery-token']) ? req.headers['x-operator-recovery-token'][0] : req.headers['x-operator-recovery-token'];
318
+ if (!timingSafeSecretMatch(supplied, options.recoveryToken)) {
319
+ send(res, 401, { ok: false, error: { code: 'RECOVERY_UNAUTHORIZED', message: 'Valid recovery token required.' } });
320
+ return;
321
+ }
322
+ try {
323
+ const reset = await options.deviceReset();
324
+ send(res, 200, { ok: true, reset });
325
+ } catch (error) {
326
+ const code = typeof (error )?.code === 'string' ? (error ).code : 'DEVICE_RESET_FAILED';
327
+ send(res, 409, { ok: false, error: { code, message: error instanceof Error ? error.message : String(error) } });
328
+ }
329
+ return;
330
+ }
331
+
332
+ if (pathname === '/v1/permissions' && req.method === 'GET') {
333
+ send(res, 200, {
334
+ ok: true,
335
+ permissions: {
336
+ allowedCapabilities: [...options.permissions.allowedCapabilities],
337
+ allowedRoots: [...options.permissions.allowedRoots],
338
+ allowExternalWrites: Boolean(options.permissions.allowExternalWrites),
339
+ allowSystemChanges: Boolean(options.permissions.allowSystemChanges),
340
+ allowDestructive: Boolean(options.permissions.allowDestructive),
341
+ approvedActionIds: [...(options.permissions.approvedActionIds ?? [])]
342
+ }
343
+ });
344
+ return;
345
+ }
346
+
347
+
348
+ if (pathname === '/v1/approvals' && req.method === 'GET') {
349
+ if (!options.approvals) {
350
+ send(res, 200, { ok: true, approvals: [], configured: false });
351
+ return;
352
+ }
353
+ const approvals = (await options.approvals.list()).map((record) => ({
354
+ actionId: record.actionId,
355
+ capability: record.capability,
356
+ risk: record.risk,
357
+ target: record.target,
358
+ status: record.status,
359
+ createdAt: record.createdAt,
360
+ pendingExpiresAt: record.pendingExpiresAt,
361
+ approvalRequestId: record.approvalRequestId,
362
+ approvalExpiresAt: record.approvalExpiresAt
363
+ }));
364
+ send(res, 200, { ok: true, approvals, configured: true });
365
+ return;
366
+ }
367
+
368
+ if (pathname.startsWith('/v1/approvals/') && req.method === 'POST') {
369
+ if (!options.approvals || !options.recoveryToken) {
370
+ send(res, 503, { ok: false, error: { code: 'APPROVALS_NOT_CONFIGURED', message: 'One-time approvals require persistent approval state and a recovery token.' } });
371
+ return;
372
+ }
373
+ const supplied = Array.isArray(req.headers['x-operator-recovery-token']) ? req.headers['x-operator-recovery-token'][0] : req.headers['x-operator-recovery-token'];
374
+ if (!timingSafeSecretMatch(supplied, options.recoveryToken)) {
375
+ send(res, 401, { ok: false, error: { code: 'RECOVERY_UNAUTHORIZED', message: 'Valid recovery token required.' } });
376
+ return;
377
+ }
378
+ try {
379
+ const actionId = boundedString(decodeURIComponent(pathname.slice('/v1/approvals/'.length)), 'actionId', 256);
380
+ const body = await readJson(req) ;
381
+ const decision = String(body.decision ?? '');
382
+ const approvalRequestId = boundedString(body.approvalRequestId, 'approvalRequestId', 128);
383
+ const record = decision === 'approve'
384
+ ? await options.approvals.approve(actionId, approvalRequestId)
385
+ : decision === 'deny'
386
+ ? await options.approvals.deny(actionId, approvalRequestId)
387
+ : null;
388
+ if (!record) {
389
+ send(res, 400, { ok: false, error: { code: 'APPROVAL_DECISION_INVALID', message: 'decision must be approve or deny.' } });
390
+ return;
391
+ }
392
+ send(res, 200, {
393
+ ok: true,
394
+ approval: {
395
+ actionId: record.actionId,
396
+ capability: record.capability,
397
+ risk: record.risk,
398
+ target: record.target,
399
+ status: record.status,
400
+ approvalRequestId: record.approvalRequestId,
401
+ approvalExpiresAt: record.approvalExpiresAt
402
+ }
403
+ });
404
+ } catch (error) {
405
+ send(res, 409, { ok: false, error: { code: 'APPROVAL_UPDATE_FAILED', message: error instanceof Error ? error.message : String(error) } });
406
+ }
407
+ return;
408
+ }
409
+
410
+ if (pathname === '/v1/emergency-stop' && req.method === 'GET') {
411
+ if (!options.emergencyStop) {
412
+ send(res, 200, { ok: true, state: { version: 1, engaged: false }, configured: false });
413
+ return;
414
+ }
415
+ send(res, 200, { ok: true, state: await options.emergencyStop.status(), configured: true });
416
+ return;
417
+ }
418
+
419
+ if (pathname === '/v1/emergency-stop' && req.method === 'POST') {
420
+ if (!options.emergencyStop || !options.recoveryToken) {
421
+ send(res, 503, { ok: false, error: { code: 'EMERGENCY_STOP_NOT_CONFIGURED', message: 'Emergency stop requires persistent state and a separate recovery token before it can be engaged.' } });
422
+ return;
423
+ }
424
+ try {
425
+ const body = await readJson(req) ;
426
+ const reason = body.reason === undefined ? undefined : String(body.reason);
427
+ const state = await options.emergencyStop.engage(reason);
428
+ await options.onEmergencyStop?.();
429
+ await options.audit?.append({
430
+ capability: 'agent.emergency-stop',
431
+ result: 'success',
432
+ risk: 'destructive',
433
+ details: { operation: 'engage' }
434
+ });
435
+ send(res, 200, { ok: true, state });
436
+ } catch (error) {
437
+ send(res, 400, { ok: false, error: { code: 'BAD_REQUEST', message: error instanceof Error ? error.message : String(error) } });
438
+ }
439
+ return;
440
+ }
441
+
442
+ if (pathname === '/v1/emergency-stop' && req.method === 'DELETE') {
443
+ if (!options.emergencyStop || !options.recoveryToken) {
444
+ send(res, 503, { ok: false, error: { code: 'EMERGENCY_STOP_NOT_CONFIGURED', message: 'Emergency stop recovery is not configured.' } });
445
+ return;
446
+ }
447
+ const supplied = Array.isArray(req.headers['x-operator-recovery-token']) ? req.headers['x-operator-recovery-token'][0] : req.headers['x-operator-recovery-token'];
448
+ if (!timingSafeSecretMatch(supplied, options.recoveryToken)) {
449
+ send(res, 401, { ok: false, error: { code: 'RECOVERY_UNAUTHORIZED', message: 'Valid recovery token required.' } });
450
+ return;
451
+ }
452
+ const state = await options.emergencyStop.clear();
453
+ try {
454
+ await options.onEmergencyClear?.();
455
+ } catch (error) {
456
+ await options.emergencyStop.engage('relay recovery callback failed');
457
+ send(res, 503, { ok: false, error: { code: 'EMERGENCY_CLEAR_FAILED', message: error instanceof Error ? error.message : String(error) } });
458
+ return;
459
+ }
460
+ await options.audit?.append({
461
+ capability: 'agent.emergency-stop',
462
+ result: 'success',
463
+ risk: 'destructive',
464
+ details: { operation: 'clear' }
465
+ });
466
+ send(res, 200, { ok: true, state });
467
+ return;
468
+ }
469
+
470
+ if (pathname === '/v1/execute' && req.method === 'POST') {
471
+ try {
472
+ if (options.emergencyStop && (await options.emergencyStop.status()).engaged) {
473
+ await options.audit?.append({
474
+ capability: 'agent.execute',
475
+ result: 'blocked',
476
+ risk: 'system',
477
+ details: { code: 'EMERGENCY_STOPPED' }
478
+ });
479
+ send(res, 423, { ok: false, error: { code: 'EMERGENCY_STOPPED', message: 'Operator execution is disabled by the local emergency stop.' } });
480
+ return;
481
+ }
482
+ const body = await readJson(req) ;
483
+ if (!body.action || typeof body.action !== 'object') {
484
+ send(res, 400, { ok: false, error: { code: 'INVALID_REQUEST', message: 'action is required.' } });
485
+ return;
486
+ }
487
+ const action = validateActionEnvelope(body.action);
488
+ const approvalAuthority = body.approvalAuthority === undefined ? undefined : validateApprovalAuthority(body.approvalAuthority);
489
+ const oneTimeApproved = options.approvals ? await options.approvals.isApproved(action, approvalAuthority) : false;
490
+ const permissions = oneTimeApproved
491
+ ? {
492
+ ...options.permissions,
493
+ approvedActionIds: [...new Set([...(options.permissions.approvedActionIds ?? []), action.id])]
494
+ }
495
+ : options.permissions;
496
+ if (oneTimeApproved) await options.approvals .consume(action, approvalAuthority);
497
+ const result = await options.runtime.execute(action, permissions);
498
+ if (result.provider === 'policy' && result.error?.code === 'APPROVAL_REQUIRED') {
499
+ await options.approvals?.register(action, approvalAuthority);
500
+ }
501
+ await options.audit?.append({
502
+ taskId: action.taskId,
503
+ capability: action.capability,
504
+ target: action.target,
505
+ result: result.ok ? 'success' : result.provider === 'policy' ? 'blocked' : 'failure',
506
+ risk: action.risk,
507
+ details: {
508
+ actionId: action.id,
509
+ provenanceKind: action.provenance.kind,
510
+ provider: result.provider,
511
+ durationMs: result.durationMs,
512
+ errorCode: result.error?.code
513
+ }
514
+ });
515
+ send(res, result.ok ? 200 : 409, result);
516
+ } catch (error) {
517
+ const message = error instanceof Error ? error.message : String(error);
518
+ const code = message === 'REQUEST_TOO_LARGE' ? 'REQUEST_TOO_LARGE' : 'BAD_REQUEST';
519
+ send(res, code === 'REQUEST_TOO_LARGE' ? 413 : 400, { ok: false, error: { code, message } });
520
+ }
521
+ return;
522
+ }
523
+
524
+ send(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: 'Route not found.' } });
525
+ });
526
+
527
+ // Keep malformed/slow clients from occupying the authenticated local boundary indefinitely.
528
+ applyBoundedHttpServerPolicy(server);
529
+
530
+ return {
531
+ server,
532
+ async listen(host = '127.0.0.1', port = 0) {
533
+ const bindHost = requireLiteralLoopbackBindHost(host, 'Local agent');
534
+ await new Promise ((resolve, reject) => {
535
+ server.once('error', reject);
536
+ server.listen(port, bindHost, () => resolve());
537
+ });
538
+ const address = server.address() ;
539
+ return { host: bindHost, port: address.port };
540
+ },
541
+ async close() {
542
+ if (!server.listening) return;
543
+ await new Promise ((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
544
+ }
545
+ };
546
+ }
547
+
548
+ function withinAuthorizedRoots(input , roots ) {
549
+ const candidate = path.resolve(input);
550
+ return roots.some((root) => {
551
+ const relative = path.relative(path.resolve(root), candidate);
552
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
553
+ });
554
+ }
555
+
556
+ function taskAuthorizedScope(goal , roots ) {
557
+ if (!goal || typeof goal !== 'object') return null;
558
+ if (goal.kind === 'semantic-workflow') {
559
+ if (!Array.isArray(goal.steps) || goal.steps.length < 1 || goal.steps.length > 20) return null;
560
+ const scopes = [];
561
+ for (const step of goal.steps) {
562
+ if ((step ).kind === 'semantic-workflow') return null;
563
+ const child = taskAuthorizedScope(step, roots);
564
+ if (!child) return null;
565
+ scopes.push(...child);
566
+ }
567
+ return [...new Set(scopes)].sort();
568
+ }
569
+ if (goal.kind === 'browser-navigation') {
570
+ try {
571
+ const url = new URL(String(goal.url ?? ''));
572
+ return [`browser:${url.origin}`];
573
+ } catch { return ['browser:invalid']; }
574
+ }
575
+ if (goal.kind === 'app-operation') return ['application:uia'];
576
+ if (typeof goal.root !== 'string' || !withinAuthorizedRoots(goal.root, roots)) return null;
577
+ return [path.resolve(goal.root)];
578
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }