@phnx-labs/agents-cli 1.22.26 → 1.22.27
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/CHANGELOG.md +244 -0
- package/README.md +18 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +345 -37
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +3 -3
- package/dist/index.js +16 -20
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +22 -0
- package/dist/lib/hosts/passthrough.js +14 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +4 -2
- package/dist/lib/hosts/ready.js +24 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +90 -34
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +19 -2
- package/dist/lib/session/db.js +54 -5
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { getCliLaunch } from "../cli-entry.js";
|
|
6
|
+
export function extractTokenCount(stdout, stderr) {
|
|
7
|
+
const matches = [
|
|
8
|
+
...`${stdout}\n${stderr}`.matchAll(/(?:tokens used|total tokens|token usage)\s*[:=]?\s*([\d,]+)/gi),
|
|
9
|
+
];
|
|
10
|
+
const raw = matches.at(-1)?.[1];
|
|
11
|
+
if (!raw)
|
|
12
|
+
return undefined;
|
|
13
|
+
const tokens = Number(raw.replaceAll(",", ""));
|
|
14
|
+
return Number.isSafeInteger(tokens) && tokens >= 0 ? tokens : undefined;
|
|
15
|
+
}
|
|
16
|
+
export async function executeCellViaAgentsRun(request) {
|
|
17
|
+
const args = [
|
|
18
|
+
"run",
|
|
19
|
+
request.agent,
|
|
20
|
+
request.prompt,
|
|
21
|
+
"--cwd",
|
|
22
|
+
request.cwd,
|
|
23
|
+
"--headless",
|
|
24
|
+
"--no-tmux",
|
|
25
|
+
];
|
|
26
|
+
if (request.model)
|
|
27
|
+
args.push("--model", request.model);
|
|
28
|
+
const launch = getCliLaunch(args);
|
|
29
|
+
return await new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(launch.command, launch.args, {
|
|
31
|
+
cwd: request.cwd,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
child.stdout.on("data", (chunk) => {
|
|
37
|
+
stdout += chunk.toString();
|
|
38
|
+
});
|
|
39
|
+
child.stderr.on("data", (chunk) => {
|
|
40
|
+
stderr += chunk.toString();
|
|
41
|
+
});
|
|
42
|
+
child.on("error", reject);
|
|
43
|
+
child.on("close", (code) => resolve({
|
|
44
|
+
exit: code,
|
|
45
|
+
stdout,
|
|
46
|
+
stderr,
|
|
47
|
+
tokens: extractTokenCount(stdout, stderr),
|
|
48
|
+
}));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function copyFixture(task, cellIndex, runRoot) {
|
|
52
|
+
const cwd = path.join(runRoot, `cell-${cellIndex}`);
|
|
53
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
54
|
+
if (fs.existsSync(task.fixtureDir))
|
|
55
|
+
fs.cpSync(task.fixtureDir, cwd, { recursive: true });
|
|
56
|
+
return cwd;
|
|
57
|
+
}
|
|
58
|
+
export async function runCells(options) {
|
|
59
|
+
if (!Number.isInteger(options.concurrency) || options.concurrency < 1)
|
|
60
|
+
throw new Error("concurrency must be a positive integer");
|
|
61
|
+
if (options.cells.length === 0)
|
|
62
|
+
throw new Error("at least one bench cell is required");
|
|
63
|
+
const runId = `${new Date().toISOString().replace(/[-:.TZ]/g, "")}-${process.pid}`;
|
|
64
|
+
const startedAt = new Date();
|
|
65
|
+
const runRoot = fs.mkdtempSync(path.join(options.tempRoot ?? os.tmpdir(), `agents-bench-${runId}-`));
|
|
66
|
+
const results = new Array(options.cells.length);
|
|
67
|
+
const execute = options.execute ?? executeCellViaAgentsRun;
|
|
68
|
+
let next = 0;
|
|
69
|
+
const worker = async () => {
|
|
70
|
+
for (;;) {
|
|
71
|
+
const index = next++;
|
|
72
|
+
if (index >= options.cells.length)
|
|
73
|
+
return;
|
|
74
|
+
const cell = options.cells[index];
|
|
75
|
+
const cwd = options.task
|
|
76
|
+
? copyFixture(options.task, index, runRoot)
|
|
77
|
+
: path.join(runRoot, `cell-${index}`);
|
|
78
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
79
|
+
const started = performance.now();
|
|
80
|
+
try {
|
|
81
|
+
const output = await execute({ ...cell, prompt: options.prompt, cwd });
|
|
82
|
+
results[index] = {
|
|
83
|
+
...cell,
|
|
84
|
+
...output,
|
|
85
|
+
wall_ms: Math.round(performance.now() - started),
|
|
86
|
+
status: output.exit === 0 ? "passed" : "failed",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
results[index] = {
|
|
91
|
+
...cell,
|
|
92
|
+
exit: null,
|
|
93
|
+
stdout: "",
|
|
94
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
95
|
+
wall_ms: Math.round(performance.now() - started),
|
|
96
|
+
status: "failed",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
await Promise.all(Array.from({ length: Math.min(options.concurrency, options.cells.length) }, worker));
|
|
102
|
+
return {
|
|
103
|
+
schema_version: 1,
|
|
104
|
+
run_id: runId,
|
|
105
|
+
...(options.task ? { task_id: options.task.id } : {}),
|
|
106
|
+
prompt: options.prompt,
|
|
107
|
+
started_at: startedAt.toISOString(),
|
|
108
|
+
finished_at: new Date().toISOString(),
|
|
109
|
+
cells: results,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BenchRunResult, BenchTask } from "./types.js";
|
|
2
|
+
export declare function parseTask(raw: unknown, sourcePath: string): BenchTask;
|
|
3
|
+
export declare function parseRunResult(raw: unknown): BenchRunResult;
|
|
4
|
+
export declare function validateRunId(runId: string): string;
|
|
5
|
+
export declare function loadTask(taskId: string, tasksRoot: string): BenchTask;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
function object(value, label) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error(`${label} must be an object`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function string(value, label) {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
10
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function parseTask(raw, sourcePath) {
|
|
14
|
+
const value = object(raw, "task.json");
|
|
15
|
+
const id = string(value.id, "task.id");
|
|
16
|
+
const prompt = string(value.prompt, "task.prompt");
|
|
17
|
+
if (!Array.isArray(value.pass) || value.pass.length === 0)
|
|
18
|
+
throw new Error("task.pass must be a non-empty array");
|
|
19
|
+
const pass = value.pass.map((item, index) => {
|
|
20
|
+
const criterion = object(item, `task.pass[${index}]`);
|
|
21
|
+
const type = string(criterion.type, `task.pass[${index}].type`);
|
|
22
|
+
if (type === "exit_zero")
|
|
23
|
+
return { type };
|
|
24
|
+
if (type === "file_exists")
|
|
25
|
+
return { type, path: string(criterion.path, `task.pass[${index}].path`) };
|
|
26
|
+
if (type === "stdout_contains")
|
|
27
|
+
return {
|
|
28
|
+
type,
|
|
29
|
+
value: string(criterion.value, `task.pass[${index}].value`),
|
|
30
|
+
};
|
|
31
|
+
if (type === "command_succeeds")
|
|
32
|
+
return {
|
|
33
|
+
type,
|
|
34
|
+
command: string(criterion.command, `task.pass[${index}].command`),
|
|
35
|
+
};
|
|
36
|
+
throw new Error(`task.pass[${index}].type is unsupported: ${type}`);
|
|
37
|
+
});
|
|
38
|
+
const taskDir = path.dirname(sourcePath);
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
prompt,
|
|
42
|
+
pass,
|
|
43
|
+
fixtureDir: path.join(taskDir, "fixture"),
|
|
44
|
+
sourcePath,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function parseRunResult(raw) {
|
|
48
|
+
const value = object(raw, "bench result");
|
|
49
|
+
if (value.schema_version !== 1)
|
|
50
|
+
throw new Error("bench result schema_version must be 1");
|
|
51
|
+
string(value.run_id, "bench result run_id");
|
|
52
|
+
string(value.prompt, "bench result prompt");
|
|
53
|
+
string(value.started_at, "bench result started_at");
|
|
54
|
+
string(value.finished_at, "bench result finished_at");
|
|
55
|
+
if (value.task_id !== undefined)
|
|
56
|
+
string(value.task_id, "bench result task_id");
|
|
57
|
+
if (!Array.isArray(value.cells))
|
|
58
|
+
throw new Error("bench result cells must be an array");
|
|
59
|
+
value.cells.forEach((rawCell, index) => {
|
|
60
|
+
const cell = object(rawCell, `bench result cells[${index}]`);
|
|
61
|
+
string(cell.agent, `bench result cells[${index}].agent`);
|
|
62
|
+
if (cell.model !== undefined)
|
|
63
|
+
string(cell.model, `bench result cells[${index}].model`);
|
|
64
|
+
if (cell.status !== "passed" && cell.status !== "failed")
|
|
65
|
+
throw new Error(`bench result cells[${index}].status must be passed or failed`);
|
|
66
|
+
if (cell.exit !== null &&
|
|
67
|
+
(!Number.isInteger(cell.exit) || cell.exit < 0))
|
|
68
|
+
throw new Error(`bench result cells[${index}].exit must be a non-negative integer or null`);
|
|
69
|
+
if (typeof cell.wall_ms !== "number" ||
|
|
70
|
+
!Number.isFinite(cell.wall_ms) ||
|
|
71
|
+
cell.wall_ms < 0)
|
|
72
|
+
throw new Error(`bench result cells[${index}].wall_ms must be a non-negative number`);
|
|
73
|
+
if (cell.tokens !== undefined &&
|
|
74
|
+
(!Number.isSafeInteger(cell.tokens) || cell.tokens < 0))
|
|
75
|
+
throw new Error(`bench result cells[${index}].tokens must be a non-negative integer`);
|
|
76
|
+
if (typeof cell.stdout !== "string" || typeof cell.stderr !== "string")
|
|
77
|
+
throw new Error(`bench result cells[${index}] stdout and stderr must be strings`);
|
|
78
|
+
});
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
export function validateRunId(runId) {
|
|
82
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(runId))
|
|
83
|
+
throw new Error(`Invalid run id: ${runId}`);
|
|
84
|
+
return runId;
|
|
85
|
+
}
|
|
86
|
+
export function loadTask(taskId, tasksRoot) {
|
|
87
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId))
|
|
88
|
+
throw new Error(`Invalid task id: ${taskId}`);
|
|
89
|
+
const sourcePath = path.join(tasksRoot, taskId, "task.json");
|
|
90
|
+
return parseTask(JSON.parse(fs.readFileSync(sourcePath, "utf8")), sourcePath);
|
|
91
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BenchRunResult } from "./types.js";
|
|
2
|
+
export declare function benchHistoryDir(): string;
|
|
3
|
+
export declare function saveRun(result: BenchRunResult, root?: string): string;
|
|
4
|
+
export declare function loadRun(runId: string, root?: string): BenchRunResult;
|
|
5
|
+
export declare function listRuns(root?: string): BenchRunResult[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { getUserAgentsDir } from "../state.js";
|
|
4
|
+
import { parseRunResult, validateRunId } from "./schema.js";
|
|
5
|
+
export function benchHistoryDir() {
|
|
6
|
+
return path.join(getUserAgentsDir(), ".history", "bench");
|
|
7
|
+
}
|
|
8
|
+
export function saveRun(result, root = benchHistoryDir()) {
|
|
9
|
+
parseRunResult(result);
|
|
10
|
+
validateRunId(result.run_id);
|
|
11
|
+
fs.mkdirSync(root, { recursive: true });
|
|
12
|
+
const target = path.join(root, `${result.run_id}.json`);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(temporary, `${JSON.stringify(result, null, 2)}\n`, {
|
|
15
|
+
mode: 0o600,
|
|
16
|
+
});
|
|
17
|
+
fs.renameSync(temporary, target);
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
export function loadRun(runId, root = benchHistoryDir()) {
|
|
21
|
+
validateRunId(runId);
|
|
22
|
+
return parseRunResult(JSON.parse(fs.readFileSync(path.join(root, `${runId}.json`), "utf8")));
|
|
23
|
+
}
|
|
24
|
+
export function listRuns(root = benchHistoryDir()) {
|
|
25
|
+
if (!fs.existsSync(root))
|
|
26
|
+
return [];
|
|
27
|
+
return fs
|
|
28
|
+
.readdirSync(root)
|
|
29
|
+
.filter((name) => name.endsWith(".json"))
|
|
30
|
+
.map((name) => parseRunResult(JSON.parse(fs.readFileSync(path.join(root, name), "utf8"))))
|
|
31
|
+
.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type BenchPassCriterion = {
|
|
2
|
+
type: "file_exists";
|
|
3
|
+
path: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "exit_zero";
|
|
6
|
+
} | {
|
|
7
|
+
type: "stdout_contains";
|
|
8
|
+
value: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "command_succeeds";
|
|
11
|
+
command: string;
|
|
12
|
+
};
|
|
13
|
+
export interface BenchTask {
|
|
14
|
+
id: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
pass: BenchPassCriterion[];
|
|
17
|
+
fixtureDir: string;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
}
|
|
20
|
+
export interface BenchCell {
|
|
21
|
+
agent: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BenchCellResult extends BenchCell {
|
|
25
|
+
status: "passed" | "failed";
|
|
26
|
+
exit: number | null;
|
|
27
|
+
wall_ms: number;
|
|
28
|
+
tokens?: number;
|
|
29
|
+
stdout: string;
|
|
30
|
+
stderr: string;
|
|
31
|
+
}
|
|
32
|
+
export interface BenchRunResult {
|
|
33
|
+
schema_version: 1;
|
|
34
|
+
run_id: string;
|
|
35
|
+
task_id?: string;
|
|
36
|
+
prompt: string;
|
|
37
|
+
started_at: string;
|
|
38
|
+
finished_at: string;
|
|
39
|
+
cells: BenchCellResult[];
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** True only for a clean, single-line `sk-ant-oat01-…` token — see {@link SETUP_TOKEN_RE}. */
|
|
2
|
+
export declare function isValidClaudeSetupToken(value: string): boolean;
|
|
1
3
|
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
2
4
|
export declare function claudeAccountTokenKey(account: string): string;
|
|
3
5
|
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
@@ -11,6 +11,24 @@ import { bundleBackend, bundleExists, readAndResolveBundleEnv } from './secrets/
|
|
|
11
11
|
* multi-account fleet.
|
|
12
12
|
*/
|
|
13
13
|
const AUTH_BUNDLE = 'auth';
|
|
14
|
+
/**
|
|
15
|
+
* A well-formed Claude OAuth setup-token: the `sk-ant-oat01-` prefix followed by
|
|
16
|
+
* token-safe characters only, on a single line. `claude setup-token` mints exactly
|
|
17
|
+
* this; nothing else is a token.
|
|
18
|
+
*
|
|
19
|
+
* The provisioning capture bug behind #1767 stored the raw `claude setup-token`
|
|
20
|
+
* TTY stream — the welcome banner, ANSI control sequences, box-drawing art, and the
|
|
21
|
+
* token buried inside — under a per-account key instead of the parsed value. That
|
|
22
|
+
* blob is not a token: injected as `Authorization: Bearer <blob>` it made Anthropic
|
|
23
|
+
* reject every request and the run crash. agents-cli only *consumes* these tokens
|
|
24
|
+
* (the mint itself is the Rush Cloud / mint-auth path), so this is the boundary
|
|
25
|
+
* where a corrupt bundle entry must be caught before it reaches the auth header.
|
|
26
|
+
*/
|
|
27
|
+
const SETUP_TOKEN_RE = /^sk-ant-oat01-[A-Za-z0-9_-]+$/;
|
|
28
|
+
/** True only for a clean, single-line `sk-ant-oat01-…` token — see {@link SETUP_TOKEN_RE}. */
|
|
29
|
+
export function isValidClaudeSetupToken(value) {
|
|
30
|
+
return SETUP_TOKEN_RE.test(value);
|
|
31
|
+
}
|
|
14
32
|
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
15
33
|
export function claudeAccountTokenKey(account) {
|
|
16
34
|
const slug = account
|
|
@@ -55,7 +73,15 @@ export function resolveClaudeSetupToken(home) {
|
|
|
55
73
|
return null;
|
|
56
74
|
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
57
75
|
const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
|
|
58
|
-
|
|
76
|
+
if (v.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
// Reject a malformed stored value (e.g. a captured setup-token TTY blob, #1767)
|
|
79
|
+
// rather than let it become an `Authorization: Bearer <blob>` header that
|
|
80
|
+
// Anthropic rejects and crashes the run. A corrupt entry is treated as no
|
|
81
|
+
// usable file-based token, so the caller falls back to the normal login.
|
|
82
|
+
if (!isValidClaudeSetupToken(v))
|
|
83
|
+
return null;
|
|
84
|
+
return v;
|
|
59
85
|
}
|
|
60
86
|
catch {
|
|
61
87
|
return null;
|
|
@@ -186,6 +186,8 @@ export interface CrabboxRunOptions extends CrabboxOptions {
|
|
|
186
186
|
onData?: (chunk: string) => void;
|
|
187
187
|
/** Force a full remote resync before running. */
|
|
188
188
|
fullResync?: boolean;
|
|
189
|
+
/** Refresh an existing lease with this idle window before running. */
|
|
190
|
+
renewIdleTimeoutSecs?: number;
|
|
189
191
|
}
|
|
190
192
|
/**
|
|
191
193
|
* Run `remoteCmd` on the leased box via `crabbox run` (crabbox syncs the dirty
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -485,6 +485,8 @@ export function crabboxRun(slug, remoteCmd, opts = {}) {
|
|
|
485
485
|
export function crabboxRunScript(slug, script, opts = {}) {
|
|
486
486
|
findCrabbox();
|
|
487
487
|
const args = ['run', '--id', slug, '--reclaim'];
|
|
488
|
+
if (opts.renewIdleTimeoutSecs !== undefined)
|
|
489
|
+
args.push('--idle-timeout', `${opts.renewIdleTimeoutSecs}s`);
|
|
488
490
|
if (opts.fullResync)
|
|
489
491
|
args.push('--full-resync');
|
|
490
492
|
args.push('--script-stdin');
|
|
@@ -233,7 +233,9 @@ export function isExpiredPoolStray(box, opts) {
|
|
|
233
233
|
return false;
|
|
234
234
|
if (box.expiresAt === null || box.expiresAt > nowSecs)
|
|
235
235
|
return false; // unexpired ⇒ reusable
|
|
236
|
-
if (box.lastTouchedAt
|
|
236
|
+
if (box.lastTouchedAt === null)
|
|
237
|
+
return false; // unknown age is never reap-safe
|
|
238
|
+
if (nowSecs - box.lastTouchedAt < graceSecs)
|
|
237
239
|
return false; // maybe active
|
|
238
240
|
return true;
|
|
239
241
|
}
|
|
@@ -329,9 +331,13 @@ export async function leaseAndRun(opts) {
|
|
|
329
331
|
let exitCode = null;
|
|
330
332
|
let toreDown = false;
|
|
331
333
|
try {
|
|
334
|
+
const renewIdleTimeoutSecs = reused && !opts.reuseBox && box.idleTimeoutSecs !== null
|
|
335
|
+
? box.idleTimeoutSecs
|
|
336
|
+
: undefined;
|
|
332
337
|
exitCode = await crabboxRunScript(box.slug, script, {
|
|
333
338
|
secretsBundle: opts.secretsBundle,
|
|
334
339
|
onData: opts.onData,
|
|
340
|
+
renewIdleTimeoutSecs,
|
|
335
341
|
});
|
|
336
342
|
}
|
|
337
343
|
finally {
|
package/dist/lib/daemon.js
CHANGED
|
@@ -20,6 +20,7 @@ import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
|
20
20
|
import { detectOverdueJobs, notifyOverdue } from './overdue.js';
|
|
21
21
|
import { runCatchup } from './catchup.js';
|
|
22
22
|
import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } from './routine-notify.js';
|
|
23
|
+
import { notifyOwnerRoutineFinish, notifyOwnerRoutineStartFailed } from './routine-notify-owner.js';
|
|
23
24
|
import { BrowserService } from './browser/service.js';
|
|
24
25
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
25
26
|
import { redactSecrets } from './redact.js';
|
|
@@ -516,6 +517,16 @@ export async function runDaemon() {
|
|
|
516
517
|
notifyRoutineFinish(final);
|
|
517
518
|
}
|
|
518
519
|
catch { /* best-effort */ }
|
|
520
|
+
// RUSH-2288: a failed/timed-out routine also reaches the OWNER's phone
|
|
521
|
+
// (in-process owner channel stack), not just the local desktop. Green
|
|
522
|
+
// runs are silent — the builder returns early. Async + swallowed so a
|
|
523
|
+
// delivery hiccup never blocks the finish path.
|
|
524
|
+
void notifyOwnerRoutineFinish(final)
|
|
525
|
+
.then((r) => {
|
|
526
|
+
if (r.attempts.length && !r.delivered)
|
|
527
|
+
log('WARN', `Owner failure-notify for '${config.name}' reached no channel (tried: ${r.attempts.map((a) => a.channel).join(', ')})`);
|
|
528
|
+
})
|
|
529
|
+
.catch(() => { });
|
|
519
530
|
},
|
|
520
531
|
});
|
|
521
532
|
log('INFO', `Job '${config.name}' spawned (run: ${meta.runId}, PID: ${meta.pid})`);
|
|
@@ -531,6 +542,15 @@ export async function runDaemon() {
|
|
|
531
542
|
notifyRoutineStartFailed(config, message);
|
|
532
543
|
}
|
|
533
544
|
catch { /* best-effort */ }
|
|
545
|
+
// RUSH-2288: the pre-spawn failure (e.g. auth_failed) is exactly the one the
|
|
546
|
+
// per-routine `agents notify` prompt can never send — its agent never ran —
|
|
547
|
+
// so the daemon reaches the owner directly.
|
|
548
|
+
void notifyOwnerRoutineStartFailed(config, message)
|
|
549
|
+
.then((r) => {
|
|
550
|
+
if (r.attempts.length && !r.delivered)
|
|
551
|
+
log('WARN', `Owner start-failure notify for '${config.name}' reached no channel (tried: ${r.attempts.map((a) => a.channel).join(', ')})`);
|
|
552
|
+
})
|
|
553
|
+
.catch(() => { });
|
|
534
554
|
}
|
|
535
555
|
};
|
|
536
556
|
let scheduler = null;
|
|
@@ -50,6 +50,8 @@ export interface SshHostKeyOptions {
|
|
|
50
50
|
/** Managed known_hosts path override (tests). Defaults to the CLI-managed store. */
|
|
51
51
|
knownHostsFile?: string;
|
|
52
52
|
}
|
|
53
|
+
/** OpenSSH argv that makes an explicit device key authoritative. */
|
|
54
|
+
export declare function deviceIdentityArgs(device: DeviceProfile): string[];
|
|
53
55
|
/**
|
|
54
56
|
* Build the argv (after the `ssh` program name) and the environment overlay
|
|
55
57
|
* for connecting to a device. For password auth this points `SSH_ASKPASS` at
|
|
@@ -82,6 +82,12 @@ export function wrapRemoteCommand(device, cmd) {
|
|
|
82
82
|
}
|
|
83
83
|
return joined;
|
|
84
84
|
}
|
|
85
|
+
/** OpenSSH argv that makes an explicit device key authoritative. */
|
|
86
|
+
export function deviceIdentityArgs(device) {
|
|
87
|
+
return device.auth?.method === 'key' && device.auth.identityFile
|
|
88
|
+
? ['-i', device.auth.identityFile, '-o', 'IdentitiesOnly=yes']
|
|
89
|
+
: [];
|
|
90
|
+
}
|
|
85
91
|
/**
|
|
86
92
|
* Build the argv (after the `ssh` program name) and the environment overlay
|
|
87
93
|
* for connecting to a device. For password auth this points `SSH_ASKPASS` at
|
|
@@ -118,6 +124,7 @@ export function buildSshInvocation(device, cmd, askpassShimPath, hostKey = {}, o
|
|
|
118
124
|
}
|
|
119
125
|
else {
|
|
120
126
|
args.push('-o', 'BatchMode=yes');
|
|
127
|
+
args.push(...deviceIdentityArgs(device));
|
|
121
128
|
}
|
|
122
129
|
// An interactive login (no remote command) needs a real tty.
|
|
123
130
|
if (!remote)
|
|
@@ -2,6 +2,7 @@ import type { AgentId } from '../types.js';
|
|
|
2
2
|
import type { DuplicateVersionHook } from '../hooks.js';
|
|
3
3
|
import type { RcSecretFinding } from '../secrets/rc-hygiene.js';
|
|
4
4
|
import type { OwnerSinkStatus } from '../channels/owner-sink.js';
|
|
5
|
+
import { type WindowsSshEnrollmentAudit } from './windows-ssh-enrollment.js';
|
|
5
6
|
import type { SyncStatusRow, OrphanRow } from '../drift.js';
|
|
6
7
|
import type { FetchStatusMarker } from '../auto-pull.js';
|
|
7
8
|
import type { VersionResourceReport } from '../doctor-diff.js';
|
|
@@ -11,7 +12,7 @@ export type FindingSeverity = 'critical' | 'warning';
|
|
|
11
12
|
* the JSON consumer group by kind. */
|
|
12
13
|
/** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
|
|
13
14
|
* below owns it, and a second copy in these comments is a fourth place to drift. */
|
|
14
|
-
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "stale-cli", "owner-sink-unreachable"];
|
|
15
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "ssh-key-enrollment", "stale-cli", "owner-sink-unreachable"];
|
|
15
16
|
/**
|
|
16
17
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
17
18
|
* the builders below and asserted against both prose rubrics by
|
|
@@ -102,6 +103,8 @@ export interface LocalFindingInputs {
|
|
|
102
103
|
platform: NodeJS.Platform;
|
|
103
104
|
policy: string | null;
|
|
104
105
|
};
|
|
106
|
+
/** Read-only Windows OpenSSH AuthorizedKeysFile/content/ACL audit. */
|
|
107
|
+
windowsSshEnrollment?: WindowsSshEnrollmentAudit | null;
|
|
105
108
|
/** `<agent>@<version>` keys whose home is an isolated copy. Their findings are
|
|
106
109
|
* never collapsed across versions: the agent-wide `agents doctor <agent> --fix`
|
|
107
110
|
* sweep deliberately skips isolated copies, so a collapsed row would print a
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* with. Keep this list exhaustive; a kind missing from it is a doc that lies.
|
|
19
19
|
* CRITICAL — logged-out (provable) · missing-hook · missing-plugin ·
|
|
20
20
|
* unwired-hook (a hook on disk that settings.json never fires) ·
|
|
21
|
-
* cli-missing · owner-sink-unreachable (the feed/notify owner lane
|
|
21
|
+
* cli-missing · ssh-key-enrollment · owner-sink-unreachable (the feed/notify owner lane
|
|
22
22
|
* cannot reach the owner from this box).
|
|
23
23
|
* WARNING — logout-unprovable (hedged) · missing-resource · content-drift ·
|
|
24
24
|
* never-synced · stale · repo-behind · repo-drift · version-skew ·
|
|
@@ -40,6 +40,7 @@ import { blocksLocalScripts } from '../platform/winpath.js';
|
|
|
40
40
|
import { loginHint } from '../signin-badge.js';
|
|
41
41
|
import { CONFIG_ENV_ISOLATED_AGENTS } from '../shims.js';
|
|
42
42
|
import { padToWidth, stringWidth } from '../session/width.js';
|
|
43
|
+
import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
|
|
43
44
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
44
45
|
/** Agents with NO per-version credential isolation: their login is shared across
|
|
45
46
|
* every installed version, so a "log into THIS version" remediation would be a
|
|
@@ -97,6 +98,7 @@ export const ALL_FINDING_KINDS = [
|
|
|
97
98
|
'rc-secret-export', // credential-shaped export in a shell rc file
|
|
98
99
|
'env-secret-export', // the file-store master key live in THIS process's env
|
|
99
100
|
'exec-policy', // Windows execution policy blocks agents.ps1
|
|
101
|
+
'ssh-key-enrollment', // Windows OpenSSH public-key path/content/ACL is invalid
|
|
100
102
|
'stale-cli',
|
|
101
103
|
'owner-sink-unreachable', // the feed/notify owner-delivery lane can't reach the owner from this box
|
|
102
104
|
];
|
|
@@ -141,6 +143,7 @@ export const FINDING_SEVERITY = {
|
|
|
141
143
|
'rc-secret-export': 'warning',
|
|
142
144
|
'env-secret-export': 'warning',
|
|
143
145
|
'exec-policy': 'warning',
|
|
146
|
+
'ssh-key-enrollment': 'critical',
|
|
144
147
|
'stale-cli': 'warning',
|
|
145
148
|
};
|
|
146
149
|
function agentName(agent) {
|
|
@@ -254,6 +257,8 @@ export function remediationFor(finding) {
|
|
|
254
257
|
return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
|
|
255
258
|
case 'exec-policy':
|
|
256
259
|
return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
|
|
260
|
+
case 'ssh-key-enrollment':
|
|
261
|
+
return 'repair the reported AuthorizedKeysFile, then rerun agents doctor';
|
|
257
262
|
case 'stale-cli':
|
|
258
263
|
return 'upgrade';
|
|
259
264
|
case 'owner-sink-unreachable':
|
|
@@ -493,6 +498,15 @@ export function buildLocalFindings(input) {
|
|
|
493
498
|
const policyFinding = execPolicyFinding(device, input.execPolicy);
|
|
494
499
|
if (policyFinding)
|
|
495
500
|
out.push(policyFinding);
|
|
501
|
+
if (input.windowsSshEnrollment) {
|
|
502
|
+
const problem = windowsSshEnrollmentProblem(input.windowsSshEnrollment);
|
|
503
|
+
if (problem) {
|
|
504
|
+
out.push(finding({
|
|
505
|
+
severity: FINDING_SEVERITY['ssh-key-enrollment'], kind: 'ssh-key-enrollment', device,
|
|
506
|
+
message: problem,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
496
510
|
// Per-version sign-in → logged-out (critical, provable) / logout-unprovable
|
|
497
511
|
// (warning). Signed-in versions produce no finding — the accounts line shows
|
|
498
512
|
// them. Agents that can't be inspected never yield a logout finding.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import { type UsageSnapshot } from '../usage.js';
|
|
3
|
+
/** An account's usage headroom rolled into one glanceable summary. */
|
|
4
|
+
export interface QuotaSummary {
|
|
5
|
+
/**
|
|
6
|
+
* `available` / `rate_limited` from the live usage windows, or `null` when
|
|
7
|
+
* there is no snapshot at all (no data yet, or the agent has no usage source).
|
|
8
|
+
*/
|
|
9
|
+
status: 'available' | 'rate_limited' | null;
|
|
10
|
+
/** Max utilization across blocking windows (0-100, rounded), or null when unknown. */
|
|
11
|
+
usedPercent: number | null;
|
|
12
|
+
/** True when the snapshot is a cached `last_seen`, not a fresh `live` read. */
|
|
13
|
+
stale: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** One installed (agent, version) on a host, fully resolved. */
|
|
16
|
+
export interface HarnessRow {
|
|
17
|
+
agent: AgentId;
|
|
18
|
+
version: string;
|
|
19
|
+
/** Account display label (email / id), or null when signed out or unidentifiable. */
|
|
20
|
+
account: string | null;
|
|
21
|
+
signedIn: boolean;
|
|
22
|
+
quota: QuotaSummary;
|
|
23
|
+
/** signed in AND not rate-limited — usable for a run right now. */
|
|
24
|
+
ready: boolean;
|
|
25
|
+
/** When `!ready`, a short reason ("signed out" / "rate-limited"). */
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
/** One host's rows, or the reason it produced none. */
|
|
29
|
+
export interface HostHarnessResult {
|
|
30
|
+
host: string;
|
|
31
|
+
rows: HarnessRow[];
|
|
32
|
+
/** Set when the host failed to probe (offline / error). */
|
|
33
|
+
error?: string;
|
|
34
|
+
/** Set when the host was skipped (control / offline). */
|
|
35
|
+
skipped?: string;
|
|
36
|
+
}
|
|
37
|
+
/** An account collapsed across the installs on one host that share it. */
|
|
38
|
+
export interface AccountGroup {
|
|
39
|
+
/** Account display label, or null for the signed-out bucket. */
|
|
40
|
+
account: string | null;
|
|
41
|
+
/** Distinct agent ids using this account, sorted. */
|
|
42
|
+
agents: AgentId[];
|
|
43
|
+
/** How many (agent, version) installs share this account. */
|
|
44
|
+
installs: number;
|
|
45
|
+
/** True when at least one install under this account is signed in. */
|
|
46
|
+
signedIn: boolean;
|
|
47
|
+
/** Representative quota — an account maps to one provider identity, so its usage is shared. */
|
|
48
|
+
quota: QuotaSummary;
|
|
49
|
+
ready: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Roll one usage snapshot into a {@link QuotaSummary}. Mirrors the blocking-window
|
|
54
|
+
* selection of {@link deriveUsageStatusFromSnapshot} (the model-specific
|
|
55
|
+
* `sonnet_week` sub-limit is excluded so hitting it doesn't read as a throttled
|
|
56
|
+
* account) and takes the highest utilization across those windows. Pure.
|
|
57
|
+
*/
|
|
58
|
+
export declare function summarizeQuota(snapshot: UsageSnapshot | null | undefined): QuotaSummary;
|
|
59
|
+
/**
|
|
60
|
+
* "Ready" = signed in AND not rate-limited. A missing quota snapshot does NOT
|
|
61
|
+
* block readiness: the account is signed in and usable, we just have no live
|
|
62
|
+
* utilization to show. Pure.
|
|
63
|
+
*/
|
|
64
|
+
export declare function computeReady(signedIn: boolean, quota: QuotaSummary): {
|
|
65
|
+
ready: boolean;
|
|
66
|
+
reason?: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Enumerate every installed (agent, version) on THIS host, resolve account +
|
|
70
|
+
* sign-in + quota + ready, and return the rows. Signed-out installs are kept
|
|
71
|
+
* (they are the actionable ones), unlike the auth-health probe which drops
|
|
72
|
+
* `unconfigured`. Quota is cache-only unless `refresh` is set.
|
|
73
|
+
*/
|
|
74
|
+
export declare function collectLocalHarnessInventory(opts?: {
|
|
75
|
+
agents?: readonly AgentId[];
|
|
76
|
+
refresh?: boolean;
|
|
77
|
+
}): Promise<HarnessRow[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Collapse a host's rows into one {@link AccountGroup} per distinct account (the
|
|
80
|
+
* signed-out installs fall into the `null` bucket). Because an account label maps
|
|
81
|
+
* to a single provider identity, every install sharing it shares one quota, so
|
|
82
|
+
* the group's quota is taken from the first row carrying real data (preferring a
|
|
83
|
+
* `rate_limited` row so a throttle is never hidden). Pure.
|
|
84
|
+
*/
|
|
85
|
+
export declare function groupByAccount(rows: HarnessRow[]): AccountGroup[];
|
|
86
|
+
/** Short human quota cell: "12%", "limited", or "—" when no data. */
|
|
87
|
+
export declare function formatQuota(quota: QuotaSummary): string;
|
|
88
|
+
/**
|
|
89
|
+
* Render the device × harness table: one block per device, one row per installed
|
|
90
|
+
* (agent, version) with account / signed-in / quota / ready. Pure.
|
|
91
|
+
*/
|
|
92
|
+
export declare function renderHarnessMatrix(results: HostHarnessResult[]): string[];
|
|
93
|
+
/**
|
|
94
|
+
* Render the device × account table: one block per device, one row per distinct
|
|
95
|
+
* account (installs sharing it collapsed), showing which harnesses use it. Pure.
|
|
96
|
+
*/
|
|
97
|
+
export declare function renderAccountsMatrix(results: HostHarnessResult[]): string[];
|
|
Binary file
|
|
@@ -26,6 +26,8 @@ export interface DeviceAddress {
|
|
|
26
26
|
/** Authentication settings for a device. */
|
|
27
27
|
export interface DeviceAuth {
|
|
28
28
|
method: DeviceAuthMethod;
|
|
29
|
+
/** Explicit private-key path passed to OpenSSH for key authentication. */
|
|
30
|
+
identityFile?: string;
|
|
29
31
|
/** Secrets bundle holding the password (when method === 'password'). */
|
|
30
32
|
bundle?: string;
|
|
31
33
|
/** Key within the bundle whose value is the password. Defaults to 'password'. */
|