@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -13,6 +13,7 @@
13
13
  import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import { createHash } from 'crypto';
16
+ import { spawnSync } from 'child_process';
16
17
  import { getMonitorsHistoryDir, ensureAgentsDir } from '../state.js';
17
18
  import { safeJoin } from '../paths.js';
18
19
  import { readRunMeta } from '../scheduling/routines.js';
@@ -199,11 +200,50 @@ export function listFires(name) {
199
200
  }
200
201
  return fires;
201
202
  }
202
- /** Run statuses that read as a healthy fire: still in flight, or settled clean. */
203
- const OK_RUN_STATUSES = new Set(['running', 'completed']);
203
+ /** Cap a display-time postcondition so `monitors runs` cannot hang on a stuck command. */
204
+ const POSTCONDITION_TIMEOUT_MS = 15_000;
205
+ /**
206
+ * Run a fire's postcondition command. Exit 0 means the intended effect happened;
207
+ * anything else (nonzero, timeout, spawn error, empty command) is "no effect".
208
+ * Real `/bin/sh -c` (or `cmd /c`) — the same seam command sources use.
209
+ */
210
+ export function evaluatePostcondition(command) {
211
+ const trimmed = command.trim();
212
+ if (!trimmed)
213
+ return { ok: false, error: 'postcondition not met: empty command' };
214
+ const [bin, args] = process.platform === 'win32'
215
+ ? ['cmd', ['/c', trimmed]]
216
+ : ['/bin/sh', ['-c', trimmed]];
217
+ const result = spawnSync(bin, args, {
218
+ encoding: 'utf-8',
219
+ timeout: POSTCONDITION_TIMEOUT_MS,
220
+ maxBuffer: 8 * 1024 * 1024,
221
+ env: { ...process.env, CLICOLOR: '0', NO_COLOR: '1', FORCE_COLOR: '0' },
222
+ });
223
+ if (result.status === 0)
224
+ return { ok: true };
225
+ if (result.error) {
226
+ const err = result.error;
227
+ if (err.code === 'ETIMEDOUT') {
228
+ return { ok: false, error: `postcondition not met: timed out after ${POSTCONDITION_TIMEOUT_MS / 1000}s` };
229
+ }
230
+ return { ok: false, error: `postcondition not met: ${err.message}` };
231
+ }
232
+ const detail = (result.stderr || result.stdout || '').trim().replace(/\s+/g, ' ').slice(0, 200);
233
+ const exit = result.status ?? 'unknown';
234
+ return { ok: false, error: `postcondition not met${detail ? `: ${detail}` : ` (exit ${exit})`}` };
235
+ }
236
+ /** Persist the postcondition result onto the existing fire record. Frozen `ok` is left as the fire-time snapshot. */
237
+ function persistPostcondition(fire, result) {
238
+ writeFireRecord(fire, {
239
+ postconditionOk: result.ok,
240
+ ...(result.error ? { postconditionError: result.error } : {}),
241
+ });
242
+ }
204
243
  /**
205
244
  * Reconcile a fire's frozen `ok` against its dispatched run's REAL, current
206
- * status — the render-time fix for RUSH-2690.
245
+ * status — the render-time fix for RUSH-2690 — and, when the run has settled
246
+ * `completed`, against a declared postcondition (PHNX-2842).
207
247
  *
208
248
  * `writeFireRecord` (this module) persists `ok` once, at fire time, from
209
249
  * `dispatchAction`'s synchronous return. For a `run`/`routine` action that
@@ -219,6 +259,14 @@ const OK_RUN_STATUSES = new Set(['running', 'completed']);
219
259
  * A fire with no `runId` (a `notify`/`webhook-out` action, or a `run`/`routine`
220
260
  * dispatch that never got a runId at all) has nothing to reconcile against —
221
261
  * its frozen `ok` is the only signal and is returned as-is.
262
+ *
263
+ * `completed` is not success by itself. An agent that exits 0 without doing
264
+ * the job (merge-on-green that never merged) used to record `ok` because
265
+ * `OK_RUN_STATUSES` treated `completed` as healthy. When the fire carries a
266
+ * `postcondition` command, this function runs it once the run has settled and
267
+ * returns `ok: false, effect: 'none'` when it fails — distinguishing "ran but
268
+ * no effect" from a working fire. The result is persisted on the fire record
269
+ * so later listings do not re-exec the command.
222
270
  */
223
271
  export function resolveFireOutcome(jobName, fire) {
224
272
  if (!fire.runId)
@@ -226,5 +274,32 @@ export function resolveFireOutcome(jobName, fire) {
226
274
  const run = readRunMeta(jobName, fire.runId);
227
275
  if (!run)
228
276
  return { ok: fire.ok !== false };
229
- return { ok: OK_RUN_STATUSES.has(run.status), runStatus: run.status };
277
+ if (run.status === 'running')
278
+ return { ok: true, runStatus: run.status };
279
+ if (run.status !== 'completed')
280
+ return { ok: false, runStatus: run.status };
281
+ if (!fire.postcondition)
282
+ return { ok: true, runStatus: 'completed' };
283
+ if (fire.postconditionOk === true) {
284
+ return { ok: true, runStatus: 'completed', effect: 'met' };
285
+ }
286
+ if (fire.postconditionOk === false) {
287
+ return {
288
+ ok: false,
289
+ runStatus: 'completed',
290
+ effect: 'none',
291
+ ...(fire.postconditionError ? { error: fire.postconditionError } : {}),
292
+ };
293
+ }
294
+ const result = evaluatePostcondition(fire.postcondition);
295
+ persistPostcondition(fire, result);
296
+ fire.postconditionOk = result.ok;
297
+ if (result.error)
298
+ fire.postconditionError = result.error;
299
+ return {
300
+ ok: result.ok,
301
+ runStatus: 'completed',
302
+ effect: result.ok ? 'met' : 'none',
303
+ ...(result.error ? { error: result.error } : {}),
304
+ };
230
305
  }
@@ -7,6 +7,8 @@ export declare const KIRO_CAPABILITY_BY_TOOL: Record<string, string | undefined>
7
7
  export declare const CANONICAL_TO_OPENCLAW_TOOL: Record<string, string>;
8
8
  /** Canonical tool -> Antigravity's action namespace. */
9
9
  export declare const ANTIGRAVITY_ACTION_BY_TOOL: Record<string, string | undefined>;
10
+ /** Filename Codex stores generated deny rules under `.codex/rules/`. */
11
+ export declare const CODEX_RULES_FILENAME = "agents-deny.rules";
10
12
  /**
11
13
  * Strip JSON comments for JSONC parsing, only OUTSIDE string literals.
12
14
  *
@@ -97,6 +97,8 @@ const CANONICAL_BY_GROK_TOOL = invertFirstWins(GROK_TOOL_BY_CANONICAL);
97
97
  const CANONICAL_BY_KIRO_CAPABILITY = invertFirstWins(KIRO_CAPABILITY_BY_TOOL);
98
98
  const CANONICAL_BY_OPENCLAW_TOOL = invertFirstWins(CANONICAL_TO_OPENCLAW_TOOL);
99
99
  const CANONICAL_BY_ANTIGRAVITY_ACTION = invertFirstWins(ANTIGRAVITY_ACTION_BY_TOOL);
100
+ /** Filename Codex stores generated deny rules under `.codex/rules/`. */
101
+ export const CODEX_RULES_FILENAME = 'agents-deny.rules';
100
102
  /** Canonical TitleCase spelling for a lowercased canonical tool name. */
101
103
  const CANONICAL_TOOL_CASE = {
102
104
  bash: 'Bash',
@@ -248,6 +250,106 @@ function existingOr(preferred, alternate) {
248
250
  function stringList(value) {
249
251
  return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
250
252
  }
253
+ /**
254
+ * Invert `convertDenyToCodexRules` (permissions.ts). The writer emits
255
+ * `prefix_rule(pattern=["git", "reset"], decision="forbidden")` from a
256
+ * canonical `Bash(git reset:*)`. Prefix-match is the native form of `:*`,
257
+ * so the reverse always reconstructs `Bash(<parts>:*)`.
258
+ *
259
+ * Only `decision = "forbidden"` is a deny; anything else is skipped rather
260
+ * than guessed into an allow (Codex grants come from sandbox_mode).
261
+ */
262
+ function convertCodexRulesToDeny(content) {
263
+ const deny = [];
264
+ const marker = 'prefix_rule';
265
+ let searchFrom = 0;
266
+ while (searchFrom < content.length) {
267
+ const start = content.indexOf(marker, searchFrom);
268
+ if (start < 0)
269
+ break;
270
+ const open = content.indexOf('(', start + marker.length);
271
+ if (open < 0)
272
+ break;
273
+ const bodyEnd = findMatchingCloser(content, open, '(', ')');
274
+ if (bodyEnd < 0) {
275
+ searchFrom = open + 1;
276
+ continue;
277
+ }
278
+ const body = content.slice(open + 1, bodyEnd);
279
+ searchFrom = bodyEnd + 1;
280
+ const decision = body.match(/decision\s*=\s*"((?:\\.|[^"\\])*)"/);
281
+ if (!decision || decision[1] !== 'forbidden')
282
+ continue;
283
+ const patternKw = body.search(/pattern\s*=/);
284
+ if (patternKw < 0)
285
+ continue;
286
+ const arrayStart = body.indexOf('[', patternKw);
287
+ if (arrayStart < 0)
288
+ continue;
289
+ const arrayEnd = findMatchingCloser(body, arrayStart, '[', ']');
290
+ if (arrayEnd < 0)
291
+ continue;
292
+ let parsed;
293
+ try {
294
+ parsed = JSON.parse(body.slice(arrayStart, arrayEnd + 1));
295
+ }
296
+ catch {
297
+ continue;
298
+ }
299
+ if (!Array.isArray(parsed))
300
+ continue;
301
+ const parts = parsed.filter((p) => typeof p === 'string' && p.length > 0);
302
+ if (parts.length === 0)
303
+ continue;
304
+ deny.push(`Bash(${parts.join(' ')}:*)`);
305
+ }
306
+ return deny;
307
+ }
308
+ /** Index of the closer matching `source[openIdx]`, skipping quoted strings. */
309
+ function findMatchingCloser(source, openIdx, open, close) {
310
+ let depth = 0;
311
+ let inString = false;
312
+ let escape = false;
313
+ for (let i = openIdx; i < source.length; i++) {
314
+ const c = source[i];
315
+ if (inString) {
316
+ if (escape) {
317
+ escape = false;
318
+ continue;
319
+ }
320
+ if (c === '\\') {
321
+ escape = true;
322
+ continue;
323
+ }
324
+ if (c === '"')
325
+ inString = false;
326
+ continue;
327
+ }
328
+ if (c === '"') {
329
+ inString = true;
330
+ continue;
331
+ }
332
+ if (c === open)
333
+ depth++;
334
+ else if (c === close) {
335
+ depth--;
336
+ if (depth === 0)
337
+ return i;
338
+ }
339
+ }
340
+ return -1;
341
+ }
342
+ function readCodexDenyRules(configPath) {
343
+ const rulesPath = path.join(path.dirname(configPath), 'rules', CODEX_RULES_FILENAME);
344
+ if (!fs.existsSync(rulesPath))
345
+ return [];
346
+ try {
347
+ return convertCodexRulesToDeny(fs.readFileSync(rulesPath, 'utf-8'));
348
+ }
349
+ catch {
350
+ return [];
351
+ }
352
+ }
251
353
  /**
252
354
  * Single source of truth for where each allowlist-capable agent stores
253
355
  * permissions and how to read them back. Keys MUST equal
@@ -309,24 +411,24 @@ export const PERMISSION_TARGETS = {
309
411
  codex: {
310
412
  home: (h) => path.join(h, '.codex', 'config.toml'),
311
413
  project: (cwd) => path.join(cwd, '.codex', 'config.toml'),
312
- lossyBecause: 'Codex has no rule list — its sandbox mode is widened into representative blanket grants',
414
+ lossyBecause: 'sandbox mode widens into representative blanket grants; deny round-trips from agents-deny.rules as Bash(prefix:*)',
313
415
  toCanonical(configPath) {
314
416
  const config = readToml(configPath);
315
- if (!config)
316
- return null;
317
417
  const allow = [];
318
- const sandboxMode = config.sandbox_mode;
319
- if (config.approval_policy === 'never' || sandboxMode === 'danger-full-access') {
320
- allow.push('Bash(*)', 'Read(**)', 'Write(**)', 'Edit(**)');
321
- }
322
- else if (sandboxMode === 'workspace-write') {
323
- allow.push('Bash(*)', 'Read(**)');
324
- }
325
- const sw = config.sandbox_workspace_write;
326
- if (sw && typeof sw === 'object' && !Array.isArray(sw) && sw.network_access) {
327
- allow.push('WebSearch(*)', 'WebFetch(*)');
418
+ if (config) {
419
+ const sandboxMode = config.sandbox_mode;
420
+ if (config.approval_policy === 'never' || sandboxMode === 'danger-full-access') {
421
+ allow.push('Bash(*)', 'Read(**)', 'Write(**)', 'Edit(**)');
422
+ }
423
+ else if (sandboxMode === 'workspace-write') {
424
+ allow.push('Bash(*)', 'Read(**)');
425
+ }
426
+ const sw = config.sandbox_workspace_write;
427
+ if (sw && typeof sw === 'object' && !Array.isArray(sw) && sw.network_access) {
428
+ allow.push('WebSearch(*)', 'WebFetch(*)');
429
+ }
328
430
  }
329
- return permissionSet(allow, []);
431
+ return permissionSet(allow, readCodexDenyRules(configPath));
330
432
  },
331
433
  },
332
434
  cursor: {
@@ -1,6 +1,6 @@
1
1
  import type { AgentId, PermissionSet, InstalledPermission, ClaudePermissions, CursorPermissions, OpenCodePermissions, CodexPermissions } from './types.js';
2
- /** Filename used for Codex Starlark deny-rules generated from permission groups. */
3
- export declare const CODEX_RULES_FILENAME = "agents-deny.rules";
2
+ import { CODEX_RULES_FILENAME } from './permissions-registry.js';
3
+ export { CODEX_RULES_FILENAME };
4
4
  export type ParsedRules = PermissionSet;
5
5
  export declare const COMPUTER_PERMISSION_RULE_PREFIX = "Computer";
6
6
  export declare const COMPUTER_APP_GATED_VERBS: readonly ["screenshot", "describe", "get-text", "launch", "raise", "click", "right-click", "type", "type-text", "key", "drag", "scroll", "ax-action", "focus", "wait"];
@@ -13,6 +13,9 @@ export declare function containsBroadGrants(rules: ParsedRules): {
13
13
  /**
14
14
  * Convert canonical deny rules to Codex Starlark .rules format.
15
15
  * E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
16
+ *
17
+ * Inverse: `toCanonical` for `PERMISSION_TARGETS.codex` reads
18
+ * `.codex/rules/agents-deny.rules` back into `Bash(<parts>:*)`.
16
19
  */
17
20
  export declare function convertDenyToCodexRules(deny: string[]): string | null;
18
21
  /**
@@ -344,4 +347,3 @@ export declare function saveDefaultPermissionSet(set: PermissionSet): {
344
347
  success: boolean;
345
348
  error?: string;
346
349
  };
347
- export {};
@@ -20,16 +20,9 @@ import { updateGeminiSettings } from './gemini-settings.js';
20
20
  // The canonical<->native tool vocabularies live in the registry, which also owns
21
21
  // the reverse projections — so the serializers below and the readers there can
22
22
  // never disagree about what `fs_read` or `developer__shell` means.
23
- import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, stripJsonComments, GROK_TOOL_BY_CANONICAL, KIRO_CAPABILITY_BY_TOOL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
23
+ import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL, KIRO_CAPABILITY_BY_TOOL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
24
+ export { CODEX_RULES_FILENAME };
24
25
  const HOME = os.homedir();
25
- // PERMISSIONS_CAPABLE_AGENTS removed — use `capableAgents('allowlist')`
26
- // from lib/capabilities.ts. The capability matrix on AgentConfig is the
27
- // single source of truth. (Per-agent native format details:
28
- // antigravity → ~/.gemini/antigravity-cli/settings.json `permissions.{allow,deny}`
29
- // grok → ~/.grok/config.toml `[permission].rules`
30
- // the writer in `applyPermissionsToVersion` handles the format dispatch.)
31
- /** Filename used for Codex Starlark deny-rules generated from permission groups. */
32
- export const CODEX_RULES_FILENAME = 'agents-deny.rules';
33
26
  export const COMPUTER_PERMISSION_RULE_PREFIX = 'Computer';
34
27
  export const COMPUTER_APP_GATED_VERBS = [
35
28
  'screenshot',
@@ -103,6 +96,9 @@ export function containsBroadGrants(rules) {
103
96
  /**
104
97
  * Convert canonical deny rules to Codex Starlark .rules format.
105
98
  * E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
99
+ *
100
+ * Inverse: `toCanonical` for `PERMISSION_TARGETS.codex` reads
101
+ * `.codex/rules/agents-deny.rules` back into `Bash(<parts>:*)`.
106
102
  */
107
103
  export function convertDenyToCodexRules(deny) {
108
104
  const rules = [];
@@ -122,6 +118,24 @@ export function convertDenyToCodexRules(deny) {
122
118
  return null;
123
119
  return `# Auto-generated by agents-cli from deny permission groups.\n# Do not edit manually — re-run "agents use" to regenerate.\n\n${rules.join('\n\n')}\n`;
124
120
  }
121
+ /**
122
+ * Write `.codex/rules/agents-deny.rules` to match `deny`, or delete it when
123
+ * the current set has nothing to forbid. The reader (PHNX-2703) now surfaces
124
+ * this file, so a later apply with an empty deny must not leave a stale
125
+ * forbid behind — `agents permissions list codex` would keep reporting it.
126
+ */
127
+ function syncCodexDenyRules(configDir, deny) {
128
+ const rulesPath = path.join(configDir, 'rules', CODEX_RULES_FILENAME);
129
+ const rulesContent = deny && deny.length > 0 ? convertDenyToCodexRules(deny) : null;
130
+ if (rulesContent) {
131
+ fs.mkdirSync(path.dirname(rulesPath), { recursive: true });
132
+ fs.writeFileSync(rulesPath, rulesContent, 'utf-8');
133
+ return;
134
+ }
135
+ if (fs.existsSync(rulesPath)) {
136
+ fs.unlinkSync(rulesPath);
137
+ }
138
+ }
125
139
  /**
126
140
  * Ensure central permissions directory exists.
127
141
  */
@@ -1274,15 +1288,7 @@ function applyCodexPermissions(set, scope = 'user', cwd, merge = true) {
1274
1288
  : newPermissions.sandbox_workspace_write;
1275
1289
  }
1276
1290
  fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
1277
- // Write .rules file for deny permissions
1278
- if (set.deny && set.deny.length > 0) {
1279
- const rulesContent = convertDenyToCodexRules(set.deny);
1280
- if (rulesContent) {
1281
- const rulesDir = path.join(configDir, 'rules');
1282
- fs.mkdirSync(rulesDir, { recursive: true });
1283
- fs.writeFileSync(path.join(rulesDir, CODEX_RULES_FILENAME), rulesContent, 'utf-8');
1284
- }
1285
- }
1291
+ syncCodexDenyRules(configDir, set.deny);
1286
1292
  return { success: true };
1287
1293
  }
1288
1294
  catch (err) {
@@ -1393,15 +1399,7 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1393
1399
  : newPermissions.sandbox_workspace_write;
1394
1400
  }
1395
1401
  fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
1396
- // Write .rules file for deny permissions
1397
- if (set.deny && set.deny.length > 0) {
1398
- const rulesContent = convertDenyToCodexRules(set.deny);
1399
- if (rulesContent) {
1400
- const rulesDir = path.join(configDir, 'rules');
1401
- fs.mkdirSync(rulesDir, { recursive: true });
1402
- fs.writeFileSync(path.join(rulesDir, CODEX_RULES_FILENAME), rulesContent, 'utf-8');
1403
- }
1404
- }
1402
+ syncCodexDenyRules(configDir, set.deny);
1405
1403
  return { success: true };
1406
1404
  }
1407
1405
  if (agentId === 'antigravity') {
@@ -248,13 +248,14 @@ export interface ResolvedProfileRun {
248
248
  */
249
249
  tierNote?: string;
250
250
  /**
251
- * Set when `requestedModel` was a tier token AND this profile resolved it
252
- * against its own `models:` map. Callers that forward a `--model` value
253
- * downstream (e.g. as `ExecOptions.model`) should substitute this in place
254
- * of the original tier token — exec.ts's native tier block only knows how
255
- * to resolve a tier against the HOST agent's own catalog, which is the
256
- * wrong catalog for a profile's own harness identity. Undefined both when
257
- * no tier was requested and when tier resolution degraded (see `tierNote`).
251
+ * Concrete model id callers should forward as `ExecOptions.model`. Set when:
252
+ * - `requestedModel` was a cost-tier token resolved against this profile's
253
+ * `models:` map, OR
254
+ * - no `--model` was requested and this is an OpenCode host whose pin lives
255
+ * in `OPENCODE_MODEL` (OpenCode does not read that env var, so the pin
256
+ * has to become `--model` on the argv).
257
+ * Undefined when the caller already passed a concrete `--model`, or when
258
+ * tier resolution degraded (see `tierNote`).
258
259
  */
259
260
  resolvedModel?: string;
260
261
  }
@@ -164,6 +164,7 @@ const MODEL_ENV_KEYS = [
164
164
  'OPENAI_MODEL',
165
165
  'GEMINI_MODEL',
166
166
  'GROK_MODEL',
167
+ 'OPENCODE_MODEL',
167
168
  ];
168
169
  /** Return the configured model env value for display. */
169
170
  export function profileModelLabel(profile) {
@@ -583,6 +584,17 @@ export function resolveProfileForRun(name, requestedModel) {
583
584
  // the raw tier token downstream and handles the message -- this function
584
585
  // doesn't compete with that canonical fallback for the no-opt-in case.
585
586
  }
587
+ // OpenCode does not honor OPENCODE_MODEL (unlike claude/codex/gemini/grok,
588
+ // whose host CLIs read their MODEL env var). Copy the pin into resolvedModel
589
+ // so callers set ExecOptions.model and buildExecCommand emits `--model`.
590
+ // An explicit `--model` (including a cost-tier token the caller may still
591
+ // discard) wins and is left alone.
592
+ if (resolved.resolvedModel === undefined && !requestedModel && profile.host.agent === 'opencode') {
593
+ const envKey = profileModelEnvKey(profile) ?? modelEnvKeyForHost('opencode');
594
+ const pinned = env[envKey];
595
+ if (pinned)
596
+ resolved.resolvedModel = pinned;
597
+ }
586
598
  return resolved;
587
599
  }
588
600
  /**
@@ -14,6 +14,15 @@
14
14
  * for local ones; {@link resolveProjectKey} adds the filesystem repo-root walk
15
15
  * for paths this machine can see.
16
16
  */
17
+ /**
18
+ * Convert an absolute cwd to Claude Code's own project-folder name under
19
+ * `~/.claude/projects/` (slashes and dots become dashes) — the encoding
20
+ * Claude Code itself performs to name each project's session-transcript and
21
+ * native-memory directory. Pure, no filesystem: session discovery and
22
+ * native-memory sync both derive from this so they agree on one directory
23
+ * for a given cwd.
24
+ */
25
+ export declare function claudeProjectDirName(cwd: string): string;
17
26
  /**
18
27
  * Resolve a stable project key from a working directory, or `undefined` when
19
28
  * the path carries nothing usable (empty, `/`, whitespace).
@@ -18,6 +18,17 @@ import * as fs from 'fs';
18
18
  import * as os from 'os';
19
19
  import * as path from 'path';
20
20
  const WORKTREE_SEGMENT = '/.agents/worktrees/';
21
+ /**
22
+ * Convert an absolute cwd to Claude Code's own project-folder name under
23
+ * `~/.claude/projects/` (slashes and dots become dashes) — the encoding
24
+ * Claude Code itself performs to name each project's session-transcript and
25
+ * native-memory directory. Pure, no filesystem: session discovery and
26
+ * native-memory sync both derive from this so they agree on one directory
27
+ * for a given cwd.
28
+ */
29
+ export function claudeProjectDirName(cwd) {
30
+ return cwd.replace(/[/.]/g, '-');
31
+ }
21
32
  /**
22
33
  * Resolve a stable project key from a working directory, or `undefined` when
23
34
  * the path carries nothing usable (empty, `/`, whitespace).
@@ -16,6 +16,53 @@ export declare function nextRunLabel(job: JobConfig, scheduler: JobScheduler, no
16
16
  export declare function localLatestRun(job: JobConfig): RunMeta | null;
17
17
  export declare function listJobsForDisplay(cwd?: string): JobConfig[];
18
18
  export declare function buildRoutineListJson(): Record<string, unknown>[];
19
+ /** One routine's live scheduler-status row for `agents routines status --json`. */
20
+ export interface RoutineStatusRow {
21
+ name: string;
22
+ /** The single device this routine is pinned to fire on, or null when unpinned. */
23
+ ownerDevice: string | null;
24
+ /** True when `devices:` names more than one device — no single owner (doctor flags it). */
25
+ ambiguousDevicePin: boolean;
26
+ /** Every device this routine is enabled on. */
27
+ enabledDevices: string[];
28
+ /** Whether THIS device is the one that fires the routine. */
29
+ runsHere: boolean;
30
+ enabled: boolean;
31
+ overdue: boolean;
32
+ nextRun: string | null;
33
+ /** Terminal status of the last local fire: completed/failed/timeout/missed/blocked/skipped/running, or null if it never ran here. */
34
+ lastStatus: RunMeta['status'] | null;
35
+ /** The last fire's failure reason, when it did not complete cleanly. Named to match `list --json`'s `failureReason`. */
36
+ failureReason: string | null;
37
+ lastRunStartedAt: string | null;
38
+ lastRunCompletedAt: string | null;
39
+ /** Present only while a run is genuinely in flight on THIS device (a live local child or a host-placed run) — never a provisional pre-spawn claim. */
40
+ inFlight: {
41
+ runId: string;
42
+ pid: number | null;
43
+ startedAt: string;
44
+ triggerKind: RunMeta['triggerKind'] | null;
45
+ } | null;
46
+ }
47
+ /**
48
+ * The per-routine rows behind `agents routines status --json`. Distinct from
49
+ * {@link buildRoutineListJson}: this is the scheduler-truth surface the daemon
50
+ * owns — per routine it names the single owner device, the last fire's outcome
51
+ * and error, and any in-flight spawn — the fields an operator (or the menu bar /
52
+ * ext) needs to answer "did this routine fire, and is one running right now?"
53
+ * that the definition-shaped `list --json` does not carry (PHNX-3215).
54
+ *
55
+ * `monitorRunningJobs()` runs first to reap runs whose process has exited, then
56
+ * `inFlight` is gated on {@link isRunGenuinelyInFlight} — NOT on `status ===
57
+ * 'running'` alone, because a provisional pre-spawn claim is `running` with a
58
+ * null pid that the reaper does not touch (RUSH-2640).
59
+ *
60
+ * The routine set is the schedulable one ({@link listJobs}, the same
61
+ * `getDaemonStatus`/`routines status` counts), not the display set
62
+ * {@link buildRoutineListJson} uses — a scheduler-status surface names what the
63
+ * daemon can actually fire, not discoverable-but-unmaterialised project routines.
64
+ */
65
+ export declare function buildRoutineStatusRows(): RoutineStatusRow[];
19
66
  /** Tool/site/directory allow-list for sandboxed job execution. */
20
67
  export interface JobAllowConfig {
21
68
  tools?: string[];
@@ -26,7 +26,7 @@ import { enabledRoutineNames, devicesWithRoutineEnabled, replaceEnabledRoutines,
26
26
  import { humanizeCron, humanizeNextRun } from '../routines-format.js';
27
27
  import { discoverProjectRoutines } from '../routines-project.js';
28
28
  import { listProjectDefs } from '../projects.js';
29
- import { monitorRunningJobs } from '../daemon/runner.js';
29
+ import { monitorRunningJobs, isRunGenuinelyInFlight } from '../daemon/runner.js';
30
30
  import { JobScheduler } from '../scheduler.js';
31
31
  import { detectOverdueJobs } from '../overdue.js';
32
32
  export function fireConditionLabel(job) {
@@ -142,6 +142,75 @@ export function buildRoutineListJson() {
142
142
  scheduler.stopAll();
143
143
  }
144
144
  }
145
+ /**
146
+ * The per-routine rows behind `agents routines status --json`. Distinct from
147
+ * {@link buildRoutineListJson}: this is the scheduler-truth surface the daemon
148
+ * owns — per routine it names the single owner device, the last fire's outcome
149
+ * and error, and any in-flight spawn — the fields an operator (or the menu bar /
150
+ * ext) needs to answer "did this routine fire, and is one running right now?"
151
+ * that the definition-shaped `list --json` does not carry (PHNX-3215).
152
+ *
153
+ * `monitorRunningJobs()` runs first to reap runs whose process has exited, then
154
+ * `inFlight` is gated on {@link isRunGenuinelyInFlight} — NOT on `status ===
155
+ * 'running'` alone, because a provisional pre-spawn claim is `running` with a
156
+ * null pid that the reaper does not touch (RUSH-2640).
157
+ *
158
+ * The routine set is the schedulable one ({@link listJobs}, the same
159
+ * `getDaemonStatus`/`routines status` counts), not the display set
160
+ * {@link buildRoutineListJson} uses — a scheduler-status surface names what the
161
+ * daemon can actually fire, not discoverable-but-unmaterialised project routines.
162
+ */
163
+ export function buildRoutineStatusRows() {
164
+ try {
165
+ monitorRunningJobs();
166
+ }
167
+ catch { /* best-effort orphan reap */ }
168
+ const jobs = listJobs();
169
+ if (jobs.length === 0)
170
+ return [];
171
+ const scheduler = new JobScheduler(async () => { });
172
+ scheduler.loadAll();
173
+ try {
174
+ const overdueSet = new Set();
175
+ try {
176
+ for (const job of detectOverdueJobs())
177
+ overdueSet.add(job.name);
178
+ }
179
+ catch {
180
+ // Best-effort indicator; never block status on detection errors.
181
+ }
182
+ const now = new Date();
183
+ return jobs.map((job) => {
184
+ const latestRun = localLatestRun(job);
185
+ const inFlight = latestRun && isRunGenuinelyInFlight(latestRun)
186
+ ? {
187
+ runId: latestRun.runId,
188
+ pid: latestRun.pid,
189
+ startedAt: latestRun.startedAt,
190
+ triggerKind: latestRun.triggerKind ?? null,
191
+ }
192
+ : null;
193
+ return {
194
+ name: job.name,
195
+ ownerDevice: routineOwnerDevice(job),
196
+ ambiguousDevicePin: hasAmbiguousDevicePin(job),
197
+ enabledDevices: devicesWithRoutineEnabled(job.name),
198
+ runsHere: jobRunsOnThisDevice(job),
199
+ enabled: job.enabled,
200
+ overdue: overdueSet.has(job.name),
201
+ nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
202
+ lastStatus: latestRun?.status ?? null,
203
+ failureReason: latestRun?.errorMessage ?? null,
204
+ lastRunStartedAt: latestRun?.startedAt ?? null,
205
+ lastRunCompletedAt: latestRun?.completedAt ?? null,
206
+ inFlight,
207
+ };
208
+ });
209
+ }
210
+ finally {
211
+ scheduler.stopAll();
212
+ }
213
+ }
145
214
  export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
146
215
  /** Canonical set of accepted GitHub trigger events — single source for validation. */
147
216
  export const GITHUB_TRIGGER_EVENTS = [
@@ -101,6 +101,41 @@ export declare const ENV_KEY_PATTERN: RegExp;
101
101
  export declare const BUNDLE_KEY_PATTERN: RegExp;
102
102
  export declare const BUNDLE_META_PREFIX = "agents-cli.bundles.";
103
103
  export declare const RESERVED_ENV_NAMES: Set<string>;
104
+ /**
105
+ * The reserved FILE-BACKED bundle that holds long-lived Claude setup-tokens.
106
+ * Usage/probe reads authenticate with these instead of the ACL-bound login item,
107
+ * so they never pop Touch ID and they can cross the fleet. A keychain- or
108
+ * vault-backed bundle of this name is a misconfiguration: the consumer used to
109
+ * return null (SEC-GAP-3) and silently fall through to Touch ID.
110
+ */
111
+ export declare const AUTH_BUNDLE_NAME = "auth";
112
+ export declare const AUTH_BUNDLE_BACKEND: SecretsBackend;
113
+ export declare const RESERVED_BUNDLE_NAMES: Set<string>;
114
+ export declare function isReservedBundleName(name: string): boolean;
115
+ /** Thrown when a reserved bundle is written or resolved on the wrong backend. */
116
+ export declare class ReservedBundleWrongBackendError extends Error {
117
+ readonly bundle: string;
118
+ readonly backend: SecretsBackend;
119
+ constructor(bundle: string, backend: SecretsBackend);
120
+ }
121
+ /** Fail loud when `name` is reserved and `backend` is not the required one. */
122
+ export declare function assertReservedBundleBackend(name: string, backend: SecretsBackend): void;
123
+ /**
124
+ * Presence + backend of the reserved `auth` bundle. `ok` is true when the
125
+ * bundle is absent (nothing to fix) or present and file-backed.
126
+ */
127
+ export declare function inspectReservedAuthBundle(): {
128
+ exists: boolean;
129
+ backend: SecretsBackend | null;
130
+ ok: boolean;
131
+ };
132
+ /**
133
+ * After a file-backed import, actually decrypt the keys and fail if any are
134
+ * unreadable. Import used to print "Imported N key(s)" from the write tally
135
+ * alone — ciphertext sealed under a forwarded AGENTS_SECRETS_PASSPHRASE that
136
+ * the destination daemon does not hold still counted as success.
137
+ */
138
+ export declare function assertFileBundleDecryptable(name: string, keys: string[]): void;
104
139
  export declare function bundleToEnvPrefix(name: string): string;
105
140
  export declare function isReservedEnvName(key: string): boolean;
106
141
  export declare function bundleKeyToEnvKey(key: string): string;