@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -13,6 +13,7 @@ import * as yaml from 'yaml';
|
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonPid, readDaemonLog, } from '../lib/daemon.js';
|
|
14
14
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
15
15
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, } from '../lib/routines.js';
|
|
16
|
+
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
16
17
|
import { getRoutinesDir } from '../lib/state.js';
|
|
17
18
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
18
19
|
import { safeJoin } from '../lib/paths.js';
|
|
@@ -21,6 +22,21 @@ import { JobScheduler } from '../lib/scheduler.js';
|
|
|
21
22
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
22
23
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
23
24
|
import { setHelpSections } from '../lib/help.js';
|
|
25
|
+
/**
|
|
26
|
+
* Human label for what fires a job: its cron schedule, or its event trigger
|
|
27
|
+
* for schedule-less (trigger-only) routines.
|
|
28
|
+
*/
|
|
29
|
+
function fireConditionLabel(job) {
|
|
30
|
+
if (job.schedule)
|
|
31
|
+
return humanizeCron(job.schedule, job.timezone);
|
|
32
|
+
if (job.trigger) {
|
|
33
|
+
const scope = job.trigger.repo
|
|
34
|
+
? ` (${job.trigger.repo}${job.trigger.branch ? `@${job.trigger.branch}` : ''})`
|
|
35
|
+
: '';
|
|
36
|
+
return `on ${job.trigger.event}${scope}`;
|
|
37
|
+
}
|
|
38
|
+
return '-';
|
|
39
|
+
}
|
|
24
40
|
/** Start or reload the background scheduler so newly-added jobs fire on time. */
|
|
25
41
|
function ensureSchedulerRunning() {
|
|
26
42
|
if (isDaemonRunning()) {
|
|
@@ -73,7 +89,7 @@ async function pickJob(message, filter, alternatives = [], cwd) {
|
|
|
73
89
|
message,
|
|
74
90
|
choices: jobs.map((job) => ({
|
|
75
91
|
value: job.name,
|
|
76
|
-
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule})`)}`,
|
|
92
|
+
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
77
93
|
})),
|
|
78
94
|
});
|
|
79
95
|
}
|
|
@@ -159,8 +175,9 @@ export function registerRoutinesCommands(program) {
|
|
|
159
175
|
agent: job.agent ?? null,
|
|
160
176
|
workflow: job.workflow ?? null,
|
|
161
177
|
repo: job.repo ?? null,
|
|
162
|
-
schedule: job.schedule,
|
|
163
|
-
scheduleHuman:
|
|
178
|
+
schedule: job.schedule ?? null,
|
|
179
|
+
scheduleHuman: fireConditionLabel(job),
|
|
180
|
+
trigger: job.trigger ?? null,
|
|
164
181
|
timezone: job.timezone ?? null,
|
|
165
182
|
enabled: job.enabled,
|
|
166
183
|
overdue: overdueSet.has(job.name),
|
|
@@ -193,7 +210,7 @@ export function registerRoutinesCommands(program) {
|
|
|
193
210
|
for (const job of jobs) {
|
|
194
211
|
const nextRun = scheduler.getNextRun(job.name);
|
|
195
212
|
const nextStr = humanizeNextRun(nextRun ?? null, now, job.timezone);
|
|
196
|
-
let schedStr =
|
|
213
|
+
let schedStr = fireConditionLabel(job);
|
|
197
214
|
if (job.endAt) {
|
|
198
215
|
const end = new Date(job.endAt);
|
|
199
216
|
const endLabel = Number.isFinite(end.getTime())
|
|
@@ -565,6 +582,82 @@ export function registerRoutinesCommands(program) {
|
|
|
565
582
|
}
|
|
566
583
|
console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
|
|
567
584
|
});
|
|
585
|
+
routinesCmd
|
|
586
|
+
.command('webhook')
|
|
587
|
+
.description('Fire trigger-based routines from a single GitHub webhook payload (read from --file or stdin). One-shot: matches and fires, then exits. For a long-running receiver, run this behind your own HTTP forwarder.')
|
|
588
|
+
.requiredOption('--event <name>', 'GitHub event name, as sent in the X-GitHub-Event header (e.g. pull_request, push, workflow_run)')
|
|
589
|
+
.option('--file <path>', 'Read the webhook JSON payload from this file instead of stdin')
|
|
590
|
+
.option('--dry-run', 'Show which routines would fire without firing them')
|
|
591
|
+
.action(async (options) => {
|
|
592
|
+
// Load the raw JSON payload: --file wins, else drain stdin.
|
|
593
|
+
let raw;
|
|
594
|
+
if (options.file) {
|
|
595
|
+
const resolved = path.resolve(options.file);
|
|
596
|
+
if (!fs.existsSync(resolved)) {
|
|
597
|
+
console.log(chalk.red(`File not found: ${resolved}`));
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
raw = fs.readFileSync(resolved, 'utf-8');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
if (process.stdin.isTTY) {
|
|
604
|
+
console.log(chalk.red('No payload provided. Pass --file <path> or pipe the webhook JSON on stdin.'));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
const chunks = [];
|
|
608
|
+
for await (const chunk of process.stdin)
|
|
609
|
+
chunks.push(chunk);
|
|
610
|
+
raw = Buffer.concat(chunks).toString('utf-8');
|
|
611
|
+
}
|
|
612
|
+
let payload;
|
|
613
|
+
try {
|
|
614
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
615
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
616
|
+
throw new Error('payload must be a JSON object');
|
|
617
|
+
}
|
|
618
|
+
payload = parsed;
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
console.log(chalk.red(`Invalid webhook payload JSON: ${err.message}`));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
const webhook = { event: options.event, payload };
|
|
625
|
+
// Matching is intentionally user-layer only (fireWebhookJobs defaults to
|
|
626
|
+
// listJobs() with no cwd), mirroring `run`/`catchup`: a webhook must never
|
|
627
|
+
// fire a cloned project repo's `.agents/routines/*.yml` and run an
|
|
628
|
+
// attacker-supplied prompt under the user's agent session.
|
|
629
|
+
if (options.dryRun) {
|
|
630
|
+
const matched = matchJobsToWebhook(listAllJobs(), webhook);
|
|
631
|
+
if (matched.length === 0) {
|
|
632
|
+
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
console.log(chalk.bold(`${matched.length} routine(s) would fire on ${options.event}:\n`));
|
|
636
|
+
for (const job of matched) {
|
|
637
|
+
console.log(` ${chalk.cyan(job.name)} — ${fireConditionLabel(job)}`);
|
|
638
|
+
}
|
|
639
|
+
console.log(chalk.gray('\n(dry run — no routines triggered)'));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Fired jobs run detached via executeJobDetached (the same path cron
|
|
643
|
+
// uses). Keep the daemon alive so each run's meta.json is finalized.
|
|
644
|
+
if (!isDaemonRunning()) {
|
|
645
|
+
const started = startDaemon();
|
|
646
|
+
if (started.pid) {
|
|
647
|
+
console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const fired = await fireWebhookJobs(webhook);
|
|
651
|
+
if (fired.length === 0) {
|
|
652
|
+
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
console.log(chalk.bold(`Fired ${fired.length} routine(s) on ${options.event}:\n`));
|
|
656
|
+
for (const f of fired) {
|
|
657
|
+
console.log(` ${chalk.cyan(f.jobName)} → ${chalk.green('started')} (run: ${f.runId})`);
|
|
658
|
+
}
|
|
659
|
+
console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
|
|
660
|
+
});
|
|
568
661
|
routinesCmd
|
|
569
662
|
.command('logs [name]')
|
|
570
663
|
.description('Read stdout from the most recent execution. Use --run to see a specific past run.')
|
|
@@ -675,9 +768,12 @@ export function registerRoutinesCommands(program) {
|
|
|
675
768
|
if (result.method === 'already-running') {
|
|
676
769
|
console.log(chalk.yellow(`Scheduler already running (PID: ${result.pid})`));
|
|
677
770
|
}
|
|
678
|
-
else {
|
|
771
|
+
else if (result.pid) {
|
|
679
772
|
console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
|
|
680
773
|
}
|
|
774
|
+
else {
|
|
775
|
+
console.log(chalk.yellow('Scheduler start dispatched but no PID surfaced. Check `agents routines status`.'));
|
|
776
|
+
}
|
|
681
777
|
});
|
|
682
778
|
routinesCmd
|
|
683
779
|
.command('stop')
|
|
@@ -23,7 +23,7 @@ import chalk from 'chalk';
|
|
|
23
23
|
import * as crypto from 'crypto';
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
-
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
26
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
27
27
|
import { getBackupsDir } from '../lib/state.js';
|
|
28
28
|
import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
|
|
29
29
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -101,7 +101,7 @@ function migrateOne(record) {
|
|
|
101
101
|
export function registerSecretsMigrateAclCommand(secrets) {
|
|
102
102
|
secrets
|
|
103
103
|
.command('migrate-acl')
|
|
104
|
-
.description('Refresh
|
|
104
|
+
.description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
|
|
105
105
|
.option('--commit', 'Perform writes (default is dry-run reporting only)')
|
|
106
106
|
.option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
|
|
107
107
|
.option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
|
|
@@ -115,88 +115,137 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
115
115
|
if (!prefix.startsWith(ITEM_PREFIX)) {
|
|
116
116
|
throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
118
|
+
// Two independent classes of work under one command:
|
|
119
|
+
// (a) Legacy ACL stragglers — items still in the file-based keychain
|
|
120
|
+
// with a pre-migration trusted-app ACL. Rewritten value-by-value.
|
|
121
|
+
// (b) Orphaned access groups — data-protection items filed under a
|
|
122
|
+
// pre-#279 non-concrete group, invisible to the pinned queries.
|
|
123
|
+
// Re-homed by the helper behind a single Touch ID.
|
|
124
|
+
// Default: only legacy stragglers for (a); `--all` forces a full rewrite.
|
|
123
125
|
const names = opts.all ? listKeychainItems(prefix) : listLegacyKeychainItems(prefix);
|
|
124
126
|
const items = names.map((item) => {
|
|
125
127
|
const localExists = hasKeychainToken(item);
|
|
126
128
|
return { item, sync: !localExists };
|
|
127
129
|
});
|
|
128
|
-
|
|
130
|
+
const orphans = listOrphanedKeychainItems(prefix);
|
|
131
|
+
if (items.length === 0 && orphans.length === 0) {
|
|
129
132
|
console.log(opts.all
|
|
130
133
|
? chalk.gray(`No keychain items with prefix '${prefix}'.`)
|
|
131
|
-
: chalk.green(`Nothing to migrate — all items under '${prefix}'
|
|
134
|
+
: chalk.green(`Nothing to migrate — all items under '${prefix}' use the modern ACL and access group.`));
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
const label = opts.all ? 'item' : 'legacy item';
|
|
135
|
-
|
|
138
|
+
if (items.length > 0) {
|
|
139
|
+
console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' with an outdated ACL.`));
|
|
140
|
+
}
|
|
141
|
+
if (orphans.length > 0) {
|
|
142
|
+
console.log(chalk.bold(`Found ${orphans.length} orphaned item(s) under '${prefix}' in a stale access group.`));
|
|
143
|
+
}
|
|
136
144
|
if (!opts.commit) {
|
|
137
145
|
for (const { item, sync } of items) {
|
|
138
146
|
console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
|
|
139
147
|
}
|
|
148
|
+
for (const item of orphans) {
|
|
149
|
+
console.log(` ${chalk.cyan(item)} ${chalk.yellow('(orphaned access group)')}`);
|
|
150
|
+
}
|
|
140
151
|
console.log();
|
|
141
152
|
console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
|
|
142
153
|
return;
|
|
143
154
|
}
|
|
144
|
-
// Commit phase
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
// ---- Commit phase ----
|
|
156
|
+
let okCount = 0;
|
|
157
|
+
let failCount = 0;
|
|
158
|
+
// (a) Legacy ACL rewrite. Snapshot every value first (one batched read
|
|
159
|
+
// behind a single helper process), encrypt, then delete + re-add. A total
|
|
160
|
+
// read failure here counts the legacy items as failed but does NOT abort —
|
|
161
|
+
// the orphan sweep (b) is independent and must still run.
|
|
162
|
+
if (items.length > 0) {
|
|
163
|
+
const fetched = getKeychainTokens(items.map((i) => i.item));
|
|
164
|
+
const records = [];
|
|
165
|
+
for (const { item, sync } of items) {
|
|
166
|
+
const value = fetched.get(item);
|
|
167
|
+
if (value === undefined) {
|
|
168
|
+
console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
records.push({ item, sync, value });
|
|
154
172
|
}
|
|
155
|
-
records.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.error(chalk.red('No items could be read. Aborting before any writes.'));
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
const passphrase = opts.passphraseEnv
|
|
162
|
-
? (() => {
|
|
163
|
-
const v = process.env[opts.passphraseEnv];
|
|
164
|
-
if (!v)
|
|
165
|
-
throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
|
|
166
|
-
if (v.length < MIN_PASSPHRASE_LEN)
|
|
167
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
168
|
-
return v;
|
|
169
|
-
})()
|
|
170
|
-
: await promptPassphrase();
|
|
171
|
-
const backupPath = writeEncryptedBackup(records, passphrase);
|
|
172
|
-
// Compute a quick fingerprint so the user can sanity-check recovery without
|
|
173
|
-
// decrypting. Hash of (count, sorted item names).
|
|
174
|
-
const fingerprint = crypto
|
|
175
|
-
.createHash('sha256')
|
|
176
|
-
.update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
|
|
177
|
-
.digest('hex')
|
|
178
|
-
.slice(0, 12);
|
|
179
|
-
console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
|
|
180
|
-
const results = [];
|
|
181
|
-
for (const record of records) {
|
|
182
|
-
const r = migrateOne(record);
|
|
183
|
-
results.push(r);
|
|
184
|
-
if (r.status === 'ok') {
|
|
185
|
-
console.log(` ${chalk.green('ok')} ${record.item}`);
|
|
173
|
+
if (records.length === 0) {
|
|
174
|
+
console.error(chalk.red('No legacy items could be read — skipping legacy rewrite.'));
|
|
175
|
+
failCount += items.length;
|
|
186
176
|
}
|
|
187
177
|
else {
|
|
188
|
-
|
|
178
|
+
const passphrase = opts.passphraseEnv
|
|
179
|
+
? (() => {
|
|
180
|
+
const v = process.env[opts.passphraseEnv];
|
|
181
|
+
if (!v)
|
|
182
|
+
throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
|
|
183
|
+
if (v.length < MIN_PASSPHRASE_LEN)
|
|
184
|
+
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
185
|
+
return v;
|
|
186
|
+
})()
|
|
187
|
+
: await promptPassphrase();
|
|
188
|
+
const backupPath = writeEncryptedBackup(records, passphrase);
|
|
189
|
+
// Quick fingerprint so the user can sanity-check recovery without decrypting.
|
|
190
|
+
const fingerprint = crypto
|
|
191
|
+
.createHash('sha256')
|
|
192
|
+
.update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
|
|
193
|
+
.digest('hex')
|
|
194
|
+
.slice(0, 12);
|
|
195
|
+
console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const record of records) {
|
|
198
|
+
const r = migrateOne(record);
|
|
199
|
+
results.push(r);
|
|
200
|
+
if (r.status === 'ok') {
|
|
201
|
+
console.log(` ${chalk.green('ok')} ${record.item}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const ok = results.filter((r) => r.status === 'ok').length;
|
|
208
|
+
okCount += ok;
|
|
209
|
+
failCount += results.length - ok;
|
|
210
|
+
if (results.length - ok > 0) {
|
|
211
|
+
console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// (b) Orphan re-home — one helper call, one Touch ID for the batch. The
|
|
216
|
+
// helper adds the pinned copy before deleting the orphan, so no pre-write
|
|
217
|
+
// backup is needed (a failed add leaves the orphan intact and readable).
|
|
218
|
+
if (orphans.length > 0) {
|
|
219
|
+
console.log(chalk.bold(`Re-homing ${orphans.length} orphaned item(s) into the current access group (one Touch ID)…`));
|
|
220
|
+
const results = migrateOrphanedKeychainItems(prefix);
|
|
221
|
+
const healed = new Set();
|
|
222
|
+
for (const r of results) {
|
|
223
|
+
healed.add(r.item);
|
|
224
|
+
if (r.status === 'ok') {
|
|
225
|
+
okCount += 1;
|
|
226
|
+
console.log(` ${chalk.green('ok')} ${r.item}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
failCount += 1;
|
|
230
|
+
console.log(` ${chalk.red(r.status)} ${r.item} ${chalk.gray(r.detail ?? '')}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Any orphan we listed but the helper couldn't reach (e.g. under a
|
|
234
|
+
// different signing team) — surface it, never drop it silently.
|
|
235
|
+
for (const item of orphans) {
|
|
236
|
+
if (!healed.has(item)) {
|
|
237
|
+
failCount += 1;
|
|
238
|
+
console.log(` ${chalk.red('fail')} ${item} ${chalk.gray('unreachable (different signing team?)')}`);
|
|
239
|
+
}
|
|
189
240
|
}
|
|
190
241
|
}
|
|
191
|
-
const
|
|
192
|
-
const failCount = results.length - okCount;
|
|
242
|
+
const total = okCount + failCount;
|
|
193
243
|
console.log();
|
|
194
244
|
if (failCount === 0) {
|
|
195
|
-
console.log(chalk.green(`Migrated ${okCount}/${
|
|
245
|
+
console.log(chalk.green(`Migrated ${okCount}/${total} item(s).`));
|
|
196
246
|
}
|
|
197
247
|
else {
|
|
198
|
-
console.error(chalk.yellow(`Migrated ${okCount}/${
|
|
199
|
-
console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
|
|
248
|
+
console.error(chalk.yellow(`Migrated ${okCount}/${total} item(s); ${failCount} failed.`));
|
|
200
249
|
process.exit(1);
|
|
201
250
|
}
|
|
202
251
|
}
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
|
+
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
|
+
import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
10
12
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
13
|
+
/**
|
|
14
|
+
* Build the child environment for `agents secrets exec`. Strips
|
|
15
|
+
* loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
|
|
16
|
+
* forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
|
|
17
|
+
* the executed command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secretEnv: Record<string, string>): NodeJS.ProcessEnv;
|
|
11
20
|
/**
|
|
12
21
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
13
22
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
@@ -16,25 +25,29 @@ export { SSH_TARGET_RE, assertValidSshTarget };
|
|
|
16
25
|
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
17
26
|
*/
|
|
18
27
|
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
28
|
+
/** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
|
|
29
|
+
* hint when a `daily` bundle is currently held by the secrets-agent. `held`
|
|
30
|
+
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
31
|
+
export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
|
|
19
32
|
/** Register the `agents secrets` command tree. */
|
|
20
33
|
export declare function registerSecretsCommands(program: Command): void;
|
|
34
|
+
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
35
|
+
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
|
36
|
+
* `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
|
|
37
|
+
* flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier —
|
|
38
|
+
* accepted here, then gated behind a loud confirmation in the command layer. */
|
|
39
|
+
export declare function parsePolicyOpt(raw: string | undefined): SecretsPolicy;
|
|
21
40
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
31
|
-
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
32
|
-
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
33
|
-
* `agents secrets exec` runs a caller-supplied command against a bundle the
|
|
34
|
-
* caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
|
|
35
|
-
* ever changes (exec'ing an untrusted command line), route through a shell that
|
|
36
|
-
* disables expansion rather than relying on this quoter. An empty arg becomes
|
|
37
|
-
* `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
|
|
38
|
-
* under `process.platform === 'win32'`).
|
|
41
|
+
* Gate a create/switch to the `never` prompt-policy behind an explicit,
|
|
42
|
+
* deliberate acknowledgement — it is the least-safe tier (no user-presence
|
|
43
|
+
* check on any read). Returns:
|
|
44
|
+
* - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
|
|
45
|
+
* - `'prompt'` — interactive shell: caller must run a loud confirm prompt.
|
|
46
|
+
* Throws in a non-interactive shell when the flag is absent, so a headless
|
|
47
|
+
* `create --policy never` can't silently downgrade a bundle's protection.
|
|
39
48
|
*/
|
|
40
|
-
export declare function
|
|
49
|
+
export declare function assertNeverPolicyAcknowledged(policy: SecretsPolicy | undefined, opts: {
|
|
50
|
+
iUnderstand?: boolean;
|
|
51
|
+
interactive: boolean;
|
|
52
|
+
}): 'ok' | 'prompt';
|
|
53
|
+
export { quoteWin32ExecArg };
|