@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
package/lib/host-env.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-(project, key) environment variable store.
|
|
3
|
+
*
|
|
4
|
+
* Project/org members set KEY=VALUE pairs scoped to a (projectId, key) on this
|
|
5
|
+
* host; values are encrypted at rest with the host master key (same
|
|
6
|
+
* secret-blind pattern as host_ssh_keys / host_github_tokens — ADR-015). The
|
|
7
|
+
* cloud + UI only ever see KEY NAMES, never values (values are write-only). The
|
|
8
|
+
* same key name (e.g. DATABASE_URL) is independent per project, and a project's
|
|
9
|
+
* values are independent per host. At task-up each project on the task gets ITS
|
|
10
|
+
* (projectId, key) values injected into the task container env under the key
|
|
11
|
+
* name (see lib/agent.ts + scripts/agent/task-up.sh).
|
|
12
|
+
*
|
|
13
|
+
* `value_enc` is a single TEXT column carrying both halves of the AES-GCM
|
|
14
|
+
* sealed value as "<ct-base64>.<nonce-base64>".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { and, eq, isNotNull } from "drizzle-orm";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
credentialStorePrefix,
|
|
21
|
+
isParameterPathSegment,
|
|
22
|
+
parameterName,
|
|
23
|
+
queueStoreWrite,
|
|
24
|
+
type CredentialStore,
|
|
25
|
+
} from "./credential-store";
|
|
26
|
+
import { isReservedTaskEnvName } from "../src/task-env-names";
|
|
27
|
+
import { getDb, schema } from "./db";
|
|
28
|
+
import { sealAesGcm, openAesGcm } from "./secrets";
|
|
29
|
+
import { signInBoxMode } from "./signin-box";
|
|
30
|
+
|
|
31
|
+
// Process-local epochs are enough for loader snapshots: every value mutation
|
|
32
|
+
// also happens in this process, and a host restart rebuilds both the epoch map
|
|
33
|
+
// and every task resolver. Keeping the counter per project lets a task ignore
|
|
34
|
+
// unrelated projects without retaining decrypted values in a shared cache.
|
|
35
|
+
const projectEnvRevisions = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
// ADR-153: what a task's view decides moves with more than values: a
|
|
38
|
+
// binding, and the store's gate opening or closing (a value write outside
|
|
39
|
+
// the records closes it too). One process-local counter covers them all.
|
|
40
|
+
let projectEnvAuthorityRevisionCount = 0;
|
|
41
|
+
|
|
42
|
+
export function noteProjectEnvValueMutation(projectId: string): void {
|
|
43
|
+
projectEnvRevisions.set(
|
|
44
|
+
projectId,
|
|
45
|
+
(projectEnvRevisions.get(projectId) ?? 0) + 1,
|
|
46
|
+
);
|
|
47
|
+
// On an activated store, a value written outside its records (an old
|
|
48
|
+
// frame, the credential store) closes the gate by trigger, which every
|
|
49
|
+
// task's view depends on, not only this project's.
|
|
50
|
+
projectEnvAuthorityRevisionCount += 1;
|
|
51
|
+
emitProjectEnvChange();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const projectEnvChangeListeners = new Set<() => void>();
|
|
55
|
+
|
|
56
|
+
/** Calls `listener` after any change to project env values, records,
|
|
57
|
+
* bindings or the store's gate; returns its removal. A listener that throws
|
|
58
|
+
* is skipped, never the change. */
|
|
59
|
+
export function onProjectEnvChange(listener: () => void): () => void {
|
|
60
|
+
projectEnvChangeListeners.add(listener);
|
|
61
|
+
return () => {
|
|
62
|
+
projectEnvChangeListeners.delete(listener);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function emitProjectEnvChange(): void {
|
|
67
|
+
for (const listener of projectEnvChangeListeners) {
|
|
68
|
+
try {
|
|
69
|
+
listener();
|
|
70
|
+
} catch {
|
|
71
|
+
// A listener's failure is its own.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Current process-local value revision for one project's env. */
|
|
77
|
+
export function projectEnvRevision(projectId: string): number {
|
|
78
|
+
return projectEnvRevisions.get(projectId) ?? 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
export function noteProjectEnvAuthorityChange(): void {
|
|
83
|
+
projectEnvAuthorityRevisionCount += 1;
|
|
84
|
+
emitProjectEnvChange();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A task's project env changed outside this runner's store (ADR-154: the
|
|
88
|
+
* cloud's task replica, or the composer starting or ending serving it):
|
|
89
|
+
* whoever follows changes (the proxy's trust delivery) looks again. The
|
|
90
|
+
* store's own revisions do not move; nothing caches a view of the cloud's. */
|
|
91
|
+
export function noteProjectEnvViewChange(): void {
|
|
92
|
+
emitProjectEnvChange();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Current process-local revision of the store's records and gate. */
|
|
96
|
+
export function projectEnvAuthorityRevision(): number {
|
|
97
|
+
return projectEnvAuthorityRevisionCount;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Pack a sealed value (ct + nonce) into the single TEXT column. */
|
|
101
|
+
export function sealProjectEnvValueForStorage(value: string): string {
|
|
102
|
+
const sealed = sealAesGcm(value);
|
|
103
|
+
return `${sealed.ct.toString("base64")}.${sealed.nonce.toString("base64")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Reverse {@link sealProjectEnvValueForStorage}; throws if malformed. */
|
|
107
|
+
export function openProjectEnvValueFromStorage(valueEnc: string): string {
|
|
108
|
+
const dot = valueEnc.indexOf(".");
|
|
109
|
+
if (dot === -1) throw new Error("malformed value_enc");
|
|
110
|
+
const ct = Buffer.from(valueEnc.slice(0, dot), "base64");
|
|
111
|
+
const nonce = Buffer.from(valueEnc.slice(dot + 1), "base64");
|
|
112
|
+
return openAesGcm(ct, nonce);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Set (insert or overwrite) a project env var, encrypting the value at rest.
|
|
116
|
+
* `orgId` is the project's org when the cloud sent it (see `noteProjectOrg`). */
|
|
117
|
+
export function setProjectEnvVar(
|
|
118
|
+
projectId: string,
|
|
119
|
+
key: string,
|
|
120
|
+
value: string,
|
|
121
|
+
orgId?: string,
|
|
122
|
+
): void {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
getDb()
|
|
125
|
+
.insert(schema.hostProjectEnv)
|
|
126
|
+
.values({
|
|
127
|
+
projectId,
|
|
128
|
+
key,
|
|
129
|
+
valueEnc: sealProjectEnvValueForStorage(value),
|
|
130
|
+
updatedAt: now,
|
|
131
|
+
...(orgId ? { orgId } : {}),
|
|
132
|
+
})
|
|
133
|
+
.onConflictDoUpdate({
|
|
134
|
+
target: [schema.hostProjectEnv.projectId, schema.hostProjectEnv.key],
|
|
135
|
+
set: { valueEnc: sealProjectEnvValueForStorage(value), updatedAt: now, ...(orgId ? { orgId } : {}) },
|
|
136
|
+
})
|
|
137
|
+
.run();
|
|
138
|
+
noteProjectEnvValueMutation(projectId);
|
|
139
|
+
if (orgId) noteProjectOrg(projectId, orgId, { except: key });
|
|
140
|
+
mirrorProjectEnvToStore(projectId, key);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The cloud told us which org owns `projectId` (an env.var.set/list frame
|
|
145
|
+
* carrying `orgId`). Every row of that project mirrored before the org was
|
|
146
|
+
* known gets the org and is re-tagged in the store, so a task machine's
|
|
147
|
+
* tag-scoped lease can read it (ADR-133). Returns the rows updated.
|
|
148
|
+
*/
|
|
149
|
+
export function noteProjectOrg(
|
|
150
|
+
projectId: string,
|
|
151
|
+
orgId: string,
|
|
152
|
+
opts: { except?: string } = {},
|
|
153
|
+
): number {
|
|
154
|
+
const rows = getDb()
|
|
155
|
+
.select({ key: schema.hostProjectEnv.key, orgId: schema.hostProjectEnv.orgId, storeRef: schema.hostProjectEnv.storeRef })
|
|
156
|
+
.from(schema.hostProjectEnv)
|
|
157
|
+
.where(eq(schema.hostProjectEnv.projectId, projectId))
|
|
158
|
+
.all()
|
|
159
|
+
.filter((row) => row.key !== opts.except && row.orgId !== orgId);
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
getDb()
|
|
162
|
+
.update(schema.hostProjectEnv)
|
|
163
|
+
.set({ orgId })
|
|
164
|
+
.where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, row.key)))
|
|
165
|
+
.run();
|
|
166
|
+
if (row.storeRef) {
|
|
167
|
+
const name = row.storeRef;
|
|
168
|
+
queueStoreWrite(`tag project env ${projectId}/${row.key}`, async (store) => {
|
|
169
|
+
await tagProjectEnvParameter(store, name, projectId, orgId);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return rows.length;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The org recorded for a project, or null while the cloud has not said. */
|
|
177
|
+
export function projectEnvOrgId(projectId: string): string | null {
|
|
178
|
+
const row = getDb()
|
|
179
|
+
.select({ orgId: schema.hostProjectEnv.orgId })
|
|
180
|
+
.from(schema.hostProjectEnv)
|
|
181
|
+
.where(and(eq(schema.hostProjectEnv.projectId, projectId), isNotNull(schema.hostProjectEnv.orgId)))
|
|
182
|
+
.get();
|
|
183
|
+
return row?.orgId ?? null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The tags a project env parameter carries: the lease policy's `org`, plus
|
|
187
|
+
* `project` for operators reading the store. */
|
|
188
|
+
export function projectEnvTags(projectId: string, orgId: string): Record<string, string> {
|
|
189
|
+
return { org: orgId, project: projectId };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tagWarned = new Set<string>();
|
|
193
|
+
/**
|
|
194
|
+
* Tag a mirrored parameter for the task-machine lease. Best-effort: the
|
|
195
|
+
* value is already in the store, and a host whose policy predates
|
|
196
|
+
* `ssm:AddTagsToResource` (host-agent/scripts/credential-store/
|
|
197
|
+
* bootstrap-aws.sh) must keep mirroring; it warns once per parameter and a
|
|
198
|
+
* later set/list retries.
|
|
199
|
+
*/
|
|
200
|
+
export async function tagProjectEnvParameter(
|
|
201
|
+
store: Pick<CredentialStore, "tag">,
|
|
202
|
+
name: string,
|
|
203
|
+
projectId: string,
|
|
204
|
+
orgId: string,
|
|
205
|
+
): Promise<boolean> {
|
|
206
|
+
try {
|
|
207
|
+
await store.tag(name, projectEnvTags(projectId, orgId));
|
|
208
|
+
tagWarned.delete(name);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (!tagWarned.has(name)) {
|
|
212
|
+
tagWarned.add(name);
|
|
213
|
+
console.warn(
|
|
214
|
+
`[project-env] credential store: tagging ${name} failed (${err instanceof Error ? err.message : String(err)}) — a task machine cannot read it until it carries org=${orgId}; re-run bootstrap-aws.sh for ssm:AddTagsToResource`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Remove a project env var (idempotent). */
|
|
222
|
+
export function deleteProjectEnvVar(projectId: string, key: string): void {
|
|
223
|
+
const result = getDb()
|
|
224
|
+
.delete(schema.hostProjectEnv)
|
|
225
|
+
.where(
|
|
226
|
+
and(
|
|
227
|
+
eq(schema.hostProjectEnv.projectId, projectId),
|
|
228
|
+
eq(schema.hostProjectEnv.key, key),
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
.run();
|
|
232
|
+
if (result.changes > 0) {
|
|
233
|
+
noteProjectEnvValueMutation(projectId);
|
|
234
|
+
mirrorProjectEnvDeletionToStore(projectId, key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Queue only the credential-store side of a committed local deletion. */
|
|
239
|
+
export function mirrorProjectEnvDeletionToStore(
|
|
240
|
+
projectId: string,
|
|
241
|
+
key: string,
|
|
242
|
+
): void {
|
|
243
|
+
const name = projectEnvParameterName(projectId, key);
|
|
244
|
+
if (name) {
|
|
245
|
+
queueStoreWrite(`delete project env ${projectId}/${key}`, (store) =>
|
|
246
|
+
store.delete(name),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Credential store mirror (ADR-134 slice 3): `<prefix>/projects/<projectId>/env/<KEY>`.
|
|
253
|
+
// The set/delete frames carry no org, and a project id is org-bound in the
|
|
254
|
+
// cloud, so the project is the owner segment. Owned hosts: no-ops.
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
export interface StoredProjectEnv {
|
|
258
|
+
v: 1;
|
|
259
|
+
kind: "project-env";
|
|
260
|
+
projectId: string;
|
|
261
|
+
key: string;
|
|
262
|
+
value: string;
|
|
263
|
+
updatedAt: number;
|
|
264
|
+
/** The project's org once the cloud has said (ADR-133 tag scope); a
|
|
265
|
+
* replacement host inherits it with the value. */
|
|
266
|
+
orgId?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Null when the project id or key cannot be a parameter path segment (a
|
|
270
|
+
* key with a character SSM refuses is kept local and logged once). */
|
|
271
|
+
export function projectEnvParameterName(projectId: string, key: string): string | null {
|
|
272
|
+
if (!isParameterPathSegment(projectId) || !isParameterPathSegment(key)) return null;
|
|
273
|
+
return parameterName(credentialStorePrefix(), "projects", projectId, "env", key);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The row as it is mirrored (value decrypted). Never logged. */
|
|
277
|
+
export function projectEnvStoreValue(projectId: string, key: string): StoredProjectEnv | null {
|
|
278
|
+
const row = getDb()
|
|
279
|
+
.select()
|
|
280
|
+
.from(schema.hostProjectEnv)
|
|
281
|
+
.where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
|
|
282
|
+
.get();
|
|
283
|
+
if (!row) return null;
|
|
284
|
+
return {
|
|
285
|
+
v: 1,
|
|
286
|
+
kind: "project-env",
|
|
287
|
+
projectId,
|
|
288
|
+
key,
|
|
289
|
+
value: openProjectEnvValueFromStorage(row.valueEnc),
|
|
290
|
+
updatedAt: row.updatedAt,
|
|
291
|
+
...(row.orgId ? { orgId: row.orgId } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function setProjectEnvStoreRef(projectId: string, key: string, storeRef: string): void {
|
|
296
|
+
getDb()
|
|
297
|
+
.update(schema.hostProjectEnv)
|
|
298
|
+
.set({ storeRef })
|
|
299
|
+
.where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
|
|
300
|
+
.run();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function listProjectEnvStoreRows(): Array<{ projectId: string; key: string; storeRef: string | null }> {
|
|
304
|
+
return getDb()
|
|
305
|
+
.select({
|
|
306
|
+
projectId: schema.hostProjectEnv.projectId,
|
|
307
|
+
key: schema.hostProjectEnv.key,
|
|
308
|
+
storeRef: schema.hostProjectEnv.storeRef,
|
|
309
|
+
})
|
|
310
|
+
.from(schema.hostProjectEnv)
|
|
311
|
+
.all();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function mirrorProjectEnvToStore(projectId: string, key: string): void {
|
|
315
|
+
// ADR-136 sign-in box: no local rows; env.var.* is a store passthrough
|
|
316
|
+
// (lib/signin-box-credentials.ts).
|
|
317
|
+
if (signInBoxMode()) return;
|
|
318
|
+
const name = projectEnvParameterName(projectId, key);
|
|
319
|
+
if (!name) return;
|
|
320
|
+
queueStoreWrite(`mirror project env ${projectId}/${key}`, async (store) => {
|
|
321
|
+
const value = projectEnvStoreValue(projectId, key);
|
|
322
|
+
if (!value) return;
|
|
323
|
+
await store.put(name, JSON.stringify(value));
|
|
324
|
+
setProjectEnvStoreRef(projectId, key, name);
|
|
325
|
+
if (value.orgId) await tagProjectEnvParameter(store, name, projectId, value.orgId);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Reconcile import: a value this host lacks. Existing rows win. */
|
|
330
|
+
export function importProjectEnvFromStore(value: StoredProjectEnv, storeRef: string): boolean {
|
|
331
|
+
const result = getDb()
|
|
332
|
+
.insert(schema.hostProjectEnv)
|
|
333
|
+
.values({
|
|
334
|
+
projectId: value.projectId,
|
|
335
|
+
key: value.key,
|
|
336
|
+
valueEnc: sealProjectEnvValueForStorage(value.value),
|
|
337
|
+
updatedAt: value.updatedAt,
|
|
338
|
+
storeRef,
|
|
339
|
+
...(value.orgId ? { orgId: value.orgId } : {}),
|
|
340
|
+
})
|
|
341
|
+
.onConflictDoNothing({ target: [schema.hostProjectEnv.projectId, schema.hostProjectEnv.key] })
|
|
342
|
+
.run();
|
|
343
|
+
if (result.changes > 0) noteProjectEnvValueMutation(value.projectId);
|
|
344
|
+
return result.changes > 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Reconcile: the row's parameter vanished elsewhere; drop the row only. */
|
|
348
|
+
export function deleteProjectEnvLocal(projectId: string, key: string): void {
|
|
349
|
+
const result = getDb()
|
|
350
|
+
.delete(schema.hostProjectEnv)
|
|
351
|
+
.where(and(eq(schema.hostProjectEnv.projectId, projectId), eq(schema.hostProjectEnv.key, key)))
|
|
352
|
+
.run();
|
|
353
|
+
if (result.changes > 0) noteProjectEnvValueMutation(projectId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Sorted list of a project's env var KEY NAMES (never the values). */
|
|
357
|
+
export function listProjectEnvKeys(projectId: string): string[] {
|
|
358
|
+
return getDb()
|
|
359
|
+
.select({ key: schema.hostProjectEnv.key })
|
|
360
|
+
.from(schema.hostProjectEnv)
|
|
361
|
+
.where(eq(schema.hostProjectEnv.projectId, projectId))
|
|
362
|
+
.all()
|
|
363
|
+
.map((row) => row.key)
|
|
364
|
+
.sort();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Decrypt + return a project's env vars as KEY→VALUE. Used at task-up injection
|
|
369
|
+
* (lib/agent.ts) — the decrypted values live only on the trusted host (ADR-015)
|
|
370
|
+
* and go straight into the task container's child env, never logged. Resilient:
|
|
371
|
+
* a single value that fails to decrypt is logged + skipped rather than aborting
|
|
372
|
+
* the whole task launch.
|
|
373
|
+
*/
|
|
374
|
+
export function getDecryptedForProject(projectId: string): Record<string, string> {
|
|
375
|
+
const rows = getDb()
|
|
376
|
+
.select({
|
|
377
|
+
key: schema.hostProjectEnv.key,
|
|
378
|
+
valueEnc: schema.hostProjectEnv.valueEnc,
|
|
379
|
+
})
|
|
380
|
+
.from(schema.hostProjectEnv)
|
|
381
|
+
.where(eq(schema.hostProjectEnv.projectId, projectId))
|
|
382
|
+
.all();
|
|
383
|
+
const out: Record<string, string> = {};
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
try {
|
|
386
|
+
out[row.key] = openProjectEnvValueFromStorage(row.valueEnc);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.warn(
|
|
389
|
+
`[host-env] skipping ${projectId}/${row.key}: decrypt failed (${
|
|
390
|
+
err instanceof Error ? err.message : String(err)
|
|
391
|
+
})`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Names a project may declare but a task must never receive from project env —
|
|
400
|
+
* the same list task-up.sh's `is_reserved_task_env_name` enforces for
|
|
401
|
+
* container tasks: git/gh plumbing, loader and shell hooks, toolchain config,
|
|
402
|
+
* the host's own `UAI_*` namespace, engine credentials, and the browser path.
|
|
403
|
+
*/
|
|
404
|
+
export { isReservedTaskEnvName } from "../src/task-env-names";
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Rollout-only project env for an ADR-121 machine task whose persisted mode
|
|
408
|
+
* is still `baked`: every project's decrypted values, lead project winning a
|
|
409
|
+
* key collision, minus the reserved names above. Loader tasks must use the
|
|
410
|
+
* declaration-pinned gateway instead. Live 2026-09-09: a machine task on Uai
|
|
411
|
+
* Cloud had the Infisical identity in the store and the host, and the agent
|
|
412
|
+
* never saw it, because only the compose path injected project env.
|
|
413
|
+
*/
|
|
414
|
+
export function projectEnvForTask(projectIds: readonly string[]): Record<string, string> {
|
|
415
|
+
const merged: Record<string, string> = {};
|
|
416
|
+
for (const projectId of [...projectIds].reverse()) {
|
|
417
|
+
Object.assign(merged, getDecryptedForProject(projectId));
|
|
418
|
+
}
|
|
419
|
+
const out: Record<string, string> = {};
|
|
420
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
421
|
+
if (!isReservedTaskEnvName(key)) out[key] = value;
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|