@phnx-labs/agents-cli 1.22.40 → 1.22.42

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.
Files changed (201) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +14 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +0 -1
  5. package/dist/cli/command-registry.d.ts +2 -0
  6. package/dist/cli/command-registry.js +5 -2
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.js +1 -1
  10. package/dist/commands/auth.d.ts +9 -0
  11. package/dist/commands/auth.js +104 -0
  12. package/dist/commands/browser-sessions-picker.js +42 -62
  13. package/dist/commands/browser.d.ts +15 -0
  14. package/dist/commands/browser.js +145 -78
  15. package/dist/commands/computer-actions.d.ts +1 -1
  16. package/dist/commands/computer-actions.js +1 -1
  17. package/dist/commands/computer-sessions-picker.js +32 -55
  18. package/dist/commands/computer.d.ts +1 -1
  19. package/dist/commands/computer.js +2 -2
  20. package/dist/commands/doctor.d.ts +1 -1
  21. package/dist/commands/doctor.js +3 -3
  22. package/dist/commands/feed.js +4 -12
  23. package/dist/commands/hooks.js +1 -1
  24. package/dist/commands/import.js +1 -1
  25. package/dist/commands/insights.js +163 -85
  26. package/dist/commands/inspect.js +2 -2
  27. package/dist/commands/mine.js +1 -1
  28. package/dist/commands/org.d.ts +6 -0
  29. package/dist/commands/org.js +174 -0
  30. package/dist/commands/packages.js +1 -1
  31. package/dist/commands/prune.js +1 -1
  32. package/dist/commands/routines.js +1 -1
  33. package/dist/commands/run-account-picker.d.ts +19 -0
  34. package/dist/commands/run-account-picker.js +81 -0
  35. package/dist/commands/secrets.js +87 -97
  36. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  37. package/dist/commands/sessions-picker-factory.js +67 -0
  38. package/dist/commands/sessions-picker.d.ts +1 -0
  39. package/dist/commands/sessions-picker.js +27 -5
  40. package/dist/commands/sessions-share.d.ts +25 -0
  41. package/dist/commands/sessions-share.js +166 -0
  42. package/dist/commands/sessions.js +2 -0
  43. package/dist/commands/setup-browser.js +5 -5
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +4 -4
  46. package/dist/commands/setup.js +5 -3
  47. package/dist/commands/sync.js +1 -1
  48. package/dist/commands/versions.js +1 -1
  49. package/dist/commands/view.js +1 -1
  50. package/dist/commands/webhook.js +14 -9
  51. package/dist/lib/account-capabilities.d.ts +10 -0
  52. package/dist/lib/account-capabilities.js +34 -0
  53. package/dist/lib/agent-spec/agents.d.ts +584 -0
  54. package/dist/lib/agent-spec/agents.js +3127 -0
  55. package/dist/lib/agent-spec/provider.js +1 -1
  56. package/dist/lib/agents.d.ts +2 -584
  57. package/dist/lib/agents.js +2 -3127
  58. package/dist/lib/browser/cdp.js +4 -0
  59. package/dist/lib/browser/chrome.js +12 -1
  60. package/dist/lib/browser/drivers/local.d.ts +8 -2
  61. package/dist/lib/browser/drivers/local.js +8 -2
  62. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  63. package/dist/lib/browser/drivers/ssh.js +11 -4
  64. package/dist/lib/browser/hygiene.d.ts +1 -1
  65. package/dist/lib/browser/hygiene.js +5 -1
  66. package/dist/lib/browser/profiles.d.ts +77 -4
  67. package/dist/lib/browser/profiles.js +121 -12
  68. package/dist/lib/browser/runtime-state.d.ts +11 -5
  69. package/dist/lib/browser/runtime-state.js +13 -9
  70. package/dist/lib/browser/service.d.ts +91 -30
  71. package/dist/lib/browser/service.js +342 -207
  72. package/dist/lib/browser/types.d.ts +96 -4
  73. package/dist/lib/browser/types.js +51 -0
  74. package/dist/lib/catchup.js +1 -1
  75. package/dist/lib/computer/actions.d.ts +1 -1
  76. package/dist/lib/computer/actions.js +1 -1
  77. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  78. package/dist/lib/computer/dispatch.d.ts +1 -1
  79. package/dist/lib/computer/download.d.ts +1 -1
  80. package/dist/lib/computer/download.js +3 -3
  81. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  82. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  83. package/dist/lib/daemon/daemon.js +1 -1
  84. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
  85. package/dist/lib/{runner.js → daemon/runner.js} +59 -118
  86. package/dist/lib/daemon-webhooks.d.ts +3 -1
  87. package/dist/lib/daemon-webhooks.js +12 -7
  88. package/dist/lib/device-config.js +1 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  90. package/dist/lib/devices/doctor-findings.js +3 -3
  91. package/dist/lib/devices/fleet-inventory.js +1 -1
  92. package/dist/lib/devices/harness-inventory.js +1 -1
  93. package/dist/lib/devices/registry.js +4 -22
  94. package/dist/lib/doctor-diff.d.ts +1 -1
  95. package/dist/lib/doctor-diff.js +1 -1
  96. package/dist/lib/drift.js +1 -1
  97. package/dist/lib/entitlement.d.ts +31 -0
  98. package/dist/lib/entitlement.js +137 -0
  99. package/dist/lib/exec.js +39 -202
  100. package/dist/lib/feed/feed.js +2 -8
  101. package/dist/lib/fs-atomic.d.ts +7 -0
  102. package/dist/lib/fs-atomic.js +9 -0
  103. package/dist/lib/harness/adapter.d.ts +160 -0
  104. package/dist/lib/harness/adapter.js +47 -0
  105. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  106. package/dist/lib/harness/adapters/claude.js +110 -0
  107. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  108. package/dist/lib/harness/adapters/codex.js +83 -0
  109. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  110. package/dist/lib/harness/adapters/copilot.js +23 -0
  111. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  112. package/dist/lib/harness/adapters/cursor.js +38 -0
  113. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  114. package/dist/lib/harness/adapters/droid.js +17 -0
  115. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  116. package/dist/lib/harness/adapters/grok.js +14 -0
  117. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  118. package/dist/lib/harness/adapters/kimi.js +49 -0
  119. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  120. package/dist/lib/harness/adapters/muse.js +44 -0
  121. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  122. package/dist/lib/harness/adapters/opencode.js +14 -0
  123. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  124. package/dist/lib/harness/exec-config-version.js +22 -0
  125. package/dist/lib/harness/index.d.ts +1 -0
  126. package/dist/lib/harness/index.js +34 -0
  127. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  128. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  129. package/dist/lib/import.js +1 -1
  130. package/dist/lib/installations/index.d.ts +4 -4
  131. package/dist/lib/installations/index.js +3 -4
  132. package/dist/lib/installations/migrate.js +6 -6
  133. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  134. package/dist/lib/{shims.js → installations/shims.js} +12 -114
  135. package/dist/lib/installations/store.d.ts +141 -3
  136. package/dist/lib/installations/store.js +475 -5
  137. package/dist/lib/installations/versions.d.ts +1 -126
  138. package/dist/lib/installations/versions.js +5 -465
  139. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  140. package/dist/lib/isolation-boundary-report.js +1 -1
  141. package/dist/lib/mailbox-gc.js +2 -3
  142. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  143. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  144. package/dist/lib/monitors/dispatch.d.ts +1 -1
  145. package/dist/lib/monitors/dispatch.js +2 -2
  146. package/dist/lib/monitors/state.d.ts +1 -1
  147. package/dist/lib/observe-aliases.d.ts +2 -2
  148. package/dist/lib/observe-aliases.js +2 -11
  149. package/dist/lib/plugins/plugins.js +1 -1
  150. package/dist/lib/plugins/skills.js +1 -1
  151. package/dist/lib/prix-account.d.ts +158 -0
  152. package/dist/lib/prix-account.js +214 -0
  153. package/dist/lib/project-key.js +7 -0
  154. package/dist/lib/project-launch.js +1 -1
  155. package/dist/lib/refresh.js +2 -2
  156. package/dist/lib/resource-inventory.d.ts +1 -1
  157. package/dist/lib/resource-inventory.js +1 -1
  158. package/dist/lib/rules/rules.js +1 -1
  159. package/dist/lib/scheduling/routines.js +1 -1
  160. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  161. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  162. package/dist/lib/secrets/bundles.d.ts +1 -1
  163. package/dist/lib/secrets/bundles.js +1 -1
  164. package/dist/lib/secrets/headless.d.ts +16 -0
  165. package/dist/lib/secrets/headless.js +21 -0
  166. package/dist/lib/secrets/remote.d.ts +9 -7
  167. package/dist/lib/secrets/remote.js +18 -9
  168. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  169. package/dist/lib/self-heal/checks/path.js +1 -1
  170. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  171. package/dist/lib/self-heal/checks/shims.js +1 -1
  172. package/dist/lib/session/discover.d.ts +39 -0
  173. package/dist/lib/session/discover.js +3 -3
  174. package/dist/lib/session/parse.js +24 -42
  175. package/dist/lib/session/recovery.js +1 -1
  176. package/dist/lib/session/share-html.d.ts +55 -0
  177. package/dist/lib/session/share-html.js +319 -0
  178. package/dist/lib/settings-manifest.d.ts +2 -0
  179. package/dist/lib/settings-manifest.js +81 -3
  180. package/dist/lib/share/publish.d.ts +38 -0
  181. package/dist/lib/share/publish.js +81 -8
  182. package/dist/lib/staleness/prune.d.ts +1 -1
  183. package/dist/lib/staleness/prune.js +1 -1
  184. package/dist/lib/staleness/writers/hooks.js +1 -1
  185. package/dist/lib/startup/command-registry.d.ts +2 -1
  186. package/dist/lib/startup/command-registry.js +5 -3
  187. package/dist/lib/teams/agents.js +2 -19
  188. package/dist/lib/teams/parsers.js +20 -26
  189. package/dist/lib/teams/registry.js +2 -21
  190. package/dist/lib/triggers/handlers.d.ts +37 -2
  191. package/dist/lib/triggers/handlers.js +57 -6
  192. package/dist/lib/triggers/webhook.d.ts +80 -6
  193. package/dist/lib/triggers/webhook.js +128 -4
  194. package/dist/lib/types.d.ts +1 -1
  195. package/dist/lib/uninstall.js +1 -1
  196. package/dist/lib/version.d.ts +9 -2
  197. package/dist/lib/version.js +11 -9
  198. package/dist/lib/wrap.d.ts +33 -0
  199. package/dist/lib/wrap.js +70 -0
  200. package/package.json +1 -1
  201. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
@@ -0,0 +1,174 @@
1
+ import chalk from 'chalk';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, removeSpaceMember, resolveMemberFromList, resolvePrixToken, resolveSpaceFromList, slugify, updateSpaceMemberRole, } from '../lib/prix-account.js';
4
+ /** Every `agents org` subcommand needs a token up front — one place to say so. */
5
+ function requireToken() {
6
+ if (!resolvePrixToken())
7
+ throw new Error("Not signed in. Run 'agents auth login' first.");
8
+ }
9
+ async function resolveSpace(explicit) {
10
+ const spaces = await listSpaces();
11
+ return resolveSpaceFromList(spaces, explicit);
12
+ }
13
+ function printSpace(space) {
14
+ console.log(`${chalk.bold(space.name)} ${chalk.gray(space.slug)} ${chalk.gray(space.id)}`);
15
+ console.log(` role: ${space.user_role}${space.organization_id ? ` org: ${space.organization_id}` : ''}`);
16
+ }
17
+ function printMembers(members) {
18
+ if (!members.length) {
19
+ console.log(chalk.gray(' No members.'));
20
+ return;
21
+ }
22
+ for (const m of members)
23
+ console.log(` ${chalk.cyan(m.email)} ${m.role} ${chalk.gray(m.user_id)}`);
24
+ }
25
+ async function runCreate(name, o) {
26
+ requireToken();
27
+ const space = await createSpace({ name, slug: o.slug ?? slugify(name), description: o.description });
28
+ if (o.json) {
29
+ console.log(JSON.stringify(space, null, 2));
30
+ return;
31
+ }
32
+ console.log(chalk.green(`Created space '${space.name}' (${space.slug}).`));
33
+ printSpace(space);
34
+ }
35
+ async function runList(o) {
36
+ requireToken();
37
+ const spaces = await listSpaces();
38
+ if (o.json) {
39
+ console.log(JSON.stringify(spaces, null, 2));
40
+ return;
41
+ }
42
+ if (!spaces.length) {
43
+ console.log(chalk.gray("No spaces. Create one with 'agents org create <name>'."));
44
+ return;
45
+ }
46
+ for (const space of spaces)
47
+ printSpace(space);
48
+ }
49
+ async function runView(spaceArg, o) {
50
+ requireToken();
51
+ const space = await resolveSpace(spaceArg);
52
+ if (o.json) {
53
+ console.log(JSON.stringify(space, null, 2));
54
+ return;
55
+ }
56
+ printSpace(space);
57
+ }
58
+ /** `--role` defaults to member when omitted; anything present must be a real role — never silently coerced. */
59
+ export function resolveInviteRole(raw) {
60
+ return raw === undefined ? 'member' : parseRole(raw);
61
+ }
62
+ async function runInvite(email, o) {
63
+ requireToken();
64
+ const role = resolveInviteRole(o.role);
65
+ const space = await resolveSpace(o.space);
66
+ const result = await createSpaceInvite(space.id, email, role);
67
+ if (o.json) {
68
+ console.log(JSON.stringify(result, null, 2));
69
+ return;
70
+ }
71
+ if (result.member_added)
72
+ console.log(chalk.green(`Added ${email} to '${space.name}' as ${role}.`));
73
+ else
74
+ console.log(chalk.green(`Invited ${email} to '${space.name}' as ${role}. They'll get an email.`));
75
+ }
76
+ async function runMembers(spaceArg, o) {
77
+ requireToken();
78
+ const space = await resolveSpace(spaceArg);
79
+ const members = await listSpaceMembers(space.id);
80
+ if (o.json) {
81
+ console.log(JSON.stringify(members, null, 2));
82
+ return;
83
+ }
84
+ console.log(chalk.bold(`${space.name} (${members.length} member${members.length === 1 ? '' : 's'})`));
85
+ printMembers(members);
86
+ }
87
+ /** Validate a role argument before any network call — same rule the backend enforces server-side. */
88
+ export function parseRole(raw) {
89
+ if (raw === 'admin' || raw === 'member')
90
+ return raw;
91
+ throw new Error(`role must be 'admin' or 'member', got '${raw}'.`);
92
+ }
93
+ async function runRole(email, roleRaw, o) {
94
+ requireToken();
95
+ const role = parseRole(roleRaw);
96
+ const space = await resolveSpace(o.space);
97
+ const members = await listSpaceMembers(space.id);
98
+ const member = resolveMemberFromList(members, email);
99
+ const result = await updateSpaceMemberRole(space.id, member.user_id, role);
100
+ if (o.json) {
101
+ console.log(JSON.stringify(result, null, 2));
102
+ return;
103
+ }
104
+ console.log(chalk.green(`${email} is now ${role} in '${space.name}'.`));
105
+ }
106
+ async function runRemove(email, o) {
107
+ requireToken();
108
+ const space = await resolveSpace(o.space);
109
+ const members = await listSpaceMembers(space.id);
110
+ const member = resolveMemberFromList(members, email);
111
+ await removeSpaceMember(space.id, member.user_id);
112
+ if (o.json) {
113
+ console.log(JSON.stringify({ removed: email, space: space.slug }, null, 2));
114
+ return;
115
+ }
116
+ console.log(chalk.green(`Removed ${email} from '${space.name}'.`));
117
+ }
118
+ async function runLeave(spaceArg, o) {
119
+ requireToken();
120
+ const resolved = resolvePrixToken();
121
+ if (!resolved)
122
+ throw new Error("Not signed in. Run 'agents auth login' first.");
123
+ const who = await fetchWhoAmI(resolved.token);
124
+ const space = await resolveSpace(spaceArg);
125
+ if (space.owner_user_id === who.userId) {
126
+ throw new Error(`You own '${space.name}'. Transfer ownership or delete the space instead of leaving it.`);
127
+ }
128
+ await removeSpaceMember(space.id, who.userId);
129
+ if (o.json) {
130
+ console.log(JSON.stringify({ left: space.slug }, null, 2));
131
+ return;
132
+ }
133
+ console.log(chalk.green(`Left '${space.name}'.`));
134
+ }
135
+ export function registerOrgCommand(program) {
136
+ const org = program.command('org').description('Create and manage a team (a Rush "space") you can share with `agents auth login` collaborators');
137
+ org.command('create <name>').description('Create a space (free tier: 1 owned space)')
138
+ .option('--slug <slug>', 'Override the derived slug')
139
+ .option('--description <text>', 'Optional description')
140
+ .option('--json', 'Machine-readable output')
141
+ .action((name, o, command) => runCreate(name, { ...o, json: !!(o.json || command.optsWithGlobals().json) }));
142
+ org.command('list').description('List spaces you own or belong to').option('--json', 'Machine-readable output')
143
+ .action((o, command) => runList({ json: !!(o.json || command.optsWithGlobals().json) }));
144
+ org.command('view [space]').description('Show one space (defaults to your only space)').option('--json', 'Machine-readable output')
145
+ .action((space, o, command) => runView(space, { json: !!(o.json || command.optsWithGlobals().json) }));
146
+ org.command('invite <email>').description('Invite (or directly add) a member')
147
+ .option('--role <role>', 'admin | member', 'member')
148
+ .option('--space <id-or-slug>', 'Space to invite into (defaults to your only space)')
149
+ .option('--json', 'Machine-readable output')
150
+ .action((email, o, command) => runInvite(email, { ...o, json: !!(o.json || command.optsWithGlobals().json) }));
151
+ org.command('members [space]').description('List a space\'s members').option('--json', 'Machine-readable output')
152
+ .action((space, o, command) => runMembers(space, { json: !!(o.json || command.optsWithGlobals().json) }));
153
+ org.command('role <email> <role>').description('Change a member\'s role (owner-only for admin)')
154
+ .option('--space <id-or-slug>', 'Space to change (defaults to your only space)')
155
+ .option('--json', 'Machine-readable output')
156
+ .action((email, role, o, command) => runRole(email, role, { ...o, json: !!(o.json || command.optsWithGlobals().json) }));
157
+ org.command('remove <email>').description('Remove a member from a space')
158
+ .option('--space <id-or-slug>', 'Space to remove from (defaults to your only space)')
159
+ .option('--json', 'Machine-readable output')
160
+ .action((email, o, command) => runRemove(email, { ...o, json: !!(o.json || command.optsWithGlobals().json) }));
161
+ org.command('leave [space]').description('Leave a space you do not own').option('--json', 'Machine-readable output')
162
+ .action((space, o, command) => runLeave(space, { json: !!(o.json || command.optsWithGlobals().json) }));
163
+ setHelpSections(org, {
164
+ examples: `agents org create acme-team
165
+ agents org list
166
+ agents org view acme-team
167
+ agents org invite dev@example.com --role admin
168
+ agents org members
169
+ agents org role dev@example.com admin
170
+ agents org remove dev@example.com
171
+ agents org leave acme-team`,
172
+ notes: 'Maps to the Rush backend\'s /api/v1/spaces (the free-tier team primitive), not /api/v1/orgs. Free tier: 1 owned space, 3 members per space. Every command needs `agents auth login` (or a `rush login` session) first. `--space` is only needed once you belong to more than one space.',
173
+ });
174
+ }
@@ -15,7 +15,7 @@ import { getRegistries, setRegistry, removeRegistry, search as searchRegistries,
15
15
  import { cloneRepo } from '../lib/git.js';
16
16
  import { discoverCommands, resolveCommandSource, installCommand, installCommandCentrally, } from '../lib/commands.js';
17
17
  import { discoverSkillsFromRepo, installSkill, installSkillCentrally, } from '../lib/plugins/skills.js';
18
- import { discoverHooksFromRepo, installHooks, installHooksCentrally, } from '../lib/hooks.js';
18
+ import { discoverHooksFromRepo, installHooks, installHooksCentrally, } from '../lib/hooks/install.js';
19
19
  import { discoverWorkflowsFromRepo, installWorkflowCentrally, } from '../lib/workflows.js';
20
20
  import { discoverSubagentsFromRepo, installSubagentCentrally, } from '../lib/subagents.js';
21
21
  import { discoverPermissionsFromRepo, installPermissionSet, } from '../lib/permissions.js';
@@ -27,7 +27,7 @@ import { formatBytes } from '../lib/format.js';
27
27
  import { confirm } from '@inquirer/prompts';
28
28
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
29
29
  import { diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/plugins/skills.js';
30
- import { listUnmanagedHooksInVersionHome, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
30
+ import { listUnmanagedHooksInVersionHome, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks/install.js';
31
31
  import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins/plugins.js';
32
32
  import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
33
33
  import { getGlobalDefault } from '../lib/installations/versions.js';
@@ -24,7 +24,7 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
24
24
  import { getRoutinesDir } from '../lib/state.js';
25
25
  import { IS_WINDOWS } from '../lib/platform/index.js';
26
26
  import { safeJoin } from '../lib/paths.js';
27
- import { executeJob, monitorRunningJobs } from '../lib/runner.js';
27
+ import { executeJob, monitorRunningJobs } from '../lib/daemon/runner.js';
28
28
  import { JobScheduler } from '../lib/scheduler.js';
29
29
  import { detectOverdueJobs } from '../lib/overdue.js';
30
30
  import { runCatchup } from '../lib/catchup.js';
@@ -12,10 +12,29 @@ export interface RunAccountChoice {
12
12
  */
13
13
  signInRequired: boolean;
14
14
  }
15
+ /** One named account row for `agents accounts switch` (reuses this picker's layout). */
16
+ export interface SwitchAccountRow {
17
+ accountName: string;
18
+ kind: 'provider' | 'native';
19
+ detail: string;
20
+ current: boolean;
21
+ candidate: RotateCandidate | null;
22
+ }
15
23
  /** Human-readable remaining capacity for every window the provider exposes. */
16
24
  export declare function formatAccountLimits(candidate: RotateCandidate): string;
17
25
  /** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
18
26
  export declare function buildRunAccountChoices(candidates: RotateCandidate[], globalDefault: string | null): RunAccountChoice[];
27
+ /**
28
+ * Aligned picker rows for `accounts switch`. Same columns as the run picker
29
+ * (identity, status, limits) but the value is the named account to set-default.
30
+ * Rows stay selectable: setting a default is not a launch.
31
+ */
32
+ export declare function buildSwitchAccountChoices(rows: SwitchAccountRow[]): RunAccountChoice[];
33
+ /**
34
+ * Prompt for the named account that becomes this harness's default.
35
+ * A cancelled picker writes nothing.
36
+ */
37
+ export declare function pickSwitchAccount(agent: AgentId, rows: SwitchAccountRow[]): Promise<string | null>;
19
38
  /**
20
39
  * Whether a zero-healthy run may recover by launching for a login, or must keep
21
40
  * failing loud. Three inputs, all of which have to hold:
@@ -124,6 +124,87 @@ export function buildRunAccountChoices(candidates, globalDefault) {
124
124
  signInRequired: row.signInRequired,
125
125
  }));
126
126
  }
127
+ function switchRowStatus(row) {
128
+ if (!row.candidate) {
129
+ return {
130
+ status: row.kind === 'provider' ? 'credential' : 'named',
131
+ limits: 'limits unavailable',
132
+ ready: true,
133
+ };
134
+ }
135
+ const readiness = readinessFromCandidate(row.candidate);
136
+ const authReason = readiness.ready ? null : readiness.reason;
137
+ const status = authReason === 'revoked'
138
+ ? 'needs re-login'
139
+ : authReason === 'signed_out'
140
+ ? 'logged out'
141
+ : authReason === 'rate_limited'
142
+ ? 'rate limited'
143
+ : authReason === 'out_of_credits'
144
+ ? 'out of credits'
145
+ : 'logged in';
146
+ const limits = authReason === 'revoked'
147
+ ? 'needs re-authentication'
148
+ : authReason === 'signed_out'
149
+ ? 'signed out'
150
+ : formatAccountLimits(row.candidate);
151
+ return { status, limits, ready: readiness.ready };
152
+ }
153
+ /**
154
+ * Aligned picker rows for `accounts switch`. Same columns as the run picker
155
+ * (identity, status, limits) but the value is the named account to set-default.
156
+ * Rows stay selectable: setting a default is not a launch.
157
+ */
158
+ export function buildSwitchAccountChoices(rows) {
159
+ const rendered = rows.map((row) => {
160
+ const { status, limits, ready } = switchRowStatus(row);
161
+ const account = row.current ? `${row.accountName} (default)` : row.accountName;
162
+ const kind = row.kind === 'provider' ? `provider · ${row.detail}` : `native · ${row.detail}`;
163
+ return { account, kind, status, limits, ready, value: row.accountName };
164
+ });
165
+ const accountWidth = Math.max(0, ...rendered.map((row) => row.account.length));
166
+ const kindWidth = Math.max(0, ...rendered.map((row) => row.kind.length));
167
+ const statusWidth = Math.max(0, ...rendered.map((row) => row.status.length));
168
+ return rendered.map((row) => ({
169
+ name: [
170
+ row.account.padEnd(accountWidth),
171
+ row.kind.padEnd(kindWidth),
172
+ row.status.padEnd(statusWidth),
173
+ row.limits,
174
+ ].join(' '),
175
+ value: row.value,
176
+ ready: row.ready,
177
+ signInRequired: false,
178
+ }));
179
+ }
180
+ /**
181
+ * Prompt for the named account that becomes this harness's default.
182
+ * A cancelled picker writes nothing.
183
+ */
184
+ export async function pickSwitchAccount(agent, rows) {
185
+ if (!isInteractiveTerminal()) {
186
+ requireInteractiveSelection(`Selecting a ${agentLabel(agent)} account`, [
187
+ `agents accounts switch ${agent} <account>`,
188
+ `agents accounts set-default ${agent} <account>`,
189
+ ]);
190
+ }
191
+ if (rows.length === 0) {
192
+ throw new Error(`No named accounts for ${agent}. Add one with 'agents accounts add <name> --provider <p>' or 'agents accounts name ${agent}@<version> <name>'.`);
193
+ }
194
+ const choices = buildSwitchAccountChoices(rows).map(({ ready: _ready, signInRequired: _signInRequired, ...choice }) => choice);
195
+ try {
196
+ return await select({
197
+ message: `Select the default ${agentLabel(agent)} account:`,
198
+ choices,
199
+ loop: false,
200
+ });
201
+ }
202
+ catch (err) {
203
+ if (isPromptCancelled(err))
204
+ return null;
205
+ throw err;
206
+ }
207
+ }
127
208
  /**
128
209
  * Whether a zero-healthy run may recover by launching for a login, or must keep
129
210
  * failing loud. Three inputs, all of which have to hold:
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon/daemon.js';
18
18
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
19
19
  import { resolveBundleForPush, pushResolvedBundleToHost, bundleEnvToDotenv } from '../lib/secrets/push.js';
20
20
  export { bundleEnvToDotenv };
21
- import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
21
+ import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, isAgentInvocationContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
22
22
  import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
23
23
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
24
24
  import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
@@ -38,7 +38,6 @@ import { frequentlyPromptedBundles } from '../lib/secrets/unlock-hints.js';
38
38
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
39
39
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
40
40
  import { discoverSyncedBundles, importSyncedBundle, } from '../lib/secrets/icloud-import.js';
41
- import { getVaultSession, vaultExists } from '../lib/secrets/vault.js';
42
41
  import { registerSecretsSyncCommands } from './secrets-sync.js';
43
42
  import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
44
43
  import { registerSecretsImportKeyringCommand } from './secrets-import.js';
@@ -924,14 +923,17 @@ export function registerSecretsCommands(program) {
924
923
  # Stop a noisy automation bundle from prompting every run: ask once a week
925
924
  agents secrets policy prod hold
926
925
 
927
- # Eval the bundle into your current shell
928
- eval "$(agents secrets export prod --plaintext)"
926
+ # Run a one-off command with secrets injected (values ride the child env, never stdout)
927
+ agents secrets exec prod -- ./deploy.sh
928
+
929
+ # Need one value inside a script? Read it under injection, not with a printer
930
+ agents secrets exec prod -- printenv STRIPE_API_KEY
931
+
932
+ # Deliberately reveal values at your terminal (interactive only)
933
+ agents secrets view prod --reveal
929
934
 
930
935
  # Push the bundle to remote machine(s) over SSH (lands as a native bundle there)
931
936
  agents secrets export prod --device yosemite-s0 --device yosemite-s1 --force
932
-
933
- # Run a one-off command with secrets injected
934
- agents secrets exec prod -- ./deploy.sh
935
937
  `,
936
938
  notes: `
937
939
  Bundles are containers; secrets are the variables inside them. Keychain values
@@ -1222,13 +1224,22 @@ export function registerSecretsCommands(program) {
1222
1224
  .command('view [name]')
1223
1225
  .alias('show')
1224
1226
  .description('Show a bundle. Keychain values are masked by default — pass --reveal to see them.')
1225
- .option('--reveal', 'Print keychain-backed values in the clear (TTY only unless --plaintext)')
1226
- .option('--plaintext', 'Allow --reveal in non-interactive shells (use with care)')
1227
+ .option('--reveal', 'Print keychain-backed values in the clear (interactive terminal only)')
1227
1228
  .option('--json', 'Emit machine-readable JSON (values masked unless --reveal) instead of the human view')
1228
1229
  .option('--device <target>', 'Show a bundle on a remote device over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
1229
1230
  .option('--devices <list>', 'Comma-separated devices to show in one shot, e.g. yosemite-s0,yosemite-s1')
1230
1231
  .action(async (name, opts) => {
1231
1232
  try {
1233
+ // `--reveal` is a deliberate HUMAN reveal: interactive terminal only, and
1234
+ // never inside an agent session (an agent under tmux has a TTY, so the
1235
+ // env markers are the load-bearing check). The old `--plaintext` escape
1236
+ // that allowed a piped/non-TTY reveal is removed (RUSH-2774) — printed
1237
+ // values land in an agent's context and session transcript.
1238
+ if (opts.reveal && (isAgentInvocationContext() || !isInteractiveTerminal())) {
1239
+ console.error(chalk.red('--reveal prints values in the clear and needs a real interactive terminal outside an agent session.'));
1240
+ console.error(chalk.dim('Run the consuming command under injection instead: agents secrets exec <bundle> -- <cmd>'));
1241
+ process.exit(1);
1242
+ }
1232
1243
  const targets = parseHostsOption({ device: opts.device, devices: opts.devices });
1233
1244
  if (targets.length > 0) {
1234
1245
  if (!name) {
@@ -1238,14 +1249,13 @@ export function registerSecretsCommands(program) {
1238
1249
  const args = ['view', name];
1239
1250
  if (opts.reveal)
1240
1251
  args.push('--reveal');
1241
- if (opts.plaintext)
1242
- args.push('--plaintext');
1243
1252
  if (opts.json)
1244
1253
  args.push('--json');
1245
1254
  // With --reveal, force a TTY so the remote keychain prompt can surface
1246
- // (and the remote's "--reveal in a non-TTY needs --plaintext" gate is
1247
- // satisfied) only when this side is itself interactive.
1248
- const tty = Boolean(opts.reveal) && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
1255
+ // and the remote's own interactive-terminal gate is satisfied. The
1256
+ // early gate above already proved this side is an interactive,
1257
+ // non-agent terminal.
1258
+ const tty = Boolean(opts.reveal);
1249
1259
  // `--reveal` streams the plaintext value back over ssh stdout — a
1250
1260
  // secret-bearing read that must pin the host key and not multiplex,
1251
1261
  // whether it runs on the interactive (tty) or `--plaintext` non-tty path.
@@ -1288,14 +1298,10 @@ export function registerSecretsCommands(program) {
1288
1298
  emitSecretAudit({ event: 'secrets.view', bundle: bundle.name, operation: opts.json ? 'view --json' : 'view', source: 'view', status: 'success' });
1289
1299
  if (opts.json) {
1290
1300
  // Machine-readable discovery for agents. Values are null unless --reveal
1291
- // (which still routes through the same non-TTY/plaintext gate + audit
1292
- // event as the human path). Gated on the explicit flag, not stdout.isTTY,
1293
- // so a piped human view never silently swaps formats.
1301
+ // (already gated above: interactive terminal, outside an agent session).
1302
+ // Gated on the explicit flag, not stdout.isTTY, so a piped human view
1303
+ // never silently swaps formats.
1294
1304
  const reveal = Boolean(opts.reveal);
1295
- if (reveal && !isInteractiveTerminal() && !opts.plaintext) {
1296
- console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
1297
- process.exit(1);
1298
- }
1299
1305
  const revealed = new Map();
1300
1306
  if (reveal) {
1301
1307
  const { env } = readAndResolveBundleEnv(bundle.name, {
@@ -1415,10 +1421,6 @@ export function registerSecretsCommands(program) {
1415
1421
  return;
1416
1422
  }
1417
1423
  const reveal = Boolean(opts.reveal);
1418
- if (reveal && !isInteractiveTerminal() && !opts.plaintext) {
1419
- console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
1420
- process.exit(1);
1421
- }
1422
1424
  // An explicit human `view --reveal` at a terminal is a deliberate value
1423
1425
  // access: resolve interactively (one Touch ID) so a locked bundle shows
1424
1426
  // its values. Under an agent (AGENTS_RUNTIME) or headless (no TTY) it
@@ -1491,50 +1493,38 @@ export function registerSecretsCommands(program) {
1491
1493
  });
1492
1494
  cmd
1493
1495
  .command('get <item> [key]')
1494
- .description('Print one secret value for shell hooks/automation. One arg = a raw keychain item by name; two args = one KEY out of a bundle (`get <bundle> <KEY>`). Cross-platform.')
1496
+ .description('Print one raw keychain item by name, for shell hooks/automation. Cross-platform. Bundle values are never printed run commands under `agents secrets exec` instead.')
1495
1497
  .action((item, key) => {
1496
- if (key === undefined) {
1497
- // Raw keychain item path unchanged.
1498
- try {
1499
- // Routes through the platform keychain layer: macOS reads bare items
1500
- // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
1501
- // encrypted-file fallback. The value goes to stdout (newline-terminated
1502
- // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
1503
- // to stderr so they never pollute the captured value.
1504
- const value = getKeychainToken(item);
1505
- // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
1506
- // `item` is the keychain service name, never the value.
1507
- emitSecretAudit({ event: 'secrets.get', item, source: 'raw-item', status: 'success' });
1508
- process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
1509
- }
1510
- catch {
1511
- // Missing item is a normal, quiet outcome for a hook probe: exit 1,
1512
- // print nothing to stdout. Callers test the exit code / empty capture.
1513
- process.exit(1);
1514
- }
1515
- return;
1498
+ if (key !== undefined) {
1499
+ // The bundle-key form (`get <bundle> <KEY>`) is REMOVED (RUSH-2774): a
1500
+ // one-liner that prints a bundle credential is the exfiltration path
1501
+ // this change closes, for every caller. The injection replacement is
1502
+ // just as short for a human script and never touches this stdout.
1503
+ console.error(chalk.red(`'secrets get <bundle> <KEY>' has been removed — printing a bundle value to stdout is the exfiltration path this blocks.`));
1504
+ console.error(chalk.dim(`Use: agents secrets exec ${item} -- printenv ${key} (or run the consuming command directly under exec)`));
1505
+ process.exit(1);
1516
1506
  }
1517
- // Bundle-key path: `get <bundle> <KEY>` prints exactly one resolved value.
1518
- // Ungated like the raw path (it IS the automation primitive); the
1519
- // `secrets.get` audit event is emitted inside readAndResolveBundleEnv.
1507
+ // Raw keychain item path a single ad-hoc token, not a credential bundle.
1508
+ // Deliberately NOT agent-gated (unlike the removed bundle-key form): shell
1509
+ // hooks that run inside agent sessions inherit the session env markers and
1510
+ // legitimately capture a raw item into their own variables (e.g. the
1511
+ // posthog analytics hook), where the value never reaches the transcript.
1512
+ // A single ad-hoc token is the accepted narrower residual (RUSH-2774).
1520
1513
  try {
1521
- if (!bundleExists(item) && !(vaultExists() && !getVaultSession().loggedIn)) {
1522
- console.error(chalk.red(`Secrets bundle '${item}' not found.`));
1523
- process.exit(1);
1524
- }
1525
- // `secrets get` is the scriptable automation primitive ($(agents secrets
1526
- // get bundle KEY)). Every read is broker-only; a locked bundle points at
1527
- // the explicit unlock command instead of raising Touch ID.
1528
- const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key], keyMode: 'storage', agentOnly: true });
1529
- if (!(key in env)) {
1530
- console.error(chalk.red(`Key '${key}' not in bundle '${item}'.`));
1531
- process.exit(1);
1532
- }
1533
- const value = env[key];
1514
+ // Routes through the platform keychain layer: macOS reads bare items
1515
+ // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
1516
+ // encrypted-file fallback. The value goes to stdout (newline-terminated
1517
+ // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
1518
+ // to stderr so they never pollute the captured value.
1519
+ const value = getKeychainToken(item);
1520
+ // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
1521
+ // `item` is the keychain service name, never the value.
1522
+ emitSecretAudit({ event: 'secrets.get', item, source: 'raw-item', status: 'success' });
1534
1523
  process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
1535
1524
  }
1536
- catch (err) {
1537
- console.error(chalk.red(err.message));
1525
+ catch {
1526
+ // Missing item is a normal, quiet outcome for a hook probe: exit 1,
1527
+ // print nothing to stdout. Callers test the exit code / empty capture.
1538
1528
  process.exit(1);
1539
1529
  }
1540
1530
  });
@@ -2133,18 +2123,23 @@ Examples:
2133
2123
  });
2134
2124
  cmd
2135
2125
  .command('export [bundle]')
2136
- .description('Resolve a bundle and print KEY=VALUE lines, push it to a 1Password vault with --to-1password, or push it to remote machine(s) over SSH with --device.')
2137
- .option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear (shell export mode)')
2126
+ .description('Move a bundle without exposing it: push to remote machine(s) over SSH with --device, to a 1Password vault with --to-1password, or to an encrypted file with --to-file.')
2127
+ // Both transport flags are hidden, not documented: the ONLY caller is the
2128
+ // machine-to-machine SSH resolve (remoteResolveEnv / verifyRemoteKeychainPush),
2129
+ // which sets AGENTS_SECRETS_REMOTE_TRANSPORT=1 on the remote invocation. The
2130
+ // old public shell-eval mode (`eval "$(agents secrets export … --plaintext)"`)
2131
+ // was the top agent-exfiltration path and is removed (RUSH-2774).
2132
+ .addOption(new Option('--plaintext', 'internal remote-resolve transport').hideHelp())
2133
+ .addOption(new Option('--format <json>', 'internal remote-resolve transport').hideHelp())
2138
2134
  .option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
2139
2135
  .option('--vault <name>', '1Password vault name (used with --to-1password)')
2140
2136
  .option('--device <target...>', 'Push the bundle over SSH to this device (host alias or user@host); repeatable for multiple machines')
2141
2137
  .option('--remote-backend <backend>', 'Backend for the bundle on the remote device (with --device): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
2142
2138
  .option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --device)')
2143
- .option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
2144
2139
  .option('--to-file <path>', `Write the bundle as an AES-256-GCM encrypted offline file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of import --from-file)`)
2145
2140
  .action(async (bundleName, opts) => {
2146
2141
  try {
2147
- const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
2142
+ const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
2148
2143
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
2149
2144
  if (opts.toFile) {
2150
2145
  // Transport, not the local store's master key — see lib/secrets/sync-passphrase.ts.
@@ -2251,41 +2246,36 @@ Examples:
2251
2246
  console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
2252
2247
  return;
2253
2248
  }
2254
- if (opts.format && opts.format !== 'shell' && opts.format !== 'json') {
2255
- console.error(chalk.red(`Invalid --format ${JSON.stringify(opts.format)}. Expected 'shell' or 'json'.`));
2249
+ // The shell-eval print mode (`eval "$(agents secrets export … --plaintext)"`)
2250
+ // is REMOVED (RUSH-2774): a whole bundle printed to stdout lands in a
2251
+ // coding agent's context and session transcript, and it was the single
2252
+ // most-copied exfiltration one-liner on the fleet. The sole surviving
2253
+ // stdout emitter is the machine-to-machine SSH resolve transport, which
2254
+ // requires the remote-invocation marker AND a non-agent context — both
2255
+ // set only by remoteResolveEnv / verifyRemoteKeychainPush building the
2256
+ // remote command. Everything else gets the refusal with the paved paths.
2257
+ const isTransportCall = process.env.AGENTS_SECRETS_REMOTE_TRANSPORT === '1' &&
2258
+ Boolean(opts.plaintext) &&
2259
+ opts.format === 'json' &&
2260
+ !isAgentInvocationContext();
2261
+ if (!isTransportCall) {
2262
+ console.error(chalk.red('export no longer prints values. Pick a destination:'));
2263
+ console.error(chalk.red(' --device <host> | --to-1password | --to-file <path>'));
2264
+ console.error(chalk.dim(`Run a command with the values injected instead: agents secrets exec ${resolvedBundleName} -- <cmd>`));
2265
+ console.error(chalk.dim(`Reveal one value at your terminal: agents secrets view ${resolvedBundleName} --reveal`));
2256
2266
  process.exit(1);
2257
2267
  }
2258
- if (!opts.plaintext) {
2259
- console.error(chalk.red('export prints secrets in the clear and requires --plaintext (works for TTY and pipes alike).'));
2260
- process.exit(1);
2261
- }
2262
- // `agents secrets export --plaintext` is what release/CI scripts eval.
2263
- // Every caller is broker-only, including a plain shell, so export never
2264
- // raises Touch ID on the interactive user's screen.
2268
+ // Machine-readable form consumed by `remoteResolveEnv` over SSH: single
2269
+ // object of injected env KEY -> value; values verbatim. Broker-only, so a
2270
+ // headless remote read fails fast rather than raising Touch ID. The
2271
+ // resolve emits its own `secrets.get` audit on the remote; no export
2272
+ // event here that would tally every peer pull as an export.
2265
2273
  const { env } = readAndResolveBundleEnv(resolvedBundleName, {
2266
- caller: `export to shell`,
2274
+ caller: 'remote resolve transport',
2267
2275
  keyMode: 'process',
2268
2276
  agentOnly: true,
2269
2277
  });
2270
- if (opts.format === 'json') {
2271
- // Machine-readable form consumed by `remoteResolveEnv` over SSH.
2272
- // Single object of injected env KEY -> value; values verbatim.
2273
- process.stdout.write(JSON.stringify(env));
2274
- return;
2275
- }
2276
- const prefix = bundleToEnvPrefix(resolvedBundleName);
2277
- for (const [k, v] of Object.entries(env)) {
2278
- const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
2279
- const needsQuotes = /[\s$`"'\\|&;<>(){}[\]!#~]/.test(v);
2280
- const output = needsQuotes ? `'${v.replace(/'/g, `'\\''`)}'` : v;
2281
- process.stdout.write(`export ${exportKey}=${output}\n`);
2282
- }
2283
- // Record the shell export (the `eval "$(...)"` path). The `--format json`
2284
- // branch above is the machine-to-machine remote-resolve transport (already
2285
- // counted as an access on this machine by the resolve above), so it is
2286
- // deliberately not counted here as an export — that would tally every peer
2287
- // pull as an export.
2288
- emitSecretAudit({ event: 'secrets.export', bundle: resolvedBundleName, operation: 'export --plaintext', source: 'shell', status: 'success', keyCount: Object.keys(env).length });
2278
+ process.stdout.write(JSON.stringify(env));
2289
2279
  }
2290
2280
  catch (err) {
2291
2281
  if (isPromptCancelled(err))