@canonmsg/codex-plugin 0.6.6 → 0.7.0

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 CHANGED
@@ -37,7 +37,7 @@ You do not need a git repo for host mode. The plugin passes `--skip-git-repo-che
37
37
 
38
38
  ## Current limitation
39
39
 
40
- The stable `codex exec --json` surface exposes completed assistant messages and tool activity, but not token-by-token text deltas. v1 therefore reports live status and publishes the final reply when the turn completes, instead of true token streaming.
40
+ The stable `codex exec --json` surface exposes thinking state, tool activity, and completed assistant-message previews, but not token-by-token text deltas. v1 therefore publishes live progress and message snapshots without claiming true token streaming.
41
41
 
42
42
  ## Working directory
43
43
 
@@ -45,12 +45,22 @@ The stable `codex exec --json` surface exposes completed assistant messages and
45
45
  canon-codex --cwd /path/to/project
46
46
  ```
47
47
 
48
+ Advertise multiple project choices to the Canon app:
49
+
50
+ ```bash
51
+ canon-codex --cwd ~/dev --workspace ~/dev/canon --workspace ~/dev/yumyumv2
52
+ ```
53
+
54
+ `--cwd` is the default workspace. Each `--workspace` value appears as a selectable workspace during session creation. Worktree mode creates a per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-workspace mode runs directly in the selected directory.
55
+
48
56
  Useful flags:
49
57
 
50
58
  ```bash
51
59
  canon-codex --cwd /path/to/project --model gpt-5.4 --full-auto
52
60
  ```
53
61
 
62
+ Codex also supports `--add-dir /extra/path` for additional writable directories passed through to `codex exec`. Canon does not yet render those extra directories as workspace choices.
63
+
54
64
  Recent Codex CLI releases no longer accept `--ask-for-approval` with `codex exec`. If you previously launched Canon with `--sandbox workspace-write --ask-for-approval never`, switch to `--full-auto`.
55
65
 
56
66
  Local smoke test:
@@ -97,17 +97,23 @@ export declare function publishHostSessionSnapshots(input: {
97
97
  liveSessionConfigByConversation?: ReadonlyMap<string, {
98
98
  model?: string;
99
99
  permissionMode?: string;
100
+ effort?: string;
101
+ runtimeControlValues?: Record<string, string>;
100
102
  workspaceId?: string;
101
103
  executionMode?: SessionWorkspaceConfig['executionMode'];
102
104
  executionBranch?: string | null;
103
105
  }>;
104
106
  }): Promise<void>;
105
- export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null;
107
+ export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
108
+ runtimeControlValues?: Record<string, string>;
109
+ }) | null;
106
110
  export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
107
111
  conversationId: string;
108
112
  agentId: string;
109
113
  extraStringFields?: readonly TExtra[];
110
- }): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null>;
114
+ }): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
115
+ runtimeControlValues?: Record<string, string>;
116
+ }) | null>;
111
117
  export declare function resolveHostWorkspaceCwd(input: {
112
118
  workspaceOptions: HostWorkspaceResolverOption[];
113
119
  config: {
@@ -162,6 +162,10 @@ export async function publishHostSessionSnapshots(input) {
162
162
  sessionConfig: {
163
163
  ...(mergedConfig.model ? { model: mergedConfig.model } : {}),
164
164
  ...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
165
+ ...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
166
+ ...(mergedConfig.runtimeControlValues
167
+ ? { runtimeControlValues: mergedConfig.runtimeControlValues }
168
+ : {}),
165
169
  ...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
166
170
  ...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
167
171
  },
@@ -183,6 +187,8 @@ export async function publishHostSessionSnapshots(input) {
183
187
  hostMode: true,
184
188
  model: snapshot.model ?? null,
185
189
  permissionMode: snapshot.permissionMode ?? null,
190
+ effort: snapshot.effort ?? null,
191
+ runtimeControlValues: snapshot.runtimeControlValues ?? null,
186
192
  workspaceId: snapshot.workspaceId ?? null,
187
193
  executionMode: snapshot.executionMode ?? null,
188
194
  executionBranch,
@@ -204,9 +210,16 @@ export function readHostSessionConfig(raw, extraStringFields = []) {
204
210
  const value = normalizeOptionalString(data[field]);
205
211
  return value ? [[field, value]] : [];
206
212
  }));
213
+ const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
214
+ ? data.runtimeControlValues
215
+ : {}).flatMap(([key, value]) => {
216
+ const normalizedValue = normalizeOptionalString(value);
217
+ return normalizedValue ? [[key, normalizedValue]] : [];
218
+ }));
207
219
  return {
208
220
  ...(baseConfig ?? {}),
209
221
  ...extraConfig,
222
+ ...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
210
223
  };
211
224
  }
212
225
  export async function loadHostSessionConfig(input) {
package/dist/host.js CHANGED
@@ -3,7 +3,7 @@ import { setDefaultResultOrder } from 'node:dns';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
6
- import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, 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';
6
+ import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, 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, writeRuntimeInfo, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
7
7
  import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
8
8
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
9
9
  import { CodexConversationAdapter, } from './adapter.js';
@@ -23,6 +23,61 @@ const CODEX_RUNTIME_CAPABILITIES = {
23
23
  };
24
24
  let workingDir = process.cwd();
25
25
  let workspaceOptions = [];
26
+ function buildCodexRuntimeDescriptor(input) {
27
+ return {
28
+ coreControls: [
29
+ {
30
+ id: 'model',
31
+ label: 'Model',
32
+ options: input.models,
33
+ defaultValue: input.models[0]?.value ?? null,
34
+ availability: 'setup_and_live',
35
+ liveBehavior: 'next_turn',
36
+ selectionPolicy: 'inherit',
37
+ },
38
+ {
39
+ id: 'workspace',
40
+ label: 'Workspace',
41
+ options: input.workspaces.map((workspace) => ({
42
+ value: workspace.id,
43
+ label: workspace.label,
44
+ })),
45
+ defaultValue: input.workspaces[0]?.id ?? null,
46
+ availability: 'setup',
47
+ liveBehavior: 'none',
48
+ selectionPolicy: 'inherit',
49
+ },
50
+ {
51
+ id: 'executionMode',
52
+ label: 'Execution mode',
53
+ options: input.executionModes.map((mode) => ({
54
+ value: mode,
55
+ label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared workspace',
56
+ description: mode === 'worktree'
57
+ ? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected workspace is a git repo.'
58
+ : 'Runs directly in the selected workspace. Canon records usage, but does not create a separate checkout.',
59
+ })),
60
+ defaultValue: null,
61
+ availability: 'setup',
62
+ liveBehavior: 'none',
63
+ selectionPolicy: 'required_explicit',
64
+ },
65
+ ],
66
+ runtimeControls: [
67
+ {
68
+ id: 'permissionMode',
69
+ label: 'Execution policy',
70
+ options: input.permissionModes,
71
+ defaultValue: input.defaultPermissionMode ?? null,
72
+ availability: 'setup',
73
+ liveBehavior: 'none',
74
+ selectionPolicy: 'inherit',
75
+ },
76
+ ],
77
+ supportsInterrupt: true,
78
+ streamingTextMode: 'snapshot',
79
+ };
80
+ }
26
81
  function normalizeRuntimeTurnState(value) {
27
82
  const normalizedTurn = normalizeTurnState(value);
28
83
  if (normalizedTurn) {
@@ -62,6 +117,14 @@ function resolveWorkspaceCwd(config) {
62
117
  defaultCwd: workingDir,
63
118
  });
64
119
  }
120
+ function resolveExecutionFallbackReason(environment) {
121
+ if (!environment?.reason || environment.mode !== 'locked') {
122
+ return null;
123
+ }
124
+ return environment.reason === 'Sharing the base workspace (locked mode)'
125
+ ? null
126
+ : environment.reason;
127
+ }
65
128
  function buildCanonPrompt(input) {
66
129
  return buildCanonHostPrompt({
67
130
  hostLabel: 'Codex',
@@ -194,6 +257,7 @@ export async function main() {
194
257
  cwd: session.cwd,
195
258
  executionMode: session.environment.mode,
196
259
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
260
+ ...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
197
261
  hostMode: true,
198
262
  clientType: 'codex',
199
263
  state: session.state.state,
@@ -570,6 +634,13 @@ export async function main() {
570
634
  ...(codexPermissionEnvelope.defaultPermissionMode
571
635
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
572
636
  : {}),
637
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
638
+ models: [],
639
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
640
+ executionModes: hostAvailableExecutionModes,
641
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
642
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
643
+ }),
573
644
  };
574
645
  const publishRuntimeHeartbeat = async () => {
575
646
  if (!streamConnected)
@@ -602,6 +673,49 @@ export async function main() {
602
673
  }).catch((error) => {
603
674
  console.error('[canon-codex] Failed to publish session snapshots:', error);
604
675
  });
676
+ await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
677
+ const session = sessions.get(conversationId);
678
+ const workspaceId = session
679
+ ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
680
+ : runtimeDescriptor.defaultWorkspaceId;
681
+ const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
682
+ const payload = {
683
+ descriptor: runtimeDescriptor.runtimeDescriptor ?? buildCodexRuntimeDescriptor({
684
+ models: runtimeDescriptor.availableModels ?? [],
685
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
686
+ executionModes: hostAvailableExecutionModes,
687
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
688
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
689
+ }),
690
+ surfaceMode: 'host',
691
+ statusItems: [
692
+ {
693
+ id: 'transport',
694
+ label: 'Transport',
695
+ value: 'exec --json',
696
+ },
697
+ {
698
+ id: 'streaming',
699
+ label: 'Live output',
700
+ value: 'Thinking, tools, and completed-message previews',
701
+ },
702
+ ],
703
+ execution: {
704
+ resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
705
+ resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
706
+ executionMode: session?.environment.mode ?? null,
707
+ executionBranch: session?.environment.branch ?? null,
708
+ worktreePath: session?.environment.worktreePath ?? null,
709
+ fallbackReason: resolveExecutionFallbackReason(session?.environment),
710
+ },
711
+ notes: [
712
+ 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not token-by-token text deltas.',
713
+ ],
714
+ };
715
+ await writeRuntimeInfo(conversationId, agentId, payload);
716
+ })).catch((error) => {
717
+ console.error('[canon-codex] Failed to publish runtime info:', error);
718
+ });
605
719
  };
606
720
  const stream = new CanonStream({
607
721
  apiKey,
@@ -643,6 +757,13 @@ export async function main() {
643
757
  ...(codexPermissionEnvelope.defaultPermissionMode
644
758
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
645
759
  : {}),
760
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
761
+ models: [],
762
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
763
+ executionModes: hostAvailableExecutionModes,
764
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
765
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
766
+ }),
646
767
  };
647
768
  }
648
769
  catch {
@@ -654,6 +775,13 @@ export async function main() {
654
775
  ...(codexPermissionEnvelope.defaultPermissionMode
655
776
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
656
777
  : {}),
778
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
779
+ models: [],
780
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
781
+ executionModes: hostAvailableExecutionModes,
782
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
783
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
784
+ }),
657
785
  };
658
786
  }
659
787
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^0.8.3",
33
- "@canonmsg/core": "^0.7.5"
32
+ "@canonmsg/agent-sdk": "^0.9.0",
33
+ "@canonmsg/core": "^0.8.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"