@runuai/host 0.9.150 → 0.9.151

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 (107) hide show
  1. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  2. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  3. package/db/migrations/meta/_journal.json +14 -0
  4. package/db/schema.ts +188 -0
  5. package/lib/agent-cli.ts +11 -13
  6. package/lib/agent.ts +177 -7
  7. package/lib/agents/durable-proc.ts +56 -44
  8. package/lib/agents/registry.ts +19 -3
  9. package/lib/agents/session-state.ts +23 -10
  10. package/lib/agents/transport.ts +101 -69
  11. package/lib/apple-one-off.ts +277 -0
  12. package/lib/apple-runtime-recycle.ts +4 -3
  13. package/lib/attachments.ts +19 -17
  14. package/lib/capture-flow.ts +319 -0
  15. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  16. package/lib/cloud-credential-host.ts +512 -0
  17. package/lib/cloud-credential-leases.ts +2544 -0
  18. package/lib/cloud-credential-runtime.ts +1035 -0
  19. package/lib/codex-auth.ts +21 -1
  20. package/lib/container-runtime.ts +76 -0
  21. package/lib/credential-cleanup-inventory.ts +405 -0
  22. package/lib/credential-consumers/engine-anchor.ts +1876 -0
  23. package/lib/credential-consumers/engines.ts +773 -0
  24. package/lib/credential-consumers/github.ts +594 -0
  25. package/lib/credential-consumers/mcp.ts +384 -0
  26. package/lib/credential-consumers/task-files.ts +127 -0
  27. package/lib/credential-store-pickup.ts +15 -2
  28. package/lib/credential-store.ts +361 -33
  29. package/lib/credentials-activation-authority.ts +546 -0
  30. package/lib/credentials-purge.ts +1006 -0
  31. package/lib/db.ts +11 -0
  32. package/lib/docker-exec.ts +3 -1
  33. package/lib/engine-accounts.ts +42 -32
  34. package/lib/engine-connect.ts +47 -2
  35. package/lib/engine-credential-env.ts +50 -0
  36. package/lib/engine-login.ts +83 -1
  37. package/lib/env-file.ts +62 -8
  38. package/lib/env.ts +5 -0
  39. package/lib/fs-aliases.ts +49 -0
  40. package/lib/git-clone-credentials.ts +334 -0
  41. package/lib/git-diff.ts +219 -177
  42. package/lib/git-identity.ts +59 -14
  43. package/lib/github-origin.ts +28 -0
  44. package/lib/github-tokens.ts +56 -21
  45. package/lib/host-env.ts +17 -4
  46. package/lib/launch-mode.ts +25 -0
  47. package/lib/legacy-runtime-retirement.ts +388 -0
  48. package/lib/machine-exec.ts +14 -3
  49. package/lib/machine-provider-aws.ts +50 -8
  50. package/lib/machine-provider-local.ts +4 -0
  51. package/lib/machine-provider.ts +4 -0
  52. package/lib/managed-operation-drain.ts +8 -0
  53. package/lib/managed-runtime.ts +371 -7
  54. package/lib/mcp-capture.ts +860 -0
  55. package/lib/mcp-connections.ts +8 -1
  56. package/lib/mcp-gateway.ts +168 -10
  57. package/lib/obs.ts +2 -6
  58. package/lib/orchestrator.ts +1830 -224
  59. package/lib/person-capture.ts +327 -0
  60. package/lib/project-env-legacy-tags.ts +130 -0
  61. package/lib/project-env-task-replica.ts +1752 -0
  62. package/lib/runner-boot.ts +10 -0
  63. package/lib/runtime-activation-tail.ts +50 -3
  64. package/lib/runtime-archive.ts +68 -1
  65. package/lib/runtime-reconnect.ts +86 -0
  66. package/lib/runtime-state.ts +429 -13
  67. package/lib/safe-http.ts +364 -0
  68. package/lib/secrets-proxy/task-trust.ts +34 -3
  69. package/lib/sidecar-resume.ts +18 -5
  70. package/lib/skills.ts +47 -8
  71. package/lib/standard-image.ts +157 -103
  72. package/lib/stopped-task-status-queue.ts +68 -10
  73. package/lib/task-credential-cleanup.ts +525 -0
  74. package/lib/task-diff-sandbox.ts +464 -0
  75. package/lib/task-diff.ts +66 -163
  76. package/lib/task-env-source.ts +68 -14
  77. package/lib/task-environment/anchor.ts +31 -0
  78. package/lib/task-environment/apple-container.ts +69 -28
  79. package/lib/task-environment/docker.ts +101 -20
  80. package/lib/task-environment/machine-environment.ts +61 -0
  81. package/lib/task-environment/machine-task-up.ts +459 -96
  82. package/lib/task-environment/machine.ts +101 -11
  83. package/lib/task-environment/self.ts +108 -50
  84. package/lib/task-environment/types.ts +89 -5
  85. package/lib/task-environment/workspace-files.ts +623 -25
  86. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  87. package/lib/task-gateway-recovery.ts +4 -1
  88. package/lib/task-gateway-state.ts +15 -0
  89. package/lib/task-machine-swap-safety.ts +729 -0
  90. package/lib/token-broker-adopt.ts +344 -0
  91. package/lib/token-broker-lambda.ts +56 -2
  92. package/lib/token-broker-local.ts +28 -4
  93. package/lib/token-broker-revision.ts +310 -0
  94. package/lib/token-broker-ssm.ts +368 -28
  95. package/lib/token-lanes/openai-oauth.ts +7 -3
  96. package/lib/token-lanes/xai-oauth.ts +7 -3
  97. package/lib/transcript.ts +9 -5
  98. package/package.json +6 -2
  99. package/scripts/agent/task-up.sh +242 -100
  100. package/src/cloud-credentials.ts +2399 -0
  101. package/src/event-outbox.ts +8 -6
  102. package/src/index.ts +1386 -875
  103. package/src/main.ts +1061 -139
  104. package/src/outbound-frame.ts +31 -0
  105. package/src/project-env-policy.ts +125 -10
  106. package/src/protocol.ts +699 -23
  107. package/src/ui/server.ts +48 -10
package/src/protocol.ts CHANGED
@@ -1,3 +1,31 @@
1
+ import {
2
+ parseCaptureFlowWire,
3
+ parseCaptureResultWire,
4
+ type CaptureFlowWire,
5
+ type CaptureResultWire,
6
+ } from "./cloud-credentials";
7
+ import type {
8
+ GithubCaptureFrame,
9
+ GithubRevokeAckFrame,
10
+ GithubRevokeFrame,
11
+ McpCaptureAckFrame,
12
+ McpCaptureCancelFrame,
13
+ McpCaptureCompleteFrame,
14
+ McpCaptureProbeFrame,
15
+ PersonCaptureAckFrame,
16
+ SshMintFrame,
17
+ CredentialChangedFrame,
18
+ CredentialRefreshFrame,
19
+ CredentialRefreshResultFrame,
20
+ CredentialsActivateFrame,
21
+ CredentialsActivateResultFrame,
22
+ CredentialsActivatedFrame,
23
+ LeaseGrantV1Frame,
24
+ LeaseRequestV1Frame,
25
+ LeaseStaleFrame,
26
+ LeaseUnavailableFrame,
27
+ } from "./cloud-credentials";
28
+
1
29
  export enum HostErrorCode {
2
30
  TaskNotFound = "task_not_found",
3
31
  ProjectNotFound = "project_not_found",
@@ -238,6 +266,14 @@ export type ProjectEnvDirective =
238
266
  | { kind: "omit" }
239
267
  | { kind: "unsupported"; reason: ProjectEnvUnsupportedReason };
240
268
 
269
+ /** The shape of a proxied key's placeholder (`@placeholder`): a prefix the
270
+ * tools that check a key's format expect, then random characters, to this
271
+ * total length. A shape only, never a literal. */
272
+ export interface ProjectEnvPlaceholderShape {
273
+ prefix: string;
274
+ totalLength: number;
275
+ }
276
+
241
277
  /** Cloud-safe declaration captured from a trusted `.env.schema` base.
242
278
  * `placeholder` is a shape only: the repository's literal never crosses the
243
279
  * bridge and is never used as a task placeholder. */
@@ -247,10 +283,7 @@ export interface ProjectEnvSchemaDeclaration {
247
283
  sensitive: boolean;
248
284
  type?: string;
249
285
  directive: ProjectEnvDirective;
250
- placeholder?: {
251
- prefix: string;
252
- totalLength: number;
253
- };
286
+ placeholder?: ProjectEnvPlaceholderShape;
254
287
  }
255
288
 
256
289
  /** A schema condition that cannot be represented on the declaration itself.
@@ -289,7 +322,17 @@ export type ProjectEnvSchemaReport = ProjectEnvSchemaObservation;
289
322
 
290
323
  /** The most permissive policy the owner has approved for one stored value. */
291
324
  export type ProjectEnvBinding =
292
- | { kind: "proxy"; domains: string[] }
325
+ | {
326
+ kind: "proxy";
327
+ domains: string[];
328
+ /**
329
+ * ADR-154 §9: the owner declared this key proxied in Settings, which
330
+ * the repository's schema can only narrow, with the placeholder shape
331
+ * Settings gives it (null: the default). Valid only under policy epoch
332
+ * 2 or later; an earlier epoch reads it as a binding it cannot accept.
333
+ */
334
+ declared?: { placeholder: ProjectEnvPlaceholderShape | null };
335
+ }
293
336
  | { kind: "passthrough" }
294
337
  | { kind: "omit" }
295
338
  | { kind: "withhold"; reason: ProjectEnvWithholdReason };
@@ -484,10 +527,15 @@ export function isProjectEnvSchemaDeclaration(
484
527
  }
485
528
  if (object.placeholder === undefined) return true;
486
529
  if (object.directive.kind !== "proxy") return false;
487
- const placeholder = exactWireObject(object.placeholder, [
488
- "prefix",
489
- "totalLength",
490
- ]);
530
+ return isProjectEnvPlaceholderShape(object.placeholder);
531
+ }
532
+
533
+ /** A placeholder shape: a prefix of the allowed characters, and room for at
534
+ * least 32 random characters after it, within the length bound. */
535
+ export function isProjectEnvPlaceholderShape(
536
+ value: unknown,
537
+ ): value is ProjectEnvPlaceholderShape {
538
+ const placeholder = exactWireObject(value, ["prefix", "totalLength"]);
491
539
  return (
492
540
  placeholder !== null &&
493
541
  typeof placeholder.prefix === "string" &&
@@ -638,15 +686,24 @@ export function isProjectEnvBinding(
638
686
  return exactWireObject(value, ["kind"]) !== null;
639
687
  }
640
688
  if (kind === "proxy") {
641
- const object = exactWireObject(value, ["kind", "domains"]);
642
- return (
643
- object !== null &&
644
- isSortedUniqueStringArray(
689
+ const object = exactWireObject(value, ["kind", "domains"], ["declared"]);
690
+ if (
691
+ object === null ||
692
+ !isSortedUniqueStringArray(
645
693
  object.domains,
646
694
  MAX_PROJECT_ENV_PROXY_DOMAINS,
647
695
  isProjectEnvDomain,
648
- ) &&
649
- object.domains.length > 0
696
+ ) ||
697
+ object.domains.length === 0
698
+ ) {
699
+ return false;
700
+ }
701
+ if (object.declared === undefined) return true;
702
+ const declared = exactWireObject(object.declared, ["placeholder"]);
703
+ return (
704
+ declared !== null &&
705
+ (declared.placeholder === null ||
706
+ isProjectEnvPlaceholderShape(declared.placeholder))
650
707
  );
651
708
  }
652
709
  if (kind === "withhold") {
@@ -656,6 +713,15 @@ export function isProjectEnvBinding(
656
713
  return false;
657
714
  }
658
715
 
716
+ /** A binding the owner declared in Settings (ADR-154 §9). */
717
+ export function isDeclaredProjectEnvBinding(
718
+ binding: ProjectEnvBinding | null,
719
+ ): binding is Extract<ProjectEnvBinding, { kind: "proxy" }> & {
720
+ declared: { placeholder: ProjectEnvPlaceholderShape | null };
721
+ } {
722
+ return binding?.kind === "proxy" && binding.declared !== undefined;
723
+ }
724
+
659
725
  /** Where a key's live value is (ADR-153 §4, "Value references"): in the
660
726
  * record's own row (owned runners), in a staged parameter named by its
661
727
  * staging id, or in the key's legacy parameter at one exact SSM version. */
@@ -1095,6 +1161,38 @@ export type ProjectEnvStagingAckFrame =
1095
1161
  error: string;
1096
1162
  };
1097
1163
 
1164
+ /** ADR-154 §9: the box tags pinned legacy parameters. Advertised by the
1165
+ * sign-in box only; the cloud sends `env.legacy.tag` to nothing else. */
1166
+ export const PROJECT_ENV_LEGACY_TAGS_PROTOCOL_FEATURE = "project-env-legacy-tags-v1";
1167
+ export const MAX_PROJECT_ENV_LEGACY_TAG_KEYS = 25;
1168
+
1169
+ /** ADR-154 §9: a v1 lease reads `/uai/projects/*` only where the parameter's
1170
+ * `org` tag is the task's workspace, and ADR-153's backfill pinned legacy
1171
+ * parameters (`<prefix>/projects/<projectId>/env/<KEY>`) without checking
1172
+ * their tags. The write-only sign-in box puts the project's `org` and
1173
+ * `project` tags on each named one and reads them back. Metadata only:
1174
+ * nothing reads or writes a value, and the cloud checks each pin itself,
1175
+ * before and after, from its own metadata reads. */
1176
+ export interface ProjectEnvLegacyTagFrame {
1177
+ kind: "env.legacy.tag";
1178
+ opId: HostOpId;
1179
+ orgId: string;
1180
+ projectId: string;
1181
+ keys: string[];
1182
+ }
1183
+
1184
+ /** `tagged`: both tags read back as asked; `missing`: no such parameter;
1185
+ * `refused`: AWS will not tag it (too many tags), so the value is withheld;
1186
+ * `retry`: anything else, asked again later. */
1187
+ export type ProjectEnvLegacyTagOutcome = "tagged" | "missing" | "refused" | "retry";
1188
+
1189
+ export interface ProjectEnvLegacyTagAckFrame {
1190
+ kind: "env.legacy.tag.ack";
1191
+ opId: HostOpId;
1192
+ /** One per key of the request, in its order. */
1193
+ results: Array<{ key: string; outcome: ProjectEnvLegacyTagOutcome }>;
1194
+ }
1195
+
1098
1196
  /** Uai Cloud is authoritative for task-machine records. A replica may read
1099
1197
  * only immutable staged values or one pinned legacy SSM version; `row` is an
1100
1198
  * owned-runner reference and is never meaningful on a sidecar. */
@@ -1118,6 +1216,85 @@ export type ProjectEnvReplicaBindingState =
1118
1216
  | ProjectEnvReplicaTombstoneState
1119
1217
  | ProjectEnvReplicaLiveState;
1120
1218
 
1219
+ /**
1220
+ * ADR-154's task-keyed replica identity. The authenticated bridge supplies
1221
+ * the runner identity; these fields bind work to one task run, one runner
1222
+ * process and one ordered stream on that runner. A reconnect under the same
1223
+ * boot id may resume the stream. A new boot id or run epoch may not.
1224
+ */
1225
+ export interface ProjectEnvReplicaV1Identity {
1226
+ taskId: string;
1227
+ runEpoch: number;
1228
+ runnerBootId: HostOpId;
1229
+ streamId: HostOpId;
1230
+ /** Null only on a live apply and its acknowledgement. */
1231
+ attemptId: HostOpId | null;
1232
+ }
1233
+
1234
+ type ProjectEnvReplicaV1AttemptIdentity = Omit<
1235
+ ProjectEnvReplicaV1Identity,
1236
+ "attemptId"
1237
+ > & {
1238
+ attemptId: HostOpId;
1239
+ };
1240
+
1241
+ type ProjectEnvReplicaV1ApplyIdentity = ProjectEnvReplicaV1Identity;
1242
+
1243
+ /** Exact identity equality used when correlating an ack with its request. */
1244
+ export function sameProjectEnvReplicaV1Identity(
1245
+ left: ProjectEnvReplicaV1Identity,
1246
+ right: ProjectEnvReplicaV1Identity,
1247
+ ): boolean {
1248
+ return (
1249
+ left.taskId === right.taskId &&
1250
+ left.runEpoch === right.runEpoch &&
1251
+ left.runnerBootId === right.runnerBootId &&
1252
+ left.streamId === right.streamId &&
1253
+ left.attemptId === right.attemptId
1254
+ );
1255
+ }
1256
+
1257
+ /**
1258
+ * Versioned in parallel with the legacy machine-scoped family so a cloud-first
1259
+ * rollout can continue serving old sidecars unchanged.
1260
+ */
1261
+ export type ProjectEnvReplicaV1BeginFrame =
1262
+ ProjectEnvReplicaV1AttemptIdentity & {
1263
+ kind: "env.replica.v1.begin";
1264
+ opId: HostOpId;
1265
+ /** Canonical: sorted and unique. The empty set is valid. */
1266
+ projectIds: string[];
1267
+ };
1268
+
1269
+ export type ProjectEnvReplicaV1PageFrame =
1270
+ ProjectEnvReplicaV1AttemptIdentity & {
1271
+ kind: "env.replica.v1.page";
1272
+ opId: HostOpId;
1273
+ projectId: string;
1274
+ after: string | null;
1275
+ recordVersion: number;
1276
+ records: ProjectEnvReplicaBindingState[];
1277
+ complete: boolean;
1278
+ };
1279
+
1280
+ export type ProjectEnvReplicaV1ApplyFrame =
1281
+ ProjectEnvReplicaV1ApplyIdentity & {
1282
+ kind: "env.replica.v1.apply";
1283
+ opId: HostOpId;
1284
+ projectId: string;
1285
+ recordVersion: number;
1286
+ state: ProjectEnvReplicaBindingState;
1287
+ delivery: ProjectEnvDeliveryCursor;
1288
+ };
1289
+
1290
+ export type ProjectEnvReplicaV1CommitFrame =
1291
+ ProjectEnvReplicaV1AttemptIdentity & {
1292
+ kind: "env.replica.v1.commit";
1293
+ opId: HostOpId;
1294
+ highWater: ProjectEnvDeliveryCursor;
1295
+ versions: ProjectEnvRecordVersion[];
1296
+ };
1297
+
1121
1298
  /** Close the sidecar's serving gate and start a whole-task replica snapshot.
1122
1299
  * Project ids are canonical (sorted, unique); the empty set is valid. */
1123
1300
  export interface ProjectEnvReplicaBeginFrame {
@@ -1282,6 +1459,91 @@ export type ProjectEnvReplicaAckFrame =
1282
1459
  }
1283
1460
  | ProjectEnvReplicaErrorAckFrame;
1284
1461
 
1462
+ type ProjectEnvReplicaV1AckCommon = ProjectEnvReplicaV1Identity & {
1463
+ kind: "env.replica.v1.ack";
1464
+ opId: HostOpId;
1465
+ policyEpochs: number[];
1466
+ appliedThrough: ProjectEnvDeliveryCursor;
1467
+ };
1468
+
1469
+ type ProjectEnvReplicaV1ErrorAckFrame = ProjectEnvReplicaV1AckCommon &
1470
+ (
1471
+ | {
1472
+ ok: false;
1473
+ phase: "applied";
1474
+ attemptId: HostOpId | null;
1475
+ code: ProjectEnvWithholdReason;
1476
+ error: string;
1477
+ }
1478
+ | {
1479
+ ok: false;
1480
+ phase: "begun" | "page" | "complete";
1481
+ attemptId: HostOpId;
1482
+ code: ProjectEnvWithholdReason;
1483
+ error: string;
1484
+ }
1485
+ );
1486
+
1487
+ /** Every acknowledgement echoes the complete task-replica identity. */
1488
+ export type ProjectEnvReplicaV1AckFrame =
1489
+ | (ProjectEnvReplicaV1AckCommon & {
1490
+ ok: true;
1491
+ phase: "begun";
1492
+ attemptId: HostOpId;
1493
+ projectId?: never;
1494
+ after?: never;
1495
+ recordVersion?: never;
1496
+ results?: never;
1497
+ complete?: never;
1498
+ result?: never;
1499
+ highWater?: never;
1500
+ code?: never;
1501
+ error?: never;
1502
+ })
1503
+ | (ProjectEnvReplicaV1AckCommon & {
1504
+ ok: true;
1505
+ phase: "page";
1506
+ attemptId: HostOpId;
1507
+ projectId: string;
1508
+ after: string | null;
1509
+ recordVersion: number;
1510
+ results: ProjectEnvReplicaResult[];
1511
+ complete: boolean;
1512
+ result?: never;
1513
+ highWater?: never;
1514
+ code?: never;
1515
+ error?: never;
1516
+ })
1517
+ | (ProjectEnvReplicaV1AckCommon & {
1518
+ ok: true;
1519
+ phase: "applied";
1520
+ attemptId: HostOpId | null;
1521
+ projectId: string;
1522
+ recordVersion: number;
1523
+ result: ProjectEnvReplicaResult;
1524
+ after?: never;
1525
+ results?: never;
1526
+ complete?: never;
1527
+ highWater?: never;
1528
+ code?: never;
1529
+ error?: never;
1530
+ })
1531
+ | (ProjectEnvReplicaV1AckCommon & {
1532
+ ok: true;
1533
+ phase: "complete";
1534
+ attemptId: HostOpId;
1535
+ highWater: ProjectEnvDeliveryCursor;
1536
+ projectId?: never;
1537
+ after?: never;
1538
+ recordVersion?: never;
1539
+ results?: never;
1540
+ complete?: never;
1541
+ result?: never;
1542
+ code?: never;
1543
+ error?: never;
1544
+ })
1545
+ | ProjectEnvReplicaV1ErrorAckFrame;
1546
+
1285
1547
  type ProjectEnvAckBase = {
1286
1548
  kind: "env.var.ack";
1287
1549
  opId: HostOpId;
@@ -1873,6 +2135,52 @@ export function isProjectEnvStagingAckFrame(
1873
2135
  return false;
1874
2136
  }
1875
2137
 
2138
+ function isProjectEnvLegacyTagKeys(value: unknown): value is string[] {
2139
+ return (
2140
+ Array.isArray(value) &&
2141
+ value.length >= 1 &&
2142
+ value.length <= MAX_PROJECT_ENV_LEGACY_TAG_KEYS &&
2143
+ value.every((key) => isProjectEnvKey(key) && PROJECT_ENV_STORE_PATH_SEGMENT_RE.test(key)) &&
2144
+ new Set(value).size === value.length
2145
+ );
2146
+ }
2147
+
2148
+ export function isProjectEnvLegacyTagFrame(
2149
+ value: unknown,
2150
+ ): value is ProjectEnvLegacyTagFrame {
2151
+ const frame = exactWireObject(value, ["kind", "opId", "orgId", "projectId", "keys"]);
2152
+ return Boolean(
2153
+ frame &&
2154
+ frame.kind === "env.legacy.tag" &&
2155
+ isHostOpId(frame.opId) &&
2156
+ isProjectEnvOrgId(frame.orgId) &&
2157
+ PROJECT_ENV_STORE_PATH_SEGMENT_RE.test(frame.orgId) &&
2158
+ isProjectEnvProjectId(frame.projectId) &&
2159
+ PROJECT_ENV_STORE_PATH_SEGMENT_RE.test(frame.projectId) &&
2160
+ isProjectEnvLegacyTagKeys(frame.keys),
2161
+ );
2162
+ }
2163
+
2164
+ function isProjectEnvLegacyTagOutcome(value: unknown): value is ProjectEnvLegacyTagOutcome {
2165
+ return value === "tagged" || value === "missing" || value === "refused" || value === "retry";
2166
+ }
2167
+
2168
+ export function isProjectEnvLegacyTagAckFrame(
2169
+ value: unknown,
2170
+ ): value is ProjectEnvLegacyTagAckFrame {
2171
+ const frame = exactWireObject(value, ["kind", "opId", "results"]);
2172
+ if (!frame || frame.kind !== "env.legacy.tag.ack" || !isHostOpId(frame.opId)) return false;
2173
+ const results = frame.results;
2174
+ if (!Array.isArray(results) || results.length > MAX_PROJECT_ENV_LEGACY_TAG_KEYS) return false;
2175
+ const keys: unknown[] = [];
2176
+ for (const item of results) {
2177
+ const result = exactWireObject(item, ["key", "outcome"]);
2178
+ if (!result || !isProjectEnvLegacyTagOutcome(result.outcome)) return false;
2179
+ keys.push(result.key);
2180
+ }
2181
+ return keys.length === 0 || isProjectEnvLegacyTagKeys(keys);
2182
+ }
2183
+
1876
2184
  export function isProjectEnvReplicaBeginFrame(
1877
2185
  value: unknown,
1878
2186
  ): value is ProjectEnvReplicaBeginFrame {
@@ -1977,6 +2285,137 @@ export function isProjectEnvReplicaCommitFrame(
1977
2285
  );
1978
2286
  }
1979
2287
 
2288
+ function isProjectEnvReplicaV1Identity(
2289
+ frame: WireObject,
2290
+ allowLive: boolean,
2291
+ ): boolean {
2292
+ return (
2293
+ isHostOpId(frame.taskId) &&
2294
+ isPositiveSafeInteger(frame.runEpoch) &&
2295
+ isHostOpId(frame.runnerBootId) &&
2296
+ isHostOpId(frame.streamId) &&
2297
+ (isHostOpId(frame.attemptId) || (allowLive && frame.attemptId === null))
2298
+ );
2299
+ }
2300
+
2301
+ export function isProjectEnvReplicaV1BeginFrame(
2302
+ value: unknown,
2303
+ ): value is ProjectEnvReplicaV1BeginFrame {
2304
+ const frame = exactWireObject(value, [
2305
+ "kind",
2306
+ "opId",
2307
+ "taskId",
2308
+ "runEpoch",
2309
+ "runnerBootId",
2310
+ "streamId",
2311
+ "attemptId",
2312
+ "projectIds",
2313
+ ]);
2314
+ return Boolean(
2315
+ frame &&
2316
+ frame.kind === "env.replica.v1.begin" &&
2317
+ isHostOpId(frame.opId) &&
2318
+ isProjectEnvReplicaV1Identity(frame, false) &&
2319
+ isProjectEnvProjectIds(frame.projectIds),
2320
+ );
2321
+ }
2322
+
2323
+ export function isProjectEnvReplicaV1PageFrame(
2324
+ value: unknown,
2325
+ ): value is ProjectEnvReplicaV1PageFrame {
2326
+ const frame = exactWireObject(value, [
2327
+ "kind",
2328
+ "opId",
2329
+ "taskId",
2330
+ "runEpoch",
2331
+ "runnerBootId",
2332
+ "streamId",
2333
+ "attemptId",
2334
+ "projectId",
2335
+ "after",
2336
+ "recordVersion",
2337
+ "records",
2338
+ "complete",
2339
+ ]);
2340
+ if (
2341
+ frame === null ||
2342
+ frame.kind !== "env.replica.v1.page" ||
2343
+ !isHostOpId(frame.opId) ||
2344
+ !isProjectEnvReplicaV1Identity(frame, false) ||
2345
+ !isProjectEnvProjectId(frame.projectId) ||
2346
+ !(frame.after === null || isProjectEnvKey(frame.after)) ||
2347
+ !isNonNegativeSafeInteger(frame.recordVersion) ||
2348
+ !isProjectEnvReplicaBindingStates(frame.records) ||
2349
+ typeof frame.complete !== "boolean"
2350
+ ) {
2351
+ return false;
2352
+ }
2353
+ const records = frame.records as ProjectEnvReplicaBindingState[];
2354
+ if (records.length === 0) {
2355
+ if (!(frame.complete && frame.after === null)) return false;
2356
+ } else if (
2357
+ frame.after !== null &&
2358
+ records[0]!.key <= frame.after
2359
+ ) {
2360
+ return false;
2361
+ }
2362
+ return isProjectEnvPageWithinBound(value);
2363
+ }
2364
+
2365
+ export function isProjectEnvReplicaV1ApplyFrame(
2366
+ value: unknown,
2367
+ ): value is ProjectEnvReplicaV1ApplyFrame {
2368
+ const frame = exactWireObject(value, [
2369
+ "kind",
2370
+ "opId",
2371
+ "taskId",
2372
+ "runEpoch",
2373
+ "runnerBootId",
2374
+ "streamId",
2375
+ "attemptId",
2376
+ "projectId",
2377
+ "recordVersion",
2378
+ "state",
2379
+ "delivery",
2380
+ ]);
2381
+ return Boolean(
2382
+ frame &&
2383
+ frame.kind === "env.replica.v1.apply" &&
2384
+ isHostOpId(frame.opId) &&
2385
+ isProjectEnvReplicaV1Identity(frame, true) &&
2386
+ isProjectEnvProjectId(frame.projectId) &&
2387
+ isPositiveSafeInteger(frame.recordVersion) &&
2388
+ isProjectEnvReplicaBindingState(frame.state) &&
2389
+ isProjectEnvMutationDelivery(frame.delivery) &&
2390
+ (frame.delivery as ProjectEnvDeliveryCursor).streamId === frame.streamId,
2391
+ );
2392
+ }
2393
+
2394
+ export function isProjectEnvReplicaV1CommitFrame(
2395
+ value: unknown,
2396
+ ): value is ProjectEnvReplicaV1CommitFrame {
2397
+ const frame = exactWireObject(value, [
2398
+ "kind",
2399
+ "opId",
2400
+ "taskId",
2401
+ "runEpoch",
2402
+ "runnerBootId",
2403
+ "streamId",
2404
+ "attemptId",
2405
+ "highWater",
2406
+ "versions",
2407
+ ]);
2408
+ return Boolean(
2409
+ frame &&
2410
+ frame.kind === "env.replica.v1.commit" &&
2411
+ isHostOpId(frame.opId) &&
2412
+ isProjectEnvReplicaV1Identity(frame, false) &&
2413
+ isProjectEnvDeliveryCursor(frame.highWater) &&
2414
+ (frame.highWater as ProjectEnvDeliveryCursor).streamId === frame.streamId &&
2415
+ isProjectEnvRecordVersions(frame.versions),
2416
+ );
2417
+ }
2418
+
1980
2419
  function hasSortedBindingStates(value: ProjectEnvBindingState[]): boolean {
1981
2420
  return value.every(
1982
2421
  (state, index) => index === 0 || value[index - 1]!.key < state.key,
@@ -2314,6 +2753,155 @@ export function isProjectEnvReplicaAckFrame(
2314
2753
  }
2315
2754
  return false;
2316
2755
  }
2756
+
2757
+ function isProjectEnvReplicaV1AckIdentity(
2758
+ frame: WireObject,
2759
+ allowLive: boolean,
2760
+ ): boolean {
2761
+ return (
2762
+ isHostOpId(frame.opId) &&
2763
+ isProjectEnvReplicaV1Identity(frame, allowLive) &&
2764
+ isProjectEnvPolicyEpochs(frame.policyEpochs) &&
2765
+ isProjectEnvDeliveryCursor(frame.appliedThrough) &&
2766
+ (frame.appliedThrough as ProjectEnvDeliveryCursor).streamId ===
2767
+ frame.streamId
2768
+ );
2769
+ }
2770
+
2771
+ export function isProjectEnvReplicaV1AckFrame(
2772
+ value: unknown,
2773
+ ): value is ProjectEnvReplicaV1AckFrame {
2774
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2775
+ const candidate = value as WireObject;
2776
+
2777
+ if (candidate.ok === false) {
2778
+ const frame = exactWireObject(value, [
2779
+ "kind",
2780
+ "opId",
2781
+ "ok",
2782
+ "phase",
2783
+ "taskId",
2784
+ "runEpoch",
2785
+ "runnerBootId",
2786
+ "streamId",
2787
+ "attemptId",
2788
+ "code",
2789
+ "error",
2790
+ "policyEpochs",
2791
+ "appliedThrough",
2792
+ ]);
2793
+ if (
2794
+ frame === null ||
2795
+ frame.kind !== "env.replica.v1.ack" ||
2796
+ !(
2797
+ frame.phase === "begun" ||
2798
+ frame.phase === "page" ||
2799
+ frame.phase === "applied" ||
2800
+ frame.phase === "complete"
2801
+ ) ||
2802
+ !isProjectEnvReplicaV1AckIdentity(frame, frame.phase === "applied") ||
2803
+ !isProjectEnvReason(frame.code) ||
2804
+ !isBoundedMessage(frame.error)
2805
+ ) {
2806
+ return false;
2807
+ }
2808
+ return true;
2809
+ }
2810
+ if (candidate.ok !== true) return false;
2811
+
2812
+ const identityKeys = [
2813
+ "kind",
2814
+ "opId",
2815
+ "ok",
2816
+ "phase",
2817
+ "taskId",
2818
+ "runEpoch",
2819
+ "runnerBootId",
2820
+ "streamId",
2821
+ "attemptId",
2822
+ ];
2823
+ const tailKeys = ["policyEpochs", "appliedThrough"];
2824
+
2825
+ if (candidate.phase === "begun") {
2826
+ const frame = exactWireObject(value, [...identityKeys, ...tailKeys]);
2827
+ return Boolean(
2828
+ frame &&
2829
+ frame.kind === "env.replica.v1.ack" &&
2830
+ isProjectEnvReplicaV1AckIdentity(frame, false),
2831
+ );
2832
+ }
2833
+
2834
+ if (candidate.phase === "page") {
2835
+ const frame = exactWireObject(value, [
2836
+ ...identityKeys,
2837
+ "projectId",
2838
+ "after",
2839
+ "recordVersion",
2840
+ "results",
2841
+ "complete",
2842
+ ...tailKeys,
2843
+ ]);
2844
+ if (
2845
+ frame === null ||
2846
+ frame.kind !== "env.replica.v1.ack" ||
2847
+ !isProjectEnvReplicaV1AckIdentity(frame, false) ||
2848
+ !isProjectEnvProjectId(frame.projectId) ||
2849
+ !(frame.after === null || isProjectEnvKey(frame.after)) ||
2850
+ !isNonNegativeSafeInteger(frame.recordVersion) ||
2851
+ !isProjectEnvReplicaResults(frame.results) ||
2852
+ typeof frame.complete !== "boolean"
2853
+ ) {
2854
+ return false;
2855
+ }
2856
+ const results = frame.results as ProjectEnvReplicaResult[];
2857
+ if (results.length === 0) {
2858
+ if (!(frame.complete && frame.after === null)) return false;
2859
+ } else if (
2860
+ frame.after !== null &&
2861
+ results[0]!.state.key <= frame.after
2862
+ ) {
2863
+ return false;
2864
+ }
2865
+ return isProjectEnvPageWithinBound(value);
2866
+ }
2867
+
2868
+ if (candidate.phase === "applied") {
2869
+ const frame = exactWireObject(value, [
2870
+ ...identityKeys,
2871
+ "projectId",
2872
+ "recordVersion",
2873
+ "result",
2874
+ ...tailKeys,
2875
+ ]);
2876
+ return Boolean(
2877
+ frame &&
2878
+ frame.kind === "env.replica.v1.ack" &&
2879
+ isProjectEnvReplicaV1AckIdentity(frame, true) &&
2880
+ isProjectEnvProjectId(frame.projectId) &&
2881
+ isPositiveSafeInteger(frame.recordVersion) &&
2882
+ isProjectEnvReplicaResult(frame.result),
2883
+ );
2884
+ }
2885
+
2886
+ if (candidate.phase === "complete") {
2887
+ const frame = exactWireObject(value, [
2888
+ ...identityKeys,
2889
+ "highWater",
2890
+ ...tailKeys,
2891
+ ]);
2892
+ return Boolean(
2893
+ frame &&
2894
+ frame.kind === "env.replica.v1.ack" &&
2895
+ isProjectEnvReplicaV1AckIdentity(frame, false) &&
2896
+ isProjectEnvDeliveryCursor(frame.highWater) &&
2897
+ (frame.highWater as ProjectEnvDeliveryCursor).streamId ===
2898
+ frame.streamId &&
2899
+ (frame.appliedThrough as ProjectEnvDeliveryCursor).sequence ===
2900
+ (frame.highWater as ProjectEnvDeliveryCursor).sequence,
2901
+ );
2902
+ }
2903
+ return false;
2904
+ }
2317
2905
  /** The owner-level account scope; every pre-scope account lives here. */
2318
2906
  export const HOST_ACCOUNT_SCOPE = "host";
2319
2907
  const ORG_ACCOUNT_SCOPE_RE = /^org:[A-Za-z0-9_-]{1,128}$/;
@@ -2561,6 +3149,9 @@ export interface EngineLoginStartFrame {
2561
3149
  * host). Only sent to hosts advertising `host-engine-account-scopes-v1`;
2562
3150
  * absent = the host scope. An org scope requires a label. */
2563
3151
  scope?: string;
3152
+ /** ADR-154: write the capture as this revision of a cloud credential.
3153
+ * Only sent to a sign-in box advertising `signin-box-capture-flows-v1`. */
3154
+ flow?: CaptureFlowWire;
2564
3155
  }
2565
3156
 
2566
3157
  /** The only generic one-time response channel. Long-lived tokens must never
@@ -2625,6 +3216,9 @@ export type EngineLoginEventFrame =
2625
3216
  /** `succeeded` of a LABELED capture: the id of the account it created,
2626
3217
  * so the client can rename/order it right away. */
2627
3218
  accountId?: string;
3219
+ /** `succeeded` of an ADR-154 capture flow: the revision written, by
3220
+ * name and version. Never material. */
3221
+ capture?: CaptureResultWire;
2628
3222
  });
2629
3223
 
2630
3224
  /** Rolling-safe, secret-free engine state. v1 intentionally makes no claim
@@ -2686,6 +3280,8 @@ export interface EngineConnectStartFrame {
2686
3280
  label?: string;
2687
3281
  /** ADR-131 D3: see EngineLoginStartFrame.scope. */
2688
3282
  scope?: string;
3283
+ /** ADR-154: see EngineLoginStartFrame.flow. */
3284
+ flow?: CaptureFlowWire;
2689
3285
  }
2690
3286
 
2691
3287
  export interface EngineConnectStopFrame {
@@ -2706,6 +3302,8 @@ export interface EngineConnectEventFrame {
2706
3302
  /** Manage-v1: `done` of a LABELED connect — the id of the account it
2707
3303
  * created, so the client can name/order it right away. */
2708
3304
  accountId?: string;
3305
+ /** ADR-154: `done` of a capture flow, see EngineLoginEventFrame. */
3306
+ capture?: CaptureResultWire;
2709
3307
  }
2710
3308
 
2711
3309
  export function isEngineConnectKind(
@@ -2727,14 +3325,15 @@ export function isEngineConnectStartFrame(
2727
3325
  const frame = exactWireObject(
2728
3326
  value,
2729
3327
  ["kind", "opId", "engine"],
2730
- ["secret", "label", "scope"],
3328
+ ["secret", "label", "scope", "flow"],
2731
3329
  );
2732
3330
  if (
2733
3331
  !frame ||
2734
3332
  frame.kind !== "engine.connect.start" ||
2735
3333
  !isHostOpId(frame.opId) ||
2736
3334
  !isEngineConnectKind(frame.engine) ||
2737
- (frame.scope !== undefined && !isEngineAccountScope(frame.scope))
3335
+ (frame.scope !== undefined && !isEngineAccountScope(frame.scope)) ||
3336
+ (frame.flow !== undefined && !isEngineCaptureFlow(frame.flow, frame.engine))
2738
3337
  ) {
2739
3338
  return false;
2740
3339
  }
@@ -2765,7 +3364,7 @@ export function isEngineConnectEventFrame(
2765
3364
  const frame = exactWireObject(
2766
3365
  value,
2767
3366
  ["kind", "opId", "engine", "phase"],
2768
- ["line", "ok", "message", "accountId"],
3367
+ ["line", "ok", "message", "accountId", "capture"],
2769
3368
  );
2770
3369
  if (
2771
3370
  !frame ||
@@ -2775,6 +3374,14 @@ export function isEngineConnectEventFrame(
2775
3374
  ) {
2776
3375
  return false;
2777
3376
  }
3377
+ if (
3378
+ Object.hasOwn(frame, "capture") &&
3379
+ (frame.phase !== "done" ||
3380
+ frame.ok !== true ||
3381
+ parseCaptureResultWire(frame.capture) === null)
3382
+ ) {
3383
+ return false;
3384
+ }
2778
3385
  if (
2779
3386
  Object.hasOwn(frame, "accountId") &&
2780
3387
  (frame.phase !== "done" ||
@@ -2992,7 +3599,7 @@ export function isEngineLoginStartFrame(
2992
3599
  const frame = exactWireObject(
2993
3600
  value,
2994
3601
  ["kind", "opId", "engine"],
2995
- ["label", "scope"],
3602
+ ["label", "scope", "flow"],
2996
3603
  );
2997
3604
  return Boolean(
2998
3605
  frame &&
@@ -3000,7 +3607,18 @@ export function isEngineLoginStartFrame(
3000
3607
  isHostOpId(frame.opId) &&
3001
3608
  isEngineLoginKind(frame.engine) &&
3002
3609
  (frame.label === undefined || isEngineAccountLabel(frame.label)) &&
3003
- (frame.scope === undefined || isEngineAccountScope(frame.scope)),
3610
+ (frame.scope === undefined || isEngineAccountScope(frame.scope)) &&
3611
+ (frame.flow === undefined || isEngineCaptureFlow(frame.flow, frame.engine)),
3612
+ );
3613
+ }
3614
+
3615
+ /** ADR-154: a capture flow for an AI account of exactly this engine. */
3616
+ function isEngineCaptureFlow(value: unknown, engine: unknown): boolean {
3617
+ const flow = parseCaptureFlowWire(value);
3618
+ return (
3619
+ flow !== null &&
3620
+ flow.location.class === "engine" &&
3621
+ flow.location.engineKind === engine
3004
3622
  );
3005
3623
  }
3006
3624
 
@@ -3070,7 +3688,7 @@ export function isEngineLoginEventFrame(
3070
3688
  const frame = exactWireObject(
3071
3689
  value,
3072
3690
  ["kind", "opId", "engine", "phase"],
3073
- ["url", "errorCode", "message", "code", "accountId"],
3691
+ ["url", "errorCode", "message", "code", "accountId", "capture"],
3074
3692
  );
3075
3693
  if (
3076
3694
  !frame ||
@@ -3097,6 +3715,12 @@ export function isEngineLoginEventFrame(
3097
3715
  ) {
3098
3716
  return false;
3099
3717
  }
3718
+ if (
3719
+ Object.hasOwn(frame, "capture") &&
3720
+ (frame.phase !== "succeeded" || parseCaptureResultWire(frame.capture) === null)
3721
+ ) {
3722
+ return false;
3723
+ }
3100
3724
  if (frame.phase === "authorize") {
3101
3725
  return (
3102
3726
  isHttpsUrl(frame.url) &&
@@ -3423,6 +4047,11 @@ export interface TaskCommandTask {
3423
4047
  engineAccountScope?: string;
3424
4048
  /** ADR-062: shared-files mount mode ("off" | "ro" | "rw"; default "ro"). */
3425
4049
  sharedFiles?: string;
4050
+ /** ADR-154: the task's run epoch, which the cloud advances at every start,
4051
+ * resume and wake dispatch. With the runner the task is assigned to, it is
4052
+ * the assignment every v1 grant, replica frame and cleanup-inventory entry
4053
+ * is fenced to. Absent from a cloud that predates it. */
4054
+ runEpoch?: number;
3426
4055
  }
3427
4056
 
3428
4057
  /**
@@ -3456,6 +4085,9 @@ export interface TaskLaunchInput {
3456
4085
  * pressed pause; no veto. */
3457
4086
  export interface TaskStopInput {
3458
4087
  reason: "idle" | "manual";
4088
+ /** ADR-154 assignment fence. New clouds name the exact run they observed;
4089
+ * absent only for rolling-compatible tasks that have never crossed v1. */
4090
+ runEpoch?: number;
3459
4091
  }
3460
4092
 
3461
4093
  export interface TaskStopResult {
@@ -3864,6 +4496,9 @@ export type CloudToHost =
3864
4496
  opId: string;
3865
4497
  generation?: number;
3866
4498
  userId: string;
4499
+ /** ADR-154 §12, sign-in box only: list with this GitHub revision of
4500
+ * the person's, read from the store, instead of a held token. */
4501
+ parameter?: string;
3867
4502
  }
3868
4503
  // Renew one installation's App ∩ user repository allowlist without sending
3869
4504
  // the user's token to the cloud. Kept separate from installation listing so
@@ -3874,6 +4509,8 @@ export type CloudToHost =
3874
4509
  generation?: number;
3875
4510
  userId: string;
3876
4511
  installationId: number;
4512
+ /** ADR-154 §12, as on `gh.installations.list`. */
4513
+ parameter?: string;
3877
4514
  }
3878
4515
  // Per-user SSH key lifecycle (ADR-029). Keys are generated + stored on the
3879
4516
  // host; only the public key crosses the bridge (ssh.key.ack). `get` fetches
@@ -3899,10 +4536,17 @@ export type CloudToHost =
3899
4536
  | ProjectEnvReconciledFrame
3900
4537
  | ProjectEnvDeliverySkipFrame
3901
4538
  | ProjectEnvStagingPutFrame
4539
+ | ProjectEnvLegacyTagFrame
3902
4540
  | ProjectEnvReplicaBeginFrame
3903
4541
  | ProjectEnvReplicaPageFrame
3904
4542
  | ProjectEnvReplicaApplyFrame
3905
4543
  | ProjectEnvReplicaCommitFrame
4544
+ // ADR-154: task-keyed v1 replicas coexist with the exact legacy family
4545
+ // until the fleet audit removes pre-v1 sidecars.
4546
+ | ProjectEnvReplicaV1BeginFrame
4547
+ | ProjectEnvReplicaV1PageFrame
4548
+ | ProjectEnvReplicaV1ApplyFrame
4549
+ | ProjectEnvReplicaV1CommitFrame
3906
4550
  // User-authed MCP connections (ADR-057). Ops run entirely host-side —
3907
4551
  // discovery, DCR, PKCE, token exchange, encrypted storage. Secrets in a
3908
4552
  // probe (headerValue, clientSecret) are write-only; nothing secret ever
@@ -3938,7 +4582,21 @@ export type CloudToHost =
3938
4582
  | { kind: "event.ack"; seq: number }
3939
4583
  // ADR-133 leases: the cloud's answer to a sidecar's `lease.request`.
3940
4584
  | { kind: "lease.grant"; opId: string; lease: TaskLease }
3941
- | { kind: "lease.deny"; opId: string; error: string };
4585
+ | { kind: "lease.deny"; opId: string; error: string }
4586
+ // ADR-154: v1 leases and the frames that keep them current. Exact shapes
4587
+ // and parsers live in ./cloud-credentials.
4588
+ | LeaseGrantV1Frame
4589
+ | LeaseUnavailableFrame
4590
+ | LeaseStaleFrame
4591
+ | CredentialChangedFrame
4592
+ | CredentialRefreshResultFrame
4593
+ | CredentialsActivateResultFrame
4594
+ | McpCaptureProbeFrame
4595
+ | McpCaptureCompleteFrame
4596
+ | McpCaptureCancelFrame
4597
+ | GithubCaptureFrame
4598
+ | SshMintFrame
4599
+ | GithubRevokeFrame;
3942
4600
 
3943
4601
  /** ADR-141: provenance of a channelDeliver. */
3944
4602
  export type DeliveryOrigin = "human" | "agent" | "system";
@@ -4034,10 +4692,26 @@ export type HostToCloud =
4034
4692
  // no reason, the cloud records none, and that host's operator Stop keeps the
4035
4693
  // pre-ADR-144 behaviour rather than mislabelling its quarantines as manual
4036
4694
  // and blocking the recovery path they exist to enable.
4037
- | { kind: "task.status"; taskId: string; status: string; reason?: string }
4695
+ | {
4696
+ kind: "task.status";
4697
+ taskId: string;
4698
+ status: string;
4699
+ reason?: string;
4700
+ /** ADR-154 assignment fence; absent on rolling/pre-v1 hosts. */
4701
+ runEpoch?: number;
4702
+ }
4038
4703
  | { kind: "ping"; ts: number }
4039
4704
  // ADR-133: a sidecar asks for (or refreshes) its task-scoped AWS lease.
4705
+ // ADR-154: a v1 runner also names the run epoch it works under, which the
4706
+ // cloud must still find current.
4040
4707
  | { kind: "lease.request"; opId: string; taskId: string }
4708
+ | LeaseRequestV1Frame
4709
+ | CredentialRefreshFrame
4710
+ | CredentialsActivateFrame
4711
+ | CredentialsActivatedFrame
4712
+ | McpCaptureAckFrame
4713
+ | PersonCaptureAckFrame
4714
+ | GithubRevokeAckFrame
4041
4715
  | {
4042
4716
  kind: "tunnel.ack";
4043
4717
  tunnelId: string;
@@ -4101,7 +4775,9 @@ export type HostToCloud =
4101
4775
  | ProjectEnvReconcileAckFrame
4102
4776
  | ProjectEnvDeliveryAckFrame
4103
4777
  | ProjectEnvStagingAckFrame
4778
+ | ProjectEnvLegacyTagAckFrame
4104
4779
  | ProjectEnvReplicaAckFrame
4780
+ | ProjectEnvReplicaV1AckFrame
4105
4781
  // Ack for mcp.op (ADR-057). `connected` = usable now; `auth_required` =
4106
4782
  // open authorizeUrl in the user's browser and wait for the callback;
4107
4783
  // `disconnected` = secrets gone. No secret material ever crosses back.