@renseiai/agentfactory 0.8.19 → 0.8.21

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 (87) hide show
  1. package/dist/src/config/repository-config.d.ts +7 -0
  2. package/dist/src/config/repository-config.d.ts.map +1 -1
  3. package/dist/src/config/repository-config.js +15 -1
  4. package/dist/src/config/repository-config.test.js +1 -1
  5. package/dist/src/governor/decision-engine-adapter.js +5 -10
  6. package/dist/src/governor/decision-engine-adapter.test.js +13 -14
  7. package/dist/src/governor/decision-engine.js +3 -7
  8. package/dist/src/governor/decision-engine.test.js +5 -5
  9. package/dist/src/index.d.ts +1 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1 -0
  12. package/dist/src/merge-queue/adapters/local.d.ts +68 -0
  13. package/dist/src/merge-queue/adapters/local.d.ts.map +1 -0
  14. package/dist/src/merge-queue/adapters/local.js +136 -0
  15. package/dist/src/merge-queue/adapters/local.test.d.ts +2 -0
  16. package/dist/src/merge-queue/adapters/local.test.d.ts.map +1 -0
  17. package/dist/src/merge-queue/adapters/local.test.js +176 -0
  18. package/dist/src/merge-queue/index.d.ts +13 -5
  19. package/dist/src/merge-queue/index.d.ts.map +1 -1
  20. package/dist/src/merge-queue/index.js +13 -6
  21. package/dist/src/merge-queue/merge-queue.integration.test.js +19 -0
  22. package/dist/src/merge-queue/merge-worker.d.ts.map +1 -1
  23. package/dist/src/merge-queue/merge-worker.js +29 -0
  24. package/dist/src/merge-queue/types.d.ts +1 -1
  25. package/dist/src/merge-queue/types.d.ts.map +1 -1
  26. package/dist/src/orchestrator/index.d.ts +4 -0
  27. package/dist/src/orchestrator/index.d.ts.map +1 -1
  28. package/dist/src/orchestrator/index.js +3 -0
  29. package/dist/src/orchestrator/orchestrator.d.ts +31 -0
  30. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  31. package/dist/src/orchestrator/orchestrator.js +263 -11
  32. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  33. package/dist/src/orchestrator/parse-work-result.js +3 -1
  34. package/dist/src/orchestrator/parse-work-result.test.js +6 -0
  35. package/dist/src/orchestrator/quality-baseline.d.ts +83 -0
  36. package/dist/src/orchestrator/quality-baseline.d.ts.map +1 -0
  37. package/dist/src/orchestrator/quality-baseline.js +313 -0
  38. package/dist/src/orchestrator/quality-baseline.test.d.ts +2 -0
  39. package/dist/src/orchestrator/quality-baseline.test.d.ts.map +1 -0
  40. package/dist/src/orchestrator/quality-baseline.test.js +448 -0
  41. package/dist/src/orchestrator/quality-ratchet.d.ts +70 -0
  42. package/dist/src/orchestrator/quality-ratchet.d.ts.map +1 -0
  43. package/dist/src/orchestrator/quality-ratchet.js +162 -0
  44. package/dist/src/orchestrator/quality-ratchet.test.d.ts +2 -0
  45. package/dist/src/orchestrator/quality-ratchet.test.d.ts.map +1 -0
  46. package/dist/src/orchestrator/quality-ratchet.test.js +335 -0
  47. package/dist/src/orchestrator/types.d.ts +2 -0
  48. package/dist/src/orchestrator/types.d.ts.map +1 -1
  49. package/dist/src/providers/codex-app-server-provider.d.ts +37 -1
  50. package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -1
  51. package/dist/src/providers/codex-app-server-provider.js +290 -35
  52. package/dist/src/providers/codex-app-server-provider.test.js +72 -12
  53. package/dist/src/providers/codex-approval-bridge.d.ts +49 -0
  54. package/dist/src/providers/codex-approval-bridge.d.ts.map +1 -0
  55. package/dist/src/providers/codex-approval-bridge.js +117 -0
  56. package/dist/src/providers/codex-approval-bridge.test.d.ts +2 -0
  57. package/dist/src/providers/codex-approval-bridge.test.d.ts.map +1 -0
  58. package/dist/src/providers/codex-approval-bridge.test.js +188 -0
  59. package/dist/src/providers/types.d.ts +25 -0
  60. package/dist/src/providers/types.d.ts.map +1 -1
  61. package/dist/src/routing/types.d.ts +1 -1
  62. package/dist/src/templates/adapters.d.ts +25 -0
  63. package/dist/src/templates/adapters.d.ts.map +1 -1
  64. package/dist/src/templates/adapters.js +70 -0
  65. package/dist/src/templates/adapters.test.js +49 -0
  66. package/dist/src/templates/index.d.ts +1 -0
  67. package/dist/src/templates/index.d.ts.map +1 -1
  68. package/dist/src/templates/registry.d.ts +8 -0
  69. package/dist/src/templates/registry.d.ts.map +1 -1
  70. package/dist/src/templates/registry.js +11 -0
  71. package/dist/src/templates/types.d.ts +22 -0
  72. package/dist/src/templates/types.d.ts.map +1 -1
  73. package/dist/src/templates/types.js +12 -0
  74. package/dist/src/tools/index.d.ts +2 -0
  75. package/dist/src/tools/index.d.ts.map +1 -1
  76. package/dist/src/tools/index.js +1 -0
  77. package/dist/src/tools/registry.d.ts +9 -1
  78. package/dist/src/tools/registry.d.ts.map +1 -1
  79. package/dist/src/tools/registry.js +13 -1
  80. package/dist/src/tools/stdio-server-entry.d.ts +25 -0
  81. package/dist/src/tools/stdio-server-entry.d.ts.map +1 -0
  82. package/dist/src/tools/stdio-server-entry.js +205 -0
  83. package/dist/src/tools/stdio-server.d.ts +87 -0
  84. package/dist/src/tools/stdio-server.d.ts.map +1 -0
  85. package/dist/src/tools/stdio-server.js +138 -0
  86. package/dist/src/workflow/workflow-types.d.ts +3 -3
  87. package/package.json +3 -2
@@ -23,6 +23,8 @@
23
23
  */
24
24
  import { spawn } from 'child_process';
25
25
  import { createInterface } from 'readline';
26
+ import { classifyTool } from '../tools/tool-category.js';
27
+ import { evaluateCommandApproval, evaluateFileChangeApproval, } from './codex-approval-bridge.js';
26
28
  function isResponse(msg) {
27
29
  return 'id' in msg && typeof msg.id === 'number';
28
30
  }
@@ -208,6 +210,60 @@ export class AppServerProcessManager {
208
210
  isHealthy() {
209
211
  return this.initialized && !!this.process && !this.process.killed;
210
212
  }
213
+ // ─── MCP Server Configuration (SUP-1744) ──────────────────────────
214
+ /** Whether MCP servers have been configured on this process */
215
+ mcpConfigured = false;
216
+ /**
217
+ * Register MCP servers with the app-server via config/batchWrite.
218
+ * Called once after initialization to tell Codex about the stdio
219
+ * MCP tool servers (af-linear, af-code-intelligence, etc.).
220
+ *
221
+ * Uses the Codex app-server `config/batchWrite` JSON-RPC method
222
+ * to register multiple MCP server configurations in a single call.
223
+ */
224
+ async configureMcpServers(servers) {
225
+ if (!this.initialized || this.mcpConfigured)
226
+ return;
227
+ if (servers.length === 0)
228
+ return;
229
+ const mcpServers = {};
230
+ for (const server of servers) {
231
+ mcpServers[server.name] = {
232
+ command: server.command,
233
+ args: server.args,
234
+ env: server.env,
235
+ };
236
+ }
237
+ try {
238
+ await this.request('config/batchWrite', {
239
+ entries: [
240
+ { key: 'mcpServers', value: mcpServers },
241
+ ],
242
+ });
243
+ this.mcpConfigured = true;
244
+ console.error(`[CodexAppServer] Configured ${servers.length} MCP servers: ${servers.map(s => s.name).join(', ')}`);
245
+ }
246
+ catch (err) {
247
+ // config/batchWrite may not be supported in all Codex versions
248
+ console.error(`[CodexAppServer] Failed to configure MCP servers: ${err instanceof Error ? err.message : String(err)}`);
249
+ }
250
+ }
251
+ /**
252
+ * Query MCP server health via mcpServerStatus/list.
253
+ * Returns the status of all registered MCP servers.
254
+ */
255
+ async getMcpServerStatus() {
256
+ if (!this.initialized)
257
+ return [];
258
+ try {
259
+ const result = await this.request('mcpServerStatus/list');
260
+ return result?.servers ?? [];
261
+ }
262
+ catch {
263
+ // mcpServerStatus/list may not be supported
264
+ return [];
265
+ }
266
+ }
211
267
  /**
212
268
  * Get the PID of the app-server process.
213
269
  */
@@ -470,13 +526,18 @@ export function mapAppServerItemEvent(method, params) {
470
526
  }];
471
527
  }
472
528
  return [];
473
- case 'mcpToolCall':
529
+ case 'mcpToolCall': {
530
+ // Normalize tool name to mcp__{server}__{tool} format (SUP-1745)
531
+ // This matches the convention used by the Claude provider for
532
+ // in-process MCP tools, enabling consistent tool tracking.
533
+ const mcpToolName = normalizeMcpToolName(item.server, item.tool);
474
534
  if (isStarted) {
475
535
  return [{
476
536
  type: 'tool_use',
477
- toolName: `mcp:${item.server}/${item.tool}`,
537
+ toolName: mcpToolName,
478
538
  toolUseId: item.id,
479
539
  input: (item.arguments ?? {}),
540
+ toolCategory: classifyTool(mcpToolName),
480
541
  raw: { method, params },
481
542
  }];
482
543
  }
@@ -486,7 +547,7 @@ export function mapAppServerItemEvent(method, params) {
486
547
  ?? (item.result?.content ? JSON.stringify(item.result.content) : '');
487
548
  return [{
488
549
  type: 'tool_result',
489
- toolName: `mcp:${item.server}/${item.tool}`,
550
+ toolName: mcpToolName,
490
551
  toolUseId: item.id,
491
552
  content,
492
553
  isError,
@@ -494,6 +555,7 @@ export function mapAppServerItemEvent(method, params) {
494
555
  }];
495
556
  }
496
557
  return [];
558
+ }
497
559
  case 'plan':
498
560
  return [{
499
561
  type: 'system',
@@ -525,11 +587,31 @@ export function mapAppServerItemEvent(method, params) {
525
587
  }
526
588
  }
527
589
  // ---------------------------------------------------------------------------
590
+ // MCP tool name normalization (SUP-1745)
591
+ // ---------------------------------------------------------------------------
592
+ /**
593
+ * Normalize Codex MCP tool names to the `mcp__{server}__{tool}` format
594
+ * used by the orchestrator and Claude provider for consistent tool tracking.
595
+ *
596
+ * Codex reports MCP tools as `server` + `tool` (e.g., server='af-linear',
597
+ * tool='af_linear_get_issue'). We normalize to 'mcp__af-linear__af_linear_get_issue'.
598
+ */
599
+ export function normalizeMcpToolName(server, tool) {
600
+ if (server && tool) {
601
+ return `mcp__${server}__${tool}`;
602
+ }
603
+ // Fallback for missing server/tool
604
+ return `mcp:${server ?? 'unknown'}/${tool ?? 'unknown'}`;
605
+ }
606
+ // ---------------------------------------------------------------------------
528
607
  // Resolve approval policy from AgentSpawnConfig
529
608
  // ---------------------------------------------------------------------------
530
609
  function resolveApprovalPolicy(config) {
610
+ // SUP-1747: Use 'onRequest' for autonomous agents so all tool executions
611
+ // flow through the approval bridge for safety evaluation. The bridge
612
+ // auto-approves safe commands and declines destructive patterns.
531
613
  if (config.autonomous)
532
- return 'never';
614
+ return 'onRequest';
533
615
  return 'unlessTrusted';
534
616
  }
535
617
  function resolveSandboxPolicy(config) {
@@ -541,6 +623,37 @@ function resolveSandboxPolicy(config) {
541
623
  };
542
624
  }
543
625
  // ---------------------------------------------------------------------------
626
+ // Base Instructions Builder (SUP-1746)
627
+ // ---------------------------------------------------------------------------
628
+ /**
629
+ * Build persistent base instructions for the Codex App Server `thread/start`.
630
+ *
631
+ * Assembles safety rules (mirroring `autonomousCanUseTool` deny patterns as
632
+ * natural-language rules) and optional project-specific instructions loaded
633
+ * from AGENTS.md or CLAUDE.md in the worktree root.
634
+ */
635
+ function buildBaseInstructions(config) {
636
+ // If explicit baseInstructions are provided (from orchestrator), use those
637
+ if (config.baseInstructions) {
638
+ return config.baseInstructions;
639
+ }
640
+ // Otherwise, build safety-only instructions as a fallback
641
+ const sections = [];
642
+ sections.push(`# Safety Rules
643
+
644
+ You are running in an AgentFactory-managed worktree. Follow these rules strictly:
645
+
646
+ 1. NEVER run: rm -rf / (or any rm of the filesystem root)
647
+ 2. NEVER run: git worktree remove, git worktree prune
648
+ 3. NEVER run: git reset --hard
649
+ 4. NEVER run: git push --force (use --force-with-lease on feature branches if needed)
650
+ 5. NEVER run: git checkout <branch>, git switch <branch> (do not change the checked-out branch)
651
+ 6. NEVER modify files in the .git directory
652
+ 7. Work only within the worktree directory: ${config.cwd}
653
+ 8. Commit changes with descriptive messages before reporting completion`);
654
+ return sections.join('\n\n');
655
+ }
656
+ // ---------------------------------------------------------------------------
544
657
  // AgentHandle for App Server threads (SUP-1737)
545
658
  // ---------------------------------------------------------------------------
546
659
  class AppServerAgentHandle {
@@ -554,9 +667,12 @@ class AppServerAgentHandle {
554
667
  totalOutputTokens: 0,
555
668
  turnCount: 0,
556
669
  };
670
+ activeTurnId = null;
557
671
  notificationQueue = [];
558
672
  notificationResolve = null;
559
673
  streamEnded = false;
674
+ /** True while we're waiting for a possible injected turn between turns */
675
+ awaitingInjection = false;
560
676
  constructor(processManager, config, resumeThreadId) {
561
677
  this.processManager = processManager;
562
678
  this.config = config;
@@ -565,12 +681,18 @@ class AppServerAgentHandle {
565
681
  get stream() {
566
682
  return this.createEventStream();
567
683
  }
568
- async injectMessage(_text) {
569
- // App Server supports turn/steer for mid-turn injection, but the
570
- // orchestrator's contract expects injectMessage to add a new user message.
571
- // For now, throw like the CLI provider — the orchestrator uses stop+resume.
572
- throw new Error('Codex App Server provider does not support mid-session message injection. ' +
573
- 'Stop and resume with a new prompt instead.');
684
+ async injectMessage(text) {
685
+ if (!this.sessionId) {
686
+ throw new Error('No active session for message injection');
687
+ }
688
+ if (this.activeTurnId) {
689
+ // Mid-turn injection: steer the active turn (SUP-1740)
690
+ await this.steerTurn(text);
691
+ }
692
+ else {
693
+ // Between-turn injection: start a new turn on the existing thread (SUP-1741)
694
+ await this.startNewTurn(text);
695
+ }
574
696
  }
575
697
  async stop() {
576
698
  if (this.sessionId) {
@@ -593,10 +715,103 @@ class AppServerAgentHandle {
593
715
  this.streamEnded = true;
594
716
  this.notificationResolve?.();
595
717
  }
718
+ /**
719
+ * Steer an active turn with additional user input (SUP-1740).
720
+ * Sends a `turn/steer` JSON-RPC request to inject a message mid-turn.
721
+ */
722
+ async steerTurn(text) {
723
+ if (!this.sessionId || !this.activeTurnId) {
724
+ throw new Error('No active turn to steer');
725
+ }
726
+ await this.processManager.request('turn/steer', {
727
+ threadId: this.sessionId,
728
+ turnId: this.activeTurnId,
729
+ input: [{ type: 'text', text }],
730
+ });
731
+ }
732
+ /**
733
+ * Handle an approval request from the App Server (SUP-1747).
734
+ *
735
+ * Evaluates the command or file change against deny patterns (ported from
736
+ * Claude's `autonomousCanUseTool`) and template-level permissions, then
737
+ * responds with accept/decline/acceptForSession via JSON-RPC.
738
+ *
739
+ * Returns a system event if the request was declined, for observability.
740
+ */
741
+ async handleApprovalRequest(notification) {
742
+ const params = notification.params ?? {};
743
+ const requestId = params.requestId;
744
+ const command = params.command;
745
+ const filePath = params.filePath;
746
+ let decision;
747
+ if (command !== undefined) {
748
+ // Command execution approval
749
+ decision = evaluateCommandApproval(command, this.config.permissionConfig);
750
+ }
751
+ else if (filePath !== undefined) {
752
+ // File change approval
753
+ decision = evaluateFileChangeApproval(filePath, this.config.cwd, this.config.permissionConfig);
754
+ }
755
+ else {
756
+ // Unknown approval request — accept by default
757
+ decision = { action: 'acceptForSession' };
758
+ }
759
+ // Respond to the App Server with the approval decision
760
+ await this.processManager.request('approval/respond', {
761
+ threadId: this.sessionId,
762
+ requestId,
763
+ decision: decision.action,
764
+ reason: decision.reason,
765
+ });
766
+ // Emit system event for declined approvals (observability)
767
+ if (decision.action === 'decline') {
768
+ const target = command ?? filePath ?? 'unknown';
769
+ return {
770
+ type: 'system',
771
+ subtype: 'approval_denied',
772
+ message: `Blocked: ${decision.reason} — ${command ? 'command' : 'file'}: ${target}`,
773
+ raw: notification,
774
+ };
775
+ }
776
+ return null;
777
+ }
778
+ /**
779
+ * Start a new turn on the existing thread with additional user input (SUP-1741).
780
+ * Used for between-turn injection when no turn is currently active.
781
+ */
782
+ async startNewTurn(text) {
783
+ if (!this.sessionId) {
784
+ throw new Error('No active session to start new turn');
785
+ }
786
+ const turnParams = {
787
+ threadId: this.sessionId,
788
+ input: [{ type: 'text', text }],
789
+ cwd: this.config.cwd,
790
+ approvalPolicy: resolveApprovalPolicy(this.config),
791
+ };
792
+ if (this.config.maxTurns) {
793
+ turnParams.maxTurns = this.config.maxTurns;
794
+ }
795
+ const sandboxPolicy = resolveSandboxPolicy(this.config);
796
+ if (sandboxPolicy) {
797
+ turnParams.sandboxPolicy = sandboxPolicy;
798
+ }
799
+ // Mark that we're no longer waiting between turns
800
+ this.awaitingInjection = false;
801
+ await this.processManager.request('turn/start', turnParams);
802
+ // Wake up the notification loop so it processes the new turn's events
803
+ this.notificationResolve?.();
804
+ }
596
805
  async *createEventStream() {
597
806
  try {
598
807
  // Ensure the app-server is running
599
808
  await this.processManager.start();
809
+ // Configure MCP servers if provided (SUP-1744)
810
+ // This registers stdio MCP tool servers (af-linear, af-code-intelligence)
811
+ // with the Codex app-server so it can discover and invoke them.
812
+ if (this.config.mcpStdioServers && this.config.mcpStdioServers.length > 0) {
813
+ await this.processManager.configureMcpServers(this.config.mcpStdioServers);
814
+ }
600
815
  // Start or resume the thread
601
816
  let threadId;
602
817
  if (this.resumeThreadId) {
@@ -614,6 +829,12 @@ class AppServerAgentHandle {
614
829
  approvalPolicy: resolveApprovalPolicy(this.config),
615
830
  serviceName: 'agentfactory',
616
831
  };
832
+ // SUP-1746: Pass persistent system instructions via `instructions` on thread/start.
833
+ // Separates safety rules and project context from per-turn task input.
834
+ const instructions = buildBaseInstructions(this.config);
835
+ if (instructions) {
836
+ threadParams.instructions = instructions;
837
+ }
617
838
  const sandboxPolicy = resolveSandboxPolicy(this.config);
618
839
  if (sandboxPolicy) {
619
840
  threadParams.sandboxPolicy = sandboxPolicy;
@@ -660,8 +881,16 @@ class AppServerAgentHandle {
660
881
  turnParams.sandboxPolicy = sandboxPolicy;
661
882
  }
662
883
  await this.processManager.request('turn/start', turnParams);
663
- // Stream notifications until turn completes
664
- let hasResult = false;
884
+ // Stream notifications until explicitly stopped.
885
+ // After a turn completes, we enter "awaiting injection" mode — the stream
886
+ // stays alive to allow injectMessage() to start a new turn. The stream
887
+ // only terminates when stop() is called or the process dies.
888
+ //
889
+ // turn/completed `result` events are intercepted and re-emitted as `system`
890
+ // events so the orchestrator doesn't interpret them as the agent finishing.
891
+ // A single `result` event is emitted when the stream actually ends.
892
+ let lastTurnSuccess = true;
893
+ let lastTurnErrors;
665
894
  while (!this.streamEnded) {
666
895
  // Wait for notifications
667
896
  if (this.notificationQueue.length === 0) {
@@ -675,17 +904,46 @@ class AppServerAgentHandle {
675
904
  // Drain the queue
676
905
  while (this.notificationQueue.length > 0) {
677
906
  const notification = this.notificationQueue.shift();
907
+ // SUP-1747: Intercept approval requests before other processing.
908
+ // The App Server emits these when approvalPolicy is 'onRequest'.
909
+ if (notification.method.endsWith('/requestApproval')) {
910
+ const deniedEvent = await this.handleApprovalRequest(notification);
911
+ if (deniedEvent) {
912
+ yield deniedEvent;
913
+ }
914
+ continue; // Don't yield as a regular AgentEvent
915
+ }
916
+ // Track active turn ID for mid-turn steering (SUP-1740)
917
+ if (notification.method === 'turn/started') {
918
+ const turn = notification.params?.turn;
919
+ if (turn?.id) {
920
+ this.activeTurnId = turn.id;
921
+ }
922
+ this.awaitingInjection = false;
923
+ }
924
+ else if (notification.method === 'turn/completed') {
925
+ this.activeTurnId = null;
926
+ // Enter awaiting-injection mode — the stream stays alive
927
+ this.awaitingInjection = true;
928
+ }
678
929
  const events = mapAppServerNotification(notification, this.mapperState);
679
930
  for (const event of events) {
931
+ // Intercept turn/completed result events — convert to system events
932
+ // so the orchestrator doesn't think the agent is done. Track the last
933
+ // turn's outcome so we can emit a proper result when the stream ends.
680
934
  if (event.type === 'result') {
681
- hasResult = true;
935
+ lastTurnSuccess = event.success;
936
+ lastTurnErrors = event.errors;
937
+ yield {
938
+ type: 'system',
939
+ subtype: 'turn_result',
940
+ message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
941
+ raw: event.raw,
942
+ };
943
+ }
944
+ else {
945
+ yield event;
682
946
  }
683
- yield event;
684
- }
685
- // If we got a result, we're done
686
- if (hasResult) {
687
- this.streamEnded = true;
688
- break;
689
947
  }
690
948
  }
691
949
  }
@@ -697,21 +955,18 @@ class AppServerAgentHandle {
697
955
  catch {
698
956
  // Best effort
699
957
  }
700
- // If we never got a result, synthesize one
701
- if (!hasResult) {
702
- yield {
703
- type: 'result',
704
- success: false,
705
- errors: ['Stream ended without a result event'],
706
- errorSubtype: 'stream_ended',
707
- cost: {
708
- inputTokens: this.mapperState.totalInputTokens || undefined,
709
- outputTokens: this.mapperState.totalOutputTokens || undefined,
710
- numTurns: this.mapperState.turnCount || undefined,
711
- },
712
- raw: null,
713
- };
714
- }
958
+ // Emit the final result event when the stream ends
959
+ yield {
960
+ type: 'result',
961
+ success: lastTurnSuccess,
962
+ errors: lastTurnErrors,
963
+ cost: {
964
+ inputTokens: this.mapperState.totalInputTokens || undefined,
965
+ outputTokens: this.mapperState.totalOutputTokens || undefined,
966
+ numTurns: this.mapperState.turnCount || undefined,
967
+ },
968
+ raw: null,
969
+ };
715
970
  }
716
971
  catch (err) {
717
972
  const message = err instanceof Error ? err.message : String(err);
@@ -743,7 +998,7 @@ class AppServerAgentHandle {
743
998
  export class CodexAppServerProvider {
744
999
  name = 'codex';
745
1000
  capabilities = {
746
- supportsMessageInjection: false,
1001
+ supportsMessageInjection: true,
747
1002
  supportsSessionResume: true,
748
1003
  };
749
1004
  /** Shared process manager — one app-server process serves all threads */
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { mapAppServerNotification, mapAppServerItemEvent, } from './codex-app-server-provider.js';
2
+ import { mapAppServerNotification, mapAppServerItemEvent, normalizeMcpToolName, } from './codex-app-server-provider.js';
3
3
  function freshState() {
4
4
  return {
5
5
  sessionId: null,
@@ -400,31 +400,33 @@ describe('mapAppServerItemEvent', () => {
400
400
  isError: false,
401
401
  });
402
402
  });
403
- it('maps mcpToolCall item.started to tool_use', () => {
403
+ // --- MCP tool call mapping (SUP-1745) ---
404
+ it('maps mcpToolCall item.started to tool_use with normalized name', () => {
404
405
  const result = mapAppServerItemEvent('item/started', {
405
406
  item: {
406
407
  id: 'mcp-1',
407
408
  type: 'mcpToolCall',
408
- server: 'linear',
409
- tool: 'create_issue',
409
+ server: 'af-linear',
410
+ tool: 'af_linear_create_issue',
410
411
  arguments: { title: 'Test' },
411
412
  status: 'in_progress',
412
413
  },
413
414
  });
414
415
  expect(result[0]).toMatchObject({
415
416
  type: 'tool_use',
416
- toolName: 'mcp:linear/create_issue',
417
+ toolName: 'mcp__af-linear__af_linear_create_issue',
417
418
  toolUseId: 'mcp-1',
418
419
  input: { title: 'Test' },
420
+ toolCategory: 'general',
419
421
  });
420
422
  });
421
- it('maps mcpToolCall item.completed to tool_result (success)', () => {
423
+ it('maps mcpToolCall item.completed to tool_result with normalized name (success)', () => {
422
424
  const result = mapAppServerItemEvent('item/completed', {
423
425
  item: {
424
426
  id: 'mcp-1',
425
427
  type: 'mcpToolCall',
426
- server: 'linear',
427
- tool: 'create_issue',
428
+ server: 'af-linear',
429
+ tool: 'af_linear_create_issue',
428
430
  arguments: {},
429
431
  result: { content: [{ text: 'Created' }] },
430
432
  status: 'completed',
@@ -432,7 +434,7 @@ describe('mapAppServerItemEvent', () => {
432
434
  });
433
435
  expect(result[0]).toMatchObject({
434
436
  type: 'tool_result',
435
- toolName: 'mcp:linear/create_issue',
437
+ toolName: 'mcp__af-linear__af_linear_create_issue',
436
438
  content: '[{"text":"Created"}]',
437
439
  isError: false,
438
440
  });
@@ -442,8 +444,8 @@ describe('mapAppServerItemEvent', () => {
442
444
  item: {
443
445
  id: 'mcp-1',
444
446
  type: 'mcpToolCall',
445
- server: 'linear',
446
- tool: 'create_issue',
447
+ server: 'af-linear',
448
+ tool: 'af_linear_create_issue',
447
449
  arguments: {},
448
450
  error: { message: 'Auth failed' },
449
451
  status: 'failed',
@@ -451,10 +453,28 @@ describe('mapAppServerItemEvent', () => {
451
453
  });
452
454
  expect(result[0]).toMatchObject({
453
455
  type: 'tool_result',
456
+ toolName: 'mcp__af-linear__af_linear_create_issue',
454
457
  isError: true,
455
458
  content: 'Auth failed',
456
459
  });
457
460
  });
461
+ it('maps mcpToolCall with code-intelligence server to searchable category', () => {
462
+ const result = mapAppServerItemEvent('item/started', {
463
+ item: {
464
+ id: 'mcp-2',
465
+ type: 'mcpToolCall',
466
+ server: 'af-code-intelligence',
467
+ tool: 'af_code_search_symbols',
468
+ arguments: { query: 'ToolRegistry' },
469
+ status: 'in_progress',
470
+ },
471
+ });
472
+ expect(result[0]).toMatchObject({
473
+ type: 'tool_use',
474
+ toolName: 'mcp__af-code-intelligence__af_code_search_symbols',
475
+ toolCategory: 'research', // search matches research category
476
+ });
477
+ });
458
478
  it('maps plan item to system event', () => {
459
479
  const result = mapAppServerItemEvent('item/completed', {
460
480
  item: { id: 'p-1', type: 'plan', text: 'Step 1: Read code' },
@@ -512,7 +532,7 @@ describe('CodexAppServerProvider', () => {
512
532
  const { createCodexAppServerProvider } = await import('./codex-app-server-provider.js');
513
533
  const provider = createCodexAppServerProvider();
514
534
  expect(provider.name).toBe('codex');
515
- expect(provider.capabilities.supportsMessageInjection).toBe(false);
535
+ expect(provider.capabilities.supportsMessageInjection).toBe(true);
516
536
  expect(provider.capabilities.supportsSessionResume).toBe(true);
517
537
  });
518
538
  });
@@ -526,4 +546,44 @@ describe('AppServerProcessManager', () => {
526
546
  expect(manager.isHealthy()).toBe(false);
527
547
  expect(manager.pid).toBeUndefined();
528
548
  });
549
+ it('configureMcpServers is a no-op when not initialized', async () => {
550
+ const { AppServerProcessManager } = await import('./codex-app-server-provider.js');
551
+ const manager = new AppServerProcessManager({ cwd: '/tmp' });
552
+ // Should not throw when not initialized
553
+ await manager.configureMcpServers([
554
+ { name: 'af-linear', command: 'node', args: ['server.js'] },
555
+ ]);
556
+ // No error means it silently skipped (not initialized)
557
+ });
558
+ it('getMcpServerStatus returns empty when not initialized', async () => {
559
+ const { AppServerProcessManager } = await import('./codex-app-server-provider.js');
560
+ const manager = new AppServerProcessManager({ cwd: '/tmp' });
561
+ const result = await manager.getMcpServerStatus();
562
+ expect(result).toEqual([]);
563
+ });
564
+ });
565
+ // ---------------------------------------------------------------------------
566
+ // normalizeMcpToolName (SUP-1745)
567
+ // ---------------------------------------------------------------------------
568
+ describe('normalizeMcpToolName', () => {
569
+ it('normalizes server and tool to mcp__ format', () => {
570
+ expect(normalizeMcpToolName('af-linear', 'af_linear_get_issue'))
571
+ .toBe('mcp__af-linear__af_linear_get_issue');
572
+ });
573
+ it('normalizes code-intelligence tools', () => {
574
+ expect(normalizeMcpToolName('af-code-intelligence', 'af_code_search_symbols'))
575
+ .toBe('mcp__af-code-intelligence__af_code_search_symbols');
576
+ });
577
+ it('falls back to mcp:server/tool format for missing server', () => {
578
+ expect(normalizeMcpToolName(undefined, 'some_tool'))
579
+ .toBe('mcp:unknown/some_tool');
580
+ });
581
+ it('falls back to mcp:server/tool format for missing tool', () => {
582
+ expect(normalizeMcpToolName('server', undefined))
583
+ .toBe('mcp:server/unknown');
584
+ });
585
+ it('handles both missing server and tool', () => {
586
+ expect(normalizeMcpToolName(undefined, undefined))
587
+ .toBe('mcp:unknown/unknown');
588
+ });
529
589
  });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Codex Approval Bridge (SUP-1747)
3
+ *
4
+ * Evaluates Codex App Server `requestApproval` events against deny patterns
5
+ * ported from Claude's `autonomousCanUseTool` callback (claude-provider.ts:33-112).
6
+ *
7
+ * When the Codex App Server's `approvalPolicy` is set to `'onRequest'`, every
8
+ * tool execution flows through this bridge. The bridge auto-approves safe commands
9
+ * and declines destructive patterns — giving Codex the same safety guardrails as Claude
10
+ * without requiring human interaction.
11
+ *
12
+ * Architecture:
13
+ * App Server emits → requestApproval notification
14
+ * Approval Bridge evaluates → deny patterns + permission config
15
+ * Bridge responds → accept | decline | acceptForSession
16
+ */
17
+ import type { CodexPermissionConfig } from '../templates/adapters.js';
18
+ export interface ApprovalDecision {
19
+ action: 'accept' | 'decline' | 'acceptForSession';
20
+ reason?: string;
21
+ }
22
+ interface DenyPattern {
23
+ pattern: RegExp;
24
+ reason: string;
25
+ }
26
+ /**
27
+ * Hardcoded safety deny patterns — always active regardless of template config.
28
+ * These mirror the deny-list in Claude's `autonomousCanUseTool` callback.
29
+ */
30
+ export declare const SAFETY_DENY_PATTERNS: DenyPattern[];
31
+ /**
32
+ * Evaluate a shell command against safety deny patterns and optional
33
+ * template-level permission patterns.
34
+ *
35
+ * Evaluation order:
36
+ * 1. Safety deny patterns (always checked first, cannot be overridden)
37
+ * 2. Template deny patterns (from `tools.disallow`)
38
+ * 3. Template allow patterns (from `tools.allow`, if present)
39
+ * 4. Default: acceptForSession
40
+ */
41
+ export declare function evaluateCommandApproval(command: string, permissionConfig?: CodexPermissionConfig): ApprovalDecision;
42
+ /**
43
+ * Evaluate a file change (write/edit) against safety rules and optional
44
+ * template-level permissions.
45
+ */
46
+ export declare function evaluateFileChangeApproval(filePath: string, cwd: string, permissionConfig?: CodexPermissionConfig): ApprovalDecision;
47
+ /** Exported for testing */
48
+ export type { DenyPattern };
49
+ //# sourceMappingURL=codex-approval-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-approval-bridge.d.ts","sourceRoot":"","sources":["../../../src/providers/codex-approval-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAMrE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,kBAAkB,CAAA;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAMD,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,EAW7C,CAAA;AAMD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,gBAAgB,CAAC,EAAE,qBAAqB,GACvC,gBAAgB,CAiDlB;AAMD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,gBAAgB,CAAC,EAAE,qBAAqB,GACvC,gBAAgB,CAsBlB;AAED,2BAA2B;AAC3B,YAAY,EAAE,WAAW,EAAE,CAAA"}