@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.
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/meta/_journal.json +14 -0
- package/db/schema.ts +188 -0
- package/lib/agent-cli.ts +11 -13
- package/lib/agent.ts +177 -7
- package/lib/agents/durable-proc.ts +56 -44
- package/lib/agents/registry.ts +19 -3
- package/lib/agents/session-state.ts +23 -10
- package/lib/agents/transport.ts +101 -69
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +4 -3
- package/lib/attachments.ts +19 -17
- package/lib/capture-flow.ts +319 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2544 -0
- package/lib/cloud-credential-runtime.ts +1035 -0
- package/lib/codex-auth.ts +21 -1
- package/lib/container-runtime.ts +76 -0
- package/lib/credential-cleanup-inventory.ts +405 -0
- package/lib/credential-consumers/engine-anchor.ts +1876 -0
- package/lib/credential-consumers/engines.ts +773 -0
- package/lib/credential-consumers/github.ts +594 -0
- package/lib/credential-consumers/mcp.ts +384 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-store-pickup.ts +15 -2
- package/lib/credential-store.ts +361 -33
- package/lib/credentials-activation-authority.ts +546 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/db.ts +11 -0
- package/lib/docker-exec.ts +3 -1
- package/lib/engine-accounts.ts +42 -32
- package/lib/engine-connect.ts +47 -2
- package/lib/engine-credential-env.ts +50 -0
- package/lib/engine-login.ts +83 -1
- package/lib/env-file.ts +62 -8
- package/lib/env.ts +5 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +219 -177
- package/lib/git-identity.ts +59 -14
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +56 -21
- package/lib/host-env.ts +17 -4
- package/lib/launch-mode.ts +25 -0
- package/lib/legacy-runtime-retirement.ts +388 -0
- package/lib/machine-exec.ts +14 -3
- package/lib/machine-provider-aws.ts +50 -8
- package/lib/machine-provider-local.ts +4 -0
- package/lib/machine-provider.ts +4 -0
- package/lib/managed-operation-drain.ts +8 -0
- package/lib/managed-runtime.ts +371 -7
- package/lib/mcp-capture.ts +860 -0
- package/lib/mcp-connections.ts +8 -1
- package/lib/mcp-gateway.ts +168 -10
- package/lib/obs.ts +2 -6
- package/lib/orchestrator.ts +1830 -224
- package/lib/person-capture.ts +327 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +50 -3
- package/lib/runtime-archive.ts +68 -1
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +429 -13
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/task-trust.ts +34 -3
- package/lib/sidecar-resume.ts +18 -5
- package/lib/skills.ts +47 -8
- package/lib/standard-image.ts +157 -103
- package/lib/stopped-task-status-queue.ts +68 -10
- package/lib/task-credential-cleanup.ts +525 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +66 -163
- package/lib/task-env-source.ts +68 -14
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +69 -28
- package/lib/task-environment/docker.ts +101 -20
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +459 -96
- package/lib/task-environment/machine.ts +101 -11
- package/lib/task-environment/self.ts +108 -50
- package/lib/task-environment/types.ts +89 -5
- package/lib/task-environment/workspace-files.ts +623 -25
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-gateway-recovery.ts +4 -1
- package/lib/task-gateway-state.ts +15 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +56 -2
- package/lib/token-broker-local.ts +28 -4
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +368 -28
- package/lib/token-lanes/openai-oauth.ts +7 -3
- package/lib/token-lanes/xai-oauth.ts +7 -3
- package/lib/transcript.ts +9 -5
- package/package.json +6 -2
- package/scripts/agent/task-up.sh +242 -100
- package/src/cloud-credentials.ts +2399 -0
- package/src/event-outbox.ts +8 -6
- package/src/index.ts +1386 -875
- package/src/main.ts +1061 -139
- package/src/outbound-frame.ts +31 -0
- package/src/project-env-policy.ts +125 -10
- package/src/protocol.ts +699 -23
- 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
|
-
|
|
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 === "
|
|
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))
|
|
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 {
|
package/lib/runtime-archive.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|