@runuai/host 0.9.148 → 0.9.150

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.
Files changed (93) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +408 -81
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -15
@@ -0,0 +1,9 @@
1
+ -- ADR-153: durable rollout fence for per-command project env delivery.
2
+ -- Existing/null rows are interpreted as `baked`; new task-up writes `loader`
3
+ -- before creating runtime resources, so recovery never launches a binary that
4
+ -- is absent from an older container image.
5
+ ALTER TABLE `uai_host_tasks` ADD `env_mode` text;
6
+ --> statement-breakpoint
7
+ -- Value-free declaration/literal metadata needed to rebuild the resolver
8
+ -- after runner restart, before the cloud re-sends task-up.
9
+ ALTER TABLE `uai_host_tasks` ADD `project_env_spec` text;
@@ -0,0 +1,147 @@
1
+ -- ADR-153: the owned runner's durable, value-free project-env authority.
2
+ --
3
+ -- This migration deliberately does not backfill host_project_env. An older
4
+ -- runtime may keep writing that table after the migration lands but before
5
+ -- the first bindings-v1 reconciliation. After its caller takes the capability
6
+ -- floor, the first begin transaction snapshots the then-current rows and
7
+ -- activates the permanent legacy-write journal below.
8
+ CREATE TABLE `host_project_env_records` (
9
+ `project_id` text NOT NULL,
10
+ `key` text NOT NULL,
11
+ `revision` integer NOT NULL,
12
+ `generation` integer NOT NULL,
13
+ `tombstone` integer NOT NULL,
14
+ `binding_json` text,
15
+ `policy_epoch` integer,
16
+ `listing_generation` integer NOT NULL,
17
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
18
+ PRIMARY KEY(`project_id`, `key`),
19
+ CONSTRAINT `host_project_env_records_head` CHECK (
20
+ (`tombstone` = 0 AND `revision` >= 1 AND `generation` >= 1) OR
21
+ (`tombstone` = 1 AND `revision` = 1 AND `generation` = 0) OR
22
+ (`tombstone` = 1 AND `revision` >= 2 AND `generation` >= 2)
23
+ ),
24
+ CONSTRAINT `host_project_env_records_binding` CHECK (
25
+ (`generation` = 0 AND `binding_json` IS NULL AND `policy_epoch` IS NULL
26
+ AND `listing_generation` >= 2 AND (`listing_generation` % 2) = 0) OR
27
+ (`generation` > 0 AND `policy_epoch` >= 1)
28
+ ),
29
+ CONSTRAINT `host_project_env_records_listing` CHECK (`listing_generation` >= 0),
30
+ CONSTRAINT `host_project_env_records_boolean` CHECK (`tombstone` IN (0, 1))
31
+ );
32
+ --> statement-breakpoint
33
+ CREATE TABLE `host_project_env_projects` (
34
+ `project_id` text PRIMARY KEY NOT NULL,
35
+ `record_version` integer NOT NULL DEFAULT 0,
36
+ CONSTRAINT `host_project_env_projects_version` CHECK (`record_version` >= 0)
37
+ );
38
+ --> statement-breakpoint
39
+ CREATE TABLE `host_project_env_authority` (
40
+ `id` text PRIMARY KEY NOT NULL DEFAULT 'host',
41
+ `activated` integer NOT NULL DEFAULT 0,
42
+ `gate_open` integer NOT NULL DEFAULT 1,
43
+ `stream_id` text,
44
+ `applied_sequence` integer NOT NULL DEFAULT 0,
45
+ `attempt_id` text,
46
+ `attempt_legacy_version` integer,
47
+ `legacy_version` integer NOT NULL DEFAULT 0,
48
+ `policy_epochs_json` text NOT NULL DEFAULT '[1]',
49
+ `modern_write` integer NOT NULL DEFAULT 0,
50
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
51
+ CONSTRAINT `host_project_env_authority_singleton` CHECK (`id` = 'host'),
52
+ CONSTRAINT `host_project_env_authority_booleans` CHECK (
53
+ `activated` IN (0, 1) AND `gate_open` IN (0, 1) AND `modern_write` IN (0, 1)
54
+ ),
55
+ CONSTRAINT `host_project_env_authority_cursor` CHECK (
56
+ `applied_sequence` >= 0 AND `legacy_version` >= 0 AND
57
+ (`attempt_legacy_version` IS NULL OR `attempt_legacy_version` >= 0)
58
+ ),
59
+ CONSTRAINT `host_project_env_authority_stream` CHECK (
60
+ (`activated` = 0 AND `stream_id` IS NULL AND `applied_sequence` = 0 AND `attempt_id` IS NULL) OR
61
+ (`activated` = 1 AND `stream_id` IS NOT NULL)
62
+ ),
63
+ CONSTRAINT `host_project_env_authority_attempt` CHECK (
64
+ (`attempt_id` IS NULL AND `attempt_legacy_version` IS NULL) OR
65
+ (`attempt_id` IS NOT NULL AND `attempt_legacy_version` IS NOT NULL AND `gate_open` = 0)
66
+ ),
67
+ CONSTRAINT `host_project_env_authority_open` CHECK (`gate_open` = 0 OR `attempt_id` IS NULL)
68
+ );
69
+ --> statement-breakpoint
70
+ INSERT INTO `host_project_env_authority` (`id`) VALUES ('host');
71
+ --> statement-breakpoint
72
+ CREATE TABLE `host_project_env_delivery_receipts` (
73
+ `stream_id` text NOT NULL,
74
+ `sequence` integer NOT NULL,
75
+ `request_kind` text NOT NULL,
76
+ `request_mac` text NOT NULL,
77
+ `ok` integer NOT NULL,
78
+ `state_json` text,
79
+ `record_version` integer,
80
+ `code` text,
81
+ `created_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
82
+ PRIMARY KEY(`stream_id`, `sequence`),
83
+ CONSTRAINT `host_project_env_delivery_receipts_sequence` CHECK (`sequence` >= 1),
84
+ CONSTRAINT `host_project_env_delivery_receipts_boolean` CHECK (`ok` IN (0, 1)),
85
+ CONSTRAINT `host_project_env_delivery_receipts_outcome` CHECK (
86
+ (`ok` = 1 AND `code` IS NULL) OR (`ok` = 0 AND `code` IS NOT NULL)
87
+ ),
88
+ CONSTRAINT `host_project_env_delivery_receipts_version` CHECK (
89
+ `record_version` IS NULL OR `record_version` >= 0
90
+ )
91
+ );
92
+ --> statement-breakpoint
93
+ CREATE TABLE `host_project_env_legacy_journal` (
94
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
95
+ `project_id` text NOT NULL,
96
+ `key` text NOT NULL,
97
+ `mutation` text NOT NULL,
98
+ `created_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
99
+ CONSTRAINT `host_project_env_legacy_journal_mutation` CHECK (`mutation` IN ('set', 'delete'))
100
+ );
101
+ --> statement-breakpoint
102
+ CREATE INDEX `host_project_env_legacy_journal_key_idx`
103
+ ON `host_project_env_legacy_journal` (`project_id`, `key`);
104
+ --> statement-breakpoint
105
+ -- These triggers are inert until the first modern reconciliation activates
106
+ -- the authority. Afterwards, any old process that changes plaintext storage
107
+ -- leaves durable evidence and closes the serving gate. Metadata-only updates
108
+ -- do neither. Modern writes raise modern_write in their enclosing transaction.
109
+ CREATE TRIGGER `host_project_env_legacy_insert`
110
+ AFTER INSERT ON `host_project_env`
111
+ WHEN (SELECT `activated` = 1 AND `modern_write` = 0
112
+ FROM `host_project_env_authority` WHERE `id` = 'host')
113
+ BEGIN
114
+ INSERT INTO `host_project_env_legacy_journal` (`project_id`, `key`, `mutation`)
115
+ VALUES (NEW.`project_id`, NEW.`key`, 'set');
116
+ UPDATE `host_project_env_authority`
117
+ SET `gate_open` = 0, `legacy_version` = `legacy_version` + 1,
118
+ `updated_at` = unixepoch() * 1000
119
+ WHERE `id` = 'host';
120
+ END;
121
+ --> statement-breakpoint
122
+ CREATE TRIGGER `host_project_env_legacy_update`
123
+ AFTER UPDATE OF `value_enc` ON `host_project_env`
124
+ WHEN OLD.`value_enc` IS NOT NEW.`value_enc`
125
+ AND (SELECT `activated` = 1 AND `modern_write` = 0
126
+ FROM `host_project_env_authority` WHERE `id` = 'host')
127
+ BEGIN
128
+ INSERT INTO `host_project_env_legacy_journal` (`project_id`, `key`, `mutation`)
129
+ VALUES (NEW.`project_id`, NEW.`key`, 'set');
130
+ UPDATE `host_project_env_authority`
131
+ SET `gate_open` = 0, `legacy_version` = `legacy_version` + 1,
132
+ `updated_at` = unixepoch() * 1000
133
+ WHERE `id` = 'host';
134
+ END;
135
+ --> statement-breakpoint
136
+ CREATE TRIGGER `host_project_env_legacy_delete`
137
+ AFTER DELETE ON `host_project_env`
138
+ WHEN (SELECT `activated` = 1 AND `modern_write` = 0
139
+ FROM `host_project_env_authority` WHERE `id` = 'host')
140
+ BEGIN
141
+ INSERT INTO `host_project_env_legacy_journal` (`project_id`, `key`, `mutation`)
142
+ VALUES (OLD.`project_id`, OLD.`key`, 'delete');
143
+ UPDATE `host_project_env_authority`
144
+ SET `gate_open` = 0, `legacy_version` = `legacy_version` + 1,
145
+ `updated_at` = unixepoch() * 1000
146
+ WHERE `id` = 'host';
147
+ END;
@@ -0,0 +1,150 @@
1
+ -- ADR-153: durable Uai Cloud task-machine project-env replica.
2
+ --
3
+ -- The live and staged views are separate from both one another and the owned
4
+ -- runner authority. A page can therefore materialise values without making
5
+ -- them visible, and commit can replace the complete task view atomically.
6
+ CREATE TABLE `host_project_env_replica_authority` (
7
+ `id` text PRIMARY KEY NOT NULL DEFAULT 'replica',
8
+ `activated` integer NOT NULL DEFAULT 0,
9
+ `gate_open` integer NOT NULL DEFAULT 0,
10
+ `stream_id` text,
11
+ `applied_sequence` integer NOT NULL DEFAULT 0,
12
+ `attempt_id` text,
13
+ `org_id` text,
14
+ `policy_epochs_json` text NOT NULL DEFAULT '[1]',
15
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
16
+ CONSTRAINT `host_project_env_replica_authority_singleton` CHECK (`id` = 'replica'),
17
+ CONSTRAINT `host_project_env_replica_authority_booleans` CHECK (`activated` IN (0, 1) AND `gate_open` IN (0, 1)),
18
+ CONSTRAINT `host_project_env_replica_authority_cursor` CHECK (`applied_sequence` >= 0),
19
+ CONSTRAINT `host_project_env_replica_authority_stream` CHECK (
20
+ (`activated` = 0 AND `stream_id` IS NULL AND `applied_sequence` = 0 AND `attempt_id` IS NULL AND `org_id` IS NULL AND `gate_open` = 0) OR
21
+ (`activated` = 1 AND `stream_id` IS NOT NULL AND `org_id` IS NOT NULL)
22
+ ),
23
+ CONSTRAINT `host_project_env_replica_authority_gate` CHECK (`gate_open` = 0 OR `attempt_id` IS NULL)
24
+ );
25
+ --> statement-breakpoint
26
+ INSERT INTO `host_project_env_replica_authority` (`id`) VALUES ('replica');
27
+ --> statement-breakpoint
28
+ CREATE TABLE `host_project_env_replica_projects` (
29
+ `project_id` text PRIMARY KEY NOT NULL,
30
+ `record_version` integer NOT NULL,
31
+ CONSTRAINT `host_project_env_replica_projects_version` CHECK (`record_version` >= 0)
32
+ );
33
+ --> statement-breakpoint
34
+ CREATE TABLE `host_project_env_replica_records` (
35
+ `project_id` text NOT NULL,
36
+ `key` text NOT NULL,
37
+ `revision` integer NOT NULL,
38
+ `generation` integer NOT NULL,
39
+ `tombstone` integer NOT NULL,
40
+ `value_ref_json` text,
41
+ `binding_json` text,
42
+ `policy_epoch` integer,
43
+ `listing_generation` integer NOT NULL,
44
+ `materialization` text NOT NULL,
45
+ `materialization_reason` text,
46
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
47
+ PRIMARY KEY(`project_id`, `key`),
48
+ CONSTRAINT `host_project_env_replica_records_head` CHECK (
49
+ (`tombstone` = 0 AND `revision` >= 1 AND `generation` >= 1 AND `value_ref_json` IS NOT NULL) OR
50
+ (`tombstone` = 1 AND `revision` = 1 AND `generation` = 0 AND `value_ref_json` IS NULL) OR
51
+ (`tombstone` = 1 AND `revision` >= 2 AND `generation` >= 2 AND `value_ref_json` IS NULL)
52
+ ),
53
+ CONSTRAINT `host_project_env_replica_records_binding` CHECK (
54
+ (`generation` = 0 AND `binding_json` IS NULL AND `policy_epoch` IS NULL AND `listing_generation` >= 2 AND (`listing_generation` % 2) = 0) OR
55
+ (`generation` > 0 AND `policy_epoch` >= 1)
56
+ ),
57
+ CONSTRAINT `host_project_env_replica_records_listing` CHECK (`listing_generation` >= 0),
58
+ CONSTRAINT `host_project_env_replica_records_boolean` CHECK (`tombstone` IN (0, 1)),
59
+ CONSTRAINT `host_project_env_replica_records_materialization` CHECK (
60
+ (`materialization` = 'installed' AND `tombstone` = 0 AND `materialization_reason` IS NULL) OR
61
+ (`materialization` IN ('withheld', 'unavailable') AND `materialization_reason` IS NOT NULL)
62
+ )
63
+ );
64
+ --> statement-breakpoint
65
+ CREATE TABLE `host_project_env_replica_values` (
66
+ `project_id` text NOT NULL,
67
+ `key` text NOT NULL,
68
+ `revision` integer NOT NULL,
69
+ `generation` integer NOT NULL,
70
+ `value_ref_json` text NOT NULL,
71
+ `value_enc` text NOT NULL,
72
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
73
+ PRIMARY KEY(`project_id`, `key`),
74
+ CONSTRAINT `host_project_env_replica_values_head` CHECK (`revision` >= 1 AND `generation` >= 1)
75
+ );
76
+ --> statement-breakpoint
77
+ CREATE TABLE `host_project_env_replica_stage_projects` (
78
+ `attempt_id` text NOT NULL,
79
+ `project_id` text NOT NULL,
80
+ `record_version` integer,
81
+ `last_key` text,
82
+ `complete` integer NOT NULL DEFAULT 0,
83
+ PRIMARY KEY(`attempt_id`, `project_id`),
84
+ CONSTRAINT `host_project_env_replica_stage_projects_version` CHECK (`record_version` IS NULL OR `record_version` >= 0),
85
+ CONSTRAINT `host_project_env_replica_stage_projects_complete` CHECK (`complete` IN (0, 1))
86
+ );
87
+ --> statement-breakpoint
88
+ CREATE TABLE `host_project_env_replica_stage_records` (
89
+ `attempt_id` text NOT NULL,
90
+ `project_id` text NOT NULL,
91
+ `key` text NOT NULL,
92
+ `revision` integer NOT NULL,
93
+ `generation` integer NOT NULL,
94
+ `tombstone` integer NOT NULL,
95
+ `value_ref_json` text,
96
+ `binding_json` text,
97
+ `policy_epoch` integer,
98
+ `listing_generation` integer NOT NULL,
99
+ `materialization` text NOT NULL,
100
+ `materialization_reason` text,
101
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
102
+ `present` integer NOT NULL DEFAULT 0,
103
+ PRIMARY KEY(`attempt_id`, `project_id`, `key`),
104
+ CONSTRAINT `host_project_env_replica_stage_records_head` CHECK (
105
+ (`tombstone` = 0 AND `revision` >= 1 AND `generation` >= 1 AND `value_ref_json` IS NOT NULL) OR
106
+ (`tombstone` = 1 AND `revision` = 1 AND `generation` = 0 AND `value_ref_json` IS NULL) OR
107
+ (`tombstone` = 1 AND `revision` >= 2 AND `generation` >= 2 AND `value_ref_json` IS NULL)
108
+ ),
109
+ CONSTRAINT `host_project_env_replica_stage_records_binding` CHECK (
110
+ (`generation` = 0 AND `binding_json` IS NULL AND `policy_epoch` IS NULL AND `listing_generation` >= 2 AND (`listing_generation` % 2) = 0) OR
111
+ (`generation` > 0 AND `policy_epoch` >= 1)
112
+ ),
113
+ CONSTRAINT `host_project_env_replica_stage_records_listing` CHECK (`listing_generation` >= 0),
114
+ CONSTRAINT `host_project_env_replica_stage_records_boolean` CHECK (`tombstone` IN (0, 1) AND `present` IN (0, 1)),
115
+ CONSTRAINT `host_project_env_replica_stage_records_materialization` CHECK (
116
+ (`materialization` = 'installed' AND `tombstone` = 0 AND `materialization_reason` IS NULL) OR
117
+ (`materialization` IN ('withheld', 'unavailable') AND `materialization_reason` IS NOT NULL)
118
+ )
119
+ );
120
+ --> statement-breakpoint
121
+ CREATE TABLE `host_project_env_replica_stage_values` (
122
+ `attempt_id` text NOT NULL,
123
+ `project_id` text NOT NULL,
124
+ `key` text NOT NULL,
125
+ `revision` integer NOT NULL,
126
+ `generation` integer NOT NULL,
127
+ `value_ref_json` text NOT NULL,
128
+ `value_enc` text NOT NULL,
129
+ `updated_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
130
+ PRIMARY KEY(`attempt_id`, `project_id`, `key`),
131
+ CONSTRAINT `host_project_env_replica_stage_values_head` CHECK (`revision` >= 1 AND `generation` >= 1)
132
+ );
133
+ --> statement-breakpoint
134
+ CREATE TABLE `host_project_env_replica_receipts` (
135
+ `stream_id` text NOT NULL,
136
+ `sequence` integer NOT NULL,
137
+ `request_mac` text NOT NULL,
138
+ `result_json` text,
139
+ `record_version` integer,
140
+ `ok` integer NOT NULL,
141
+ `code` text,
142
+ `created_at` integer NOT NULL DEFAULT (unixepoch() * 1000),
143
+ PRIMARY KEY(`stream_id`, `sequence`),
144
+ CONSTRAINT `host_project_env_replica_receipts_sequence` CHECK (`sequence` >= 1),
145
+ CONSTRAINT `host_project_env_replica_receipts_boolean` CHECK (`ok` IN (0, 1)),
146
+ CONSTRAINT `host_project_env_replica_receipts_outcome` CHECK (
147
+ (`ok` = 1 AND `result_json` IS NOT NULL AND `record_version` IS NOT NULL AND `code` IS NULL) OR
148
+ (`ok` = 0 AND `result_json` IS NULL AND `record_version` IS NULL AND `code` IS NOT NULL)
149
+ )
150
+ );
@@ -169,6 +169,27 @@
169
169
  "when": 1788900000005,
170
170
  "tag": "0023_project_env_org",
171
171
  "breakpoints": true
172
+ },
173
+ {
174
+ "idx": 24,
175
+ "version": "6",
176
+ "when": 1788900000006,
177
+ "tag": "0024_task_env_mode",
178
+ "breakpoints": true
179
+ },
180
+ {
181
+ "idx": 25,
182
+ "version": "6",
183
+ "when": 1788900000007,
184
+ "tag": "0025_project_env_authority",
185
+ "breakpoints": true
186
+ },
187
+ {
188
+ "idx": 26,
189
+ "version": "6",
190
+ "when": 1788900000008,
191
+ "tag": "0026_project_env_replica",
192
+ "breakpoints": true
172
193
  }
173
194
  ]
174
195
  }
package/db/schema.ts CHANGED
@@ -29,6 +29,11 @@ export const hostTasks = sqliteTable(
29
29
  // Docker Compose only when their legacy runtime identity is complete.
30
30
  environmentProvider: text("environment_provider"),
31
31
  environmentLocator: text("environment_locator"),
32
+ // ADR-153 rollout fence. Null rows predate the loader and mean `baked`.
33
+ envMode: text("env_mode"),
34
+ // Value-free resolver metadata for rebuilding the project-env gateway
35
+ // after a runner restart, before the cloud re-sends task-up.
36
+ projectEnvSpec: text("project_env_spec"),
32
37
  statusMirror: text("status_mirror"),
33
38
  // Owner + project slugs — received in TaskLaunchInput at task-up and
34
39
  // persisted here so the local UI (ADR-028) can show "who/what" without a
@@ -37,8 +42,8 @@ export const hostTasks = sqliteTable(
37
42
  ownerEmail: text("owner_email"),
38
43
  ownerName: text("owner_name"),
39
44
  projectSlugs: text("project_slugs").notNull().default("[]"),
40
- // Lead project first. Machine tasks resolve per-project env through these
41
- // ids at every agent spawn (Docker tasks take env from compose instead).
45
+ // Lead project first. Retained for legacy/baked machine-task agent spawns;
46
+ // loader tasks resolve only declared names through projectEnvSpec.
42
47
  projectIds: text("project_ids").notNull().default("[]"),
43
48
  // ADR-131 D3: which engine-account scope this task resolves credentials
44
49
  // from ("org:<orgId>" on a shared host; null = "host"). Persisted so a
@@ -202,6 +207,311 @@ export const hostProjectEnv = sqliteTable(
202
207
  export type HostProjectEnv = typeof hostProjectEnv.$inferSelect;
203
208
  export type NewHostProjectEnv = typeof hostProjectEnv.$inferInsert;
204
209
 
210
+ // ADR-153: value-free authority records live beside (not inside) the legacy
211
+ // encrypted value row. A delete therefore keeps its binding/head forever,
212
+ // while a live owned-runner head always refers to that row.
213
+ export const hostProjectEnvRecords = sqliteTable(
214
+ "host_project_env_records",
215
+ {
216
+ projectId: text("project_id").notNull(),
217
+ key: text("key").notNull(),
218
+ revision: integer("revision").notNull(),
219
+ generation: integer("generation").notNull(),
220
+ tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
221
+ bindingJson: text("binding_json"),
222
+ policyEpoch: integer("policy_epoch"),
223
+ listingGeneration: integer("listing_generation").notNull(),
224
+ updatedAt: integer("updated_at", { mode: "number" })
225
+ .notNull()
226
+ .default(sql`(unixepoch() * 1000)`),
227
+ },
228
+ (t) => ({
229
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
230
+ }),
231
+ );
232
+
233
+ export const hostProjectEnvProjects = sqliteTable("host_project_env_projects", {
234
+ projectId: text("project_id").primaryKey(),
235
+ recordVersion: integer("record_version").notNull().default(0),
236
+ });
237
+
238
+ // Singleton activation, serving gate and ordered-delivery cursor. `modernWrite`
239
+ // is raised only inside a mutation transaction so the permanent legacy table
240
+ // triggers can distinguish this build from an old process still writing it.
241
+ export const hostProjectEnvAuthority = sqliteTable("host_project_env_authority", {
242
+ id: text("id").primaryKey().default("host"),
243
+ activated: integer("activated", { mode: "boolean" }).notNull().default(false),
244
+ gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(true),
245
+ streamId: text("stream_id"),
246
+ appliedSequence: integer("applied_sequence").notNull().default(0),
247
+ attemptId: text("attempt_id"),
248
+ attemptLegacyVersion: integer("attempt_legacy_version"),
249
+ legacyVersion: integer("legacy_version").notNull().default(0),
250
+ policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
251
+ modernWrite: integer("modern_write", { mode: "boolean" }).notNull().default(false),
252
+ updatedAt: integer("updated_at", { mode: "number" })
253
+ .notNull()
254
+ .default(sql`(unixepoch() * 1000)`),
255
+ });
256
+
257
+ // A receipt contains a keyed request MAC and a value-free result. In
258
+ // particular, a set's plaintext is never replayable from SQLite.
259
+ export const hostProjectEnvDeliveryReceipts = sqliteTable(
260
+ "host_project_env_delivery_receipts",
261
+ {
262
+ streamId: text("stream_id").notNull(),
263
+ sequence: integer("sequence").notNull(),
264
+ requestKind: text("request_kind").notNull(),
265
+ requestMac: text("request_mac").notNull(),
266
+ ok: integer("ok", { mode: "boolean" }).notNull(),
267
+ stateJson: text("state_json"),
268
+ recordVersion: integer("record_version"),
269
+ code: text("code"),
270
+ createdAt: integer("created_at", { mode: "number" })
271
+ .notNull()
272
+ .default(sql`(unixepoch() * 1000)`),
273
+ },
274
+ (t) => ({ pk: primaryKey({ columns: [t.streamId, t.sequence] }) }),
275
+ );
276
+
277
+ export const hostProjectEnvLegacyJournal = sqliteTable(
278
+ "host_project_env_legacy_journal",
279
+ {
280
+ id: integer("id").primaryKey({ autoIncrement: true }),
281
+ projectId: text("project_id").notNull(),
282
+ key: text("key").notNull(),
283
+ mutation: text("mutation").notNull(),
284
+ createdAt: integer("created_at", { mode: "number" })
285
+ .notNull()
286
+ .default(sql`(unixepoch() * 1000)`),
287
+ },
288
+ (t) => ({ keyIdx: index("host_project_env_legacy_journal_key_idx").on(t.projectId, t.key) }),
289
+ );
290
+
291
+ // ADR-153 Uai Cloud task-machine replica. These tables are deliberately
292
+ // separate from the owned-runner authority above: a sidecar stages a complete
293
+ // task snapshot while serving nothing, then promotes it atomically. In
294
+ // particular, materialising a cloud value must not fire the owned authority's
295
+ // permanent legacy-write triggers.
296
+ export const hostProjectEnvReplicaAuthority = sqliteTable(
297
+ "host_project_env_replica_authority",
298
+ {
299
+ id: text("id").primaryKey().default("replica"),
300
+ activated: integer("activated", { mode: "boolean" }).notNull().default(false),
301
+ gateOpen: integer("gate_open", { mode: "boolean" }).notNull().default(false),
302
+ streamId: text("stream_id"),
303
+ appliedSequence: integer("applied_sequence").notNull().default(0),
304
+ attemptId: text("attempt_id"),
305
+ orgId: text("org_id"),
306
+ policyEpochsJson: text("policy_epochs_json").notNull().default("[1]"),
307
+ updatedAt: integer("updated_at", { mode: "number" })
308
+ .notNull()
309
+ .default(sql`(unixepoch() * 1000)`),
310
+ },
311
+ (t) => ({
312
+ singleton: check("host_project_env_replica_authority_singleton", sql`${t.id} = 'replica'`),
313
+ booleans: check(
314
+ "host_project_env_replica_authority_booleans",
315
+ sql`${t.activated} IN (0, 1) AND ${t.gateOpen} IN (0, 1)`,
316
+ ),
317
+ cursor: check("host_project_env_replica_authority_cursor", sql`${t.appliedSequence} >= 0`),
318
+ stream: check(
319
+ "host_project_env_replica_authority_stream",
320
+ 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)`,
321
+ ),
322
+ gate: check(
323
+ "host_project_env_replica_authority_gate",
324
+ sql`${t.gateOpen} = 0 OR ${t.attemptId} IS NULL`,
325
+ ),
326
+ }),
327
+ );
328
+
329
+ export const hostProjectEnvReplicaProjects = sqliteTable(
330
+ "host_project_env_replica_projects",
331
+ {
332
+ projectId: text("project_id").primaryKey(),
333
+ recordVersion: integer("record_version").notNull(),
334
+ },
335
+ (t) => ({
336
+ version: check("host_project_env_replica_projects_version", sql`${t.recordVersion} >= 0`),
337
+ }),
338
+ );
339
+
340
+ const replicaRecordColumns = () => ({
341
+ projectId: text("project_id").notNull(),
342
+ key: text("key").notNull(),
343
+ revision: integer("revision").notNull(),
344
+ generation: integer("generation").notNull(),
345
+ tombstone: integer("tombstone", { mode: "boolean" }).notNull(),
346
+ valueRefJson: text("value_ref_json"),
347
+ bindingJson: text("binding_json"),
348
+ policyEpoch: integer("policy_epoch"),
349
+ listingGeneration: integer("listing_generation").notNull(),
350
+ materialization: text("materialization").notNull(),
351
+ materializationReason: text("materialization_reason"),
352
+ updatedAt: integer("updated_at", { mode: "number" })
353
+ .notNull()
354
+ .default(sql`(unixepoch() * 1000)`),
355
+ });
356
+
357
+ export const hostProjectEnvReplicaRecords = sqliteTable(
358
+ "host_project_env_replica_records",
359
+ replicaRecordColumns(),
360
+ (t) => ({
361
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
362
+ head: check(
363
+ "host_project_env_replica_records_head",
364
+ 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)`,
365
+ ),
366
+ binding: check(
367
+ "host_project_env_replica_records_binding",
368
+ 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)`,
369
+ ),
370
+ listing: check(
371
+ "host_project_env_replica_records_listing",
372
+ sql`${t.listingGeneration} >= 0`,
373
+ ),
374
+ boolean: check(
375
+ "host_project_env_replica_records_boolean",
376
+ sql`${t.tombstone} IN (0, 1)`,
377
+ ),
378
+ materialization: check(
379
+ "host_project_env_replica_records_materialization",
380
+ 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)`,
381
+ ),
382
+ }),
383
+ );
384
+
385
+ export const hostProjectEnvReplicaValues = sqliteTable(
386
+ "host_project_env_replica_values",
387
+ {
388
+ projectId: text("project_id").notNull(),
389
+ key: text("key").notNull(),
390
+ revision: integer("revision").notNull(),
391
+ generation: integer("generation").notNull(),
392
+ valueRefJson: text("value_ref_json").notNull(),
393
+ valueEnc: text("value_enc").notNull(),
394
+ updatedAt: integer("updated_at", { mode: "number" })
395
+ .notNull()
396
+ .default(sql`(unixepoch() * 1000)`),
397
+ },
398
+ (t) => ({
399
+ pk: primaryKey({ columns: [t.projectId, t.key] }),
400
+ head: check(
401
+ "host_project_env_replica_values_head",
402
+ sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
403
+ ),
404
+ }),
405
+ );
406
+
407
+ export const hostProjectEnvReplicaStageProjects = sqliteTable(
408
+ "host_project_env_replica_stage_projects",
409
+ {
410
+ attemptId: text("attempt_id").notNull(),
411
+ projectId: text("project_id").notNull(),
412
+ recordVersion: integer("record_version"),
413
+ lastKey: text("last_key"),
414
+ complete: integer("complete", { mode: "boolean" }).notNull().default(false),
415
+ },
416
+ (t) => ({
417
+ pk: primaryKey({ columns: [t.attemptId, t.projectId] }),
418
+ version: check(
419
+ "host_project_env_replica_stage_projects_version",
420
+ sql`${t.recordVersion} IS NULL OR ${t.recordVersion} >= 0`,
421
+ ),
422
+ completeBoolean: check(
423
+ "host_project_env_replica_stage_projects_complete",
424
+ sql`${t.complete} IN (0, 1)`,
425
+ ),
426
+ }),
427
+ );
428
+
429
+ export const hostProjectEnvReplicaStageRecords = sqliteTable(
430
+ "host_project_env_replica_stage_records",
431
+ {
432
+ attemptId: text("attempt_id").notNull(),
433
+ ...replicaRecordColumns(),
434
+ present: integer("present", { mode: "boolean" }).notNull().default(false),
435
+ },
436
+ (t) => ({
437
+ pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
438
+ head: check(
439
+ "host_project_env_replica_stage_records_head",
440
+ 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)`,
441
+ ),
442
+ binding: check(
443
+ "host_project_env_replica_stage_records_binding",
444
+ 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)`,
445
+ ),
446
+ listing: check(
447
+ "host_project_env_replica_stage_records_listing",
448
+ sql`${t.listingGeneration} >= 0`,
449
+ ),
450
+ boolean: check(
451
+ "host_project_env_replica_stage_records_boolean",
452
+ sql`${t.tombstone} IN (0, 1) AND ${t.present} IN (0, 1)`,
453
+ ),
454
+ materialization: check(
455
+ "host_project_env_replica_stage_records_materialization",
456
+ 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)`,
457
+ ),
458
+ }),
459
+ );
460
+
461
+ export const hostProjectEnvReplicaStageValues = sqliteTable(
462
+ "host_project_env_replica_stage_values",
463
+ {
464
+ attemptId: text("attempt_id").notNull(),
465
+ projectId: text("project_id").notNull(),
466
+ key: text("key").notNull(),
467
+ revision: integer("revision").notNull(),
468
+ generation: integer("generation").notNull(),
469
+ valueRefJson: text("value_ref_json").notNull(),
470
+ valueEnc: text("value_enc").notNull(),
471
+ updatedAt: integer("updated_at", { mode: "number" })
472
+ .notNull()
473
+ .default(sql`(unixepoch() * 1000)`),
474
+ },
475
+ (t) => ({
476
+ pk: primaryKey({ columns: [t.attemptId, t.projectId, t.key] }),
477
+ head: check(
478
+ "host_project_env_replica_stage_values_head",
479
+ sql`${t.revision} >= 1 AND ${t.generation} >= 1`,
480
+ ),
481
+ }),
482
+ );
483
+
484
+ export const hostProjectEnvReplicaReceipts = sqliteTable(
485
+ "host_project_env_replica_receipts",
486
+ {
487
+ streamId: text("stream_id").notNull(),
488
+ sequence: integer("sequence").notNull(),
489
+ requestMac: text("request_mac").notNull(),
490
+ resultJson: text("result_json"),
491
+ recordVersion: integer("record_version"),
492
+ ok: integer("ok", { mode: "boolean" }).notNull(),
493
+ code: text("code"),
494
+ createdAt: integer("created_at", { mode: "number" })
495
+ .notNull()
496
+ .default(sql`(unixepoch() * 1000)`),
497
+ },
498
+ (t) => ({
499
+ pk: primaryKey({ columns: [t.streamId, t.sequence] }),
500
+ sequenceCheck: check(
501
+ "host_project_env_replica_receipts_sequence",
502
+ sql`${t.sequence} >= 1`,
503
+ ),
504
+ booleanCheck: check(
505
+ "host_project_env_replica_receipts_boolean",
506
+ sql`${t.ok} IN (0, 1)`,
507
+ ),
508
+ outcome: check(
509
+ "host_project_env_replica_receipts_outcome",
510
+ 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)`,
511
+ ),
512
+ }),
513
+ );
514
+
205
515
  // User-authed MCP connections (ADR-057). One row per cloud connection id; all
206
516
  // secret material (static header, OAuth client secret, PKCE verifier, tokens)
207
517
  // is sealed with the host master key in single-column `ct.nonce` base64 form