@canonmsg/codex-plugin 0.23.3 → 0.23.5

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
@@ -14,6 +14,7 @@ npm install -g @canonmsg/codex-plugin
14
14
  codex login status
15
15
 
16
16
  # Register (approve in Canon when prompted)
17
+ export CANON_ENVIRONMENT_ID=canon-prod-v1
17
18
  canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567"
18
19
 
19
20
  # Run inside a project and keep the host process running
@@ -39,30 +40,51 @@ canon-necromance revive my-codex
39
40
 
40
41
  Do not run registration again unless Canon tells you the saved API key is invalid. If you registered multiple profiles, relaunch the same one with `CANON_AGENT=<profile> canon-codex --cwd /path/to/project`.
41
42
 
43
+ `canon-codex-setup` reprints these steps and the optional flags at any time.
44
+
42
45
  Public docs: <https://canonmail.com/agents/integrations>. Coding-host concepts: <https://canonmail.com/agents/coding-agents>.
43
46
 
44
- You do not need a git repo for host mode. The plugin passes `--skip-git-repo-check` to Codex, so any readable working directory is valid.
47
+ You do not need a git repo for host mode. Any readable working directory is valid.
45
48
 
46
- ## What v1 supports
49
+ ## What the host supports
47
50
 
48
51
  - Canon messages routed into Codex turns
49
- - One Codex thread per Canon conversation
50
- - Resume by thread ID across turns
52
+ - One Codex thread per Canon conversation, resumed by thread ID across turns
53
+ - Queued input while a turn is running
51
54
  - RTDB session state and activity heartbeat
52
55
  - Interrupt by terminating the active Codex turn
53
56
  - Tool/running status surfaced while Codex is working
57
+ - Reasoning-effort selection, resolved against what the active model accepts
58
+
59
+ ## Transports
60
+
61
+ The host picks a transport at startup and logs the choice as
62
+ `Codex transport: app-server` or `Codex transport: exec --json`.
54
63
 
55
- ## Current limitation
64
+ `codex app-server` is preferred and is selected when `codex app-server --help`
65
+ succeeds. On it Canon additionally routes native plan mode and plan review,
66
+ compact, rich cards, blocking approval gates, runtime questions, live message
67
+ deltas, and model/effort discovery from the runtime.
56
68
 
57
- 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.
69
+ `codex exec --json` is the fallback for older Codex CLIs. It reports thinking
70
+ state, tool activity, and completed assistant-message previews, but not
71
+ token-by-token deltas, and it cannot block on approvals — Canon labels the
72
+ session with that warning rather than implying a gate it does not have. Without
73
+ discovery it also offers no model picker and a fixed effort list.
58
74
 
59
- Current Canon control truth for Codex host mode:
75
+ Set `CANON_CODEX_TRANSPORT=exec` or `CANON_CODEX_TRANSPORT=app-server` to skip
76
+ the probe when it misfires.
60
77
 
61
- - model is live-visible, but current changes apply on the next turn rather than mid-turn
78
+ Canon control truth for Codex host mode:
79
+
80
+ - model is live-visible, but changes apply on the next turn rather than mid-turn
81
+ - reasoning effort is live-editable and also applies on the next turn; the option list comes from the active model
62
82
  - workspace selection is setup-only
63
83
  - execution mode selection is setup-only
64
- - the current `Execution policy`/permission choice is setup-only
65
- - 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
84
+ - the `Execution policy`/permission choice is session-creation-only; mid-session change requests are ignored and the applied value is re-published
85
+
86
+ The runtime descriptor the host publishes is the authoritative capability list;
87
+ what Canon renders always comes from it, not from this file.
66
88
 
67
89
  ## Working directory
68
90
 
@@ -85,14 +107,23 @@ Worktree mode is project isolation, not an operating-system sandbox. The Codex C
85
107
  Useful flags:
86
108
 
87
109
  ```bash
88
- canon-codex --cwd /path/to/project --model gpt-5.5 --full-auto
110
+ canon-codex --cwd /path/to/project --model <model-id> --full-auto
89
111
  ```
90
112
 
91
- 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.
113
+ Canon hardcodes no model IDs: on the app-server transport the picker is built
114
+ from what the local Codex runtime reports, and `--model` pins the default. A
115
+ model your installed Codex CLI does not know fails early with an upgrade
116
+ message.
92
117
 
93
- `gpt-5.5` requires a recent local Codex CLI. Older CLIs fail early with an upgrade message; upgrade Codex or choose a model supported by your installed CLI.
118
+ `--add-dir /extra/path` adds a writable directory: the exec transport passes it
119
+ through to `codex exec`, the app-server transport adds it to the thread's
120
+ writable roots. Canon does not render those extra directories as workspace
121
+ choices.
94
122
 
95
- 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`.
123
+ `--ask-for-approval` is rejected by Canon itself the host exits with an error
124
+ pointing at `--full-auto`, `--sandbox`, or Canon permission modes. If you
125
+ previously launched with `--sandbox workspace-write --ask-for-approval never`,
126
+ switch to `--full-auto`.
96
127
 
97
128
  Do not start Canon with `--sandbox danger-full-access` as an unlabeled default. Use `--dangerously-bypass-approvals-and-sandbox` only when you intentionally want Canon to advertise the owner-only Bypass policy.
98
129
 
@@ -125,7 +156,7 @@ canon-codex --cwd /path/to/project --codex-bin /absolute/path/to/codex
125
156
  If Canon rejects authenticated requests with `401 Invalid API key`, the stored Canon profile needs a fresh key. Reconnect the same profile to overwrite `~/.canon/agents.json`, then restart or revive the host:
126
157
 
127
158
  ```bash
128
- canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567" --profile my-codex
159
+ CANON_ENVIRONMENT_ID=canon-prod-v1 canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567" --profile my-codex
129
160
  ```
130
161
 
131
162
  If Codex reports API-key quota errors while another local tool on the same machine uses OpenAI API keys, check Codex's own stored login state:
@@ -147,10 +178,13 @@ Restart `canon-codex` after changing the Codex login state.
147
178
  ## Multiple agents
148
179
 
149
180
  ```bash
181
+ export CANON_ENVIRONMENT_ID=canon-prod-v1
150
182
  canon-codex-register --name "Frontend" --description "React work" --phone "+1..." --profile frontend
151
183
  CANON_AGENT=frontend canon-codex --cwd ~/projects/frontend
152
184
  ```
153
185
 
186
+ With more than one registered profile, `CANON_AGENT` is required — the host refuses to guess and lists the available profiles instead.
187
+
154
188
  ## Development
155
189
 
156
190
  ```bash
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
+ import { normalizePlanStepStatus, renderPlanSteps } from '@canonmsg/coding-agent-host';
3
4
  const DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS = 5_000;
4
5
  export class CodexAppServerAdapter {
5
6
  cwd;
@@ -783,23 +784,20 @@ function summarizeItem(item) {
783
784
  return 'Searching the web';
784
785
  return null;
785
786
  }
787
+ /**
788
+ * Codex's plan update in the shared plan grammar (`renderPlanSteps` in
789
+ * @canonmsg/coding-agent-host), so a plan card reads the same whichever host
790
+ * produced it. Codex numbers steps by their position in the plan it sent, so an
791
+ * entry with no step text is passed through as a blank that keeps its slot
792
+ * rather than renumbering the steps below it.
793
+ */
786
794
  function renderPlan(params) {
787
795
  const explanation = readString(params, 'explanation');
788
796
  const plan = Array.isArray(params.plan) ? params.plan : [];
789
- const lines = [
790
- explanation ? `**Plan**\n${explanation}` : '**Plan**',
791
- ...plan.flatMap((entry, index) => {
792
- if (!isRecord(entry))
793
- return [];
794
- const step = readString(entry, 'step');
795
- if (!step)
796
- return [];
797
- const status = readString(entry, 'status') ?? 'pending';
798
- const marker = status === 'completed' ? '[x]' : status === 'inProgress' ? '[~]' : '[ ]';
799
- return [`${index + 1}. ${marker} ${step}`];
800
- }),
801
- ];
802
- return lines.join('\n');
797
+ return renderPlanSteps(plan.map((entry) => ({
798
+ text: (isRecord(entry) ? readString(entry, 'step') : undefined) ?? '',
799
+ status: normalizePlanStepStatus(isRecord(entry) ? readString(entry, 'status') : undefined),
800
+ })), { explanation });
803
801
  }
804
802
  function defaultServerRequestResult(method) {
805
803
  if (method.includes('requestApproval'))
package/dist/host.js CHANGED
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
- import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
8
+ import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, } from '@canonmsg/coding-agent-host';
9
9
  import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
@@ -74,8 +74,12 @@ export function buildCodexLiveSessionConfig(input) {
74
74
  };
75
75
  }
76
76
  const MAX_SESSIONS = 12;
77
- const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
77
+ // IDLE_TIMEOUT_MS (30 minutes) is shared with the other coding-agent hosts.
78
78
  const HEARTBEAT_MS = 30_000;
79
+ /** How Codex says it finished the work but Canon would not take the answer. */
80
+ const CODEX_UNDELIVERABLE_FINAL_WORDING = {
81
+ lead: 'The Codex host completed the turn, but Canon could not deliver the reply',
82
+ };
79
83
  const IDLE_CHECK_MS = 60_000;
80
84
  const PLAN_REVIEW_TIMEOUT_MS = 10 * 60_000;
81
85
  const CODEX_RUNTIME_CAPABILITIES = {
@@ -1591,10 +1595,14 @@ export async function main() {
1591
1595
  writeTurn(session);
1592
1596
  stopVisibleWorkSignal(session);
1593
1597
  upsertTurnBlock(session, {
1594
- id: `plan:${session.currentTurnId}`,
1598
+ // Spelled through the shared helper, and with the same fallback
1599
+ // the other host uses: a plan arriving before a turn id would
1600
+ // otherwise mint the literal id `plan:null`, which two such turns
1601
+ // would share — one margin row for two plans.
1602
+ id: buildTrailBlockId('plan', session.currentTurnId ?? 'turn'),
1595
1603
  kind: 'plan',
1596
1604
  status: 'running',
1597
- title: 'Plan',
1605
+ title: PLAN_BLOCK_TITLE,
1598
1606
  text: event.text,
1599
1607
  });
1600
1608
  writeCodexStreaming(session, event.text, 'streaming');
@@ -1801,7 +1809,10 @@ export async function main() {
1801
1809
  const message = error instanceof ExecutionEnvironmentError
1802
1810
  ? error.userMessage
1803
1811
  : error instanceof CanonApiError
1804
- ? `The Codex host completed the turn, but Canon could not deliver the reply: ${error.message}`
1812
+ // Shared with the other coding-agent hosts, which is where the bound
1813
+ // on the quoted error comes from: an unbounded API error body would
1814
+ // push this notice over the very limit that rejected the reply.
1815
+ ? buildUndeliverableFinalNotice({ error, wording: CODEX_UNDELIVERABLE_FINAL_WORDING })
1805
1816
  : `The Codex host failed during the turn: ${error instanceof Error ? error.message : String(error)}`;
1806
1817
  session.state.lastError = message;
1807
1818
  writeState(session);
@@ -1,15 +1,12 @@
1
+ import { buildTrailBlockId, normalizeTrailKey } from '@canonmsg/coding-agent-host';
1
2
  export function createCommandBlockTracker() {
2
3
  return {
3
4
  sequence: 0,
4
5
  running: [],
5
6
  };
6
7
  }
7
- function normalizeOptionalString(value) {
8
- const normalized = value?.trim();
9
- return normalized ? normalized : undefined;
10
- }
11
8
  function textBlockId(turnId, itemId) {
12
- return `message:${turnId ?? 'turn'}:${normalizeOptionalString(itemId) ?? 'latest'}`;
9
+ return buildTrailBlockId('message', turnId ?? 'turn', normalizeTrailKey(itemId) ?? 'latest');
13
10
  }
14
11
  function buildLiveText(blocks) {
15
12
  return blocks
@@ -54,12 +51,14 @@ export function applyTextSegmentBlock(state, input) {
54
51
  function nextCommandBlockId(tracker, turnId, itemId) {
55
52
  const stableTurnId = turnId ?? 'turn';
56
53
  if (itemId)
57
- return `command:${stableTurnId}:${itemId}`;
54
+ return buildTrailBlockId('command', stableTurnId, itemId);
55
+ // Older Codex runtimes report no item id, so the per-turn sequence is what
56
+ // keeps two commands from sharing a row.
58
57
  tracker.sequence += 1;
59
- return `command:${stableTurnId}:${tracker.sequence}`;
58
+ return buildTrailBlockId('command', stableTurnId, tracker.sequence);
60
59
  }
61
60
  export function beginCommandBlock(tracker, input) {
62
- const itemId = normalizeOptionalString(input.itemId);
61
+ const itemId = normalizeTrailKey(input.itemId);
63
62
  if (itemId) {
64
63
  const existing = tracker.running.find((block) => block.itemId === itemId);
65
64
  if (existing)
@@ -74,7 +73,7 @@ export function beginCommandBlock(tracker, input) {
74
73
  return blockId;
75
74
  }
76
75
  export function claimCommandBlock(tracker, input) {
77
- const itemId = normalizeOptionalString(input.itemId);
76
+ const itemId = normalizeTrailKey(input.itemId);
78
77
  const command = input.command.trim();
79
78
  const matchingIndex = itemId
80
79
  ? tracker.running.findIndex((block) => block.itemId === itemId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.23.3",
3
+ "version": "0.23.5",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^7.1.1",
33
- "@canonmsg/coding-agent-host": "^0.2.2",
34
- "@canonmsg/core": "^8.0.0"
32
+ "@canonmsg/agent-sdk": "^7.1.3",
33
+ "@canonmsg/coding-agent-host": "^0.3.1",
34
+ "@canonmsg/core": "^8.2.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"