@runuai/host 0.9.139 → 0.9.140
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/src/cli.ts +35 -6
- package/src/main.ts +93 -8
- package/src/protocol.ts +11 -5
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,
|