@phnx-labs/agents-cli 1.20.35 → 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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- 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.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- 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 +156 -75
- 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.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- 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/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- 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/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- 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 +112 -8
- 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 +57 -0
- package/dist/lib/daemon.js +192 -16
- 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 +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- 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 +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- 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 +14 -0
- package/dist/lib/hosts/tasks.js +15 -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/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- 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/plugins.js +16 -6
- 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 +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- 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 +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -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/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/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- 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/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- 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/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents watchdog` — the watchdog CONSUMER (RUSH-1415).
|
|
3
|
+
*
|
|
4
|
+
* Runs the tick loop that ties the merged pieces together: list active sessions,
|
|
5
|
+
* classify stalls, read the tail, decide (deterministic promise-without-toolcall
|
|
6
|
+
* by default), run the resolver safety gate, and inject "Continue." into the EXACT
|
|
7
|
+
* split — all without the Swift menu-bar. See src/lib/watchdog/runner.ts.
|
|
8
|
+
*
|
|
9
|
+
* agents watchdog one tick, dry — prints what it WOULD nudge/skip and why
|
|
10
|
+
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
|
+
* agents watchdog --watch daemon loop (poll every --interval)
|
|
12
|
+
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
+
* agents watchdog enable|disable flip the global auto-nudge sentinel (default OFF)
|
|
14
|
+
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
|
+
*/
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { setHelpSections } from '../lib/help.js';
|
|
20
|
+
import { parseDuration } from '../lib/hooks/cache.js';
|
|
21
|
+
import { getRuntimeStateDir } from '../lib/state.js';
|
|
22
|
+
import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
23
|
+
/** Default state dir the runner and these subcommands share. */
|
|
24
|
+
function stateDir() {
|
|
25
|
+
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
26
|
+
}
|
|
27
|
+
/** Global auto-nudge sentinel — present = enabled. Default OFF (opt-in). */
|
|
28
|
+
function enabledSentinelPath() {
|
|
29
|
+
return path.join(stateDir(), 'enabled');
|
|
30
|
+
}
|
|
31
|
+
function isGloballyEnabled() {
|
|
32
|
+
return fs.existsSync(enabledSentinelPath());
|
|
33
|
+
}
|
|
34
|
+
function setGloballyEnabled(on) {
|
|
35
|
+
const p = enabledSentinelPath();
|
|
36
|
+
if (on) {
|
|
37
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
38
|
+
fs.writeFileSync(p, 'enabled\n');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
fs.rmSync(p);
|
|
43
|
+
}
|
|
44
|
+
catch { /* already off */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Parse a duration flag ("60s", "5m", "1h") to ms, or fall back to `fallbackMs`. */
|
|
48
|
+
function durationMsOr(raw, fallbackMs) {
|
|
49
|
+
if (raw === undefined)
|
|
50
|
+
return fallbackMs;
|
|
51
|
+
const secs = parseDuration(raw);
|
|
52
|
+
return secs === null ? fallbackMs : secs * 1000;
|
|
53
|
+
}
|
|
54
|
+
function humanMs(ms) {
|
|
55
|
+
if (ms >= 3600_000)
|
|
56
|
+
return `${Math.round(ms / 3600_000)}h`;
|
|
57
|
+
if (ms >= 60_000)
|
|
58
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
59
|
+
return `${Math.round(ms / 1000)}s`;
|
|
60
|
+
}
|
|
61
|
+
function colorForOutcome(o) {
|
|
62
|
+
if (o.injected)
|
|
63
|
+
return chalk.green;
|
|
64
|
+
if (o.addressable === false)
|
|
65
|
+
return chalk.yellow;
|
|
66
|
+
if (o.stall === 'stalled' && o.decision === 'nudge')
|
|
67
|
+
return chalk.cyan;
|
|
68
|
+
return chalk.dim;
|
|
69
|
+
}
|
|
70
|
+
/** Render one tick's outcomes as a human status block. */
|
|
71
|
+
function printTick(result, willInject) {
|
|
72
|
+
const { counts } = result;
|
|
73
|
+
const mode = willInject ? chalk.green('nudge') : chalk.dim('dry');
|
|
74
|
+
console.log(`${chalk.bold('watchdog')} ${mode} ` +
|
|
75
|
+
`${counts.total} live · ${counts.stalled} stalled · ` +
|
|
76
|
+
`${chalk.green(String(counts.nudged))} nudged · ` +
|
|
77
|
+
`${chalk.yellow(String(counts.unaddressable))} un-addressable`);
|
|
78
|
+
for (const o of result.outcomes) {
|
|
79
|
+
const tag = o.injected ? 'NUDGED'
|
|
80
|
+
: o.addressable === false ? 'FLAGGED'
|
|
81
|
+
: o.decision === 'nudge' ? 'WOULD-NUDGE'
|
|
82
|
+
: 'skip';
|
|
83
|
+
const c = colorForOutcome(o);
|
|
84
|
+
const who = o.label || o.sessionId?.slice(0, 8) || o.kind;
|
|
85
|
+
const where = o.host ? chalk.dim(`[${o.host}]`) : '';
|
|
86
|
+
const rail = o.rail ? chalk.dim(`→${o.rail}`) : '';
|
|
87
|
+
console.log(` ${c(tag.padEnd(11))} ${chalk.bold(who)} ${where} ${rail}`);
|
|
88
|
+
console.log(` ${chalk.dim(o.reason)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
|
94
|
+
/** Register the `agents watchdog` command tree. */
|
|
95
|
+
export function registerWatchdogCommand(program) {
|
|
96
|
+
const cmd = program
|
|
97
|
+
.command('watchdog')
|
|
98
|
+
.description('Auto-nudge stalled agent terminals: detect stalls, resolve the exact split, inject "Continue." — no menu-bar needed.')
|
|
99
|
+
.option('--nudge', 'Actually inject (default is a dry run that only reports what it would do)')
|
|
100
|
+
.option('--watch', 'Daemon loop: run a tick every --interval until interrupted')
|
|
101
|
+
.option('--interval <dur>', 'Poll interval in --watch mode (e.g. 30s, 1m)', '30s')
|
|
102
|
+
.option('--stall <dur>', 'Idle time before a session counts as stalled', humanMs(DEFAULT_THRESHOLDS.stallMs))
|
|
103
|
+
.option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
|
|
104
|
+
.option('--dormant <dur>', 'Idle time after which a session is left alone (dormant)', humanMs(DEFAULT_THRESHOLDS.dormantMs))
|
|
105
|
+
.option('--text <text>', 'Nudge text delivered into the terminal', 'Continue.')
|
|
106
|
+
.option('--smart', 'Use the LLM decider (agents run) instead of the deterministic path (non-reproducible)')
|
|
107
|
+
.option('--smart-agent <agent>', 'Agent the --smart decider runs as', 'claude')
|
|
108
|
+
.option('--allow-ghostty-focus', 'Permit the coarse, focus-stealing Ghostty path (off by default)')
|
|
109
|
+
.option('--json', 'Emit the tick result as JSON (for the menu-bar / scripts)')
|
|
110
|
+
.action(async (opts) => {
|
|
111
|
+
const thresholds = {
|
|
112
|
+
stallMs: durationMsOr(opts.stall, DEFAULT_THRESHOLDS.stallMs),
|
|
113
|
+
cooldownMs: durationMsOr(opts.cooldown, DEFAULT_THRESHOLDS.cooldownMs),
|
|
114
|
+
dormantMs: durationMsOr(opts.dormant, DEFAULT_THRESHOLDS.dormantMs),
|
|
115
|
+
};
|
|
116
|
+
// Injection gate: --nudge is the explicit per-run opt-in; the global sentinel
|
|
117
|
+
// enables the automatic daemon. Default OFF: bare `agents watchdog` is dry.
|
|
118
|
+
// Re-read the sentinel every tick so a running `--watch` daemon reflects a
|
|
119
|
+
// later `agents watchdog enable`/`disable` (or the Swift menu-bar toggle)
|
|
120
|
+
// from another shell without a restart.
|
|
121
|
+
const computeWillInject = () => opts.nudge === true || (opts.watch === true && isGloballyEnabled());
|
|
122
|
+
const tickOnce = async (willInject) => runWatchdogTick({
|
|
123
|
+
nudge: willInject,
|
|
124
|
+
nudgeText: opts.text,
|
|
125
|
+
smart: opts.smart === true,
|
|
126
|
+
smartAgent: opts.smartAgent,
|
|
127
|
+
thresholds,
|
|
128
|
+
allowGhosttyFocus: opts.allowGhosttyFocus === true,
|
|
129
|
+
stateDir: stateDir(),
|
|
130
|
+
});
|
|
131
|
+
if (!opts.watch) {
|
|
132
|
+
const willInject = computeWillInject();
|
|
133
|
+
const result = await tickOnce(willInject);
|
|
134
|
+
if (opts.json)
|
|
135
|
+
console.log(JSON.stringify(result, null, 2));
|
|
136
|
+
else
|
|
137
|
+
printTick(result, willInject);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Daemon loop.
|
|
141
|
+
const intervalMs = durationMsOr(opts.interval, 30_000);
|
|
142
|
+
if (!computeWillInject() && !opts.json) {
|
|
143
|
+
console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY (global auto-nudge is ${isGloballyEnabled() ? 'on' : 'off'}). ` +
|
|
144
|
+
`Pass --nudge or run 'agents watchdog enable' to inject.`));
|
|
145
|
+
}
|
|
146
|
+
// eslint-disable-next-line no-constant-condition
|
|
147
|
+
while (true) {
|
|
148
|
+
// Re-evaluated each tick: picks up enable/disable flips mid-run.
|
|
149
|
+
const willInject = computeWillInject();
|
|
150
|
+
const result = await tickOnce(willInject);
|
|
151
|
+
if (opts.json)
|
|
152
|
+
console.log(JSON.stringify(result));
|
|
153
|
+
else
|
|
154
|
+
printTick(result, willInject);
|
|
155
|
+
await sleep(intervalMs);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
setHelpSections(cmd, {
|
|
159
|
+
examples: `
|
|
160
|
+
# One tick, dry — see what it WOULD nudge and why (safe, no injection)
|
|
161
|
+
agents watchdog
|
|
162
|
+
|
|
163
|
+
# One tick, actually inject "Continue." into stalled+addressable splits
|
|
164
|
+
agents watchdog --nudge
|
|
165
|
+
|
|
166
|
+
# Watch loop every 30s, tighter stall threshold
|
|
167
|
+
agents watchdog --watch --interval 30s --stall 60s --cooldown 5m
|
|
168
|
+
|
|
169
|
+
# Machine-readable for the menu-bar
|
|
170
|
+
agents watchdog --json
|
|
171
|
+
|
|
172
|
+
# Turn on the global auto-nudge (so --watch injects without --nudge)
|
|
173
|
+
agents watchdog enable
|
|
174
|
+
|
|
175
|
+
# Leave one session detected-but-untouched
|
|
176
|
+
agents watchdog policy <sessionId> handsoff
|
|
177
|
+
`,
|
|
178
|
+
notes: `
|
|
179
|
+
Decision path (default, deterministic): a session is nudged only when its
|
|
180
|
+
transcript tail shows it ANNOUNCED an action but no tool call followed
|
|
181
|
+
(promise-without-toolcall) AND it is not waiting on the user. Completions and
|
|
182
|
+
open questions are skipped.
|
|
183
|
+
|
|
184
|
+
Safety gate: a nudge is delivered ONLY when the resolver can name the EXACT
|
|
185
|
+
split the agent lives in (tmux / iTerm / IDE terminal). Un-addressable stalls
|
|
186
|
+
(e.g. Ghostty with no tmux) are flagged for the menu-bar and SKIPPED — never
|
|
187
|
+
a guessed or frontmost target.
|
|
188
|
+
|
|
189
|
+
Policy: global auto-nudge defaults OFF (opt-in via 'enable' or --nudge).
|
|
190
|
+
Per-session: off (ignore), keep (default), handsoff (detect + flag, never inject).
|
|
191
|
+
|
|
192
|
+
State (tray-readable): ${path.join('~/.agents/.cache/state/watchdog', '{nudges,flags,last-tick}.json')}
|
|
193
|
+
`,
|
|
194
|
+
});
|
|
195
|
+
// --- global enable/disable/status -----------------------------------------
|
|
196
|
+
cmd.command('enable')
|
|
197
|
+
.description('Turn ON global auto-nudge (so `agents watchdog --watch` injects without --nudge).')
|
|
198
|
+
.action(() => {
|
|
199
|
+
setGloballyEnabled(true);
|
|
200
|
+
console.log(chalk.green('watchdog: global auto-nudge ENABLED'));
|
|
201
|
+
});
|
|
202
|
+
cmd.command('disable')
|
|
203
|
+
.description('Turn OFF global auto-nudge (back to detect-only unless --nudge is passed).')
|
|
204
|
+
.action(() => {
|
|
205
|
+
setGloballyEnabled(false);
|
|
206
|
+
console.log(chalk.yellow('watchdog: global auto-nudge DISABLED'));
|
|
207
|
+
});
|
|
208
|
+
cmd.command('status')
|
|
209
|
+
.description('Show whether global auto-nudge is on and where state is written.')
|
|
210
|
+
.option('--json', 'Emit status as JSON (for the menu-bar / scripts)')
|
|
211
|
+
.action((_opts, command) => {
|
|
212
|
+
// The parent `watchdog` command also declares --json and greedily parses it
|
|
213
|
+
// before dispatching here, so `watchdog status --json` lands the flag on the
|
|
214
|
+
// parent, not this subcommand. optsWithGlobals() merges both levels, so we
|
|
215
|
+
// read it correctly regardless of which command commander bound it to.
|
|
216
|
+
const json = command.optsWithGlobals().json === true;
|
|
217
|
+
const on = isGloballyEnabled();
|
|
218
|
+
if (json) {
|
|
219
|
+
console.log(JSON.stringify({ enabled: on, stateDir: stateDir() }));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
console.log(`global auto-nudge: ${on ? chalk.green('ON') : chalk.dim('off')}`);
|
|
223
|
+
console.log(`state dir: ${chalk.dim(stateDir())}`);
|
|
224
|
+
});
|
|
225
|
+
// --- per-session policy ----------------------------------------------------
|
|
226
|
+
cmd.command('policy <sessionId> <policy>')
|
|
227
|
+
.description('Set per-session policy: off (ignore) | keep (default) | handsoff (detect + flag, never inject).')
|
|
228
|
+
.action((sessionId, policy) => {
|
|
229
|
+
const p = policy.toLowerCase();
|
|
230
|
+
if (p !== 'off' && p !== 'keep' && p !== 'handsoff') {
|
|
231
|
+
console.error(chalk.red(`invalid policy '${policy}'. Use: off | keep | handsoff`));
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
writePolicySentinel(stateDir(), sessionId, p);
|
|
236
|
+
console.log(`watchdog: session ${chalk.bold(sessionId.slice(0, 8))} policy = ${chalk.cyan(p)}`);
|
|
237
|
+
});
|
|
238
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ if (IS_DEV_BUILD) {
|
|
|
51
51
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
52
52
|
// first byte of output), the registry maps a command name to a thunk that
|
|
53
53
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
54
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
54
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
55
55
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
56
56
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
57
57
|
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
@@ -183,6 +183,7 @@ Credentials and profiles:
|
|
|
183
183
|
|
|
184
184
|
Diagnostics:
|
|
185
185
|
doctor [agent[@version]] Diagnose CLI availability, sync status, and resource divergence
|
|
186
|
+
check CI drift gate: exit non-zero when resources are out of sync
|
|
186
187
|
usage [agent] Show rate-limit and quota usage per agent
|
|
187
188
|
|
|
188
189
|
Config sync:
|
|
@@ -190,6 +191,7 @@ Config sync:
|
|
|
190
191
|
pull Clone or pull the system repo at ~/.agents/.system/
|
|
191
192
|
repo init --path <dir> Scaffold your own editable repo from a template
|
|
192
193
|
repo add <path|gh:user/repo> Merge an extra repo after the system repo
|
|
194
|
+
lock [--frozen] Write/verify agents.lock (SHA-256 of resolved resources); --frozen fails on drift
|
|
193
195
|
|
|
194
196
|
Beta features:
|
|
195
197
|
beta Enable preview features (factory, drive, and more)
|
|
@@ -789,6 +791,7 @@ async function registerAllEagerCommands() {
|
|
|
789
791
|
await reg(loadTrash);
|
|
790
792
|
await reg(loadRestore);
|
|
791
793
|
await reg(loadDoctor);
|
|
794
|
+
await reg(loadCheck);
|
|
792
795
|
await reg(loadStatus);
|
|
793
796
|
registerExecAliasCommand(program);
|
|
794
797
|
await reg(loadProfiles);
|
|
@@ -798,6 +801,7 @@ async function registerAllEagerCommands() {
|
|
|
798
801
|
await reg(loadMenubar);
|
|
799
802
|
await reg(loadBeta);
|
|
800
803
|
await reg(loadSync);
|
|
804
|
+
await reg(loadLock);
|
|
801
805
|
await reg(loadRefreshRules);
|
|
802
806
|
await reg(loadDrive);
|
|
803
807
|
await reg(loadFactory);
|
|
@@ -807,11 +811,13 @@ async function registerAllEagerCommands() {
|
|
|
807
811
|
await reg(loadAlias);
|
|
808
812
|
await reg(loadPty);
|
|
809
813
|
await reg(loadTmux);
|
|
814
|
+
await reg(loadWatchdog);
|
|
810
815
|
await reg(loadBrowser);
|
|
811
816
|
await reg(loadComputer);
|
|
812
817
|
await reg(loadHosts);
|
|
813
818
|
await reg(loadLogs);
|
|
814
819
|
await reg(loadEvents);
|
|
820
|
+
await reg(loadAudit);
|
|
815
821
|
await reg(loadSsh);
|
|
816
822
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
817
823
|
registerJobsCronAliasCommand(program, 'cron');
|
|
@@ -983,7 +989,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
983
989
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
984
990
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
985
991
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
986
|
-
const sentinelValue = '
|
|
992
|
+
const sentinelValue = 'v11';
|
|
987
993
|
let needRun = true;
|
|
988
994
|
try {
|
|
989
995
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
@@ -1016,6 +1022,15 @@ if (process.platform === 'darwin' && process.env.AGENTS_SKIP_MIGRATION !== '1')
|
|
|
1016
1022
|
}
|
|
1017
1023
|
catch { /* never block CLI startup on the menu bar */ }
|
|
1018
1024
|
}
|
|
1025
|
+
// Bare invocation prints the root help. Commander only auto-displays help on
|
|
1026
|
+
// an empty parse when subcommands are registered, and the lazy-startup path
|
|
1027
|
+
// registers none for a bare call — without this branch, `agents` exits
|
|
1028
|
+
// silently. Runs after first-run setup and migrations so those still fire;
|
|
1029
|
+
// exits 0 to match `agents --help` (and the pre-fix exit code).
|
|
1030
|
+
if (passedArgs.length === 0) {
|
|
1031
|
+
program.outputHelp();
|
|
1032
|
+
process.exit(0);
|
|
1033
|
+
}
|
|
1019
1034
|
try {
|
|
1020
1035
|
await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
|
|
1021
1036
|
await program.parseAsync();
|
|
@@ -1036,6 +1051,14 @@ catch (err) {
|
|
|
1036
1051
|
console.error(err.message);
|
|
1037
1052
|
process.exit(1);
|
|
1038
1053
|
}
|
|
1054
|
+
// A --host targeting a password-auth device throws this from resolveHost.
|
|
1055
|
+
// It carries an actionable message (switch to key auth / enroll as a host);
|
|
1056
|
+
// handling it here covers every resolveHost caller (run, hosts check/rm,
|
|
1057
|
+
// secrets --host) at the source instead of a catch at each call site.
|
|
1058
|
+
if (err.name === 'DeviceOffloadUnsupportedError') {
|
|
1059
|
+
console.error(err.message);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1039
1062
|
}
|
|
1040
1063
|
throw err;
|
|
1041
1064
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent, tamper-evident audit log of every dispatched run (issue #347).
|
|
3
|
+
*
|
|
4
|
+
* Every run that reaches the single dispatch chokepoint in `agents run`
|
|
5
|
+
* (src/commands/exec.ts) appends ONE record here. Records form a hash chain:
|
|
6
|
+
* each record embeds the previous record's `hash` as `prevHash`, and its own
|
|
7
|
+
* `hash` is `sha256(canonicalJSON(record-without-hash))`. Because `prevHash`
|
|
8
|
+
* is part of the hashed payload, rewriting or reordering any record breaks the
|
|
9
|
+
* chain from that point forward — `verifyAuditChain()` reports the first index
|
|
10
|
+
* that fails to reproduce.
|
|
11
|
+
*
|
|
12
|
+
* Storage is append-only JSONL under the durable-runtime bucket
|
|
13
|
+
* (`~/.agents/.history/audit/log.jsonl`), one record per line. `.history/` is
|
|
14
|
+
* machine-local, gitignored, and NEVER synced by `agents repo push/pull` — so
|
|
15
|
+
* the `repo` field (a git remote url that can embed an access token) never
|
|
16
|
+
* lands in a version-controlled DotAgents repo, and a cross-machine pull can't
|
|
17
|
+
* splice a foreign chain into this one. The write path is deliberately cheap
|
|
18
|
+
* and non-fatal: a logging failure must never crash a run (see
|
|
19
|
+
* `recordDispatchedRun`). Concurrent writers (parallel teams/routines dispatch)
|
|
20
|
+
* are serialized by an advisory lock file so the chain never forks — see
|
|
21
|
+
* `appendAuditRecord`.
|
|
22
|
+
*
|
|
23
|
+
* Governance context: this is the evidence trail an operator needs to answer
|
|
24
|
+
* "which agent, at which version, ran against which repo, and did it succeed?"
|
|
25
|
+
* — the kind of automated-decision logging the EU AI Act (Art. 12) expects of
|
|
26
|
+
* high-risk systems, kept tamper-evident so the log itself can be trusted.
|
|
27
|
+
*/
|
|
28
|
+
/** First record's `prevHash` — a fixed anchor so the chain has a root. */
|
|
29
|
+
export declare const GENESIS_HASH = "GENESIS";
|
|
30
|
+
/** A run's outcome, derived from its process exit code. */
|
|
31
|
+
export type AuditOutcome = 'ok' | 'fail';
|
|
32
|
+
/** One immutable audit record. `hash` seals every other field, `prevHash` included. */
|
|
33
|
+
export interface AuditRecord {
|
|
34
|
+
ts: string;
|
|
35
|
+
agent: string;
|
|
36
|
+
version: string;
|
|
37
|
+
repo: string;
|
|
38
|
+
mode: string;
|
|
39
|
+
outcome: AuditOutcome;
|
|
40
|
+
exit: number;
|
|
41
|
+
prevHash: string;
|
|
42
|
+
hash: string;
|
|
43
|
+
}
|
|
44
|
+
/** The caller-supplied fields — the chain fields (`prevHash`/`hash`) are computed here. */
|
|
45
|
+
export type AuditEntry = Omit<AuditRecord, 'prevHash' | 'hash'>;
|
|
46
|
+
/**
|
|
47
|
+
* Absolute path to the append-only audit log. Lives under `.history/` — the
|
|
48
|
+
* durable-but-machine-local runtime bucket that is gitignored and never synced
|
|
49
|
+
* by `agents repo push/pull`. Keeping it here (not under a top-level, tracked
|
|
50
|
+
* `~/.agents/` path) means the token-bearing `repo` field can never leak into a
|
|
51
|
+
* version-controlled DotAgents repo, and no cross-machine pull can fork the chain.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getAuditLogPath(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Append one record to the hash chain and return it. Links to the previous
|
|
56
|
+
* record's `hash` (or `GENESIS_HASH` for the first), computes the sealing
|
|
57
|
+
* hash, and writes a single JSONL line. Synchronous so the record is durable
|
|
58
|
+
* before the caller proceeds.
|
|
59
|
+
*
|
|
60
|
+
* Read-last-hash + append run under the canonical advisory file lock
|
|
61
|
+
* (`withFileLock`, backed by proper-lockfile). Without it, two concurrent
|
|
62
|
+
* writers — the norm under parallel teams/routines dispatch — both read the
|
|
63
|
+
* same last hash and both write `prevHash=H`, forking the chain into a false
|
|
64
|
+
* "tampered" verdict. The lock forces a total order so every record links off
|
|
65
|
+
* the genuinely-previous one. `ensureLockTarget` creates the file first because
|
|
66
|
+
* proper-lockfile locks an existing path.
|
|
67
|
+
*/
|
|
68
|
+
export declare function appendAuditRecord(entry: AuditEntry, logPath?: string): AuditRecord;
|
|
69
|
+
/**
|
|
70
|
+
* Walk the chain and confirm every record reproduces. A record is valid when
|
|
71
|
+
* (a) its `prevHash` matches the prior record's `hash` (GENESIS for the first)
|
|
72
|
+
* AND (b) recomputing its sealing hash from its own fields reproduces the
|
|
73
|
+
* stored `hash`. Returns the first failing index in `brokenAt`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function verifyAuditChain(logPath?: string): {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
brokenAt?: number;
|
|
78
|
+
};
|
|
79
|
+
/** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
|
|
80
|
+
export declare function readAuditLog(logPath?: string): AuditRecord[];
|
|
81
|
+
/**
|
|
82
|
+
* Record ONE dispatched run at the single exec chokepoint. Non-fatal by
|
|
83
|
+
* contract: any failure is caught and warned, never thrown — an audit-log
|
|
84
|
+
* hiccup must not crash a run that already finished.
|
|
85
|
+
*/
|
|
86
|
+
export declare function recordDispatchedRun(run: {
|
|
87
|
+
agent: string;
|
|
88
|
+
version: string;
|
|
89
|
+
mode: string;
|
|
90
|
+
cwd: string;
|
|
91
|
+
exitCode: number;
|
|
92
|
+
}): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent, tamper-evident audit log of every dispatched run (issue #347).
|
|
3
|
+
*
|
|
4
|
+
* Every run that reaches the single dispatch chokepoint in `agents run`
|
|
5
|
+
* (src/commands/exec.ts) appends ONE record here. Records form a hash chain:
|
|
6
|
+
* each record embeds the previous record's `hash` as `prevHash`, and its own
|
|
7
|
+
* `hash` is `sha256(canonicalJSON(record-without-hash))`. Because `prevHash`
|
|
8
|
+
* is part of the hashed payload, rewriting or reordering any record breaks the
|
|
9
|
+
* chain from that point forward — `verifyAuditChain()` reports the first index
|
|
10
|
+
* that fails to reproduce.
|
|
11
|
+
*
|
|
12
|
+
* Storage is append-only JSONL under the durable-runtime bucket
|
|
13
|
+
* (`~/.agents/.history/audit/log.jsonl`), one record per line. `.history/` is
|
|
14
|
+
* machine-local, gitignored, and NEVER synced by `agents repo push/pull` — so
|
|
15
|
+
* the `repo` field (a git remote url that can embed an access token) never
|
|
16
|
+
* lands in a version-controlled DotAgents repo, and a cross-machine pull can't
|
|
17
|
+
* splice a foreign chain into this one. The write path is deliberately cheap
|
|
18
|
+
* and non-fatal: a logging failure must never crash a run (see
|
|
19
|
+
* `recordDispatchedRun`). Concurrent writers (parallel teams/routines dispatch)
|
|
20
|
+
* are serialized by an advisory lock file so the chain never forks — see
|
|
21
|
+
* `appendAuditRecord`.
|
|
22
|
+
*
|
|
23
|
+
* Governance context: this is the evidence trail an operator needs to answer
|
|
24
|
+
* "which agent, at which version, ran against which repo, and did it succeed?"
|
|
25
|
+
* — the kind of automated-decision logging the EU AI Act (Art. 12) expects of
|
|
26
|
+
* high-risk systems, kept tamper-evident so the log itself can be trusted.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
import { spawnSync } from 'child_process';
|
|
31
|
+
import { sha256 } from '../staleness/fingerprint.js';
|
|
32
|
+
import { getHistoryDir } from '../state.js';
|
|
33
|
+
import { ensureLockTarget, withFileLock } from '../fs-atomic.js';
|
|
34
|
+
/** First record's `prevHash` — a fixed anchor so the chain has a root. */
|
|
35
|
+
export const GENESIS_HASH = 'GENESIS';
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path to the append-only audit log. Lives under `.history/` — the
|
|
38
|
+
* durable-but-machine-local runtime bucket that is gitignored and never synced
|
|
39
|
+
* by `agents repo push/pull`. Keeping it here (not under a top-level, tracked
|
|
40
|
+
* `~/.agents/` path) means the token-bearing `repo` field can never leak into a
|
|
41
|
+
* version-controlled DotAgents repo, and no cross-machine pull can fork the chain.
|
|
42
|
+
*/
|
|
43
|
+
export function getAuditLogPath() {
|
|
44
|
+
return path.join(getHistoryDir(), 'audit', 'log.jsonl');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic JSON: object keys sorted recursively so the same logical
|
|
48
|
+
* record always serializes to the same bytes (hashing must be reproducible
|
|
49
|
+
* across processes and machines). Values here are all primitives, but the
|
|
50
|
+
* recursion keeps it correct if the record ever nests.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalJSON(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object')
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return '[' + value.map(canonicalJSON).join(',') + ']';
|
|
57
|
+
const obj = value;
|
|
58
|
+
const keys = Object.keys(obj).sort();
|
|
59
|
+
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
|
|
60
|
+
}
|
|
61
|
+
/** Compute the sealing hash for a record whose `prevHash` is already set. */
|
|
62
|
+
function hashRecord(record) {
|
|
63
|
+
return sha256(canonicalJSON(record));
|
|
64
|
+
}
|
|
65
|
+
/** Read + parse every record in the log, oldest-first. Missing file → []. */
|
|
66
|
+
function readRecords(logPath) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = fs.readFileSync(logPath, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return []; // no log yet
|
|
73
|
+
}
|
|
74
|
+
const records = [];
|
|
75
|
+
for (const line of raw.split('\n')) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
continue;
|
|
79
|
+
records.push(JSON.parse(trimmed));
|
|
80
|
+
}
|
|
81
|
+
return records;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Append one record to the hash chain and return it. Links to the previous
|
|
85
|
+
* record's `hash` (or `GENESIS_HASH` for the first), computes the sealing
|
|
86
|
+
* hash, and writes a single JSONL line. Synchronous so the record is durable
|
|
87
|
+
* before the caller proceeds.
|
|
88
|
+
*
|
|
89
|
+
* Read-last-hash + append run under the canonical advisory file lock
|
|
90
|
+
* (`withFileLock`, backed by proper-lockfile). Without it, two concurrent
|
|
91
|
+
* writers — the norm under parallel teams/routines dispatch — both read the
|
|
92
|
+
* same last hash and both write `prevHash=H`, forking the chain into a false
|
|
93
|
+
* "tampered" verdict. The lock forces a total order so every record links off
|
|
94
|
+
* the genuinely-previous one. `ensureLockTarget` creates the file first because
|
|
95
|
+
* proper-lockfile locks an existing path.
|
|
96
|
+
*/
|
|
97
|
+
export function appendAuditRecord(entry, logPath = getAuditLogPath()) {
|
|
98
|
+
ensureLockTarget(logPath);
|
|
99
|
+
return withFileLock(logPath, () => {
|
|
100
|
+
const existing = readRecords(logPath);
|
|
101
|
+
const prevHash = existing.length ? existing[existing.length - 1].hash : GENESIS_HASH;
|
|
102
|
+
const unsealed = { ...entry, prevHash };
|
|
103
|
+
const record = { ...unsealed, hash: hashRecord(unsealed) };
|
|
104
|
+
fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
|
|
105
|
+
return record;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Walk the chain and confirm every record reproduces. A record is valid when
|
|
110
|
+
* (a) its `prevHash` matches the prior record's `hash` (GENESIS for the first)
|
|
111
|
+
* AND (b) recomputing its sealing hash from its own fields reproduces the
|
|
112
|
+
* stored `hash`. Returns the first failing index in `brokenAt`.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyAuditChain(logPath = getAuditLogPath()) {
|
|
115
|
+
let records;
|
|
116
|
+
try {
|
|
117
|
+
records = readRecords(logPath);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// A line that won't even parse is itself tamper evidence — the log is corrupt.
|
|
121
|
+
return { ok: false, brokenAt: 0 };
|
|
122
|
+
}
|
|
123
|
+
let expectedPrev = GENESIS_HASH;
|
|
124
|
+
for (let i = 0; i < records.length; i++) {
|
|
125
|
+
const r = records[i];
|
|
126
|
+
if (r.prevHash !== expectedPrev)
|
|
127
|
+
return { ok: false, brokenAt: i };
|
|
128
|
+
const { hash, ...unsealed } = r;
|
|
129
|
+
if (hashRecord(unsealed) !== hash)
|
|
130
|
+
return { ok: false, brokenAt: i };
|
|
131
|
+
expectedPrev = r.hash;
|
|
132
|
+
}
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
/** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
|
|
136
|
+
export function readAuditLog(logPath = getAuditLogPath()) {
|
|
137
|
+
return readRecords(logPath);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a stable repo label for a run: the git remote origin url when the
|
|
141
|
+
* cwd is inside a repo with one, otherwise the cwd itself. Best-effort — any
|
|
142
|
+
* failure falls back to the cwd.
|
|
143
|
+
*/
|
|
144
|
+
function repoLabel(cwd) {
|
|
145
|
+
try {
|
|
146
|
+
const res = spawnSync('git', ['-C', cwd, 'config', '--get', 'remote.origin.url'], {
|
|
147
|
+
encoding: 'utf-8',
|
|
148
|
+
timeout: 2000,
|
|
149
|
+
});
|
|
150
|
+
const url = res.status === 0 ? res.stdout.trim() : '';
|
|
151
|
+
return url || cwd;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return cwd;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Record ONE dispatched run at the single exec chokepoint. Non-fatal by
|
|
159
|
+
* contract: any failure is caught and warned, never thrown — an audit-log
|
|
160
|
+
* hiccup must not crash a run that already finished.
|
|
161
|
+
*/
|
|
162
|
+
export function recordDispatchedRun(run) {
|
|
163
|
+
try {
|
|
164
|
+
appendAuditRecord({
|
|
165
|
+
ts: new Date().toISOString(),
|
|
166
|
+
agent: run.agent,
|
|
167
|
+
version: run.version,
|
|
168
|
+
repo: repoLabel(run.cwd),
|
|
169
|
+
mode: run.mode,
|
|
170
|
+
outcome: run.exitCode === 0 ? 'ok' : 'fail',
|
|
171
|
+
exit: run.exitCode,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
process.stderr.write(`[agents] audit log write failed: ${err.message}\n`);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/lib/auto-pull.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { getFetchCacheDir } from './state.js';
|
|
17
|
+
import { backgroundSpawnOptions } from './platform/process.js';
|
|
17
18
|
/** Where lock files and per-repo status markers live. */
|
|
18
19
|
function fetchStateDir() {
|
|
19
20
|
return getFetchCacheDir();
|
|
@@ -38,7 +39,7 @@ export function spawnDetachedSync() {
|
|
|
38
39
|
return;
|
|
39
40
|
try {
|
|
40
41
|
const child = spawn(process.execPath, [workerPath], {
|
|
41
|
-
|
|
42
|
+
...backgroundSpawnOptions(),
|
|
42
43
|
stdio: 'ignore',
|
|
43
44
|
env: process.env,
|
|
44
45
|
});
|
|
@@ -36,6 +36,16 @@ export declare function getRunningChromeInfo(profileName: string): {
|
|
|
36
36
|
pid: number;
|
|
37
37
|
port: number;
|
|
38
38
|
} | null;
|
|
39
|
+
/**
|
|
40
|
+
* Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
|
|
41
|
+
* (lsof doesn't exist there). Returns false on any tooling error so port
|
|
42
|
+
* allocation degrades to "assume free" rather than throwing.
|
|
43
|
+
*
|
|
44
|
+
* Exported as the canonical cross-platform probe — `findFreeProfilePort`
|
|
45
|
+
* (profiles.ts) must use this rather than shelling out to lsof directly,
|
|
46
|
+
* or every port scans as free on Windows.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isPortInUse(port: number): boolean;
|
|
39
49
|
export declare function allocatePort(): number;
|
|
40
50
|
export interface PortOccupant {
|
|
41
51
|
pid: number;
|