@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
package/src/protocol.ts CHANGED
@@ -128,6 +128,2192 @@ export const HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE =
128
128
  * unrelated respawn and never stops a removed one, so the cloud refuses to
129
129
  * edit or remove a running task's agents on it. */
130
130
  export const LIVE_ROSTER_EDIT_PROTOCOL_FEATURE = "live-roster-edit-v1";
131
+ /** ADR-153 declarations + owner bindings. Advertising this means the host
132
+ * understands every additive env frame field, persists bindings separately
133
+ * from values, and reports immutable repository schema observations. Keep it
134
+ * out of buildCapabilities until that whole contract is wired end to end. */
135
+ export const PROJECT_ENV_BINDINGS_PROTOCOL_FEATURE =
136
+ "project-env-bindings-v1";
137
+
138
+ /** Existing env writes are bounded by a 512 KiB raw request, not a smaller
139
+ * key-specific cap. Keep the additive binding ack able to describe any key
140
+ * the current API can already have stored. */
141
+ export const MAX_PROJECT_ENV_KEY_CHARS = 512 * 1024;
142
+ export const MAX_PROJECT_ENV_PROJECT_ID_CHARS = 128;
143
+ export const MAX_PROJECT_ENV_SCHEMA_DECLARATIONS = 256;
144
+ export const MAX_PROJECT_ENV_SCHEMA_ISSUES =
145
+ MAX_PROJECT_ENV_SCHEMA_DECLARATIONS + 16;
146
+ export const MAX_PROJECT_ENV_SCHEMA_REPORTS = 256;
147
+ export const MAX_PROJECT_ENV_PROXY_DOMAINS = 16;
148
+ export const MAX_PROJECT_ENV_REASON_CODE_CHARS = 64;
149
+ export const MAX_PROJECT_ENV_TYPE_CHARS = 128;
150
+ export const MAX_PROJECT_ENV_PLACEHOLDER_CHARS = 256;
151
+ /** Modern inventory pages stay well below the authenticated bridge's 100 MiB
152
+ * frame ceiling. A page builder measures the complete encoded frame and must
153
+ * still return at least one record when any remain. */
154
+ export const MAX_PROJECT_ENV_PAGE_BYTES = 8 * 1024 * 1024;
155
+ /** Kept as the explicit owned-runner ack bound for callers already using it. */
156
+ export const MAX_PROJECT_ENV_ACK_PAGE_BYTES = MAX_PROJECT_ENV_PAGE_BYTES;
157
+ export const MAX_PROJECT_ENV_POLICY_EPOCHS = 64;
158
+ /** The one epoch a pre-record value can be migrated under in bindings-v1. */
159
+ export const INITIAL_PROJECT_ENV_POLICY_EPOCH = 1;
160
+ /**
161
+ * Highest schema-observation interpretation this build understands.
162
+ *
163
+ * Bump this for any change to parser, reader, or commit-metadata semantics.
164
+ * The wire guard accepts every historical positive epoch through this value,
165
+ * but rejects a future epoch until this peer knows how to interpret it.
166
+ */
167
+ export const CURRENT_PROJECT_ENV_PARSER_EPOCH = 1;
168
+
169
+ export const PROJECT_ENV_UNSUPPORTED_REASON_CODES = [
170
+ "malformed",
171
+ "invalid_value",
172
+ "conflicting_forms",
173
+ "missing_domain",
174
+ "option_path",
175
+ "option_method",
176
+ "option_block",
177
+ "option_keys",
178
+ "option_substitute_in",
179
+ "option_transform",
180
+ "option_approval",
181
+ "option_rules",
182
+ "option_unknown",
183
+ "wildcard_domain",
184
+ "ip_literal",
185
+ "localhost",
186
+ "reserved_domain",
187
+ "invalid_domain",
188
+ "too_many_domains",
189
+ "header_rule",
190
+ "duplicate_key",
191
+ ] as const;
192
+ export const PROJECT_ENV_SCHEMA_ISSUE_CODES = [
193
+ "header_proxy_rule",
194
+ "proxy_config",
195
+ "placeholder_ignored",
196
+ ] as const;
197
+ /** Unavailable reasons from a runner that never reached the commit's
198
+ * schema: they say nothing about it, so they carry no commit (ADR-153 §4,
199
+ * "The latest observation"). */
200
+ export const PROJECT_ENV_SCHEMA_UNREACHED_REASON_CODES = [
201
+ "base_commit_missing",
202
+ "schema_read_failed",
203
+ ] as const;
204
+ /** Unavailable reasons from a runner that read the commit's schema and could
205
+ * not interpret it: a verdict about that commit, so they carry it. */
206
+ export const PROJECT_ENV_SCHEMA_UNREADABLE_REASON_CODES = [
207
+ "schema_not_regular_file",
208
+ "schema_too_large",
209
+ "schema_too_many_declarations",
210
+ "schema_unterminated_value",
211
+ "schema_not_text",
212
+ "schema_malformed",
213
+ ] as const;
214
+ export const PROJECT_ENV_SCHEMA_UNAVAILABLE_REASON_CODES = [
215
+ ...PROJECT_ENV_SCHEMA_UNREACHED_REASON_CODES,
216
+ ...PROJECT_ENV_SCHEMA_UNREADABLE_REASON_CODES,
217
+ ] as const;
218
+
219
+ /** Reason and issue codes are lower-case snake_case on the wire. They remain
220
+ * open strings so a newer peer can explain a fail-closed state to an older
221
+ * one without making the whole observation unreadable. */
222
+ export type KnownProjectEnvUnsupportedReason =
223
+ (typeof PROJECT_ENV_UNSUPPORTED_REASON_CODES)[number];
224
+ export type KnownProjectEnvSchemaIssueCode =
225
+ (typeof PROJECT_ENV_SCHEMA_ISSUE_CODES)[number];
226
+ export type KnownProjectEnvSchemaUnavailableReason =
227
+ (typeof PROJECT_ENV_SCHEMA_UNAVAILABLE_REASON_CODES)[number];
228
+ export type ProjectEnvUnsupportedReason = string;
229
+ export type ProjectEnvSchemaUnavailableReason = string;
230
+ export type ProjectEnvSchemaIssueCode = string;
231
+ export type ProjectEnvWithholdReason = string;
232
+
233
+ /** What one repository declaration asks Uai to do with a stored value. */
234
+ export type ProjectEnvDirective =
235
+ | { kind: "none" }
236
+ | { kind: "proxy"; domains: string[] }
237
+ | { kind: "passthrough" }
238
+ | { kind: "omit" }
239
+ | { kind: "unsupported"; reason: ProjectEnvUnsupportedReason };
240
+
241
+ /** Cloud-safe declaration captured from a trusted `.env.schema` base.
242
+ * `placeholder` is a shape only: the repository's literal never crosses the
243
+ * bridge and is never used as a task placeholder. */
244
+ export interface ProjectEnvSchemaDeclaration {
245
+ key: string;
246
+ required: boolean;
247
+ sensitive: boolean;
248
+ type?: string;
249
+ directive: ProjectEnvDirective;
250
+ placeholder?: {
251
+ prefix: string;
252
+ totalLength: number;
253
+ };
254
+ }
255
+
256
+ /** A schema condition that cannot be represented on the declaration itself.
257
+ * Missing `key` means project-wide. */
258
+ export interface ProjectEnvSchemaIssue {
259
+ code: ProjectEnvSchemaIssueCode;
260
+ key?: string;
261
+ }
262
+
263
+ /** One immutable observation per task/project. OID and commit time are a
264
+ * coherent pair: both identify the commit reached, or both are null for a
265
+ * proven-empty repository or a failure before reaching a commit. An unreadable
266
+ * base is unavailable, never a resolved empty schema. */
267
+ export type ProjectEnvSchemaObservation =
268
+ | {
269
+ projectId: string;
270
+ status: "resolved";
271
+ parserEpoch: number;
272
+ baseOid: string | null;
273
+ baseCommitTimeMs: number | null;
274
+ declarations: ProjectEnvSchemaDeclaration[];
275
+ issues: ProjectEnvSchemaIssue[];
276
+ }
277
+ | {
278
+ projectId: string;
279
+ status: "unavailable";
280
+ parserEpoch: number;
281
+ baseOid: string | null;
282
+ baseCommitTimeMs: number | null;
283
+ reason: ProjectEnvSchemaUnavailableReason;
284
+ };
285
+
286
+ /** Wire/report synonym: the host's durable observation is what task-up
287
+ * reports to the cloud. */
288
+ export type ProjectEnvSchemaReport = ProjectEnvSchemaObservation;
289
+
290
+ /** The most permissive policy the owner has approved for one stored value. */
291
+ export type ProjectEnvBinding =
292
+ | { kind: "proxy"; domains: string[] }
293
+ | { kind: "passthrough" }
294
+ | { kind: "omit" }
295
+ | { kind: "withhold"; reason: ProjectEnvWithholdReason };
296
+
297
+ /** A modern host reports every record of the project, tombstones and fences
298
+ * included (ADR-153 §4, "Where bindings live" and "Protocol"): the record's
299
+ * head, its binding (null: none), the policy epoch that was written under,
300
+ * and the highest listing generation a change to it carried. The record
301
+ * outlives its value, so a stale compare-and-swap cannot match a re-created
302
+ * one. A fence has never held a value: its head is `r1, g0`, with no binding
303
+ * and no epoch. A legacy host omits the whole bindings field rather than
304
+ * manufacturing these entries. */
305
+ export interface ProjectEnvBindingState {
306
+ key: string;
307
+ head: ProjectEnvHead;
308
+ binding: ProjectEnvBinding | null;
309
+ /** Null exactly for a fence. */
310
+ policyEpoch: number | null;
311
+ listingGeneration: number;
312
+ }
313
+
314
+ const PROJECT_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
315
+ const PROJECT_ENV_REASON_RE = /^[a-z][a-z0-9_]{0,63}$/;
316
+ const PROJECT_ENV_OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
317
+ const PROJECT_ENV_DOMAIN_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
318
+ const PROJECT_ENV_PLACEHOLDER_PREFIX_RE = /^(?:[A-Za-z0-9._~-]*[_-])?$/;
319
+ const PROJECT_ENV_NEVER_PROXIED_SUFFIXES = [
320
+ "github.com",
321
+ "githubusercontent.com",
322
+ "runuai.com",
323
+ "localhost",
324
+ ] as const;
325
+
326
+ function isProjectEnvKey(value: unknown): value is string {
327
+ return (
328
+ typeof value === "string" &&
329
+ value.length <= MAX_PROJECT_ENV_KEY_CHARS &&
330
+ PROJECT_ENV_KEY_RE.test(value)
331
+ );
332
+ }
333
+
334
+ function isProjectEnvProjectId(value: unknown): value is string {
335
+ return (
336
+ typeof value === "string" &&
337
+ value.length > 0 &&
338
+ value.length <= MAX_PROJECT_ENV_PROJECT_ID_CHARS
339
+ );
340
+ }
341
+
342
+ function isProjectEnvReason(value: unknown): value is string {
343
+ return (
344
+ typeof value === "string" &&
345
+ value.length <= MAX_PROJECT_ENV_REASON_CODE_CHARS &&
346
+ PROJECT_ENV_REASON_RE.test(value)
347
+ );
348
+ }
349
+
350
+ function isProjectEnvParserEpoch(value: unknown): value is number {
351
+ return (
352
+ typeof value === "number" &&
353
+ Number.isSafeInteger(value) &&
354
+ value >= 1 &&
355
+ value <= CURRENT_PROJECT_ENV_PARSER_EPOCH
356
+ );
357
+ }
358
+
359
+ /** A canonical public DNS name that ADR-153 permits as a proxy boundary. */
360
+ function isProjectEnvDomain(value: unknown): value is string {
361
+ if (
362
+ typeof value !== "string" ||
363
+ value.length === 0 ||
364
+ value.length > 253 ||
365
+ value.endsWith(".") ||
366
+ value !== value.toLowerCase()
367
+ ) {
368
+ return false;
369
+ }
370
+ const labels = value.split(".");
371
+ if (
372
+ labels.length < 2 ||
373
+ !labels.every((label) => PROJECT_ENV_DOMAIN_LABEL_RE.test(label))
374
+ ) {
375
+ return false;
376
+ }
377
+ const last = labels[labels.length - 1] ?? "";
378
+ if (/^[0-9]+$/.test(last) || /^0x[0-9a-f]*$/.test(last)) return false;
379
+ return !PROJECT_ENV_NEVER_PROXIED_SUFFIXES.some(
380
+ (suffix) => value === suffix || value.endsWith(`.${suffix}`),
381
+ );
382
+ }
383
+
384
+ function isUniqueStringArray(
385
+ value: unknown,
386
+ max: number,
387
+ item: (candidate: unknown) => candidate is string,
388
+ ): value is string[] {
389
+ return (
390
+ isDenseArray(value) &&
391
+ value.length <= max &&
392
+ value.every(item) &&
393
+ new Set(value).size === value.length
394
+ );
395
+ }
396
+
397
+ /** JSON arrays are dense. Reject a JavaScript hole before it becomes a null
398
+ * on the wire and changes the value a validator thought it approved. */
399
+ function isDenseArray(value: unknown): value is unknown[] {
400
+ if (!Array.isArray(value)) return false;
401
+ for (let index = 0; index < value.length; index += 1) {
402
+ if (!Object.hasOwn(value, index)) return false;
403
+ }
404
+ return true;
405
+ }
406
+
407
+ function isSortedUniqueStringArray(
408
+ value: unknown,
409
+ max: number,
410
+ item: (candidate: unknown) => candidate is string,
411
+ ): value is string[] {
412
+ return (
413
+ isUniqueStringArray(value, max, item) &&
414
+ value.every((candidate, index) => index === 0 || value[index - 1]! < candidate)
415
+ );
416
+ }
417
+
418
+ export function isProjectEnvKeys(value: unknown): value is string[] {
419
+ return (
420
+ isDenseArray(value) &&
421
+ value.every(isProjectEnvKey) &&
422
+ new Set(value).size === value.length
423
+ );
424
+ }
425
+
426
+ function hasExactlyTheSameStrings(
427
+ left: readonly string[],
428
+ right: readonly string[],
429
+ ): boolean {
430
+ if (left.length !== right.length) return false;
431
+ const rightSet = new Set(right);
432
+ return left.every((item) => rightSet.has(item));
433
+ }
434
+
435
+ export function isProjectEnvDirective(
436
+ value: unknown,
437
+ ): value is ProjectEnvDirective {
438
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
439
+ const kind = (value as { kind?: unknown }).kind;
440
+ if (kind === "none" || kind === "passthrough" || kind === "omit") {
441
+ return exactWireObject(value, ["kind"]) !== null;
442
+ }
443
+ if (kind === "proxy") {
444
+ const object = exactWireObject(value, ["kind", "domains"]);
445
+ return (
446
+ object !== null &&
447
+ isSortedUniqueStringArray(
448
+ object.domains,
449
+ MAX_PROJECT_ENV_PROXY_DOMAINS,
450
+ isProjectEnvDomain,
451
+ ) &&
452
+ object.domains.length > 0
453
+ );
454
+ }
455
+ if (kind === "unsupported") {
456
+ const object = exactWireObject(value, ["kind", "reason"]);
457
+ return object !== null && isProjectEnvReason(object.reason);
458
+ }
459
+ return false;
460
+ }
461
+
462
+ export function isProjectEnvSchemaDeclaration(
463
+ value: unknown,
464
+ ): value is ProjectEnvSchemaDeclaration {
465
+ const object = exactWireObject(
466
+ value,
467
+ ["key", "required", "sensitive", "directive"],
468
+ ["type", "placeholder"],
469
+ );
470
+ if (
471
+ object === null ||
472
+ !isProjectEnvKey(object.key) ||
473
+ typeof object.required !== "boolean" ||
474
+ typeof object.sensitive !== "boolean" ||
475
+ !isProjectEnvDirective(object.directive) ||
476
+ (object.directive.kind !== "none" && object.sensitive !== true) ||
477
+ (object.type !== undefined &&
478
+ (typeof object.type !== "string" ||
479
+ object.type.length === 0 ||
480
+ object.type.length > MAX_PROJECT_ENV_TYPE_CHARS ||
481
+ /[\u0000-\u001f\u007f]/.test(object.type)))
482
+ ) {
483
+ return false;
484
+ }
485
+ if (object.placeholder === undefined) return true;
486
+ if (object.directive.kind !== "proxy") return false;
487
+ const placeholder = exactWireObject(object.placeholder, [
488
+ "prefix",
489
+ "totalLength",
490
+ ]);
491
+ return (
492
+ placeholder !== null &&
493
+ typeof placeholder.prefix === "string" &&
494
+ PROJECT_ENV_PLACEHOLDER_PREFIX_RE.test(placeholder.prefix) &&
495
+ typeof placeholder.totalLength === "number" &&
496
+ Number.isSafeInteger(placeholder.totalLength) &&
497
+ placeholder.totalLength >= placeholder.prefix.length + 32 &&
498
+ placeholder.totalLength <= MAX_PROJECT_ENV_PLACEHOLDER_CHARS
499
+ );
500
+ }
501
+
502
+ export function isProjectEnvSchemaIssue(
503
+ value: unknown,
504
+ ): value is ProjectEnvSchemaIssue {
505
+ const object = exactWireObject(value, ["code"], ["key"]);
506
+ return (
507
+ object !== null &&
508
+ isProjectEnvReason(object.code) &&
509
+ (object.key === undefined || isProjectEnvKey(object.key))
510
+ );
511
+ }
512
+
513
+ export function isProjectEnvSchemaObservation(
514
+ value: unknown,
515
+ ): value is ProjectEnvSchemaObservation {
516
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
517
+ const status = (value as { status?: unknown }).status;
518
+ if (status === "unavailable") {
519
+ const object = exactWireObject(value, [
520
+ "projectId",
521
+ "status",
522
+ "parserEpoch",
523
+ "baseOid",
524
+ "baseCommitTimeMs",
525
+ "reason",
526
+ ]);
527
+ const emptyBase =
528
+ object?.baseOid === null && object.baseCommitTimeMs === null;
529
+ const committedBase =
530
+ typeof object?.baseOid === "string" &&
531
+ PROJECT_ENV_OID_RE.test(object.baseOid) &&
532
+ typeof object.baseCommitTimeMs === "number" &&
533
+ Number.isSafeInteger(object.baseCommitTimeMs) &&
534
+ object.baseCommitTimeMs >= 0;
535
+ // A known reason fixes which base it carries; the comparator orders a
536
+ // report by it. A reason this peer does not know yet is taken at its base.
537
+ const reason: unknown = object?.reason;
538
+ const coherentBase = (
539
+ PROJECT_ENV_SCHEMA_UNREACHED_REASON_CODES as readonly unknown[]
540
+ ).includes(reason)
541
+ ? emptyBase
542
+ : (PROJECT_ENV_SCHEMA_UNREADABLE_REASON_CODES as readonly unknown[]).includes(
543
+ reason,
544
+ )
545
+ ? committedBase
546
+ : emptyBase || committedBase;
547
+ return (
548
+ object !== null &&
549
+ isProjectEnvProjectId(object.projectId) &&
550
+ isProjectEnvParserEpoch(object.parserEpoch) &&
551
+ isProjectEnvReason(object.reason) &&
552
+ coherentBase
553
+ );
554
+ }
555
+ if (status !== "resolved") return false;
556
+ const object = exactWireObject(value, [
557
+ "projectId",
558
+ "status",
559
+ "parserEpoch",
560
+ "baseOid",
561
+ "baseCommitTimeMs",
562
+ "declarations",
563
+ "issues",
564
+ ]);
565
+ if (
566
+ object === null ||
567
+ !isProjectEnvProjectId(object.projectId) ||
568
+ !isProjectEnvParserEpoch(object.parserEpoch) ||
569
+ !isDenseArray(object.declarations) ||
570
+ object.declarations.length > MAX_PROJECT_ENV_SCHEMA_DECLARATIONS ||
571
+ !object.declarations.every(isProjectEnvSchemaDeclaration) ||
572
+ new Set(
573
+ object.declarations.map(
574
+ (declaration: ProjectEnvSchemaDeclaration) => declaration.key,
575
+ ),
576
+ ).size !== object.declarations.length ||
577
+ !isDenseArray(object.issues) ||
578
+ object.issues.length > MAX_PROJECT_ENV_SCHEMA_ISSUES ||
579
+ !object.issues.every(isProjectEnvSchemaIssue) ||
580
+ new Set(
581
+ object.issues.map(
582
+ (issue: ProjectEnvSchemaIssue) => `${issue.code}\u0000${issue.key ?? ""}`,
583
+ ),
584
+ ).size !== object.issues.length
585
+ ) {
586
+ return false;
587
+ }
588
+ const emptyBase = object.baseOid === null && object.baseCommitTimeMs === null;
589
+ const committedBase =
590
+ typeof object.baseOid === "string" &&
591
+ PROJECT_ENV_OID_RE.test(object.baseOid) &&
592
+ typeof object.baseCommitTimeMs === "number" &&
593
+ Number.isSafeInteger(object.baseCommitTimeMs) &&
594
+ object.baseCommitTimeMs >= 0;
595
+ return (
596
+ (emptyBase &&
597
+ object.declarations.length === 0 &&
598
+ object.issues.length === 0) ||
599
+ committedBase
600
+ );
601
+ }
602
+
603
+ export const isProjectEnvSchemaReport = isProjectEnvSchemaObservation;
604
+
605
+ export function isProjectEnvSchemaObservations(
606
+ value: unknown,
607
+ projectIds?: readonly string[],
608
+ ): value is ProjectEnvSchemaObservation[] {
609
+ if (
610
+ !isDenseArray(value) ||
611
+ (projectIds === undefined &&
612
+ value.length > MAX_PROJECT_ENV_SCHEMA_REPORTS) ||
613
+ !value.every(isProjectEnvSchemaObservation)
614
+ ) {
615
+ return false;
616
+ }
617
+ const observedProjectIds = value.map((observation) => observation.projectId);
618
+ if (new Set(observedProjectIds).size !== observedProjectIds.length) {
619
+ return false;
620
+ }
621
+ if (projectIds === undefined) return true;
622
+ return (
623
+ isDenseArray(projectIds) &&
624
+ projectIds.every(isProjectEnvProjectId) &&
625
+ new Set(projectIds).size === projectIds.length &&
626
+ hasExactlyTheSameStrings(observedProjectIds, projectIds)
627
+ );
628
+ }
629
+
630
+ export const isProjectEnvSchemaReports = isProjectEnvSchemaObservations;
631
+
632
+ export function isProjectEnvBinding(
633
+ value: unknown,
634
+ ): value is ProjectEnvBinding {
635
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
636
+ const kind = (value as { kind?: unknown }).kind;
637
+ if (kind === "passthrough" || kind === "omit") {
638
+ return exactWireObject(value, ["kind"]) !== null;
639
+ }
640
+ if (kind === "proxy") {
641
+ const object = exactWireObject(value, ["kind", "domains"]);
642
+ return (
643
+ object !== null &&
644
+ isSortedUniqueStringArray(
645
+ object.domains,
646
+ MAX_PROJECT_ENV_PROXY_DOMAINS,
647
+ isProjectEnvDomain,
648
+ ) &&
649
+ object.domains.length > 0
650
+ );
651
+ }
652
+ if (kind === "withhold") {
653
+ const object = exactWireObject(value, ["kind", "reason"]);
654
+ return object !== null && isProjectEnvReason(object.reason);
655
+ }
656
+ return false;
657
+ }
658
+
659
+ /** Where a key's live value is (ADR-153 §4, "Value references"): in the
660
+ * record's own row (owned runners), in a staged parameter named by its
661
+ * staging id, or in the key's legacy parameter at one exact SSM version. */
662
+ export type ProjectEnvValueRef =
663
+ | { kind: "row" }
664
+ | { kind: "staged"; stagingId: string }
665
+ | { kind: "legacy"; ssmVersion: number };
666
+
667
+ /** A key's head (ADR-153 §4, "Where bindings live"). A tombstone has no
668
+ * value, so no value reference; a live head always has one. A removal's
669
+ * fence is the tombstone at generation 0 (`isProjectEnvFence`). */
670
+ export type ProjectEnvHead =
671
+ | {
672
+ revision: number;
673
+ generation: number;
674
+ tombstone: false;
675
+ valueRef: ProjectEnvValueRef;
676
+ }
677
+ | { revision: number; generation: number; tombstone: true; valueRef: null };
678
+
679
+ export type ProjectEnvLiveHead = Extract<
680
+ ProjectEnvHead,
681
+ { tombstone: false }
682
+ >;
683
+
684
+ /** One runner's durable cursor in the cloud-authored env delivery stream.
685
+ * The stream id changes if the cloud must replace or reconstruct the stream;
686
+ * sequence zero is the beginning of a new stream, before any mutation. */
687
+ export interface ProjectEnvDeliveryCursor {
688
+ streamId: HostOpId;
689
+ sequence: number;
690
+ }
691
+
692
+ /** A staging id is a lowercase UUID: unique per attempt, never reused. */
693
+ const PROJECT_ENV_STAGING_ID_RE =
694
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
695
+ const PROJECT_ENV_STORE_PATH_SEGMENT_RE = /^[A-Za-z0-9_.-]+$/;
696
+
697
+ /** AWS SSM Advanced parameters accept at most 8 KiB. The limit applies to
698
+ * the complete v2 envelope, not just its secret value. */
699
+ export const MAX_PROJECT_ENV_STAGED_ENVELOPE_BYTES = 8 * 1024;
700
+
701
+ const isNonNegativeSafeInteger = (value: unknown): value is number =>
702
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
703
+
704
+ const isPositiveSafeInteger = (value: unknown): value is number =>
705
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
706
+
707
+ export function isProjectEnvValueRef(
708
+ value: unknown,
709
+ ): value is ProjectEnvValueRef {
710
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
711
+ const kind = (value as { kind?: unknown }).kind;
712
+ if (kind === "row") return exactWireObject(value, ["kind"]) !== null;
713
+ if (kind === "staged") {
714
+ const object = exactWireObject(value, ["kind", "stagingId"]);
715
+ return (
716
+ object !== null &&
717
+ typeof object.stagingId === "string" &&
718
+ PROJECT_ENV_STAGING_ID_RE.test(object.stagingId)
719
+ );
720
+ }
721
+ if (kind === "legacy") {
722
+ const object = exactWireObject(value, ["kind", "ssmVersion"]);
723
+ return object !== null && isPositiveSafeInteger(object.ssmVersion);
724
+ }
725
+ return false;
726
+ }
727
+
728
+ /** The heads the head table reaches, and only those (ADR-153 §4): a live
729
+ * head with both counters positive; a tombstone with both at 2 or more, since
730
+ * a delete or removal advances both from a live head; and a fence, which is
731
+ * exactly `r1, g0`, since nothing but a first save moves it. */
732
+ export function isProjectEnvHead(value: unknown): value is ProjectEnvHead {
733
+ const object = exactWireObject(value, [
734
+ "revision",
735
+ "generation",
736
+ "tombstone",
737
+ "valueRef",
738
+ ]);
739
+ if (object === null) return false;
740
+ const { revision, generation, tombstone, valueRef } = object;
741
+ if (tombstone === false) {
742
+ return (
743
+ isPositiveSafeInteger(revision) &&
744
+ isPositiveSafeInteger(generation) &&
745
+ isProjectEnvValueRef(valueRef)
746
+ );
747
+ }
748
+ if (tombstone !== true || valueRef !== null) return false;
749
+ if (generation === 0) return revision === 1;
750
+ return (
751
+ isPositiveSafeInteger(revision) &&
752
+ isPositiveSafeInteger(generation) &&
753
+ revision >= 2 &&
754
+ generation >= 2
755
+ );
756
+ }
757
+
758
+ /** Whether a head is a removal's fence: the one record that has never held a
759
+ * value, so a save expecting it is a first save (ADR-153 §4). */
760
+ export function isProjectEnvFence(head: ProjectEnvHead): boolean {
761
+ return head.generation === 0;
762
+ }
763
+
764
+ /** Whether two heads are the same, field by field. */
765
+ export function sameProjectEnvHead(
766
+ left: ProjectEnvHead,
767
+ right: ProjectEnvHead,
768
+ ): boolean {
769
+ if (
770
+ left.revision !== right.revision ||
771
+ left.generation !== right.generation ||
772
+ left.tombstone !== right.tombstone
773
+ ) {
774
+ return false;
775
+ }
776
+ const a = left.valueRef;
777
+ const b = right.valueRef;
778
+ if (a === null || b === null) return a === b;
779
+ if (a.kind !== b.kind) return false;
780
+ if (a.kind === "staged") {
781
+ return b.kind === "staged" && a.stagingId === b.stagingId;
782
+ }
783
+ if (a.kind === "legacy") {
784
+ return b.kind === "legacy" && a.ssmVersion === b.ssmVersion;
785
+ }
786
+ return true;
787
+ }
788
+
789
+ export function isProjectEnvDeliveryCursor(
790
+ value: unknown,
791
+ ): value is ProjectEnvDeliveryCursor {
792
+ const object = exactWireObject(value, ["streamId", "sequence"]);
793
+ return Boolean(
794
+ object &&
795
+ isHostOpId(object.streamId) &&
796
+ isNonNegativeSafeInteger(object.sequence),
797
+ );
798
+ }
799
+
800
+ export function isProjectEnvBindingState(
801
+ value: unknown,
802
+ ): value is ProjectEnvBindingState {
803
+ const object = exactWireObject(value, [
804
+ "key",
805
+ "head",
806
+ "binding",
807
+ "policyEpoch",
808
+ "listingGeneration",
809
+ ]);
810
+ if (
811
+ object === null ||
812
+ !isProjectEnvKey(object.key) ||
813
+ !isProjectEnvHead(object.head)
814
+ ) {
815
+ return false;
816
+ }
817
+ const { head, binding, policyEpoch, listingGeneration } = object;
818
+ if (!isNonNegativeSafeInteger(listingGeneration)) return false;
819
+
820
+ // A fence: no binding, no epoch, and a removal's listing generation.
821
+ if (isProjectEnvFence(head)) {
822
+ return (
823
+ binding === null &&
824
+ policyEpoch === null &&
825
+ listingGeneration >= 2 &&
826
+ listingGeneration % 2 === 0
827
+ );
828
+ }
829
+ if (
830
+ !isPositiveSafeInteger(policyEpoch) ||
831
+ !(binding === null || isProjectEnvBinding(binding))
832
+ ) {
833
+ return false;
834
+ }
835
+
836
+ // Generation zero exists only for the two states migration can create:
837
+ // an existing live row, or a backfilled legacy row a late-write sweep then
838
+ // tombstoned. Neither has ever been approved, and staging is feature-era.
839
+ if (listingGeneration === 0) {
840
+ if (
841
+ binding !== null ||
842
+ policyEpoch !== INITIAL_PROJECT_ENV_POLICY_EPOCH
843
+ ) {
844
+ return false;
845
+ }
846
+ if (!head.tombstone) {
847
+ return (
848
+ head.revision === 1 &&
849
+ head.generation === 1 &&
850
+ (head.valueRef.kind === "row" || head.valueRef.kind === "legacy")
851
+ );
852
+ }
853
+ return head.revision === 2 && head.generation === 2;
854
+ }
855
+
856
+ // A live value belongs to a listed key. A tombstone can carry either the
857
+ // listed generation of a value delete or an even removal generation.
858
+ return head.tombstone || listingGeneration % 2 === 1;
859
+ }
860
+
861
+ export function isProjectEnvBindingStates(
862
+ value: unknown,
863
+ keys?: readonly string[],
864
+ ): value is ProjectEnvBindingState[] {
865
+ if (
866
+ !isDenseArray(value) ||
867
+ !value.every(isProjectEnvBindingState)
868
+ ) {
869
+ return false;
870
+ }
871
+ const stateKeys = value.map((state) => state.key);
872
+ if (new Set(stateKeys).size !== stateKeys.length) return false;
873
+ // Tombstones and fences are reported too; the live records are exactly
874
+ // the ack's keys.
875
+ const liveKeys = value
876
+ .filter((state) => !state.head.tombstone)
877
+ .map((state) => state.key);
878
+ return (
879
+ keys === undefined ||
880
+ (isProjectEnvKeys(keys) &&
881
+ liveKeys.length === keys.length &&
882
+ liveKeys.every((key, index) => key === keys[index]))
883
+ );
884
+ }
885
+
886
+ /** One reconciliation reads a stable page walk from each project under this
887
+ * id. `streamId` names the cloud-authored ordered mutation stream the runner
888
+ * must drain before the attempt may open its delivery gate. */
889
+ export interface ProjectEnvReconciliationAttempt {
890
+ attemptId: HostOpId;
891
+ streamId: HostOpId;
892
+ }
893
+
894
+ export interface ProjectEnvRecordVersion {
895
+ projectId: string;
896
+ recordVersion: number;
897
+ }
898
+
899
+ type ProjectEnvListFrameBase = {
900
+ kind: "env.var.list";
901
+ opId: HostOpId;
902
+ projectId: string;
903
+ orgId?: string;
904
+ };
905
+
906
+ export type ProjectEnvListFrame = ProjectEnvListFrameBase &
907
+ (
908
+ | {
909
+ /** The exact legacy request; a bare list is never a modern page. */
910
+ after?: never;
911
+ refreshFromStore?: never;
912
+ reconciliationAttemptId?: never;
913
+ }
914
+ | {
915
+ /** Every owned-runner modern page belongs to an explicit attempt. */
916
+ after?: string;
917
+ refreshFromStore?: never;
918
+ reconciliationAttemptId: HostOpId;
919
+ }
920
+ );
921
+
922
+ /** Begins a new inventory attempt and closes the runner's delivery gate
923
+ * before any page is read. Separate from list so a runner with zero projects
924
+ * still has an active attempt and stale completion markers cannot open it. */
925
+ export interface ProjectEnvReconcileBeginFrame {
926
+ kind: "env.reconcile.begin";
927
+ opId: HostOpId;
928
+ attempt: ProjectEnvReconciliationAttempt;
929
+ }
930
+
931
+ type ProjectEnvSetFrameBase = {
932
+ kind: "env.var.set";
933
+ opId: HostOpId;
934
+ projectId: string;
935
+ key: string;
936
+ value: string;
937
+ orgId?: string;
938
+ };
939
+
940
+ type ProjectEnvLegacySetFrame = ProjectEnvSetFrameBase & {
941
+ expectedHead?: never;
942
+ initialBinding?: never;
943
+ policyEpoch?: never;
944
+ listingGeneration?: never;
945
+ delivery?: never;
946
+ reconciliationAttemptId?: never;
947
+ };
948
+
949
+ type ProjectEnvModernSetFrameBase = ProjectEnvSetFrameBase & {
950
+ policyEpoch: number;
951
+ listingGeneration: number;
952
+ delivery: ProjectEnvDeliveryCursor;
953
+ reconciliationAttemptId?: HostOpId;
954
+ };
955
+
956
+ type ProjectEnvModernSetFrame = ProjectEnvModernSetFrameBase &
957
+ (
958
+ | {
959
+ /** No record is unambiguously a first save. */
960
+ expectedHead: null;
961
+ initialBinding: ProjectEnvBinding | null;
962
+ }
963
+ | {
964
+ /** A fence is also a first save, enforced by the runtime guard because
965
+ * TypeScript cannot exclude numeric generation zero from `number`. */
966
+ expectedHead: ProjectEnvHead;
967
+ initialBinding?: ProjectEnvBinding | null;
968
+ }
969
+ );
970
+
971
+ export type ProjectEnvSetFrame =
972
+ | ProjectEnvLegacySetFrame
973
+ | ProjectEnvModernSetFrame;
974
+
975
+ type ProjectEnvDeleteFrameBase = {
976
+ kind: "env.var.delete";
977
+ opId: HostOpId;
978
+ projectId: string;
979
+ key: string;
980
+ };
981
+
982
+ type ProjectEnvLegacyDeleteFrame = ProjectEnvDeleteFrameBase & {
983
+ expectedHead?: never;
984
+ policyEpoch?: never;
985
+ listingGeneration?: never;
986
+ delivery?: never;
987
+ reconciliationAttemptId?: never;
988
+ };
989
+
990
+ type ProjectEnvModernDeleteFrame = ProjectEnvDeleteFrameBase & {
991
+ expectedHead: ProjectEnvLiveHead;
992
+ policyEpoch: number;
993
+ listingGeneration: number;
994
+ delivery: ProjectEnvDeliveryCursor;
995
+ reconciliationAttemptId?: HostOpId;
996
+ };
997
+
998
+ export type ProjectEnvDeleteFrame =
999
+ | ProjectEnvLegacyDeleteFrame
1000
+ | ProjectEnvModernDeleteFrame;
1001
+
1002
+ /** One project-wide removal as applied to an owned runner. It fences a
1003
+ * missing row, tombstones a live row, and is idempotent on a tombstone/fence. */
1004
+ export interface ProjectEnvRemoveFrame {
1005
+ kind: "env.var.remove";
1006
+ opId: HostOpId;
1007
+ projectId: string;
1008
+ key: string;
1009
+ listingGeneration: number;
1010
+ delivery: ProjectEnvDeliveryCursor;
1011
+ reconciliationAttemptId?: HostOpId;
1012
+ }
1013
+
1014
+ /** Owner-approved binding CAS. The cloud derives the binding; the browser
1015
+ * never supplies it. Approve is available only while a value is live. */
1016
+ export interface ProjectEnvBindingSetFrame {
1017
+ kind: "env.binding.set";
1018
+ opId: HostOpId;
1019
+ projectId: string;
1020
+ key: string;
1021
+ binding: ProjectEnvBinding | null;
1022
+ expectedHead: ProjectEnvLiveHead;
1023
+ policyEpoch: number;
1024
+ listingGeneration: number;
1025
+ delivery: ProjectEnvDeliveryCursor;
1026
+ orgId?: string;
1027
+ reconciliationAttemptId?: HostOpId;
1028
+ }
1029
+
1030
+ /** The cloud sends this only after a complete, stable inventory and after it
1031
+ * has applied every queued mutation through `highWater`. The runner accepts it
1032
+ * only for the named attempt, cursor and still-current project versions. */
1033
+ export interface ProjectEnvReconciledFrame {
1034
+ kind: "env.reconciled";
1035
+ opId: HostOpId;
1036
+ attemptId: HostOpId;
1037
+ highWater: ProjectEnvDeliveryCursor;
1038
+ versions: ProjectEnvRecordVersion[];
1039
+ }
1040
+
1041
+ /** Closes one reserved delivery sequence whose original operation can no
1042
+ * longer be replayed (notably a plaintext save lost with its cloud process).
1043
+ * If the operation already applied, this is an idempotent cursor read. */
1044
+ export interface ProjectEnvDeliverySkipFrame {
1045
+ kind: "env.delivery.skip";
1046
+ opId: HostOpId;
1047
+ delivery: ProjectEnvDeliveryCursor;
1048
+ reconciliationAttemptId?: HostOpId;
1049
+ }
1050
+
1051
+ /** The secret-blind cloud reserves a unique staging id before asking the
1052
+ * write-only sign-in box to create its immutable SSM parameter. The deadline
1053
+ * remains cloud-side: a late or ambiguous create is harmless garbage that the
1054
+ * permanent sweep removes, and host clock skew must not decide promotion. */
1055
+ export interface ProjectEnvStagingPutFrame {
1056
+ kind: "env.staging.put";
1057
+ opId: HostOpId;
1058
+ orgId: string;
1059
+ projectId: string;
1060
+ key: string;
1061
+ stagingId: string;
1062
+ value: string;
1063
+ }
1064
+
1065
+ export type ProjectEnvStagingErrorCode =
1066
+ | "staging_conflict"
1067
+ | "staging_invalid"
1068
+ | "staging_unavailable";
1069
+
1070
+ export function isProjectEnvStagingErrorCode(
1071
+ value: unknown,
1072
+ ): value is ProjectEnvStagingErrorCode {
1073
+ return (
1074
+ value === "staging_conflict" ||
1075
+ value === "staging_invalid" ||
1076
+ value === "staging_unavailable"
1077
+ );
1078
+ }
1079
+
1080
+ /** A success means the deterministic envelope exists at the unique path and
1081
+ * its tenant tags are in place. Values and store metadata never cross back. */
1082
+ export type ProjectEnvStagingAckFrame =
1083
+ | {
1084
+ kind: "env.staging.ack";
1085
+ opId: HostOpId;
1086
+ ok: true;
1087
+ stagingId: string;
1088
+ }
1089
+ | {
1090
+ kind: "env.staging.ack";
1091
+ opId: HostOpId;
1092
+ ok: false;
1093
+ stagingId: string;
1094
+ code: ProjectEnvStagingErrorCode;
1095
+ error: string;
1096
+ };
1097
+
1098
+ /** Uai Cloud is authoritative for task-machine records. A replica may read
1099
+ * only immutable staged values or one pinned legacy SSM version; `row` is an
1100
+ * owned-runner reference and is never meaningful on a sidecar. */
1101
+ export type ProjectEnvReplicaHead =
1102
+ | Extract<ProjectEnvHead, { tombstone: true }>
1103
+ | (Omit<ProjectEnvLiveHead, "valueRef"> & {
1104
+ valueRef: Exclude<ProjectEnvValueRef, { kind: "row" }>;
1105
+ });
1106
+
1107
+ type ProjectEnvReplicaBindingStateBase = Omit<
1108
+ ProjectEnvBindingState,
1109
+ "head"
1110
+ >;
1111
+ type ProjectEnvReplicaTombstoneState = ProjectEnvReplicaBindingStateBase & {
1112
+ head: Extract<ProjectEnvReplicaHead, { tombstone: true }>;
1113
+ };
1114
+ type ProjectEnvReplicaLiveState = ProjectEnvReplicaBindingStateBase & {
1115
+ head: Extract<ProjectEnvReplicaHead, { tombstone: false }>;
1116
+ };
1117
+ export type ProjectEnvReplicaBindingState =
1118
+ | ProjectEnvReplicaTombstoneState
1119
+ | ProjectEnvReplicaLiveState;
1120
+
1121
+ /** Close the sidecar's serving gate and start a whole-task replica snapshot.
1122
+ * Project ids are canonical (sorted, unique); the empty set is valid. */
1123
+ export interface ProjectEnvReplicaBeginFrame {
1124
+ kind: "env.replica.begin";
1125
+ opId: HostOpId;
1126
+ attempt: ProjectEnvReconciliationAttempt;
1127
+ projectIds: string[];
1128
+ }
1129
+
1130
+ /** One stable page of the cloud-authoritative project snapshot. `after` is
1131
+ * the exclusive cursor used for this page and is null on the first page. */
1132
+ export interface ProjectEnvReplicaPageFrame {
1133
+ kind: "env.replica.page";
1134
+ opId: HostOpId;
1135
+ attemptId: HostOpId;
1136
+ projectId: string;
1137
+ after: string | null;
1138
+ recordVersion: number;
1139
+ records: ProjectEnvReplicaBindingState[];
1140
+ complete: boolean;
1141
+ }
1142
+
1143
+ /** An ordered exact-state delta. Re-sending the current state under a later
1144
+ * delivery is also the durable materialisation retry for an unavailable SSM
1145
+ * value; applying its metadata is independent from fetching its value. */
1146
+ export interface ProjectEnvReplicaApplyFrame {
1147
+ kind: "env.replica.apply";
1148
+ opId: HostOpId;
1149
+ projectId: string;
1150
+ recordVersion: number;
1151
+ state: ProjectEnvReplicaBindingState;
1152
+ delivery: ProjectEnvDeliveryCursor;
1153
+ reconciliationAttemptId?: HostOpId;
1154
+ }
1155
+
1156
+ /** Activate the staged whole-task snapshot only after every delivery through
1157
+ * `highWater` has been applied. No page or delta changes the live view before
1158
+ * this marker commits. */
1159
+ export interface ProjectEnvReplicaCommitFrame {
1160
+ kind: "env.replica.commit";
1161
+ opId: HostOpId;
1162
+ attemptId: HostOpId;
1163
+ highWater: ProjectEnvDeliveryCursor;
1164
+ versions: ProjectEnvRecordVersion[];
1165
+ }
1166
+
1167
+ type ProjectEnvReplicaInstalled = { kind: "installed" };
1168
+ type ProjectEnvReplicaWithheld = {
1169
+ kind: "withheld";
1170
+ reason: ProjectEnvWithholdReason;
1171
+ };
1172
+ type ProjectEnvReplicaUnavailable = {
1173
+ kind: "unavailable";
1174
+ reason: ProjectEnvWithholdReason;
1175
+ };
1176
+ export type ProjectEnvReplicaMaterialization =
1177
+ | ProjectEnvReplicaInstalled
1178
+ | ProjectEnvReplicaWithheld
1179
+ | ProjectEnvReplicaUnavailable;
1180
+
1181
+ /** Exact retained metadata is echoed because a removal can advance only the
1182
+ * listing generation while leaving the head unchanged. Values never cross. */
1183
+ export type ProjectEnvReplicaResult =
1184
+ | {
1185
+ state: ProjectEnvReplicaTombstoneState;
1186
+ materialization: ProjectEnvReplicaWithheld;
1187
+ }
1188
+ | {
1189
+ state: ProjectEnvReplicaLiveState;
1190
+ materialization: ProjectEnvReplicaMaterialization;
1191
+ };
1192
+
1193
+ type ProjectEnvReplicaErrorAckFrame = {
1194
+ kind: "env.replica.ack";
1195
+ opId: HostOpId;
1196
+ ok: false;
1197
+ code: ProjectEnvWithholdReason;
1198
+ error: string;
1199
+ policyEpochs: number[];
1200
+ appliedThrough: ProjectEnvDeliveryCursor;
1201
+ } &
1202
+ (
1203
+ | { phase: "applied"; attemptId: HostOpId | null }
1204
+ | {
1205
+ phase: "begun" | "page" | "complete";
1206
+ attemptId: HostOpId;
1207
+ }
1208
+ );
1209
+
1210
+ export type ProjectEnvReplicaAckFrame =
1211
+ | {
1212
+ kind: "env.replica.ack";
1213
+ opId: HostOpId;
1214
+ ok: true;
1215
+ phase: "begun";
1216
+ attemptId: HostOpId;
1217
+ policyEpochs: number[];
1218
+ appliedThrough: ProjectEnvDeliveryCursor;
1219
+ projectId?: never;
1220
+ after?: never;
1221
+ recordVersion?: never;
1222
+ results?: never;
1223
+ complete?: never;
1224
+ result?: never;
1225
+ highWater?: never;
1226
+ code?: never;
1227
+ error?: never;
1228
+ }
1229
+ | {
1230
+ kind: "env.replica.ack";
1231
+ opId: HostOpId;
1232
+ ok: true;
1233
+ phase: "page";
1234
+ attemptId: HostOpId;
1235
+ projectId: string;
1236
+ after: string | null;
1237
+ recordVersion: number;
1238
+ results: ProjectEnvReplicaResult[];
1239
+ complete: boolean;
1240
+ policyEpochs: number[];
1241
+ appliedThrough: ProjectEnvDeliveryCursor;
1242
+ result?: never;
1243
+ highWater?: never;
1244
+ code?: never;
1245
+ error?: never;
1246
+ }
1247
+ | {
1248
+ kind: "env.replica.ack";
1249
+ opId: HostOpId;
1250
+ ok: true;
1251
+ phase: "applied";
1252
+ attemptId: HostOpId | null;
1253
+ projectId: string;
1254
+ recordVersion: number;
1255
+ result: ProjectEnvReplicaResult;
1256
+ policyEpochs: number[];
1257
+ appliedThrough: ProjectEnvDeliveryCursor;
1258
+ after?: never;
1259
+ results?: never;
1260
+ complete?: never;
1261
+ highWater?: never;
1262
+ code?: never;
1263
+ error?: never;
1264
+ }
1265
+ | {
1266
+ kind: "env.replica.ack";
1267
+ opId: HostOpId;
1268
+ ok: true;
1269
+ phase: "complete";
1270
+ attemptId: HostOpId;
1271
+ highWater: ProjectEnvDeliveryCursor;
1272
+ policyEpochs: number[];
1273
+ appliedThrough: ProjectEnvDeliveryCursor;
1274
+ projectId?: never;
1275
+ after?: never;
1276
+ recordVersion?: never;
1277
+ results?: never;
1278
+ complete?: never;
1279
+ result?: never;
1280
+ code?: never;
1281
+ error?: never;
1282
+ }
1283
+ | ProjectEnvReplicaErrorAckFrame;
1284
+
1285
+ type ProjectEnvAckBase = {
1286
+ kind: "env.var.ack";
1287
+ opId: HostOpId;
1288
+ ok: true;
1289
+ keys: string[];
1290
+ };
1291
+
1292
+ type ProjectEnvAckSuccessOnly = {
1293
+ code?: never;
1294
+ error?: never;
1295
+ };
1296
+
1297
+ export type ProjectEnvLegacyAckFrame = ProjectEnvAckBase &
1298
+ ProjectEnvAckSuccessOnly & {
1299
+ bindings?: never;
1300
+ policyEpochs?: never;
1301
+ recordVersion?: never;
1302
+ appliedThrough?: never;
1303
+ complete?: never;
1304
+ reconciliationAttemptId?: never;
1305
+ };
1306
+
1307
+ export type ProjectEnvMutationAckFrame = ProjectEnvAckBase &
1308
+ ProjectEnvAckSuccessOnly & {
1309
+ bindings: [ProjectEnvBindingState];
1310
+ policyEpochs: number[];
1311
+ recordVersion: number;
1312
+ appliedThrough: ProjectEnvDeliveryCursor;
1313
+ complete?: never;
1314
+ reconciliationAttemptId?: HostOpId;
1315
+ };
1316
+
1317
+ export type ProjectEnvListAckFrame = ProjectEnvAckBase &
1318
+ ProjectEnvAckSuccessOnly & {
1319
+ bindings: ProjectEnvBindingState[];
1320
+ policyEpochs: number[];
1321
+ recordVersion: number;
1322
+ appliedThrough: ProjectEnvDeliveryCursor;
1323
+ complete: boolean;
1324
+ reconciliationAttemptId?: HostOpId;
1325
+ };
1326
+
1327
+ type ProjectEnvLegacyErrorAckFrame = {
1328
+ kind: "env.var.ack";
1329
+ opId: HostOpId;
1330
+ ok: false;
1331
+ error: string;
1332
+ keys?: never;
1333
+ bindings?: never;
1334
+ policyEpochs?: never;
1335
+ recordVersion?: never;
1336
+ appliedThrough?: never;
1337
+ complete?: never;
1338
+ reconciliationAttemptId?: never;
1339
+ code?: never;
1340
+ };
1341
+
1342
+ type ProjectEnvModernErrorAckFrame = {
1343
+ kind: "env.var.ack";
1344
+ opId: HostOpId;
1345
+ ok: false;
1346
+ code: ProjectEnvWithholdReason;
1347
+ error: string;
1348
+ /** Current cursor after the attempted mutation. Equal to the request's
1349
+ * cursor only when a terminal refusal consumed its reservation. */
1350
+ appliedThrough: ProjectEnvDeliveryCursor;
1351
+ reconciliationAttemptId?: HostOpId;
1352
+ keys?: never;
1353
+ bindings?: never;
1354
+ policyEpochs?: never;
1355
+ recordVersion?: never;
1356
+ complete?: never;
1357
+ };
1358
+
1359
+ export type ProjectEnvAckFrame =
1360
+ | ProjectEnvLegacyAckFrame
1361
+ | ProjectEnvMutationAckFrame
1362
+ | ProjectEnvListAckFrame
1363
+ | ProjectEnvLegacyErrorAckFrame
1364
+ | ProjectEnvModernErrorAckFrame;
1365
+
1366
+ export type ProjectEnvReconcileAckFrame =
1367
+ | {
1368
+ kind: "env.reconcile.ack";
1369
+ opId: HostOpId;
1370
+ ok: true;
1371
+ phase: "begun";
1372
+ attemptId: HostOpId;
1373
+ appliedThrough: ProjectEnvDeliveryCursor;
1374
+ }
1375
+ | {
1376
+ kind: "env.reconcile.ack";
1377
+ opId: HostOpId;
1378
+ ok: true;
1379
+ phase: "complete";
1380
+ attemptId: HostOpId;
1381
+ highWater: ProjectEnvDeliveryCursor;
1382
+ appliedThrough: ProjectEnvDeliveryCursor;
1383
+ }
1384
+ | {
1385
+ kind: "env.reconcile.ack";
1386
+ opId: HostOpId;
1387
+ ok: false;
1388
+ attemptId: HostOpId;
1389
+ phase: "begun" | "complete";
1390
+ code: ProjectEnvWithholdReason;
1391
+ error: string;
1392
+ appliedThrough: ProjectEnvDeliveryCursor;
1393
+ };
1394
+
1395
+ export type ProjectEnvDeliveryAckFrame =
1396
+ | {
1397
+ kind: "env.delivery.ack";
1398
+ opId: HostOpId;
1399
+ ok: true;
1400
+ appliedThrough: ProjectEnvDeliveryCursor;
1401
+ }
1402
+ | {
1403
+ kind: "env.delivery.ack";
1404
+ opId: HostOpId;
1405
+ ok: false;
1406
+ code: ProjectEnvWithholdReason;
1407
+ error: string;
1408
+ appliedThrough: ProjectEnvDeliveryCursor;
1409
+ };
1410
+
1411
+ export function isProjectEnvReconciliationAttempt(
1412
+ value: unknown,
1413
+ ): value is ProjectEnvReconciliationAttempt {
1414
+ const object = exactWireObject(value, ["attemptId", "streamId"]);
1415
+ return Boolean(
1416
+ object &&
1417
+ isHostOpId(object.attemptId) &&
1418
+ isHostOpId(object.streamId),
1419
+ );
1420
+ }
1421
+
1422
+ export function isProjectEnvRecordVersions(
1423
+ value: unknown,
1424
+ ): value is ProjectEnvRecordVersion[] {
1425
+ if (!isDenseArray(value)) return false;
1426
+ let previous = "";
1427
+ for (const candidate of value) {
1428
+ const object = exactWireObject(candidate, ["projectId", "recordVersion"]);
1429
+ if (
1430
+ object === null ||
1431
+ !isProjectEnvProjectId(object.projectId) ||
1432
+ !isNonNegativeSafeInteger(object.recordVersion) ||
1433
+ (previous !== "" && previous >= object.projectId)
1434
+ ) {
1435
+ return false;
1436
+ }
1437
+ previous = object.projectId;
1438
+ }
1439
+ return true;
1440
+ }
1441
+
1442
+ export function isProjectEnvPolicyEpochs(value: unknown): value is number[] {
1443
+ return (
1444
+ isDenseArray(value) &&
1445
+ value.length > 0 &&
1446
+ value.length <= MAX_PROJECT_ENV_POLICY_EPOCHS &&
1447
+ value.every(
1448
+ (epoch, index) =>
1449
+ isPositiveSafeInteger(epoch) &&
1450
+ (index === 0 || (value[index - 1] as number) < epoch),
1451
+ )
1452
+ );
1453
+ }
1454
+
1455
+ export function isProjectEnvProjectIds(value: unknown): value is string[] {
1456
+ return (
1457
+ isDenseArray(value) &&
1458
+ value.length <= MAX_PROJECT_ENV_SCHEMA_REPORTS &&
1459
+ value.every(
1460
+ (projectId, index) =>
1461
+ isProjectEnvProjectId(projectId) &&
1462
+ (index === 0 || (value[index - 1] as string) < projectId),
1463
+ )
1464
+ );
1465
+ }
1466
+
1467
+ export function isProjectEnvReplicaBindingState(
1468
+ value: unknown,
1469
+ ): value is ProjectEnvReplicaBindingState {
1470
+ return (
1471
+ isProjectEnvBindingState(value) &&
1472
+ (value.head.tombstone || value.head.valueRef.kind !== "row")
1473
+ );
1474
+ }
1475
+
1476
+ function isProjectEnvReplicaBindingStates(
1477
+ value: unknown,
1478
+ ): value is ProjectEnvReplicaBindingState[] {
1479
+ return (
1480
+ isDenseArray(value) &&
1481
+ value.every(isProjectEnvReplicaBindingState) &&
1482
+ value.every(
1483
+ (state, index) => index === 0 || value[index - 1]!.key < state.key,
1484
+ )
1485
+ );
1486
+ }
1487
+
1488
+ export function isProjectEnvReplicaMaterialization(
1489
+ value: unknown,
1490
+ ): value is ProjectEnvReplicaMaterialization {
1491
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1492
+ const kind = (value as { kind?: unknown }).kind;
1493
+ if (kind === "installed") {
1494
+ return exactWireObject(value, ["kind"]) !== null;
1495
+ }
1496
+ if (kind === "withheld" || kind === "unavailable") {
1497
+ const object = exactWireObject(value, ["kind", "reason"]);
1498
+ return object !== null && isProjectEnvReason(object.reason);
1499
+ }
1500
+ return false;
1501
+ }
1502
+
1503
+ export function isProjectEnvReplicaResult(
1504
+ value: unknown,
1505
+ ): value is ProjectEnvReplicaResult {
1506
+ const object = exactWireObject(value, ["state", "materialization"]);
1507
+ if (
1508
+ object === null ||
1509
+ !isProjectEnvReplicaBindingState(object.state) ||
1510
+ !isProjectEnvReplicaMaterialization(object.materialization)
1511
+ ) {
1512
+ return false;
1513
+ }
1514
+ return object.state.head.tombstone
1515
+ ? object.materialization.kind === "withheld"
1516
+ : true;
1517
+ }
1518
+
1519
+ function isProjectEnvReplicaResults(
1520
+ value: unknown,
1521
+ ): value is ProjectEnvReplicaResult[] {
1522
+ return (
1523
+ isDenseArray(value) &&
1524
+ value.every(isProjectEnvReplicaResult) &&
1525
+ value.every(
1526
+ (result, index) =>
1527
+ index === 0 || value[index - 1]!.state.key < result.state.key,
1528
+ )
1529
+ );
1530
+ }
1531
+
1532
+ function isProjectEnvPageWithinBound(value: unknown): boolean {
1533
+ try {
1534
+ const encoded = JSON.stringify(value);
1535
+ return (
1536
+ typeof encoded === "string" &&
1537
+ utf8ByteLength(encoded) <= MAX_PROJECT_ENV_PAGE_BYTES
1538
+ );
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1543
+
1544
+ function isProjectEnvMutationDelivery(
1545
+ value: unknown,
1546
+ ): value is ProjectEnvDeliveryCursor {
1547
+ return isProjectEnvDeliveryCursor(value) && value.sequence > 0;
1548
+ }
1549
+
1550
+ function isProjectEnvOrgId(value: unknown): value is string {
1551
+ return (
1552
+ typeof value === "string" && value.length > 0 && value.length <= 128
1553
+ );
1554
+ }
1555
+
1556
+ export function isProjectEnvListFrame(
1557
+ value: unknown,
1558
+ ): value is ProjectEnvListFrame {
1559
+ const frame = exactWireObject(
1560
+ value,
1561
+ ["kind", "opId", "projectId"],
1562
+ ["orgId", "after", "reconciliationAttemptId"],
1563
+ );
1564
+ if (
1565
+ frame === null ||
1566
+ frame.kind !== "env.var.list" ||
1567
+ !isHostOpId(frame.opId) ||
1568
+ !isProjectEnvProjectId(frame.projectId) ||
1569
+ (frame.orgId !== undefined && !isProjectEnvOrgId(frame.orgId)) ||
1570
+ (frame.after !== undefined && !isProjectEnvKey(frame.after)) ||
1571
+ (frame.reconciliationAttemptId !== undefined &&
1572
+ !isHostOpId(frame.reconciliationAttemptId))
1573
+ ) {
1574
+ return false;
1575
+ }
1576
+ // A bare list is the exact legacy request. Every modern page belongs to an
1577
+ // explicit attempt, so shutdown and dispatch never have to guess.
1578
+ if (frame.reconciliationAttemptId !== undefined) return true;
1579
+ return frame.after === undefined;
1580
+ }
1581
+
1582
+ export function isProjectEnvReconcileBeginFrame(
1583
+ value: unknown,
1584
+ ): value is ProjectEnvReconcileBeginFrame {
1585
+ const frame = exactWireObject(value, ["kind", "opId", "attempt"]);
1586
+ return Boolean(
1587
+ frame &&
1588
+ frame.kind === "env.reconcile.begin" &&
1589
+ isHostOpId(frame.opId) &&
1590
+ isProjectEnvReconciliationAttempt(frame.attempt),
1591
+ );
1592
+ }
1593
+
1594
+ export function isProjectEnvSetFrame(
1595
+ value: unknown,
1596
+ ): value is ProjectEnvSetFrame {
1597
+ const base = exactWireObject(
1598
+ value,
1599
+ ["kind", "opId", "projectId", "key", "value"],
1600
+ [
1601
+ "orgId",
1602
+ "expectedHead",
1603
+ "initialBinding",
1604
+ "policyEpoch",
1605
+ "listingGeneration",
1606
+ "delivery",
1607
+ "reconciliationAttemptId",
1608
+ ],
1609
+ );
1610
+ if (
1611
+ base === null ||
1612
+ base.kind !== "env.var.set" ||
1613
+ !isHostOpId(base.opId) ||
1614
+ !isProjectEnvProjectId(base.projectId) ||
1615
+ !isProjectEnvKey(base.key) ||
1616
+ typeof base.value !== "string" ||
1617
+ (base.orgId !== undefined && !isProjectEnvOrgId(base.orgId))
1618
+ ) {
1619
+ return false;
1620
+ }
1621
+ const modernFields = [
1622
+ "expectedHead",
1623
+ "initialBinding",
1624
+ "policyEpoch",
1625
+ "listingGeneration",
1626
+ "delivery",
1627
+ "reconciliationAttemptId",
1628
+ ].filter((key) => Object.hasOwn(base, key));
1629
+ if (modernFields.length === 0) return true;
1630
+ if (
1631
+ !Object.hasOwn(base, "expectedHead") ||
1632
+ !Object.hasOwn(base, "policyEpoch") ||
1633
+ !Object.hasOwn(base, "listingGeneration") ||
1634
+ !Object.hasOwn(base, "delivery") ||
1635
+ !(base.expectedHead === null || isProjectEnvHead(base.expectedHead)) ||
1636
+ !isPositiveSafeInteger(base.policyEpoch) ||
1637
+ !isPositiveSafeInteger(base.listingGeneration) ||
1638
+ base.listingGeneration % 2 !== 1 ||
1639
+ !isProjectEnvMutationDelivery(base.delivery) ||
1640
+ (base.reconciliationAttemptId !== undefined &&
1641
+ !isHostOpId(base.reconciliationAttemptId))
1642
+ ) {
1643
+ return false;
1644
+ }
1645
+ const firstSave =
1646
+ base.expectedHead === null ||
1647
+ (isProjectEnvHead(base.expectedHead) && isProjectEnvFence(base.expectedHead));
1648
+ const hasInitialBinding = Object.hasOwn(base, "initialBinding");
1649
+ return firstSave
1650
+ ? hasInitialBinding &&
1651
+ (base.initialBinding === null || isProjectEnvBinding(base.initialBinding))
1652
+ : !hasInitialBinding;
1653
+ }
1654
+
1655
+ export function isProjectEnvDeleteFrame(
1656
+ value: unknown,
1657
+ ): value is ProjectEnvDeleteFrame {
1658
+ const frame = exactWireObject(
1659
+ value,
1660
+ ["kind", "opId", "projectId", "key"],
1661
+ [
1662
+ "expectedHead",
1663
+ "policyEpoch",
1664
+ "listingGeneration",
1665
+ "delivery",
1666
+ "reconciliationAttemptId",
1667
+ ],
1668
+ );
1669
+ if (
1670
+ frame === null ||
1671
+ frame.kind !== "env.var.delete" ||
1672
+ !isHostOpId(frame.opId) ||
1673
+ !isProjectEnvProjectId(frame.projectId) ||
1674
+ !isProjectEnvKey(frame.key)
1675
+ ) {
1676
+ return false;
1677
+ }
1678
+ const modernFields = [
1679
+ "expectedHead",
1680
+ "policyEpoch",
1681
+ "listingGeneration",
1682
+ "delivery",
1683
+ "reconciliationAttemptId",
1684
+ ].filter((key) => Object.hasOwn(frame, key));
1685
+ if (modernFields.length === 0) return true;
1686
+ return (
1687
+ Object.hasOwn(frame, "expectedHead") &&
1688
+ Object.hasOwn(frame, "policyEpoch") &&
1689
+ Object.hasOwn(frame, "listingGeneration") &&
1690
+ Object.hasOwn(frame, "delivery") &&
1691
+ isProjectEnvHead(frame.expectedHead) &&
1692
+ !frame.expectedHead.tombstone &&
1693
+ isPositiveSafeInteger(frame.policyEpoch) &&
1694
+ isPositiveSafeInteger(frame.listingGeneration) &&
1695
+ frame.listingGeneration % 2 === 1 &&
1696
+ isProjectEnvMutationDelivery(frame.delivery) &&
1697
+ (frame.reconciliationAttemptId === undefined ||
1698
+ isHostOpId(frame.reconciliationAttemptId))
1699
+ );
1700
+ }
1701
+
1702
+ export function isProjectEnvRemoveFrame(
1703
+ value: unknown,
1704
+ ): value is ProjectEnvRemoveFrame {
1705
+ const frame = exactWireObject(
1706
+ value,
1707
+ [
1708
+ "kind",
1709
+ "opId",
1710
+ "projectId",
1711
+ "key",
1712
+ "listingGeneration",
1713
+ "delivery",
1714
+ ],
1715
+ ["reconciliationAttemptId"],
1716
+ );
1717
+ return Boolean(
1718
+ frame &&
1719
+ frame.kind === "env.var.remove" &&
1720
+ isHostOpId(frame.opId) &&
1721
+ isProjectEnvProjectId(frame.projectId) &&
1722
+ isProjectEnvKey(frame.key) &&
1723
+ isPositiveSafeInteger(frame.listingGeneration) &&
1724
+ (frame.listingGeneration as number) >= 2 &&
1725
+ (frame.listingGeneration as number) % 2 === 0 &&
1726
+ isProjectEnvMutationDelivery(frame.delivery) &&
1727
+ (frame.reconciliationAttemptId === undefined ||
1728
+ isHostOpId(frame.reconciliationAttemptId)),
1729
+ );
1730
+ }
1731
+
1732
+ export function isProjectEnvBindingSetFrame(
1733
+ value: unknown,
1734
+ ): value is ProjectEnvBindingSetFrame {
1735
+ const frame = exactWireObject(
1736
+ value,
1737
+ [
1738
+ "kind",
1739
+ "opId",
1740
+ "projectId",
1741
+ "key",
1742
+ "binding",
1743
+ "expectedHead",
1744
+ "policyEpoch",
1745
+ "listingGeneration",
1746
+ "delivery",
1747
+ ],
1748
+ ["orgId", "reconciliationAttemptId"],
1749
+ );
1750
+ return Boolean(
1751
+ frame &&
1752
+ frame.kind === "env.binding.set" &&
1753
+ isHostOpId(frame.opId) &&
1754
+ isProjectEnvProjectId(frame.projectId) &&
1755
+ isProjectEnvKey(frame.key) &&
1756
+ (frame.binding === null || isProjectEnvBinding(frame.binding)) &&
1757
+ isProjectEnvHead(frame.expectedHead) &&
1758
+ !frame.expectedHead.tombstone &&
1759
+ isPositiveSafeInteger(frame.policyEpoch) &&
1760
+ isPositiveSafeInteger(frame.listingGeneration) &&
1761
+ (frame.listingGeneration as number) % 2 === 1 &&
1762
+ isProjectEnvMutationDelivery(frame.delivery) &&
1763
+ (frame.orgId === undefined || isProjectEnvOrgId(frame.orgId)) &&
1764
+ (frame.reconciliationAttemptId === undefined ||
1765
+ isHostOpId(frame.reconciliationAttemptId)),
1766
+ );
1767
+ }
1768
+
1769
+ export function isProjectEnvReconciledFrame(
1770
+ value: unknown,
1771
+ ): value is ProjectEnvReconciledFrame {
1772
+ const frame = exactWireObject(value, [
1773
+ "kind",
1774
+ "opId",
1775
+ "attemptId",
1776
+ "highWater",
1777
+ "versions",
1778
+ ]);
1779
+ return Boolean(
1780
+ frame &&
1781
+ frame.kind === "env.reconciled" &&
1782
+ isHostOpId(frame.opId) &&
1783
+ isHostOpId(frame.attemptId) &&
1784
+ isProjectEnvDeliveryCursor(frame.highWater) &&
1785
+ isProjectEnvRecordVersions(frame.versions),
1786
+ );
1787
+ }
1788
+
1789
+ export function isProjectEnvDeliverySkipFrame(
1790
+ value: unknown,
1791
+ ): value is ProjectEnvDeliverySkipFrame {
1792
+ const frame = exactWireObject(
1793
+ value,
1794
+ ["kind", "opId", "delivery"],
1795
+ ["reconciliationAttemptId"],
1796
+ );
1797
+ return Boolean(
1798
+ frame &&
1799
+ frame.kind === "env.delivery.skip" &&
1800
+ isHostOpId(frame.opId) &&
1801
+ isProjectEnvMutationDelivery(frame.delivery) &&
1802
+ (frame.reconciliationAttemptId === undefined ||
1803
+ isHostOpId(frame.reconciliationAttemptId)),
1804
+ );
1805
+ }
1806
+
1807
+ export function isProjectEnvStagingPutFrame(
1808
+ value: unknown,
1809
+ ): value is ProjectEnvStagingPutFrame {
1810
+ const frame = exactWireObject(value, [
1811
+ "kind",
1812
+ "opId",
1813
+ "orgId",
1814
+ "projectId",
1815
+ "key",
1816
+ "stagingId",
1817
+ "value",
1818
+ ]);
1819
+ return Boolean(
1820
+ frame &&
1821
+ frame.kind === "env.staging.put" &&
1822
+ isHostOpId(frame.opId) &&
1823
+ isProjectEnvOrgId(frame.orgId) &&
1824
+ PROJECT_ENV_STORE_PATH_SEGMENT_RE.test(frame.orgId) &&
1825
+ isProjectEnvProjectId(frame.projectId) &&
1826
+ PROJECT_ENV_STORE_PATH_SEGMENT_RE.test(frame.projectId) &&
1827
+ isProjectEnvKey(frame.key) &&
1828
+ typeof frame.stagingId === "string" &&
1829
+ PROJECT_ENV_STAGING_ID_RE.test(frame.stagingId) &&
1830
+ typeof frame.value === "string",
1831
+ );
1832
+ }
1833
+
1834
+ export function isProjectEnvStagingAckFrame(
1835
+ value: unknown,
1836
+ ): value is ProjectEnvStagingAckFrame {
1837
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1838
+ const candidate = value as WireObject;
1839
+ if (candidate.ok === true) {
1840
+ const frame = exactWireObject(value, [
1841
+ "kind",
1842
+ "opId",
1843
+ "ok",
1844
+ "stagingId",
1845
+ ]);
1846
+ return Boolean(
1847
+ frame &&
1848
+ frame.kind === "env.staging.ack" &&
1849
+ isHostOpId(frame.opId) &&
1850
+ typeof frame.stagingId === "string" &&
1851
+ PROJECT_ENV_STAGING_ID_RE.test(frame.stagingId),
1852
+ );
1853
+ }
1854
+ if (candidate.ok === false) {
1855
+ const frame = exactWireObject(value, [
1856
+ "kind",
1857
+ "opId",
1858
+ "ok",
1859
+ "stagingId",
1860
+ "code",
1861
+ "error",
1862
+ ]);
1863
+ return Boolean(
1864
+ frame &&
1865
+ frame.kind === "env.staging.ack" &&
1866
+ isHostOpId(frame.opId) &&
1867
+ typeof frame.stagingId === "string" &&
1868
+ PROJECT_ENV_STAGING_ID_RE.test(frame.stagingId) &&
1869
+ isProjectEnvStagingErrorCode(frame.code) &&
1870
+ isBoundedMessage(frame.error),
1871
+ );
1872
+ }
1873
+ return false;
1874
+ }
1875
+
1876
+ export function isProjectEnvReplicaBeginFrame(
1877
+ value: unknown,
1878
+ ): value is ProjectEnvReplicaBeginFrame {
1879
+ const frame = exactWireObject(value, [
1880
+ "kind",
1881
+ "opId",
1882
+ "attempt",
1883
+ "projectIds",
1884
+ ]);
1885
+ return Boolean(
1886
+ frame &&
1887
+ frame.kind === "env.replica.begin" &&
1888
+ isHostOpId(frame.opId) &&
1889
+ isProjectEnvReconciliationAttempt(frame.attempt) &&
1890
+ isProjectEnvProjectIds(frame.projectIds),
1891
+ );
1892
+ }
1893
+
1894
+ export function isProjectEnvReplicaPageFrame(
1895
+ value: unknown,
1896
+ ): value is ProjectEnvReplicaPageFrame {
1897
+ const frame = exactWireObject(value, [
1898
+ "kind",
1899
+ "opId",
1900
+ "attemptId",
1901
+ "projectId",
1902
+ "after",
1903
+ "recordVersion",
1904
+ "records",
1905
+ "complete",
1906
+ ]);
1907
+ if (
1908
+ frame === null ||
1909
+ frame.kind !== "env.replica.page" ||
1910
+ !isHostOpId(frame.opId) ||
1911
+ !isHostOpId(frame.attemptId) ||
1912
+ !isProjectEnvProjectId(frame.projectId) ||
1913
+ !(frame.after === null || isProjectEnvKey(frame.after)) ||
1914
+ !isNonNegativeSafeInteger(frame.recordVersion) ||
1915
+ !isProjectEnvReplicaBindingStates(frame.records) ||
1916
+ typeof frame.complete !== "boolean"
1917
+ ) {
1918
+ return false;
1919
+ }
1920
+ const records = frame.records as ProjectEnvReplicaBindingState[];
1921
+ if (records.length === 0) {
1922
+ if (!(frame.complete && frame.after === null)) return false;
1923
+ } else if (
1924
+ frame.after !== null &&
1925
+ records[0]!.key <= frame.after
1926
+ ) {
1927
+ return false;
1928
+ }
1929
+ return isProjectEnvPageWithinBound(value);
1930
+ }
1931
+
1932
+ export function isProjectEnvReplicaApplyFrame(
1933
+ value: unknown,
1934
+ ): value is ProjectEnvReplicaApplyFrame {
1935
+ const frame = exactWireObject(
1936
+ value,
1937
+ [
1938
+ "kind",
1939
+ "opId",
1940
+ "projectId",
1941
+ "recordVersion",
1942
+ "state",
1943
+ "delivery",
1944
+ ],
1945
+ ["reconciliationAttemptId"],
1946
+ );
1947
+ return Boolean(
1948
+ frame &&
1949
+ frame.kind === "env.replica.apply" &&
1950
+ isHostOpId(frame.opId) &&
1951
+ isProjectEnvProjectId(frame.projectId) &&
1952
+ isPositiveSafeInteger(frame.recordVersion) &&
1953
+ isProjectEnvReplicaBindingState(frame.state) &&
1954
+ isProjectEnvMutationDelivery(frame.delivery) &&
1955
+ (frame.reconciliationAttemptId === undefined ||
1956
+ isHostOpId(frame.reconciliationAttemptId)),
1957
+ );
1958
+ }
1959
+
1960
+ export function isProjectEnvReplicaCommitFrame(
1961
+ value: unknown,
1962
+ ): value is ProjectEnvReplicaCommitFrame {
1963
+ const frame = exactWireObject(value, [
1964
+ "kind",
1965
+ "opId",
1966
+ "attemptId",
1967
+ "highWater",
1968
+ "versions",
1969
+ ]);
1970
+ return Boolean(
1971
+ frame &&
1972
+ frame.kind === "env.replica.commit" &&
1973
+ isHostOpId(frame.opId) &&
1974
+ isHostOpId(frame.attemptId) &&
1975
+ isProjectEnvDeliveryCursor(frame.highWater) &&
1976
+ isProjectEnvRecordVersions(frame.versions),
1977
+ );
1978
+ }
1979
+
1980
+ function hasSortedBindingStates(value: ProjectEnvBindingState[]): boolean {
1981
+ return value.every(
1982
+ (state, index) => index === 0 || value[index - 1]!.key < state.key,
1983
+ );
1984
+ }
1985
+
1986
+ export function isProjectEnvAckFrame(
1987
+ value: unknown,
1988
+ ): value is ProjectEnvAckFrame {
1989
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1990
+ const candidate = value as WireObject;
1991
+ if (candidate.ok === false) {
1992
+ if (Object.hasOwn(candidate, "appliedThrough")) {
1993
+ const frame = exactWireObject(value, [
1994
+ "kind",
1995
+ "opId",
1996
+ "ok",
1997
+ "code",
1998
+ "error",
1999
+ "appliedThrough",
2000
+ ], ["reconciliationAttemptId"]);
2001
+ return Boolean(
2002
+ frame &&
2003
+ frame.kind === "env.var.ack" &&
2004
+ isHostOpId(frame.opId) &&
2005
+ isProjectEnvReason(frame.code) &&
2006
+ isBoundedMessage(frame.error) &&
2007
+ isProjectEnvDeliveryCursor(frame.appliedThrough) &&
2008
+ (frame.reconciliationAttemptId === undefined ||
2009
+ isHostOpId(frame.reconciliationAttemptId)),
2010
+ );
2011
+ }
2012
+ const frame = exactWireObject(value, ["kind", "opId", "ok", "error"]);
2013
+ return Boolean(
2014
+ frame &&
2015
+ frame.kind === "env.var.ack" &&
2016
+ isHostOpId(frame.opId) &&
2017
+ isBoundedMessage(frame.error),
2018
+ );
2019
+ }
2020
+ if (candidate.ok !== true) return false;
2021
+ const legacy = exactWireObject(value, ["kind", "opId", "ok", "keys"]);
2022
+ if (legacy !== null) {
2023
+ return (
2024
+ legacy.kind === "env.var.ack" &&
2025
+ isHostOpId(legacy.opId) &&
2026
+ isProjectEnvKeys(legacy.keys)
2027
+ );
2028
+ }
2029
+ const frame = exactWireObject(
2030
+ value,
2031
+ [
2032
+ "kind",
2033
+ "opId",
2034
+ "ok",
2035
+ "keys",
2036
+ "bindings",
2037
+ "policyEpochs",
2038
+ "recordVersion",
2039
+ "appliedThrough",
2040
+ ],
2041
+ ["complete", "reconciliationAttemptId"],
2042
+ );
2043
+ if (
2044
+ frame === null ||
2045
+ frame.kind !== "env.var.ack" ||
2046
+ !isHostOpId(frame.opId) ||
2047
+ !isProjectEnvBindingStates(frame.bindings, frame.keys as readonly string[]) ||
2048
+ !hasSortedBindingStates(frame.bindings as ProjectEnvBindingState[]) ||
2049
+ !isProjectEnvPolicyEpochs(frame.policyEpochs) ||
2050
+ !isNonNegativeSafeInteger(frame.recordVersion) ||
2051
+ !isProjectEnvDeliveryCursor(frame.appliedThrough) ||
2052
+ (frame.reconciliationAttemptId !== undefined &&
2053
+ !isHostOpId(frame.reconciliationAttemptId))
2054
+ ) {
2055
+ return false;
2056
+ }
2057
+ const states = frame.bindings as ProjectEnvBindingState[];
2058
+ if (frame.complete === undefined) {
2059
+ return states.length === 1;
2060
+ }
2061
+ return (
2062
+ typeof frame.complete === "boolean" &&
2063
+ (frame.complete || states.length > 0)
2064
+ );
2065
+ }
2066
+
2067
+ export function isProjectEnvReconcileAckFrame(
2068
+ value: unknown,
2069
+ ): value is ProjectEnvReconcileAckFrame {
2070
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2071
+ const candidate = value as WireObject;
2072
+ if (candidate.ok === true) {
2073
+ const phase = candidate.phase;
2074
+ const frame = exactWireObject(value, [
2075
+ "kind",
2076
+ "opId",
2077
+ "ok",
2078
+ "phase",
2079
+ "attemptId",
2080
+ "appliedThrough",
2081
+ ...(phase === "complete" ? ["highWater"] : []),
2082
+ ]);
2083
+ return Boolean(
2084
+ frame &&
2085
+ frame.kind === "env.reconcile.ack" &&
2086
+ isHostOpId(frame.opId) &&
2087
+ (frame.phase === "begun" || frame.phase === "complete") &&
2088
+ isHostOpId(frame.attemptId) &&
2089
+ isProjectEnvDeliveryCursor(frame.appliedThrough) &&
2090
+ (frame.phase === "begun"
2091
+ ? frame.highWater === undefined
2092
+ : isProjectEnvDeliveryCursor(frame.highWater) &&
2093
+ frame.appliedThrough.streamId === frame.highWater.streamId &&
2094
+ frame.appliedThrough.sequence === frame.highWater.sequence),
2095
+ );
2096
+ }
2097
+ if (candidate.ok === false) {
2098
+ const frame = exactWireObject(value, [
2099
+ "kind",
2100
+ "opId",
2101
+ "ok",
2102
+ "attemptId",
2103
+ "phase",
2104
+ "code",
2105
+ "error",
2106
+ "appliedThrough",
2107
+ ]);
2108
+ return Boolean(
2109
+ frame &&
2110
+ frame.kind === "env.reconcile.ack" &&
2111
+ isHostOpId(frame.opId) &&
2112
+ isHostOpId(frame.attemptId) &&
2113
+ (frame.phase === "begun" || frame.phase === "complete") &&
2114
+ isProjectEnvReason(frame.code) &&
2115
+ isBoundedMessage(frame.error) &&
2116
+ isProjectEnvDeliveryCursor(frame.appliedThrough),
2117
+ );
2118
+ }
2119
+ return false;
2120
+ }
2121
+
2122
+ export function isProjectEnvDeliveryAckFrame(
2123
+ value: unknown,
2124
+ ): value is ProjectEnvDeliveryAckFrame {
2125
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2126
+ const candidate = value as WireObject;
2127
+ if (candidate.ok === true) {
2128
+ const frame = exactWireObject(value, [
2129
+ "kind",
2130
+ "opId",
2131
+ "ok",
2132
+ "appliedThrough",
2133
+ ]);
2134
+ return Boolean(
2135
+ frame &&
2136
+ frame.kind === "env.delivery.ack" &&
2137
+ isHostOpId(frame.opId) &&
2138
+ isProjectEnvDeliveryCursor(frame.appliedThrough),
2139
+ );
2140
+ }
2141
+ if (candidate.ok === false) {
2142
+ const frame = exactWireObject(value, [
2143
+ "kind",
2144
+ "opId",
2145
+ "ok",
2146
+ "code",
2147
+ "error",
2148
+ "appliedThrough",
2149
+ ]);
2150
+ return Boolean(
2151
+ frame &&
2152
+ frame.kind === "env.delivery.ack" &&
2153
+ isHostOpId(frame.opId) &&
2154
+ isProjectEnvReason(frame.code) &&
2155
+ isBoundedMessage(frame.error) &&
2156
+ isProjectEnvDeliveryCursor(frame.appliedThrough),
2157
+ );
2158
+ }
2159
+ return false;
2160
+ }
2161
+
2162
+ export function isProjectEnvReplicaAckFrame(
2163
+ value: unknown,
2164
+ ): value is ProjectEnvReplicaAckFrame {
2165
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2166
+ const candidate = value as WireObject;
2167
+ if (candidate.ok === false) {
2168
+ const frame = exactWireObject(value, [
2169
+ "kind",
2170
+ "opId",
2171
+ "ok",
2172
+ "phase",
2173
+ "attemptId",
2174
+ "code",
2175
+ "error",
2176
+ "policyEpochs",
2177
+ "appliedThrough",
2178
+ ]);
2179
+ if (
2180
+ frame === null ||
2181
+ frame.kind !== "env.replica.ack" ||
2182
+ !isHostOpId(frame.opId) ||
2183
+ !(
2184
+ frame.phase === "begun" ||
2185
+ frame.phase === "page" ||
2186
+ frame.phase === "applied" ||
2187
+ frame.phase === "complete"
2188
+ ) ||
2189
+ !(
2190
+ frame.attemptId === null || isHostOpId(frame.attemptId)
2191
+ ) ||
2192
+ (frame.phase !== "applied" && frame.attemptId === null) ||
2193
+ !isProjectEnvReason(frame.code) ||
2194
+ !isBoundedMessage(frame.error) ||
2195
+ !isProjectEnvPolicyEpochs(frame.policyEpochs) ||
2196
+ !isProjectEnvDeliveryCursor(frame.appliedThrough)
2197
+ ) {
2198
+ return false;
2199
+ }
2200
+ return true;
2201
+ }
2202
+ if (candidate.ok !== true) return false;
2203
+
2204
+ if (candidate.phase === "begun") {
2205
+ const frame = exactWireObject(value, [
2206
+ "kind",
2207
+ "opId",
2208
+ "ok",
2209
+ "phase",
2210
+ "attemptId",
2211
+ "policyEpochs",
2212
+ "appliedThrough",
2213
+ ]);
2214
+ return Boolean(
2215
+ frame &&
2216
+ frame.kind === "env.replica.ack" &&
2217
+ isHostOpId(frame.opId) &&
2218
+ isHostOpId(frame.attemptId) &&
2219
+ isProjectEnvPolicyEpochs(frame.policyEpochs) &&
2220
+ isProjectEnvDeliveryCursor(frame.appliedThrough),
2221
+ );
2222
+ }
2223
+
2224
+ if (candidate.phase === "page") {
2225
+ const frame = exactWireObject(value, [
2226
+ "kind",
2227
+ "opId",
2228
+ "ok",
2229
+ "phase",
2230
+ "attemptId",
2231
+ "projectId",
2232
+ "after",
2233
+ "recordVersion",
2234
+ "results",
2235
+ "complete",
2236
+ "policyEpochs",
2237
+ "appliedThrough",
2238
+ ]);
2239
+ if (
2240
+ frame === null ||
2241
+ frame.kind !== "env.replica.ack" ||
2242
+ !isHostOpId(frame.opId) ||
2243
+ !isHostOpId(frame.attemptId) ||
2244
+ !isProjectEnvProjectId(frame.projectId) ||
2245
+ !(frame.after === null || isProjectEnvKey(frame.after)) ||
2246
+ !isNonNegativeSafeInteger(frame.recordVersion) ||
2247
+ !isProjectEnvReplicaResults(frame.results) ||
2248
+ typeof frame.complete !== "boolean" ||
2249
+ !isProjectEnvPolicyEpochs(frame.policyEpochs) ||
2250
+ !isProjectEnvDeliveryCursor(frame.appliedThrough)
2251
+ ) {
2252
+ return false;
2253
+ }
2254
+ const results = frame.results as ProjectEnvReplicaResult[];
2255
+ if (results.length === 0) {
2256
+ if (!(frame.complete && frame.after === null)) return false;
2257
+ } else if (
2258
+ frame.after !== null &&
2259
+ results[0]!.state.key <= frame.after
2260
+ ) {
2261
+ return false;
2262
+ }
2263
+ return isProjectEnvPageWithinBound(value);
2264
+ }
2265
+
2266
+ if (candidate.phase === "applied") {
2267
+ const frame = exactWireObject(value, [
2268
+ "kind",
2269
+ "opId",
2270
+ "ok",
2271
+ "phase",
2272
+ "attemptId",
2273
+ "projectId",
2274
+ "recordVersion",
2275
+ "result",
2276
+ "policyEpochs",
2277
+ "appliedThrough",
2278
+ ]);
2279
+ return Boolean(
2280
+ frame &&
2281
+ frame.kind === "env.replica.ack" &&
2282
+ isHostOpId(frame.opId) &&
2283
+ (frame.attemptId === null || isHostOpId(frame.attemptId)) &&
2284
+ isProjectEnvProjectId(frame.projectId) &&
2285
+ isPositiveSafeInteger(frame.recordVersion) &&
2286
+ isProjectEnvReplicaResult(frame.result) &&
2287
+ isProjectEnvPolicyEpochs(frame.policyEpochs) &&
2288
+ isProjectEnvDeliveryCursor(frame.appliedThrough),
2289
+ );
2290
+ }
2291
+
2292
+ if (candidate.phase === "complete") {
2293
+ const frame = exactWireObject(value, [
2294
+ "kind",
2295
+ "opId",
2296
+ "ok",
2297
+ "phase",
2298
+ "attemptId",
2299
+ "highWater",
2300
+ "policyEpochs",
2301
+ "appliedThrough",
2302
+ ]);
2303
+ return Boolean(
2304
+ frame &&
2305
+ frame.kind === "env.replica.ack" &&
2306
+ isHostOpId(frame.opId) &&
2307
+ isHostOpId(frame.attemptId) &&
2308
+ isProjectEnvDeliveryCursor(frame.highWater) &&
2309
+ isProjectEnvPolicyEpochs(frame.policyEpochs) &&
2310
+ isProjectEnvDeliveryCursor(frame.appliedThrough) &&
2311
+ frame.appliedThrough.streamId === frame.highWater.streamId &&
2312
+ frame.appliedThrough.sequence === frame.highWater.sequence,
2313
+ );
2314
+ }
2315
+ return false;
2316
+ }
131
2317
  /** The owner-level account scope; every pre-scope account lives here. */
132
2318
  export const HOST_ACCOUNT_SCOPE = "host";
133
2319
  const ORG_ACCOUNT_SCOPE_RE = /^org:[A-Za-z0-9_-]{1,128}$/;
@@ -1056,6 +3242,10 @@ export interface HostCapabilities {
1056
3242
  /** Optional protocol features. Absence means an older host and fails closed
1057
3243
  * for features whose fallback would weaken isolation (ADR-083). */
1058
3244
  protocolFeatures?: string[];
3245
+ /** Policy interpretations retained by this runner. Required whenever
3246
+ * `project-env-bindings-v1` is advertised so the cloud never guesses an
3247
+ * epoch while deciding whether a stored binding can still be applied. */
3248
+ projectEnvPolicyEpochs?: number[];
1059
3249
  agentKinds: Array<{
1060
3250
  kind: string;
1061
3251
  label: string;
@@ -1157,6 +3347,13 @@ export interface TaskUpResult {
1157
3347
  worktreePath: string;
1158
3348
  codeServerPort?: number;
1159
3349
  previewPorts?: Array<{ name: string; hostPort: number }>;
3350
+ /** The value-delivery mode this exact task-machine start actually used.
3351
+ * Absent on older runners and therefore interpreted by the cloud as the
3352
+ * fail-safe baked mode. */
3353
+ envMode?: "baked" | "loader";
3354
+ /** Immutable, value-free schema observation captured for every task project
3355
+ * before any task-controlled process started. Absent means a legacy host. */
3356
+ envSchemas?: ProjectEnvSchemaObservation[];
1160
3357
  /** Degraded start: uai-init failed twice; the task is up without deps/editor. */
1161
3358
  initWarning?: string;
1162
3359
  }
@@ -1693,16 +3890,19 @@ export type CloudToHost =
1693
3890
  // mirrored parameter for the tag-scoped task-machine lease policy (ADR-133,
1694
3891
  // docs/credential-store.md). A list carrying it also backfills rows that
1695
3892
  // were mirrored before the org was known. Absent from an older cloud.
1696
- | { kind: "env.var.list"; opId: string; projectId: string; orgId?: string }
1697
- | {
1698
- kind: "env.var.set";
1699
- opId: string;
1700
- projectId: string;
1701
- key: string;
1702
- value: string;
1703
- orgId?: string;
1704
- }
1705
- | { kind: "env.var.delete"; opId: string; projectId: string; key: string }
3893
+ | ProjectEnvListFrame
3894
+ | ProjectEnvSetFrame
3895
+ | ProjectEnvDeleteFrame
3896
+ | ProjectEnvRemoveFrame
3897
+ | ProjectEnvBindingSetFrame
3898
+ | ProjectEnvReconcileBeginFrame
3899
+ | ProjectEnvReconciledFrame
3900
+ | ProjectEnvDeliverySkipFrame
3901
+ | ProjectEnvStagingPutFrame
3902
+ | ProjectEnvReplicaBeginFrame
3903
+ | ProjectEnvReplicaPageFrame
3904
+ | ProjectEnvReplicaApplyFrame
3905
+ | ProjectEnvReplicaCommitFrame
1706
3906
  // User-authed MCP connections (ADR-057). Ops run entirely host-side —
1707
3907
  // discovery, DCR, PKCE, token exchange, encrypted storage. Secrets in a
1708
3908
  // probe (headerValue, clientSecret) are write-only; nothing secret ever
@@ -1893,11 +4093,15 @@ export type HostToCloud =
1893
4093
  // is null when the user has no key (after delete, or a get-miss).
1894
4094
  | { kind: "ssh.key.ack"; userId: string; ok: true; publicKey: string | null }
1895
4095
  | { kind: "ssh.key.ack"; userId: string; ok: false; error: string }
1896
- // Ack for env.var.list / env.var.set / env.var.delete. Returns the project's
1897
- // KEY NAMES only — values never cross the bridge (secret-blind, ADR-015).
1898
- // `opId` correlates to the request.
1899
- | { kind: "env.var.ack"; opId: string; ok: true; keys: string[] }
1900
- | { kind: "env.var.ack"; opId: string; ok: false; error: string }
4096
+ // Acks for project-env inventory and mutations. Modern acks carry only
4097
+ // record metadata, policy epochs and an ordered delivery cursor; values
4098
+ // never cross back (secret-blind, ADR-015). Omitting all modern metadata is
4099
+ // the legacy shape, not an unbound modern host.
4100
+ | ProjectEnvAckFrame
4101
+ | ProjectEnvReconcileAckFrame
4102
+ | ProjectEnvDeliveryAckFrame
4103
+ | ProjectEnvStagingAckFrame
4104
+ | ProjectEnvReplicaAckFrame
1901
4105
  // Ack for mcp.op (ADR-057). `connected` = usable now; `auth_required` =
1902
4106
  // open authorizeUrl in the user's browser and wait for the callback;
1903
4107
  // `disconnected` = secrets gone. No secret material ever crosses back.