@canonmsg/codex-plugin 0.6.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/host-runtime.d.ts +0 -3
- package/dist/host-runtime.js +4 -15
- package/dist/host.js +34 -9
- package/dist/permission-mode.d.ts +31 -0
- package/dist/permission-mode.js +59 -0
- package/package.json +3 -3
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;
|
package/dist/host-runtime.js
CHANGED
|
@@ -49,21 +49,10 @@ export function renderCanonHostInboundContent(message, materialized) {
|
|
|
49
49
|
const body = message.text || '';
|
|
50
50
|
const placeholders = [];
|
|
51
51
|
const attachments = message.attachments ?? [];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
placeholders.push(describeAttachment(att, mat));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else if (message.contentType === 'audio' && message.audioUrl) {
|
|
60
|
-
const duration = message.audioDurationMs
|
|
61
|
-
? ` (${Math.round(message.audioDurationMs / 1000)}s)`
|
|
62
|
-
: '';
|
|
63
|
-
placeholders.push(`[Voice message${duration}]`);
|
|
64
|
-
}
|
|
65
|
-
else if (message.contentType === 'image' && message.imageUrl) {
|
|
66
|
-
placeholders.push('[Image attached]');
|
|
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));
|
|
67
56
|
}
|
|
68
57
|
const rendered = [...placeholders, body].filter(Boolean).join('\n');
|
|
69
58
|
return rendered || '[Empty message]';
|
package/dist/host.js
CHANGED
|
@@ -9,6 +9,7 @@ import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMe
|
|
|
9
9
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
10
10
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
11
11
|
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
12
|
+
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
12
13
|
const MAX_SESSIONS = 12;
|
|
13
14
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14
15
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -42,7 +43,11 @@ async function publishAgentRuntime(agentId, runtime) {
|
|
|
42
43
|
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
43
44
|
}
|
|
44
45
|
async function loadSessionConfig(conversationId, agentId) {
|
|
45
|
-
return loadHostSessionConfig({
|
|
46
|
+
return loadHostSessionConfig({
|
|
47
|
+
conversationId,
|
|
48
|
+
agentId,
|
|
49
|
+
extraStringFields: ['permissionMode'],
|
|
50
|
+
});
|
|
46
51
|
}
|
|
47
52
|
const SESSION_EXECUTION_MODE_REQUIRED = 'Session execution mode required; please select a mode before starting the session.';
|
|
48
53
|
function requireSessionExecutionMode(config) {
|
|
@@ -265,6 +270,14 @@ async function main() {
|
|
|
265
270
|
const sessionCwd = environment.cwd;
|
|
266
271
|
const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
|
|
267
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']);
|
|
268
281
|
const session = {
|
|
269
282
|
conversationId,
|
|
270
283
|
cwd: sessionCwd,
|
|
@@ -274,15 +287,17 @@ async function main() {
|
|
|
274
287
|
threadId: storedThreadId,
|
|
275
288
|
codexBin: typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex',
|
|
276
289
|
model: sessionModel ?? null,
|
|
277
|
-
sandbox:
|
|
290
|
+
sandbox: approvalOverride ? approvalOverride.sandbox : defaultSandbox,
|
|
278
291
|
approvalPolicy: (typeof args['ask-for-approval'] === 'string'
|
|
279
292
|
? args['ask-for-approval']
|
|
280
293
|
: null),
|
|
281
294
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
282
295
|
addDirs: args['add-dir'] ?? [],
|
|
283
296
|
configOverrides: args.config ?? [],
|
|
284
|
-
fullAuto:
|
|
285
|
-
bypassApprovalsAndSandbox:
|
|
297
|
+
fullAuto: approvalOverride ? approvalOverride.fullAuto : defaultFullAuto,
|
|
298
|
+
bypassApprovalsAndSandbox: approvalOverride
|
|
299
|
+
? approvalOverride.bypassApprovalsAndSandbox
|
|
300
|
+
: defaultBypass,
|
|
286
301
|
}),
|
|
287
302
|
queue: [],
|
|
288
303
|
running: false,
|
|
@@ -334,10 +349,7 @@ async function main() {
|
|
|
334
349
|
try {
|
|
335
350
|
materialized = await materializeMessageMedia({
|
|
336
351
|
id: input.message.id,
|
|
337
|
-
attachments: input.message.attachments,
|
|
338
|
-
imageUrl: input.message.imageUrl ?? null,
|
|
339
|
-
audioUrl: input.message.audioUrl ?? null,
|
|
340
|
-
audioDurationMs: input.message.audioDurationMs ?? null,
|
|
352
|
+
attachments: input.message.attachments ?? [],
|
|
341
353
|
}, { agentId, conversationId: input.conversationId });
|
|
342
354
|
}
|
|
343
355
|
catch (error) {
|
|
@@ -538,11 +550,16 @@ async function main() {
|
|
|
538
550
|
const hostAvailableExecutionModes = allowWorktrees
|
|
539
551
|
? [...EXECUTION_ENVIRONMENT_MODES]
|
|
540
552
|
: ['locked'];
|
|
553
|
+
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
541
554
|
let runtimeDescriptor = {
|
|
542
555
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
543
556
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
544
557
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
545
558
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
559
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
560
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
561
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
562
|
+
: {}),
|
|
546
563
|
};
|
|
547
564
|
const publishRuntimeHeartbeat = async () => {
|
|
548
565
|
if (!streamConnected)
|
|
@@ -587,6 +604,10 @@ async function main() {
|
|
|
587
604
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
588
605
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
589
606
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
607
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
608
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
609
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
610
|
+
: {}),
|
|
590
611
|
};
|
|
591
612
|
}
|
|
592
613
|
catch {
|
|
@@ -594,6 +615,10 @@ async function main() {
|
|
|
594
615
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
595
616
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
596
617
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
618
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
619
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
620
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
621
|
+
: {}),
|
|
597
622
|
};
|
|
598
623
|
}
|
|
599
624
|
try {
|
|
@@ -661,7 +686,7 @@ async function main() {
|
|
|
661
686
|
writeState(session);
|
|
662
687
|
}
|
|
663
688
|
if (control.permissionMode) {
|
|
664
|
-
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})`);
|
|
665
690
|
}
|
|
666
691
|
if (control.effort) {
|
|
667
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.6.
|
|
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,8 +22,8 @@
|
|
|
22
22
|
"prepack": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@canonmsg/agent-sdk": "^0.8.
|
|
26
|
-
"@canonmsg/core": "^0.7.
|
|
25
|
+
"@canonmsg/agent-sdk": "^0.8.1",
|
|
26
|
+
"@canonmsg/core": "^0.7.1"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=18.0.0"
|