@phnx-labs/agents-cli 1.20.50 → 1.20.52
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 +40 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.js +7 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +66 -8
- package/dist/commands/factory.js +9 -4
- package/dist/commands/feed.d.ts +9 -0
- package/dist/commands/feed.js +69 -0
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +9 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +23 -0
- package/dist/commands/secrets.js +174 -70
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +5 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +15 -24
- package/dist/lib/cloud/rush.js +7 -32
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.d.ts +6 -0
- package/dist/lib/crabbox/lease.js +40 -10
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +73 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +288 -101
- package/dist/lib/exec.js +32 -14
- package/dist/lib/feed.d.ts +56 -0
- package/dist/lib/feed.js +251 -0
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +24 -10
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.js +10 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +38 -8
- package/dist/lib/secrets/icloud-import.d.ts +70 -0
- package/dist/lib/secrets/icloud-import.js +173 -0
- package/dist/lib/secrets/index.d.ts +36 -0
- package/dist/lib/secrets/index.js +99 -9
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +60 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +100 -11
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/types.d.ts +24 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -105,13 +105,19 @@ export function getHookShimPath(name) {
|
|
|
105
105
|
/**
|
|
106
106
|
* Generate (or refresh) the shim script for a hook. Idempotent — only writes
|
|
107
107
|
* when the content differs from what's on disk. Returns the absolute shim path.
|
|
108
|
+
*
|
|
109
|
+
* A shim is generated when the hook opts into caching (`cache`) and/or declares
|
|
110
|
+
* `matches:` predicates. When `matches` is present the shim gates execution on
|
|
111
|
+
* those predicates before running the underlying script (see `renderShim`);
|
|
112
|
+
* when `cache` is absent the shim is a thin pass-through wrapper that only
|
|
113
|
+
* applies the gate and forwards stdin/stdout unchanged.
|
|
108
114
|
*/
|
|
109
115
|
export function generateHookShim(args) {
|
|
110
116
|
const shimsDir = args.paths?.shimsDir ?? getHookShimsDir();
|
|
111
117
|
const cacheDir = args.paths?.cacheDir ?? getHookCacheDir();
|
|
112
118
|
const logsDir = args.paths?.logsDir ?? getLogsDir();
|
|
113
119
|
const shimPath = resolveContainedHookShimPath(shimsDir, args.name);
|
|
114
|
-
const content = renderShim(args.name, args.scriptPath, args.cache, { cacheDir, logsDir });
|
|
120
|
+
const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir });
|
|
115
121
|
fs.mkdirSync(shimsDir, { recursive: true });
|
|
116
122
|
let existing = null;
|
|
117
123
|
if (fs.existsSync(shimPath)) {
|
|
@@ -132,24 +138,204 @@ export function generateHookShim(args) {
|
|
|
132
138
|
}
|
|
133
139
|
return shimPath;
|
|
134
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The matches: gate, as a self-contained Python program run once per fire.
|
|
143
|
+
*
|
|
144
|
+
* Reads the hook's `matches:` block from $MATCHES_JSON and the event JSON from
|
|
145
|
+
* stdin, then prints `FIRE` or `SKIP`. A faithful port of `shouldFire()` in
|
|
146
|
+
* src/lib/hooks/match.ts — all declared predicates AND together, an empty block
|
|
147
|
+
* always fires, and the same ReDoS guard (`isSafeHookRegex`) rejects unsafe
|
|
148
|
+
* regexes to `SKIP`. Kept in double-quotes/apostrophe-free so it survives being
|
|
149
|
+
* embedded in a single-quoted `python -c '...'` argument in the shim. Behavioural
|
|
150
|
+
* parity with shouldFire() is pinned by a conformance test (match-parity.test.ts).
|
|
151
|
+
*
|
|
152
|
+
* On any exception it does NOT print SKIP — the shim treats a missing/garbled
|
|
153
|
+
* verdict as FIRE (fail-open), so a broken gate never silently disables a hook.
|
|
154
|
+
*/
|
|
155
|
+
const GATE_PY = `import json, os, re, subprocess, sys
|
|
156
|
+
|
|
157
|
+
def arr(v):
|
|
158
|
+
if v is None:
|
|
159
|
+
return []
|
|
160
|
+
return v if isinstance(v, list) else [v]
|
|
161
|
+
|
|
162
|
+
def max_group_depth(src):
|
|
163
|
+
depth = 0
|
|
164
|
+
mx = 0
|
|
165
|
+
escaped = False
|
|
166
|
+
in_class = False
|
|
167
|
+
for ch in src:
|
|
168
|
+
if escaped:
|
|
169
|
+
escaped = False
|
|
170
|
+
continue
|
|
171
|
+
if ch == chr(92):
|
|
172
|
+
escaped = True
|
|
173
|
+
continue
|
|
174
|
+
if ch == "[":
|
|
175
|
+
in_class = True
|
|
176
|
+
continue
|
|
177
|
+
if ch == "]":
|
|
178
|
+
in_class = False
|
|
179
|
+
continue
|
|
180
|
+
if in_class:
|
|
181
|
+
continue
|
|
182
|
+
if ch == "(":
|
|
183
|
+
depth += 1
|
|
184
|
+
if depth > mx:
|
|
185
|
+
mx = depth
|
|
186
|
+
elif ch == ")" and depth > 0:
|
|
187
|
+
depth -= 1
|
|
188
|
+
return mx
|
|
189
|
+
|
|
190
|
+
_NESTED = re.compile(r"\\((?:\\?:)?[^)]*[*+][?+*{,\\d}]*[^)]*\\)\\s*(?:[+*]|\\{\\d*,?\\d*\\})")
|
|
191
|
+
|
|
192
|
+
def is_safe(src):
|
|
193
|
+
if len(src) > 200:
|
|
194
|
+
return False
|
|
195
|
+
if max_group_depth(src) > 3:
|
|
196
|
+
return False
|
|
197
|
+
if _NESTED.search(src):
|
|
198
|
+
return False
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def compile_rx(src):
|
|
202
|
+
if not is_safe(src):
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
return re.compile(src)
|
|
206
|
+
except re.error:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def find_root(start):
|
|
210
|
+
d = os.path.abspath(start)
|
|
211
|
+
while True:
|
|
212
|
+
if os.path.exists(os.path.join(d, ".git")):
|
|
213
|
+
return d
|
|
214
|
+
parent = os.path.dirname(d)
|
|
215
|
+
if parent == d:
|
|
216
|
+
return None
|
|
217
|
+
d = parent
|
|
218
|
+
|
|
219
|
+
def git_dirty(cwd):
|
|
220
|
+
try:
|
|
221
|
+
out = subprocess.run(
|
|
222
|
+
["git", "status", "--porcelain"],
|
|
223
|
+
cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
224
|
+
)
|
|
225
|
+
return len(out.stdout.decode().strip()) > 0
|
|
226
|
+
except Exception:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def should_fire():
|
|
230
|
+
m = json.loads(os.environ.get("MATCHES_JSON") or "{}")
|
|
231
|
+
if not m:
|
|
232
|
+
return True
|
|
233
|
+
try:
|
|
234
|
+
inp = json.load(sys.stdin)
|
|
235
|
+
except Exception:
|
|
236
|
+
inp = {}
|
|
237
|
+
cwd = inp.get("cwd") or os.getcwd()
|
|
238
|
+
|
|
239
|
+
v = m.get("prompt_contains")
|
|
240
|
+
if v is not None:
|
|
241
|
+
if v not in (inp.get("prompt") or ""):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
v = m.get("prompt_matches")
|
|
245
|
+
if v is not None:
|
|
246
|
+
rx = compile_rx(v)
|
|
247
|
+
if rx is None or not rx.search(inp.get("prompt") or ""):
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
v = m.get("tool_name")
|
|
251
|
+
if v is not None:
|
|
252
|
+
allowed = arr(v)
|
|
253
|
+
if allowed:
|
|
254
|
+
tn = inp.get("tool_name")
|
|
255
|
+
if not tn or tn not in allowed:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
v = m.get("tool_args_match")
|
|
259
|
+
if v is not None:
|
|
260
|
+
ta = inp.get("tool_args")
|
|
261
|
+
ser = ta if isinstance(ta, str) else json.dumps(ta if ta is not None else "", separators=(",", ":"))
|
|
262
|
+
rx = compile_rx(v)
|
|
263
|
+
if rx is None or not rx.search(ser):
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
v = m.get("cwd_includes")
|
|
267
|
+
if v is not None:
|
|
268
|
+
needles = arr(v)
|
|
269
|
+
if needles and not any(n in cwd for n in needles):
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
v = m.get("project_has")
|
|
273
|
+
if v is not None:
|
|
274
|
+
root = find_root(cwd)
|
|
275
|
+
if not root or not os.path.exists(os.path.join(root, v)):
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
v = m.get("git_dirty")
|
|
279
|
+
if v is not None:
|
|
280
|
+
if bool(v) != git_dirty(cwd):
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
print("FIRE" if should_fire() else "SKIP")
|
|
286
|
+
`;
|
|
287
|
+
/**
|
|
288
|
+
* Gate-only pass-through tail: no caching, just run the underlying script with
|
|
289
|
+
* stdin forwarded and stdout/exit code propagated, plus one timing log line.
|
|
290
|
+
* Used when a hook declares \`matches:\` but no \`cache:\`.
|
|
291
|
+
*/
|
|
292
|
+
const PASSTHROUGH_TAIL = `now_ns() { "$PY" -c 'import time; print(int(time.time()*1e9))'; }
|
|
293
|
+
START_NS=$(now_ns)
|
|
294
|
+
EXIT=0
|
|
295
|
+
if printf '%s' "$STDIN_PAYLOAD" | "$SOURCE"; then
|
|
296
|
+
EXIT=0
|
|
297
|
+
else
|
|
298
|
+
EXIT=$?
|
|
299
|
+
fi
|
|
300
|
+
END_NS=$(now_ns)
|
|
301
|
+
MS=$(( (END_NS - START_NS) / 1000000 ))
|
|
302
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
303
|
+
LOG_FILE="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
304
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":%d,"cache":"%s","exit":%d}\\n' \\
|
|
305
|
+
"$TS" "$HOOK_NAME" "$MS" "none" "$EXIT" >>"$LOG_FILE" 2>/dev/null || true
|
|
306
|
+
|
|
307
|
+
exit "$EXIT"`;
|
|
135
308
|
/**
|
|
136
309
|
* Render the bash shim. Bash 3.2-compatible (macOS default). Uses Python for
|
|
137
310
|
* hashing + monotonic-ish nanosecond timing + portable mtime, resolved at
|
|
138
311
|
* runtime (python3, then python) so a Windows Microsoft Store `python3` alias
|
|
139
312
|
* stub — which exits non-zero without running — doesn't silently break caching.
|
|
313
|
+
*
|
|
314
|
+
* When `matches` is set, an early gate block evaluates the `matches:` predicates
|
|
315
|
+
* against the event JSON on stdin and exits 0 without running the script when
|
|
316
|
+
* they don't hold — this is the runtime enforcement of the documented `matches:`
|
|
317
|
+
* gating (mirrors `shouldFire()` in match.ts). When `cache` is null the shim is
|
|
318
|
+
* a gate-only pass-through: it forwards stdin to the script and its stdout back,
|
|
319
|
+
* with no cache read/write.
|
|
140
320
|
*/
|
|
141
|
-
function renderShim(name, scriptPath, cache, paths) {
|
|
142
|
-
const ttl = typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0);
|
|
143
|
-
const key = cache
|
|
144
|
-
const prefetch = cache
|
|
321
|
+
function renderShim(name, scriptPath, cache, matches, paths) {
|
|
322
|
+
const ttl = cache ? (typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0)) : 0;
|
|
323
|
+
const key = cache?.key ?? 'global';
|
|
324
|
+
const prefetch = cache?.prefetch ?? 'none';
|
|
145
325
|
const { cacheDir, logsDir } = paths;
|
|
326
|
+
const hasMatches = matches != null && Object.keys(matches).length > 0;
|
|
327
|
+
const matchesJson = hasMatches ? JSON.stringify(matches) : '';
|
|
146
328
|
// sh-escape: wrap in single quotes, escape any embedded single quotes.
|
|
147
329
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
330
|
+
const cacheHeader = cache
|
|
331
|
+
? `# Cache: key=${key} ttl=${ttl}s prefetch=${prefetch}`
|
|
332
|
+
: `# Cache: none (gate-only pass-through)`;
|
|
333
|
+
const matchesHeader = hasMatches ? `\n# Matches: ${matchesJson}` : '';
|
|
148
334
|
return `#!/usr/bin/env bash
|
|
149
335
|
# GENERATED by agents-cli. Do not edit — re-run \`agents hooks sync\` to refresh.
|
|
150
336
|
# Hook: ${name}
|
|
151
337
|
# Source: ${scriptPath}
|
|
152
|
-
|
|
338
|
+
${cacheHeader}${matchesHeader}
|
|
153
339
|
set -u
|
|
154
340
|
|
|
155
341
|
HOOK_NAME=${q(name)}
|
|
@@ -159,6 +345,7 @@ LOGS_DIR=${q(logsDir)}
|
|
|
159
345
|
TTL=${ttl}
|
|
160
346
|
PREFETCH=${q(prefetch)}
|
|
161
347
|
KEY_MODE=${q(key)}
|
|
348
|
+
MATCHES_JSON=${q(matchesJson)}
|
|
162
349
|
|
|
163
350
|
mkdir -p "$CACHE_DIR" "$LOGS_DIR"
|
|
164
351
|
|
|
@@ -178,7 +365,33 @@ done
|
|
|
178
365
|
# Read stdin once (Claude/Codex/Gemini pass JSON on stdin to every hook).
|
|
179
366
|
STDIN_PAYLOAD="$(cat || true)"
|
|
180
367
|
|
|
181
|
-
#
|
|
368
|
+
# --- matches: gate (issue #744 / RUSH-1506) -------------------------------
|
|
369
|
+
# Enforce the hook's declared \`matches:\` predicates at fire time. Mirrors
|
|
370
|
+
# shouldFire() in src/lib/hooks/match.ts: all declared predicates AND together;
|
|
371
|
+
# an empty/absent block always fires. When the predicates don't hold we exit 0
|
|
372
|
+
# WITHOUT running the script (a skipped hook is not an error). Fail-open: any
|
|
373
|
+
# gate-eval error runs the script, so a broken predicate can never silently
|
|
374
|
+
# disable a safety hook (e.g. git-guard).
|
|
375
|
+
if [ -n "$MATCHES_JSON" ]; then
|
|
376
|
+
_GATE="$(printf '%s' "$STDIN_PAYLOAD" | MATCHES_JSON="$MATCHES_JSON" "$PY" -c ${q(GATE_PY)} 2>/dev/null || printf FIRE)"
|
|
377
|
+
[ -z "$_GATE" ] && _GATE=FIRE
|
|
378
|
+
if [ "$_GATE" = SKIP ]; then
|
|
379
|
+
_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
380
|
+
_LOG_FILE="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
381
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":0,"cache":"skip","exit":0}\\n' \\
|
|
382
|
+
"$_TS" "$HOOK_NAME" >>"$_LOG_FILE" 2>/dev/null || true
|
|
383
|
+
exit 0
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
386
|
+
${cache ? CACHE_TAIL : PASSTHROUGH_TAIL}
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Cache tail: the full cache lookup / stale-while-revalidate / timing machinery.
|
|
391
|
+
* Emitted only when the hook opts into \`cache:\`. (When only \`matches:\` is set,
|
|
392
|
+
* PASSTHROUGH_TAIL runs instead — no cache read/write.)
|
|
393
|
+
*/
|
|
394
|
+
const CACHE_TAIL = `# Portable sha1 — \`shasum\` is Perl, missing on minimal Linux images;
|
|
182
395
|
# \`sha1sum\` is coreutils, missing on macOS. Truncate to 12 hex chars.
|
|
183
396
|
sha1_12() { "$PY" -c 'import hashlib,sys; print(hashlib.sha1(sys.stdin.read().encode()).hexdigest()[:12])'; }
|
|
184
397
|
|
|
@@ -267,7 +480,6 @@ printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":%d,"cache":"%s","exit":%
|
|
|
267
480
|
|
|
268
481
|
exit "$EXIT"
|
|
269
482
|
`;
|
|
270
|
-
}
|
|
271
483
|
/**
|
|
272
484
|
* Remove a hook's shim. Called by the registrar's garbage collection when a
|
|
273
485
|
* hook is renamed/deleted or has its `cache:` field removed.
|
package/dist/lib/hooks.js
CHANGED
|
@@ -153,10 +153,12 @@ import { getHookShimsDir } from './state.js';
|
|
|
153
153
|
/**
|
|
154
154
|
* Resolve the command path to register for a hook.
|
|
155
155
|
*
|
|
156
|
-
* Returns either the raw script path (
|
|
157
|
-
* the path to a generated
|
|
158
|
-
* side effect when `cache:` is configured
|
|
159
|
-
*
|
|
156
|
+
* Returns either the raw script path (neither `cache:` nor `matches:` set,
|
|
157
|
+
* legacy behavior) or the path to a generated wrapper shim. The shim is written
|
|
158
|
+
* as a side effect when `cache:` and/or `matches:` is configured — it enforces
|
|
159
|
+
* the `matches:` gate at fire time and layers the caching/timing machinery when
|
|
160
|
+
* `cache:` is set. The agent-native settings file gets the same shape either
|
|
161
|
+
* way — just a different command path.
|
|
160
162
|
*/
|
|
161
163
|
function resolveHookCommand(name, hookDef, resolveScript) {
|
|
162
164
|
const scriptPath = resolveScript(hookDef.script);
|
|
@@ -165,13 +167,20 @@ function resolveHookCommand(name, hookDef, resolveScript) {
|
|
|
165
167
|
if (!isValidHookShimName(name))
|
|
166
168
|
return null;
|
|
167
169
|
const cache = parseCacheConfig(hookDef.cache);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const matches = hookDef.matches;
|
|
171
|
+
const hasMatches = matches != null && Object.keys(matches).length > 0;
|
|
172
|
+
if (!cache && !hasMatches) {
|
|
173
|
+
// No caching and no matches: gate opted in — make sure a previously
|
|
174
|
+
// generated shim from an earlier `cache:`/`matches:` config is gone so the
|
|
175
|
+
// JSONL doesn't keep claiming hits.
|
|
171
176
|
removeHookShim(name);
|
|
172
177
|
return toPortableCommand(scriptPath);
|
|
173
178
|
}
|
|
174
|
-
|
|
179
|
+
// A shim is generated when the hook opts into caching and/or declares
|
|
180
|
+
// `matches:` predicates. The shim enforces the `matches:` gate at fire time
|
|
181
|
+
// (skipping the script when predicates don't hold) and, when `cache:` is set,
|
|
182
|
+
// layers the cache/timing machinery on top.
|
|
183
|
+
return toPortableCommand(generateHookShim({ name, scriptPath, cache, matches }));
|
|
175
184
|
}
|
|
176
185
|
/**
|
|
177
186
|
* Extensions that are NEVER hooks — docs, configuration, plain data. A file
|
|
@@ -1019,9 +1028,11 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1019
1028
|
const configDir = path.join(versionHome, configDirName);
|
|
1020
1029
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
1021
1030
|
let config = {};
|
|
1031
|
+
let existingRaw;
|
|
1022
1032
|
if (fs.existsSync(settingsPath)) {
|
|
1023
1033
|
try {
|
|
1024
|
-
|
|
1034
|
+
existingRaw = fs.readFileSync(settingsPath, 'utf-8');
|
|
1035
|
+
config = JSON.parse(existingRaw);
|
|
1025
1036
|
}
|
|
1026
1037
|
catch {
|
|
1027
1038
|
errors.push('Failed to parse settings.json');
|
|
@@ -1102,7 +1113,10 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1102
1113
|
}
|
|
1103
1114
|
try {
|
|
1104
1115
|
fs.mkdirSync(configDir, { recursive: true });
|
|
1105
|
-
|
|
1116
|
+
const nextRaw = JSON.stringify(config, null, 2);
|
|
1117
|
+
if (existingRaw !== nextRaw) {
|
|
1118
|
+
fs.writeFileSync(settingsPath, nextRaw, 'utf-8');
|
|
1119
|
+
}
|
|
1106
1120
|
}
|
|
1107
1121
|
catch (err) {
|
|
1108
1122
|
errors.push(`Failed to write settings.json: ${err.message}`);
|
|
@@ -34,6 +34,7 @@ const REMOTE_PASSTHROUGH = {
|
|
|
34
34
|
sync: { nonInteractive: ['--yes'] },
|
|
35
35
|
teams: {},
|
|
36
36
|
message: {},
|
|
37
|
+
feed: {},
|
|
37
38
|
};
|
|
38
39
|
/** `--no-tty` is stripped like the routing flags but carries no value. */
|
|
39
40
|
const STRIP_SPECS = [...HOST_ROUTING_SPECS, { long: 'no-tty', takesValue: false }];
|
|
@@ -14,7 +14,10 @@ export declare function needsWindowsShell(binary: string, platform?: NodeJS.Plat
|
|
|
14
14
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
15
15
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
16
16
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
17
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
17
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
18
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
19
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
20
|
+
* into the caller's terminal once per probe.
|
|
18
21
|
*/
|
|
19
22
|
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
20
23
|
/**
|
|
@@ -27,11 +27,17 @@ export function needsWindowsShell(binary, platform = process.platform) {
|
|
|
27
27
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
28
28
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
29
29
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
30
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
30
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
31
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
32
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
33
|
+
* into the caller's terminal once per probe.
|
|
31
34
|
*/
|
|
32
35
|
export function findExecutable(name, platform = process.platform) {
|
|
33
36
|
try {
|
|
34
|
-
const out = execFileSync(whichCommand(platform), [name], {
|
|
37
|
+
const out = execFileSync(whichCommand(platform), [name], {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
|
+
});
|
|
35
41
|
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
36
42
|
return first || null;
|
|
37
43
|
}
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -74,11 +74,3 @@ export interface GetAgentResourcesOptions {
|
|
|
74
74
|
* This is the source of truth - not the tracking data in agents.yaml.
|
|
75
75
|
*/
|
|
76
76
|
export declare function getAgentResources(agentId: AgentId, options?: GetAgentResourcesOptions): AgentResources;
|
|
77
|
-
/**
|
|
78
|
-
* Get resources for all agents.
|
|
79
|
-
*/
|
|
80
|
-
export declare function getAllAgentResources(agentIds: AgentId[], options?: GetAgentResourcesOptions & {
|
|
81
|
-
cliStates?: Record<AgentId, {
|
|
82
|
-
installed: boolean;
|
|
83
|
-
}>;
|
|
84
|
-
}): AgentResources[];
|
package/dist/lib/resources.js
CHANGED
|
@@ -180,13 +180,3 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
180
180
|
workflows,
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Get resources for all agents.
|
|
185
|
-
*/
|
|
186
|
-
export function getAllAgentResources(agentIds, options = {}) {
|
|
187
|
-
const { cliStates, ...restOptions } = options;
|
|
188
|
-
return agentIds.map((agentId) => {
|
|
189
|
-
const cliInstalled = cliStates?.[agentId]?.installed ?? true;
|
|
190
|
-
return getAgentResources(agentId, { ...restOptions, cliInstalled });
|
|
191
|
-
});
|
|
192
|
-
}
|
package/dist/lib/rotate.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getAccountInfo } from './agents.js';
|
|
|
10
10
|
import { readMeta, writeMeta, getHelpersDir } from './state.js';
|
|
11
11
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
12
12
|
import { getProjectRunConfigs } from './run-config.js';
|
|
13
|
+
import { emit } from './events.js';
|
|
13
14
|
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
14
15
|
function getRotateDir() {
|
|
15
16
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
@@ -388,6 +389,7 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd()) {
|
|
|
388
389
|
}
|
|
389
390
|
recordRotationPick(agent, rotation.picked.version);
|
|
390
391
|
}
|
|
392
|
+
emit('rotation.resolved', { module: 'rotate', agent, version: rotation.picked.version, strategy, healthy: rotation.healthy.length, excluded: rotation.excluded.length });
|
|
391
393
|
return { version: rotation.picked.version, rotation };
|
|
392
394
|
}
|
|
393
395
|
return { version: fallback, rotation: null };
|
package/dist/lib/runner.js
CHANGED
|
@@ -74,8 +74,16 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
74
74
|
appendModelAndReasoning(cmd, config);
|
|
75
75
|
}
|
|
76
76
|
if (config.agent === 'codex') {
|
|
77
|
-
if (mode === '
|
|
78
|
-
|
|
77
|
+
if (mode === 'plan') {
|
|
78
|
+
// The template defaults to workspace-write; plan means read-only.
|
|
79
|
+
const sbIndex = cmd.indexOf('--sandbox');
|
|
80
|
+
if (sbIndex !== -1)
|
|
81
|
+
cmd[sbIndex + 1] = 'read-only';
|
|
82
|
+
}
|
|
83
|
+
else if (mode === 'edit' || mode === 'auto') {
|
|
84
|
+
// Keep the workspace-write sandbox — no approval bypass; only skip drops
|
|
85
|
+
// the guardrails. Re-enable network, which workspace-write turns off.
|
|
86
|
+
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
79
87
|
}
|
|
80
88
|
else if (mode === 'skip') {
|
|
81
89
|
// Remove sandbox restriction, just --dangerously-bypass-approvals-and-sandbox
|
|
Binary file
|
|
Binary file
|
|
@@ -50,6 +50,18 @@ export declare const META_CACHE_PREFIX = "!meta:";
|
|
|
50
50
|
* cache on every bump and produced a recurring Touch ID storm.
|
|
51
51
|
*/
|
|
52
52
|
export declare function shouldSelfHealForUpgrade(persistent: boolean, storeSize: number, runningVersion: string, onDiskVersion: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Client-side twin of shouldSelfHealForUpgrade: whether ensureAgentRunning may
|
|
55
|
+
* tear down a reachable broker whose running version differs from the client's
|
|
56
|
+
* on-disk version. Only while it holds NO real unlocks — tearing down a hot
|
|
57
|
+
* broker wipes every held bundle, so the next read of each one re-prompts for
|
|
58
|
+
* Touch ID. On a machine where installed versions churn (dev builds stamp a
|
|
59
|
+
* fresh 0.0.0-dev.<sha> on every install; an npm copy and a dev copy invoke in
|
|
60
|
+
* turn), an unguarded teardown produced a rolling Touch ID storm — the exact
|
|
61
|
+
* failure #435 fixed on the server side. A hot, protocol-compatible broker
|
|
62
|
+
* keeps serving; its own sweep adopts the new code at the next quiet moment.
|
|
63
|
+
*/
|
|
64
|
+
export declare function shouldTeardownVersionSkewedBroker(realHeldBundles: number): boolean;
|
|
53
65
|
export interface StoredBundle {
|
|
54
66
|
bundle: SecretsBundle;
|
|
55
67
|
env: Record<string, string>;
|
|
@@ -172,6 +184,15 @@ export declare function agentGetSync(name: string): {
|
|
|
172
184
|
bundle: SecretsBundle;
|
|
173
185
|
env: Record<string, string>;
|
|
174
186
|
} | null;
|
|
187
|
+
/**
|
|
188
|
+
* Synchronously evict one bundle from the broker. Called after a mutating
|
|
189
|
+
* keychain write (add / rotate / remove / rename / delete) so the broker never
|
|
190
|
+
* keeps serving the pre-write snapshot for up to the ~7d hold — the next read
|
|
191
|
+
* re-resolves from the keychain (one prompt) and re-caches fresh values.
|
|
192
|
+
* Best-effort: no broker, no socket, or any failure is a silent no-op.
|
|
193
|
+
* macOS only.
|
|
194
|
+
*/
|
|
195
|
+
export declare function agentEvictSync(name: string): void;
|
|
175
196
|
/**
|
|
176
197
|
* Read the cached `secrets list` metadata snapshot for the given keychain
|
|
177
198
|
* name-set hash, or null on miss / no broker / off-darwin. Reuses the value
|
|
@@ -74,6 +74,20 @@ export function shouldSelfHealForUpgrade(persistent, storeSize, runningVersion,
|
|
|
74
74
|
return false;
|
|
75
75
|
return onDiskVersion !== runningVersion;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Client-side twin of shouldSelfHealForUpgrade: whether ensureAgentRunning may
|
|
79
|
+
* tear down a reachable broker whose running version differs from the client's
|
|
80
|
+
* on-disk version. Only while it holds NO real unlocks — tearing down a hot
|
|
81
|
+
* broker wipes every held bundle, so the next read of each one re-prompts for
|
|
82
|
+
* Touch ID. On a machine where installed versions churn (dev builds stamp a
|
|
83
|
+
* fresh 0.0.0-dev.<sha> on every install; an npm copy and a dev copy invoke in
|
|
84
|
+
* turn), an unguarded teardown produced a rolling Touch ID storm — the exact
|
|
85
|
+
* failure #435 fixed on the server side. A hot, protocol-compatible broker
|
|
86
|
+
* keeps serving; its own sweep adopts the new code at the next quiet moment.
|
|
87
|
+
*/
|
|
88
|
+
export function shouldTeardownVersionSkewedBroker(realHeldBundles) {
|
|
89
|
+
return realHeldBundles === 0;
|
|
90
|
+
}
|
|
77
91
|
function onDarwin() {
|
|
78
92
|
return process.platform === 'darwin';
|
|
79
93
|
}
|
|
@@ -581,6 +595,50 @@ export function agentGetSync(name) {
|
|
|
581
595
|
return null;
|
|
582
596
|
}
|
|
583
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Inline node program for the synchronous evict path. Mirrors SYNC_GET_PROGRAM:
|
|
600
|
+
* writeBundle is synchronous and called synchronously everywhere, so a stale
|
|
601
|
+
* broker entry must be evicted without awaiting a socket round-trip. Sends one
|
|
602
|
+
* {cmd:'lock', name} and exits 0 (evicted or nothing held) / 3 (agent down).
|
|
603
|
+
* argv after -e: [execPath, <socket>, <name>].
|
|
604
|
+
*/
|
|
605
|
+
const SYNC_LOCK_PROGRAM = `
|
|
606
|
+
const net = require('net');
|
|
607
|
+
const sock = process.argv[1], name = process.argv[2];
|
|
608
|
+
const c = net.createConnection(sock);
|
|
609
|
+
let buf = '';
|
|
610
|
+
const down = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
|
|
611
|
+
const timer = setTimeout(down, 2000);
|
|
612
|
+
c.on('error', down);
|
|
613
|
+
c.on('connect', () => c.write(JSON.stringify({ cmd: 'lock', name }) + '\\n'));
|
|
614
|
+
c.setEncoding('utf-8');
|
|
615
|
+
c.on('data', (d) => {
|
|
616
|
+
buf += d;
|
|
617
|
+
const nl = buf.indexOf('\\n');
|
|
618
|
+
if (nl < 0) return;
|
|
619
|
+
clearTimeout(timer);
|
|
620
|
+
try { c.destroy(); } catch (e) {}
|
|
621
|
+
process.exit(0);
|
|
622
|
+
});
|
|
623
|
+
`;
|
|
624
|
+
/**
|
|
625
|
+
* Synchronously evict one bundle from the broker. Called after a mutating
|
|
626
|
+
* keychain write (add / rotate / remove / rename / delete) so the broker never
|
|
627
|
+
* keeps serving the pre-write snapshot for up to the ~7d hold — the next read
|
|
628
|
+
* re-resolves from the keychain (one prompt) and re-caches fresh values.
|
|
629
|
+
* Best-effort: no broker, no socket, or any failure is a silent no-op.
|
|
630
|
+
* macOS only.
|
|
631
|
+
*/
|
|
632
|
+
export function agentEvictSync(name) {
|
|
633
|
+
if (!onDarwin())
|
|
634
|
+
return;
|
|
635
|
+
if (!agentSocketExists())
|
|
636
|
+
return;
|
|
637
|
+
try {
|
|
638
|
+
spawnSync(process.execPath, ['-e', SYNC_LOCK_PROGRAM, socketPath(), name], { timeout: 3000 });
|
|
639
|
+
}
|
|
640
|
+
catch { /* best-effort */ }
|
|
641
|
+
}
|
|
584
642
|
// Key inside the cached entry's env that holds the JSON metadata snapshot.
|
|
585
643
|
const META_SNAPSHOT_KEY = '__snapshot__';
|
|
586
644
|
/**
|
|
@@ -733,11 +791,15 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
|
|
|
733
791
|
// Self-heal: if a broker is reachable but running pre-upgrade code (its
|
|
734
792
|
// reported version != the version on disk now), tear it down so the paths
|
|
735
793
|
// below bring up a fresh one on current code. A current, reachable broker is
|
|
736
|
-
// accepted immediately
|
|
794
|
+
// accepted immediately — and so is a version-skewed one that still holds
|
|
795
|
+
// real unlocks (see shouldTeardownVersionSkewedBroker: wiping a hot cache
|
|
796
|
+
// re-prompts Touch ID for every held bundle).
|
|
737
797
|
const ping = await agentPing();
|
|
738
798
|
if (ping.reachable) {
|
|
739
799
|
if (ping.cliVersion === undefined || ping.cliVersion === getCliVersionFresh())
|
|
740
800
|
return true;
|
|
801
|
+
if (!shouldTeardownVersionSkewedBroker((await agentStatus()).length))
|
|
802
|
+
return true;
|
|
741
803
|
await teardownStaleBroker();
|
|
742
804
|
}
|
|
743
805
|
// Path 1: the persistent service. installSecretsAgentService is idempotent and
|
|
@@ -89,6 +89,9 @@ export interface LegacyBundleCandidate {
|
|
|
89
89
|
file: string;
|
|
90
90
|
keys: string[];
|
|
91
91
|
}
|
|
92
|
+
export declare const BUNDLE_NAME_PATTERN: RegExp;
|
|
93
|
+
export declare const ENV_KEY_PATTERN: RegExp;
|
|
94
|
+
export declare const BUNDLE_META_PREFIX = "agents-cli.bundles.";
|
|
92
95
|
export declare const RESERVED_ENV_NAMES: Set<string>;
|
|
93
96
|
export declare function bundleToEnvPrefix(name: string): string;
|
|
94
97
|
export declare function isReservedEnvName(key: string): boolean;
|
|
@@ -114,7 +117,29 @@ export declare function readBundle(name: string): SecretsBundle;
|
|
|
114
117
|
export declare function secretsDefaultPolicy(): SecretsPolicy;
|
|
115
118
|
/** The effective prompt policy of a bundle (absent ⇒ the configured default). */
|
|
116
119
|
export declare function bundlePolicy(bundle: SecretsBundle): SecretsPolicy;
|
|
117
|
-
|
|
120
|
+
/** Options for writeBundle. */
|
|
121
|
+
export interface WriteBundleOptions {
|
|
122
|
+
/**
|
|
123
|
+
* Skip evicting the bundle from the secrets-agent broker after the write.
|
|
124
|
+
* Only for writers that change nothing the broker serves — today that is
|
|
125
|
+
* stampLastUsed (a usage-telemetry timestamp, fired on every broker HIT):
|
|
126
|
+
* evicting there would make the cache destroy itself on first use. Every
|
|
127
|
+
* mutating writer (add / rotate / remove / rename / policy / import) must
|
|
128
|
+
* leave this unset so a broker-held copy never serves stale values for up
|
|
129
|
+
* to the ~7d hold.
|
|
130
|
+
*/
|
|
131
|
+
skipBrokerEviction?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a bundle write should evict the broker-held copy. Pure + exported
|
|
135
|
+
* for regression coverage. Skips when the writer opted out (stampLastUsed),
|
|
136
|
+
* when the broker integration is disabled (AGENTS_SECRETS_NO_AGENT — the same
|
|
137
|
+
* kill-switch the read fast-path honors), or when a test keychain backend is
|
|
138
|
+
* installed (an in-memory backend has no real keychain behind it, and a test
|
|
139
|
+
* writing bundle 'prod' must never evict the user's real 'prod' unlock).
|
|
140
|
+
*/
|
|
141
|
+
export declare function shouldEvictAfterBundleWrite(skipRequested: boolean, noAgentEnv: string | undefined, backendOverridden: boolean): boolean;
|
|
142
|
+
export declare function writeBundle(bundle: SecretsBundle, opts?: WriteBundleOptions): void;
|
|
118
143
|
export declare function deleteBundle(name: string): boolean;
|
|
119
144
|
export declare function listBundles(): SecretsBundle[];
|
|
120
145
|
export interface BundleEntryInfo {
|
|
@@ -140,6 +165,13 @@ export interface ResolveBundleOptions {
|
|
|
140
165
|
* needs live values. Also honored via AGENTS_SECRETS_NO_AGENT=1.
|
|
141
166
|
*/
|
|
142
167
|
noAgent?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Resolve only from an already-unlocked secrets-agent snapshot. If the
|
|
170
|
+
* broker has no snapshot, fail before touching Keychain or any other store.
|
|
171
|
+
* Background processes use this to guarantee they never surface a biometric
|
|
172
|
+
* prompt that nobody can answer.
|
|
173
|
+
*/
|
|
174
|
+
agentOnly?: boolean;
|
|
143
175
|
/**
|
|
144
176
|
* Inject only this subset of keys from the bundle. Keys not in this list are
|
|
145
177
|
* silently excluded from the returned env map. An error is thrown if any
|