@runuai/host 0.9.149 → 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 +1847 -236
  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 +1062 -140
  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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Identity of this runner process lifetime.
3
+ *
4
+ * The bridge replay cursor and ADR-154's in-memory credential replicas must
5
+ * use the same value. Keeping the singleton in its own module prevents two
6
+ * subsystems from each drawing a plausible but different boot id.
7
+ */
8
+ import { newId } from "./ulid";
9
+
10
+ export const RUNNER_BOOT_ID = newId();
@@ -1,4 +1,11 @@
1
- export type RuntimeActivationAttemptResult = "complete" | "retry";
1
+ /**
2
+ * `retry`: a transient failure, followed up within the epoch's bounded
3
+ * budget. `rerun`: the attempt was cut short by a one-time, monotonic
4
+ * transition (ADR-154: the runner leaving its own credentials), and the
5
+ * pass it now needs (one that sweeps first) runs at once, outside that
6
+ * budget and with a fresh one of its own, at most once per epoch.
7
+ */
8
+ export type RuntimeActivationAttemptResult = "complete" | "retry" | "rerun";
2
9
 
3
10
  type ActivationAttempt = () => Promise<RuntimeActivationAttemptResult>;
4
11
  type RetrySchedule = (callback: () => void, delayMs: number) => () => void;
@@ -118,6 +125,10 @@ export class RuntimeActivationTail {
118
125
  private cancelRetry: (() => void) | null = null;
119
126
  private exhaustionReported = false;
120
127
  private stopped = false;
128
+ /** Who waits for no activation to be under way (whenSettled). */
129
+ private settledWaiters: Array<() => void> = [];
130
+ /** The epochs whose one rerun has been run. */
131
+ private readonly rerunEpochs = new Set<number>();
121
132
 
122
133
  constructor(options: RuntimeActivationTailOptions) {
123
134
  this.isCurrent = options.isCurrent;
@@ -162,7 +173,19 @@ export class RuntimeActivationTail {
162
173
  if (this.inFlight?.promise !== running) return;
163
174
  this.inFlight = null;
164
175
  if (
165
- result === "retry" &&
176
+ result === "rerun" &&
177
+ !this.stopped &&
178
+ this.isCurrent(epoch) &&
179
+ !this.rerunEpochs.has(epoch)
180
+ ) {
181
+ // Outside the transient budget, with a fresh one of its own.
182
+ this.rerunEpochs.add(epoch);
183
+ this.cancelScheduledRetry();
184
+ this.retryAttempts = 0;
185
+ this.exhaustionReported = false;
186
+ void this.request(epoch, attempt);
187
+ } else if (
188
+ (result === "retry" || result === "rerun") &&
166
189
  !this.stopped &&
167
190
  this.isCurrent(epoch)
168
191
  ) {
@@ -170,6 +193,7 @@ export class RuntimeActivationTail {
170
193
  } else {
171
194
  this.resolve(epoch);
172
195
  }
196
+ this.notifySettled();
173
197
  });
174
198
  this.inFlight = { epoch, promise: running };
175
199
  return running;
@@ -196,13 +220,30 @@ export class RuntimeActivationTail {
196
220
  cancel = this.schedule(() => {
197
221
  if (this.cancelRetry !== cancel) return;
198
222
  this.cancelRetry = null;
199
- if (this.stopped || !this.isCurrent(epoch)) return;
223
+ if (this.stopped || !this.isCurrent(epoch)) {
224
+ this.notifySettled();
225
+ return;
226
+ }
200
227
  void this.request(epoch, attempt);
201
228
  }, this.retryDelayMs);
202
229
  this.cancelRetry = cancel;
203
230
  return true;
204
231
  }
205
232
 
233
+ /** Whether an activation is under way: one running, or a retry of one
234
+ * scheduled (#p2jdax: a recheck asked for meanwhile waits for it). */
235
+ underWay(): boolean {
236
+ return !this.stopped && (this.inFlight !== null || this.cancelRetry !== null);
237
+ }
238
+
239
+ /** Resolves once no activation is under way; never, once stopped (a
240
+ * stopping host starts nothing more). */
241
+ whenSettled(): Promise<void> {
242
+ if (this.stopped) return new Promise<void>(() => {});
243
+ if (!this.underWay()) return Promise.resolve();
244
+ return new Promise<void>((resolve) => this.settledWaiters.push(resolve));
245
+ }
246
+
206
247
  /** Clear retry bookkeeping after another path proves the epoch operational. */
207
248
  resolve(epoch: number): void {
208
249
  if (this.retryEpoch !== epoch) return;
@@ -210,6 +251,12 @@ export class RuntimeActivationTail {
210
251
  this.retryEpoch = null;
211
252
  this.retryAttempts = 0;
212
253
  this.exhaustionReported = false;
254
+ this.notifySettled();
255
+ }
256
+
257
+ private notifySettled(): void {
258
+ if (this.stopped || this.underWay()) return;
259
+ for (const resolve of this.settledWaiters.splice(0)) resolve();
213
260
  }
214
261
 
215
262
  stop(): void {
@@ -38,6 +38,19 @@ export const BUNDLED_RUNTIME_CAPABILITY =
38
38
  export const MANAGED_RUNTIME_CAPABILITY =
39
39
  "agent/managed-runtime-capability.json" as const;
40
40
  export const MANAGED_RUNTIME_CAPABILITY_VERSION = 4 as const;
41
+ /**
42
+ * ADR-154 §13's cutover marker: this generation was built at or after the
43
+ * release that moved credentials to the cloud. It is a second file rather
44
+ * than capability 5 because the 0.9.149 and 0.9.150 validator accepts exactly
45
+ * capability 4, so the cutover release must still carry
46
+ * `{"capabilitySchemaVersion":4}` or no owned runner could install it. Like
47
+ * that marker in its day, this additive `agent/` file is ignored by every
48
+ * older validator. A generation without it predates the cutover and resolves
49
+ * to credentials capability 0.
50
+ */
51
+ export const MANAGED_RUNTIME_CREDENTIALS =
52
+ "agent/managed-runtime-credentials.json" as const;
53
+ export const MANAGED_RUNTIME_CREDENTIALS_VERSION = 1 as const;
41
54
  /** Plugin directory == plugin binary name in Apple's signed pkg payload. */
42
55
  const APPLE_CONTAINER_PLUGINS = Object.freeze([
43
56
  "container-core-images",
@@ -122,6 +135,10 @@ export interface ManagedRuntimeCapabilityDescriptorV4 {
122
135
  readonly capabilitySchemaVersion: 4;
123
136
  }
124
137
 
138
+ export interface ManagedRuntimeCredentialsDescriptor {
139
+ readonly credentialsCapabilityVersion: number;
140
+ }
141
+
125
142
  export type RuntimePlatformCapabilities =
126
143
  | { readonly capabilitySchemaVersion: 1 }
127
144
  | RuntimeCapabilityDescriptorV3;
@@ -129,6 +146,8 @@ export type RuntimePlatformCapabilities =
129
146
  export type ResolvedRuntimeCapabilities = RuntimePlatformCapabilities & {
130
147
  /** Effective rollback/update floor carried by this whole generation. */
131
148
  readonly runtimeCapabilityVersion: 1 | 3 | 4;
149
+ /** ADR-154 cutover marker; 0 for a generation built before the cutover. */
150
+ readonly credentialsCapabilityVersion: number;
132
151
  };
133
152
 
134
153
  export type RuntimeDescriptor = RuntimeDescriptorV1;
@@ -473,6 +492,8 @@ function validateEntryLayout(
473
492
  byPath,
474
493
  platformCapabilities.capabilitySchemaVersion,
475
494
  );
495
+ const credentialsCapabilityVersion =
496
+ validateArchivedManagedRuntimeCredentials(byPath);
476
497
  if (platformCapabilities.capabilitySchemaVersion === 3) {
477
498
  const bundled = platformCapabilities.bundledRuntime;
478
499
  const pluginBinaries = APPLE_CONTAINER_PLUGINS.map(
@@ -531,7 +552,31 @@ function validateEntryLayout(
531
552
  throw new Error(`runtime archive executable mode is invalid: ${executable}`);
532
553
  }
533
554
  }
534
- return { ...platformCapabilities, runtimeCapabilityVersion };
555
+ return {
556
+ ...platformCapabilities,
557
+ runtimeCapabilityVersion,
558
+ credentialsCapabilityVersion,
559
+ };
560
+ }
561
+
562
+ function validateArchivedManagedRuntimeCredentials(
563
+ byPath: ReadonlyMap<string, ArchiveEntry>,
564
+ ): number {
565
+ const entry = byPath.get(MANAGED_RUNTIME_CREDENTIALS);
566
+ if (entry === undefined) return 0;
567
+ if (entry.type !== "0" || entry.data.length > MAX_PAX_BYTES) {
568
+ throw new Error(
569
+ "managed runtime credentials descriptor is not a regular file or is too large",
570
+ );
571
+ }
572
+ let raw: unknown;
573
+ try {
574
+ raw = JSON.parse(entry.data.toString("utf8"));
575
+ } catch {
576
+ throw new Error("managed runtime credentials descriptor is invalid JSON");
577
+ }
578
+ return validateManagedRuntimeCredentialsDescriptor(raw)
579
+ .credentialsCapabilityVersion;
535
580
  }
536
581
 
537
582
  function validateArchivedManagedRuntimeCapability(
@@ -672,6 +717,28 @@ export function validateManagedRuntimeCapabilityDescriptor(
672
717
  };
673
718
  }
674
719
 
720
+ export function validateManagedRuntimeCredentialsDescriptor(
721
+ value: unknown,
722
+ ): ManagedRuntimeCredentialsDescriptor {
723
+ const object = strictObject(
724
+ value,
725
+ ["credentialsCapabilityVersion"],
726
+ "managed runtime credentials descriptor",
727
+ );
728
+ // Any version from 1 up, unlike the exact match above: that exact match is
729
+ // why the cutover needed this second file, and a later release must be able
730
+ // to raise this one in place. The floor compares it numerically.
731
+ const version = object.credentialsCapabilityVersion;
732
+ if (
733
+ typeof version !== "number" ||
734
+ !Number.isSafeInteger(version) ||
735
+ version < 1
736
+ ) {
737
+ throw new Error("unsupported managed runtime credentials descriptor");
738
+ }
739
+ return { credentialsCapabilityVersion: version };
740
+ }
741
+
675
742
  function validateBundledRuntimeDescriptor(
676
743
  value: unknown,
677
744
  ): BundledRuntimeDescriptor {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * A known-ready container runtime revalidated when the bridge reconnects: a
3
+ * healthy daemon can still have restarted while the bridge was offline. One
4
+ * label scan keeps ordinary cloud/network flaps cheap; only a missing,
5
+ * exited or unknowable active app takes the full per-task recovery.
6
+ *
7
+ * ADR-154: it runs on the credentials it began on, as an activation does
8
+ * (credentialsActivationFence). Off the runner's own before this process's
9
+ * credential sweep has succeeded, or once leaving them begins, it neither
10
+ * recovers nor publishes: it hands the runtime to the credentials-aware
11
+ * activation, which sweeps first. Off them after the sweep, its recovery
12
+ * goes through the credentials wrapper (the sweep already done).
13
+ */
14
+
15
+ import {
16
+ cloudCredentialsSwept,
17
+ credentialsActivationFence,
18
+ onOwnCredentials,
19
+ recoverRuntimeOnCredentials,
20
+ } from "./cloud-credential-host";
21
+
22
+ export interface RuntimeReconnectDeps {
23
+ /** Suspends a ready runtime for revalidation; its epoch, or null. */
24
+ suspend(): number | null;
25
+ /** Probes the daemon again: ready or not. */
26
+ probe(): Promise<boolean>;
27
+ /** The readiness epoch is still this one, and the daemon ready. */
28
+ epochIsCurrent(epoch: number): boolean;
29
+ /** The epoch is current and the runner is not stopping. */
30
+ activationIsCurrent(epoch: number): boolean;
31
+ persistedEnvironmentsMatch(): Promise<boolean>;
32
+ recoveryVerdict(): Promise<"clean" | "needed" | "unknown">;
33
+ /** The orchestrator's per-task recovery. */
34
+ recover(isCurrent: () => boolean): Promise<boolean>;
35
+ /** A bounded activation retry for the epoch. */
36
+ scheduleRetry(epoch: number): void;
37
+ /** A full, credentials-aware activation pass for the epoch. */
38
+ requestActivation(epoch: number): void;
39
+ /** Marks the runtime operational for the epoch, resumes deferred agent
40
+ * work and re-advertises; false when it could not be marked. */
41
+ publish(epoch: number): boolean;
42
+ }
43
+
44
+ export async function revalidateRuntimeOnConnect(deps: RuntimeReconnectDeps): Promise<void> {
45
+ const epoch = deps.suspend();
46
+ if (epoch === null) return;
47
+ const fence = credentialsActivationFence(() => deps.activationIsCurrent(epoch));
48
+ const handOver = (): void => {
49
+ if (deps.activationIsCurrent(epoch)) deps.requestActivation(epoch);
50
+ };
51
+ /** Not current any more: leaving the runner's own credentials hands the
52
+ * runtime to the activation; a stale epoch ends here. */
53
+ const stopped = (): boolean => {
54
+ if (fence.isCurrent()) return false;
55
+ if (fence.leftLegacy()) handOver();
56
+ return true;
57
+ };
58
+ try {
59
+ if (!(await deps.probe()) || !deps.epochIsCurrent(epoch)) return;
60
+ if (!onOwnCredentials() && !cloudCredentialsSwept()) {
61
+ handOver();
62
+ return;
63
+ }
64
+ if (!(await deps.persistedEnvironmentsMatch())) return;
65
+ if (stopped()) return;
66
+ let recovered = (await deps.recoveryVerdict()) === "clean";
67
+ if (!recovered) {
68
+ recovered = await recoverRuntimeOnCredentials(deps.recover, () => fence.isCurrent());
69
+ }
70
+ if (stopped()) return;
71
+ // Prove the daemon once more after recovery so a mid-sweep loss cannot
72
+ // reopen commands and tunnels on an unresolved backend.
73
+ if (!(await deps.probe()) || !deps.epochIsCurrent(epoch)) return;
74
+ if (stopped()) return;
75
+ if (!recovered) {
76
+ deps.scheduleRetry(epoch);
77
+ return;
78
+ }
79
+ deps.publish(epoch);
80
+ } catch (error) {
81
+ console.warn(
82
+ `[host-agent] container runtime reconnect validation failed: ${error instanceof Error ? error.message : String(error)}`,
83
+ );
84
+ if (deps.activationIsCurrent(epoch)) deps.scheduleRetry(epoch);
85
+ }
86
+ }