@phnx-labs/agents-cli 1.20.72 → 1.20.73

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 (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -104,6 +104,22 @@ export declare function convertToClaudeFormat(set: PermissionSet): ClaudePermiss
104
104
  * (`~/.cursor/cli-config.json` permissions.allow/deny).
105
105
  */
106
106
  export declare function convertToCursorFormat(set: PermissionSet): CursorPermissions;
107
+ type CopilotToolApproval = {
108
+ kind: 'commands';
109
+ commandIdentifiers: string[];
110
+ } | {
111
+ kind: 'read' | 'write';
112
+ } | {
113
+ kind: 'mcp';
114
+ serverName: string;
115
+ toolName: string | null;
116
+ };
117
+ export interface CopilotPermissionsConfig {
118
+ locations: Record<string, {
119
+ tool_approvals?: CopilotToolApproval[];
120
+ allowed_directories?: string[];
121
+ }>;
122
+ }
107
123
  /**
108
124
  * Convert canonical permission set to Gemini format.
109
125
  * Gemini reads tool allow/deny lists from settings.json under
@@ -117,6 +133,7 @@ export declare function convertToGeminiFormat(set: PermissionSet): {
117
133
  exclude?: string[];
118
134
  };
119
135
  };
136
+ export declare function convertToCopilotFormat(set: PermissionSet, location: string): CopilotPermissionsConfig;
120
137
  /** Convert canonical Bash rules into Droid's command arrays. */
121
138
  export declare function convertToDroidFormat(set: PermissionSet): {
122
139
  commandAllowlist: string[];
@@ -137,6 +154,31 @@ export declare function convertToOpenClawFormat(set: PermissionSet): {
137
154
  alsoAllow: string[];
138
155
  deny: string[];
139
156
  };
157
+ export type ForgePermission = 'allow' | 'deny' | 'confirm';
158
+ export type ForgeOperation = 'read' | 'write' | 'command' | 'url';
159
+ export type ForgePolicy = {
160
+ permission: ForgePermission;
161
+ rule: Partial<Record<ForgeOperation, string>>;
162
+ };
163
+ /**
164
+ * Convert canonical permissions to ForgeCode's permissions.yaml policy list.
165
+ *
166
+ * ForgeCode gates built-in tools by operation family (`read`, `write`,
167
+ * `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
168
+ * file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
169
+ * bypass permissions.yaml entirely; agents-cli therefore maps only canonical
170
+ * built-in allow/deny rules and does not try to express per-named-MCP-tool
171
+ * grants.
172
+ */
173
+ export declare function convertToForgeFormat(set: PermissionSet): {
174
+ policies: ForgePolicy[];
175
+ };
176
+ export declare function convertToHermesFormat(set: PermissionSet): {
177
+ command_allowlist: string[];
178
+ approvals: {
179
+ deny: string[];
180
+ };
181
+ };
140
182
  /**
141
183
  * Convert canonical permission set to Antigravity format.
142
184
  * Antigravity reads ~/.gemini/antigravity-cli/settings.json with
@@ -252,7 +294,7 @@ export declare function openCodeConfigPath(scope: 'user' | 'project', cwd?: stri
252
294
  * Apply a permission set to a specific version's home directory.
253
295
  * This writes to {versionHome}/.{agent}/settings.json (or equivalent).
254
296
  */
255
- export declare function applyPermissionsToVersion(agentId: AgentId, set: PermissionSet, versionHome: string, merge?: boolean): {
297
+ export declare function applyPermissionsToVersion(agentId: AgentId, set: PermissionSet, versionHome: string, merge?: boolean, cwd?: string): {
256
298
  success: boolean;
257
299
  error?: string;
258
300
  };
@@ -302,3 +344,4 @@ export declare function saveDefaultPermissionSet(set: PermissionSet): {
302
344
  success: boolean;
303
345
  error?: string;
304
346
  };
347
+ export {};
@@ -433,14 +433,27 @@ export function removePermissionSet(name) {
433
433
  // ============================================================================
434
434
  // Agent-specific converters
435
435
  // ============================================================================
436
+ /**
437
+ * Map a canonical rule to Claude settings.json syntax.
438
+ * Claude's file-permission checks only match Edit(path) rules — an Edit rule
439
+ * covers every file-editing tool (Write, Edit, NotebookEdit) — and Claude
440
+ * warns at startup about unmatched Write(path) rules. Canonical Write(path)
441
+ * stays for other agents' converters; for Claude it is emitted as Edit(path).
442
+ */
443
+ function canonicalToClaudeRule(perm) {
444
+ if (perm.startsWith('Write(')) {
445
+ return perm.replace(/^Write/, 'Edit');
446
+ }
447
+ return perm;
448
+ }
436
449
  /**
437
450
  * Convert canonical permission set to Claude format.
438
451
  * Claude uses: { permissions: { allow: ["Bash(*)", "Read(**)"], deny: [] } }
439
452
  */
440
453
  export function convertToClaudeFormat(set) {
441
454
  const permissions = {
442
- allow: [...set.allow],
443
- deny: set.deny ? [...set.deny] : [],
455
+ allow: [...new Set(set.allow.map(canonicalToClaudeRule))],
456
+ deny: set.deny ? [...new Set(set.deny.map(canonicalToClaudeRule))] : [],
444
457
  };
445
458
  if (set.additionalDirectories?.length) {
446
459
  permissions.additionalDirectories = [...set.additionalDirectories];
@@ -540,6 +553,89 @@ function normalizeBashPattern(pattern) {
540
553
  return pattern.slice(0, -2) + ' *';
541
554
  return pattern;
542
555
  }
556
+ function canonicalBashToCopilotCommandIdentifier(pattern) {
557
+ if (pattern === '*' || pattern === '**')
558
+ return null;
559
+ if (pattern.endsWith(':*'))
560
+ return pattern;
561
+ if (pattern.endsWith(' *'))
562
+ return `${pattern.slice(0, -2)}:*`;
563
+ return pattern;
564
+ }
565
+ function canonicalToCopilotMcpApproval(parsed) {
566
+ const parts = parsed.pattern.split(/[.:/]/).filter(Boolean);
567
+ if (parts.length === 0)
568
+ return null;
569
+ const [serverName, toolName] = parts;
570
+ return { kind: 'mcp', serverName, toolName: toolName ?? null };
571
+ }
572
+ function canonicalToCopilotApproval(permission) {
573
+ const parsed = parseCanonicalPattern(permission);
574
+ if (!parsed)
575
+ return null;
576
+ if (parsed.tool === 'bash') {
577
+ const identifier = canonicalBashToCopilotCommandIdentifier(parsed.pattern);
578
+ return identifier ? { kind: 'commands', commandIdentifiers: [identifier] } : null;
579
+ }
580
+ if ((parsed.tool === 'write' || parsed.tool === 'edit') && (parsed.pattern === '*' || parsed.pattern === '**')) {
581
+ return { kind: 'write' };
582
+ }
583
+ if (parsed.tool === 'read' && (parsed.pattern === '*' || parsed.pattern === '**')) {
584
+ return { kind: 'read' };
585
+ }
586
+ if (parsed.tool === 'mcp') {
587
+ return canonicalToCopilotMcpApproval(parsed);
588
+ }
589
+ return null;
590
+ }
591
+ function mergeCopilotApprovals(existing, incoming) {
592
+ const byKey = new Map();
593
+ const add = (approval) => {
594
+ if (approval.kind === 'commands') {
595
+ const current = byKey.get('commands');
596
+ const commandIdentifiers = new Set([...(current?.commandIdentifiers ?? []), ...approval.commandIdentifiers]);
597
+ byKey.set('commands', { kind: 'commands', commandIdentifiers: Array.from(commandIdentifiers).sort() });
598
+ return;
599
+ }
600
+ byKey.set(JSON.stringify(approval), approval);
601
+ };
602
+ for (const approval of existing) {
603
+ if (!approval || typeof approval !== 'object' || Array.isArray(approval))
604
+ continue;
605
+ const record = approval;
606
+ if (record.kind === 'commands' && Array.isArray(record.commandIdentifiers)) {
607
+ add({ kind: 'commands', commandIdentifiers: record.commandIdentifiers.filter((v) => typeof v === 'string') });
608
+ }
609
+ else if (record.kind === 'read' || record.kind === 'write') {
610
+ add({ kind: record.kind });
611
+ }
612
+ else if (record.kind === 'mcp' && typeof record.serverName === 'string' && (typeof record.toolName === 'string' || record.toolName === null)) {
613
+ add({ kind: 'mcp', serverName: record.serverName, toolName: record.toolName });
614
+ }
615
+ }
616
+ for (const approval of incoming)
617
+ add(approval);
618
+ return Array.from(byKey.values());
619
+ }
620
+ export function convertToCopilotFormat(set, location) {
621
+ const approvals = [];
622
+ for (const permission of set.allow) {
623
+ const approval = canonicalToCopilotApproval(permission);
624
+ if (approval)
625
+ approvals.push(approval);
626
+ }
627
+ const allowedDirectories = (set.additionalDirectories ?? [])
628
+ .filter((dir) => dir.trim().length > 0)
629
+ .map((dir) => path.isAbsolute(dir) ? dir : path.resolve(location, dir));
630
+ return {
631
+ locations: {
632
+ [location]: {
633
+ ...(approvals.length > 0 ? { tool_approvals: mergeCopilotApprovals([], approvals) } : {}),
634
+ ...(allowedDirectories.length > 0 ? { allowed_directories: Array.from(new Set(allowedDirectories)).sort() } : {}),
635
+ },
636
+ },
637
+ };
638
+ }
543
639
  /** Convert canonical Bash rules into Droid's command arrays. */
544
640
  export function convertToDroidFormat(set) {
545
641
  const commands = (permissions) => {
@@ -614,6 +710,83 @@ export function convertToOpenClawFormat(set) {
614
710
  deny: map(set.deny ?? []),
615
711
  };
616
712
  }
713
+ const FORGE_OPERATION_BY_CANONICAL = {
714
+ bash: 'command',
715
+ read: 'read',
716
+ grep: 'read',
717
+ glob: 'read',
718
+ write: 'write',
719
+ edit: 'write',
720
+ notebookedit: 'write',
721
+ webfetch: 'url',
722
+ websearch: 'url',
723
+ };
724
+ function canonicalToForgePolicy(perm, permission) {
725
+ if (BLANKET_BASH_FORMS.has(perm)) {
726
+ return { permission, rule: { command: '*' } };
727
+ }
728
+ const parsed = parseCanonicalPattern(perm);
729
+ if (!parsed)
730
+ return null;
731
+ const operation = FORGE_OPERATION_BY_CANONICAL[parsed.tool];
732
+ if (!operation)
733
+ return null;
734
+ const pattern = parsed.tool === 'bash'
735
+ ? normalizeBashPattern(parsed.pattern)
736
+ : (parsed.tool === 'webfetch' || parsed.tool === 'websearch') && parsed.pattern.startsWith('domain:')
737
+ ? `${parsed.pattern.slice('domain:'.length)}*`
738
+ : parsed.pattern === '**'
739
+ ? '**/*'
740
+ : parsed.pattern;
741
+ return { permission, rule: { [operation]: pattern } };
742
+ }
743
+ /**
744
+ * Convert canonical permissions to ForgeCode's permissions.yaml policy list.
745
+ *
746
+ * ForgeCode gates built-in tools by operation family (`read`, `write`,
747
+ * `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
748
+ * file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
749
+ * bypass permissions.yaml entirely; agents-cli therefore maps only canonical
750
+ * built-in allow/deny rules and does not try to express per-named-MCP-tool
751
+ * grants.
752
+ */
753
+ export function convertToForgeFormat(set) {
754
+ const policies = [];
755
+ const seen = new Set();
756
+ const add = (policy) => {
757
+ if (!policy)
758
+ return;
759
+ const key = `${policy.permission}|${JSON.stringify(policy.rule)}`;
760
+ if (seen.has(key))
761
+ return;
762
+ seen.add(key);
763
+ policies.push(policy);
764
+ };
765
+ for (const perm of set.allow)
766
+ add(canonicalToForgePolicy(perm, 'allow'));
767
+ for (const perm of set.deny ?? [])
768
+ add(canonicalToForgePolicy(perm, 'deny'));
769
+ return { policies };
770
+ }
771
+ export function convertToHermesFormat(set) {
772
+ const commands = (permissions) => {
773
+ const out = new Set();
774
+ for (const perm of permissions) {
775
+ if (BLANKET_BASH_FORMS.has(perm)) {
776
+ out.add('*');
777
+ continue;
778
+ }
779
+ const parsed = parseCanonicalPattern(perm);
780
+ if (parsed?.tool === 'bash')
781
+ out.add(normalizeBashPattern(parsed.pattern));
782
+ }
783
+ return Array.from(out).sort();
784
+ };
785
+ return {
786
+ command_allowlist: commands(set.allow),
787
+ approvals: { deny: commands(set.deny ?? []) },
788
+ };
789
+ }
617
790
  /**
618
791
  * Convert canonical permission set to Antigravity format.
619
792
  * Antigravity reads ~/.gemini/antigravity-cli/settings.json with
@@ -1144,8 +1317,9 @@ export function applyClaudePermissions(set, scope = 'user', cwd, merge = true) {
1144
1317
  const newPermissions = convertToClaudeFormat(set);
1145
1318
  if (merge && config.permissions) {
1146
1319
  const existing = config.permissions;
1147
- const mergedAllow = new Set([...(existing.allow || []), ...newPermissions.permissions.allow]);
1148
- const mergedDeny = new Set([...(existing.deny || []), ...newPermissions.permissions.deny]);
1320
+ // Rewrite stale Write(path) rules already installed in settings.json too.
1321
+ const mergedAllow = new Set([...(existing.allow || []).map(canonicalToClaudeRule), ...newPermissions.permissions.allow]);
1322
+ const mergedDeny = new Set([...(existing.deny || []).map(canonicalToClaudeRule), ...newPermissions.permissions.deny]);
1149
1323
  config.permissions = {
1150
1324
  allow: [...mergedAllow],
1151
1325
  deny: [...mergedDeny],
@@ -1292,7 +1466,7 @@ function applyPermissionsToAgent(agentId, set, scope = 'user', cwd, merge = true
1292
1466
  * Apply a permission set to a specific version's home directory.
1293
1467
  * This writes to {versionHome}/.{agent}/settings.json (or equivalent).
1294
1468
  */
1295
- export function applyPermissionsToVersion(agentId, set, versionHome, merge = true) {
1469
+ export function applyPermissionsToVersion(agentId, set, versionHome, merge = true, cwd) {
1296
1470
  const configDir = path.join(versionHome, agentConfigDirName(agentId));
1297
1471
  try {
1298
1472
  fs.mkdirSync(configDir, { recursive: true });
@@ -1305,8 +1479,9 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1305
1479
  const newPermissions = convertToClaudeFormat(set);
1306
1480
  if (merge && config.permissions) {
1307
1481
  const existing = config.permissions;
1308
- const mergedAllow = new Set([...(existing.allow || []), ...newPermissions.permissions.allow]);
1309
- const mergedDeny = new Set([...(existing.deny || []), ...newPermissions.permissions.deny]);
1482
+ // Rewrite stale Write(path) rules already installed in settings.json too.
1483
+ const mergedAllow = new Set([...(existing.allow || []).map(canonicalToClaudeRule), ...newPermissions.permissions.allow]);
1484
+ const mergedDeny = new Set([...(existing.deny || []).map(canonicalToClaudeRule), ...newPermissions.permissions.deny]);
1310
1485
  const mergedDirs = new Set([...(existing.additionalDirectories || []), ...(newPermissions.permissions.additionalDirectories || [])]);
1311
1486
  const perms = {
1312
1487
  allow: [...mergedAllow],
@@ -1593,6 +1768,50 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1593
1768
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1594
1769
  return { success: true };
1595
1770
  }
1771
+ if (agentId === 'copilot') {
1772
+ const configPath = path.join(versionHome, '.copilot', 'permissions-config.json');
1773
+ let config = { locations: {} };
1774
+ if (fs.existsSync(configPath)) {
1775
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1776
+ }
1777
+ const location = path.resolve(cwd ?? process.cwd());
1778
+ const converted = convertToCopilotFormat(set, location);
1779
+ const incoming = converted.locations[location]?.tool_approvals ?? [];
1780
+ const incomingDirectories = converted.locations[location]?.allowed_directories ?? [];
1781
+ if (merge && incoming.length === 0 && incomingDirectories.length === 0)
1782
+ return { success: true };
1783
+ const locations = (typeof config.locations === 'object' && config.locations !== null && !Array.isArray(config.locations))
1784
+ ? config.locations
1785
+ : {};
1786
+ const existingLocation = (typeof locations[location] === 'object' && locations[location] !== null && !Array.isArray(locations[location]))
1787
+ ? locations[location]
1788
+ : {};
1789
+ const existingApprovals = Array.isArray(existingLocation.tool_approvals) ? existingLocation.tool_approvals : [];
1790
+ const nextApprovals = merge ? mergeCopilotApprovals(existingApprovals, incoming) : incoming;
1791
+ const existingDirectories = Array.isArray(existingLocation.allowed_directories)
1792
+ ? existingLocation.allowed_directories.filter((v) => typeof v === 'string')
1793
+ : [];
1794
+ const nextDirectories = merge
1795
+ ? Array.from(new Set([...existingDirectories, ...incomingDirectories])).sort()
1796
+ : incomingDirectories;
1797
+ const nextLocation = { ...existingLocation };
1798
+ if (nextApprovals.length > 0)
1799
+ nextLocation.tool_approvals = nextApprovals;
1800
+ else
1801
+ delete nextLocation.tool_approvals;
1802
+ if (nextDirectories.length > 0)
1803
+ nextLocation.allowed_directories = nextDirectories;
1804
+ else
1805
+ delete nextLocation.allowed_directories;
1806
+ if (Object.keys(nextLocation).length > 0)
1807
+ locations[location] = nextLocation;
1808
+ else
1809
+ delete locations[location];
1810
+ config.locations = locations;
1811
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1812
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1813
+ return { success: true };
1814
+ }
1596
1815
  if (agentId === 'kiro') {
1597
1816
  const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
1598
1817
  let config = {};
@@ -1669,6 +1888,64 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1669
1888
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1670
1889
  return { success: true };
1671
1890
  }
1891
+ if (agentId === 'forge') {
1892
+ const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
1893
+ let config = {};
1894
+ if (fs.existsSync(permissionsPath)) {
1895
+ const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
1896
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1897
+ config = parsed;
1898
+ }
1899
+ }
1900
+ const newPolicies = convertToForgeFormat(set).policies;
1901
+ if (merge) {
1902
+ const existingPolicies = Array.isArray(config.policies) ? config.policies : [];
1903
+ const seen = new Set();
1904
+ config.policies = [...existingPolicies, ...newPolicies].filter((policy) => {
1905
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy))
1906
+ return false;
1907
+ const key = JSON.stringify(policy);
1908
+ if (seen.has(key))
1909
+ return false;
1910
+ seen.add(key);
1911
+ return true;
1912
+ });
1913
+ }
1914
+ else {
1915
+ config.policies = newPolicies;
1916
+ }
1917
+ fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
1918
+ fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
1919
+ return { success: true };
1920
+ }
1921
+ if (agentId === 'hermes') {
1922
+ const configPath = path.join(versionHome, '.hermes', 'config.yaml');
1923
+ let config = {};
1924
+ if (fs.existsSync(configPath)) {
1925
+ const parsed = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
1926
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1927
+ config = parsed;
1928
+ }
1929
+ }
1930
+ const converted = convertToHermesFormat(set);
1931
+ const approvals = (typeof config.approvals === 'object' && config.approvals !== null && !Array.isArray(config.approvals))
1932
+ ? config.approvals
1933
+ : {};
1934
+ if (merge) {
1935
+ const existingAllow = Array.isArray(config.command_allowlist) ? config.command_allowlist : [];
1936
+ const existingDeny = Array.isArray(approvals.deny) ? approvals.deny : [];
1937
+ config.command_allowlist = Array.from(new Set([...existingAllow, ...converted.command_allowlist])).sort();
1938
+ approvals.deny = Array.from(new Set([...existingDeny, ...converted.approvals.deny])).sort();
1939
+ }
1940
+ else {
1941
+ config.command_allowlist = converted.command_allowlist;
1942
+ approvals.deny = converted.approvals.deny;
1943
+ }
1944
+ config.approvals = approvals;
1945
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1946
+ fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
1947
+ return { success: true };
1948
+ }
1672
1949
  return { success: false, error: `Agent '${agentId}' does not support permissions` };
1673
1950
  }
1674
1951
  catch (err) {
@@ -8,18 +8,12 @@
8
8
  * (+ per-agent symlinks). Delegates to compileRulesForProject, which is
9
9
  * the same helper management-side `agents sync` uses.
10
10
  *
11
- * 2. Mirror project resources from `<cwd>/.agents/{subagents,commands,skills}`
12
- * into the agent's workspace-local discovery dirs (`<cwd>/.claude/agents/`,
13
- * `<cwd>/.claude/commands/`, `<cwd>/.claude/skills/`). File-level symlinks
14
- * so edits in the source dir are live without re-sync. Skips any dest
15
- * entry that already exists and isn't one of our symlinks (don't-clobber).
16
- * v1: claude-only. Other agents have varying workspace conventions (amp
17
- * uses `~/.config/amp`, antigravity uses `~/.gemini/antigravity-cli`,
18
- * codex/gemini/cursor lack subagent support entirely) — they're follow-up
19
- * material. NOTE: `.mcp.json` is intentionally NOT auto-symlinked from
20
- * the launch path — that's a supply-chain surface (cloning a hostile
21
- * repo would auto-register an attacker MCP server). Belongs to full
22
- * `agents sync` with explicit opt-in, not the hot path.
11
+ * 2. Copy project resources from `<cwd>/.agents/{commands,skills,subagents,workflows}`
12
+ * into the agent's workspace-local discovery dir (`<cwd>/.claude/`,
13
+ * `<cwd>/.codex/`, etc.) with an ownership manifest at
14
+ * `<cwd>/.{agent}/.agents-managed.json`. The manifest is the only clobber
15
+ * authority: paths it lists are removed and refreshed, pre-existing paths it
16
+ * does not list are skipped as user-owned.
23
17
  *
24
18
  * 3. Synthesize four scope-grouped plugin marketplaces under the version's
25
19
  * `<versionHome>/.{agent}/plugins/marketplaces/` (for plugin-capable agents):