@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- 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 +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- 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 +56 -3
- package/dist/lib/cloud/host.js +1 -0
- 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/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- 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/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- 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/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -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 +3 -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/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- 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/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- 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/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- 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.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- 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-bundle.js +1 -1
- 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/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- 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/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
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'. */
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `user@host` all resolve identically no matter which subcommand called.
|
|
19
19
|
*/
|
|
20
20
|
import chalk from 'chalk';
|
|
21
|
-
import { sshTargetFor } from '../hosts/types.js';
|
|
21
|
+
import { hostIdentityArgs, sshTargetFor } from '../hosts/types.js';
|
|
22
22
|
import { matchHost, splitUserHost } from '../hosts/registry.js';
|
|
23
23
|
import { normalizeHost } from '../machine-id.js';
|
|
24
24
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
@@ -61,7 +61,14 @@ async function toResolvedTarget(token) {
|
|
|
61
61
|
}
|
|
62
62
|
const hostPart = host.device ? host.device.name : splitUserHost(host.name).host;
|
|
63
63
|
const name = host.device ? host.device.name : host.name;
|
|
64
|
-
|
|
64
|
+
const extraSshArgs = hostIdentityArgs(host);
|
|
65
|
+
return {
|
|
66
|
+
target,
|
|
67
|
+
machine: normalizeHost(hostPart),
|
|
68
|
+
name,
|
|
69
|
+
os: host.os ?? resolveRemoteOsSync(name),
|
|
70
|
+
...(extraSshArgs.length > 0 ? { extraSshArgs } : {}),
|
|
71
|
+
};
|
|
65
72
|
}
|
|
66
73
|
/**
|
|
67
74
|
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
|
@@ -15,6 +15,9 @@ function renderHost(device) {
|
|
|
15
15
|
const lines = [`Host ${device.name}`, ` HostName ${hostName}`];
|
|
16
16
|
if (device.user)
|
|
17
17
|
lines.push(` User ${device.user}`);
|
|
18
|
+
if (device.auth.method === 'key' && device.auth.identityFile) {
|
|
19
|
+
lines.push(` IdentityFile ${device.auth.identityFile}`, ' IdentitiesOnly yes');
|
|
20
|
+
}
|
|
18
21
|
return lines.join('\n');
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface WindowsSshEnrollment {
|
|
2
|
+
administrator: boolean;
|
|
3
|
+
expectedPath: string;
|
|
4
|
+
configuredPaths: string[];
|
|
5
|
+
fileExists: boolean;
|
|
6
|
+
hasPublicKey: boolean;
|
|
7
|
+
owner: string | null;
|
|
8
|
+
systemFullControl: boolean;
|
|
9
|
+
administratorsFullControl: boolean;
|
|
10
|
+
unexpectedAclPrincipals: string[];
|
|
11
|
+
}
|
|
12
|
+
export type WindowsSshEnrollmentAudit = {
|
|
13
|
+
status: WindowsSshEnrollment;
|
|
14
|
+
} | {
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function diagnoseWindowsSshFailure(stderr: string, timedOut: boolean): string;
|
|
18
|
+
export declare function parseWindowsSshEnrollment(stdout: string): WindowsSshEnrollment;
|
|
19
|
+
export declare function auditWindowsSshEnrollment(platform?: NodeJS.Platform): WindowsSshEnrollmentAudit | null;
|
|
20
|
+
export declare function windowsSshEnrollmentProblem(audit: WindowsSshEnrollmentAudit): string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
export function diagnoseWindowsSshFailure(stderr, timedOut) {
|
|
3
|
+
if (timedOut || /connection timed out|operation timed out/i.test(stderr)) {
|
|
4
|
+
return 'Windows SSH unreachable: port 22 did not answer before the timeout';
|
|
5
|
+
}
|
|
6
|
+
if (/host key verification failed|remote host identification has changed/i.test(stderr)) {
|
|
7
|
+
return 'Windows OpenSSH host-key verification failed; verify and re-pin the device host key';
|
|
8
|
+
}
|
|
9
|
+
if (/permission denied.*publickey|permission denied \(publickey/i.test(stderr)) {
|
|
10
|
+
return 'port 22 and OpenSSH are reachable, but the public key was rejected; recover through the Windows console or password-auth profile, then run agents doctor on that box to inspect its effective AuthorizedKeysFile and ACL';
|
|
11
|
+
}
|
|
12
|
+
if (/connection refused/i.test(stderr)) {
|
|
13
|
+
return 'Windows host is reachable but OpenSSH is not listening on port 22';
|
|
14
|
+
}
|
|
15
|
+
return stderr.trim() || 'Windows SSH probe failed';
|
|
16
|
+
}
|
|
17
|
+
const AUDIT_SCRIPT = String.raw `
|
|
18
|
+
$ErrorActionPreference = 'Stop'
|
|
19
|
+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
20
|
+
$administratorSid = 'S-1-5-32-544'
|
|
21
|
+
$administrator = @($identity.Groups | Where-Object { $_.Value -eq $administratorSid }).Count -gt 0
|
|
22
|
+
$expected = if ($administrator) { Join-Path $env:ProgramData 'ssh\administrators_authorized_keys' } else { Join-Path $env:USERPROFILE '.ssh\authorized_keys' }
|
|
23
|
+
$sshd = Join-Path $env:WINDIR 'System32\OpenSSH\sshd.exe'
|
|
24
|
+
$effective = (& $sshd -T -C "user=$env:USERNAME,host=$env:COMPUTERNAME,addr=127.0.0.1" 2>$null | Select-String '^authorizedkeysfile ' | Select-Object -First 1).Line
|
|
25
|
+
$configured = if ($effective) { @($effective.Substring('authorizedkeysfile '.Length).Split(' ', [StringSplitOptions]::RemoveEmptyEntries)) } else { @() }
|
|
26
|
+
$exists = Test-Path -LiteralPath $expected -PathType Leaf
|
|
27
|
+
$acl = if ($exists) { Get-Acl -LiteralPath $expected } else { $null }
|
|
28
|
+
$rules = if ($acl) { @($acl.Access | Where-Object AccessControlType -eq Allow) } else { @() }
|
|
29
|
+
$full = [Security.AccessControl.FileSystemRights]::FullControl
|
|
30
|
+
$system = @($rules | Where-Object { $_.IdentityReference.Value -in @('NT AUTHORITY\SYSTEM', 'S-1-5-18') -and ($_.FileSystemRights -band $full) -eq $full }).Count -gt 0
|
|
31
|
+
$admins = @($rules | Where-Object { $_.IdentityReference.Value -in @('BUILTIN\Administrators', 'S-1-5-32-544') -and ($_.FileSystemRights -band $full) -eq $full }).Count -gt 0
|
|
32
|
+
$allowed = @('NT AUTHORITY\SYSTEM', 'S-1-5-18', 'BUILTIN\Administrators', 'S-1-5-32-544')
|
|
33
|
+
$unexpected = if ($administrator) { @($rules | ForEach-Object { $_.IdentityReference.Value } | Where-Object { $_ -notin $allowed } | Sort-Object -Unique) } else { @() }
|
|
34
|
+
$hasKey = $exists -and [bool](Select-String -LiteralPath $expected -Pattern '^\s*(ssh-(rsa|ed25519)|ecdsa-sha2-)' -Quiet)
|
|
35
|
+
[ordered]@{ administrator=$administrator; expectedPath=$expected; configuredPaths=$configured; fileExists=$exists; hasPublicKey=$hasKey; owner=if($acl){$acl.Owner}else{$null}; systemFullControl=$system; administratorsFullControl=$admins; unexpectedAclPrincipals=$unexpected } | ConvertTo-Json -Compress
|
|
36
|
+
`;
|
|
37
|
+
export function parseWindowsSshEnrollment(stdout) {
|
|
38
|
+
const raw = JSON.parse(stdout.trim());
|
|
39
|
+
const strings = (value) => Array.isArray(value)
|
|
40
|
+
? value.filter((item) => typeof item === 'string')
|
|
41
|
+
: typeof value === 'string' ? [value] : [];
|
|
42
|
+
if (typeof raw.administrator !== 'boolean' || typeof raw.expectedPath !== 'string') {
|
|
43
|
+
throw new Error('Windows SSH enrollment audit returned an invalid payload');
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
administrator: raw.administrator,
|
|
47
|
+
expectedPath: raw.expectedPath,
|
|
48
|
+
configuredPaths: strings(raw.configuredPaths),
|
|
49
|
+
fileExists: raw.fileExists === true,
|
|
50
|
+
hasPublicKey: raw.hasPublicKey === true,
|
|
51
|
+
owner: typeof raw.owner === 'string' ? raw.owner : null,
|
|
52
|
+
systemFullControl: raw.systemFullControl === true,
|
|
53
|
+
administratorsFullControl: raw.administratorsFullControl === true,
|
|
54
|
+
unexpectedAclPrincipals: strings(raw.unexpectedAclPrincipals),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function auditWindowsSshEnrollment(platform = process.platform) {
|
|
58
|
+
if (platform !== 'win32')
|
|
59
|
+
return null;
|
|
60
|
+
const encoded = Buffer.from(AUDIT_SCRIPT, 'utf16le').toString('base64');
|
|
61
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
timeout: 10_000,
|
|
65
|
+
});
|
|
66
|
+
if (result.status !== 0)
|
|
67
|
+
return { error: (result.stderr || 'audit command failed').trim() };
|
|
68
|
+
try {
|
|
69
|
+
return { status: parseWindowsSshEnrollment(result.stdout) };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function windowsSshEnrollmentProblem(audit) {
|
|
76
|
+
if ('error' in audit)
|
|
77
|
+
return `Windows OpenSSH enrollment audit failed: ${audit.error}`;
|
|
78
|
+
const status = audit.status;
|
|
79
|
+
const normalize = (path) => path.replace(/\//g, '\\').toLowerCase();
|
|
80
|
+
const expected = normalize(status.expectedPath);
|
|
81
|
+
const effective = status.configuredPaths.map((path) => normalize(path)
|
|
82
|
+
.replace('__programdata__', normalize(process.env.ProgramData ?? 'C:\\ProgramData'))
|
|
83
|
+
.replace(/^\.ssh\\/, `${normalize(process.env.USERPROFILE ?? '')}\\.ssh\\`));
|
|
84
|
+
if (effective.length > 0 && !effective.includes(expected)) {
|
|
85
|
+
return `OpenSSH AuthorizedKeysFile resolves to ${status.configuredPaths.join(', ')}, not ${status.expectedPath}`;
|
|
86
|
+
}
|
|
87
|
+
if (!status.fileExists)
|
|
88
|
+
return `SSH public-key file missing: ${status.expectedPath}`;
|
|
89
|
+
if (!status.hasPublicKey)
|
|
90
|
+
return `no public key enrolled in ${status.expectedPath}`;
|
|
91
|
+
if (status.administrator && (!status.systemFullControl || !status.administratorsFullControl)) {
|
|
92
|
+
return `${status.expectedPath} ACL must grant FullControl to SYSTEM and Administrators`;
|
|
93
|
+
}
|
|
94
|
+
if (status.administrator && status.unexpectedAclPrincipals.length > 0) {
|
|
95
|
+
return `${status.expectedPath} ACL grants access to unexpected principals: ${status.unexpectedAclPrincipals.join(', ')}`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
18
|
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
19
|
/** Runtime guard for an event kind arriving from outside this process. */
|
package/dist/lib/events.js
CHANGED
|
@@ -113,7 +113,7 @@ const EVENT_TYPE_TABLE = {
|
|
|
113
113
|
'skill.install': true, 'skill.remove': true,
|
|
114
114
|
'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
|
|
115
115
|
'computer.action': true,
|
|
116
|
-
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
116
|
+
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.lease-denied': true, 'secrets.lease-expire': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
117
117
|
'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
|
|
118
118
|
'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
|
|
119
119
|
'hook.fire': true, 'hook.complete': true, 'hook.error': true,
|
|
@@ -141,7 +141,7 @@ export function isEventType(value) {
|
|
|
141
141
|
}
|
|
142
142
|
const AUDIT_EVENTS = new Set([
|
|
143
143
|
'command.start', 'command.end',
|
|
144
|
-
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view',
|
|
144
|
+
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view', 'secrets.lease-denied', 'secrets.lease-expire',
|
|
145
145
|
'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
146
146
|
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
147
147
|
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
package/dist/lib/exec.js
CHANGED
|
@@ -31,6 +31,7 @@ import { isTmuxInstalled } from './tmux/binary.js';
|
|
|
31
31
|
import { shellQuote } from './ssh-exec.js';
|
|
32
32
|
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
33
33
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
34
|
+
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
34
35
|
/**
|
|
35
36
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
36
37
|
*
|
|
@@ -967,6 +968,12 @@ export function buildExecCommand(options) {
|
|
|
967
968
|
// on the default command is parsed as a project path, not a message.
|
|
968
969
|
cmd.push('--prompt', options.prompt);
|
|
969
970
|
}
|
|
971
|
+
else if (interactive && options.agent === 'claude') {
|
|
972
|
+
// Claude's -p is --print, not a prompt-value flag. In an interactive run
|
|
973
|
+
// the initial prompt is positional; emitting `-p /continue <id>` turns a
|
|
974
|
+
// focus recovery into a one-shot print process that immediately exits.
|
|
975
|
+
cmd.push(options.prompt);
|
|
976
|
+
}
|
|
970
977
|
else if (template.promptFlag === 'positional') {
|
|
971
978
|
cmd.push(options.prompt);
|
|
972
979
|
}
|
|
@@ -1285,7 +1292,7 @@ export function formatPaneTail(raw, maxLines = 30) {
|
|
|
1285
1292
|
* (Ctrl-b d) — return 0 and LEAVE the session for `agents focus` to re-attach.
|
|
1286
1293
|
*/
|
|
1287
1294
|
async function runInTmux(options, executable, args) {
|
|
1288
|
-
const { createSession,
|
|
1295
|
+
const { createSession, killSession, paneExitStatus, prepareSessionForResume, setSessionHook, slugifyName, agentPaneDiedHook, markSessionHookSchema } = await import('./tmux/session.js');
|
|
1289
1296
|
const { getDefaultSocketPath } = await import('./tmux/paths.js');
|
|
1290
1297
|
const { attachTmux, runTmux } = await import('./tmux/binary.js');
|
|
1291
1298
|
const socket = getDefaultSocketPath();
|
|
@@ -1294,15 +1301,11 @@ async function runInTmux(options, executable, args) {
|
|
|
1294
1301
|
const name = slugifyName(`ag-${options.agent}-${idSeed}`);
|
|
1295
1302
|
// A native resume must not create a competing wrapper for a live session.
|
|
1296
1303
|
// A retained dead pane is reaped before the harness resumes normally.
|
|
1297
|
-
if (options.resume && await
|
|
1298
|
-
const existing = await createSession({ name, cwd, socket, source: 'cli', attachExisting: true });
|
|
1304
|
+
if (options.resume && await prepareSessionForResume(name, socket) === 'attach') {
|
|
1299
1305
|
if (options.sessionId)
|
|
1300
1306
|
writeSessionAliasRecord(options.sessionId, name);
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return { exitCode: 0, stderr: '', stdout: '' };
|
|
1304
|
-
}
|
|
1305
|
-
await killSession(name, socket);
|
|
1307
|
+
await attachTmux({ socket, args: ['attach-session', '-t', name] });
|
|
1308
|
+
return { exitCode: 0, stderr: '', stdout: '' };
|
|
1306
1309
|
}
|
|
1307
1310
|
// SessionStart learns some harness IDs only after launch. Carry the wrapper
|
|
1308
1311
|
// name into that hook so it can bind both identities durably.
|
|
@@ -1980,6 +1983,13 @@ export async function runWithFallback(options) {
|
|
|
1980
1983
|
}
|
|
1981
1984
|
for (let i = 0; i < chain.length; i++) {
|
|
1982
1985
|
const { agent, version, envOverride } = chain[i];
|
|
1986
|
+
// Every fallback entry can target a different harness/version home. Sync
|
|
1987
|
+
// its active preset immediately before dispatch so entries 2..N cannot
|
|
1988
|
+
// inherit the stale rules file left by the primary entry.
|
|
1989
|
+
const rulesVersion = version ?? resolveVersion(agent);
|
|
1990
|
+
if (rulesVersion) {
|
|
1991
|
+
applyActiveRulesPresetAtRun(agent, rulesVersion, getVersionHomePath(agent, rulesVersion));
|
|
1992
|
+
}
|
|
1983
1993
|
// Record the entry we're about to attempt so the caller (audit log) sees the
|
|
1984
1994
|
// agent+version that actually ran, even after a rate-limit handoff.
|
|
1985
1995
|
if (options.dispatchSink) {
|
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as os from 'os';
|
|
11
11
|
import { pushBundleToHost } from '../secrets/push.js';
|
|
12
|
-
import { sshTargetFor } from '../devices/connect.js';
|
|
12
|
+
import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
|
|
13
13
|
import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
|
|
14
14
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
15
15
|
import { sshExec } from '../ssh-exec.js';
|
|
@@ -282,13 +282,14 @@ export function probeDevice(device, opts) {
|
|
|
282
282
|
return { device: device.name, reachable: false, platform: device.platform, installedAgents: [], note: e.message };
|
|
283
283
|
}
|
|
284
284
|
const hint = osHint(device.platform);
|
|
285
|
-
const
|
|
285
|
+
const extraSshArgs = deviceIdentityArgs(device);
|
|
286
|
+
const ready = readyProbe(target, hint, extraSshArgs);
|
|
286
287
|
if (!ready.reachable) {
|
|
287
288
|
return { device: device.name, reachable: false, platform: device.platform, installedAgents: [], note: 'unreachable' };
|
|
288
289
|
}
|
|
289
290
|
let installed = [];
|
|
290
291
|
const remoteCmd = buildRemoteAgentsInvocation(['teams', 'doctor', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
291
|
-
const res = sshExec(target, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
292
|
+
const res = sshExec(target, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
292
293
|
if (res.code === 0) {
|
|
293
294
|
try {
|
|
294
295
|
const map = JSON.parse(res.stdout);
|
|
@@ -301,7 +302,7 @@ export function probeDevice(device, opts) {
|
|
|
301
302
|
let installedVersions;
|
|
302
303
|
if (opts?.withVersions) {
|
|
303
304
|
const viewCmd = buildRemoteAgentsInvocation(['view', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
304
|
-
const vres = sshExec(target, viewCmd, { timeoutMs: 30000, multiplex: true });
|
|
305
|
+
const vres = sshExec(target, viewCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
305
306
|
if (vres.code === 0)
|
|
306
307
|
installedVersions = parseInstalledVersions(vres.stdout);
|
|
307
308
|
}
|
|
@@ -310,7 +311,7 @@ export function probeDevice(device, opts) {
|
|
|
310
311
|
// Metadata only — `secrets list --json` returns names + timestamps and never
|
|
311
312
|
// values, which is why this is safe to run across the fleet.
|
|
312
313
|
const listCmd = buildRemoteAgentsInvocation(['secrets', 'list', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
313
|
-
const lres = sshExec(target, listCmd, { timeoutMs: 30000, multiplex: true });
|
|
314
|
+
const lres = sshExec(target, listCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
314
315
|
if (lres.code === 0)
|
|
315
316
|
remoteBundles = parseRemoteBundles(lres.stdout);
|
|
316
317
|
}
|
|
@@ -382,12 +383,13 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
382
383
|
}
|
|
383
384
|
const hint = osHint(device.platform);
|
|
384
385
|
const env = remoteEnv(device.platform);
|
|
386
|
+
const extraSshArgs = deviceIdentityArgs(device);
|
|
385
387
|
let ok = true;
|
|
386
|
-
const sshAgents = (args, input) => sshExec(target, buildRemoteAgentsInvocation(args, undefined, hint, env), { timeoutMs: 300000, multiplex: true, input });
|
|
388
|
+
const sshAgents = (args, input) => sshExec(target, buildRemoteAgentsInvocation(args, undefined, hint, env), { timeoutMs: 300000, multiplex: true, input, extraSshArgs });
|
|
387
389
|
// 1. agents-cli install/upgrade.
|
|
388
390
|
const cliAction = row.actions.find((a) => a.kind === 'install-cli' || a.kind === 'upgrade-cli');
|
|
389
391
|
if (cliAction) {
|
|
390
|
-
const r = bootstrapAgentsCli(target, ctx.targetCliVersion, hint);
|
|
392
|
+
const r = bootstrapAgentsCli(target, ctx.targetCliVersion, hint, extraSshArgs);
|
|
391
393
|
steps.push({ kind: cliAction.kind, ok: r.ok, detail: cliAction.detail });
|
|
392
394
|
ok = ok && r.ok;
|
|
393
395
|
}
|
|
@@ -44,6 +44,7 @@ export interface PendingLogin {
|
|
|
44
44
|
platform?: string;
|
|
45
45
|
/** ssh dial target for the device. */
|
|
46
46
|
target: string;
|
|
47
|
+
extraSshArgs?: string[];
|
|
47
48
|
flow: LoginFlow;
|
|
48
49
|
/** True when a device-code flow this command can actually drive. */
|
|
49
50
|
remotable: boolean;
|
|
@@ -75,7 +76,7 @@ export declare function selectLoginTargets(devices: DeviceProfile[], agents: (Ag
|
|
|
75
76
|
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
76
77
|
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
77
78
|
*/
|
|
78
|
-
export declare function buildRemoteLoginSshCommand(target: string, flow: LoginFlow): string;
|
|
79
|
+
export declare function buildRemoteLoginSshCommand(target: string, flow: LoginFlow, extraSshArgs?: string[]): string;
|
|
79
80
|
export type LoginMode = 'bulk' | 'interactive';
|
|
80
81
|
/** Per-pair status shape shared between the driver, the status endpoint, and the page. */
|
|
81
82
|
export type LoginStatusState = 'pending' | 'driving' | 'ready' | 'authorized' | 'error' | 'skipped';
|
|
@@ -138,7 +139,7 @@ export interface DriveResult extends ScrapedLogin {
|
|
|
138
139
|
* in the browser; the caller polls the credential file for completion. Testable
|
|
139
140
|
* against a fake {@link PtyDriver}.
|
|
140
141
|
*/
|
|
141
|
-
export declare function driveRemoteLogin(target: string, flow: LoginFlow, driver: PtyDriver, opts?: DriveOptions): Promise<DriveResult>;
|
|
142
|
+
export declare function driveRemoteLogin(target: string, flow: LoginFlow, driver: PtyDriver, opts?: DriveOptions, extraSshArgs?: string[]): Promise<DriveResult>;
|
|
142
143
|
export interface DetectOptions {
|
|
143
144
|
agents?: (AgentId | string)[];
|
|
144
145
|
devices?: string[];
|
|
@@ -157,7 +158,7 @@ export declare function detectPending(opts?: DetectOptions): Promise<PendingLogi
|
|
|
157
158
|
* bumping after the human completes the browser step. POSIX `stat` only (the
|
|
158
159
|
* device-code agents are all POSIX-fleet).
|
|
159
160
|
*/
|
|
160
|
-
export declare function remoteFileMtime(target: string, homeRel: string): Promise<number>;
|
|
161
|
+
export declare function remoteFileMtime(target: string, homeRel: string, extraSshArgs?: string[]): Promise<number>;
|
|
161
162
|
export interface RunFleetLoginOptions {
|
|
162
163
|
agents?: (AgentId | string)[];
|
|
163
164
|
devices?: string[];
|