@canonmsg/codex-plugin 0.5.0 → 0.6.1
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/adapter.d.ts +1 -1
- package/dist/adapter.js +9 -3
- package/dist/host-runtime.d.ts +20 -4
- package/dist/host-runtime.js +36 -29
- package/dist/host.js +78 -15
- package/dist/permission-mode.d.ts +31 -0
- package/dist/permission-mode.js +59 -0
- package/package.json +3 -2
package/dist/adapter.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class CodexConversationAdapter {
|
|
|
63
63
|
setModel(model: string | null): void;
|
|
64
64
|
isRunning(): boolean;
|
|
65
65
|
interrupt(): Promise<void>;
|
|
66
|
-
runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void): Promise<CodexTurnResult>;
|
|
66
|
+
runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[]): Promise<CodexTurnResult>;
|
|
67
67
|
private buildArgs;
|
|
68
68
|
private clearActiveProcess;
|
|
69
69
|
}
|
package/dist/adapter.js
CHANGED
|
@@ -47,11 +47,11 @@ export class CodexConversationAdapter {
|
|
|
47
47
|
this.child.kill('SIGKILL');
|
|
48
48
|
}, 5_000);
|
|
49
49
|
}
|
|
50
|
-
async runTurn(prompt, onEvent, onLog) {
|
|
50
|
+
async runTurn(prompt, onEvent, onLog, imagePaths = []) {
|
|
51
51
|
if (this.child) {
|
|
52
52
|
throw new Error('A Codex turn is already in progress for this conversation');
|
|
53
53
|
}
|
|
54
|
-
const args = this.buildArgs(prompt);
|
|
54
|
+
const args = this.buildArgs(prompt, imagePaths);
|
|
55
55
|
const child = spawn(this.codexBin, args, {
|
|
56
56
|
cwd: this.cwd,
|
|
57
57
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -141,7 +141,7 @@ export class CodexConversationAdapter {
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
-
buildArgs(prompt) {
|
|
144
|
+
buildArgs(prompt, imagePaths = []) {
|
|
145
145
|
if (this.threadId) {
|
|
146
146
|
const args = ['exec', 'resume', '--json', '--skip-git-repo-check'];
|
|
147
147
|
if (this.model) {
|
|
@@ -159,6 +159,9 @@ export class CodexConversationAdapter {
|
|
|
159
159
|
if (this.bypassApprovalsAndSandbox) {
|
|
160
160
|
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
161
161
|
}
|
|
162
|
+
for (const imagePath of imagePaths) {
|
|
163
|
+
args.push('-i', imagePath);
|
|
164
|
+
}
|
|
162
165
|
args.push(this.threadId, prompt);
|
|
163
166
|
return args;
|
|
164
167
|
}
|
|
@@ -190,6 +193,9 @@ export class CodexConversationAdapter {
|
|
|
190
193
|
if (execMode.bypassApprovalsAndSandbox) {
|
|
191
194
|
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
192
195
|
}
|
|
196
|
+
for (const imagePath of imagePaths) {
|
|
197
|
+
args.push('-i', imagePath);
|
|
198
|
+
}
|
|
193
199
|
args.push(prompt);
|
|
194
200
|
return args;
|
|
195
201
|
}
|
package/dist/host-runtime.d.ts
CHANGED
|
@@ -28,9 +28,6 @@ export interface HostInboundParticipantContext {
|
|
|
28
28
|
type HostInboundMessage = {
|
|
29
29
|
text?: string | null;
|
|
30
30
|
contentType?: CanonMessage['contentType'] | null;
|
|
31
|
-
audioUrl?: string | null;
|
|
32
|
-
audioDurationMs?: number | null;
|
|
33
|
-
imageUrl?: string | null;
|
|
34
31
|
attachments?: CanonMessage['attachments'];
|
|
35
32
|
senderType?: CanonMessage['senderType'];
|
|
36
33
|
mentions?: string[] | null;
|
|
@@ -49,7 +46,26 @@ export declare function buildCanonHostPrompt(input: {
|
|
|
49
46
|
workSessions?: MessageCreatedPayload['workSessions'];
|
|
50
47
|
buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
|
|
51
48
|
}): string;
|
|
52
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Render the **text portion** of an inbound Canon message. Images are
|
|
51
|
+
* referenced by short placeholders — their actual bytes are delivered to the
|
|
52
|
+
* host as native vision/media inputs (Codex `-i <file>`, Anthropic image
|
|
53
|
+
* blocks). URLs are intentionally *not* inlined, since the harness never
|
|
54
|
+
* needs to refetch and earlier `[Image: <url>]` inlining caused vision
|
|
55
|
+
* models to see a string about an image instead of the image itself.
|
|
56
|
+
*
|
|
57
|
+
* `materialized` may be passed so non-image attachments can reference a
|
|
58
|
+
* local path the agent can Read. Without it we fall back to an unadorned
|
|
59
|
+
* placeholder; the vision path still works because image args carry the
|
|
60
|
+
* file path directly.
|
|
61
|
+
*/
|
|
62
|
+
export declare function renderCanonHostInboundContent(message: HostInboundMessage, materialized?: ReadonlyArray<{
|
|
63
|
+
kind: 'image' | 'audio' | 'file';
|
|
64
|
+
path: string;
|
|
65
|
+
fileName?: string;
|
|
66
|
+
durationMs?: number;
|
|
67
|
+
index: number;
|
|
68
|
+
}>): string;
|
|
53
69
|
export declare function buildHydratedInboundContext(input: {
|
|
54
70
|
agentId: string;
|
|
55
71
|
conversation: CanonConversation | null;
|
package/dist/host-runtime.js
CHANGED
|
@@ -32,38 +32,45 @@ export function buildCanonHostPrompt(input) {
|
|
|
32
32
|
input.content,
|
|
33
33
|
].join('\n');
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Render the **text portion** of an inbound Canon message. Images are
|
|
37
|
+
* referenced by short placeholders — their actual bytes are delivered to the
|
|
38
|
+
* host as native vision/media inputs (Codex `-i <file>`, Anthropic image
|
|
39
|
+
* blocks). URLs are intentionally *not* inlined, since the harness never
|
|
40
|
+
* needs to refetch and earlier `[Image: <url>]` inlining caused vision
|
|
41
|
+
* models to see a string about an image instead of the image itself.
|
|
42
|
+
*
|
|
43
|
+
* `materialized` may be passed so non-image attachments can reference a
|
|
44
|
+
* local path the agent can Read. Without it we fall back to an unadorned
|
|
45
|
+
* placeholder; the vision path still works because image args carry the
|
|
46
|
+
* file path directly.
|
|
47
|
+
*/
|
|
48
|
+
export function renderCanonHostInboundContent(message, materialized) {
|
|
49
|
+
const body = message.text || '';
|
|
50
|
+
const placeholders = [];
|
|
51
|
+
const attachments = message.attachments ?? [];
|
|
52
|
+
for (let i = 0; i < attachments.length; i += 1) {
|
|
53
|
+
const att = attachments[i];
|
|
54
|
+
const mat = materialized?.find((m) => m.index === i) ?? null;
|
|
55
|
+
placeholders.push(describeAttachment(att, mat));
|
|
54
56
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const rendered = [...placeholders, body].filter(Boolean).join('\n');
|
|
58
|
+
return rendered || '[Empty message]';
|
|
59
|
+
}
|
|
60
|
+
function describeAttachment(attachment, materialized) {
|
|
61
|
+
if (attachment.kind === 'image') {
|
|
62
|
+
return '[Image attached]';
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (attachment.kind === 'audio') {
|
|
65
|
+
const durationMs = materialized?.durationMs ?? attachment.durationMs;
|
|
66
|
+
const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
|
|
67
|
+
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
68
|
+
return `[Voice message${duration}${ref}]`;
|
|
65
69
|
}
|
|
66
|
-
|
|
70
|
+
// file
|
|
71
|
+
const label = materialized?.fileName ?? attachment.fileName ?? 'File';
|
|
72
|
+
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
73
|
+
return `[File: ${label}${ref}]`;
|
|
67
74
|
}
|
|
68
75
|
export function buildHydratedInboundContext(input) {
|
|
69
76
|
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
package/dist/host.js
CHANGED
|
@@ -3,11 +3,13 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
3
3
|
setDefaultResultOrder('ipv4first');
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
|
|
7
|
+
import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, isEnabledFlag, CanonClient, CanonStream, clearSessionState, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfile, initRTDBAuth, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, releaseLock, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
|
|
7
8
|
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
8
9
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
9
10
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
10
11
|
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
12
|
+
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
11
13
|
const MAX_SESSIONS = 12;
|
|
12
14
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
13
15
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -41,7 +43,19 @@ async function publishAgentRuntime(agentId, runtime) {
|
|
|
41
43
|
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
42
44
|
}
|
|
43
45
|
async function loadSessionConfig(conversationId, agentId) {
|
|
44
|
-
return loadHostSessionConfig({
|
|
46
|
+
return loadHostSessionConfig({
|
|
47
|
+
conversationId,
|
|
48
|
+
agentId,
|
|
49
|
+
extraStringFields: ['permissionMode'],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const SESSION_EXECUTION_MODE_REQUIRED = 'Session execution mode required; please select a mode before starting the session.';
|
|
53
|
+
function requireSessionExecutionMode(config) {
|
|
54
|
+
const mode = config?.executionMode;
|
|
55
|
+
if (!mode) {
|
|
56
|
+
throw new ExecutionEnvironmentError(SESSION_EXECUTION_MODE_REQUIRED, SESSION_EXECUTION_MODE_REQUIRED);
|
|
57
|
+
}
|
|
58
|
+
return mode;
|
|
45
59
|
}
|
|
46
60
|
function resolveWorkspaceCwd(config) {
|
|
47
61
|
return resolveHostWorkspaceCwd({
|
|
@@ -57,8 +71,8 @@ function buildCanonPrompt(input) {
|
|
|
57
71
|
...input,
|
|
58
72
|
});
|
|
59
73
|
}
|
|
60
|
-
function renderInboundContent(message) {
|
|
61
|
-
return renderCanonHostInboundContent(message);
|
|
74
|
+
function renderInboundContent(message, materialized) {
|
|
75
|
+
return renderCanonHostInboundContent(message, materialized);
|
|
62
76
|
}
|
|
63
77
|
function summarizeCommand(command) {
|
|
64
78
|
const trimmed = command.trim();
|
|
@@ -241,17 +255,29 @@ async function main() {
|
|
|
241
255
|
}
|
|
242
256
|
const creation = (async () => {
|
|
243
257
|
const config = await loadSessionConfig(conversationId, agentId);
|
|
258
|
+
const sessionExecutionMode = requireSessionExecutionMode(config);
|
|
259
|
+
if (sessionExecutionMode === 'worktree' && !allowWorktrees) {
|
|
260
|
+
throw new ExecutionEnvironmentError('This host does not allow worktree sessions (launched without --enable-worktrees).', 'This Canon host was started without worktree isolation enabled. Choose "Lock the workspace" or restart the host with --enable-worktrees.');
|
|
261
|
+
}
|
|
244
262
|
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
245
263
|
const environment = prepareConversationEnvironment({
|
|
246
264
|
agentId,
|
|
247
265
|
conversationId,
|
|
248
266
|
workspaceCwd,
|
|
249
|
-
allowWorktrees,
|
|
267
|
+
allowWorktrees: sessionExecutionMode === 'worktree',
|
|
250
268
|
});
|
|
251
269
|
try {
|
|
252
270
|
const sessionCwd = environment.cwd;
|
|
253
271
|
const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
|
|
254
272
|
const storedThreadId = loadStoredThreadId(agentId, conversationId, sessionCwd);
|
|
273
|
+
if (config?.permissionMode
|
|
274
|
+
&& !codexPermissionEnvelope.availablePermissionModes.some((option) => option.value === config.permissionMode)) {
|
|
275
|
+
throw new ExecutionEnvironmentError(`Permission mode "${config.permissionMode}" is not supported by this Codex host.`, 'This Canon host was started with stricter approval settings. Choose one of the advertised permission modes or restart the host with more permissive flags.');
|
|
276
|
+
}
|
|
277
|
+
const approvalOverride = mapCanonPermissionToCodex(config?.permissionMode);
|
|
278
|
+
const defaultSandbox = (typeof args.sandbox === 'string' ? args.sandbox : null);
|
|
279
|
+
const defaultFullAuto = Boolean(args['full-auto']);
|
|
280
|
+
const defaultBypass = Boolean(args['dangerously-bypass-approvals-and-sandbox']);
|
|
255
281
|
const session = {
|
|
256
282
|
conversationId,
|
|
257
283
|
cwd: sessionCwd,
|
|
@@ -261,15 +287,17 @@ async function main() {
|
|
|
261
287
|
threadId: storedThreadId,
|
|
262
288
|
codexBin: typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex',
|
|
263
289
|
model: sessionModel ?? null,
|
|
264
|
-
sandbox:
|
|
290
|
+
sandbox: approvalOverride ? approvalOverride.sandbox : defaultSandbox,
|
|
265
291
|
approvalPolicy: (typeof args['ask-for-approval'] === 'string'
|
|
266
292
|
? args['ask-for-approval']
|
|
267
293
|
: null),
|
|
268
294
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
269
295
|
addDirs: args['add-dir'] ?? [],
|
|
270
296
|
configOverrides: args.config ?? [],
|
|
271
|
-
fullAuto:
|
|
272
|
-
bypassApprovalsAndSandbox:
|
|
297
|
+
fullAuto: approvalOverride ? approvalOverride.fullAuto : defaultFullAuto,
|
|
298
|
+
bypassApprovalsAndSandbox: approvalOverride
|
|
299
|
+
? approvalOverride.bypassApprovalsAndSandbox
|
|
300
|
+
: defaultBypass,
|
|
273
301
|
}),
|
|
274
302
|
queue: [],
|
|
275
303
|
running: false,
|
|
@@ -303,8 +331,8 @@ async function main() {
|
|
|
303
331
|
pendingSessionCreations.delete(conversationId);
|
|
304
332
|
}
|
|
305
333
|
}
|
|
306
|
-
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false) {
|
|
307
|
-
const nextPrompt = { prompt, intent, sourceMessageId, markAccepted };
|
|
334
|
+
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = []) {
|
|
335
|
+
const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths };
|
|
308
336
|
if (toFront) {
|
|
309
337
|
session.queue.unshift(nextPrompt);
|
|
310
338
|
}
|
|
@@ -316,7 +344,22 @@ async function main() {
|
|
|
316
344
|
void runNextTurn(session);
|
|
317
345
|
}
|
|
318
346
|
async function enqueueInboundMessage(input) {
|
|
319
|
-
|
|
347
|
+
let materialized = [];
|
|
348
|
+
if (input.message.id) {
|
|
349
|
+
try {
|
|
350
|
+
materialized = await materializeMessageMedia({
|
|
351
|
+
id: input.message.id,
|
|
352
|
+
attachments: input.message.attachments ?? [],
|
|
353
|
+
}, { agentId, conversationId: input.conversationId });
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const imagePaths = materialized
|
|
360
|
+
.map((attachment) => getCodexImagePath(attachment))
|
|
361
|
+
.filter((path) => path !== null);
|
|
362
|
+
const content = renderInboundContent(input.message, materialized);
|
|
320
363
|
const hydrated = await loadHydratedInboundContext({
|
|
321
364
|
conversationId: input.conversationId,
|
|
322
365
|
message: input.message,
|
|
@@ -360,14 +403,14 @@ async function main() {
|
|
|
360
403
|
workSessions,
|
|
361
404
|
});
|
|
362
405
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
363
|
-
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted);
|
|
406
|
+
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths);
|
|
364
407
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
365
408
|
await session.adapter.interrupt().catch(() => { });
|
|
366
409
|
clearStreaming(input.conversationId);
|
|
367
410
|
client.setTyping(input.conversationId, false).catch(() => { });
|
|
368
411
|
return;
|
|
369
412
|
}
|
|
370
|
-
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted);
|
|
413
|
+
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths);
|
|
371
414
|
}
|
|
372
415
|
async function runNextTurn(session) {
|
|
373
416
|
if (session.running || session.closed)
|
|
@@ -393,6 +436,7 @@ async function main() {
|
|
|
393
436
|
updatedAt: { '.sv': 'timestamp' },
|
|
394
437
|
}).catch(() => { });
|
|
395
438
|
try {
|
|
439
|
+
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
396
440
|
const result = await session.adapter.runTurn(nextTurn.prompt, (event) => {
|
|
397
441
|
session.lastActivity = Date.now();
|
|
398
442
|
if (event.type === 'thread.started') {
|
|
@@ -426,7 +470,7 @@ async function main() {
|
|
|
426
470
|
}
|
|
427
471
|
}, (line) => {
|
|
428
472
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
|
|
429
|
-
});
|
|
473
|
+
}, turnImagePaths);
|
|
430
474
|
if (result.threadId) {
|
|
431
475
|
saveStoredThreadId(agentId, session.conversationId, session.cwd, result.threadId);
|
|
432
476
|
}
|
|
@@ -503,10 +547,19 @@ async function main() {
|
|
|
503
547
|
}
|
|
504
548
|
let controlStopped = false;
|
|
505
549
|
let streamConnected = false;
|
|
550
|
+
const hostAvailableExecutionModes = allowWorktrees
|
|
551
|
+
? [...EXECUTION_ENVIRONMENT_MODES]
|
|
552
|
+
: ['locked'];
|
|
553
|
+
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
506
554
|
let runtimeDescriptor = {
|
|
507
555
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
508
556
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
509
557
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
558
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
559
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
560
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
561
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
562
|
+
: {}),
|
|
510
563
|
};
|
|
511
564
|
const publishRuntimeHeartbeat = async () => {
|
|
512
565
|
if (!streamConnected)
|
|
@@ -550,12 +603,22 @@ async function main() {
|
|
|
550
603
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
551
604
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
552
605
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
606
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
607
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
608
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
609
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
610
|
+
: {}),
|
|
553
611
|
};
|
|
554
612
|
}
|
|
555
613
|
catch {
|
|
556
614
|
runtimeDescriptor = {
|
|
557
615
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
558
616
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
617
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
618
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
619
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
620
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
621
|
+
: {}),
|
|
559
622
|
};
|
|
560
623
|
}
|
|
561
624
|
try {
|
|
@@ -623,7 +686,7 @@ async function main() {
|
|
|
623
686
|
writeState(session);
|
|
624
687
|
}
|
|
625
688
|
if (control.permissionMode) {
|
|
626
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}]
|
|
689
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] approval mode is session-creation-only; ignoring mid-session change request (${control.permissionMode})`);
|
|
627
690
|
}
|
|
628
691
|
if (control.effort) {
|
|
629
692
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] effort control is not mapped yet (${control.effort})`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CodexSandboxMode } from './adapter.js';
|
|
2
|
+
export declare const CODEX_PERMISSION_OPTIONS: readonly [{
|
|
3
|
+
readonly value: "readonly";
|
|
4
|
+
readonly label: "Read-only";
|
|
5
|
+
}, {
|
|
6
|
+
readonly value: "workspace";
|
|
7
|
+
readonly label: "Workspace-write";
|
|
8
|
+
}, {
|
|
9
|
+
readonly value: "full-auto";
|
|
10
|
+
readonly label: "Full auto";
|
|
11
|
+
}, {
|
|
12
|
+
readonly value: "bypass";
|
|
13
|
+
readonly label: "Bypass (dangerous)";
|
|
14
|
+
}];
|
|
15
|
+
export type CodexPermissionMode = (typeof CODEX_PERMISSION_OPTIONS)[number]['value'];
|
|
16
|
+
export type CodexApprovalShape = {
|
|
17
|
+
sandbox: CodexSandboxMode | null;
|
|
18
|
+
fullAuto: boolean;
|
|
19
|
+
bypassApprovalsAndSandbox: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type CodexPermissionEnvelope = {
|
|
22
|
+
defaultPermissionMode?: CodexPermissionMode;
|
|
23
|
+
availablePermissionModes: ReadonlyArray<(typeof CODEX_PERMISSION_OPTIONS)[number]>;
|
|
24
|
+
};
|
|
25
|
+
export declare function mapCanonPermissionToCodex(mode: string | null | undefined): CodexApprovalShape | null;
|
|
26
|
+
export declare function deriveCodexPermissionEnvelope(args: {
|
|
27
|
+
'full-auto'?: unknown;
|
|
28
|
+
'dangerously-bypass-approvals-and-sandbox'?: unknown;
|
|
29
|
+
'ask-for-approval'?: unknown;
|
|
30
|
+
sandbox?: unknown;
|
|
31
|
+
}): CodexPermissionEnvelope;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const CODEX_PERMISSION_OPTIONS = [
|
|
2
|
+
{ value: 'readonly', label: 'Read-only' },
|
|
3
|
+
{ value: 'workspace', label: 'Workspace-write' },
|
|
4
|
+
{ value: 'full-auto', label: 'Full auto' },
|
|
5
|
+
{ value: 'bypass', label: 'Bypass (dangerous)' },
|
|
6
|
+
];
|
|
7
|
+
export function mapCanonPermissionToCodex(mode) {
|
|
8
|
+
switch (mode) {
|
|
9
|
+
case 'readonly':
|
|
10
|
+
return { sandbox: 'read-only', fullAuto: false, bypassApprovalsAndSandbox: false };
|
|
11
|
+
case 'workspace':
|
|
12
|
+
return { sandbox: 'workspace-write', fullAuto: false, bypassApprovalsAndSandbox: false };
|
|
13
|
+
case 'full-auto':
|
|
14
|
+
return { sandbox: 'workspace-write', fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
15
|
+
case 'bypass':
|
|
16
|
+
return { sandbox: null, fullAuto: false, bypassApprovalsAndSandbox: true };
|
|
17
|
+
default:
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function codexOptionsThrough(mode) {
|
|
22
|
+
const index = CODEX_PERMISSION_OPTIONS.findIndex((option) => option.value === mode);
|
|
23
|
+
return index >= 0 ? CODEX_PERMISSION_OPTIONS.slice(0, index + 1) : [];
|
|
24
|
+
}
|
|
25
|
+
function isLegacyFullAuto(args) {
|
|
26
|
+
return args['ask-for-approval'] === 'never'
|
|
27
|
+
&& (args.sandbox === 'workspace-write' || args.sandbox == null);
|
|
28
|
+
}
|
|
29
|
+
export function deriveCodexPermissionEnvelope(args) {
|
|
30
|
+
if (args.sandbox === 'read-only') {
|
|
31
|
+
return {
|
|
32
|
+
defaultPermissionMode: 'readonly',
|
|
33
|
+
availablePermissionModes: codexOptionsThrough('readonly'),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (args.sandbox === 'danger-full-access') {
|
|
37
|
+
return {
|
|
38
|
+
availablePermissionModes: args['dangerously-bypass-approvals-and-sandbox']
|
|
39
|
+
? [...CODEX_PERMISSION_OPTIONS]
|
|
40
|
+
: codexOptionsThrough('full-auto'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (args['dangerously-bypass-approvals-and-sandbox']) {
|
|
44
|
+
return {
|
|
45
|
+
defaultPermissionMode: 'bypass',
|
|
46
|
+
availablePermissionModes: [...CODEX_PERMISSION_OPTIONS],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (args['full-auto'] || isLegacyFullAuto(args)) {
|
|
50
|
+
return {
|
|
51
|
+
defaultPermissionMode: 'full-auto',
|
|
52
|
+
availablePermissionModes: codexOptionsThrough('full-auto'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
defaultPermissionMode: 'workspace',
|
|
57
|
+
availablePermissionModes: codexOptionsThrough('workspace'),
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/host.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"prepack": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@canonmsg/
|
|
25
|
+
"@canonmsg/agent-sdk": "^0.8.1",
|
|
26
|
+
"@canonmsg/core": "^0.7.1"
|
|
26
27
|
},
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=18.0.0"
|