@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,310 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
|
|
6
|
+
import { env } from "./env";
|
|
7
|
+
import type { HostTask } from "../db/schema";
|
|
8
|
+
import type {
|
|
9
|
+
TaskCommandProject,
|
|
10
|
+
TaskCommandTask,
|
|
11
|
+
TaskDownInput,
|
|
12
|
+
TaskLaunchInput,
|
|
13
|
+
} from "../src/protocol";
|
|
14
|
+
|
|
15
|
+
export function createTaskUpCommandDb(input: TaskLaunchInput): string {
|
|
16
|
+
const dbPath = newCommandDbPath(input.task.id);
|
|
17
|
+
const db = openCommandDb(dbPath);
|
|
18
|
+
try {
|
|
19
|
+
seedMetadata(db, withRuntime(input.task, null), input.projects);
|
|
20
|
+
} finally {
|
|
21
|
+
db.close();
|
|
22
|
+
}
|
|
23
|
+
return dbPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createTaskDownCommandDb(
|
|
27
|
+
input: TaskDownInput,
|
|
28
|
+
runtime: HostTask | null,
|
|
29
|
+
): string {
|
|
30
|
+
const dbPath = newCommandDbPath(input.taskId);
|
|
31
|
+
const db = openCommandDb(dbPath);
|
|
32
|
+
try {
|
|
33
|
+
seedMetadata(db, withRuntime(input.task, runtime), input.projects);
|
|
34
|
+
} finally {
|
|
35
|
+
db.close();
|
|
36
|
+
}
|
|
37
|
+
return dbPath;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Environment-provider teardown reconstructs from a durable task locator, not
|
|
42
|
+
* from the cloud command payload that originally provisioned it. The teardown
|
|
43
|
+
* script needs only the exact task identity/runtime row; it deliberately
|
|
44
|
+
* derives every destructive Docker/filesystem target from that identity.
|
|
45
|
+
*/
|
|
46
|
+
export function createTaskEnvironmentDownCommandDb(
|
|
47
|
+
taskId: string,
|
|
48
|
+
runtime: HostTask | null,
|
|
49
|
+
): string {
|
|
50
|
+
const dbPath = newCommandDbPath(taskId);
|
|
51
|
+
const db = openCommandDb(dbPath);
|
|
52
|
+
try {
|
|
53
|
+
insertTask(db, runtimeTask(taskId, runtime));
|
|
54
|
+
} finally {
|
|
55
|
+
db.close();
|
|
56
|
+
}
|
|
57
|
+
return dbPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createTaskStatusCommandDb(
|
|
61
|
+
taskId: string,
|
|
62
|
+
runtime: HostTask | null,
|
|
63
|
+
): string {
|
|
64
|
+
const dbPath = newCommandDbPath(taskId);
|
|
65
|
+
const db = openCommandDb(dbPath);
|
|
66
|
+
try {
|
|
67
|
+
insertTask(db, runtimeTask(taskId, runtime));
|
|
68
|
+
} finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
return dbPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function removeCommandDb(path: string): void {
|
|
75
|
+
rmSync(path, { force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function newCommandDbPath(taskId: string): string {
|
|
79
|
+
if (!existsSync(env.commandDbDir)) {
|
|
80
|
+
mkdirSync(env.commandDbDir, { recursive: true, mode: 0o700 });
|
|
81
|
+
}
|
|
82
|
+
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
83
|
+
return join(env.commandDbDir, `${taskId}-${suffix}.sqlite`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openCommandDb(path: string): Database.Database {
|
|
87
|
+
const db = new Database(path);
|
|
88
|
+
db.pragma("journal_mode = WAL");
|
|
89
|
+
db.exec(schemaSql);
|
|
90
|
+
return db;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Seed the per-task command DB the bash scripts read (ADR-022 shape):
|
|
95
|
+
* - one uai_users row for the owner,
|
|
96
|
+
* - one uai_projects row per selected project,
|
|
97
|
+
* - one uai_task_projects row per (task, project, position),
|
|
98
|
+
* - one uai_tasks row for the task (+ mirrored runtime fields).
|
|
99
|
+
* Column names mirror the cloud Drizzle schema.
|
|
100
|
+
*/
|
|
101
|
+
function seedMetadata(
|
|
102
|
+
db: Database.Database,
|
|
103
|
+
task: CommandTaskRow,
|
|
104
|
+
projects: TaskCommandProject[],
|
|
105
|
+
): void {
|
|
106
|
+
db.prepare(
|
|
107
|
+
`INSERT OR IGNORE INTO uai_users (id, clerk_user_id, email)
|
|
108
|
+
VALUES (@id, @clerk_user_id, @email)`,
|
|
109
|
+
).run({
|
|
110
|
+
id: task.owner_user_id,
|
|
111
|
+
clerk_user_id: null,
|
|
112
|
+
email: "host-command@example.invalid",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const insertProject = db.prepare(
|
|
116
|
+
`INSERT INTO uai_projects (
|
|
117
|
+
id, owner_user_id, name, slug, repo_url, default_prompt,
|
|
118
|
+
preview_ports, env, tool_versions, extra
|
|
119
|
+
) VALUES (
|
|
120
|
+
@id, @owner_user_id, @name, @slug, @repo_url, @default_prompt,
|
|
121
|
+
@preview_ports, @env, @tool_versions, @extra
|
|
122
|
+
)`,
|
|
123
|
+
);
|
|
124
|
+
const insertTaskProject = db.prepare(
|
|
125
|
+
`INSERT INTO uai_task_projects (task_id, project_id, position)
|
|
126
|
+
VALUES (@task_id, @project_id, @position)`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
for (const project of projects) {
|
|
130
|
+
insertProject.run({
|
|
131
|
+
id: project.id,
|
|
132
|
+
owner_user_id: project.ownerUserId,
|
|
133
|
+
name: project.name,
|
|
134
|
+
slug: project.slug,
|
|
135
|
+
repo_url: project.repoUrl,
|
|
136
|
+
default_prompt: project.defaultPrompt,
|
|
137
|
+
preview_ports: project.previewPorts,
|
|
138
|
+
env: project.env,
|
|
139
|
+
tool_versions: project.toolVersions ?? null,
|
|
140
|
+
extra: project.extra ?? null,
|
|
141
|
+
});
|
|
142
|
+
insertTaskProject.run({
|
|
143
|
+
task_id: task.id,
|
|
144
|
+
project_id: project.id,
|
|
145
|
+
position: project.position,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
insertTask(db, task);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function insertTask(db: Database.Database, task: CommandTaskRow): void {
|
|
153
|
+
db.prepare(
|
|
154
|
+
`INSERT INTO uai_tasks (
|
|
155
|
+
id, owner_user_id, host_id, name, slug, branch, status,
|
|
156
|
+
global_context, reviewer_order, agents, pr_context,
|
|
157
|
+
worktree_path, compose_project, code_server_port, preview_ports,
|
|
158
|
+
preview_env, owner_org_id, shared_files, locked_at, started_at, ended_at
|
|
159
|
+
) VALUES (
|
|
160
|
+
@id, @owner_user_id, @host_id, @name, @slug, @branch, @status,
|
|
161
|
+
@global_context, @reviewer_order, @agents, @pr_context,
|
|
162
|
+
@worktree_path, @compose_project, @code_server_port, @preview_ports,
|
|
163
|
+
@preview_env, @owner_org_id, @shared_files, @locked_at, @started_at, @ended_at
|
|
164
|
+
)`,
|
|
165
|
+
).run(task);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The shape of a row inserted into `uai_tasks`. The leading columns mirror
|
|
170
|
+
* the cloud `tasks` schema; the trailing columns are host runtime fields the
|
|
171
|
+
* bash scripts write back during task-up / task-down.
|
|
172
|
+
*/
|
|
173
|
+
interface CommandTaskRow {
|
|
174
|
+
id: string;
|
|
175
|
+
owner_user_id: string;
|
|
176
|
+
host_id: string;
|
|
177
|
+
name: string;
|
|
178
|
+
slug: string;
|
|
179
|
+
branch: string;
|
|
180
|
+
status: string;
|
|
181
|
+
global_context: string | null;
|
|
182
|
+
reviewer_order: string | null;
|
|
183
|
+
agents: string;
|
|
184
|
+
pr_context: string | null;
|
|
185
|
+
worktree_path: string | null;
|
|
186
|
+
compose_project: string | null;
|
|
187
|
+
code_server_port: number | null;
|
|
188
|
+
preview_ports: string;
|
|
189
|
+
preview_env: string | null;
|
|
190
|
+
owner_org_id: string | null;
|
|
191
|
+
shared_files: string | null;
|
|
192
|
+
locked_at: number | null;
|
|
193
|
+
started_at: number | null;
|
|
194
|
+
ended_at: number | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function withRuntime(
|
|
198
|
+
task: TaskCommandTask,
|
|
199
|
+
runtime: HostTask | null,
|
|
200
|
+
): CommandTaskRow {
|
|
201
|
+
return {
|
|
202
|
+
id: task.id,
|
|
203
|
+
owner_user_id: task.ownerUserId,
|
|
204
|
+
host_id: task.hostId,
|
|
205
|
+
name: task.name,
|
|
206
|
+
slug: task.slug,
|
|
207
|
+
branch: task.branch,
|
|
208
|
+
status: runtime?.statusMirror ?? task.status,
|
|
209
|
+
global_context: task.globalContext ?? null,
|
|
210
|
+
reviewer_order: task.reviewerOrder
|
|
211
|
+
? JSON.stringify(task.reviewerOrder)
|
|
212
|
+
: null,
|
|
213
|
+
agents: JSON.stringify(task.agents ?? []),
|
|
214
|
+
pr_context: null,
|
|
215
|
+
worktree_path: runtime?.worktreePath ?? null,
|
|
216
|
+
compose_project: runtime?.composeProject ?? null,
|
|
217
|
+
code_server_port: runtime?.codeServerPort ?? null,
|
|
218
|
+
preview_ports: runtime?.previewPorts ?? "[]",
|
|
219
|
+
preview_env: task.previewEnv ? JSON.stringify(task.previewEnv) : null,
|
|
220
|
+
owner_org_id: task.ownerOrgId ?? null,
|
|
221
|
+
shared_files: task.sharedFiles ?? null,
|
|
222
|
+
locked_at: runtime?.lockedAt ?? null,
|
|
223
|
+
started_at: runtime?.startedAt ?? null,
|
|
224
|
+
ended_at: runtime?.endedAt ?? null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function runtimeTask(taskId: string, runtime: HostTask | null): CommandTaskRow {
|
|
229
|
+
return {
|
|
230
|
+
id: taskId,
|
|
231
|
+
owner_user_id: "host-runtime",
|
|
232
|
+
host_id: "host-runtime",
|
|
233
|
+
name: taskId,
|
|
234
|
+
slug: taskId,
|
|
235
|
+
branch: `task/${taskId}`,
|
|
236
|
+
status: runtime?.statusMirror ?? "queued",
|
|
237
|
+
global_context: null,
|
|
238
|
+
reviewer_order: null,
|
|
239
|
+
agents: "[]",
|
|
240
|
+
pr_context: null,
|
|
241
|
+
worktree_path: runtime?.worktreePath ?? null,
|
|
242
|
+
compose_project: runtime?.composeProject ?? null,
|
|
243
|
+
code_server_port: runtime?.codeServerPort ?? null,
|
|
244
|
+
preview_ports: runtime?.previewPorts ?? "[]",
|
|
245
|
+
preview_env: null,
|
|
246
|
+
owner_org_id: null,
|
|
247
|
+
shared_files: null,
|
|
248
|
+
locked_at: runtime?.lockedAt ?? null,
|
|
249
|
+
started_at: runtime?.startedAt ?? null,
|
|
250
|
+
ended_at: runtime?.endedAt ?? null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const schemaSql = `
|
|
255
|
+
CREATE TABLE IF NOT EXISTS uai_users (
|
|
256
|
+
id text PRIMARY KEY,
|
|
257
|
+
clerk_user_id text UNIQUE,
|
|
258
|
+
email text NOT NULL
|
|
259
|
+
);
|
|
260
|
+
CREATE TABLE IF NOT EXISTS uai_projects (
|
|
261
|
+
id text PRIMARY KEY,
|
|
262
|
+
owner_user_id text NOT NULL,
|
|
263
|
+
name text NOT NULL,
|
|
264
|
+
slug text NOT NULL,
|
|
265
|
+
repo_url text NOT NULL,
|
|
266
|
+
default_prompt text NOT NULL DEFAULT '',
|
|
267
|
+
preview_ports text NOT NULL DEFAULT '[]',
|
|
268
|
+
env text NOT NULL DEFAULT '[]',
|
|
269
|
+
tool_versions text,
|
|
270
|
+
extra text
|
|
271
|
+
);
|
|
272
|
+
CREATE TABLE IF NOT EXISTS uai_task_projects (
|
|
273
|
+
task_id text NOT NULL,
|
|
274
|
+
project_id text NOT NULL,
|
|
275
|
+
position integer NOT NULL DEFAULT 0,
|
|
276
|
+
PRIMARY KEY (task_id, project_id)
|
|
277
|
+
);
|
|
278
|
+
CREATE TABLE IF NOT EXISTS uai_tasks (
|
|
279
|
+
id text PRIMARY KEY,
|
|
280
|
+
owner_user_id text NOT NULL,
|
|
281
|
+
host_id text NOT NULL,
|
|
282
|
+
name text NOT NULL,
|
|
283
|
+
slug text NOT NULL,
|
|
284
|
+
branch text NOT NULL,
|
|
285
|
+
status text NOT NULL DEFAULT 'queued',
|
|
286
|
+
global_context text,
|
|
287
|
+
reviewer_order text,
|
|
288
|
+
agents text NOT NULL DEFAULT '[]',
|
|
289
|
+
pr_context text,
|
|
290
|
+
worktree_path text,
|
|
291
|
+
compose_project text,
|
|
292
|
+
code_server_port integer,
|
|
293
|
+
preview_ports text NOT NULL DEFAULT '[]',
|
|
294
|
+
preview_env text,
|
|
295
|
+
owner_org_id text,
|
|
296
|
+
shared_files text,
|
|
297
|
+
pr_url text,
|
|
298
|
+
locked_at integer,
|
|
299
|
+
started_at integer,
|
|
300
|
+
ended_at integer,
|
|
301
|
+
updated_at integer NOT NULL DEFAULT (unixepoch() * 1000)
|
|
302
|
+
);
|
|
303
|
+
CREATE TABLE IF NOT EXISTS uai_task_events (
|
|
304
|
+
id text PRIMARY KEY,
|
|
305
|
+
task_id text NOT NULL,
|
|
306
|
+
ts integer NOT NULL DEFAULT (unixepoch() * 1000),
|
|
307
|
+
kind text NOT NULL,
|
|
308
|
+
payload text NOT NULL DEFAULT '{}'
|
|
309
|
+
);
|
|
310
|
+
`;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded recovery for Docker's local-network address pool.
|
|
3
|
+
*
|
|
4
|
+
* Compose allocates one bridge network per local task. A task that fails after
|
|
5
|
+
* creating its network can remain resumable in `error`, and Docker retains the
|
|
6
|
+
* empty network. Enough such networks eventually make every later task fail
|
|
7
|
+
* with "all predefined address pools have been fully subnetted".
|
|
8
|
+
*
|
|
9
|
+
* This sweep is deliberately narrower than `docker network prune`:
|
|
10
|
+
*
|
|
11
|
+
* - the exact task/project identity must be present in trusted host state;
|
|
12
|
+
* - the task must not be starting or running;
|
|
13
|
+
* - Docker must independently report no attached container; and
|
|
14
|
+
* - deletion happens inside that task's lifecycle slot, so Resume/Stop cannot
|
|
15
|
+
* interleave with the proof. The slot is taken only if it is free at once:
|
|
16
|
+
* the sweep never waits on a task's lifecycle.
|
|
17
|
+
*
|
|
18
|
+
* Any unreadable or malformed observation means "do nothing", never
|
|
19
|
+
* permission to delete.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { HostTask } from "../db/schema";
|
|
23
|
+
import { dockerCli, type DockerResult } from "./docker-exec";
|
|
24
|
+
import { getHostTask } from "./runtime-state";
|
|
25
|
+
import { isSafeHostTaskId } from "./task-identity";
|
|
26
|
+
import { isTerminal } from "./task-status";
|
|
27
|
+
import { parseTaskEnvironmentLocator } from "./task-environment/types";
|
|
28
|
+
|
|
29
|
+
const COMPOSE_PROJECT_LABEL = "com.docker.compose.project";
|
|
30
|
+
const NETWORK_LIST_FORMAT =
|
|
31
|
+
`{{.ID}}\t{{.Name}}\t{{.Label "${COMPOSE_PROJECT_LABEL}"}}`;
|
|
32
|
+
const MAX_NETWORK_LIST_BYTES = 2 * 1024 * 1024;
|
|
33
|
+
const MAX_NETWORK_INSPECT_BYTES = 1024 * 1024;
|
|
34
|
+
const DOCKER_TIMEOUT_MS = 10_000;
|
|
35
|
+
|
|
36
|
+
interface ComposeNetworkCandidate {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
composeProject: string;
|
|
40
|
+
taskId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ComposeNetworkLifecycle {
|
|
44
|
+
/** Runs `operation` in the task's lifecycle slot only if that slot is free
|
|
45
|
+
* at once (the orchestrator's `tryRunTaskLifecycle`); never waits. */
|
|
46
|
+
tryRunTaskLifecycle<T>(
|
|
47
|
+
taskId: string,
|
|
48
|
+
operation: () => Promise<T>,
|
|
49
|
+
): Promise<{ ran: true; value: T } | { ran: false }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ComposeNetworkRecoveryDeps {
|
|
53
|
+
docker(args: string[], options?: {
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
maxOutputBytes?: number;
|
|
56
|
+
}): Promise<DockerResult>;
|
|
57
|
+
task(taskId: string): HostTask | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ComposeNetworkRecoveryResult {
|
|
61
|
+
inventory: "complete" | "unavailable" | "invalid";
|
|
62
|
+
candidates: number;
|
|
63
|
+
removed: number;
|
|
64
|
+
/** Candidates left alone because their task's lifecycle slot was busy. */
|
|
65
|
+
busy: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultDeps: ComposeNetworkRecoveryDeps = {
|
|
69
|
+
docker: dockerCli,
|
|
70
|
+
task: getHostTask,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function parseCandidate(
|
|
74
|
+
line: string,
|
|
75
|
+
): ComposeNetworkCandidate | "unrelated" | null {
|
|
76
|
+
const fields = line.split("\t");
|
|
77
|
+
if (fields.length !== 3) return null;
|
|
78
|
+
const [id, name, composeProject] = fields as [string, string, string];
|
|
79
|
+
if (!/^[a-f0-9]{12,64}$/.test(id)) return null;
|
|
80
|
+
if (!composeProject.startsWith("task-")) return "unrelated";
|
|
81
|
+
const taskId = composeProject.slice("task-".length);
|
|
82
|
+
if (!isSafeHostTaskId(taskId) || taskId !== taskId.toLowerCase()) return null;
|
|
83
|
+
if (name !== `${composeProject}_default`) return null;
|
|
84
|
+
return { id, name, composeProject, taskId };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseCandidates(stdout: string): ComposeNetworkCandidate[] | null {
|
|
88
|
+
const candidates: ComposeNetworkCandidate[] = [];
|
|
89
|
+
const ids = new Set<string>();
|
|
90
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
91
|
+
if (line.length === 0) continue;
|
|
92
|
+
const candidate = parseCandidate(line);
|
|
93
|
+
if (candidate === "unrelated") continue;
|
|
94
|
+
if (candidate === null || ids.has(candidate.id)) return null;
|
|
95
|
+
ids.add(candidate.id);
|
|
96
|
+
candidates.push(candidate);
|
|
97
|
+
}
|
|
98
|
+
return candidates;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function taskOwnsReclaimableNetwork(
|
|
102
|
+
task: HostTask | null,
|
|
103
|
+
candidate: ComposeNetworkCandidate,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (
|
|
106
|
+
task === null ||
|
|
107
|
+
task.taskId !== candidate.taskId ||
|
|
108
|
+
(task.environmentProvider !== null &&
|
|
109
|
+
task.environmentProvider !== "docker-compose") ||
|
|
110
|
+
(task.statusMirror !== "error" &&
|
|
111
|
+
task.statusMirror !== "stopped" &&
|
|
112
|
+
!(typeof task.statusMirror === "string" && isTerminal(task.statusMirror)))
|
|
113
|
+
) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (task.composeProject === candidate.composeProject) return true;
|
|
117
|
+
if (!task.environmentLocator) return false;
|
|
118
|
+
try {
|
|
119
|
+
const locator = parseTaskEnvironmentLocator(task.environmentLocator);
|
|
120
|
+
if (
|
|
121
|
+
locator.schemaVersion !== 1 ||
|
|
122
|
+
locator.provider !== "docker-compose" ||
|
|
123
|
+
!locator.value ||
|
|
124
|
+
typeof locator.value !== "object" ||
|
|
125
|
+
Array.isArray(locator.value)
|
|
126
|
+
) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const value = locator.value as Record<string, unknown>;
|
|
130
|
+
return (
|
|
131
|
+
value.schemaVersion === 2 &&
|
|
132
|
+
value.taskId === candidate.taskId &&
|
|
133
|
+
value.composeProject === candidate.composeProject &&
|
|
134
|
+
value.containerName === `${candidate.composeProject}-app-1`
|
|
135
|
+
);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function inspectedEmptyNetwork(
|
|
142
|
+
stdout: string,
|
|
143
|
+
candidate: ComposeNetworkCandidate,
|
|
144
|
+
): { fullId: string } | null {
|
|
145
|
+
let parsed: unknown;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(stdout);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(parsed) || parsed.length !== 1) return null;
|
|
152
|
+
const network = parsed[0];
|
|
153
|
+
if (!network || typeof network !== "object" || Array.isArray(network)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const record = network as Record<string, unknown>;
|
|
157
|
+
const labels = record.Labels;
|
|
158
|
+
const containers = record.Containers;
|
|
159
|
+
if (
|
|
160
|
+
typeof record.Id !== "string" ||
|
|
161
|
+
!/^[a-f0-9]{64}$/.test(record.Id) ||
|
|
162
|
+
!record.Id.startsWith(candidate.id) ||
|
|
163
|
+
record.Name !== candidate.name ||
|
|
164
|
+
record.Driver !== "bridge" ||
|
|
165
|
+
record.Scope !== "local" ||
|
|
166
|
+
!labels ||
|
|
167
|
+
typeof labels !== "object" ||
|
|
168
|
+
Array.isArray(labels) ||
|
|
169
|
+
(labels as Record<string, unknown>)[COMPOSE_PROJECT_LABEL] !==
|
|
170
|
+
candidate.composeProject ||
|
|
171
|
+
!containers ||
|
|
172
|
+
typeof containers !== "object" ||
|
|
173
|
+
Array.isArray(containers) ||
|
|
174
|
+
Object.keys(containers).length !== 0
|
|
175
|
+
) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return { fullId: record.Id };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function reclaimCandidate(
|
|
182
|
+
candidate: ComposeNetworkCandidate,
|
|
183
|
+
deps: ComposeNetworkRecoveryDeps,
|
|
184
|
+
): Promise<boolean> {
|
|
185
|
+
if (!taskOwnsReclaimableNetwork(deps.task(candidate.taskId), candidate)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const inspected = await deps.docker(
|
|
189
|
+
["network", "inspect", candidate.id],
|
|
190
|
+
{
|
|
191
|
+
timeoutMs: DOCKER_TIMEOUT_MS,
|
|
192
|
+
maxOutputBytes: MAX_NETWORK_INSPECT_BYTES,
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
if (inspected.status !== 0 || inspected.outputTruncated) return false;
|
|
196
|
+
const empty = inspectedEmptyNetwork(inspected.stdout, candidate);
|
|
197
|
+
if (empty === null) return false;
|
|
198
|
+
|
|
199
|
+
// `.Containers` is the network's own endpoint inventory. Independently ask
|
|
200
|
+
// Docker's all-container inventory as a second positive emptiness proof.
|
|
201
|
+
const attached = await deps.docker(
|
|
202
|
+
["ps", "-aq", "--filter", `network=${empty.fullId}`],
|
|
203
|
+
{
|
|
204
|
+
timeoutMs: DOCKER_TIMEOUT_MS,
|
|
205
|
+
maxOutputBytes: MAX_NETWORK_INSPECT_BYTES,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
if (
|
|
209
|
+
attached.status !== 0 ||
|
|
210
|
+
attached.outputTruncated ||
|
|
211
|
+
attached.stdout.trim().length !== 0
|
|
212
|
+
) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const removed = await deps.docker(["network", "rm", empty.fullId], {
|
|
217
|
+
timeoutMs: DOCKER_TIMEOUT_MS,
|
|
218
|
+
maxOutputBytes: MAX_NETWORK_INSPECT_BYTES,
|
|
219
|
+
});
|
|
220
|
+
return removed.status === 0 && !removed.outputTruncated;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove only positively-owned, empty task networks. This is best-effort host
|
|
225
|
+
* hygiene: an unavailable inventory never blocks a task that may still fit in
|
|
226
|
+
* Docker's pool, and it never authorizes deletion.
|
|
227
|
+
*/
|
|
228
|
+
export async function reclaimUnusedUaiComposeNetworks(
|
|
229
|
+
lifecycle: ComposeNetworkLifecycle,
|
|
230
|
+
deps: ComposeNetworkRecoveryDeps = defaultDeps,
|
|
231
|
+
): Promise<ComposeNetworkRecoveryResult> {
|
|
232
|
+
const listed = await deps.docker(
|
|
233
|
+
[
|
|
234
|
+
"network",
|
|
235
|
+
"ls",
|
|
236
|
+
"--filter",
|
|
237
|
+
`label=${COMPOSE_PROJECT_LABEL}`,
|
|
238
|
+
"--format",
|
|
239
|
+
NETWORK_LIST_FORMAT,
|
|
240
|
+
],
|
|
241
|
+
{
|
|
242
|
+
timeoutMs: DOCKER_TIMEOUT_MS,
|
|
243
|
+
maxOutputBytes: MAX_NETWORK_LIST_BYTES,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
if (listed.status !== 0 || listed.outputTruncated) {
|
|
247
|
+
return { inventory: "unavailable", candidates: 0, removed: 0, busy: 0 };
|
|
248
|
+
}
|
|
249
|
+
const candidates = parseCandidates(listed.stdout);
|
|
250
|
+
if (candidates === null) {
|
|
251
|
+
return { inventory: "invalid", candidates: 0, removed: 0, busy: 0 };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let removed = 0;
|
|
255
|
+
let busy = 0;
|
|
256
|
+
for (const candidate of candidates) {
|
|
257
|
+
// Avoid taking a live start's slot merely to rediscover in-slot that it
|
|
258
|
+
// is not reclaimable. This observation grants no authority; the complete
|
|
259
|
+
// ownership/status check is repeated after the lifecycle slot is held.
|
|
260
|
+
if (!taskOwnsReclaimableNetwork(deps.task(candidate.taskId), candidate)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// Never wait for the candidate's slot. A Resume marks its task `starting`
|
|
264
|
+
// only partway into its own slot, so the row can still read `error` while
|
|
265
|
+
// that slot is held, and waiting for it meant waiting out the whole
|
|
266
|
+
// start; a stuck operation on any stopped task would hold every later
|
|
267
|
+
// start here. A busy task's network is left for a later start's sweep.
|
|
268
|
+
const attempt = await lifecycle.tryRunTaskLifecycle(
|
|
269
|
+
candidate.taskId,
|
|
270
|
+
// The trusted row and Docker evidence are deliberately read only after
|
|
271
|
+
// entering the candidate task's lifecycle slot.
|
|
272
|
+
() => reclaimCandidate(candidate, deps),
|
|
273
|
+
);
|
|
274
|
+
if (!attempt.ran) busy += 1;
|
|
275
|
+
else if (attempt.value) removed += 1;
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
inventory: "complete",
|
|
279
|
+
candidates: candidates.length,
|
|
280
|
+
removed,
|
|
281
|
+
busy,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Exact, session-scoped defense for diagnostics that reach host telemetry. */
|
|
2
|
+
const active = new Map<string, number>();
|
|
3
|
+
|
|
4
|
+
export function registerConnectionSecret(key: string): () => void {
|
|
5
|
+
if (!key) return () => {};
|
|
6
|
+
active.set(key, (active.get(key) ?? 0) + 1);
|
|
7
|
+
let released = false;
|
|
8
|
+
return () => {
|
|
9
|
+
if (released) return;
|
|
10
|
+
released = true;
|
|
11
|
+
const remaining = (active.get(key) ?? 1) - 1;
|
|
12
|
+
if (remaining) active.set(key, remaining);
|
|
13
|
+
else active.delete(key);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function redactActiveConnectionSecrets(text: string): string {
|
|
18
|
+
// Empty replacement cannot itself contain another active secret.
|
|
19
|
+
let previous: string;
|
|
20
|
+
do {
|
|
21
|
+
previous = text;
|
|
22
|
+
for (const key of active.keys()) text = text.split(key).join("");
|
|
23
|
+
} while (text !== previous);
|
|
24
|
+
return text;
|
|
25
|
+
}
|