@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,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154 §3 on the sign-in box: write one capture as one immutable revision,
|
|
3
|
+
* exactly where the cloud's flow says, and never read it back.
|
|
4
|
+
*
|
|
5
|
+
* The cloud begins every capture (lib/credential-metadata.ts) and hands the
|
|
6
|
+
* box the revision's parameter names. The box checks them, builds the two
|
|
7
|
+
* parts from what the sign-in produced, and creates them, broker-only part
|
|
8
|
+
* first:
|
|
9
|
+
*
|
|
10
|
+
* - lease-readable: what a task may receive: a static key as entered, or an
|
|
11
|
+
* OAuth login's brokered copy (access token in, refresh token replaced);
|
|
12
|
+
* - broker-only (OAuth logins only): the full material, which only the
|
|
13
|
+
* broker reads and only the broker ever overwrites.
|
|
14
|
+
*
|
|
15
|
+
* Both are created, never overwritten (`putImmutable`), with tags naming the
|
|
16
|
+
* workspace and person the role policy checks. The answer carries names and
|
|
17
|
+
* versions, never material.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
ENGINE_CREDENTIAL_FILES,
|
|
21
|
+
ENGINE_ENV_CREDENTIAL_KEYS,
|
|
22
|
+
MAX_CREDENTIAL_VALUE_BYTES,
|
|
23
|
+
parseCaptureFlowWire,
|
|
24
|
+
serializeLeasedValue,
|
|
25
|
+
type CaptureFlowWire,
|
|
26
|
+
type CaptureResultWire,
|
|
27
|
+
type CredentialLocation,
|
|
28
|
+
} from "../src/cloud-credentials";
|
|
29
|
+
import { parseCustomConnectionMaterial, type CustomConnectionMaterial } from "../src/custom-connections";
|
|
30
|
+
import { ImmutableParameterAbortedError, storeFailureKind, type CredentialStore } from "./credential-store";
|
|
31
|
+
import { laneFor } from "./token-lanes/registry";
|
|
32
|
+
import {
|
|
33
|
+
emptyRefreshMeta,
|
|
34
|
+
refreshMetaFrom,
|
|
35
|
+
type CredentialFiles,
|
|
36
|
+
type RefreshMeta,
|
|
37
|
+
} from "./token-lanes/types";
|
|
38
|
+
|
|
39
|
+
/** What only the broker reads: the full material of an OAuth login. */
|
|
40
|
+
export interface BrokerEngineValue {
|
|
41
|
+
v: 1;
|
|
42
|
+
part: "broker";
|
|
43
|
+
location: CredentialLocation;
|
|
44
|
+
class: "engine";
|
|
45
|
+
lane: string;
|
|
46
|
+
files: CredentialFiles;
|
|
47
|
+
refresh: RefreshMeta;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type EngineCaptureMaterial =
|
|
51
|
+
| { authKind: "env"; env: Record<string, string> }
|
|
52
|
+
| { authKind: "config-dir"; files: CredentialFiles }
|
|
53
|
+
| { authKind: "custom-connection"; connection: CustomConnectionMaterial };
|
|
54
|
+
|
|
55
|
+
export interface BuiltCapture {
|
|
56
|
+
lease: string;
|
|
57
|
+
broker: string | null;
|
|
58
|
+
expiresAt: number | null;
|
|
59
|
+
/** Metadata the lease part also carries (never material): an SSH key's
|
|
60
|
+
* public half, so a retry can tell which key the revision holds. The
|
|
61
|
+
* identity tags always win over these. */
|
|
62
|
+
leaseTags?: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ParsedCaptureFlow = CaptureFlowWire & { location: CredentialLocation };
|
|
66
|
+
|
|
67
|
+
export function parseCaptureFlow(value: unknown): ParsedCaptureFlow | null {
|
|
68
|
+
return parseCaptureFlowWire(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onlyKeys(record: Record<string, string>, allowed: readonly string[]): boolean {
|
|
72
|
+
const keys = Object.keys(record);
|
|
73
|
+
return keys.length > 0 && keys.every((key) => allowed.includes(key));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The two parts of an AI account's capture, or an error the sign-in shows.
|
|
78
|
+
* The flow's broker part must exist exactly when the material rotates: a
|
|
79
|
+
* rotating login without one would leave its refresh token nowhere the
|
|
80
|
+
* broker can reach (and a task holding it would refresh it itself), and a
|
|
81
|
+
* static key needs none.
|
|
82
|
+
*/
|
|
83
|
+
export function buildEngineCapture(
|
|
84
|
+
flow: ParsedCaptureFlow,
|
|
85
|
+
material: EngineCaptureMaterial,
|
|
86
|
+
): BuiltCapture | { error: string } {
|
|
87
|
+
const { location } = flow;
|
|
88
|
+
if (location.class !== "engine") {
|
|
89
|
+
return { error: "This capture is not an AI account." };
|
|
90
|
+
}
|
|
91
|
+
const kind = location.engineKind!;
|
|
92
|
+
if (material.authKind === "custom-connection") {
|
|
93
|
+
const connection = parseCustomConnectionMaterial(material.connection);
|
|
94
|
+
if (!connection || connection.engine !== kind || flow.brokerParameter !== null) {
|
|
95
|
+
return { error: "unsupported" };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
lease: serializeLeasedValue({ v: 1, part: "lease", location, class: "engine", authKind: "custom-connection", connection }),
|
|
99
|
+
broker: null,
|
|
100
|
+
expiresAt: connection.expiresAt ?? null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (material.authKind === "env") {
|
|
104
|
+
if (!onlyKeys(material.env, ENGINE_ENV_CREDENTIAL_KEYS[kind] ?? [])) {
|
|
105
|
+
return { error: "This key is not one the engine reads." };
|
|
106
|
+
}
|
|
107
|
+
if (flow.brokerParameter !== null) {
|
|
108
|
+
return { error: "A pasted key does not rotate; it has no broker part." };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
lease: serializeLeasedValue({
|
|
112
|
+
v: 1,
|
|
113
|
+
part: "lease",
|
|
114
|
+
location,
|
|
115
|
+
class: "engine",
|
|
116
|
+
authKind: "env",
|
|
117
|
+
env: { ...material.env },
|
|
118
|
+
}),
|
|
119
|
+
broker: null,
|
|
120
|
+
expiresAt: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (!onlyKeys(material.files, ENGINE_CREDENTIAL_FILES[kind] ?? [])) {
|
|
124
|
+
return { error: "The sign-in produced files the engine does not use." };
|
|
125
|
+
}
|
|
126
|
+
const lane = laneFor(
|
|
127
|
+
{ kind, authKind: "config-dir", id: location.credentialId!, scope: location.scope },
|
|
128
|
+
material.files,
|
|
129
|
+
);
|
|
130
|
+
if (lane === null) {
|
|
131
|
+
if (flow.brokerParameter !== null) {
|
|
132
|
+
return { error: "This credential does not rotate; it has no broker part." };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
lease: serializeLeasedValue({
|
|
136
|
+
v: 1,
|
|
137
|
+
part: "lease",
|
|
138
|
+
location,
|
|
139
|
+
class: "engine",
|
|
140
|
+
authKind: "config-dir",
|
|
141
|
+
files: { ...material.files },
|
|
142
|
+
expiresAt: null,
|
|
143
|
+
}),
|
|
144
|
+
broker: null,
|
|
145
|
+
expiresAt: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (flow.brokerParameter === null) {
|
|
149
|
+
return { error: "This sign-in rotates, and the cloud gave it nowhere to keep its refresh token." };
|
|
150
|
+
}
|
|
151
|
+
const expiresAt = lane.expiresAt(material.files);
|
|
152
|
+
const broker: BrokerEngineValue = {
|
|
153
|
+
v: 1,
|
|
154
|
+
part: "broker",
|
|
155
|
+
location,
|
|
156
|
+
class: "engine",
|
|
157
|
+
lane: lane.id,
|
|
158
|
+
files: { ...material.files },
|
|
159
|
+
refresh: { ...emptyRefreshMeta(lane.id), expiresAt },
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
lease: serializeLeasedValue({
|
|
163
|
+
v: 1,
|
|
164
|
+
part: "lease",
|
|
165
|
+
location,
|
|
166
|
+
class: "engine",
|
|
167
|
+
authKind: "config-dir",
|
|
168
|
+
files: lane.brokeredCopy(material.files),
|
|
169
|
+
expiresAt,
|
|
170
|
+
}),
|
|
171
|
+
broker: JSON.stringify(broker),
|
|
172
|
+
expiresAt,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The tags every part carries: the identity a retry is checked against,
|
|
177
|
+
* and the workspace and person the role policy compares. */
|
|
178
|
+
export function captureTags(
|
|
179
|
+
flow: ParsedCaptureFlow,
|
|
180
|
+
part: "lease" | "broker",
|
|
181
|
+
): Record<string, string> {
|
|
182
|
+
return {
|
|
183
|
+
...(flow.orgId !== null ? { org: flow.orgId } : {}),
|
|
184
|
+
...(flow.userId !== null ? { user: flow.userId } : {}),
|
|
185
|
+
uai_credential: flow.credentialId,
|
|
186
|
+
uai_revision: flow.revision,
|
|
187
|
+
uai_part: part,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create both parts, broker-only first: a crash after it leaves a revision
|
|
193
|
+
* the cloud never promotes, which the sweep removes; a lease-readable part
|
|
194
|
+
* never exists without the refresh token it was brokered from. A retry after
|
|
195
|
+
* a lost answer finds its own create by its tags and carries on.
|
|
196
|
+
*/
|
|
197
|
+
export async function writeCapture(
|
|
198
|
+
store: CredentialStore,
|
|
199
|
+
flow: ParsedCaptureFlow,
|
|
200
|
+
built: BuiltCapture,
|
|
201
|
+
options: { signal?: AbortSignal } = {},
|
|
202
|
+
): Promise<CaptureResultWire> {
|
|
203
|
+
if (!store.putImmutable) {
|
|
204
|
+
throw new Error("This credential store cannot create immutable parameters.");
|
|
205
|
+
}
|
|
206
|
+
// A caller's deadline covers the writes too: past it nothing more is sent,
|
|
207
|
+
// and a part already sent is the cloud's to sweep with the revision.
|
|
208
|
+
const { signal } = options;
|
|
209
|
+
if (built.broker !== null) {
|
|
210
|
+
if (flow.brokerParameter === null) throw new Error("no broker part to write");
|
|
211
|
+
if (signal?.aborted) throw new ImmutableParameterAbortedError();
|
|
212
|
+
await store.putImmutable(flow.brokerParameter, built.broker, captureTags(flow, "broker"), { signal });
|
|
213
|
+
}
|
|
214
|
+
if (signal?.aborted) throw new ImmutableParameterAbortedError();
|
|
215
|
+
await store.putImmutable(
|
|
216
|
+
flow.leaseParameter,
|
|
217
|
+
built.lease,
|
|
218
|
+
{ ...built.leaseTags, ...captureTags(flow, "lease") },
|
|
219
|
+
{ signal },
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
credentialId: flow.credentialId,
|
|
223
|
+
revision: flow.revision,
|
|
224
|
+
// Created, never overwritten by the box: version 1. Only the broker
|
|
225
|
+
// writes later versions.
|
|
226
|
+
leaseVersion: 1,
|
|
227
|
+
expiresAt: built.expiresAt,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface CaptureOutcome {
|
|
232
|
+
ok: boolean;
|
|
233
|
+
message: string;
|
|
234
|
+
/** Present exactly when `ok`. */
|
|
235
|
+
capture?: CaptureResultWire;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The sign-in box's sink for an AI account's capture: check the flow, build
|
|
240
|
+
* both parts, create them. Every failure is a message the sign-in shows, and
|
|
241
|
+
* none of them carries material.
|
|
242
|
+
*/
|
|
243
|
+
export async function captureEngineAccount(
|
|
244
|
+
store: CredentialStore | null,
|
|
245
|
+
wire: CaptureFlowWire,
|
|
246
|
+
engine: string,
|
|
247
|
+
material: EngineCaptureMaterial,
|
|
248
|
+
engineLabel: string,
|
|
249
|
+
): Promise<CaptureOutcome> {
|
|
250
|
+
const flow = parseCaptureFlow(wire);
|
|
251
|
+
if (!flow || flow.location.class !== "engine" || flow.location.engineKind !== engine) {
|
|
252
|
+
return { ok: false, message: "The sign-in's destination is invalid; nothing was kept." };
|
|
253
|
+
}
|
|
254
|
+
if (!store) {
|
|
255
|
+
return { ok: false, message: "This station has no credential store to hold the account." };
|
|
256
|
+
}
|
|
257
|
+
const built = buildEngineCapture(flow, material);
|
|
258
|
+
if ("error" in built) return { ok: false, message: built.error };
|
|
259
|
+
try {
|
|
260
|
+
const capture = await writeCapture(store, flow, built);
|
|
261
|
+
return { ok: true, message: `${engineLabel} signed in.`, capture };
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// A category, never the error's words: a store failure can echo input.
|
|
264
|
+
console.warn(`[capture-flow] ${engine} ${flow.credentialId}/${flow.revision}: write failed (${storeFailureKind(err)})`);
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
message: "The credential store refused the write; nothing was kept on this station.",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A stored broker-only part, exactly, for the revision at `expected`; null
|
|
275
|
+
* for anything else. Only the broker reads it (token-broker-revision.ts).
|
|
276
|
+
*/
|
|
277
|
+
export function parseBrokerEngineValue(
|
|
278
|
+
raw: string,
|
|
279
|
+
expected: CredentialLocation,
|
|
280
|
+
): BrokerEngineValue | null {
|
|
281
|
+
if (typeof raw !== "string" || new TextEncoder().encode(raw).length > MAX_CREDENTIAL_VALUE_BYTES) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
let parsed: unknown;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(raw);
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
291
|
+
const o = parsed as Record<string, unknown>;
|
|
292
|
+
const location = o.location as Record<string, unknown> | null;
|
|
293
|
+
if (
|
|
294
|
+
o.v !== 1 ||
|
|
295
|
+
o.part !== "broker" ||
|
|
296
|
+
o.class !== "engine" ||
|
|
297
|
+
expected.class !== "engine" ||
|
|
298
|
+
typeof o.lane !== "string" ||
|
|
299
|
+
!location ||
|
|
300
|
+
typeof location !== "object" ||
|
|
301
|
+
location.class !== expected.class ||
|
|
302
|
+
location.scope !== expected.scope ||
|
|
303
|
+
location.ownerId !== expected.ownerId ||
|
|
304
|
+
location.engineKind !== expected.engineKind ||
|
|
305
|
+
location.credentialId !== expected.credentialId ||
|
|
306
|
+
location.revision !== expected.revision
|
|
307
|
+
) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const allowed = ENGINE_CREDENTIAL_FILES[expected.engineKind ?? ""] ?? [];
|
|
311
|
+
const files = o.files as Record<string, unknown> | null;
|
|
312
|
+
if (
|
|
313
|
+
!files ||
|
|
314
|
+
typeof files !== "object" ||
|
|
315
|
+
Array.isArray(files) ||
|
|
316
|
+
Object.keys(files).length === 0 ||
|
|
317
|
+
!Object.entries(files).every(([name, text]) => allowed.includes(name) && typeof text === "string")
|
|
318
|
+
) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const refresh = refreshMetaFrom(o.refresh);
|
|
322
|
+
if (!refresh || refresh.lane !== o.lane) return null;
|
|
323
|
+
return {
|
|
324
|
+
v: 1,
|
|
325
|
+
part: "broker",
|
|
326
|
+
location: expected,
|
|
327
|
+
class: "engine",
|
|
328
|
+
lane: o.lane,
|
|
329
|
+
files: files as CredentialFiles,
|
|
330
|
+
refresh,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed boundary between the in-memory lease registry and the durable
|
|
3
|
+
* task credential cleanup procedure.
|
|
4
|
+
*
|
|
5
|
+
* The registry describes why its gate closed and whether the cleanup is
|
|
6
|
+
* selective or whole-task. The durable coordinator uses a smaller audit
|
|
7
|
+
* vocabulary and represents selectivity solely by the presence of
|
|
8
|
+
* `credentialIds`. Keep that lossy conversion here so a new registry reason
|
|
9
|
+
* cannot accidentally become a whole-task cleanup (or vice versa).
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
CredentialCleanupReason as LeaseCredentialCleanupReason,
|
|
13
|
+
CredentialCleanupRequest as LeaseCredentialCleanupRequest,
|
|
14
|
+
} from "./cloud-credential-leases";
|
|
15
|
+
import type {
|
|
16
|
+
TaskCredentialCleanupCoordinator,
|
|
17
|
+
TaskCredentialCleanupReason,
|
|
18
|
+
TaskCredentialCleanupRequest,
|
|
19
|
+
} from "./task-credential-cleanup";
|
|
20
|
+
|
|
21
|
+
export type CredentialCleanupRunnerKind = "owned" | "task-machine";
|
|
22
|
+
|
|
23
|
+
type LeaseCleanupMode = LeaseCredentialCleanupRequest["mode"];
|
|
24
|
+
type CleanupReasonByMode = Readonly<
|
|
25
|
+
Record<LeaseCleanupMode, TaskCredentialCleanupReason | null>
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `null` marks a reason/mode pair the registry must never emit. The `satisfies`
|
|
30
|
+
* clause makes additions to either the reason union or the mode union a
|
|
31
|
+
* compile-time decision at this boundary.
|
|
32
|
+
*/
|
|
33
|
+
const CLEANUP_REASON_BY_MODE = {
|
|
34
|
+
starting: { selective: null, full: null },
|
|
35
|
+
recovery: { selective: null, full: "restart" },
|
|
36
|
+
superseded: { selective: null, full: "assignment" },
|
|
37
|
+
"minimum-revision": { selective: "removed", full: "removed" },
|
|
38
|
+
"manifest-narrowed": { selective: null, full: "removed" },
|
|
39
|
+
denied: { selective: null, full: "denied" },
|
|
40
|
+
expired: { selective: null, full: "expired" },
|
|
41
|
+
stopped: { selective: null, full: "stop" },
|
|
42
|
+
parked: { selective: null, full: "park" },
|
|
43
|
+
teardown: { selective: null, full: "teardown" },
|
|
44
|
+
// The registry emits this fail-closed fallback when its synchronous gate
|
|
45
|
+
// callback throws during assignment start/recovery. The durable vocabulary
|
|
46
|
+
// has no internal-callback-failure reason; `restart` is the conservative
|
|
47
|
+
// whole-task recovery category and, critically, lets the coordinator write
|
|
48
|
+
// its pending witness before retrying the quarantine.
|
|
49
|
+
"cleanup-failed": { selective: null, full: "restart" },
|
|
50
|
+
// A launch slot that could not be retired in its runtime (ADR-154 §6):
|
|
51
|
+
// the whole run's cleanup, in the same conservative category.
|
|
52
|
+
"fence-failed": { selective: null, full: "restart" },
|
|
53
|
+
// Shutdown ends assignments without a cleanup: the next process's
|
|
54
|
+
// recovery sweep runs it.
|
|
55
|
+
shutdown: { selective: null, full: null },
|
|
56
|
+
"lease-replaced": { selective: "removed", full: null },
|
|
57
|
+
} as const satisfies Readonly<
|
|
58
|
+
Record<LeaseCredentialCleanupReason, CleanupReasonByMode>
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
function mappedReason(
|
|
62
|
+
reason: LeaseCredentialCleanupReason,
|
|
63
|
+
mode: LeaseCleanupMode,
|
|
64
|
+
): TaskCredentialCleanupReason {
|
|
65
|
+
const mapped = CLEANUP_REASON_BY_MODE[reason][mode];
|
|
66
|
+
if (mapped === null) {
|
|
67
|
+
throw new Error(`invalid ${mode} lease credential cleanup reason: ${reason}`);
|
|
68
|
+
}
|
|
69
|
+
return mapped;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Convert one registry callback into the coordinator's durable request. */
|
|
73
|
+
export function adaptLeaseCredentialCleanupRequest(
|
|
74
|
+
request: Readonly<LeaseCredentialCleanupRequest>,
|
|
75
|
+
runnerKind: CredentialCleanupRunnerKind,
|
|
76
|
+
): TaskCredentialCleanupRequest {
|
|
77
|
+
const reason = mappedReason(request.reason, request.mode);
|
|
78
|
+
|
|
79
|
+
if (request.mode === "selective") {
|
|
80
|
+
if (request.removeFilesSession) {
|
|
81
|
+
throw new Error("selective credential cleanup cannot remove the files session");
|
|
82
|
+
}
|
|
83
|
+
if (request.removed.length === 0) {
|
|
84
|
+
throw new Error("selective credential cleanup requires a removed credential");
|
|
85
|
+
}
|
|
86
|
+
// Several revisions of one stable credential can retire before a floor
|
|
87
|
+
// grant lands. Inventory is keyed by the stable id, so collapse those
|
|
88
|
+
// qualified identities rather than widening or rejecting the cleanup.
|
|
89
|
+
const credentialIds = [
|
|
90
|
+
...new Set(request.removed.map((identity) => identity.credentialId)),
|
|
91
|
+
];
|
|
92
|
+
return {
|
|
93
|
+
taskId: request.assignment.taskId,
|
|
94
|
+
runEpoch: request.assignment.runEpoch,
|
|
95
|
+
reason,
|
|
96
|
+
credentialIds,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (request.removed.length !== 0) {
|
|
101
|
+
throw new Error("full credential cleanup cannot carry selective identities");
|
|
102
|
+
}
|
|
103
|
+
const expectedFilesSession = runnerKind === "task-machine";
|
|
104
|
+
if (request.removeFilesSession !== expectedFilesSession) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${runnerKind} full credential cleanup has the wrong files-session scope`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
taskId: request.assignment.taskId,
|
|
111
|
+
runEpoch: request.assignment.runEpoch,
|
|
112
|
+
reason,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Callback with the exact shape consumed by TaskCredentialLeaseRegistry. */
|
|
117
|
+
export function createLeaseCredentialCleanupAdapter(
|
|
118
|
+
coordinator: Pick<TaskCredentialCleanupCoordinator, "cleanup">,
|
|
119
|
+
runnerKind: CredentialCleanupRunnerKind,
|
|
120
|
+
): (request: LeaseCredentialCleanupRequest) => Promise<void> {
|
|
121
|
+
return (request) =>
|
|
122
|
+
coordinator.cleanup(adaptLeaseCredentialCleanupRequest(request, runnerKind));
|
|
123
|
+
}
|