@runuai/host 0.9.139 → 0.9.141
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/browser-testing.ts +6 -1
- package/lib/engine-connect.ts +5 -2
- package/lib/engine-login.ts +26 -7
- package/lib/github-tokens.ts +281 -17
- package/lib/host-env.ts +4 -0
- package/lib/mcp-connections.ts +208 -5
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/ssh.ts +109 -6
- package/package.json +1 -1
- package/scripts/agent/task-up.sh +37 -1
- package/src/cli.ts +35 -6
- package/src/main.ts +93 -8
- package/src/protocol.ts +11 -5
package/src/cli.ts
CHANGED
|
@@ -1585,12 +1585,21 @@ async function cmdSetup(rest: string[]): Promise<void> {
|
|
|
1585
1585
|
process.exitCode = 1;
|
|
1586
1586
|
return;
|
|
1587
1587
|
}
|
|
1588
|
+
// ADR-136 step 4: a replace-machine enrollment answers with the EXISTING
|
|
1589
|
+
// row's id — this machine adopts it instead of its own proposal, so the
|
|
1590
|
+
// runner keeps its identity (and every task created against it).
|
|
1591
|
+
const outcome =
|
|
1592
|
+
success && typeof success === "object" && !Array.isArray(success)
|
|
1593
|
+
? (success as Record<string, unknown>)
|
|
1594
|
+
: null;
|
|
1595
|
+
const replacedHostId =
|
|
1596
|
+
outcome?.replaced === true && typeof outcome.hostId === "string" && outcome.hostId.length > 0
|
|
1597
|
+
? outcome.hostId
|
|
1598
|
+
: null;
|
|
1588
1599
|
if (
|
|
1589
|
-
!
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
(success as Record<string, unknown>).ok !== true ||
|
|
1593
|
-
(success as Record<string, unknown>).hostId !== pending.hostId
|
|
1600
|
+
!outcome ||
|
|
1601
|
+
outcome.ok !== true ||
|
|
1602
|
+
(replacedHostId === null && outcome.hostId !== pending.hostId)
|
|
1594
1603
|
) {
|
|
1595
1604
|
console.error(red("enrollment outcome did not match the proposed host identity"));
|
|
1596
1605
|
console.error(dim("the proposed identity is saved locally; retry this exact command."));
|
|
@@ -1614,7 +1623,27 @@ async function cmdSetup(rest: string[]): Promise<void> {
|
|
|
1614
1623
|
process.exitCode = 1;
|
|
1615
1624
|
return;
|
|
1616
1625
|
}
|
|
1617
|
-
|
|
1626
|
+
if (replacedHostId !== null) {
|
|
1627
|
+
// The pending commit wrote the proposed id; the row this machine now
|
|
1628
|
+
// answers as is the replaced one. Same secret, so the bridge admits it.
|
|
1629
|
+
try {
|
|
1630
|
+
writeEnvValuesAtomic(envLocalPath(), { UAI_HOST_ID: replacedHostId });
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
console.error(
|
|
1633
|
+
red(
|
|
1634
|
+
`replaced, but could not record the runner id in ${envLocalPath()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1635
|
+
),
|
|
1636
|
+
);
|
|
1637
|
+
console.error(dim(`set UAI_HOST_ID=${replacedHostId} there by hand before starting.`));
|
|
1638
|
+
process.exitCode = 1;
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
console.log(
|
|
1642
|
+
green("✓ replaced") + dim(` — this machine now answers as runner ${replacedHostId}; config written to ${envLocalPath()}`),
|
|
1643
|
+
);
|
|
1644
|
+
} else {
|
|
1645
|
+
console.log(green("✓ enrolled") + dim(` — config written to ${envLocalPath()}`));
|
|
1646
|
+
}
|
|
1618
1647
|
printTelemetryNotice();
|
|
1619
1648
|
console.log("");
|
|
1620
1649
|
console.log("Start the host:");
|
package/src/main.ts
CHANGED
|
@@ -264,6 +264,15 @@ import {
|
|
|
264
264
|
signInBoxAllowsCommand,
|
|
265
265
|
signInBoxMode,
|
|
266
266
|
} from "../lib/signin-box";
|
|
267
|
+
import {
|
|
268
|
+
deleteProjectEnvStoreOnly,
|
|
269
|
+
listProjectEnvKeysStoreOnly,
|
|
270
|
+
purgeLocalUserCredentialSecrets,
|
|
271
|
+
reconcileUserCredentialMetadataFromStore,
|
|
272
|
+
setProjectEnvStoreOnly,
|
|
273
|
+
} from "../lib/signin-box-credentials";
|
|
274
|
+
import { loadGithubTokenIntoCache } from "../lib/github-tokens";
|
|
275
|
+
import { ensureSshKeyStoreFirst } from "../lib/ssh";
|
|
267
276
|
import { resumeSidecarTask } from "../lib/sidecar-resume";
|
|
268
277
|
import { declareAgentClisReady } from "../lib/standard-image";
|
|
269
278
|
|
|
@@ -557,12 +566,24 @@ async function startSignInBox(): Promise<void> {
|
|
|
557
566
|
ownerHome: process.env.UAI_OWNER_HOME?.trim() || homedir(),
|
|
558
567
|
envFile: join(env.uaiHome, ".env.local"),
|
|
559
568
|
});
|
|
569
|
+
const users = purgeLocalUserCredentialSecrets();
|
|
560
570
|
const purged =
|
|
561
|
-
secrets.stripped +
|
|
571
|
+
secrets.stripped +
|
|
572
|
+
secrets.removed +
|
|
573
|
+
slots.files.length +
|
|
574
|
+
slots.dirs.length +
|
|
575
|
+
slots.envKeys.length +
|
|
576
|
+
users.githubStripped +
|
|
577
|
+
users.sshStripped +
|
|
578
|
+
users.mcpStripped +
|
|
579
|
+
users.projectEnvRemoved +
|
|
580
|
+
users.unmirroredRemoved;
|
|
562
581
|
if (purged > 0 || secrets.errors > 0) {
|
|
563
582
|
console.warn(
|
|
564
583
|
`[signin-box] purged local credentials: ${secrets.stripped} account rows stripped, ${secrets.removed} removed, ` +
|
|
565
|
-
`${slots.files.length} default-slot files, ${slots.dirs.length} stray dirs, ${slots.envKeys.length} env keys` +
|
|
584
|
+
`${slots.files.length} default-slot files, ${slots.dirs.length} stray dirs, ${slots.envKeys.length} env keys, ` +
|
|
585
|
+
`${users.githubStripped} github, ${users.sshStripped} ssh, ${users.mcpStripped} mcp rows stripped, ` +
|
|
586
|
+
`${users.projectEnvRemoved} project env rows and ${users.unmirroredRemoved} unmirrored rows removed` +
|
|
566
587
|
(secrets.errors > 0 ? `, ${secrets.errors} errors` : ""),
|
|
567
588
|
);
|
|
568
589
|
}
|
|
@@ -572,13 +593,25 @@ async function startSignInBox(): Promise<void> {
|
|
|
572
593
|
timer.unref?.();
|
|
573
594
|
});
|
|
574
595
|
try {
|
|
575
|
-
const outcome = await Promise.race([
|
|
596
|
+
const outcome = await Promise.race([
|
|
597
|
+
Promise.all([reconcileEngineAccountMetadataFromStore(), reconcileUserCredentialMetadataFromStore()]),
|
|
598
|
+
deadline,
|
|
599
|
+
]);
|
|
576
600
|
if (outcome === "timeout") {
|
|
577
|
-
console.warn("[signin-box]
|
|
578
|
-
} else
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
601
|
+
console.warn("[signin-box] metadata reconcile did not finish in time; the lists fill on the next start");
|
|
602
|
+
} else {
|
|
603
|
+
const [accounts, users] = outcome;
|
|
604
|
+
if (!accounts.skipped) {
|
|
605
|
+
console.log(
|
|
606
|
+
`[signin-box] account metadata reconciled: ${accounts.imported} imported, ${accounts.removed} removed, ${accounts.errors} errors`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (!users.skipped) {
|
|
610
|
+
console.log(
|
|
611
|
+
`[signin-box] user credential metadata reconciled: ${users.imported} imported, ${users.removed} removed, ` +
|
|
612
|
+
`${users.tagged} legacy parameters tagged, ${users.unreadable} unreadable, ${users.errors} errors`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
582
615
|
}
|
|
583
616
|
} catch (error) {
|
|
584
617
|
console.warn(
|
|
@@ -1628,6 +1661,11 @@ async function connect(): Promise<void> {
|
|
|
1628
1661
|
});
|
|
1629
1662
|
},
|
|
1630
1663
|
};
|
|
1664
|
+
// ADR-136 sign-in box: the prior grant is not on disk; read it
|
|
1665
|
+
// into the cache so an account switch still revokes it remotely.
|
|
1666
|
+
if (signInBoxMode()) {
|
|
1667
|
+
await loadGithubTokenIntoCache(frame.userId).catch(() => false);
|
|
1668
|
+
}
|
|
1631
1669
|
let result: Awaited<ReturnType<typeof onConnectSet>>;
|
|
1632
1670
|
try {
|
|
1633
1671
|
result = await onConnectSet(
|
|
@@ -1793,6 +1831,11 @@ async function connect(): Promise<void> {
|
|
|
1793
1831
|
error: "stale GitHub credential generation",
|
|
1794
1832
|
};
|
|
1795
1833
|
}
|
|
1834
|
+
// ADR-136 sign-in box: the grant is not on disk; read it into
|
|
1835
|
+
// the cache so the disconnect still revokes it remotely.
|
|
1836
|
+
if (signInBoxMode()) {
|
|
1837
|
+
await loadGithubTokenIntoCache(frame.userId).catch(() => false);
|
|
1838
|
+
}
|
|
1796
1839
|
await onConnectClear(frame.userId, {
|
|
1797
1840
|
invalidateCredentials: invalidateTaskGithubGitCredentials,
|
|
1798
1841
|
reconcile: (taskId, userId) => {
|
|
@@ -1878,6 +1921,24 @@ async function connect(): Promise<void> {
|
|
|
1878
1921
|
);
|
|
1879
1922
|
break;
|
|
1880
1923
|
}
|
|
1924
|
+
if (frame.kind === "ssh.key.ensure" && signInBoxMode()) {
|
|
1925
|
+
// ADR-136 sign-in box: the keypair goes to the store first; only
|
|
1926
|
+
// the public half stays here.
|
|
1927
|
+
void managedMutationDrain.track(() =>
|
|
1928
|
+
ensureSshKeyStoreFirst(frame.userId).then(
|
|
1929
|
+
(publicKey) =>
|
|
1930
|
+
send(socket, { kind: "ssh.key.ack", userId: frame.userId, ok: true, publicKey }),
|
|
1931
|
+
(err) =>
|
|
1932
|
+
send(socket, {
|
|
1933
|
+
kind: "ssh.key.ack",
|
|
1934
|
+
userId: frame.userId,
|
|
1935
|
+
ok: false,
|
|
1936
|
+
error: err instanceof Error ? err.message : "ssh key op failed",
|
|
1937
|
+
}),
|
|
1938
|
+
),
|
|
1939
|
+
);
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1881
1942
|
try {
|
|
1882
1943
|
const publicKey =
|
|
1883
1944
|
frame.kind === "ssh.key.ensure"
|
|
@@ -1920,6 +1981,30 @@ async function connect(): Promise<void> {
|
|
|
1920
1981
|
case "env.var.list":
|
|
1921
1982
|
case "env.var.set":
|
|
1922
1983
|
case "env.var.delete": {
|
|
1984
|
+
if (signInBoxMode()) {
|
|
1985
|
+
// ADR-136 sign-in box: no local rows — every op is a store call,
|
|
1986
|
+
// and the ack lists names from the store.
|
|
1987
|
+
void managedMutationDrain.track(() =>
|
|
1988
|
+
(frame.kind === "env.var.set"
|
|
1989
|
+
? setProjectEnvStoreOnly(frame.projectId, frame.key, frame.value, frame.orgId)
|
|
1990
|
+
: frame.kind === "env.var.delete"
|
|
1991
|
+
? deleteProjectEnvStoreOnly(frame.projectId, frame.key)
|
|
1992
|
+
: Promise.resolve()
|
|
1993
|
+
)
|
|
1994
|
+
.then(() => listProjectEnvKeysStoreOnly(frame.projectId))
|
|
1995
|
+
.then(
|
|
1996
|
+
(keys) => send(socket, { kind: "env.var.ack", opId: frame.opId, ok: true, keys }),
|
|
1997
|
+
(err) =>
|
|
1998
|
+
send(socket, {
|
|
1999
|
+
kind: "env.var.ack",
|
|
2000
|
+
opId: frame.opId,
|
|
2001
|
+
ok: false,
|
|
2002
|
+
error: err instanceof Error ? err.message : "env var op failed",
|
|
2003
|
+
}),
|
|
2004
|
+
),
|
|
2005
|
+
);
|
|
2006
|
+
break;
|
|
2007
|
+
}
|
|
1923
2008
|
try {
|
|
1924
2009
|
if (frame.kind === "env.var.set") {
|
|
1925
2010
|
setProjectEnvVar(frame.projectId, frame.key, frame.value, frame.orgId);
|
package/src/protocol.ts
CHANGED
|
@@ -1489,27 +1489,33 @@ export interface HostCommands {
|
|
|
1489
1489
|
* exactly like the local API; re-advertises capabilities on success. */
|
|
1490
1490
|
engineAccountRemove(
|
|
1491
1491
|
ctx: CommandContext,
|
|
1492
|
-
input: { id: string; scope?: string },
|
|
1492
|
+
input: { id: string; scope?: string; hostId?: string },
|
|
1493
1493
|
): Promise<HostCommandResult<{ removed: boolean; message?: string }>>;
|
|
1494
1494
|
/** Manage-v1: relabel an extra account (never `default:*`); a scope may
|
|
1495
1495
|
* rename only its own accounts. Re-advertises on success. */
|
|
1496
1496
|
engineAccountRename(
|
|
1497
1497
|
ctx: CommandContext,
|
|
1498
|
-
input: { id: string; label: string; scope?: string },
|
|
1498
|
+
input: { id: string; label: string; scope?: string; hostId?: string },
|
|
1499
1499
|
): Promise<HostCommandResult<{ renamed: boolean; message?: string }>>;
|
|
1500
1500
|
/** Manage-v1: set the priority order of a kind's extra accounts within a
|
|
1501
1501
|
* scope — `ids` must be exactly that set. Tasks pick the first healthy
|
|
1502
1502
|
* account in this order. Re-advertises on success. */
|
|
1503
1503
|
engineAccountReorder(
|
|
1504
1504
|
ctx: CommandContext,
|
|
1505
|
-
input: { kind: string; ids: string[]; scope?: string },
|
|
1505
|
+
input: { kind: string; ids: string[]; scope?: string; hostId?: string },
|
|
1506
1506
|
): Promise<HostCommandResult<{ reordered: boolean; message?: string }>>;
|
|
1507
1507
|
/** Disconnect-v1: forget the host's DEFAULT credential for an engine — the
|
|
1508
1508
|
* .env.local key and/or the login file the adapter detects. Extras are
|
|
1509
|
-
* untouched (remove them by id). Re-advertises on success.
|
|
1509
|
+
* untouched (remove them by id). Re-advertises on success.
|
|
1510
|
+
*
|
|
1511
|
+
* `hostId` on the four account commands above: the runner the account lives
|
|
1512
|
+
* on. The cloud's bridge routes by it (2026-09-14: without it these fell to
|
|
1513
|
+
* the any-open-host fallback and landed on whichever runner connected
|
|
1514
|
+
* first — the label step after a sign-in answered "That account no longer
|
|
1515
|
+
* exists" and Disconnect did nothing). The host ignores the field. */
|
|
1510
1516
|
engineDisconnect(
|
|
1511
1517
|
ctx: CommandContext,
|
|
1512
|
-
input: { engine: EngineConnectKind },
|
|
1518
|
+
input: { engine: EngineConnectKind; hostId?: string },
|
|
1513
1519
|
): Promise<HostCommandResult<{ disconnected: boolean; message?: string }>>;
|
|
1514
1520
|
cloneRepo(
|
|
1515
1521
|
ctx: CommandContext,
|