@runuai/host 0.9.120 → 0.9.122
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/lib/agents/transport.ts +2 -1
- package/lib/browser-testing.ts +2 -1
- package/lib/credential-store.ts +4 -0
- package/lib/lease.ts +216 -0
- package/lib/mcp-gateway.ts +2 -1
- package/lib/orchestrator.ts +4 -3
- package/lib/shared-files-s3.ts +153 -0
- package/lib/sidecar.ts +58 -0
- package/lib/standard-image.ts +7 -0
- package/lib/task-environment/index.ts +27 -6
- package/lib/task-environment/legacy-adoption.ts +2 -1
- package/lib/task-environment/machine-task-up.ts +24 -6
- package/lib/task-environment/self.ts +346 -0
- package/lib/task-environment/types.ts +13 -0
- package/package.json +1 -1
- package/src/cli.ts +7 -2
- package/src/index.ts +9 -4
- package/src/main.ts +121 -47
- package/src/protocol.ts +29 -1
package/src/main.ts
CHANGED
|
@@ -248,6 +248,10 @@ import {
|
|
|
248
248
|
isEngineAccountLabel,
|
|
249
249
|
isEngineConnectKind,
|
|
250
250
|
} from "./protocol";
|
|
251
|
+
import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
|
|
252
|
+
import { LeaseManager } from "../lib/lease";
|
|
253
|
+
import { markSidecarReady, sidecarMode, sidecarTaskId } from "../lib/sidecar";
|
|
254
|
+
import { declareAgentClisReady } from "../lib/standard-image";
|
|
251
255
|
|
|
252
256
|
setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
|
|
253
257
|
|
|
@@ -457,24 +461,54 @@ migrateHostDb();
|
|
|
457
461
|
// credential store. Import what this host lacks and upload what was never
|
|
458
462
|
// mirrored, off the startup path — task-up resolves locally either way.
|
|
459
463
|
configureEngineAccountStore({ hostId });
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
)
|
|
471
|
-
|
|
464
|
+
/** The credential-store reconciles. At boot on a host with its own AWS
|
|
465
|
+
* identity; after the first lease on a sidecar (ADR-133), which has no
|
|
466
|
+
* credentials before that. */
|
|
467
|
+
function reconcileStores(): Promise<void> {
|
|
468
|
+
const engineAccounts = reconcileEngineAccountsFromStore({ hostId })
|
|
469
|
+
.then((r) => {
|
|
470
|
+
if (r.skipped) return;
|
|
471
|
+
console.log(
|
|
472
|
+
`[engine-accounts] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
|
|
473
|
+
);
|
|
474
|
+
})
|
|
475
|
+
.catch((error: unknown) => {
|
|
476
|
+
console.warn(
|
|
477
|
+
`[engine-accounts] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
const userCredentials = reconcileUserCredentialsFromStore()
|
|
481
|
+
.then((r) => {
|
|
482
|
+
if (r.skipped) return;
|
|
483
|
+
console.log(
|
|
484
|
+
`[user-credentials] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
|
|
485
|
+
);
|
|
486
|
+
})
|
|
487
|
+
.catch((error: unknown) => {
|
|
488
|
+
console.warn(
|
|
489
|
+
`[user-credentials] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
const projectEnv = reconcileProjectEnvFromStore()
|
|
493
|
+
.then((r) => {
|
|
494
|
+
if (r.skipped) return;
|
|
495
|
+
console.log(
|
|
496
|
+
`[project-env] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
|
|
497
|
+
);
|
|
498
|
+
})
|
|
499
|
+
.catch((error: unknown) => {
|
|
500
|
+
console.warn(
|
|
501
|
+
`[project-env] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
return Promise.all([engineAccounts, userCredentials, projectEnv]).then(() => undefined);
|
|
505
|
+
}
|
|
472
506
|
// Image + agent-CLI maintenance runs where the compute is owned: an owned
|
|
473
507
|
// host keeps its own image current on an hourly schedule (a task-up reuses
|
|
474
508
|
// the last pass instead of blocking on one); a Uai-operated host sets
|
|
475
509
|
// UAI_IMAGE_MAINTENANCE=external — a cloud job keeps the payload AMI fresh —
|
|
476
|
-
// and reconciles only once, at start.
|
|
477
|
-
if (env.UAI_IMAGE_MAINTENANCE === "host") {
|
|
510
|
+
// and reconciles only once, at start. A sidecar (ADR-133) has no image.
|
|
511
|
+
if (env.UAI_IMAGE_MAINTENANCE === "host" && !sidecarMode()) {
|
|
478
512
|
const maintenanceTimer = setInterval(() => {
|
|
479
513
|
void ensureStandardImage().catch(() => undefined);
|
|
480
514
|
}, MAINTENANCE_INTERVAL_MS);
|
|
@@ -489,38 +523,55 @@ const repushBrokeredCopies = async (kind: string): Promise<void> => {
|
|
|
489
523
|
await getOrchestrator().refreshEngineAccountCopies(kind);
|
|
490
524
|
};
|
|
491
525
|
configureTokenBroker({ repush: repushBrokeredCopies });
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
526
|
+
// ADR-133: a sidecar never refreshes sign-ins (the broker Lambda does), and
|
|
527
|
+
// reads the store only once it holds a lease — see leaseManager below.
|
|
528
|
+
if (!sidecarMode()) {
|
|
529
|
+
startTokenBrokerLoop();
|
|
530
|
+
startCredentialStorePickup({ repush: repushBrokeredCopies });
|
|
531
|
+
void reconcileStores();
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* ADR-133 sidecar: the task-scoped AWS lease. Requested when the bridge is
|
|
535
|
+
* ready, refreshed before expiry; the first grant is what lets the store
|
|
536
|
+
* reconciles and the S3 file mounts run. A denial means the task is no
|
|
537
|
+
* longer ours to run — exit 3 and let the cloud's sweep reclaim the machine.
|
|
538
|
+
*/
|
|
539
|
+
const leaseManager: LeaseManager | null = sidecarMode()
|
|
540
|
+
? new LeaseManager({
|
|
541
|
+
taskId: sidecarTaskId()!,
|
|
542
|
+
send: (frame) => {
|
|
543
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
544
|
+
send(ws, frame);
|
|
545
|
+
return true;
|
|
546
|
+
},
|
|
547
|
+
onFirstGrant: () => {
|
|
548
|
+
console.log("[lease] granted — reconciling the credential store");
|
|
549
|
+
void reconcileStores().finally(() => {
|
|
550
|
+
// Task-up waits for this: only now are engine accounts, GitHub
|
|
551
|
+
// tokens, and project env on the machine (lib/sidecar.ts).
|
|
552
|
+
markSidecarReady();
|
|
553
|
+
console.log("[lease] credential store reconciled — the task may start");
|
|
554
|
+
});
|
|
555
|
+
startCredentialStorePickup({ repush: repushBrokeredCopies });
|
|
556
|
+
},
|
|
557
|
+
onGrant: (lease) => {
|
|
558
|
+
console.log(
|
|
559
|
+
`[lease] holds until ${new Date(lease.expiresAt).toISOString()}`,
|
|
560
|
+
);
|
|
561
|
+
},
|
|
562
|
+
onDenied: (error) => {
|
|
563
|
+
console.error(`[lease] denied: ${error} — this machine no longer runs its task`);
|
|
564
|
+
process.exit(3);
|
|
565
|
+
},
|
|
566
|
+
})
|
|
567
|
+
: null;
|
|
568
|
+
if (sidecarMode()) declareAgentClisReady();
|
|
518
569
|
// Reconcile a durable retention-off setting before recovery/connect can make
|
|
519
570
|
// crash-left owner-facing terminal history observable again. Correctness
|
|
520
571
|
// tombstones remain intact inside applyTerminalHistoryRetention.
|
|
521
572
|
applyTerminalHistoryRetention();
|
|
522
573
|
try {
|
|
523
|
-
await configureBundledContainerRuntime();
|
|
574
|
+
if (!sidecarMode()) await configureBundledContainerRuntime();
|
|
524
575
|
} catch {
|
|
525
576
|
// A malformed/tampered active generation must never fall back to PATH or an
|
|
526
577
|
// inferred asset layout. Keep Docker-first operation available and surface
|
|
@@ -529,7 +580,17 @@ try {
|
|
|
529
580
|
"[host-agent] signed bundled runtime validation failed; the bundled runtime is disabled",
|
|
530
581
|
);
|
|
531
582
|
}
|
|
532
|
-
const initialRuntime
|
|
583
|
+
const initialRuntime: Awaited<ReturnType<typeof initializeContainerRuntime>> =
|
|
584
|
+
sidecarMode()
|
|
585
|
+
? // ADR-133: a task machine runs its agents directly; nothing here ever
|
|
586
|
+
// dials a container runtime.
|
|
587
|
+
{
|
|
588
|
+
status: "no-runtime",
|
|
589
|
+
preference: "auto",
|
|
590
|
+
message: "sidecar mode — no container runtime on a task machine",
|
|
591
|
+
action: "none: a task machine runs its agents directly",
|
|
592
|
+
}
|
|
593
|
+
: await initializeContainerRuntime();
|
|
533
594
|
// Do not touch the selected daemon merely because detection succeeded. The
|
|
534
595
|
// activation gate below must first prove that every durable TaskEnvironment
|
|
535
596
|
// belongs to this exact provider/endpoint. The manager's first admitted login
|
|
@@ -612,12 +673,16 @@ const stopAgentClisReadySubscription = onAgentClisReady(() =>
|
|
|
612
673
|
void connect();
|
|
613
674
|
// Local browser UI (ADR-028) — same single process, alongside the WSS client.
|
|
614
675
|
// Best-effort: a UI bind failure must not take the host service down.
|
|
615
|
-
|
|
676
|
+
// A task machine has no operator sitting at it (ADR-133): no local UI, no
|
|
677
|
+
// Mac vnode watch.
|
|
678
|
+
if (!sidecarMode()) void startLocalUi();
|
|
616
679
|
// ADR-118 follow-up: vnode-pressure observability (macOS/apple-container
|
|
617
680
|
// hosts). Logs only on band transitions; the sensor caches its sysctl read.
|
|
618
|
-
|
|
619
|
-
vnodePressureTimer
|
|
620
|
-
|
|
681
|
+
if (!sidecarMode()) {
|
|
682
|
+
const vnodePressureTimer = setInterval(() => void logVnodePressure(), 60_000);
|
|
683
|
+
vnodePressureTimer.unref?.();
|
|
684
|
+
void logVnodePressure();
|
|
685
|
+
}
|
|
621
686
|
|
|
622
687
|
async function startLocalUi(): Promise<void> {
|
|
623
688
|
try {
|
|
@@ -717,6 +782,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
717
782
|
},
|
|
718
783
|
}
|
|
719
784
|
: {}),
|
|
785
|
+
...(sidecarTaskId() ? { sidecar: { taskId: sidecarTaskId()! } } : {}),
|
|
720
786
|
maintenanceReady: areAgentClisReady(),
|
|
721
787
|
mcpGateway: mcpGateway.state(),
|
|
722
788
|
engineLogins: engineLoginManager.capabilities(),
|
|
@@ -1206,6 +1272,7 @@ async function connect(): Promise<void> {
|
|
|
1206
1272
|
reconnectAttempt = 0;
|
|
1207
1273
|
markConnected();
|
|
1208
1274
|
console.log(`[host-agent] connected as ${hostId}`);
|
|
1275
|
+
leaseManager?.reconnected();
|
|
1209
1276
|
setHostObsTag(hostId);
|
|
1210
1277
|
addHostBreadcrumb("bridge", "connected");
|
|
1211
1278
|
|
|
@@ -1405,6 +1472,10 @@ async function connect(): Promise<void> {
|
|
|
1405
1472
|
case "tunnel.close":
|
|
1406
1473
|
closeTunnel(socket, frame.tunnelId, frame.reason);
|
|
1407
1474
|
break;
|
|
1475
|
+
case "lease.grant":
|
|
1476
|
+
case "lease.deny":
|
|
1477
|
+
leaseManager?.handle(frame);
|
|
1478
|
+
break;
|
|
1408
1479
|
case "gh.connect.set": {
|
|
1409
1480
|
const runtimeProblem = containerRuntimeProblem();
|
|
1410
1481
|
if (runtimeProblem) {
|
|
@@ -2141,7 +2212,7 @@ async function resolveTunnelTarget(
|
|
|
2141
2212
|
// operational. After a daemon restart an unrelated local process could bind
|
|
2142
2213
|
// a stale port before recovery refreshes the task's mappings.
|
|
2143
2214
|
const machineTask = getHostTask(frame.taskId);
|
|
2144
|
-
if (machineTask
|
|
2215
|
+
if (machineTask && isEnvironmentTransportProvider(machineTask.environmentProvider)) {
|
|
2145
2216
|
// ADR-121: a machine's address is orchestrator-routable (bridge IP on
|
|
2146
2217
|
// the Linux guinea pig, VPC-private on EC2) — editor and previews dial
|
|
2147
2218
|
// it straight via the handle's ownership-proven route, independent of
|
|
@@ -3100,6 +3171,9 @@ function rejectFrameDuringShutdown(
|
|
|
3100
3171
|
case "tunnel.data":
|
|
3101
3172
|
case "tunnel.requestEnd":
|
|
3102
3173
|
case "tunnel.close":
|
|
3174
|
+
// A lease answer is housekeeping the sidecar still wants during a drain.
|
|
3175
|
+
case "lease.grant":
|
|
3176
|
+
case "lease.deny":
|
|
3103
3177
|
return false;
|
|
3104
3178
|
}
|
|
3105
3179
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1074,6 +1074,9 @@ export interface HostCapabilities {
|
|
|
1074
1074
|
* Present when the host is configured for them; the cloud shows the
|
|
1075
1075
|
* provider/region on the host page and, eventually, per-task backing
|
|
1076
1076
|
* choice keys off it. */
|
|
1077
|
+
/** ADR-133 sidecar mode: this host IS the task's machine — exactly one
|
|
1078
|
+
* task, no container runtime, tunnels on loopback. */
|
|
1079
|
+
sidecar?: { taskId: string };
|
|
1077
1080
|
machineTasks?: {
|
|
1078
1081
|
provider: string;
|
|
1079
1082
|
region?: string;
|
|
@@ -1695,9 +1698,32 @@ export type CloudToHost =
|
|
|
1695
1698
|
// only entries it never transmitted, replaying nothing.
|
|
1696
1699
|
| { kind: "event.resume"; afterSeq: number | null }
|
|
1697
1700
|
// ADR-103: cumulative — the host trims outbox entries with seq <= seq.
|
|
1698
|
-
| { kind: "event.ack"; seq: number }
|
|
1701
|
+
| { kind: "event.ack"; seq: number }
|
|
1702
|
+
// ADR-133 leases: the cloud's answer to a sidecar's `lease.request`.
|
|
1703
|
+
| { kind: "lease.grant"; opId: string; lease: TaskLease }
|
|
1704
|
+
| { kind: "lease.deny"; opId: string; error: string };
|
|
1699
1705
|
|
|
1700
1706
|
/** One MCP-connection operation (ADR-057), executed by the host. */
|
|
1707
|
+
/**
|
|
1708
|
+
* ADR-133: the short-lived AWS credentials a task machine runs on — an STS
|
|
1709
|
+
* session tagged with the task's workspace and owner, issued by the cloud
|
|
1710
|
+
* over the bridge and held in the sidecar's memory only. `expiresAt` is the
|
|
1711
|
+
* session expiry (epoch ms); the sidecar re-requests before it.
|
|
1712
|
+
*/
|
|
1713
|
+
export interface TaskLease {
|
|
1714
|
+
accessKeyId: string;
|
|
1715
|
+
secretAccessKey: string;
|
|
1716
|
+
sessionToken: string;
|
|
1717
|
+
expiresAt: number;
|
|
1718
|
+
region: string;
|
|
1719
|
+
/** Files bucket and this task's prefixes inside it (S3 shared files). */
|
|
1720
|
+
files: {
|
|
1721
|
+
bucket: string;
|
|
1722
|
+
orgPrefix: string | null;
|
|
1723
|
+
userPrefix: string;
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1701
1727
|
export type McpOp =
|
|
1702
1728
|
// Probe `url` (MCP initialize). No auth → connected. 401 → walk the OAuth
|
|
1703
1729
|
// discovery chain (RFC 9728 → 8414 → 7591), prepare PKCE, and ack
|
|
@@ -1751,6 +1777,8 @@ export type HostToCloud =
|
|
|
1751
1777
|
// a task's containers, so the cloud mirrors the status (currently "stopped").
|
|
1752
1778
|
| { kind: "task.status"; taskId: string; status: string }
|
|
1753
1779
|
| { kind: "ping"; ts: number }
|
|
1780
|
+
// ADR-133: a sidecar asks for (or refreshes) its task-scoped AWS lease.
|
|
1781
|
+
| { kind: "lease.request"; opId: string; taskId: string }
|
|
1754
1782
|
| {
|
|
1755
1783
|
kind: "tunnel.ack";
|
|
1756
1784
|
tunnelId: string;
|