@moondeck/station 0.0.1 → 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 +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/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
- package/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** ADR-164: the workspace switch is cloud-only. The host independently
|
|
2
|
+
* checks the concrete runtime identity; unknown and task machines refuse. */
|
|
3
|
+
export function isOwnedInternalConnectionRuntime(provider: string | null | undefined): boolean {
|
|
4
|
+
return provider === "docker-compose" || provider === "apple-container";
|
|
5
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize asynchronous maintenance while coalescing callers for the same
|
|
3
|
+
* requested configuration. A different key always queues a distinct pass;
|
|
4
|
+
* this is stronger than a plain in-flight memo, which can let configuration C
|
|
5
|
+
* accidentally join an older B operation and then be declared reconciled.
|
|
6
|
+
*/
|
|
7
|
+
export class KeyedPromiseTail<T> {
|
|
8
|
+
private tail: { key: string; promise: Promise<T> } | null = null;
|
|
9
|
+
|
|
10
|
+
run(key: string, operation: () => Promise<T>): Promise<T> {
|
|
11
|
+
if (this.tail?.key === key) return this.tail.promise;
|
|
12
|
+
const predecessor = this.tail?.promise;
|
|
13
|
+
let running!: Promise<T>;
|
|
14
|
+
running = (predecessor
|
|
15
|
+
? predecessor.catch(() => undefined)
|
|
16
|
+
: Promise.resolve()
|
|
17
|
+
).then(operation).finally(() => {
|
|
18
|
+
if (this.tail?.promise === running) this.tail = null;
|
|
19
|
+
});
|
|
20
|
+
this.tail = { key, promise: running };
|
|
21
|
+
return running;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this host was launched for development or tests, or is an
|
|
3
|
+
* installed product. An installed service leaves NODE_ENV unset, so "not
|
|
4
|
+
* production" is the wrong test; these markers are the positive signals of a
|
|
5
|
+
* development launch. Telemetry's default DSN (lib/obs) and any development
|
|
6
|
+
* override of where credentials are sent (lib/github-origin) both read this.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type LaunchEnv = {
|
|
10
|
+
NODE_ENV?: string;
|
|
11
|
+
UAI_AGENT_FROM_REPO?: string;
|
|
12
|
+
UAI_LAUNCHED_FROM_REPO?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** A development or test launch: an explicit dev/test NODE_ENV, or a host
|
|
16
|
+
* started from a repository checkout (src/load-env.ts stamps that). */
|
|
17
|
+
export function isDevelopmentLaunch(env: LaunchEnv = process.env): boolean {
|
|
18
|
+
return (
|
|
19
|
+
env.NODE_ENV === "development" ||
|
|
20
|
+
env.NODE_ENV === "test" ||
|
|
21
|
+
env.UAI_AGENT_FROM_REPO === "1" ||
|
|
22
|
+
env.UAI_AGENT_FROM_REPO === "true" ||
|
|
23
|
+
env.UAI_LAUNCHED_FROM_REPO === "1"
|
|
24
|
+
);
|
|
25
|
+
}
|
package/lib/lease.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-133 task leases — the sidecar's AWS credentials.
|
|
3
|
+
*
|
|
4
|
+
* A task machine holds no long-lived AWS credential. At connect (and ten
|
|
5
|
+
* minutes before every expiry) the sidecar sends `lease.request` over the
|
|
6
|
+
* bridge; the cloud checks that this host row is the machine of a running
|
|
7
|
+
* task, assumes the `uai-task-machine` role with session tags for the task's
|
|
8
|
+
* workspace and owner, and answers `lease.grant` with the STS session. The
|
|
9
|
+
* session is held here in memory and handed to exactly two consumers:
|
|
10
|
+
*
|
|
11
|
+
* - the credential store's `aws` CLI calls (Parameter Store reads for engine
|
|
12
|
+
* accounts, GitHub tokens, MCP secrets, project env) through `leaseAwsEnv()`;
|
|
13
|
+
* - Mountpoint for Amazon S3 (shared files) through a `credential_process`
|
|
14
|
+
* that prints the current session — so a refreshed lease reaches a mount
|
|
15
|
+
* that is already up without remounting.
|
|
16
|
+
*
|
|
17
|
+
* Revocation is the next refresh being denied: `onDenied` fires and the
|
|
18
|
+
* sidecar exits, and the cloud's sweep terminates the machine. A lease is
|
|
19
|
+
* bound to one task and one machine; nothing here can ask for another's.
|
|
20
|
+
*/
|
|
21
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
|
|
24
|
+
import type { TaskLease } from "../src/protocol";
|
|
25
|
+
|
|
26
|
+
/** Re-request this long before the session expires. */
|
|
27
|
+
export const LEASE_REFRESH_LEAD_MS = 10 * 60_000;
|
|
28
|
+
/** A denied or unanswered request is retried this often until it lands. */
|
|
29
|
+
export const LEASE_RETRY_MS = 30_000;
|
|
30
|
+
/** The cloud must answer within this window or the request is retried. */
|
|
31
|
+
export const LEASE_REQUEST_TIMEOUT_MS = 20_000;
|
|
32
|
+
|
|
33
|
+
/** Where the `credential_process` command reads the live session. Root-only:
|
|
34
|
+
* the agents run as `node` and never see it. */
|
|
35
|
+
export const LEASE_CREDENTIALS_FILE = "/run/uai/lease-credentials.json";
|
|
36
|
+
/** The AWS config Mountpoint is pointed at (`AWS_CONFIG_FILE`). */
|
|
37
|
+
export const LEASE_AWS_CONFIG_FILE = "/run/uai/aws-config";
|
|
38
|
+
/**
|
|
39
|
+
* The `credential_process` line: `cat` of the JSON, run by the AWS CLI and
|
|
40
|
+
* by Mountpoint's CRT profile provider through `/bin/sh -c`. NOT a script
|
|
41
|
+
* under /run — Debian mounts /run `noexec`, so a helper there fails with
|
|
42
|
+
* EACCES even for root and both clients fall through to the instance
|
|
43
|
+
* profile, which has no S3 rights (live 2026-09-10, hostless smoke 6: every
|
|
44
|
+
* mount answered "assumed-role/uai-task-machine-instance … is not
|
|
45
|
+
* authorized to perform: s3:ListBucket").
|
|
46
|
+
*/
|
|
47
|
+
export const LEASE_CREDENTIAL_PROCESS = `/bin/cat ${LEASE_CREDENTIALS_FILE}`;
|
|
48
|
+
|
|
49
|
+
export interface LeaseSender {
|
|
50
|
+
(frame: { kind: "lease.request"; opId: string; taskId: string }): boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LeaseManagerOptions {
|
|
54
|
+
taskId: string;
|
|
55
|
+
send: LeaseSender;
|
|
56
|
+
/** First grant — the store reconciles and the S3 mount wait for this. */
|
|
57
|
+
onFirstGrant?: (lease: TaskLease) => void;
|
|
58
|
+
/** Every grant, including refreshes. */
|
|
59
|
+
onGrant?: (lease: TaskLease) => void;
|
|
60
|
+
/** The cloud refused: the task is no longer ours. */
|
|
61
|
+
onDenied?: (error: string) => void;
|
|
62
|
+
/** Test seam: skip the /run/uai files. */
|
|
63
|
+
writeFiles?: boolean;
|
|
64
|
+
now?: () => number;
|
|
65
|
+
setTimer?: typeof setTimeout;
|
|
66
|
+
clearTimer?: typeof clearTimeout;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let current: TaskLease | null = null;
|
|
70
|
+
|
|
71
|
+
/** The AWS environment for a child process that should act under the lease
|
|
72
|
+
* (the credential store's CLI runner). Empty when no lease is held — the
|
|
73
|
+
* caller then runs with whatever the process has, which on a task machine
|
|
74
|
+
* is nothing. */
|
|
75
|
+
export function leaseAwsEnv(): Record<string, string> {
|
|
76
|
+
if (!current) return {};
|
|
77
|
+
return {
|
|
78
|
+
AWS_ACCESS_KEY_ID: current.accessKeyId,
|
|
79
|
+
AWS_SECRET_ACCESS_KEY: current.secretAccessKey,
|
|
80
|
+
AWS_SESSION_TOKEN: current.sessionToken,
|
|
81
|
+
AWS_REGION: current.region,
|
|
82
|
+
AWS_DEFAULT_REGION: current.region,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function currentLease(): TaskLease | null {
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Test seam. */
|
|
91
|
+
export function setCurrentLeaseForTesting(lease: TaskLease | null): void {
|
|
92
|
+
current = lease;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The JSON shape the AWS SDK credential_process contract expects. */
|
|
96
|
+
export function credentialProcessOutput(lease: TaskLease): string {
|
|
97
|
+
return JSON.stringify({
|
|
98
|
+
Version: 1,
|
|
99
|
+
AccessKeyId: lease.accessKeyId,
|
|
100
|
+
SecretAccessKey: lease.secretAccessKey,
|
|
101
|
+
SessionToken: lease.sessionToken,
|
|
102
|
+
Expiration: new Date(lease.expiresAt).toISOString(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The AWS config file content for a lease: default profile, region, and
|
|
107
|
+
* the `credential_process` that serves the live session. Exported for the
|
|
108
|
+
* test that pins it. */
|
|
109
|
+
export function leaseAwsConfig(lease: Pick<TaskLease, "region">): string {
|
|
110
|
+
return `[default]\nregion = ${lease.region}\ncredential_process = ${LEASE_CREDENTIAL_PROCESS}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The org and user a current, unexpired lease was issued for — the credential store paths a
|
|
114
|
+
* sidecar may list (`/uai/orgs/<org>`, `/uai/users/<user>`; the task-machine
|
|
115
|
+
* policy grants nothing above them). Null on an owned host or before the
|
|
116
|
+
* first grant; `orgId` is null for a lease with no org scope. Absent from
|
|
117
|
+
* the lease when the cloud predates the field. */
|
|
118
|
+
export function leaseScope(
|
|
119
|
+
now: number = Date.now(),
|
|
120
|
+
): { orgId: string | null; userId: string } | null {
|
|
121
|
+
if (
|
|
122
|
+
!current ||
|
|
123
|
+
current.expiresAt <= now ||
|
|
124
|
+
typeof current.userId !== "string"
|
|
125
|
+
) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { orgId: current.orgId ?? null, userId: current.userId };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeLeaseFiles(lease: TaskLease): void {
|
|
132
|
+
mkdirSync(dirname(LEASE_CREDENTIALS_FILE), { recursive: true, mode: 0o700 });
|
|
133
|
+
writeFileSync(LEASE_CREDENTIALS_FILE, credentialProcessOutput(lease), { mode: 0o600 });
|
|
134
|
+
chmodSync(LEASE_CREDENTIALS_FILE, 0o600);
|
|
135
|
+
writeFileSync(LEASE_AWS_CONFIG_FILE, leaseAwsConfig(lease), { mode: 0o600 });
|
|
136
|
+
chmodSync(LEASE_AWS_CONFIG_FILE, 0o600);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class LeaseManager {
|
|
140
|
+
readonly #opts: Required<Pick<LeaseManagerOptions, "now" | "setTimer" | "clearTimer">> &
|
|
141
|
+
LeaseManagerOptions;
|
|
142
|
+
#pending: { opId: string; timer: ReturnType<typeof setTimeout> } | null = null;
|
|
143
|
+
#refresh: ReturnType<typeof setTimeout> | null = null;
|
|
144
|
+
#granted = false;
|
|
145
|
+
#stopped = false;
|
|
146
|
+
#sequence = 0;
|
|
147
|
+
|
|
148
|
+
constructor(opts: LeaseManagerOptions) {
|
|
149
|
+
this.#opts = {
|
|
150
|
+
...opts,
|
|
151
|
+
now: opts.now ?? Date.now,
|
|
152
|
+
setTimer: opts.setTimer ?? setTimeout,
|
|
153
|
+
clearTimer: opts.clearTimer ?? clearTimeout,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Ask now. Idempotent while a request is in flight. */
|
|
158
|
+
request(): void {
|
|
159
|
+
if (this.#stopped || this.#pending) return;
|
|
160
|
+
const opId = `lease-${this.#opts.taskId}-${++this.#sequence}-${this.#opts.now()}`;
|
|
161
|
+
const sent = this.#opts.send({ kind: "lease.request", opId, taskId: this.#opts.taskId });
|
|
162
|
+
const timer = this.#opts.setTimer(() => {
|
|
163
|
+
// Unanswered (the socket dropped, the cloud was mid-deploy): try again.
|
|
164
|
+
this.#pending = null;
|
|
165
|
+
this.#armRetry();
|
|
166
|
+
}, LEASE_REQUEST_TIMEOUT_MS);
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
this.#pending = { opId, timer };
|
|
169
|
+
if (!sent) {
|
|
170
|
+
// Nothing on the wire; retry once the socket is back.
|
|
171
|
+
this.#opts.clearTimer(timer);
|
|
172
|
+
this.#pending = null;
|
|
173
|
+
this.#armRetry();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Bridge dispatch hands every `lease.*` frame here. */
|
|
178
|
+
handle(frame: { kind: "lease.grant"; opId: string; lease: TaskLease } | { kind: "lease.deny"; opId: string; error: string }): void {
|
|
179
|
+
if (!this.#pending || this.#pending.opId !== frame.opId) return;
|
|
180
|
+
this.#opts.clearTimer(this.#pending.timer);
|
|
181
|
+
this.#pending = null;
|
|
182
|
+
if (frame.kind === "lease.deny") {
|
|
183
|
+
current = null;
|
|
184
|
+
this.#opts.onDenied?.(frame.error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
current = frame.lease;
|
|
188
|
+
if (this.#opts.writeFiles !== false) {
|
|
189
|
+
try {
|
|
190
|
+
writeLeaseFiles(frame.lease);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[lease] could not write the credential_process files: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const first = !this.#granted;
|
|
198
|
+
this.#granted = true;
|
|
199
|
+
this.#armRefresh(frame.lease);
|
|
200
|
+
if (first) this.#opts.onFirstGrant?.(frame.lease);
|
|
201
|
+
this.#opts.onGrant?.(frame.lease);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The socket reconnected: whatever was pending is void; ask again. */
|
|
205
|
+
reconnected(): void {
|
|
206
|
+
if (this.#pending) {
|
|
207
|
+
this.#opts.clearTimer(this.#pending.timer);
|
|
208
|
+
this.#pending = null;
|
|
209
|
+
}
|
|
210
|
+
this.request();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
stop(): void {
|
|
214
|
+
this.#stopped = true;
|
|
215
|
+
if (this.#pending) this.#opts.clearTimer(this.#pending.timer);
|
|
216
|
+
if (this.#refresh) this.#opts.clearTimer(this.#refresh);
|
|
217
|
+
this.#pending = null;
|
|
218
|
+
this.#refresh = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#armRefresh(lease: TaskLease): void {
|
|
222
|
+
if (this.#refresh) this.#opts.clearTimer(this.#refresh);
|
|
223
|
+
const delay = Math.max(1_000, lease.expiresAt - LEASE_REFRESH_LEAD_MS - this.#opts.now());
|
|
224
|
+
this.#refresh = this.#opts.setTimer(() => {
|
|
225
|
+
this.#refresh = null;
|
|
226
|
+
this.request();
|
|
227
|
+
}, delay);
|
|
228
|
+
this.#refresh.unref?.();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#armRetry(): void {
|
|
232
|
+
if (this.#stopped || this.#refresh) return;
|
|
233
|
+
this.#refresh = this.#opts.setTimer(() => {
|
|
234
|
+
this.#refresh = null;
|
|
235
|
+
this.request();
|
|
236
|
+
}, LEASE_RETRY_MS);
|
|
237
|
+
this.#refresh.unref?.();
|
|
238
|
+
}
|
|
239
|
+
}
|