@canonmsg/codex-plugin 0.6.6 → 0.9.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,15 @@ 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 assistant-message snapshots without claiming true token streaming.
41
+
42
+ Current Canon control truth for Codex host mode:
43
+
44
+ - model is live-visible, but current changes apply on the next turn rather than mid-turn
45
+ - workspace selection is setup-only
46
+ - execution mode selection is setup-only
47
+ - the current `Execution policy`/permission choice is setup-only
48
+ - advanced Codex-only controls such as effort, sandbox policy, approval reviewer, or apps/plugins inventory are not exposed on the current transport unless the runtime can actually report them
41
49
 
42
50
  ## Working directory
43
51
 
@@ -45,12 +53,24 @@ The stable `codex exec --json` surface exposes completed assistant messages and
45
53
  canon-codex --cwd /path/to/project
46
54
  ```
47
55
 
56
+ Advertise multiple project choices to the Canon app:
57
+
58
+ ```bash
59
+ canon-codex --cwd ~/dev --workspace-root ~/dev
60
+ ```
61
+
62
+ `--cwd` is the default workspace. Each `--workspace-root` value is an approved local root; the host discovers immediate child projects with common markers such as `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, or `go.mod` and publishes them as selectable projects during session creation. Use repeated `--workspace /path/to/project` entries to advertise specific projects outside those roots. Worktree mode creates a per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-project mode runs directly in the selected directory.
63
+
64
+ If worktree isolation is requested for a project that cannot support it, Canon may fall back to shared-project execution and surface the fallback reason in session details instead of failing the session outright.
65
+
48
66
  Useful flags:
49
67
 
50
68
  ```bash
51
69
  canon-codex --cwd /path/to/project --model gpt-5.4 --full-auto
52
70
  ```
53
71
 
72
+ 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.
73
+
54
74
  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
75
 
56
76
  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 { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, 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,93 @@ const CODEX_RUNTIME_CAPABILITIES = {
23
23
  };
24
24
  let workingDir = process.cwd();
25
25
  let workspaceOptions = [];
26
+ let workspaceRoots = [];
27
+ let workspaceRootMetadata = [];
28
+ function buildCodexRuntimeDescriptor(input) {
29
+ return {
30
+ coreControls: [
31
+ {
32
+ id: 'model',
33
+ label: 'Model',
34
+ options: input.models,
35
+ defaultValue: input.models[0]?.value ?? null,
36
+ availability: 'setup_and_live',
37
+ liveBehavior: 'next_turn',
38
+ selectionPolicy: 'inherit',
39
+ },
40
+ {
41
+ id: 'workspace',
42
+ label: 'Project',
43
+ options: input.workspaces.map((workspace) => ({
44
+ value: workspace.id,
45
+ label: workspace.label,
46
+ ...(workspace.description ? { description: workspace.description } : {}),
47
+ ...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
48
+ ...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
49
+ ...(workspace.source ? { source: workspace.source } : {}),
50
+ })),
51
+ defaultValue: input.workspaces[0]?.id ?? null,
52
+ availability: 'setup',
53
+ liveBehavior: 'none',
54
+ selectionPolicy: 'inherit',
55
+ description: input.workspaceRoots?.length
56
+ ? 'Choose one of the projects discovered inside the approved local roots for this host.'
57
+ : 'Choose one of the local projects advertised by this host.',
58
+ },
59
+ {
60
+ id: 'executionMode',
61
+ label: 'Execution mode',
62
+ options: input.executionModes.map((mode) => ({
63
+ value: mode,
64
+ label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
65
+ description: mode === 'worktree'
66
+ ? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
67
+ : 'Runs directly in the selected project folder. Changes happen there.',
68
+ })),
69
+ defaultValue: null,
70
+ availability: 'setup',
71
+ liveBehavior: 'none',
72
+ selectionPolicy: 'required_explicit',
73
+ },
74
+ ],
75
+ runtimeControls: [
76
+ {
77
+ id: 'permissionMode',
78
+ label: 'Execution policy',
79
+ options: input.permissionModes,
80
+ defaultValue: input.defaultPermissionMode ?? null,
81
+ availability: 'setup',
82
+ liveBehavior: 'none',
83
+ selectionPolicy: 'inherit',
84
+ },
85
+ ],
86
+ actions: [
87
+ {
88
+ id: 'stop',
89
+ label: 'Stop',
90
+ description: 'Interrupt the current Codex exec turn.',
91
+ aliases: ['stop'],
92
+ category: 'turn',
93
+ placements: ['composer_slash', 'command_palette'],
94
+ availability: ['busy'],
95
+ dispatch: { kind: 'signal', signal: 'interrupt' },
96
+ },
97
+ {
98
+ id: 'stop-and-clear-queue',
99
+ label: 'Stop & clear queue',
100
+ description: 'Interrupt the current Codex exec turn and drop queued Canon messages.',
101
+ aliases: ['stop-clear', 'clear-queue'],
102
+ category: 'turn',
103
+ placements: ['composer_slash', 'command_palette', 'session_strip'],
104
+ availability: ['busy_with_queue'],
105
+ dispatch: { kind: 'signal', signal: 'stop_and_drop' },
106
+ },
107
+ ],
108
+ workspaceRoots: input.workspaceRoots,
109
+ supportsInterrupt: true,
110
+ streamingTextMode: 'snapshot',
111
+ };
112
+ }
26
113
  function normalizeRuntimeTurnState(value) {
27
114
  const normalizedTurn = normalizeTurnState(value);
28
115
  if (normalizedTurn) {
@@ -49,11 +136,10 @@ async function loadSessionConfig(conversationId, agentId) {
49
136
  extraStringFields: ['permissionMode'],
50
137
  });
51
138
  }
52
- // Default to 'locked' (shared workspace) when no mode has been picked. The
53
- // UI still lets owners flip to 'worktree'; this just stops sessions from
54
- // failing closed when the mode has never been written.
55
139
  function resolveSessionExecutionMode(config) {
56
- return config?.executionMode ?? 'locked';
140
+ if (config?.executionMode)
141
+ return config.executionMode;
142
+ throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
57
143
  }
58
144
  function resolveWorkspaceCwd(config) {
59
145
  return resolveHostWorkspaceCwd({
@@ -62,6 +148,14 @@ function resolveWorkspaceCwd(config) {
62
148
  defaultCwd: workingDir,
63
149
  });
64
150
  }
151
+ function resolveExecutionFallbackReason(environment) {
152
+ if (!environment?.reason || environment.mode !== 'locked') {
153
+ return null;
154
+ }
155
+ return environment.reason === 'Sharing the base workspace (locked mode)'
156
+ ? null
157
+ : environment.reason;
158
+ }
65
159
  function buildCanonPrompt(input) {
66
160
  return buildCanonHostPrompt({
67
161
  hostLabel: 'Codex',
@@ -101,6 +195,7 @@ export async function main() {
101
195
  'codex-profile': { type: 'string' },
102
196
  'add-dir': { type: 'string', multiple: true },
103
197
  workspace: { type: 'string', multiple: true },
198
+ 'workspace-root': { type: 'string', multiple: true },
104
199
  config: { type: 'string', multiple: true },
105
200
  'codex-bin': { type: 'string' },
106
201
  'full-auto': { type: 'boolean' },
@@ -109,7 +204,17 @@ export async function main() {
109
204
  strict: true,
110
205
  });
111
206
  workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
112
- workspaceOptions = buildConfiguredWorkspaceOptions(workingDir, args.workspace ?? []);
207
+ const workspaceDiscovery = buildConfiguredWorkspaceOptionsWithRoots({
208
+ primaryCwd: workingDir,
209
+ configuredWorkspaces: args.workspace ?? [],
210
+ workspaceRoots: args['workspace-root'] ?? [],
211
+ });
212
+ workspaceOptions = workspaceDiscovery.workspaceOptions;
213
+ workspaceRoots = workspaceDiscovery.workspaceRoots;
214
+ workspaceRootMetadata = buildPublicWorkspaceRoots(workspaceRoots);
215
+ for (const warning of workspaceDiscovery.warnings) {
216
+ console.error(`[canon-codex] ${warning}`);
217
+ }
113
218
  if (typeof args['ask-for-approval'] === 'string') {
114
219
  console.error('[canon-codex] Note: newer Codex CLI releases do not accept --ask-for-approval for `codex exec`; Canon will translate compatible legacy usage when possible.');
115
220
  }
@@ -194,6 +299,10 @@ export async function main() {
194
299
  cwd: session.cwd,
195
300
  executionMode: session.environment.mode,
196
301
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
302
+ ...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
303
+ ...(resolveExecutionFallbackReason(session.environment)
304
+ ? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
305
+ : {}),
197
306
  hostMode: true,
198
307
  clientType: 'codex',
199
308
  state: session.state.state,
@@ -227,11 +336,33 @@ export async function main() {
227
336
  clearStreaming(conversationId);
228
337
  client.setTyping(conversationId, false).catch(() => { });
229
338
  }
339
+ function refreshVisibleWorkSignal(session) {
340
+ if (!session.running || session.closed)
341
+ return;
342
+ if (session.turnState !== 'thinking' && session.turnState !== 'tool')
343
+ return;
344
+ client.setTyping(session.conversationId, true, 'thinking').catch(() => { });
345
+ }
346
+ function startVisibleWorkSignal(session) {
347
+ refreshVisibleWorkSignal(session);
348
+ if (session.typingKeepaliveTimer)
349
+ return;
350
+ session.typingKeepaliveTimer = setInterval(() => {
351
+ refreshVisibleWorkSignal(session);
352
+ }, 3500);
353
+ }
354
+ function stopVisibleWorkSignal(session) {
355
+ if (session.typingKeepaliveTimer) {
356
+ clearInterval(session.typingKeepaliveTimer);
357
+ session.typingKeepaliveTimer = null;
358
+ }
359
+ }
230
360
  function closeSession(conversationId) {
231
361
  const session = sessions.get(conversationId);
232
362
  if (!session)
233
363
  return;
234
364
  session.closed = true;
365
+ stopVisibleWorkSignal(session);
235
366
  releaseConversationEnvironment(session.environment);
236
367
  clearStreaming(conversationId);
237
368
  clearSessionState(conversationId, agentId).catch(() => { });
@@ -319,6 +450,7 @@ export async function main() {
319
450
  currentTurnOpenedAt: null,
320
451
  lastAcceptedIntent: null,
321
452
  lastActivity: Date.now(),
453
+ typingKeepaliveTimer: null,
322
454
  closed: false,
323
455
  };
324
456
  sessions.set(conversationId, session);
@@ -398,7 +530,12 @@ export async function main() {
398
530
  const message = error instanceof Error ? error.message : String(error);
399
531
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
400
532
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
401
- await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`).catch(() => { });
533
+ await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
534
+ metadata: {
535
+ turnSemantics: 'turn_complete',
536
+ turnComplete: true,
537
+ },
538
+ }).catch(() => { });
402
539
  return;
403
540
  }
404
541
  const turnMetadata = normalizeTurnMetadata(input.message.metadata);
@@ -439,7 +576,7 @@ export async function main() {
439
576
  await markQueuedMessageAccepted(session.conversationId, nextTurn.sourceMessageId, nextTurn.markAccepted);
440
577
  writeState(session);
441
578
  writeTurn(session);
442
- client.setTyping(session.conversationId, true, 'thinking').catch(() => { });
579
+ startVisibleWorkSignal(session);
443
580
  rtdbWrite(`/streaming/${session.conversationId}/${agentId}`, {
444
581
  text: 'Thinking…',
445
582
  status: 'thinking',
@@ -457,6 +594,8 @@ export async function main() {
457
594
  if (event.type === 'message') {
458
595
  session.turnState = 'streaming';
459
596
  writeTurn(session);
597
+ stopVisibleWorkSignal(session);
598
+ client.setTyping(session.conversationId, false).catch(() => { });
460
599
  rtdbWrite(`/streaming/${session.conversationId}/${agentId}`, {
461
600
  text: event.text,
462
601
  status: 'streaming',
@@ -467,7 +606,7 @@ export async function main() {
467
606
  if (event.type === 'command.started') {
468
607
  session.turnState = 'tool';
469
608
  writeTurn(session);
470
- client.setTyping(session.conversationId, false).catch(() => { });
609
+ startVisibleWorkSignal(session);
471
610
  rtdbWrite(`/streaming/${session.conversationId}/${agentId}`, {
472
611
  text: summarizeCommand(event.command),
473
612
  status: 'tool',
@@ -519,6 +658,7 @@ export async function main() {
519
658
  else if (result.interrupted) {
520
659
  session.turnState = 'interrupted';
521
660
  writeTurn(session);
661
+ stopVisibleWorkSignal(session);
522
662
  clearStreaming(session.conversationId);
523
663
  client.setTyping(session.conversationId, false).catch(() => { });
524
664
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn interrupted`);
@@ -541,6 +681,7 @@ export async function main() {
541
681
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
542
682
  }
543
683
  finally {
684
+ stopVisibleWorkSignal(session);
544
685
  session.running = false;
545
686
  session.state.state = 'idle';
546
687
  session.turnState = 'idle';
@@ -570,6 +711,14 @@ export async function main() {
570
711
  ...(codexPermissionEnvelope.defaultPermissionMode
571
712
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
572
713
  : {}),
714
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
715
+ models: [],
716
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
717
+ workspaceRoots: workspaceRootMetadata,
718
+ executionModes: hostAvailableExecutionModes,
719
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
720
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
721
+ }),
573
722
  };
574
723
  const publishRuntimeHeartbeat = async () => {
575
724
  if (!streamConnected)
@@ -602,6 +751,59 @@ export async function main() {
602
751
  }).catch((error) => {
603
752
  console.error('[canon-codex] Failed to publish session snapshots:', error);
604
753
  });
754
+ await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
755
+ const session = sessions.get(conversationId);
756
+ const workspaceId = session
757
+ ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
758
+ : runtimeDescriptor.defaultWorkspaceId;
759
+ const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
760
+ const payload = {
761
+ descriptor: runtimeDescriptor.runtimeDescriptor ?? buildCodexRuntimeDescriptor({
762
+ models: runtimeDescriptor.availableModels ?? [],
763
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
764
+ workspaceRoots: workspaceRootMetadata,
765
+ executionModes: hostAvailableExecutionModes,
766
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
767
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
768
+ }),
769
+ surfaceMode: 'host',
770
+ statusItems: [
771
+ {
772
+ id: 'transport',
773
+ label: 'Transport',
774
+ value: 'exec --json',
775
+ },
776
+ {
777
+ id: 'streaming',
778
+ label: 'Live output',
779
+ value: 'Thinking, tools, and completed-message previews',
780
+ },
781
+ {
782
+ id: 'nativeActions',
783
+ label: 'Native actions',
784
+ value: 'Limited until app-server transport',
785
+ tone: 'warning',
786
+ },
787
+ ],
788
+ execution: {
789
+ resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
790
+ resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
791
+ workspaceRootId: workspace?.workspaceRootId ?? null,
792
+ workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
793
+ executionMode: session?.environment.mode ?? null,
794
+ executionBranch: session?.environment.branch ?? null,
795
+ worktreePath: session?.environment.worktreePath ?? null,
796
+ fallbackReason: resolveExecutionFallbackReason(session?.environment),
797
+ },
798
+ notes: [
799
+ '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.',
800
+ 'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
801
+ ],
802
+ };
803
+ await writeRuntimeInfo(conversationId, agentId, payload);
804
+ })).catch((error) => {
805
+ console.error('[canon-codex] Failed to publish runtime info:', error);
806
+ });
605
807
  };
606
808
  const stream = new CanonStream({
607
809
  apiKey,
@@ -643,6 +845,14 @@ export async function main() {
643
845
  ...(codexPermissionEnvelope.defaultPermissionMode
644
846
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
645
847
  : {}),
848
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
849
+ models: [],
850
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
851
+ workspaceRoots: workspaceRootMetadata,
852
+ executionModes: hostAvailableExecutionModes,
853
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
854
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
855
+ }),
646
856
  };
647
857
  }
648
858
  catch {
@@ -654,6 +864,14 @@ export async function main() {
654
864
  ...(codexPermissionEnvelope.defaultPermissionMode
655
865
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
656
866
  : {}),
867
+ runtimeDescriptor: buildCodexRuntimeDescriptor({
868
+ models: [],
869
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
870
+ workspaceRoots: workspaceRootMetadata,
871
+ executionModes: hostAvailableExecutionModes,
872
+ permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
873
+ defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
874
+ }),
657
875
  };
658
876
  }
659
877
  try {
package/dist/setup.js CHANGED
@@ -21,8 +21,10 @@ export function main() {
21
21
  console.log('');
22
22
  console.log(' 2. Start the host in a project directory and keep it running');
23
23
  console.log(' canon-codex --cwd /path/to/project');
24
+ console.log(' canon-codex --cwd ~/dev --workspace-root ~/dev');
24
25
  console.log('');
25
26
  console.log(' A git repo is not required; any readable directory works.');
27
+ console.log(' Use --workspace-root to let Canon offer discovered projects inside an approved root.');
26
28
  console.log('');
27
29
  console.log('Optional flags:');
28
30
  console.log(' --model gpt-5.4');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.6.6",
3
+ "version": "0.9.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.2",
33
+ "@canonmsg/core": "^0.10.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"