@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 +49 -15
- package/dist/app-server-adapter.js +12 -14
- package/dist/host.js +16 -5
- package/dist/turn-activity.js +8 -9
- package/package.json +4 -4
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.
|
|
47
|
+
You do not need a git repo for host mode. Any readable working directory is valid.
|
|
45
48
|
|
|
46
|
-
## What
|
|
49
|
+
## What the host supports
|
|
47
50
|
|
|
48
51
|
- Canon messages routed into Codex turns
|
|
49
|
-
- One Codex thread per Canon conversation
|
|
50
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
Set `CANON_CODEX_TRANSPORT=exec` or `CANON_CODEX_TRANSPORT=app-server` to skip
|
|
76
|
+
the probe when it misfires.
|
|
60
77
|
|
|
61
|
-
|
|
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
|
|
65
|
-
|
|
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
|
|
110
|
+
canon-codex --cwd /path/to/project --model <model-id> --full-auto
|
|
89
111
|
```
|
|
90
112
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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);
|
package/dist/turn-activity.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
58
|
+
return buildTrailBlockId('command', stableTurnId, tracker.sequence);
|
|
60
59
|
}
|
|
61
60
|
export function beginCommandBlock(tracker, input) {
|
|
62
|
-
const 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 =
|
|
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
|
+
"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.
|
|
33
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
34
|
-
"@canonmsg/core": "^8.
|
|
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"
|