@phnx-labs/agents-cli 1.22.68 → 1.22.70
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 +46 -0
- package/README.md +17 -14
- package/dist/bootstrap.js +3 -0
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +33 -23
- package/dist/commands/feed.js +20 -7
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +35 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/ipc.js +9 -1
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/daemon.js +28 -8
- package/dist/lib/daemon/runner.js +6 -2
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +7 -34
- package/dist/lib/feed-broadcast.d.ts +47 -5
- package/dist/lib/feed-broadcast.js +127 -24
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +55 -0
- package/dist/lib/owner-message.js +64 -0
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core agent configuration and detection module.
|
|
3
3
|
*
|
|
4
4
|
* Defines the canonical registry of current and legacy AI coding agents (Claude,
|
|
5
|
-
* Codex, Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp,
|
|
5
|
+
* Codex, Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Goose, Grok)
|
|
6
6
|
* with their CLI commands, config paths, capability flags, and MCP integration
|
|
7
7
|
* points.
|
|
8
8
|
*
|
|
@@ -413,52 +413,6 @@ export const AGENTS = {
|
|
|
413
413
|
supportsHooks: true,
|
|
414
414
|
capabilities: { hooks: { since: '0.3.130' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '1.1.1' }, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'], interactiveRepl: true },
|
|
415
415
|
},
|
|
416
|
-
// Oh My Pi (`omp`, omp.sh) — a Bun-based, terminal-first coding agent that runs
|
|
417
|
-
// against many model providers (OpenRouter, OpenAI, Anthropic, xAI, DeepSeek,
|
|
418
|
-
// Ollama, LM Studio, …). It is Claude-compatible: it natively discovers
|
|
419
|
-
// `.claude/commands`, `.mcp.json`, and Claude-shaped subagents, and keeps its
|
|
420
|
-
// own native resources under `~/.omp/agent/` (config dir reported by
|
|
421
|
-
// `omp config path`). configDir points AT the agent dir (not `~/.omp`) so the
|
|
422
|
-
// rules file lands at `~/.omp/agent/AGENTS.md`, the user context file omp reads.
|
|
423
|
-
pi: {
|
|
424
|
-
id: 'pi',
|
|
425
|
-
name: 'Pi',
|
|
426
|
-
sessionDir: null,
|
|
427
|
-
sessionFileExt: null,
|
|
428
|
-
versionStdoutMatch: 'semver',
|
|
429
|
-
unmanagedBinary: 'path',
|
|
430
|
-
mcpRegister: 'cli',
|
|
431
|
-
mcpAddHttp: 'transport',
|
|
432
|
-
mcpAddStdio: 'simple',
|
|
433
|
-
mcpConfigWrite: 'json-mcpServers',
|
|
434
|
-
color: 'magenta',
|
|
435
|
-
cliCommand: 'omp',
|
|
436
|
-
npmPackage: '@oh-my-pi/pi-coding-agent',
|
|
437
|
-
configDir: path.join(HOME, '.omp', 'agent'),
|
|
438
|
-
commandsDir: path.join(HOME, '.omp', 'agent', 'commands'),
|
|
439
|
-
commandsSubdir: 'commands',
|
|
440
|
-
skillsDir: path.join(HOME, '.omp', 'agent', 'skills'),
|
|
441
|
-
hooksDir: 'hooks',
|
|
442
|
-
instructionsFile: 'AGENTS.md',
|
|
443
|
-
format: 'markdown',
|
|
444
|
-
variableSyntax: '$ARGUMENTS',
|
|
445
|
-
// omp hooks are per-tool JS/TS extension modules discovered from
|
|
446
|
-
// `~/.omp/agent/hooks/{pre,post}/<tool>.<ext>` (loaded as HookFactory code),
|
|
447
|
-
// NOT the event->shell-command registrations agents-cli's hook sync writes.
|
|
448
|
-
// The two models don't map, so hooks stay off (capabilities.hooks:false).
|
|
449
|
-
supportsHooks: false,
|
|
450
|
-
// MCP: omp reads `.mcp.json` with the Claude `{ "mcpServers": {...} }` schema
|
|
451
|
-
// at user scope (`~/.omp/agent/.mcp.json`) and project scope (`<root>/.mcp.json`),
|
|
452
|
-
// stdio + http + sse with headers. skills (`~/.omp/agent/skills/<name>/SKILL.md`),
|
|
453
|
-
// commands (`~/.omp/agent/commands/*.md`), and subagents (`~/.omp/agent/agents/*.md`,
|
|
454
|
-
// Claude-shaped) are all native. allowlist is OFF: omp gates approval per-TOOL
|
|
455
|
-
// only (`tools.approval` record: allow|prompt|deny) with no command/path/domain
|
|
456
|
-
// patterns, so agents-cli's granular permission format has nothing to map to.
|
|
457
|
-
// plugins are npm packages / TS modules, not the Claude marketplace manifest.
|
|
458
|
-
// interactiveRepl: true — bare `omp` runs the TUI; `omp -p` is the one-shot
|
|
459
|
-
// form that answers a prompt and exits (upstream README, "Four entry points").
|
|
460
|
-
capabilities: { hooks: false, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
|
|
461
|
-
},
|
|
462
416
|
openclaw: {
|
|
463
417
|
id: 'openclaw',
|
|
464
418
|
name: 'OpenClaw',
|
|
@@ -560,39 +514,6 @@ export const AGENTS = {
|
|
|
560
514
|
// interactiveRepl: false — amp requires a prompt; bare invocation exits immediately.
|
|
561
515
|
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'], interactiveRepl: false },
|
|
562
516
|
},
|
|
563
|
-
kiro: {
|
|
564
|
-
id: 'kiro',
|
|
565
|
-
name: 'Kiro',
|
|
566
|
-
sessionDir: null,
|
|
567
|
-
sessionFileExt: null,
|
|
568
|
-
versionStdoutMatch: 'semver',
|
|
569
|
-
unmanagedBinary: 'path',
|
|
570
|
-
mcpRegister: 'cli',
|
|
571
|
-
mcpAddHttp: 'transport',
|
|
572
|
-
mcpAddStdio: 'simple',
|
|
573
|
-
mcpConfigWrite: 'json-mcpServers',
|
|
574
|
-
color: 'greenBright',
|
|
575
|
-
cliCommand: 'kiro-cli',
|
|
576
|
-
npmPackage: '',
|
|
577
|
-
installScript: 'brew install --cask kiro-cli',
|
|
578
|
-
configDir: path.join(HOME, '.kiro'),
|
|
579
|
-
commandsDir: path.join(HOME, '.kiro', 'commands'),
|
|
580
|
-
commandsSubdir: 'commands',
|
|
581
|
-
skillsDir: path.join(HOME, '.kiro', 'skills'),
|
|
582
|
-
// Hooks: v3 standalone files under ~/.kiro/hooks/*.json
|
|
583
|
-
// (`{ "version": "v1", "hooks": [...] }`). Fixed PreToolUse/PostToolUse
|
|
584
|
-
// firing in kiro-cli 0.10; fully stable by 2.6.1. Launch always passes
|
|
585
|
-
// --v3 (see AGENT_COMMANDS.kiro) so the standalone files actually load.
|
|
586
|
-
// See registerHooksForKiro.
|
|
587
|
-
hooksDir: 'hooks',
|
|
588
|
-
instructionsFile: 'AGENTS.md',
|
|
589
|
-
format: 'markdown',
|
|
590
|
-
variableSyntax: '$ARGUMENTS',
|
|
591
|
-
supportsHooks: true,
|
|
592
|
-
// interactiveRepl: false — kiro-cli requires a prompt for coding sessions; bare
|
|
593
|
-
// invocation does not open a persistent REPL.
|
|
594
|
-
capabilities: { hooks: { since: '0.10.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '2.8.0' }, skills: true, commands: true, plugins: false, subagents: { since: '1.23.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'], interactiveRepl: false },
|
|
595
|
-
},
|
|
596
517
|
goose: {
|
|
597
518
|
id: 'goose',
|
|
598
519
|
name: 'Goose',
|
|
@@ -1117,7 +1038,7 @@ export const MANAGED_AGENT_IDS = ALL_AGENT_IDS.filter((id) => !AGENTS[id].deprec
|
|
|
1117
1038
|
* `curl … | sh` / `brew install` script that carries NO version token — the
|
|
1118
1039
|
* installer can only ever fetch the *current* release, and the binary then keeps
|
|
1119
1040
|
* itself up to date in place (droid, grok, antigravity, cursor, hermes,
|
|
1120
|
-
*
|
|
1041
|
+
* goose). There is no semver to pin, so agents-cli must not model these as
|
|
1121
1042
|
* having multiple installable version-homes the way it does for npm-packaged
|
|
1122
1043
|
* agents (claude, codex, kimi, …).
|
|
1123
1044
|
*
|
|
@@ -3029,8 +2950,6 @@ export const AGENT_NAME_ALIASES = {
|
|
|
3029
2950
|
gh: 'copilot',
|
|
3030
2951
|
amp: 'amp',
|
|
3031
2952
|
sourcegraph: 'amp',
|
|
3032
|
-
kiro: 'kiro',
|
|
3033
|
-
'kiro-cli': 'kiro',
|
|
3034
2953
|
goose: 'goose',
|
|
3035
2954
|
'block-goose': 'goose',
|
|
3036
2955
|
antigravity: 'antigravity',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** True when `AGENTS_PROFILE_BOOT` is set — callers can skip building label strings. */
|
|
2
|
+
export declare function bootProfileEnabled(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Record a named stage boundary. No-op unless `AGENTS_PROFILE_BOOT` is set, so
|
|
5
|
+
* this is free to call unconditionally on the launch path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bootMark(label: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Print the collected timeline to stderr, once. Called right before the harness
|
|
10
|
+
* child is spawned (the end of the pre-exec window) and again as an
|
|
11
|
+
* `process.on('exit')` backstop for launch paths that error out before spawn.
|
|
12
|
+
* `reason` labels the final boundary (e.g. `spawn`, `exit`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function flushBootProfile(reason: string): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time profiler for the `agents run` pre-exec phase (PHNX-3585).
|
|
3
|
+
*
|
|
4
|
+
* The AGI EXT "New Claude" boot spends the whole `agents run` wrapper cost
|
|
5
|
+
* BEFORE the harness prints anything — version resolution, account rotation,
|
|
6
|
+
* config sync, login preflight. This module makes that window measurable
|
|
7
|
+
* without a debugger: gate it on `AGENTS_PROFILE_BOOT=1` and the run path
|
|
8
|
+
* stamps named marks, then flushes a per-stage timeline to stderr the instant
|
|
9
|
+
* before the child is spawned (the moment the wrapper's work ends).
|
|
10
|
+
*
|
|
11
|
+
* All marks are `performance.now()` values, which Node measures from
|
|
12
|
+
* `performance.timeOrigin` ≈ process start — so a mark's absolute value is
|
|
13
|
+
* "ms since the `agents` process began", and consecutive marks give the cost
|
|
14
|
+
* of each stage. When the env flag is off, every function here is a couple of
|
|
15
|
+
* cheap branches and pushes nothing, so it is safe to leave wired on the hot
|
|
16
|
+
* path (the committed `scripts/bench-boot.sh` benchmark drives it).
|
|
17
|
+
*/
|
|
18
|
+
import { performance } from 'node:perf_hooks';
|
|
19
|
+
const ENABLED = process.env.AGENTS_PROFILE_BOOT === '1' || process.env.AGENTS_PROFILE_BOOT === 'true';
|
|
20
|
+
const marks = [];
|
|
21
|
+
let flushed = false;
|
|
22
|
+
/** True when `AGENTS_PROFILE_BOOT` is set — callers can skip building label strings. */
|
|
23
|
+
export function bootProfileEnabled() {
|
|
24
|
+
return ENABLED;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Record a named stage boundary. No-op unless `AGENTS_PROFILE_BOOT` is set, so
|
|
28
|
+
* this is free to call unconditionally on the launch path.
|
|
29
|
+
*/
|
|
30
|
+
export function bootMark(label) {
|
|
31
|
+
if (!ENABLED)
|
|
32
|
+
return;
|
|
33
|
+
marks.push({ label, at: performance.now() });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Print the collected timeline to stderr, once. Called right before the harness
|
|
37
|
+
* child is spawned (the end of the pre-exec window) and again as an
|
|
38
|
+
* `process.on('exit')` backstop for launch paths that error out before spawn.
|
|
39
|
+
* `reason` labels the final boundary (e.g. `spawn`, `exit`).
|
|
40
|
+
*/
|
|
41
|
+
export function flushBootProfile(reason) {
|
|
42
|
+
if (!ENABLED || flushed)
|
|
43
|
+
return;
|
|
44
|
+
flushed = true;
|
|
45
|
+
bootMark(reason);
|
|
46
|
+
if (marks.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const start = 0; // process start
|
|
49
|
+
const end = marks[marks.length - 1].at;
|
|
50
|
+
const width = Math.max(...marks.map((m) => m.label.length));
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`[boot-profile] pre-exec timeline (total ${(end - start).toFixed(1)}ms since process start)`);
|
|
53
|
+
let prev = start;
|
|
54
|
+
for (const m of marks) {
|
|
55
|
+
const delta = m.at - prev;
|
|
56
|
+
prev = m.at;
|
|
57
|
+
lines.push(` ${m.label.padEnd(width)} +${delta.toFixed(1).padStart(7)}ms @${m.at.toFixed(1).padStart(8)}ms`);
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
60
|
+
}
|
|
61
|
+
// Backstop: a run that exits before reaching the spawn (a login dead-end, a
|
|
62
|
+
// missing install) still emits whatever stages it reached, so the profile is
|
|
63
|
+
// never silently empty.
|
|
64
|
+
if (ENABLED) {
|
|
65
|
+
process.on('exit', () => flushBootProfile('exit'));
|
|
66
|
+
}
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -736,7 +736,15 @@ export class BrowserIPCServer {
|
|
|
736
736
|
};
|
|
737
737
|
}
|
|
738
738
|
if (request.profile) {
|
|
739
|
-
|
|
739
|
+
try {
|
|
740
|
+
await this.service.stopProfile(request.profile, {
|
|
741
|
+
fleetRemote: request.fleetRemote,
|
|
742
|
+
actor: request.actor,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
747
|
+
}
|
|
740
748
|
return { ok: true };
|
|
741
749
|
}
|
|
742
750
|
return {
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
* The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
|
|
17
17
|
* inside the browser daemon at the top of `resolveOrCreateTask` — the one
|
|
18
18
|
* chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
|
|
19
|
-
* for the task-less `browser start` command
|
|
19
|
+
* for the task-less `browser start` command and `BrowserService.stopProfile` for
|
|
20
|
+
* the task-less `browser stop --profile` command. It has to live there because ~18
|
|
20
21
|
* page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
|
|
21
22
|
* to a browser implicitly, and gating only the `browser start` command left every
|
|
22
23
|
* one of them ungated; gating only the create branch of `resolveOrCreateTask` left
|
|
23
|
-
* the attach paths ungated (RUSH-3064)
|
|
24
|
-
*
|
|
24
|
+
* the attach paths ungated (RUSH-3064); `stop --profile` is handled by `bindTask`'s
|
|
25
|
+
* early return before `resolveOrCreateTask` ever runs, so it needed its own gate
|
|
26
|
+
* call too (PHNX-3317). {@link assertRemoteControlAllowed} remains as a fast-fail
|
|
27
|
+
* CLI-side check so a refused `start` never auto-creates a profile.
|
|
25
28
|
*/
|
|
26
29
|
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation` and `markFleetRemote`. */
|
|
27
30
|
export declare const FLEET_REMOTE_ENV = "AGENTS_FLEET_REMOTE";
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
* The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
|
|
17
17
|
* inside the browser daemon at the top of `resolveOrCreateTask` — the one
|
|
18
18
|
* chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
|
|
19
|
-
* for the task-less `browser start` command
|
|
19
|
+
* for the task-less `browser start` command and `BrowserService.stopProfile` for
|
|
20
|
+
* the task-less `browser stop --profile` command. It has to live there because ~18
|
|
20
21
|
* page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
|
|
21
22
|
* to a browser implicitly, and gating only the `browser start` command left every
|
|
22
23
|
* one of them ungated; gating only the create branch of `resolveOrCreateTask` left
|
|
23
|
-
* the attach paths ungated (RUSH-3064)
|
|
24
|
-
*
|
|
24
|
+
* the attach paths ungated (RUSH-3064); `stop --profile` is handled by `bindTask`'s
|
|
25
|
+
* early return before `resolveOrCreateTask` ever runs, so it needed its own gate
|
|
26
|
+
* call too (PHNX-3317). {@link assertRemoteControlAllowed} remains as a fast-fail
|
|
27
|
+
* CLI-side check so a refused `start` never auto-creates a profile.
|
|
25
28
|
*/
|
|
26
29
|
import { getConfigValue } from '../device-config.js';
|
|
27
30
|
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation` and `markFleetRemote`. */
|
|
@@ -263,7 +263,10 @@ export declare class BrowserService {
|
|
|
263
263
|
ok: boolean;
|
|
264
264
|
profile?: string;
|
|
265
265
|
}>;
|
|
266
|
-
stopProfile(profileRef: ProfileName | ConnectionKey
|
|
266
|
+
stopProfile(profileRef: ProfileName | ConnectionKey, opts?: {
|
|
267
|
+
fleetRemote?: boolean;
|
|
268
|
+
actor?: string;
|
|
269
|
+
}): Promise<void>;
|
|
267
270
|
navigate(taskId: string, url: string, profileRef?: ProfileName | ConnectionKey): Promise<{
|
|
268
271
|
tabId: string;
|
|
269
272
|
url: string;
|
|
@@ -773,7 +773,13 @@ export class BrowserService {
|
|
|
773
773
|
async done(taskName) {
|
|
774
774
|
return this.stop(taskName);
|
|
775
775
|
}
|
|
776
|
-
async stopProfile(profileRef) {
|
|
776
|
+
async stopProfile(profileRef, opts = {}) {
|
|
777
|
+
// Consent gate — this is a fleet-remote destructive path (kills the
|
|
778
|
+
// profile's browser process and clears its runtime dir) that reached the
|
|
779
|
+
// daemon without ever hitting resolveOrCreateTask's gate, since it is a
|
|
780
|
+
// task-less stop. Same per-request marker rule as every other gated verb —
|
|
781
|
+
// see remote-control.ts.
|
|
782
|
+
assertRemoteControlAllowedForRequest(opts.fleetRemote, { actor: opts.actor });
|
|
777
783
|
// Connections are keyed by the runtime key `<profile>@<device>` (see
|
|
778
784
|
// start()) while callers pass the bare profile name (or, occasionally, an
|
|
779
785
|
// exact key). A plain `connections.get(profileRef)` therefore missed every
|
|
@@ -2,10 +2,11 @@ import type { ChannelProvider } from '../registry.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Split one message into the notification's title and body.
|
|
4
4
|
*
|
|
5
|
-
* A broadcast sink hands us `composeBroadcastMessage`'s shape —
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* daemon notifications already read.
|
|
5
|
+
* A broadcast sink hands us `composeBroadcastMessage`'s shape — a title line, a
|
|
6
|
+
* blank line, then the body (and a `Sent from …` footer) — so honouring the first
|
|
7
|
+
* newline puts the scannable head in the title and the rest in the body, which is
|
|
8
|
+
* how the existing daemon notifications already read. (Desktop is a plain sink, so
|
|
9
|
+
* the message carries no URLs to split off.)
|
|
9
10
|
*
|
|
10
11
|
* Notification banners show roughly two lines before an ellipsis, so a long
|
|
11
12
|
* single-line message is split at the title boundary rather than truncated away:
|
|
@@ -28,10 +28,11 @@ const TITLE_MAX = 64;
|
|
|
28
28
|
/**
|
|
29
29
|
* Split one message into the notification's title and body.
|
|
30
30
|
*
|
|
31
|
-
* A broadcast sink hands us `composeBroadcastMessage`'s shape —
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* daemon notifications already read.
|
|
31
|
+
* A broadcast sink hands us `composeBroadcastMessage`'s shape — a title line, a
|
|
32
|
+
* blank line, then the body (and a `Sent from …` footer) — so honouring the first
|
|
33
|
+
* newline puts the scannable head in the title and the rest in the body, which is
|
|
34
|
+
* how the existing daemon notifications already read. (Desktop is a plain sink, so
|
|
35
|
+
* the message carries no URLs to split off.)
|
|
35
36
|
*
|
|
36
37
|
* Notification banners show roughly two lines before an ellipsis, so a long
|
|
37
38
|
* single-line message is split at the title boundary rather than truncated away:
|
|
@@ -33,6 +33,12 @@ export interface SendResult {
|
|
|
33
33
|
attachments?: string[];
|
|
34
34
|
/** Mailbox provider returns the enqueued message id. */
|
|
35
35
|
msgId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The exact body handed to the provider for THIS destination. Set by the owner
|
|
38
|
+
* fan-out (`sendToOwner`) so a per-destination compose is observable — Slack
|
|
39
|
+
* carries the `mrkdwn` labeled-link variant, iMessage the plain one (PHNX-3698).
|
|
40
|
+
*/
|
|
41
|
+
body?: string;
|
|
36
42
|
/** Per-destination results when the owner policy selects multiple channels. */
|
|
37
43
|
deliveries?: SendResult[];
|
|
38
44
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Meta } from '../types.js';
|
|
12
12
|
import type { SendResult } from './registry.js';
|
|
13
|
+
import type { SinkMessageFormat } from '../sink-format.js';
|
|
13
14
|
/** Normalized delivery request after CLI/config resolution. */
|
|
14
15
|
export interface SendEnvelope {
|
|
15
16
|
text: string;
|
|
@@ -78,8 +79,16 @@ export declare function resolveSendEnvelope(input: ResolveSendInput, meta: Meta)
|
|
|
78
79
|
* internal caller that already has a resolved envelope.
|
|
79
80
|
*/
|
|
80
81
|
export declare function deliverEnvelope(envelope: SendEnvelope, meta: Meta): Promise<SendResult>;
|
|
81
|
-
/**
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Resolve + deliver in one step (CLI happy path).
|
|
84
|
+
*
|
|
85
|
+
* `ownerCompose`, when given, shapes the body PER owner destination on the
|
|
86
|
+
* policy fan-out — Slack gets `mrkdwn` labeled links, iMessage stays plain
|
|
87
|
+
* (PHNX-3698). Only the owner-policy path uses it; a direct `--channel`/`--to`
|
|
88
|
+
* send is delivered verbatim. The envelope's own `text` remains the plain
|
|
89
|
+
* default (validation, `--json`, dry-run display).
|
|
90
|
+
*/
|
|
91
|
+
export declare function sendMessage(input: ResolveSendInput, meta: Meta, ownerCompose?: (format: SinkMessageFormat) => string): Promise<{
|
|
83
92
|
result: SendResult;
|
|
84
93
|
envelope: SendEnvelope;
|
|
85
94
|
} | {
|
|
@@ -108,8 +108,16 @@ export async function deliverEnvelope(envelope, meta) {
|
|
|
108
108
|
dryRun: envelope.dryRun,
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Resolve + deliver in one step (CLI happy path).
|
|
113
|
+
*
|
|
114
|
+
* `ownerCompose`, when given, shapes the body PER owner destination on the
|
|
115
|
+
* policy fan-out — Slack gets `mrkdwn` labeled links, iMessage stays plain
|
|
116
|
+
* (PHNX-3698). Only the owner-policy path uses it; a direct `--channel`/`--to`
|
|
117
|
+
* send is delivered verbatim. The envelope's own `text` remains the plain
|
|
118
|
+
* default (validation, `--json`, dry-run display).
|
|
119
|
+
*/
|
|
120
|
+
export async function sendMessage(input, meta, ownerCompose) {
|
|
113
121
|
const resolved = resolveSendEnvelope(input, meta);
|
|
114
122
|
if (!resolved.ok)
|
|
115
123
|
return { error: resolved.error };
|
|
@@ -123,6 +131,7 @@ export async function sendMessage(input, meta) {
|
|
|
123
131
|
thread: resolved.envelope.thread,
|
|
124
132
|
attachments: resolved.envelope.attachments,
|
|
125
133
|
from: resolved.envelope.from,
|
|
134
|
+
...(ownerCompose ? { composeForFormat: ownerCompose } : {}),
|
|
126
135
|
})
|
|
127
136
|
: await deliverEnvelope(resolved.envelope, meta);
|
|
128
137
|
return { result, envelope: resolved.envelope };
|
|
@@ -79,11 +79,100 @@ export function resolveClaudeSetupToken(home) {
|
|
|
79
79
|
// Require a known account (email) up front: without it we cannot key a
|
|
80
80
|
// per-account token, and we must NOT fall back to a bare shared key that
|
|
81
81
|
// would misapply one account's setup-token to another.
|
|
82
|
-
const email = readClaudeAccountEmail(home)
|
|
82
|
+
const email = readClaudeAccountEmail(home)
|
|
83
|
+
// Self-heal (PHNX-3660): a home provisioned before seed-on-attach carries an
|
|
84
|
+
// `.oauth_token` but no identity. Recover the email from the bundle and
|
|
85
|
+
// write it back, so the home converges instead of needing a re-attach.
|
|
86
|
+
// Explicit-home only: with no home the probe targets the operator's real
|
|
87
|
+
// ~/.claude.json, which a library read must never rewrite.
|
|
88
|
+
?? (home ? discoverClaudeAccountEmailFromOauthToken(home) : null);
|
|
83
89
|
if (!email)
|
|
84
90
|
return null;
|
|
85
91
|
return resolveClaudeSetupTokenForEmail(email, home ?? os.homedir());
|
|
86
92
|
}
|
|
93
|
+
/** Negative/positive discovery cache, keyed by home + .oauth_token fingerprint (SHOULD-2). */
|
|
94
|
+
const discoveryCache = new Map();
|
|
95
|
+
/**
|
|
96
|
+
* Recover a home's account email from its `.claude/.oauth_token` by matching
|
|
97
|
+
* the token VALUE against the `auth` bundle (the slug encodes the email, and
|
|
98
|
+
* the re-encode check makes the decode lossless or fail). On a match the
|
|
99
|
+
* identity is written back via {@link seedClaudeWorkerHomeIdentity}, so this
|
|
100
|
+
* runs at most once per home. Returns null — and writes nothing — when the
|
|
101
|
+
* file is missing, malformed, or matches no bundle key: a token that no longer
|
|
102
|
+
* exists in the bundle must not resurrect an account mapping. A no-match
|
|
103
|
+
* result is cached against the token file's fingerprint so a rotated-out home
|
|
104
|
+
* does not re-decrypt the bundle on every probe.
|
|
105
|
+
*/
|
|
106
|
+
function discoverClaudeAccountEmailFromOauthToken(home) {
|
|
107
|
+
try {
|
|
108
|
+
const tokenPath = path.join(home, '.claude', '.oauth_token');
|
|
109
|
+
const fingerprint = credentialFingerprint(tokenPath);
|
|
110
|
+
if (fingerprint === 'missing')
|
|
111
|
+
return null;
|
|
112
|
+
const cached = discoveryCache.get(home);
|
|
113
|
+
if (cached && cached.fingerprint === fingerprint)
|
|
114
|
+
return cached.email;
|
|
115
|
+
const email = discoverEmailUncached(home, tokenPath);
|
|
116
|
+
discoveryCache.set(home, { fingerprint, email });
|
|
117
|
+
return email;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err instanceof ReservedBundleWrongBackendError)
|
|
121
|
+
throw err;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function discoverEmailUncached(home, tokenPath) {
|
|
126
|
+
let token;
|
|
127
|
+
try {
|
|
128
|
+
token = fs.readFileSync(tokenPath, 'utf-8').trim();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!isValidClaudeSetupToken(token))
|
|
134
|
+
return null;
|
|
135
|
+
if (!bundleExists(AUTH_BUNDLE))
|
|
136
|
+
return null;
|
|
137
|
+
if (bundleBackend(AUTH_BUNDLE) !== 'file') {
|
|
138
|
+
throw new ReservedBundleWrongBackendError(AUTH_BUNDLE, bundleBackend(AUTH_BUNDLE));
|
|
139
|
+
}
|
|
140
|
+
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
141
|
+
for (const [key, value] of Object.entries(env)) {
|
|
142
|
+
if (value.trim() !== token)
|
|
143
|
+
continue;
|
|
144
|
+
const email = emailFromTokenKey(key);
|
|
145
|
+
if (!email)
|
|
146
|
+
continue;
|
|
147
|
+
seedClaudeWorkerHomeIdentity(home, email);
|
|
148
|
+
return email;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decode the email a `CLAUDE_CODE_OAUTH_TOKEN_<slug>` key encodes — or null
|
|
154
|
+
* when the decode is ambiguous. The mapping collapses every non-alphanumeric
|
|
155
|
+
* to `_`, so a `_` surviving in the decoded address cannot be told apart from
|
|
156
|
+
* a folded `.`/`+`/etc. (`FIRST_DOT_LAST_AT_GMAIL_DOT_COM` decodes to
|
|
157
|
+
* `first_dot_last@gmail.com`, which round-trips — wrongly — under the bare
|
|
158
|
+
* re-encode check; review BLOCKER 1). Only a fully unambiguous decode — local
|
|
159
|
+
* and domain pure `[a-z0-9]`, dots in the domain only — is trusted.
|
|
160
|
+
*/
|
|
161
|
+
function emailFromTokenKey(key) {
|
|
162
|
+
const prefix = 'CLAUDE_CODE_OAUTH_TOKEN_';
|
|
163
|
+
if (!key.startsWith(prefix))
|
|
164
|
+
return null;
|
|
165
|
+
const slug = key.slice(prefix.length);
|
|
166
|
+
const [local, domain, ...rest] = slug.split('_AT_');
|
|
167
|
+
if (!local || !domain || rest.length > 0)
|
|
168
|
+
return null;
|
|
169
|
+
const email = `${local}@${domain.replace(/_DOT_/g, '.')}`.toLowerCase();
|
|
170
|
+
if (!/^[a-z0-9]+@[a-z0-9.]+$/.test(email))
|
|
171
|
+
return null;
|
|
172
|
+
if (claudeAccountTokenKey(email) !== key)
|
|
173
|
+
return null;
|
|
174
|
+
return email;
|
|
175
|
+
}
|
|
87
176
|
/**
|
|
88
177
|
* Resolve a long-lived setup-token for an EXPLICIT account email, independent of
|
|
89
178
|
* any version home's `.claude.json`. This is what lets `agents accounts attach`
|
|
@@ -167,14 +256,29 @@ export function seedClaudeWorkerHomeIdentity(versionHome, email) {
|
|
|
167
256
|
try {
|
|
168
257
|
doc = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
169
258
|
}
|
|
170
|
-
catch {
|
|
171
|
-
// Missing
|
|
259
|
+
catch (err) {
|
|
260
|
+
// Missing file → write a fresh minimal document. A file that EXISTS but
|
|
261
|
+
// does not parse is being concurrently rewritten by Claude Code itself —
|
|
262
|
+
// skip it rather than overwrite a live config with the minimal doc.
|
|
263
|
+
if (err.code !== 'ENOENT')
|
|
264
|
+
continue;
|
|
172
265
|
}
|
|
173
266
|
const existing = (doc.oauthAccount && typeof doc.oauthAccount === 'object'
|
|
174
267
|
? doc.oauthAccount
|
|
175
268
|
: {});
|
|
176
269
|
doc.oauthAccount = { ...existing, emailAddress: trimmed };
|
|
177
270
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
178
|
-
|
|
271
|
+
// Temp-write + rename: a reader mid-write never sees a truncated doc.
|
|
272
|
+
const tmp = `${p}.agents-${process.pid}.tmp`;
|
|
273
|
+
try {
|
|
274
|
+
fs.writeFileSync(tmp, JSON.stringify(doc));
|
|
275
|
+
fs.renameSync(tmp, p);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
try {
|
|
279
|
+
fs.rmSync(tmp, { force: true });
|
|
280
|
+
}
|
|
281
|
+
catch { /* best effort */ }
|
|
282
|
+
}
|
|
179
283
|
}
|
|
180
284
|
}
|
package/dist/lib/cloud/rush.d.ts
CHANGED
|
@@ -8,10 +8,18 @@ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOpt
|
|
|
8
8
|
/**
|
|
9
9
|
* Returns true when ~/.rush/user.yaml exists, carries an access_token, and
|
|
10
10
|
* the token has not passed its expires_at timestamp (Unix seconds). A missing
|
|
11
|
-
* expires_at
|
|
12
|
-
*
|
|
11
|
+
* expires_at, or `expires_at: 0` (a non-expiring Phoenix `pid_` bearer), is
|
|
12
|
+
* treated as non-expired (see isRushSessionExpired, PHNX-3645). Pass yamlPath
|
|
13
|
+
* to override the default path in tests.
|
|
13
14
|
*/
|
|
14
15
|
export declare function isRushSessionValid(yamlPath?: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Read the Rush session access token from ~/.rush/user.yaml. Exported (with an
|
|
18
|
+
* overridable yamlPath, like isRushSessionValid) so the freshness behavior —
|
|
19
|
+
* including the `expires_at: 0` non-expiring case (PHNX-3645) — is directly
|
|
20
|
+
* testable; the class methods call it with the default path.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readToken(yamlPath?: string): string;
|
|
15
23
|
/** One version's entry in the account manifest sent on every dispatch. */
|
|
16
24
|
export interface AccountManifestEntry {
|
|
17
25
|
version: string;
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as os from 'os';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { resolveDispatchRepos, normalizeProviderStatus, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
13
|
+
import { isRushSessionExpired } from '../rush-session.js';
|
|
13
14
|
import { parseSSE } from './stream.js';
|
|
14
15
|
import { listInstalledVersions, getVersionHomePath } from '../installations/versions.js';
|
|
15
16
|
import { getAccountInfo } from '../agents.js';
|
|
@@ -19,8 +20,9 @@ const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
|
19
20
|
/**
|
|
20
21
|
* Returns true when ~/.rush/user.yaml exists, carries an access_token, and
|
|
21
22
|
* the token has not passed its expires_at timestamp (Unix seconds). A missing
|
|
22
|
-
* expires_at
|
|
23
|
-
*
|
|
23
|
+
* expires_at, or `expires_at: 0` (a non-expiring Phoenix `pid_` bearer), is
|
|
24
|
+
* treated as non-expired (see isRushSessionExpired, PHNX-3645). Pass yamlPath
|
|
25
|
+
* to override the default path in tests.
|
|
24
26
|
*/
|
|
25
27
|
export function isRushSessionValid(yamlPath = USER_YAML) {
|
|
26
28
|
try {
|
|
@@ -30,8 +32,7 @@ export function isRushSessionValid(yamlPath = USER_YAML) {
|
|
|
30
32
|
const data = yaml.parse(raw);
|
|
31
33
|
if (!data?.session?.access_token)
|
|
32
34
|
return false;
|
|
33
|
-
|
|
34
|
-
if (typeof expiresAt === 'number' && expiresAt <= Date.now() / 1000)
|
|
35
|
+
if (isRushSessionExpired(data.session.expires_at))
|
|
35
36
|
return false;
|
|
36
37
|
return true;
|
|
37
38
|
}
|
|
@@ -39,19 +40,24 @@ export function isRushSessionValid(yamlPath = USER_YAML) {
|
|
|
39
40
|
return false;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Read the Rush session access token from ~/.rush/user.yaml. Exported (with an
|
|
45
|
+
* overridable yamlPath, like isRushSessionValid) so the freshness behavior —
|
|
46
|
+
* including the `expires_at: 0` non-expiring case (PHNX-3645) — is directly
|
|
47
|
+
* testable; the class methods call it with the default path.
|
|
48
|
+
*/
|
|
49
|
+
export function readToken(yamlPath = USER_YAML) {
|
|
50
|
+
if (!fs.existsSync(yamlPath)) {
|
|
45
51
|
throw new Error('Not logged in to Rush. Run `rush login` first.');
|
|
46
52
|
}
|
|
47
|
-
const raw = fs.readFileSync(
|
|
53
|
+
const raw = fs.readFileSync(yamlPath, 'utf-8');
|
|
48
54
|
const data = yaml.parse(raw);
|
|
49
55
|
const token = data?.session?.access_token;
|
|
50
56
|
if (!token) {
|
|
51
57
|
throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
|
|
52
58
|
}
|
|
53
59
|
const expiresAt = data.session?.expires_at;
|
|
54
|
-
if (
|
|
60
|
+
if (isRushSessionExpired(expiresAt)) {
|
|
55
61
|
const expiredAt = new Date(expiresAt * 1000).toISOString();
|
|
56
62
|
throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
|
|
57
63
|
}
|