@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.
@@ -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,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
- if (attachments.length > 0) {
53
- for (let i = 0; i < attachments.length; i += 1) {
54
- const att = attachments[i];
55
- const mat = materialized?.find((m) => m.index === i) ?? null;
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({ conversationId, agentId });
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: (typeof args.sandbox === 'string' ? args.sandbox : null),
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: Boolean(args['full-auto']),
285
- bypassApprovalsAndSandbox: Boolean(args['dangerously-bypass-approvals-and-sandbox']),
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)}] permissionMode control is not mapped yet (${control.permissionMode})`);
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.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,8 +22,8 @@
22
22
  "prepack": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@canonmsg/agent-sdk": "^0.8.0",
26
- "@canonmsg/core": "^0.7.0"
25
+ "@canonmsg/agent-sdk": "^0.8.1",
26
+ "@canonmsg/core": "^0.7.1"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=18.0.0"