@runuai/host 0.9.169 → 0.9.171
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/agent-cli.ts +56 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agents/codex.ts +222 -41
- package/lib/agents/machine-durable.ts +8 -2
- package/lib/agents/types.ts +16 -2
- package/lib/credentials-activation-authority.ts +158 -10
- package/lib/legacy-runtime-retirement.ts +43 -8
- package/lib/orchestrator.ts +72 -33
- package/package.json +1 -1
- package/src/main.ts +12 -0
- package/src/protocol.ts +93 -0
package/src/protocol.ts
CHANGED
|
@@ -4140,6 +4140,99 @@ export interface HostCapabilities {
|
|
|
4140
4140
|
// the per-host gh-connected state shown on the host detail page. Re-advertised
|
|
4141
4141
|
// whenever a token is added/removed. Optional: older hosts omit it.
|
|
4142
4142
|
githubUsers?: string[];
|
|
4143
|
+
/** ADR-154 §13: where this runner's switch to cloud credentials stands, and
|
|
4144
|
+
* what stops it. Optional: hosts before 0.9.171 omit it, and the cloud
|
|
4145
|
+
* then says only that the runner has not switched. */
|
|
4146
|
+
credentialsSwitch?: CredentialsSwitchCapability;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
/**
|
|
4150
|
+
* ADR-154 §13: the runner's switch to cloud credentials, as its capability
|
|
4151
|
+
* frame reports it. `state` is the runner's credentials mode. `stuck` is the
|
|
4152
|
+
* latest substantive refusal or blocker while the switch has not finished:
|
|
4153
|
+
* `by: "cloud"` the cloud's answer, `by: "runner"` something on the machine.
|
|
4154
|
+
* A transient one (the container runtime still preparing after a reconnect,
|
|
4155
|
+
* the bridge down) never replaces a substantive one; it shows only when
|
|
4156
|
+
* there is nothing else to say. Bounded and secret-free: the runner's own log
|
|
4157
|
+
* words (task ids, Compose project names, folder paths with the home
|
|
4158
|
+
* directory as `~`), never a credential.
|
|
4159
|
+
*/
|
|
4160
|
+
export interface CredentialsSwitchCapability {
|
|
4161
|
+
state: "legacy" | "pending" | "active";
|
|
4162
|
+
stuck?: CredentialsSwitchStuck;
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
export interface CredentialsSwitchStuck {
|
|
4166
|
+
by: "cloud" | "runner";
|
|
4167
|
+
/** The refusal or blocker in one line. */
|
|
4168
|
+
detail: string;
|
|
4169
|
+
/** Each blocker on its own, when the runner found several. */
|
|
4170
|
+
blockers?: string[];
|
|
4171
|
+
/** The `task-*` Compose projects nothing proves Uai's, which the operator
|
|
4172
|
+
* removes (`docker compose -p <name> down`) or renames: the first
|
|
4173
|
+
* `MAX_CREDENTIALS_SWITCH_PROJECTS` names, and how many there are. */
|
|
4174
|
+
composeProjects?: { names: string[]; total: number };
|
|
4175
|
+
/** Nothing is wrong yet: the runner could not look (see above). */
|
|
4176
|
+
transient?: boolean;
|
|
4177
|
+
/** Epoch ms this refusal or blocker was first seen, unchanged since. */
|
|
4178
|
+
since: number;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
export const MAX_CREDENTIALS_SWITCH_DETAIL_CHARS = 600;
|
|
4182
|
+
export const MAX_CREDENTIALS_SWITCH_BLOCKERS = 20;
|
|
4183
|
+
export const MAX_CREDENTIALS_SWITCH_BLOCKER_CHARS = 300;
|
|
4184
|
+
export const MAX_CREDENTIALS_SWITCH_PROJECTS = 100;
|
|
4185
|
+
|
|
4186
|
+
/** The capability, exact and bounded; undefined when it is not one. The
|
|
4187
|
+
* cloud drops a malformed one alone, never the frame around it. */
|
|
4188
|
+
export function parseCredentialsSwitchCapability(value: unknown): CredentialsSwitchCapability | undefined {
|
|
4189
|
+
if (!isPlainRecord(value)) return undefined;
|
|
4190
|
+
const { state, stuck } = value;
|
|
4191
|
+
if (state !== "legacy" && state !== "pending" && state !== "active") return undefined;
|
|
4192
|
+
if (Object.keys(value).some((key) => key !== "state" && key !== "stuck")) return undefined;
|
|
4193
|
+
if (stuck === undefined) return { state };
|
|
4194
|
+
if (!isPlainRecord(stuck)) return undefined;
|
|
4195
|
+
const allowed = new Set(["by", "detail", "blockers", "composeProjects", "transient", "since"]);
|
|
4196
|
+
if (Object.keys(stuck).some((key) => !allowed.has(key))) return undefined;
|
|
4197
|
+
const boundedText = (text: unknown, max: number): text is string =>
|
|
4198
|
+
typeof text === "string" && text.length >= 1 && text.length <= max;
|
|
4199
|
+
if (stuck.by !== "cloud" && stuck.by !== "runner") return undefined;
|
|
4200
|
+
if (!boundedText(stuck.detail, MAX_CREDENTIALS_SWITCH_DETAIL_CHARS)) return undefined;
|
|
4201
|
+
if (typeof stuck.since !== "number" || !Number.isSafeInteger(stuck.since) || stuck.since < 0) return undefined;
|
|
4202
|
+
if (stuck.transient !== undefined && typeof stuck.transient !== "boolean") return undefined;
|
|
4203
|
+
const out: CredentialsSwitchStuck = { by: stuck.by, detail: stuck.detail, since: stuck.since };
|
|
4204
|
+
if (stuck.blockers !== undefined) {
|
|
4205
|
+
if (
|
|
4206
|
+
!Array.isArray(stuck.blockers) ||
|
|
4207
|
+
stuck.blockers.length > MAX_CREDENTIALS_SWITCH_BLOCKERS ||
|
|
4208
|
+
!stuck.blockers.every((blocker) => boundedText(blocker, MAX_CREDENTIALS_SWITCH_BLOCKER_CHARS))
|
|
4209
|
+
) {
|
|
4210
|
+
return undefined;
|
|
4211
|
+
}
|
|
4212
|
+
out.blockers = [...stuck.blockers];
|
|
4213
|
+
}
|
|
4214
|
+
if (stuck.composeProjects !== undefined) {
|
|
4215
|
+
const projects = stuck.composeProjects;
|
|
4216
|
+
if (!isPlainRecord(projects) || Object.keys(projects).some((key) => key !== "names" && key !== "total")) return undefined;
|
|
4217
|
+
const { names, total } = projects;
|
|
4218
|
+
if (
|
|
4219
|
+
!Array.isArray(names) ||
|
|
4220
|
+
names.length > MAX_CREDENTIALS_SWITCH_PROJECTS ||
|
|
4221
|
+
!names.every((name) => typeof name === "string" && /^task-[a-z0-9][a-z0-9_-]{0,127}$/.test(name)) ||
|
|
4222
|
+
typeof total !== "number" ||
|
|
4223
|
+
!Number.isSafeInteger(total) ||
|
|
4224
|
+
total < names.length
|
|
4225
|
+
) {
|
|
4226
|
+
return undefined;
|
|
4227
|
+
}
|
|
4228
|
+
out.composeProjects = { names: [...names] as string[], total };
|
|
4229
|
+
}
|
|
4230
|
+
if (stuck.transient === true) out.transient = true;
|
|
4231
|
+
return { state, stuck: out };
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
4235
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4143
4236
|
}
|
|
4144
4237
|
|
|
4145
4238
|
/** One transcript projection the cloud asks the host to append to (ADR-083). */
|