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,264 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+
5
+ import { evidence } from '../core/evidence.js';
6
+ import { OperatorError } from '../core/errors.js';
7
+ import { PathScope } from './path-scope.js';
8
+
9
+ function sha256(buffer ) {
10
+ return crypto.createHash('sha256').update(buffer).digest('hex');
11
+ }
12
+
13
+ const DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
14
+ const DEFAULT_MAX_WRITE_BYTES = 2 * 1024 * 1024;
15
+ const MAX_CONFIGURED_IO_BYTES = 64 * 1024 * 1024;
16
+
17
+ const SCORE = {
18
+ reliability: 0.98,
19
+ latency: 0.97,
20
+ determinism: 0.99,
21
+ security: 0.96,
22
+ reversibility: 0.78,
23
+ informationQuality: 0.99,
24
+ interactionCost: 0.01
25
+ };
26
+
27
+ export class FilesystemProvider {
28
+ name = 'filesystem.native';
29
+ #scope ;
30
+ #maxReadBytes ;
31
+ #maxWriteBytes ;
32
+ #replaceClaimHook ;
33
+ #pathLeaseHook ;
34
+
35
+ constructor(options
36
+
37
+
38
+
39
+
40
+
41
+
42
+ ) {
43
+ this.#scope = new PathScope(options.allowedRoots, { windowsPathLeaseExecutable: options.windowsPathLeaseExecutable });
44
+ this.#maxReadBytes = boundedBytes(options.maxReadBytes, DEFAULT_MAX_READ_BYTES);
45
+ this.#maxWriteBytes = boundedBytes(options.maxWriteBytes, DEFAULT_MAX_WRITE_BYTES);
46
+ this.#replaceClaimHook = options.replaceClaimHook;
47
+ this.#pathLeaseHook = options.pathLeaseHook;
48
+ }
49
+
50
+ supports(action ) {
51
+ return ['file.read', 'file.list', 'file.write', 'file.create', 'file.replace'].includes(action.capability);
52
+ }
53
+
54
+ score() { return SCORE; }
55
+
56
+ async execute(action ) {
57
+ const started = performance.now();
58
+ try {
59
+ if (action.capability === 'file.read') return await this.#read(action, started);
60
+ if (action.capability === 'file.list') return await this.#list(action, started);
61
+ if (['file.write', 'file.create', 'file.replace'].includes(action.capability)) return await this.#write(action, started);
62
+ throw new OperatorError('UNSUPPORTED_ACTION', action.capability);
63
+ } catch (error) {
64
+ const op = error instanceof OperatorError
65
+ ? error
66
+ : new OperatorError('FILESYSTEM_ERROR', error instanceof Error ? error.message : String(error));
67
+ return {
68
+ ok: false,
69
+ capability: action.capability,
70
+ provider: this.name,
71
+ evidence: [evidence('filesystem', 'fail', op.message, { code: op.code })],
72
+ error: { code: op.code, message: op.message, retryable: op.retryable },
73
+ durationMs: Math.round(performance.now() - started)
74
+ };
75
+ }
76
+ }
77
+
78
+ async #read(action , started ) {
79
+ const requested = requiredString(action.input.path, 'path');
80
+ return await this.#scope.withExisting(requested, async (filePath) => {
81
+ await this.#pathLeaseHook?.(action.capability, filePath);
82
+ const stat = await fs.stat(filePath);
83
+ if (!stat.isFile()) throw new OperatorError('NOT_A_FILE', 'Requested path is not a file.');
84
+ if (stat.size > this.#maxReadBytes) {
85
+ throw new OperatorError('READ_TOO_LARGE', `File exceeds ${this.#maxReadBytes} byte read limit.`, { details: { size: stat.size } });
86
+ }
87
+ const data = await fs.readFile(filePath);
88
+ const encoding = action.input.encoding === 'base64' ? 'base64' : 'utf8';
89
+ const digest = sha256(data);
90
+ return {
91
+ ok: true,
92
+ capability: action.capability,
93
+ provider: this.name,
94
+ output: { path: filePath, size: data.byteLength, sha256: digest, content: data.toString(encoding) },
95
+ evidence: [evidence('file_read', 'pass', 'File read from authorized scope.', { path: filePath, size: data.byteLength, sha256: digest })],
96
+ durationMs: Math.round(performance.now() - started)
97
+ };
98
+ });
99
+ }
100
+
101
+ async #list(action , started ) {
102
+ const requested = requiredString(action.input.path, 'path');
103
+ return await this.#scope.withExisting(requested, async (dirPath) => {
104
+ await this.#pathLeaseHook?.(action.capability, dirPath);
105
+ const stat = await fs.stat(dirPath);
106
+ if (!stat.isDirectory()) throw new OperatorError('NOT_A_DIRECTORY', 'Requested path is not a directory.');
107
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
108
+ const bounded = entries.slice(0, 500).map((entry) => ({
109
+ name: entry.name,
110
+ type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : entry.isSymbolicLink() ? 'symlink' : 'other'
111
+ }));
112
+ return {
113
+ ok: true,
114
+ capability: action.capability,
115
+ provider: this.name,
116
+ output: { path: dirPath, entries: bounded, truncated: entries.length > bounded.length },
117
+ evidence: [evidence('directory_list', 'pass', 'Directory listed from authorized scope.', { path: dirPath, count: bounded.length })],
118
+ durationMs: Math.round(performance.now() - started)
119
+ };
120
+ });
121
+ }
122
+
123
+ async #write(action , started ) {
124
+ const requested = requiredString(action.input.path, 'path');
125
+ if (typeof action.input.content !== 'string') throw new OperatorError('WRITE_CONTENT_INVALID', 'File content must be a string.');
126
+ const content = action.input.content;
127
+ const contentBytes = Buffer.byteLength(content, 'utf8');
128
+ if (contentBytes > this.#maxWriteBytes) {
129
+ throw new OperatorError('WRITE_TOO_LARGE', `Content exceeds ${this.#maxWriteBytes} byte write limit.`, { details: { size: contentBytes } });
130
+ }
131
+ const expectedSha = normalizeExpectedSha(action.input.expectedSha256);
132
+ const mode = action.capability === 'file.create' ? 'create' : action.capability === 'file.replace' ? 'replace' : 'write';
133
+ if (mode === 'replace' && !expectedSha) {
134
+ throw new OperatorError('PRECONDITION_REQUIRED', 'file.replace requires expectedSha256 from a fresh file.read.');
135
+ }
136
+
137
+ return await this.#scope.withForWrite(requested, async (filePath) => {
138
+ await this.#pathLeaseHook?.(action.capability, filePath);
139
+ let beforeSha = null;
140
+ try {
141
+ const targetStat = await fs.lstat(filePath);
142
+ if (targetStat.isSymbolicLink()) {
143
+ throw new OperatorError('WRITE_SYMLINK_DENIED', 'Refusing to write through or replace an existing symbolic link.');
144
+ }
145
+ if (!targetStat.isFile()) throw new OperatorError('NOT_A_FILE', 'Existing write target is not a regular file.');
146
+ if (mode === 'create') throw new OperatorError('TARGET_EXISTS', 'file.create refuses to overwrite an existing file.');
147
+ const before = await fs.readFile(filePath);
148
+ beforeSha = sha256(before);
149
+ if (expectedSha && expectedSha !== beforeSha) {
150
+ throw new OperatorError('PRECONDITION_FAILED', 'File changed since it was inspected.', { details: { expectedSha, actualSha: beforeSha } });
151
+ }
152
+ } catch (error) {
153
+ if (error instanceof OperatorError) throw error;
154
+ const code = (error ).code;
155
+ if (code !== 'ENOENT') throw error;
156
+ if (mode === 'replace') throw new OperatorError('TARGET_MISSING', 'file.replace requires an existing file.');
157
+ if (expectedSha) throw new OperatorError('PRECONDITION_FAILED', 'Expected existing file is missing.');
158
+ }
159
+
160
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.operator-${crypto.randomUUID()}.tmp`);
161
+ let renamed = false;
162
+ try {
163
+ await fs.writeFile(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
164
+ if (mode === 'create') {
165
+ await fs.writeFile(filePath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
166
+ await fs.rm(tempPath, { force: true });
167
+ } else if (mode === 'replace') {
168
+ beforeSha = await replaceWithExpectedSha(filePath, tempPath, expectedSha , this.#replaceClaimHook);
169
+ } else {
170
+ await fs.rename(tempPath, filePath);
171
+ }
172
+ renamed = true;
173
+ } finally {
174
+ if (!renamed) await fs.rm(tempPath, { force: true }).catch(() => undefined);
175
+ }
176
+
177
+ const after = await fs.readFile(filePath);
178
+ const afterSha = sha256(after);
179
+ if (afterSha !== sha256(Buffer.from(content, 'utf8'))) {
180
+ throw new OperatorError('WRITE_POSTCONDITION_FAILED', 'Written bytes do not match requested content.');
181
+ }
182
+
183
+ return {
184
+ ok: true,
185
+ capability: action.capability,
186
+ provider: this.name,
187
+ output: { path: filePath, bytes: after.byteLength, beforeSha256: beforeSha, afterSha256: afterSha },
188
+ evidence: [
189
+ evidence('file_write', 'pass', 'Atomic file write completed inside authorized scope.', { path: filePath }),
190
+ evidence('postcondition', 'pass', 'Written bytes match requested content.', { afterSha256: afterSha, bytes: after.byteLength })
191
+ ],
192
+ durationMs: Math.round(performance.now() - started)
193
+ };
194
+ });
195
+ }
196
+ }
197
+
198
+ function requiredString(value , field ) {
199
+ if (typeof value !== 'string' || value.length === 0 || value.includes('\0')) {
200
+ throw new OperatorError('FILESYSTEM_INPUT_INVALID', `${field} must be a non-empty string without NUL bytes.`);
201
+ }
202
+ return value;
203
+ }
204
+
205
+ function normalizeExpectedSha(value ) {
206
+ if (value === undefined) return undefined;
207
+ if (typeof value !== 'string' || !/^[0-9a-f]{64}$/i.test(value)) {
208
+ throw new OperatorError('PRECONDITION_INVALID', 'expectedSha256 must be exactly 64 hexadecimal characters.');
209
+ }
210
+ return value.toLowerCase();
211
+ }
212
+
213
+ function boundedBytes(value , fallback ) {
214
+ if (value === undefined) return fallback;
215
+ const parsed = Number(value);
216
+ if (!Number.isFinite(parsed)) return fallback;
217
+ return Math.min(Math.max(Math.trunc(parsed), 1), MAX_CONFIGURED_IO_BYTES);
218
+ }
219
+
220
+ async function replaceWithExpectedSha(
221
+ filePath ,
222
+ tempPath ,
223
+ expectedSha ,
224
+ afterClaim
225
+ ) {
226
+ const backupPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.operator-${crypto.randomUUID()}.bak`);
227
+ let claimed = false;
228
+ try {
229
+ try { await fs.rename(filePath, backupPath); claimed = true; }
230
+ catch (error) {
231
+ if ((error ).code === 'ENOENT') throw new OperatorError('TARGET_MISSING', 'file.replace requires an existing file.');
232
+ throw error;
233
+ }
234
+ const stat = await fs.lstat(backupPath);
235
+ if (stat.isSymbolicLink()) throw new OperatorError('WRITE_SYMLINK_DENIED', 'Refusing to replace a symbolic link.');
236
+ if (!stat.isFile()) throw new OperatorError('NOT_A_FILE', 'Existing write target is not a regular file.');
237
+ const before = await fs.readFile(backupPath);
238
+ const beforeSha = sha256(before);
239
+ if (beforeSha !== expectedSha) throw new OperatorError('PRECONDITION_FAILED', 'File changed since it was inspected.', { details: { expectedSha, actualSha: beforeSha } });
240
+ await afterClaim?.(filePath);
241
+ try { await fs.link(tempPath, filePath); }
242
+ catch (error) {
243
+ if ((error ).code === 'EEXIST') throw new OperatorError('PRECONDITION_FAILED', 'File changed during replacement.');
244
+ throw error;
245
+ }
246
+ await fs.rm(tempPath, { force: true });
247
+ await fs.rm(backupPath, { force: true });
248
+ claimed = false;
249
+ return beforeSha;
250
+ } finally {
251
+ if (claimed) await restoreClaimedPath(backupPath, filePath);
252
+ }
253
+ }
254
+ async function restoreClaimedPath(backupPath , filePath ) {
255
+ try {
256
+ await fs.link(backupPath, filePath);
257
+ } catch (error) {
258
+ if ((error ).code !== 'EEXIST') {
259
+ throw new OperatorError('WRITE_RECOVERY_FAILED', 'Could not restore the claimed file after a failed replacement.');
260
+ }
261
+ } finally {
262
+ await fs.rm(backupPath, { force: true });
263
+ }
264
+ }
@@ -0,0 +1,415 @@
1
+ import { spawn } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { evidence } from '../core/evidence.js';
8
+ import { OperatorError } from '../core/errors.js';
9
+ import { resolveSupportedGitExecutable } from '../core/trusted-executable.js';
10
+ import { PathScope } from './path-scope.js';
11
+
12
+ const SCORE = {
13
+ reliability: 0.99,
14
+ latency: 0.92,
15
+ determinism: 0.99,
16
+ security: 0.98,
17
+ reversibility: 0.99,
18
+ informationQuality: 0.99,
19
+ interactionCost: 0.01
20
+ };
21
+
22
+ const REF_PREFIX = 'refs/operator/checkpoints/';
23
+ const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
24
+ const DISABLED_HOOKS_PATH = process.platform === 'win32' ? 'NUL' : '/dev/null';
25
+ const SAFE_GIT_PREFIX = ['--no-pager', '--no-lazy-fetch', '-c', 'core.fsmonitor=false', '-c', `core.hooksPath=${DISABLED_HOOKS_PATH}`];
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+ export class GitCheckpointProvider {
50
+ name = 'git.checkpoint.native';
51
+ #scope ;
52
+
53
+ constructor(options ) {
54
+ this.#scope = new PathScope(options.allowedRoots);
55
+ }
56
+
57
+ supports(action ) {
58
+ return ['git.checkpoint.create', 'git.checkpoint.inspect', 'git.checkpoint.restore'].includes(action.capability);
59
+ }
60
+
61
+ advertises(action ) {
62
+ if (!this.supports(action)) return false;
63
+ try { resolveSupportedGitExecutable(gitEnvironment({})); return true; } catch { return false; }
64
+ }
65
+
66
+ score() { return SCORE; }
67
+
68
+ async execute(action ) {
69
+ const started = performance.now();
70
+ try {
71
+ resolveSupportedGitExecutable(gitEnvironment({}));
72
+ if (action.capability === 'git.checkpoint.create') return await this.#create(action, started);
73
+ if (action.capability === 'git.checkpoint.inspect') return await this.#inspect(action, started);
74
+ if (action.capability === 'git.checkpoint.restore') return await this.#restore(action, started);
75
+ throw new OperatorError('UNSUPPORTED_ACTION', action.capability);
76
+ } catch (error) {
77
+ const op = error instanceof OperatorError
78
+ ? error
79
+ : new OperatorError('GIT_CHECKPOINT_ERROR', error instanceof Error ? error.message : String(error));
80
+ return {
81
+ ok: false,
82
+ capability: action.capability,
83
+ provider: this.name,
84
+ evidence: [evidence('git_checkpoint', 'fail', op.message, { code: op.code })],
85
+ error: { code: op.code, message: op.message, retryable: op.retryable },
86
+ durationMs: Math.round(performance.now() - started)
87
+ };
88
+ }
89
+ }
90
+
91
+ async #create(action , started ) {
92
+ const cwd = String(action.input.cwd ?? '');
93
+ const label = typeof action.input.label === 'string' ? action.input.label.trim().slice(0, 160) : undefined;
94
+ const checkpoint = await this.#createCheckpoint(cwd, label);
95
+ return {
96
+ ok: true,
97
+ capability: action.capability,
98
+ provider: this.name,
99
+ output: checkpoint,
100
+ evidence: [
101
+ evidence('git_checkpoint', 'pass', 'Checkpoint stored in the repository object database without moving HEAD, branch, index, or working tree.', {
102
+ checkpointId: checkpoint.id,
103
+ commit: checkpoint.commit,
104
+ head: checkpoint.head,
105
+ indexTree: checkpoint.indexTree,
106
+ worktreeTree: checkpoint.worktreeTree
107
+ }),
108
+ evidence('git_execution_boundary', 'pass', 'Repository-local clean/smudge/process filters were absent before Git materialized the checkpoint tree.'),
109
+ evidence('postcondition', 'pass', 'Repository state fingerprint is unchanged after checkpoint creation.', { fingerprint: checkpoint.fingerprint })
110
+ ],
111
+ durationMs: Math.round(performance.now() - started)
112
+ };
113
+ }
114
+
115
+ async #inspect(action , started ) {
116
+ const cwd = String(action.input.cwd ?? '');
117
+ const state = await this.#captureState(cwd);
118
+ const refs = await runGit(state.root, ['for-each-ref', '--format=%(refname)%00%(objectname)%00%(creatordate:iso-strict)', REF_PREFIX], {});
119
+ const checkpoints = refs.stdout
120
+ .split(/\r?\n/)
121
+ .filter(Boolean)
122
+ .slice(0, 200)
123
+ .flatMap((line) => {
124
+ const [ref, commit, createdAt] = line.split('\0');
125
+ if (!ref?.startsWith(REF_PREFIX) || !commit) return [];
126
+ return [{ id: ref.slice(REF_PREFIX.length), ref, commit, createdAt: createdAt ?? '' }];
127
+ });
128
+ return {
129
+ ok: true,
130
+ capability: action.capability,
131
+ provider: this.name,
132
+ output: { current: state, checkpoints },
133
+ evidence: [evidence('git_checkpoint_state', 'pass', 'Current repository snapshot fingerprint and bounded checkpoint refs inspected.', {
134
+ fingerprint: state.fingerprint,
135
+ checkpointCount: checkpoints.length
136
+ })],
137
+ durationMs: Math.round(performance.now() - started)
138
+ };
139
+ }
140
+
141
+ async #restore(action , started ) {
142
+ const cwd = String(action.input.cwd ?? '');
143
+ const checkpointId = validateCheckpointId(String(action.input.checkpointId ?? ''));
144
+ const expectedCurrentFingerprint = String(action.input.expectedCurrentFingerprint ?? '');
145
+ if (!/^[0-9a-f]{64}$/i.test(expectedCurrentFingerprint)) {
146
+ throw new OperatorError('CHECKPOINT_FINGERPRINT_REQUIRED', 'expectedCurrentFingerprint from git.checkpoint.inspect is required for restore.');
147
+ }
148
+
149
+ const current = await this.#captureState(cwd);
150
+ if (current.fingerprint !== expectedCurrentFingerprint) {
151
+ throw new OperatorError('CHECKPOINT_STATE_CHANGED', 'Repository state changed after the supplied restore precondition was captured.', {
152
+ retryable: true,
153
+ details: { expectedCurrentFingerprint, actualCurrentFingerprint: current.fingerprint }
154
+ });
155
+ }
156
+
157
+ const target = await this.#readCheckpoint(current.root, checkpointId);
158
+ if (target.head !== current.head) {
159
+ throw new OperatorError('CHECKPOINT_HEAD_CHANGED', 'Current HEAD differs from the checkpoint base HEAD; refusing to overwrite a different revision.', {
160
+ details: { checkpointHead: target.head, currentHead: current.head }
161
+ });
162
+ }
163
+
164
+ const recovery = await this.#createCheckpoint(current.root, `automatic recovery before restore ${checkpointId}`);
165
+ try {
166
+ await this.#restoreState(current.root, target);
167
+ const restored = await this.#captureState(current.root);
168
+ if (restored.head !== target.head || restored.indexTree !== target.indexTree || restored.worktreeTree !== target.worktreeTree) {
169
+ throw new OperatorError('CHECKPOINT_RESTORE_POSTCONDITION_FAILED', 'Git restore completed but repository trees do not match the checkpoint.', {
170
+ details: { expected: target, actual: restored }
171
+ });
172
+ }
173
+ return {
174
+ ok: true,
175
+ capability: action.capability,
176
+ provider: this.name,
177
+ output: {
178
+ checkpointId,
179
+ restored,
180
+ recoveryCheckpointId: recovery.id,
181
+ recoveryFingerprint: recovery.fingerprint
182
+ },
183
+ evidence: [
184
+ evidence('git_checkpoint_restore', 'pass', 'Working tree and index restored to the checkpoint while HEAD remained unchanged.', {
185
+ checkpointId,
186
+ head: restored.head,
187
+ indexTree: restored.indexTree,
188
+ worktreeTree: restored.worktreeTree
189
+ }),
190
+ evidence('recovery_checkpoint', 'pass', 'A pre-restore recovery checkpoint was retained so the restore itself can be undone.', { recoveryCheckpointId: recovery.id }),
191
+ evidence('postcondition', 'pass', 'Restored index and working-tree tree hashes exactly match the checkpoint.', { fingerprint: restored.fingerprint })
192
+ ],
193
+ durationMs: Math.round(performance.now() - started)
194
+ };
195
+ } catch (error) {
196
+ try {
197
+ await this.#restoreState(current.root, recovery);
198
+ throw new OperatorError('CHECKPOINT_RESTORE_FAILED_RECOVERED', error instanceof Error ? error.message : String(error), {
199
+ retryable: true,
200
+ details: { recoveryCheckpointId: recovery.id }
201
+ });
202
+ } catch (rollbackError) {
203
+ if (rollbackError instanceof OperatorError && rollbackError.code === 'CHECKPOINT_RESTORE_FAILED_RECOVERED') throw rollbackError;
204
+ throw new OperatorError('CHECKPOINT_RESTORE_AND_RECOVERY_FAILED', 'Checkpoint restore failed and automatic recovery could not be verified.', {
205
+ details: {
206
+ restoreError: error instanceof Error ? error.message : String(error),
207
+ recoveryError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
208
+ recoveryCheckpointId: recovery.id
209
+ }
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ async #createCheckpoint(cwd , label ) {
216
+ const before = await this.#captureState(cwd);
217
+ const id = crypto.randomUUID();
218
+ const createdAt = new Date().toISOString();
219
+ const message = [
220
+ `Operator checkpoint ${id}`,
221
+ '',
222
+ `Operator-Checkpoint-Version: 1`,
223
+ `Operator-Index-Tree: ${before.indexTree}`,
224
+ `Operator-Base-Head: ${before.head ?? 'UNBORN'}`,
225
+ `Operator-Created-At: ${createdAt}`,
226
+ ...(label ? [`Operator-Label: ${label.replace(/[\r\n]+/g, ' ')}`] : [])
227
+ ].join('\n');
228
+ const args = ['commit-tree', before.worktreeTree];
229
+ if (before.head) args.push('-p', before.head);
230
+ args.push('-m', message);
231
+ const commit = (await runGit(before.root, args, checkpointIdentityEnv())).stdout.trim();
232
+ if (!/^[0-9a-f]{40,64}$/i.test(commit)) throw new OperatorError('CHECKPOINT_COMMIT_FAILED', 'Git did not return a checkpoint commit object.');
233
+ const ref = `${REF_PREFIX}${id}`;
234
+ await runGit(before.root, ['update-ref', ref, commit], {});
235
+ const after = await this.#captureState(before.root);
236
+ if (after.fingerprint !== before.fingerprint) {
237
+ throw new OperatorError('CHECKPOINT_MUTATED_WORKTREE', 'Checkpoint creation unexpectedly changed repository state.');
238
+ }
239
+ return { ...before, id, ref, commit, createdAt, label };
240
+ }
241
+
242
+ async #captureState(cwd ) {
243
+ const requested = await this.#scope.resolveExisting(cwd);
244
+ const rootResult = await runGit(requested, ['rev-parse', '--show-toplevel'], {});
245
+ const root = await this.#scope.resolveExisting(rootResult.stdout.trim());
246
+ await assertNoRepoLocalContentFilters(root);
247
+ const headResult = await runGit(root, ['rev-parse', '--verify', 'HEAD'], {}, true);
248
+ const head = headResult.code === 0 ? headResult.stdout.trim() : undefined;
249
+ const indexTree = (await runGit(root, ['write-tree'], {})).stdout.trim();
250
+ const worktreeTree = await snapshotWorktreeTree(root, head);
251
+ const fingerprint = crypto.createHash('sha256').update(`${head ?? 'UNBORN'}\0${indexTree}\0${worktreeTree}`).digest('hex');
252
+ return { root, head, indexTree, worktreeTree, fingerprint };
253
+ }
254
+
255
+ async #readCheckpoint(root , id ) {
256
+ const ref = `${REF_PREFIX}${id}`;
257
+ const commitResult = await runGit(root, ['rev-parse', '--verify', `${ref}^{commit}`], {}, true);
258
+ if (commitResult.code !== 0) throw new OperatorError('CHECKPOINT_NOT_FOUND', `Checkpoint ${id} was not found.`);
259
+ const commit = commitResult.stdout.trim();
260
+ const raw = (await runGit(root, ['cat-file', 'commit', commit], {})).stdout;
261
+ const [headers, ...messageParts] = raw.split('\n\n');
262
+ const tree = headers.split(/\r?\n/).find((line) => line.startsWith('tree '))?.slice(5).trim();
263
+ const parent = headers.split(/\r?\n/).find((line) => line.startsWith('parent '))?.slice(7).trim();
264
+ const message = messageParts.join('\n\n');
265
+ const indexTree = trailer(message, 'Operator-Index-Tree');
266
+ const baseHead = trailer(message, 'Operator-Base-Head');
267
+ const createdAt = trailer(message, 'Operator-Created-At');
268
+ const label = trailer(message, 'Operator-Label');
269
+ if (!tree || !indexTree || !baseHead || !createdAt) throw new OperatorError('CHECKPOINT_METADATA_INVALID', 'Checkpoint metadata is incomplete.');
270
+ const head = baseHead === 'UNBORN' ? undefined : baseHead;
271
+ if ((head ?? undefined) !== (parent ?? undefined)) throw new OperatorError('CHECKPOINT_METADATA_INVALID', 'Checkpoint parent does not match recorded base HEAD.');
272
+ const fingerprint = crypto.createHash('sha256').update(`${head ?? 'UNBORN'}\0${indexTree}\0${tree}`).digest('hex');
273
+ return { root, head, indexTree, worktreeTree: tree, fingerprint, id, ref, commit, createdAt, label };
274
+ }
275
+
276
+ async #restoreState(root , state ) {
277
+ await assertNoRepoLocalContentFilters(root);
278
+ const current = await this.#captureState(root);
279
+ const diff = await runGit(root, ['diff', '--name-status', '-z', '--no-renames', '--no-ext-diff', '--no-textconv', state.worktreeTree, current.worktreeTree], {});
280
+ for (const item of parseNameStatus(diff.stdout)) {
281
+ const absolute = path.resolve(root, item.path);
282
+ const relative = path.relative(root, absolute);
283
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new OperatorError('CHECKPOINT_PATH_ESCAPE', 'Git diff returned an unsafe restore path.');
284
+ try {
285
+ const stat = await fs.lstat(absolute);
286
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
287
+ throw new OperatorError('CHECKPOINT_DIRECTORY_CONFLICT', `Refusing to recursively delete directory ${item.path} during restore.`);
288
+ }
289
+ await fs.rm(absolute, { force: true });
290
+ } catch (error) {
291
+ if ((error ).code !== 'ENOENT') throw error;
292
+ }
293
+ }
294
+ await runGit(root, ['read-tree', '--reset', '-u', state.worktreeTree], {});
295
+ await runGit(root, ['read-tree', state.indexTree], {});
296
+ }
297
+ }
298
+
299
+ function validateCheckpointId(value ) {
300
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) throw new OperatorError('INVALID_CHECKPOINT_ID', 'checkpointId is invalid.');
301
+ return value;
302
+ }
303
+
304
+ function trailer(message , key ) {
305
+ const prefix = `${key}: `;
306
+ return message.split(/\r?\n/).find((line) => line.startsWith(prefix))?.slice(prefix.length).trim() || undefined;
307
+ }
308
+
309
+ function parseNameStatus(raw ) {
310
+ const fields = raw.split('\0').filter(Boolean);
311
+ const output = [];
312
+ for (let index = 0; index + 1 < fields.length; index += 2) {
313
+ const status = fields[index] ?? '';
314
+ const file = fields[index + 1] ?? '';
315
+ if (!/^[AMD]$/.test(status) || !file) throw new OperatorError('CHECKPOINT_DIFF_INVALID', 'Unexpected Git name-status output during restore.');
316
+ output.push({ status, path: file });
317
+ }
318
+ return output;
319
+ }
320
+
321
+ async function assertNoRepoLocalContentFilters(root ) {
322
+ const configured = await runGit(root, ['config', '--name-only', '--get-regexp', '^filter\\..*\\.(clean|smudge|process)$'], {}, true);
323
+ if (configured.code === 0 && configured.stdout.trim()) {
324
+ throw new OperatorError('GIT_LOCAL_FILTER_DENIED', 'Repository-local Git clean/smudge/process filters are disabled for checkpoint operations because they can execute arbitrary commands.', {
325
+ details: { keys: configured.stdout.split(/\r?\n/).filter(Boolean).slice(0, 50) }
326
+ });
327
+ }
328
+ if (![0, 1].includes(configured.code)) {
329
+ throw new OperatorError('GIT_CONFIG_INSPECTION_FAILED', configured.stderr.trim() || 'Unable to inspect repository-local Git filter configuration.');
330
+ }
331
+ }
332
+
333
+ async function snapshotWorktreeTree(root , head ) {
334
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'operator-git-index-'));
335
+ const indexPath = path.join(tempDir, 'index');
336
+ const env = { GIT_INDEX_FILE: indexPath };
337
+ try {
338
+ if (head) await runGit(root, ['read-tree', head], env);
339
+ else await runGit(root, ['read-tree', '--empty'], env);
340
+ await runGit(root, ['add', '-A', '--', '.'], env);
341
+ return (await runGit(root, ['write-tree'], env)).stdout.trim();
342
+ } finally {
343
+ await fs.rm(tempDir, { recursive: true, force: true });
344
+ }
345
+ }
346
+
347
+ function checkpointIdentityEnv() {
348
+ return {
349
+ GIT_AUTHOR_NAME: 'Operator Checkpoint',
350
+ GIT_AUTHOR_EMAIL: 'operator@local.invalid',
351
+ GIT_COMMITTER_NAME: 'Operator Checkpoint',
352
+ GIT_COMMITTER_EMAIL: 'operator@local.invalid'
353
+ };
354
+ }
355
+
356
+ function gitEnvironment(extraEnv ) {
357
+ const nullConfig = process.platform === 'win32' ? 'NUL' : '/dev/null';
358
+ const env = {
359
+ GIT_PAGER: '',
360
+ GIT_TERMINAL_PROMPT: '0',
361
+ GIT_CONFIG_GLOBAL: nullConfig,
362
+ GIT_CONFIG_SYSTEM: nullConfig,
363
+ GIT_CONFIG_NOSYSTEM: '1',
364
+ GIT_NO_LAZY_FETCH: '1'
365
+ };
366
+ for (const key of ['PATH', 'Path', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'USER', 'USERNAME', 'LOGNAME', 'TMP', 'TEMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE']) {
367
+ if (process.env[key] !== undefined) env[key] = process.env[key];
368
+ }
369
+ for (const [key, value] of Object.entries(extraEnv)) {
370
+ if (value !== undefined) env[key] = value;
371
+ }
372
+ return env;
373
+ }
374
+
375
+ async function runGit(cwd , args , extraEnv , allowNonZero = false) {
376
+ return await new Promise((resolve, reject) => {
377
+ const safeArgs = [...SAFE_GIT_PREFIX, ...args];
378
+ const environment = gitEnvironment(extraEnv);
379
+ const gitExecutable = resolveSupportedGitExecutable(environment);
380
+ const child = spawn(gitExecutable, safeArgs, {
381
+ cwd,
382
+ shell: false,
383
+ windowsHide: true,
384
+ stdio: ['ignore', 'pipe', 'pipe'],
385
+ env: environment
386
+ });
387
+ const stdout = [];
388
+ const stderr = [];
389
+ let bytes = 0;
390
+ const capture = (bucket ) => (chunk ) => {
391
+ if (bytes >= MAX_OUTPUT_BYTES) return;
392
+ const remaining = MAX_OUTPUT_BYTES - bytes;
393
+ const sliced = chunk.subarray(0, remaining);
394
+ bucket.push(sliced);
395
+ bytes += sliced.byteLength;
396
+ };
397
+ child.stdout.on('data', capture(stdout));
398
+ child.stderr.on('data', capture(stderr));
399
+ child.once('error', reject);
400
+ const timer = setTimeout(() => {
401
+ child.kill('SIGTERM');
402
+ setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
403
+ }, 30_000);
404
+ timer.unref();
405
+ child.once('close', (code) => {
406
+ clearTimeout(timer);
407
+ const result = { code: code ?? -1, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') };
408
+ if (result.code !== 0 && !allowNonZero) {
409
+ reject(new OperatorError('GIT_COMMAND_FAILED', `git ${args[0] ?? ''} failed: ${result.stderr.trim().slice(0, 1000) || `exit ${result.code}`}`));
410
+ return;
411
+ }
412
+ resolve(result);
413
+ });
414
+ });
415
+ }