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,347 @@
|
|
|
1
|
+
// Per-session dual-homed egress proxy sidecar (plan 0117d; ADR 0016 §2/§3).
|
|
2
|
+
//
|
|
3
|
+
// Topology (probe-validated 2026-07-05): an in-process host proxy is
|
|
4
|
+
// unreachable from a confined sandbox — an `--internal` Docker network drops
|
|
5
|
+
// host-gateway traffic, and a plain bridge leaks the whole internet. The only
|
|
6
|
+
// mode giving both reachability AND confinement is a dual-homed proxy:
|
|
7
|
+
//
|
|
8
|
+
// sandbox ──[oma-egress-<sid>, --internal]──▶ proxy sidecar ──[bridge]──▶ upstreams
|
|
9
|
+
//
|
|
10
|
+
// The sandbox joins the internal-only network, whose sole route out is the
|
|
11
|
+
// sidecar; a raw socket to anything else is dropped by --internal. The sidecar
|
|
12
|
+
// runs the SAME appliance image, only a different command (egress-proxy-main),
|
|
13
|
+
// so the proxy lives OUT of the long-lived control-plane process and the
|
|
14
|
+
// control plane never joins the sandbox network.
|
|
15
|
+
//
|
|
16
|
+
// Secret/CA delivery: the control plane writes the resolved SessionEgressBundle
|
|
17
|
+
// (real secrets) to a sidecar-only mount (mode 0600, never the sandbox, never
|
|
18
|
+
// env). The sidecar mints its own MITM CA and publishes only the cert to a
|
|
19
|
+
// shared dir; the CA private key never leaves the sidecar. The control plane
|
|
20
|
+
// waits for the sidecar's `ready` marker, then mounts that cert into the
|
|
21
|
+
// sandbox trust bundle.
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
const SIDECAR_LABEL_KEY = "open-managed-agents.egress-sidecar";
|
|
27
|
+
const SIDECAR_LABEL_VALUE = "docker-local";
|
|
28
|
+
const DEFAULT_READINESS_TIMEOUT_MS = 20_000;
|
|
29
|
+
export const DEFAULT_SIDECAR_PORT = 8080;
|
|
30
|
+
const DEFAULT_SIDECAR_MEMORY = "256m";
|
|
31
|
+
const DEFAULT_SIDECAR_PIDS_LIMIT = "128";
|
|
32
|
+
const DEFAULT_SIDECAR_TMPFS_SIZE = "64m";
|
|
33
|
+
export const SIDECAR_NETWORK_ALIAS = "oma-egress-proxy";
|
|
34
|
+
/** `<uid>:<gid>` the sidecar runs as: the control-plane's own ids. */
|
|
35
|
+
function currentUserSpec() {
|
|
36
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
37
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 0;
|
|
38
|
+
return `${uid}:${gid}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The proxy URL a sandbox uses: auth token rides in the password slot. The
|
|
42
|
+
* token is percent-encoded so a URL delimiter (`@ : / ? #`) can't malform the
|
|
43
|
+
* URL or change the credentials the client sends (the seam also constrains the
|
|
44
|
+
* charset — {@link resolveSessionEgressBundle} — so this is defense in depth).
|
|
45
|
+
*/
|
|
46
|
+
export function egressProxyUrl(sidecar) {
|
|
47
|
+
const token = encodeURIComponent(sidecar.proxyAuthToken);
|
|
48
|
+
return `http://srt:${token}@${sidecar.proxyHost}:${sidecar.proxyPort}`;
|
|
49
|
+
}
|
|
50
|
+
export async function createEgressSidecar(opts) {
|
|
51
|
+
const now = opts.now ?? Date.now;
|
|
52
|
+
const port = opts.bundle.listenPort || DEFAULT_SIDECAR_PORT;
|
|
53
|
+
const suffix = `${now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
const networkName = `oma-egress-${sanitize(opts.sessionId)}-${suffix}`;
|
|
55
|
+
const containerName = `oma-egress-proxy-${sanitize(opts.sessionId)}-${suffix}`;
|
|
56
|
+
const root = mkdtempSync(join(tmpdir(), "oma-egress-"));
|
|
57
|
+
const sharedDir = join(root, "shared");
|
|
58
|
+
const bundlePath = join(root, "bundle.json");
|
|
59
|
+
// Real secrets live here; only the sidecar mounts it, and only the owner
|
|
60
|
+
// can read it.
|
|
61
|
+
mkdirSync(sharedDir, { recursive: true });
|
|
62
|
+
writeFileSync(bundlePath, JSON.stringify(opts.bundle), { mode: 0o600 });
|
|
63
|
+
let networkCreated = false;
|
|
64
|
+
let containerStarted = false;
|
|
65
|
+
const dispose = () => {
|
|
66
|
+
if (containerStarted) {
|
|
67
|
+
spawnSync(opts.dockerCommand, ["rm", "-f", containerName], {
|
|
68
|
+
stdio: "ignore",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (networkCreated) {
|
|
72
|
+
spawnSync(opts.dockerCommand, ["network", "rm", networkName], {
|
|
73
|
+
stdio: "ignore",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
dockerOrThrow(opts.dockerCommand, [
|
|
80
|
+
"network",
|
|
81
|
+
"create",
|
|
82
|
+
"--internal",
|
|
83
|
+
"--label",
|
|
84
|
+
`${SIDECAR_LABEL_KEY}=${SIDECAR_LABEL_VALUE}`,
|
|
85
|
+
"--label",
|
|
86
|
+
`open-managed-agents.session-id=${opts.sessionId}`,
|
|
87
|
+
networkName,
|
|
88
|
+
], opts.operationTimeoutMs);
|
|
89
|
+
networkCreated = true;
|
|
90
|
+
dockerOrThrow(opts.dockerCommand, buildSidecarRunArgs({
|
|
91
|
+
containerName,
|
|
92
|
+
image: opts.sidecarImage,
|
|
93
|
+
sharedDir,
|
|
94
|
+
bundlePath,
|
|
95
|
+
repoMount: opts.sidecarRepoMount,
|
|
96
|
+
user: currentUserSpec(),
|
|
97
|
+
labels: sidecarLabels(opts.labels, opts.sessionId),
|
|
98
|
+
}), opts.operationTimeoutMs);
|
|
99
|
+
containerStarted = true;
|
|
100
|
+
// Dual-home: the run above put the sidecar on the default bridge (upstream
|
|
101
|
+
// egress); now attach the per-session internal net (sandbox side).
|
|
102
|
+
dockerOrThrow(opts.dockerCommand, sidecarNetworkConnectArgs(networkName, containerName), opts.operationTimeoutMs);
|
|
103
|
+
await waitForReady(join(sharedDir, "ready"), opts.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, now, opts.sleepMs ?? sleepAsync, () => sidecarLogs(opts.dockerCommand, containerName));
|
|
104
|
+
// The sidecar has read the bundle into memory by the time it signals ready.
|
|
105
|
+
// Drop the plaintext-secret file from disk now so a later control-plane
|
|
106
|
+
// crash (before dispose) leaves no resolved secrets behind — the at-rest
|
|
107
|
+
// window shrinks to the ~1s of sidecar startup.
|
|
108
|
+
rmSync(bundlePath, { force: true });
|
|
109
|
+
return {
|
|
110
|
+
networkName,
|
|
111
|
+
// Container names include full session IDs and can exceed DNS's 63-byte
|
|
112
|
+
// label limit. The network is per-session, so one short stable alias is
|
|
113
|
+
// both unambiguous and resolvable from the sandbox.
|
|
114
|
+
proxyHost: SIDECAR_NETWORK_ALIAS,
|
|
115
|
+
proxyPort: port,
|
|
116
|
+
proxyAuthToken: opts.bundle.proxyAuthToken,
|
|
117
|
+
sharedDirHostPath: sharedDir,
|
|
118
|
+
dispose,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
dispose();
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export function sidecarNetworkConnectArgs(networkName, containerName) {
|
|
127
|
+
return [
|
|
128
|
+
"network",
|
|
129
|
+
"connect",
|
|
130
|
+
"--alias",
|
|
131
|
+
SIDECAR_NETWORK_ALIAS,
|
|
132
|
+
networkName,
|
|
133
|
+
containerName,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Merge caller labels with the reserved ownership labels the crash reaper
|
|
138
|
+
* matches on. Caller labels are applied FIRST so they can never shadow the
|
|
139
|
+
* reserved keys — otherwise a caller could hide a sidecar from reaping.
|
|
140
|
+
*/
|
|
141
|
+
export function sidecarLabels(callerLabels, sessionId) {
|
|
142
|
+
return {
|
|
143
|
+
...callerLabels,
|
|
144
|
+
[SIDECAR_LABEL_KEY]: SIDECAR_LABEL_VALUE,
|
|
145
|
+
"open-managed-agents.session-id": sessionId,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function buildSidecarRunArgs(opts) {
|
|
149
|
+
const labels = Object.entries(opts.labels).flatMap(([k, v]) => [
|
|
150
|
+
"--label",
|
|
151
|
+
`${k}=${v}`,
|
|
152
|
+
]);
|
|
153
|
+
return [
|
|
154
|
+
"run",
|
|
155
|
+
"-d",
|
|
156
|
+
"--name",
|
|
157
|
+
opts.containerName,
|
|
158
|
+
// Start on the default bridge for upstream egress; the internal net is
|
|
159
|
+
// attached afterwards. The sidecar is now part of the security boundary
|
|
160
|
+
// (it holds resolved secrets + the MITM CA and is reachable by the
|
|
161
|
+
// sandbox), so it gets the same least-privilege hardening as the sandbox
|
|
162
|
+
// (docker.ts buildDockerRunArgs): no caps, no new privs, read-only root,
|
|
163
|
+
// non-root, resource-bounded. Its ONLY writable surface is a small tmpfs
|
|
164
|
+
// (CA minting) plus the shared dir it must publish ca.crt/ready to.
|
|
165
|
+
"--network",
|
|
166
|
+
"bridge",
|
|
167
|
+
"--cap-drop",
|
|
168
|
+
"ALL",
|
|
169
|
+
"--security-opt",
|
|
170
|
+
"no-new-privileges",
|
|
171
|
+
"--read-only",
|
|
172
|
+
"--tmpfs",
|
|
173
|
+
`/tmp:rw,nosuid,nodev,size=${opts.tmpfsSize ?? DEFAULT_SIDECAR_TMPFS_SIZE}`,
|
|
174
|
+
"--user",
|
|
175
|
+
opts.user,
|
|
176
|
+
"--memory",
|
|
177
|
+
opts.memory ?? DEFAULT_SIDECAR_MEMORY,
|
|
178
|
+
"--pids-limit",
|
|
179
|
+
opts.pidsLimit ?? DEFAULT_SIDECAR_PIDS_LIMIT,
|
|
180
|
+
...labels,
|
|
181
|
+
...(opts.repoMount ? ["-v", `${opts.repoMount}:/app:ro`] : []),
|
|
182
|
+
"-v",
|
|
183
|
+
`${opts.sharedDir}:/shared`,
|
|
184
|
+
"-v",
|
|
185
|
+
`${opts.bundlePath}:/bundle.json:ro`,
|
|
186
|
+
"--env",
|
|
187
|
+
"OMA_EGRESS_BUNDLE_PATH=/bundle.json",
|
|
188
|
+
"--env",
|
|
189
|
+
"OMA_EGRESS_SHARED_DIR=/shared",
|
|
190
|
+
// Read-only root + non-root: give node a writable HOME on the tmpfs.
|
|
191
|
+
"--env",
|
|
192
|
+
"HOME=/tmp",
|
|
193
|
+
"--workdir",
|
|
194
|
+
"/app",
|
|
195
|
+
opts.image,
|
|
196
|
+
"node",
|
|
197
|
+
"--experimental-transform-types",
|
|
198
|
+
"--disable-warning=ExperimentalWarning",
|
|
199
|
+
"src/egress-proxy-main.ts",
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
async function waitForReady(readyPath, timeoutMs, now, sleepMs, logs) {
|
|
203
|
+
const deadline = now() + timeoutMs;
|
|
204
|
+
for (;;) {
|
|
205
|
+
try {
|
|
206
|
+
statSync(readyPath);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// not ready yet
|
|
211
|
+
}
|
|
212
|
+
if (now() >= deadline) {
|
|
213
|
+
throw new Error(`egress sidecar did not become ready within ${timeoutMs}ms:\n${logs()}`);
|
|
214
|
+
}
|
|
215
|
+
await sleepMs(100);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function sidecarLogs(dockerCommand, containerName) {
|
|
219
|
+
const result = spawnSync(dockerCommand, ["logs", "--tail", "40", containerName], {
|
|
220
|
+
encoding: "utf8",
|
|
221
|
+
});
|
|
222
|
+
return `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
223
|
+
}
|
|
224
|
+
function dockerOrThrow(dockerCommand, args, timeoutMs) {
|
|
225
|
+
const result = spawnSync(dockerCommand, args, {
|
|
226
|
+
encoding: "utf8",
|
|
227
|
+
timeout: timeoutMs,
|
|
228
|
+
});
|
|
229
|
+
if (result.status !== 0) {
|
|
230
|
+
throw new Error(`docker ${args.join(" ")} failed with ${result.status}: ${result.stderr || result.stdout || "(no output)"}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function sleepAsync(ms) {
|
|
234
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
235
|
+
}
|
|
236
|
+
function sanitize(value) {
|
|
237
|
+
return (value
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.replace(/[^a-z0-9_.-]+/g, "-")
|
|
240
|
+
.replace(/^-+|-+$/g, "")
|
|
241
|
+
.slice(0, 32) || "x");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Crash cleanup for egress sidecars — the happy path is `EgressSidecar.dispose`;
|
|
245
|
+
* this reaps what a control-plane crash orphaned. Removes labelled sidecar
|
|
246
|
+
* CONTAINERS and per-session `--internal` NETWORKS older than the threshold,
|
|
247
|
+
* and sweeps stale `oma-egress-*` temp roots. A crash-orphaned 0600
|
|
248
|
+
* `bundle.json` (real secrets) is scrubbed aggressively — gated only on the
|
|
249
|
+
* readiness timeout, not the whole-dir threshold — so leftover secret material
|
|
250
|
+
* does not sit at rest for up to the (24h-default) container threshold; the dir
|
|
251
|
+
* itself keeps that threshold because its ca.crt is bind-mounted into a live
|
|
252
|
+
* sandbox. Meant to run once at startup, like `reapDockerSandboxContainers`.
|
|
253
|
+
*/
|
|
254
|
+
export function reapEgressSidecars(opts = {}) {
|
|
255
|
+
const dockerCommand = opts.dockerCommand ?? "docker";
|
|
256
|
+
const olderThanMs = opts.olderThanMs ?? 0;
|
|
257
|
+
const now = opts.now?.() ?? Date.now();
|
|
258
|
+
const filter = ["--filter", `label=${SIDECAR_LABEL_KEY}=${SIDECAR_LABEL_VALUE}`];
|
|
259
|
+
const expired = (kind, ids) => ids.filter((id) => {
|
|
260
|
+
const created = dockerCreatedAt(dockerCommand, kind, id);
|
|
261
|
+
return created === undefined || now - created >= olderThanMs;
|
|
262
|
+
});
|
|
263
|
+
const containers = dockerList(dockerCommand, ["ps", "-aq", ...filter]);
|
|
264
|
+
const staleContainers = expired("container", containers);
|
|
265
|
+
if (staleContainers.length > 0) {
|
|
266
|
+
spawnSync(dockerCommand, ["rm", "-f", ...staleContainers], { stdio: "ignore" });
|
|
267
|
+
}
|
|
268
|
+
const networks = dockerList(dockerCommand, ["network", "ls", "-q", ...filter]);
|
|
269
|
+
const staleNetworks = expired("network", networks);
|
|
270
|
+
for (const id of staleNetworks) {
|
|
271
|
+
// A network with a still-live endpoint refuses removal; that is correct —
|
|
272
|
+
// only orphans are reaped.
|
|
273
|
+
spawnSync(dockerCommand, ["network", "rm", id], { stdio: "ignore" });
|
|
274
|
+
}
|
|
275
|
+
sweepStaleTempRoots(opts.tmpDir ?? tmpdir(), olderThanMs, now);
|
|
276
|
+
}
|
|
277
|
+
function dockerList(dockerCommand, args) {
|
|
278
|
+
const result = spawnSync(dockerCommand, args, { encoding: "utf8" });
|
|
279
|
+
return (result.stdout ?? "").split("\n").filter(Boolean);
|
|
280
|
+
}
|
|
281
|
+
function dockerCreatedAt(dockerCommand, kind, id) {
|
|
282
|
+
const args = kind === "network"
|
|
283
|
+
? ["network", "inspect", id, "--format", "{{json .Created}}"]
|
|
284
|
+
: ["inspect", id, "--format", "{{json .Created}}"];
|
|
285
|
+
const result = spawnSync(dockerCommand, args, { encoding: "utf8" });
|
|
286
|
+
if (result.status !== 0)
|
|
287
|
+
return undefined;
|
|
288
|
+
try {
|
|
289
|
+
const created = JSON.parse(result.stdout);
|
|
290
|
+
const ms = new Date(created).getTime();
|
|
291
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function sweepStaleTempRoots(parent, olderThanMs, now) {
|
|
298
|
+
let entries;
|
|
299
|
+
try {
|
|
300
|
+
entries = readdirSync(parent);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const name of entries) {
|
|
306
|
+
if (!name.startsWith("oma-egress-"))
|
|
307
|
+
continue;
|
|
308
|
+
const full = join(parent, name);
|
|
309
|
+
try {
|
|
310
|
+
const stat = statSync(full);
|
|
311
|
+
if (!stat.isDirectory())
|
|
312
|
+
continue;
|
|
313
|
+
// Scrub a crash-orphaned resolved-secret bundle aggressively, decoupled
|
|
314
|
+
// from the (conservative, live-sidecar-safe) whole-dir threshold. The
|
|
315
|
+
// dir's ca.crt is bind-mounted read-only into a LIVE sandbox for its
|
|
316
|
+
// lifetime, so the dir keeps olderThanMs — but bundle.json (mode 0600,
|
|
317
|
+
// real secrets) is unlinked at readiness, so its mere presence means the
|
|
318
|
+
// sidecar died before readiness. Gate on the readiness timeout, not zero
|
|
319
|
+
// age, so we never race a sibling process's still-booting sidecar.
|
|
320
|
+
scrubOrphanedSecretBundle(join(full, "bundle.json"), now);
|
|
321
|
+
if (now - stat.mtimeMs >= olderThanMs) {
|
|
322
|
+
rmSync(full, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// gone already / racing another reaper — fine
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// A bundle.json older than the readiness timeout cannot belong to a healthy or
|
|
331
|
+
// still-booting sidecar (a healthy one unlinks it within ~1s of start; a
|
|
332
|
+
// readiness-timed-out one disposes its own root). Older than that ⇒ the
|
|
333
|
+
// control plane crashed before readiness — remove the secret file now rather
|
|
334
|
+
// than let it wait for the whole-dir reap (up to 24h). force ⇒ no-op if absent.
|
|
335
|
+
const ORPHAN_SECRET_BUNDLE_MIN_AGE_MS = DEFAULT_READINESS_TIMEOUT_MS;
|
|
336
|
+
function scrubOrphanedSecretBundle(bundlePath, now) {
|
|
337
|
+
let mtimeMs;
|
|
338
|
+
try {
|
|
339
|
+
mtimeMs = statSync(bundlePath).mtimeMs;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return; // no bundle.json (the normal post-readiness case)
|
|
343
|
+
}
|
|
344
|
+
if (now - mtimeMs >= ORPHAN_SECRET_BUNDLE_MIN_AGE_MS) {
|
|
345
|
+
rmSync(bundlePath, { force: true });
|
|
346
|
+
}
|
|
347
|
+
}
|