@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,16 @@
|
|
|
1
|
+
/** Immutable write-only task secret writer. Never return a value or provider error. */
|
|
2
|
+
import { createHash, createHmac } from "node:crypto";
|
|
3
|
+
import { isTaskSecretStagingPut, type TaskSecretStagingPut } from "../src/task-secrets";
|
|
4
|
+
import { configuredCredentialStore, type CredentialStore } from "./credential-store";
|
|
5
|
+
import { masterKey } from "./secrets";
|
|
6
|
+
export function taskSecretParameter(taskId: string, name: string, stagingId: string): string {
|
|
7
|
+
return `/uai/lease/tasks/${taskId}/secrets/${createHash("sha256").update(name).digest("hex")}/${stagingId}`;
|
|
8
|
+
}
|
|
9
|
+
export async function writeTaskSecretStaging(frame: TaskSecretStagingPut, options: { store?: CredentialStore | null; hmacKey?: Buffer } = {}): Promise<void> {
|
|
10
|
+
if (!isTaskSecretStagingPut(frame)) throw new Error("invalid task secret staging request");
|
|
11
|
+
const store = Object.hasOwn(options, "store") ? options.store : configuredCredentialStore();
|
|
12
|
+
if (!store?.putImmutable) throw new Error("task secret writer unavailable");
|
|
13
|
+
const envelope = JSON.stringify({ v: 1, kind: "task-secret", orgId: frame.orgId, taskId: frame.taskId, name: frame.name, stagingId: frame.stagingId, value: frame.value });
|
|
14
|
+
const mac = createHmac("sha256", options.hmacKey ?? masterKey()).update("uai.task-secret.staging.v1\0").update(envelope).digest("hex");
|
|
15
|
+
await store.putImmutable(taskSecretParameter(frame.taskId, frame.name, frame.stagingId), envelope, { org: frame.orgId, task: frame.taskId, uai_staging_mac: mac });
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const ACTIVE_STATUSES = [
|
|
2
|
+
"queued",
|
|
3
|
+
"starting",
|
|
4
|
+
"running",
|
|
5
|
+
"error",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export const STOPPED_STATUSES = ["stopped"] as const;
|
|
9
|
+
|
|
10
|
+
export const TERMINAL_STATUSES = [
|
|
11
|
+
"finished",
|
|
12
|
+
"canceled",
|
|
13
|
+
"killed",
|
|
14
|
+
"shipped",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export const DASHBOARD_STATUSES = [
|
|
18
|
+
...ACTIVE_STATUSES,
|
|
19
|
+
...STOPPED_STATUSES,
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export const NON_TERMINAL_STATUSES = [
|
|
23
|
+
...ACTIVE_STATUSES,
|
|
24
|
+
...STOPPED_STATUSES,
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type TaskStatus =
|
|
28
|
+
| (typeof ACTIVE_STATUSES)[number]
|
|
29
|
+
| (typeof STOPPED_STATUSES)[number]
|
|
30
|
+
| (typeof TERMINAL_STATUSES)[number];
|
|
31
|
+
|
|
32
|
+
export function isActive(status: string): boolean {
|
|
33
|
+
return (ACTIVE_STATUSES as readonly string[]).includes(status);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isStopped(status: string): boolean {
|
|
37
|
+
return (STOPPED_STATUSES as readonly string[]).includes(status);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isTerminal(status: string): boolean {
|
|
41
|
+
return (TERMINAL_STATUSES as readonly string[]).includes(status);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isOnDashboard(status: string): boolean {
|
|
45
|
+
return isActive(status) || isStopped(status);
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-token minting (ADR-048) — the host mints the token the in-container `uai`
|
|
3
|
+
* CLI uses; the cloud verifies it (lib/task-token.ts). The signing key is the
|
|
4
|
+
* TASK's ephemeral `cliSecret`, issued by the cloud at task-up (not a static
|
|
5
|
+
* shared secret). Algorithm MUST stay identical to the cloud side.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export interface TaskTokenPayload {
|
|
10
|
+
taskId: string;
|
|
11
|
+
userId: string;
|
|
12
|
+
permissions: string[];
|
|
13
|
+
agentId?: string;
|
|
14
|
+
/** ADR-157 §3: the agent's roster generation; the cloud requires it to match. */
|
|
15
|
+
generation?: string;
|
|
16
|
+
/** Server-issued lead context, frozen into a lead process token by the host. */
|
|
17
|
+
leadRunId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PREFIX = "uai_";
|
|
21
|
+
|
|
22
|
+
function b64url(buf: Buffer): string {
|
|
23
|
+
return buf
|
|
24
|
+
.toString("base64")
|
|
25
|
+
.replace(/\+/g, "-")
|
|
26
|
+
.replace(/\//g, "_")
|
|
27
|
+
.replace(/=+$/, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function signTaskToken(payload: TaskTokenPayload, secret: string): string {
|
|
31
|
+
const body = b64url(
|
|
32
|
+
Buffer.from(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
t: payload.taskId,
|
|
35
|
+
u: payload.userId,
|
|
36
|
+
p: payload.permissions,
|
|
37
|
+
...(payload.agentId ? { a: payload.agentId } : {}),
|
|
38
|
+
...(payload.generation ? { g: payload.generation } : {}),
|
|
39
|
+
...(payload.leadRunId ? { l: payload.leadRunId } : {}),
|
|
40
|
+
iat: Math.floor(Date.now() / 1000),
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
const sig = b64url(createHmac("sha256", secret).update(body).digest());
|
|
45
|
+
return `${PREFIX}${body}.${sig}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154, owner answer 3: what Uai Cloud already keeps in Parameter Store
|
|
3
|
+
* under today's layout (ADR-134), its workspaces' AI accounts
|
|
4
|
+
* (`<prefix>/orgs/<org>/engines/<kind>/<id>`), is adopted in place. The cloud
|
|
5
|
+
* lists the accounts through the broker (metadata only: the broker is the
|
|
6
|
+
* one identity that reads those values), opens a revision for each, and
|
|
7
|
+
* names it here. People's MCP connections follow separately, once a fence
|
|
8
|
+
* keeps the v0 task machines that refresh them themselves from doing so.
|
|
9
|
+
*
|
|
10
|
+
* One refresher per grant, always (§7):
|
|
11
|
+
* - before anything is copied, the adoption marker (`<prefix>/adopted/...`) is
|
|
12
|
+
* created under the scan's own lock, so no scan run is refreshing the
|
|
13
|
+
* account meanwhile and none refreshes it again;
|
|
14
|
+
* - the full material becomes the revision's broker-only part and what a
|
|
15
|
+
* task may hold its lease-readable part, both created (never overwritten)
|
|
16
|
+
* with the tags the role policy checks, as a capture's are;
|
|
17
|
+
* - today's parameter then keeps what a task may hold only (the brokered
|
|
18
|
+
* copy): the one refresh token
|
|
19
|
+
* left is the revision's, which only the cloud's scheduler names. Until
|
|
20
|
+
* the fleet audit each refresh of that revision writes the same there,
|
|
21
|
+
* for the runners still reading it (`syncLegacyCopy`).
|
|
22
|
+
*
|
|
23
|
+
* A value that can never be adopted as it is answers `unsupported`, before
|
|
24
|
+
* any marker: it keeps its refresher. A retry after a lost answer finds its
|
|
25
|
+
* own marker and creates, and carries on where it stopped. Nothing here
|
|
26
|
+
* returns or logs material, nor an error's message (only its kind).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
brokerErrorKind,
|
|
31
|
+
brokerParameterName,
|
|
32
|
+
DEFAULT_CREDENTIAL_STORE_PREFIX,
|
|
33
|
+
leaseParameterName,
|
|
34
|
+
legacyAdoptedRoot,
|
|
35
|
+
legacyAdoptionMarkerName,
|
|
36
|
+
MAX_LEGACY_ACCOUNTS,
|
|
37
|
+
parseLegacyEngineParameterName,
|
|
38
|
+
parseLegacyParameterName,
|
|
39
|
+
type BrokerAdoptAnswer,
|
|
40
|
+
type BrokerAdoptRequest,
|
|
41
|
+
type BrokerLegacyAccount,
|
|
42
|
+
type BrokerLegacyListAnswer,
|
|
43
|
+
type CredentialLocation,
|
|
44
|
+
type LegacyEngineParameter,
|
|
45
|
+
} from "../src/cloud-credentials";
|
|
46
|
+
import { buildEngineCapture, captureTags, parseCaptureFlow, type EngineCaptureMaterial } from "./capture-flow";
|
|
47
|
+
import type { RevisionStore, StoredParameter } from "./token-broker-revision";
|
|
48
|
+
import { laneFor } from "./token-lanes/registry";
|
|
49
|
+
import type { CredentialFiles, TokenLane } from "./token-lanes/types";
|
|
50
|
+
|
|
51
|
+
/** What adoption needs beyond a refresh: create-only writes, today's
|
|
52
|
+
* accounts and markers, and the scan's lock. */
|
|
53
|
+
export interface AdoptionStore extends RevisionStore {
|
|
54
|
+
/** The exact ADR-134 root this store reads and writes. */
|
|
55
|
+
readonly legacyPrefix: string;
|
|
56
|
+
/**
|
|
57
|
+
* Create a parameter with its identity tags, never overwriting:
|
|
58
|
+
* `matched` when it exists already with exactly these tags (this create,
|
|
59
|
+
* retried). Throws when it exists as anything else.
|
|
60
|
+
*/
|
|
61
|
+
create(name: string, value: string, tags: Record<string, string>): Promise<"created" | "matched">;
|
|
62
|
+
/** Every parameter under today's workspace accounts root, decrypted. */
|
|
63
|
+
listLegacy(): Promise<Array<{ name: string; value: string }>>;
|
|
64
|
+
/** Every adoption marker of a workspace account, by the marker's name. */
|
|
65
|
+
listMarkers(): Promise<Array<{ name: string; value: string }>>;
|
|
66
|
+
/** The scan's lock (one run at a time), or null while a run holds it. */
|
|
67
|
+
scanLock(): Promise<(() => Promise<void>) | null>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An AI account's value under today's layout, as the shared host wrote it. */
|
|
71
|
+
export interface LegacyEngineValue {
|
|
72
|
+
/** The whole envelope: rewritten with only its files changed. */
|
|
73
|
+
raw: Record<string, unknown>;
|
|
74
|
+
label: string;
|
|
75
|
+
authKind: "env" | "config-dir";
|
|
76
|
+
position: number;
|
|
77
|
+
env: Record<string, string> | null;
|
|
78
|
+
files: CredentialFiles | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stringMap(value: unknown): Record<string, string> | null {
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
83
|
+
const out: Record<string, string> = {};
|
|
84
|
+
for (const [key, text] of Object.entries(value as Record<string, unknown>)) {
|
|
85
|
+
if (typeof text !== "string") return null;
|
|
86
|
+
out[key] = text;
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function jsonObject(value: string): Record<string, unknown> | null {
|
|
92
|
+
try {
|
|
93
|
+
const parsed: unknown = JSON.parse(value);
|
|
94
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Today's value for exactly this account, or null. */
|
|
101
|
+
export function parseLegacyEngineValue(value: string, legacy: LegacyEngineParameter): LegacyEngineValue | null {
|
|
102
|
+
const o = jsonObject(value);
|
|
103
|
+
if (
|
|
104
|
+
!o ||
|
|
105
|
+
o.v !== 1 ||
|
|
106
|
+
o.id !== legacy.accountId ||
|
|
107
|
+
o.kind !== legacy.engineKind ||
|
|
108
|
+
o.scope !== `org:${legacy.orgId}` ||
|
|
109
|
+
typeof o.label !== "string" ||
|
|
110
|
+
o.label.trim().length === 0 ||
|
|
111
|
+
(o.authKind !== "env" && o.authKind !== "config-dir")
|
|
112
|
+
) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const env = o.authKind === "env" ? stringMap(o.env) : null;
|
|
116
|
+
const files = o.authKind === "config-dir" ? stringMap(o.files) : null;
|
|
117
|
+
if (o.authKind === "env" ? env === null : files === null) return null;
|
|
118
|
+
return {
|
|
119
|
+
raw: o,
|
|
120
|
+
label: o.label.trim(),
|
|
121
|
+
authKind: o.authKind,
|
|
122
|
+
position: typeof o.position === "number" && Number.isSafeInteger(o.position) ? o.position : 0,
|
|
123
|
+
env,
|
|
124
|
+
files,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function laneOf(legacy: LegacyEngineParameter, value: LegacyEngineValue): TokenLane | null {
|
|
129
|
+
if (value.authKind !== "config-dir" || !value.files) return null;
|
|
130
|
+
return laneFor(
|
|
131
|
+
{ kind: legacy.engineKind, authKind: "config-dir", id: legacy.accountId, scope: "org" },
|
|
132
|
+
value.files,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whether an account's material is one a revision can hold, without a
|
|
137
|
+
* revision: the capture's own checks, on a stand-in destination. */
|
|
138
|
+
function engineUnsupported(legacy: LegacyEngineParameter, value: LegacyEngineValue): string | null {
|
|
139
|
+
const lane = laneOf(legacy, value);
|
|
140
|
+
const location: CredentialLocation = {
|
|
141
|
+
class: "engine",
|
|
142
|
+
scope: "org",
|
|
143
|
+
ownerId: legacy.orgId,
|
|
144
|
+
engineKind: legacy.engineKind,
|
|
145
|
+
credentialId: "adoption-check",
|
|
146
|
+
revision: "adoption-check",
|
|
147
|
+
};
|
|
148
|
+
const flow = parseCaptureFlow({
|
|
149
|
+
credentialId: "adoption-check",
|
|
150
|
+
revision: "adoption-check",
|
|
151
|
+
leaseParameter: leaseParameterName(location),
|
|
152
|
+
brokerParameter: lane ? brokerParameterName(location) : null,
|
|
153
|
+
orgId: legacy.orgId,
|
|
154
|
+
userId: null,
|
|
155
|
+
});
|
|
156
|
+
if (!flow) return "destination";
|
|
157
|
+
const built = buildEngineCapture(flow, material(value));
|
|
158
|
+
return "error" in built ? (value.authKind === "env" ? "env_keys" : "files") : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function material(value: LegacyEngineValue): EngineCaptureMaterial {
|
|
162
|
+
return value.authKind === "env" ? { authKind: "env", env: value.env! } : { authKind: "config-dir", files: value.files! };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function legacyNameOfMarker(markerName: string, prefix: string): string | null {
|
|
166
|
+
const adoptedRoot = legacyAdoptedRoot(prefix);
|
|
167
|
+
if (!markerName.startsWith(`${adoptedRoot}/`)) return null;
|
|
168
|
+
const name = `${prefix}${markerName.slice(adoptedRoot.length)}`;
|
|
169
|
+
return parseLegacyParameterName(name, prefix) ? name : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseMarker(value: string): { credentialId: string; revision: string } | null {
|
|
173
|
+
const o = jsonObject(value);
|
|
174
|
+
return typeof o?.credentialId === "string" && typeof o.revision === "string"
|
|
175
|
+
? { credentialId: o.credentialId, revision: o.revision }
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sameFiles(a: CredentialFiles, b: CredentialFiles): boolean {
|
|
180
|
+
const keys = Object.keys(a).sort();
|
|
181
|
+
const other = Object.keys(b).sort();
|
|
182
|
+
return keys.length === other.length && keys.every((k, i) => k === other[i] && a[k] === b[k]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const unavailable = (error: unknown): { outcome: "unavailable"; error: string } => ({
|
|
186
|
+
outcome: "unavailable",
|
|
187
|
+
error: typeof error === "string" ? error : brokerErrorKind(error),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* One page of today's workspace AI accounts, metadata only (never a value),
|
|
192
|
+
* in parameter-name order after `after`: each with its lane, the revision it
|
|
193
|
+
* was adopted into once it was, and why it cannot be adopted as it is.
|
|
194
|
+
*/
|
|
195
|
+
export async function listLegacyAccounts(store: AdoptionStore, after: string | null = null): Promise<BrokerLegacyListAnswer> {
|
|
196
|
+
try {
|
|
197
|
+
const [values, markers] = await Promise.all([store.listLegacy(), store.listMarkers()]);
|
|
198
|
+
const adopted = new Map<string, { credentialId: string; revision: string }>();
|
|
199
|
+
for (const marker of markers) {
|
|
200
|
+
const name = legacyNameOfMarker(marker.name, store.legacyPrefix);
|
|
201
|
+
const named = parseMarker(marker.value);
|
|
202
|
+
if (name && named) adopted.set(name, named);
|
|
203
|
+
}
|
|
204
|
+
const ordered = values
|
|
205
|
+
.filter(
|
|
206
|
+
({ name }) =>
|
|
207
|
+
parseLegacyEngineParameterName(name, store.legacyPrefix) !== null && (after === null || name > after),
|
|
208
|
+
)
|
|
209
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
210
|
+
const accounts: BrokerLegacyAccount[] = [];
|
|
211
|
+
let next: string | null = null;
|
|
212
|
+
for (const { name, value } of ordered) {
|
|
213
|
+
if (accounts.length >= MAX_LEGACY_ACCOUNTS) {
|
|
214
|
+
next = accounts.at(-1)!.parameter;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const legacy = parseLegacyEngineParameterName(name, store.legacyPrefix)!;
|
|
218
|
+
const parsed = parseLegacyEngineValue(value, legacy);
|
|
219
|
+
accounts.push({
|
|
220
|
+
parameter: name,
|
|
221
|
+
orgId: legacy.orgId,
|
|
222
|
+
engineKind: legacy.engineKind,
|
|
223
|
+
accountId: legacy.accountId,
|
|
224
|
+
label: parsed ? parsed.label.slice(0, 256) : legacy.accountId,
|
|
225
|
+
authKind: parsed?.authKind ?? "config-dir",
|
|
226
|
+
position: parsed?.position ?? 0,
|
|
227
|
+
lane: parsed ? (laneOf(legacy, parsed)?.id ?? null) : null,
|
|
228
|
+
adopted: adopted.get(name) ?? null,
|
|
229
|
+
unsupported: parsed ? engineUnsupported(legacy, parsed) : "value",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { outcome: "listed", accounts, next };
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return unavailable(error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Adopt one parameter as the revision the cloud named. */
|
|
239
|
+
export async function adoptLegacyAccount(request: BrokerAdoptRequest, store: AdoptionStore): Promise<BrokerAdoptAnswer> {
|
|
240
|
+
const legacy = parseLegacyParameterName(request.legacyParameter, store.legacyPrefix)!;
|
|
241
|
+
let releaseCredential: (() => Promise<void>) | null = null;
|
|
242
|
+
let releaseScan: (() => Promise<void>) | null = null;
|
|
243
|
+
try {
|
|
244
|
+
releaseCredential = await store.lock(request.credentialId);
|
|
245
|
+
if (!releaseCredential) return { outcome: "busy" };
|
|
246
|
+
// No scan run is refreshing today's parameter while it is copied.
|
|
247
|
+
releaseScan = await store.scanLock();
|
|
248
|
+
if (!releaseScan) return { outcome: "busy" };
|
|
249
|
+
|
|
250
|
+
const current = await store.get(request.legacyParameter);
|
|
251
|
+
if (!current) return { outcome: "absent" };
|
|
252
|
+
const flow = parseCaptureFlow({
|
|
253
|
+
credentialId: request.credentialId,
|
|
254
|
+
revision: request.revision,
|
|
255
|
+
leaseParameter: request.leaseParameter,
|
|
256
|
+
brokerParameter: request.brokerParameter,
|
|
257
|
+
orgId: request.orgId,
|
|
258
|
+
userId: request.userId,
|
|
259
|
+
});
|
|
260
|
+
if (!flow) return { outcome: "unsupported", reason: "destination" };
|
|
261
|
+
|
|
262
|
+
// Built and checked before the marker: what cannot be adopted keeps its
|
|
263
|
+
// refresher.
|
|
264
|
+
const value = parseLegacyEngineValue(current.value, legacy);
|
|
265
|
+
if (!value) return { outcome: "unsupported", reason: "value" };
|
|
266
|
+
const parts = buildEngineCapture(flow, material(value));
|
|
267
|
+
if ("error" in parts) return { outcome: "unsupported", reason: value.authKind === "env" ? "env_keys" : "files" };
|
|
268
|
+
// Today's parameter keeps the brokered copy only.
|
|
269
|
+
let rewrite: string | null = null;
|
|
270
|
+
const lane = laneOf(legacy, value);
|
|
271
|
+
if (lane && parts.broker !== null && value.files) {
|
|
272
|
+
const brokered = lane.brokeredCopy(value.files);
|
|
273
|
+
if (!sameFiles(brokered, value.files)) rewrite = JSON.stringify({ ...value.raw, files: brokered });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 1. The marker: from here nothing refreshes today's parameter.
|
|
277
|
+
const markerName = legacyAdoptionMarkerName(request.legacyParameter, store.legacyPrefix);
|
|
278
|
+
const marker = await store.get(markerName);
|
|
279
|
+
if (marker) {
|
|
280
|
+
const named = parseMarker(marker.value);
|
|
281
|
+
if (!named || named.credentialId !== request.credentialId || named.revision !== request.revision) {
|
|
282
|
+
return unavailable("adopted into another revision");
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
await store.create(
|
|
286
|
+
markerName,
|
|
287
|
+
JSON.stringify({ credentialId: request.credentialId, revision: request.revision }),
|
|
288
|
+
{ uai_credential: request.credentialId, uai_revision: request.revision, uai_part: "adopted" },
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 2. Both parts, broker-only first, as a capture writes them. A retry
|
|
293
|
+
// matches its own creates and never rebuilds the broker part from what
|
|
294
|
+
// step 3 left in today's parameter.
|
|
295
|
+
if (parts.broker !== null) await store.create(request.brokerParameter!, parts.broker, captureTags(flow, "broker"));
|
|
296
|
+
await store.create(request.leaseParameter, parts.lease, captureTags(flow, "lease"));
|
|
297
|
+
|
|
298
|
+
// 3. Today's parameter keeps what a task may hold only.
|
|
299
|
+
if (rewrite !== null) await store.overwrite(request.legacyParameter, rewrite);
|
|
300
|
+
|
|
301
|
+
const lease = await store.get(request.leaseParameter);
|
|
302
|
+
if (!lease) return unavailable("the lease-readable part is missing");
|
|
303
|
+
return { outcome: "adopted", version: lease.version, expiresAt: parts.expiresAt };
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return unavailable(error);
|
|
306
|
+
} finally {
|
|
307
|
+
if (releaseScan) await releaseScan();
|
|
308
|
+
if (releaseCredential) await releaseCredential();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Whether today's parameter of an adopted revision now agrees with it. */
|
|
313
|
+
export type LegacySync = "synced" | "behind";
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Until the fleet audit: keep today's parameter of an adopted AI account
|
|
317
|
+
* agreeing with its revision, the brokered copy only, for the runners still
|
|
318
|
+
* reading it. Never creates it, and never fails the refresh it rides on: it
|
|
319
|
+
* says whether the write landed, and the cloud asks again when it did not.
|
|
320
|
+
*/
|
|
321
|
+
export async function syncLegacyCopy(
|
|
322
|
+
store: Pick<RevisionStore, "get" | "overwrite">,
|
|
323
|
+
legacyParameter: string,
|
|
324
|
+
lane: TokenLane,
|
|
325
|
+
files: CredentialFiles,
|
|
326
|
+
legacyPrefix: string = DEFAULT_CREDENTIAL_STORE_PREFIX,
|
|
327
|
+
): Promise<LegacySync> {
|
|
328
|
+
const legacy = parseLegacyEngineParameterName(legacyParameter, legacyPrefix);
|
|
329
|
+
if (!legacy) return "synced";
|
|
330
|
+
try {
|
|
331
|
+
const current: StoredParameter | null = await store.get(legacyParameter);
|
|
332
|
+
if (!current) return "synced";
|
|
333
|
+
const value = parseLegacyEngineValue(current.value, legacy);
|
|
334
|
+
if (!value || value.authKind !== "config-dir" || !value.files) return "synced";
|
|
335
|
+
const brokered = lane.brokeredCopy(files);
|
|
336
|
+
if (sameFiles(brokered, value.files)) return "synced";
|
|
337
|
+
await store.overwrite(legacyParameter, JSON.stringify({ ...value.raw, files: brokered }));
|
|
338
|
+
return "synced";
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.warn(`[token-broker] today's copy of ${legacyParameter} was not updated (${brokerErrorKind(error)})`);
|
|
341
|
+
return "behind";
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker Lambda entrypoint (ADR-135). Runs one broker cycle against the
|
|
3
|
+
* credential store. Invoked by the six-hourly EventBridge rule, and
|
|
4
|
+
* synchronously by hosts for the reactive path with `{ "names": [...] }`
|
|
5
|
+
* (parameter names to refresh now). Returns the cycle summary; logs one
|
|
6
|
+
* audit line per decision and never a value.
|
|
7
|
+
*
|
|
8
|
+
* ADR-154 §7: the cloud also invokes it with one revision to refresh
|
|
9
|
+
* (`{ v: 1, credentialId, revision, brokerParameter, leaseParameter, force }`),
|
|
10
|
+
* which never scans and never takes the cycle's lock: one lock per credential.
|
|
11
|
+
* Owner answer 3: `{ v: 1, op: "legacy.list", after }` lists a page of
|
|
12
|
+
* today's workspace accounts (metadata only) and `{ v: 1, op:
|
|
13
|
+
* "legacy.adopt", … }` adopts one account into a revision;
|
|
14
|
+
* an adoption takes its credential's lock and the cycle's.
|
|
15
|
+
*
|
|
16
|
+
* Lives in lib/ (the package import boundary) and is bundled by
|
|
17
|
+
* host-agent/scripts/token-broker/build.sh (esbuild, AWS SDK left external —
|
|
18
|
+
* the Node runtime provides it). Nothing in the host imports it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { SSMClient } from "@aws-sdk/client-ssm";
|
|
22
|
+
|
|
23
|
+
import { runBrokerCycle } from "./token-broker";
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_CREDENTIAL_STORE_PREFIX,
|
|
26
|
+
parseBrokerAdoptRequest,
|
|
27
|
+
parseBrokerLegacyListRequest,
|
|
28
|
+
parseBrokerRevisionRequest,
|
|
29
|
+
parseCredentialStorePrefix,
|
|
30
|
+
} from "../src/cloud-credentials";
|
|
31
|
+
import { adoptLegacyAccount, listLegacyAccounts } from "./token-broker-adopt";
|
|
32
|
+
import { refreshRevision } from "./token-broker-revision";
|
|
33
|
+
import { acquireBrokerLock, SsmAdoptionStore, SsmBrokerStorage, SsmRevisionStore } from "./token-broker-ssm";
|
|
34
|
+
|
|
35
|
+
interface BrokerEvent {
|
|
36
|
+
names?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_FORCED_NAMES = 50;
|
|
40
|
+
|
|
41
|
+
export async function handler(event: BrokerEvent = {}): Promise<Record<string, unknown>> {
|
|
42
|
+
const configuredPrefix = process.env.UAI_CREDENTIAL_STORE_PREFIX || DEFAULT_CREDENTIAL_STORE_PREFIX;
|
|
43
|
+
const prefix = parseCredentialStorePrefix(configuredPrefix);
|
|
44
|
+
if (!prefix) throw new Error("invalid UAI_CREDENTIAL_STORE_PREFIX");
|
|
45
|
+
const kmsKeyId = process.env.UAI_CREDENTIAL_STORE_KMS_KEY || "alias/uai-credentials";
|
|
46
|
+
// ADR-154 §7: the cloud names one revision; the broker refreshes exactly
|
|
47
|
+
// that one and answers for it. Anything shaped like a request but not one
|
|
48
|
+
// is refused, never read as a scan.
|
|
49
|
+
if ((event as { v?: unknown }).v !== undefined) {
|
|
50
|
+
// Owner answer 3: today's workspace accounts, listed (metadata only)
|
|
51
|
+
// and adopted one at a time into the revision the cloud names.
|
|
52
|
+
const op = (event as { op?: unknown }).op;
|
|
53
|
+
if (op === "legacy.list") {
|
|
54
|
+
const list = parseBrokerLegacyListRequest(event, prefix);
|
|
55
|
+
if (!list) return { outcome: "unavailable", error: "malformed request" };
|
|
56
|
+
const listed = await listLegacyAccounts(new SsmAdoptionStore({ client: new SSMClient({}), kmsKeyId, prefix }), list.after);
|
|
57
|
+
console.log(
|
|
58
|
+
`[token-broker] legacy.list ${JSON.stringify({ outcome: listed.outcome, accounts: listed.outcome === "listed" ? listed.accounts.length : 0 })}`,
|
|
59
|
+
);
|
|
60
|
+
return listed;
|
|
61
|
+
}
|
|
62
|
+
if (op === "legacy.adopt") {
|
|
63
|
+
const adopt = parseBrokerAdoptRequest(event, prefix);
|
|
64
|
+
if (!adopt) return { outcome: "unavailable", error: "malformed request" };
|
|
65
|
+
const adopted = await adoptLegacyAccount(adopt, new SsmAdoptionStore({ client: new SSMClient({}), kmsKeyId, prefix }));
|
|
66
|
+
console.log(
|
|
67
|
+
`[token-broker] legacy.adopt ${JSON.stringify({ credentialId: adopt.credentialId, revision: adopt.revision, outcome: adopted.outcome })}`,
|
|
68
|
+
);
|
|
69
|
+
return adopted;
|
|
70
|
+
}
|
|
71
|
+
const request = parseBrokerRevisionRequest(event, prefix);
|
|
72
|
+
if (!request) return { outcome: "unavailable", error: "malformed request" };
|
|
73
|
+
const answer = await refreshRevision(request, new SsmRevisionStore({ client: new SSMClient({}), kmsKeyId }), {
|
|
74
|
+
legacyPrefix: prefix,
|
|
75
|
+
});
|
|
76
|
+
console.log(
|
|
77
|
+
`[token-broker] revision ${JSON.stringify({ credentialId: request.credentialId, revision: request.revision, force: request.force, outcome: answer.outcome })}`,
|
|
78
|
+
);
|
|
79
|
+
return answer;
|
|
80
|
+
}
|
|
81
|
+
const names = Array.isArray(event.names)
|
|
82
|
+
? event.names.filter((n): n is string => typeof n === "string" && n.startsWith(`${prefix}/`)).slice(0, MAX_FORCED_NAMES)
|
|
83
|
+
: [];
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
const client = new SSMClient({});
|
|
86
|
+
// One run at a time, whatever the account's concurrency settings allow:
|
|
87
|
+
// two refreshers for the same credential is the bug this exists to end.
|
|
88
|
+
const release = await acquireBrokerLock({ client, prefix });
|
|
89
|
+
if (!release) {
|
|
90
|
+
console.log("[token-broker] another run holds the lock; skipped");
|
|
91
|
+
return { skipped: "locked", forced: names.length, ms: Date.now() - started };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const summary = await runBrokerCycle({
|
|
95
|
+
storage: new SsmBrokerStorage({ prefix, kmsKeyId, client }),
|
|
96
|
+
force: new Set(names),
|
|
97
|
+
log: (line) => console.log(`[token-broker] ${line}`),
|
|
98
|
+
});
|
|
99
|
+
const result = { ...summary, forced: names.length, ms: Date.now() - started };
|
|
100
|
+
console.log(`[token-broker] cycle ${JSON.stringify(result)}`);
|
|
101
|
+
return result;
|
|
102
|
+
} finally {
|
|
103
|
+
await release();
|
|
104
|
+
}
|
|
105
|
+
}
|