@runuai/host 0.9.170 → 0.9.172
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/claude.ts +13 -0
- package/lib/agents/codex.ts +11 -5
- package/lib/agents/session-state.ts +77 -0
- package/lib/agents/transport.ts +2 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +3 -25
- package/lib/assignment-evidence.ts +2 -1
- package/lib/capture-flow.ts +14 -1
- package/lib/cloud-credential-leases.ts +9 -3
- package/lib/cloud-credential-runtime.ts +8 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/credential-cleanup-inventory.ts +2 -1
- package/lib/credential-consumers/engines.ts +29 -5
- package/lib/credentials-activation-authority.ts +158 -10
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +299 -0
- package/lib/custom-connection-runtime.ts +70 -0
- package/lib/engine-accounts.ts +1 -1
- package/lib/engine-credential-env.ts +2 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/legacy-runtime-retirement.ts +43 -8
- package/lib/obs.ts +2 -1
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +308 -26
- package/lib/secrets-proxy/address.ts +23 -0
- package/lib/task-credential-cleanup.ts +13 -3
- package/package.json +1 -1
- package/runner/runner.mjs +231 -3
- package/src/capture-wire.ts +102 -0
- package/src/cloud-credentials.ts +36 -372
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +207 -0
- package/src/main.ts +116 -3
- package/src/protocol.ts +111 -0
package/src/main.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
+
import "./load-env";
|
|
3
|
+
import { discoverCustomConnection, saveCustomConnection, saveCustomConnectionManual } from "../lib/custom-connection-capture";
|
|
4
|
+
import { OpenRouterBudgetPoller } from "../lib/openrouter-budget";
|
|
5
|
+
import { onLeasedEnginesChanged, leasedEnginesFor, isLeasedEngineAccount } from "../lib/credential-consumers/engines";
|
|
6
|
+
import { isOwnedInternalConnectionRuntime } from "../lib/internal-connections";
|
|
7
|
+
import { CUSTOM_CONNECTIONS_FEATURE, ENGINE_CONNECT_CUSTOM_FEATURE, ENGINE_CONNECT_CUSTOM_MANUAL_FEATURE, INTERNAL_CONNECTIONS_FEATURE, connectionOpId, parseCustomConnectionMaterial, parseCustomConnectionEnabled, parseEngineConnectCustomDiscoverFrame, parseEngineConnectCustomSaveFrame, parseEngineConnectCustomManualSaveFrame } from "./custom-connections";
|
|
2
8
|
import type { AssignmentDelivery, DeliveryOrigin } from "./protocol";
|
|
9
|
+
import { parseCaptureFlowWire } from "./cloud-credentials";
|
|
3
10
|
|
|
4
|
-
import "./load-env";
|
|
5
11
|
import { sampleDiskPressure } from "../lib/disk-pressure";
|
|
6
12
|
import { TASK_SECRETS_FEATURE, isTaskSecretStagingPut } from "./task-secrets";
|
|
7
13
|
import { writeTaskSecretStaging } from "../lib/task-secret-staging";
|
|
@@ -181,7 +187,7 @@ import {
|
|
|
181
187
|
rejectContainerRuntimeSelection,
|
|
182
188
|
reprobeContainerRuntimeMachineIdentity,
|
|
183
189
|
} from "../lib/container-runtime";
|
|
184
|
-
import { persistedTaskEnvironmentsMatchMachine, reconstructPersistedTaskEnvironment } from "../lib/task-environment";
|
|
190
|
+
import { machineTasksEnabled, persistedTaskEnvironmentsMatchMachine, reconstructPersistedTaskEnvironment } from "../lib/task-environment";
|
|
185
191
|
import { reconcileRunningTaskPersonCredentials } from "../lib/task-person-credentials";
|
|
186
192
|
import { logVnodePressure } from "../lib/vnode-pressure";
|
|
187
193
|
import { resolveAppleTunnelRoute } from "./apple-tunnel-route";
|
|
@@ -509,6 +515,15 @@ const bridgeUrl =
|
|
|
509
515
|
|
|
510
516
|
let ws: WebSocket | null = null;
|
|
511
517
|
let stopping = false;
|
|
518
|
+
const openRouterBudgets = new OpenRouterBudgetPoller({ emit: frame => {
|
|
519
|
+
if (!stopping && credentialFramesReady && ws?.readyState === WebSocket.OPEN) send(ws, frame);
|
|
520
|
+
} });
|
|
521
|
+
onLeasedEnginesChanged(taskId => {
|
|
522
|
+
if (stopping) { openRouterBudgets.stop(taskId); return; }
|
|
523
|
+
const lease = leasedEnginesFor(taskId);
|
|
524
|
+
openRouterBudgets.sync(taskId, lease?.open ? lease.accounts.flatMap(account => isLeasedEngineAccount(account) && account.authKind === "custom-connection" && account.connectionMaterial?.providerType === "openrouter" ? [{ credentialId: account.credentialId, revision: account.revision, apiKey: account.connectionMaterial.apiKey }] : []) : []);
|
|
525
|
+
});
|
|
526
|
+
|
|
512
527
|
let fatal = false;
|
|
513
528
|
/** Auth-rejected (4001) retry cadence. A genuinely revoked credential costs
|
|
514
529
|
* one connection attempt per interval; a transient rejection (the bridge's
|
|
@@ -523,6 +538,17 @@ let shutdownRequested = false;
|
|
|
523
538
|
let shutdownExitCode = 0;
|
|
524
539
|
const managedMutationDrain = new ManagedOperationDrain(maybeExit);
|
|
525
540
|
const hostLogManager = createHostLogManager();
|
|
541
|
+
const customConnectionOperations = new Map<AbortController, Promise<void>>();
|
|
542
|
+
|
|
543
|
+
function runCustomConnectionOperation(operation: (signal: AbortSignal) => Promise<void>): void {
|
|
544
|
+
void managedMutationDrain.track(async () => {
|
|
545
|
+
const controller = new AbortController();
|
|
546
|
+
const work = Promise.resolve().then(() => operation(controller.signal));
|
|
547
|
+
customConnectionOperations.set(controller, work);
|
|
548
|
+
try { await work; } finally { customConnectionOperations.delete(controller); }
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
526
552
|
const pendingRemoteOperationCleanups = new Set<Promise<void>>();
|
|
527
553
|
let stopActiveRemoteOperations = (): Promise<void> => Promise.resolve();
|
|
528
554
|
|
|
@@ -545,8 +571,10 @@ function trackRemoteOperationCleanup(cleanup: Promise<void>): Promise<void> {
|
|
|
545
571
|
function stopRemoteOperationManagers(): Promise<void> {
|
|
546
572
|
hostLogManager.stopAll();
|
|
547
573
|
engineConnectManager.stopAll();
|
|
574
|
+
for (const controller of customConnectionOperations.keys()) controller.abort();
|
|
548
575
|
return trackRemoteOperationCleanup(
|
|
549
576
|
Promise.all([
|
|
577
|
+
...customConnectionOperations.values(),
|
|
550
578
|
engineLoginManager.stopAll(),
|
|
551
579
|
// Tunnel upstreams are generation-scoped resources just like login
|
|
552
580
|
// containers; a bridge replacement or shutdown must join their
|
|
@@ -784,6 +812,9 @@ const credentialsActivation = new CredentialsActivationAuthority<CredentialsReti
|
|
|
784
812
|
requestManagedHostRestart();
|
|
785
813
|
},
|
|
786
814
|
log: (line) => console.warn(line),
|
|
815
|
+
// What stops the switch reaches the cloud too, not only this log.
|
|
816
|
+
onStatusChange: () => sendCapabilities(),
|
|
817
|
+
redact: redactHomeDirectory,
|
|
787
818
|
});
|
|
788
819
|
setCredentialsModeSource(() => credentialsActivation.mode());
|
|
789
820
|
/** ADR-154 §13: the steady/operator cadence. Active runners report their
|
|
@@ -1030,6 +1061,7 @@ const taskFiles = sidecarMode()
|
|
|
1030
1061
|
: null;
|
|
1031
1062
|
const cloudCredentials = new CloudCredentialRuntime({
|
|
1032
1063
|
runnerKind: sidecarMode() ? "task-machine" : "owned",
|
|
1064
|
+
internalRuntimeProvider: (assignment) => internalRuntimeProvider(assignment.taskId),
|
|
1033
1065
|
send: sendCredentialFrame,
|
|
1034
1066
|
quiesceProcesses: (request) => getOrchestrator().quiesceLeasedEngineAgents(request),
|
|
1035
1067
|
mcpServedChanged: (taskId) => getOrchestrator().mcpServedChanged(taskId),
|
|
@@ -1489,12 +1521,30 @@ void refreshDiskSnapshot();
|
|
|
1489
1521
|
const diskTimer = setInterval(() => void refreshDiskSnapshot(), 60_000);
|
|
1490
1522
|
diskTimer.unref?.();
|
|
1491
1523
|
|
|
1524
|
+
/** A persisted provider wins over this runner's default selection. Legacy
|
|
1525
|
+
* resources without a proven provider are unknown; only a new/empty task
|
|
1526
|
+
* may use the selected provisioning backend as its authoritative plan. */
|
|
1527
|
+
function internalRuntimeProvider(taskId?: string): string | null {
|
|
1528
|
+
if (sidecarMode() || signInBoxMode()) return null;
|
|
1529
|
+
const task = taskId ? getHostTask(taskId) : undefined;
|
|
1530
|
+
if (task?.environmentProvider !== null && task?.environmentProvider !== undefined) return task.environmentProvider;
|
|
1531
|
+
if (task && (task.composeProject !== null || task.worktreePath !== null)) return null;
|
|
1532
|
+
if (machineTasksEnabled()) return null;
|
|
1533
|
+
const runtime = containerRuntimeCapabilityState();
|
|
1534
|
+
if (runtime.status !== "ready") return null;
|
|
1535
|
+
return runtime.provider === "docker" ? "docker-compose" : runtime.provider === "apple-container" ? "apple-container" : null;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1492
1538
|
/** Build the current host capability advertisement (ADR-021). */
|
|
1493
1539
|
function buildCapabilities(): HostCapabilities {
|
|
1494
1540
|
const runtime = containerRuntimeCapabilityState();
|
|
1495
1541
|
const projectEnvBindings = projectEnvBindingCapability();
|
|
1496
1542
|
return {
|
|
1497
1543
|
version: packageVersion(),
|
|
1544
|
+
customConnections: credentialsMode() === "active",
|
|
1545
|
+
engineConnectCustom: signInBoxMode(),
|
|
1546
|
+
engineConnectCustomManual: signInBoxMode(),
|
|
1547
|
+
internalConnections: isOwnedInternalConnectionRuntime(internalRuntimeProvider()),
|
|
1498
1548
|
...(diskSnapshot ? { disk: diskSnapshot } : {}),
|
|
1499
1549
|
protocolFeatures: [
|
|
1500
1550
|
"host-workspace-file-read-v1",
|
|
@@ -1540,6 +1590,9 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1540
1590
|
// ADR-161 §1.11: this image's `uai files` reads cloud links. The cloud
|
|
1541
1591
|
// captures the advertisement at container creation.
|
|
1542
1592
|
HOST_AGENT_FILES_FEATURE,
|
|
1593
|
+
...(credentialsMode() === "active" ? [CUSTOM_CONNECTIONS_FEATURE] : []),
|
|
1594
|
+
...(signInBoxMode() ? [ENGINE_CONNECT_CUSTOM_FEATURE, ENGINE_CONNECT_CUSTOM_MANUAL_FEATURE] : []),
|
|
1595
|
+
...(isOwnedInternalConnectionRuntime(internalRuntimeProvider()) ? [INTERNAL_CONNECTIONS_FEATURE] : []),
|
|
1543
1596
|
// ADR-154: only the sign-in box writes cloud credentials.
|
|
1544
1597
|
...(signInBoxMode()
|
|
1545
1598
|
? [
|
|
@@ -1581,9 +1634,18 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1581
1634
|
engineAccounts: engineAccountSummaries(),
|
|
1582
1635
|
engines: engineCatalogCapabilities(),
|
|
1583
1636
|
githubUsers: connectedUserIds(),
|
|
1637
|
+
// ADR-154 §13: where the switch to cloud credentials stands, and why.
|
|
1638
|
+
credentialsSwitch: credentialsActivation.status(),
|
|
1584
1639
|
};
|
|
1585
1640
|
}
|
|
1586
1641
|
|
|
1642
|
+
/** A path under the home directory, as `~/…`: a status line leaves the
|
|
1643
|
+
* machine, and the account name in a path is not the cloud's to keep. */
|
|
1644
|
+
function redactHomeDirectory(text: string): string {
|
|
1645
|
+
const home = homedir();
|
|
1646
|
+
return home.length > 1 ? text.split(home).join("~") : text;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1587
1649
|
// engineCliStatuses probes binaries asynchronously; capabilities build
|
|
1588
1650
|
// synchronously. The snapshot starts empty (cliInstalled: false) and each
|
|
1589
1651
|
// build kicks a refresh that re-advertises only when the answer changed.
|
|
@@ -3046,7 +3108,7 @@ async function connect(): Promise<void> {
|
|
|
3046
3108
|
}
|
|
3047
3109
|
|
|
3048
3110
|
const frame = parseCloudFrame(data);
|
|
3049
|
-
if (!frame) return;
|
|
3111
|
+
if (!frame) { replyToInvalidCustomConnectionFrame(socket, data); return; }
|
|
3050
3112
|
if (stopping && rejectFrameDuringShutdown(socket, frame)) return;
|
|
3051
3113
|
if (signInBoxMode() && rejectFrameOnSignInBox(socket, frame)) return;
|
|
3052
3114
|
if (!onOwnCredentials() && rejectCredentialFrameOffOwnCredentials(socket, frame)) return;
|
|
@@ -3597,6 +3659,21 @@ async function connect(): Promise<void> {
|
|
|
3597
3659
|
);
|
|
3598
3660
|
break;
|
|
3599
3661
|
}
|
|
3662
|
+
case "engine.connect.custom.discover": {
|
|
3663
|
+
if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
|
|
3664
|
+
runCustomConnectionOperation(signal => discoverCustomConnection(frame, { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result })));
|
|
3665
|
+
break;
|
|
3666
|
+
}
|
|
3667
|
+
case "engine.connect.custom.manual-save": {
|
|
3668
|
+
if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
|
|
3669
|
+
runCustomConnectionOperation(signal => saveCustomConnectionManual(frame, configuredCredentialStore(), { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result })));
|
|
3670
|
+
break;
|
|
3671
|
+
}
|
|
3672
|
+
case "engine.connect.custom.save": {
|
|
3673
|
+
if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
|
|
3674
|
+
runCustomConnectionOperation(signal => saveCustomConnection(frame, configuredCredentialStore(), { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result })));
|
|
3675
|
+
break;
|
|
3676
|
+
}
|
|
3600
3677
|
case "mcp.capture.probe":
|
|
3601
3678
|
case "mcp.capture.complete": {
|
|
3602
3679
|
// ADR-154 §11: only the sign-in box runs an MCP handshake as a
|
|
@@ -4819,6 +4896,24 @@ const PICKER_FAILURE_TEXT: Record<GitHubRepositoryAccessErrorCode, string> = {
|
|
|
4819
4896
|
invalid_response: "GitHub answered with something unexpected",
|
|
4820
4897
|
};
|
|
4821
4898
|
|
|
4899
|
+
function replyToInvalidCustomConnectionFrame(socket: WebSocket, data: RawData): void {
|
|
4900
|
+
let value: unknown;
|
|
4901
|
+
try { value = JSON.parse(rawDataToBuffer(data).toString("utf8")); } catch { return; }
|
|
4902
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
4903
|
+
const frame = value as Record<string, unknown>;
|
|
4904
|
+
if (!connectionOpId(frame.opId)) return;
|
|
4905
|
+
if (frame.kind === "engine.connect.custom.discover") {
|
|
4906
|
+
send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unsupported" } });
|
|
4907
|
+
} else if (frame.kind === "engine.connect.custom.save" || frame.kind === "engine.connect.custom.manual-save") {
|
|
4908
|
+
const settings = parseCustomConnectionMaterial(frame.settings);
|
|
4909
|
+
const flow = parseCaptureFlowWire(frame.flow);
|
|
4910
|
+
const supported = settings && (frame.kind !== "engine.connect.custom.manual-save" || (settings.providerType === "custom" && settings.internal === true)) && parseCustomConnectionEnabled(frame.enabled) && flow &&
|
|
4911
|
+
flow.location.class === "engine" && flow.location.engineKind === settings.engine && flow.brokerParameter === null;
|
|
4912
|
+
send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId,
|
|
4913
|
+
result: { ok: false, reason: supported ? "invalid-models" : "unsupported" } });
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4822
4917
|
function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
4823
4918
|
let parsed: unknown;
|
|
4824
4919
|
try {
|
|
@@ -5080,6 +5175,9 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
5080
5175
|
// ADR-154: the v1 lease, credential and task env replica frames, parsed
|
|
5081
5176
|
// to their exact shapes here; only a runner on cloud credentials acts on
|
|
5082
5177
|
// them.
|
|
5178
|
+
if (frame.kind === "engine.connect.custom.discover") return parseEngineConnectCustomDiscoverFrame(frame);
|
|
5179
|
+
if (frame.kind === "engine.connect.custom.manual-save") return parseEngineConnectCustomManualSaveFrame(frame);
|
|
5180
|
+
if (frame.kind === "engine.connect.custom.save") return parseEngineConnectCustomSaveFrame(frame);
|
|
5083
5181
|
if (frame.kind === "lease.grant.v1") return parseLeaseGrantV1Frame(frame);
|
|
5084
5182
|
if (frame.kind === "lease.unavailable") return parseLeaseUnavailableFrame(frame);
|
|
5085
5183
|
if (frame.kind === "lease.stale") return parseLeaseStaleFrame(frame);
|
|
@@ -5201,6 +5299,13 @@ function rejectFrameDuringShutdown(
|
|
|
5201
5299
|
): boolean {
|
|
5202
5300
|
const error = "managed host is restarting; retry the operation";
|
|
5203
5301
|
switch (frame.kind) {
|
|
5302
|
+
case "engine.connect.custom.discover":
|
|
5303
|
+
send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unreachable" } });
|
|
5304
|
+
return true;
|
|
5305
|
+
case "engine.connect.custom.manual-save":
|
|
5306
|
+
case "engine.connect.custom.save":
|
|
5307
|
+
send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unreachable" } });
|
|
5308
|
+
return true;
|
|
5204
5309
|
case "command":
|
|
5205
5310
|
send(socket, {
|
|
5206
5311
|
kind: "result",
|
|
@@ -5614,6 +5719,12 @@ function expectTaskAgents(value: unknown): TaskAgent[] {
|
|
|
5614
5719
|
) {
|
|
5615
5720
|
out.accountId = row.accountId;
|
|
5616
5721
|
}
|
|
5722
|
+
if (row.accountBinding !== undefined) {
|
|
5723
|
+
if (row.accountBinding !== "required" && row.accountBinding !== "preferred") throw new Error("invalid command args: account binding");
|
|
5724
|
+
if (row.accountBinding === "required" && row.kind !== "claude" && row.kind !== "codex") throw new Error("invalid command args: unsupported required account engine");
|
|
5725
|
+
if (row.accountBinding === "required" && !out.accountId) throw new Error("invalid command args: required account missing");
|
|
5726
|
+
out.accountBinding = row.accountBinding;
|
|
5727
|
+
}
|
|
5617
5728
|
if (row.uaiProvider === "anthropic" || row.uaiProvider === "openai") {
|
|
5618
5729
|
out.uaiProvider = row.uaiProvider;
|
|
5619
5730
|
}
|
|
@@ -6095,6 +6206,7 @@ function reconnectDelay(): number {
|
|
|
6095
6206
|
function requestShutdown(signal: NodeJS.Signals): void {
|
|
6096
6207
|
console.log(`[host-agent] ${signal} received; shutting down`);
|
|
6097
6208
|
stopping = true;
|
|
6209
|
+
openRouterBudgets.stop();
|
|
6098
6210
|
// Cancel human-paced operations without invalidating this socket's exact
|
|
6099
6211
|
// generation: already-admitted one-shot config writes may still ack while
|
|
6100
6212
|
// the ordinary in-flight drain completes.
|
|
@@ -6107,6 +6219,7 @@ function requestShutdown(signal: NodeJS.Signals): void {
|
|
|
6107
6219
|
// it). Exit waits for it.
|
|
6108
6220
|
void cloudCredentials
|
|
6109
6221
|
.shutdown()
|
|
6222
|
+
.then(() => openRouterBudgets.drain())
|
|
6110
6223
|
.catch((error: unknown) =>
|
|
6111
6224
|
console.warn(`[credentials] shutdown failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
6112
6225
|
)
|
package/src/protocol.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export * from "./custom-connections";
|
|
2
|
+
import type { AccountBinding, EngineConnectCustomDiscoverFrame, EngineConnectCustomDiscoveredFrame, EngineConnectCustomSaveFrame, EngineConnectCustomManualSaveFrame, EngineConnectCustomSavedFrame, OpenRouterBudgetFrame } from "./custom-connections";
|
|
1
3
|
import type { TaskSecretApplied, TaskSecretStagingPut, TaskSecretStagingAck } from "./task-secrets";
|
|
2
4
|
import {
|
|
3
5
|
parseCaptureFlowWire,
|
|
@@ -3154,6 +3156,7 @@ export interface TaskAgent {
|
|
|
3154
3156
|
effort?: string;
|
|
3155
3157
|
/** ADR-157 addendum §7: preferred cloud-leased AI account. */
|
|
3156
3158
|
accountId?: string;
|
|
3159
|
+
accountBinding?: AccountBinding;
|
|
3157
3160
|
defaultPrompt?: string;
|
|
3158
3161
|
initialPrompt?: string;
|
|
3159
3162
|
/** ADR-046 reference material resolved from the agent's persona + team.
|
|
@@ -4068,6 +4071,10 @@ export function isHostConfigAckFrame(
|
|
|
4068
4071
|
* changes. The cloud caches it per hostId to drive the task-creation pickers.
|
|
4069
4072
|
*/
|
|
4070
4073
|
export interface HostCapabilities {
|
|
4074
|
+
customConnections?: boolean;
|
|
4075
|
+
engineConnectCustom?: boolean;
|
|
4076
|
+
engineConnectCustomManual?: boolean;
|
|
4077
|
+
internalConnections?: boolean;
|
|
4071
4078
|
/** Filesystem capacity, refreshed each minute. Null means unmeasurable
|
|
4072
4079
|
* (for example Docker in a VM); available excludes root-reserved blocks. */
|
|
4073
4080
|
disk?: {
|
|
@@ -4140,6 +4147,99 @@ export interface HostCapabilities {
|
|
|
4140
4147
|
// the per-host gh-connected state shown on the host detail page. Re-advertised
|
|
4141
4148
|
// whenever a token is added/removed. Optional: older hosts omit it.
|
|
4142
4149
|
githubUsers?: string[];
|
|
4150
|
+
/** ADR-154 §13: where this runner's switch to cloud credentials stands, and
|
|
4151
|
+
* what stops it. Optional: hosts before 0.9.171 omit it, and the cloud
|
|
4152
|
+
* then says only that the runner has not switched. */
|
|
4153
|
+
credentialsSwitch?: CredentialsSwitchCapability;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
/**
|
|
4157
|
+
* ADR-154 §13: the runner's switch to cloud credentials, as its capability
|
|
4158
|
+
* frame reports it. `state` is the runner's credentials mode. `stuck` is the
|
|
4159
|
+
* latest substantive refusal or blocker while the switch has not finished:
|
|
4160
|
+
* `by: "cloud"` the cloud's answer, `by: "runner"` something on the machine.
|
|
4161
|
+
* A transient one (the container runtime still preparing after a reconnect,
|
|
4162
|
+
* the bridge down) never replaces a substantive one; it shows only when
|
|
4163
|
+
* there is nothing else to say. Bounded and secret-free: the runner's own log
|
|
4164
|
+
* words (task ids, Compose project names, folder paths with the home
|
|
4165
|
+
* directory as `~`), never a credential.
|
|
4166
|
+
*/
|
|
4167
|
+
export interface CredentialsSwitchCapability {
|
|
4168
|
+
state: "legacy" | "pending" | "active";
|
|
4169
|
+
stuck?: CredentialsSwitchStuck;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
export interface CredentialsSwitchStuck {
|
|
4173
|
+
by: "cloud" | "runner";
|
|
4174
|
+
/** The refusal or blocker in one line. */
|
|
4175
|
+
detail: string;
|
|
4176
|
+
/** Each blocker on its own, when the runner found several. */
|
|
4177
|
+
blockers?: string[];
|
|
4178
|
+
/** The `task-*` Compose projects nothing proves Uai's, which the operator
|
|
4179
|
+
* removes (`docker compose -p <name> down`) or renames: the first
|
|
4180
|
+
* `MAX_CREDENTIALS_SWITCH_PROJECTS` names, and how many there are. */
|
|
4181
|
+
composeProjects?: { names: string[]; total: number };
|
|
4182
|
+
/** Nothing is wrong yet: the runner could not look (see above). */
|
|
4183
|
+
transient?: boolean;
|
|
4184
|
+
/** Epoch ms this refusal or blocker was first seen, unchanged since. */
|
|
4185
|
+
since: number;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
export const MAX_CREDENTIALS_SWITCH_DETAIL_CHARS = 600;
|
|
4189
|
+
export const MAX_CREDENTIALS_SWITCH_BLOCKERS = 20;
|
|
4190
|
+
export const MAX_CREDENTIALS_SWITCH_BLOCKER_CHARS = 300;
|
|
4191
|
+
export const MAX_CREDENTIALS_SWITCH_PROJECTS = 100;
|
|
4192
|
+
|
|
4193
|
+
/** The capability, exact and bounded; undefined when it is not one. The
|
|
4194
|
+
* cloud drops a malformed one alone, never the frame around it. */
|
|
4195
|
+
export function parseCredentialsSwitchCapability(value: unknown): CredentialsSwitchCapability | undefined {
|
|
4196
|
+
if (!isPlainRecord(value)) return undefined;
|
|
4197
|
+
const { state, stuck } = value;
|
|
4198
|
+
if (state !== "legacy" && state !== "pending" && state !== "active") return undefined;
|
|
4199
|
+
if (Object.keys(value).some((key) => key !== "state" && key !== "stuck")) return undefined;
|
|
4200
|
+
if (stuck === undefined) return { state };
|
|
4201
|
+
if (!isPlainRecord(stuck)) return undefined;
|
|
4202
|
+
const allowed = new Set(["by", "detail", "blockers", "composeProjects", "transient", "since"]);
|
|
4203
|
+
if (Object.keys(stuck).some((key) => !allowed.has(key))) return undefined;
|
|
4204
|
+
const boundedText = (text: unknown, max: number): text is string =>
|
|
4205
|
+
typeof text === "string" && text.length >= 1 && text.length <= max;
|
|
4206
|
+
if (stuck.by !== "cloud" && stuck.by !== "runner") return undefined;
|
|
4207
|
+
if (!boundedText(stuck.detail, MAX_CREDENTIALS_SWITCH_DETAIL_CHARS)) return undefined;
|
|
4208
|
+
if (typeof stuck.since !== "number" || !Number.isSafeInteger(stuck.since) || stuck.since < 0) return undefined;
|
|
4209
|
+
if (stuck.transient !== undefined && typeof stuck.transient !== "boolean") return undefined;
|
|
4210
|
+
const out: CredentialsSwitchStuck = { by: stuck.by, detail: stuck.detail, since: stuck.since };
|
|
4211
|
+
if (stuck.blockers !== undefined) {
|
|
4212
|
+
if (
|
|
4213
|
+
!Array.isArray(stuck.blockers) ||
|
|
4214
|
+
stuck.blockers.length > MAX_CREDENTIALS_SWITCH_BLOCKERS ||
|
|
4215
|
+
!stuck.blockers.every((blocker) => boundedText(blocker, MAX_CREDENTIALS_SWITCH_BLOCKER_CHARS))
|
|
4216
|
+
) {
|
|
4217
|
+
return undefined;
|
|
4218
|
+
}
|
|
4219
|
+
out.blockers = [...stuck.blockers];
|
|
4220
|
+
}
|
|
4221
|
+
if (stuck.composeProjects !== undefined) {
|
|
4222
|
+
const projects = stuck.composeProjects;
|
|
4223
|
+
if (!isPlainRecord(projects) || Object.keys(projects).some((key) => key !== "names" && key !== "total")) return undefined;
|
|
4224
|
+
const { names, total } = projects;
|
|
4225
|
+
if (
|
|
4226
|
+
!Array.isArray(names) ||
|
|
4227
|
+
names.length > MAX_CREDENTIALS_SWITCH_PROJECTS ||
|
|
4228
|
+
!names.every((name) => typeof name === "string" && /^task-[a-z0-9][a-z0-9_-]{0,127}$/.test(name)) ||
|
|
4229
|
+
typeof total !== "number" ||
|
|
4230
|
+
!Number.isSafeInteger(total) ||
|
|
4231
|
+
total < names.length
|
|
4232
|
+
) {
|
|
4233
|
+
return undefined;
|
|
4234
|
+
}
|
|
4235
|
+
out.composeProjects = { names: [...names] as string[], total };
|
|
4236
|
+
}
|
|
4237
|
+
if (stuck.transient === true) out.transient = true;
|
|
4238
|
+
return { state, stuck: out };
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
4242
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4143
4243
|
}
|
|
4144
4244
|
|
|
4145
4245
|
/** One transcript projection the cloud asks the host to append to (ADR-083). */
|
|
@@ -4811,6 +4911,9 @@ export interface TaskMachineRetireAckFrame {
|
|
|
4811
4911
|
}
|
|
4812
4912
|
|
|
4813
4913
|
export type CloudToHost =
|
|
4914
|
+
| EngineConnectCustomDiscoverFrame
|
|
4915
|
+
| EngineConnectCustomSaveFrame
|
|
4916
|
+
| EngineConnectCustomManualSaveFrame
|
|
4814
4917
|
| {
|
|
4815
4918
|
kind: "command";
|
|
4816
4919
|
commandId: string;
|
|
@@ -4990,6 +5093,9 @@ export interface AssignmentDelivery {
|
|
|
4990
5093
|
recipientGeneration: string | null;
|
|
4991
5094
|
}
|
|
4992
5095
|
export interface AssignmentTurnEvidence {
|
|
5096
|
+
credentialId?: string | null;
|
|
5097
|
+
credentialRevision?: string | null;
|
|
5098
|
+
connection?: boolean | null;
|
|
4993
5099
|
leadRunId?: string | null;
|
|
4994
5100
|
turnId: string;
|
|
4995
5101
|
assignmentId: string | null;
|
|
@@ -5063,6 +5169,9 @@ export type McpOp =
|
|
|
5063
5169
|
| { kind: "disconnect"; connectionId: string };
|
|
5064
5170
|
|
|
5065
5171
|
export type HostToCloud =
|
|
5172
|
+
| EngineConnectCustomDiscoveredFrame
|
|
5173
|
+
| EngineConnectCustomSavedFrame
|
|
5174
|
+
| OpenRouterBudgetFrame
|
|
5066
5175
|
// ADR-103: `bootId` scopes event seqs to one host process lifetime — the
|
|
5067
5176
|
// cloud's replay watermark only applies within a matching boot. Optional
|
|
5068
5177
|
// for pre-103 hosts (which also never send seq'd events).
|
|
@@ -5265,6 +5374,8 @@ export type HostEvent =
|
|
|
5265
5374
|
* `credentialId` is the leased AI account the turn ran on (host
|
|
5266
5375
|
* 0.9.167+); older runners omit it and the cloud stores null. */
|
|
5267
5376
|
usage?: {
|
|
5377
|
+
connection?: boolean;
|
|
5378
|
+
credentialRevision?: string;
|
|
5268
5379
|
model?: string;
|
|
5269
5380
|
inputTokens?: number;
|
|
5270
5381
|
outputTokens?: number;
|