@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.
- package/dist/src/config/repository-config.d.ts +7 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +15 -1
- package/dist/src/config/repository-config.test.js +1 -1
- package/dist/src/governor/decision-engine-adapter.js +5 -10
- package/dist/src/governor/decision-engine-adapter.test.js +13 -14
- package/dist/src/governor/decision-engine.js +3 -7
- package/dist/src/governor/decision-engine.test.js +5 -5
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/local.d.ts +68 -0
- package/dist/src/merge-queue/adapters/local.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.js +136 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.test.js +176 -0
- package/dist/src/merge-queue/index.d.ts +13 -5
- package/dist/src/merge-queue/index.d.ts.map +1 -1
- package/dist/src/merge-queue/index.js +13 -6
- package/dist/src/merge-queue/merge-queue.integration.test.js +19 -0
- package/dist/src/merge-queue/merge-worker.d.ts.map +1 -1
- package/dist/src/merge-queue/merge-worker.js +29 -0
- package/dist/src/merge-queue/types.d.ts +1 -1
- package/dist/src/merge-queue/types.d.ts.map +1 -1
- package/dist/src/orchestrator/index.d.ts +4 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +3 -0
- package/dist/src/orchestrator/orchestrator.d.ts +31 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +263 -11
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +3 -1
- package/dist/src/orchestrator/parse-work-result.test.js +6 -0
- package/dist/src/orchestrator/quality-baseline.d.ts +83 -0
- package/dist/src/orchestrator/quality-baseline.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.js +313 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.test.js +448 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts +70 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.js +162 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.test.js +335 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.d.ts +37 -1
- package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.js +290 -35
- package/dist/src/providers/codex-app-server-provider.test.js +72 -12
- package/dist/src/providers/codex-approval-bridge.d.ts +49 -0
- package/dist/src/providers/codex-approval-bridge.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.js +117 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts +2 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.test.js +188 -0
- package/dist/src/providers/types.d.ts +25 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/routing/types.d.ts +1 -1
- package/dist/src/templates/adapters.d.ts +25 -0
- package/dist/src/templates/adapters.d.ts.map +1 -1
- package/dist/src/templates/adapters.js +70 -0
- package/dist/src/templates/adapters.test.js +49 -0
- package/dist/src/templates/index.d.ts +1 -0
- package/dist/src/templates/index.d.ts.map +1 -1
- package/dist/src/templates/registry.d.ts +8 -0
- package/dist/src/templates/registry.d.ts.map +1 -1
- package/dist/src/templates/registry.js +11 -0
- package/dist/src/templates/types.d.ts +22 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +12 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/registry.d.ts +9 -1
- package/dist/src/tools/registry.d.ts.map +1 -1
- package/dist/src/tools/registry.js +13 -1
- package/dist/src/tools/stdio-server-entry.d.ts +25 -0
- package/dist/src/tools/stdio-server-entry.d.ts.map +1 -0
- package/dist/src/tools/stdio-server-entry.js +205 -0
- package/dist/src/tools/stdio-server.d.ts +87 -0
- package/dist/src/tools/stdio-server.d.ts.map +1 -0
- package/dist/src/tools/stdio-server.js +138 -0
- package/dist/src/workflow/workflow-types.d.ts +3 -3
- 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:
|
|
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:
|
|
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 '
|
|
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(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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(
|
|
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"}
|