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,255 @@
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
+ import { WindowsDpapiProtector } from '../../../src/core/device-identity.js';
6
+
7
+ import { OperatorError } from '../../../src/core/errors.js';
8
+ import { readDurableStateText, writeDurableStateText } from '../../../src/core/durable-state.js';
9
+ import { requireLiteralLoopbackBindHost } from '../../../src/core/network-authority.js';
10
+
11
+ const CONFIG_VERSION = 1 ;
12
+ const MAX_CONFIG_BYTES = 256 * 1024;
13
+ const MAX_ROOTS = 32;
14
+ const DEFAULT_AGENT_PORT = 47_100;
15
+ const DEFAULT_MCP_PORT = 47_200;
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+ export class BootstrapConfigStore {
39
+ #stateDir ;
40
+ #file ;
41
+ #protector ;
42
+
43
+ constructor(stateDir , protector ) {
44
+ this.#stateDir = path.resolve(stateDir);
45
+ this.#file = path.join(this.#stateDir, 'bootstrap.json');
46
+ this.#protector = protector;
47
+ }
48
+
49
+ async configure(rootInput ) {
50
+ const root = await canonicalDirectory(rootInput);
51
+ let existing = null;
52
+ try { existing = await this.load(); }
53
+ catch (error) {
54
+ if ((error ).code !== 'ENOENT') throw error;
55
+ }
56
+
57
+ const configuredAt = existing?.configuredAt ?? new Date().toISOString();
58
+ const agentToken = existing?.agentToken ?? randomSecret();
59
+ const recoveryToken = existing?.recoveryToken ?? randomSecret();
60
+ const protectedBytes = await this.#protectSecrets(agentToken, recoveryToken);
61
+ const stored = {
62
+ version: CONFIG_VERSION,
63
+ configuredAt,
64
+ allowedRoots: [root],
65
+ agent: { host: '127.0.0.1', port: existing?.agentPort ?? DEFAULT_AGENT_PORT },
66
+ mcp: { host: '127.0.0.1', port: existing?.mcpPort ?? DEFAULT_MCP_PORT },
67
+ secretProtection: { scheme: this.#protector.scheme, ciphertextBase64: protectedBytes.toString('base64') }
68
+ };
69
+
70
+ try {
71
+ await writeDurableStateText(this.#file, JSON.stringify(stored, null, 2), {
72
+ maxBytes: MAX_CONFIG_BYTES,
73
+ errorCode: 'BOOTSTRAP_CONFIG_INVALID',
74
+ invalidMessage: 'Operator bootstrap configuration is invalid.'
75
+ });
76
+ } finally {
77
+ protectedBytes.fill(0);
78
+ }
79
+ return storedToResolved(this.#stateDir, stored, agentToken, recoveryToken);
80
+ }
81
+
82
+ async load() {
83
+ const text = await readDurableStateText(this.#file, {
84
+ maxBytes: MAX_CONFIG_BYTES,
85
+ errorCode: 'BOOTSTRAP_CONFIG_INVALID',
86
+ invalidMessage: 'Operator bootstrap configuration is invalid.'
87
+ });
88
+ let parsed ;
89
+ try { parsed = JSON.parse(text); }
90
+ catch { throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Operator bootstrap configuration is not valid JSON.'); }
91
+ const stored = validateStored(parsed);
92
+ const ciphertext = Buffer.from(stored.secretProtection.ciphertextBase64, 'base64');
93
+ let plaintext ;
94
+ try {
95
+ plaintext = await this.#protector.unprotect(ciphertext);
96
+ const secrets = validateSecrets(JSON.parse(plaintext.toString('utf8')));
97
+ return storedToResolved(this.#stateDir, stored, secrets.agentToken, secrets.recoveryToken);
98
+ } finally {
99
+ ciphertext.fill(0);
100
+ plaintext?.fill(0);
101
+ }
102
+ }
103
+
104
+ async #protectSecrets(agentToken , recoveryToken ) {
105
+ const plaintext = Buffer.from(JSON.stringify({ agentToken, recoveryToken }), 'utf8');
106
+ try { return await this.#protector.protect(plaintext); }
107
+ finally { plaintext.fill(0); }
108
+ }
109
+ }
110
+
111
+ export function defaultOperatorStateDir() {
112
+ return path.resolve(process.env.OPERATOR_STATE_DIR ?? path.join(os.homedir(), '.operator'));
113
+ }
114
+
115
+ export function windowsBootstrapProtector() {
116
+ if (process.platform !== 'win32') {
117
+ throw new OperatorError('BOOTSTRAP_WINDOWS_REQUIRED', 'One-command protected setup is currently supported by the Windows release.');
118
+ }
119
+ const helper = process.env.OPERATOR_WINDOWS_DPAPI_PATH;
120
+ if (!helper || !path.isAbsolute(helper)) {
121
+ throw new OperatorError('WINDOWS_DPAPI_HELPER_REQUIRED', 'Packaged Windows DPAPI helper is required for Operator setup.');
122
+ }
123
+ return new WindowsDpapiProtector(helper);
124
+ }
125
+
126
+ export function applyBootstrapEnvironment(config ) {
127
+ process.env.OPERATOR_STATE_DIR = config.stateDir;
128
+ process.env.OPERATOR_ALLOWED_ROOTS = config.allowedRoots.join(path.delimiter);
129
+ process.env.OPERATOR_AGENT_TOKEN = config.agentToken;
130
+ process.env.OPERATOR_RECOVERY_TOKEN = config.recoveryToken;
131
+ process.env.OPERATOR_AGENT_HOST = config.agentHost;
132
+ process.env.OPERATOR_AGENT_PORT = String(config.agentPort);
133
+ process.env.OPERATOR_AGENT_URL = `http://${formatHost(config.agentHost)}:${config.agentPort}`;
134
+ process.env.OPERATOR_MCP_HOST = config.mcpHost;
135
+ process.env.OPERATOR_MCP_PORT = String(config.mcpPort);
136
+ }
137
+
138
+ async function canonicalDirectory(input ) {
139
+ const resolved = path.resolve(input);
140
+ let stat;
141
+ try { stat = await fs.stat(resolved); }
142
+ catch { throw new OperatorError('BOOTSTRAP_ROOT_INVALID', `Authorized root does not exist: ${resolved}`); }
143
+ if (!stat.isDirectory()) throw new OperatorError('BOOTSTRAP_ROOT_INVALID', `Authorized root is not a directory: ${resolved}`);
144
+ return await fs.realpath(resolved);
145
+ }
146
+
147
+ function randomSecret() {
148
+ return crypto.randomBytes(32).toString('base64url');
149
+ }
150
+
151
+ function validPort(input , label ) {
152
+ const value = Number(input);
153
+ if (!Number.isSafeInteger(value) || value < 1_024 || value > 65_535) {
154
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', `${label} must be an integer port from 1024 through 65535.`);
155
+ }
156
+ return value;
157
+ }
158
+
159
+ function validConfiguredAt(input ) {
160
+ const value = String(input ?? '');
161
+ const time = Date.parse(value);
162
+ if (!Number.isFinite(time) || new Date(time).toISOString() !== value) {
163
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Bootstrap configuredAt must be an ISO timestamp.');
164
+ }
165
+ return value;
166
+ }
167
+
168
+ function validateStored(input ) {
169
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
170
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Bootstrap configuration must be a JSON object.');
171
+ }
172
+ const value = input ;
173
+ if (value.version !== CONFIG_VERSION) throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Unsupported bootstrap configuration version.');
174
+ if (!Array.isArray(value.allowedRoots) || value.allowedRoots.length < 1 || value.allowedRoots.length > MAX_ROOTS) {
175
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', `Bootstrap configuration must contain 1-${MAX_ROOTS} authorized roots.`);
176
+ }
177
+ const allowedRoots = value.allowedRoots.map((root) => {
178
+ const candidate = String(root ?? '');
179
+ if (!candidate || !path.isAbsolute(candidate)) throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Authorized roots must be absolute paths.');
180
+ return path.resolve(candidate);
181
+ });
182
+ if (new Set(allowedRoots.map((root) => process.platform === 'win32' ? root.toLowerCase() : root)).size !== allowedRoots.length) {
183
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Authorized roots must be unique.');
184
+ }
185
+ const agent = validEndpoint(value.agent, 'Local agent');
186
+ const mcp = validEndpoint(value.mcp, 'MCP server');
187
+ const protection = value.secretProtection ;
188
+ if (!protection || protection.scheme !== 'windows-dpapi-current-user') {
189
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Bootstrap secrets must use Windows DPAPI CurrentUser protection.');
190
+ }
191
+ const ciphertextBase64 = String(protection.ciphertextBase64 ?? '');
192
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(ciphertextBase64) || ciphertextBase64.length > 128 * 1024) {
193
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Bootstrap secret ciphertext is invalid.');
194
+ }
195
+ return {
196
+ version: CONFIG_VERSION,
197
+ configuredAt: validConfiguredAt(value.configuredAt),
198
+ allowedRoots,
199
+ agent,
200
+ mcp,
201
+ secretProtection: { scheme: 'windows-dpapi-current-user', ciphertextBase64 }
202
+ };
203
+ }
204
+
205
+ function validEndpoint(input , label ) {
206
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
207
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', `${label} endpoint is invalid.`);
208
+ }
209
+ const value = input ;
210
+ return {
211
+ host: requireLiteralLoopbackBindHost(String(value.host ?? ''), label),
212
+ port: validPort(value.port, `${label} port`)
213
+ };
214
+ }
215
+
216
+ function validateSecrets(input ) {
217
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
218
+ throw new OperatorError('BOOTSTRAP_SECRET_INVALID', 'Protected bootstrap secret payload is invalid.');
219
+ }
220
+ const value = input ;
221
+ const agentToken = validSecret(value.agentToken, 'agent token');
222
+ const recoveryToken = validSecret(value.recoveryToken, 'recovery token');
223
+ return { agentToken, recoveryToken };
224
+ }
225
+
226
+ function validSecret(input , label ) {
227
+ const value = String(input ?? '');
228
+ if (value.length < 32 || Buffer.byteLength(value, 'utf8') > 4096) {
229
+ throw new OperatorError('BOOTSTRAP_SECRET_INVALID', `Protected ${label} is invalid.`);
230
+ }
231
+ return value;
232
+ }
233
+
234
+ function storedToResolved(
235
+ stateDir ,
236
+ stored ,
237
+ agentToken ,
238
+ recoveryToken
239
+ ) {
240
+ return {
241
+ stateDir,
242
+ configuredAt: stored.configuredAt,
243
+ allowedRoots: [...stored.allowedRoots],
244
+ agentHost: stored.agent.host,
245
+ agentPort: stored.agent.port,
246
+ mcpHost: stored.mcp.host,
247
+ mcpPort: stored.mcp.port,
248
+ agentToken,
249
+ recoveryToken
250
+ };
251
+ }
252
+
253
+ function formatHost(host ) {
254
+ return host.includes(':') ? `[${host}]` : host;
255
+ }
@@ -0,0 +1,237 @@
1
+ import { spawn } from 'node:child_process';
2
+ import http from 'node:http';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { DeviceIdentityStore } from '../../../src/core/device-identity.js';
6
+ import { OperatorError } from '../../../src/core/errors.js';
7
+ import { safeChildEnvironment } from '../../../src/core/child-environment.js';
8
+ import { PRODUCT_NAME } from '../../../src/core/product-identity.js';
9
+ import {
10
+ BootstrapConfigStore,
11
+ applyBootstrapEnvironment,
12
+ defaultOperatorStateDir,
13
+ windowsBootstrapProtector
14
+ } from './bootstrap-config.js';
15
+
16
+ const args = process.argv.slice(2);
17
+ const command = args[0] ?? 'run';
18
+
19
+ try {
20
+ if (command === 'setup') await setup(args.slice(1));
21
+ else if (command === 'verify') await verify(args.slice(1));
22
+ else if (command === 'run' && args.length === 1) await run();
23
+ else if (command === '--help' || command === '-h' || command === 'help') printHelp();
24
+ else if (args.length === 0) await run();
25
+ else throw new OperatorError('CLI_ARGUMENT_INVALID', `Unknown Operator command: ${command}`);
26
+ } catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ console.error(`[operator] ${message}`);
29
+ process.exitCode = 1;
30
+ }
31
+
32
+ async function setup(rest ) {
33
+ const { root, startRuntime } = parseSetupArgs(rest);
34
+ const stateDir = defaultOperatorStateDir();
35
+ const store = new BootstrapConfigStore(stateDir, windowsBootstrapProtector());
36
+ const config = await store.configure(root);
37
+ applyBootstrapEnvironment(config);
38
+
39
+ const identity = await new DeviceIdentityStore(stateDir).loadOrCreate();
40
+ await verifyConfiguredState(stateDir, config);
41
+ if (startRuntime) await ensureRuntimeReady(config);
42
+ console.log('Operator setup: PASS');
43
+ console.log(`Authorized root: ${config.allowedRoots[0]}`);
44
+ console.log(`Device: ${identity.deviceName} (${identity.deviceId})`);
45
+ console.log('Secrets: protected with Windows DPAPI CurrentUser');
46
+ console.log('Verification: PASS');
47
+ console.log(`Runtime: ${startRuntime ? 'running' : 'not started (--no-start)'}`);
48
+ console.log(startRuntime ? 'Ready: connect Operator in ChatGPT.' : 'Ready: run operator when you want to start the runtime.');
49
+ }
50
+
51
+ async function verify(rest ) {
52
+ if (rest.length > 0) throw new OperatorError('CLI_ARGUMENT_INVALID', 'Operator verify does not accept arguments.');
53
+ const stateDir = defaultOperatorStateDir();
54
+ const store = new BootstrapConfigStore(stateDir, windowsBootstrapProtector());
55
+ const config = await store.load();
56
+ applyBootstrapEnvironment(config);
57
+
58
+ await verifyConfiguredState(stateDir, config);
59
+
60
+ console.log('Operator verify: PASS');
61
+ console.log(`Authorized roots: ${config.allowedRoots.length}`);
62
+ console.log(`Local agent: ${config.agentHost}:${config.agentPort}`);
63
+ console.log(`Local MCP: ${config.mcpHost}:${config.mcpPort}/mcp`);
64
+ console.log('Device identity: unlocked and signature verified');
65
+ console.log('Secrets: DPAPI protected; no plaintext bootstrap secrets detected');
66
+ console.log('Ready: run operator');
67
+ }
68
+
69
+ async function run() {
70
+ const stateDir = defaultOperatorStateDir();
71
+ const store = new BootstrapConfigStore(stateDir, windowsBootstrapProtector());
72
+ let config;
73
+ try { config = await store.load(); }
74
+ catch (error) {
75
+ if ((error ).code === 'ENOENT') {
76
+ throw new OperatorError('BOOTSTRAP_REQUIRED', 'Operator is not configured. Run "operator setup" once.');
77
+ }
78
+ throw error;
79
+ }
80
+ applyBootstrapEnvironment(config);
81
+ await import('./main.js');
82
+ await import('../../mcp-server/src/server.js');
83
+ }
84
+
85
+ function parseSetupArgs(rest ) {
86
+ let root = process.env.OPERATOR_INVOKE_CWD ?? process.cwd();
87
+ let startRuntime = true;
88
+ for (let index = 0; index < rest.length; index += 1) {
89
+ const arg = rest[index];
90
+ if (arg === '--no-start') { startRuntime = false; continue; }
91
+ if (arg === '--root' && rest[index + 1]) { root = rest[index + 1] ; index += 1; continue; }
92
+ throw new OperatorError('CLI_ARGUMENT_INVALID', 'Usage: operator setup [--root <authorized-folder>] [--no-start]');
93
+ }
94
+ return { root, startRuntime };
95
+ }
96
+
97
+ async function ensureRuntimeReady(config ) {
98
+ if (await runtimeHealthy(config)) return;
99
+ const dpapi = process.env.OPERATOR_WINDOWS_DPAPI_PATH;
100
+ const uia = process.env.OPERATOR_WINDOWS_UIA_PATH;
101
+ const pathLease = process.env.OPERATOR_WINDOWS_PATH_LEASE_PATH;
102
+ if (
103
+ !dpapi || !path.isAbsolute(dpapi)
104
+ || !uia || !path.isAbsolute(uia)
105
+ || !pathLease || !path.isAbsolute(pathLease)
106
+ ) {
107
+ throw new OperatorError('BOOTSTRAP_RUNTIME_START_FAILED', 'Packaged native helpers are required to start Operator automatically.');
108
+ }
109
+ const child = spawn(process.execPath, ['--experimental-strip-types', import.meta.filename, 'run'], {
110
+ cwd: config.allowedRoots[0],
111
+ detached: true,
112
+ env: {
113
+ ...safeChildEnvironment('desktop'),
114
+ OPERATOR_STATE_DIR: config.stateDir,
115
+ OPERATOR_WINDOWS_DPAPI_PATH: dpapi,
116
+ OPERATOR_WINDOWS_UIA_PATH: uia,
117
+ OPERATOR_WINDOWS_PATH_LEASE_PATH: pathLease
118
+ },
119
+ shell: false,
120
+ stdio: 'ignore',
121
+ windowsHide: true
122
+ });
123
+ child.unref();
124
+ for (let attempt = 0; attempt < 40; attempt += 1) {
125
+ await new Promise((resolve) => setTimeout(resolve, 250));
126
+ if (await runtimeHealthy(config)) return;
127
+ }
128
+ throw new OperatorError('BOOTSTRAP_RUNTIME_START_FAILED', 'Operator runtime did not become healthy after setup. Run "operator verify" for diagnostics.');
129
+ }
130
+
131
+ async function runtimeHealthy(config ) {
132
+ const agentBase = `http://${formatHost(config.agentHost)}:${config.agentPort}`;
133
+ const mcpBase = `http://${formatHost(config.mcpHost)}:${config.mcpPort}`;
134
+ try {
135
+ const agent = await readHealthJson(`${agentBase}/v1/settings`, {
136
+ authorization: `Bearer ${config.agentToken}`
137
+ });
138
+ if (agent.status !== 200 || (agent.body ).ok !== true) return false;
139
+ const mcp = await readHealthJson(`${mcpBase}/health`);
140
+ const body = mcp.body ;
141
+ return mcp.status === 200 && body.ok === true && body.service === PRODUCT_NAME;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ async function readHealthJson(url , headers = {}) {
148
+ return await new Promise((resolve, reject) => {
149
+ const req = http.get(url, {
150
+ agent: false,
151
+ headers: { ...headers, connection: 'close' }
152
+ }, (res) => {
153
+ const chunks = [];
154
+ let bytes = 0;
155
+ res.on('data', (chunk ) => {
156
+ bytes += chunk.byteLength;
157
+ if (bytes > 16 * 1024) {
158
+ req.destroy(new Error('Operator readiness response exceeded 16 KiB.'));
159
+ return;
160
+ }
161
+ chunks.push(chunk);
162
+ });
163
+ res.on('end', () => {
164
+ try {
165
+ resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) });
166
+ } catch (error) {
167
+ reject(error);
168
+ }
169
+ });
170
+ });
171
+ req.setTimeout(750, () => req.destroy(new Error('Operator readiness probe timed out.')));
172
+ req.once('error', reject);
173
+ });
174
+ }
175
+
176
+ function formatHost(host ) {
177
+ return host.includes(':') ? `[${host}]` : host;
178
+ }
179
+
180
+ async function verifyConfiguredState(stateDir , config ) {
181
+ for (const root of config.allowedRoots) {
182
+ const stat = await fs.stat(root);
183
+ if (!stat.isDirectory()) throw new OperatorError('BOOTSTRAP_ROOT_INVALID', `Authorized root is no longer a directory: ${root}`);
184
+ }
185
+ if (config.agentPort === config.mcpPort) {
186
+ throw new OperatorError('BOOTSTRAP_CONFIG_INVALID', 'Local agent and MCP server ports must be distinct.');
187
+ }
188
+ await verifyDeviceIdentity(stateDir);
189
+ await verifyMcpRuntime();
190
+ await verifySecretsAreNotPlaintext(stateDir, config.agentToken, config.recoveryToken);
191
+ }
192
+
193
+ async function verifyDeviceIdentity(stateDir ) {
194
+ const store = new DeviceIdentityStore(stateDir);
195
+ const existing = await store.loadExisting();
196
+ if (!existing) throw new OperatorError('DEVICE_IDENTITY_MISSING', 'Operator device identity is absent. Run Operator to begin fresh device enrollment.');
197
+ const probe = Buffer.from('operator-one-command-verify-v1', 'utf8');
198
+ const signature = await store.sign(probe);
199
+ if (!(await store.verify(probe, signature))) {
200
+ throw new OperatorError('DEVICE_IDENTITY_VERIFY_FAILED', 'Device identity signature round trip failed.');
201
+ }
202
+ }
203
+
204
+ async function verifyMcpRuntime() {
205
+ const root = path.resolve(import.meta.dirname, '../../mcp-server');
206
+ const required = [
207
+ 'src/server.js',
208
+ 'src/local-agent-client.js',
209
+ 'node_modules/fastify/package.json',
210
+ 'node_modules/zod/package.json',
211
+ 'node_modules/@modelcontextprotocol/server/package.json',
212
+ 'node_modules/@modelcontextprotocol/fastify/package.json'
213
+ ];
214
+ for (const relative of required) {
215
+ try { await fs.access(path.join(root, relative)); }
216
+ catch { throw new OperatorError('MCP_RUNTIME_MISSING', `Packaged MCP runtime is incomplete: ${relative}`); }
217
+ }
218
+ }
219
+
220
+ async function verifySecretsAreNotPlaintext(stateDir , ...secrets ) {
221
+ const text = await fs.readFile(path.join(stateDir, 'bootstrap.json'), 'utf8');
222
+ for (const secret of secrets) {
223
+ if (text.includes(secret)) throw new OperatorError('BOOTSTRAP_SECRET_EXPOSED', 'Bootstrap configuration contains a plaintext secret.');
224
+ }
225
+ }
226
+
227
+ function printHelp() {
228
+ console.log([
229
+ 'Operator',
230
+ '',
231
+ ' operator setup [--root <folder>] Configure, verify, and start Operator once.',
232
+ ' operator verify Re-run protected config/runtime verification.',
233
+ ' operator Start the local agent and MCP server.',
234
+ '',
235
+ 'Setup never prints or stores plaintext bearer/recovery secrets.'
236
+ ].join('\n'));
237
+ }