@phnx-labs/agents-cli 1.22.84 → 1.22.86

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 (259) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/alias.js +7 -3
  9. package/dist/commands/apply.js +1 -1
  10. package/dist/commands/auth-mint.d.ts +7 -3
  11. package/dist/commands/auth-mint.js +21 -9
  12. package/dist/commands/auth.js +2 -3
  13. package/dist/commands/browser.js +10 -10
  14. package/dist/commands/daemon.d.ts +7 -3
  15. package/dist/commands/daemon.js +40 -25
  16. package/dist/commands/doctor.js +28 -2
  17. package/dist/commands/exec.js +87 -39
  18. package/dist/commands/fleet-capture.js +21 -2
  19. package/dist/commands/harness-wizard.js +2 -2
  20. package/dist/commands/lease.js +7 -12
  21. package/dist/commands/profiles.d.ts +2 -2
  22. package/dist/commands/profiles.js +20 -13
  23. package/dist/commands/repo.js +3 -3
  24. package/dist/commands/run-account-picker.d.ts +3 -3
  25. package/dist/commands/run-account-picker.js +29 -30
  26. package/dist/commands/secrets-passthrough.d.ts +20 -0
  27. package/dist/commands/secrets-passthrough.js +43 -0
  28. package/dist/commands/setup-accounts.d.ts +3 -2
  29. package/dist/commands/setup-accounts.js +4 -4
  30. package/dist/commands/setup-secrets.d.ts +16 -21
  31. package/dist/commands/setup-secrets.js +43 -224
  32. package/dist/commands/ssh.js +2 -2
  33. package/dist/commands/sync.js +1 -1
  34. package/dist/commands/update.d.ts +10 -0
  35. package/dist/commands/update.js +43 -40
  36. package/dist/commands/versions.d.ts +11 -11
  37. package/dist/commands/versions.js +131 -83
  38. package/dist/commands/view.d.ts +1 -9
  39. package/dist/commands/view.js +41 -55
  40. package/dist/commands/webhook.js +5 -5
  41. package/dist/commands/workflows.js +2 -1
  42. package/dist/index.d.ts +12 -12
  43. package/dist/index.js +13 -42
  44. package/dist/lib/account-capabilities.d.ts +1 -1
  45. package/dist/lib/account-capabilities.js +1 -1
  46. package/dist/lib/account-catalog.d.ts +121 -6
  47. package/dist/lib/account-catalog.js +421 -7
  48. package/dist/lib/account-registry.d.ts +8 -30
  49. package/dist/lib/account-registry.js +36 -79
  50. package/dist/lib/account-schema.d.ts +1 -1
  51. package/dist/lib/account-schema.js +1 -1
  52. package/dist/lib/accounting/account-pool-collect.js +16 -4
  53. package/dist/lib/accounting/rotate.d.ts +20 -7
  54. package/dist/lib/accounting/rotate.js +89 -13
  55. package/dist/lib/accounting/usage.js +11 -16
  56. package/dist/lib/accounts/add.d.ts +138 -0
  57. package/dist/lib/accounts/add.js +651 -0
  58. package/dist/lib/accounts/migrate.d.ts +117 -0
  59. package/dist/lib/accounts/migrate.js +536 -0
  60. package/dist/lib/accounts/slots.d.ts +13 -0
  61. package/dist/lib/accounts/slots.js +100 -0
  62. package/dist/lib/agent-spec/agents.d.ts +16 -0
  63. package/dist/lib/agent-spec/agents.js +38 -4
  64. package/dist/lib/app-bundle-install.js +5 -4
  65. package/dist/lib/auth-health.d.ts +3 -0
  66. package/dist/lib/auth-health.js +11 -0
  67. package/dist/lib/auth-mint.d.ts +52 -16
  68. package/dist/lib/auth-mint.js +117 -39
  69. package/dist/lib/browser/chrome.d.ts +1 -1
  70. package/dist/lib/browser/chrome.js +5 -5
  71. package/dist/lib/byok-usage.js +3 -3
  72. package/dist/lib/claude-account-token.d.ts +47 -3
  73. package/dist/lib/claude-account-token.js +172 -50
  74. package/dist/lib/cloud/antigravity.js +4 -4
  75. package/dist/lib/cloud/cursor.js +4 -4
  76. package/dist/lib/crabbox/cli.d.ts +1 -1
  77. package/dist/lib/crabbox/cli.js +6 -6
  78. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  79. package/dist/lib/crabbox/runtimes.js +5 -5
  80. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  81. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  82. package/dist/lib/daemon/auth-sync-service.js +32 -1
  83. package/dist/lib/daemon/daemon.d.ts +6 -16
  84. package/dist/lib/daemon/daemon.js +52 -97
  85. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  86. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  87. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  88. package/dist/lib/daemon/harness-update-service.js +1 -1
  89. package/dist/lib/daemon/runner.d.ts +15 -3
  90. package/dist/lib/daemon/runner.js +49 -24
  91. package/dist/lib/daemon-health.d.ts +1 -2
  92. package/dist/lib/daemon-health.js +7 -6
  93. package/dist/lib/daemon-services.d.ts +1 -1
  94. package/dist/lib/daemon-services.js +1 -11
  95. package/dist/lib/daemon-webhooks.d.ts +8 -7
  96. package/dist/lib/daemon-webhooks.js +10 -9
  97. package/dist/lib/device-config.js +1 -1
  98. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  99. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  100. package/dist/lib/devices/harness-inventory.js +126 -4
  101. package/dist/lib/doctor-diff.js +2 -1
  102. package/dist/lib/exec-account-home.d.ts +38 -0
  103. package/dist/lib/exec-account-home.js +164 -0
  104. package/dist/lib/exec.d.ts +30 -0
  105. package/dist/lib/exec.js +101 -31
  106. package/dist/lib/fleet/apply.d.ts +2 -2
  107. package/dist/lib/fleet/apply.js +4 -4
  108. package/dist/lib/fleet/auth-sync.js +4 -3
  109. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  110. package/dist/lib/harness/adapter.d.ts +7 -7
  111. package/dist/lib/harness/adapter.js +10 -6
  112. package/dist/lib/harness/adapters/grok.js +8 -3
  113. package/dist/lib/harness/adapters/muse.js +1 -1
  114. package/dist/lib/harness/adapters/opencode.js +12 -3
  115. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  116. package/dist/lib/harness-auth-capabilities.js +52 -0
  117. package/dist/lib/helper-versions.d.ts +6 -4
  118. package/dist/lib/helper-versions.js +5 -4
  119. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  120. package/dist/lib/hosts/credential-transport.js +37 -0
  121. package/dist/lib/hosts/dispatch.d.ts +7 -0
  122. package/dist/lib/hosts/dispatch.js +13 -6
  123. package/dist/lib/identity/client.d.ts +3 -3
  124. package/dist/lib/identity/client.js +3 -3
  125. package/dist/lib/installations/index.d.ts +1 -1
  126. package/dist/lib/installations/index.js +1 -1
  127. package/dist/lib/installations/migrate.js +9 -0
  128. package/dist/lib/installations/resolve.js +1 -1
  129. package/dist/lib/installations/shims.d.ts +21 -4
  130. package/dist/lib/installations/shims.js +86 -5
  131. package/dist/lib/installations/store.d.ts +43 -0
  132. package/dist/lib/installations/store.js +75 -7
  133. package/dist/lib/installations/versions.js +1 -1
  134. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  135. package/dist/lib/menubar/install-menubar.js +3 -3
  136. package/dist/lib/native-accounts.d.ts +35 -0
  137. package/dist/lib/native-accounts.js +41 -0
  138. package/dist/lib/net-close.d.ts +11 -0
  139. package/dist/lib/net-close.js +25 -0
  140. package/dist/lib/openclaw-keychain.js +27 -1
  141. package/dist/lib/profiles.js +22 -9
  142. package/dist/lib/project-resources.js +1 -1
  143. package/dist/lib/reserved-stores.d.ts +53 -0
  144. package/dist/lib/reserved-stores.js +120 -0
  145. package/dist/lib/secrets-client.d.ts +199 -0
  146. package/dist/lib/secrets-client.js +718 -0
  147. package/dist/lib/secrets-policy.d.ts +235 -0
  148. package/dist/lib/secrets-policy.js +505 -0
  149. package/dist/lib/secrets-types.d.ts +188 -0
  150. package/dist/lib/secrets-types.js +24 -0
  151. package/dist/lib/self-heal/checks/shims.js +5 -3
  152. package/dist/lib/service-manifest.js +3 -4
  153. package/dist/lib/session/db.d.ts +18 -0
  154. package/dist/lib/session/db.js +47 -14
  155. package/dist/lib/session/sync/config.js +2 -2
  156. package/dist/lib/sha256-asset.d.ts +11 -16
  157. package/dist/lib/sha256-asset.js +11 -16
  158. package/dist/lib/share/config.js +14 -5
  159. package/dist/lib/signin-badge.d.ts +26 -0
  160. package/dist/lib/signin-badge.js +42 -0
  161. package/dist/lib/staleness/detectors/workflows.js +3 -105
  162. package/dist/lib/staleness/writers/workflows.js +2 -1
  163. package/dist/lib/state.d.ts +7 -4
  164. package/dist/lib/state.js +18 -7
  165. package/dist/lib/sync-umbrella.js +2 -2
  166. package/dist/lib/teams/agents.js +1 -1
  167. package/dist/lib/types.d.ts +74 -33
  168. package/dist/lib/view-types.d.ts +6 -3
  169. package/dist/lib/workflows-registry.d.ts +71 -0
  170. package/dist/lib/workflows-registry.js +280 -0
  171. package/dist/lib/workflows.d.ts +21 -21
  172. package/dist/lib/workflows.js +12 -327
  173. package/package.json +2 -3
  174. package/scripts/postinstall.js +35 -30
  175. package/dist/commands/secrets-import.d.ts +0 -18
  176. package/dist/commands/secrets-import.js +0 -74
  177. package/dist/commands/secrets-migrate.d.ts +0 -25
  178. package/dist/commands/secrets-migrate.js +0 -334
  179. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  180. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  181. package/dist/commands/secrets-sync.d.ts +0 -11
  182. package/dist/commands/secrets-sync.js +0 -153
  183. package/dist/commands/secrets-vault.d.ts +0 -10
  184. package/dist/commands/secrets-vault.js +0 -130
  185. package/dist/commands/secrets.d.ts +0 -212
  186. package/dist/commands/secrets.js +0 -3030
  187. package/dist/lib/accounts/connect.d.ts +0 -176
  188. package/dist/lib/accounts/connect.js +0 -453
  189. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  190. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  191. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  192. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  193. package/dist/lib/secrets/agent.d.ts +0 -358
  194. package/dist/lib/secrets/agent.js +0 -1291
  195. package/dist/lib/secrets/audit.d.ts +0 -46
  196. package/dist/lib/secrets/audit.js +0 -101
  197. package/dist/lib/secrets/bundles.d.ts +0 -290
  198. package/dist/lib/secrets/bundles.js +0 -1547
  199. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  200. package/dist/lib/secrets/download-keychain.js +0 -70
  201. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  202. package/dist/lib/secrets/drivers/rush.js +0 -90
  203. package/dist/lib/secrets/fallback.d.ts +0 -48
  204. package/dist/lib/secrets/fallback.js +0 -48
  205. package/dist/lib/secrets/filestore.d.ts +0 -222
  206. package/dist/lib/secrets/filestore.js +0 -1099
  207. package/dist/lib/secrets/headless.d.ts +0 -43
  208. package/dist/lib/secrets/headless.js +0 -56
  209. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  210. package/dist/lib/secrets/icloud-import.js +0 -206
  211. package/dist/lib/secrets/index.d.ts +0 -451
  212. package/dist/lib/secrets/index.js +0 -1568
  213. package/dist/lib/secrets/install-helper.d.ts +0 -72
  214. package/dist/lib/secrets/install-helper.js +0 -245
  215. package/dist/lib/secrets/lease.d.ts +0 -25
  216. package/dist/lib/secrets/lease.js +0 -44
  217. package/dist/lib/secrets/linux.d.ts +0 -77
  218. package/dist/lib/secrets/linux.js +0 -393
  219. package/dist/lib/secrets/list-filter.d.ts +0 -109
  220. package/dist/lib/secrets/list-filter.js +0 -261
  221. package/dist/lib/secrets/mcp.d.ts +0 -93
  222. package/dist/lib/secrets/mcp.js +0 -211
  223. package/dist/lib/secrets/profiles.d.ts +0 -10
  224. package/dist/lib/secrets/profiles.js +0 -13
  225. package/dist/lib/secrets/push.d.ts +0 -133
  226. package/dist/lib/secrets/push.js +0 -273
  227. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  228. package/dist/lib/secrets/rc-hygiene.js +0 -143
  229. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  230. package/dist/lib/secrets/read-backoff.js +0 -64
  231. package/dist/lib/secrets/reaper.d.ts +0 -90
  232. package/dist/lib/secrets/reaper.js +0 -243
  233. package/dist/lib/secrets/remote.d.ts +0 -152
  234. package/dist/lib/secrets/remote.js +0 -344
  235. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  236. package/dist/lib/secrets/reserved-sync.js +0 -147
  237. package/dist/lib/secrets/scope.d.ts +0 -26
  238. package/dist/lib/secrets/scope.js +0 -29
  239. package/dist/lib/secrets/session-store.d.ts +0 -107
  240. package/dist/lib/secrets/session-store.js +0 -342
  241. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  242. package/dist/lib/secrets/sync-backend.js +0 -13
  243. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  244. package/dist/lib/secrets/sync-commands.js +0 -21
  245. package/dist/lib/secrets/sync.d.ts +0 -48
  246. package/dist/lib/secrets/sync.js +0 -237
  247. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  248. package/dist/lib/secrets/unlock-hints.js +0 -36
  249. package/dist/lib/secrets/usage-db.d.ts +0 -46
  250. package/dist/lib/secrets/usage-db.js +0 -96
  251. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  252. package/dist/lib/secrets/vault-age-helper.js +0 -34
  253. package/dist/lib/secrets/vault.d.ts +0 -49
  254. package/dist/lib/secrets/vault.js +0 -399
  255. package/dist/lib/secrets/windows.d.ts +0 -81
  256. package/dist/lib/secrets/windows.js +0 -556
  257. package/scripts/install-helper.js +0 -97
  258. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  259. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -19,14 +19,29 @@
19
19
  * (`AbortSignal.timeout` at the leaf), and the supervisor abandons + restarts a
20
20
  * hung tick regardless.
21
21
  */
22
+ import * as fsp from 'node:fs/promises';
23
+ import * as path from 'node:path';
22
24
  import { BasePeriodicService } from './service.js';
23
- import { runUsageRefreshTick, runFleetCacheWarmTick } from '../daemon-ticks.js';
25
+ import { runUsageRefreshTick, refreshLocalFleetAuthState } from '../daemon-ticks.js';
26
+ import { getDaemonDir } from '../state.js';
27
+ import { getCliLaunch } from '../cli-entry.js';
28
+ import { execFileBounded } from '../exec-bounded.js';
29
+ import { fleetSharedStatePath, FLEET_SHARED_STATE_VERSION } from '../fleet-shared-state.js';
30
+ import { withFileLockAsync } from '../fs-atomic.js';
31
+ import { machineId } from '../machine-id.js';
32
+ import { getConfigValueAsync, isHeadedDeviceRole } from '../device-config.js';
33
+ import { harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
24
34
  /** Usage cache refresh cadence. */
25
35
  export const USAGE_STATE_TICK_MS = 60_000;
26
36
  /** Fleet-auth refresh cadence — deliberately slower (rate-limited endpoint). */
27
37
  export const AUTH_STATE_TICK_MS = 3 * 60_000;
28
38
  /** A hung refresh is abandoned at this deadline; a real sweep is far under it. */
29
39
  const REFRESH_DEADLINE_MS = 2 * 60_000;
40
+ const NOTIFY_DEADLINE_MS = 30_000;
41
+ const ACCOUNT_TRANSITIONS_FILE = 'account-auth-transitions.json';
42
+ // Resolve the current CLI once at daemon startup. getCliLaunch performs sync
43
+ // filesystem probes, so it must never run inside a service tick.
44
+ const FEED_POST_LAUNCH = getCliLaunch([]);
30
45
  /**
31
46
  * A promise that rejects when `signal` aborts (immediately if already aborted),
32
47
  * so a tick can `Promise.race` its work against the supervisor's deadline and
@@ -74,16 +89,197 @@ export class AccountAuthService extends BasePeriodicService {
74
89
  intervalMs = AUTH_STATE_TICK_MS;
75
90
  deadlineMs = REFRESH_DEADLINE_MS;
76
91
  refresh;
77
- constructor(refresh = runFleetCacheWarmTick) {
92
+ stateFile;
93
+ notify;
94
+ constructor(refresh = async (signal) => (await refreshLocalFleetAuthState({ signal })).authRows, options = {}) {
78
95
  super();
79
96
  this.refresh = refresh;
97
+ this.stateFile = options.stateFile ?? path.join(getDaemonDir(), ACCOUNT_TRANSITIONS_FILE);
98
+ this.notify = options.notify ?? postImportantAccountTransition;
80
99
  }
81
100
  async onStart() { }
82
101
  async onStop() { }
83
102
  async onTick(_ctx, signal) {
84
- await Promise.race([
103
+ const rows = await Promise.race([
85
104
  this.refresh(signal),
86
105
  abortRejection(signal, 'auth refresh aborted at deadline'),
87
106
  ]);
107
+ if (rows) {
108
+ await publishAccountDaemonStateRows(rows);
109
+ await processAccountAuthTransitions(rows, {
110
+ stateFile: this.stateFile,
111
+ notify: this.notify,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ export async function publishAccountDaemonStateRows(rows, userAgentsDir) {
117
+ const device = machineId();
118
+ const file = fleetSharedStatePath(device, userAgentsDir);
119
+ await fsp.mkdir(path.dirname(file), { recursive: true });
120
+ const handle = await fsp.open(file, 'a', 0o600);
121
+ await handle.close();
122
+ const role = (await getConfigValueAsync('role', { device })).value;
123
+ const headed = isHeadedDeviceRole(role);
124
+ const accountRows = [...collapseAccountRows(rows).values()].map((row) => ({
125
+ // The registered account id is the only stable identity: two accounts may
126
+ // share one email (different orgs), so the display label is never a key.
127
+ // Unregistered legacy homes have no registry id and fall back to the label.
128
+ accountId: row.accountId ?? row.account ?? row.version,
129
+ identityLabel: row.account,
130
+ harness: row.agent,
131
+ authMode: harnessWorkerIsPerDevice(row.agent)
132
+ ? 'per-device'
133
+ : headed ? 'native' : 'durable',
134
+ verdict: row.health.verdict === 'unconfigured'
135
+ ? 'missing'
136
+ : row.health.verdict === 'error' ? 'unverified' : row.health.verdict,
137
+ checkedAt: new Date(row.health.checkedAt).toISOString(),
138
+ }));
139
+ await withFileLockAsync(file, async () => {
140
+ const raw = await fsp.readFile(file, 'utf-8');
141
+ let current = {};
142
+ if (raw.trim()) {
143
+ const parsed = JSON.parse(raw);
144
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
145
+ throw new Error(`Cannot publish account state: ${file} is not an object.`);
146
+ }
147
+ current = parsed;
148
+ if (current.device !== device || current.version !== FLEET_SHARED_STATE_VERSION) {
149
+ throw new Error(`Cannot publish account state: ${file} has an unrecognized owner or version.`);
150
+ }
151
+ }
152
+ const next = {
153
+ ...current,
154
+ version: FLEET_SHARED_STATE_VERSION,
155
+ device,
156
+ accounts: { rows: accountRows },
157
+ };
158
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
159
+ if (serialized === raw)
160
+ return;
161
+ const temp = `${file}.${process.pid}.accounts.tmp`;
162
+ await fsp.writeFile(temp, serialized, { mode: 0o600 });
163
+ await fsp.rename(temp, file);
164
+ });
165
+ }
166
+ function transitionKey(row) {
167
+ // Stable identity first (registered account id); the display label is the
168
+ // fallback for unregistered legacy homes, never the key for a named account.
169
+ return `${row.agent}:${row.accountId ?? row.account ?? `version:${row.version}`}`;
170
+ }
171
+ function verdictRank(verdict) {
172
+ switch (verdict) {
173
+ case 'revoked': return 7;
174
+ case 'expired': return 6;
175
+ case 'rate_limited': return 5;
176
+ case 'live': return 4;
177
+ case 'unverified': return 3;
178
+ case 'error': return 2;
179
+ case 'unconfigured': return 1;
180
+ }
181
+ }
182
+ function collapseAccountRows(rows) {
183
+ const accounts = new Map();
184
+ for (const row of rows) {
185
+ const key = transitionKey(row);
186
+ const current = accounts.get(key);
187
+ if (!current || verdictRank(row.health.verdict) > verdictRank(current.health.verdict)) {
188
+ accounts.set(key, row);
189
+ }
190
+ }
191
+ return accounts;
192
+ }
193
+ async function readTransitionState(file) {
194
+ try {
195
+ const parsed = JSON.parse(await fsp.readFile(file, 'utf-8'));
196
+ if ((parsed.version === 1 || parsed.version === 2) && parsed.entries && typeof parsed.entries === 'object') {
197
+ // Version 1 files predate the outbox; they upgrade with an empty one.
198
+ return { version: 2, entries: parsed.entries, pending: parsed.pending ?? {} };
199
+ }
200
+ }
201
+ catch (error) {
202
+ if (error.code !== 'ENOENT') {
203
+ throw new Error(`Cannot read account transition state at ${file}: ${error.message}`);
204
+ }
205
+ }
206
+ return { version: 2, entries: {}, pending: {} };
207
+ }
208
+ async function writeTransitionState(file, state) {
209
+ await fsp.mkdir(path.dirname(file), { recursive: true });
210
+ const temp = `${file}.${process.pid}.tmp`;
211
+ await fsp.writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
212
+ await fsp.rename(temp, file);
213
+ }
214
+ /**
215
+ * Detect live → expired/revoked transitions and deliver one owner-important
216
+ * feed post each, through a persisted outbox:
217
+ *
218
+ * 1. Newly-dead accounts are ENQUEUED into `pending` and the state is written
219
+ * BEFORE any delivery is attempted — a crash or a failed sink can never
220
+ * strand a transition in the already-recorded `entries` with no retry.
221
+ * 2. Every pending item (fresh and retried) is delivered; an item leaves
222
+ * `pending` only after its notifier resolves, and the state is written
223
+ * again. A crash in the narrow window between a successful post and that
224
+ * second write re-sends once — at-least-once, never zero.
225
+ *
226
+ * Delivery failures keep their outbox entries AND throw after the state is
227
+ * durable, so the supervisor's circuit breaker still sees the unhealthy tick.
228
+ */
229
+ export async function processAccountAuthTransitions(rows, options = {}) {
230
+ const file = options.stateFile ?? path.join(getDaemonDir(), ACCOUNT_TRANSITIONS_FILE);
231
+ const notify = options.notify ?? postImportantAccountTransition;
232
+ const previous = await readTransitionState(file);
233
+ const next = {
234
+ version: 2,
235
+ entries: { ...previous.entries },
236
+ pending: { ...previous.pending },
237
+ };
238
+ for (const [key, row] of collapseAccountRows(rows)) {
239
+ const before = previous.entries[key]?.verdict;
240
+ const after = row.health.verdict;
241
+ next.entries[key] = { verdict: after, checkedAt: row.health.checkedAt };
242
+ if (before === 'live' && (after === 'expired' || after === 'revoked') && !next.pending[key]) {
243
+ next.pending[key] = {
244
+ agent: row.agent,
245
+ account: row.account ?? row.version,
246
+ verdict: after,
247
+ };
248
+ }
249
+ }
250
+ // Durable BEFORE delivery: a pending entry survives a crash mid-notify.
251
+ await writeTransitionState(file, next);
252
+ const delivered = [];
253
+ const failures = [];
254
+ for (const [key, transition] of Object.entries(next.pending)) {
255
+ try {
256
+ await notify(transition);
257
+ delete next.pending[key];
258
+ delivered.push(transition);
259
+ }
260
+ catch (error) {
261
+ failures.push(`${transition.agent} account ${transition.account}: ${error.message}`);
262
+ }
263
+ }
264
+ await writeTransitionState(file, next);
265
+ if (failures.length > 0) {
266
+ throw new Error(`Account expiry notification failed (kept in the outbox for the next tick): ${failures.join('; ')}`);
267
+ }
268
+ return delivered;
269
+ }
270
+ async function postImportantAccountTransition(transition) {
271
+ const title = `${transition.agent} account ${transition.verdict}`;
272
+ const text = `${transition.agent} account ${transition.account} changed from live to ${transition.verdict}. Run agents accounts list ${transition.agent} for the exact fix.`;
273
+ const args = [...FEED_POST_LAUNCH.args,
274
+ 'feed', 'post',
275
+ '--session', 'account-state-daemon',
276
+ '--title', title,
277
+ '--level', 'important',
278
+ text,
279
+ ];
280
+ const result = await execFileBounded(FEED_POST_LAUNCH.command, args, { timeoutMs: NOTIFY_DEADLINE_MS });
281
+ if (result.code !== 0) {
282
+ const detail = result.timedOut ? 'timed out' : (result.stderr.trim() || `exit ${result.code}`);
283
+ throw new Error(`Account expiry feed notification failed: ${detail}`);
88
284
  }
89
285
  }
@@ -22,7 +22,24 @@ export class AuthSyncService extends BasePeriodicService {
22
22
  // Nothing to release — the supervisor's timer teardown is the only cleanup.
23
23
  }
24
24
  async onTick(ctx) {
25
- const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets/reserved-sync.js');
25
+ const { publishReservedAuthVerdict, reconcileLocalWorkerSlots, syncReservedAuthBundle, syncReservedStores, } = await import('../secrets-policy.js');
26
+ // Worker-side slot materialization FIRST (PHNX-3940 T6): for each registered
27
+ // account whose durable key is already on this box, create the HOME-shaped
28
+ // slot the picker and spawn read. It touches only local state — the registry
29
+ // copy and the file-backed store — so it never waits on the git exchange
30
+ // below. It used to sit after `if (!transport.success) return`, so a single
31
+ // `git rebase timed out` postponed every slot on the box by another tick.
32
+ // Self-gated on device role: a headed box returns immediately.
33
+ try {
34
+ const slots = reconcileLocalWorkerSlots();
35
+ if (slots.provisioned.length > 0)
36
+ ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
37
+ for (const err of slots.errors)
38
+ ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
39
+ }
40
+ catch (err) {
41
+ ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
42
+ }
26
43
  const published = await publishReservedAuthVerdict();
27
44
  if (published.error)
28
45
  ctx.log('WARN', `auth-sync: verdict: ${published.error}`);
@@ -44,5 +61,19 @@ export class AuthSyncService extends BasePeriodicService {
44
61
  for (const err of result.errors) {
45
62
  ctx.log('WARN', `auth-sync: ${err.device}: ${err.message}`);
46
63
  }
64
+ // Generalized per-account, per-key, per-role reserved-store push (PHNX-3940
65
+ // T6). On the elected headed publisher this pushes every portable account's
66
+ // reserved `__<harness>__` store to the worker peers missing it. The push is
67
+ // the only transport — provisioning (above) writes only locally (invariant 1).
68
+ try {
69
+ const stores = await syncReservedStores();
70
+ for (const p of stores.pushed)
71
+ ctx.log('INFO', `auth-sync: pushed ${p.bundle} (${p.keys.length} key(s)) to ${p.device}`);
72
+ for (const err of stores.errors)
73
+ ctx.log('WARN', `auth-sync: reserved-store ${err.device}: ${err.message}`);
74
+ }
75
+ catch (err) {
76
+ ctx.log('WARN', `auth-sync: reserved-store sync: ${err.message}`);
77
+ }
47
78
  }
48
79
  }
@@ -53,15 +53,6 @@ export declare function daemonSystemdUnitName(): string;
53
53
  * start` is the deliberate override and is never gated by this.
54
54
  */
55
55
  export declare const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
56
- /**
57
- * RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
58
- * broker. The startup host decision is one-shot; this drives the periodic
59
- * self-heal re-check. Take over ONLY when the daemon is not already hosting AND
60
- * no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
61
- * start has since died or crash-looped. Never take over while our in-process
62
- * broker is hosting, and never clobber a reachable (healthy) broker.
63
- */
64
- export declare function shouldTakeOverBroker(isHosting: boolean, brokerReachable: boolean): boolean;
65
56
  /**
66
57
  * What a gate re-evaluation must do with the routines scheduler. The daemon
67
58
  * re-evaluates `scheduler.enabled` on every SIGHUP reload so flipping the key
@@ -436,13 +427,12 @@ export declare function findSurvivingStateDirDaemons(exclude: Set<number>): numb
436
427
  * from launchd/systemd if applicable.
437
428
  *
438
429
  * The SIGTERM → grace → killTree sequence is unchanged; what it adds is
439
- * verification. After the daemon is gone it checks that the secrets broker socket
440
- * and browser IPC binding actually released — a stale socket present on disk but
441
- * with no live owner is the orphan that keeps clients holding unlocked bundles
442
- * hanging (`daemon.ts` broker-hosting; the two-brokers-on-one-socket bug) — and
443
- * that no `__daemon-run` for THIS state dir survives. A killTree escalation exits
444
- * without running the daemon's graceful handleShutdown, so those sockets can be
445
- * left stale; this reclaims each (the owner is provably dead) and reports it. It
430
+ * verification. After the daemon is gone it checks that the browser IPC
431
+ * binding actually released — a stale socket present on disk but with no live
432
+ * owner is the orphan that keeps clients hanging on it — and that no
433
+ * `__daemon-run` for THIS state dir survives. A killTree escalation exits
434
+ * without running the daemon's graceful handleShutdown, so that socket can be
435
+ * left stale; this reclaims it (the owner is provably dead) and reports it. It
446
436
  * never reports success on an unverified stop.
447
437
  */
448
438
  export declare function stopDaemon(): DaemonStopResult;
@@ -24,7 +24,6 @@ import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } fro
24
24
  import { notifyOwnerRoutineFinish, notifyOwnerRoutineStartFailed } from '../routine-notify-owner.js';
25
25
  import { BrowserService } from '../browser/service.js';
26
26
  import { getSocketPath as getBrowserIpcSocketPath } from '../browser/ipc.js';
27
- import { secretsBrokerSocketPath, brokerPidAlive } from '../secrets/agent.js';
28
27
  import { redactSecrets } from '../redact.js';
29
28
  import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from '../cli-entry.js';
30
29
  import { localBinDir } from '../platform/posixpath.js';
@@ -33,7 +32,6 @@ import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, re
33
32
  import { ServiceSupervisor } from './supervisor.js';
34
33
  import { SessionIndexService } from './session-index-service.js';
35
34
  import { SessionSummarizerService } from './session-summarizer-service.js';
36
- import { SecretsBrokerService } from './secrets-broker-service.js';
37
35
  import { MonitorEngineService } from './monitor-engine-service.js';
38
36
  import { AccountUsageService, AccountAuthService } from './account-state-daemon-service.js';
39
37
  import { CatchupService } from './catchup-service.js';
@@ -43,7 +41,6 @@ import { DeviceProbeService } from './device-probe-service.js';
43
41
  import { SelfHealService } from './self-heal-service.js';
44
42
  import { SelfUpdateService } from './self-update-service.js';
45
43
  import { HarnessUpdateService } from './harness-update-service.js';
46
- import { KeychainReapService } from './keychain-reap-service.js';
47
44
  import { AuthSyncService } from './auth-sync-service.js';
48
45
  import { UsageSyncService } from './usage-sync-service.js';
49
46
  import { StateDirCheckService } from './state-dir-check-service.js';
@@ -125,18 +122,15 @@ export function daemonSystemdUnitName() {
125
122
  /**
126
123
  * Cadences for the in-process background ticks, named here beside the other
127
124
  * tick constants rather than left as inline literals at their `setInterval`
128
- * (RUSH-2423). Self-heal, keychain-reap, state-dir-check, watchdog, and
129
- * device-probe cadences moved to their own `*-service.ts` files (RUSH-3193
130
- * P3, alongside session-index/account-state/etc.) — see those files for the
131
- * per-service trade-offs (self-heal's 6h/cheap-to-be-late repair cadence,
132
- * keychain-reap's 5min `ps`-shell cost bound, state-dir-check's env override
133
- * for tests, watchdog/device-probe's shared 3min in-process housekeeping
134
- * cadence, NOT a routine — RUSH-2495).
135
- *
136
- * - **Broker self-heal** is a bare `agentPing`, and the failure it recovers
137
- * from wedges every keychain-backed secret on the box, so it runs minutely.
125
+ * (RUSH-2423). Self-heal, state-dir-check, watchdog, and device-probe
126
+ * cadences moved to their own `*-service.ts` files (RUSH-3193 P3, alongside
127
+ * session-index/account-state/etc.) — see those files for the per-service
128
+ * trade-offs (self-heal's 6h/cheap-to-be-late repair cadence, state-dir-check's
129
+ * env override for tests, watchdog/device-probe's shared 3min in-process
130
+ * housekeeping cadence, NOT a routine — RUSH-2495). The secrets broker and its
131
+ * self-heal/reap ticks moved out of this daemon entirely with the standalone
132
+ * `secrets` engine (PHNX-3989 OWN-1) — this daemon no longer hosts that broker.
138
133
  */
139
- // BROKER_SELF_HEAL_TICK_MS moved to secrets-broker-service.ts (RUSH-3193 P2).
140
134
  // Session-index warm interval/deadline live in session-index-service.ts now
141
135
  // (RUSH-3193 — migrated onto ServiceSupervisor).
142
136
  const WEDGE_THRESHOLD_TICKS = 3;
@@ -169,17 +163,6 @@ const DAEMON_START_LIMIT_BURST = 5;
169
163
  * start` is the deliberate override and is never gated by this.
170
164
  */
171
165
  export const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
172
- /**
173
- * RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
174
- * broker. The startup host decision is one-shot; this drives the periodic
175
- * self-heal re-check. Take over ONLY when the daemon is not already hosting AND
176
- * no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
177
- * start has since died or crash-looped. Never take over while our in-process
178
- * broker is hosting, and never clobber a reachable (healthy) broker.
179
- */
180
- export function shouldTakeOverBroker(isHosting, brokerReachable) {
181
- return !isHosting && !brokerReachable;
182
- }
183
166
  export function schedulerGateTransition(running, enabled) {
184
167
  if (running)
185
168
  return enabled ? 'reload' : 'stop';
@@ -504,11 +487,10 @@ export function claimDaemonInstance() {
504
487
  const existing = resolveLiveDaemonPid(true);
505
488
  if (existing !== null && existing !== process.pid) {
506
489
  // Evict, and WAIT for the incumbent to be provably dead — its graceful
507
- // handleShutdown releasing the browser IPC binding and the secrets broker
508
- // socket — before we write our pid and (later, in runDaemon) bind our own.
509
- // Binding before the release recreates the two-brokers-on-one-socket orphan
510
- // documented at stopDaemon below, so the pid file is not written until the
511
- // prior owner is gone.
490
+ // handleShutdown releasing the browser IPC binding — before we write our
491
+ // pid and (later, in runDaemon) bind our own. Binding before the release
492
+ // recreates the two-owners-on-one-socket orphan documented at stopDaemon
493
+ // below, so the pid file is not written until the prior owner is gone.
512
494
  if (!evictIncumbentDaemon(existing))
513
495
  return false;
514
496
  }
@@ -521,8 +503,10 @@ export function claimDaemonInstance() {
521
503
  }
522
504
  /**
523
505
  * SIGTERM a live incumbent daemon and block until it is provably dead, so its
524
- * graceful handleShutdown has released the browser IPC binding and the secrets
525
- * broker socket BEFORE the newcomer binds anything of its own (SING-11). Escalates
506
+ * graceful handleShutdown has released the browser IPC binding BEFORE the
507
+ * newcomer binds anything of its own (SING-11) — the daemon no longer hosts a
508
+ * secrets broker socket to release (the standalone `secrets` CLI owns it now,
509
+ * PHNX-3989 OWN-1). Escalates
526
510
  * to killTree after the grace window. Passes the POSITIVE pid so the kill reaches
527
511
  * only the incumbent daemon — never its detached routine children, which run in
528
512
  * their own process groups and must survive takeover (SING-11a); the new daemon
@@ -878,12 +862,12 @@ export function assertTestDaemonHome(daemonDir = getDaemonDir(), testHome = proc
878
862
  // named in stack traces, readable without scrolling through runDaemon's
879
863
  // 500-line body, and not recreated on every function invocation.
880
864
  //
881
- // self-heal and keychain-reap moved to SelfHealService / KeychainReapService
882
- // on the ServiceSupervisor (RUSH-3193 P3) — the supervisor's own per-tick
883
- // deadline + inFlight guard replaces their local `healing`/`reapingKeychain`
884
- // flags. state-dir-check moved to StateDirCheckService (RUSH-3193 P3),
885
- // registered after `handleShutdown` is declared — see its registration site
886
- // below. runBrokerSelfHeal was moved into SecretsBrokerService (RUSH-3193 P2).
865
+ // self-heal moved to SelfHealService on the ServiceSupervisor (RUSH-3193 P3)
866
+ // — the supervisor's own per-tick deadline + inFlight guard replaces its local
867
+ // `healing` flag. state-dir-check moved to StateDirCheckService (RUSH-3193
868
+ // P3), registered after `handleShutdown` is declared — see its registration
869
+ // site below. The secrets broker (self-heal, reap, and hosting) moved out of
870
+ // this daemon entirely with the standalone `secrets` engine (PHNX-3989 OWN-1).
887
871
  // ---------------------------------------------------------------------------
888
872
  export async function runDaemon() {
889
873
  // PHNX-2545 test-home tripwire — FIRST, before this daemon claims an instance,
@@ -999,21 +983,17 @@ export async function runDaemon() {
999
983
  catch (err) {
1000
984
  log('ERROR', `Stray daemon reaper failed: ${err.message}`);
1001
985
  }
1002
- // Socket services: secrets broker, monitor engine, account-state, and
1003
- // browser IPC are all managed by the ServiceSupervisor (RUSH-3193 P2).
1004
- // The supervisor is created here — before the scheduler — so the secrets
1005
- // broker starts socket-first (#416) and `agents secrets` resolves within
1006
- // ms of daemon start, exactly as before.
986
+ // Socket services: monitor engine, account-state, and browser IPC are all
987
+ // managed by the ServiceSupervisor (RUSH-3193 P2). The secrets broker moved
988
+ // with the standalone `secrets` engine (PHNX-3989 OWN-1) — this daemon no
989
+ // longer hosts or takes over that broker; the standalone owns its own
990
+ // lifecycle exclusively.
1007
991
  const supervisor = new ServiceSupervisor();
1008
992
  if (isEnabled('session-state')) {
1009
993
  supervisor.register(new SessionStateService(() => supervisor.runNow('session-state')));
1010
994
  }
1011
995
  else
1012
996
  log('INFO', 'Live session-state service disabled');
1013
- if (isEnabled('secrets-broker'))
1014
- supervisor.register(new SecretsBrokerService());
1015
- else
1016
- log('INFO', 'Secrets broker service disabled; daemon not hosting it');
1017
997
  const monitorEngineSvc = new MonitorEngineService();
1018
998
  if (isEnabled('monitors'))
1019
999
  supervisor.register(monitorEngineSvc);
@@ -1071,10 +1051,10 @@ export async function runDaemon() {
1071
1051
  supervisor.register(new SessionSummarizerService());
1072
1052
  else
1073
1053
  log('INFO', 'Session summarizer service disabled');
1074
- // Watchdog, device-probe, self-heal, and keychain-reap are all periodic
1075
- // services managed by the ServiceSupervisor (RUSH-3193 P3). Each is gated
1076
- // the same way as the socket services above; state-dir-check is registered
1077
- // separately, later, after `handleShutdown` exists (see below).
1054
+ // Watchdog, device-probe, and self-heal are all periodic services managed
1055
+ // by the ServiceSupervisor (RUSH-3193 P3). Each is gated the same way as
1056
+ // the socket services above; state-dir-check is registered separately,
1057
+ // later, after `handleShutdown` exists (see below).
1078
1058
  if (isEnabled('watchdog'))
1079
1059
  supervisor.register(new WatchdogService());
1080
1060
  else
@@ -1095,10 +1075,6 @@ export async function runDaemon() {
1095
1075
  supervisor.register(new HarnessUpdateService());
1096
1076
  else
1097
1077
  log('INFO', 'Harness-update service disabled');
1098
- if (isEnabled('keychain-reap'))
1099
- supervisor.register(new KeychainReapService());
1100
- else
1101
- log('INFO', 'Keychain-reap service disabled');
1102
1078
  if (isEnabled('auth-sync'))
1103
1079
  supervisor.register(new AuthSyncService());
1104
1080
  else
@@ -1129,7 +1105,7 @@ export async function runDaemon() {
1129
1105
  activeServiceSupervisor = supervisor;
1130
1106
  // scheduler.enabled=false in this machine's device doc means NO routines fire
1131
1107
  // here — the scheduler and its catchup recovery simply never start, while the
1132
- // daemon keeps its other duties (secrets broker, browser IPC, session sync).
1108
+ // daemon keeps its other duties (browser IPC, session sync).
1133
1109
  // The refusal message is the same one the start surfaces
1134
1110
  // (`routines add` auto-start, manual `routines start`) raise. The gate is
1135
1111
  // re-evaluated on every SIGHUP reload (handleReload below) via
@@ -1338,22 +1314,23 @@ export async function runDaemon() {
1338
1314
  // Browser orphan reap and IPC server start are now managed by BrowserIPCService
1339
1315
  // on the supervisor (RUSH-3193 P2). The orphan reap runs inside onStart().
1340
1316
  // Webhook receivers: signed webhook receiver(s) + their funnel (RUSH-2548).
1341
- // Started after the broker (above) so it resolves each receiver's signing
1342
- // secret headlessly — no AGENTS_SECRETS_PASSPHRASE, no nohup. Binds nothing
1343
- // unless daemon/webhooks.yaml declares a receiver, so an unconfigured box no-ops.
1317
+ // Resolves each receiver's signing secret headlessly through the standalone
1318
+ // `secrets` CLI (an agentOnly secrets-client read) — no AGENTS_SECRETS_PASSPHRASE,
1319
+ // no nohup. Binds nothing unless daemon/webhooks.yaml declares a receiver, so an
1320
+ // unconfigured box no-ops.
1344
1321
  // Signed webhook ingress is owned by WebhookReceiverService, including
1345
1322
  // per-service failure isolation, measured health, and shutdown cleanup.
1346
1323
  // Resource self-heal is now managed by SelfHealService on the supervisor
1347
1324
  // (RUSH-3193 P3), registered above alongside the socket services. The
1348
1325
  // supervisor's immediate first tick on start replaces the old
1349
1326
  // SELF_HEAL_KICKOFF_MS (30s) delayed kickoff timer — see self-heal-service.ts.
1350
- // Broker self-heal (RUSH-1817) is now encapsulated in SecretsBrokerService
1351
- // (RUSH-3193 P2). The interval fires inside onStart() and is stopped in onStop().
1352
- // RUSH-2232: keychain-reap is now managed by KeychainReapService on the
1353
- // supervisor (RUSH-3193 P3), registered above alongside the socket services.
1354
- // RUSH-2501: reap tmux sessions whose panes are all dead. Runs on the same
1355
- // 5-min cadence as the keychain reaper. Daemon-only (single executor).
1356
- // Dead managed panes and their orphan helpers are reaped by TmuxReapService.
1327
+ // The secrets broker's self-heal and keychain-reap ticks (formerly
1328
+ // SecretsBrokerService / KeychainReapService, RUSH-1817 / RUSH-2232) moved
1329
+ // out of this daemon entirely with the standalone `secrets` engine
1330
+ // (PHNX-3989 OWN-1) — the standalone owns its own broker lifecycle.
1331
+ // RUSH-2501: reap tmux sessions whose panes are all dead. Daemon-only
1332
+ // (single executor). Dead managed panes and their orphan helpers are
1333
+ // reaped by TmuxReapService.
1357
1334
  // RUSH-2622: close abandoned browser-task tabs on the same 5-min cadence,
1358
1335
  // reusing the daemon's long-lived BrowserService when browser IPC is enabled.
1359
1336
  // Abandoned browser tasks are reaped by BrowserTaskReapService, sharing the
@@ -1501,7 +1478,7 @@ export async function runDaemon() {
1501
1478
  });
1502
1479
  // State-dir self-check (RUSH-2367 self-terminate guard) is registered on
1503
1480
  // the supervisor here — AFTER `handleShutdown` above — rather than
1504
- // alongside watchdog/device-probe/self-heal/keychain-reap earlier. The
1481
+ // alongside watchdog/device-probe/self-heal earlier. The
1505
1482
  // supervisor fires an immediate first tick on `register()`+`start()`; doing
1506
1483
  // that before `handleShutdown` exists would reference the const in its
1507
1484
  // temporal dead zone the moment a mismatch is ever detected. Registering it
@@ -1661,9 +1638,8 @@ Environment=AGENTS_REAL_HOME=${realHome}
1661
1638
  WantedBy=default.target`;
1662
1639
  }
1663
1640
  // Binary-resolution helpers (getAgentsBinPath / isNodeScriptEntry / getCliLaunch)
1664
- // live in ./cli-entry.js — a leaf module the secrets broker also imports without
1665
- // forming a cycle. Re-exported so existing `from './daemon.js'` importers of
1666
- // getAgentsBinPath keep resolving.
1641
+ // live in ./cli-entry.js — a leaf module. Re-exported so existing
1642
+ // `from './daemon.js'` importers of getAgentsBinPath keep resolving.
1667
1643
  export { getAgentsBinPath };
1668
1644
  /**
1669
1645
  * Ask the service manager for the daemon's live PID. Used as a fallback when
@@ -2207,13 +2183,12 @@ export function findSurvivingStateDirDaemons(exclude) {
2207
2183
  * from launchd/systemd if applicable.
2208
2184
  *
2209
2185
  * The SIGTERM → grace → killTree sequence is unchanged; what it adds is
2210
- * verification. After the daemon is gone it checks that the secrets broker socket
2211
- * and browser IPC binding actually released — a stale socket present on disk but
2212
- * with no live owner is the orphan that keeps clients holding unlocked bundles
2213
- * hanging (`daemon.ts` broker-hosting; the two-brokers-on-one-socket bug) — and
2214
- * that no `__daemon-run` for THIS state dir survives. A killTree escalation exits
2215
- * without running the daemon's graceful handleShutdown, so those sockets can be
2216
- * left stale; this reclaims each (the owner is provably dead) and reports it. It
2186
+ * verification. After the daemon is gone it checks that the browser IPC
2187
+ * binding actually released — a stale socket present on disk but with no live
2188
+ * owner is the orphan that keeps clients hanging on it — and that no
2189
+ * `__daemon-run` for THIS state dir survives. A killTree escalation exits
2190
+ * without running the daemon's graceful handleShutdown, so that socket can be
2191
+ * left stale; this reclaims it (the owner is provably dead) and reports it. It
2217
2192
  * never reports success on an unverified stop.
2218
2193
  */
2219
2194
  export function stopDaemon() {
@@ -2447,26 +2422,6 @@ function stopDaemonLocked() {
2447
2422
  else {
2448
2423
  released.push('browser IPC socket');
2449
2424
  }
2450
- // Secrets broker socket: released when it is gone, or still owned by a
2451
- // DIFFERENT live broker (a standalone service the daemon never hosted). A
2452
- // socket present with NO live owner is the orphan — reclaim it.
2453
- const brokerSock = secretsBrokerSocketPath();
2454
- if (!fs.existsSync(brokerSock)) {
2455
- released.push('secrets broker socket');
2456
- }
2457
- else if (brokerPidAlive()) {
2458
- released.push('secrets broker socket (standalone owner)');
2459
- }
2460
- else {
2461
- try {
2462
- fs.unlinkSync(brokerSock);
2463
- }
2464
- catch { /* raced with a fresh bind */ }
2465
- if (fs.existsSync(brokerSock))
2466
- surviving.push('secrets broker socket not released');
2467
- else
2468
- released.push('secrets broker socket (reclaimed)');
2469
- }
2470
2425
  }
2471
2426
  // ── The state files a killed daemon cannot clean up itself (RUSH-2421) ─────
2472
2427
  // handleShutdown removes the lifetime marker, the heartbeat and this pid's
@@ -1,4 +1,3 @@
1
- import { type KeychainBackend } from '../secrets/index.js';
2
1
  /**
3
2
  * Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
4
3
  *
@@ -9,15 +8,14 @@ import { type KeychainBackend } from '../secrets/index.js';
9
8
  * process-spawning / real-daemon integration tests across workers; the
10
9
  * helpers shared by more than one slice live here.
11
10
  */
12
- /** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
13
- export declare function makeMemoryBackend(): {
14
- backend: KeychainBackend;
15
- store: Map<string, string>;
16
- };
17
11
  /**
18
- * Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
19
- * beforeEach/afterEach every slice of the original daemon.test.ts ran under,
20
- * verbatim. Call once at the top level of each slice file.
12
+ * Isolates every slice against a fresh, empty standalone `secrets` store
13
+ * (PHNX-3989) — an in-process fake keychain can no longer serve reads, since
14
+ * the client always spawns the real standalone, and several slices here also
15
+ * spawn a REAL `__daemon-run` subprocess that must see the SAME isolated
16
+ * store (a fresh `SECRETS_HOME` covers both: `useFreshSecretsHome` sets it as
17
+ * a `process.env` var, which every spawned child inherits by default). Call
18
+ * once at the top level of each slice file.
21
19
  */
22
20
  export declare function installKeychainHermeticity(): void;
23
21
  export declare const REPO_ROOT: string;