@moondeck/station 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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/meta/_journal.json +223 -0
- package/db/schema.ts +850 -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 +1453 -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 +891 -0
- package/lib/env.ts +195 -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 +131 -4
- 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/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -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/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -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 +5545 -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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
|
|
3
|
+
import { dockerCli, type DockerResult } from "./docker-exec";
|
|
4
|
+
import { reprobeContainerRuntimeMachineIdentity } from "./container-runtime";
|
|
5
|
+
|
|
6
|
+
type DockerRunner = (
|
|
7
|
+
args: string[],
|
|
8
|
+
options?: { timeoutMs?: number },
|
|
9
|
+
) => Promise<DockerResult>;
|
|
10
|
+
|
|
11
|
+
export type TunnelContainerProof =
|
|
12
|
+
| {
|
|
13
|
+
kind: "running";
|
|
14
|
+
containerId: string;
|
|
15
|
+
published: { host: string; port: number } | null;
|
|
16
|
+
containerIp: string | null;
|
|
17
|
+
network: string | null;
|
|
18
|
+
labels: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
| { kind: "unavailable" }
|
|
21
|
+
| { kind: "daemon-unavailable"; detail: string };
|
|
22
|
+
|
|
23
|
+
interface DockerContainerInspection {
|
|
24
|
+
Id?: unknown;
|
|
25
|
+
Name?: unknown;
|
|
26
|
+
Config?: { Labels?: Record<string, unknown> | null };
|
|
27
|
+
State?: { Running?: unknown };
|
|
28
|
+
NetworkSettings?: {
|
|
29
|
+
Ports?: Record<
|
|
30
|
+
string,
|
|
31
|
+
Array<{ HostIp?: unknown; HostPort?: unknown }> | null
|
|
32
|
+
>;
|
|
33
|
+
Networks?: Record<string, { IPAddress?: unknown }>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const COMPOSE_PROJECT_LABEL = "com.docker.compose.project";
|
|
38
|
+
const COMPOSE_SERVICE_LABEL = "com.docker.compose.service";
|
|
39
|
+
const CONTAINER_ID = /^[a-f0-9]{64}$/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prove that an exact container is running and resolve its current routes.
|
|
43
|
+
*
|
|
44
|
+
* Persisted host ports and cached container IPs are unsafe after an unnoticed
|
|
45
|
+
* daemon restart: Docker may remap the app, and an unrelated local process (or
|
|
46
|
+
* another container) can inherit the old address. Every tunnel open crosses
|
|
47
|
+
* this proof before bytes are forwarded.
|
|
48
|
+
*/
|
|
49
|
+
export async function inspectTunnelContainer(
|
|
50
|
+
container: string,
|
|
51
|
+
containerPort: number,
|
|
52
|
+
deps: { docker?: DockerRunner } = {},
|
|
53
|
+
): Promise<TunnelContainerProof> {
|
|
54
|
+
const run = deps.docker ?? dockerCli;
|
|
55
|
+
const inspected = await run(
|
|
56
|
+
["inspect", "--format", "{{json .}}", container],
|
|
57
|
+
{ timeoutMs: 5_000 },
|
|
58
|
+
);
|
|
59
|
+
if (inspected.status !== 0) {
|
|
60
|
+
if (!deps.docker) {
|
|
61
|
+
return (await reprobeContainerRuntimeMachineIdentity()) === null
|
|
62
|
+
? { kind: "daemon-unavailable", detail: "docker did not answer" }
|
|
63
|
+
: { kind: "unavailable" };
|
|
64
|
+
}
|
|
65
|
+
const daemon = await run(
|
|
66
|
+
["info", "--format", "{{.ServerVersion}}"],
|
|
67
|
+
{ timeoutMs: 5_000 },
|
|
68
|
+
);
|
|
69
|
+
if (daemon.status !== 0 || !daemon.stdout.trim()) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "daemon-unavailable",
|
|
72
|
+
// This reaches cloud-visible command errors. Docker's own diagnostic
|
|
73
|
+
// can contain the pinned unix socket (and therefore a host username),
|
|
74
|
+
// so keep endpoint details in Docker's local logs rather than the wire.
|
|
75
|
+
detail: "docker did not answer",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { kind: "unavailable" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let value: DockerContainerInspection;
|
|
82
|
+
try {
|
|
83
|
+
value = JSON.parse(inspected.stdout.trim()) as DockerContainerInspection;
|
|
84
|
+
} catch {
|
|
85
|
+
return { kind: "unavailable" };
|
|
86
|
+
}
|
|
87
|
+
if (value.State?.Running !== true) return { kind: "unavailable" };
|
|
88
|
+
|
|
89
|
+
const containerId = exactContainerId(value.Id);
|
|
90
|
+
if (!containerId) return { kind: "unavailable" };
|
|
91
|
+
const address = firstContainerAddress(value);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
kind: "running",
|
|
95
|
+
containerId,
|
|
96
|
+
published: publishedAddress(value, containerPort),
|
|
97
|
+
containerIp: address?.ip ?? null,
|
|
98
|
+
network: address?.network ?? null,
|
|
99
|
+
labels: stringLabels(value.Config?.Labels),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stronger proof for editor/preview routing into a task's app. Docker's exact
|
|
105
|
+
* name lookup is necessary but not sufficient: a recreated or foreign
|
|
106
|
+
* container can reuse it. Bind the route to Compose's project/service labels,
|
|
107
|
+
* the full container ID, and only the expected project default network.
|
|
108
|
+
*/
|
|
109
|
+
export async function inspectTaskAppTunnelContainer(
|
|
110
|
+
request: {
|
|
111
|
+
readonly container: string;
|
|
112
|
+
readonly composeProject: string;
|
|
113
|
+
readonly containerPort: number;
|
|
114
|
+
},
|
|
115
|
+
deps: { docker?: DockerRunner } = {},
|
|
116
|
+
): Promise<TunnelContainerProof> {
|
|
117
|
+
const run = deps.docker ?? dockerCli;
|
|
118
|
+
const inspected = await run(
|
|
119
|
+
["inspect", "--format", "{{json .}}", request.container],
|
|
120
|
+
{ timeoutMs: 5_000 },
|
|
121
|
+
);
|
|
122
|
+
if (inspected.status !== 0) {
|
|
123
|
+
if (!deps.docker) {
|
|
124
|
+
return (await reprobeContainerRuntimeMachineIdentity()) === null
|
|
125
|
+
? { kind: "daemon-unavailable", detail: "docker did not answer" }
|
|
126
|
+
: { kind: "unavailable" };
|
|
127
|
+
}
|
|
128
|
+
const daemon = await run(
|
|
129
|
+
["info", "--format", "{{.ServerVersion}}"],
|
|
130
|
+
{ timeoutMs: 5_000 },
|
|
131
|
+
);
|
|
132
|
+
if (daemon.status !== 0 || !daemon.stdout.trim()) {
|
|
133
|
+
return { kind: "daemon-unavailable", detail: "docker did not answer" };
|
|
134
|
+
}
|
|
135
|
+
return { kind: "unavailable" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let value: DockerContainerInspection;
|
|
139
|
+
try {
|
|
140
|
+
value = JSON.parse(inspected.stdout.trim()) as DockerContainerInspection;
|
|
141
|
+
} catch {
|
|
142
|
+
return { kind: "unavailable" };
|
|
143
|
+
}
|
|
144
|
+
const labels = stringLabels(value.Config?.Labels);
|
|
145
|
+
const containerId = exactContainerId(value.Id);
|
|
146
|
+
const network = `${request.composeProject}_default`;
|
|
147
|
+
const ip = exactNetworkAddress(value, network);
|
|
148
|
+
if (
|
|
149
|
+
value.State?.Running !== true ||
|
|
150
|
+
value.Name !== `/${request.container}` ||
|
|
151
|
+
!containerId ||
|
|
152
|
+
labels[COMPOSE_PROJECT_LABEL] !== request.composeProject ||
|
|
153
|
+
labels[COMPOSE_SERVICE_LABEL] !== "app" ||
|
|
154
|
+
!ip
|
|
155
|
+
) {
|
|
156
|
+
return { kind: "unavailable" };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
kind: "running",
|
|
160
|
+
containerId,
|
|
161
|
+
published: publishedAddress(value, request.containerPort),
|
|
162
|
+
containerIp: ip,
|
|
163
|
+
network,
|
|
164
|
+
labels,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function exactContainerId(value: unknown): string | null {
|
|
169
|
+
return typeof value === "string" && CONTAINER_ID.test(value) ? value : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function stringLabels(
|
|
173
|
+
labels: Record<string, unknown> | null | undefined,
|
|
174
|
+
): Record<string, string> {
|
|
175
|
+
if (!labels) return {};
|
|
176
|
+
return Object.fromEntries(
|
|
177
|
+
Object.entries(labels).filter(
|
|
178
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function publishedAddress(
|
|
184
|
+
value: DockerContainerInspection,
|
|
185
|
+
containerPort: number,
|
|
186
|
+
): { host: string; port: number } | null {
|
|
187
|
+
const bindings = value.NetworkSettings?.Ports?.[`${containerPort}/tcp`];
|
|
188
|
+
if (!Array.isArray(bindings)) return null;
|
|
189
|
+
const candidates = bindings.flatMap((binding) => {
|
|
190
|
+
const port = parsePort(binding.HostPort);
|
|
191
|
+
const host = loopbackForBinding(binding.HostIp);
|
|
192
|
+
return port && host ? [{ host, port }] : [];
|
|
193
|
+
});
|
|
194
|
+
return (
|
|
195
|
+
candidates.find((candidate) => candidate.host === "127.0.0.1") ??
|
|
196
|
+
candidates[0] ??
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function loopbackForBinding(value: unknown): string | null {
|
|
202
|
+
if (value === "127.0.0.1" || value === "0.0.0.0" || value === "") {
|
|
203
|
+
return "127.0.0.1";
|
|
204
|
+
}
|
|
205
|
+
if (value === "::1" || value === "::") return "::1";
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parsePort(value: unknown): number | null {
|
|
210
|
+
if (typeof value !== "string" || !/^\d{1,5}$/.test(value)) return null;
|
|
211
|
+
const port = Number(value);
|
|
212
|
+
return Number.isInteger(port) && port >= 1 && port <= 65_535 ? port : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function firstContainerAddress(
|
|
216
|
+
value: DockerContainerInspection,
|
|
217
|
+
): { network: string; ip: string } | null {
|
|
218
|
+
const networks = value.NetworkSettings?.Networks;
|
|
219
|
+
if (!networks || typeof networks !== "object") return null;
|
|
220
|
+
for (const [name, network] of Object.entries(networks)) {
|
|
221
|
+
const address = network?.IPAddress;
|
|
222
|
+
if (typeof address === "string" && isIP(address) !== 0) {
|
|
223
|
+
return { network: name, ip: address };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function exactNetworkAddress(
|
|
230
|
+
value: DockerContainerInspection,
|
|
231
|
+
network: string,
|
|
232
|
+
): string | null {
|
|
233
|
+
const address = value.NetworkSettings?.Networks?.[network]?.IPAddress;
|
|
234
|
+
return typeof address === "string" && isIP(address) !== 0 ? address : null;
|
|
235
|
+
}
|
package/lib/ulid.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
|
|
3
|
+
export function newId(): string {
|
|
4
|
+
return ulid().toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A ULID with its canonical UPPERCASE base32, unlike `newId()` above.
|
|
9
|
+
*
|
|
10
|
+
* Renamed from `monotonicUlid`, which it never was: the plain generator
|
|
11
|
+
* randomises the suffix, so two ids minted in the same millisecond do not
|
|
12
|
+
* sort in call order. Nothing in this package imports it — it is kept only to
|
|
13
|
+
* mirror `lib/ulid.ts`, and it is renamed here too so the false promise is
|
|
14
|
+
* not sitting in the copy someone reads next.
|
|
15
|
+
*/
|
|
16
|
+
export { ulid as rawUlid };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `systemctl --user` needs to reach the user's service manager:
|
|
3
|
+
* XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS. A login shell has both. A
|
|
4
|
+
* stripped environment does not (an old-host command run with only the old
|
|
5
|
+
* defaults, a non-login shell, `su` without `-l`), and there systemctl fails
|
|
6
|
+
* with "Failed to connect to bus: No medium found" (the 2026-11-01 rehearsal,
|
|
7
|
+
* uninstall-legacy on Linux).
|
|
8
|
+
*
|
|
9
|
+
* Every value is checked before it is used, inherited or derived alike, so a
|
|
10
|
+
* borrowed environment can't point this user's systemctl at someone else's
|
|
11
|
+
* manager:
|
|
12
|
+
* - a runtime directory must be a real directory (not a link) owned by this
|
|
13
|
+
* uid and not group- or world-writable;
|
|
14
|
+
* - a bus must be `unix:path=<absolute path>` naming a socket (not a link)
|
|
15
|
+
* owned by this uid.
|
|
16
|
+
* An inherited value that fails is replaced by the one derived the way
|
|
17
|
+
* pam_systemd sets it (/run/user/<uid>, and its `bus`), or left unset.
|
|
18
|
+
*
|
|
19
|
+
* An inherited bus address in any other form (`unix:abstract=`, `tcp:`,
|
|
20
|
+
* `unix:tmpdir=`, a `;`-separated list) can't be checked this way and is
|
|
21
|
+
* dropped. The systemd user bus is always `unix:path=$XDG_RUNTIME_DIR/bus`, so
|
|
22
|
+
* nothing this runs needs another form.
|
|
23
|
+
*
|
|
24
|
+
* Every path, inherited or derived, must also be plain: letters, digits and
|
|
25
|
+
* `_ / . -` only, the bytes D-Bus never escapes. A D-Bus client percent-decodes
|
|
26
|
+
* an address and splits it at `;` and `,`, so `…/target%2fbus` would name a
|
|
27
|
+
* socket other than the one checked, and a runtime dir holding
|
|
28
|
+
* `;unix:path=…` would emit a second address. A path that isn't plain is
|
|
29
|
+
* dropped, never escaped, so what is checked is byte for byte what is used.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { lstatSync } from "node:fs";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
|
|
35
|
+
export interface UserBusEntry {
|
|
36
|
+
readonly uid: number;
|
|
37
|
+
readonly mode: number;
|
|
38
|
+
readonly kind: "directory" | "socket" | "other";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UserBusProbe {
|
|
42
|
+
readonly uid: number;
|
|
43
|
+
/** lstat (never through a link), or null when absent or unreadable. */
|
|
44
|
+
stat(target: string): UserBusEntry | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const systemUserBusProbe = (): UserBusProbe => ({
|
|
48
|
+
uid: process.getuid?.() ?? -1,
|
|
49
|
+
stat: (target) => {
|
|
50
|
+
try {
|
|
51
|
+
const entry = lstatSync(target);
|
|
52
|
+
return {
|
|
53
|
+
uid: entry.uid,
|
|
54
|
+
mode: entry.mode,
|
|
55
|
+
kind: entry.isDirectory() ? "directory" : entry.isSocket() ? "socket" : "other",
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** An absolute path of bytes D-Bus never escapes, so it reads back as itself. */
|
|
64
|
+
const plainPath = (target: string) => /^\/[A-Za-z0-9_/.-]*$/.test(target);
|
|
65
|
+
|
|
66
|
+
function ownRuntimeDir(dir: string | undefined, probe: UserBusProbe): string | undefined {
|
|
67
|
+
if (!dir || !plainPath(dir) || probe.uid < 0) return undefined;
|
|
68
|
+
const entry = probe.stat(dir);
|
|
69
|
+
return entry?.kind === "directory" && entry.uid === probe.uid && (entry.mode & 0o022) === 0 ? dir : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ownBusSocket(socket: string, probe: UserBusProbe): boolean {
|
|
73
|
+
if (!plainPath(socket) || probe.uid < 0) return false;
|
|
74
|
+
const entry = probe.stat(socket);
|
|
75
|
+
return entry?.kind === "socket" && entry.uid === probe.uid;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The socket of a `unix:path=<p>[,guid=<hex>]` address, or null for any other form. */
|
|
79
|
+
function busSocketPath(address: string): string | null {
|
|
80
|
+
const match = /^unix:path=([^,;]+)(?:,guid=[0-9a-f]+)?$/.exec(address);
|
|
81
|
+
return match ? match[1]! : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function userBusEnv(
|
|
85
|
+
inherited: NodeJS.ProcessEnv,
|
|
86
|
+
probe: UserBusProbe,
|
|
87
|
+
): { XDG_RUNTIME_DIR?: string; DBUS_SESSION_BUS_ADDRESS?: string } {
|
|
88
|
+
const runtimeDir =
|
|
89
|
+
ownRuntimeDir(inherited.XDG_RUNTIME_DIR || undefined, probe) ??
|
|
90
|
+
ownRuntimeDir(probe.uid >= 0 ? `/run/user/${probe.uid}` : undefined, probe);
|
|
91
|
+
let bus: string | undefined;
|
|
92
|
+
const inheritedBus = inherited.DBUS_SESSION_BUS_ADDRESS;
|
|
93
|
+
const inheritedSocket = inheritedBus ? busSocketPath(inheritedBus) : null;
|
|
94
|
+
if (inheritedBus && inheritedSocket && ownBusSocket(inheritedSocket, probe)) {
|
|
95
|
+
bus = inheritedBus;
|
|
96
|
+
} else if (runtimeDir && ownBusSocket(path.join(runtimeDir, "bus"), probe)) {
|
|
97
|
+
bus = `unix:path=${path.join(runtimeDir, "bus")}`;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
101
|
+
...(bus ? { DBUS_SESSION_BUS_ADDRESS: bus } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `inherited` (its defined values) with the user-bus variables replaced by the
|
|
107
|
+
* checked ones: an inherited value that fails the checks is removed, never
|
|
108
|
+
* passed on.
|
|
109
|
+
*/
|
|
110
|
+
export function withUserBusEnv(inherited: NodeJS.ProcessEnv, probe: UserBusProbe): Record<string, string> {
|
|
111
|
+
const env: Record<string, string> = {};
|
|
112
|
+
for (const [name, value] of Object.entries(inherited)) {
|
|
113
|
+
if (value !== undefined && name !== "XDG_RUNTIME_DIR" && name !== "DBUS_SESSION_BUS_ADDRESS") env[name] = value;
|
|
114
|
+
}
|
|
115
|
+
return { ...env, ...userBusEnv(inherited, probe) };
|
|
116
|
+
}
|