@edgehero/pi-dispatch 4.3.1 → 5.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/.env.example +84 -3
- package/README.md +21 -7
- package/deploy/console.service +52 -0
- package/deploy/docker-compose.yml +3 -1
- package/deploy/pi-dispatch-valkey.container +4 -2
- package/deploy/sandbox-gateway.service +23 -0
- package/deploy/worker-env-wrapper.cmd +6 -0
- package/deploy/worker-env-wrapper.sh +10 -1
- package/package.json +118 -19
- package/src/agent-snapshot.mjs +49 -0
- package/src/answerer-runtime.mjs +194 -0
- package/src/answerer.mjs +178 -0
- package/src/approval-policy.mjs +76 -0
- package/src/approval-runtime.mjs +188 -0
- package/src/approval-session.mjs +137 -0
- package/src/approval-state.mjs +170 -0
- package/src/auth-cli.mjs +86 -0
- package/src/auth-connections.mjs +145 -0
- package/src/auth-policy.mjs +32 -0
- package/src/auth-store.mjs +97 -0
- package/src/azure-host.mjs +66 -4
- package/src/azure-prompt.mjs +5 -4
- package/src/backend-conformance.mjs +34 -2
- package/src/backend-local.mjs +103 -34
- package/src/backend-openshell.mjs +1540 -0
- package/src/backend-podman.mjs +63 -2
- package/src/backend-registry.mjs +61 -4
- package/src/backends.mjs +395 -6
- package/src/bullmq-internals.mjs +208 -0
- package/src/canary-cli.mjs +28 -0
- package/src/canary-lock.mjs +41 -0
- package/src/canary-runtime.mjs +237 -0
- package/src/canary.mjs +53 -0
- package/src/cancel-cli.mjs +47 -34
- package/src/cancel-state.mjs +213 -15
- package/src/capabilities.mjs +36 -1
- package/src/capacity-cli.mjs +28 -8
- package/src/capacity-records.mjs +75 -16
- package/src/capacity.mjs +65 -0
- package/src/chat-cli.mjs +288 -0
- package/src/chat-fork.mjs +547 -0
- package/src/chat-mode.mjs +156 -0
- package/src/chat-view.mjs +720 -0
- package/src/checkpoint-paths.mjs +113 -0
- package/src/cli.mjs +136 -41
- package/src/comment-tables.mjs +322 -0
- package/src/community-auth-adapters.mjs +87 -0
- package/src/community-auth-helper.mjs +76 -0
- package/src/community-auth-metadata.mjs +6 -0
- package/src/community-auth-output-pins.mjs +38 -0
- package/src/community-auth-patches.mjs +168 -0
- package/src/community-auth-pins.mjs +38 -0
- package/src/config.mjs +190 -29
- package/src/connection.mjs +83 -7
- package/src/console-answer.mjs +113 -0
- package/src/console-audit.mjs +604 -0
- package/src/console-bootstrap.mjs +325 -0
- package/src/console-client.mjs +848 -0
- package/src/console-devices.mjs +372 -0
- package/src/console-install.mjs +239 -0
- package/src/console-open.mjs +99 -0
- package/src/console-probe.mjs +106 -0
- package/src/console-tokens.mjs +459 -0
- package/src/container-names.mjs +51 -0
- package/src/container-spec.mjs +20 -1
- package/src/cpu-reserve.mjs +4 -1
- package/src/defer-reasons.mjs +200 -0
- package/src/defer-write.mjs +208 -0
- package/src/deployment-venue.mjs +23 -6
- package/src/docker-run.mjs +11 -0
- package/src/doctor-bundles.mjs +130 -0
- package/src/doctor-model-proposals.mjs +109 -0
- package/src/doctor-openshell.mjs +238 -0
- package/src/doctor-results.mjs +602 -0
- package/src/doctor-spawner.mjs +215 -0
- package/src/doctor.mjs +1657 -196
- package/src/dollar-budget.mjs +212 -3
- package/src/endpoint-status.mjs +100 -0
- package/src/env-allowlist.mjs +96 -6
- package/src/env-file.mjs +6 -4
- package/src/envelope.mjs +5 -3
- package/src/file-expect.mjs +47 -0
- package/src/forgejo-host.mjs +42 -5
- package/src/forgejo-prompt.mjs +5 -4
- package/src/forges.mjs +33 -3
- package/src/fork-job.mjs +58 -0
- package/src/fork-policy.mjs +23 -0
- package/src/fork-store.mjs +338 -0
- package/src/get-token.mjs +1 -1
- package/src/git-dirty.mjs +119 -14
- package/src/git-hardening.mjs +85 -0
- package/src/github-host.mjs +31 -6
- package/src/github-prompt.mjs +7 -16
- package/src/gitlab-host.mjs +42 -3
- package/src/gitlab-prompt.mjs +5 -4
- package/src/held-document.mjs +23 -0
- package/src/held-replies.mjs +325 -0
- package/src/held-settle.mjs +266 -0
- package/src/hook-runs.mjs +188 -0
- package/src/host-budget.mjs +8 -5
- package/src/host-clone.mjs +72 -0
- package/src/host-loads.mjs +557 -0
- package/src/host-registry.mjs +66 -6
- package/src/host-telemetry.mjs +92 -0
- package/src/http-body.mjs +78 -0
- package/src/image-preflight.mjs +156 -13
- package/src/import-pi.mjs +60 -172
- package/src/inbox-relay.mjs +482 -0
- package/src/inbox-settle.mjs +196 -0
- package/src/index.mjs +734 -129
- package/src/init.mjs +35 -5
- package/src/job-data-version.mjs +75 -0
- package/src/job-id.mjs +30 -15
- package/src/job-layout.mjs +22 -0
- package/src/job-size.mjs +16 -0
- package/src/live-jobs.mjs +14 -6
- package/src/live-probes.mjs +6 -2
- package/src/live-transcript.mjs +872 -0
- package/src/loaded-record.mjs +609 -0
- package/src/mcp-config.mjs +4 -2
- package/src/mcp-preflight.mjs +36 -5
- package/src/model-attempts.mjs +84 -0
- package/src/model-catalog.mjs +83 -0
- package/src/model-cooldowns.mjs +22 -0
- package/src/model-failover-protocol.mjs +11 -0
- package/src/model-failover.mjs +134 -0
- package/src/model-observation.mjs +50 -0
- package/src/model-proposal-protocol.mjs +21 -0
- package/src/model-proposals.mjs +75 -0
- package/src/model-ref.mjs +67 -10
- package/src/model-roles-file.mjs +24 -0
- package/src/model-roles.mjs +73 -0
- package/src/model-status.mjs +34 -0
- package/src/models-json.mjs +12 -2
- package/src/money.mjs +1 -1
- package/src/oauth-openai.mjs +121 -0
- package/src/older-state.mjs +249 -0
- package/src/on-failure.mjs +17 -13
- package/src/open-browser.mjs +31 -2
- package/src/opener-cli.mjs +135 -0
- package/src/opener-protocol.mjs +27 -0
- package/src/opener-terminal.mjs +128 -0
- package/src/openshell-cli.mjs +155 -0
- package/src/openshell-policy.mjs +163 -0
- package/src/outbox.mjs +14 -0
- package/src/output-cap.mjs +1 -0
- package/src/package-canary-record.mjs +26 -0
- package/src/package-canary.mjs +135 -0
- package/src/package-names.mjs +9 -0
- package/src/package-stage.mjs +418 -0
- package/src/packages.mjs +24 -16
- package/src/parked.mjs +2 -1
- package/src/pause-windows.mjs +136 -17
- package/src/podman-stack.mjs +40 -14
- package/src/portfolio-snapshot.mjs +26 -7
- package/src/prepare-github.mjs +41 -77
- package/src/prepare-local.mjs +32 -2
- package/src/prepare.mjs +45 -11
- package/src/private-store-boundary.mjs +70 -0
- package/src/processor.mjs +1550 -173
- package/src/projects.mjs +23 -0
- package/src/prompt-fence.mjs +18 -0
- package/src/provider-key.mjs +7 -0
- package/src/provider-steering.mjs +1 -0
- package/src/question-context.mjs +35 -0
- package/src/question-group-runtime.mjs +91 -0
- package/src/question-groups.mjs +171 -0
- package/src/queue.mjs +264 -55
- package/src/reason-catalog.mjs +448 -0
- package/src/receiver-status.mjs +230 -0
- package/src/record-reason.mjs +37 -0
- package/src/recovery-queue.mjs +566 -0
- package/src/redact-secrets.mjs +219 -0
- package/src/reserve-estimate.mjs +101 -0
- package/src/reserved-env.mjs +36 -1
- package/src/retention.mjs +67 -0
- package/src/run-budget-ledger.mjs +283 -0
- package/src/run-checkpoint.mjs +627 -0
- package/src/run-container.mjs +749 -33
- package/src/run-history.mjs +791 -45
- package/src/run-mirror.mjs +75 -7
- package/src/run-progress.mjs +751 -0
- package/src/runtime-observations.mjs +3 -1
- package/src/runtime-settings.mjs +272 -37
- package/src/sandbox-cli.mjs +78 -2
- package/src/sandbox-deployment.mjs +51 -0
- package/src/sandbox-gateway-protocol.mjs +378 -0
- package/src/sandbox-hosts.mjs +100 -0
- package/src/sandbox-inventory.mjs +83 -0
- package/src/sandbox-policy.mjs +225 -0
- package/src/sandbox-prepare-requests.mjs +145 -0
- package/src/sandbox-prepare.mjs +285 -0
- package/src/sandbox-refs.mjs +70 -0
- package/src/sandbox-session-monitor.mjs +4 -0
- package/src/sandbox-session-watch.mjs +124 -0
- package/src/sandbox-sessions.mjs +161 -0
- package/src/sandbox-store.mjs +63 -6
- package/src/sandbox-workspace-guard.mjs +39 -0
- package/src/sandbox.mjs +27 -200
- package/src/scheduler-stall-guard.mjs +14 -8
- package/src/schedules.mjs +19 -6
- package/src/secret-bundles.mjs +245 -0
- package/src/secret-profiles.mjs +103 -3
- package/src/secrets.mjs +19 -9
- package/src/service-env.mjs +38 -5
- package/src/service.mjs +638 -106
- package/src/session-key.mjs +60 -0
- package/src/session-store.mjs +168 -10
- package/src/setup-contract.mjs +40 -0
- package/src/setup-egress.mjs +113 -0
- package/src/setup-host-entry.mjs +36 -0
- package/src/setup-host.mjs +242 -0
- package/src/setup-lock.mjs +41 -0
- package/src/size-records.mjs +15 -3
- package/src/start.mjs +1353 -135
- package/src/status-comment.mjs +415 -0
- package/src/stderr-transcript.mjs +318 -0
- package/src/steer-ack.mjs +122 -0
- package/src/steer-cli.mjs +213 -0
- package/src/steer-framing.mjs +185 -0
- package/src/steer-inbox.mjs +693 -0
- package/src/store-version.mjs +139 -0
- package/src/subscription-config.mjs +56 -0
- package/src/subscription-limits.mjs +3 -0
- package/src/subscription-quota.mjs +259 -0
- package/src/subscription-runtime.mjs +123 -0
- package/src/subscription-status.mjs +42 -0
- package/src/subscription-wait.mjs +11 -0
- package/src/subscriptions.mjs +7 -1
- package/src/thinking-level.mjs +19 -0
- package/src/touch-outbox.mjs +61 -0
- package/src/touch-state.mjs +133 -0
- package/src/triggers-file.mjs +299 -39
- package/src/triggers.mjs +780 -38
- package/src/trust-paths.mjs +171 -0
- package/src/up.mjs +402 -53
- package/src/updates-cli.mjs +61 -0
- package/src/updates-look.mjs +127 -0
- package/src/updates-reconcile.mjs +182 -0
- package/src/updates-subscription-adapters.mjs +8 -0
- package/src/updates.mjs +252 -0
- package/src/valkey-auth.mjs +32 -9
- package/src/valkey-endpoint.mjs +14 -4
- package/src/wait-for.mjs +1 -1
- package/src/wait-state.mjs +62 -3
- package/src/watch-board.mjs +38 -0
- package/src/watch-closer.mjs +1 -1
- package/src/watch-contract.mjs +102 -0
- package/src/watch-decision.mjs +78 -0
- package/src/watch-drainer.mjs +14 -0
- package/src/watch-issue-adapter.mjs +44 -0
- package/src/watch-issue.mjs +111 -0
- package/src/watch-record.mjs +10 -0
- package/src/watch-runtime.mjs +167 -0
- package/src/watch-store.mjs +183 -0
- package/src/watch-trace.mjs +34 -0
- package/src/workspace-checkpoint.mjs +1750 -0
- package/src/zero-touch-fixes.mjs +99 -0
- package/src/zero-touch.mjs +225 -0
- package/src/repeat-slot.mjs +0 -13
package/src/backend-podman.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
|
-
import { JOB_NAME_PREFIX, execDockerBounded, jobContainerName, makeReaper, makeStopContainer } from "./backend-local.mjs";
|
|
33
|
+
import { JOB_NAME_PREFIX, execDockerBounded, isRuntimeName, jobContainerName, makeContainerGone, makeJobContainerLister, makePauseVerbs, makeReaper, makeStopContainer } from "./backend-local.mjs";
|
|
34
34
|
import { BACKENDS, PODMAN_ADDS_NO_MOUNTS, PODMAN_BACKEND, PODMAN_CONF_WIDENS_JOB, PODMAN_NETWORK_HELPER_KEYS, PODMAN_WIDENING_KEYS, PODMAN_BOUNDS_DELEGATED, PODMAN_SERVICE_LOCAL, observationRefusalIsTransient, observationRefusals, unobservedFloor } from "./backends.mjs";
|
|
35
35
|
import { CONTAINER_HOME, SHIPPED_IMAGE_UID } from "./container-spec.mjs";
|
|
36
36
|
import { buildPodmanRunArgs } from "./docker-run.mjs";
|
|
@@ -981,9 +981,15 @@ export const PODMAN_BACKEND_OPTIONS = Object.freeze([
|
|
|
981
981
|
"authFromPi",
|
|
982
982
|
"forgeHosts",
|
|
983
983
|
"onOutput",
|
|
984
|
+
"openLiveTranscript",
|
|
985
|
+
// issue #646: the inbox relay's opener, as start.mjs hands local's
|
|
986
|
+
"openInboxRelay",
|
|
984
987
|
// the floor the per-job observation preflight judges, and the boot-built reaper
|
|
985
988
|
"backendFloor",
|
|
986
989
|
"reap",
|
|
990
|
+
// issue #681: the host budget's leftover listing and gone-check, as start.mjs hands them (its listing seam)
|
|
991
|
+
"listJobContainers",
|
|
992
|
+
"containerGone",
|
|
987
993
|
// the facts, the identity and the files the observations and the job user come from
|
|
988
994
|
"readInfo",
|
|
989
995
|
"platform",
|
|
@@ -1002,6 +1008,33 @@ export const PODMAN_BACKEND_OPTIONS = Object.freeze([
|
|
|
1002
1008
|
"makeStopContainer",
|
|
1003
1009
|
]);
|
|
1004
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* Whether this venue can FREEZE a job (issue #645, the `pausable` row), from a `podman info` read: `{ pausable, reason }`,
|
|
1013
|
+
* with `pausable` `true`, `false`, or `null` when the read did not answer. Rootless Podman cannot pause on cgroup v1
|
|
1014
|
+
* (`podman pause` refuses it there: the v1 freezer is a hierarchy the user is never delegated), and every podman venue
|
|
1015
|
+
* is rootless because a rootful one is refused, so the cgroup version decides it. The version comes from the SAME read
|
|
1016
|
+
* the venue admits jobs on and `podmanBoundsDelegated` judges, so the freeze and the bounds cannot disagree about which
|
|
1017
|
+
* version this host runs. A version `podman info` did not give is not v1: the CLI is then left to answer for itself,
|
|
1018
|
+
* which it does with a non-zero exit the verb returns as `{ ok: false }`. The bundle's verbs judge the KEPT read only, so
|
|
1019
|
+
* an unanswered or not-yet-taken read (`pausable: null`) also leaves the CLI to answer.
|
|
1020
|
+
*/
|
|
1021
|
+
export function podmanPausable(read) {
|
|
1022
|
+
if (read?.answered !== true || !read.info) return { pausable: null, reason: read?.reason ?? "unanswered" };
|
|
1023
|
+
if (read.info.cgroupVersion === "v1") return { pausable: false, reason: "cgroup-v1-rootless" };
|
|
1024
|
+
return { pausable: true, reason: null };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** An execFile-shaped runner (resolves on exit 0, rejects with `.code` otherwise) as `execDockerBounded`'s `{ code, error }`. */
|
|
1028
|
+
function settledRun(execFn) {
|
|
1029
|
+
return (cmd, args) =>
|
|
1030
|
+
Promise.resolve()
|
|
1031
|
+
.then(() => execFn(cmd, args))
|
|
1032
|
+
.then(
|
|
1033
|
+
() => ({ code: 0, error: null }),
|
|
1034
|
+
(err) => ({ code: typeof err?.code === "number" ? err.code : null, error: err ?? { code: null } }),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1005
1038
|
/** The keeper read's bound: one `podman inspect`, on a pre-spend gate, like the proxy's own read beside it. */
|
|
1006
1039
|
export const NETNS_KEEPER_READ_TIMEOUT_MS = 10_000;
|
|
1007
1040
|
|
|
@@ -1061,7 +1094,7 @@ export function keeperPreflight(proxyPreflight, { armed, proxy, info, spawnFn =
|
|
|
1061
1094
|
|
|
1062
1095
|
/**
|
|
1063
1096
|
* The podman bundle: `{ name, declares, neverStartedExits, containerName, namePrefix, binds, runContainer,
|
|
1064
|
-
* imagePreflight, egressPreflight, stopContainer, reap, jobUserPreflight, observationPreflight }`.
|
|
1097
|
+
* imagePreflight, egressPreflight, stopContainer, pause, unpause, reap, jobUserPreflight, observationPreflight }`.
|
|
1065
1098
|
*
|
|
1066
1099
|
* BUILT here from PR #425's factories with `bin: "podman"` (and `buildPodmanRunArgs` for the run), rather than taken
|
|
1067
1100
|
* built as `makeLocalBackend` does, because the pairing IS the adapter: a run under one binary and a stop, a preflight or
|
|
@@ -1087,8 +1120,12 @@ export function makePodmanBackend(opts = {}) {
|
|
|
1087
1120
|
authFromPi = false,
|
|
1088
1121
|
forgeHosts = {},
|
|
1089
1122
|
onOutput,
|
|
1123
|
+
openLiveTranscript = null,
|
|
1124
|
+
openInboxRelay = null,
|
|
1090
1125
|
backendFloor = {},
|
|
1091
1126
|
reap,
|
|
1127
|
+
listJobContainers,
|
|
1128
|
+
containerGone,
|
|
1092
1129
|
readInfo = makePodmanInfoReader(),
|
|
1093
1130
|
platform = process.platform,
|
|
1094
1131
|
euid = process.geteuid?.(),
|
|
@@ -1105,9 +1142,25 @@ export function makePodmanBackend(opts = {}) {
|
|
|
1105
1142
|
makeStopContainer: makeStopContainerFn = makeStopContainer,
|
|
1106
1143
|
} = opts ?? {};
|
|
1107
1144
|
if (reap !== undefined && typeof reap !== "function") throw new Error(`backend "${PODMAN_BACKEND}": reap must be a function (makePodmanReaper)`);
|
|
1145
|
+
for (const [key, fn] of Object.entries({ listJobContainers, containerGone })) {
|
|
1146
|
+
if (fn !== undefined && typeof fn !== "function") throw new Error(`backend "${PODMAN_BACKEND}": ${key} must be a function when it is passed`);
|
|
1147
|
+
}
|
|
1108
1148
|
const spawnSeam = spawnFn ? { spawnFn } : {};
|
|
1109
1149
|
const execSeam = exec ? { exec } : spawnFn ? { exec: execViaSpawn(spawnFn) } : {};
|
|
1110
1150
|
const info = cachedPodmanInfo(readInfo, { log });
|
|
1151
|
+
// The freezer verbs (issue #645) under the venue's own binary, through the same exec seam as the stop, so a test's fake
|
|
1152
|
+
// child sees every podman spawn. In production the bounded runner; `makePauseVerbs` bounds an injected one too.
|
|
1153
|
+
const verbs = makePauseVerbs({ bin: "podman", ...(execSeam.exec ? { run: settledRun(execSeam.exec) } : {}) });
|
|
1154
|
+
// The name first, then cgroup v1, both BEFORE any spawn. The version comes from the KEPT answered `podman info`
|
|
1155
|
+
// (`peek`), never a fresh read: a read here would be a second spawn with a bound of its own beside the verb's 15 s,
|
|
1156
|
+
// and every job is admitted on an answered read (`observationPreflight`), so a running job always has one kept.
|
|
1157
|
+
// With none kept (no job admitted yet) the verb runs and `podman pause`'s own refusal comes back as `{ ok: false }`.
|
|
1158
|
+
const gated = (fn) => async (name) => {
|
|
1159
|
+
if (!isRuntimeName(name)) return { ok: false, reason: "invalid-name" };
|
|
1160
|
+
const can = podmanPausable(info.peek?.() ?? null);
|
|
1161
|
+
if (can.pausable === false) return { ok: false, reason: can.reason };
|
|
1162
|
+
return fn(name);
|
|
1163
|
+
};
|
|
1111
1164
|
|
|
1112
1165
|
const runContainer = makeRunContainerFn({
|
|
1113
1166
|
image,
|
|
@@ -1116,6 +1169,8 @@ export function makePodmanBackend(opts = {}) {
|
|
|
1116
1169
|
egressProxy,
|
|
1117
1170
|
...(openJobLog ? { openJobLog } : {}),
|
|
1118
1171
|
...(onOutput ? { onOutput } : {}),
|
|
1172
|
+
...(openLiveTranscript ? { openLiveTranscript } : {}),
|
|
1173
|
+
...(openInboxRelay ? { openInboxRelay } : {}),
|
|
1119
1174
|
globalPiDir,
|
|
1120
1175
|
allowGlobalExtensions,
|
|
1121
1176
|
packagePaths,
|
|
@@ -1192,7 +1247,13 @@ export function makePodmanBackend(opts = {}) {
|
|
|
1192
1247
|
imagePreflight: makeImagePreflightFn({ image, bin: "podman", ...spawnSeam }),
|
|
1193
1248
|
egressPreflight: keeperPreflight(makeEgressPreflightFn({ proxy: egressProxy, armed: egress, bin: "podman", ...spawnSeam }), { armed: egress, proxy: egressProxy, info, spawnFn }),
|
|
1194
1249
|
stopContainer: makeStopContainerFn({ bin: "podman", ...execSeam }),
|
|
1250
|
+
pause: gated(verbs.pause),
|
|
1251
|
+
unpause: gated(verbs.unpause),
|
|
1195
1252
|
reap: reap ?? makePodmanReaper({ log, ...execSeam }),
|
|
1253
|
+
// Issue #681: this store's leftover job containers and whether one is gone, asked of podman through the same exec
|
|
1254
|
+
// seam as the stop and the reaper. The wiring used to pick the CLI from the venue's name instead.
|
|
1255
|
+
listJobContainers: listJobContainers ?? makeJobContainerLister({ bin: "podman", ...execSeam }),
|
|
1256
|
+
containerGone: containerGone ?? makeContainerGone({ binOf: () => "podman", ...execSeam }),
|
|
1196
1257
|
jobUserPreflight,
|
|
1197
1258
|
observationPreflight,
|
|
1198
1259
|
};
|
package/src/backend-registry.mjs
CHANGED
|
@@ -66,8 +66,19 @@ export const BACKEND_NOT_REGISTERED = "BACKEND_NOT_REGISTERED";
|
|
|
66
66
|
export const OPTIONAL_PREFLIGHT_DEFAULTS = Object.freeze({
|
|
67
67
|
observationPreflight: () => ({ ok: true }),
|
|
68
68
|
jobUserPreflight: () => ({ user: null, home: null }),
|
|
69
|
+
// Issue #704 part 3: a project secret bundle judged against the venue (`(job, { endpoints, vars, shadowed })`). A
|
|
70
|
+
// venue that carries none admits every bundle, which is what a venue whose container holds its env as values does:
|
|
71
|
+
// the variables join the env, and `endpoints` means nothing there. Only a venue whose own layer holds credentials
|
|
72
|
+
// (both OpenShell venues, which carry one) has a narrower answer.
|
|
73
|
+
bundleGate: () => ({ ok: true }),
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
/** The freezer verbs a bundle MAY carry (issue #645, the `pausable` row), always as a pair. */
|
|
77
|
+
export const PAUSE_VERBS = Object.freeze(["pause", "unpause"]);
|
|
78
|
+
|
|
79
|
+
/** Whether a (validated) bundle carries both freezer verbs. */
|
|
80
|
+
const isPausable = (bundle) => typeof bundle?.pause === "function" && typeof bundle?.unpause === "function";
|
|
81
|
+
|
|
71
82
|
/** The venue's own member when it carries one, else the absent answer, as a promise either way. */
|
|
72
83
|
async function optionalPreflight(bundle, fn, ...args) {
|
|
73
84
|
return typeof bundle[fn] === "function" ? bundle[fn](...args) : OPTIONAL_PREFLIGHT_DEFAULTS[fn]();
|
|
@@ -131,6 +142,9 @@ export function resolveBackendName(data, defaultName) {
|
|
|
131
142
|
* @param bundles the blessed backend bundles (each from a `make<Name>Backend`), keyed by their own `name`
|
|
132
143
|
* @param defaultName the venue a job that names none runs in -- `PI_BACKENDS[0]`
|
|
133
144
|
*/
|
|
145
|
+
/** The functions every registered bundle must carry, checked at boot. */
|
|
146
|
+
export const REQUIRED_FUNCTIONS = Object.freeze(["runContainer", "imagePreflight", "egressPreflight", "stopContainer", "reap", "containerName", "listJobContainers", "containerGone"]);
|
|
147
|
+
|
|
134
148
|
export function makeBackendRegistry({ bundles = [], defaultName, blessed = null, reaps = null } = {}) {
|
|
135
149
|
const byName = new Map();
|
|
136
150
|
for (const bundle of bundles) {
|
|
@@ -144,7 +158,10 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
144
158
|
// `containerName` is in this list because THIS MODULE calls it: `registry.containerName(job)` builds
|
|
145
159
|
// the name the abort then stops. Omitting it built fine and threw at the first pickup, which is the
|
|
146
160
|
// property this constructor claims to have and did not.
|
|
147
|
-
|
|
161
|
+
// `listJobContainers` and `containerGone` (issue #681) because the host budget asks every venue both: its leftover
|
|
162
|
+
// containers at boot, and whether an orphan's container is gone before its hold is freed. Without them the wiring
|
|
163
|
+
// guessed the runtime from the venue's name, and every venue but podman was asked through docker.
|
|
164
|
+
for (const fn of REQUIRED_FUNCTIONS) {
|
|
148
165
|
if (typeof bundle[fn] !== "function") throw new Error(`backend registry: ${JSON.stringify(bundle.name)} has no ${fn}()`);
|
|
149
166
|
}
|
|
150
167
|
if (!Array.isArray(bundle.neverStartedExits) || !bundle.neverStartedExits.every(Number.isInteger)) {
|
|
@@ -159,6 +176,13 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
159
176
|
for (const fn of Object.keys(OPTIONAL_PREFLIGHT_DEFAULTS)) {
|
|
160
177
|
if (bundle[fn] !== undefined && typeof bundle[fn] !== "function") throw new Error(`backend registry: ${JSON.stringify(bundle.name)} has a ${fn} that is not a function`);
|
|
161
178
|
}
|
|
179
|
+
// The freezer verbs (issue #645) are optional too, but only as a PAIR: a venue that could pause and not unpause
|
|
180
|
+
// would freeze a paid job for good at its first live checkpoint. Refused at boot, like a typo'd preflight.
|
|
181
|
+
const verbs = PAUSE_VERBS.filter((fn) => bundle[fn] !== undefined);
|
|
182
|
+
for (const fn of verbs) {
|
|
183
|
+
if (typeof bundle[fn] !== "function") throw new Error(`backend registry: ${JSON.stringify(bundle.name)} has a ${fn} that is not a function`);
|
|
184
|
+
}
|
|
185
|
+
if (verbs.length === 1) throw new Error(`backend registry: ${JSON.stringify(bundle.name)} carries ${verbs[0]} without its pair (pause and unpause come together, or neither)`);
|
|
162
186
|
byName.set(bundle.name, bundle);
|
|
163
187
|
}
|
|
164
188
|
if (byName.size === 0) throw new Error("backend registry: at least one backend must be registered");
|
|
@@ -215,15 +239,38 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
215
239
|
* the abort path had to change: the container's NAME is not enough to find the runtime that holds
|
|
216
240
|
* it once there is more than one, and `index.mjs` had only the name.
|
|
217
241
|
*/
|
|
218
|
-
|
|
242
|
+
// The venue's freezer verbs ride on the call bag (issue #645), from the SAME bundle that runs the container, so a
|
|
243
|
+
// live checkpoint can never freeze a job through another venue's CLI. A venue without them gets none, and its
|
|
244
|
+
// container gets no live checkpoints; the call bag is then exactly the caller's.
|
|
245
|
+
// Issue #646: a steer job is also told whether its venue BINDS mounts (the bundle's own `binds`), which runContainer
|
|
246
|
+
// arms steering on: a copying venue would upload the inbox once and never see a later message. Only for a steer job,
|
|
247
|
+
// so every other call bag stays exactly the caller's (plus the verbs above).
|
|
248
|
+
runContainer: (args) => {
|
|
249
|
+
const bundle = backendFor(args?.job);
|
|
250
|
+
const withVerbs = isPausable(bundle) ? { ...args, pause: bundle.pause, unpause: bundle.unpause } : args;
|
|
251
|
+
return bundle.runContainer(args?.job?.steer === true ? { ...withVerbs, binds: bundle.binds === true } : withVerbs);
|
|
252
|
+
},
|
|
219
253
|
imagePreflight: (job) => backendFor(job).imagePreflight(job),
|
|
220
254
|
egressPreflight: (job) => backendFor(job).egressPreflight(job),
|
|
221
|
-
// The
|
|
222
|
-
// required one, so an unregistered name still throws BACKEND_NOT_REGISTERED rather than being waved
|
|
255
|
+
// The OPTIONAL per-venue preflights (issue #354; `bundleGate`, #704 part 3), dispatched through the same
|
|
256
|
+
// resolution as every required one, so an unregistered name still throws BACKEND_NOT_REGISTERED rather than being waved
|
|
223
257
|
// through by a default. A venue that carries none gets the answer the processor's own defaults give.
|
|
224
258
|
observationPreflight: (job) => optionalPreflight(backendFor(job), "observationPreflight", job),
|
|
225
259
|
jobUserPreflight: (job, opts) => optionalPreflight(backendFor(job), "jobUserPreflight", job, opts),
|
|
260
|
+
bundleGate: (job, opts) => optionalPreflight(backendFor(job), "bundleGate", job, opts),
|
|
226
261
|
stopContainer: (name, job) => backendFor(job).stopContainer(name, job),
|
|
262
|
+
// Issue #645: whether this job's venue can freeze it, and the verbs themselves, dispatched like every per-job
|
|
263
|
+
// function. A venue without verbs answers `{ ok: false, reason: "not-pausable" }`, never a throw: the caller is a
|
|
264
|
+
// best-effort checkpoint beside a running job.
|
|
265
|
+
pausable: (job) => isPausable(backendFor(job)),
|
|
266
|
+
pause: async (name, job) => {
|
|
267
|
+
const bundle = backendFor(job);
|
|
268
|
+
return isPausable(bundle) ? bundle.pause(name) : { ok: false, reason: "not-pausable" };
|
|
269
|
+
},
|
|
270
|
+
unpause: async (name, job) => {
|
|
271
|
+
const bundle = backendFor(job);
|
|
272
|
+
return isPausable(bundle) ? bundle.unpause(name) : { ok: false, reason: "not-pausable" };
|
|
273
|
+
},
|
|
227
274
|
// ON THE SURFACE, not reached for through `backendFor` by a call site. Both are per-job backend
|
|
228
275
|
// FACTS rather than functions, and an earlier draft left them off: the wiring rebuilt
|
|
229
276
|
// `neverStartedExits` at the call site and `index.mjs` imported `jobContainerName` from the local
|
|
@@ -231,6 +278,16 @@ export function makeBackendRegistry({ bundles = [], defaultName, blessed = null,
|
|
|
231
278
|
// module's header says is structurally impossible -- and the container NAME is the argument the
|
|
232
279
|
// abort's `stopContainer` receives, so building it locally while resolving the venue per job was the
|
|
233
280
|
// same defect in the one call the slice exists to make dispatchable.
|
|
281
|
+
// Issue #681: the host budget's gone-check, asked of the venue that holds the container. `venue` is the job's data or
|
|
282
|
+
// `{ backend }`, as the budget keeps it. A venue no bundle answers for is `null` (unknown), never a throw: the
|
|
283
|
+
// caller is the budget's sweep, and `null` keeps the hold, which is the safe answer for room nobody can account for.
|
|
284
|
+
containerGone: async (name, venue) => {
|
|
285
|
+
const bundle = byName.get(resolveBackendName(venue ?? {}, defaultName));
|
|
286
|
+
return bundle ? bundle.containerGone(name) : null;
|
|
287
|
+
},
|
|
288
|
+
// Each registered venue's leftover-container lister, keyed by name, for the budget's boot seed.
|
|
289
|
+
// Called ON the bundle, as `containerGone` is, so an adapter whose lister is a method reading `this` still works.
|
|
290
|
+
listers: () => Object.fromEntries([...byName].map(([name, bundle]) => [name, () => bundle.listJobContainers()])),
|
|
234
291
|
neverStartedExits: (job) => backendFor(job).neverStartedExits,
|
|
235
292
|
containerName: (job) => backendFor(job).containerName(job?.id),
|
|
236
293
|
// The reaper map, validated above against the registered set, so the boot sweep can be handed
|