@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/db/schema.ts
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-local SQLite schema (ADR-020).
|
|
3
|
+
*
|
|
4
|
+
* The cloud DB owns metadata and lifecycle. Each host owns only the runtime
|
|
5
|
+
* fields needed to recover containers, discover local ports, and tunnel
|
|
6
|
+
* editor/preview traffic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sql } from "drizzle-orm";
|
|
10
|
+
import {
|
|
11
|
+
blob,
|
|
12
|
+
check,
|
|
13
|
+
index,
|
|
14
|
+
integer,
|
|
15
|
+
primaryKey,
|
|
16
|
+
sqliteTable,
|
|
17
|
+
text,
|
|
18
|
+
} from "drizzle-orm/sqlite-core";
|
|
19
|
+
|
|
20
|
+
export const hostTasks = sqliteTable(
|
|
21
|
+
"uai_host_tasks",
|
|
22
|
+
{
|
|
23
|
+
taskId: text("task_id").primaryKey(),
|
|
24
|
+
// ADR-154: the v1 epoch whose runtime was successfully established.
|
|
25
|
+
// Admission's monotonic floor lives separately so deleting this recovery
|
|
26
|
+
// row can never make an older dispatch current again. 0 means no v1
|
|
27
|
+
// runtime has been proven for this row (including every legacy row).
|
|
28
|
+
runEpoch: integer("run_epoch").notNull().default(0),
|
|
29
|
+
codeServerPort: integer("code_server_port"),
|
|
30
|
+
previewPorts: text("preview_ports").notNull().default("[]"),
|
|
31
|
+
composeProject: text("compose_project"),
|
|
32
|
+
worktreePath: text("worktree_path"),
|
|
33
|
+
// ADR-101 Phase C. Null rows predate the provider seam and are inferred as
|
|
34
|
+
// Docker Compose only when their legacy runtime identity is complete.
|
|
35
|
+
environmentProvider: text("environment_provider"),
|
|
36
|
+
environmentLocator: text("environment_locator"),
|
|
37
|
+
// ADR-153 rollout fence. Null rows predate the loader and mean `baked`.
|
|
38
|
+
envMode: text("env_mode"),
|
|
39
|
+
// Value-free resolver metadata for rebuilding the project-env gateway
|
|
40
|
+
// after a runner restart, before the cloud re-sends task-up.
|
|
41
|
+
projectEnvSpec: text("project_env_spec"),
|
|
42
|
+
// ADR-154 Files v1: the normalized mode from the cloud's task launch.
|
|
43
|
+
// Recovery must never trust the task-writable `.uai/files-mounted`
|
|
44
|
+
// marker as authority for what may be mounted.
|
|
45
|
+
sharedFilesMode: text("shared_files_mode", {
|
|
46
|
+
enum: ["off", "ro", "rw"],
|
|
47
|
+
}),
|
|
48
|
+
statusMirror: text("status_mirror"),
|
|
49
|
+
// Owner + project slugs — received in TaskLaunchInput at task-up and
|
|
50
|
+
// persisted here so the local UI (ADR-028) can show "who/what" without a
|
|
51
|
+
// cloud round-trip. The cloud stays the audit authority.
|
|
52
|
+
ownerUserId: text("owner_user_id"),
|
|
53
|
+
ownerEmail: text("owner_email"),
|
|
54
|
+
ownerName: text("owner_name"),
|
|
55
|
+
projectSlugs: text("project_slugs").notNull().default("[]"),
|
|
56
|
+
// Lead project first. Retained for legacy/baked machine-task agent spawns;
|
|
57
|
+
// loader tasks resolve only declared names through projectEnvSpec.
|
|
58
|
+
projectIds: text("project_ids").notNull().default("[]"),
|
|
59
|
+
// ADR-131 D3: which engine-account scope this task resolves credentials
|
|
60
|
+
// from ("org:<orgId>" on a shared host; null = "host"). Persisted so a
|
|
61
|
+
// host restart re-provisions the same scope before the cloud re-sends it.
|
|
62
|
+
engineAccountScope: text("engine_account_scope"),
|
|
63
|
+
lockedAt: integer("locked_at", { mode: "number" }),
|
|
64
|
+
startedAt: integer("started_at", { mode: "number" }),
|
|
65
|
+
endedAt: integer("ended_at", { mode: "number" }),
|
|
66
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
67
|
+
.notNull()
|
|
68
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
69
|
+
},
|
|
70
|
+
(table) => ({
|
|
71
|
+
runEpochCheck: check(
|
|
72
|
+
"uai_host_tasks_run_epoch",
|
|
73
|
+
sql`${table.runEpoch} >= 0`,
|
|
74
|
+
),
|
|
75
|
+
environmentPair: check(
|
|
76
|
+
"uai_host_tasks_environment_pair",
|
|
77
|
+
sql`(${table.environmentProvider} IS NULL) = (${table.environmentLocator} IS NULL)`,
|
|
78
|
+
),
|
|
79
|
+
sharedFilesModeCheck: check(
|
|
80
|
+
"uai_host_tasks_shared_files_mode",
|
|
81
|
+
sql`${table.sharedFilesMode} IS NULL OR ${table.sharedFilesMode} IN ('off', 'ro', 'rw')`,
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export type HostTask = typeof hostTasks.$inferSelect;
|
|
87
|
+
export type NewHostTask = typeof hostTasks.$inferInsert;
|
|
88
|
+
|
|
89
|
+
// ADR-154's non-secret assignment authority. It intentionally has no foreign
|
|
90
|
+
// key to uai_host_tasks: terminal-history retention and orphan GC may remove a
|
|
91
|
+
// runtime row, but neither is allowed to erase the highest epoch this runner
|
|
92
|
+
// has admitted or reopen a closed assignment.
|
|
93
|
+
export const hostTaskRunAssignments = sqliteTable(
|
|
94
|
+
"host_task_run_assignments",
|
|
95
|
+
{
|
|
96
|
+
taskId: text("task_id").primaryKey(),
|
|
97
|
+
highestRunEpoch: integer("highest_run_epoch").notNull(),
|
|
98
|
+
assignmentOpen: integer("assignment_open", { mode: "boolean" })
|
|
99
|
+
.notNull()
|
|
100
|
+
.default(true),
|
|
101
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
102
|
+
.notNull()
|
|
103
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
104
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
105
|
+
.notNull()
|
|
106
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
107
|
+
},
|
|
108
|
+
(table) => ({
|
|
109
|
+
runEpochCheck: check(
|
|
110
|
+
"host_task_run_assignments_epoch",
|
|
111
|
+
sql`${table.highestRunEpoch} >= 1`,
|
|
112
|
+
),
|
|
113
|
+
assignmentOpenCheck: check(
|
|
114
|
+
"host_task_run_assignments_open",
|
|
115
|
+
sql`${table.assignmentOpen} IN (0, 1)`,
|
|
116
|
+
),
|
|
117
|
+
taskIdCheck: check(
|
|
118
|
+
"host_task_run_assignments_task_id",
|
|
119
|
+
sql`length(${table.taskId}) > 0`,
|
|
120
|
+
),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export type HostTaskRunAssignment =
|
|
125
|
+
typeof hostTaskRunAssignments.$inferSelect;
|
|
126
|
+
export type NewHostTaskRunAssignment =
|
|
127
|
+
typeof hostTaskRunAssignments.$inferInsert;
|
|
128
|
+
|
|
129
|
+
// ADR-154: values never land here. This is only the durable witness needed
|
|
130
|
+
// to remove a task-local credential file after a runner crash. The exact
|
|
131
|
+
// runtime locator is part of the key because a later run may recreate the
|
|
132
|
+
// same task id on a different container or VM.
|
|
133
|
+
export const hostCredentialCleanupPaths = sqliteTable(
|
|
134
|
+
"host_credential_cleanup_paths",
|
|
135
|
+
{
|
|
136
|
+
taskId: text("task_id").notNull(),
|
|
137
|
+
runEpoch: integer("run_epoch").notNull(),
|
|
138
|
+
runtimeLocator: text("runtime_locator").notNull(),
|
|
139
|
+
lane: text("lane", {
|
|
140
|
+
enum: ["engine-auth", "github", "ssh", "files-session"],
|
|
141
|
+
}).notNull(),
|
|
142
|
+
path: text("path").notNull(),
|
|
143
|
+
credentialId: text("credential_id").notNull(),
|
|
144
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
145
|
+
.notNull()
|
|
146
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
147
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
148
|
+
.notNull()
|
|
149
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
150
|
+
},
|
|
151
|
+
(table) => ({
|
|
152
|
+
pk: primaryKey({
|
|
153
|
+
columns: [
|
|
154
|
+
table.taskId,
|
|
155
|
+
table.runEpoch,
|
|
156
|
+
table.runtimeLocator,
|
|
157
|
+
table.path,
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
taskCredentialIdx: index(
|
|
161
|
+
"host_credential_cleanup_paths_task_credential_idx",
|
|
162
|
+
).on(table.taskId, table.runEpoch, table.credentialId),
|
|
163
|
+
runEpochCheck: check(
|
|
164
|
+
"host_credential_cleanup_paths_run_epoch",
|
|
165
|
+
sql`${table.runEpoch} >= 1`,
|
|
166
|
+
),
|
|
167
|
+
identityCheck: check(
|
|
168
|
+
"host_credential_cleanup_paths_identity",
|
|
169
|
+
sql`length(${table.taskId}) > 0 AND length(${table.runtimeLocator}) > 0 AND length(${table.credentialId}) > 0`,
|
|
170
|
+
),
|
|
171
|
+
laneCheck: check(
|
|
172
|
+
"host_credential_cleanup_paths_lane",
|
|
173
|
+
sql`${table.lane} IN ('engine-auth', 'github', 'ssh', 'files-session')`,
|
|
174
|
+
),
|
|
175
|
+
absolutePathCheck: check(
|
|
176
|
+
"host_credential_cleanup_paths_absolute_path",
|
|
177
|
+
sql`substr(${table.path}, 1, 1) = '/'`,
|
|
178
|
+
),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
export type HostCredentialCleanupPath =
|
|
183
|
+
typeof hostCredentialCleanupPaths.$inferSelect;
|
|
184
|
+
export type NewHostCredentialCleanupPath =
|
|
185
|
+
typeof hostCredentialCleanupPaths.$inferInsert;
|
|
186
|
+
|
|
187
|
+
// A task remains fail-closed across a host crash even when cleanup has not yet
|
|
188
|
+
// reached (or has already deleted) an inventoried path. This row contains no
|
|
189
|
+
// credential value; it is cleared only after the entire cleanup procedure has
|
|
190
|
+
// completed successfully through the recorded epoch.
|
|
191
|
+
export const hostCredentialCleanupPending = sqliteTable(
|
|
192
|
+
"host_credential_cleanup_pending",
|
|
193
|
+
{
|
|
194
|
+
taskId: text("task_id").primaryKey(),
|
|
195
|
+
runEpoch: integer("run_epoch").notNull(),
|
|
196
|
+
reason: text("reason", {
|
|
197
|
+
enum: [
|
|
198
|
+
"removed",
|
|
199
|
+
"denied",
|
|
200
|
+
"expired",
|
|
201
|
+
"assignment",
|
|
202
|
+
"restart",
|
|
203
|
+
"stop",
|
|
204
|
+
"park",
|
|
205
|
+
"teardown",
|
|
206
|
+
],
|
|
207
|
+
}).notNull(),
|
|
208
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
209
|
+
.notNull()
|
|
210
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
211
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
212
|
+
.notNull()
|
|
213
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
214
|
+
},
|
|
215
|
+
(table) => ({
|
|
216
|
+
runEpochCheck: check(
|
|
217
|
+
"host_credential_cleanup_pending_run_epoch",
|
|
218
|
+
sql`${table.runEpoch} >= 1`,
|
|
219
|
+
),
|
|
220
|
+
reasonCheck: check(
|
|
221
|
+
"host_credential_cleanup_pending_reason",
|
|
222
|
+
sql`${table.reason} IN ('removed', 'denied', 'expired', 'assignment', 'restart', 'stop', 'park', 'teardown')`,
|
|
223
|
+
),
|
|
224
|
+
taskIdCheck: check(
|
|
225
|
+
"host_credential_cleanup_pending_task_id",
|
|
226
|
+
sql`length(${table.taskId}) > 0`,
|
|
227
|
+
),
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
export type HostCredentialCleanupPending =
|
|
232
|
+
typeof hostCredentialCleanupPending.$inferSelect;
|
|
233
|
+
export type NewHostCredentialCleanupPending =
|
|
234
|
+
typeof hostCredentialCleanupPending.$inferInsert;
|
|
235
|
+
|
|
236
|
+
// ADR-154 §6: every runtime a leased engine of a task's run was started in,
|
|
237
|
+
// recorded before it starts. An env-token engine leaves no credential file
|
|
238
|
+
// for the cleanup inventory to name, yet the cleanup must prove it stopped
|
|
239
|
+
// wherever it ran, an older runtime included. Value-free.
|
|
240
|
+
export const hostLeasedEngineRuntimes = sqliteTable(
|
|
241
|
+
"host_leased_engine_runtimes",
|
|
242
|
+
{
|
|
243
|
+
taskId: text("task_id").notNull(),
|
|
244
|
+
runEpoch: integer("run_epoch").notNull(),
|
|
245
|
+
runtimeLocator: text("runtime_locator").notNull(),
|
|
246
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
247
|
+
.notNull()
|
|
248
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
249
|
+
},
|
|
250
|
+
(table) => ({
|
|
251
|
+
pk: primaryKey({
|
|
252
|
+
columns: [table.taskId, table.runEpoch, table.runtimeLocator],
|
|
253
|
+
}),
|
|
254
|
+
runEpochCheck: check(
|
|
255
|
+
"host_leased_engine_runtimes_run_epoch",
|
|
256
|
+
sql`${table.runEpoch} >= 1`,
|
|
257
|
+
),
|
|
258
|
+
identityCheck: check(
|
|
259
|
+
"host_leased_engine_runtimes_identity",
|
|
260
|
+
sql`length(${table.taskId}) > 0 AND length(${table.runtimeLocator}) > 0`,
|
|
261
|
+
),
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
export type HostLeasedEngineRuntime =
|
|
266
|
+
typeof hostLeasedEngineRuntimes.$inferSelect;
|
|
267
|
+
|
|
268
|
+
// The boot sweep's owed re-stops (2026-10-30): a stopped app container the
|
|
269
|
+
// sweep starts only to remove its credential files is recorded here, by its
|
|
270
|
+
// exact id and runtime, BEFORE it starts. Only proof that this container is
|
|
271
|
+
// stopped again, or gone, retires the row; removing a credential path never
|
|
272
|
+
// does. A crash after the start, a start that timed out yet started it, or a
|
|
273
|
+
// stop that failed all leave the row for the next cleanup of the task to
|
|
274
|
+
// settle first. Value-free.
|
|
275
|
+
export const hostCredentialCleanupRestops = sqliteTable(
|
|
276
|
+
"host_credential_cleanup_restops",
|
|
277
|
+
{
|
|
278
|
+
taskId: text("task_id").notNull(),
|
|
279
|
+
containerId: text("container_id").notNull(),
|
|
280
|
+
runEpoch: integer("run_epoch").notNull(),
|
|
281
|
+
runtimeLocator: text("runtime_locator").notNull(),
|
|
282
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
283
|
+
.notNull()
|
|
284
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
285
|
+
},
|
|
286
|
+
(table) => ({
|
|
287
|
+
pk: primaryKey({ columns: [table.taskId, table.containerId] }),
|
|
288
|
+
runEpochCheck: check(
|
|
289
|
+
"host_credential_cleanup_restops_run_epoch",
|
|
290
|
+
sql`${table.runEpoch} >= 1`,
|
|
291
|
+
),
|
|
292
|
+
identityCheck: check(
|
|
293
|
+
"host_credential_cleanup_restops_identity",
|
|
294
|
+
sql`length(${table.taskId}) > 0 AND length(${table.containerId}) > 0 AND length(${table.runtimeLocator}) > 0`,
|
|
295
|
+
),
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
export type HostCredentialCleanupRestop =
|
|
300
|
+
typeof hostCredentialCleanupRestops.$inferSelect;
|
|
301
|
+
|
|
302
|
+
// Singleton host-local settings row (ADR-100). Runtime switches that must be
|
|
303
|
+
// visible before SQLite opens remain in `.env.local`; retention policy belongs
|
|
304
|
+
// here because it governs owner-facing local history only. Correctness
|
|
305
|
+
// tombstones and orphan-GC evidence are never controlled by this setting.
|
|
306
|
+
export const hostSettings = sqliteTable(
|
|
307
|
+
"host_settings",
|
|
308
|
+
{
|
|
309
|
+
id: text("id").primaryKey().default("host"),
|
|
310
|
+
retainTerminalHistory: integer("retain_terminal_history", {
|
|
311
|
+
mode: "boolean",
|
|
312
|
+
})
|
|
313
|
+
.notNull()
|
|
314
|
+
.default(true),
|
|
315
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
316
|
+
.notNull()
|
|
317
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
318
|
+
},
|
|
319
|
+
(table) => ({
|
|
320
|
+
singleton: check("host_settings_singleton", sql`${table.id} = 'host'`),
|
|
321
|
+
retentionBoolean: check(
|
|
322
|
+
"host_settings_retention_boolean",
|
|
323
|
+
sql`${table.retainTerminalHistory} IN (0, 1)`,
|
|
324
|
+
),
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
export type HostSettings = typeof hostSettings.$inferSelect;
|
|
329
|
+
export type NewHostSettings = typeof hostSettings.$inferInsert;
|
|
330
|
+
|
|
331
|
+
// Host-side task lifecycle event log (ADR-028). HostEvent (the agent-output
|
|
332
|
+
// stream) is forwarded to the cloud, not stored; this table is the local
|
|
333
|
+
// `/api/events` feed — appended on task.created/started/ended (+ ship).
|
|
334
|
+
export const hostEvents = sqliteTable(
|
|
335
|
+
"uai_host_events",
|
|
336
|
+
{
|
|
337
|
+
id: text("id").primaryKey(), // ulid
|
|
338
|
+
taskId: text("task_id").notNull(),
|
|
339
|
+
kind: text("kind").notNull(), // task.created | task.started | task.ended | task.ship
|
|
340
|
+
ts: integer("ts", { mode: "number" })
|
|
341
|
+
.notNull()
|
|
342
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
343
|
+
detail: text("detail"),
|
|
344
|
+
},
|
|
345
|
+
(t) => ({
|
|
346
|
+
taskIdIdx: index("uai_host_events_task_id_idx").on(t.taskId),
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
export type HostEventRow = typeof hostEvents.$inferSelect;
|
|
351
|
+
export type NewHostEventRow = typeof hostEvents.$inferInsert;
|
|
352
|
+
|
|
353
|
+
// GitHub user refresh token, encrypted at rest with the host master key
|
|
354
|
+
// (ADR-027). The ONLY place a GitHub secret lives at rest. Access tokens are
|
|
355
|
+
// never stored — they go straight into the container's gh config.
|
|
356
|
+
export const githubTokens = sqliteTable("host_github_tokens", {
|
|
357
|
+
userId: text("user_id").primaryKey(),
|
|
358
|
+
installationId: integer("installation_id").notNull(),
|
|
359
|
+
/** Generation of the cloud binding whose credential this row contains.
|
|
360
|
+
* Readers trust the token only while it matches the durable mutation fence
|
|
361
|
+
* below, so a crash between claiming a newer generation and deleting this
|
|
362
|
+
* row leaves a safely inert old ciphertext rather than a usable old token. */
|
|
363
|
+
generation: integer("generation").notNull().default(0),
|
|
364
|
+
// ADR-033: holds a long-lived ACCESS token when kind="access", or a refresh
|
|
365
|
+
// token when kind="refresh" (legacy ADR-027). Encrypted at rest either way.
|
|
366
|
+
refreshTokenCt: blob("refresh_token_ct").notNull(),
|
|
367
|
+
refreshTokenNonce: blob("refresh_token_nonce").notNull(),
|
|
368
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at"),
|
|
369
|
+
// "access" (non-expiring, ADR-033) | "refresh" (expiring, ADR-027).
|
|
370
|
+
kind: text("kind").notNull().default("refresh"),
|
|
371
|
+
updatedAt: integer("updated_at").notNull(),
|
|
372
|
+
// ADR-134 slice 2: the credential-store parameter this row mirrors to.
|
|
373
|
+
storeRef: text("store_ref"),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
export type GithubToken = typeof githubTokens.$inferSelect;
|
|
377
|
+
export type NewGithubToken = typeof githubTokens.$inferInsert;
|
|
378
|
+
|
|
379
|
+
// Durable monotonic tombstone for GitHub credential mutations. Kept separate
|
|
380
|
+
// from the token row so a disconnect can reject a delayed older connect even
|
|
381
|
+
// after the encrypted credential itself has been deleted.
|
|
382
|
+
export const githubCredentialGenerations = sqliteTable(
|
|
383
|
+
"host_github_credential_generations",
|
|
384
|
+
{
|
|
385
|
+
userId: text("user_id").primaryKey(),
|
|
386
|
+
generation: integer("generation").notNull().default(0),
|
|
387
|
+
mutation: text("mutation").notNull().default("set"),
|
|
388
|
+
updatedAt: integer("updated_at").notNull(),
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Per-user ed25519 SSH key (ADR-029). Generated ON the host; the private key
|
|
393
|
+
// lives encrypted at rest with the host master key (same secret-blind pattern
|
|
394
|
+
// as host_github_tokens — ADR-015). The cloud only ever sees `public_key`,
|
|
395
|
+
// which the user pastes into GitHub. Keyed by the internal cloud user id.
|
|
396
|
+
export const sshKeys = sqliteTable("host_ssh_keys", {
|
|
397
|
+
userId: text("user_id").primaryKey(),
|
|
398
|
+
publicKey: text("public_key").notNull(), // OpenSSH "ssh-ed25519 AAAA… uai-<user>"
|
|
399
|
+
privateKeyCt: blob("private_key_ct").notNull(),
|
|
400
|
+
privateKeyNonce: blob("private_key_nonce").notNull(),
|
|
401
|
+
createdAt: integer("created_at").notNull(),
|
|
402
|
+
updatedAt: integer("updated_at").notNull(),
|
|
403
|
+
// ADR-134 slice 2: the credential-store parameter this row mirrors to.
|
|
404
|
+
storeRef: text("store_ref"),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
export type SshKey = typeof sshKeys.$inferSelect;
|
|
408
|
+
export type NewSshKey = typeof sshKeys.$inferInsert;
|
|
409
|
+
|
|
410
|
+
// Per-(project, key) environment variable values, encrypted at rest with the
|
|
411
|
+
// host master key (same secret-blind pattern as host_ssh_keys /
|
|
412
|
+
// host_github_tokens — ADR-015). The cloud only ever sees KEY NAMES, never
|
|
413
|
+
// values (values are write-only). Set by project/org members on the project
|
|
414
|
+
// settings page (grouped by host); at task-up each project on the task gets ITS
|
|
415
|
+
// (projectId, key) values injected under the key name. The same key name is
|
|
416
|
+
// independent per project, and a project's values are independent per host.
|
|
417
|
+
export const hostProjectEnv = sqliteTable(
|
|
418
|
+
"host_project_env",
|
|
419
|
+
{
|
|
420
|
+
projectId: text("project_id").notNull(),
|
|
421
|
+
key: text("key").notNull(),
|
|
422
|
+
valueEnc: text("value_enc").notNull(), // sealAesGcm output (base64 ct.nonce)
|
|
423
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
424
|
+
.notNull()
|
|
425
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
426
|
+
// ADR-134 slice 3: the credential-store parameter this row mirrors to.
|
|
427
|
+
storeRef: text("store_ref"),
|
|
428
|
+
// The project's org, learned from an env.var.set/list frame that carries
|
|
429
|
+
// it (cloud from 2026-09-10) or from a stored value that does. It tags the
|
|
430
|
+
// mirrored parameter (`org`, `project`) so a task machine's tag-scoped
|
|
431
|
+
// lease can read it (ADR-133). Null until the cloud has said.
|
|
432
|
+
orgId: text("org_id"),
|
|
433
|
+
},
|
|
434
|
+
(t) => ({
|
|
435
|
+
pk: primaryKey({ columns: [t.projectId, t.key] }),
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
export type HostProjectEnv = typeof hostProjectEnv.$inferSelect;
|
|
440
|
+
export type NewHostProjectEnv = typeof hostProjectEnv.$inferInsert;
|
|
441
|
+
|
|
442
|
+
// ADR-153: value-free authority records live beside (not inside) the legacy
|
|
443
|
+
// encrypted value row. A delete therefore keeps its binding/head forever,
|
|
444
|
+
// while a live owned-runner head always refers to that row.
|
|
445
|
+
export const hostProjectEnvRecords = sqliteTable(
|
|
446
|
+
"host_project_env_records",
|
|
447
|
+
{
|
|
448
|
+
projectId: text("project_id").notNull(),
|
|
449
|
+
key: text("key").notNull(),
|
|
450
|
+
revision: integer("revision").notNull(),
|
|
451
|
+
generation: integer("generation").notNull(),
|
|
452
|
+
tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
|
|
453
|
+
bindingJson: text("binding_json"),
|
|
454
|
+
policyEpoch: integer("policy_epoch"),
|
|
455
|
+
listingGeneration: integer("listing_generation").notNull(),
|
|
456
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
457
|
+
.notNull()
|
|
458
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
459
|
+
},
|
|
460
|
+
(t) => ({
|
|
461
|
+
pk: primaryKey({ columns: [t.projectId, t.key] }),
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
export const hostProjectEnvProjects = sqliteTable("host_project_env_projects", {
|
|
466
|
+
projectId: text("project_id").primaryKey(),
|
|
467
|
+
recordVersion: integer("record_version").notNull().default(0),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Singleton activation, serving gate and ordered-delivery cursor. `modernWrite`
|
|
471
|
+
// is raised only inside a mutation transaction so the permanent legacy table
|
|
472
|
+
// triggers can distinguish this build from an old process still writing it.
|
|
473
|
+
export const hostProjectEnvAuthority = sqliteTable("host_project_env_authority", {
|
|
474
|
+
id: text("id").primaryKey().default("host"),
|
|
475
|
+
activated: integer("activated", { mode: "boolean" }).notNull().default(false),
|
|
476
|
+
gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(true),
|
|
477
|
+
streamId: text("stream_id"),
|
|
478
|
+
appliedSequence: integer("applied_sequence").notNull().default(0),
|
|
479
|
+
attemptId: text("attempt_id"),
|
|
480
|
+
attemptLegacyVersion: integer("attempt_legacy_version"),
|
|
481
|
+
legacyVersion: integer("legacy_version").notNull().default(0),
|
|
482
|
+
policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
|
|
483
|
+
modernWrite: integer("modern_write", { mode: "boolean" }).notNull().default(false),
|
|
484
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
485
|
+
.notNull()
|
|
486
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// A receipt contains a keyed request MAC and a value-free result. In
|
|
490
|
+
// particular, a set's plaintext is never replayable from SQLite.
|
|
491
|
+
export const hostProjectEnvDeliveryReceipts = sqliteTable(
|
|
492
|
+
"host_project_env_delivery_receipts",
|
|
493
|
+
{
|
|
494
|
+
streamId: text("stream_id").notNull(),
|
|
495
|
+
sequence: integer("sequence").notNull(),
|
|
496
|
+
requestKind: text("request_kind").notNull(),
|
|
497
|
+
requestMac: text("request_mac").notNull(),
|
|
498
|
+
ok: integer("ok", { mode: "boolean" }).notNull(),
|
|
499
|
+
stateJson: text("state_json"),
|
|
500
|
+
recordVersion: integer("record_version"),
|
|
501
|
+
code: text("code"),
|
|
502
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
503
|
+
.notNull()
|
|
504
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
505
|
+
},
|
|
506
|
+
(t) => ({ pk: primaryKey({ columns: [t.streamId, t.sequence] }) }),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
export const hostProjectEnvLegacyJournal = sqliteTable(
|
|
510
|
+
"host_project_env_legacy_journal",
|
|
511
|
+
{
|
|
512
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
513
|
+
projectId: text("project_id").notNull(),
|
|
514
|
+
key: text("key").notNull(),
|
|
515
|
+
mutation: text("mutation").notNull(),
|
|
516
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
517
|
+
.notNull()
|
|
518
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
519
|
+
},
|
|
520
|
+
(t) => ({ keyIdx: index("host_project_env_legacy_journal_key_idx").on(t.projectId, t.key) }),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// ADR-153 Uai Cloud task-machine replica. These tables are deliberately
|
|
524
|
+
// separate from the owned-runner authority above: a sidecar stages a complete
|
|
525
|
+
// task snapshot while serving nothing, then promotes it atomically. In
|
|
526
|
+
// particular, materialising a cloud value must not fire the owned authority's
|
|
527
|
+
// permanent legacy-write triggers.
|
|
528
|
+
export const hostProjectEnvReplicaAuthority = sqliteTable(
|
|
529
|
+
"host_project_env_replica_authority",
|
|
530
|
+
{
|
|
531
|
+
id: text("id").primaryKey().default("replica"),
|
|
532
|
+
activated: integer("activated", { mode: "boolean" }).notNull().default(false),
|
|
533
|
+
gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(false),
|
|
534
|
+
streamId: text("stream_id"),
|
|
535
|
+
appliedSequence: integer("applied_sequence").notNull().default(0),
|
|
536
|
+
attemptId: text("attempt_id"),
|
|
537
|
+
orgId: text("org_id"),
|
|
538
|
+
policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
|
|
539
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
540
|
+
.notNull()
|
|
541
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
542
|
+
},
|
|
543
|
+
(t) => ({
|
|
544
|
+
singleton: check("host_project_env_replica_authority_singleton", sql`${t.id} = 'replica'`),
|
|
545
|
+
booleans: check(
|
|
546
|
+
"host_project_env_replica_authority_booleans",
|
|
547
|
+
sql`${t.activated} IN (0, 1) AND ${t.gateOpen} IN (0, 1)`,
|
|
548
|
+
),
|
|
549
|
+
cursor: check("host_project_env_replica_authority_cursor", sql`${t.appliedSequence} >= 0`),
|
|
550
|
+
stream: check(
|
|
551
|
+
"host_project_env_replica_authority_stream",
|
|
552
|
+
sql`(${t.activated} = 0 AND ${t.streamId} IS NULL AND ${t.appliedSequence} = 0 AND ${t.attemptId} IS NULL AND ${t.orgId} IS NULL AND ${t.gateOpen} = 0) OR (${t.activated} = 1 AND ${t.streamId} IS NOT NULL AND ${t.orgId} IS NOT NULL)`,
|
|
553
|
+
),
|
|
554
|
+
gate: check(
|
|
555
|
+
"host_project_env_replica_authority_gate",
|
|
556
|
+
sql`${t.gateOpen} = 0 OR ${t.attemptId} IS NULL`,
|
|
557
|
+
),
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
export const hostProjectEnvReplicaProjects = sqliteTable(
|
|
562
|
+
"host_project_env_replica_projects",
|
|
563
|
+
{
|
|
564
|
+
projectId: text("project_id").primaryKey(),
|
|
565
|
+
recordVersion: integer("record_version").notNull(),
|
|
566
|
+
},
|
|
567
|
+
(t) => ({
|
|
568
|
+
version: check("host_project_env_replica_projects_version", sql`${t.recordVersion} >= 0`),
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const replicaRecordColumns = () => ({
|
|
573
|
+
projectId: text("project_id").notNull(),
|
|
574
|
+
key: text("key").notNull(),
|
|
575
|
+
revision: integer("revision").notNull(),
|
|
576
|
+
generation: integer("generation").notNull(),
|
|
577
|
+
tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
|
|
578
|
+
valueRefJson: text("value_ref_json"),
|
|
579
|
+
bindingJson: text("binding_json"),
|
|
580
|
+
policyEpoch: integer("policy_epoch"),
|
|
581
|
+
listingGeneration: integer("listing_generation").notNull(),
|
|
582
|
+
materialization: text("materialization").notNull(),
|
|
583
|
+
materializationReason: text("materialization_reason"),
|
|
584
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
585
|
+
.notNull()
|
|
586
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
export const hostProjectEnvReplicaRecords = sqliteTable(
|
|
590
|
+
"host_project_env_replica_records",
|
|
591
|
+
replicaRecordColumns(),
|
|
592
|
+
(t) => ({
|
|
593
|
+
pk: primaryKey({ columns: [t.projectId, t.key] }),
|
|
594
|
+
head: check(
|
|
595
|
+
"host_project_env_replica_records_head",
|
|
596
|
+
sql`(${t.tombstone} = 0 AND ${t.revision} >= 1 AND ${t.generation} >= 1 AND ${t.valueRefJson} IS NOT NULL) OR (${t.tombstone} = 1 AND ${t.revision} = 1 AND ${t.generation} = 0 AND ${t.valueRefJson} IS NULL) OR (${t.tombstone} = 1 AND ${t.revision} >= 2 AND ${t.generation} >= 2 AND ${t.valueRefJson} IS NULL)`,
|
|
597
|
+
),
|
|
598
|
+
binding: check(
|
|
599
|
+
"host_project_env_replica_records_binding",
|
|
600
|
+
sql`(${t.generation} = 0 AND ${t.bindingJson} IS NULL AND ${t.policyEpoch} IS NULL AND ${t.listingGeneration} >= 2 AND (${t.listingGeneration} % 2) = 0) OR (${t.generation} > 0 AND ${t.policyEpoch} >= 1)`,
|
|
601
|
+
),
|
|
602
|
+
listing: check(
|
|
603
|
+
"host_project_env_replica_records_listing",
|
|
604
|
+
sql`${t.listingGeneration} >= 0`,
|
|
605
|
+
),
|
|
606
|
+
boolean: check(
|
|
607
|
+
"host_project_env_replica_records_boolean",
|
|
608
|
+
sql`${t.tombstone} IN (0, 1)`,
|
|
609
|
+
),
|
|
610
|
+
materialization: check(
|
|
611
|
+
"host_project_env_replica_records_materialization",
|
|
612
|
+
sql`(${t.materialization} = 'installed' AND ${t.tombstone} = 0 AND ${t.materializationReason} IS NULL) OR (${t.materialization} IN ('withheld', 'unavailable') AND ${t.materializationReason} IS NOT NULL)`,
|
|
613
|
+
),
|
|
614
|
+
}),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
export const hostProjectEnvReplicaValues = sqliteTable(
|
|
618
|
+
"host_project_env_replica_values",
|
|
619
|
+
{
|
|
620
|
+
projectId: text("project_id").notNull(),
|
|
621
|
+
key: text("key").notNull(),
|
|
622
|
+
revision: integer("revision").notNull(),
|
|
623
|
+
generation: integer("generation").notNull(),
|
|
624
|
+
valueRefJson: text("value_ref_json").notNull(),
|
|
625
|
+
valueEnc: text("value_enc").notNull(),
|
|
626
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
627
|
+
.notNull()
|
|
628
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
629
|
+
},
|
|
630
|
+
(t) => ({
|
|
631
|
+
pk: primaryKey({ columns: [t.projectId, t.key] }),
|
|
632
|
+
head: check(
|
|
633
|
+
"host_project_env_replica_values_head",
|
|
634
|
+
sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
|
|
635
|
+
),
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
export const hostProjectEnvReplicaStageProjects = sqliteTable(
|
|
640
|
+
"host_project_env_replica_stage_projects",
|
|
641
|
+
{
|
|
642
|
+
attemptId: text("attempt_id").notNull(),
|
|
643
|
+
projectId: text("project_id").notNull(),
|
|
644
|
+
recordVersion: integer("record_version"),
|
|
645
|
+
lastKey: text("last_key"),
|
|
646
|
+
complete: integer("complete", { mode: "boolean" }).notNull().default(false),
|
|
647
|
+
},
|
|
648
|
+
(t) => ({
|
|
649
|
+
pk: primaryKey({ columns: [t.attemptId, t.projectId] }),
|
|
650
|
+
version: check(
|
|
651
|
+
"host_project_env_replica_stage_projects_version",
|
|
652
|
+
sql`${t.recordVersion} IS NULL OR ${t.recordVersion} >= 0`,
|
|
653
|
+
),
|
|
654
|
+
completeBoolean: check(
|
|
655
|
+
"host_project_env_replica_stage_projects_complete",
|
|
656
|
+
sql`${t.complete} IN (0, 1)`,
|
|
657
|
+
),
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
export const hostProjectEnvReplicaStageRecords = sqliteTable(
|
|
662
|
+
"host_project_env_replica_stage_records",
|
|
663
|
+
{
|
|
664
|
+
attemptId: text("attempt_id").notNull(),
|
|
665
|
+
...replicaRecordColumns(),
|
|
666
|
+
present: integer("present", { mode: "boolean" }).notNull().default(false),
|
|
667
|
+
},
|
|
668
|
+
(t) => ({
|
|
669
|
+
pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
|
|
670
|
+
head: check(
|
|
671
|
+
"host_project_env_replica_stage_records_head",
|
|
672
|
+
sql`(${t.tombstone} = 0 AND ${t.revision} >= 1 AND ${t.generation} >= 1 AND ${t.valueRefJson} IS NOT NULL) OR (${t.tombstone} = 1 AND ${t.revision} = 1 AND ${t.generation} = 0 AND ${t.valueRefJson} IS NULL) OR (${t.tombstone} = 1 AND ${t.revision} >= 2 AND ${t.generation} >= 2 AND ${t.valueRefJson} IS NULL)`,
|
|
673
|
+
),
|
|
674
|
+
binding: check(
|
|
675
|
+
"host_project_env_replica_stage_records_binding",
|
|
676
|
+
sql`(${t.generation} = 0 AND ${t.bindingJson} IS NULL AND ${t.policyEpoch} IS NULL AND ${t.listingGeneration} >= 2 AND (${t.listingGeneration} % 2) = 0) OR (${t.generation} > 0 AND ${t.policyEpoch} >= 1)`,
|
|
677
|
+
),
|
|
678
|
+
listing: check(
|
|
679
|
+
"host_project_env_replica_stage_records_listing",
|
|
680
|
+
sql`${t.listingGeneration} >= 0`,
|
|
681
|
+
),
|
|
682
|
+
boolean: check(
|
|
683
|
+
"host_project_env_replica_stage_records_boolean",
|
|
684
|
+
sql`${t.tombstone} IN (0, 1) AND ${t.present} IN (0, 1)`,
|
|
685
|
+
),
|
|
686
|
+
materialization: check(
|
|
687
|
+
"host_project_env_replica_stage_records_materialization",
|
|
688
|
+
sql`(${t.materialization} = 'installed' AND ${t.tombstone} = 0 AND ${t.materializationReason} IS NULL) OR (${t.materialization} IN ('withheld', 'unavailable') AND ${t.materializationReason} IS NOT NULL)`,
|
|
689
|
+
),
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
export const hostProjectEnvReplicaStageValues = sqliteTable(
|
|
694
|
+
"host_project_env_replica_stage_values",
|
|
695
|
+
{
|
|
696
|
+
attemptId: text("attempt_id").notNull(),
|
|
697
|
+
projectId: text("project_id").notNull(),
|
|
698
|
+
key: text("key").notNull(),
|
|
699
|
+
revision: integer("revision").notNull(),
|
|
700
|
+
generation: integer("generation").notNull(),
|
|
701
|
+
valueRefJson: text("value_ref_json").notNull(),
|
|
702
|
+
valueEnc: text("value_enc").notNull(),
|
|
703
|
+
updatedAt: integer("updated_at", { mode: "number" })
|
|
704
|
+
.notNull()
|
|
705
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
706
|
+
},
|
|
707
|
+
(t) => ({
|
|
708
|
+
pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
|
|
709
|
+
head: check(
|
|
710
|
+
"host_project_env_replica_stage_values_head",
|
|
711
|
+
sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
|
|
712
|
+
),
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
export const hostProjectEnvReplicaReceipts = sqliteTable(
|
|
717
|
+
"host_project_env_replica_receipts",
|
|
718
|
+
{
|
|
719
|
+
streamId: text("stream_id").notNull(),
|
|
720
|
+
sequence: integer("sequence").notNull(),
|
|
721
|
+
requestMac: text("request_mac").notNull(),
|
|
722
|
+
resultJson: text("result_json"),
|
|
723
|
+
recordVersion: integer("record_version"),
|
|
724
|
+
ok: integer("ok", { mode: "boolean" }).notNull(),
|
|
725
|
+
code: text("code"),
|
|
726
|
+
createdAt: integer("created_at", { mode: "number" })
|
|
727
|
+
.notNull()
|
|
728
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
729
|
+
},
|
|
730
|
+
(t) => ({
|
|
731
|
+
pk: primaryKey({ columns: [t.streamId, t.sequence] }),
|
|
732
|
+
sequenceCheck: check(
|
|
733
|
+
"host_project_env_replica_receipts_sequence",
|
|
734
|
+
sql`${t.sequence} >= 1`,
|
|
735
|
+
),
|
|
736
|
+
booleanCheck: check(
|
|
737
|
+
"host_project_env_replica_receipts_boolean",
|
|
738
|
+
sql`${t.ok} IN (0, 1)`,
|
|
739
|
+
),
|
|
740
|
+
outcome: check(
|
|
741
|
+
"host_project_env_replica_receipts_outcome",
|
|
742
|
+
sql`(${t.ok} = 1 AND ${t.resultJson} IS NOT NULL AND ${t.recordVersion} IS NOT NULL AND ${t.code} IS NULL) OR (${t.ok} = 0 AND ${t.resultJson} IS NULL AND ${t.recordVersion} IS NULL AND ${t.code} IS NOT NULL)`,
|
|
743
|
+
),
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
// User-authed MCP connections (ADR-057). One row per cloud connection id; all
|
|
748
|
+
// secret material (static header, OAuth client secret, PKCE verifier, tokens)
|
|
749
|
+
// is sealed with the host master key in single-column `ct.nonce` base64 form
|
|
750
|
+
// (same packing as host_project_env). The cloud only ever holds the non-secret
|
|
751
|
+
// metadata; acks never echo secrets back.
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// host_agent_sessions — ADR-061 durable agent sessions. One row per
|
|
754
|
+
// (task, agent): where the live in-container runner's session dir is and how
|
|
755
|
+
// far the host has consumed its outbox. The byte offset is what turns a host
|
|
756
|
+
// restart into "resume tailing" instead of "kill and respawn"; attachability
|
|
757
|
+
// itself is judged live off the runner's heartbeat file, not this row.
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
export const hostAgentSessions = sqliteTable(
|
|
761
|
+
"host_agent_sessions",
|
|
762
|
+
{
|
|
763
|
+
taskId: text("task_id").notNull(),
|
|
764
|
+
agentId: text("agent_id").notNull(),
|
|
765
|
+
// Host-side absolute path of the session dir (on the workspace mount,
|
|
766
|
+
// identical in-container — task-up bind-mounts the workspace at its own
|
|
767
|
+
// host path).
|
|
768
|
+
sessionDir: text("session_dir").notNull(),
|
|
769
|
+
containerName: text("container_name").notNull(),
|
|
770
|
+
kind: text("kind").notNull(), // "claude" | "codex" | ...
|
|
771
|
+
// Attach only when the process was launched under the same host-owned
|
|
772
|
+
// policy generation. Null preserves legacy attach behavior for ordinary
|
|
773
|
+
// agents; Secretary transitions use an explicit key.
|
|
774
|
+
attachCompatibilityKey: text("attach_compatibility_key"),
|
|
775
|
+
outboxOffset: integer("outbox_offset", { mode: "number" })
|
|
776
|
+
.notNull()
|
|
777
|
+
.default(0),
|
|
778
|
+
status: text("status").notNull().default("running"), // running|closed
|
|
779
|
+
// ADR-078: Codex thread id (from thread/start). Persisted so a respawned
|
|
780
|
+
// app-server after a host restart resumes the SAME thread (thread/resume)
|
|
781
|
+
// instead of losing the conversation. Null for non-codex kinds / pre-resume.
|
|
782
|
+
codexThreadId: text("codex_thread_id"),
|
|
783
|
+
createdAt: integer("created_at", { mode: "number" }).notNull(),
|
|
784
|
+
updatedAt: integer("updated_at", { mode: "number" }).notNull(),
|
|
785
|
+
},
|
|
786
|
+
(t) => ({
|
|
787
|
+
pk: primaryKey({ columns: [t.taskId, t.agentId] }),
|
|
788
|
+
}),
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
export type HostAgentSession = typeof hostAgentSessions.$inferSelect;
|
|
792
|
+
|
|
793
|
+
export const mcpConnections = sqliteTable("host_mcp_connections", {
|
|
794
|
+
id: text("id").primaryKey(), // cloud uai_mcp_connections id
|
|
795
|
+
userId: text("user_id").notNull(),
|
|
796
|
+
url: text("url").notNull(),
|
|
797
|
+
// "oauth" | "token" | "none"
|
|
798
|
+
authKind: text("auth_kind").notNull(),
|
|
799
|
+
// OAuth machinery discovered at probe time.
|
|
800
|
+
tokenEndpoint: text("token_endpoint"),
|
|
801
|
+
redirectUri: text("redirect_uri"),
|
|
802
|
+
clientId: text("client_id"),
|
|
803
|
+
clientSecretEnc: text("client_secret_enc"),
|
|
804
|
+
// PKCE verifier held between probe and oauth.complete.
|
|
805
|
+
pkceVerifierEnc: text("pkce_verifier_enc"),
|
|
806
|
+
// Sealed JSON: {"headerName","headerValue"} (token kind) or
|
|
807
|
+
// {"accessToken","refreshToken","expiresAt"} (oauth kind). Null for "none".
|
|
808
|
+
secretEnc: text("secret_enc"),
|
|
809
|
+
status: text("status").notNull().default("pending"), // pending|connected
|
|
810
|
+
scopes: text("scopes"),
|
|
811
|
+
updatedAt: integer("updated_at", { mode: "number" }).notNull(),
|
|
812
|
+
// ADR-134 slice 2: the credential-store parameter this row mirrors to.
|
|
813
|
+
storeRef: text("store_ref"),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
export type McpConnection = typeof mcpConnections.$inferSelect;
|
|
817
|
+
|
|
818
|
+
// Extra (non-default) engine accounts (ADR-076). One row per additional
|
|
819
|
+
// credential for an engine kind, beyond the single legacy slot (.env.local /
|
|
820
|
+
// ~/.codex / ~/.local/share/opencode) which is synthesized as the "default"
|
|
821
|
+
// account and NOT stored here. Secret material is sealed with the host master
|
|
822
|
+
// key (same secret-blind pattern as host_github_tokens — ADR-015):
|
|
823
|
+
// - authKind "env": secretEnc = sealed JSON of env vars, e.g.
|
|
824
|
+
// {"CLAUDE_CODE_OAUTH_TOKEN":"sk-ant-oat…"}.
|
|
825
|
+
// - authKind "config-dir": configDir = the host dir holding this account's
|
|
826
|
+
// auth files (an isolated ~/.codex-acct-<id> etc.);
|
|
827
|
+
// secretEnc is null (the dir IS the secret at rest).
|
|
828
|
+
// Rotation state (cooldown / last-used) is in-memory only, not persisted.
|
|
829
|
+
export const engineAccounts = sqliteTable("host_engine_accounts", {
|
|
830
|
+
id: text("id").primaryKey(), // ulid
|
|
831
|
+
kind: text("kind").notNull(), // "claude" | "codex" | "opencode" | …
|
|
832
|
+
label: text("label").notNull(), // display, e.g. "work"
|
|
833
|
+
authKind: text("auth_kind").notNull(), // "env" | "config-dir"
|
|
834
|
+
secretEnc: text("secret_enc"), // sealed JSON (env kind); null for config-dir
|
|
835
|
+
configDir: text("config_dir"), // host dir (config-dir kind); null for env
|
|
836
|
+
// ADR-131 D3: "host" (owner-level, default) or "org:<orgId>" (a workspace's
|
|
837
|
+
// account on a shared host). Resolution is by exact scope.
|
|
838
|
+
scope: text("scope").notNull().default("host"),
|
|
839
|
+
// Manage-v1: priority within (scope, kind) — tasks pick the first healthy
|
|
840
|
+
// account in ascending position; ties break on created_at.
|
|
841
|
+
position: integer("position", { mode: "number" }).notNull().default(0),
|
|
842
|
+
// ADR-134: the credential-store parameter this row mirrors to (null on an
|
|
843
|
+
// owned host, or before the first successful mirror). The store is the
|
|
844
|
+
// truth when configured; this row is the local cache.
|
|
845
|
+
storeRef: text("store_ref"),
|
|
846
|
+
createdAt: integer("created_at", { mode: "number" }).notNull(),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
export type EngineAccountRow = typeof engineAccounts.$inferSelect;
|
|
850
|
+
export type NewEngineAccountRow = typeof engineAccounts.$inferInsert;
|