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.
- package/DISCLOSURE +47 -0
- package/LICENSE +64 -0
- package/README.md +44 -0
- package/bin/operator.mjs +10 -0
- package/package.json +58 -0
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
- package/runtime/app/apps/local-agent/src/cli.js +237 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
- package/runtime/app/apps/local-agent/src/main.js +311 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
- package/runtime/app/apps/local-agent/src/remote.js +130 -0
- package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
- package/runtime/app/apps/local-agent/src/server.js +578 -0
- package/runtime/app/package.json +4 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
- package/runtime/app/src/capabilities/browser-cdp.js +384 -0
- package/runtime/app/src/capabilities/browser-managed.js +345 -0
- package/runtime/app/src/capabilities/docker.js +448 -0
- package/runtime/app/src/capabilities/filesystem.js +264 -0
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
- package/runtime/app/src/capabilities/git-write.js +321 -0
- package/runtime/app/src/capabilities/git.js +252 -0
- package/runtime/app/src/capabilities/index.js +16 -0
- package/runtime/app/src/capabilities/path-scope.js +86 -0
- package/runtime/app/src/capabilities/postgres.js +566 -0
- package/runtime/app/src/capabilities/process.js +258 -0
- package/runtime/app/src/capabilities/project-command.js +599 -0
- package/runtime/app/src/capabilities/project-transaction.js +215 -0
- package/runtime/app/src/capabilities/project.js +67 -0
- package/runtime/app/src/capabilities/system.js +40 -0
- package/runtime/app/src/capabilities/vscode.js +312 -0
- package/runtime/app/src/capabilities/windows-uia.js +331 -0
- package/runtime/app/src/core/account-device-registry.js +604 -0
- package/runtime/app/src/core/action-identity.js +44 -0
- package/runtime/app/src/core/audit.js +295 -0
- package/runtime/app/src/core/capability-policy.js +58 -0
- package/runtime/app/src/core/child-environment.js +29 -0
- package/runtime/app/src/core/device-enrollment.js +417 -0
- package/runtime/app/src/core/device-identity.js +436 -0
- package/runtime/app/src/core/device-registry.js +477 -0
- package/runtime/app/src/core/device-reset.js +178 -0
- package/runtime/app/src/core/device-routing.js +305 -0
- package/runtime/app/src/core/durable-state.js +306 -0
- package/runtime/app/src/core/errors.js +20 -0
- package/runtime/app/src/core/evidence.js +9 -0
- package/runtime/app/src/core/index.js +17 -0
- package/runtime/app/src/core/machine-state.js +160 -0
- package/runtime/app/src/core/network-authority.js +20 -0
- package/runtime/app/src/core/policy.js +67 -0
- package/runtime/app/src/core/product-identity.js +7 -0
- package/runtime/app/src/core/provenance.js +18 -0
- package/runtime/app/src/core/provider-learning.js +274 -0
- package/runtime/app/src/core/public-plugin-surface.js +25 -0
- package/runtime/app/src/core/public-restricted-data.js +105 -0
- package/runtime/app/src/core/rate-limit.js +118 -0
- package/runtime/app/src/core/relay-client.js +568 -0
- package/runtime/app/src/core/relay-delivery-store.js +512 -0
- package/runtime/app/src/core/relay-result-store.js +321 -0
- package/runtime/app/src/core/release-update.js +253 -0
- package/runtime/app/src/core/router.js +96 -0
- package/runtime/app/src/core/runtime.js +136 -0
- package/runtime/app/src/core/scoped-path-syntax.js +16 -0
- package/runtime/app/src/core/session-token.js +498 -0
- package/runtime/app/src/core/task-orchestrator.js +1148 -0
- package/runtime/app/src/core/task-store.js +517 -0
- package/runtime/app/src/core/task.js +142 -0
- package/runtime/app/src/core/trusted-executable.js +124 -0
- package/runtime/app/src/core/types.js +83 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +380 -0
- package/src/cli.mjs +345 -0
- package/src/publish-check.mjs +22 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
import { evidence } from '../core/evidence.js';
|
|
4
|
+
import { OperatorError } from '../core/errors.js';
|
|
5
|
+
import { resolveTrustedExecutable } from '../core/trusted-executable.js';
|
|
6
|
+
import { PathScope } from './path-scope.js';
|
|
7
|
+
|
|
8
|
+
const SCORE = {
|
|
9
|
+
reliability: 0.95,
|
|
10
|
+
latency: 0.95,
|
|
11
|
+
determinism: 0.94,
|
|
12
|
+
security: 0.9,
|
|
13
|
+
reversibility: 0.55,
|
|
14
|
+
informationQuality: 0.98,
|
|
15
|
+
interactionCost: 0.02
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
|
+
const MAX_TIMEOUT_MS = 10 * 60_000;
|
|
20
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
21
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const MAX_ARGS = 200;
|
|
23
|
+
const MAX_ARG_BYTES = 16 * 1024;
|
|
24
|
+
|
|
25
|
+
// Child processes receive only environment needed for ordinary executable lookup,
|
|
26
|
+
// user directories, temporary files and locale handling. Ambient credentials,
|
|
27
|
+
// runtime injection flags (NODE_OPTIONS, PYTHONPATH, GIT_*), proxies and all
|
|
28
|
+
// OPERATOR_* authority remain in the local-agent process only.
|
|
29
|
+
const SAFE_ENV_KEYS = new Set([
|
|
30
|
+
'PATH', 'PATHEXT',
|
|
31
|
+
'SYSTEMROOT', 'WINDIR', 'COMSPEC',
|
|
32
|
+
'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH',
|
|
33
|
+
'TMP', 'TEMP', 'TMPDIR',
|
|
34
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'COLORTERM',
|
|
35
|
+
'PROGRAMFILES', 'PROGRAMFILES(X86)', 'PROGRAMW6432', 'PROGRAMDATA',
|
|
36
|
+
'LOCALAPPDATA', 'APPDATA'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export class ProcessProvider {
|
|
40
|
+
name = 'process.argv';
|
|
41
|
+
#scope ;
|
|
42
|
+
#allowedExecutables ;
|
|
43
|
+
#maxOutputBytes ;
|
|
44
|
+
#requiredRisk ;
|
|
45
|
+
#environmentOverrides ;
|
|
46
|
+
|
|
47
|
+
constructor(options
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
) {
|
|
54
|
+
this.#scope = new PathScope(options.allowedRoots);
|
|
55
|
+
this.#allowedExecutables = new Set(options.allowedExecutables.map((item) => item.trim().toLowerCase()).filter(Boolean));
|
|
56
|
+
this.#maxOutputBytes = boundedInteger(options.maxOutputBytes, DEFAULT_MAX_OUTPUT_BYTES, 1024, MAX_OUTPUT_BYTES);
|
|
57
|
+
this.#requiredRisk = options.requiredRisk;
|
|
58
|
+
this.#environmentOverrides = validateEnvironmentOverrides(options.environmentOverrides);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
supports(action ) { return action.capability === 'terminal.execute'; }
|
|
62
|
+
score() { return SCORE; }
|
|
63
|
+
|
|
64
|
+
async execute(action , context = {}) {
|
|
65
|
+
const started = performance.now();
|
|
66
|
+
const executable = String(action.input.executable ?? '').trim();
|
|
67
|
+
const args = Array.isArray(action.input.args) ? action.input.args.map(String) : [];
|
|
68
|
+
const timeoutMs = boundedInteger(action.input.timeoutMs, DEFAULT_TIMEOUT_MS, 100, MAX_TIMEOUT_MS);
|
|
69
|
+
|
|
70
|
+
if (this.#requiredRisk && action.risk !== this.#requiredRisk) {
|
|
71
|
+
return failure(action, this.name, started, 'PROCESS_RISK_MISMATCH', `Process execution requires ${this.#requiredRisk} risk classification.`);
|
|
72
|
+
}
|
|
73
|
+
if (!executable || executable.includes('\0')) {
|
|
74
|
+
return failure(action, this.name, started, 'PROCESS_INPUT_INVALID', 'Executable must be a non-empty string without NUL bytes.');
|
|
75
|
+
}
|
|
76
|
+
if (args.length > MAX_ARGS || args.some((arg) => arg.includes('\0') || Buffer.byteLength(arg, 'utf8') > MAX_ARG_BYTES)) {
|
|
77
|
+
return failure(action, this.name, started, 'PROCESS_INPUT_INVALID', `Process arguments are limited to ${MAX_ARGS} entries and ${MAX_ARG_BYTES} UTF-8 bytes each.`);
|
|
78
|
+
}
|
|
79
|
+
if (!this.#allowedExecutables.has(executable.toLowerCase())) {
|
|
80
|
+
return failure(action, this.name, started, 'EXECUTABLE_DENIED', `Executable ${executable} is not allowlisted.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const cwd = await this.#scope.resolveExisting(String(action.input.cwd ?? ''));
|
|
85
|
+
const output = await runProcess(executable, args, cwd, timeoutMs, this.#maxOutputBytes, this.#environmentOverrides, context.signal);
|
|
86
|
+
const ok = output.exitCode === 0;
|
|
87
|
+
return {
|
|
88
|
+
ok,
|
|
89
|
+
capability: action.capability,
|
|
90
|
+
provider: this.name,
|
|
91
|
+
output,
|
|
92
|
+
evidence: [
|
|
93
|
+
evidence('process_exit', ok ? 'pass' : 'fail', `Process exited with code ${output.exitCode}.`, { exitCode: output.exitCode, signal: output.signal }),
|
|
94
|
+
evidence('process_invocation', 'info', 'Process executed shell-free with a bounded, credential-scrubbed environment.', {
|
|
95
|
+
executable,
|
|
96
|
+
argCount: args.length,
|
|
97
|
+
cwd,
|
|
98
|
+
timeoutMs,
|
|
99
|
+
outputLimitBytes: this.#maxOutputBytes
|
|
100
|
+
})
|
|
101
|
+
],
|
|
102
|
+
error: ok ? undefined : { code: 'NONZERO_EXIT', message: `Process exited with code ${output.exitCode}.`, retryable: false },
|
|
103
|
+
durationMs: Math.round(performance.now() - started)
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const op = error instanceof OperatorError
|
|
107
|
+
? error
|
|
108
|
+
: new OperatorError('PROCESS_ERROR', error instanceof Error ? error.message : String(error), { retryable: true });
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
capability: action.capability,
|
|
112
|
+
provider: this.name,
|
|
113
|
+
evidence: [evidence('process', 'fail', op.message, { code: op.code })],
|
|
114
|
+
error: { code: op.code, message: op.message, retryable: op.retryable },
|
|
115
|
+
durationMs: Math.round(performance.now() - started)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function failure(action , provider , started , code , message ) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
capability: action.capability,
|
|
125
|
+
provider,
|
|
126
|
+
evidence: [evidence('process_policy', 'fail', message, { code })],
|
|
127
|
+
error: { code, message, retryable: false },
|
|
128
|
+
durationMs: Math.round(performance.now() - started)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function boundedInteger(value , fallback , min , max ) {
|
|
133
|
+
const parsed = Number(value ?? fallback);
|
|
134
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
135
|
+
return Math.min(Math.max(Math.trunc(parsed), min), max);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function safeChildEnvironment(source = process.env, overrides = {}) {
|
|
139
|
+
const safe = {};
|
|
140
|
+
for (const [key, value] of Object.entries(source)) {
|
|
141
|
+
if (value === undefined) continue;
|
|
142
|
+
if (!SAFE_ENV_KEYS.has(key.toUpperCase())) continue;
|
|
143
|
+
safe[key] = value;
|
|
144
|
+
}
|
|
145
|
+
for (const [key, value] of Object.entries(overrides)) safe[key] = value;
|
|
146
|
+
return safe;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateEnvironmentOverrides(value ) {
|
|
150
|
+
const output = {};
|
|
151
|
+
for (const [key, item] of Object.entries(value ?? {})) {
|
|
152
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof item !== 'string' || item.includes('\0')) {
|
|
153
|
+
throw new OperatorError('PROCESS_ENV_OVERRIDE_INVALID', 'Trusted process environment override is invalid.');
|
|
154
|
+
}
|
|
155
|
+
output[key] = item;
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze(output);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runProcess(executable , args , cwd , timeoutMs , maxOutputBytes , environmentOverrides , signal )
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
return await new Promise((resolve, reject) => {
|
|
168
|
+
if (signal?.aborted) {
|
|
169
|
+
reject(new OperatorError('EXECUTION_ABORTED', 'Process execution was cancelled.', { retryable: false }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const childEnvironment = safeChildEnvironment(process.env, environmentOverrides);
|
|
173
|
+
const trustedExecutable = resolveTrustedExecutable(executable, childEnvironment);
|
|
174
|
+
const child = spawn(trustedExecutable, args, {
|
|
175
|
+
cwd,
|
|
176
|
+
shell: false,
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
179
|
+
env: childEnvironment
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const stdout = [];
|
|
183
|
+
const stderr = [];
|
|
184
|
+
let bytes = 0;
|
|
185
|
+
let truncated = false;
|
|
186
|
+
let timedOut = false;
|
|
187
|
+
let aborted = false;
|
|
188
|
+
let settled = false;
|
|
189
|
+
let forceKillTimer ;
|
|
190
|
+
|
|
191
|
+
const clearTimers = () => {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
194
|
+
signal?.removeEventListener('abort', onAbort);
|
|
195
|
+
};
|
|
196
|
+
const rejectOnce = (error ) => {
|
|
197
|
+
if (settled) return;
|
|
198
|
+
settled = true;
|
|
199
|
+
clearTimers();
|
|
200
|
+
reject(error);
|
|
201
|
+
};
|
|
202
|
+
const capture = (bucket ) => (chunk ) => {
|
|
203
|
+
if (bytes >= maxOutputBytes) { truncated = true; return; }
|
|
204
|
+
const remaining = maxOutputBytes - bytes;
|
|
205
|
+
const sliced = chunk.subarray(0, remaining);
|
|
206
|
+
bucket.push(sliced);
|
|
207
|
+
bytes += sliced.byteLength;
|
|
208
|
+
if (sliced.byteLength < chunk.byteLength) truncated = true;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
child.stdout.on('data', capture(stdout));
|
|
212
|
+
child.stderr.on('data', capture(stderr));
|
|
213
|
+
child.once('error', rejectOnce);
|
|
214
|
+
|
|
215
|
+
const terminateChild = () => {
|
|
216
|
+
try { child.kill('SIGTERM'); } catch { /* close/error path reports the outcome */ }
|
|
217
|
+
if (!forceKillTimer) {
|
|
218
|
+
forceKillTimer = setTimeout(() => {
|
|
219
|
+
try { child.kill('SIGKILL'); } catch { /* process may already be gone */ }
|
|
220
|
+
}, 1000);
|
|
221
|
+
forceKillTimer.unref();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const onAbort = () => {
|
|
225
|
+
aborted = true;
|
|
226
|
+
terminateChild();
|
|
227
|
+
};
|
|
228
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
229
|
+
if (signal?.aborted) onAbort();
|
|
230
|
+
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
timedOut = true;
|
|
233
|
+
terminateChild();
|
|
234
|
+
}, timeoutMs);
|
|
235
|
+
timer.unref();
|
|
236
|
+
|
|
237
|
+
child.once('close', (exitCode, closeSignal) => {
|
|
238
|
+
if (settled) return;
|
|
239
|
+
settled = true;
|
|
240
|
+
clearTimers();
|
|
241
|
+
if (aborted) {
|
|
242
|
+
reject(new OperatorError('EXECUTION_ABORTED', 'Process execution was cancelled.', { retryable: false }));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (timedOut) {
|
|
246
|
+
reject(new OperatorError('PROCESS_TIMEOUT', `Process exceeded ${timeoutMs}ms timeout.`, { retryable: true }));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
resolve({
|
|
250
|
+
exitCode,
|
|
251
|
+
signal: closeSignal,
|
|
252
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
253
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
254
|
+
truncated
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|