@phnx-labs/agents-cli 1.22.115 → 1.22.116
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 +149 -14
- package/README.md +1 -1
- package/dist/commands/browser.js +11 -0
- package/dist/commands/exec.js +212 -141
- package/dist/commands/feed.js +65 -14
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +157 -3
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +12 -4
- package/dist/commands/ssh.js +1 -69
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.js +2 -2
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +4 -3
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +208 -36
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +58 -3
- package/dist/lib/feed/hub-server.js +306 -54
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +21 -0
- package/dist/lib/hosts/remote-cmd.js +26 -2
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/secrets-cli.d.ts +3 -3
- package/dist/lib/secrets-cli.js +1 -1
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/setup-tool-install.js +2 -1
- package/dist/lib/setup-tool-status.d.ts +1 -1
- package/dist/lib/setup-tool-status.js +6 -1
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/package.json +1 -1
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native-login inventory for `agents apply
|
|
2
|
+
* Native-login inventory for `agents apply`.
|
|
3
3
|
* Native OAuth/session files are identified only to report device readiness;
|
|
4
4
|
* agents-cli never serializes or materializes them on another device.
|
|
5
5
|
*
|
|
@@ -28,7 +28,7 @@ export declare const KEYCHAIN_BOUND_ON_MAC: ReadonlySet<string>;
|
|
|
28
28
|
* collapsing the fleet to a single working login (droid/WorkOS collapsed 10 boxes
|
|
29
29
|
* to 1 overnight — RUSH-1958). Add any newly-discovered single-use-rotation
|
|
30
30
|
* harness here; the predicate below is the one place the propagation decision is
|
|
31
|
-
* made. See also `
|
|
31
|
+
* made. See also `usage.ts` for the per-machine-login policy.
|
|
32
32
|
*/
|
|
33
33
|
export declare const SINGLE_USE_ROTATING_REFRESH_AGENTS: ReadonlySet<string>;
|
|
34
34
|
/**
|
|
@@ -48,59 +48,6 @@ export declare function isCredentialSafeToPropagate(_agent: string): boolean;
|
|
|
48
48
|
export declare function hasPortableAuthFiles(agent: string): boolean;
|
|
49
49
|
/** Which agents `apply` can propagate auth for at all. */
|
|
50
50
|
export declare function isPropagatableAgent(agent: string): boolean;
|
|
51
|
-
/**
|
|
52
|
-
* The shape of a login flow — enough for `agents fleet login` to drive a remote
|
|
53
|
-
* box's device-code OAuth over SSH and scrape the verification URL + user code.
|
|
54
|
-
*
|
|
55
|
-
* `flowType` decides remotability:
|
|
56
|
-
* - `device-code` — the remote box prints a URL + short code the human enters in
|
|
57
|
-
* a browser on THIS machine. The only flow `fleet login` can orchestrate.
|
|
58
|
-
* - `loopback` — the CLI opens a browser and listens on 127.0.0.1 on the
|
|
59
|
-
* remote box; the redirect must reach that box's own loopback, so it can only
|
|
60
|
-
* be completed with a browser on the same machine. Non-remotable.
|
|
61
|
-
* - `api-key` — a pasted key, not an OAuth handshake. Non-remotable.
|
|
62
|
-
* - `unknown` — flow not yet characterized (no captured output). Non-remotable
|
|
63
|
-
* until someone captures the real pattern and fills the regexes.
|
|
64
|
-
*/
|
|
65
|
-
export type LoginFlowType = 'device-code' | 'loopback' | 'api-key' | 'unknown';
|
|
66
|
-
export interface LoginFlow {
|
|
67
|
-
/**
|
|
68
|
-
* The exact command that starts the login flow on the remote box, run as
|
|
69
|
-
* `ssh -tt <box> <loginCommand>`. Bare `<cli>` for agents whose device flow
|
|
70
|
-
* starts on launch (droid, kimi, antigravity), `<cli> login` for
|
|
71
|
-
* codex/grok, `<cli> auth login` for opencode.
|
|
72
|
-
*/
|
|
73
|
-
loginCommand: string;
|
|
74
|
-
flowType: LoginFlowType;
|
|
75
|
-
/**
|
|
76
|
-
* Keystrokes sent after launch to force the device-code path when the CLI
|
|
77
|
-
* presents a menu (codex: pick "Sign in with Device Code") or requires a
|
|
78
|
-
* sub-command inside a TUI (kimi: `/login`). Sent verbatim through the PTY,
|
|
79
|
-
* so it must include the submitting CR (`\r`). Absent when the flow needs no
|
|
80
|
-
* steering.
|
|
81
|
-
*/
|
|
82
|
-
deviceCodeSelect?: string;
|
|
83
|
-
/** Captures the verification URL from scraped login output (group 1 preferred). */
|
|
84
|
-
verificationUrlRegex?: RegExp;
|
|
85
|
-
/** Captures the user code from scraped login output (group 1 preferred). */
|
|
86
|
-
userCodeRegex?: RegExp;
|
|
87
|
-
/**
|
|
88
|
-
* Home-relative credential file that appears / bumps mtime on success — the
|
|
89
|
-
* completion signal. Mirrors {@link FLEET_AUTH_FILES} (first entry for agents
|
|
90
|
-
* with several).
|
|
91
|
-
*/
|
|
92
|
-
successFile: string;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Per-agent login flows, populated from live login-output captures (the only
|
|
96
|
-
* ground truth — no fixtures exist upstream). Agents whose real device-code
|
|
97
|
-
* output has not been captured are marked honestly (`unknown` / no regexes) so
|
|
98
|
-
* `fleet login` flags them non-remotable instead of guessing.
|
|
99
|
-
*
|
|
100
|
-
* Only the `device-code` entries are ones `fleet login` drives; the rest are
|
|
101
|
-
* carried so the command can explain WHY a logged-out pair is not remotable.
|
|
102
|
-
*/
|
|
103
|
-
export declare const FLEET_LOGIN_FLOWS: Record<string, LoginFlow>;
|
|
104
51
|
export interface SnapshotOptions {
|
|
105
52
|
/** Home directory to read credential files from. */
|
|
106
53
|
home: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native-login inventory for `agents apply
|
|
2
|
+
* Native-login inventory for `agents apply`.
|
|
3
3
|
* Native OAuth/session files are identified only to report device readiness;
|
|
4
4
|
* agents-cli never serializes or materializes them on another device.
|
|
5
5
|
*
|
|
@@ -35,7 +35,7 @@ export const KEYCHAIN_BOUND_ON_MAC = new Set(['claude', 'antigravity']);
|
|
|
35
35
|
* collapsing the fleet to a single working login (droid/WorkOS collapsed 10 boxes
|
|
36
36
|
* to 1 overnight — RUSH-1958). Add any newly-discovered single-use-rotation
|
|
37
37
|
* harness here; the predicate below is the one place the propagation decision is
|
|
38
|
-
* made. See also `
|
|
38
|
+
* made. See also `usage.ts` for the per-machine-login policy.
|
|
39
39
|
*/
|
|
40
40
|
export const SINGLE_USE_ROTATING_REFRESH_AGENTS = new Set(['droid']);
|
|
41
41
|
/**
|
|
@@ -61,93 +61,6 @@ export function hasPortableAuthFiles(agent) {
|
|
|
61
61
|
export function isPropagatableAgent(agent) {
|
|
62
62
|
return hasPortableAuthFiles(agent) && isCredentialSafeToPropagate(agent);
|
|
63
63
|
}
|
|
64
|
-
/** The success-file for an agent — the primary portable credential (first spec). */
|
|
65
|
-
function successFileFor(agent) {
|
|
66
|
-
const specs = FLEET_AUTH_FILES[agent];
|
|
67
|
-
if (!specs || specs.length === 0)
|
|
68
|
-
throw new Error(`no auth file spec for agent '${agent}'`);
|
|
69
|
-
return specs[0].rel;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Per-agent login flows, populated from live login-output captures (the only
|
|
73
|
-
* ground truth — no fixtures exist upstream). Agents whose real device-code
|
|
74
|
-
* output has not been captured are marked honestly (`unknown` / no regexes) so
|
|
75
|
-
* `fleet login` flags them non-remotable instead of guessing.
|
|
76
|
-
*
|
|
77
|
-
* Only the `device-code` entries are ones `fleet login` drives; the rest are
|
|
78
|
-
* carried so the command can explain WHY a logged-out pair is not remotable.
|
|
79
|
-
*/
|
|
80
|
-
export const FLEET_LOGIN_FLOWS = {
|
|
81
|
-
// droid: launches its TUI and prints, verbatim:
|
|
82
|
-
// "If the link does not open automatically, please visit
|
|
83
|
-
// https://auth.factory.ai/device and enter code MJQW-NQRM to complete
|
|
84
|
-
// authentication." then "Waiting for authentication to complete...".
|
|
85
|
-
droid: {
|
|
86
|
-
loginCommand: 'droid',
|
|
87
|
-
flowType: 'device-code',
|
|
88
|
-
verificationUrlRegex: /please visit\s+(https:\/\/\S+?)\s+and enter code/i,
|
|
89
|
-
userCodeRegex: /enter code\s+([A-Z0-9]{4}-[A-Z0-9]{4})/i,
|
|
90
|
-
successFile: successFileFor('droid'),
|
|
91
|
-
},
|
|
92
|
-
// codex login shows a numbered menu; option 2 ("Sign in with Device Code",
|
|
93
|
-
// "Sign in from another device with a one-time code") is the remotable one.
|
|
94
|
-
// The post-selection URL/code output was not captured this session, so the
|
|
95
|
-
// regexes are best-effort generic device-code patterns — TODO: tighten once a
|
|
96
|
-
// real codex device-code screen is captured.
|
|
97
|
-
codex: {
|
|
98
|
-
loginCommand: 'codex login',
|
|
99
|
-
flowType: 'device-code',
|
|
100
|
-
deviceCodeSelect: '\x1b[B\r', // down-arrow once (to option 2) + Enter
|
|
101
|
-
verificationUrlRegex: /(https:\/\/\S*(?:device|activate|login|auth)\S*)/i,
|
|
102
|
-
userCodeRegex: /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/,
|
|
103
|
-
successFile: successFileFor('codex'),
|
|
104
|
-
},
|
|
105
|
-
// kimi: launch bare `kimi`, then `/login` inside the TUI, which prints
|
|
106
|
-
// "Select a platform and authenticate". Post-selection URL/code not captured —
|
|
107
|
-
// best-effort regexes, TODO: tighten with a real capture.
|
|
108
|
-
kimi: {
|
|
109
|
-
loginCommand: 'kimi',
|
|
110
|
-
flowType: 'device-code',
|
|
111
|
-
deviceCodeSelect: '/login\r',
|
|
112
|
-
verificationUrlRegex: /(https:\/\/\S+)/i,
|
|
113
|
-
userCodeRegex: /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/,
|
|
114
|
-
successFile: successFileFor('kimi'),
|
|
115
|
-
},
|
|
116
|
-
// grok: `grok login`. No captured pattern this session — flowType is
|
|
117
|
-
// device-code (best guess for the remotable path) but regexes are left unset
|
|
118
|
-
// (TODO: capture grok's login output), so scrapeLogin yields nothing until a
|
|
119
|
-
// real pattern is added and driveRemoteLogin will time out rather than guess.
|
|
120
|
-
grok: {
|
|
121
|
-
loginCommand: 'grok login',
|
|
122
|
-
flowType: 'device-code',
|
|
123
|
-
successFile: successFileFor('grok'),
|
|
124
|
-
},
|
|
125
|
-
// antigravity: bare `agy`, Google OAuth. Keychain-bound on macOS and loopback
|
|
126
|
-
// (browser + 127.0.0.1 listener on the box itself) — non-remotable.
|
|
127
|
-
antigravity: {
|
|
128
|
-
loginCommand: 'agy',
|
|
129
|
-
flowType: 'loopback',
|
|
130
|
-
successFile: successFileFor('antigravity'),
|
|
131
|
-
},
|
|
132
|
-
// opencode: `opencode auth login`. No captured pattern — unknown/non-remotable.
|
|
133
|
-
opencode: {
|
|
134
|
-
loginCommand: 'opencode auth login',
|
|
135
|
-
flowType: 'unknown',
|
|
136
|
-
successFile: successFileFor('opencode'),
|
|
137
|
-
},
|
|
138
|
-
// claude: interactive `/login` inside the TUI is a loopback browser flow, and
|
|
139
|
-
// the token is keychain-bound on macOS. That native login stays per-box and
|
|
140
|
-
// is never copied. The shareable `claude setup-token` mint is a different
|
|
141
|
-
// credential class — minted by `agents accounts add claude [name]` /
|
|
142
|
-
// `agents accounts login claude#<name>` (`lib/auth-mint.ts`), not as a
|
|
143
|
-
// fleet-login device-code flow (a setup-token is account-scoped, not
|
|
144
|
-
// per-machine).
|
|
145
|
-
claude: {
|
|
146
|
-
loginCommand: 'claude',
|
|
147
|
-
flowType: 'loopback',
|
|
148
|
-
successFile: successFileFor('claude'),
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
64
|
/**
|
|
152
65
|
* Capture portable credential files for the given agents from a source home.
|
|
153
66
|
* Returns the readable file payloads plus the list of agents whose auth is
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export const HARNESS_AUTH = {
|
|
2
2
|
claude: { login: ['auth', 'login'], status: ['auth', 'status'], identity: 'strong', worker: 'setup-token', slotEnv: 'CLAUDE_CONFIG_DIR' },
|
|
3
3
|
codex: { login: ['login'], status: ['login', 'status'], identity: 'strong', worker: ['api-key:OPENAI_API_KEY', 'per-device:device-auth'], slotEnv: 'CODEX_HOME' },
|
|
4
|
-
|
|
4
|
+
// `--device-auth` pins the device-code flow: bare `grok login` defaults to
|
|
5
|
+
// `--oauth`, the loopback browser flow, which opens whatever browser profile
|
|
6
|
+
// the OS defaults to. The device-code screen prints the URL + code instead,
|
|
7
|
+
// so the human finishes it in the browser profile they choose — which is
|
|
8
|
+
// what makes it usable over an SSH shell on a worker.
|
|
9
|
+
grok: { login: ['login', '--device-auth'], status: null, identity: 'strong', worker: 'api-key:XAI_API_KEY', slotEnv: 'GROK_HOME' },
|
|
5
10
|
// auth.json has no email claim; identity is the sorted provider-id join
|
|
6
11
|
// (`resolveOpenCodeAccountId`). NATIVE_ACCOUNT_CAPABILITIES.opencode.inspection
|
|
7
12
|
// is already 'opaque'.
|
|
8
13
|
opencode: { login: ['auth', 'login'], status: ['auth', 'list'], identity: 'opaque', worker: 'api-key:provider', slotEnv: 'XDG_DATA_HOME' },
|
|
9
14
|
cursor: { login: ['login'], status: ['status'], identity: 'strong', worker: 'api-key:CURSOR_API_KEY', slotEnv: null },
|
|
10
15
|
// No finite login argv — launch bare `kimi`, then `/login` in the TUI
|
|
11
|
-
// (`loginHint('kimi') === 'kimi'
|
|
16
|
+
// (`loginHint('kimi') === 'kimi'`).
|
|
12
17
|
kimi: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'KIMI_CODE_HOME' },
|
|
13
18
|
antigravity: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
14
19
|
droid: { login: null, status: null, identity: 'opaque', worker: 'api-key:FACTORY_API_KEY', slotEnv: null },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Host } from './types.js';
|
|
2
|
+
import { remoteShellFor } from './remote-cmd.js';
|
|
2
3
|
import { type HostTask } from './tasks.js';
|
|
3
4
|
import { type HostCredentials } from './credentials.js';
|
|
4
5
|
import { deriveMirroredCwd, homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
@@ -31,6 +32,13 @@ export declare function withActorEnv(env?: Record<string, string>): Record<strin
|
|
|
31
32
|
* place.
|
|
32
33
|
*/
|
|
33
34
|
export declare function remoteRunShellPrelude(agent: string, extra?: Record<string, string>): string;
|
|
35
|
+
/**
|
|
36
|
+
* The env every remote `agents run` dispatch carries across the SSH hop —
|
|
37
|
+
* actor provenance, the `run auto` chain-hop guard, and the caller's path
|
|
38
|
+
* markers. `remoteRunShellPrelude` renders it as POSIX exports; a Windows peer
|
|
39
|
+
* gets the same record as `$env:` assignments in its PowerShell script.
|
|
40
|
+
*/
|
|
41
|
+
export declare function remoteRunEnv(agent: string, extra?: Record<string, string>): Record<string, string>;
|
|
34
42
|
/**
|
|
35
43
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
36
44
|
*
|
|
@@ -146,7 +154,7 @@ export interface DispatchOptions {
|
|
|
146
154
|
copyCreds?: HostCredentials;
|
|
147
155
|
}
|
|
148
156
|
export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
|
|
149
|
-
interface InteractiveDispatchOptions {
|
|
157
|
+
export interface InteractiveDispatchOptions {
|
|
150
158
|
agent: string;
|
|
151
159
|
/** Explicit agent version pin (e.g. "2.1.207") to forward as `agent@version`. */
|
|
152
160
|
version?: string;
|
|
@@ -199,6 +207,17 @@ interface InteractiveDispatchOptions {
|
|
|
199
207
|
* infer headless from the prompt).
|
|
200
208
|
*/
|
|
201
209
|
export declare function buildInteractiveRunForwardedArgs(opts: InteractiveDispatchOptions): string[];
|
|
210
|
+
/**
|
|
211
|
+
* The remote command of an interactive host dispatch, by peer shell.
|
|
212
|
+
*
|
|
213
|
+
* A Windows peer's sshd hands the command to PowerShell, which cannot parse the
|
|
214
|
+
* POSIX `export …; { cd … || cd "$HOME"; } && agents …` form (a `run --device
|
|
215
|
+
* <windows box>` from a TTY died with `At line:1 char:420` at the `||`). That
|
|
216
|
+
* peer gets the rendered PowerShell every other `--device` site uses — env,
|
|
217
|
+
* mirrored cwd and argv inside one `-EncodedCommand` — the interactive analogue
|
|
218
|
+
* of `buildWindowsDetachedLaunchCommand`.
|
|
219
|
+
*/
|
|
220
|
+
export declare function buildInteractiveRemoteCommand(remoteShell: ReturnType<typeof remoteShellFor>, opts: InteractiveDispatchOptions): string;
|
|
202
221
|
/**
|
|
203
222
|
* Run an agent interactively on a host, forwarding the local TTY over SSH.
|
|
204
223
|
* Returns the SSH exit code. The remote `agents` CLI is responsible for its own
|
|
@@ -13,7 +13,7 @@ import { randomUUID } from 'crypto';
|
|
|
13
13
|
import { sshExec, sshStream, shellQuote } from '../ssh-exec.js';
|
|
14
14
|
import { hostIdentityArgs, sshTargetFor } from './types.js';
|
|
15
15
|
import { ensureHostReady } from './ready.js';
|
|
16
|
-
import { encodePowershell, powershellQuote, remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
16
|
+
import { buildWindowsAgentsCommand, encodePowershell, powershellQuote, remoteShellFor, posixEnvExports, windowsRemotePath, windowsSetLocation } from './remote-cmd.js';
|
|
17
17
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
18
18
|
import { resolveActor, actorEnv } from '../actor.js';
|
|
19
19
|
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
@@ -85,10 +85,19 @@ export function withActorEnv(env) {
|
|
|
85
85
|
* place.
|
|
86
86
|
*/
|
|
87
87
|
export function remoteRunShellPrelude(agent, extra = {}) {
|
|
88
|
-
const
|
|
89
|
-
const exports = posixEnvExports(withActorEnv({ ...guard, ...extra }));
|
|
88
|
+
const exports = posixEnvExports(remoteRunEnv(agent, extra));
|
|
90
89
|
return exports ? `${exports}; ` : '';
|
|
91
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The env every remote `agents run` dispatch carries across the SSH hop —
|
|
93
|
+
* actor provenance, the `run auto` chain-hop guard, and the caller's path
|
|
94
|
+
* markers. `remoteRunShellPrelude` renders it as POSIX exports; a Windows peer
|
|
95
|
+
* gets the same record as `$env:` assignments in its PowerShell script.
|
|
96
|
+
*/
|
|
97
|
+
export function remoteRunEnv(agent, extra = {}) {
|
|
98
|
+
const guard = agent === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {};
|
|
99
|
+
return withActorEnv({ ...guard, ...extra });
|
|
100
|
+
}
|
|
92
101
|
/**
|
|
93
102
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
94
103
|
*
|
|
@@ -107,21 +116,12 @@ export function buildDetachedLaunchCommand(inner) {
|
|
|
107
116
|
].join(' ');
|
|
108
117
|
return `bash -lc ${shellQuote(`node -e ${shellQuote(nodeScript)}`)}`;
|
|
109
118
|
}
|
|
110
|
-
function windowsRemotePath(path) {
|
|
111
|
-
return path.startsWith('$HOME/')
|
|
112
|
-
? `(Join-Path $HOME ${powershellQuote(path.slice('$HOME/'.length))})`
|
|
113
|
-
: powershellQuote(path);
|
|
114
|
-
}
|
|
115
119
|
/** Build the detached PowerShell launch protocol used by Windows SSH hosts. */
|
|
116
120
|
export function buildWindowsDetachedLaunchCommand(opts) {
|
|
117
121
|
const log = windowsRemotePath(opts.remoteLog);
|
|
118
122
|
const exit = windowsRemotePath(opts.remoteExit);
|
|
119
123
|
const env = Object.entries(opts.env).map(([key, value]) => `$env:${key} = ${powershellQuote(value)}`);
|
|
120
|
-
const cwd = opts.remoteCwd
|
|
121
|
-
? opts.mirrorCwd
|
|
122
|
-
? `try { Set-Location -LiteralPath ${powershellQuote(opts.remoteCwd)} } catch { Set-Location -LiteralPath $HOME }`
|
|
123
|
-
: `Set-Location -LiteralPath ${powershellQuote(opts.remoteCwd)}`
|
|
124
|
-
: '';
|
|
124
|
+
const cwd = opts.remoteCwd ? windowsSetLocation(opts.remoteCwd, opts.mirrorCwd) : '';
|
|
125
125
|
const inner = [
|
|
126
126
|
`$ProgressPreference = 'SilentlyContinue'`,
|
|
127
127
|
`$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'`,
|
|
@@ -273,7 +273,7 @@ async function launchDetached(host, target, opts) {
|
|
|
273
273
|
mirrorCwd: opts.mirrorCwd,
|
|
274
274
|
remoteLog,
|
|
275
275
|
remoteExit,
|
|
276
|
-
env:
|
|
276
|
+
env: remoteRunEnv(opts.agentLabel),
|
|
277
277
|
})
|
|
278
278
|
: `mkdir -p ${REMOTE_DIR}; ${buildDetachedLaunchCommand(inner)}`;
|
|
279
279
|
const res = sshExec(target, launch, {
|
|
@@ -471,19 +471,17 @@ export function buildInteractiveRunForwardedArgs(opts) {
|
|
|
471
471
|
return args;
|
|
472
472
|
}
|
|
473
473
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
474
|
+
* The remote command of an interactive host dispatch, by peer shell.
|
|
475
|
+
*
|
|
476
|
+
* A Windows peer's sshd hands the command to PowerShell, which cannot parse the
|
|
477
|
+
* POSIX `export …; { cd … || cd "$HOME"; } && agents …` form (a `run --device
|
|
478
|
+
* <windows box>` from a TTY died with `At line:1 char:420` at the `||`). That
|
|
479
|
+
* peer gets the rendered PowerShell every other `--device` site uses — env,
|
|
480
|
+
* mirrored cwd and argv inside one `-EncodedCommand` — the interactive analogue
|
|
481
|
+
* of `buildWindowsDetachedLaunchCommand`.
|
|
477
482
|
*/
|
|
478
|
-
export
|
|
479
|
-
const
|
|
480
|
-
// Concrete version pins fail loud here (RUSH-2313) so we never open a TTY
|
|
481
|
-
// to a box that cannot run the pin.
|
|
482
|
-
const { warnings } = ensureHostReady(host, { agent: opts.agent, version: opts.version });
|
|
483
|
-
for (const w of warnings)
|
|
484
|
-
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
485
|
-
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
486
|
-
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
483
|
+
export function buildInteractiveRemoteCommand(remoteShell, opts) {
|
|
484
|
+
const forwardedArgs = buildInteractiveRunForwardedArgs(opts);
|
|
487
485
|
// Forward actor provenance so the interactive remote run inherits it rather
|
|
488
486
|
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028); a `run auto`
|
|
489
487
|
// dispatch also gets the chain-hop guard (remoteRunShellPrelude).
|
|
@@ -496,11 +494,35 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
496
494
|
// longer forces the tmux wrap on a TTY-followed run — the peer's
|
|
497
495
|
// tmux.enabled decides that. Set here, on the interactive path only:
|
|
498
496
|
// `launchDetached` already setsids the headless one.
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
497
|
+
const env = { [REMOTE_INTERACTIVE_ENV]: '1' };
|
|
498
|
+
if (remoteShell === 'powershell') {
|
|
499
|
+
if (opts.copyCreds)
|
|
500
|
+
throw new Error('--copy-creds cannot ride an interactive dispatch to a Windows host');
|
|
501
|
+
return buildWindowsAgentsCommand({
|
|
502
|
+
args: forwardedArgs,
|
|
503
|
+
env: remoteRunEnv(opts.agent, env),
|
|
504
|
+
cwd: opts.remoteCwd,
|
|
505
|
+
mirrorCwd: opts.mirrorCwd,
|
|
506
|
+
});
|
|
503
507
|
}
|
|
508
|
+
const invocation = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
509
|
+
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
510
|
+
const remoteCmd = `${remoteRunShellPrelude(opts.agent, env)}${cwd}${invocation}`;
|
|
511
|
+
return opts.copyCreds ? wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds) : remoteCmd;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Run an agent interactively on a host, forwarding the local TTY over SSH.
|
|
515
|
+
* Returns the SSH exit code. The remote `agents` CLI is responsible for its own
|
|
516
|
+
* tmux wrapping; the local machine is just the transport.
|
|
517
|
+
*/
|
|
518
|
+
export async function runInteractiveOnHost(host, opts) {
|
|
519
|
+
const target = sshTargetFor(host);
|
|
520
|
+
// Concrete version pins fail loud here (RUSH-2313) so we never open a TTY
|
|
521
|
+
// to a box that cannot run the pin.
|
|
522
|
+
const { warnings } = ensureHostReady(host, { agent: opts.agent, version: opts.version });
|
|
523
|
+
for (const w of warnings)
|
|
524
|
+
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
525
|
+
const remoteCmd = buildInteractiveRemoteCommand(remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)), opts);
|
|
504
526
|
// Credentials ride this stream when --copy-creds is set: verify the host key
|
|
505
527
|
// strictly against the managed pin and force a fresh connection so a stale
|
|
506
528
|
// accept-new control socket can't bypass the check (RUSH-1767).
|
|
@@ -112,6 +112,12 @@ interface WindowsAgentsCommand {
|
|
|
112
112
|
env?: Record<string, string>;
|
|
113
113
|
/** Directory to enter before running (`--remote-cwd`). */
|
|
114
114
|
cwd?: string;
|
|
115
|
+
/**
|
|
116
|
+
* `cwd` was DERIVED from the local cwd rather than named by the user (see
|
|
117
|
+
* `deriveMirroredCwd`): a directory the peer does not have falls back to
|
|
118
|
+
* `$HOME` instead of failing the run, exactly as `remoteCdPrefix({ mirror })`.
|
|
119
|
+
*/
|
|
120
|
+
mirrorCwd?: boolean;
|
|
115
121
|
/**
|
|
116
122
|
* Append `exit $LASTEXITCODE` so a native `agents` exit code propagates out
|
|
117
123
|
* through `powershell.exe` (which otherwise exits 0 regardless). Default true;
|
|
@@ -188,6 +194,21 @@ export declare function stripClixml(stdout: string): string;
|
|
|
188
194
|
* with it.
|
|
189
195
|
*/
|
|
190
196
|
export declare function windowsAgentsInvocation(args: string[], binName?: 'agents' | 'ag'): string;
|
|
197
|
+
/**
|
|
198
|
+
* PowerShell expression for a remote path. A home-anchored path (`~/x`,
|
|
199
|
+
* `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
|
|
200
|
+
* counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
|
|
201
|
+
* another box resolves on the peer. Any other path is quoted verbatim.
|
|
202
|
+
*/
|
|
203
|
+
export declare function windowsRemotePath(p: string): string;
|
|
204
|
+
/**
|
|
205
|
+
* The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
|
|
206
|
+
* spelled out because a missing directory is a NON-terminating error for
|
|
207
|
+
* `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
|
|
208
|
+
* form must genuinely land in `$HOME` when the peer lacks the checkout, and the
|
|
209
|
+
* explicit form must abort rather than run the command in the wrong directory.
|
|
210
|
+
*/
|
|
211
|
+
export declare function windowsSetLocation(cwd: string, mirror?: boolean): string;
|
|
191
212
|
export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
|
|
192
213
|
/**
|
|
193
214
|
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { shellQuote } from '../ssh-exec.js';
|
|
11
11
|
import { pwshLiteral, pwshNativeExecStatements } from '../pwsh.js';
|
|
12
12
|
import { quoteWin32ExecArg } from '../platform/exec.js';
|
|
13
|
+
import { homeRemainder } from '../project-root.js';
|
|
13
14
|
import * as zlib from 'node:zlib';
|
|
14
15
|
/**
|
|
15
16
|
* Remove routing flags (and their values) from a command's args, leaving the
|
|
@@ -363,8 +364,31 @@ export function windowsAgentsInvocation(args, binName = 'agents') {
|
|
|
363
364
|
// would produce invalid PowerShell.
|
|
364
365
|
].join('\n');
|
|
365
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* PowerShell expression for a remote path. A home-anchored path (`~/x`,
|
|
369
|
+
* `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
|
|
370
|
+
* counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
|
|
371
|
+
* another box resolves on the peer. Any other path is quoted verbatim.
|
|
372
|
+
*/
|
|
373
|
+
export function windowsRemotePath(p) {
|
|
374
|
+
const rest = homeRemainder(p);
|
|
375
|
+
if (rest === null)
|
|
376
|
+
return powershellQuote(p);
|
|
377
|
+
return rest === '' ? '$HOME' : `(Join-Path $HOME ${powershellQuote(rest)})`;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
|
|
381
|
+
* spelled out because a missing directory is a NON-terminating error for
|
|
382
|
+
* `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
|
|
383
|
+
* form must genuinely land in `$HOME` when the peer lacks the checkout, and the
|
|
384
|
+
* explicit form must abort rather than run the command in the wrong directory.
|
|
385
|
+
*/
|
|
386
|
+
export function windowsSetLocation(cwd, mirror = false) {
|
|
387
|
+
const enter = `Set-Location -LiteralPath ${windowsRemotePath(cwd)} -ErrorAction Stop`;
|
|
388
|
+
return mirror ? `try { ${enter} } catch { Set-Location -LiteralPath $HOME }` : enter;
|
|
389
|
+
}
|
|
366
390
|
export function windowsAgentsScript(cmd) {
|
|
367
|
-
const { args, env, cwd, propagateExit = true, remapExit255 = false } = cmd;
|
|
391
|
+
const { args, env, cwd, mirrorCwd = false, propagateExit = true, remapExit255 = false } = cmd;
|
|
368
392
|
// `Stop` FIRST, before the env assignments and the Set-Location: a failing
|
|
369
393
|
// `Set-Location` (a cwd that does not exist on the peer) must abort rather than
|
|
370
394
|
// continue into the launcher and run the command in the wrong directory.
|
|
@@ -373,7 +397,7 @@ export function windowsAgentsScript(cmd) {
|
|
|
373
397
|
for (const [k, v] of Object.entries(env))
|
|
374
398
|
parts.push(`$env:${k} = ${powershellQuote(v)}`);
|
|
375
399
|
if (cwd)
|
|
376
|
-
parts.push(
|
|
400
|
+
parts.push(windowsSetLocation(cwd, mirrorCwd));
|
|
377
401
|
parts.push(windowsAgentsInvocation(args));
|
|
378
402
|
if (propagateExit) {
|
|
379
403
|
if (remapExit255)
|
package/dist/lib/mailbox.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface MailboxMessage {
|
|
|
22
22
|
* receipts back to the feed store.
|
|
23
23
|
*/
|
|
24
24
|
blockId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Which ASK of that block this message answers, and which claim sent it. A
|
|
27
|
+
* blockId is per SESSION and is reused by every generation of that session's
|
|
28
|
+
* questions, so without these a `consumed` receipt raised on drain cannot say
|
|
29
|
+
* WHICH question it acknowledges -- and a late one for question N would
|
|
30
|
+
* resolve question N+1 (PHNX-3999). Carried on the message so the binding
|
|
31
|
+
* survives the process that enqueued it.
|
|
32
|
+
*/
|
|
33
|
+
generation?: string;
|
|
34
|
+
attempt?: string;
|
|
25
35
|
/**
|
|
26
36
|
* Drop reason when a message is archived without delivery (expired, dead box, etc.).
|
|
27
37
|
* Set by the TTL/liveness layer, not by writers.
|
|
@@ -49,6 +59,8 @@ export declare function enqueue(boxDir: string, msg: {
|
|
|
49
59
|
text: string;
|
|
50
60
|
from?: string;
|
|
51
61
|
blockId?: string;
|
|
62
|
+
generation?: string;
|
|
63
|
+
attempt?: string;
|
|
52
64
|
ttlSeconds?: number;
|
|
53
65
|
}): string;
|
|
54
66
|
/** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
|
package/dist/lib/mailbox.js
CHANGED
|
@@ -97,6 +97,8 @@ export function enqueue(boxDir, msg) {
|
|
|
97
97
|
ts: now.toISOString(),
|
|
98
98
|
text: msg.text,
|
|
99
99
|
blockId: msg.blockId,
|
|
100
|
+
...(msg.generation !== undefined ? { generation: msg.generation } : {}),
|
|
101
|
+
...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
|
|
100
102
|
};
|
|
101
103
|
const ttlSeconds = msg.ttlSeconds ?? resolveDefaultTtlSeconds();
|
|
102
104
|
if (ttlSeconds > 0) {
|
|
@@ -128,7 +130,14 @@ export function readMessage(file) {
|
|
|
128
130
|
if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
|
|
129
131
|
return null;
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
// Field-by-field on purpose (an unknown key from a foreign writer is dropped),
|
|
134
|
+
// so EVERY field the receipt binding depends on must be listed here or it is
|
|
135
|
+
// silently lost on read and the binding it enables never fires.
|
|
136
|
+
return {
|
|
137
|
+
msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text,
|
|
138
|
+
expiresAt: m.expiresAt, blockId: m.blockId, generation: m.generation, attempt: m.attempt,
|
|
139
|
+
dropped: m.dropped,
|
|
140
|
+
};
|
|
132
141
|
}
|
|
133
142
|
/** True when a message has a parsed expiry in the past. */
|
|
134
143
|
export function isExpired(msg, now = new Date()) {
|
|
@@ -230,7 +239,12 @@ function consumeClaimed(boxDir, name, expectedTo) {
|
|
|
230
239
|
if (msg.blockId) {
|
|
231
240
|
try {
|
|
232
241
|
const feedRoot = process.env.AGENTS_FEED_DIR;
|
|
233
|
-
|
|
242
|
+
// The origin rides the message, so the receipt names the ask it answers.
|
|
243
|
+
recordMessageReceipt(msg.blockId, {
|
|
244
|
+
msgId: msg.msgId, status: 'consumed', at: new Date().toISOString(), from: msg.from,
|
|
245
|
+
...(msg.generation !== undefined ? { generation: msg.generation } : {}),
|
|
246
|
+
...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
|
|
247
|
+
}, feedRoot);
|
|
234
248
|
}
|
|
235
249
|
catch {
|
|
236
250
|
// Receipt surfacing is best-effort; never stall delivery.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DeviceStats } from '../devices/health.js';
|
|
1
2
|
import type { WatchdogTickResult } from '../watchdog/runner.js';
|
|
2
3
|
/**
|
|
3
4
|
* One registered fleet device, for the menu-bar's collapsible DEVICES section.
|
|
@@ -20,6 +21,47 @@ interface MenubarDevice {
|
|
|
20
21
|
interactive: boolean;
|
|
21
22
|
isLocal: boolean;
|
|
22
23
|
preferred: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The operator's role mark — `worker` | `personal` | `desktop`, or `unknown`
|
|
26
|
+
* when the device was never marked (PHNX-3999 F25). Read from the same
|
|
27
|
+
* device-scope config `--device auto` consults, so the menu shows the fact that
|
|
28
|
+
* governs placement rather than a second notion of it.
|
|
29
|
+
*/
|
|
30
|
+
role: string;
|
|
31
|
+
/**
|
|
32
|
+
* True when automatic placement (`agents run --device auto`) may pick this
|
|
33
|
+
* device: the verdict of the ONE canonical pool filter, so a `personal` box the
|
|
34
|
+
* user sits at reads as ineligible here exactly as it behaves there.
|
|
35
|
+
*/
|
|
36
|
+
autoEligible: boolean;
|
|
37
|
+
/** Hardware facts and the current reading, or null when never observed. */
|
|
38
|
+
stats: MenubarDeviceStats | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One device's hardware facts and last reading, projected from the fleet-stats
|
|
42
|
+
* cache the CLI already keeps (`~/.agents/.cache/.fleet-stats.json`).
|
|
43
|
+
*
|
|
44
|
+
* Cache-only by construction: the snapshot never probes the fleet, so opening
|
|
45
|
+
* Settings costs nothing and a device nobody has measured says so. Every value
|
|
46
|
+
* is `null` when that number was not observed — never 0, which would render as a
|
|
47
|
+
* real "idle, empty disk" reading. `stale` carries the CLI's own freshness bound
|
|
48
|
+
* so the menu can label an old reading instead of showing it as live, and
|
|
49
|
+
* `observedAt` / `specsObservedAt` give the observation age F25 asks for
|
|
50
|
+
* (hardware totals survive an unreachable probe; current-state readings do not).
|
|
51
|
+
*/
|
|
52
|
+
interface MenubarDeviceStats {
|
|
53
|
+
reachable: boolean;
|
|
54
|
+
observedAt: string;
|
|
55
|
+
stale: boolean;
|
|
56
|
+
cpus: number | null;
|
|
57
|
+
memTotalBytes: number | null;
|
|
58
|
+
memFreeBytes: number | null;
|
|
59
|
+
memPercent: number | null;
|
|
60
|
+
diskTotalBytes: number | null;
|
|
61
|
+
diskFreeBytes: number | null;
|
|
62
|
+
diskUsedPercent: number | null;
|
|
63
|
+
loadPercent: number | null;
|
|
64
|
+
specsObservedAt: string | null;
|
|
23
65
|
}
|
|
24
66
|
interface MenubarSnapshot {
|
|
25
67
|
version: 1;
|
|
@@ -55,6 +97,15 @@ interface MenubarSnapshot {
|
|
|
55
97
|
* (only `defaultProject`) is omitted so the native app keeps its own default.
|
|
56
98
|
*/
|
|
57
99
|
export declare function buildMenuPreferences(): Record<string, unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
|
|
102
|
+
* when the device has no cached reading at all.
|
|
103
|
+
*
|
|
104
|
+
* `?? null` rather than a default: an absent number means "not observed", and the
|
|
105
|
+
* menu renders that as unavailable. A 0 would be indistinguishable from a real
|
|
106
|
+
* measurement of an idle box or a full disk.
|
|
107
|
+
*/
|
|
108
|
+
export declare function projectDeviceStats(row: DeviceStats | undefined): MenubarDeviceStats | null;
|
|
58
109
|
export declare function readLastWatchdogTick(stateDir?: string): WatchdogTickResult | null;
|
|
59
110
|
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
60
111
|
export declare function computeMenubarSnapshot(): Promise<MenubarSnapshot>;
|