@phnx-labs/agents-cli 1.22.8 → 1.22.10
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 +64 -4
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/exec.js +41 -9
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -1
- package/dist/commands/harness.js +316 -76
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/secrets.js +38 -39
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/ssh.js +30 -8
- package/dist/commands/teams.js +12 -1
- package/dist/commands/workflows.js +1 -1
- package/dist/index.js +9 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.js +18 -1
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/plugins.d.ts +14 -5
- package/dist/lib/plugins.js +29 -5
- package/dist/lib/redact.js +4 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources/workflows.d.ts +1 -1
- package/dist/lib/resources/workflows.js +22 -20
- 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/bundles.d.ts +9 -0
- package/dist/lib/secrets/bundles.js +9 -23
- package/dist/lib/secrets/headless.d.ts +3 -15
- package/dist/lib/secrets/headless.js +5 -33
- package/dist/lib/secrets/index.d.ts +8 -0
- package/dist/lib/secrets/index.js +16 -36
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/share/config.js +5 -7
- package/dist/lib/startup/command-registry.js +15 -1
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/dist/lib/workflows.d.ts +19 -4
- package/dist/lib/workflows.js +81 -7
- package/package.json +1 -1
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
5
|
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
-
* Resolution order: project > user > system.
|
|
6
|
+
* Resolution order (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
-
import { parseWorkflowFrontmatter, countWorkflowSubagents } from '../workflows.js';
|
|
11
|
+
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs, isBareWorkflowName, } from '../workflows.js';
|
|
12
12
|
function getLayerDirs(cwd) {
|
|
13
13
|
const projectDir = getProjectAgentsDir(cwd);
|
|
14
14
|
const extraRepos = getEnabledExtraRepos();
|
|
@@ -32,20 +32,27 @@ function listWorkflowsInDir(dir) {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/** Precedence-ordered (dir, layer) pairs for name lookup / listing. */
|
|
36
|
+
function orderedWorkflowSearchDirs(cwd) {
|
|
37
|
+
const dirs = getLayerDirs(cwd);
|
|
38
|
+
const out = [];
|
|
39
|
+
if (dirs.project)
|
|
40
|
+
out.push({ dir: dirs.project, layer: 'project' });
|
|
41
|
+
out.push({ dir: dirs.user, layer: 'user' });
|
|
42
|
+
for (const pluginDir of listPluginWorkflowDirs(cwd ?? process.cwd())) {
|
|
43
|
+
out.push({ dir: pluginDir, layer: 'plugin' });
|
|
44
|
+
}
|
|
45
|
+
for (const extraDir of dirs.extra)
|
|
46
|
+
out.push({ dir: extraDir, layer: 'system' });
|
|
47
|
+
out.push({ dir: dirs.system, layer: 'system' });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
35
50
|
class WorkflowsHandlerImpl {
|
|
36
51
|
kind = 'workflow';
|
|
37
52
|
listAll(_agent, cwd) {
|
|
38
|
-
const dirs = getLayerDirs(cwd);
|
|
39
53
|
const seen = new Set();
|
|
40
54
|
const results = [];
|
|
41
|
-
const
|
|
42
|
-
if (dirs.project)
|
|
43
|
-
layerDirs.push({ dir: dirs.project, layer: 'project' });
|
|
44
|
-
layerDirs.push({ dir: dirs.user, layer: 'user' });
|
|
45
|
-
layerDirs.push({ dir: dirs.system, layer: 'system' });
|
|
46
|
-
for (const extraDir of dirs.extra)
|
|
47
|
-
layerDirs.push({ dir: extraDir, layer: 'system' });
|
|
48
|
-
for (const { dir, layer } of layerDirs) {
|
|
55
|
+
for (const { dir, layer } of orderedWorkflowSearchDirs(cwd)) {
|
|
49
56
|
for (const { name, path: workflowPath } of listWorkflowsInDir(dir)) {
|
|
50
57
|
if (seen.has(name))
|
|
51
58
|
continue;
|
|
@@ -69,15 +76,10 @@ class WorkflowsHandlerImpl {
|
|
|
69
76
|
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
77
|
}
|
|
71
78
|
resolve(_agent, name, cwd) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
searchDirs.push({ dir: dirs.user, layer: 'user' });
|
|
77
|
-
searchDirs.push({ dir: dirs.system, layer: 'system' });
|
|
78
|
-
for (const extraDir of dirs.extra)
|
|
79
|
-
searchDirs.push({ dir: extraDir, layer: 'system' });
|
|
80
|
-
for (const { dir, layer } of searchDirs) {
|
|
79
|
+
// Same bare-name gate as resolveWorkflowRef — never path-join traversal refs.
|
|
80
|
+
if (!isBareWorkflowName(name))
|
|
81
|
+
return null;
|
|
82
|
+
for (const { dir, layer } of orderedWorkflowSearchDirs(cwd)) {
|
|
81
83
|
const workflowPath = path.join(dir, name);
|
|
82
84
|
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
83
85
|
if (fm) {
|
|
Binary file
|
|
Binary file
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import { type BundleValue, type SecretRef } from './index.js';
|
|
24
24
|
/** Which store carries a bundle's items. */
|
|
25
25
|
export type SecretsBackend = 'keychain' | 'file' | 'vault';
|
|
26
|
+
/** Disable the broker-only guard for in-memory keychain tests. */
|
|
27
|
+
export declare function setKeychainAgentOnlyBypassForTest(bypass: boolean): void;
|
|
26
28
|
/**
|
|
27
29
|
* Discover a bundle's backend by location: a file-backed bundle's metadata
|
|
28
30
|
* item exists in the encrypted-file store. This is a plain file-existence
|
|
@@ -184,6 +186,13 @@ export declare function deleteBundle(name: string): boolean;
|
|
|
184
186
|
* extra keychain read is issued. Exported for tests. Returns the count healed.
|
|
185
187
|
*/
|
|
186
188
|
export declare function healKeychainBundleMetadata(metaJsonByName: Map<string, string>): number;
|
|
189
|
+
/**
|
|
190
|
+
* One-time driver around healKeychainBundleMetadata (RUSH-1759). macOS + real
|
|
191
|
+
* keychain only — libsecret/CredMan have no biometry ACL to shed, and a test
|
|
192
|
+
* backend has no real keychain — and gated by a sentinel so it runs at most
|
|
193
|
+
* once. Best-effort: a heal failure never breaks bundle listing.
|
|
194
|
+
*/
|
|
195
|
+
export declare function healKeychainBundleMetadataAclOnce(metaJsonByName: Map<string, string>): void;
|
|
187
196
|
export declare function listBundles(): SecretsBundle[];
|
|
188
197
|
export interface BundleEntryInfo {
|
|
189
198
|
key: string;
|
|
@@ -86,6 +86,11 @@ const vaultStore = {
|
|
|
86
86
|
delete: vaultDeleteItem,
|
|
87
87
|
list: vaultListItems,
|
|
88
88
|
};
|
|
89
|
+
let keychainAgentOnlyBypassForTest = false;
|
|
90
|
+
/** Disable the broker-only guard for in-memory keychain tests. */
|
|
91
|
+
export function setKeychainAgentOnlyBypassForTest(bypass) {
|
|
92
|
+
keychainAgentOnlyBypassForTest = bypass;
|
|
93
|
+
}
|
|
89
94
|
function itemStore(backend) {
|
|
90
95
|
if (backend === 'file')
|
|
91
96
|
return fileItemStore;
|
|
@@ -606,7 +611,7 @@ export function healKeychainBundleMetadata(metaJsonByName) {
|
|
|
606
611
|
* backend has no real keychain — and gated by a sentinel so it runs at most
|
|
607
612
|
* once. Best-effort: a heal failure never breaks bundle listing.
|
|
608
613
|
*/
|
|
609
|
-
function healKeychainBundleMetadataAclOnce(metaJsonByName) {
|
|
614
|
+
export function healKeychainBundleMetadataAclOnce(metaJsonByName) {
|
|
610
615
|
if (metaJsonByName.size === 0)
|
|
611
616
|
return;
|
|
612
617
|
if (process.platform !== 'darwin')
|
|
@@ -684,7 +689,6 @@ export function listBundles() {
|
|
|
684
689
|
// still prompt once; it heals on the next interactive scan.)
|
|
685
690
|
const fetched = getKeychainTokens(keychainServices, { silentNoAcl: true });
|
|
686
691
|
const keychainBundles = [];
|
|
687
|
-
const metaJsonByName = new Map();
|
|
688
692
|
for (const service of keychainServices) {
|
|
689
693
|
const json = fetched.get(service);
|
|
690
694
|
if (json === undefined)
|
|
@@ -695,7 +699,6 @@ export function listBundles() {
|
|
|
695
699
|
const bundle = parseBundleMeta(nameHint, json, 'keychain');
|
|
696
700
|
if (bundle) {
|
|
697
701
|
keychainBundles.push(bundle);
|
|
698
|
-
metaJsonByName.set(bundle.name, json);
|
|
699
702
|
}
|
|
700
703
|
}
|
|
701
704
|
for (const bundle of keychainBundles)
|
|
@@ -707,13 +710,6 @@ export function listBundles() {
|
|
|
707
710
|
if (useAgent && keychainBundles.length > 0) {
|
|
708
711
|
agentAutoLoadMetaSync(nameSetHash, keychainBundles, secretsHoldMs());
|
|
709
712
|
}
|
|
710
|
-
// One-time RUSH-1759 heal: bundles written before the metadata-no-ACL
|
|
711
|
-
// change carry an ACL'd metadata item, so this fresh read (a broker miss)
|
|
712
|
-
// popped Touch ID just to enumerate them. Re-home each metadata item
|
|
713
|
-
// no-ACL now — reusing the JSON we just read, so the heal adds no extra
|
|
714
|
-
// prompt — and every later enumeration is silent. Runs at most once (a
|
|
715
|
-
// sentinel under the regenerable helpers dir).
|
|
716
|
-
healKeychainBundleMetadataAclOnce(metaJsonByName);
|
|
717
713
|
}
|
|
718
714
|
}
|
|
719
715
|
}
|
|
@@ -1099,24 +1095,14 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1099
1095
|
return filtered;
|
|
1100
1096
|
}
|
|
1101
1097
|
}
|
|
1102
|
-
// Never/no-ACL bundles remain prompt-free regardless.
|
|
1103
|
-
//
|
|
1104
|
-
// Explicit opt-in ONLY — a deliberate NARROWING of the agent-triggered approval
|
|
1105
|
-
// added in RUSH-2032 (b99796f8 removed this throw so an agent could raise the
|
|
1106
|
-
// sheet itself; 4eeada68 generalized the daemon rule into `!interactiveUnlock`).
|
|
1107
|
-
// That default — true whenever an agent name was present — was the spec, not a
|
|
1108
|
-
// bug. It is unwanted: each keychain read runs in its own helper process, so the
|
|
1109
|
-
// biometric assertion never reuses and one agent launch meant one sheet per
|
|
1110
|
-
// bundle. `agentOnly` decides alone now; a human in a plain shell carries no
|
|
1111
|
-
// AGENTS_RUNTIME, so isHeadlessSecretsContext() is false, agentOnly is false, the
|
|
1112
|
-
// guard never fires, and they still get their prompt. No caller passes this flag;
|
|
1113
|
-
// it remains the seam for a future unlock path that wants the sheet on purpose.
|
|
1098
|
+
// Never/no-ACL bundles remain prompt-free regardless. Every ordinary caller
|
|
1099
|
+
// sets agentOnly; only the unlock handler opts into interactive authentication.
|
|
1114
1100
|
const interactiveUnlock = opts.interactiveUnlock ?? false;
|
|
1115
1101
|
// A `never`-policy bundle's items carry no biometry ACL, so once the policy
|
|
1116
1102
|
// check below proves that, the batch read is silent even in a headless
|
|
1117
1103
|
// context — attest it to the raw-read storm guard via `silentNoAcl`.
|
|
1118
1104
|
let verifiedNoAclBundle = false;
|
|
1119
|
-
if (opts.agentOnly && backend === 'keychain' && !interactiveUnlock) {
|
|
1105
|
+
if (opts.agentOnly && backend === 'keychain' && !interactiveUnlock && !keychainAgentOnlyBypassForTest) {
|
|
1120
1106
|
try {
|
|
1121
1107
|
verifiedNoAclBundle = bundlePolicy(readBundle(name)) === 'never';
|
|
1122
1108
|
}
|
|
@@ -5,22 +5,13 @@
|
|
|
5
5
|
* importing bundles.ts (which already imports index.ts).
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* True when the current process
|
|
9
|
-
* must NEVER raise a Keychain biometry prompt on the
|
|
10
|
-
* a prompt nobody is watching. Two signals, either sufficient:
|
|
8
|
+
* True when the current process was structurally launched by an agent runtime
|
|
9
|
+
* that must NEVER raise a Keychain biometry prompt on the user's screen:
|
|
11
10
|
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
12
11
|
* launch, interactive included, and inherited by everything spawned beneath
|
|
13
12
|
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
14
13
|
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
15
14
|
* teams/agents.ts).
|
|
16
|
-
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
17
|
-
* stdio is redirected to a log — e.g. a release script run in the
|
|
18
|
-
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
19
|
-
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
20
|
-
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
21
|
-
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
22
|
-
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
23
|
-
* broker-only — the agent, not the human, is the caller there.
|
|
24
15
|
*
|
|
25
16
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
26
17
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -33,7 +24,4 @@
|
|
|
33
24
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
34
25
|
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
35
26
|
*/
|
|
36
|
-
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform
|
|
37
|
-
stdin?: boolean;
|
|
38
|
-
stdout?: boolean;
|
|
39
|
-
}): boolean;
|
|
27
|
+
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
@@ -5,22 +5,13 @@
|
|
|
5
5
|
* importing bundles.ts (which already imports index.ts).
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* True when the current process
|
|
9
|
-
* must NEVER raise a Keychain biometry prompt on the
|
|
10
|
-
* a prompt nobody is watching. Two signals, either sufficient:
|
|
8
|
+
* True when the current process was structurally launched by an agent runtime
|
|
9
|
+
* that must NEVER raise a Keychain biometry prompt on the user's screen:
|
|
11
10
|
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
12
11
|
* launch, interactive included, and inherited by everything spawned beneath
|
|
13
12
|
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
14
13
|
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
15
14
|
* teams/agents.ts).
|
|
16
|
-
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
17
|
-
* stdio is redirected to a log — e.g. a release script run in the
|
|
18
|
-
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
19
|
-
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
20
|
-
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
21
|
-
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
22
|
-
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
23
|
-
* broker-only — the agent, not the human, is the caller there.
|
|
24
15
|
*
|
|
25
16
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
26
17
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -33,31 +24,12 @@
|
|
|
33
24
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
34
25
|
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
35
26
|
*/
|
|
36
|
-
export function isHeadlessSecretsContext(env = process.env, platform = process.platform
|
|
37
|
-
// Injected so the TTY branch below is testable: it is the branch that decides a
|
|
38
|
-
// plain human shell still prompts, which is this guard's entire safety argument,
|
|
39
|
-
// and reading process.* directly made it unreachable from a test.
|
|
40
|
-
tty = { stdin: process.stdin.isTTY, stdout: process.stdout.isTTY }) {
|
|
27
|
+
export function isHeadlessSecretsContext(env = process.env, platform = process.platform) {
|
|
41
28
|
if (platform !== 'darwin')
|
|
42
29
|
return false; // no biometry prompt to suppress off-darwin
|
|
43
|
-
|
|
44
|
-
if (override === '1')
|
|
45
|
-
return true;
|
|
46
|
-
if (override === '0')
|
|
47
|
-
return false;
|
|
48
|
-
// Every AGENT-LAUNCH runtime resolves broker-only, interactive included.
|
|
49
|
-
// `terminal` was missing, which made an agent terminal the one launch path
|
|
50
|
-
// still allowed to pop Touch ID: exec.ts sets AGENTS_RUNTIME='terminal' for an
|
|
51
|
-
// interactive run (exec.ts:430), that fell through to the TTY check below, and
|
|
52
|
-
// a TTY meant "a human is watching, so prompting is fine". It is not fine —
|
|
53
|
-
// opening a terminal is not a request to authenticate, and a launch that needs
|
|
54
|
-
// a locked bundle should say so and point at `agents secrets unlock`, not grab
|
|
55
|
-
// the fingerprint sensor. AGENTS_RUNTIME is INHERITED by everything spawned under
|
|
56
|
-
// an agent, so `agents secrets export` run beneath one resolves broker-only too —
|
|
57
|
-
// correctly: there the agent, not the human, is the caller. A plain shell carries
|
|
58
|
-
// no AGENTS_RUNTIME, so a person running it themselves still gets the sheet.
|
|
30
|
+
// Every agent-launch runtime resolves broker-only, interactive included.
|
|
59
31
|
const runtime = env.AGENTS_RUNTIME;
|
|
60
32
|
if (runtime === 'headless' || runtime === 'teams' || runtime === 'terminal')
|
|
61
33
|
return true;
|
|
62
|
-
return
|
|
34
|
+
return false;
|
|
63
35
|
}
|
|
@@ -143,6 +143,14 @@ export declare function healHmacKeyNoAclOnce(rec: HmacKeyRecord): boolean;
|
|
|
143
143
|
* see readAndResolveBundleEnv.
|
|
144
144
|
*/
|
|
145
145
|
export declare function keychainServiceAlias(item: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* One-shot per process: activate hashing on machines with nothing to move,
|
|
148
|
+
* finish a crash-interrupted delete phase (silent), and run the interactive
|
|
149
|
+
* one-time re-key when cleartext-named items exist and a human is present.
|
|
150
|
+
* Never throws — a failed attempt leaves the process on cleartext names
|
|
151
|
+
* (exact pre-#316 behavior) and the next process retries.
|
|
152
|
+
*/
|
|
153
|
+
export declare function maybeAutoRekey(): void;
|
|
146
154
|
/** One re-keyed (or failed) item, by its old cleartext service name. */
|
|
147
155
|
export interface RekeyPlanItem {
|
|
148
156
|
oldService: string;
|
|
@@ -247,30 +247,7 @@ export function readHmacKeyRecord() {
|
|
|
247
247
|
catch {
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
// Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
|
|
252
|
-
// (pre the metadata/hmackey no-ACL migration fix) re-stamped this
|
|
253
|
-
// contractually-no-ACL item with a biometry ACL, so the read just above pops the
|
|
254
|
-
// generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
|
|
255
|
-
// `agents devices list` stats probe the SessionStart hook runs, and every other
|
|
256
|
-
// background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
|
|
257
|
-
// cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
|
|
258
|
-
// (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
|
|
259
|
-
// it never converged exactly these reads and the machine prompted forever.
|
|
260
|
-
// Re-store the record no-ACL once per machine here (the read that produced it has
|
|
261
|
-
// already happened — and already prompted if the item was ACL'd); every
|
|
262
|
-
// subsequent read, in this process and all future ones, is silent.
|
|
263
|
-
if (record && !record.healedNoAcl) {
|
|
264
|
-
try {
|
|
265
|
-
healHmacKeyNoAclOnce(record);
|
|
266
|
-
record.healedNoAcl = true;
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
// A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
|
|
270
|
-
// read) rather than a silent wrong state; the next process retries.
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return record;
|
|
250
|
+
return parseHmacKeyRecord(raw);
|
|
274
251
|
}
|
|
275
252
|
function writeHmacKeyRecord(rec) {
|
|
276
253
|
// JSON.stringify drops undefined fields (used to clear pendingDeletes).
|
|
@@ -325,15 +302,13 @@ function resolveHashState() {
|
|
|
325
302
|
export function keychainServiceAlias(item) {
|
|
326
303
|
return prepareServiceName(item);
|
|
327
304
|
}
|
|
328
|
-
function prepareServiceName(item
|
|
305
|
+
function prepareServiceName(item) {
|
|
329
306
|
if (rawScopeDepth > 0)
|
|
330
307
|
return item;
|
|
331
308
|
if (!isOurItem(item))
|
|
332
309
|
return item;
|
|
333
310
|
if (item === HMAC_KEY_ITEM || item.startsWith(HASHED_SERVICE_PREFIX))
|
|
334
311
|
return item;
|
|
335
|
-
if (opts?.autoRekey)
|
|
336
|
-
maybeAutoRekey();
|
|
337
312
|
const st = resolveHashState();
|
|
338
313
|
if (!st.active || !st.key)
|
|
339
314
|
return item;
|
|
@@ -354,7 +329,6 @@ function prepareListPrefix(prefix) {
|
|
|
354
329
|
return { prefix };
|
|
355
330
|
if (prefix.startsWith(HASHED_SERVICE_PREFIX))
|
|
356
331
|
return { prefix };
|
|
357
|
-
maybeAutoRekey();
|
|
358
332
|
const st = resolveHashState();
|
|
359
333
|
if (!st.active || !st.key)
|
|
360
334
|
return { prefix };
|
|
@@ -429,7 +403,7 @@ function finishPendingDeletes(rec) {
|
|
|
429
403
|
* Never throws — a failed attempt leaves the process on cleartext names
|
|
430
404
|
* (exact pre-#316 behavior) and the next process retries.
|
|
431
405
|
*/
|
|
432
|
-
function maybeAutoRekey() {
|
|
406
|
+
export function maybeAutoRekey() {
|
|
433
407
|
if (autoRekeyAttempted || rekeyRunning || rawScopeDepth > 0)
|
|
434
408
|
return;
|
|
435
409
|
autoRekeyAttempted = true;
|
|
@@ -446,10 +420,14 @@ function maybeAutoRekey() {
|
|
|
446
420
|
return;
|
|
447
421
|
const st = resolveHashState();
|
|
448
422
|
if (st.active) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
423
|
+
if (st.record && !st.record.healedNoAcl) {
|
|
424
|
+
try {
|
|
425
|
+
healHmacKeyNoAclOnce(st.record);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
/* the next explicit unlock retries */
|
|
429
|
+
}
|
|
430
|
+
}
|
|
453
431
|
if (st.record?.pendingDeletes?.length) {
|
|
454
432
|
try {
|
|
455
433
|
finishPendingDeletes(st.record);
|
|
@@ -820,7 +798,7 @@ export function getKeychainToken(item, context = {}) {
|
|
|
820
798
|
// Errors keep the requested (human-readable) name; the storage name may be
|
|
821
799
|
// an opaque hash.
|
|
822
800
|
const requested = item;
|
|
823
|
-
item = prepareServiceName(item
|
|
801
|
+
item = prepareServiceName(item);
|
|
824
802
|
if (backend)
|
|
825
803
|
return backend.get(item);
|
|
826
804
|
assertRawKeychainReadAllowed(requested, context);
|
|
@@ -848,6 +826,7 @@ export function getKeychainToken(item, context = {}) {
|
|
|
848
826
|
...process.env,
|
|
849
827
|
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
850
828
|
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
829
|
+
AGENTS_KEYCHAIN_SKIP_AUTH_UI: context.silentNoAcl ? '1' : '0',
|
|
851
830
|
},
|
|
852
831
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
853
832
|
});
|
|
@@ -890,7 +869,7 @@ export function getKeychainTokens(items, context = {}) {
|
|
|
890
869
|
// whether those were cleartext (hashed here) or already-hashed (enumerated).
|
|
891
870
|
const requestedByStorage = new Map();
|
|
892
871
|
const storageItems = items.map((item) => {
|
|
893
|
-
const storage = prepareServiceName(item
|
|
872
|
+
const storage = prepareServiceName(item);
|
|
894
873
|
if (!requestedByStorage.has(storage))
|
|
895
874
|
requestedByStorage.set(storage, item);
|
|
896
875
|
return storage;
|
|
@@ -939,6 +918,7 @@ export function getKeychainTokens(items, context = {}) {
|
|
|
939
918
|
...process.env,
|
|
940
919
|
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
941
920
|
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
921
|
+
AGENTS_KEYCHAIN_SKIP_AUTH_UI: context.silentNoAcl ? '1' : '0',
|
|
942
922
|
// The signed helper's own vocabulary is unchanged (it predates the rename
|
|
943
923
|
// and ships as a separately-versioned binary), so map to its legacy token.
|
|
944
924
|
AGENTS_KEYCHAIN_DEFAULT_POLICY: (context.defaultPolicy ?? 'hold') === 'hold' ? 'daily' : context.defaultPolicy,
|
|
@@ -1037,7 +1017,7 @@ export function setKeychainToken(item, value, opts) {
|
|
|
1037
1017
|
if (/[\x00=\r\n]/.test(item))
|
|
1038
1018
|
throw new Error('Secret item name contains invalid characters.');
|
|
1039
1019
|
const requested = item;
|
|
1040
|
-
item = prepareServiceName(item
|
|
1020
|
+
item = prepareServiceName(item);
|
|
1041
1021
|
if (backend) {
|
|
1042
1022
|
backend.set(item, value, opts);
|
|
1043
1023
|
return;
|
|
@@ -31,7 +31,11 @@ export declare function safeReadSessionFile(filePath: string, maxBytes?: number)
|
|
|
31
31
|
/**
|
|
32
32
|
* Auto-detect agent type from file path and parse the session.
|
|
33
33
|
*/
|
|
34
|
-
export
|
|
34
|
+
export interface ParseSessionOptions {
|
|
35
|
+
/** Keep normalized tool results compact by default; renderers can request full output. */
|
|
36
|
+
maxToolOutputChars?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function parseSession(filePath: string, agent?: SessionAgentId, opts?: ParseSessionOptions): SessionEvent[];
|
|
35
39
|
/** Infer the agent type from a session file path using known directory conventions. */
|
|
36
40
|
export declare function detectAgent(filePath: string): SessionAgentId | null;
|
|
37
41
|
/**
|
|
@@ -112,10 +112,12 @@ export function safeReadSessionFile(filePath, maxBytes = SESSION_FILE_MAX_BYTES)
|
|
|
112
112
|
}
|
|
113
113
|
return fs.readFileSync(filePath, 'utf-8');
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
function truncateNormalizedToolOutput(output, maxChars) {
|
|
116
|
+
if (!Number.isFinite(maxChars) || output.length <= maxChars)
|
|
117
|
+
return output;
|
|
118
|
+
return `${output.slice(0, maxChars)}\n\n[Output truncated: ${output.length - maxChars} characters omitted.]`;
|
|
119
|
+
}
|
|
120
|
+
export function parseSession(filePath, agent, opts = {}) {
|
|
119
121
|
const detected = agent || detectAgent(filePath);
|
|
120
122
|
if (!detected) {
|
|
121
123
|
throw new Error(`Cannot detect agent type from path: ${filePath}`);
|
|
@@ -166,7 +168,11 @@ export function parseSession(filePath, agent) {
|
|
|
166
168
|
// from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
|
|
167
169
|
// slicing and `--include user` count only genuine user intent — one place,
|
|
168
170
|
// every harness, instead of per-consumer regex.
|
|
171
|
+
const maxToolOutputChars = opts.maxToolOutputChars ?? 500;
|
|
169
172
|
for (const e of events) {
|
|
173
|
+
if (e.type === 'tool_result' && e.output) {
|
|
174
|
+
e.output = truncateNormalizedToolOutput(e.output, maxToolOutputChars);
|
|
175
|
+
}
|
|
170
176
|
sanitizeEvent(e);
|
|
171
177
|
if (e.type === 'message' && e.role === 'user' && isSyntheticUserMessage(e.content)) {
|
|
172
178
|
e._synthetic = true;
|
|
@@ -496,7 +502,11 @@ export function parseClaudeContent(content) {
|
|
|
496
502
|
exitCode: structured.exitCode,
|
|
497
503
|
statusCode: structured.statusCode,
|
|
498
504
|
errorCode: structured.errorCode,
|
|
499
|
-
|
|
505
|
+
// Not truncated here: `parseSession` caps every event's `output`
|
|
506
|
+
// centrally via `maxToolOutputChars` (default 500, the same
|
|
507
|
+
// bound this site used to hardcode), so the render path can ask
|
|
508
|
+
// for the full text with `Infinity`.
|
|
509
|
+
output,
|
|
500
510
|
});
|
|
501
511
|
}
|
|
502
512
|
if (toolId)
|
|
@@ -686,7 +696,7 @@ export function parseCodexContent(content) {
|
|
|
686
696
|
exitCode: result.exitCode,
|
|
687
697
|
statusCode: result.statusCode,
|
|
688
698
|
errorCode: result.errorCode,
|
|
689
|
-
output: result.text
|
|
699
|
+
output: result.text,
|
|
690
700
|
});
|
|
691
701
|
if (callId)
|
|
692
702
|
callMap.delete(callId);
|
|
@@ -741,7 +751,7 @@ export function parseCodexContent(content) {
|
|
|
741
751
|
exitCode: result.exitCode,
|
|
742
752
|
statusCode: result.statusCode,
|
|
743
753
|
errorCode: result.errorCode,
|
|
744
|
-
output: result.text
|
|
754
|
+
output: result.text,
|
|
745
755
|
});
|
|
746
756
|
if (callId)
|
|
747
757
|
callMap.delete(callId);
|
|
@@ -865,7 +875,7 @@ export function parseGemini(filePath) {
|
|
|
865
875
|
tool: toolName,
|
|
866
876
|
callId: typeof tc.id === 'string' ? tc.id : undefined,
|
|
867
877
|
success: tc.status === 'success',
|
|
868
|
-
output
|
|
878
|
+
output,
|
|
869
879
|
});
|
|
870
880
|
}
|
|
871
881
|
}
|
|
@@ -1242,7 +1252,7 @@ export function parseGrok(filePath) {
|
|
|
1242
1252
|
callId,
|
|
1243
1253
|
success: structuredError ? false : structuredSuccess ? true : undefined,
|
|
1244
1254
|
outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
|
|
1245
|
-
output: output
|
|
1255
|
+
output: output,
|
|
1246
1256
|
});
|
|
1247
1257
|
if (callId)
|
|
1248
1258
|
toolCallMap.delete(callId);
|
|
@@ -1360,7 +1370,7 @@ export function parseOpenCode(filePath) {
|
|
|
1360
1370
|
tool: toolName,
|
|
1361
1371
|
callId,
|
|
1362
1372
|
success: state.status === 'completed',
|
|
1363
|
-
output: outputStr
|
|
1373
|
+
output: outputStr,
|
|
1364
1374
|
});
|
|
1365
1375
|
}
|
|
1366
1376
|
break;
|
|
@@ -1470,7 +1480,7 @@ export function parseRush(filePath) {
|
|
|
1470
1480
|
tool: info?.tool ?? (typeof raw.name === 'string' ? raw.name : 'unknown'),
|
|
1471
1481
|
callId,
|
|
1472
1482
|
success,
|
|
1473
|
-
output: outputStr
|
|
1483
|
+
output: outputStr,
|
|
1474
1484
|
});
|
|
1475
1485
|
if (callId)
|
|
1476
1486
|
toolCallMap.delete(callId);
|
|
@@ -1680,7 +1690,7 @@ export function parseKimi(filePath) {
|
|
|
1680
1690
|
callId,
|
|
1681
1691
|
success: structuredError ? false : structuredSuccess ? true : undefined,
|
|
1682
1692
|
outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
|
|
1683
|
-
output: output
|
|
1693
|
+
output: output,
|
|
1684
1694
|
});
|
|
1685
1695
|
if (callId) {
|
|
1686
1696
|
toolCallMap.delete(callId);
|
|
@@ -1825,7 +1835,7 @@ function parseAnthropicMessageJsonl(filePath, agent) {
|
|
|
1825
1835
|
else {
|
|
1826
1836
|
events.push({
|
|
1827
1837
|
type: 'tool_result', agent, timestamp, tool: toolInfo?.tool, callId: toolId, success: true,
|
|
1828
|
-
output: output
|
|
1838
|
+
output: output,
|
|
1829
1839
|
});
|
|
1830
1840
|
}
|
|
1831
1841
|
if (toolId)
|
|
@@ -46,6 +46,11 @@ const SYNTHETIC_USER_MESSAGE_PATTERNS = [
|
|
|
46
46
|
/^\s*<\/?user-prompt-submit-hook>/i,
|
|
47
47
|
/^\s*<\/?persisted-output>/i,
|
|
48
48
|
/^\s*<permissions instructions>/i,
|
|
49
|
+
/^\s*<(?:apps|plugins|skills)_instructions>/i,
|
|
50
|
+
/^\s*<(?:multi_agent_mode|environment_context)>/i,
|
|
51
|
+
/^\s*## (?:In-flight in this repo|Host & Fleet)/i,
|
|
52
|
+
/^\s*Your current session id is\b/i,
|
|
53
|
+
/^\s*Linear context skipped:/i,
|
|
49
54
|
/^\s*<collaboration_mode>/i,
|
|
50
55
|
/^\s*Caveat: The messages below were generated by the user while running/i,
|
|
51
56
|
/^\s*\[Request interrupted/i,
|
|
@@ -108,6 +108,9 @@ export declare function filterEvents(events: SessionEvent[], opts: FilterOptions
|
|
|
108
108
|
*/
|
|
109
109
|
export interface RenderConversationMarkdownOptions {
|
|
110
110
|
redact?: boolean;
|
|
111
|
+
knownSecrets?: readonly string[];
|
|
112
|
+
reasoning?: 'omit' | 'fold' | 'include';
|
|
113
|
+
maxToolOutputChars?: number;
|
|
111
114
|
}
|
|
112
115
|
export declare function renderConversationMarkdown(events: SessionEvent[], opts?: RenderConversationMarkdownOptions): string;
|
|
113
116
|
/**
|
|
@@ -923,10 +923,23 @@ export function filterEvents(events, opts) {
|
|
|
923
923
|
const sliced = applyTurnSlice(events, opts);
|
|
924
924
|
return applyRoleFilter(sliced, opts);
|
|
925
925
|
}
|
|
926
|
+
function markdownFence(content, language = '') {
|
|
927
|
+
const fence = content.includes('```') ? '````' : '```';
|
|
928
|
+
return `${fence}${language}\n${content}\n${fence}`;
|
|
929
|
+
}
|
|
930
|
+
function truncateToolOutput(content, maxChars) {
|
|
931
|
+
if (content.length <= maxChars)
|
|
932
|
+
return content;
|
|
933
|
+
return `${content.slice(0, maxChars)}\n\n[Output truncated: ${content.length - maxChars} characters omitted.]`;
|
|
934
|
+
}
|
|
926
935
|
export function renderConversationMarkdown(events, opts = {}) {
|
|
927
936
|
const parts = [];
|
|
928
937
|
const shouldRedact = opts.redact !== false;
|
|
929
|
-
const sanitize = (text) => shouldRedact ? redactSecrets(text) : text;
|
|
938
|
+
const sanitize = (text) => shouldRedact ? redactSecrets(text, opts.knownSecrets) : text;
|
|
939
|
+
// Preserve the long-standing `sessions <id> --markdown` full-fidelity default.
|
|
940
|
+
// The shareable `sessions render` surface passes `omit` explicitly.
|
|
941
|
+
const reasoning = opts.reasoning ?? 'include';
|
|
942
|
+
const maxToolOutputChars = opts.maxToolOutputChars ?? 4000;
|
|
930
943
|
for (const event of events) {
|
|
931
944
|
if (event.type === 'message') {
|
|
932
945
|
if (event.role === 'user') {
|
|
@@ -937,27 +950,37 @@ export function renderConversationMarkdown(events, opts = {}) {
|
|
|
937
950
|
}
|
|
938
951
|
}
|
|
939
952
|
else if (event.type === 'thinking') {
|
|
940
|
-
if (event.content)
|
|
941
|
-
parts.push(`###
|
|
953
|
+
if (event.content && reasoning === 'include') {
|
|
954
|
+
parts.push(`### Reasoning\n\n${sanitize(event.content)}`);
|
|
955
|
+
}
|
|
956
|
+
else if (event.content && reasoning === 'fold') {
|
|
957
|
+
parts.push(`<details>\n<summary>Reasoning</summary>\n\n${sanitize(event.content)}\n\n</details>`);
|
|
958
|
+
}
|
|
942
959
|
}
|
|
943
960
|
else if (event.type === 'tool_use') {
|
|
944
961
|
const tool = event.tool || 'unknown';
|
|
945
962
|
if (event.command) {
|
|
946
|
-
|
|
963
|
+
const args = event.args && Object.keys(event.args).length > 0
|
|
964
|
+
? `\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`
|
|
965
|
+
: '';
|
|
966
|
+
parts.push(`### Tool: ${tool}\n\n${markdownFence(sanitize(event.command), 'bash')}${args}`);
|
|
967
|
+
}
|
|
968
|
+
else if (event.args && Object.keys(event.args).length > 0) {
|
|
969
|
+
parts.push(`### Tool: ${tool}\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`);
|
|
947
970
|
}
|
|
948
971
|
else if (event.path) {
|
|
949
|
-
parts.push(`### Tool: ${tool}\n\n\`${shortenPathTrace(event.path)}\``);
|
|
972
|
+
parts.push(`### Tool: ${tool}\n\n\`${sanitize(shortenPathTrace(event.path))}\``);
|
|
950
973
|
}
|
|
951
974
|
else {
|
|
952
975
|
const summary = summarizeToolUse(tool, event.args);
|
|
953
|
-
parts.push(`### Tool: ${tool}\n\n${summary}`);
|
|
976
|
+
parts.push(`### Tool: ${tool}\n\n${sanitize(summary)}`);
|
|
954
977
|
}
|
|
955
978
|
}
|
|
956
979
|
else if (event.type === 'tool_result') {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const body = sanitize(
|
|
960
|
-
parts.push(`### Tool Result\n\n
|
|
980
|
+
const output = event.content || event.output;
|
|
981
|
+
if (output) {
|
|
982
|
+
const body = sanitize(truncateToolOutput(output, maxToolOutputChars));
|
|
983
|
+
parts.push(`### Tool Result${event.tool ? `: ${event.tool}` : ''}\n\n${markdownFence(body)}`);
|
|
961
984
|
}
|
|
962
985
|
}
|
|
963
986
|
else if (event.type === 'error') {
|