open-managed-agents 0.1.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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, open, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join as joinHostPath, posix } from "node:path";
|
|
6
|
+
import { CMA_GLOB_READY_MARKER, CmaGlobReadinessFilter, CmaGlobStreamCollector, cmaGlobToRipgrepGlob, compileCmaGlob, } from "./cma-glob.js";
|
|
7
|
+
import { CMA_GREP_READY_MARKER, CmaGrepStreamCollector, } from "./cma-grep.js";
|
|
8
|
+
import { matchGlob } from "./glob.js";
|
|
9
|
+
import { createSandboxInvocationStats, createSandboxToolDefinitions, recordSandboxInvocation, } from "./provider.js";
|
|
10
|
+
import { MAX_SESSION_OUTPUT_BYTES, MAX_SESSION_OUTPUT_FILE_BYTES, MAX_SESSION_OUTPUT_FILES, } from "../../../files/types.js";
|
|
11
|
+
import { DEFAULT_OMA_SANDBOX_IMAGE } from "./image.js";
|
|
12
|
+
export const DEFAULT_MICROSANDBOX_COMMAND = "msb";
|
|
13
|
+
export const DEFAULT_MICROSANDBOX_IMAGE = DEFAULT_OMA_SANDBOX_IMAGE;
|
|
14
|
+
export const DEFAULT_MICROSANDBOX_WORKSPACE = "/workspace";
|
|
15
|
+
export const DEFAULT_MICROSANDBOX_UPLOADS_PATH = "/mnt/session/uploads";
|
|
16
|
+
export const DEFAULT_MICROSANDBOX_OUTPUTS_PATH = "/mnt/session/outputs";
|
|
17
|
+
export const DEFAULT_MICROSANDBOX_MAX_BUFFER = 16 * 1024 * 1024;
|
|
18
|
+
const DEFAULT_MICROSANDBOX_OPERATION_TIMEOUT_MS = 10_000;
|
|
19
|
+
export const DEFAULT_MICROSANDBOX_STARTUP_TIMEOUT_MS = 5 * 60_000;
|
|
20
|
+
const DEFAULT_MICROSANDBOX_CPUS = "1";
|
|
21
|
+
const DEFAULT_MICROSANDBOX_MEMORY = "1G";
|
|
22
|
+
const DEFAULT_MICROSANDBOX_OCI_UPPER_SIZE = "1G";
|
|
23
|
+
const DEFAULT_MICROSANDBOX_MAX_DURATION = "2h";
|
|
24
|
+
const DEFAULT_MICROSANDBOX_SECURITY = "restricted";
|
|
25
|
+
const DEFAULT_MICROSANDBOX_UPLOADS_TMPFS_SIZE = "64M";
|
|
26
|
+
const DEFAULT_MICROSANDBOX_OUTPUTS_TMPFS_SIZE = "100M";
|
|
27
|
+
const SANDBOX_LABEL_KEY = "open-managed-agents.sandbox";
|
|
28
|
+
const SANDBOX_LABEL_VALUE = "microsandbox-local";
|
|
29
|
+
const OWNER_LABEL_KEY = "open-managed-agents.owner";
|
|
30
|
+
const OWNER_LABEL_VALUE = "open-managed-agents";
|
|
31
|
+
const BASH_DISPATCH_PREFIX = "__OMA_DISPATCHED__:";
|
|
32
|
+
const BASH_TERMINAL_PREFIX = "__OMA_TERMINAL__:";
|
|
33
|
+
const MICROSANDBOX_ENV_ALLOWLIST = new Set([
|
|
34
|
+
"HOME",
|
|
35
|
+
"PATH",
|
|
36
|
+
"TEMP",
|
|
37
|
+
"TMP",
|
|
38
|
+
"TMPDIR",
|
|
39
|
+
]);
|
|
40
|
+
export class NodeMicrosandboxCli {
|
|
41
|
+
command;
|
|
42
|
+
env;
|
|
43
|
+
nodeExecutable;
|
|
44
|
+
constructor(opts = {}) {
|
|
45
|
+
this.command = opts.command ?? DEFAULT_MICROSANDBOX_COMMAND;
|
|
46
|
+
this.nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
47
|
+
this.env = microsandboxCliEnv(opts.env ?? process.env, {
|
|
48
|
+
nodeExecutable: this.nodeExecutable,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
exec(args, opts = {}) {
|
|
52
|
+
return execMicrosandboxCommand(this.command, args, {
|
|
53
|
+
...opts,
|
|
54
|
+
env: microsandboxCliEnv({ ...this.env, ...opts.env }, { nodeExecutable: this.nodeExecutable }),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
execSync(args, opts = {}) {
|
|
58
|
+
return execMicrosandboxCommandSync(this.command, args, {
|
|
59
|
+
...opts,
|
|
60
|
+
env: microsandboxCliEnv({ ...this.env, ...opts.env }, { nodeExecutable: this.nodeExecutable }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function createMicrosandboxSandboxProviderFactory(opts = {}) {
|
|
65
|
+
const cli = opts.cli ??
|
|
66
|
+
new NodeMicrosandboxCli({
|
|
67
|
+
command: opts.command ?? DEFAULT_MICROSANDBOX_COMMAND,
|
|
68
|
+
});
|
|
69
|
+
const providerOpts = { ...opts, cli };
|
|
70
|
+
let swept = false;
|
|
71
|
+
let sweepPromise;
|
|
72
|
+
return async (workspaceId, sessionId) => {
|
|
73
|
+
if (!swept && opts.reapStaleSandboxesOlderThanMs !== undefined) {
|
|
74
|
+
sweepPromise ??= reapMicrosandboxSandboxes({
|
|
75
|
+
cli,
|
|
76
|
+
olderThanMs: opts.reapStaleSandboxesOlderThanMs,
|
|
77
|
+
resourceNamePrefix: opts.resourceNamePrefix,
|
|
78
|
+
now: opts.now,
|
|
79
|
+
}).then((count) => {
|
|
80
|
+
if (count > 0)
|
|
81
|
+
opts.onReaped?.(count);
|
|
82
|
+
swept = true;
|
|
83
|
+
}, (error) => {
|
|
84
|
+
sweepPromise = undefined;
|
|
85
|
+
throw error;
|
|
86
|
+
});
|
|
87
|
+
await sweepPromise;
|
|
88
|
+
}
|
|
89
|
+
return createMicrosandboxSandboxProvider(workspaceId, sessionId, providerOpts);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function createMicrosandboxSandboxProvider(workspaceId, sessionId, opts = {}) {
|
|
93
|
+
const resolved = resolveMicrosandboxOptions(opts);
|
|
94
|
+
const sandboxName = microsandboxResourceName({
|
|
95
|
+
prefix: resolved.resourceNamePrefix,
|
|
96
|
+
workspaceId,
|
|
97
|
+
sessionId,
|
|
98
|
+
purpose: "sandbox",
|
|
99
|
+
now: resolved.now,
|
|
100
|
+
random: resolved.random,
|
|
101
|
+
});
|
|
102
|
+
const volumeName = microsandboxResourceName({
|
|
103
|
+
prefix: resolved.resourceNamePrefix,
|
|
104
|
+
workspaceId,
|
|
105
|
+
sessionId,
|
|
106
|
+
purpose: "workspace-volume",
|
|
107
|
+
now: resolved.now,
|
|
108
|
+
random: resolved.random,
|
|
109
|
+
});
|
|
110
|
+
let volumeCreated = false;
|
|
111
|
+
let sandboxCreated = false;
|
|
112
|
+
try {
|
|
113
|
+
await microsandboxChecked(resolved.cli, buildMicrosandboxVolumeCreateArgs(volumeName), { timeoutMs: resolved.operationTimeoutMs });
|
|
114
|
+
volumeCreated = true;
|
|
115
|
+
await microsandboxChecked(resolved.cli, buildMicrosandboxCreateArgs({
|
|
116
|
+
sandboxName,
|
|
117
|
+
volumeName,
|
|
118
|
+
image: resolved.image,
|
|
119
|
+
workspacePath: resolved.workspacePath,
|
|
120
|
+
workdir: resolved.workspacePath,
|
|
121
|
+
cpus: resolved.cpus,
|
|
122
|
+
memory: resolved.memory,
|
|
123
|
+
ociUpperSize: resolved.ociUpperSize,
|
|
124
|
+
maxDuration: resolved.maxDuration,
|
|
125
|
+
security: resolved.security,
|
|
126
|
+
labels: {
|
|
127
|
+
[SANDBOX_LABEL_KEY]: SANDBOX_LABEL_VALUE,
|
|
128
|
+
[OWNER_LABEL_KEY]: OWNER_LABEL_VALUE,
|
|
129
|
+
"open-managed-agents.workspace-id": workspaceId,
|
|
130
|
+
"open-managed-agents.session-id": sessionId,
|
|
131
|
+
"open-managed-agents.created-at": new Date(resolved.now?.() ?? Date.now()).toISOString(),
|
|
132
|
+
},
|
|
133
|
+
}), { timeoutMs: resolved.startupTimeoutMs });
|
|
134
|
+
sandboxCreated = true;
|
|
135
|
+
await microsandboxChecked(resolved.cli, buildMicrosandboxShellExecArgs({
|
|
136
|
+
sandboxName,
|
|
137
|
+
...buildMicrosandboxPrepareMountsCommand(resolved.uploadsPath, resolved.outputsPath),
|
|
138
|
+
workdir: resolved.workspacePath,
|
|
139
|
+
timeout: timeoutSecondsText(resolved.operationTimeoutMs),
|
|
140
|
+
user: "0",
|
|
141
|
+
}), { timeoutMs: resolved.operationTimeoutMs + 2_000 });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (volumeCreated || sandboxCreated) {
|
|
145
|
+
forceRemoveMicrosandboxSandbox(resolved.cli, sandboxName, resolved);
|
|
146
|
+
}
|
|
147
|
+
if (volumeCreated) {
|
|
148
|
+
forceRemoveMicrosandboxVolume(resolved.cli, volumeName, resolved);
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
const invocations = createSandboxInvocationStats();
|
|
153
|
+
const disposed = { value: false };
|
|
154
|
+
let disposeAttempted = false;
|
|
155
|
+
let poisoned = false;
|
|
156
|
+
let poisonPromise;
|
|
157
|
+
const disposeInfrastructure = () => {
|
|
158
|
+
if (disposeAttempted)
|
|
159
|
+
return;
|
|
160
|
+
disposeAttempted = true;
|
|
161
|
+
disposed.value = true;
|
|
162
|
+
forceRemoveMicrosandboxSandbox(resolved.cli, sandboxName, resolved);
|
|
163
|
+
forceRemoveMicrosandboxVolume(resolved.cli, volumeName, resolved);
|
|
164
|
+
};
|
|
165
|
+
const poisonInfrastructure = () => {
|
|
166
|
+
poisoned = true;
|
|
167
|
+
disposed.value = true;
|
|
168
|
+
if (poisonPromise)
|
|
169
|
+
return poisonPromise;
|
|
170
|
+
poisonPromise = (async () => {
|
|
171
|
+
await removeMicrosandboxSandboxChecked(resolved.cli, sandboxName, resolved.operationTimeoutMs);
|
|
172
|
+
disposeAttempted = true;
|
|
173
|
+
forceRemoveMicrosandboxVolume(resolved.cli, volumeName, resolved);
|
|
174
|
+
})();
|
|
175
|
+
return poisonPromise;
|
|
176
|
+
};
|
|
177
|
+
const shell = (command, execOpts = {}, guestUser) => microsandboxChecked(resolved.cli, buildMicrosandboxShellExecArgs({
|
|
178
|
+
sandboxName,
|
|
179
|
+
script: command.script,
|
|
180
|
+
args: command.args,
|
|
181
|
+
workdir: resolved.workspacePath,
|
|
182
|
+
timeout: execOpts.guestTimeout === false
|
|
183
|
+
? undefined
|
|
184
|
+
: timeoutSecondsText(execOpts.timeoutMs ?? resolved.operationTimeoutMs),
|
|
185
|
+
stream: command.input !== undefined ||
|
|
186
|
+
execOpts.input !== undefined ||
|
|
187
|
+
execOpts.onData !== undefined ||
|
|
188
|
+
execOpts.onStdout !== undefined ||
|
|
189
|
+
execOpts.onStderr !== undefined,
|
|
190
|
+
user: guestUser,
|
|
191
|
+
}), {
|
|
192
|
+
...execOpts,
|
|
193
|
+
input: execOpts.input ?? command.input,
|
|
194
|
+
timeoutMs: (execOpts.timeoutMs ?? resolved.operationTimeoutMs) + 2_000,
|
|
195
|
+
});
|
|
196
|
+
const exists = async (absolutePath) => {
|
|
197
|
+
const result = await resolved.cli.exec(buildMicrosandboxShellExecArgs({
|
|
198
|
+
sandboxName,
|
|
199
|
+
script: "test -e \"$1\"",
|
|
200
|
+
args: [absolutePath],
|
|
201
|
+
workdir: resolved.workspacePath,
|
|
202
|
+
timeout: timeoutSecondsText(resolved.operationTimeoutMs),
|
|
203
|
+
}), { timeoutMs: resolved.operationTimeoutMs + 2_000 });
|
|
204
|
+
if (result.signal !== null)
|
|
205
|
+
throw new Error("microsandbox exists timed out");
|
|
206
|
+
if (result.status === 0)
|
|
207
|
+
return true;
|
|
208
|
+
if (result.status === 1 && result.stderr.length === 0)
|
|
209
|
+
return false;
|
|
210
|
+
throw new Error(`msb exists failed with ${result.status}: ${errorText(result)}`);
|
|
211
|
+
};
|
|
212
|
+
await shell(buildMicrosandboxCmaGrepPreflightCommand(), {
|
|
213
|
+
timeoutMs: 3_000,
|
|
214
|
+
guestTimeout: false,
|
|
215
|
+
});
|
|
216
|
+
const materializeFileResources = async (mounts) => {
|
|
217
|
+
if (mounts.length === 0)
|
|
218
|
+
return;
|
|
219
|
+
recordSandboxNotDisposed(disposed);
|
|
220
|
+
for (const kind of ["upload", "skill"]) {
|
|
221
|
+
const selected = mounts.filter((mount) => mount.kind === kind);
|
|
222
|
+
if (selected.length === 0)
|
|
223
|
+
continue;
|
|
224
|
+
const destination = kind === "upload" ? resolved.uploadsPath : "/workspace/skills";
|
|
225
|
+
const tempRoot = await mkdtemp(joinHostPath(tmpdir(), "oma-msb-mounts-"));
|
|
226
|
+
try {
|
|
227
|
+
for (const mount of selected) {
|
|
228
|
+
const relativePath = assertInsideMicrosandboxUploadsPath(mount.mountPath, destination);
|
|
229
|
+
const hostPath = await writeMountFile(tempRoot, relativePath, mount);
|
|
230
|
+
const guestPath = posix.join(destination, relativePath);
|
|
231
|
+
await shell(buildMicrosandboxMkdirCommand(posix.dirname(guestPath)), {}, "0");
|
|
232
|
+
await microsandboxChecked(resolved.cli, buildMicrosandboxCopyArgs(hostPath, microsandboxPathRef(sandboxName, guestPath)), { timeoutMs: resolved.operationTimeoutMs });
|
|
233
|
+
}
|
|
234
|
+
await shell(kind === "upload"
|
|
235
|
+
? buildMicrosandboxNormalizeUploadsCommand(destination)
|
|
236
|
+
: buildMicrosandboxNormalizeSkillsCommand(destination), {}, "0");
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
await rm(tempRoot, { force: true, recursive: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const collectOutputFiles = async () => {
|
|
244
|
+
recordSandboxNotDisposed(disposed);
|
|
245
|
+
const listing = await shell(buildMicrosandboxOutputListingCommand(resolved.outputsPath, {
|
|
246
|
+
maxFiles: resolved.maxOutputFiles,
|
|
247
|
+
maxFileBytes: resolved.maxOutputFileBytes,
|
|
248
|
+
maxBytes: resolved.maxOutputBytes,
|
|
249
|
+
}));
|
|
250
|
+
const records = parseOutputListing(listing.stdout);
|
|
251
|
+
return records.map((record) => {
|
|
252
|
+
const absolutePath = assertInsideMicrosandboxOutputPath(posix.join(resolved.outputsPath, record.relativePath), resolved.outputsPath);
|
|
253
|
+
return {
|
|
254
|
+
relativePath: record.relativePath,
|
|
255
|
+
filename: posix.basename(record.relativePath),
|
|
256
|
+
mimeType: mimeTypeForFilename(record.relativePath),
|
|
257
|
+
sizeBytes: record.sizeBytes,
|
|
258
|
+
sha256: record.sha256,
|
|
259
|
+
bytes: microsandboxOutputBytes(() => shell(buildMicrosandboxReadFileCommand(absolutePath), {
|
|
260
|
+
maxBuffer: resolved.maxOutputFileBytes,
|
|
261
|
+
})),
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
const readOps = {
|
|
266
|
+
access: async (absolutePath) => {
|
|
267
|
+
recordSandboxInvocation(invocations, disposed, "read");
|
|
268
|
+
await shell(buildMicrosandboxFileAccessCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath), "read"));
|
|
269
|
+
},
|
|
270
|
+
readFile: async (absolutePath) => {
|
|
271
|
+
recordSandboxInvocation(invocations, disposed, "read");
|
|
272
|
+
return (await shell(buildMicrosandboxReadFileCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath)))).stdout;
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const writeOps = {
|
|
276
|
+
mkdir: async (dir) => {
|
|
277
|
+
recordSandboxInvocation(invocations, disposed, "write");
|
|
278
|
+
await shell(buildMicrosandboxMkdirCommand(assertInsideMicrosandboxWorkspace(dir, resolved.workspacePath)));
|
|
279
|
+
},
|
|
280
|
+
writeFile: async (absolutePath, content) => {
|
|
281
|
+
recordSandboxInvocation(invocations, disposed, "write");
|
|
282
|
+
await shell(buildMicrosandboxWriteFileCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath), content));
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
const editOps = {
|
|
286
|
+
access: async (absolutePath) => {
|
|
287
|
+
recordSandboxInvocation(invocations, disposed, "edit");
|
|
288
|
+
await shell(buildMicrosandboxFileAccessCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath), "edit"));
|
|
289
|
+
},
|
|
290
|
+
readFile: async (absolutePath) => {
|
|
291
|
+
recordSandboxInvocation(invocations, disposed, "edit");
|
|
292
|
+
return (await shell(buildMicrosandboxReadFileCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath)))).stdout;
|
|
293
|
+
},
|
|
294
|
+
writeFile: async (absolutePath, content) => {
|
|
295
|
+
recordSandboxInvocation(invocations, disposed, "edit");
|
|
296
|
+
await shell(buildMicrosandboxWriteFileCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath), content));
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
const findOps = {
|
|
300
|
+
exists: async (absolutePath) => {
|
|
301
|
+
recordSandboxInvocation(invocations, disposed, "find");
|
|
302
|
+
return exists(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath));
|
|
303
|
+
},
|
|
304
|
+
glob: async (pattern, cwd, options) => {
|
|
305
|
+
recordSandboxInvocation(invocations, disposed, "find");
|
|
306
|
+
const root = assertInsideMicrosandboxWorkspace(cwd, resolved.workspacePath);
|
|
307
|
+
const result = await shell(buildMicrosandboxGlobEnumerationCommand(root, options.ignore));
|
|
308
|
+
const files = result.stdout
|
|
309
|
+
.toString("utf8")
|
|
310
|
+
.split("\n")
|
|
311
|
+
.filter(Boolean);
|
|
312
|
+
return matchGlob(files, pattern, {
|
|
313
|
+
ignore: options.ignore,
|
|
314
|
+
limit: options.limit,
|
|
315
|
+
}).map((rel) => posix.join(root, rel));
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const globOps = {
|
|
319
|
+
glob: async ({ pattern, cwd, signal, maxMatches, maxRawBytes, maxOutputBytes, outputBase, timeoutMs }) => {
|
|
320
|
+
recordSandboxInvocation(invocations, disposed, "glob");
|
|
321
|
+
const root = assertInsideMicrosandboxWorkspace(cwd, resolved.workspacePath);
|
|
322
|
+
if (signal.aborted)
|
|
323
|
+
throw new Error("Operation aborted");
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const ownershipToken = `oma-glob-${randomUUID()}`;
|
|
326
|
+
const protocolBytes = Buffer.byteLength(CMA_GLOB_READY_MARKER, "utf8") + 1;
|
|
327
|
+
const abortFromCaller = () => controller.abort();
|
|
328
|
+
signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
329
|
+
let streamError;
|
|
330
|
+
let sawStdout = false;
|
|
331
|
+
const readiness = new CmaGlobReadinessFilter(CMA_GLOB_READY_MARKER);
|
|
332
|
+
const collector = new CmaGlobStreamCollector(compileCmaGlob(pattern), {
|
|
333
|
+
root,
|
|
334
|
+
maxMatches,
|
|
335
|
+
maxRawBytes,
|
|
336
|
+
maxOutputBytes,
|
|
337
|
+
join: posix.join,
|
|
338
|
+
formatForOutput: outputBase === undefined
|
|
339
|
+
? undefined
|
|
340
|
+
: (absolutePath) => posix.relative(outputBase, absolutePath),
|
|
341
|
+
onLimit: () => controller.abort(),
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
const result = await shell(buildMicrosandboxCmaGlobEnumerationCommand(root, ownershipToken), {
|
|
345
|
+
signal: controller.signal,
|
|
346
|
+
timeoutMs,
|
|
347
|
+
guestTimeout: false,
|
|
348
|
+
maxBuffer: maxRawBytes + protocolBytes + 64 * 1024,
|
|
349
|
+
onStdout: (chunk) => {
|
|
350
|
+
sawStdout = true;
|
|
351
|
+
try {
|
|
352
|
+
const filenames = readiness.push(chunk);
|
|
353
|
+
if (filenames)
|
|
354
|
+
collector.push(filenames);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
streamError = error;
|
|
358
|
+
controller.abort();
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
// Test/custom CLI implementations may return buffered stdout without
|
|
363
|
+
// invoking the optional streaming callback.
|
|
364
|
+
if (!sawStdout && result.stdout.length > 0) {
|
|
365
|
+
const filenames = readiness.push(result.stdout);
|
|
366
|
+
if (filenames)
|
|
367
|
+
collector.push(filenames);
|
|
368
|
+
}
|
|
369
|
+
if (streamError)
|
|
370
|
+
throw streamError;
|
|
371
|
+
if (signal.aborted)
|
|
372
|
+
throw new Error("Operation aborted");
|
|
373
|
+
readiness.assertReady();
|
|
374
|
+
collector.finish();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (streamError)
|
|
378
|
+
throw streamError;
|
|
379
|
+
if (signal.aborted)
|
|
380
|
+
throw new Error("Operation aborted");
|
|
381
|
+
if (!collector.limitReached)
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
try {
|
|
386
|
+
if (!readiness.ready) {
|
|
387
|
+
await poisonInfrastructure();
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
try {
|
|
391
|
+
await shell(buildMicrosandboxCmaGlobCleanupCommand(ownershipToken), {
|
|
392
|
+
timeoutMs: 3_000,
|
|
393
|
+
guestTimeout: false,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch (cleanupError) {
|
|
397
|
+
await poisonInfrastructure();
|
|
398
|
+
throw cleanupError;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
signal.removeEventListener("abort", abortFromCaller);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (signal.aborted)
|
|
407
|
+
throw new Error("Operation aborted");
|
|
408
|
+
return collector.matches;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
const grepOps = {
|
|
412
|
+
grep: async ({ pattern, cwd, signal, glob, headLimit, maxRawBytes, maxOutputBytes, outputBase, timeoutMs }) => {
|
|
413
|
+
recordSandboxInvocation(invocations, disposed, "grep");
|
|
414
|
+
const root = assertInsideMicrosandboxSearchRoot(cwd, [
|
|
415
|
+
resolved.workspacePath,
|
|
416
|
+
resolved.uploadsPath,
|
|
417
|
+
"/workspace/skills",
|
|
418
|
+
]);
|
|
419
|
+
if (signal.aborted)
|
|
420
|
+
throw new Error("Operation aborted");
|
|
421
|
+
const controller = new AbortController();
|
|
422
|
+
const protocolBytes = Buffer.byteLength(CMA_GREP_READY_MARKER, "utf8") + 1;
|
|
423
|
+
const abortFromCaller = () => controller.abort();
|
|
424
|
+
signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
425
|
+
let streamError;
|
|
426
|
+
let sawStdout = false;
|
|
427
|
+
const ownershipToken = `oma-grep-${randomUUID()}`;
|
|
428
|
+
const readiness = new CmaGlobReadinessFilter(CMA_GREP_READY_MARKER);
|
|
429
|
+
const collector = new CmaGrepStreamCollector({
|
|
430
|
+
root,
|
|
431
|
+
maxMatches: headLimit,
|
|
432
|
+
maxRawBytes,
|
|
433
|
+
maxOutputBytes,
|
|
434
|
+
join: posix.join,
|
|
435
|
+
formatForOutput: outputBase === undefined
|
|
436
|
+
? undefined
|
|
437
|
+
: (absolutePath) => posix.relative(outputBase, absolutePath),
|
|
438
|
+
onLimit: () => controller.abort(),
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
try {
|
|
442
|
+
const result = await shell(buildMicrosandboxCmaGrepSearchCommand(root, ownershipToken, pattern, glob), {
|
|
443
|
+
signal: controller.signal,
|
|
444
|
+
timeoutMs,
|
|
445
|
+
guestTimeout: false,
|
|
446
|
+
maxBuffer: maxRawBytes + protocolBytes + 64 * 1024,
|
|
447
|
+
onStdout: (chunk) => {
|
|
448
|
+
sawStdout = true;
|
|
449
|
+
try {
|
|
450
|
+
const filenames = readiness.push(chunk);
|
|
451
|
+
if (filenames)
|
|
452
|
+
collector.push(filenames);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
streamError = error;
|
|
456
|
+
controller.abort();
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
if (!sawStdout && result.stdout.length > 0) {
|
|
461
|
+
const filenames = readiness.push(result.stdout);
|
|
462
|
+
if (filenames)
|
|
463
|
+
collector.push(filenames);
|
|
464
|
+
}
|
|
465
|
+
if (streamError)
|
|
466
|
+
throw streamError;
|
|
467
|
+
if (signal.aborted)
|
|
468
|
+
throw new Error("Operation aborted");
|
|
469
|
+
readiness.assertReady();
|
|
470
|
+
collector.finish();
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
if (streamError)
|
|
474
|
+
throw streamError;
|
|
475
|
+
if (signal.aborted)
|
|
476
|
+
throw new Error("Operation aborted");
|
|
477
|
+
if (!collector.limitReached)
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
if (!readiness.ready) {
|
|
482
|
+
await poisonInfrastructure();
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
try {
|
|
486
|
+
await shell(buildMicrosandboxCmaGrepCleanupCommand(ownershipToken), {
|
|
487
|
+
timeoutMs: 3_000,
|
|
488
|
+
guestTimeout: false,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch (cleanupError) {
|
|
492
|
+
await poisonInfrastructure();
|
|
493
|
+
throw cleanupError;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (signal.aborted)
|
|
498
|
+
throw new Error("Operation aborted");
|
|
499
|
+
return collector.matches;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
signal.removeEventListener("abort", abortFromCaller);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
const lsOps = {
|
|
507
|
+
exists: async (absolutePath) => {
|
|
508
|
+
recordSandboxInvocation(invocations, disposed, "ls");
|
|
509
|
+
return exists(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath));
|
|
510
|
+
},
|
|
511
|
+
stat: async (absolutePath) => {
|
|
512
|
+
recordSandboxInvocation(invocations, disposed, "ls");
|
|
513
|
+
const result = await shell(buildMicrosandboxStatCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath)));
|
|
514
|
+
const kind = result.stdout.toString("utf8");
|
|
515
|
+
return { isDirectory: () => kind === "directory" };
|
|
516
|
+
},
|
|
517
|
+
readdir: async (absolutePath) => {
|
|
518
|
+
recordSandboxInvocation(invocations, disposed, "ls");
|
|
519
|
+
const result = await shell(buildMicrosandboxReaddirCommand(assertInsideMicrosandboxWorkspace(absolutePath, resolved.workspacePath)));
|
|
520
|
+
return result.stdout.toString("utf8").split("\n").filter(Boolean);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
const bashOps = {
|
|
524
|
+
exec: async (command, cwd, options) => {
|
|
525
|
+
recordSandboxInvocation(invocations, disposed, "bash");
|
|
526
|
+
const path = assertInsideMicrosandboxWorkspace(cwd, resolved.workspacePath);
|
|
527
|
+
const timeoutSeconds = options.timeout !== undefined && options.timeout > 0
|
|
528
|
+
? options.timeout
|
|
529
|
+
: resolved.operationTimeoutMs / 1000;
|
|
530
|
+
const execId = randomExecId();
|
|
531
|
+
const dispatchToken = randomExecId();
|
|
532
|
+
const pidFile = posix.join(resolved.workspacePath, `.oma-msb-exec-${execId}.pid`);
|
|
533
|
+
const shellCommand = buildMicrosandboxBashCommand(command, timeoutSeconds, pidFile, dispatchToken);
|
|
534
|
+
const dispatchFilter = createMicrosandboxBashDispatchFilter(dispatchToken);
|
|
535
|
+
try {
|
|
536
|
+
const result = await resolved.cli.exec(buildMicrosandboxShellExecArgs({
|
|
537
|
+
sandboxName,
|
|
538
|
+
script: shellCommand.script,
|
|
539
|
+
args: shellCommand.args,
|
|
540
|
+
workdir: path,
|
|
541
|
+
stream: true,
|
|
542
|
+
}), {
|
|
543
|
+
onData: (chunk) => {
|
|
544
|
+
const forwarded = dispatchFilter.chunk(chunk);
|
|
545
|
+
if (forwarded.length > 0)
|
|
546
|
+
options.onData(forwarded);
|
|
547
|
+
},
|
|
548
|
+
signal: options.signal,
|
|
549
|
+
timeoutMs: Math.ceil(timeoutSeconds * 1000) + 2_000,
|
|
550
|
+
});
|
|
551
|
+
if (result.signal !== null) {
|
|
552
|
+
killMicrosandboxGuestProcessGroup(resolved.cli, sandboxName, pidFile, resolved);
|
|
553
|
+
throw new Error(`timeout:${timeoutSeconds}`);
|
|
554
|
+
}
|
|
555
|
+
if (!dispatchFilter.dispatchSeen()) {
|
|
556
|
+
throw new Error("microsandbox bash failed before command dispatch");
|
|
557
|
+
}
|
|
558
|
+
const terminal = dispatchFilter.terminalRecord();
|
|
559
|
+
if (terminal === undefined) {
|
|
560
|
+
throw new Error("microsandbox bash failed before command completion");
|
|
561
|
+
}
|
|
562
|
+
if (result.status !==
|
|
563
|
+
(terminal.kind === "timeout" ? 137 : terminal.exitCode)) {
|
|
564
|
+
throw new Error("microsandbox bash exit disagreed with command completion");
|
|
565
|
+
}
|
|
566
|
+
if (terminal.kind === "timeout") {
|
|
567
|
+
throw new Error(`timeout:${timeoutSeconds}`);
|
|
568
|
+
}
|
|
569
|
+
return { exitCode: terminal.exitCode };
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
if (isAbortError(error) || isMicrosandboxOutputOverflow(error)) {
|
|
573
|
+
killMicrosandboxGuestProcessGroup(resolved.cli, sandboxName, pidFile, resolved);
|
|
574
|
+
if (isAbortError(error))
|
|
575
|
+
throw new Error("aborted");
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
const operations = {
|
|
582
|
+
bash: bashOps,
|
|
583
|
+
read: readOps,
|
|
584
|
+
write: writeOps,
|
|
585
|
+
edit: editOps,
|
|
586
|
+
find: findOps,
|
|
587
|
+
glob: globOps,
|
|
588
|
+
grep: grepOps,
|
|
589
|
+
ls: lsOps,
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
cwd: resolved.workspacePath,
|
|
593
|
+
collectOutputFiles,
|
|
594
|
+
invocations,
|
|
595
|
+
materializeFileResources,
|
|
596
|
+
operations,
|
|
597
|
+
toolNames: new Set(["bash", "read", "write", "edit", "glob", "grep", "ls"]),
|
|
598
|
+
tools: createSandboxToolDefinitions(resolved.workspacePath, operations, invocations, disposed),
|
|
599
|
+
isPoisoned: () => poisoned,
|
|
600
|
+
dispose: disposeInfrastructure,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
export function microsandboxCliEnv(source = process.env, opts = {}) {
|
|
604
|
+
const nodeDir = dirname(opts.nodeExecutable ?? process.execPath);
|
|
605
|
+
const path = source.PATH;
|
|
606
|
+
const out = {};
|
|
607
|
+
for (const key of MICROSANDBOX_ENV_ALLOWLIST) {
|
|
608
|
+
if (source[key] !== undefined)
|
|
609
|
+
out[key] = source[key];
|
|
610
|
+
}
|
|
611
|
+
out.PATH =
|
|
612
|
+
path === undefined || path.length === 0
|
|
613
|
+
? nodeDir
|
|
614
|
+
: path.split(":").includes(nodeDir)
|
|
615
|
+
? path
|
|
616
|
+
: `${nodeDir}:${path}`;
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
export function buildMicrosandboxVolumeCreateArgs(volumeName) {
|
|
620
|
+
assertMicrosandboxArg(volumeName, "volumeName");
|
|
621
|
+
return ["volume", "create", "--name", volumeName];
|
|
622
|
+
}
|
|
623
|
+
export function buildMicrosandboxVolumeRemoveArgs(volumeName) {
|
|
624
|
+
assertMicrosandboxArg(volumeName, "volumeName");
|
|
625
|
+
return ["volume", "remove", volumeName];
|
|
626
|
+
}
|
|
627
|
+
export function buildMicrosandboxVolumeListArgs() {
|
|
628
|
+
return ["volume", "list", "--format", "json"];
|
|
629
|
+
}
|
|
630
|
+
export function buildMicrosandboxVolumeInspectArgs(volumeName) {
|
|
631
|
+
assertMicrosandboxArg(volumeName, "volumeName");
|
|
632
|
+
return ["volume", "inspect", volumeName];
|
|
633
|
+
}
|
|
634
|
+
export function buildMicrosandboxCreateArgs(opts) {
|
|
635
|
+
const workspacePath = opts.workspacePath ?? DEFAULT_MICROSANDBOX_WORKSPACE;
|
|
636
|
+
assertMicrosandboxArg(opts.sandboxName, "sandboxName");
|
|
637
|
+
assertMicrosandboxArg(opts.volumeName, "volumeName");
|
|
638
|
+
assertMicrosandboxArg(opts.image ?? DEFAULT_MICROSANDBOX_IMAGE, "image");
|
|
639
|
+
const labels = Object.entries(opts.labels ?? {}).flatMap(([key, value]) => [
|
|
640
|
+
"--label",
|
|
641
|
+
microsandboxLabelArg(key, value),
|
|
642
|
+
]);
|
|
643
|
+
return [
|
|
644
|
+
"create",
|
|
645
|
+
opts.image ?? DEFAULT_MICROSANDBOX_IMAGE,
|
|
646
|
+
"--name",
|
|
647
|
+
opts.sandboxName,
|
|
648
|
+
"--mount-named",
|
|
649
|
+
`${opts.volumeName}:${workspacePath}`,
|
|
650
|
+
"--workdir",
|
|
651
|
+
opts.workdir ?? workspacePath,
|
|
652
|
+
"--no-net",
|
|
653
|
+
"--tmpfs",
|
|
654
|
+
`${DEFAULT_MICROSANDBOX_UPLOADS_PATH}:${DEFAULT_MICROSANDBOX_UPLOADS_TMPFS_SIZE}:nosuid,nodev,noexec`,
|
|
655
|
+
"--tmpfs",
|
|
656
|
+
`${DEFAULT_MICROSANDBOX_OUTPUTS_PATH}:${DEFAULT_MICROSANDBOX_OUTPUTS_TMPFS_SIZE}:nosuid,nodev,noexec`,
|
|
657
|
+
"--cpus",
|
|
658
|
+
opts.cpus ?? DEFAULT_MICROSANDBOX_CPUS,
|
|
659
|
+
"--memory",
|
|
660
|
+
opts.memory ?? DEFAULT_MICROSANDBOX_MEMORY,
|
|
661
|
+
"--oci-upper-size",
|
|
662
|
+
opts.ociUpperSize ?? DEFAULT_MICROSANDBOX_OCI_UPPER_SIZE,
|
|
663
|
+
"--max-duration",
|
|
664
|
+
opts.maxDuration ?? DEFAULT_MICROSANDBOX_MAX_DURATION,
|
|
665
|
+
"--security",
|
|
666
|
+
opts.security ?? DEFAULT_MICROSANDBOX_SECURITY,
|
|
667
|
+
"--pull",
|
|
668
|
+
opts.pullPolicy ?? "if-missing",
|
|
669
|
+
"--quiet",
|
|
670
|
+
...labels,
|
|
671
|
+
];
|
|
672
|
+
}
|
|
673
|
+
export function buildMicrosandboxExecArgs(opts) {
|
|
674
|
+
assertMicrosandboxArg(opts.sandboxName, "sandboxName");
|
|
675
|
+
const out = ["exec"];
|
|
676
|
+
if (opts.stream)
|
|
677
|
+
out.push("--stream");
|
|
678
|
+
if (opts.user !== undefined) {
|
|
679
|
+
assertMicrosandboxArg(opts.user, "user");
|
|
680
|
+
out.push("--user", opts.user);
|
|
681
|
+
}
|
|
682
|
+
if (opts.timeout !== undefined)
|
|
683
|
+
out.push("--timeout", opts.timeout);
|
|
684
|
+
if (opts.workdir !== undefined)
|
|
685
|
+
out.push("--workdir", opts.workdir);
|
|
686
|
+
out.push(opts.sandboxName, "--", ...opts.command);
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
export function buildMicrosandboxShellExecArgs(opts) {
|
|
690
|
+
return buildMicrosandboxExecArgs({
|
|
691
|
+
sandboxName: opts.sandboxName,
|
|
692
|
+
workdir: opts.workdir,
|
|
693
|
+
timeout: opts.timeout,
|
|
694
|
+
stream: opts.stream,
|
|
695
|
+
user: opts.user,
|
|
696
|
+
command: ["/bin/bash", "-lc", opts.script, "bash", ...(opts.args ?? [])],
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
export function buildMicrosandboxCopyArgs(source, destination) {
|
|
700
|
+
return ["copy", source, destination];
|
|
701
|
+
}
|
|
702
|
+
export function microsandboxPathRef(sandboxName, absolutePath) {
|
|
703
|
+
assertMicrosandboxArg(sandboxName, "sandboxName");
|
|
704
|
+
return `${sandboxName}:${absolutePath}`;
|
|
705
|
+
}
|
|
706
|
+
export function buildMicrosandboxStopArgs(sandboxName) {
|
|
707
|
+
assertMicrosandboxArg(sandboxName, "sandboxName");
|
|
708
|
+
return ["stop", sandboxName];
|
|
709
|
+
}
|
|
710
|
+
export function buildMicrosandboxStartArgs(sandboxName) {
|
|
711
|
+
assertMicrosandboxArg(sandboxName, "sandboxName");
|
|
712
|
+
return ["start", sandboxName];
|
|
713
|
+
}
|
|
714
|
+
export function buildMicrosandboxRemoveArgs(sandboxName) {
|
|
715
|
+
assertMicrosandboxArg(sandboxName, "sandboxName");
|
|
716
|
+
return ["remove", "--force", sandboxName];
|
|
717
|
+
}
|
|
718
|
+
export function buildMicrosandboxListArgs(labels = []) {
|
|
719
|
+
return [
|
|
720
|
+
"list",
|
|
721
|
+
"--format",
|
|
722
|
+
"json",
|
|
723
|
+
...labels.flatMap((label) => ["--label", microsandboxLabelFilterArg(label)]),
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
export function buildMicrosandboxInspectArgs(sandboxName) {
|
|
727
|
+
assertMicrosandboxArg(sandboxName, "sandboxName");
|
|
728
|
+
return ["inspect", sandboxName, "--format", "json"];
|
|
729
|
+
}
|
|
730
|
+
export function buildMicrosandboxFileAccessCommand(absolutePath, mode) {
|
|
731
|
+
return {
|
|
732
|
+
script: mode === "read"
|
|
733
|
+
? "test -r \"$1\" -a -f \"$1\""
|
|
734
|
+
: "test -r \"$1\" -a -w \"$1\" -a -f \"$1\"",
|
|
735
|
+
args: [absolutePath],
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
export function buildMicrosandboxReadFileCommand(absolutePath) {
|
|
739
|
+
return { script: "cat \"$1\"", args: [absolutePath] };
|
|
740
|
+
}
|
|
741
|
+
export function buildMicrosandboxWriteFileCommand(absolutePath, content) {
|
|
742
|
+
return {
|
|
743
|
+
script: "cat > \"$1\"",
|
|
744
|
+
args: [absolutePath],
|
|
745
|
+
input: content,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
export function buildMicrosandboxMkdirCommand(absolutePath) {
|
|
749
|
+
return { script: "mkdir -p \"$1\"", args: [absolutePath] };
|
|
750
|
+
}
|
|
751
|
+
export function buildMicrosandboxPrepareMountsCommand(uploadsPath, outputsPath) {
|
|
752
|
+
return {
|
|
753
|
+
script: "chown 0:0 \"$1\" && chmod 755 \"$1\" && chown 65534:65534 \"$2\" && chmod 700 \"$2\"",
|
|
754
|
+
args: [uploadsPath, outputsPath],
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
export function buildMicrosandboxNormalizeUploadsCommand(uploadsPath) {
|
|
758
|
+
return {
|
|
759
|
+
script: "find \"$1\" -type d -exec chmod 755 {} + && find \"$1\" -type f -exec chmod 444 {} +",
|
|
760
|
+
args: [uploadsPath],
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
export function buildMicrosandboxNormalizeSkillsCommand(skillsPath) {
|
|
764
|
+
return { script: "find \"$1\" -type d -exec chmod 755 {} + && find \"$1\" -type f -exec chmod 444 {} + && find \"$1\" -path '*/scripts/*' -type f -exec chmod 555 {} +", args: [skillsPath] };
|
|
765
|
+
}
|
|
766
|
+
export function buildMicrosandboxStatCommand(absolutePath) {
|
|
767
|
+
return {
|
|
768
|
+
script: "if [ -d \"$1\" ]; then printf directory; elif [ -e \"$1\" ]; then printf file; else exit 1; fi",
|
|
769
|
+
args: [absolutePath],
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
export function buildMicrosandboxReaddirCommand(absolutePath) {
|
|
773
|
+
return { script: "ls -1A \"$1\"", args: [absolutePath] };
|
|
774
|
+
}
|
|
775
|
+
export function buildMicrosandboxCmaGlobCleanupCommand(ownershipToken) {
|
|
776
|
+
return {
|
|
777
|
+
script: [
|
|
778
|
+
"pids=''",
|
|
779
|
+
"for f in /proc/[0-9]*/environ; do",
|
|
780
|
+
" pid=${f#/proc/}; pid=${pid%/environ}",
|
|
781
|
+
" if tr '\\0' '\\n' < \"$f\" 2>/dev/null | grep -Fqx \"OMA_GLOB_OWNER=$1\"; then",
|
|
782
|
+
" start=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
783
|
+
" case \"$start\" in ''|*[!0-9]*) :;; *) pids=\"$pids $pid:$start\";; esac",
|
|
784
|
+
" fi",
|
|
785
|
+
"done",
|
|
786
|
+
"for entry in $pids; do",
|
|
787
|
+
" pid=${entry%%:*}; start=${entry#*:}",
|
|
788
|
+
" current=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
789
|
+
" if [ \"$current\" = \"$start\" ] && tr '\\0' '\\n' < \"/proc/$pid/environ\" 2>/dev/null | grep -Fqx \"OMA_GLOB_OWNER=$1\"; then",
|
|
790
|
+
" kill -KILL \"-$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
|
|
791
|
+
" fi",
|
|
792
|
+
"done",
|
|
793
|
+
"attempt=0",
|
|
794
|
+
"while [ -n \"$pids\" ] && [ \"$attempt\" -lt 40 ]; do",
|
|
795
|
+
" remaining=''",
|
|
796
|
+
" for entry in $pids; do",
|
|
797
|
+
" pid=${entry%%:*}; start=${entry#*:}",
|
|
798
|
+
" current=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
799
|
+
" if [ \"$current\" = \"$start\" ] && tr '\\0' '\\n' < \"/proc/$pid/environ\" 2>/dev/null | grep -Fqx \"OMA_GLOB_OWNER=$1\"; then remaining=\"$remaining $entry\"; fi",
|
|
800
|
+
" done",
|
|
801
|
+
" pids=$remaining",
|
|
802
|
+
" [ -z \"$pids\" ] && break",
|
|
803
|
+
" attempt=$((attempt + 1)); sleep 0.05",
|
|
804
|
+
"done",
|
|
805
|
+
"[ -z \"$pids\" ]",
|
|
806
|
+
].join("\n"),
|
|
807
|
+
args: [ownershipToken],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
export function buildMicrosandboxCmaGrepCleanupCommand(ownershipToken) {
|
|
811
|
+
return {
|
|
812
|
+
script: [
|
|
813
|
+
"pids=''",
|
|
814
|
+
"for f in /proc/[0-9]*/environ; do",
|
|
815
|
+
" pid=${f#/proc/}; pid=${pid%/environ}",
|
|
816
|
+
" if tr '\\0' '\\n' < \"$f\" 2>/dev/null | grep -Fqx \"OMA_GREP_OWNER=$1\"; then",
|
|
817
|
+
" start=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
818
|
+
" case \"$start\" in ''|*[!0-9]*) :;; *) pids=\"$pids $pid:$start\";; esac",
|
|
819
|
+
" fi",
|
|
820
|
+
"done",
|
|
821
|
+
"for entry in $pids; do",
|
|
822
|
+
" pid=${entry%%:*}; start=${entry#*:}",
|
|
823
|
+
" current=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
824
|
+
" if [ \"$current\" = \"$start\" ] && tr '\\0' '\\n' < \"/proc/$pid/environ\" 2>/dev/null | grep -Fqx \"OMA_GREP_OWNER=$1\"; then",
|
|
825
|
+
" kill -KILL \"-$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
|
|
826
|
+
" fi",
|
|
827
|
+
"done",
|
|
828
|
+
"attempt=0",
|
|
829
|
+
"while [ -n \"$pids\" ] && [ \"$attempt\" -lt 40 ]; do",
|
|
830
|
+
" remaining=''",
|
|
831
|
+
" for entry in $pids; do",
|
|
832
|
+
" pid=${entry%%:*}; start=${entry#*:}",
|
|
833
|
+
" current=$(awk '{print $22}' \"/proc/$pid/stat\" 2>/dev/null || true)",
|
|
834
|
+
" if [ \"$current\" = \"$start\" ] && tr '\\0' '\\n' < \"/proc/$pid/environ\" 2>/dev/null | grep -Fqx \"OMA_GREP_OWNER=$1\"; then remaining=\"$remaining $entry\"; fi",
|
|
835
|
+
" done",
|
|
836
|
+
" pids=$remaining",
|
|
837
|
+
" [ -z \"$pids\" ] && break",
|
|
838
|
+
" attempt=$((attempt + 1)); sleep 0.05",
|
|
839
|
+
"done",
|
|
840
|
+
"[ -z \"$pids\" ]",
|
|
841
|
+
].join("\n"),
|
|
842
|
+
args: [ownershipToken],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
export function buildMicrosandboxCmaGrepPreflightCommand() {
|
|
846
|
+
return {
|
|
847
|
+
script: [
|
|
848
|
+
"set -eu",
|
|
849
|
+
"dir=\".oma-grep-preflight-$$\"",
|
|
850
|
+
"mkdir \"$dir\"",
|
|
851
|
+
"trap 'rm -rf \"$dir\"' EXIT",
|
|
852
|
+
"printf 'needle\\n' > \"$dir/text.txt\"",
|
|
853
|
+
"printf 'other\\n' > \"$dir/no-match.txt\"",
|
|
854
|
+
"printf '\\0needle\\0tail' > \"$dir/binary.bin\"",
|
|
855
|
+
"LC_ALL=C rg --no-config --files-with-matches --null -- 'needle' \"$dir/text.txt\" | grep -Fq \"$dir/text.txt\"",
|
|
856
|
+
"set +e",
|
|
857
|
+
"LC_ALL=C rg --no-config --files-with-matches --null -- 'needle' \"$dir/no-match.txt\" >/dev/null 2>&1",
|
|
858
|
+
"code=$?",
|
|
859
|
+
"set -e",
|
|
860
|
+
"[ \"$code\" -eq 1 ]",
|
|
861
|
+
"set +e",
|
|
862
|
+
"LC_ALL=C rg --no-config --files-with-matches --null -- '[' \"$dir/text.txt\" >/dev/null 2>&1",
|
|
863
|
+
"code=$?",
|
|
864
|
+
"set -e",
|
|
865
|
+
"[ \"$code\" -ne 0 ] && [ \"$code\" -ne 1 ]",
|
|
866
|
+
"last=$(LC_ALL=C rg --no-config --files-with-matches --null -- 'needle' \"$dir/text.txt\" | tail -c 1 | od -An -tu1)",
|
|
867
|
+
"[ \"$(printf '%s' \"$last\" | tr -d ' ')\" = 0 ]",
|
|
868
|
+
"LC_ALL=C rg --no-config -qaU -- '\\x00' \"$dir/binary.bin\"",
|
|
869
|
+
"set +e",
|
|
870
|
+
"LC_ALL=C rg --no-config -qaU -- '\\x00' \"$dir/text.txt\"",
|
|
871
|
+
"code=$?",
|
|
872
|
+
"set -e",
|
|
873
|
+
"[ \"$code\" -eq 1 ]",
|
|
874
|
+
].join("\n"),
|
|
875
|
+
args: [],
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
export function buildMicrosandboxCmaGrepSearchCommand(root, ownershipToken, pattern, glob) {
|
|
879
|
+
const ripgrepGlob = glob === undefined ? "" : cmaGlobToRipgrepGlob(glob);
|
|
880
|
+
return {
|
|
881
|
+
script: [
|
|
882
|
+
"set -eu",
|
|
883
|
+
"exec setsid env \"OMA_GREP_OWNER=$2\" bash -c '",
|
|
884
|
+
"set -euo pipefail",
|
|
885
|
+
"printf \"%s\\0\" __OMA_GREP_READY__",
|
|
886
|
+
"emit_text_matches() {",
|
|
887
|
+
" while IFS= read -r -d \"\" path; do",
|
|
888
|
+
" set +e",
|
|
889
|
+
" LC_ALL=C rg --no-config -qaU -- \"\\x00\" \"$path\"",
|
|
890
|
+
" binary_code=$?",
|
|
891
|
+
" set -e",
|
|
892
|
+
" if [ \"$binary_code\" -eq 1 ]; then printf \"%s\\0\" \"$path\"; elif [ \"$binary_code\" -ne 0 ]; then exit \"$binary_code\"; fi",
|
|
893
|
+
" done",
|
|
894
|
+
"}",
|
|
895
|
+
"set +e",
|
|
896
|
+
"if [ -n \"$3\" ]; then",
|
|
897
|
+
" LC_ALL=C rg --no-config --hidden --no-ignore --color never --files-with-matches --null --glob \"$3\" -- \"$2\" \"$1\" | emit_text_matches",
|
|
898
|
+
"else",
|
|
899
|
+
" LC_ALL=C rg --no-config --hidden --no-ignore --color never --files-with-matches --null -- \"$2\" \"$1\" | emit_text_matches",
|
|
900
|
+
"fi",
|
|
901
|
+
"code=$?",
|
|
902
|
+
"[ \"$code\" -eq 0 ] || [ \"$code\" -eq 1 ]",
|
|
903
|
+
"' oma-rg \"$1\" \"$3\" \"$4\"",
|
|
904
|
+
].join("\n"),
|
|
905
|
+
args: [root, ownershipToken, pattern, ripgrepGlob],
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
export function buildMicrosandboxCmaGlobEnumerationCommand(root, ownershipToken) {
|
|
909
|
+
return {
|
|
910
|
+
script: [
|
|
911
|
+
"set -eu",
|
|
912
|
+
"setsid env \"OMA_GLOB_OWNER=$2\" sh -c 'set -eu; printf \"%s\\0\" __OMA_GLOB_READY__; cd \"$1\"; exec find . -type f -print0' \"$2\" \"$1\" &",
|
|
913
|
+
"child=$!",
|
|
914
|
+
"wait \"$child\"",
|
|
915
|
+
].join("\n"),
|
|
916
|
+
args: [root, ownershipToken],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
export function buildMicrosandboxGlobEnumerationCommand(root, ignore = []) {
|
|
920
|
+
const prunedDirectoryNames = directoryNamesPrunedByIgnoreGlobs(ignore);
|
|
921
|
+
return {
|
|
922
|
+
script: [
|
|
923
|
+
"cd \"$1\"",
|
|
924
|
+
"shift",
|
|
925
|
+
"if [ \"$#\" -eq 0 ]; then",
|
|
926
|
+
" find . -type f | sed 's#^./##' | sort",
|
|
927
|
+
"else",
|
|
928
|
+
" find . \\( -type d \\( \"$@\" \\) -prune \\) -o -type f -print | sed 's#^./##' | sort",
|
|
929
|
+
"fi",
|
|
930
|
+
].join("\n"),
|
|
931
|
+
args: [
|
|
932
|
+
root,
|
|
933
|
+
...prunedDirectoryNames.flatMap((name, index) => index === 0 ? ["-name", name] : ["-o", "-name", name]),
|
|
934
|
+
],
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
export function buildMicrosandboxOutputListingCommand(outputRoot, limits = {
|
|
938
|
+
maxFiles: MAX_SESSION_OUTPUT_FILES,
|
|
939
|
+
maxFileBytes: MAX_SESSION_OUTPUT_FILE_BYTES,
|
|
940
|
+
maxBytes: MAX_SESSION_OUTPUT_BYTES,
|
|
941
|
+
}) {
|
|
942
|
+
return {
|
|
943
|
+
script: [
|
|
944
|
+
"root=\"$1\"",
|
|
945
|
+
"max_files=\"$2\"",
|
|
946
|
+
"max_file_bytes=\"$3\"",
|
|
947
|
+
"max_bytes=\"$4\"",
|
|
948
|
+
"if [ ! -d \"$root\" ]; then exit 0; fi",
|
|
949
|
+
"cd \"$root\"",
|
|
950
|
+
"count=0",
|
|
951
|
+
"total=0",
|
|
952
|
+
"find . -type f -print0 | sort -z | while IFS= read -r -d '' file; do",
|
|
953
|
+
" rel=\"${file#./}\"",
|
|
954
|
+
" size=$(wc -c < \"$file\")",
|
|
955
|
+
" count=$((count + 1))",
|
|
956
|
+
" if [ \"$count\" -gt \"$max_files\" ]; then",
|
|
957
|
+
" printf 'session output file count exceeds %s\\n' \"$max_files\" >&2",
|
|
958
|
+
" exit 42",
|
|
959
|
+
" fi",
|
|
960
|
+
" if [ \"$size\" -gt \"$max_file_bytes\" ]; then",
|
|
961
|
+
" printf 'session output file exceeds %s bytes: %s\\n' \"$max_file_bytes\" \"$rel\" >&2",
|
|
962
|
+
" exit 42",
|
|
963
|
+
" fi",
|
|
964
|
+
" total=$((total + size))",
|
|
965
|
+
" if [ \"$total\" -gt \"$max_bytes\" ]; then",
|
|
966
|
+
" printf 'session output bytes exceed %s\\n' \"$max_bytes\" >&2",
|
|
967
|
+
" exit 42",
|
|
968
|
+
" fi",
|
|
969
|
+
" sha=$(sha256sum \"$file\" | awk '{print $1}')",
|
|
970
|
+
" printf '%s\\0%s\\0%s\\0' \"$rel\" \"$size\" \"$sha\"",
|
|
971
|
+
"done",
|
|
972
|
+
].join("\n"),
|
|
973
|
+
args: [
|
|
974
|
+
outputRoot,
|
|
975
|
+
String(limits.maxFiles),
|
|
976
|
+
String(limits.maxFileBytes),
|
|
977
|
+
String(limits.maxBytes),
|
|
978
|
+
],
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
export function buildMicrosandboxBashCommand(command, timeoutSeconds, pidFile, dispatchToken = "") {
|
|
982
|
+
return {
|
|
983
|
+
script: [
|
|
984
|
+
"pidfile=\"$1\"",
|
|
985
|
+
"timeout_secs=\"$2\"",
|
|
986
|
+
"command=\"$3\"",
|
|
987
|
+
"dispatch_token=\"$4\"",
|
|
988
|
+
"timeout_file=\"${pidfile}.timeout\"",
|
|
989
|
+
"timer_file=\"${pidfile}.timer\"",
|
|
990
|
+
"rm -f \"$pidfile\" \"$timeout_file\" \"$timer_file\"",
|
|
991
|
+
"printf '%s\\n' \"__OMA_DISPATCHED__:${dispatch_token}\"",
|
|
992
|
+
"terminal_exit() { printf '%s\\n' \"__OMA_TERMINAL__:${dispatch_token}:exit:$1\"; }",
|
|
993
|
+
"terminal_timeout() { printf '%s\\n' \"__OMA_TERMINAL__:${dispatch_token}:timeout:137\"; }",
|
|
994
|
+
"setsid /bin/bash -lc \"$command\" &",
|
|
995
|
+
"pid=$!",
|
|
996
|
+
"printf '%s' \"$pid\" > \"$pidfile\"",
|
|
997
|
+
"(",
|
|
998
|
+
" sleep \"$timeout_secs\"",
|
|
999
|
+
" if kill -0 \"$pid\" 2>/dev/null; then",
|
|
1000
|
+
" : > \"$timeout_file\"",
|
|
1001
|
+
" kill -KILL \"-$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
|
|
1002
|
+
" fi",
|
|
1003
|
+
") &",
|
|
1004
|
+
"timer=$!",
|
|
1005
|
+
"printf '%s' \"$timer\" > \"$timer_file\"",
|
|
1006
|
+
"wait \"$pid\"",
|
|
1007
|
+
"status=$?",
|
|
1008
|
+
"if [ -f \"$timeout_file\" ]; then",
|
|
1009
|
+
" kill \"$timer\" 2>/dev/null || true",
|
|
1010
|
+
" wait \"$timer\" 2>/dev/null || true",
|
|
1011
|
+
" rm -f \"$pidfile\" \"$timeout_file\" \"$timer_file\"",
|
|
1012
|
+
" terminal_timeout",
|
|
1013
|
+
" exit 137",
|
|
1014
|
+
"fi",
|
|
1015
|
+
"kill \"$timer\" 2>/dev/null || true",
|
|
1016
|
+
"wait \"$timer\" 2>/dev/null || true",
|
|
1017
|
+
"rm -f \"$pidfile\" \"$timeout_file\" \"$timer_file\"",
|
|
1018
|
+
"terminal_exit \"$status\"",
|
|
1019
|
+
"exit \"$status\"",
|
|
1020
|
+
].join("\n"),
|
|
1021
|
+
args: [pidFile, String(timeoutSeconds), command, dispatchToken],
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
export function buildMicrosandboxKillProcessGroupCommand(pidFile) {
|
|
1025
|
+
return {
|
|
1026
|
+
script: [
|
|
1027
|
+
"pidfile=\"$1\"",
|
|
1028
|
+
"timerfile=\"${pidfile}.timer\"",
|
|
1029
|
+
"timeoutfile=\"${pidfile}.timeout\"",
|
|
1030
|
+
"i=0",
|
|
1031
|
+
"while [ \"$i\" -lt 50 ]; do",
|
|
1032
|
+
" [ -f \"$pidfile\" ] && break",
|
|
1033
|
+
" sleep 0.01",
|
|
1034
|
+
" i=$((i + 1))",
|
|
1035
|
+
"done",
|
|
1036
|
+
"if [ -f \"$timerfile\" ]; then",
|
|
1037
|
+
" timer=$(cat \"$timerfile\")",
|
|
1038
|
+
" kill -KILL \"$timer\" 2>/dev/null || true",
|
|
1039
|
+
"fi",
|
|
1040
|
+
"if [ -f \"$pidfile\" ]; then",
|
|
1041
|
+
" pid=$(cat \"$pidfile\")",
|
|
1042
|
+
" kill -KILL \"-$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
|
|
1043
|
+
" i=0",
|
|
1044
|
+
" while [ \"$i\" -lt 50 ]; do",
|
|
1045
|
+
" kill -0 \"$pid\" 2>/dev/null || break",
|
|
1046
|
+
" sleep 0.02",
|
|
1047
|
+
" i=$((i + 1))",
|
|
1048
|
+
" done",
|
|
1049
|
+
" rm -f \"$pidfile\" \"$timerfile\" \"$timeoutfile\"",
|
|
1050
|
+
"fi",
|
|
1051
|
+
].join("\n"),
|
|
1052
|
+
args: [pidFile],
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
export function microsandboxResourceName(opts) {
|
|
1056
|
+
return [
|
|
1057
|
+
sanitizeMicrosandboxNamePart(opts.prefix ?? "oma"),
|
|
1058
|
+
sanitizeMicrosandboxNamePart(opts.workspaceId),
|
|
1059
|
+
sanitizeMicrosandboxNamePart(opts.sessionId),
|
|
1060
|
+
sanitizeMicrosandboxNamePart(opts.purpose),
|
|
1061
|
+
(opts.now?.() ?? Date.now()).toString(36),
|
|
1062
|
+
(opts.random?.() ?? Math.random()).toString(36).slice(2, 8),
|
|
1063
|
+
].join("-");
|
|
1064
|
+
}
|
|
1065
|
+
export function execMicrosandboxCommand(command, args, opts = {}) {
|
|
1066
|
+
return new Promise((resolve, reject) => {
|
|
1067
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MICROSANDBOX_MAX_BUFFER;
|
|
1068
|
+
const child = spawn(command, [...args], {
|
|
1069
|
+
cwd: opts.cwd,
|
|
1070
|
+
env: opts.env,
|
|
1071
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1072
|
+
});
|
|
1073
|
+
const stdout = [];
|
|
1074
|
+
const stderr = [];
|
|
1075
|
+
let outputBytes = 0;
|
|
1076
|
+
let settled = false;
|
|
1077
|
+
let terminalError;
|
|
1078
|
+
let timeout;
|
|
1079
|
+
const finish = (result) => {
|
|
1080
|
+
if (settled)
|
|
1081
|
+
return;
|
|
1082
|
+
settled = true;
|
|
1083
|
+
if (timeout)
|
|
1084
|
+
clearTimeout(timeout);
|
|
1085
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1086
|
+
if (terminalError)
|
|
1087
|
+
reject(terminalError);
|
|
1088
|
+
else
|
|
1089
|
+
resolve(result);
|
|
1090
|
+
};
|
|
1091
|
+
const fail = (error) => {
|
|
1092
|
+
if (settled)
|
|
1093
|
+
return;
|
|
1094
|
+
settled = true;
|
|
1095
|
+
if (timeout)
|
|
1096
|
+
clearTimeout(timeout);
|
|
1097
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1098
|
+
reject(error);
|
|
1099
|
+
};
|
|
1100
|
+
const onAbort = () => {
|
|
1101
|
+
if (settled || terminalError)
|
|
1102
|
+
return;
|
|
1103
|
+
terminalError = new Error("aborted");
|
|
1104
|
+
terminalError.name = "AbortError";
|
|
1105
|
+
child.kill("SIGKILL");
|
|
1106
|
+
};
|
|
1107
|
+
const collect = (target, chunk, onStreamData) => {
|
|
1108
|
+
outputBytes += chunk.byteLength;
|
|
1109
|
+
if (outputBytes > maxBuffer) {
|
|
1110
|
+
if (!terminalError) {
|
|
1111
|
+
terminalError = new Error(`Microsandbox command output exceeded ${maxBuffer} bytes`);
|
|
1112
|
+
child.kill("SIGKILL");
|
|
1113
|
+
}
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
target.push(chunk);
|
|
1117
|
+
onStreamData?.(chunk);
|
|
1118
|
+
opts.onData?.(chunk);
|
|
1119
|
+
};
|
|
1120
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk, opts.onStdout));
|
|
1121
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk, opts.onStderr));
|
|
1122
|
+
child.stdin.on("error", fail);
|
|
1123
|
+
child.stdout.on("error", fail);
|
|
1124
|
+
child.stderr.on("error", fail);
|
|
1125
|
+
child.on("error", fail);
|
|
1126
|
+
child.on("close", (status, signal) => finish({
|
|
1127
|
+
status,
|
|
1128
|
+
signal,
|
|
1129
|
+
stdout: Buffer.concat(stdout),
|
|
1130
|
+
stderr: Buffer.concat(stderr),
|
|
1131
|
+
}));
|
|
1132
|
+
if (opts.signal?.aborted)
|
|
1133
|
+
onAbort();
|
|
1134
|
+
else
|
|
1135
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1136
|
+
try {
|
|
1137
|
+
if (opts.input !== undefined)
|
|
1138
|
+
child.stdin.end(opts.input);
|
|
1139
|
+
else
|
|
1140
|
+
child.stdin.end();
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
fail(error);
|
|
1144
|
+
}
|
|
1145
|
+
if (opts.timeoutMs !== undefined) {
|
|
1146
|
+
timeout = setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
export function execMicrosandboxCommandSync(command, args, opts = {}) {
|
|
1151
|
+
const result = spawnSync(command, [...args], {
|
|
1152
|
+
cwd: opts.cwd,
|
|
1153
|
+
env: opts.env,
|
|
1154
|
+
input: opts.input,
|
|
1155
|
+
maxBuffer: opts.maxBuffer ?? DEFAULT_MICROSANDBOX_MAX_BUFFER,
|
|
1156
|
+
timeout: opts.timeoutMs,
|
|
1157
|
+
encoding: "buffer",
|
|
1158
|
+
});
|
|
1159
|
+
if (result.error)
|
|
1160
|
+
throw result.error;
|
|
1161
|
+
return {
|
|
1162
|
+
stdout: result.stdout,
|
|
1163
|
+
stderr: result.stderr,
|
|
1164
|
+
status: result.status,
|
|
1165
|
+
signal: result.signal,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
export function assertInsideMicrosandboxWorkspace(absolutePath, workspacePath = DEFAULT_MICROSANDBOX_WORKSPACE) {
|
|
1169
|
+
if (!posix.isAbsolute(absolutePath)) {
|
|
1170
|
+
throw new Error(`Sandbox path must be absolute: ${absolutePath}`);
|
|
1171
|
+
}
|
|
1172
|
+
const root = posix.resolve(workspacePath);
|
|
1173
|
+
const path = posix.resolve(absolutePath);
|
|
1174
|
+
const rel = posix.relative(root, path);
|
|
1175
|
+
if (rel === "" || (!rel.startsWith("..") && !posix.isAbsolute(rel))) {
|
|
1176
|
+
return path;
|
|
1177
|
+
}
|
|
1178
|
+
throw new Error(`Sandbox path escapes workspace: ${absolutePath}`);
|
|
1179
|
+
}
|
|
1180
|
+
export function assertInsideMicrosandboxSearchRoot(absolutePath, roots) {
|
|
1181
|
+
if (!posix.isAbsolute(absolutePath)) {
|
|
1182
|
+
throw new Error(`Sandbox search path must be absolute: ${absolutePath}`);
|
|
1183
|
+
}
|
|
1184
|
+
const path = posix.resolve(absolutePath);
|
|
1185
|
+
for (const rootPath of roots) {
|
|
1186
|
+
const root = posix.resolve(rootPath);
|
|
1187
|
+
const rel = posix.relative(root, path);
|
|
1188
|
+
if (rel === "" || (!rel.startsWith("..") && !posix.isAbsolute(rel))) {
|
|
1189
|
+
return path;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
throw new Error(`Sandbox search path escapes approved roots: ${absolutePath}`);
|
|
1193
|
+
}
|
|
1194
|
+
export function assertInsideMicrosandboxUploadsPath(absolutePath, uploadsPath = DEFAULT_MICROSANDBOX_UPLOADS_PATH) {
|
|
1195
|
+
if (!posix.isAbsolute(absolutePath)) {
|
|
1196
|
+
throw new Error(`Session file mount path must be absolute: ${absolutePath}`);
|
|
1197
|
+
}
|
|
1198
|
+
const root = posix.resolve(uploadsPath);
|
|
1199
|
+
const path = posix.resolve(absolutePath);
|
|
1200
|
+
const rel = posix.relative(root, path);
|
|
1201
|
+
if (rel !== "" && !rel.startsWith("..") && !posix.isAbsolute(rel)) {
|
|
1202
|
+
return rel;
|
|
1203
|
+
}
|
|
1204
|
+
throw new Error(`Session file mount path escapes uploads root: ${absolutePath}`);
|
|
1205
|
+
}
|
|
1206
|
+
export function assertInsideMicrosandboxOutputPath(absolutePath, outputsPath = DEFAULT_MICROSANDBOX_OUTPUTS_PATH) {
|
|
1207
|
+
if (!posix.isAbsolute(absolutePath)) {
|
|
1208
|
+
throw new Error(`Session output path must be absolute: ${absolutePath}`);
|
|
1209
|
+
}
|
|
1210
|
+
const root = posix.resolve(outputsPath);
|
|
1211
|
+
const path = posix.resolve(absolutePath);
|
|
1212
|
+
const rel = posix.relative(root, path);
|
|
1213
|
+
if (rel !== "" && !rel.startsWith("..") && !posix.isAbsolute(rel)) {
|
|
1214
|
+
return path;
|
|
1215
|
+
}
|
|
1216
|
+
throw new Error(`Session output path escapes outputs root: ${absolutePath}`);
|
|
1217
|
+
}
|
|
1218
|
+
export function directoryNamesPrunedByIgnoreGlobs(ignore) {
|
|
1219
|
+
const names = new Set();
|
|
1220
|
+
for (const pattern of ignore) {
|
|
1221
|
+
const normalized = pattern.replaceAll("\\", "/");
|
|
1222
|
+
const match = /(?:^|\/)([^/*?[\]{}!]+)\/\*\*$/.exec(normalized);
|
|
1223
|
+
if (match?.[1])
|
|
1224
|
+
names.add(match[1]);
|
|
1225
|
+
}
|
|
1226
|
+
return [...names].sort();
|
|
1227
|
+
}
|
|
1228
|
+
export async function reapMicrosandboxSandboxes(opts) {
|
|
1229
|
+
const cli = opts.cli ??
|
|
1230
|
+
new NodeMicrosandboxCli({
|
|
1231
|
+
command: opts.command ?? DEFAULT_MICROSANDBOX_COMMAND,
|
|
1232
|
+
});
|
|
1233
|
+
const resourceNamePrefix = opts.resourceNamePrefix ?? "oma";
|
|
1234
|
+
const listedSandboxes = await microsandboxChecked(cli, buildMicrosandboxListArgs([
|
|
1235
|
+
`${SANDBOX_LABEL_KEY}=${SANDBOX_LABEL_VALUE}`,
|
|
1236
|
+
`${OWNER_LABEL_KEY}=${OWNER_LABEL_VALUE}`,
|
|
1237
|
+
...(opts.labelFilters ?? []),
|
|
1238
|
+
]));
|
|
1239
|
+
const sandboxNames = parseMicrosandboxListedNames(listedSandboxes.stdout);
|
|
1240
|
+
const now = opts.now?.() ?? Date.now();
|
|
1241
|
+
const attachedVolumes = new Set();
|
|
1242
|
+
const expiredSandboxes = [];
|
|
1243
|
+
for (const name of sandboxNames) {
|
|
1244
|
+
try {
|
|
1245
|
+
const inspected = await microsandboxChecked(cli, buildMicrosandboxInspectArgs(name));
|
|
1246
|
+
const createdAt = parseMicrosandboxCreatedAt(inspected.stdout);
|
|
1247
|
+
if (now - createdAt.getTime() >= opts.olderThanMs) {
|
|
1248
|
+
expiredSandboxes.push(name);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
collectMicrosandboxVolumeRefs(inspected.stdout, attachedVolumes);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
// One stale/corrupt resource must not wedge all future session creation.
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
for (const name of expiredSandboxes) {
|
|
1259
|
+
await microsandboxChecked(cli, buildMicrosandboxRemoveArgs(name)).catch(() => undefined);
|
|
1260
|
+
}
|
|
1261
|
+
const listedVolumes = await microsandboxChecked(cli, buildMicrosandboxVolumeListArgs());
|
|
1262
|
+
const volumeNames = parseMicrosandboxListedNames(listedVolumes.stdout).filter((name) => isOmaMicrosandboxWorkspaceVolumeName(name, resourceNamePrefix) &&
|
|
1263
|
+
!attachedVolumes.has(name));
|
|
1264
|
+
const expiredVolumes = [];
|
|
1265
|
+
for (const name of volumeNames) {
|
|
1266
|
+
try {
|
|
1267
|
+
const inspected = await microsandboxChecked(cli, buildMicrosandboxVolumeInspectArgs(name));
|
|
1268
|
+
const createdAt = parseMicrosandboxCreatedAt(inspected.stdout);
|
|
1269
|
+
if (now - createdAt.getTime() >= opts.olderThanMs) {
|
|
1270
|
+
expiredVolumes.push(name);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// Skip malformed or concurrently removed volumes.
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
for (const name of expiredVolumes) {
|
|
1278
|
+
await microsandboxChecked(cli, buildMicrosandboxVolumeRemoveArgs(name)).catch(() => undefined);
|
|
1279
|
+
}
|
|
1280
|
+
return expiredSandboxes.length + expiredVolumes.length;
|
|
1281
|
+
}
|
|
1282
|
+
function resolveMicrosandboxOptions(opts) {
|
|
1283
|
+
return {
|
|
1284
|
+
image: opts.image ?? DEFAULT_MICROSANDBOX_IMAGE,
|
|
1285
|
+
cli: opts.cli ??
|
|
1286
|
+
new NodeMicrosandboxCli({ command: opts.command ?? DEFAULT_MICROSANDBOX_COMMAND }),
|
|
1287
|
+
workspacePath: opts.workspacePath ?? DEFAULT_MICROSANDBOX_WORKSPACE,
|
|
1288
|
+
uploadsPath: DEFAULT_MICROSANDBOX_UPLOADS_PATH,
|
|
1289
|
+
outputsPath: DEFAULT_MICROSANDBOX_OUTPUTS_PATH,
|
|
1290
|
+
operationTimeoutMs: opts.operationTimeoutMs ?? DEFAULT_MICROSANDBOX_OPERATION_TIMEOUT_MS,
|
|
1291
|
+
startupTimeoutMs: opts.startupTimeoutMs ?? DEFAULT_MICROSANDBOX_STARTUP_TIMEOUT_MS,
|
|
1292
|
+
resourceNamePrefix: opts.resourceNamePrefix ?? "oma",
|
|
1293
|
+
cpus: opts.cpus ?? DEFAULT_MICROSANDBOX_CPUS,
|
|
1294
|
+
memory: opts.memory ?? DEFAULT_MICROSANDBOX_MEMORY,
|
|
1295
|
+
ociUpperSize: opts.ociUpperSize ?? DEFAULT_MICROSANDBOX_OCI_UPPER_SIZE,
|
|
1296
|
+
maxDuration: opts.maxDuration ?? DEFAULT_MICROSANDBOX_MAX_DURATION,
|
|
1297
|
+
security: opts.security ?? DEFAULT_MICROSANDBOX_SECURITY,
|
|
1298
|
+
maxOutputFiles: opts.maxOutputFiles ?? MAX_SESSION_OUTPUT_FILES,
|
|
1299
|
+
maxOutputFileBytes: opts.maxOutputFileBytes ?? MAX_SESSION_OUTPUT_FILE_BYTES,
|
|
1300
|
+
maxOutputBytes: opts.maxOutputBytes ?? MAX_SESSION_OUTPUT_BYTES,
|
|
1301
|
+
now: opts.now,
|
|
1302
|
+
random: opts.random,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async function microsandboxChecked(cli, args, opts = {}) {
|
|
1306
|
+
const result = await cli.exec(args, opts);
|
|
1307
|
+
if (result.signal !== null || result.status !== 0) {
|
|
1308
|
+
throw new Error(`msb ${args.join(" ")} failed with ${result.signal ?? result.status}: ${errorText(result)}`);
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
stdout: result.stdout,
|
|
1312
|
+
stderr: result.stderr,
|
|
1313
|
+
exitCode: result.status,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function forceRemoveMicrosandboxSandbox(cli, sandboxName, resolved) {
|
|
1317
|
+
try {
|
|
1318
|
+
cli.execSync(buildMicrosandboxRemoveArgs(sandboxName), {
|
|
1319
|
+
timeoutMs: resolved.operationTimeoutMs,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
// Synchronous dispose mirrors Docker-local: cleanup is best-effort at the
|
|
1324
|
+
// terminal hook, while create-time partial cleanup preserves the original
|
|
1325
|
+
// construction error.
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async function removeMicrosandboxSandboxChecked(cli, sandboxName, timeoutMs) {
|
|
1329
|
+
const result = await cli.exec(buildMicrosandboxRemoveArgs(sandboxName), {
|
|
1330
|
+
timeoutMs,
|
|
1331
|
+
});
|
|
1332
|
+
if (result.signal === null && result.status === 0)
|
|
1333
|
+
return;
|
|
1334
|
+
const detail = errorText(result);
|
|
1335
|
+
if (/not found|does not exist|no such/i.test(detail))
|
|
1336
|
+
return;
|
|
1337
|
+
throw new Error(`Failed to remove poisoned microsandbox ${sandboxName}: ${detail}`);
|
|
1338
|
+
}
|
|
1339
|
+
function forceRemoveMicrosandboxVolume(cli, volumeName, resolved) {
|
|
1340
|
+
try {
|
|
1341
|
+
cli.execSync(buildMicrosandboxVolumeRemoveArgs(volumeName), {
|
|
1342
|
+
timeoutMs: resolved.operationTimeoutMs,
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
// See forceRemoveMicrosandboxSandbox.
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function killMicrosandboxGuestProcessGroup(cli, sandboxName, pidFile, resolved) {
|
|
1350
|
+
const command = buildMicrosandboxKillProcessGroupCommand(pidFile);
|
|
1351
|
+
try {
|
|
1352
|
+
cli.execSync(buildMicrosandboxShellExecArgs({
|
|
1353
|
+
sandboxName,
|
|
1354
|
+
script: command.script,
|
|
1355
|
+
args: command.args,
|
|
1356
|
+
}), { timeoutMs: resolved.operationTimeoutMs });
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
// If the control-plane lost the msb exec handle, this follow-up kill is the
|
|
1360
|
+
// best remaining way to stop guest work without destroying the session.
|
|
1361
|
+
// A failed kill must not mask the original abort/timeout/output error.
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function errorText(result) {
|
|
1365
|
+
return (result.stderr.toString("utf8") ||
|
|
1366
|
+
result.stdout.toString("utf8") ||
|
|
1367
|
+
"no output");
|
|
1368
|
+
}
|
|
1369
|
+
function isAbortError(error) {
|
|
1370
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1371
|
+
}
|
|
1372
|
+
function isMicrosandboxOutputOverflow(error) {
|
|
1373
|
+
return (error instanceof Error &&
|
|
1374
|
+
error.message.startsWith("Microsandbox command output exceeded "));
|
|
1375
|
+
}
|
|
1376
|
+
function timeoutSecondsText(timeoutMs) {
|
|
1377
|
+
return `${Math.ceil(timeoutMs / 1000)}s`;
|
|
1378
|
+
}
|
|
1379
|
+
function parseOutputListing(stdout) {
|
|
1380
|
+
if (stdout.length === 0)
|
|
1381
|
+
return [];
|
|
1382
|
+
const fields = stdout.toString("utf8").split("\0");
|
|
1383
|
+
if (fields.at(-1) === "")
|
|
1384
|
+
fields.pop();
|
|
1385
|
+
if (fields.length % 3 !== 0) {
|
|
1386
|
+
throw new Error("Microsandbox output listing returned malformed records");
|
|
1387
|
+
}
|
|
1388
|
+
const out = [];
|
|
1389
|
+
for (let index = 0; index < fields.length; index += 3) {
|
|
1390
|
+
const relativePath = fields[index];
|
|
1391
|
+
const sizeBytes = Number(fields[index + 1]);
|
|
1392
|
+
const sha256 = fields[index + 2];
|
|
1393
|
+
if (relativePath.length === 0 ||
|
|
1394
|
+
posix.isAbsolute(relativePath) ||
|
|
1395
|
+
relativePath.split("/").includes("..")) {
|
|
1396
|
+
throw new Error(`Unsafe session output path: ${relativePath}`);
|
|
1397
|
+
}
|
|
1398
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 0) {
|
|
1399
|
+
throw new Error(`Invalid session output size: ${relativePath}`);
|
|
1400
|
+
}
|
|
1401
|
+
if (!/^[0-9a-f]{64}$/i.test(sha256)) {
|
|
1402
|
+
throw new Error(`Invalid session output checksum: ${relativePath}`);
|
|
1403
|
+
}
|
|
1404
|
+
out.push({ relativePath, sizeBytes, sha256 });
|
|
1405
|
+
}
|
|
1406
|
+
return out;
|
|
1407
|
+
}
|
|
1408
|
+
function parseMicrosandboxListedNames(stdout) {
|
|
1409
|
+
const text = stdout.toString("utf8").trim();
|
|
1410
|
+
if (text.length === 0)
|
|
1411
|
+
return [];
|
|
1412
|
+
const parsed = JSON.parse(text);
|
|
1413
|
+
if (!Array.isArray(parsed)) {
|
|
1414
|
+
throw new Error("Microsandbox list returned non-array JSON");
|
|
1415
|
+
}
|
|
1416
|
+
return parsed.map((entry) => {
|
|
1417
|
+
const name = typeof entry === "string"
|
|
1418
|
+
? entry
|
|
1419
|
+
: fieldString(entry, ["name", "Name", "id", "ID"]);
|
|
1420
|
+
assertMicrosandboxArg(name, "listed sandbox name");
|
|
1421
|
+
return name;
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
function parseMicrosandboxCreatedAt(stdout) {
|
|
1425
|
+
const parsed = JSON.parse(stdout.toString("utf8"));
|
|
1426
|
+
const raw = fieldString(parsed, [
|
|
1427
|
+
"created_at",
|
|
1428
|
+
"createdAt",
|
|
1429
|
+
"Created",
|
|
1430
|
+
"created",
|
|
1431
|
+
]);
|
|
1432
|
+
const date = new Date(raw);
|
|
1433
|
+
if (Number.isNaN(date.getTime())) {
|
|
1434
|
+
throw new Error(`Invalid microsandbox creation timestamp: ${raw}`);
|
|
1435
|
+
}
|
|
1436
|
+
return date;
|
|
1437
|
+
}
|
|
1438
|
+
function collectMicrosandboxVolumeRefs(stdout, out) {
|
|
1439
|
+
visitJsonStrings(JSON.parse(stdout.toString("utf8")), (value) => {
|
|
1440
|
+
for (const segment of value.split(":")) {
|
|
1441
|
+
if (segment.includes("-workspace-volume-"))
|
|
1442
|
+
out.add(segment);
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function visitJsonStrings(value, visit) {
|
|
1447
|
+
if (typeof value === "string") {
|
|
1448
|
+
visit(value);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
if (Array.isArray(value)) {
|
|
1452
|
+
value.forEach((entry) => visitJsonStrings(entry, visit));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof value === "object" && value !== null) {
|
|
1456
|
+
Object.values(value).forEach((entry) => visitJsonStrings(entry, visit));
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function isOmaMicrosandboxWorkspaceVolumeName(name, prefix) {
|
|
1460
|
+
return (name.startsWith(`${sanitizeMicrosandboxNamePart(prefix)}-`) &&
|
|
1461
|
+
name.includes("-workspace-volume-"));
|
|
1462
|
+
}
|
|
1463
|
+
function fieldString(value, fields) {
|
|
1464
|
+
if (typeof value !== "object" || value === null) {
|
|
1465
|
+
throw new Error("Microsandbox JSON entry must be an object");
|
|
1466
|
+
}
|
|
1467
|
+
const record = value;
|
|
1468
|
+
for (const field of fields) {
|
|
1469
|
+
const raw = record[field];
|
|
1470
|
+
if (typeof raw === "string" && raw.length > 0)
|
|
1471
|
+
return raw;
|
|
1472
|
+
}
|
|
1473
|
+
throw new Error(`Microsandbox JSON entry missing ${fields.join("/")}`);
|
|
1474
|
+
}
|
|
1475
|
+
function mimeTypeForFilename(filename) {
|
|
1476
|
+
const lower = filename.toLowerCase();
|
|
1477
|
+
if (lower.endsWith(".txt"))
|
|
1478
|
+
return "text/plain";
|
|
1479
|
+
if (lower.endsWith(".json"))
|
|
1480
|
+
return "application/json";
|
|
1481
|
+
if (lower.endsWith(".csv"))
|
|
1482
|
+
return "text/csv";
|
|
1483
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm"))
|
|
1484
|
+
return "text/html";
|
|
1485
|
+
if (lower.endsWith(".md"))
|
|
1486
|
+
return "text/markdown";
|
|
1487
|
+
if (lower.endsWith(".pptx")) {
|
|
1488
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1489
|
+
}
|
|
1490
|
+
if (lower.endsWith(".png"))
|
|
1491
|
+
return "image/png";
|
|
1492
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
1493
|
+
return "image/jpeg";
|
|
1494
|
+
if (lower.endsWith(".pdf"))
|
|
1495
|
+
return "application/pdf";
|
|
1496
|
+
return "application/octet-stream";
|
|
1497
|
+
}
|
|
1498
|
+
async function* microsandboxOutputBytes(read) {
|
|
1499
|
+
yield (await read()).stdout;
|
|
1500
|
+
}
|
|
1501
|
+
async function writeMountFile(tempRoot, relativePath, mount) {
|
|
1502
|
+
const segments = relativePath.split("/");
|
|
1503
|
+
const filePath = joinHostPath(tempRoot, ...segments);
|
|
1504
|
+
await mkdir(joinHostPath(tempRoot, ...segments.slice(0, -1)), {
|
|
1505
|
+
recursive: true,
|
|
1506
|
+
mode: 0o755,
|
|
1507
|
+
});
|
|
1508
|
+
const handle = await open(filePath, "w", 0o644);
|
|
1509
|
+
const hash = createHash("sha256");
|
|
1510
|
+
let size = 0;
|
|
1511
|
+
try {
|
|
1512
|
+
for await (const chunk of chunksForMount(mount.bytes)) {
|
|
1513
|
+
const buffer = Buffer.from(chunk);
|
|
1514
|
+
size += buffer.byteLength;
|
|
1515
|
+
hash.update(buffer);
|
|
1516
|
+
await handle.write(buffer);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
finally {
|
|
1520
|
+
await handle.close();
|
|
1521
|
+
}
|
|
1522
|
+
const sha256 = hash.digest("hex");
|
|
1523
|
+
if (size !== mount.sizeBytes) {
|
|
1524
|
+
throw new Error(`Session file mount ${mount.snapshotFileId} size mismatch: expected ${mount.sizeBytes}, got ${size}`);
|
|
1525
|
+
}
|
|
1526
|
+
if (sha256 !== mount.sha256) {
|
|
1527
|
+
throw new Error(`Session file mount ${mount.snapshotFileId} failed integrity validation`);
|
|
1528
|
+
}
|
|
1529
|
+
return filePath;
|
|
1530
|
+
}
|
|
1531
|
+
async function* chunksForMount(bytes) {
|
|
1532
|
+
if (bytes instanceof Uint8Array) {
|
|
1533
|
+
yield bytes;
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
yield* bytes;
|
|
1537
|
+
}
|
|
1538
|
+
function recordSandboxNotDisposed(disposed) {
|
|
1539
|
+
if (disposed.value)
|
|
1540
|
+
throw new Error("Sandbox provider is disposed");
|
|
1541
|
+
}
|
|
1542
|
+
function randomExecId() {
|
|
1543
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1544
|
+
}
|
|
1545
|
+
function bashDispatchSentinel(token) {
|
|
1546
|
+
return Buffer.from(`${BASH_DISPATCH_PREFIX}${token}\n`);
|
|
1547
|
+
}
|
|
1548
|
+
function bashTerminalPrefix(token) {
|
|
1549
|
+
return Buffer.from(`${BASH_TERMINAL_PREFIX}${token}:`);
|
|
1550
|
+
}
|
|
1551
|
+
export function createMicrosandboxBashDispatchFilter(token) {
|
|
1552
|
+
const dispatchSentinel = bashDispatchSentinel(token);
|
|
1553
|
+
const terminalPrefix = bashTerminalPrefix(token);
|
|
1554
|
+
let dispatchSeen = false;
|
|
1555
|
+
let terminalRecord;
|
|
1556
|
+
let pending = Buffer.alloc(0);
|
|
1557
|
+
let terminalCandidate = Buffer.alloc(0);
|
|
1558
|
+
const filterAfterDispatch = (chunk) => {
|
|
1559
|
+
pending = Buffer.concat([pending, chunk]);
|
|
1560
|
+
const index = pending.indexOf(terminalPrefix);
|
|
1561
|
+
if (index >= 0) {
|
|
1562
|
+
const lineEnd = pending.indexOf("\n", index);
|
|
1563
|
+
if (lineEnd < 0) {
|
|
1564
|
+
const out = pending.subarray(0, index);
|
|
1565
|
+
pending = pending.subarray(index);
|
|
1566
|
+
return out;
|
|
1567
|
+
}
|
|
1568
|
+
const line = pending.subarray(index, lineEnd).toString("utf8");
|
|
1569
|
+
terminalRecord = parseBashTerminalRecord(line, token);
|
|
1570
|
+
const out = Buffer.concat([
|
|
1571
|
+
pending.subarray(0, index),
|
|
1572
|
+
pending.subarray(lineEnd + 1),
|
|
1573
|
+
]);
|
|
1574
|
+
pending = Buffer.alloc(0);
|
|
1575
|
+
return out;
|
|
1576
|
+
}
|
|
1577
|
+
if (terminalCandidate.length > 0) {
|
|
1578
|
+
const combined = Buffer.concat([terminalCandidate, pending]);
|
|
1579
|
+
if (isPrefixOf(combined, terminalPrefix)) {
|
|
1580
|
+
terminalCandidate = combined;
|
|
1581
|
+
pending = Buffer.alloc(0);
|
|
1582
|
+
return Buffer.alloc(0);
|
|
1583
|
+
}
|
|
1584
|
+
const out = combined;
|
|
1585
|
+
terminalCandidate = Buffer.alloc(0);
|
|
1586
|
+
pending = Buffer.alloc(0);
|
|
1587
|
+
return out;
|
|
1588
|
+
}
|
|
1589
|
+
const candidateStart = findTerminalPrefixCandidateStart(pending, terminalPrefix);
|
|
1590
|
+
if (candidateStart < 0) {
|
|
1591
|
+
const out = pending;
|
|
1592
|
+
pending = Buffer.alloc(0);
|
|
1593
|
+
return out;
|
|
1594
|
+
}
|
|
1595
|
+
const candidate = pending.subarray(candidateStart);
|
|
1596
|
+
if (isPrefixOf(candidate, terminalPrefix)) {
|
|
1597
|
+
const out = pending.subarray(0, candidateStart);
|
|
1598
|
+
terminalCandidate = candidate;
|
|
1599
|
+
pending = Buffer.alloc(0);
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1602
|
+
const out = pending;
|
|
1603
|
+
pending = Buffer.alloc(0);
|
|
1604
|
+
return out;
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
chunk: (chunk) => {
|
|
1608
|
+
if (dispatchSeen)
|
|
1609
|
+
return filterAfterDispatch(chunk);
|
|
1610
|
+
pending = Buffer.concat([pending, chunk]);
|
|
1611
|
+
const index = pending.indexOf(dispatchSentinel);
|
|
1612
|
+
if (index < 0)
|
|
1613
|
+
return Buffer.alloc(0);
|
|
1614
|
+
dispatchSeen = true;
|
|
1615
|
+
const afterDispatch = Buffer.concat([
|
|
1616
|
+
pending.subarray(0, index),
|
|
1617
|
+
pending.subarray(index + dispatchSentinel.length),
|
|
1618
|
+
]);
|
|
1619
|
+
pending = Buffer.alloc(0);
|
|
1620
|
+
return filterAfterDispatch(afterDispatch);
|
|
1621
|
+
},
|
|
1622
|
+
dispatchSeen: () => dispatchSeen,
|
|
1623
|
+
terminalRecord: () => terminalRecord,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function parseBashTerminalRecord(line, token) {
|
|
1627
|
+
const prefix = `${BASH_TERMINAL_PREFIX}${token}:`;
|
|
1628
|
+
if (!line.startsWith(prefix))
|
|
1629
|
+
return undefined;
|
|
1630
|
+
const payload = line.slice(prefix.length);
|
|
1631
|
+
const [kind, exitCodeText, extra] = payload.split(":");
|
|
1632
|
+
if (extra !== undefined || !/^(0|[1-9][0-9]*)$/.test(exitCodeText ?? "")) {
|
|
1633
|
+
return undefined;
|
|
1634
|
+
}
|
|
1635
|
+
const exitCode = Number(exitCodeText);
|
|
1636
|
+
if (kind === "exit")
|
|
1637
|
+
return { kind, exitCode };
|
|
1638
|
+
if (kind === "timeout")
|
|
1639
|
+
return { kind, exitCode };
|
|
1640
|
+
return undefined;
|
|
1641
|
+
}
|
|
1642
|
+
function findTerminalPrefixCandidateStart(buffer, terminalPrefix) {
|
|
1643
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
1644
|
+
if (buffer[index] === terminalPrefix[0])
|
|
1645
|
+
return index;
|
|
1646
|
+
}
|
|
1647
|
+
return -1;
|
|
1648
|
+
}
|
|
1649
|
+
function isPrefixOf(candidate, value) {
|
|
1650
|
+
if (candidate.length > value.length)
|
|
1651
|
+
return false;
|
|
1652
|
+
return value.subarray(0, candidate.length).equals(candidate);
|
|
1653
|
+
}
|
|
1654
|
+
function assertMicrosandboxArg(value, label) {
|
|
1655
|
+
if (value.length === 0 || value.startsWith("-")) {
|
|
1656
|
+
throw new Error(`Microsandbox ${label} cannot be empty or start with '-'`);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
function microsandboxLabelArg(key, value) {
|
|
1660
|
+
if (key.length === 0 ||
|
|
1661
|
+
key.startsWith("-") ||
|
|
1662
|
+
key.includes("=") ||
|
|
1663
|
+
value.startsWith("-")) {
|
|
1664
|
+
throw new Error("Microsandbox labels cannot be empty, flag-like, or contain '=' in keys");
|
|
1665
|
+
}
|
|
1666
|
+
return `${key}=${value}`;
|
|
1667
|
+
}
|
|
1668
|
+
function microsandboxLabelFilterArg(label) {
|
|
1669
|
+
if (label.length === 0 || label.startsWith("-")) {
|
|
1670
|
+
throw new Error("Microsandbox label filters cannot be empty or flag-like");
|
|
1671
|
+
}
|
|
1672
|
+
return label;
|
|
1673
|
+
}
|
|
1674
|
+
function sanitizeMicrosandboxNamePart(value) {
|
|
1675
|
+
const sanitized = value.toLowerCase().replaceAll(/[^a-z0-9_.-]+/g, "-");
|
|
1676
|
+
return sanitized.replaceAll(/^-+|-+$/g, "") || "x";
|
|
1677
|
+
}
|