@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,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter Store adapter for the broker core (ADR-135) — the storage the
|
|
3
|
+
* broker Lambda runs against. Reads the ADR-134 engine-account parameters
|
|
4
|
+
* under `<prefix>/orgs` and `<prefix>/hosts`, hands each to the core as a
|
|
5
|
+
* credential, and writes refreshed material + the `refresh` block back with
|
|
6
|
+
* `Overwrite`. Only the Lambda imports this module: it depends on the AWS
|
|
7
|
+
* SDK the Lambda runtime provides, which the host package does not ship.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
GetParameterCommand,
|
|
12
|
+
GetParameterHistoryCommand,
|
|
13
|
+
GetParametersByPathCommand,
|
|
14
|
+
ListTagsForResourceCommand,
|
|
15
|
+
PutParameterCommand,
|
|
16
|
+
SSMClient,
|
|
17
|
+
} from "@aws-sdk/client-ssm";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
BROKER_PARAMETER_ROOT,
|
|
21
|
+
idPathSegment,
|
|
22
|
+
legacyAdoptedRoot,
|
|
23
|
+
legacyEngineParameterRoot,
|
|
24
|
+
} from "../src/cloud-credentials";
|
|
25
|
+
import type { BrokerCredential, BrokerStorage } from "./token-broker";
|
|
26
|
+
import type { AdoptionStore } from "./token-broker-adopt";
|
|
27
|
+
import type { RevisionStore, StoredParameter } from "./token-broker-revision";
|
|
28
|
+
import { refreshMetaFrom, type CredentialFiles, type RefreshMeta } from "./token-lanes/types";
|
|
29
|
+
|
|
30
|
+
export interface SsmBrokerStorageOptions {
|
|
31
|
+
prefix: string;
|
|
32
|
+
kmsKeyId: string;
|
|
33
|
+
client?: SSMClient;
|
|
34
|
+
region?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface StoredValue {
|
|
38
|
+
raw: Record<string, unknown>;
|
|
39
|
+
version: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `<prefix>/orgs/<o>/engines/<kind>/<id>` and the corresponding host path. */
|
|
43
|
+
export function engineParameterHeader(
|
|
44
|
+
name: string,
|
|
45
|
+
prefix: string,
|
|
46
|
+
): { kind: string; id: string; scope: string } | null {
|
|
47
|
+
if (!name.startsWith(`${prefix}/`)) return null;
|
|
48
|
+
const parts = name.slice(prefix.length + 1).split("/");
|
|
49
|
+
if (parts.length !== 5 || parts[2] !== "engines") return null;
|
|
50
|
+
const [owner, ownerId, , kind, id] = parts as [string, string, string, string, string];
|
|
51
|
+
if (owner === "orgs") return { kind, id, scope: `org:${ownerId}` };
|
|
52
|
+
if (owner === "hosts") return { kind, id, scope: "host" };
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class SsmBrokerStorage implements BrokerStorage {
|
|
57
|
+
private readonly client: SSMClient;
|
|
58
|
+
private readonly values = new Map<string, StoredValue>();
|
|
59
|
+
|
|
60
|
+
constructor(private readonly opts: SsmBrokerStorageOptions) {
|
|
61
|
+
this.client = opts.client ?? new SSMClient(opts.region ? { region: opts.region } : {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async list(): Promise<BrokerCredential[]> {
|
|
65
|
+
// ADR-154, owner answer 3: an adopted account has one refresher, the
|
|
66
|
+
// cloud's scheduler, never this scan. Markers that cannot be listed stop
|
|
67
|
+
// the run rather than risk a second refresher.
|
|
68
|
+
const adopted = await adoptedLegacyNames(this.client, this.opts.prefix);
|
|
69
|
+
const out: BrokerCredential[] = [];
|
|
70
|
+
for (const path of [`${this.opts.prefix}/orgs`, `${this.opts.prefix}/hosts`]) {
|
|
71
|
+
let nextToken: string | undefined;
|
|
72
|
+
do {
|
|
73
|
+
const page = await this.client.send(
|
|
74
|
+
new GetParametersByPathCommand({
|
|
75
|
+
Path: path,
|
|
76
|
+
Recursive: true,
|
|
77
|
+
WithDecryption: true,
|
|
78
|
+
NextToken: nextToken,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
for (const p of page.Parameters ?? []) {
|
|
82
|
+
if (!p.Name || typeof p.Value !== "string") continue;
|
|
83
|
+
if (adopted.has(p.Name)) continue;
|
|
84
|
+
const header = engineParameterHeader(p.Name, this.opts.prefix);
|
|
85
|
+
if (!header) continue;
|
|
86
|
+
let raw: unknown;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(p.Value);
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
93
|
+
const value = raw as Record<string, unknown>;
|
|
94
|
+
if (value.v !== 1 || value.authKind !== "config-dir") continue;
|
|
95
|
+
const files = value.files;
|
|
96
|
+
if (!files || typeof files !== "object" || Array.isArray(files)) continue;
|
|
97
|
+
const materials: CredentialFiles = {};
|
|
98
|
+
for (const [k, v] of Object.entries(files as Record<string, unknown>)) {
|
|
99
|
+
if (typeof v === "string") materials[k] = v;
|
|
100
|
+
}
|
|
101
|
+
this.values.set(p.Name, { raw: value, version: p.Version });
|
|
102
|
+
out.push({
|
|
103
|
+
name: p.Name,
|
|
104
|
+
header: { kind: header.kind, authKind: "config-dir", id: header.id, scope: header.scope },
|
|
105
|
+
files: materials,
|
|
106
|
+
refresh: refreshMetaFrom(value.refresh),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
nextToken = page.NextToken;
|
|
110
|
+
} while (nextToken);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async save(
|
|
116
|
+
credential: BrokerCredential,
|
|
117
|
+
files: CredentialFiles | null,
|
|
118
|
+
refresh: RefreshMeta,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const stored = this.values.get(credential.name);
|
|
121
|
+
const raw = stored?.raw ?? { v: 1 };
|
|
122
|
+
const next = { ...raw, refresh, ...(files ? { files } : {}) };
|
|
123
|
+
await this.client.send(
|
|
124
|
+
new PutParameterCommand({
|
|
125
|
+
Name: credential.name,
|
|
126
|
+
Value: JSON.stringify(next),
|
|
127
|
+
Type: "SecureString",
|
|
128
|
+
KeyId: this.opts.kmsKeyId,
|
|
129
|
+
Tier: "Advanced",
|
|
130
|
+
Overwrite: true,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
this.values.set(credential.name, { raw: next, version: undefined });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Mutual exclusion between runs. Reserved concurrency 1 is the first line
|
|
139
|
+
// (deploy-aws.sh sets it when the account's limit allows); this lock is the
|
|
140
|
+
// one that always holds. A lock is one parameter that is never deleted: AWS
|
|
141
|
+
// asks for 30 seconds between deleting a parameter and creating one of the
|
|
142
|
+
// same name (DeleteParameter), and adoptions take the scan lock one after
|
|
143
|
+
// another. Free, it holds RELEASED_LOCK; held, its holder's
|
|
144
|
+
// `<time>:<nonce>`. A holder that died keeps it at most LOCK_STALE_MS; a
|
|
145
|
+
// later run takes it over.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export const LOCK_STALE_MS = 5 * 60_000;
|
|
149
|
+
/** A holder frees the lock only this far inside the stale window: past it,
|
|
150
|
+
* a run that found the lock stale may hold it now. The broker's 120-second
|
|
151
|
+
* Lambda timeout (deploy-aws.sh) keeps every holder well inside. */
|
|
152
|
+
const LOCK_RELEASE_MARGIN_MS = 60_000;
|
|
153
|
+
/** A free lock: time zero, which every reader takes for stale (the build
|
|
154
|
+
* before this one too, which deleted the parameter instead). */
|
|
155
|
+
export const RELEASED_LOCK = "0:released";
|
|
156
|
+
|
|
157
|
+
interface LockToken {
|
|
158
|
+
heldAt: number;
|
|
159
|
+
/** The SSM version this claimant had to create. Null for the previous
|
|
160
|
+
* build's `<time>:<nonce>` tokens and recovery fences, which stay
|
|
161
|
+
* conservatively held until released or stale. */
|
|
162
|
+
expectedVersion: number | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function lockToken(value: string | undefined): LockToken | null {
|
|
166
|
+
if (typeof value !== "string") return null;
|
|
167
|
+
const parts = value.split(":");
|
|
168
|
+
const [heldAtText, expectedText] = parts;
|
|
169
|
+
const heldAt = Number(heldAtText);
|
|
170
|
+
if (!Number.isSafeInteger(heldAt) || heldAt <= 0) return null;
|
|
171
|
+
const expectedVersion = Number(expectedText);
|
|
172
|
+
return {
|
|
173
|
+
heldAt,
|
|
174
|
+
expectedVersion:
|
|
175
|
+
parts.length === 3 && Number.isSafeInteger(expectedVersion) && expectedVersion > 0
|
|
176
|
+
? expectedVersion
|
|
177
|
+
: null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A contender can land after the elected holder has already released. Its
|
|
182
|
+
* token is not a lock (its returned SSM version missed the version it named),
|
|
183
|
+
* but it may also have overwritten a newer holder that is still working.
|
|
184
|
+
* Replay every intervening version: an elected token opens a hold, RELEASED
|
|
185
|
+
* closes it, and a losing token changes neither. Only a complete chain ending
|
|
186
|
+
* with no open hold proves that the current false token is free. */
|
|
187
|
+
async function holderBeforeFalseToken(
|
|
188
|
+
client: SSMClient,
|
|
189
|
+
name: string,
|
|
190
|
+
expectedVersion: number,
|
|
191
|
+
currentVersion: number,
|
|
192
|
+
): Promise<LockToken | null | undefined> {
|
|
193
|
+
let nextToken: string | undefined;
|
|
194
|
+
const history = new Map<number, string>();
|
|
195
|
+
try {
|
|
196
|
+
do {
|
|
197
|
+
const page = await client.send(
|
|
198
|
+
new GetParameterHistoryCommand({
|
|
199
|
+
Name: name,
|
|
200
|
+
WithDecryption: false,
|
|
201
|
+
MaxResults: 50,
|
|
202
|
+
NextToken: nextToken,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
for (const item of page.Parameters ?? []) {
|
|
206
|
+
if (
|
|
207
|
+
item.Version !== undefined &&
|
|
208
|
+
item.Version >= expectedVersion &&
|
|
209
|
+
item.Version < currentVersion &&
|
|
210
|
+
item.Value !== undefined
|
|
211
|
+
) {
|
|
212
|
+
history.set(item.Version, item.Value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
nextToken = page.NextToken;
|
|
216
|
+
} while (nextToken);
|
|
217
|
+
} catch {
|
|
218
|
+
// Missing IAM or a transient history read fails closed. The false token
|
|
219
|
+
// becomes stale normally; it must never be mistaken for a free live lock.
|
|
220
|
+
}
|
|
221
|
+
let holder: LockToken | null = null;
|
|
222
|
+
for (let version = expectedVersion; version < currentVersion; version += 1) {
|
|
223
|
+
const value = history.get(version);
|
|
224
|
+
// A missing/expired history entry cannot prove the holder is gone.
|
|
225
|
+
if (value === undefined) return undefined;
|
|
226
|
+
if (value === RELEASED_LOCK) {
|
|
227
|
+
holder = null;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const claim = lockToken(value);
|
|
231
|
+
// The previous build's two-field token won its own election; for a new
|
|
232
|
+
// token, only the exact SSM version it named did. Losing writes do not
|
|
233
|
+
// erase an already-open hold.
|
|
234
|
+
if (claim && (claim.expectedVersion === null || claim.expectedVersion === version)) {
|
|
235
|
+
holder = claim;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return holder;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function lockWriteConflict(error: unknown): boolean {
|
|
242
|
+
const name = (error as { name?: string }).name;
|
|
243
|
+
return name === "ParameterAlreadyExists" || name === "TooManyUpdates";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** History can age out (SSM retains 100 versions) or be temporarily
|
|
247
|
+
* unavailable. Replace an unprovable false token with a fresh fence and admit
|
|
248
|
+
* nobody for one stale window. Any hidden holder predates this write and is
|
|
249
|
+
* therefore stale when the fence is; a normal holder release may free it
|
|
250
|
+
* sooner. This is deliberately not an elected holder and never returns a
|
|
251
|
+
* release callback. */
|
|
252
|
+
async function installRecoveryFence(
|
|
253
|
+
client: SSMClient,
|
|
254
|
+
name: string,
|
|
255
|
+
now: () => number,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const token = `${now()}:recovery:${Math.random().toString(36).slice(2)}`;
|
|
258
|
+
try {
|
|
259
|
+
await client.send(
|
|
260
|
+
new PutParameterCommand({ Name: name, Value: token, Type: "String", Overwrite: true }),
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (!lockWriteConflict(error)) throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function brokerLockName(prefix: string): string {
|
|
268
|
+
return `${prefix}/locks/token-broker`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface BrokerLockOptions {
|
|
272
|
+
client: SSMClient;
|
|
273
|
+
prefix: string;
|
|
274
|
+
now?: () => number;
|
|
275
|
+
staleMs?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Try to take the lock. Returns a release function, or null when another
|
|
279
|
+
* run holds a fresh one. */
|
|
280
|
+
export async function acquireBrokerLock(opts: BrokerLockOptions): Promise<(() => Promise<void>) | null> {
|
|
281
|
+
return acquireParameterLock({ ...opts, name: brokerLockName(opts.prefix) });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The same lock, at any parameter name. */
|
|
285
|
+
export async function acquireParameterLock(opts: {
|
|
286
|
+
client: SSMClient;
|
|
287
|
+
name: string;
|
|
288
|
+
now?: () => number;
|
|
289
|
+
staleMs?: number;
|
|
290
|
+
}): Promise<(() => Promise<void>) | null> {
|
|
291
|
+
const name = opts.name;
|
|
292
|
+
const now = opts.now ?? Date.now;
|
|
293
|
+
const staleMs = opts.staleMs ?? LOCK_STALE_MS;
|
|
294
|
+
let current: { Parameter?: { Value?: string; Version?: number } } | null;
|
|
295
|
+
try {
|
|
296
|
+
current = await opts.client.send(new GetParameterCommand({ Name: name }));
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if ((err as { name?: string }).name !== "ParameterNotFound") throw err;
|
|
299
|
+
current = null;
|
|
300
|
+
}
|
|
301
|
+
if (current !== null) {
|
|
302
|
+
const claim = lockToken(current.Parameter?.Value);
|
|
303
|
+
const currentVersion = current.Parameter?.Version;
|
|
304
|
+
if (claim) {
|
|
305
|
+
const isElected =
|
|
306
|
+
claim.expectedVersion === null ||
|
|
307
|
+
currentVersion === undefined ||
|
|
308
|
+
claim.expectedVersion === currentVersion;
|
|
309
|
+
if (isElected) {
|
|
310
|
+
if (now() - claim.heldAt < staleMs) return null;
|
|
311
|
+
} else {
|
|
312
|
+
// Versions only rise. A token naming a future version is malformed,
|
|
313
|
+
// not evidence that a previous holder released. Fence recovery rather
|
|
314
|
+
// than leaving the malformed lineage wedged forever.
|
|
315
|
+
if (claim.expectedVersion! > currentVersion) {
|
|
316
|
+
await installRecoveryFence(opts.client, name, now);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
// Replay a mismatched token even when that token's own timestamp is
|
|
320
|
+
// stale: it may have landed late over a much newer, live holder.
|
|
321
|
+
const holder = await holderBeforeFalseToken(
|
|
322
|
+
opts.client,
|
|
323
|
+
name,
|
|
324
|
+
claim.expectedVersion!,
|
|
325
|
+
currentVersion,
|
|
326
|
+
);
|
|
327
|
+
if (holder === undefined) {
|
|
328
|
+
await installRecoveryFence(opts.client, name, now);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (holder !== null && now() - holder.heldAt < staleMs) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Timed at the write, so a run that lost the election below leaves a token
|
|
338
|
+
// no older than ours.
|
|
339
|
+
const takenAt = now();
|
|
340
|
+
const read = current?.Parameter?.Version;
|
|
341
|
+
const expectedVersion = current === null ? 1 : read === undefined ? null : read + 1;
|
|
342
|
+
if (expectedVersion === null) return null;
|
|
343
|
+
const token = `${takenAt}:${expectedVersion}:${Math.random().toString(36).slice(2)}`;
|
|
344
|
+
if (current === null) {
|
|
345
|
+
// Never taken (or deleted by the build before this one): created once.
|
|
346
|
+
try {
|
|
347
|
+
await opts.client.send(new PutParameterCommand({ Name: name, Value: token, Type: "String", Overwrite: false }));
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (lockWriteConflict(err)) return null;
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
/**
|
|
354
|
+
* SSM has no conditional overwrite, but it numbers every write: ours is
|
|
355
|
+
* the lock only if it is the very next version after the one we read
|
|
356
|
+
* free or stale. Of runs that read the same version, exactly one gets
|
|
357
|
+
* the next; the others may leave their tokens in the value after it, but
|
|
358
|
+
* a token is only written over a lock read free or stale, so none of
|
|
359
|
+
* them holds it meanwhile.
|
|
360
|
+
*/
|
|
361
|
+
let written: { Version?: number };
|
|
362
|
+
try {
|
|
363
|
+
written = await opts.client.send(
|
|
364
|
+
new PutParameterCommand({ Name: name, Value: token, Type: "String", Overwrite: true }),
|
|
365
|
+
);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (lockWriteConflict(error)) return null;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
if (written.Version !== expectedVersion) return null;
|
|
371
|
+
}
|
|
372
|
+
return async () => {
|
|
373
|
+
// Inside the hold no other run can hold the lock, whatever token a run
|
|
374
|
+
// that lost the election left in the value: free it. Late in it, a run
|
|
375
|
+
// that found it stale may hold it already; leave it, stale anyway.
|
|
376
|
+
if (now() - takenAt >= Math.max(0, staleMs - LOCK_RELEASE_MARGIN_MS)) return;
|
|
377
|
+
try {
|
|
378
|
+
await opts.client.send(
|
|
379
|
+
new PutParameterCommand({ Name: name, Value: RELEASED_LOCK, Type: "String", Overwrite: true }),
|
|
380
|
+
);
|
|
381
|
+
} catch {
|
|
382
|
+
/* the stale window covers a failed release */
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// ADR-154 §7: one named revision, for the exact-name entry point
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/** One credential's lock: one invoke at a time per credential, whichever
|
|
392
|
+
* revision it names, since an invoke the cloud gave up on may still run. */
|
|
393
|
+
export function revisionLockName(credentialId: string): string {
|
|
394
|
+
return `${BROKER_PARAMETER_ROOT}/locks/${idPathSegment(credentialId)}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export class SsmRevisionStore implements RevisionStore {
|
|
398
|
+
constructor(
|
|
399
|
+
private readonly opts: { client: SSMClient; kmsKeyId: string; now?: () => number },
|
|
400
|
+
) {}
|
|
401
|
+
|
|
402
|
+
async get(name: string): Promise<StoredParameter | null> {
|
|
403
|
+
try {
|
|
404
|
+
const out = await this.opts.client.send(new GetParameterCommand({ Name: name, WithDecryption: true }));
|
|
405
|
+
const parameter = out.Parameter;
|
|
406
|
+
if (parameter?.Value === undefined || parameter.Version === undefined) return null;
|
|
407
|
+
return { value: parameter.Value, version: parameter.Version };
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if ((err as { name?: string }).name === "ParameterNotFound") return null;
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async overwrite(name: string, value: string): Promise<number> {
|
|
415
|
+
const out = await this.opts.client.send(
|
|
416
|
+
new PutParameterCommand({
|
|
417
|
+
Name: name,
|
|
418
|
+
Value: value,
|
|
419
|
+
Type: "SecureString",
|
|
420
|
+
KeyId: this.opts.kmsKeyId,
|
|
421
|
+
Overwrite: true,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
if (out.Version === undefined) throw new Error("PutParameter returned no version");
|
|
425
|
+
return out.Version;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
lock(credentialId: string): Promise<(() => Promise<void>) | null> {
|
|
429
|
+
return acquireParameterLock({
|
|
430
|
+
client: this.opts.client,
|
|
431
|
+
name: revisionLockName(credentialId),
|
|
432
|
+
now: this.opts.now,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// ADR-154, owner answer 3: adoption of today's workspace AI accounts
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
async function parametersUnder(
|
|
442
|
+
client: SSMClient,
|
|
443
|
+
path: string,
|
|
444
|
+
decrypt: boolean,
|
|
445
|
+
): Promise<Array<{ name: string; value: string }>> {
|
|
446
|
+
const out: Array<{ name: string; value: string }> = [];
|
|
447
|
+
let nextToken: string | undefined;
|
|
448
|
+
do {
|
|
449
|
+
const page = await client.send(
|
|
450
|
+
new GetParametersByPathCommand({ Path: path, Recursive: true, WithDecryption: decrypt, NextToken: nextToken }),
|
|
451
|
+
);
|
|
452
|
+
for (const p of page.Parameters ?? []) {
|
|
453
|
+
if (p.Name && typeof p.Value === "string") out.push({ name: p.Name, value: p.Value });
|
|
454
|
+
}
|
|
455
|
+
nextToken = page.NextToken;
|
|
456
|
+
} while (nextToken);
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The legacy parameter names (under `prefix`) an adoption marker names. */
|
|
461
|
+
export async function adoptedLegacyNames(client: SSMClient, prefix: string): Promise<Set<string>> {
|
|
462
|
+
const names = new Set<string>();
|
|
463
|
+
const adoptedRoot = legacyAdoptedRoot(prefix);
|
|
464
|
+
for (const marker of await parametersUnder(client, `${adoptedRoot}/orgs`, false)) {
|
|
465
|
+
names.add(`${prefix}${marker.name.slice(adoptedRoot.length)}`);
|
|
466
|
+
}
|
|
467
|
+
return names;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export class SsmAdoptionStore extends SsmRevisionStore implements AdoptionStore {
|
|
471
|
+
constructor(
|
|
472
|
+
private readonly adoption: { client: SSMClient; kmsKeyId: string; prefix: string; now?: () => number },
|
|
473
|
+
) {
|
|
474
|
+
super(adoption);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
get legacyPrefix(): string {
|
|
478
|
+
return this.adoption.prefix;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async create(name: string, value: string, tags: Record<string, string>): Promise<"created" | "matched"> {
|
|
482
|
+
const entries = Object.entries(tags);
|
|
483
|
+
const marker = name.startsWith(`${legacyAdoptedRoot(this.adoption.prefix)}/`);
|
|
484
|
+
try {
|
|
485
|
+
await this.adoption.client.send(
|
|
486
|
+
new PutParameterCommand({
|
|
487
|
+
Name: name,
|
|
488
|
+
Value: value,
|
|
489
|
+
// A marker names ids only; every part is a secret.
|
|
490
|
+
Type: marker ? "String" : "SecureString",
|
|
491
|
+
...(marker ? {} : { KeyId: this.adoption.kmsKeyId }),
|
|
492
|
+
Tier: marker ? "Standard" : "Advanced",
|
|
493
|
+
Overwrite: false,
|
|
494
|
+
Tags: entries.map(([Key, Value]) => ({ Key, Value })),
|
|
495
|
+
}),
|
|
496
|
+
);
|
|
497
|
+
return "created";
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if ((error as { name?: string }).name !== "ParameterAlreadyExists") throw error;
|
|
500
|
+
}
|
|
501
|
+
// Exists: this create's retry only when its identity tags all match.
|
|
502
|
+
const listed = await this.adoption.client.send(
|
|
503
|
+
new ListTagsForResourceCommand({ ResourceType: "Parameter", ResourceId: name }),
|
|
504
|
+
);
|
|
505
|
+
const observed = new Map((listed.TagList ?? []).map((tag) => [tag.Key, tag.Value]));
|
|
506
|
+
for (const [key, expected] of entries) {
|
|
507
|
+
const actual = observed.get(key);
|
|
508
|
+
if (actual === undefined) throw new Error(`${name} exists and its tags are not visible yet`);
|
|
509
|
+
if (actual !== expected) throw new Error(`${name} exists as another credential's`);
|
|
510
|
+
}
|
|
511
|
+
return "matched";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
listLegacy(): Promise<Array<{ name: string; value: string }>> {
|
|
515
|
+
return parametersUnder(this.adoption.client, legacyEngineParameterRoot(this.adoption.prefix), true);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
listMarkers(): Promise<Array<{ name: string; value: string }>> {
|
|
519
|
+
return parametersUnder(this.adoption.client, `${legacyAdoptedRoot(this.adoption.prefix)}/orgs`, false);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
scanLock(): Promise<(() => Promise<void>) | null> {
|
|
523
|
+
return acquireBrokerLock({ client: this.adoption.client, prefix: this.adoption.prefix, now: this.adoption.now });
|
|
524
|
+
}
|
|
525
|
+
}
|