@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark for the agent-execution hot path: env construction (buildExecEnv),
|
|
3
|
+
* argv assembly (buildExecCommand), and process spawn (execAgent -> spawnAgent).
|
|
4
|
+
*
|
|
5
|
+
* No mocking. Runs against this machine's REAL ~/.agents layout — the actual
|
|
6
|
+
* agents.yaml (state.ts:1124 readMeta), the actual installed version homes
|
|
7
|
+
* under ~/.agents/.history/versions/<agent>/ (versions.ts:1284
|
|
8
|
+
* listInstalledVersions), and, for the spawn group, the actual installed
|
|
9
|
+
* `claude` binary. `process.cwd()` during a real `vitest bench` run is this
|
|
10
|
+
* package's directory inside whatever checkout invoked it (typically several
|
|
11
|
+
* levels deep under a git worktree), so the project-version directory walk in
|
|
12
|
+
* versions.ts:2287 getProjectVersion runs its REAL depth here, not a synthetic
|
|
13
|
+
* shallow stub.
|
|
14
|
+
*
|
|
15
|
+
* The dominant, measured cost driver in buildExecEnv turned out to be neither
|
|
16
|
+
* of the two caches below on their own -- it's claude-account-token.ts:51
|
|
17
|
+
* resolveClaudeSetupToken(), called from exec.ts:424 for EVERY resolved claude
|
|
18
|
+
* version (pinned or auto-resolved) whose version home has a signed-in
|
|
19
|
+
* account. It is entirely uncached: each call re-derives an AES key via
|
|
20
|
+
* `scryptSync` (secrets/filestore.ts:208-210, invoked from
|
|
21
|
+
* decryptForFallback at secrets/filestore.ts:230-240) to decrypt the
|
|
22
|
+
* file-backed `auth` secrets bundle. Measured standalone (a one-off
|
|
23
|
+
* `performance.now()` probe against the real bundle, not part of this
|
|
24
|
+
* benchmark file): ~150-170ms per call, first call and every call after —
|
|
25
|
+
* scrypt is deliberately memory-hard/slow and nothing memoizes the derived
|
|
26
|
+
* key or the decrypted token across calls. A version home with NO signed-in
|
|
27
|
+
* account short-circuits at claude-account-token.ts:56-57
|
|
28
|
+
* (readClaudeAccountEmail returns null) before ever reaching the decrypt, so
|
|
29
|
+
* the SAME code path costs ~0.07ms or ~150ms purely depending on whether that
|
|
30
|
+
* specific version is logged in — that split, not warm/cold cache state, is
|
|
31
|
+
* the headline finding, so it is what this file isolates directly with two
|
|
32
|
+
* explicit version pins discovered from the real ~/.agents/.history/versions/
|
|
33
|
+
* layout: `loggedInClaudeVersion` and `loggedOutClaudeVersion`.
|
|
34
|
+
*
|
|
35
|
+
* Two cache regimes are ALSO benchmarked, because they cost meaningfully
|
|
36
|
+
* different amounts on top of the above:
|
|
37
|
+
*
|
|
38
|
+
* - WARM: state.ts:1124 readMeta()'s mtime-keyed cache and actor.ts:163
|
|
39
|
+
* resolveActor()'s process-lifetime cache are both hot. This is the
|
|
40
|
+
* steady state inside one long-lived orchestrator process that calls
|
|
41
|
+
* buildExecEnv many times (loop.ts:214, teams, runner.ts:720).
|
|
42
|
+
* - COLD: both caches invalidated before every sample, matching the FIRST
|
|
43
|
+
* buildExecEnv call in a freshly spawned `agents run` process -- the
|
|
44
|
+
* common case, since every real CLI invocation is its own process.
|
|
45
|
+
* Measured here it costs about the same as warm (readMeta's re-parse of a
|
|
46
|
+
* ~5KB agents.yaml and a fresh actor resolve are both sub-millisecond) --
|
|
47
|
+
* NOT because the invalidation is a no-op, but because this shell's
|
|
48
|
+
* process.env already carries `AGENTS_ACTOR=UNRESOLVED@zion` (inherited
|
|
49
|
+
* from the agent harness that launched this session), so actor.ts:149
|
|
50
|
+
* `inheritedActor(env)` returns immediately and computeActor() never
|
|
51
|
+
* reaches the SSH branch. A genuinely fresh interactive terminal (no
|
|
52
|
+
* inherited AGENTS_ACTOR) would instead hit actor.ts:152-154 and, on an
|
|
53
|
+
* SSH-connected box (SSH_CONNECTION set), pay actor.ts:62 tailscaleWhois()
|
|
54
|
+
* — a real `spawnSync('tailscale', ['whois', ...])` subprocess capped at
|
|
55
|
+
* 2s (actor.ts:53 WHOIS_TIMEOUT_MS) — on that literal first call. That
|
|
56
|
+
* cost is real but environment-dependent and not reproduced by this bench
|
|
57
|
+
* run; it is called out here rather than silently assumed.
|
|
58
|
+
*
|
|
59
|
+
* The execAgent group spawns the REAL installed `claude` binary (real fork+exec,
|
|
60
|
+
* real buildExecEnv/buildExecCommand output) with `passthroughArgs: ['--version']`
|
|
61
|
+
* appended. Claude's own arg parsing short-circuits on `--version` before doing
|
|
62
|
+
* any network/session work (verified: `claude -p "x" --permission-mode plan
|
|
63
|
+
* --version` exits in ~0.2s printing only the version string) — this measures
|
|
64
|
+
* exec.ts's own spawn overhead without paying for (or depending on) a real,
|
|
65
|
+
* non-deterministic, network-bound agent conversation.
|
|
66
|
+
*/
|
|
67
|
+
import { describe, bench } from 'vitest';
|
|
68
|
+
import * as fs from 'node:fs';
|
|
69
|
+
import * as path from 'node:path';
|
|
70
|
+
import { randomUUID } from 'node:crypto';
|
|
71
|
+
import { buildExecEnv, buildExecCommand, execAgent } from './exec.js';
|
|
72
|
+
import { resetActorCache } from './actor.js';
|
|
73
|
+
import { getUserAgentsDir, getHistoryDir } from './state.js';
|
|
74
|
+
import { listInstalledVersions } from './versions.js';
|
|
75
|
+
function execOpts(over) {
|
|
76
|
+
return { mode: 'plan', effort: 'auto', cwd: process.cwd(), ...over };
|
|
77
|
+
}
|
|
78
|
+
// Discovered from the REAL version-home layout so this bench runs unmodified
|
|
79
|
+
// on any dev box (or degrades gracefully when nothing is installed, e.g. a
|
|
80
|
+
// bare CI runner -- this file is not wired into `vitest run`, see
|
|
81
|
+
// vitest.config.ts:9, so that only matters for a human running it locally).
|
|
82
|
+
const installedClaudeVersions = listInstalledVersions('claude');
|
|
83
|
+
const installedCodex = listInstalledVersions('codex').at(-1);
|
|
84
|
+
/**
|
|
85
|
+
* Whether a claude version's home has a resolvable oauth account email
|
|
86
|
+
* (claude-account-token.ts:29 readClaudeAccountEmail's own check), WITHOUT
|
|
87
|
+
* going through the secrets bundle at all -- this only reads the plaintext
|
|
88
|
+
* `.claude.json` account marker, never a secret value, so it is safe to run
|
|
89
|
+
* as bench setup and never prints anything sensitive.
|
|
90
|
+
*/
|
|
91
|
+
function hasResolvableAccount(version) {
|
|
92
|
+
const home = path.join(getHistoryDir(), 'versions', 'claude', version, 'home');
|
|
93
|
+
for (const p of [path.join(home, '.claude', '.claude.json'), path.join(home, '.claude.json')]) {
|
|
94
|
+
try {
|
|
95
|
+
const email = JSON.parse(fs.readFileSync(p, 'utf-8')).oauthAccount?.emailAddress;
|
|
96
|
+
if (typeof email === 'string' && email.trim().length > 0)
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch { /* try the next candidate path */ }
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const loggedInClaudeVersion = installedClaudeVersions.find(hasResolvableAccount);
|
|
104
|
+
const loggedOutClaudeVersion = installedClaudeVersions.find((v) => !hasResolvableAccount(v));
|
|
105
|
+
const metaFile = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
106
|
+
/**
|
|
107
|
+
* Force the next buildExecEnv call to pay full cold-start cost: clears the
|
|
108
|
+
* process-lifetime actor cache (actor.ts:157 `cached`) and bumps agents.yaml's
|
|
109
|
+
* mtime so state.ts:1130 readMeta()'s stamp check misses and it re-parses --
|
|
110
|
+
* exactly the "another process touched the file" invalidation path the module
|
|
111
|
+
* doc at state.ts:1120 describes.
|
|
112
|
+
*
|
|
113
|
+
* tinybench's bench-level `setup` hook (the only per-task hook vitest's
|
|
114
|
+
* `bench(name, fn, options)` actually exposes -- `beforeEach`/`afterEach` are
|
|
115
|
+
* tinybench's internal per-Task `FnOptions`, not part of the `Options` shape
|
|
116
|
+
* vitest forwards, and using them here is a type error) fires ONCE before a
|
|
117
|
+
* task's `run()`, not once per sample. So to measure a genuinely cold call
|
|
118
|
+
* rather than one cold sample diluted into a mean with many warm ones, every
|
|
119
|
+
* cold bench() below pairs this `setup` with `{ iterations: 1, time: 1,
|
|
120
|
+
* warmupIterations: 0, warmupTime: 0 }` -- no warmup, and a near-zero time
|
|
121
|
+
* budget so the run loop stops as soon as its 1-sample floor is met.
|
|
122
|
+
*/
|
|
123
|
+
function invalidateCaches(_task, mode) {
|
|
124
|
+
if (mode !== 'run')
|
|
125
|
+
return;
|
|
126
|
+
resetActorCache();
|
|
127
|
+
try {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
fs.utimesSync(metaFile, now, now);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// No ~/.agents/agents.yaml on this box -- readMeta() has nothing cached
|
|
133
|
+
// to invalidate either, so the cold/warm distinction collapses; proceed.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Bench options for a single, genuinely-cold sample — see invalidateCaches doc. */
|
|
137
|
+
const COLD_SAMPLE_OPTS = { setup: invalidateCaches, iterations: 1, time: 1, warmupIterations: 0, warmupTime: 0 };
|
|
138
|
+
describe.skipIf(!loggedInClaudeVersion || !loggedOutClaudeVersion)('buildExecEnv — resolveClaudeSetupToken cost split (exec.ts:424, claude-account-token.ts:51): same code path, signed-in vs not', () => {
|
|
139
|
+
bench('pinned version WITH a signed-in account (pays the uncached scrypt decrypt every call)', () => {
|
|
140
|
+
buildExecEnv(execOpts({ agent: 'claude', version: loggedInClaudeVersion, sessionId: randomUUID() }));
|
|
141
|
+
});
|
|
142
|
+
bench('pinned version WITHOUT a signed-in account (readClaudeAccountEmail short-circuits, claude-account-token.ts:56)', () => {
|
|
143
|
+
buildExecEnv(execOpts({ agent: 'claude', version: loggedOutClaudeVersion, sessionId: randomUUID() }));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('buildExecEnv — warm cache (steady state: loop.ts/teams/runner calling it repeatedly in one process)', () => {
|
|
147
|
+
bench('claude, auto-resolved version (resolveVersion + isVersionInstalled + resolveClaudeSetupToken chain)', () => {
|
|
148
|
+
buildExecEnv(execOpts({ agent: 'claude', sessionId: randomUUID() }));
|
|
149
|
+
});
|
|
150
|
+
bench('codex, explicit pinned version (no claude-account-token path at all)', () => {
|
|
151
|
+
buildExecEnv(execOpts({ agent: 'codex', version: installedCodex ?? '0.146.0', sessionId: randomUUID() }));
|
|
152
|
+
});
|
|
153
|
+
bench('codex, auto-resolved version', () => {
|
|
154
|
+
buildExecEnv(execOpts({ agent: 'codex', sessionId: randomUUID() }));
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('buildExecEnv — cold cache (single sample: the first call in a fresh `agents run` process)', () => {
|
|
158
|
+
bench('claude, auto-resolved version', () => {
|
|
159
|
+
buildExecEnv(execOpts({ agent: 'claude', sessionId: randomUUID() }));
|
|
160
|
+
}, COLD_SAMPLE_OPTS);
|
|
161
|
+
bench.skipIf(!loggedOutClaudeVersion)('claude, pinned version WITHOUT a signed-in account (isolates readMeta+actor cold cost from the scrypt cost above)', () => {
|
|
162
|
+
buildExecEnv(execOpts({ agent: 'claude', version: loggedOutClaudeVersion, sessionId: randomUUID() }));
|
|
163
|
+
}, COLD_SAMPLE_OPTS);
|
|
164
|
+
});
|
|
165
|
+
describe('buildExecCommand — argv assembly (runs immediately before spawn in spawnAgent, exec.ts:1745)', () => {
|
|
166
|
+
bench('claude headless, explicit pinned version', () => {
|
|
167
|
+
buildExecCommand(execOpts({
|
|
168
|
+
agent: 'claude', version: loggedOutClaudeVersion ?? installedClaudeVersions.at(-1) ?? '2.1.221', prompt: 'benchmark prompt', sessionId: randomUUID(),
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
bench('claude headless, auto-resolved version + model tier (re-walks resolveVersion a second time, exec.ts:972)', () => {
|
|
172
|
+
buildExecCommand(execOpts({
|
|
173
|
+
agent: 'claude', prompt: 'benchmark prompt', model: 'sonnet', sessionId: randomUUID(),
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe.skipIf(!loggedOutClaudeVersion)('execAgent — real subprocess spawn (real claude binary; --version passthrough keeps it network-free)', () => {
|
|
178
|
+
bench('claude headless spawn, pinned version without a signed-in account (isolates spawn overhead from the scrypt cost above)', async () => {
|
|
179
|
+
await execAgent(execOpts({
|
|
180
|
+
agent: 'claude',
|
|
181
|
+
version: loggedOutClaudeVersion,
|
|
182
|
+
prompt: 'benchmark prompt',
|
|
183
|
+
passthroughArgs: ['--version'],
|
|
184
|
+
}));
|
|
185
|
+
}, { time: 3000, iterations: 15 });
|
|
186
|
+
});
|
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[];
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import * as http from 'http';
|
|
30
30
|
import { isControlDevice } from '../devices/registry.js';
|
|
31
31
|
import { loadDevices } from '../devices/registry.js';
|
|
32
|
-
import { fleetDialTarget } from '../devices/connect.js';
|
|
32
|
+
import { deviceIdentityArgs, fleetDialTarget } from '../devices/connect.js';
|
|
33
33
|
import { planFleetTargets } from '../devices/fleet.js';
|
|
34
34
|
import { assertValidSshTarget, shellQuote, sshExecAsync } from '../ssh-exec.js';
|
|
35
35
|
import { machineId } from '../session/sync/config.js';
|
|
@@ -134,6 +134,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
134
134
|
agent,
|
|
135
135
|
platform: device.platform,
|
|
136
136
|
target: fleetDialTarget(device),
|
|
137
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
137
138
|
flow,
|
|
138
139
|
remotable,
|
|
139
140
|
reason,
|
|
@@ -152,7 +153,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
152
153
|
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
153
154
|
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
154
155
|
*/
|
|
155
|
-
export function buildRemoteLoginSshCommand(target, flow) {
|
|
156
|
+
export function buildRemoteLoginSshCommand(target, flow, extraSshArgs = []) {
|
|
156
157
|
assertValidSshTarget(target);
|
|
157
158
|
// The agent CLIs (kimi/droid/codex/…) are agents-cli shims that live in
|
|
158
159
|
// ~/.agents/.cache/shims and are only on PATH in an interactive/login shell.
|
|
@@ -165,6 +166,7 @@ export function buildRemoteLoginSshCommand(target, flow) {
|
|
|
165
166
|
'ssh', '-tt',
|
|
166
167
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
167
168
|
'-o', 'ConnectTimeout=10',
|
|
169
|
+
...extraSshArgs.map(shellQuote),
|
|
168
170
|
shellQuote(target),
|
|
169
171
|
shellQuote(remoteCmd),
|
|
170
172
|
].join(' ');
|
|
@@ -367,7 +369,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
367
369
|
* in the browser; the caller polls the credential file for completion. Testable
|
|
368
370
|
* against a fake {@link PtyDriver}.
|
|
369
371
|
*/
|
|
370
|
-
export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
372
|
+
export async function driveRemoteLogin(target, flow, driver, opts = {}, extraSshArgs = []) {
|
|
371
373
|
const initialDelayMs = opts.initialDelayMs ?? 4000;
|
|
372
374
|
const pollMs = opts.pollMs ?? 1000;
|
|
373
375
|
const timeoutMs = opts.timeoutMs ?? 90000;
|
|
@@ -377,7 +379,7 @@ export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
|
377
379
|
// `ssh -tt` process it drives) leaks until the sidecar's idle reaper. On the
|
|
378
380
|
// normal return paths the caller owns stop() via the returned sessionId.
|
|
379
381
|
try {
|
|
380
|
-
await driver.exec(id, buildRemoteLoginSshCommand(target, flow));
|
|
382
|
+
await driver.exec(id, buildRemoteLoginSshCommand(target, flow, extraSshArgs));
|
|
381
383
|
await sleep(initialDelayMs);
|
|
382
384
|
if (flow.deviceCodeSelect) {
|
|
383
385
|
await driver.write(id, flow.deviceCodeSelect);
|
|
@@ -431,9 +433,9 @@ export async function detectPending(opts = {}) {
|
|
|
431
433
|
* bumping after the human completes the browser step. POSIX `stat` only (the
|
|
432
434
|
* device-code agents are all POSIX-fleet).
|
|
433
435
|
*/
|
|
434
|
-
export async function remoteFileMtime(target, homeRel) {
|
|
436
|
+
export async function remoteFileMtime(target, homeRel, extraSshArgs = []) {
|
|
435
437
|
const cmd = `stat -c %Y "$HOME/${homeRel}" 2>/dev/null || stat -f %m "$HOME/${homeRel}" 2>/dev/null || echo 0`;
|
|
436
|
-
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true }).catch(() => null);
|
|
438
|
+
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs }).catch(() => null);
|
|
437
439
|
if (!res || res.code !== 0)
|
|
438
440
|
return 0;
|
|
439
441
|
const n = parseInt(res.stdout.trim(), 10);
|
|
@@ -507,10 +509,10 @@ export async function runFleetLogin(opts = {}) {
|
|
|
507
509
|
const k = key(p.device, p.agent);
|
|
508
510
|
const st = statuses.get(k);
|
|
509
511
|
st.state = 'driving';
|
|
510
|
-
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
512
|
+
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
511
513
|
let sessionId;
|
|
512
514
|
try {
|
|
513
|
-
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive);
|
|
515
|
+
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive, p.extraSshArgs);
|
|
514
516
|
sessionId = r.sessionId;
|
|
515
517
|
if (r.url && r.code) {
|
|
516
518
|
st.url = r.url;
|
|
@@ -539,7 +541,7 @@ export async function runFleetLogin(opts = {}) {
|
|
|
539
541
|
st.detail = 'device code expired before authorization';
|
|
540
542
|
break;
|
|
541
543
|
}
|
|
542
|
-
const mtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
544
|
+
const mtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
543
545
|
if (mtime > baseMtime) {
|
|
544
546
|
st.state = 'authorized';
|
|
545
547
|
break;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare function readGeminiSettings(settingsPath: string): Record<string, unknown>;
|
|
2
2
|
export declare function writeGeminiSettings(settingsPath: string, settings: Record<string, unknown>): void;
|
|
3
3
|
export declare function updateGeminiSettings(settingsPath: string, mutate: (settings: Record<string, unknown>) => void): Record<string, unknown>;
|
|
4
|
-
export declare function setGeminiAutoUpdateDisabled(settings: Record<string, unknown>): void;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic reader/writer for a `.gemini/…/settings.json`-shaped JSON config.
|
|
3
|
+
*
|
|
4
|
+
* Despite the name, this module is no longer gemini-specific: gemini itself
|
|
5
|
+
* is hard-deprecated and has no live caller left (its own settings writer,
|
|
6
|
+
* `generateGeminiConfig`, was removed with the RUSH-2202 runner.ts fix — a
|
|
7
|
+
* gemini routine is rejected before it ever reaches sandbox prep). Antigravity
|
|
8
|
+
* nests its own `settings.json` under the same `.gemini/antigravity-cli/` tree
|
|
9
|
+
* and shares this exact shape, so `permissions.ts` reuses `updateGeminiSettings`
|
|
10
|
+
* for antigravity's live permission writes. Keep the generic helpers; do not
|
|
11
|
+
* reintroduce gemini-only logic here.
|
|
12
|
+
*/
|
|
1
13
|
import * as fs from 'fs';
|
|
2
14
|
import * as path from 'path';
|
|
3
15
|
function isRecord(value) {
|
|
@@ -24,10 +36,3 @@ export function updateGeminiSettings(settingsPath, mutate) {
|
|
|
24
36
|
writeGeminiSettings(settingsPath, settings);
|
|
25
37
|
return settings;
|
|
26
38
|
}
|
|
27
|
-
export function setGeminiAutoUpdateDisabled(settings) {
|
|
28
|
-
const general = isRecord(settings.general) ? settings.general : {};
|
|
29
|
-
settings.general = {
|
|
30
|
-
...general,
|
|
31
|
-
enableAutoUpdate: false,
|
|
32
|
-
};
|
|
33
|
-
}
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -217,12 +217,15 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
217
217
|
*
|
|
218
218
|
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
219
219
|
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
220
|
-
* `90s`, `1h`.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* `90s`, `1h`. Suffixed durations longer than 24 hours are rejected as likely
|
|
221
|
+
* typos, while bare seconds stay uncapped for backward compatibility. This
|
|
222
|
+
* intentionally does NOT reuse {@link parseTimeout} from routines.ts — that one
|
|
223
|
+
* returns milliseconds, has no seconds (`s`) unit, and floors at one minute,
|
|
224
|
+
* none of which fit hook timeouts (typically 5–600s).
|
|
223
225
|
*
|
|
224
|
-
* Returns the seconds value, or `null` when the input is not a positive number
|
|
225
|
-
* or a
|
|
226
|
+
* Returns the seconds value, or `null` when the input is not a positive number,
|
|
227
|
+
* is not parseable, or is a suffixed duration longer than 24 hours — the caller
|
|
228
|
+
* decides how to surface that.
|
|
226
229
|
*/
|
|
227
230
|
export declare function normalizeHookTimeoutSeconds(value: unknown): number | null;
|
|
228
231
|
/**
|