@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-only release switch for ADR-153 project-env bindings.
|
|
3
|
+
*
|
|
4
|
+
* Capability advertisement and task admission move together: once a sidecar
|
|
5
|
+
* advertises the protocol, its task cannot expose a legacy env view before the
|
|
6
|
+
* replica's first complete reconciliation. Advertisement also waits for the
|
|
7
|
+
* durable managed-runtime floor, so no acknowledged generation can roll back
|
|
8
|
+
* to code that serves the old value-only store.
|
|
9
|
+
*/
|
|
10
|
+
import { PROJECT_ENV_BINDINGS_PROTOCOL_FEATURE } from "../src/protocol";
|
|
11
|
+
import { PROJECT_ENV_POLICY_EPOCHS } from "../src/project-env-policy";
|
|
12
|
+
import { MANAGED_RUNTIME_CAPABILITY_VERSION } from "./runtime-archive";
|
|
13
|
+
|
|
14
|
+
const PROJECT_ENV_BINDINGS_ENABLED = true;
|
|
15
|
+
export const PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY =
|
|
16
|
+
MANAGED_RUNTIME_CAPABILITY_VERSION;
|
|
17
|
+
|
|
18
|
+
let capabilityFloorClaim: Promise<void> | null = null;
|
|
19
|
+
let capabilityFloorClaimFailed = false;
|
|
20
|
+
let capabilityReady = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claim the irreversible rollback floor before the first capability frame.
|
|
24
|
+
* The rejected promise is deliberately retained: one process gets one claim
|
|
25
|
+
* attempt and stays fail-closed until restart if durable persistence fails.
|
|
26
|
+
*/
|
|
27
|
+
export async function initializeProjectEnvBindingCapability(
|
|
28
|
+
claimFloor: (minimumCapability: number) => Promise<void>,
|
|
29
|
+
): Promise<boolean> {
|
|
30
|
+
if (!PROJECT_ENV_BINDINGS_ENABLED) return false;
|
|
31
|
+
capabilityFloorClaim ??= Promise.resolve()
|
|
32
|
+
.then(() => claimFloor(PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY))
|
|
33
|
+
.then(() => {
|
|
34
|
+
capabilityReady = true;
|
|
35
|
+
})
|
|
36
|
+
.catch((error: unknown) => {
|
|
37
|
+
capabilityFloorClaimFailed = true;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
await capabilityFloorClaim;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Complete the in-process half of the first-upgrade transaction. The managed
|
|
46
|
+
* runtime calls this synchronously under its operation lock after floor 4 is
|
|
47
|
+
* durable and before it removes activationPending, so task admission cannot
|
|
48
|
+
* observe a value-only feature state between those two commits.
|
|
49
|
+
*/
|
|
50
|
+
export function completeProjectEnvBindingCapabilityAfterRuntimeActivation(): void {
|
|
51
|
+
if (!PROJECT_ENV_BINDINGS_ENABLED || capabilityReady) return;
|
|
52
|
+
if (capabilityFloorClaim === null || !capabilityFloorClaimFailed) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"project env capability has no failed activation claim to complete",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
capabilityFloorClaim = Promise.resolve();
|
|
58
|
+
capabilityFloorClaimFailed = false;
|
|
59
|
+
capabilityReady = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One nullable bundle makes the feature id and its required policy epochs an
|
|
63
|
+
* atomic advertisement, and is also TaskUp's release admission switch. */
|
|
64
|
+
export function projectEnvBindingCapability(): {
|
|
65
|
+
feature: string;
|
|
66
|
+
policyEpochs: number[];
|
|
67
|
+
} | null {
|
|
68
|
+
return PROJECT_ENV_BINDINGS_ENABLED && capabilityReady
|
|
69
|
+
? {
|
|
70
|
+
feature: PROJECT_ENV_BINDINGS_PROTOCOL_FEATURE,
|
|
71
|
+
policyEpochs: [...PROJECT_ENV_POLICY_EPOCHS],
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154 §9: the sign-in box tags pinned legacy env parameters.
|
|
3
|
+
*
|
|
4
|
+
* ADR-153's backfill pinned `<prefix>/projects/<projectId>/env/<KEY>` by
|
|
5
|
+
* version and LastModifiedDate without checking its tags, and a v1 lease
|
|
6
|
+
* reads `/uai/projects/*` only where the `org` tag is the task's workspace.
|
|
7
|
+
* The box puts the project's `org` and `project` tags on each parameter the
|
|
8
|
+
* cloud names and reads them back, with `ssm:AddTagsToResource` and
|
|
9
|
+
* `ssm:ListTagsForResource`, which its policy already grants on
|
|
10
|
+
* `/uai/projects/*`. It never reads a value; the cloud checks each pin,
|
|
11
|
+
* before and after, from its own metadata reads.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
isProjectEnvLegacyTagFrame,
|
|
16
|
+
type ProjectEnvLegacyTagAckFrame,
|
|
17
|
+
type ProjectEnvLegacyTagFrame,
|
|
18
|
+
type ProjectEnvLegacyTagOutcome,
|
|
19
|
+
} from "../src/protocol";
|
|
20
|
+
import {
|
|
21
|
+
CredentialStoreCommandError,
|
|
22
|
+
configuredCredentialStore,
|
|
23
|
+
credentialStorePrefix,
|
|
24
|
+
parameterName,
|
|
25
|
+
type CredentialStore,
|
|
26
|
+
} from "./credential-store";
|
|
27
|
+
import { PROJECT_ENV_STAGING_TAGS } from "./project-env-staging";
|
|
28
|
+
|
|
29
|
+
/** A frame starts no key after this; the rest are asked again. With one
|
|
30
|
+
* key's two CLI calls after it (30 seconds each at most), the answer still
|
|
31
|
+
* reaches the cloud inside its 90-second wait (lib/host-bridge.ts), and a
|
|
32
|
+
* managed restart, which waits for drained work, waits that long at most. */
|
|
33
|
+
export const LEGACY_TAG_BUDGET_MS = 20_000;
|
|
34
|
+
|
|
35
|
+
/** AWS's answers for a parameter that is not there. */
|
|
36
|
+
const MISSING_CODES = new Set(["InvalidResourceId", "ParameterNotFound"]);
|
|
37
|
+
/** AWS will put no more tags on it: nothing a retry changes. */
|
|
38
|
+
const REFUSED_CODES = new Set(["TooManyTagsError"]);
|
|
39
|
+
|
|
40
|
+
/** The legacy layout's name for one key, as the cloud's
|
|
41
|
+
* `projectEnvCloudLegacyParameterName` writes it. */
|
|
42
|
+
export function projectEnvLegacyParameterName(
|
|
43
|
+
projectId: string,
|
|
44
|
+
key: string,
|
|
45
|
+
prefix = credentialStorePrefix(),
|
|
46
|
+
): string {
|
|
47
|
+
return parameterName(prefix, "projects", projectId, "env", key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function tagOne(
|
|
51
|
+
store: CredentialStore,
|
|
52
|
+
name: string,
|
|
53
|
+
tags: Record<string, string>,
|
|
54
|
+
): Promise<ProjectEnvLegacyTagOutcome> {
|
|
55
|
+
try {
|
|
56
|
+
await store.tag(name, tags);
|
|
57
|
+
// Tags are eventually consistent: one not read back yet is a retry, and
|
|
58
|
+
// tagging again is harmless.
|
|
59
|
+
const read = await store.listTags(name);
|
|
60
|
+
return Object.entries(tags).every(([key, value]) => read[key] === value) ? "tagged" : "retry";
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const code = error instanceof CredentialStoreCommandError ? error.awsCode : null;
|
|
63
|
+
if (code !== null && MISSING_CODES.has(code)) return "missing";
|
|
64
|
+
if (code !== null && REFUSED_CODES.has(code)) return "refused";
|
|
65
|
+
return "retry";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ProjectEnvLegacyTagOptions {
|
|
70
|
+
store?: CredentialStore | null;
|
|
71
|
+
prefix?: string;
|
|
72
|
+
now?: () => number;
|
|
73
|
+
/** When the frame arrived: its budget runs from there. */
|
|
74
|
+
startedAt?: number;
|
|
75
|
+
budgetMs?: number;
|
|
76
|
+
stopping?: () => boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Tag each named parameter in turn; one key's failure is its own. Keys
|
|
80
|
+
* past the budget, or after the host begins shutting down, are asked again
|
|
81
|
+
* without a call. */
|
|
82
|
+
export async function tagProjectEnvLegacyParameters(
|
|
83
|
+
frame: ProjectEnvLegacyTagFrame,
|
|
84
|
+
options: ProjectEnvLegacyTagOptions = {},
|
|
85
|
+
): Promise<ProjectEnvLegacyTagAckFrame["results"]> {
|
|
86
|
+
if (!isProjectEnvLegacyTagFrame(frame)) {
|
|
87
|
+
throw new Error("invalid legacy environment tag request");
|
|
88
|
+
}
|
|
89
|
+
const store = Object.prototype.hasOwnProperty.call(options, "store")
|
|
90
|
+
? options.store
|
|
91
|
+
: configuredCredentialStore();
|
|
92
|
+
if (!store) return frame.keys.map((key) => ({ key, outcome: "retry" }));
|
|
93
|
+
const prefix = options.prefix ?? credentialStorePrefix();
|
|
94
|
+
const tags = {
|
|
95
|
+
[PROJECT_ENV_STAGING_TAGS.org]: frame.orgId,
|
|
96
|
+
[PROJECT_ENV_STAGING_TAGS.project]: frame.projectId,
|
|
97
|
+
};
|
|
98
|
+
const now = options.now ?? Date.now;
|
|
99
|
+
const startedAt = options.startedAt ?? now();
|
|
100
|
+
const budgetMs = options.budgetMs ?? LEGACY_TAG_BUDGET_MS;
|
|
101
|
+
const results: ProjectEnvLegacyTagAckFrame["results"] = [];
|
|
102
|
+
for (const key of frame.keys) {
|
|
103
|
+
if (now() - startedAt >= budgetMs || options.stopping?.() === true) {
|
|
104
|
+
results.push({ key, outcome: "retry" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const name = projectEnvLegacyParameterName(frame.projectId, key, prefix);
|
|
108
|
+
results.push({ key, outcome: await tagOne(store, name, tags) });
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One frame at a time, in arrival order: a frame whose key stalled in a CLI
|
|
117
|
+
* call (its budget stops it starting more, not the call it is in) cannot
|
|
118
|
+
* land its tags after a later frame's, which may name another workspace or
|
|
119
|
+
* have been read back already. A frame that waited out its budget in line
|
|
120
|
+
* answers at once, each key asked again.
|
|
121
|
+
*/
|
|
122
|
+
export function queueProjectEnvLegacyTag(
|
|
123
|
+
frame: ProjectEnvLegacyTagFrame,
|
|
124
|
+
options: ProjectEnvLegacyTagOptions = {},
|
|
125
|
+
): Promise<ProjectEnvLegacyTagAckFrame["results"]> {
|
|
126
|
+
const startedAt = options.startedAt ?? (options.now ?? Date.now)();
|
|
127
|
+
const run = queue.then(() => tagProjectEnvLegacyParameters(frame, { ...options, startedAt }));
|
|
128
|
+
queue = run.catch(() => undefined);
|
|
129
|
+
return run;
|
|
130
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-153 project-env resolution and shell-document rendering.
|
|
3
|
+
*
|
|
4
|
+
* This module is the one value-bearing seam between the encrypted host store
|
|
5
|
+
* and a task's authenticated loopback gateway. It keeps one rendered snapshot
|
|
6
|
+
* in memory and invalidates it synchronously with project-value mutations, so
|
|
7
|
+
* a rotation or deletion is visible to the next command without a restart.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getDecryptedForProject,
|
|
14
|
+
isReservedTaskEnvName,
|
|
15
|
+
projectEnvRevision,
|
|
16
|
+
} from "./host-env";
|
|
17
|
+
import type { TaskEnvView } from "./project-env-task-policy";
|
|
18
|
+
import type { TaskProjectEnvSpec } from "./runtime-state";
|
|
19
|
+
import type {
|
|
20
|
+
TaskGatewayEnvResolver,
|
|
21
|
+
TaskGatewayEnvSnapshot,
|
|
22
|
+
} from "./task-gateway-state";
|
|
23
|
+
|
|
24
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25
|
+
|
|
26
|
+
export interface ResolvedTaskProjectEnv {
|
|
27
|
+
/** Only currently-delivered names. A deleted name disappears on refresh. */
|
|
28
|
+
readonly values: Readonly<Record<string, string>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where a task's project values come from (ADR-153 §6, "Reconciling a
|
|
33
|
+
* runner"): one snapshot's view of the store; nothing at all, while the
|
|
34
|
+
* store is unreconciled or a read of it failed; or, only for a build whose
|
|
35
|
+
* store predates records, the stored values as they are. `none` never falls
|
|
36
|
+
* back to the stored values.
|
|
37
|
+
*/
|
|
38
|
+
export type TaskEnvSource =
|
|
39
|
+
| { kind: "view"; view: TaskEnvView }
|
|
40
|
+
| { kind: "none" }
|
|
41
|
+
| { kind: "stored" };
|
|
42
|
+
|
|
43
|
+
const STORED: TaskEnvSource = { kind: "stored" };
|
|
44
|
+
|
|
45
|
+
function compareAscii(left: string, right: string): number {
|
|
46
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function previewNameAllowed(name: string): boolean {
|
|
50
|
+
// UAI_EDITOR_THEME is a cloud-owned non-secret task literal consumed once
|
|
51
|
+
// while seeding the editor. Every other reserved name remains host-owned.
|
|
52
|
+
return name === "UAI_EDITOR_THEME" || !isReservedTaskEnvName(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve lead-project precedence before applying that row's declaration.
|
|
57
|
+
* With the task's view (ADR-153 §4), that view is the only source of project
|
|
58
|
+
* values: each delivered key's value as of its decision's head, from one
|
|
59
|
+
* snapshot of the store, never a separate read of the store here. The view
|
|
60
|
+
* already applied ownership and policy to the live records, so the names
|
|
61
|
+
* frozen into the task at launch do not filter it: a key added since reaches
|
|
62
|
+
* the next command, as a changed one does. With `none`, there are no project
|
|
63
|
+
* values at all.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveTaskProjectEnv(
|
|
66
|
+
spec: TaskProjectEnvSpec,
|
|
67
|
+
source: TaskEnvSource = STORED,
|
|
68
|
+
): ResolvedTaskProjectEnv {
|
|
69
|
+
const view = source.kind === "view" ? source.view : null;
|
|
70
|
+
const selected: Record<string, string> = Object.create(null) as Record<
|
|
71
|
+
string,
|
|
72
|
+
string
|
|
73
|
+
>;
|
|
74
|
+
const claimed = new Set<string>();
|
|
75
|
+
const projects = [...spec.projects].sort(
|
|
76
|
+
(left, right) =>
|
|
77
|
+
left.position - right.position ||
|
|
78
|
+
compareAscii(left.projectId, right.projectId),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (view !== null) {
|
|
82
|
+
for (const policy of view.policies) {
|
|
83
|
+
if (policy.kind !== "deliver") continue;
|
|
84
|
+
const value = Object.hasOwn(view.values, policy.key) ? view.values[policy.key] : undefined;
|
|
85
|
+
const project = projects.find((candidate) => candidate.projectId === policy.projectId);
|
|
86
|
+
if (
|
|
87
|
+
value === undefined ||
|
|
88
|
+
project === undefined ||
|
|
89
|
+
!ENV_NAME.test(policy.key) ||
|
|
90
|
+
isReservedTaskEnvName(policy.key) ||
|
|
91
|
+
value.includes("\0")
|
|
92
|
+
) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
selected[policy.key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const project of source.kind === "stored" ? projects : []) {
|
|
100
|
+
const stored = getDecryptedForProject(project.projectId);
|
|
101
|
+
const declared = new Set(project.declaredNames);
|
|
102
|
+
for (const [name, value] of Object.entries(stored)) {
|
|
103
|
+
if (
|
|
104
|
+
claimed.has(name) ||
|
|
105
|
+
!ENV_NAME.test(name) ||
|
|
106
|
+
isReservedTaskEnvName(name)
|
|
107
|
+
) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// The first stored row owns this key even when that project's schema
|
|
111
|
+
// does not declare it. Falling through would pair a later project's
|
|
112
|
+
// declaration with a different project's value, violating ADR-153's
|
|
113
|
+
// one-authority-unit rule.
|
|
114
|
+
claimed.add(name);
|
|
115
|
+
if (!declared.has(name)) continue;
|
|
116
|
+
if (typeof value !== "string" || value.includes("\0")) continue;
|
|
117
|
+
selected[name] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Preview/theme literals are task-scoped and intentionally win a collision
|
|
122
|
+
// with project env, matching the pre-loader Compose contract.
|
|
123
|
+
for (const [name, value] of Object.entries(spec.previewEnv)) {
|
|
124
|
+
if (
|
|
125
|
+
ENV_NAME.test(name) &&
|
|
126
|
+
previewNameAllowed(name) &&
|
|
127
|
+
!value.includes("\0")
|
|
128
|
+
) {
|
|
129
|
+
selected[name] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { values: selected };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** POSIX single-quote syntax, valid in dash, bash and zsh. */
|
|
136
|
+
export function quoteTaskEnvValue(value: string): string {
|
|
137
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render the complete document validated by env-hook.sh. The digest covers
|
|
142
|
+
* the value-bearing prefix; its raw hex is repeated only in the terminal
|
|
143
|
+
* sentinel, while HTTP receives the quoted strong ETag.
|
|
144
|
+
*/
|
|
145
|
+
export function renderTaskProjectEnv(
|
|
146
|
+
resolved: ResolvedTaskProjectEnv,
|
|
147
|
+
): TaskGatewayEnvSnapshot {
|
|
148
|
+
const names = Object.keys(resolved.values).sort(compareAscii);
|
|
149
|
+
let prefix = `# uai-env-v1 keys=${names.join(" ")}\n`;
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
prefix += `export ${name}=${quoteTaskEnvValue(resolved.values[name] ?? "")}\n`;
|
|
152
|
+
}
|
|
153
|
+
const revision = createHash("sha256").update(prefix, "utf8").digest("hex");
|
|
154
|
+
return {
|
|
155
|
+
body: `${prefix}# uai-env-v1 end=${revision}\n`,
|
|
156
|
+
etag: `"${revision}"`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolver closure installed on a durable task-gateway generation. The
|
|
162
|
+
* rendered snapshot stays in memory while every project used by the task has
|
|
163
|
+
* the same value revision. A set, delete, or credential-store reconcile bumps
|
|
164
|
+
* that project's revision synchronously, so the next request recomputes and
|
|
165
|
+
* still observes rotations and removals immediately.
|
|
166
|
+
*
|
|
167
|
+
* `uaiEnv` is the runner's own env for the task (the secrets proxy's trust
|
|
168
|
+
* variables while it runs), read per request: reserved names only, so it
|
|
169
|
+
* never collides with a project value, and its change re-renders too.
|
|
170
|
+
*
|
|
171
|
+
* `proxiedEnv` is the task's proxy-mode keys (ADR-153): each such name is
|
|
172
|
+
* withheld from the project values, and carries its placeholder only while
|
|
173
|
+
* the key is usable. Also read per request, and part of the cache key, so a
|
|
174
|
+
* change of readiness alone (pins in, pins failed) re-renders.
|
|
175
|
+
*/
|
|
176
|
+
export function taskProjectEnvResolver(
|
|
177
|
+
spec: TaskProjectEnvSpec,
|
|
178
|
+
uaiEnv: () => Readonly<Record<string, string>> = () => ({}),
|
|
179
|
+
proxiedEnv: () => {
|
|
180
|
+
readonly withheld: readonly string[];
|
|
181
|
+
readonly placeholders: Readonly<Record<string, string>>;
|
|
182
|
+
} = () => ({ withheld: [], placeholders: {} }),
|
|
183
|
+
taskSource: () => TaskEnvSource = () => STORED,
|
|
184
|
+
taskSecrets: () => { revision: string; withheld: readonly string[]; values: Readonly<Record<string, string>> } = () => ({ revision: "", withheld: [], values: {} }),
|
|
185
|
+
): TaskGatewayEnvResolver {
|
|
186
|
+
const projectIds = [...new Set(spec.projects.map((project) => project.projectId))];
|
|
187
|
+
let revisions: number[] | undefined;
|
|
188
|
+
let runnerKey: string | undefined;
|
|
189
|
+
let snapshot: TaskGatewayEnvSnapshot | undefined;
|
|
190
|
+
|
|
191
|
+
return () => {
|
|
192
|
+
const currentRevisions = projectIds.map(projectEnvRevision);
|
|
193
|
+
const currentUai = uaiEnv();
|
|
194
|
+
const currentProxied = proxiedEnv();
|
|
195
|
+
const currentSource = taskSource();
|
|
196
|
+
const secrets = taskSecrets();
|
|
197
|
+
const currentRunnerKey = JSON.stringify([
|
|
198
|
+
secrets.revision,
|
|
199
|
+
Object.entries(currentUai).sort(([a], [b]) => compareAscii(a, b)),
|
|
200
|
+
[...currentProxied.withheld].sort(compareAscii),
|
|
201
|
+
Object.entries(currentProxied.placeholders).sort(([a], [b]) => compareAscii(a, b)),
|
|
202
|
+
// The source, and a view's decisions and heads, never its values: a
|
|
203
|
+
// head that moves re-renders.
|
|
204
|
+
currentSource.kind === "view" ? [currentSource.kind, currentSource.view.policies] : [currentSource.kind],
|
|
205
|
+
]);
|
|
206
|
+
if (
|
|
207
|
+
snapshot &&
|
|
208
|
+
runnerKey === currentRunnerKey &&
|
|
209
|
+
revisions?.length === currentRevisions.length &&
|
|
210
|
+
revisions.every((revision, index) => revision === currentRevisions[index])
|
|
211
|
+
) {
|
|
212
|
+
return snapshot;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const resolved = resolveTaskProjectEnv(spec, currentSource);
|
|
216
|
+
const values: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
217
|
+
Object.assign(values, resolved.values);
|
|
218
|
+
const withheld = new Set([
|
|
219
|
+
...currentProxied.withheld,
|
|
220
|
+
...secrets.withheld,
|
|
221
|
+
...(currentSource.kind === "view" ? currentSource.view.notDelivered : []),
|
|
222
|
+
]);
|
|
223
|
+
for (const name of withheld) delete values[name];
|
|
224
|
+
// With no project env at all, no placeholder stands in for one either.
|
|
225
|
+
const placeholders = currentSource.kind === "none" ? {} : currentProxied.placeholders;
|
|
226
|
+
for (const [name, placeholder] of Object.entries(placeholders)) {
|
|
227
|
+
// Only in place of a withheld proxy-mode value, never a new name.
|
|
228
|
+
if (withheld.has(name) && ENV_NAME.test(name) && !isReservedTaskEnvName(name) && !placeholder.includes("\0")) {
|
|
229
|
+
values[name] = placeholder;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
Object.assign(values, secrets.values);
|
|
233
|
+
for (const [name, value] of Object.entries(currentUai)) {
|
|
234
|
+
// Only reserved names: Uai's own variables, never a project's.
|
|
235
|
+
if (ENV_NAME.test(name) && isReservedTaskEnvName(name) && !value.includes("\0")) values[name] = value;
|
|
236
|
+
}
|
|
237
|
+
snapshot = renderTaskProjectEnv({ values });
|
|
238
|
+
revisions = currentRevisions;
|
|
239
|
+
runnerKey = currentRunnerKey;
|
|
240
|
+
return snapshot;
|
|
241
|
+
};
|
|
242
|
+
}
|