@moondeck/station 0.0.1 → 1.0.1
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 +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/0031_first_turn_receipts.sql +9 -0
- package/db/migrations/meta/_journal.json +230 -0
- package/db/schema.ts +871 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1470 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +937 -0
- package/lib/env.ts +195 -0
- package/lib/first-turn-receipts.ts +62 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/bridge-diagnostics.ts +39 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2585 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/enrollment-next-steps.ts +24 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2907 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6363 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5550 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests only: the task's runtime as a `RuntimeNamespace` (#cb3j0z), its
|
|
3
|
+
* workspace the host directory `root` mounted at /workspace. What it is asked
|
|
4
|
+
* to make, remove and link is recorded and done there. A path through a link
|
|
5
|
+
* the task made resolves inside the runtime, never to the host's own files,
|
|
6
|
+
* so nothing is done on the host for it.
|
|
7
|
+
*/
|
|
8
|
+
import { lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { RuntimeNamespace } from "./workspace-files";
|
|
12
|
+
|
|
13
|
+
/** Whether a runtime path below /workspace passes through a link before its
|
|
14
|
+
* last component. */
|
|
15
|
+
export function throughLink(root: string, path: string): boolean {
|
|
16
|
+
let at = root;
|
|
17
|
+
for (const part of relative("/workspace", path).split("/").slice(0, -1)) {
|
|
18
|
+
at = join(at, part);
|
|
19
|
+
try {
|
|
20
|
+
if (lstatSync(at).isSymbolicLink()) return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hostRuntimeNamespace(root: string, calls: string[][] = []): RuntimeNamespace {
|
|
29
|
+
const at = (path: string): string | null => (throughLink(root, path) ? null : join(root, relative("/workspace", path)));
|
|
30
|
+
return {
|
|
31
|
+
workspacePath: "/workspace",
|
|
32
|
+
makeDirectories: async (path) => {
|
|
33
|
+
calls.push(["mkdir", path]);
|
|
34
|
+
const host = at(path);
|
|
35
|
+
if (host) mkdirSync(host, { recursive: true });
|
|
36
|
+
},
|
|
37
|
+
remove: async (path) => {
|
|
38
|
+
calls.push(["rm", path]);
|
|
39
|
+
const host = at(path);
|
|
40
|
+
if (host) rmSync(host, { force: true });
|
|
41
|
+
},
|
|
42
|
+
replaceLink: async (target, path) => {
|
|
43
|
+
calls.push(["ln", target, path]);
|
|
44
|
+
const host = at(path);
|
|
45
|
+
if (!host) return;
|
|
46
|
+
rmSync(host, { force: true });
|
|
47
|
+
symlinkSync(target, host);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable host half of task-machine Files retirement.
|
|
3
|
+
*
|
|
4
|
+
* A Files boundary that cannot be proved closed must never be repaired by a
|
|
5
|
+
* sidecar process restart: the task's other processes outlive that restart
|
|
6
|
+
* (`uai-sidecar.service` uses KillMode=process). Establish one exact
|
|
7
|
+
* task/run request, persist it before containment when storage permits, kill
|
|
8
|
+
* every other process in the sidecar's cgroup, and replay the same operation
|
|
9
|
+
* until the cloud durably accepts it. A storage failure cannot skip the kill
|
|
10
|
+
* or cloud handoff: the same in-memory operation retries persistence too.
|
|
11
|
+
* Only a different kernel boot id clears the record; that full boot is the
|
|
12
|
+
* proof boundary the Files consumer needs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
closeSync,
|
|
17
|
+
constants,
|
|
18
|
+
fstatSync,
|
|
19
|
+
fsyncSync,
|
|
20
|
+
lstatSync,
|
|
21
|
+
openSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { dirname, resolve } from "node:path";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
isHostOpId,
|
|
31
|
+
TASK_MACHINE_RETIRE_REASONS,
|
|
32
|
+
type TaskMachineRetireAckFrame,
|
|
33
|
+
type TaskMachineRetireFrame,
|
|
34
|
+
} from "../src/protocol";
|
|
35
|
+
import { newId } from "./ulid";
|
|
36
|
+
import { isSafeHostTaskId } from "./task-identity";
|
|
37
|
+
import type {
|
|
38
|
+
TaskFilesMachineRetirementHandler,
|
|
39
|
+
TaskFilesMachineRetirementReason,
|
|
40
|
+
TaskFilesMachineRetirementRequest,
|
|
41
|
+
} from "./task-files-machine-retirement";
|
|
42
|
+
|
|
43
|
+
export const TASK_FILES_MACHINE_RETIREMENT_RECORD =
|
|
44
|
+
"task-files-machine-retirement-v1.json";
|
|
45
|
+
export const TASK_FILES_MACHINE_RETIREMENT_RETRY_MS = 5_000;
|
|
46
|
+
const RECORD_SCHEMA_VERSION = 1;
|
|
47
|
+
const MAX_RECORD_BYTES = 4_096;
|
|
48
|
+
const CGROUP_ROOT = "/sys/fs/cgroup";
|
|
49
|
+
const SELF_CGROUP = "/proc/self/cgroup";
|
|
50
|
+
const KERNEL_BOOT_ID = "/proc/sys/kernel/random/boot_id";
|
|
51
|
+
|
|
52
|
+
interface RetirementRecord {
|
|
53
|
+
schemaVersion: 1;
|
|
54
|
+
machineBootId: string;
|
|
55
|
+
opId: string;
|
|
56
|
+
taskId: string;
|
|
57
|
+
runEpoch: number;
|
|
58
|
+
reason: TaskFilesMachineRetirementReason;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type TimerHandle = unknown;
|
|
62
|
+
|
|
63
|
+
export interface TaskFilesMachineRetirementOutboxDeps {
|
|
64
|
+
recordPath: string;
|
|
65
|
+
/** Authenticated bridge send. False means the next reconnect/tick retries. */
|
|
66
|
+
send(frame: TaskMachineRetireFrame): boolean;
|
|
67
|
+
machineBootId?: () => string;
|
|
68
|
+
quarantine?: () => void;
|
|
69
|
+
newOpId?: () => string;
|
|
70
|
+
setTimer?: (callback: () => void, ms: number) => TimerHandle;
|
|
71
|
+
clearTimer?: (timer: TimerHandle) => void;
|
|
72
|
+
log?: (line: string) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type RetirementOutboxStartup =
|
|
76
|
+
| { kind: "ready" }
|
|
77
|
+
| { kind: "pending"; request: TaskFilesMachineRetirementRequest }
|
|
78
|
+
| { kind: "damaged"; error: Error };
|
|
79
|
+
|
|
80
|
+
function isReason(value: unknown): value is TaskFilesMachineRetirementReason {
|
|
81
|
+
return (TASK_MACHINE_RETIRE_REASONS as readonly unknown[]).includes(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseRecord(text: string): RetirementRecord {
|
|
85
|
+
let value: unknown;
|
|
86
|
+
try {
|
|
87
|
+
value = JSON.parse(text);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error("the task-machine retirement record is not JSON", {
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
94
|
+
throw new Error("the task-machine retirement record is not an object");
|
|
95
|
+
}
|
|
96
|
+
const row = value as Record<string, unknown>;
|
|
97
|
+
const keys = Object.keys(row).sort();
|
|
98
|
+
const expected = [
|
|
99
|
+
"machineBootId",
|
|
100
|
+
"opId",
|
|
101
|
+
"reason",
|
|
102
|
+
"runEpoch",
|
|
103
|
+
"schemaVersion",
|
|
104
|
+
"taskId",
|
|
105
|
+
].sort();
|
|
106
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
107
|
+
throw new Error("the task-machine retirement record has unknown fields");
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
row.schemaVersion !== RECORD_SCHEMA_VERSION ||
|
|
111
|
+
typeof row.machineBootId !== "string" ||
|
|
112
|
+
!/^[0-9a-f-]{16,64}$/i.test(row.machineBootId) ||
|
|
113
|
+
!isHostOpId(row.opId) ||
|
|
114
|
+
!isSafeHostTaskId(row.taskId) ||
|
|
115
|
+
!Number.isSafeInteger(row.runEpoch) ||
|
|
116
|
+
(row.runEpoch as number) < 1 ||
|
|
117
|
+
!isReason(row.reason)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("the task-machine retirement record is invalid");
|
|
120
|
+
}
|
|
121
|
+
return row as unknown as RetirementRecord;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readSecureRecord(path: string): RetirementRecord | null {
|
|
125
|
+
let info;
|
|
126
|
+
try {
|
|
127
|
+
info = lstatSync(path);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
const expectedUid = process.getuid?.() ?? info.uid;
|
|
133
|
+
if (
|
|
134
|
+
!info.isFile() ||
|
|
135
|
+
info.isSymbolicLink() ||
|
|
136
|
+
info.uid !== expectedUid ||
|
|
137
|
+
(info.mode & 0o077) !== 0 ||
|
|
138
|
+
info.size > MAX_RECORD_BYTES
|
|
139
|
+
) {
|
|
140
|
+
throw new Error("the task-machine retirement record is not a private owned file");
|
|
141
|
+
}
|
|
142
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
143
|
+
try {
|
|
144
|
+
const opened = fstatSync(fd);
|
|
145
|
+
if (opened.dev !== info.dev || opened.ino !== info.ino) {
|
|
146
|
+
throw new Error("the task-machine retirement record changed while opening");
|
|
147
|
+
}
|
|
148
|
+
return parseRecord(readFileSync(fd, "utf8"));
|
|
149
|
+
} finally {
|
|
150
|
+
closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function syncDirectory(path: string): void {
|
|
155
|
+
const fd = openSync(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
156
|
+
try {
|
|
157
|
+
fsyncSync(fd);
|
|
158
|
+
} finally {
|
|
159
|
+
closeSync(fd);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeRecord(path: string, record: RetirementRecord): void {
|
|
164
|
+
const staged = `${path}.${String(process.pid)}.${record.opId}.tmp`;
|
|
165
|
+
let fd: number | null = null;
|
|
166
|
+
try {
|
|
167
|
+
fd = openSync(
|
|
168
|
+
staged,
|
|
169
|
+
constants.O_WRONLY |
|
|
170
|
+
constants.O_CREAT |
|
|
171
|
+
constants.O_EXCL |
|
|
172
|
+
constants.O_NOFOLLOW,
|
|
173
|
+
0o600,
|
|
174
|
+
);
|
|
175
|
+
writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
|
|
176
|
+
fsyncSync(fd);
|
|
177
|
+
closeSync(fd);
|
|
178
|
+
fd = null;
|
|
179
|
+
renameSync(staged, path);
|
|
180
|
+
syncDirectory(path);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (fd !== null) closeSync(fd);
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(staged);
|
|
185
|
+
} catch (cleanupError) {
|
|
186
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
187
|
+
throw new AggregateError(
|
|
188
|
+
[error, cleanupError],
|
|
189
|
+
"the task-machine retirement record and its staging file could not be persisted",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function removeRecord(path: string): void {
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync(path);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
syncDirectory(path);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function kernelBootId(): string {
|
|
208
|
+
const value = readFileSync(KERNEL_BOOT_ID, "utf8").trim();
|
|
209
|
+
if (!/^[0-9a-f-]{16,64}$/i.test(value)) {
|
|
210
|
+
throw new Error("the machine kernel boot id is invalid");
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function selfCgroupProcessesPath(): string {
|
|
216
|
+
const line = readFileSync(SELF_CGROUP, "utf8")
|
|
217
|
+
.split("\n")
|
|
218
|
+
.find((entry) => entry.startsWith("0::"));
|
|
219
|
+
if (!line) throw new Error("the sidecar has no unified cgroup");
|
|
220
|
+
const relative = line.slice(3);
|
|
221
|
+
if (
|
|
222
|
+
!relative.startsWith("/") ||
|
|
223
|
+
relative.includes("\0") ||
|
|
224
|
+
relative.split("/").includes("..")
|
|
225
|
+
) {
|
|
226
|
+
throw new Error("the sidecar cgroup path is unsafe");
|
|
227
|
+
}
|
|
228
|
+
const path = resolve(CGROUP_ROOT, `.${relative}`, "cgroup.procs");
|
|
229
|
+
if (!path.startsWith(`${CGROUP_ROOT}/`)) {
|
|
230
|
+
throw new Error("the sidecar cgroup escaped the cgroup root");
|
|
231
|
+
}
|
|
232
|
+
return path;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Every process beside this runner in the sidecar service cgroup. */
|
|
236
|
+
export function taskMachinePeerProcessIds(): number[] {
|
|
237
|
+
return readFileSync(selfCgroupProcessesPath(), "utf8")
|
|
238
|
+
.split("\n")
|
|
239
|
+
.map((value) => Number(value.trim()))
|
|
240
|
+
.filter(
|
|
241
|
+
(pid) =>
|
|
242
|
+
Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A process's parent from /proc; null once the process is gone. */
|
|
247
|
+
export function processParentId(pid: number): number | null {
|
|
248
|
+
let stat: string;
|
|
249
|
+
try {
|
|
250
|
+
stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
// `pid (comm) state ppid ...`: comm may itself hold spaces or parens.
|
|
256
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
257
|
+
if (!Number.isSafeInteger(ppid) || ppid < 0) {
|
|
258
|
+
throw new Error(`process ${pid} has an unreadable parent`);
|
|
259
|
+
}
|
|
260
|
+
return ppid;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Every process in the sidecar service cgroup that this runner did not start.
|
|
265
|
+
* The fresh-incarnation proof asks this, not `taskMachinePeerProcessIds`: the
|
|
266
|
+
* runner's own helpers share its cgroup from boot, tsx's esbuild service above
|
|
267
|
+
* all (a long-lived child of every runner that compiled a module; live
|
|
268
|
+
* 2026-10-20, every marked machine stayed pending "not quiescent" on it), and
|
|
269
|
+
* a pending runner admits no task, so nothing it started is a task process.
|
|
270
|
+
* What an earlier runner process left behind (KillMode=process) was reparented
|
|
271
|
+
* when that process ended: it is never this runner's descendant, so it counts.
|
|
272
|
+
*/
|
|
273
|
+
export function taskMachineForeignProcessIds(): number[] {
|
|
274
|
+
return foreignProcessIdsWith({
|
|
275
|
+
list: taskMachinePeerProcessIds,
|
|
276
|
+
parent: processParentId,
|
|
277
|
+
self: process.pid,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface ForeignProcessDeps {
|
|
282
|
+
list(): readonly number[];
|
|
283
|
+
parent(pid: number): number | null;
|
|
284
|
+
self: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Exported seam keeps the ancestry walk executable in tests. */
|
|
288
|
+
export function foreignProcessIdsWith(deps: ForeignProcessDeps): number[] {
|
|
289
|
+
const foreign: number[] = [];
|
|
290
|
+
for (const pid of deps.list()) {
|
|
291
|
+
if (pid === deps.self) continue;
|
|
292
|
+
const seen = new Set<number>();
|
|
293
|
+
let current: number | null = pid;
|
|
294
|
+
let ancestry: "ours" | "foreign" | "gone" = "foreign";
|
|
295
|
+
for (;;) {
|
|
296
|
+
const parent = deps.parent(current);
|
|
297
|
+
if (parent === null) {
|
|
298
|
+
// The process itself is gone: nothing to count. An ancestor gone
|
|
299
|
+
// mid-walk leaves an orphan the next look sees reparented: fail
|
|
300
|
+
// closed now, as that look would.
|
|
301
|
+
if (current === pid) ancestry = "gone";
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (parent === deps.self) {
|
|
305
|
+
ancestry = "ours";
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (parent <= 1 || seen.has(parent)) break;
|
|
309
|
+
seen.add(parent);
|
|
310
|
+
current = parent;
|
|
311
|
+
}
|
|
312
|
+
if (ancestry === "foreign") foreign.push(pid);
|
|
313
|
+
}
|
|
314
|
+
return foreign;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Kill everything in the sidecar service cgroup except this root runner.
|
|
318
|
+
* The task's agents, editor, terminals and sudo children all descend from the
|
|
319
|
+
* service; KillMode=process is precisely why they otherwise survive. */
|
|
320
|
+
export function quarantineTaskMachineProcesses(): void {
|
|
321
|
+
quarantineTaskMachineProcessesWith({
|
|
322
|
+
list: () => [process.pid, ...taskMachinePeerProcessIds()],
|
|
323
|
+
kill: (pid) => process.kill(pid, "SIGKILL"),
|
|
324
|
+
self: process.pid,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface TaskMachineProcessQuarantineDeps {
|
|
329
|
+
list(): number[];
|
|
330
|
+
kill(pid: number): void;
|
|
331
|
+
self: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Exported seam keeps the real cgroup algorithm executable in tests. */
|
|
335
|
+
export function quarantineTaskMachineProcessesWith(
|
|
336
|
+
deps: TaskMachineProcessQuarantineDeps,
|
|
337
|
+
): void {
|
|
338
|
+
const others = deps
|
|
339
|
+
.list()
|
|
340
|
+
.filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== deps.self);
|
|
341
|
+
if (others.length === 0) return;
|
|
342
|
+
for (const pid of others) {
|
|
343
|
+
try {
|
|
344
|
+
deps.kill(pid);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const remaining = deps
|
|
350
|
+
.list()
|
|
351
|
+
.filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== deps.self);
|
|
352
|
+
if (remaining.length > 0) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`task processes remained after the retirement quarantine (${remaining.join(", ")})`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export class TaskFilesMachineRetirementOutbox {
|
|
360
|
+
readonly #deps: Required<
|
|
361
|
+
Pick<
|
|
362
|
+
TaskFilesMachineRetirementOutboxDeps,
|
|
363
|
+
"machineBootId" | "quarantine" | "newOpId" | "setTimer" | "clearTimer" | "log"
|
|
364
|
+
>
|
|
365
|
+
> &
|
|
366
|
+
Pick<TaskFilesMachineRetirementOutboxDeps, "recordPath" | "send">;
|
|
367
|
+
readonly startup: RetirementOutboxStartup;
|
|
368
|
+
#record: RetirementRecord | null = null;
|
|
369
|
+
#recordPersisted = false;
|
|
370
|
+
#timer: TimerHandle | null = null;
|
|
371
|
+
#accepted = false;
|
|
372
|
+
#damaged = false;
|
|
373
|
+
#lastPersistenceError: string | null = null;
|
|
374
|
+
#lastQuarantineError: string | null = null;
|
|
375
|
+
#stopped = false;
|
|
376
|
+
|
|
377
|
+
constructor(deps: TaskFilesMachineRetirementOutboxDeps) {
|
|
378
|
+
this.#deps = {
|
|
379
|
+
...deps,
|
|
380
|
+
machineBootId: deps.machineBootId ?? kernelBootId,
|
|
381
|
+
quarantine: deps.quarantine ?? quarantineTaskMachineProcesses,
|
|
382
|
+
newOpId: deps.newOpId ?? newId,
|
|
383
|
+
setTimer: deps.setTimer ?? ((callback, ms) => setTimeout(callback, ms)),
|
|
384
|
+
clearTimer: deps.clearTimer ?? ((timer) => clearTimeout(timer as NodeJS.Timeout)),
|
|
385
|
+
log: deps.log ?? ((line) => console.warn(line)),
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
const stored = readSecureRecord(deps.recordPath);
|
|
389
|
+
if (stored === null) {
|
|
390
|
+
this.startup = { kind: "ready" };
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (stored.machineBootId !== this.#deps.machineBootId()) {
|
|
394
|
+
removeRecord(deps.recordPath);
|
|
395
|
+
this.startup = { kind: "ready" };
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.#record = stored;
|
|
399
|
+
this.#recordPersisted = true;
|
|
400
|
+
this.startup = {
|
|
401
|
+
kind: "pending",
|
|
402
|
+
request: {
|
|
403
|
+
assignment: { taskId: stored.taskId, runEpoch: stored.runEpoch },
|
|
404
|
+
reason: stored.reason,
|
|
405
|
+
error: new Error("task-machine retirement is still pending on this boot"),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.startup = {
|
|
410
|
+
kind: "damaged",
|
|
411
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
readonly handler: TaskFilesMachineRetirementHandler = (request) => {
|
|
417
|
+
if (this.#stopped) throw new Error("task-machine retirement is shut down");
|
|
418
|
+
const existing = this.#record;
|
|
419
|
+
if (
|
|
420
|
+
existing &&
|
|
421
|
+
(existing.taskId !== request.assignment.taskId ||
|
|
422
|
+
existing.runEpoch !== request.assignment.runEpoch)
|
|
423
|
+
) {
|
|
424
|
+
// First failure owns this one-task machine. A later assignment cannot
|
|
425
|
+
// replace the exact run the cloud must stop.
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
if (!existing) {
|
|
429
|
+
const record: RetirementRecord = {
|
|
430
|
+
schemaVersion: RECORD_SCHEMA_VERSION,
|
|
431
|
+
machineBootId: this.#deps.machineBootId(),
|
|
432
|
+
opId: this.#deps.newOpId(),
|
|
433
|
+
taskId: request.assignment.taskId,
|
|
434
|
+
runEpoch: request.assignment.runEpoch,
|
|
435
|
+
reason: request.reason,
|
|
436
|
+
};
|
|
437
|
+
this.#record = record;
|
|
438
|
+
}
|
|
439
|
+
// The cycle persists first when it can. If storage refuses the write, it
|
|
440
|
+
// still quarantines and sends this same op, then retries all three: a
|
|
441
|
+
// full disk must not leave the already-running task uncontained.
|
|
442
|
+
this.#cycle();
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/** A same-boot record that cannot be read may name an irreversible Files
|
|
447
|
+
* failure. It has no safe exact run to send to the cloud, but it still
|
|
448
|
+
* closes local admission and retries the cgroup quarantine until this
|
|
449
|
+
* process stops. */
|
|
450
|
+
quarantineDamagedStartup(): void {
|
|
451
|
+
if (this.startup.kind !== "damaged" || this.#stopped) return;
|
|
452
|
+
this.#damaged = true;
|
|
453
|
+
this.#cycle();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The authenticated bridge is back; replay a same-boot request. */
|
|
457
|
+
bridgeAuthenticated(): void {
|
|
458
|
+
this.#cycle();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
acknowledge(frame: TaskMachineRetireAckFrame): void {
|
|
462
|
+
const record = this.#record;
|
|
463
|
+
if (
|
|
464
|
+
!record ||
|
|
465
|
+
frame.opId !== record.opId ||
|
|
466
|
+
frame.taskId !== record.taskId ||
|
|
467
|
+
frame.runEpoch !== record.runEpoch
|
|
468
|
+
) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (frame.outcome === "accepted") {
|
|
472
|
+
this.#accepted = true;
|
|
473
|
+
this.#deps.log(
|
|
474
|
+
`[credentials] task ${record.taskId} run ${String(record.runEpoch)}: exact machine retirement accepted; waiting for the machine to stop`,
|
|
475
|
+
);
|
|
476
|
+
this.#reconcileTimer();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// A terminal rejection must never reopen this boot. Stop retrying a
|
|
480
|
+
// request the cloud has authoritatively refused, but retain the durable
|
|
481
|
+
// latch for an operator-visible fail-closed machine.
|
|
482
|
+
this.#accepted = true;
|
|
483
|
+
this.#deps.log(
|
|
484
|
+
`[credentials] task ${record.taskId} run ${String(record.runEpoch)}: machine retirement ${frame.outcome}${frame.error ? ` (${frame.error})` : ""}; this boot remains quarantined`,
|
|
485
|
+
);
|
|
486
|
+
this.#reconcileTimer();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
stop(): void {
|
|
490
|
+
this.#stopped = true;
|
|
491
|
+
this.#cancelTimer();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
#cycle(): void {
|
|
495
|
+
const record = this.#record;
|
|
496
|
+
if (this.#stopped || (!record && !this.#damaged)) return;
|
|
497
|
+
this.#tryPersistRecord();
|
|
498
|
+
this.#tryQuarantine();
|
|
499
|
+
// A failed quarantine must not strand the durable handoff. The cloud's
|
|
500
|
+
// accepted exact-instance stop is the final fence; local retry continues
|
|
501
|
+
// independently until either the cgroup is empty or the instance dies.
|
|
502
|
+
if (record && !this.#accepted) {
|
|
503
|
+
this.#deps.send({
|
|
504
|
+
kind: "task.machine.retire",
|
|
505
|
+
opId: record.opId,
|
|
506
|
+
taskId: record.taskId,
|
|
507
|
+
runEpoch: record.runEpoch,
|
|
508
|
+
reason: record.reason,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
this.#reconcileTimer();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#tryPersistRecord(): void {
|
|
515
|
+
const record = this.#record;
|
|
516
|
+
if (!record || this.#recordPersisted) return;
|
|
517
|
+
try {
|
|
518
|
+
writeRecord(this.#deps.recordPath, record);
|
|
519
|
+
this.#recordPersisted = true;
|
|
520
|
+
if (this.#lastPersistenceError !== null) {
|
|
521
|
+
this.#deps.log(
|
|
522
|
+
"[credentials] task-machine retirement record is now durable",
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
this.#lastPersistenceError = null;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
528
|
+
if (detail !== this.#lastPersistenceError) {
|
|
529
|
+
this.#deps.log(
|
|
530
|
+
`[credentials] task-machine retirement record could not be persisted (${detail}); containment proceeds in memory and persistence retries`,
|
|
531
|
+
);
|
|
532
|
+
this.#lastPersistenceError = detail;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#tryQuarantine(): void {
|
|
538
|
+
try {
|
|
539
|
+
this.#deps.quarantine();
|
|
540
|
+
if (this.#lastQuarantineError !== null) {
|
|
541
|
+
this.#deps.log(
|
|
542
|
+
"[credentials] task-machine process quarantine is now complete",
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
this.#lastQuarantineError = null;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
548
|
+
if (detail !== this.#lastQuarantineError) {
|
|
549
|
+
this.#deps.log(
|
|
550
|
+
`[credentials] task-machine process quarantine is incomplete (${detail}); retrying while the cloud stop proceeds`,
|
|
551
|
+
);
|
|
552
|
+
this.#lastQuarantineError = detail;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#needsRetry(): boolean {
|
|
558
|
+
return (
|
|
559
|
+
!this.#stopped &&
|
|
560
|
+
(this.#record !== null || this.#damaged)
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#reconcileTimer(): void {
|
|
565
|
+
if (this.#needsRetry()) this.#armTimer();
|
|
566
|
+
else this.#cancelTimer();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#armTimer(): void {
|
|
570
|
+
if (this.#timer || !this.#needsRetry()) return;
|
|
571
|
+
const timer = this.#deps.setTimer(() => {
|
|
572
|
+
if (this.#timer !== timer) return;
|
|
573
|
+
this.#timer = null;
|
|
574
|
+
this.#cycle();
|
|
575
|
+
}, TASK_FILES_MACHINE_RETIREMENT_RETRY_MS);
|
|
576
|
+
if (
|
|
577
|
+
typeof timer === "object" &&
|
|
578
|
+
timer !== null &&
|
|
579
|
+
"unref" in timer &&
|
|
580
|
+
typeof (timer as { unref?: unknown }).unref === "function"
|
|
581
|
+
) {
|
|
582
|
+
(timer as { unref(): void }).unref();
|
|
583
|
+
}
|
|
584
|
+
this.#timer = timer;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#cancelTimer(): void {
|
|
588
|
+
if (!this.#timer) return;
|
|
589
|
+
this.#deps.clearTimer(this.#timer);
|
|
590
|
+
this.#timer = null;
|
|
591
|
+
}
|
|
592
|
+
}
|