@phnx-labs/agents-cli 1.22.114 → 1.22.115
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 +110 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +199 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +2 -7
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.js +1 -8
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +20 -10
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +92 -233
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +1 -7
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.js +2 -2
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.js +21 -1
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/hub-server.d.ts +86 -0
- package/dist/lib/feed/hub-server.js +334 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/remote-cmd.d.ts +51 -1
- package/dist/lib/hosts/remote-cmd.js +125 -8
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +26 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +215 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
|
@@ -5,11 +5,29 @@ export declare const PEER_BACKOFF_BASE_MS = 2000;
|
|
|
5
5
|
export declare const PEER_BACKOFF_CAP_MS = 60000;
|
|
6
6
|
/** Consecutive failed spawns before the peer is parked. */
|
|
7
7
|
export declare const PEER_PARK_AFTER_FAILURES = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
|
|
10
|
+
* gives way to {@link PEER_RETIRED_RECHECK_MS}.
|
|
11
|
+
*
|
|
12
|
+
* The cap alone bounds the delay but not the total work: a box that is off for a
|
|
13
|
+
* weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
|
|
14
|
+
* per peer, each one already known to fail. Ten consecutive failures is well
|
|
15
|
+
* past any transient network event, so past that point the peer is treated as
|
|
16
|
+
* genuinely absent and re-dialed on the slow cadence instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PEER_RETIRE_AFTER_FAILURES = 10;
|
|
19
|
+
/**
|
|
20
|
+
* Re-dial cadence for a retired peer. Still bounded rather than never, because a
|
|
21
|
+
* box can come back without anything touching the device registry — waiting
|
|
22
|
+
* only on a registry change would leave it unreachable until an operator acted.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PEER_RETIRED_RECHECK_MS: number;
|
|
8
25
|
/**
|
|
9
26
|
* Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
|
|
10
|
-
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}
|
|
27
|
+
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
|
|
28
|
+
* and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
|
|
11
29
|
*/
|
|
12
|
-
export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number): number;
|
|
30
|
+
export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number, retireAfter?: number, retiredMs?: number): number;
|
|
13
31
|
interface PeerStreamOptions {
|
|
14
32
|
/** The device to subscribe to. */
|
|
15
33
|
device: DeviceProfile;
|
|
@@ -29,6 +47,10 @@ interface PeerStreamOptions {
|
|
|
29
47
|
backoffCapMs?: number;
|
|
30
48
|
/** Override the park threshold (tests). */
|
|
31
49
|
parkAfterFailures?: number;
|
|
50
|
+
/** Override the retire threshold (tests). */
|
|
51
|
+
retireAfterFailures?: number;
|
|
52
|
+
/** Override the retired re-dial cadence (tests). */
|
|
53
|
+
retiredRecheckMs?: number;
|
|
32
54
|
/** Override the ssh binary (tests). */
|
|
33
55
|
sshBin?: string;
|
|
34
56
|
/** Override the parked peer's registry re-check cadence (tests). */
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* - a peer that fails {@link PEER_PARK_AFTER_FAILURES} spawns in a row parked —
|
|
20
20
|
* it stops the reconnect cycle and re-dials only when the device registry
|
|
21
21
|
* changes or the capped backoff elapses;
|
|
22
|
+
* - a peer that fails {@link PEER_RETIRE_AFTER_FAILURES} in a row RETIRED — the
|
|
23
|
+
* capped 60 s ladder is itself unbounded in total work, so past that point the
|
|
24
|
+
* re-dial drops to {@link PEER_RETIRED_RECHECK_MS};
|
|
22
25
|
* - abort listeners removed per iteration, so a watcher open for hours does not
|
|
23
26
|
* accumulate one per reconnect on the caller's AbortSignal.
|
|
24
27
|
*/
|
|
@@ -34,17 +37,37 @@ export const PEER_BACKOFF_BASE_MS = 2_000;
|
|
|
34
37
|
export const PEER_BACKOFF_CAP_MS = 60_000;
|
|
35
38
|
/** Consecutive failed spawns before the peer is parked. */
|
|
36
39
|
export const PEER_PARK_AFTER_FAILURES = 3;
|
|
40
|
+
/**
|
|
41
|
+
* Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
|
|
42
|
+
* gives way to {@link PEER_RETIRED_RECHECK_MS}.
|
|
43
|
+
*
|
|
44
|
+
* The cap alone bounds the delay but not the total work: a box that is off for a
|
|
45
|
+
* weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
|
|
46
|
+
* per peer, each one already known to fail. Ten consecutive failures is well
|
|
47
|
+
* past any transient network event, so past that point the peer is treated as
|
|
48
|
+
* genuinely absent and re-dialed on the slow cadence instead.
|
|
49
|
+
*/
|
|
50
|
+
export const PEER_RETIRE_AFTER_FAILURES = 10;
|
|
51
|
+
/**
|
|
52
|
+
* Re-dial cadence for a retired peer. Still bounded rather than never, because a
|
|
53
|
+
* box can come back without anything touching the device registry — waiting
|
|
54
|
+
* only on a registry change would leave it unreachable until an operator acted.
|
|
55
|
+
*/
|
|
56
|
+
export const PEER_RETIRED_RECHECK_MS = 15 * 60_000;
|
|
37
57
|
/** Bytes of a peer's stderr retained for the `unavailable` reason. */
|
|
38
58
|
const PEER_STDERR_BYTES = 2_048;
|
|
39
59
|
/** How often a parked peer re-checks the device registry for a refresh. */
|
|
40
60
|
const PEER_REGISTRY_POLL_MS = 5_000;
|
|
41
61
|
/**
|
|
42
62
|
* Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
|
|
43
|
-
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}
|
|
63
|
+
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
|
|
64
|
+
* and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
|
|
44
65
|
*/
|
|
45
|
-
export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS) {
|
|
66
|
+
export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS, retireAfter = PEER_RETIRE_AFTER_FAILURES, retiredMs = PEER_RETIRED_RECHECK_MS) {
|
|
46
67
|
if (failures <= 0)
|
|
47
68
|
return 0;
|
|
69
|
+
if (failures >= retireAfter)
|
|
70
|
+
return retiredMs;
|
|
48
71
|
return Math.min(cap, base * 2 ** (failures - 1));
|
|
49
72
|
}
|
|
50
73
|
/** Bounded tail of a child's stderr, kept for the unavailable reason. */
|
|
@@ -133,10 +156,14 @@ export async function streamFromPeer(options) {
|
|
|
133
156
|
failures += 1;
|
|
134
157
|
const exit = code == null ? 'ssh failed' : `ssh exited ${code}`;
|
|
135
158
|
const parked = failures >= parkAfter;
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
const retireAfter = options.retireAfterFailures ?? PEER_RETIRE_AFTER_FAILURES;
|
|
160
|
+
const retired = failures >= retireAfter;
|
|
161
|
+
const delay = peerBackoffDelayMs(failures, options.backoffBaseMs, options.backoffCapMs, retireAfter, options.retiredRecheckMs);
|
|
162
|
+
options.onUnavailable(retired
|
|
163
|
+
? `${reasonFor(exit, stderr)} — retired after ${failures} failed connections, re-dialing in ${Math.round(delay / 60_000)}min or on a device refresh`
|
|
164
|
+
: parked
|
|
165
|
+
? `${reasonFor(exit, stderr)} — parked after ${failures} failed connections, retrying in ${Math.round(delay / 1000)}s or on a device refresh`
|
|
166
|
+
: reasonFor(exit, stderr));
|
|
140
167
|
await parkedWait(options, delay);
|
|
141
168
|
}
|
|
142
169
|
}
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* MANAGED-FIRST — the mere PRESENCE of an r2.backups bundle is NOT a BYO
|
|
19
19
|
* override. A signed-in user with a stale r2.backups bundle still backs up to
|
|
20
20
|
* managed unless they opt out explicitly: `--byo`, `AGENTS_SESSIONS_BACKEND=byo`,
|
|
21
|
-
* or a DI write token. This mirrors
|
|
22
|
-
* config is deliberately not an override) so the product's
|
|
23
|
-
* contract is identical across surfaces.
|
|
21
|
+
* or a DI write token. This mirrors the artifact-share backend contract (a
|
|
22
|
+
* persisted BYO config is deliberately not an override) so the product's
|
|
23
|
+
* managed-first contract is identical across surfaces.
|
|
24
24
|
*/
|
|
25
25
|
import { type PhoenixSession } from '../../identity/client.js';
|
|
26
26
|
import { type R2Config } from './config.js';
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* MANAGED-FIRST — the mere PRESENCE of an r2.backups bundle is NOT a BYO
|
|
19
19
|
* override. A signed-in user with a stale r2.backups bundle still backs up to
|
|
20
20
|
* managed unless they opt out explicitly: `--byo`, `AGENTS_SESSIONS_BACKEND=byo`,
|
|
21
|
-
* or a DI write token. This mirrors
|
|
22
|
-
* config is deliberately not an override) so the product's
|
|
23
|
-
* contract is identical across surfaces.
|
|
21
|
+
* or a DI write token. This mirrors the artifact-share backend contract (a
|
|
22
|
+
* persisted BYO config is deliberately not an override) so the product's
|
|
23
|
+
* managed-first contract is identical across surfaces.
|
|
24
24
|
*/
|
|
25
25
|
import { readSession } from '../../identity/client.js';
|
|
26
26
|
import { selectStorageBackendKind } from '../../storage/selection.js';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Cloudflare orchestration for the managed session-backup store. The generic
|
|
2
|
-
// request primitives live in
|
|
2
|
+
// request primitives live in cloudflare/provision; only the isolated sessions
|
|
3
3
|
// resource choices and Worker template belong here. Mirrors lib/traces/provision.ts.
|
|
4
4
|
//
|
|
5
5
|
// This is the OPERATOR provisioning path — cutting the `agents-sessions` Worker +
|
|
6
6
|
// bucket + custom domain once. Product traffic never runs it; a signed-in user
|
|
7
7
|
// talks to the already-deployed Worker through `SessionsHttpClient`.
|
|
8
|
-
import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, putWorkerSecret, } from '../../
|
|
8
|
+
import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, putWorkerSecret, } from '../../cloudflare/provision.js';
|
|
9
9
|
import { DEFAULT_SESSIONS_BUCKET_NAME, DEFAULT_SESSIONS_DOMAIN, DEFAULT_SESSIONS_WORKER_NAME, } from './managed-config.js';
|
|
10
10
|
import { renderSessionsWorkerScript } from './worker-template.js';
|
|
11
11
|
/** Bind the Phoenix identity base URL used to verify every session read and write. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { installCli, resolveCliManifest } from './cli-resources.js';
|
|
2
|
+
import { getCachedToolSetup, refreshToolSetup } from './setup-tool-status.js';
|
|
3
|
+
const PACKAGES = {
|
|
4
|
+
browser: '@phnx-labs/browser-cli@0.1.3',
|
|
5
|
+
computer: '@phnx-labs/computer-cli@0.1.5',
|
|
6
|
+
};
|
|
7
|
+
/** Use the host installer; setup must not install or start a tool from a read. */
|
|
8
|
+
export async function installSetupTool(tool) {
|
|
9
|
+
if (getCachedToolSetup().find((row) => row.tool === tool)?.installed)
|
|
10
|
+
return true;
|
|
11
|
+
const manifest = resolveCliManifest(tool) ?? {
|
|
12
|
+
name: tool,
|
|
13
|
+
check: { kind: 'which', cmd: tool },
|
|
14
|
+
install: [{ npm: PACKAGES[tool] }],
|
|
15
|
+
source: 'builtin',
|
|
16
|
+
path: '',
|
|
17
|
+
};
|
|
18
|
+
const result = installCli(manifest);
|
|
19
|
+
if (result.error)
|
|
20
|
+
console.error(result.error);
|
|
21
|
+
const rows = await refreshToolSetup(tool);
|
|
22
|
+
const installed = rows[0]?.installed === true;
|
|
23
|
+
if (!installed && !result.error)
|
|
24
|
+
console.error(`${tool} installation did not produce a standalone executable on PATH.`);
|
|
25
|
+
return result.installed && installed;
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const SETUP_TOOLS: readonly ["browser", "computer", "secrets"];
|
|
2
|
+
export type SetupTool = typeof SETUP_TOOLS[number];
|
|
3
|
+
export interface ToolSetupRow {
|
|
4
|
+
tool: SetupTool;
|
|
5
|
+
installed: boolean | null;
|
|
6
|
+
executable?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
readiness: 'ready' | 'needs-setup' | 'permission-required' | 'stopped' | 'unsupported' | 'unknown';
|
|
9
|
+
detail: string;
|
|
10
|
+
checkedAtMs: number | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ToolSetupOptions {
|
|
13
|
+
cacheDir?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function toolSetupCacheDir(options?: ToolSetupOptions): string;
|
|
16
|
+
/** Presence is metadata; health is the last explicit check, never a timer probe. */
|
|
17
|
+
export declare function getCachedToolSetup(options?: ToolSetupOptions): ToolSetupRow[];
|
|
18
|
+
export declare function toolReadiness(tool: SetupTool, status: unknown): Pick<ToolSetupRow, 'readiness' | 'detail'>;
|
|
19
|
+
/** A shared disk lock coalesces overlapping requests from separate CLI clients. */
|
|
20
|
+
export declare function refreshToolSetup(tool?: SetupTool | 'all', options?: ToolSetupOptions): Promise<ToolSetupRow[]>;
|
|
21
|
+
/** File notifications are shared by the daemon collector; no background probes. */
|
|
22
|
+
export declare function subscribeToolSetup(listener: (rows: ToolSetupRow[]) => void, options?: ToolSetupOptions): () => void;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { findInPath } from './agent-spec/agents.js';
|
|
4
|
+
import { getCacheDir, getDeviceMetaPath, getHelpersDir, getUserAgentsDir } from './state.js';
|
|
5
|
+
import { atomicWriteJsonSync, withFileLockAsync } from './fs-atomic.js';
|
|
6
|
+
import { probeCapture } from './probe.js';
|
|
7
|
+
import { invocation, isStandaloneComputer } from './computer-client.js';
|
|
8
|
+
import { getSocketPath as browserSocketPath } from './browser/ipc.js';
|
|
9
|
+
export const SETUP_TOOLS = ['browser', 'computer', 'secrets'];
|
|
10
|
+
export function toolSetupCacheDir(options = {}) {
|
|
11
|
+
return path.join(options.cacheDir ?? getCacheDir(), 'setup-tools');
|
|
12
|
+
}
|
|
13
|
+
function cachePath(tool, options) {
|
|
14
|
+
return path.join(toolSetupCacheDir(options), `${tool}.json`);
|
|
15
|
+
}
|
|
16
|
+
function setupInputs(tool) {
|
|
17
|
+
const inputs = [path.join(getUserAgentsDir(), 'agents.yaml'), getDeviceMetaPath()];
|
|
18
|
+
if (tool === 'browser')
|
|
19
|
+
inputs.push(browserSocketPath());
|
|
20
|
+
if (tool === 'computer')
|
|
21
|
+
inputs.push(process.env.COMPUTER_HELPER_SOCKET || path.join(getHelpersDir(), 'computer.sock'));
|
|
22
|
+
return inputs;
|
|
23
|
+
}
|
|
24
|
+
function inputStamp(tool) {
|
|
25
|
+
return setupInputs(tool).map((file) => {
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.statSync(file);
|
|
28
|
+
return `${file}:${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return `${file}:missing`;
|
|
32
|
+
}
|
|
33
|
+
}).join('|');
|
|
34
|
+
}
|
|
35
|
+
function binaryMetadata(tool) {
|
|
36
|
+
const empty = { tool, installed: false, readiness: 'needs-setup', detail: 'CLI is not installed.', checkedAtMs: null };
|
|
37
|
+
try {
|
|
38
|
+
const explicit = process.env[`${tool.toUpperCase()}_BIN`]?.trim();
|
|
39
|
+
const accept = (candidate) => {
|
|
40
|
+
if (tool === 'computer')
|
|
41
|
+
return isStandaloneComputer(candidate);
|
|
42
|
+
if (tool === 'browser') {
|
|
43
|
+
try {
|
|
44
|
+
return !fs.realpathSync(candidate).endsWith(path.join('dist', 'browser.js'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
let executable = explicit ? (accept(explicit) ? explicit : null) : findInPath(tool, { accept });
|
|
53
|
+
if (!executable)
|
|
54
|
+
return { row: empty, fingerprint: 'missing' };
|
|
55
|
+
if (/\.(cmd|ps1)$/i.test(executable)) {
|
|
56
|
+
const packageDir = path.join(path.dirname(executable), 'node_modules', '@phnx-labs', `${tool}-cli`);
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
58
|
+
const entry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[tool];
|
|
59
|
+
if (pkg.name !== `@phnx-labs/${tool}-cli` || typeof entry !== 'string')
|
|
60
|
+
throw new Error('unrecognized npm launcher');
|
|
61
|
+
const resolved = path.resolve(packageDir, entry);
|
|
62
|
+
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`))
|
|
63
|
+
throw new Error('invalid npm entrypoint');
|
|
64
|
+
executable = resolved;
|
|
65
|
+
}
|
|
66
|
+
fs.accessSync(executable, process.platform === 'win32' || /\.[cm]?js$/i.test(executable) ? fs.constants.R_OK : fs.constants.X_OK);
|
|
67
|
+
const real = fs.realpathSync(executable);
|
|
68
|
+
const stat = fs.statSync(real);
|
|
69
|
+
if (!stat.isFile())
|
|
70
|
+
throw new Error('not a file');
|
|
71
|
+
let version;
|
|
72
|
+
let dir = path.dirname(real);
|
|
73
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
76
|
+
if (pkg.name === `@phnx-labs/${tool}-cli` && typeof pkg.version === 'string') {
|
|
77
|
+
version = pkg.version;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* Executables need not be npm packages. */ }
|
|
82
|
+
const parent = path.dirname(dir);
|
|
83
|
+
if (parent === dir)
|
|
84
|
+
break;
|
|
85
|
+
dir = parent;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
fingerprint: `${real}:${stat.size}:${stat.mtimeMs}:${version ?? ''}|${inputStamp(tool)}`,
|
|
89
|
+
row: { tool, installed: true, executable, version, readiness: 'unknown', detail: 'Installed. Health has not been checked.', checkedAtMs: null },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { fingerprint: 'unavailable', row: { ...empty, installed: null, readiness: 'unknown', detail: 'Executable could not be inspected.' } };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function readCache(tool, options) {
|
|
97
|
+
try {
|
|
98
|
+
const cached = JSON.parse(fs.readFileSync(cachePath(tool, options), 'utf8'));
|
|
99
|
+
if (cached.v === 1 && cached.row?.tool === tool && typeof cached.fingerprint === 'string')
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
catch { /* No health result yet. */ }
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** Presence is metadata; health is the last explicit check, never a timer probe. */
|
|
106
|
+
export function getCachedToolSetup(options = {}) {
|
|
107
|
+
return SETUP_TOOLS.map((tool) => {
|
|
108
|
+
const { row, fingerprint } = binaryMetadata(tool);
|
|
109
|
+
const cached = readCache(tool, options);
|
|
110
|
+
return cached?.fingerprint === fingerprint ? { ...cached.row, executable: row.executable, version: row.version } : row;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function toolReadiness(tool, status) {
|
|
114
|
+
if (!status || typeof status !== 'object')
|
|
115
|
+
return { readiness: 'unknown', detail: 'The CLI returned an unreadable health result.' };
|
|
116
|
+
const value = status;
|
|
117
|
+
if (tool === 'computer') {
|
|
118
|
+
if (value.installed === false)
|
|
119
|
+
return { readiness: 'needs-setup', detail: 'CLI installed; computer helper needs setup.' };
|
|
120
|
+
if (value.running === true && value.trusted === true)
|
|
121
|
+
return { readiness: 'ready', detail: 'Helper is running and Accessibility is granted. Screen Recording is checked when capturing.' };
|
|
122
|
+
if (value.running === true && value.trusted === false)
|
|
123
|
+
return { readiness: 'permission-required', detail: 'Grant Accessibility to Agents Computer in System Settings.' };
|
|
124
|
+
if (value.running === false)
|
|
125
|
+
return { readiness: 'stopped', detail: 'Computer helper is stopped or unreachable.' };
|
|
126
|
+
}
|
|
127
|
+
if (tool === 'browser') {
|
|
128
|
+
if (value.running === true)
|
|
129
|
+
return { readiness: 'ready', detail: 'Browser service is running.' };
|
|
130
|
+
if (value.running === false)
|
|
131
|
+
return { readiness: 'stopped', detail: 'Browser service is stopped.' };
|
|
132
|
+
}
|
|
133
|
+
return { readiness: 'unknown', detail: typeof value.error === 'string' && value.error.trim() ? value.error.slice(0, 500) : 'The CLI did not report a recognized health state.' };
|
|
134
|
+
}
|
|
135
|
+
async function checkTool(row) {
|
|
136
|
+
if (!row.installed || !row.executable)
|
|
137
|
+
return { ...row, checkedAtMs: Date.now() };
|
|
138
|
+
// The standalone has no non-interactive health JSON. Do not list bundles or
|
|
139
|
+
// unlock the broker merely to paint a settings row.
|
|
140
|
+
if (row.tool === 'secrets')
|
|
141
|
+
return { ...row, checkedAtMs: Date.now(), detail: 'Installed. Secret access is checked when used; this check does not unlock secrets.' };
|
|
142
|
+
try {
|
|
143
|
+
const { command, prefix } = invocation(row.executable);
|
|
144
|
+
const { stdout } = await probeCapture(command, [...prefix, 'status', '--json'], 8000, { acceptedExitCodes: [0, 1], maxOutputBytes: 256 * 1024 });
|
|
145
|
+
return { ...row, ...toolReadiness(row.tool, JSON.parse(stdout)), checkedAtMs: Date.now() };
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { ...row, readiness: 'unknown', detail: 'Health check failed or timed out. Check again to retry.', checkedAtMs: Date.now() };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** A shared disk lock coalesces overlapping requests from separate CLI clients. */
|
|
152
|
+
export async function refreshToolSetup(tool = 'all', options = {}) {
|
|
153
|
+
const requestedAt = Date.now();
|
|
154
|
+
const dir = toolSetupCacheDir(options);
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
156
|
+
for (const selected of tool === 'all' ? SETUP_TOOLS : [tool]) {
|
|
157
|
+
const file = cachePath(selected, options);
|
|
158
|
+
await withFileLockAsync(file, async () => {
|
|
159
|
+
const metadata = binaryMetadata(selected);
|
|
160
|
+
const cached = readCache(selected, options);
|
|
161
|
+
if (cached?.fingerprint === metadata.fingerprint && (cached.row.checkedAtMs ?? 0) >= requestedAt)
|
|
162
|
+
return;
|
|
163
|
+
const row = await checkTool(metadata.row);
|
|
164
|
+
atomicWriteJsonSync(file, { v: 1, fingerprint: metadata.fingerprint, row });
|
|
165
|
+
}, { realpath: false, acquireTimeoutMs: 20_000 });
|
|
166
|
+
}
|
|
167
|
+
return getCachedToolSetup(options).filter((row) => tool === 'all' || row.tool === tool);
|
|
168
|
+
}
|
|
169
|
+
/** File notifications are shared by the daemon collector; no background probes. */
|
|
170
|
+
export function subscribeToolSetup(listener, options = {}) {
|
|
171
|
+
const dir = toolSetupCacheDir(options);
|
|
172
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
173
|
+
const paths = new Set([dir, ...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean)]);
|
|
174
|
+
for (const tool of SETUP_TOOLS)
|
|
175
|
+
for (const input of setupInputs(tool))
|
|
176
|
+
paths.add(path.dirname(input));
|
|
177
|
+
for (const row of getCachedToolSetup(options)) {
|
|
178
|
+
if (row.executable) {
|
|
179
|
+
paths.add(path.dirname(row.executable));
|
|
180
|
+
try {
|
|
181
|
+
paths.add(path.dirname(fs.realpathSync(row.executable)));
|
|
182
|
+
}
|
|
183
|
+
catch { /* Removed since discovery. */ }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let previous = JSON.stringify(getCachedToolSetup(options));
|
|
187
|
+
let debounce;
|
|
188
|
+
const changed = () => {
|
|
189
|
+
if (debounce)
|
|
190
|
+
return;
|
|
191
|
+
debounce = setTimeout(() => {
|
|
192
|
+
debounce = undefined;
|
|
193
|
+
const rows = getCachedToolSetup(options);
|
|
194
|
+
const next = JSON.stringify(rows);
|
|
195
|
+
if (next !== previous) {
|
|
196
|
+
previous = next;
|
|
197
|
+
listener(rows);
|
|
198
|
+
}
|
|
199
|
+
}, 150);
|
|
200
|
+
debounce.unref();
|
|
201
|
+
};
|
|
202
|
+
const watchers = [];
|
|
203
|
+
for (const target of paths) {
|
|
204
|
+
try {
|
|
205
|
+
const watcher = fs.watch(target, { persistent: false }, changed);
|
|
206
|
+
watcher.on('error', changed);
|
|
207
|
+
watchers.push(watcher);
|
|
208
|
+
}
|
|
209
|
+
catch { /* Absent PATH entries have no executable to report. */ }
|
|
210
|
+
}
|
|
211
|
+
changed();
|
|
212
|
+
return () => { if (debounce)
|
|
213
|
+
clearTimeout(debounce); for (const watcher of watchers)
|
|
214
|
+
watcher.close(); };
|
|
215
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Best-effort runtime env for spawned agents. Never throws AND never prompts.
|
|
2
|
+
*
|
|
3
|
+
* Auto-injecting the share write token on every `agents run` is a background
|
|
4
|
+
* convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
|
|
5
|
+
* ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
|
|
6
|
+
* always `agentOnly`: it resolves the token only from the injected env or an
|
|
7
|
+
* already-held / no-ACL bundle, and silently returns undefined otherwise (the
|
|
8
|
+
* agent can still publish via its own `artifacts share` credentials). For
|
|
9
|
+
* zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
|
|
10
|
+
* share`) or make it no-ACL (`agents secrets policy share never`). */
|
|
11
|
+
export declare function shareRuntimeEnv(): Record<string, string> | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Runtime share-token injection for spawned agents.
|
|
2
|
+
//
|
|
3
|
+
// The artifact share ENGINE moved to `@phnx-labs/artifacts-cli` (PHNX-3992);
|
|
4
|
+
// agents-cli no longer publishes artifacts itself. What stays here is the thin
|
|
5
|
+
// glue that lets a dispatched agent inherit the operator's BYO write token so
|
|
6
|
+
// its own `artifacts share` can publish headlessly: `run`/`teams`/`cloud`
|
|
7
|
+
// dispatch call `shareRuntimeEnv()` and forward the result into the agent env.
|
|
8
|
+
// artifacts-cli honors the same `SHARE_WRITE_TOKEN` variable, so the injection
|
|
9
|
+
// contract is unchanged by the extraction.
|
|
10
|
+
//
|
|
11
|
+
// The endpoint config still lives in `agents.yaml` under `share:` (Meta.share)
|
|
12
|
+
// so a previously-provisioned BYO endpoint keeps syncing fleet-wide; the raw
|
|
13
|
+
// write token lives in the keychain-backed `share` secrets bundle, never on disk.
|
|
14
|
+
import { readMeta } from './state.js';
|
|
15
|
+
import { bundleExistsSync as bundleExists, readAndResolveBundleEnvSync as readAndResolveBundleEnv, } from './secrets-client.js';
|
|
16
|
+
const SHARE_BUNDLE = 'share';
|
|
17
|
+
const SHARE_TOKEN_KEY = 'WRITE_TOKEN';
|
|
18
|
+
const SHARE_TOKEN_ENV_KEY = 'SHARE_WRITE_TOKEN';
|
|
19
|
+
/** Trim; empty / whitespace-only strings are absent. */
|
|
20
|
+
function nonempty(v) {
|
|
21
|
+
const t = v?.trim();
|
|
22
|
+
return t ? t : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Whether a BYO share endpoint is still configured in `agents.yaml`. */
|
|
25
|
+
function hasShareEndpoint() {
|
|
26
|
+
return !!nonempty(readMeta().share?.baseUrl);
|
|
27
|
+
}
|
|
28
|
+
/** The write token as injected ephemerally into fleet/cloud agents. */
|
|
29
|
+
function readWriteTokenEnv(env = process.env) {
|
|
30
|
+
const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
|
|
31
|
+
return token ? token : null;
|
|
32
|
+
}
|
|
33
|
+
/** Best-effort runtime env for spawned agents. Never throws AND never prompts.
|
|
34
|
+
*
|
|
35
|
+
* Auto-injecting the share write token on every `agents run` is a background
|
|
36
|
+
* convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
|
|
37
|
+
* ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
|
|
38
|
+
* always `agentOnly`: it resolves the token only from the injected env or an
|
|
39
|
+
* already-held / no-ACL bundle, and silently returns undefined otherwise (the
|
|
40
|
+
* agent can still publish via its own `artifacts share` credentials). For
|
|
41
|
+
* zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
|
|
42
|
+
* share`) or make it no-ACL (`agents secrets policy share never`). */
|
|
43
|
+
export function shareRuntimeEnv() {
|
|
44
|
+
if (!hasShareEndpoint())
|
|
45
|
+
return undefined;
|
|
46
|
+
const fromEnv = readWriteTokenEnv();
|
|
47
|
+
if (fromEnv)
|
|
48
|
+
return { [SHARE_TOKEN_ENV_KEY]: fromEnv };
|
|
49
|
+
try {
|
|
50
|
+
if (!bundleExists(SHARE_BUNDLE))
|
|
51
|
+
return undefined;
|
|
52
|
+
const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
|
|
53
|
+
caller: 'share',
|
|
54
|
+
keys: [SHARE_TOKEN_KEY],
|
|
55
|
+
agentOnly: true, // never raise a Touch ID sheet on an agent launch (SEC-13)
|
|
56
|
+
});
|
|
57
|
+
const token = env[SHARE_TOKEN_KEY];
|
|
58
|
+
return token ? { [SHARE_TOKEN_ENV_KEY]: token } : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -117,16 +117,12 @@ type DeviceAutoHostOptions = {
|
|
|
117
117
|
device?: string;
|
|
118
118
|
on?: string;
|
|
119
119
|
computer?: string;
|
|
120
|
-
/** @deprecated Hidden alias for `--device auto`. */
|
|
121
|
-
smart?: boolean;
|
|
122
120
|
balanced?: boolean;
|
|
123
121
|
strategy?: string;
|
|
124
122
|
};
|
|
125
123
|
export type DeviceAutoApplyResult = {
|
|
126
124
|
/** True when automatic placement ran successfully. */
|
|
127
125
|
attempted: boolean;
|
|
128
|
-
/** True when deprecated `--smart` was seen. */
|
|
129
|
-
deprecationSmart: boolean;
|
|
130
126
|
/** Present when affinity resolved without error. */
|
|
131
127
|
banner?: {
|
|
132
128
|
hostLabel: string;
|
|
@@ -135,7 +131,7 @@ export type DeviceAutoApplyResult = {
|
|
|
135
131
|
};
|
|
136
132
|
};
|
|
137
133
|
/**
|
|
138
|
-
* Apply `--device auto`
|
|
134
|
+
* Apply `--device auto` onto run options.
|
|
139
135
|
* Mutates `options` in place. Placement failures propagate without rewriting
|
|
140
136
|
* `auto`, so callers fail loud instead of silently launching locally.
|
|
141
137
|
*/
|
package/dist/lib/smart-launch.js
CHANGED
|
@@ -176,7 +176,7 @@ export async function resolveDeviceAuto(agent, opts = {}) {
|
|
|
176
176
|
if (eligiblePool.length === 0) {
|
|
177
177
|
throw new Error(formatNoHealthyDeviceError(pool, signals, agent));
|
|
178
178
|
}
|
|
179
|
-
// `agents devices
|
|
179
|
+
// `agents devices config <name> auto-launch.preferred on` boosts a device in the ranking — resolved
|
|
180
180
|
// over the same pool so a fleet default reaches a doc-less box.
|
|
181
181
|
const preferred = opts.preferred ?? autoLaunchPreferredSet(pool, { roster: pool });
|
|
182
182
|
const picked = pickBestDevice(eligiblePool, [], { signals, agentLabel: agent, preferred });
|
|
@@ -260,21 +260,14 @@ export function isDeviceAuto(value) {
|
|
|
260
260
|
}
|
|
261
261
|
const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
|
|
262
262
|
/**
|
|
263
|
-
* Apply `--device auto`
|
|
263
|
+
* Apply `--device auto` onto run options.
|
|
264
264
|
* Mutates `options` in place. Placement failures propagate without rewriting
|
|
265
265
|
* `auto`, so callers fail loud instead of silently launching locally.
|
|
266
266
|
*/
|
|
267
267
|
export async function applyDeviceAutoToOptions(options, deps = {}) {
|
|
268
|
-
let deprecationSmart = false;
|
|
269
|
-
if (options.smart) {
|
|
270
|
-
const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
|
|
271
|
-
if (!anyHost)
|
|
272
|
-
options.device = 'auto';
|
|
273
|
-
deprecationSmart = true;
|
|
274
|
-
}
|
|
275
268
|
const hasAuto = HOST_SLOTS.some((k) => isDeviceAuto(options[k]));
|
|
276
269
|
if (!hasAuto) {
|
|
277
|
-
return { attempted: false
|
|
270
|
+
return { attempted: false };
|
|
278
271
|
}
|
|
279
272
|
const accountPickerRequested = deps.accountPickerRequested ?? false;
|
|
280
273
|
const resolve = deps.resolve ?? ((accountPicker) => resolveDeviceAuto(deps.agent, { accountPicker }));
|
|
@@ -296,7 +289,6 @@ export async function applyDeviceAutoToOptions(options, deps = {}) {
|
|
|
296
289
|
const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
|
|
297
290
|
return {
|
|
298
291
|
attempted: true,
|
|
299
|
-
deprecationSmart,
|
|
300
292
|
banner: { hostLabel, deviceHint, acctNote },
|
|
301
293
|
};
|
|
302
294
|
}
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -173,6 +173,50 @@ interface SshExecRawStreamOptions extends SshExecOptions {
|
|
|
173
173
|
* offset-resumed log following where a UTF-8 decode boundary must not shift the
|
|
174
174
|
* byte cursor.
|
|
175
175
|
*/
|
|
176
|
+
/** Grace between SIGTERM and SIGKILL for a stream that will not stop. */
|
|
177
|
+
export declare const SSH_STREAM_KILL_GRACE_MS = 3000;
|
|
178
|
+
/** Bytes of a stream's stderr retained; the rest is dropped, with a note. */
|
|
179
|
+
export declare const SSH_STREAM_MAX_STDERR: number;
|
|
180
|
+
export interface SshStreamResult {
|
|
181
|
+
code: number | null;
|
|
182
|
+
stderr: Buffer;
|
|
183
|
+
timedOut: boolean;
|
|
184
|
+
/** True when the child had to be SIGKILLed after ignoring SIGTERM. */
|
|
185
|
+
killed: boolean;
|
|
186
|
+
/** True when stderr was truncated at {@link SSH_STREAM_MAX_STDERR}. */
|
|
187
|
+
stderrTruncated: boolean;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Stream one ssh command's stdout, with the args and env a CALLER built.
|
|
191
|
+
*
|
|
192
|
+
* This exists because {@link sshExecRawStream} assembles its own ssh args, which
|
|
193
|
+
* means it cannot carry a device's canonical auth — the askpass shim for a
|
|
194
|
+
* password-auth box, `-i`/`IdentitiesOnly` for an explicit identity file, or the
|
|
195
|
+
* managed known-hosts pinning. A caller that has already built those through
|
|
196
|
+
* `buildSshInvocation` needs a way to run them, and duplicating the transport to
|
|
197
|
+
* get it would be the worse answer. So this takes `args`/`env` verbatim and adds
|
|
198
|
+
* only the lifecycle guarantees:
|
|
199
|
+
*
|
|
200
|
+
* - **SIGTERM then SIGKILL.** A timeout or abort that only sends SIGTERM leaks a
|
|
201
|
+
* child that ignores it — ssh does, mid-handshake — and the promise never
|
|
202
|
+
* settles. After {@link SSH_STREAM_KILL_GRACE_MS} the child is SIGKILLed and
|
|
203
|
+
* `killed` says so.
|
|
204
|
+
* - **Bounded stderr.** A peer that writes endlessly to stderr would otherwise
|
|
205
|
+
* grow this buffer without limit; it is capped and `stderrTruncated` says so.
|
|
206
|
+
* - **`onStdout` cannot break the caller's cleanup.** A throw from the consumer
|
|
207
|
+
* is captured and re-thrown by the awaiting caller, so a write failure ends the
|
|
208
|
+
* transfer instead of escaping as an unhandled error inside a stream event.
|
|
209
|
+
*/
|
|
210
|
+
export declare function sshStreamWithArgs(opts: {
|
|
211
|
+
args: string[];
|
|
212
|
+
env?: Record<string, string>;
|
|
213
|
+
onStdout: (chunk: Buffer) => void;
|
|
214
|
+
timeoutMs?: number;
|
|
215
|
+
killGraceMs?: number;
|
|
216
|
+
maxStderrBytes?: number;
|
|
217
|
+
signal?: AbortSignal;
|
|
218
|
+
sshBin?: string;
|
|
219
|
+
}): Promise<SshStreamResult>;
|
|
176
220
|
export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
|
|
177
221
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
178
222
|
export declare function sshReachable(target: string, timeoutMs?: number): boolean;
|