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,599 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { evidence } from '../core/evidence.js';
7
+ import { OperatorError } from '../core/errors.js';
8
+ import { readDurableStateBytes, readDurableStateText } from '../core/durable-state.js';
9
+ import { PathScope } from './path-scope.js';
10
+ import { ProcessProvider } from './process.js';
11
+
12
+ const SCORE = {
13
+ reliability: 0.99,
14
+ latency: 0.96,
15
+ determinism: 0.99,
16
+ security: 0.98,
17
+ reversibility: 0.8,
18
+ informationQuality: 0.99,
19
+ interactionCost: 0.01
20
+ };
21
+
22
+ const MAX_REGISTRY_BYTES = 512 * 1024;
23
+ const MAX_COMMANDS_PER_PROJECT = 100;
24
+ const MAX_ARTIFACTS_PER_COMMAND = 50;
25
+ const MAX_JSON_ARTIFACT_BYTES = 4 * 1024 * 1024;
26
+ const MAX_HASH_ARTIFACT_BYTES = 64 * 1024 * 1024;
27
+ const DEFAULT_TIMEOUT_MS = 10 * 60_000;
28
+ const REGISTRY_OPTIONS = {
29
+ maxBytes: MAX_REGISTRY_BYTES,
30
+ errorCode: 'COMMAND_REGISTRY_INVALID',
31
+ invalidMessage: 'Trusted command registry is invalid.'
32
+ } ;
33
+ const ARTIFACT_FILE_OPTIONS = {
34
+ maxBytes: MAX_HASH_ARTIFACT_BYTES,
35
+ errorCode: 'ARTIFACT_FILE_INVALID',
36
+ invalidMessage: 'Trusted artifact file is invalid.'
37
+ } ;
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+ export class ProjectCommandProvider {
86
+ name = 'project.command.trusted';
87
+ #scope ;
88
+ #allowedRoots ;
89
+ #allowedExecutables ;
90
+ #registryPath ;
91
+
92
+ constructor(options ) {
93
+ this.#scope = new PathScope(options.allowedRoots);
94
+ this.#allowedRoots = options.allowedRoots.map((root) => path.resolve(root));
95
+ this.#allowedExecutables = new Set(options.allowedExecutables.map((value) => value.toLowerCase()));
96
+ this.#registryPath = path.resolve(options.registryPath ?? path.join(os.homedir(), '.operator', 'project-commands.json'));
97
+ }
98
+
99
+ supports(action ) {
100
+ return ['project.command.inspect', 'project.command.run'].includes(action.capability);
101
+ }
102
+
103
+ score() { return SCORE; }
104
+
105
+ async resolveRisk(action ) {
106
+ if (action.capability === 'project.command.inspect') return 'read';
107
+ if (action.capability !== 'project.command.run') throw new OperatorError('CAPABILITY_RISK_UNRESOLVED', 'Unsupported project command capability.');
108
+ const projectRoot = await this.#scope.resolveExisting(String(action.input.path ?? action.input.cwd ?? ''));
109
+ const registry = await this.#readRegistry();
110
+ const project = await this.#matchProject(registry, projectRoot);
111
+ if (!project) throw new OperatorError('PROJECT_COMMANDS_NOT_REGISTERED', 'No trusted command registry entry exists for this project.');
112
+ const commandId = validateCommandId(String(action.input.commandId ?? ''));
113
+ const command = project.commands.find((candidate) => candidate.id === commandId);
114
+ if (!command) throw new OperatorError('PROJECT_COMMAND_NOT_REGISTERED', `Trusted command ${commandId} is not registered for this project.`);
115
+ return command.risk ;
116
+ }
117
+
118
+ async execute(action , context = {}) {
119
+ const started = performance.now();
120
+ try {
121
+ const projectRoot = await this.#scope.resolveExisting(String(action.input.path ?? action.input.cwd ?? ''));
122
+ const registry = await this.#readRegistry();
123
+ const project = await this.#matchProject(registry, projectRoot);
124
+
125
+ if (action.capability === 'project.command.inspect') {
126
+ return {
127
+ ok: true,
128
+ capability: action.capability,
129
+ provider: this.name,
130
+ output: {
131
+ projectRoot,
132
+ registryConfigured: registry !== null,
133
+ registryLocation: 'operator-local-config',
134
+ commands: project?.commands.map(({ id, title, kind, executable, args, cwd, timeoutMs, risk, artifacts }) => ({
135
+ id,
136
+ title,
137
+ kind,
138
+ executable,
139
+ args,
140
+ cwd,
141
+ timeoutMs,
142
+ risk,
143
+ artifacts
144
+ })) ?? []
145
+ },
146
+ evidence: [
147
+ evidence('command_registry', 'pass', 'Returned only commands from the trusted local Operator registry; project manifests were not treated as executable authority.', {
148
+ projectRoot,
149
+ commandCount: project?.commands.length ?? 0,
150
+ registryConfigured: registry !== null
151
+ })
152
+ ],
153
+ durationMs: Math.round(performance.now() - started)
154
+ };
155
+ }
156
+
157
+ if (!project) throw new OperatorError('PROJECT_COMMANDS_NOT_REGISTERED', 'No trusted command registry entry exists for this project.');
158
+ const commandId = validateCommandId(String(action.input.commandId ?? ''));
159
+ const command = project.commands.find((candidate) => candidate.id === commandId);
160
+ if (!command) throw new OperatorError('PROJECT_COMMAND_NOT_REGISTERED', `Trusted command ${commandId} is not registered for this project.`);
161
+
162
+ const expectedRisk = String(action.input.expectedRisk ?? '');
163
+ if (expectedRisk !== command.risk) {
164
+ throw new OperatorError('PROJECT_COMMAND_RISK_MISMATCH', 'Caller must acknowledge the command risk declared in the trusted registry.', {
165
+ details: { expectedRisk, registeredRisk: command.risk }
166
+ });
167
+ }
168
+ if (action.risk !== command.risk) {
169
+ throw new OperatorError('PROJECT_COMMAND_ACTION_RISK_MISMATCH', 'The action risk evaluated by local policy must exactly match the trusted registry risk.', {
170
+ details: { actionRisk: action.risk, registeredRisk: command.risk }
171
+ });
172
+ }
173
+
174
+ const cwd = await resolveCommandCwd(projectRoot, command.cwd);
175
+ const beforeArtifacts = await snapshotArtifacts(projectRoot, command.artifacts);
176
+ const process = new ProcessProvider({
177
+ allowedRoots: [projectRoot],
178
+ allowedExecutables: [...this.#allowedExecutables]
179
+ });
180
+ const result = await process.execute({
181
+ ...action,
182
+ capability: 'terminal.execute',
183
+ risk: command.risk ,
184
+ input: {
185
+ executable: command.executable,
186
+ args: command.args,
187
+ cwd,
188
+ timeoutMs: command.timeoutMs
189
+ },
190
+ provenance: { kind: 'trusted_policy', source: `project-command:${command.id}` }
191
+ }, context);
192
+
193
+ const baseOutput = {
194
+ command: {
195
+ id: command.id,
196
+ title: command.title,
197
+ kind: command.kind,
198
+ executable: command.executable,
199
+ args: command.args,
200
+ cwd: command.cwd,
201
+ risk: command.risk,
202
+ artifacts: command.artifacts
203
+ },
204
+ execution: result.output
205
+ };
206
+
207
+ if (!result.ok) {
208
+ return {
209
+ ...result,
210
+ capability: action.capability,
211
+ provider: this.name,
212
+ output: {
213
+ ...baseOutput,
214
+ validation: {
215
+ configured: command.artifacts.length > 0,
216
+ passed: false,
217
+ skipped: true,
218
+ reason: 'command_failed',
219
+ artifacts: []
220
+ }
221
+ },
222
+ evidence: [
223
+ evidence('command_registry', 'pass', 'Executed an explicitly registered trusted project command through the shell-free process provider.', {
224
+ commandId: command.id,
225
+ executable: command.executable,
226
+ risk: command.risk,
227
+ cwd
228
+ }),
229
+ ...result.evidence
230
+ ]
231
+ };
232
+ }
233
+
234
+ const validations = await validateArtifacts(projectRoot, command.artifacts, beforeArtifacts);
235
+ const validationPassed = validations.every((item) => item.passed);
236
+ const validation = {
237
+ configured: command.artifacts.length > 0,
238
+ passed: validationPassed,
239
+ skipped: false,
240
+ artifacts: validations
241
+ };
242
+ const validationEvidence = command.artifacts.length === 0
243
+ ? evidence('artifact_validation', 'info', 'Trusted command has no declared artifact validators.', { commandId: command.id })
244
+ : evidence('artifact_validation', validationPassed ? 'pass' : 'fail', validationPassed
245
+ ? 'All trusted artifact postconditions passed.'
246
+ : 'One or more trusted artifact postconditions failed.', {
247
+ commandId: command.id,
248
+ artifactCount: validations.length,
249
+ failed: validations.filter((item) => !item.passed).map((item) => item.path)
250
+ });
251
+
252
+ return {
253
+ ...result,
254
+ ok: validationPassed,
255
+ capability: action.capability,
256
+ provider: this.name,
257
+ output: { ...baseOutput, validation },
258
+ evidence: [
259
+ evidence('command_registry', 'pass', 'Executed an explicitly registered trusted project command through the shell-free process provider.', {
260
+ commandId: command.id,
261
+ executable: command.executable,
262
+ risk: command.risk,
263
+ cwd
264
+ }),
265
+ ...result.evidence,
266
+ validationEvidence
267
+ ],
268
+ error: validationPassed ? undefined : {
269
+ code: 'ARTIFACT_VALIDATION_FAILED',
270
+ message: 'Command exited successfully but trusted artifact validation failed.',
271
+ retryable: false
272
+ }
273
+ };
274
+ } catch (error) {
275
+ const op = error instanceof OperatorError
276
+ ? error
277
+ : new OperatorError('PROJECT_COMMAND_ERROR', error instanceof Error ? error.message : String(error));
278
+ return {
279
+ ok: false,
280
+ capability: action.capability,
281
+ provider: this.name,
282
+ evidence: [evidence('command_registry', 'fail', op.message, { code: op.code })],
283
+ error: { code: op.code, message: op.message, retryable: op.retryable },
284
+ durationMs: Math.round(performance.now() - started)
285
+ };
286
+ }
287
+ }
288
+
289
+ async #readRegistry() {
290
+ let realRegistry ;
291
+ try {
292
+ realRegistry = await fs.realpath(this.#registryPath);
293
+ } catch (error) {
294
+ if ((error ).code === 'ENOENT') return null;
295
+ throw error;
296
+ }
297
+
298
+ for (const root of this.#allowedRoots) {
299
+ let realRoot = root;
300
+ try { realRoot = await fs.realpath(root); } catch { /* retain lexical root */ }
301
+ if (isWithin(this.#registryPath, root) || isWithin(realRegistry, realRoot)) {
302
+ throw new OperatorError('COMMAND_REGISTRY_INSIDE_PROJECT_DENIED', 'Trusted command registry must live outside all project-authorized roots so project files cannot rewrite execution authority.');
303
+ }
304
+ }
305
+
306
+ let raw ;
307
+ try {
308
+ raw = await readDurableStateText(this.#registryPath, REGISTRY_OPTIONS);
309
+ } catch (error) {
310
+ if ((error ).code === 'ENOENT') return null;
311
+ throw error;
312
+ }
313
+
314
+ let parsed ;
315
+ try { parsed = JSON.parse(raw); } catch {
316
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', 'Trusted command registry is not valid JSON.');
317
+ }
318
+ return validateRegistry(parsed, this.#allowedExecutables);
319
+ }
320
+
321
+ async #matchProject(registry , requestedRoot ) {
322
+ if (!registry) return undefined;
323
+ const canonicalAllowedRoots = await Promise.all(this.#allowedRoots.map(async (root) => {
324
+ try { return await fs.realpath(root); } catch { return root; }
325
+ }));
326
+ for (const project of registry.projects) {
327
+ const plausiblyInScope = this.#allowedRoots.some((root) => isWithin(project.root, root))
328
+ || canonicalAllowedRoots.some((root) => isWithin(project.root, root));
329
+ if (!plausiblyInScope) continue;
330
+ let canonical ;
331
+ try {
332
+ canonical = await this.#scope.resolveExisting(project.root);
333
+ } catch (error) {
334
+ if (error instanceof OperatorError && error.code === 'PATH_OUTSIDE_SCOPE') continue;
335
+ throw error;
336
+ }
337
+ if (canonical === requestedRoot) return { ...project, root: canonical };
338
+ }
339
+ return undefined;
340
+ }
341
+ }
342
+
343
+ function validateRegistry(input , allowedExecutables ) {
344
+ if (!input || typeof input !== 'object') throw new OperatorError('COMMAND_REGISTRY_INVALID', 'Trusted command registry must be an object.');
345
+ const raw = input ;
346
+ if (raw.version !== 1 || !Array.isArray(raw.projects) || raw.projects.length > 100) {
347
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', 'Trusted command registry requires version=1 and at most 100 projects.');
348
+ }
349
+ const projects = raw.projects.map((entry, projectIndex) => {
350
+ if (!entry || typeof entry !== 'object') throw new OperatorError('COMMAND_REGISTRY_INVALID', `Project entry ${projectIndex} is invalid.`);
351
+ const project = entry ;
352
+ const root = String(project.root ?? '');
353
+ if (!path.isAbsolute(root)) throw new OperatorError('COMMAND_REGISTRY_INVALID', `Project ${projectIndex} root must be absolute.`);
354
+ if (!Array.isArray(project.commands) || project.commands.length > MAX_COMMANDS_PER_PROJECT) {
355
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Project ${projectIndex} commands must be an array of at most ${MAX_COMMANDS_PER_PROJECT}.`);
356
+ }
357
+ const seen = new Set ();
358
+ const commands = project.commands.map((candidate, commandIndex) => {
359
+ if (!candidate || typeof candidate !== 'object') throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandIndex} is invalid.`);
360
+ const command = candidate ;
361
+ const id = validateCommandId(String(command.id ?? ''));
362
+ if (seen.has(id)) throw new OperatorError('COMMAND_REGISTRY_INVALID', `Duplicate command id ${id}.`);
363
+ seen.add(id);
364
+ const executable = String(command.executable ?? '');
365
+ if (!/^[A-Za-z0-9._+-]+(?:\.exe)?$/i.test(executable) || !allowedExecutables.has(executable.toLowerCase())) {
366
+ throw new OperatorError('COMMAND_EXECUTABLE_DENIED', `Command ${id} executable is not in the local-agent executable allowlist.`);
367
+ }
368
+ const args = Array.isArray(command.args) ? command.args.map((value) => String(value)) : [];
369
+ if (args.length > 200 || args.some((arg) => arg.length > 4000 || arg.includes('\0'))) {
370
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${id} arguments exceed bounds.`);
371
+ }
372
+ const cwd = String(command.cwd ?? '.');
373
+ if (path.isAbsolute(cwd) || cwd.includes('\0')) throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${id} cwd must be relative.`);
374
+ const timeoutMs = Math.min(Math.max(Number(command.timeoutMs ?? DEFAULT_TIMEOUT_MS), 100), 10 * 60_000);
375
+ const risk = String(command.risk ?? 'write');
376
+ if (!['read', 'write', 'external'].includes(risk)) throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${id} risk is invalid.`);
377
+ const kind = typeof command.kind === 'string' && ['build', 'test', 'lint', 'format', 'dev', 'database', 'custom'].includes(command.kind)
378
+ ? command.kind
379
+ : undefined;
380
+ const title = typeof command.title === 'string' ? command.title.trim().slice(0, 160) || undefined : undefined;
381
+ const rawArtifacts = command.artifacts === undefined ? [] : command.artifacts;
382
+ if (!Array.isArray(rawArtifacts) || rawArtifacts.length > MAX_ARTIFACTS_PER_COMMAND) {
383
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${id} artifacts must be an array of at most ${MAX_ARTIFACTS_PER_COMMAND}.`);
384
+ }
385
+ const artifacts = rawArtifacts.map((rawArtifact, artifactIndex) => validateArtifact(rawArtifact, id, artifactIndex));
386
+ return { id, title, kind, executable, args, cwd, timeoutMs, risk: risk , artifacts };
387
+ });
388
+ return { root: path.resolve(root), commands };
389
+ });
390
+ return { version: 1, projects };
391
+ }
392
+
393
+ function validateArtifact(input , commandId , index ) {
394
+ if (!input || typeof input !== 'object') throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandId} artifact ${index} is invalid.`);
395
+ const raw = input ;
396
+ const artifactPath = String(raw.path ?? '').trim();
397
+ if (!artifactPath || artifactPath.length > 1000 || artifactPath.includes('\0') || artifactPath.includes('\r') || artifactPath.includes('\n') || path.isAbsolute(artifactPath)) {
398
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandId} artifact ${index} path must be a bounded relative path.`);
399
+ }
400
+ const normalized = path.normalize(artifactPath);
401
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`) || path.isAbsolute(normalized)) {
402
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandId} artifact ${index} escapes the project root.`);
403
+ }
404
+ const kind = String(raw.kind ?? 'file');
405
+ if (!['file', 'directory', 'json'].includes(kind)) throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandId} artifact ${index} kind is invalid.`);
406
+ const minBytesValue = Number(raw.minBytes ?? (kind === 'directory' ? 0 : 1));
407
+ if (!Number.isSafeInteger(minBytesValue) || minBytesValue < 0 || minBytesValue > 1024 * 1024 * 1024) {
408
+ throw new OperatorError('COMMAND_REGISTRY_INVALID', `Command ${commandId} artifact ${index} minBytes is invalid.`);
409
+ }
410
+ return {
411
+ path: artifactPath,
412
+ kind: kind ,
413
+ minBytes: minBytesValue,
414
+ mustChange: raw.mustChange === true
415
+ };
416
+ }
417
+
418
+ function validateCommandId(value ) {
419
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value)) throw new OperatorError('INVALID_COMMAND_ID', 'commandId must be 1-64 safe identifier characters.');
420
+ return value;
421
+ }
422
+
423
+ async function resolveCommandCwd(projectRoot , relativeCwd ) {
424
+ const absolute = path.resolve(projectRoot, relativeCwd);
425
+ if (!isWithin(absolute, projectRoot)) throw new OperatorError('COMMAND_CWD_OUTSIDE_PROJECT', 'Registered command cwd escapes the project root.');
426
+ const stat = await fs.stat(absolute);
427
+ if (!stat.isDirectory()) throw new OperatorError('COMMAND_CWD_INVALID', 'Registered command cwd is not an existing directory.');
428
+ return absolute;
429
+ }
430
+
431
+ async function snapshotArtifacts(projectRoot , artifacts ) {
432
+ const output = new Map ();
433
+ for (const artifact of artifacts) {
434
+ const snapshot = await snapshotArtifact(projectRoot, artifact.path);
435
+ if (snapshot.type === 'symlink' || snapshot.type === 'hardlink') {
436
+ throw new OperatorError('ARTIFACT_FILE_INVALID', `Trusted artifact ${artifact.path} must not be a symbolic or hard link.`);
437
+ }
438
+ output.set(artifact.path, snapshot);
439
+ }
440
+ return output;
441
+ }
442
+
443
+ async function snapshotArtifact(projectRoot , relativePath ) {
444
+ const absolute = path.resolve(projectRoot, relativePath);
445
+ if (!isWithin(absolute, projectRoot)) throw new OperatorError('ARTIFACT_PATH_OUTSIDE_PROJECT', `Artifact ${relativePath} escapes the project root.`);
446
+ const canonicalProjectRoot = await fs.realpath(projectRoot);
447
+ try {
448
+ const stat = await fs.lstat(absolute);
449
+ if (stat.isSymbolicLink()) return { path: relativePath, exists: true, type: 'symlink', size: stat.size };
450
+
451
+ const canonical = await fs.realpath(absolute);
452
+ if (!isWithin(canonical, canonicalProjectRoot)) {
453
+ throw new OperatorError('ARTIFACT_PATH_OUTSIDE_PROJECT', `Artifact ${relativePath} resolves outside the project root.`);
454
+ }
455
+ const canonicalStat = await fs.lstat(canonical);
456
+ if (!sameFile(stat, canonicalStat)) {
457
+ throw new OperatorError('ARTIFACT_PATH_CHANGED', `Artifact ${relativePath} changed while its path was being resolved.`);
458
+ }
459
+
460
+ if (canonicalStat.isDirectory()) {
461
+ const entries = (await fs.readdir(canonical)).sort();
462
+ const current = await fs.realpath(absolute);
463
+ if (current !== canonical) throw new OperatorError('ARTIFACT_PATH_CHANGED', `Artifact ${relativePath} changed while it was being inspected.`);
464
+ const fingerprint = crypto.createHash('sha256').update(entries.join('\0')).digest('hex');
465
+ return { path: relativePath, exists: true, type: 'directory', size: entries.length, fingerprint };
466
+ }
467
+ if (canonicalStat.isFile()) {
468
+ if (canonicalStat.nlink !== 1) return { path: relativePath, exists: true, type: 'hardlink', size: canonicalStat.size };
469
+ const fingerprint = canonicalStat.size <= MAX_HASH_ARTIFACT_BYTES
470
+ ? crypto.createHash('sha256').update(await readDurableStateBytes(canonical, ARTIFACT_FILE_OPTIONS)).digest('hex')
471
+ : crypto.createHash('sha256').update(`${canonicalStat.size}\0${canonicalStat.mtimeMs}`).digest('hex');
472
+ const current = await fs.realpath(absolute);
473
+ if (current !== canonical) throw new OperatorError('ARTIFACT_PATH_CHANGED', `Artifact ${relativePath} changed while it was being inspected.`);
474
+ return { path: relativePath, exists: true, type: 'file', size: canonicalStat.size, fingerprint };
475
+ }
476
+ return { path: relativePath, exists: true, type: 'other', size: canonicalStat.size };
477
+ } catch (error) {
478
+ if ((error ).code === 'ENOENT') {
479
+ await assertNearestExistingParentInsideProject(absolute, canonicalProjectRoot, relativePath);
480
+ return { path: relativePath, exists: false };
481
+ }
482
+ throw error;
483
+ }
484
+ }
485
+
486
+ async function validateArtifacts(
487
+ projectRoot ,
488
+ artifacts ,
489
+ before
490
+ ) {
491
+ const validations = [];
492
+ for (const artifact of artifacts) {
493
+ const beforeSnapshot = before.get(artifact.path) ?? { path: artifact.path, exists: false };
494
+ const after = await snapshotArtifact(projectRoot, artifact.path);
495
+ const checks = [];
496
+ checks.push({ check: 'exists', passed: after.exists, detail: after.exists ? 'Artifact exists.' : 'Artifact is missing.' });
497
+
498
+ const expectedType = artifact.kind === 'directory' ? 'directory' : 'file';
499
+ const typePassed = after.exists && after.type === expectedType;
500
+ checks.push({ check: 'type', passed: typePassed, detail: typePassed
501
+ ? `Artifact type is ${expectedType}.`
502
+ : `Expected ${expectedType}; observed ${after.type ?? 'missing'}.` });
503
+
504
+ if (expectedType === 'file') {
505
+ const size = after.size ?? 0;
506
+ checks.push({ check: 'minBytes', passed: typePassed && size >= artifact.minBytes, detail: `Observed ${size} bytes; required at least ${artifact.minBytes}.` });
507
+ }
508
+
509
+ if (after.type === 'symlink') {
510
+ checks.push({ check: 'symlink', passed: false, detail: 'Artifact symlinks are not accepted for trusted validation.' });
511
+ }
512
+ if (after.type === 'hardlink') {
513
+ checks.push({ check: 'hardlink', passed: false, detail: 'Hard-linked artifact files are not accepted for trusted validation.' });
514
+ }
515
+
516
+ if (artifact.mustChange) {
517
+ const changed = after.exists && (!beforeSnapshot.exists || after.type !== beforeSnapshot.type || after.fingerprint !== beforeSnapshot.fingerprint || after.size !== beforeSnapshot.size);
518
+ checks.push({ check: 'mustChange', passed: changed, detail: changed ? 'Artifact changed during this command run.' : 'Artifact did not change during this command run.' });
519
+ }
520
+
521
+ if (artifact.kind === 'json') {
522
+ let jsonPassed = false;
523
+ let detail = 'JSON artifact is unavailable.';
524
+ if (after.type === 'file') {
525
+ if ((after.size ?? 0) > MAX_JSON_ARTIFACT_BYTES) {
526
+ detail = `JSON artifact exceeds ${MAX_JSON_ARTIFACT_BYTES} byte parse limit.`;
527
+ } else {
528
+ try {
529
+ JSON.parse(await readTrustedArtifactText(projectRoot, artifact.path, MAX_JSON_ARTIFACT_BYTES));
530
+ jsonPassed = true;
531
+ detail = 'JSON artifact parsed successfully.';
532
+ } catch (error) {
533
+ if (error instanceof OperatorError) throw error;
534
+ detail = 'JSON artifact is not valid JSON.';
535
+ }
536
+ }
537
+ }
538
+ checks.push({ check: 'json', passed: jsonPassed, detail });
539
+ }
540
+
541
+ validations.push({
542
+ path: artifact.path,
543
+ kind: artifact.kind,
544
+ passed: checks.every((check) => check.passed),
545
+ checks,
546
+ before: beforeSnapshot,
547
+ after
548
+ });
549
+ }
550
+ return validations;
551
+ }
552
+
553
+ async function readTrustedArtifactText(projectRoot , relativePath , maxBytes ) {
554
+ const absolute = path.resolve(projectRoot, relativePath);
555
+ const canonicalProjectRoot = await fs.realpath(projectRoot);
556
+ const stat = await fs.lstat(absolute);
557
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
558
+ throw new OperatorError('ARTIFACT_FILE_INVALID', `Trusted artifact ${relativePath} must be a single-link regular file.`);
559
+ }
560
+ const canonical = await fs.realpath(absolute);
561
+ if (!isWithin(canonical, canonicalProjectRoot)) {
562
+ throw new OperatorError('ARTIFACT_PATH_OUTSIDE_PROJECT', `Artifact ${relativePath} resolves outside the project root.`);
563
+ }
564
+ const raw = await readDurableStateBytes(canonical, {
565
+ maxBytes,
566
+ errorCode: 'ARTIFACT_FILE_INVALID',
567
+ invalidMessage: `Trusted artifact ${relativePath} is invalid.`
568
+ });
569
+ const current = await fs.realpath(absolute);
570
+ if (current !== canonical) throw new OperatorError('ARTIFACT_PATH_CHANGED', `Artifact ${relativePath} changed while it was being read.`);
571
+ return raw.toString('utf8');
572
+ }
573
+
574
+ async function assertNearestExistingParentInsideProject(absolute , canonicalProjectRoot , relativePath ) {
575
+ let current = path.dirname(absolute);
576
+ while (true) {
577
+ try {
578
+ const canonical = await fs.realpath(current);
579
+ if (!isWithin(canonical, canonicalProjectRoot)) {
580
+ throw new OperatorError('ARTIFACT_PATH_OUTSIDE_PROJECT', `Artifact ${relativePath} has a parent directory outside the project root.`);
581
+ }
582
+ return;
583
+ } catch (error) {
584
+ if ((error ).code !== 'ENOENT') throw error;
585
+ const parent = path.dirname(current);
586
+ if (parent === current) throw error;
587
+ current = parent;
588
+ }
589
+ }
590
+ }
591
+
592
+ function sameFile(left , right ) {
593
+ return left.dev === right.dev && left.ino === right.ino;
594
+ }
595
+
596
+ function isWithin(candidate , root ) {
597
+ const rel = path.relative(path.resolve(root), path.resolve(candidate));
598
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
599
+ }