@phnx-labs/agents-cli 1.20.64 → 1.20.65
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 +39 -3
- package/README.md +37 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +94 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +36 -6
- package/dist/commands/ssh.js +45 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +34 -14
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +53 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +26 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* machine's stable identity. Credentials come from the `r2.backups` secrets
|
|
4
4
|
* bundle (OS keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
5
|
*/
|
|
6
|
-
import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
|
|
6
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../../secrets/bundles.js';
|
|
7
7
|
/** Secrets bundle holding the R2 credentials. */
|
|
8
8
|
export const SYNC_BUNDLE = 'r2.backups';
|
|
9
9
|
/**
|
|
@@ -12,7 +12,13 @@ export const SYNC_BUNDLE = 'r2.backups';
|
|
|
12
12
|
* without real credentials (no silent fallback).
|
|
13
13
|
*/
|
|
14
14
|
function resolveR2Config() {
|
|
15
|
-
|
|
15
|
+
// The daemon monitor loop is headless by construction (no TTY, ever), so
|
|
16
|
+
// isHeadlessSecretsContext() is true here and this resolves broker-only — a
|
|
17
|
+
// broker miss can never pop an unattended Touch ID sheet on the user's screen
|
|
18
|
+
// (same rationale as daemon.ts readDaemonClaudeOAuthToken). Using the shared
|
|
19
|
+
// predicate rather than a literal keeps it consistent with the other callers
|
|
20
|
+
// and lets any interactive caller of loadR2Config still prompt.
|
|
21
|
+
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'sessions-sync', agentOnly: isHeadlessSecretsContext() });
|
|
16
22
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
17
23
|
const bucket = env.R2_BUCKET_NAME?.trim();
|
|
18
24
|
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
|
|
@@ -41,6 +41,29 @@ export interface SessionAttachment {
|
|
|
41
41
|
mediaType: string;
|
|
42
42
|
sizeBytes?: number;
|
|
43
43
|
}
|
|
44
|
+
/** One checklist item, as Claude's `TodoWrite` (`content`) or Codex's `update_plan` (`step`) emits it. */
|
|
45
|
+
export interface TodoItem {
|
|
46
|
+
content: string;
|
|
47
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
48
|
+
/** Present-continuous label shown while this item is the active step. */
|
|
49
|
+
activeForm?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Live plan progress derived from the most recent checklist write in the transcript
|
|
53
|
+
* (Claude `TodoWrite` / Codex `update_plan`, RUSH-1380). Lets consumers show "N/M
|
|
54
|
+
* done" + the current step for any session — including remote / device-dispatched
|
|
55
|
+
* agents that carry no local tool-call stream — instead of only a coarse
|
|
56
|
+
* working/idle verb.
|
|
57
|
+
*/
|
|
58
|
+
export interface TodoProgress {
|
|
59
|
+
items: TodoItem[];
|
|
60
|
+
/** Count of completed items. */
|
|
61
|
+
done: number;
|
|
62
|
+
/** Total items. */
|
|
63
|
+
total: number;
|
|
64
|
+
/** The in-progress item's activeForm (falls back to its content). The live step. */
|
|
65
|
+
activeForm?: string;
|
|
66
|
+
}
|
|
44
67
|
/** Metadata attached when a session was spawned by `agents teams`. */
|
|
45
68
|
export interface TeamOrigin {
|
|
46
69
|
/** Teammate name if set, otherwise first 8 chars of the agent UUID. */
|
|
@@ -116,6 +139,13 @@ export interface SessionMeta {
|
|
|
116
139
|
* recover the plan text — the CLI now carries it on the metadata row.
|
|
117
140
|
*/
|
|
118
141
|
plan?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Live plan progress from the most recent checklist write (Claude `TodoWrite`
|
|
144
|
+
* / Codex `update_plan`, RUSH-1503). Populated on `agents sessions <id> --json`
|
|
145
|
+
* from the state engine so the Factory panel reads the CLI's computed checklist
|
|
146
|
+
* instead of re-parsing the transcript. Absent when the session wrote no list.
|
|
147
|
+
*/
|
|
148
|
+
todos?: TodoProgress;
|
|
119
149
|
/**
|
|
120
150
|
* True when the session was spawned programmatically (SDK entrypoint) rather
|
|
121
151
|
* than by a human at the Claude CLI. Captured at scan time from the JSONL
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export declare const REMOTE_HELPER_PORT = 8765;
|
|
|
49
49
|
export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
|
|
50
50
|
/** Basename of the cross-published exe under native/computer-win/dist. */
|
|
51
51
|
export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
|
|
52
|
+
/** Basename of the daemon's shared-secret token file under %LOCALAPPDATA%\agents. */
|
|
53
|
+
export declare const WIN_HELPER_TOKEN_FILE = "helper-token";
|
|
52
54
|
/**
|
|
53
55
|
* Locate a locally built Windows daemon exe — a repo-checkout build output from
|
|
54
56
|
* `scripts/build-win.sh`, or one bundled next to the package. Local paths are
|
|
@@ -101,6 +103,16 @@ export declare function remoteStatePath(device: string): string;
|
|
|
101
103
|
export declare function readRemoteState(device: string): RemoteTunnelState | null;
|
|
102
104
|
export declare function writeRemoteState(state: RemoteTunnelState): void;
|
|
103
105
|
export declare function clearRemoteState(device: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Local file holding the shared-secret token for a device's helper daemon.
|
|
108
|
+
* Written at `setup --host` (0600), read at `start --host` so the RPC client
|
|
109
|
+
* authenticates. The remote daemon reads the same value from its own
|
|
110
|
+
* `--token-file`; the CLI is the source of truth and never round-trips it back.
|
|
111
|
+
*/
|
|
112
|
+
export declare function helperTokenPath(device: string): string;
|
|
113
|
+
export declare function readHelperToken(device: string): string | null;
|
|
114
|
+
export declare function writeHelperToken(device: string, token: string): void;
|
|
115
|
+
export declare function clearHelperToken(device: string): void;
|
|
104
116
|
/** Resolve a registered device to its ssh pieces, or throw a clear error. */
|
|
105
117
|
export declare function resolveRemoteDevice(name: string): Promise<{
|
|
106
118
|
device: DeviceProfile;
|
|
@@ -123,7 +135,13 @@ export declare function buildVerifyPushScript(remotePath: string, expectedBytes:
|
|
|
123
135
|
* survives ssh disconnect — the same rationale as the browser WMI launch. The
|
|
124
136
|
* task is started immediately so the caller need not log out/in.
|
|
125
137
|
*/
|
|
126
|
-
|
|
138
|
+
/**
|
|
139
|
+
* PowerShell that writes the shared-secret token under %LOCALAPPDATA%\agents
|
|
140
|
+
* with an owner-only ACL (inheritance removed, read granted only to the current
|
|
141
|
+
* user) and echoes the resolved path so the caller can pass it to `--token-file`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function buildWriteTokenScript(token: string): string;
|
|
144
|
+
export declare function buildRegisterTaskScript(port: number, taskName: string, tokenPath: string): string;
|
|
127
145
|
/** PowerShell that unregisters the task and stops any running daemon process. */
|
|
128
146
|
export declare function buildUnregisterTaskScript(taskName: string): string;
|
|
129
147
|
/** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
|
package/dist/lib/ssh-tunnel.js
CHANGED
|
@@ -103,6 +103,8 @@ export const REMOTE_HELPER_PORT = 8765;
|
|
|
103
103
|
export const REMOTE_TASK_NAME = 'AgentsComputerHelper';
|
|
104
104
|
/** Basename of the cross-published exe under native/computer-win/dist. */
|
|
105
105
|
export const WIN_HELPER_EXE = 'computer-helper-win.exe';
|
|
106
|
+
/** Basename of the daemon's shared-secret token file under %LOCALAPPDATA%\agents. */
|
|
107
|
+
export const WIN_HELPER_TOKEN_FILE = 'helper-token';
|
|
106
108
|
/**
|
|
107
109
|
* Locate a locally built Windows daemon exe — a repo-checkout build output from
|
|
108
110
|
* `scripts/build-win.sh`, or one bundled next to the package. Local paths are
|
|
@@ -234,6 +236,37 @@ export function clearRemoteState(device) {
|
|
|
234
236
|
/* already gone */
|
|
235
237
|
}
|
|
236
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Local file holding the shared-secret token for a device's helper daemon.
|
|
241
|
+
* Written at `setup --host` (0600), read at `start --host` so the RPC client
|
|
242
|
+
* authenticates. The remote daemon reads the same value from its own
|
|
243
|
+
* `--token-file`; the CLI is the source of truth and never round-trips it back.
|
|
244
|
+
*/
|
|
245
|
+
export function helperTokenPath(device) {
|
|
246
|
+
return path.join(remoteStateDir(), `${device}.token`);
|
|
247
|
+
}
|
|
248
|
+
export function readHelperToken(device) {
|
|
249
|
+
try {
|
|
250
|
+
const t = fs.readFileSync(helperTokenPath(device), 'utf-8').trim();
|
|
251
|
+
return t.length > 0 ? t : null;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export function writeHelperToken(device, token) {
|
|
258
|
+
const dir = remoteStateDir();
|
|
259
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
260
|
+
fs.writeFileSync(helperTokenPath(device), token, { mode: 0o600 });
|
|
261
|
+
}
|
|
262
|
+
export function clearHelperToken(device) {
|
|
263
|
+
try {
|
|
264
|
+
fs.unlinkSync(helperTokenPath(device));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
/* already gone */
|
|
268
|
+
}
|
|
269
|
+
}
|
|
237
270
|
/** Resolve a registered device to its ssh pieces, or throw a clear error. */
|
|
238
271
|
export async function resolveRemoteDevice(name) {
|
|
239
272
|
const device = await getDevice(name);
|
|
@@ -284,10 +317,26 @@ export function buildVerifyPushScript(remotePath, expectedBytes) {
|
|
|
284
317
|
* survives ssh disconnect — the same rationale as the browser WMI launch. The
|
|
285
318
|
* task is started immediately so the caller need not log out/in.
|
|
286
319
|
*/
|
|
287
|
-
|
|
320
|
+
/**
|
|
321
|
+
* PowerShell that writes the shared-secret token under %LOCALAPPDATA%\agents
|
|
322
|
+
* with an owner-only ACL (inheritance removed, read granted only to the current
|
|
323
|
+
* user) and echoes the resolved path so the caller can pass it to `--token-file`.
|
|
324
|
+
*/
|
|
325
|
+
export function buildWriteTokenScript(token) {
|
|
326
|
+
return [
|
|
327
|
+
`$dir = Join-Path $env:LOCALAPPDATA 'agents'`,
|
|
328
|
+
`New-Item -ItemType Directory -Force -Path $dir | Out-Null`,
|
|
329
|
+
`$tok = Join-Path $dir '${WIN_HELPER_TOKEN_FILE}'`,
|
|
330
|
+
`Set-Content -LiteralPath $tok -Value ${psSingleQuote(token)} -NoNewline -Encoding ascii`,
|
|
331
|
+
`icacls $tok /inheritance:r /grant:r ("$($env:USERNAME):(R)") | Out-Null`,
|
|
332
|
+
`Write-Output $tok`,
|
|
333
|
+
].join('; ');
|
|
334
|
+
}
|
|
335
|
+
export function buildRegisterTaskScript(port, taskName, tokenPath) {
|
|
336
|
+
const argument = `--port ${port} --token-file "${tokenPath}"`;
|
|
288
337
|
return [
|
|
289
338
|
`$exe = Join-Path (Join-Path $env:LOCALAPPDATA 'agents') '${WIN_HELPER_EXE}'`,
|
|
290
|
-
`$action = New-ScheduledTaskAction -Execute $exe -Argument
|
|
339
|
+
`$action = New-ScheduledTaskAction -Execute $exe -Argument ${psSingleQuote(argument)}`,
|
|
291
340
|
`$trigger = New-ScheduledTaskTrigger -AtLogOn`,
|
|
292
341
|
`$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest`,
|
|
293
342
|
`$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)`,
|
|
@@ -369,13 +418,28 @@ export async function setupRemoteHelper(name) {
|
|
|
369
418
|
if (verify.code !== 0) {
|
|
370
419
|
throw new Error(`verifying helper exe on '${name}' failed (exit ${verify.code ?? 'null'}): ${verify.stderr.trim() || verify.stdout.trim()}`);
|
|
371
420
|
}
|
|
372
|
-
//
|
|
373
|
-
|
|
421
|
+
// Provision the auth token: generate it locally, write it on the remote with
|
|
422
|
+
// an owner-only ACL, and register the task with --token-file. The daemon now
|
|
423
|
+
// refuses to start without a token, so a token-less (open-to-any-local-process)
|
|
424
|
+
// daemon can no longer be stood up through the CLI.
|
|
425
|
+
const token = generateToken();
|
|
426
|
+
const tokWrite = sshExec(target, encodePowerShell(buildWriteTokenScript(token)), { timeoutMs: 60_000 });
|
|
427
|
+
if (tokWrite.code !== 0) {
|
|
428
|
+
throw new Error(`writing helper token on '${name}' failed (exit ${tokWrite.code ?? 'null'}): ${tokWrite.stderr.trim() || tokWrite.stdout.trim()}`);
|
|
429
|
+
}
|
|
430
|
+
const remoteTokenPath = tokWrite.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
431
|
+
if (!remoteTokenPath) {
|
|
432
|
+
throw new Error(`writing helper token on '${name}' did not return a destination path`);
|
|
433
|
+
}
|
|
434
|
+
// Register + start the LOGON task, pointing it at the token file.
|
|
435
|
+
const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME, remoteTokenPath)), {
|
|
374
436
|
timeoutMs: 60_000,
|
|
375
437
|
});
|
|
376
438
|
if (reg.code !== 0) {
|
|
377
439
|
throw new Error(`registering scheduled task on '${name}' failed (exit ${reg.code ?? 'null'}): ${reg.stderr.trim() || reg.stdout.trim()}`);
|
|
378
440
|
}
|
|
441
|
+
// Persist the token locally so `start --host` authenticates to the daemon.
|
|
442
|
+
writeHelperToken(name, token);
|
|
379
443
|
return { target, taskName: REMOTE_TASK_NAME };
|
|
380
444
|
}
|
|
381
445
|
/** Reserve a free local TCP port by binding :0 and reading the assigned port. */
|
|
@@ -403,9 +467,17 @@ export async function startRemoteTunnel(name) {
|
|
|
403
467
|
const tunnelPid = tunnel.pid ?? 0;
|
|
404
468
|
// Verify the daemon answers through the tunnel before we record it. This is
|
|
405
469
|
// the real end-to-end check: tunnel up + daemon listening + RPC round-trips.
|
|
406
|
-
|
|
470
|
+
// The daemon now requires a token, so the probe must authenticate with the one
|
|
471
|
+
// provisioned at `setup`. A missing token means setup predates auth — guide
|
|
472
|
+
// the user to re-run it rather than silently failing the RPC.
|
|
473
|
+
const token = readHelperToken(name);
|
|
407
474
|
const prevTcp = process.env.COMPUTER_HELPER_TCP;
|
|
475
|
+
const prevTok = process.env.COMPUTER_HELPER_TOKEN;
|
|
408
476
|
process.env.COMPUTER_HELPER_TCP = `127.0.0.1:${localPort}`;
|
|
477
|
+
if (token)
|
|
478
|
+
process.env.COMPUTER_HELPER_TOKEN = token;
|
|
479
|
+
else
|
|
480
|
+
delete process.env.COMPUTER_HELPER_TOKEN;
|
|
409
481
|
const client = openComputerClient();
|
|
410
482
|
let ok = false;
|
|
411
483
|
let probeErr = '';
|
|
@@ -424,6 +496,10 @@ export async function startRemoteTunnel(name) {
|
|
|
424
496
|
delete process.env.COMPUTER_HELPER_TCP;
|
|
425
497
|
else
|
|
426
498
|
process.env.COMPUTER_HELPER_TCP = prevTcp;
|
|
499
|
+
if (prevTok === undefined)
|
|
500
|
+
delete process.env.COMPUTER_HELPER_TOKEN;
|
|
501
|
+
else
|
|
502
|
+
process.env.COMPUTER_HELPER_TOKEN = prevTok;
|
|
427
503
|
}
|
|
428
504
|
if (!ok) {
|
|
429
505
|
try {
|
|
@@ -431,8 +507,10 @@ export async function startRemoteTunnel(name) {
|
|
|
431
507
|
process.kill(tunnelPid);
|
|
432
508
|
}
|
|
433
509
|
catch { /* gone */ }
|
|
434
|
-
|
|
435
|
-
`Is it installed? Run: agents computer setup --host ${name}`
|
|
510
|
+
const hint = token
|
|
511
|
+
? `Is it installed? Run: agents computer setup --host ${name}`
|
|
512
|
+
: `No auth token on record for '${name}' — re-run: agents computer setup --host ${name}`;
|
|
513
|
+
throw new Error(`tunnel to '${name}' opened but the daemon did not answer (${probeErr}). ${hint}`);
|
|
436
514
|
}
|
|
437
515
|
const state = {
|
|
438
516
|
device: name,
|
|
@@ -473,6 +551,7 @@ export async function stopRemoteHelper(name) {
|
|
|
473
551
|
/* device gone / offline — local teardown still succeeds */
|
|
474
552
|
}
|
|
475
553
|
clearRemoteState(name);
|
|
554
|
+
clearHelperToken(name);
|
|
476
555
|
return { tunnelKilled, taskRemoved };
|
|
477
556
|
}
|
|
478
557
|
/**
|
|
@@ -43,6 +43,7 @@ export declare const loadImport: ModuleLoader;
|
|
|
43
43
|
export declare const loadPackages: ModuleLoader;
|
|
44
44
|
export declare const loadDaemon: ModuleLoader;
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
|
+
export declare const loadMonitors: ModuleLoader;
|
|
46
47
|
export declare const loadRun: ModuleLoader;
|
|
47
48
|
export declare const loadDefaults: ModuleLoader;
|
|
48
49
|
export declare const loadModels: ModuleLoader;
|
|
@@ -50,6 +51,7 @@ export declare const loadPrune: ModuleLoader;
|
|
|
50
51
|
export declare const loadTrash: ModuleLoader;
|
|
51
52
|
export declare const loadRestore: ModuleLoader;
|
|
52
53
|
export declare const loadDoctor: ModuleLoader;
|
|
54
|
+
export declare const loadApply: ModuleLoader;
|
|
53
55
|
export declare const loadCheck: ModuleLoader;
|
|
54
56
|
export declare const loadStatus: ModuleLoader;
|
|
55
57
|
export declare const loadProfiles: ModuleLoader;
|
|
@@ -21,6 +21,7 @@ export const loadImport = async () => (await import('../../commands/import.js'))
|
|
|
21
21
|
export const loadPackages = async () => (await import('../../commands/packages.js')).registerPackagesCommands;
|
|
22
22
|
export const loadDaemon = async () => (await import('../../commands/daemon.js')).registerDaemonCommands;
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
|
+
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
24
25
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
25
26
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
26
27
|
export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
|
|
@@ -28,6 +29,7 @@ export const loadPrune = async () => (await import('../../commands/prune.js')).r
|
|
|
28
29
|
export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
|
|
29
30
|
export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
|
|
30
31
|
export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
|
|
32
|
+
export const loadApply = async () => (await import('../../commands/apply.js')).registerApplyCommand;
|
|
31
33
|
export const loadCheck = async () => (await import('../../commands/check.js')).registerCheckCommand;
|
|
32
34
|
export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
|
|
33
35
|
export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
|
|
@@ -121,12 +123,14 @@ export const COMMAND_LOADERS = {
|
|
|
121
123
|
install: [loadPackages],
|
|
122
124
|
daemon: [loadDaemon],
|
|
123
125
|
routines: [loadRoutines],
|
|
126
|
+
monitors: [loadMonitors],
|
|
124
127
|
run: [loadRun],
|
|
125
128
|
defaults: [loadDefaults],
|
|
126
129
|
models: [loadModels],
|
|
127
130
|
trash: [loadTrash],
|
|
128
131
|
restore: [loadRestore],
|
|
129
132
|
doctor: [loadDoctor],
|
|
133
|
+
apply: [loadApply],
|
|
130
134
|
check: [loadCheck],
|
|
131
135
|
status: [loadStatus],
|
|
132
136
|
profiles: [loadProfiles],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -128,6 +128,11 @@ export declare function getSystemRoutinesDir(): string;
|
|
|
128
128
|
export declare function getProjectRoutinesDir(cwd?: string): string | null;
|
|
129
129
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
|
130
130
|
export declare function getRunsDir(): string;
|
|
131
|
+
/** Path to monitor YAML definitions (~/.agents/monitors/). */
|
|
132
|
+
export declare function getMonitorsDir(): string;
|
|
133
|
+
/** Path to the durable per-monitor state-diff store + fire history
|
|
134
|
+
* (~/.agents/.history/monitors/). */
|
|
135
|
+
export declare function getMonitorsHistoryDir(): string;
|
|
131
136
|
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
132
137
|
export declare function getMailboxRootDir(): string;
|
|
133
138
|
/** Root for open-block feed records (~/.agents/.history/feed/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -83,12 +83,19 @@ const HISTORY_DIR = path.join(USER_AGENTS_DIR, '.history');
|
|
|
83
83
|
const CACHE_DIR = path.join(USER_AGENTS_DIR, '.cache');
|
|
84
84
|
// Top-level user dirs (config/definitions only — runtime moves into .history/.cache).
|
|
85
85
|
const ROUTINES_DIR = path.join(USER_AGENTS_DIR, 'routines');
|
|
86
|
+
// Monitor definitions (event-triggered watchers). Sibling of ROUTINES_DIR: a
|
|
87
|
+
// monitor is a routine whose trigger is a watched source instead of a clock.
|
|
88
|
+
const MONITORS_DIR = path.join(USER_AGENTS_DIR, 'monitors');
|
|
86
89
|
const TEAMS_DIR = path.join(USER_AGENTS_DIR, 'teams');
|
|
87
90
|
// History bucket (durable).
|
|
88
91
|
const SESSIONS_DIR = path.join(HISTORY_DIR, 'sessions');
|
|
89
92
|
const SESSIONS_DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
|
|
90
93
|
const VERSIONS_DIR = path.join(HISTORY_DIR, 'versions');
|
|
91
94
|
const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
|
|
95
|
+
// Durable per-monitor state-diff store + fire history (last-seen value/hash,
|
|
96
|
+
// fires/<id>/). Sibling of RUNS_DIR — the native diff store that replaces the
|
|
97
|
+
// hand-rolled markdown memory files monitors used to need.
|
|
98
|
+
const MONITORS_HISTORY_DIR = path.join(HISTORY_DIR, 'monitors');
|
|
92
99
|
const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
|
|
93
100
|
const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
|
|
94
101
|
const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
|
|
@@ -324,6 +331,11 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
|
|
|
324
331
|
}
|
|
325
332
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
|
326
333
|
export function getRunsDir() { return RUNS_DIR; }
|
|
334
|
+
/** Path to monitor YAML definitions (~/.agents/monitors/). */
|
|
335
|
+
export function getMonitorsDir() { return MONITORS_DIR; }
|
|
336
|
+
/** Path to the durable per-monitor state-diff store + fire history
|
|
337
|
+
* (~/.agents/.history/monitors/). */
|
|
338
|
+
export function getMonitorsHistoryDir() { return MONITORS_HISTORY_DIR; }
|
|
327
339
|
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
328
340
|
export function getMailboxRootDir() { return MAILBOX_DIR; }
|
|
329
341
|
/** Root for open-block feed records (~/.agents/.history/feed/). */
|
|
@@ -33,6 +33,13 @@ export interface SessionMeta {
|
|
|
33
33
|
export interface CreateSessionOptions {
|
|
34
34
|
name: string;
|
|
35
35
|
cmd?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Redacted copy of `cmd` to persist in SessionMeta.cmd instead of the real
|
|
38
|
+
* command. When set, `cmd` is still executed but only `metaCmd` is written to
|
|
39
|
+
* disk — used to keep resolved secret values out of the informational cmd
|
|
40
|
+
* field (see exec.ts buildTmuxAgentCommand, RUSH-1758). Defaults to `cmd`.
|
|
41
|
+
*/
|
|
42
|
+
metaCmd?: string;
|
|
36
43
|
cwd?: string;
|
|
37
44
|
env?: NodeJS.ProcessEnv;
|
|
38
45
|
socket?: string;
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -106,7 +106,9 @@ export async function createSession(opts) {
|
|
|
106
106
|
name: opts.name,
|
|
107
107
|
socket,
|
|
108
108
|
createdAt: Date.now(),
|
|
109
|
-
|
|
109
|
+
// Persist the redacted copy when provided so resolved secret values in the
|
|
110
|
+
// launch command never land on disk (RUSH-1758).
|
|
111
|
+
cmd: opts.metaCmd ?? opts.cmd,
|
|
110
112
|
cwd: opts.cwd,
|
|
111
113
|
source: opts.source ?? 'cli',
|
|
112
114
|
labels: opts.labels,
|
|
@@ -99,6 +99,18 @@ export interface DeliveryStore {
|
|
|
99
99
|
markJob(id: string, jobName: string): void;
|
|
100
100
|
}
|
|
101
101
|
export declare function createMemoryDeliveryStore(maxEntries?: number): DeliveryStore;
|
|
102
|
+
/**
|
|
103
|
+
* A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
|
|
104
|
+
* seen delivery ids survive a process restart and are bounded by AGE, not by a
|
|
105
|
+
* fixed entry count — so a captured valid delivery cannot re-fire after a
|
|
106
|
+
* restart or after count-based LRU eviction would have dropped it.
|
|
107
|
+
*
|
|
108
|
+
* `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
|
|
109
|
+
* still on record (younger than the window) is rejected as a duplicate; entries
|
|
110
|
+
* older than the window are pruned (keeping the file bounded) since a webhook
|
|
111
|
+
* source will not legitimately retry a delivery that old.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createFileDeliveryStore(filePath: string, retentionMs?: number): DeliveryStore;
|
|
102
114
|
export interface RateLimiter {
|
|
103
115
|
take(key: string): boolean;
|
|
104
116
|
}
|
|
@@ -117,6 +129,12 @@ export interface WebhookServerOptions {
|
|
|
117
129
|
rateLimiter?: RateLimiter;
|
|
118
130
|
rateLimitPerMinute?: number;
|
|
119
131
|
maxBodyBytes?: number;
|
|
132
|
+
/** Per-IP ingress throttle applied BEFORE the body read (bad-sig flood guard). */
|
|
133
|
+
ipRateLimiter?: RateLimiter;
|
|
134
|
+
/** Per-source-IP requests/minute allowed through to the body read. Default 120. */
|
|
135
|
+
ipRateLimitPerMinute?: number;
|
|
136
|
+
/** Max concurrent TCP connections the receiver accepts. Default 256. */
|
|
137
|
+
maxConnections?: number;
|
|
120
138
|
}
|
|
121
139
|
/**
|
|
122
140
|
* Start a localhost-bound receiver. It accepts only:
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* verification, idempotency, source allow-listing, and rate limiting.
|
|
13
13
|
*/
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
|
+
import * as fs from 'fs';
|
|
15
16
|
import * as http from 'http';
|
|
17
|
+
import * as path from 'path';
|
|
16
18
|
import { jobRunsOnThisDevice, listJobs } from '../routines.js';
|
|
17
19
|
import { executeJobDetached } from '../runner.js';
|
|
18
20
|
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
|
|
@@ -236,6 +238,88 @@ export function createMemoryDeliveryStore(maxEntries = 1000) {
|
|
|
236
238
|
},
|
|
237
239
|
};
|
|
238
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
|
|
243
|
+
* seen delivery ids survive a process restart and are bounded by AGE, not by a
|
|
244
|
+
* fixed entry count — so a captured valid delivery cannot re-fire after a
|
|
245
|
+
* restart or after count-based LRU eviction would have dropped it.
|
|
246
|
+
*
|
|
247
|
+
* `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
|
|
248
|
+
* still on record (younger than the window) is rejected as a duplicate; entries
|
|
249
|
+
* older than the window are pruned (keeping the file bounded) since a webhook
|
|
250
|
+
* source will not legitimately retry a delivery that old.
|
|
251
|
+
*/
|
|
252
|
+
export function createFileDeliveryStore(filePath, retentionMs = 14 * 24 * 60 * 60 * 1000) {
|
|
253
|
+
const seen = new Map();
|
|
254
|
+
// Load persisted state (best-effort: a corrupt/missing file starts empty).
|
|
255
|
+
try {
|
|
256
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
257
|
+
const loadedAt = Date.now();
|
|
258
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
259
|
+
if (typeof entry?.updatedAt !== 'number' || loadedAt - entry.updatedAt > retentionMs)
|
|
260
|
+
continue;
|
|
261
|
+
seen.set(id, {
|
|
262
|
+
complete: entry.complete === true,
|
|
263
|
+
jobs: new Set(Array.isArray(entry.jobs) ? entry.jobs : []),
|
|
264
|
+
updatedAt: entry.updatedAt,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// no prior file / unreadable — start empty
|
|
270
|
+
}
|
|
271
|
+
const persist = () => {
|
|
272
|
+
const snapshot = {};
|
|
273
|
+
for (const [id, entry] of seen) {
|
|
274
|
+
snapshot[id] = { complete: entry.complete, jobs: [...entry.jobs], updatedAt: entry.updatedAt };
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
278
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
279
|
+
fs.writeFileSync(tmp, JSON.stringify(snapshot), 'utf-8');
|
|
280
|
+
fs.renameSync(tmp, filePath);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// best-effort durability; an unwritable dir must not crash ingress
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const prune = (now) => {
|
|
287
|
+
for (const [id, entry] of seen) {
|
|
288
|
+
if (now - entry.updatedAt > retentionMs)
|
|
289
|
+
seen.delete(id);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const touch = (id) => {
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
prune(now);
|
|
295
|
+
let current = seen.get(id);
|
|
296
|
+
if (!current) {
|
|
297
|
+
current = { complete: false, jobs: new Set(), updatedAt: now };
|
|
298
|
+
seen.set(id, current);
|
|
299
|
+
}
|
|
300
|
+
current.updatedAt = now;
|
|
301
|
+
return current;
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
seen: (id) => {
|
|
305
|
+
const entry = seen.get(id);
|
|
306
|
+
if (!entry)
|
|
307
|
+
return false;
|
|
308
|
+
if (Date.now() - entry.updatedAt > retentionMs)
|
|
309
|
+
return false;
|
|
310
|
+
return entry.complete === true;
|
|
311
|
+
},
|
|
312
|
+
mark: (id) => {
|
|
313
|
+
touch(id).complete = true;
|
|
314
|
+
persist();
|
|
315
|
+
},
|
|
316
|
+
completedJobs: (id) => new Set(seen.get(id)?.jobs ?? []),
|
|
317
|
+
markJob: (id, jobName) => {
|
|
318
|
+
touch(id).jobs.add(jobName);
|
|
319
|
+
persist();
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
239
323
|
export function createMemoryRateLimiter(limit, windowMs) {
|
|
240
324
|
const buckets = new Map();
|
|
241
325
|
return {
|
|
@@ -285,6 +369,7 @@ async function readRawBody(req, maxBytes) {
|
|
|
285
369
|
export function startWebhookServer(options) {
|
|
286
370
|
const deliveryStore = options.deliveryStore ?? createMemoryDeliveryStore();
|
|
287
371
|
const rateLimiter = options.rateLimiter ?? createMemoryRateLimiter(options.rateLimitPerMinute ?? 60, 60_000);
|
|
372
|
+
const ipRateLimiter = options.ipRateLimiter ?? createMemoryRateLimiter(options.ipRateLimitPerMinute ?? 120, 60_000);
|
|
288
373
|
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
289
374
|
const server = http.createServer((req, res) => {
|
|
290
375
|
void (async () => {
|
|
@@ -305,6 +390,23 @@ export function startWebhookServer(options) {
|
|
|
305
390
|
res.end(JSON.stringify({ ok: false, error: `missing ${source} webhook secret` }));
|
|
306
391
|
return;
|
|
307
392
|
}
|
|
393
|
+
// Per-IP throttle + declared-size cap BEFORE the (expensive) body read +
|
|
394
|
+
// HMAC. A bad-signature flood of 1 MiB POSTs must be rejected without
|
|
395
|
+
// forcing a full body read and an HMAC per request — the signed-delivery
|
|
396
|
+
// rate limit further down runs only after a signature passes, so it can't
|
|
397
|
+
// shed this load on its own.
|
|
398
|
+
const ip = req.socket.remoteAddress ?? 'unknown';
|
|
399
|
+
if (!ipRateLimiter.take(ip)) {
|
|
400
|
+
res.writeHead(429, { 'content-type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ ok: false, error: 'rate limit exceeded' }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const declaredLength = Number.parseInt(header(req.headers, 'content-length') ?? '', 10);
|
|
405
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
406
|
+
res.writeHead(413, { 'content-type': 'application/json' });
|
|
407
|
+
res.end(JSON.stringify({ ok: false, error: `payload exceeds ${maxBodyBytes} bytes` }));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
308
410
|
try {
|
|
309
411
|
const rawBody = await readRawBody(req, maxBodyBytes);
|
|
310
412
|
const valid = source === 'github'
|
|
@@ -357,6 +459,9 @@ export function startWebhookServer(options) {
|
|
|
357
459
|
}
|
|
358
460
|
})();
|
|
359
461
|
});
|
|
462
|
+
// Connection cap: bound how many concurrent TCP connections the receiver
|
|
463
|
+
// will hold open, so a flood cannot exhaust file descriptors / memory.
|
|
464
|
+
server.maxConnections = options.maxConnections ?? 256;
|
|
360
465
|
server.listen(options.port ?? 0, options.host ?? '127.0.0.1');
|
|
361
466
|
return server;
|
|
362
467
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -515,6 +515,20 @@ export interface PluginManifest {
|
|
|
515
515
|
userConfig?: PluginUserConfigField[];
|
|
516
516
|
/** Other plugin names this plugin depends on. Missing deps produce a warning. */
|
|
517
517
|
dependencies?: string[];
|
|
518
|
+
/**
|
|
519
|
+
* Inline hook configuration (or a path to a hooks JSON file) declared directly
|
|
520
|
+
* in the manifest, per the official plugin format — an execution surface even
|
|
521
|
+
* when the plugin ships no `hooks/` directory. Untyped because the shape is a
|
|
522
|
+
* path string or an inline event map; capability detection only needs to know
|
|
523
|
+
* whether it is present and non-empty.
|
|
524
|
+
*/
|
|
525
|
+
hooks?: unknown;
|
|
526
|
+
/**
|
|
527
|
+
* Inline MCP-server configuration (or a path to an MCP JSON file) declared
|
|
528
|
+
* directly in the manifest — an execution surface even when the plugin ships
|
|
529
|
+
* no `.mcp.json`. Untyped for the same reason as `hooks`.
|
|
530
|
+
*/
|
|
531
|
+
mcpServers?: unknown;
|
|
518
532
|
}
|
|
519
533
|
/** A plugin found on disk with its parsed manifest and resource inventory. */
|
|
520
534
|
export interface DiscoveredPlugin {
|
|
@@ -637,11 +651,15 @@ export interface Meta {
|
|
|
637
651
|
* bundles without an explicit per-bundle policy: `daily` (the default) asks
|
|
638
652
|
* once per ~7 days, `always` asks every time. `auto` (default on) lets the
|
|
639
653
|
* first real keychain read of a `daily` bundle populate the broker so
|
|
640
|
-
* concurrent runs read silently — set it `false` to force a prompt on every read.
|
|
654
|
+
* concurrent runs read silently — set it `false` to force a prompt on every read.
|
|
655
|
+
* `holdMs` caps how long an unlocked/auto-cached bundle is held before the next
|
|
656
|
+
* read re-prompts (default 7 days; e.g. 86400000 for a 24h cap). Clamped to
|
|
657
|
+
* [1 minute, 30 days]. */
|
|
641
658
|
secrets?: {
|
|
642
659
|
policy?: 'always' | 'daily';
|
|
643
660
|
agent?: {
|
|
644
661
|
auto?: boolean;
|
|
662
|
+
holdMs?: number;
|
|
645
663
|
};
|
|
646
664
|
};
|
|
647
665
|
/** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
|
|
@@ -698,6 +716,13 @@ export interface Meta {
|
|
|
698
716
|
* are never copied. `inline` hosts carry their own address/user.
|
|
699
717
|
*/
|
|
700
718
|
hosts?: Record<string, HostEntry>;
|
|
719
|
+
/**
|
|
720
|
+
* Declarative fleet profile (`agents apply` / `ag apply`). Additive to the
|
|
721
|
+
* schema — project `agents:` version-pins are untouched. Declares which agents
|
|
722
|
+
* every device should have, which config to sync, and how login propagates.
|
|
723
|
+
* Full shape in `lib/fleet/types.ts` (FleetManifest).
|
|
724
|
+
*/
|
|
725
|
+
fleet?: import('./fleet/types.js').FleetManifest;
|
|
701
726
|
}
|
|
702
727
|
/** Persisted agent-host entry in agents.yaml (overlay or inline). */
|
|
703
728
|
export interface HostEntry {
|
package/dist/lib/usage.js
CHANGED
|
@@ -207,12 +207,14 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
207
207
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
208
208
|
}
|
|
209
209
|
if (snapshot) {
|
|
210
|
-
// Compact rows show
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
210
|
+
// Compact rows show every BLOCKING window — the same set
|
|
211
|
+
// deriveUsageStatusFromSnapshot uses for the rate-limited badge — so an
|
|
212
|
+
// account throttled by its month window (Droid meters on 5h/week/month)
|
|
213
|
+
// shows the bar that explains why. Claude's Sonnet week is a per-model
|
|
214
|
+
// sub-limit, not a blocking window; it renders only in the full
|
|
215
|
+
// per-version usage section.
|
|
214
216
|
const windows = snapshot.windows
|
|
215
|
-
.filter((window) => window.key
|
|
217
|
+
.filter((window) => window.key !== 'sonnet_week')
|
|
216
218
|
.map((window) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
|
|
217
219
|
if (windows.length > 0) {
|
|
218
220
|
parts.push(windows.join(' '));
|