@peakflo/agent-harness 0.2.9-rc.4 → 0.2.9-rc.6
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/README.md +32 -0
- package/dist/adapter-resolver.d.ts +10 -0
- package/dist/adapter-resolver.js +107 -17
- package/dist/adapters/acp-adapter.d.ts +15 -0
- package/dist/adapters/acp-adapter.js +144 -22
- package/dist/adapters/claude-code-adapter.d.ts +25 -0
- package/dist/adapters/claude-code-adapter.js +117 -2
- package/dist/adapters/codex-app-server-adapter.d.ts +26 -0
- package/dist/adapters/codex-app-server-adapter.js +129 -55
- package/dist/adapters/codex-approval-text.d.ts +46 -0
- package/dist/adapters/codex-approval-text.js +314 -0
- package/dist/adapters/cursor-acp.d.ts +84 -0
- package/dist/adapters/cursor-acp.js +202 -0
- package/dist/runtime/session-runner.d.ts +29 -0
- package/dist/runtime/session-runner.js +6 -1
- package/package.json +1 -1
|
@@ -702,7 +702,18 @@ class ClaudeCodeAdapter {
|
|
|
702
702
|
// the mode above decorative in exactly the way G1 was.
|
|
703
703
|
...(claudePermissionMode === 'bypassPermissions'
|
|
704
704
|
? { allowDangerouslySkipPermissions: true }
|
|
705
|
-
: {
|
|
705
|
+
: {
|
|
706
|
+
/*
|
|
707
|
+
* ADDED, not in 20x. In every mode that asks, the SDK calls this
|
|
708
|
+
* before a tool that needs permission. Without it the SDK has
|
|
709
|
+
* nobody to ask and DENIES the tool: nothing unsafe runs, but
|
|
710
|
+
* "Ask before each action" asked nobody and refused everything.
|
|
711
|
+
* The question is held here until the runner brings the person's
|
|
712
|
+
* answer (`getPendingApproval` / `respondToApproval`, the same
|
|
713
|
+
* two methods the Codex and ACP adapters have).
|
|
714
|
+
*/
|
|
715
|
+
canUseTool: (toolName, input, extra) => this.askToolApproval(sessionId, toolName, input, extra?.signal)
|
|
716
|
+
}),
|
|
706
717
|
...(secretHooks ? { hooks: secretHooks } : {}),
|
|
707
718
|
};
|
|
708
719
|
// Determine session continuation mode
|
|
@@ -804,6 +815,92 @@ class ClaudeCodeAdapter {
|
|
|
804
815
|
// Start consuming stream
|
|
805
816
|
session.streamTask = this.consumeStream(sessionId, session);
|
|
806
817
|
}
|
|
818
|
+
/* ---------------------------------------------------------------------- */
|
|
819
|
+
/* Tool approvals (ADDED, not in 20x) — see `canUseTool` in startQuery */
|
|
820
|
+
/* ---------------------------------------------------------------------- */
|
|
821
|
+
/**
|
|
822
|
+
* Questions waiting for a person, oldest first, per session. Claude can ask
|
|
823
|
+
* about several tools at once (parallel tool use); they are answered one at
|
|
824
|
+
* a time, in the order they were asked, and none is answered by a reply
|
|
825
|
+
* that was meant for another.
|
|
826
|
+
*/
|
|
827
|
+
pendingToolApprovals = new Map();
|
|
828
|
+
askToolApproval(sessionId, toolName, input, signal) {
|
|
829
|
+
return new Promise((resolve) => {
|
|
830
|
+
// The command, path or URL is shown in full: a person must not approve
|
|
831
|
+
// from a shortened view of what will run. Only the fallback, a dump of
|
|
832
|
+
// an unknown tool's whole input, is cut, and says so.
|
|
833
|
+
const dump = JSON.stringify(input ?? {});
|
|
834
|
+
const detail = typeof input?.command === 'string' ? input.command
|
|
835
|
+
: typeof input?.file_path === 'string' ? input.file_path
|
|
836
|
+
: typeof input?.url === 'string' ? input.url
|
|
837
|
+
: dump.length > 300 ? `${dump.slice(0, 300)}… (input shortened)` : dump;
|
|
838
|
+
const toolCallId = `claude-approval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
839
|
+
const onAbort = () => entry.settle(false, 'The turn was stopped before this action was approved.');
|
|
840
|
+
const entry = {
|
|
841
|
+
toolCallId,
|
|
842
|
+
question: `${toolName}: ${detail}`,
|
|
843
|
+
settle: (approved, reason) => {
|
|
844
|
+
signal?.removeEventListener('abort', onAbort);
|
|
845
|
+
const queue = this.pendingToolApprovals.get(sessionId) ?? [];
|
|
846
|
+
const index = queue.indexOf(entry);
|
|
847
|
+
if (index < 0)
|
|
848
|
+
return; // already answered
|
|
849
|
+
queue.splice(index, 1);
|
|
850
|
+
if (queue.length === 0)
|
|
851
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
852
|
+
if (approved)
|
|
853
|
+
resolve({ behavior: 'allow', updatedInput: input });
|
|
854
|
+
else
|
|
855
|
+
resolve({ behavior: 'deny', message: reason ?? 'The user did not approve this action.' });
|
|
856
|
+
// The next question in the queue, if any, is now the one to show.
|
|
857
|
+
if (queue.length > 0)
|
|
858
|
+
this.onDataAvailable?.(sessionId);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
const queue = this.pendingToolApprovals.get(sessionId) ?? [];
|
|
862
|
+
queue.push(entry);
|
|
863
|
+
this.pendingToolApprovals.set(sessionId, queue);
|
|
864
|
+
if (signal?.aborted)
|
|
865
|
+
onAbort();
|
|
866
|
+
else
|
|
867
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
868
|
+
this.onDataAvailable?.(sessionId);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Refuse every question still waiting for this session. Called when the
|
|
873
|
+
* turn is aborted or the session is destroyed, so no tool is left waiting
|
|
874
|
+
* on an answer that can no longer come — whether or not the SDK passed an
|
|
875
|
+
* abort signal with the question.
|
|
876
|
+
*/
|
|
877
|
+
denyPendingToolApprovals(sessionId, reason) {
|
|
878
|
+
for (const entry of [...(this.pendingToolApprovals.get(sessionId) ?? [])]) {
|
|
879
|
+
entry.settle(false, reason);
|
|
880
|
+
}
|
|
881
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
882
|
+
}
|
|
883
|
+
getPendingApproval(sessionId) {
|
|
884
|
+
const pending = this.pendingToolApprovals.get(sessionId)?.[0];
|
|
885
|
+
if (!pending)
|
|
886
|
+
return null;
|
|
887
|
+
return {
|
|
888
|
+
toolCallId: pending.toolCallId,
|
|
889
|
+
question: pending.question,
|
|
890
|
+
options: [
|
|
891
|
+
{ optionId: 'allow', name: 'Allow', kind: 'allow' },
|
|
892
|
+
{ optionId: 'deny', name: 'Deny', kind: 'reject' }
|
|
893
|
+
]
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async respondToApproval(sessionId, approved, optionId) {
|
|
897
|
+
const pending = this.pendingToolApprovals.get(sessionId)?.[0];
|
|
898
|
+
if (!pending)
|
|
899
|
+
return false;
|
|
900
|
+
// Only an explicit allow allows. A reply naming any other option denies.
|
|
901
|
+
pending.settle(approved && (optionId === undefined || optionId === 'allow'));
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
807
904
|
async getStatus(sessionId, _config) {
|
|
808
905
|
const session = this.sessions.get(sessionId);
|
|
809
906
|
if (!session) {
|
|
@@ -887,6 +984,7 @@ class ClaudeCodeAdapter {
|
|
|
887
984
|
session.enqueuePrompt = null;
|
|
888
985
|
session.backgroundTasks.clear();
|
|
889
986
|
session.abortController?.abort();
|
|
987
|
+
this.denyPendingToolApprovals(sessionId, 'The turn was stopped before this action was approved.');
|
|
890
988
|
// Wait for stream to finish cleanup
|
|
891
989
|
if (session.streamTask) {
|
|
892
990
|
console.log(`[ClaudeCodeAdapter] Waiting for stream cleanup...`);
|
|
@@ -912,6 +1010,7 @@ class ClaudeCodeAdapter {
|
|
|
912
1010
|
session.enqueuePrompt = null;
|
|
913
1011
|
session.backgroundTasks.clear();
|
|
914
1012
|
session.abortController?.abort();
|
|
1013
|
+
this.denyPendingToolApprovals(sessionId, 'The session was closed before this action was approved.');
|
|
915
1014
|
// Wait for stream task to complete
|
|
916
1015
|
if (session.streamTask) {
|
|
917
1016
|
try {
|
|
@@ -1185,7 +1284,20 @@ class ClaudeCodeAdapter {
|
|
|
1185
1284
|
const errName = error instanceof Error ? error.name : '';
|
|
1186
1285
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1187
1286
|
const errStack = error instanceof Error ? error.stack : undefined;
|
|
1188
|
-
|
|
1287
|
+
// CHANGED from 20x: the runner stops the session at the end of EVERY
|
|
1288
|
+
// turn (one process per turn), and the SDK reports that stop as a plain
|
|
1289
|
+
// `Error: Operation aborted`. That is the expected end of a finished
|
|
1290
|
+
// turn, not a failure: it used to be logged as "Stream error" with a
|
|
1291
|
+
// stack trace after every successful turn, and marked the session failed.
|
|
1292
|
+
// Both conditions: the adapter asked for the stop AND the error is the
|
|
1293
|
+
// SDK's own word for a stopped process. An unrelated failure that
|
|
1294
|
+
// happens to arrive after the stop is still reported below.
|
|
1295
|
+
const stoppedByUs = session.abortController?.signal.aborted === true &&
|
|
1296
|
+
/\babort(ed)?\b/i.test(errMsg);
|
|
1297
|
+
if (stoppedByUs) {
|
|
1298
|
+
console.log('[ClaudeCodeAdapter] Stream stopped at the end of the turn');
|
|
1299
|
+
}
|
|
1300
|
+
else if (errName === 'AbortError' || errMsg.includes('aborted by user')) {
|
|
1189
1301
|
console.log('[ClaudeCodeAdapter] Stream aborted by user');
|
|
1190
1302
|
// Don't set error status - this is normal when sending a new message
|
|
1191
1303
|
}
|
|
@@ -1221,6 +1333,9 @@ class ClaudeCodeAdapter {
|
|
|
1221
1333
|
}
|
|
1222
1334
|
finally {
|
|
1223
1335
|
console.log('[ClaudeCodeAdapter] Stream consumption ended');
|
|
1336
|
+
// ADDED, not in 20x: the stream that asked is gone (ended, failed or the
|
|
1337
|
+
// process exited), so no answer can reach it. Nothing may stay queued.
|
|
1338
|
+
this.denyPendingToolApprovals(sessionId, 'The turn ended before this action was approved.');
|
|
1224
1339
|
// Always reset queryIterator when the stream ends (process exited).
|
|
1225
1340
|
// Previously only error paths did this, leaving a truthy-but-exhausted
|
|
1226
1341
|
// iterator after normal completion. That caused the next sendPrompt to
|
|
@@ -6,6 +6,30 @@
|
|
|
6
6
|
* 20x's CodingAgentAdapter contract.
|
|
7
7
|
*/
|
|
8
8
|
import type { CodingAgentAdapter, SessionConfig, SessionStatus, SessionMessage, MessagePart } from './coding-agent-adapter';
|
|
9
|
+
/**
|
|
10
|
+
* `thread/items/list` returns each item inside an envelope:
|
|
11
|
+
* `{ turnId, item: { type: 'userMessage', … }, startedAtMs, completedAtMs }`.
|
|
12
|
+
* Read as an item, the envelope has no `type`, no role and no text, so it fell
|
|
13
|
+
* through to the "unknown item is a tool call" branch and the transcript showed
|
|
14
|
+
* a "tool / tool" card holding the echoed prompt as JSON after every reply.
|
|
15
|
+
*/
|
|
16
|
+
export declare function unwrapListedThreadItem(entry: unknown): unknown;
|
|
17
|
+
/** A decision that approves more than the one request it answers, or may. */
|
|
18
|
+
export declare function isStandingApprovalDecision(optionId: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* CHANGED from 20x, which sends `on-request` for every mode but "allow".
|
|
21
|
+
*
|
|
22
|
+
* `on-request` leaves it to the MODEL to ask, and it asks only when it wants
|
|
23
|
+
* to leave the sandbox. Inside a workspace-write sandbox that is never: Codex
|
|
24
|
+
* created files and ran shell commands with no prompt at all while the agent
|
|
25
|
+
* was set to "Ask before each action". Seen on staging and reproduced.
|
|
26
|
+
*
|
|
27
|
+
* `untrusted` is Codex's own "ask first" policy: commands on its short list of
|
|
28
|
+
* read-only ones (ls, cat, ...) run, and everything else — any other command,
|
|
29
|
+
* any file change — is sent to the client as an approval request before it
|
|
30
|
+
* runs. That is what "Ask before each action" says.
|
|
31
|
+
*/
|
|
32
|
+
export declare function codexApprovalPolicy(config: Pick<SessionConfig, 'permissionMode'>): 'never' | 'untrusted';
|
|
9
33
|
interface PendingApproval {
|
|
10
34
|
requestId: string | number;
|
|
11
35
|
toolCallId: string;
|
|
@@ -16,6 +40,8 @@ interface PendingApproval {
|
|
|
16
40
|
kind: string;
|
|
17
41
|
}>;
|
|
18
42
|
responseKind: 'execCommand' | 'commandExecution' | 'fileChange' | 'permissions' | 'elicitation' | 'userInput' | 'generic';
|
|
43
|
+
/** What `item/permissions/requestApproval` asked for; echoed back on allow. */
|
|
44
|
+
requestedPermissions?: unknown;
|
|
19
45
|
}
|
|
20
46
|
export declare class CodexAppServerAdapter implements CodingAgentAdapter {
|
|
21
47
|
private sessions;
|
|
@@ -41,6 +41,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
})();
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.CodexAppServerAdapter = void 0;
|
|
44
|
+
exports.unwrapListedThreadItem = unwrapListedThreadItem;
|
|
45
|
+
exports.isStandingApprovalDecision = isStandingApprovalDecision;
|
|
46
|
+
exports.codexApprovalPolicy = codexApprovalPolicy;
|
|
44
47
|
/**
|
|
45
48
|
* Extracted verbatim from 20x `src/main/adapters/codex-app-server-adapter.ts`.
|
|
46
49
|
* CHANGED: imports only — `../child-stream-guards` ->
|
|
@@ -54,7 +57,62 @@ const os_1 = require("os");
|
|
|
54
57
|
const path_1 = require("path");
|
|
55
58
|
const util_1 = require("util");
|
|
56
59
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
60
|
+
const codex_approval_text_1 = require("./codex-approval-text");
|
|
57
61
|
const DEFAULT_CODEX_APP_SERVER_MODEL = 'gpt-5.5';
|
|
62
|
+
/**
|
|
63
|
+
* `thread/items/list` returns each item inside an envelope:
|
|
64
|
+
* `{ turnId, item: { type: 'userMessage', … }, startedAtMs, completedAtMs }`.
|
|
65
|
+
* Read as an item, the envelope has no `type`, no role and no text, so it fell
|
|
66
|
+
* through to the "unknown item is a tool call" branch and the transcript showed
|
|
67
|
+
* a "tool / tool" card holding the echoed prompt as JSON after every reply.
|
|
68
|
+
*/
|
|
69
|
+
function unwrapListedThreadItem(entry) {
|
|
70
|
+
if (!isObject(entry))
|
|
71
|
+
return entry;
|
|
72
|
+
if (asString(entry.type))
|
|
73
|
+
return entry;
|
|
74
|
+
return isObject(entry.item) ? entry.item : entry;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The decisions that answer ONE request and nothing more. A list of what is
|
|
78
|
+
* known to be safe, not of what is known to be unsafe: a decision Codex adds
|
|
79
|
+
* later (one that remembers a rule under a name nobody here has seen) is
|
|
80
|
+
* treated as standing until someone adds it here. The cost of that: a new
|
|
81
|
+
* ONE-TIME decision from Codex is also hidden until it is added; the withheld
|
|
82
|
+
* names are logged by `handleApprovalRequest` so that is visible.
|
|
83
|
+
*/
|
|
84
|
+
const ONE_TIME_APPROVAL_DECISIONS = new Set([
|
|
85
|
+
'accept',
|
|
86
|
+
'approved',
|
|
87
|
+
'decline',
|
|
88
|
+
'denied',
|
|
89
|
+
'cancel',
|
|
90
|
+
'abort'
|
|
91
|
+
]);
|
|
92
|
+
/** A decision that approves more than the one request it answers, or may. */
|
|
93
|
+
function isStandingApprovalDecision(optionId) {
|
|
94
|
+
return !ONE_TIME_APPROVAL_DECISIONS.has(optionId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* CHANGED from 20x, which sends `on-request` for every mode but "allow".
|
|
98
|
+
*
|
|
99
|
+
* `on-request` leaves it to the MODEL to ask, and it asks only when it wants
|
|
100
|
+
* to leave the sandbox. Inside a workspace-write sandbox that is never: Codex
|
|
101
|
+
* created files and ran shell commands with no prompt at all while the agent
|
|
102
|
+
* was set to "Ask before each action". Seen on staging and reproduced.
|
|
103
|
+
*
|
|
104
|
+
* `untrusted` is Codex's own "ask first" policy: commands on its short list of
|
|
105
|
+
* read-only ones (ls, cat, ...) run, and everything else — any other command,
|
|
106
|
+
* any file change — is sent to the client as an approval request before it
|
|
107
|
+
* runs. That is what "Ask before each action" says.
|
|
108
|
+
*/
|
|
109
|
+
function codexApprovalPolicy(config) {
|
|
110
|
+
// `'allow'` is what `mapPermissionMode` makes of the agent's
|
|
111
|
+
// `bypassPermissions` ("Don't ask for permission"), and of nothing else.
|
|
112
|
+
// Only an explicit "allow" runs without asking. Anything else, including a
|
|
113
|
+
// value this build does not know, asks.
|
|
114
|
+
return config.permissionMode === 'allow' ? 'never' : 'untrusted';
|
|
115
|
+
}
|
|
58
116
|
const MAX_IPC_TOOL_INPUT_CHARS = 20_000;
|
|
59
117
|
const MAX_IPC_TOOL_OUTPUT_CHARS = 100_000;
|
|
60
118
|
const MIN_THREAD_LEVEL_ASSISTANT_DEDUPE_CHARS = 40;
|
|
@@ -220,12 +278,6 @@ function isSubpath(parent, child) {
|
|
|
220
278
|
const rel = (0, path_1.relative)((0, path_1.resolve)(parent), (0, path_1.resolve)(child));
|
|
221
279
|
return rel === '' || (!!rel && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel));
|
|
222
280
|
}
|
|
223
|
-
function summarizeApproval(params, fallback) {
|
|
224
|
-
const command = asString(params.command);
|
|
225
|
-
const reason = asString(params.reason);
|
|
226
|
-
const itemId = asString(params.itemId) || asString(params.callId);
|
|
227
|
-
return [command || fallback, reason, itemId ? `id: ${itemId}` : ''].filter(Boolean).join('\n');
|
|
228
|
-
}
|
|
229
281
|
function normalizeDecisionName(decision) {
|
|
230
282
|
if (typeof decision === 'string')
|
|
231
283
|
return decision;
|
|
@@ -234,24 +286,6 @@ function normalizeDecisionName(decision) {
|
|
|
234
286
|
}
|
|
235
287
|
return 'accept';
|
|
236
288
|
}
|
|
237
|
-
function decisionLabel(decision) {
|
|
238
|
-
switch (decision) {
|
|
239
|
-
case 'accept':
|
|
240
|
-
case 'approved':
|
|
241
|
-
return 'Allow';
|
|
242
|
-
case 'acceptForSession':
|
|
243
|
-
case 'approved_for_session':
|
|
244
|
-
return 'Allow for Session';
|
|
245
|
-
case 'decline':
|
|
246
|
-
case 'denied':
|
|
247
|
-
return 'Deny';
|
|
248
|
-
case 'cancel':
|
|
249
|
-
case 'abort':
|
|
250
|
-
return 'Deny and Stop';
|
|
251
|
-
default:
|
|
252
|
-
return decision;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
289
|
class CodexAppServerAdapter {
|
|
256
290
|
sessions = new Map();
|
|
257
291
|
codexExecutablePath = null;
|
|
@@ -269,7 +303,7 @@ class CodexAppServerAdapter {
|
|
|
269
303
|
const result = await this.sendRpcRequest(session, 'thread/start', {
|
|
270
304
|
cwd: config.workspaceDir,
|
|
271
305
|
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
272
|
-
approvalPolicy: config
|
|
306
|
+
approvalPolicy: codexApprovalPolicy(config),
|
|
273
307
|
approvalsReviewer: 'user',
|
|
274
308
|
sandbox: this.resolveSandboxMode(config),
|
|
275
309
|
developerInstructions: config.systemPrompt || null,
|
|
@@ -295,7 +329,7 @@ class CodexAppServerAdapter {
|
|
|
295
329
|
threadId: sessionId,
|
|
296
330
|
cwd: config.workspaceDir,
|
|
297
331
|
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
298
|
-
approvalPolicy: config
|
|
332
|
+
approvalPolicy: codexApprovalPolicy(config),
|
|
299
333
|
approvalsReviewer: 'user',
|
|
300
334
|
sandbox: this.resolveSandboxMode(config),
|
|
301
335
|
runtimeWorkspaceRoots: this.buildRuntimeWorkspaceRoots(config.workspaceDir),
|
|
@@ -347,7 +381,7 @@ class CodexAppServerAdapter {
|
|
|
347
381
|
cwd: config.workspaceDir,
|
|
348
382
|
model: config.model || DEFAULT_CODEX_APP_SERVER_MODEL,
|
|
349
383
|
effort: config.reasoningEffort && config.reasoningEffort !== 'max' ? config.reasoningEffort : null,
|
|
350
|
-
approvalPolicy: config
|
|
384
|
+
approvalPolicy: codexApprovalPolicy(config),
|
|
351
385
|
approvalsReviewer: 'user',
|
|
352
386
|
sandbox: this.resolveSandboxMode(config),
|
|
353
387
|
sandboxPolicy: this.buildSandboxPolicy(config),
|
|
@@ -483,14 +517,24 @@ class CodexAppServerAdapter {
|
|
|
483
517
|
if (!approval)
|
|
484
518
|
return;
|
|
485
519
|
const selected = optionId || approval.options.find((option) => approved
|
|
486
|
-
? ['
|
|
487
|
-
: ['
|
|
488
|
-
|
|
520
|
+
? ['accept', 'approved'].includes(option.optionId)
|
|
521
|
+
: ['decline', 'denied'].includes(option.optionId))?.optionId || (
|
|
522
|
+
// The older requests take `approved` / `denied`.
|
|
523
|
+
approval.responseKind === 'execCommand'
|
|
524
|
+
? (approved ? 'approved' : 'denied')
|
|
525
|
+
: (approved ? 'accept' : 'decline'));
|
|
526
|
+
const response = this.buildApprovalResponse(approval.responseKind, selected, approved, approval.requestedPermissions);
|
|
489
527
|
this.sendRpcResponse(session, approval.requestId, response);
|
|
490
528
|
session.pendingApproval = null;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
529
|
+
// CHANGED from 20x, which marked the session idle on every denial. A
|
|
530
|
+
// denial that continues the turn (`decline`, `denied`) is followed by the
|
|
531
|
+
// model's own answer; calling the session idle here ended the turn before
|
|
532
|
+
// that answer arrived. Only a denial that stops the turn is idle at once.
|
|
533
|
+
// (No button sends `cancel` or `abort` any more; a caller that names one
|
|
534
|
+
// explicitly still gets the stop it asked for.)
|
|
535
|
+
session.status = !approved && (selected === 'cancel' || selected === 'abort')
|
|
536
|
+
? coding_agent_adapter_1.SessionStatusType.IDLE
|
|
537
|
+
: coding_agent_adapter_1.SessionStatusType.BUSY;
|
|
494
538
|
}
|
|
495
539
|
async getRunningTools(sessionId, _config) {
|
|
496
540
|
return Array.from(this.sessions.get(sessionId)?.runningTools.values() || []);
|
|
@@ -822,44 +866,73 @@ class CodexAppServerAdapter {
|
|
|
822
866
|
if (session.config.permissionMode === 'allow') {
|
|
823
867
|
const responseKind = this.getApprovalResponseKind(request.method);
|
|
824
868
|
const selected = responseKind === 'execCommand' ? 'approved' : 'accept';
|
|
825
|
-
this.sendRpcResponse(session, request.id, this.buildApprovalResponse(responseKind, selected, true));
|
|
869
|
+
this.sendRpcResponse(session, request.id, this.buildApprovalResponse(responseKind, selected, true, params.permissions));
|
|
826
870
|
return;
|
|
827
871
|
}
|
|
828
872
|
const toolCallId = asString(params.approvalId) || asString(params.itemId) || asString(params.callId) || String(request.id);
|
|
829
873
|
const responseKind = this.getApprovalResponseKind(request.method);
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
874
|
+
// A decision that also writes a standing rule ("…WithExecpolicyAmendment",
|
|
875
|
+
// "…ForSession") would let one click approve later commands nobody saw, and
|
|
876
|
+
// the amendment is saved into the Codex config on the runner. Offer only
|
|
877
|
+
// the decisions known to answer this one request; the rest are logged.
|
|
878
|
+
const offered = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
|
|
879
|
+
const withheld = offered.map(normalizeDecisionName).filter(isStandingApprovalDecision);
|
|
880
|
+
if (withheld.length > 0) {
|
|
881
|
+
console.log(`[CodexAppServerAdapter] Not offering approval decisions: ${withheld.join(', ')}`);
|
|
882
|
+
}
|
|
883
|
+
/*
|
|
884
|
+
* CHANGED from 20x: ONE "Allow" AND ONE "Deny", AND DENY LETS THE TURN GO ON.
|
|
885
|
+
*
|
|
886
|
+
* Codex has two denials. `cancel` ("abort" in the older form) also
|
|
887
|
+
* interrupts the turn: the chat went idle with no word from the agent, as
|
|
888
|
+
* if nothing had been asked. `decline` ("denied") refuses the action and
|
|
889
|
+
* lets the model carry on, so it can say that it was not allowed and what
|
|
890
|
+
* it will do instead. Both are in the protocol for every request that has
|
|
891
|
+
* a decision, whether or not `availableDecisions` lists them, so "Deny" is
|
|
892
|
+
* always the one that continues.
|
|
893
|
+
*/
|
|
894
|
+
const legacy = responseKind === 'execCommand';
|
|
895
|
+
const approvalOptions = legacy
|
|
896
|
+
? [
|
|
897
|
+
{ optionId: 'approved', name: 'Allow', kind: 'allow' },
|
|
898
|
+
{ optionId: 'denied', name: 'Deny', kind: 'reject' }
|
|
899
|
+
]
|
|
840
900
|
: [
|
|
841
901
|
{ optionId: 'accept', name: 'Allow', kind: 'allow' },
|
|
842
|
-
{ optionId: '
|
|
902
|
+
{ optionId: 'decline', name: 'Deny', kind: 'reject' }
|
|
843
903
|
];
|
|
904
|
+
// The item the request is about, when it has been announced: a file
|
|
905
|
+
// change's paths are on the item, not on the request.
|
|
906
|
+
const itemId = asString(params.itemId) || asString(params.callId);
|
|
907
|
+
const item = itemId ? session.runningTools.get(`tool-${itemId}`)?.input : undefined;
|
|
844
908
|
session.pendingApproval = {
|
|
845
909
|
requestId: request.id,
|
|
846
910
|
toolCallId,
|
|
847
|
-
question:
|
|
911
|
+
question: (0, codex_approval_text_1.describeCodexApproval)(request.method, params, item, session.config.workspaceDir),
|
|
848
912
|
options: approvalOptions,
|
|
849
|
-
responseKind
|
|
913
|
+
responseKind,
|
|
914
|
+
...(responseKind === 'permissions' ? { requestedPermissions: params.permissions } : {})
|
|
850
915
|
};
|
|
851
916
|
session.status = coding_agent_adapter_1.SessionStatusType.WAITING_APPROVAL;
|
|
852
917
|
this.onDataAvailable?.(session.threadId || session.sessionId);
|
|
853
918
|
}
|
|
854
|
-
buildApprovalResponse(responseKind, selected, approved) {
|
|
919
|
+
buildApprovalResponse(responseKind, selected, approved, requestedPermissions) {
|
|
855
920
|
switch (responseKind) {
|
|
856
921
|
case 'execCommand':
|
|
857
|
-
|
|
922
|
+
// The older requests (`execCommandApproval`, `applyPatchApproval`)
|
|
923
|
+
// take a ReviewDecision. `denied` continues the turn; `abort` stops it.
|
|
924
|
+
return { decision: approved ? 'approved' : (selected === 'abort' ? 'abort' : 'denied') };
|
|
858
925
|
case 'commandExecution':
|
|
859
|
-
return { decision: selected };
|
|
860
926
|
case 'fileChange':
|
|
927
|
+
return { decision: approved ? 'accept' : (selected === 'cancel' ? 'cancel' : 'decline') };
|
|
861
928
|
case 'permissions':
|
|
862
|
-
|
|
929
|
+
// CHANGED from 20x, which answered `{ decision }`. This request has no
|
|
930
|
+
// decision: the answer is the permissions granted, for this turn only.
|
|
931
|
+
// Granting nothing is the denial.
|
|
932
|
+
return {
|
|
933
|
+
permissions: approved && isObject(requestedPermissions) ? requestedPermissions : {},
|
|
934
|
+
scope: 'turn'
|
|
935
|
+
};
|
|
863
936
|
case 'elicitation':
|
|
864
937
|
return approved
|
|
865
938
|
? { action: 'accept', content: {} }
|
|
@@ -869,11 +942,11 @@ class CodexAppServerAdapter {
|
|
|
869
942
|
? { response: selected }
|
|
870
943
|
: { response: null };
|
|
871
944
|
default:
|
|
872
|
-
return { decision: approved ?
|
|
945
|
+
return { decision: approved ? 'accept' : 'decline' };
|
|
873
946
|
}
|
|
874
947
|
}
|
|
875
948
|
getApprovalResponseKind(method) {
|
|
876
|
-
if (method === 'execCommandApproval')
|
|
949
|
+
if (method === 'execCommandApproval' || method === 'applyPatchApproval')
|
|
877
950
|
return 'execCommand';
|
|
878
951
|
if (method.includes('commandExecution'))
|
|
879
952
|
return 'commandExecution';
|
|
@@ -922,8 +995,9 @@ class CodexAppServerAdapter {
|
|
|
922
995
|
}
|
|
923
996
|
bufferThreadItems(session, result) {
|
|
924
997
|
const items = isObject(result) && Array.isArray(result.data) ? result.data : [];
|
|
925
|
-
for (const
|
|
926
|
-
|
|
998
|
+
for (const entry of items) {
|
|
999
|
+
const turnId = isObject(entry) ? asString(entry.turnId) : undefined;
|
|
1000
|
+
this.bufferReconciledThreadItem(session, unwrapListedThreadItem(entry), turnId);
|
|
927
1001
|
}
|
|
928
1002
|
}
|
|
929
1003
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person is shown when `codex app-server` asks for an approval.
|
|
3
|
+
*
|
|
4
|
+
* The card used to show the protocol itself: `item/fileChange/requestApproval`
|
|
5
|
+
* and `id: exec-…`, with the file name only in the item above it. This module
|
|
6
|
+
* turns each kind of request the app-server can send into a first line that
|
|
7
|
+
* says what will happen ("Write: src/a.ts", "Run: npm test") and further lines
|
|
8
|
+
* with what helps to decide (where, why, which files).
|
|
9
|
+
*
|
|
10
|
+
* The first line is the title; the control plane's card shows it on its own.
|
|
11
|
+
* No ids: nobody can decide anything from one.
|
|
12
|
+
*
|
|
13
|
+
* Request kinds, from `codex app-server generate-json-schema` (ServerRequest):
|
|
14
|
+
* item/commandExecution/requestApproval a command, or input to a terminal
|
|
15
|
+
* item/fileChange/requestApproval a patch; the files are on the item
|
|
16
|
+
* item/permissions/requestApproval wider sandbox access for the turn
|
|
17
|
+
* mcpServer/elicitation/request an MCP server asks (incl. to run
|
|
18
|
+
* one of its tools)
|
|
19
|
+
* item/tool/requestUserInput the model asks a question
|
|
20
|
+
* execCommandApproval, applyPatchApproval the older forms of the first two
|
|
21
|
+
* Kept out of the adapter, which is a copy of 20x's, so it can be tested alone.
|
|
22
|
+
*/
|
|
23
|
+
type Json = Record<string, unknown>;
|
|
24
|
+
interface FileChange {
|
|
25
|
+
path: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
added: number;
|
|
28
|
+
removed: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The files of a change, from either shape Codex uses:
|
|
32
|
+
* v2 item `changes: [{ path, kind: { type }, diff }]`
|
|
33
|
+
* older request `fileChanges: { [path]: { add | delete | update: {…} } }`
|
|
34
|
+
*/
|
|
35
|
+
export declare function readFileChanges(source: unknown): FileChange[];
|
|
36
|
+
/**
|
|
37
|
+
* The text of one approval request.
|
|
38
|
+
*
|
|
39
|
+
* @param item the thread item the request is about, when the adapter has seen
|
|
40
|
+
* it (`item/started` arrives before the request). A file change's paths are
|
|
41
|
+
* only there.
|
|
42
|
+
*/
|
|
43
|
+
export declare function describeCodexApproval(method: string, params: Json, item?: Json,
|
|
44
|
+
/** The session's workspace; paths inside it are shown relative to it. */
|
|
45
|
+
workspaceDir?: string): string;
|
|
46
|
+
export {};
|