@canonmsg/codex-plugin 0.9.4 → 0.9.6
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/dist/adapter.d.ts +2 -0
- package/dist/adapter.js +16 -12
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -26
- package/dist/codex-cli-version.d.ts +2 -0
- package/dist/codex-cli-version.js +22 -0
- package/dist/error-format.d.ts +16 -0
- package/dist/error-format.js +106 -0
- package/dist/host-lifecycle.d.ts +4 -0
- package/dist/host-lifecycle.js +3 -0
- package/dist/host.js +296 -124
- package/dist/permission-mode.js +1 -5
- package/dist/register.js +24 -0
- package/dist/session-store.d.ts +11 -2
- package/dist/session-store.js +36 -3
- package/dist/setup.js +14 -0
- package/package.json +2 -2
package/dist/adapter.d.ts
CHANGED
|
@@ -60,10 +60,12 @@ export declare class CodexConversationAdapter {
|
|
|
60
60
|
bypassApprovalsAndSandbox?: boolean;
|
|
61
61
|
});
|
|
62
62
|
getThreadId(): string | null;
|
|
63
|
+
clearThreadId(): void;
|
|
63
64
|
setModel(model: string | null): void;
|
|
64
65
|
isRunning(): boolean;
|
|
65
66
|
interrupt(): Promise<void>;
|
|
66
67
|
runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[]): Promise<CodexTurnResult>;
|
|
67
68
|
private buildArgs;
|
|
69
|
+
private canResumeWithCurrentPolicy;
|
|
68
70
|
private clearActiveProcess;
|
|
69
71
|
}
|
package/dist/adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
+
import { isRecoverableCodexThreadError } from './error-format.js';
|
|
3
4
|
export class CodexConversationAdapter {
|
|
4
5
|
cwd;
|
|
5
6
|
codexBin;
|
|
@@ -31,6 +32,9 @@ export class CodexConversationAdapter {
|
|
|
31
32
|
getThreadId() {
|
|
32
33
|
return this.threadId;
|
|
33
34
|
}
|
|
35
|
+
clearThreadId() {
|
|
36
|
+
this.threadId = null;
|
|
37
|
+
}
|
|
34
38
|
setModel(model) {
|
|
35
39
|
this.model = model;
|
|
36
40
|
}
|
|
@@ -118,6 +122,8 @@ export class CodexConversationAdapter {
|
|
|
118
122
|
if (isIgnorableCodexLog(trimmed))
|
|
119
123
|
return;
|
|
120
124
|
lastErrorText = trimmed;
|
|
125
|
+
if (isRecoverableCodexThreadError(trimmed))
|
|
126
|
+
return;
|
|
121
127
|
onLog?.(trimmed);
|
|
122
128
|
});
|
|
123
129
|
return await new Promise((resolve, reject) => {
|
|
@@ -142,7 +148,7 @@ export class CodexConversationAdapter {
|
|
|
142
148
|
});
|
|
143
149
|
}
|
|
144
150
|
buildArgs(prompt, imagePaths = []) {
|
|
145
|
-
if (this.threadId) {
|
|
151
|
+
if (this.threadId && this.canResumeWithCurrentPolicy()) {
|
|
146
152
|
const args = ['exec', 'resume', '--json', '--skip-git-repo-check'];
|
|
147
153
|
if (this.model) {
|
|
148
154
|
args.push('-m', this.model);
|
|
@@ -165,10 +171,12 @@ export class CodexConversationAdapter {
|
|
|
165
171
|
args.push(this.threadId, prompt);
|
|
166
172
|
return args;
|
|
167
173
|
}
|
|
174
|
+
if (this.threadId) {
|
|
175
|
+
this.threadId = null;
|
|
176
|
+
}
|
|
168
177
|
const args = ['exec', '--json', '--color', 'never', '-C', this.cwd, '--skip-git-repo-check'];
|
|
169
178
|
const execMode = resolveExecMode({
|
|
170
179
|
sandbox: this.sandbox,
|
|
171
|
-
approvalPolicy: this.legacyApprovalPolicy,
|
|
172
180
|
fullAuto: this.fullAuto,
|
|
173
181
|
bypassApprovalsAndSandbox: this.bypassApprovalsAndSandbox,
|
|
174
182
|
});
|
|
@@ -199,6 +207,12 @@ export class CodexConversationAdapter {
|
|
|
199
207
|
args.push(prompt);
|
|
200
208
|
return args;
|
|
201
209
|
}
|
|
210
|
+
canResumeWithCurrentPolicy() {
|
|
211
|
+
if (this.bypassApprovalsAndSandbox || this.fullAuto) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return this.sandbox === null;
|
|
215
|
+
}
|
|
202
216
|
clearActiveProcess() {
|
|
203
217
|
if (this.interruptTimer) {
|
|
204
218
|
clearTimeout(this.interruptTimer);
|
|
@@ -237,18 +251,8 @@ function resolveExecMode(input) {
|
|
|
237
251
|
if (input.fullAuto) {
|
|
238
252
|
return { fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
239
253
|
}
|
|
240
|
-
if (shouldTranslateLegacyApprovalMode(input)) {
|
|
241
|
-
return { fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
242
|
-
}
|
|
243
254
|
return { fullAuto: false, bypassApprovalsAndSandbox: false };
|
|
244
255
|
}
|
|
245
|
-
function shouldTranslateLegacyApprovalMode(input) {
|
|
246
|
-
// Newer Codex CLI releases no longer accept --ask-for-approval for `exec`.
|
|
247
|
-
// Keep the compatibility shim isolated here so the rest of the adapter only
|
|
248
|
-
// deals with the supported execution switches.
|
|
249
|
-
return input.approvalPolicy === 'never'
|
|
250
|
-
&& (input.sandbox === 'workspace-write' || input.sandbox == null);
|
|
251
|
-
}
|
|
252
256
|
function isIgnorableCodexLog(line) {
|
|
253
257
|
return [
|
|
254
258
|
'Reading additional input from stdin...',
|
package/dist/cli-entry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
2
|
+
export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
|
package/dist/cli-entry.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
export function isDirectExecution(moduleUrl) {
|
|
5
|
-
const entry = process.argv[1];
|
|
6
|
-
if (!entry)
|
|
7
|
-
return false;
|
|
8
|
-
const abs = resolve(entry);
|
|
9
|
-
let real = abs;
|
|
10
|
-
try {
|
|
11
|
-
real = realpathSync(abs);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// argv[1] may not exist on disk in rare harnesses; fall back to abs
|
|
15
|
-
}
|
|
16
|
-
return (pathToFileURL(real).href === moduleUrl ||
|
|
17
|
-
pathToFileURL(abs).href === moduleUrl);
|
|
18
|
-
}
|
|
19
|
-
export function runCli(moduleUrl, main, onError) {
|
|
20
|
-
if (!isDirectExecution(moduleUrl)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
Promise.resolve()
|
|
24
|
-
.then(() => main())
|
|
25
|
-
.catch(onError);
|
|
26
|
-
}
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { parseCodexCliVersion, } from './error-format.js';
|
|
3
|
+
export function detectCodexCliVersion(codexBin, timeoutMs = 2_000) {
|
|
4
|
+
const result = spawnSync(codexBin, ['--version'], {
|
|
5
|
+
encoding: 'utf-8',
|
|
6
|
+
timeout: timeoutMs,
|
|
7
|
+
});
|
|
8
|
+
const raw = [result.stdout, result.stderr]
|
|
9
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
10
|
+
.join('\n')
|
|
11
|
+
.trim();
|
|
12
|
+
return {
|
|
13
|
+
codexBin,
|
|
14
|
+
raw: raw || null,
|
|
15
|
+
version: parseCodexCliVersion(raw),
|
|
16
|
+
error: result.error
|
|
17
|
+
? result.error.message
|
|
18
|
+
: result.status === 0
|
|
19
|
+
? null
|
|
20
|
+
: `codex --version exited with status ${result.status ?? 'unknown'}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MIN_CODEX_VERSION_FOR_GPT_5_5 = "0.125.0";
|
|
2
|
+
export interface CodexCliVersionStatus {
|
|
3
|
+
codexBin: string;
|
|
4
|
+
raw: string | null;
|
|
5
|
+
version: string | null;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function normalizeCodexDiagnosticText(value: string | null | undefined): string | null;
|
|
9
|
+
export declare function isRecoverableCodexThreadError(value: string | null | undefined): boolean;
|
|
10
|
+
export declare function isCodexUpgradeRequiredError(value: string | null | undefined): boolean;
|
|
11
|
+
export declare function compareSemverLike(a: string, b: string): number;
|
|
12
|
+
export declare function parseCodexCliVersion(raw: string | null | undefined): string | null;
|
|
13
|
+
export declare function isGpt55Model(model: string | null | undefined): boolean;
|
|
14
|
+
export declare function codexCliUpgradeMessage(model: string): string;
|
|
15
|
+
export declare function buildCodexModelGuardMessage(model: string | null | undefined, status: Pick<CodexCliVersionStatus, 'version'>): string | null;
|
|
16
|
+
export declare function formatCodexTurnFailure(errorText: string | null | undefined): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const MIN_CODEX_VERSION_FOR_GPT_5_5 = '0.125.0';
|
|
2
|
+
function extractErrorMessage(value) {
|
|
3
|
+
if (!value || typeof value !== 'object')
|
|
4
|
+
return null;
|
|
5
|
+
const data = value;
|
|
6
|
+
const direct = typeof data.message === 'string' ? data.message.trim() : '';
|
|
7
|
+
if (direct)
|
|
8
|
+
return direct;
|
|
9
|
+
const error = data.error;
|
|
10
|
+
if (typeof error === 'string' && error.trim())
|
|
11
|
+
return error.trim();
|
|
12
|
+
if (error && typeof error === 'object') {
|
|
13
|
+
const nested = error;
|
|
14
|
+
const message = typeof nested.message === 'string' ? nested.message.trim() : '';
|
|
15
|
+
if (message)
|
|
16
|
+
return message;
|
|
17
|
+
const type = typeof nested.type === 'string' ? nested.type.trim() : '';
|
|
18
|
+
if (type)
|
|
19
|
+
return type;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function parseJsonMessage(text) {
|
|
24
|
+
try {
|
|
25
|
+
return extractErrorMessage(JSON.parse(text));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function normalizeCodexDiagnosticText(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return null;
|
|
34
|
+
const normalized = value.replace(/^error:\s*/i, '').trim();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return null;
|
|
37
|
+
const directJson = parseJsonMessage(normalized);
|
|
38
|
+
if (directJson)
|
|
39
|
+
return directJson;
|
|
40
|
+
for (const line of normalized.split(/\r?\n/)) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed.startsWith('{'))
|
|
43
|
+
continue;
|
|
44
|
+
const lineJson = parseJsonMessage(trimmed);
|
|
45
|
+
if (lineJson)
|
|
46
|
+
return lineJson;
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
export function isRecoverableCodexThreadError(value) {
|
|
51
|
+
const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
|
|
52
|
+
return /thread .*not found/.test(normalized)
|
|
53
|
+
|| normalized.includes('unknown thread')
|
|
54
|
+
|| normalized.includes('find_thread_path')
|
|
55
|
+
|| normalized.includes('state db discrepancy');
|
|
56
|
+
}
|
|
57
|
+
export function isCodexUpgradeRequiredError(value) {
|
|
58
|
+
const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
|
|
59
|
+
return normalized.includes('requires a newer version of codex')
|
|
60
|
+
|| (normalized.includes('upgrade') && normalized.includes('codex'));
|
|
61
|
+
}
|
|
62
|
+
export function compareSemverLike(a, b) {
|
|
63
|
+
const parse = (value) => value.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
64
|
+
const left = parse(a);
|
|
65
|
+
const right = parse(b);
|
|
66
|
+
const length = Math.max(left.length, right.length, 3);
|
|
67
|
+
for (let index = 0; index < length; index += 1) {
|
|
68
|
+
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
69
|
+
if (diff !== 0)
|
|
70
|
+
return diff;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
export function parseCodexCliVersion(raw) {
|
|
75
|
+
if (!raw)
|
|
76
|
+
return null;
|
|
77
|
+
const match = raw.match(/\b(\d+\.\d+\.\d+)\b/);
|
|
78
|
+
return match?.[1] ?? null;
|
|
79
|
+
}
|
|
80
|
+
export function isGpt55Model(model) {
|
|
81
|
+
return typeof model === 'string' && /^gpt-5\.5(?:$|[-_:])/i.test(model.trim());
|
|
82
|
+
}
|
|
83
|
+
export function codexCliUpgradeMessage(model) {
|
|
84
|
+
return `The selected Codex model (${model}) requires a newer local Codex CLI. Update @openai/codex to ${MIN_CODEX_VERSION_FOR_GPT_5_5} or newer, then restart this Canon host.`;
|
|
85
|
+
}
|
|
86
|
+
export function buildCodexModelGuardMessage(model, status) {
|
|
87
|
+
const modelId = typeof model === 'string' ? model.trim() : '';
|
|
88
|
+
if (!isGpt55Model(modelId))
|
|
89
|
+
return null;
|
|
90
|
+
if (!status.version)
|
|
91
|
+
return null;
|
|
92
|
+
return compareSemverLike(status.version, MIN_CODEX_VERSION_FOR_GPT_5_5) < 0
|
|
93
|
+
? codexCliUpgradeMessage(modelId)
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
export function formatCodexTurnFailure(errorText) {
|
|
97
|
+
const normalized = normalizeCodexDiagnosticText(errorText);
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
return 'The Codex session stopped unexpectedly before sending a final reply.';
|
|
100
|
+
}
|
|
101
|
+
if (isCodexUpgradeRequiredError(normalized)) {
|
|
102
|
+
return codexCliUpgradeMessage('gpt-5.5');
|
|
103
|
+
}
|
|
104
|
+
const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
|
|
105
|
+
return `Codex failed before sending a final reply: ${shortened}`;
|
|
106
|
+
}
|
package/dist/host.js
CHANGED
|
@@ -6,9 +6,38 @@ import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk
|
|
|
6
6
|
import { buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
7
7
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
8
8
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
9
|
-
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
9
|
+
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
10
10
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
11
|
+
import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
12
|
+
import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
|
|
13
|
+
import { startCodexStreamInBackground } from './host-lifecycle.js';
|
|
11
14
|
import { runCli } from './cli-entry.js';
|
|
15
|
+
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
16
|
+
|
|
17
|
+
USAGE
|
|
18
|
+
canon-codex [flags]
|
|
19
|
+
|
|
20
|
+
COMMON FLAGS
|
|
21
|
+
--cwd <path> Project directory to run Codex from
|
|
22
|
+
--workspace <path> Additional project to expose in Canon
|
|
23
|
+
--workspace-root <path> Discover projects under an approved root
|
|
24
|
+
--model <model> Default Codex model for new turns
|
|
25
|
+
--sandbox <mode> Codex sandbox mode
|
|
26
|
+
--full-auto Allow non-interactive write access
|
|
27
|
+
--codex-bin <path> Codex CLI binary to run
|
|
28
|
+
--config <key=value> Forward config to Codex CLI
|
|
29
|
+
--help, -h Show this help
|
|
30
|
+
--version, -V Show package version
|
|
31
|
+
|
|
32
|
+
AUTH
|
|
33
|
+
CANON_AGENT=<profile> canon-codex --cwd /path/to/project
|
|
34
|
+
CANON_API_KEY=agk_live_... canon-codex --cwd /path/to/project
|
|
35
|
+
|
|
36
|
+
EXAMPLES
|
|
37
|
+
canon-codex --cwd ~/dev/canon
|
|
38
|
+
canon-codex --cwd ~/dev/canon --workspace-root ~/dev --full-auto
|
|
39
|
+
|
|
40
|
+
Keep this terminal open while you want Canon to reach the agent.`;
|
|
12
41
|
const MAX_SESSIONS = 12;
|
|
13
42
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14
43
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -25,7 +54,7 @@ let workspaceOptions = [];
|
|
|
25
54
|
let workspaceRoots = [];
|
|
26
55
|
let workspaceRootMetadata = [];
|
|
27
56
|
function buildCodexRuntimeDescriptor(input) {
|
|
28
|
-
|
|
57
|
+
const descriptor = buildFirstPartyCodingRuntimeDescriptor({
|
|
29
58
|
clientType: 'codex',
|
|
30
59
|
models: input.models,
|
|
31
60
|
workspaces: input.workspaces,
|
|
@@ -57,6 +86,27 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
57
86
|
},
|
|
58
87
|
],
|
|
59
88
|
});
|
|
89
|
+
if (input.models.length > 0) {
|
|
90
|
+
return descriptor;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...descriptor,
|
|
94
|
+
coreControls: descriptor.coreControls.filter((control) => control.id !== 'model'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function modelOptionLabel(model) {
|
|
98
|
+
if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
|
|
99
|
+
return 'GPT-5.5';
|
|
100
|
+
if (/^gpt-5\.4-mini(?:$|[-_:])/i.test(model))
|
|
101
|
+
return 'GPT-5.4 Mini';
|
|
102
|
+
if (/^gpt-5\.4(?:$|[-_:])/i.test(model))
|
|
103
|
+
return 'GPT-5.4';
|
|
104
|
+
return model;
|
|
105
|
+
}
|
|
106
|
+
function buildCodexModelOptions(model) {
|
|
107
|
+
return typeof model === 'string' && model.trim()
|
|
108
|
+
? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
|
|
109
|
+
: [];
|
|
60
110
|
}
|
|
61
111
|
function normalizeRuntimeTurnState(value) {
|
|
62
112
|
const normalizedTurn = normalizeTurnState(value);
|
|
@@ -95,6 +145,48 @@ function resolveExecutionFallbackReason(environment) {
|
|
|
95
145
|
? null
|
|
96
146
|
: environment.reason;
|
|
97
147
|
}
|
|
148
|
+
function stringArg(args, key) {
|
|
149
|
+
const value = args[key];
|
|
150
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
151
|
+
}
|
|
152
|
+
function boolArg(args, key) {
|
|
153
|
+
return args[key] === true;
|
|
154
|
+
}
|
|
155
|
+
function resolveCodexEffectiveRuntimePolicy(input) {
|
|
156
|
+
const model = input.config?.model ?? stringArg(input.args, 'model');
|
|
157
|
+
const permissionMode = input.config?.permissionMode ?? input.permissionEnvelope.defaultPermissionMode;
|
|
158
|
+
if (permissionMode
|
|
159
|
+
&& !input.permissionEnvelope.availablePermissionModes.some((option) => option.value === permissionMode)) {
|
|
160
|
+
throw new ExecutionEnvironmentError(`Permission mode "${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.');
|
|
161
|
+
}
|
|
162
|
+
const approvalOverride = mapCanonPermissionToCodex(permissionMode);
|
|
163
|
+
const defaultSandbox = (stringArg(input.args, 'sandbox') ?? null);
|
|
164
|
+
const defaultApprovalPolicy = (stringArg(input.args, 'ask-for-approval') ?? null);
|
|
165
|
+
const sandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
|
|
166
|
+
const approvalPolicy = approvalOverride ? null : defaultApprovalPolicy;
|
|
167
|
+
const fullAuto = approvalOverride ? approvalOverride.fullAuto : boolArg(input.args, 'full-auto');
|
|
168
|
+
const bypassApprovalsAndSandbox = approvalOverride
|
|
169
|
+
? approvalOverride.bypassApprovalsAndSandbox
|
|
170
|
+
: boolArg(input.args, 'dangerously-bypass-approvals-and-sandbox');
|
|
171
|
+
const fingerprint = buildCodexThreadPolicyFingerprint({
|
|
172
|
+
baseCwd: input.environment.baseCwd,
|
|
173
|
+
executionMode: input.environment.mode,
|
|
174
|
+
permissionMode: permissionMode ?? null,
|
|
175
|
+
sandbox,
|
|
176
|
+
approvalPolicy,
|
|
177
|
+
fullAuto,
|
|
178
|
+
bypassApprovalsAndSandbox,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
...(model ? { model } : {}),
|
|
182
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
183
|
+
sandbox,
|
|
184
|
+
approvalPolicy,
|
|
185
|
+
fullAuto,
|
|
186
|
+
bypassApprovalsAndSandbox,
|
|
187
|
+
fingerprint,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
98
190
|
function buildCanonPrompt(input) {
|
|
99
191
|
return buildCanonHostPrompt({
|
|
100
192
|
hostLabel: 'Codex',
|
|
@@ -112,14 +204,6 @@ function summarizeCommand(command) {
|
|
|
112
204
|
const shortened = trimmed.length > 140 ? `${trimmed.slice(0, 137)}...` : trimmed;
|
|
113
205
|
return `Running: ${shortened}`;
|
|
114
206
|
}
|
|
115
|
-
function formatTurnFailure(errorText) {
|
|
116
|
-
if (!errorText) {
|
|
117
|
-
return 'The Codex session stopped unexpectedly before sending a final reply.';
|
|
118
|
-
}
|
|
119
|
-
const normalized = errorText.replace(/^error:\s*/i, '').trim();
|
|
120
|
-
const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
|
|
121
|
-
return `Codex failed before sending a final reply: ${shortened}`;
|
|
122
|
-
}
|
|
123
207
|
function sleep(ms) {
|
|
124
208
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
125
209
|
}
|
|
@@ -142,6 +226,11 @@ export async function main() {
|
|
|
142
226
|
},
|
|
143
227
|
strict: true,
|
|
144
228
|
});
|
|
229
|
+
if (typeof args['ask-for-approval'] === 'string') {
|
|
230
|
+
console.error('[canon-codex] --ask-for-approval is no longer supported by Canon. Use --full-auto, --sandbox, or Canon permission modes instead.');
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
145
234
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
146
235
|
const workspaceDiscovery = buildConfiguredWorkspaceOptionsWithRoots({
|
|
147
236
|
primaryCwd: workingDir,
|
|
@@ -154,8 +243,13 @@ export async function main() {
|
|
|
154
243
|
for (const warning of workspaceDiscovery.warnings) {
|
|
155
244
|
console.error(`[canon-codex] ${warning}`);
|
|
156
245
|
}
|
|
157
|
-
|
|
158
|
-
|
|
246
|
+
const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
|
|
247
|
+
const codexCliStatus = detectCodexCliVersion(codexBin);
|
|
248
|
+
if (codexCliStatus.version) {
|
|
249
|
+
console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
|
|
159
253
|
}
|
|
160
254
|
const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
|
|
161
255
|
console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
@@ -261,9 +355,19 @@ export async function main() {
|
|
|
261
355
|
});
|
|
262
356
|
}
|
|
263
357
|
function writeState(session) {
|
|
358
|
+
const appliedAt = Date.now();
|
|
359
|
+
const controlState = {};
|
|
360
|
+
if (session.state.model !== undefined) {
|
|
361
|
+
controlState.model = { value: session.state.model, source: 'applied', appliedAt };
|
|
362
|
+
}
|
|
363
|
+
if (session.state.permissionMode !== undefined) {
|
|
364
|
+
controlState.permissionMode = { value: session.state.permissionMode, source: 'applied', appliedAt };
|
|
365
|
+
}
|
|
264
366
|
runtimeState.writeSessionState(session.conversationId, {
|
|
265
367
|
lastError: session.state.lastError,
|
|
266
368
|
model: session.state.model,
|
|
369
|
+
permissionMode: session.state.permissionMode,
|
|
370
|
+
controlState,
|
|
267
371
|
cwd: session.cwd,
|
|
268
372
|
executionMode: session.environment.mode,
|
|
269
373
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
@@ -382,16 +486,17 @@ export async function main() {
|
|
|
382
486
|
});
|
|
383
487
|
try {
|
|
384
488
|
const sessionCwd = environment.cwd;
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
489
|
+
const policy = resolveCodexEffectiveRuntimePolicy({
|
|
490
|
+
args,
|
|
491
|
+
config,
|
|
492
|
+
permissionEnvelope: codexPermissionEnvelope,
|
|
493
|
+
environment,
|
|
494
|
+
});
|
|
495
|
+
const modelGuard = buildCodexModelGuardMessage(policy.model, codexCliStatus);
|
|
496
|
+
if (modelGuard) {
|
|
497
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
390
498
|
}
|
|
391
|
-
const
|
|
392
|
-
const defaultSandbox = (typeof args.sandbox === 'string' ? args.sandbox : null);
|
|
393
|
-
const defaultFullAuto = Boolean(args['full-auto']);
|
|
394
|
-
const defaultBypass = Boolean(args['dangerously-bypass-approvals-and-sandbox']);
|
|
499
|
+
const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
|
|
395
500
|
const session = {
|
|
396
501
|
conversationId,
|
|
397
502
|
cwd: sessionCwd,
|
|
@@ -399,26 +504,24 @@ export async function main() {
|
|
|
399
504
|
adapter: new CodexConversationAdapter({
|
|
400
505
|
cwd: sessionCwd,
|
|
401
506
|
threadId: storedThreadId,
|
|
402
|
-
codexBin
|
|
403
|
-
model:
|
|
404
|
-
sandbox:
|
|
405
|
-
approvalPolicy:
|
|
406
|
-
? args['ask-for-approval']
|
|
407
|
-
: null),
|
|
507
|
+
codexBin,
|
|
508
|
+
model: policy.model ?? null,
|
|
509
|
+
sandbox: policy.sandbox,
|
|
510
|
+
approvalPolicy: policy.approvalPolicy,
|
|
408
511
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
409
512
|
addDirs: args['add-dir'] ?? [],
|
|
410
513
|
configOverrides: args.config ?? [],
|
|
411
|
-
fullAuto:
|
|
412
|
-
bypassApprovalsAndSandbox:
|
|
413
|
-
? approvalOverride.bypassApprovalsAndSandbox
|
|
414
|
-
: defaultBypass,
|
|
514
|
+
fullAuto: policy.fullAuto,
|
|
515
|
+
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
415
516
|
}),
|
|
416
517
|
queue: [],
|
|
417
518
|
running: false,
|
|
418
519
|
state: {
|
|
419
|
-
model:
|
|
520
|
+
model: policy.model,
|
|
521
|
+
permissionMode: policy.permissionMode,
|
|
420
522
|
state: 'idle',
|
|
421
523
|
},
|
|
524
|
+
policyFingerprint: policy.fingerprint,
|
|
422
525
|
turnState: 'idle',
|
|
423
526
|
currentTurnId: null,
|
|
424
527
|
currentTurnOpenedAt: null,
|
|
@@ -428,7 +531,10 @@ export async function main() {
|
|
|
428
531
|
closed: false,
|
|
429
532
|
};
|
|
430
533
|
sessions.set(conversationId, session);
|
|
431
|
-
await
|
|
534
|
+
await Promise.all([
|
|
535
|
+
baselineControlSignal(conversationId),
|
|
536
|
+
baselineSessionControl(conversationId),
|
|
537
|
+
]);
|
|
432
538
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
|
|
433
539
|
writeState(session);
|
|
434
540
|
writeTurn(session);
|
|
@@ -557,11 +663,15 @@ export async function main() {
|
|
|
557
663
|
status: 'thinking',
|
|
558
664
|
}).catch(() => { });
|
|
559
665
|
try {
|
|
666
|
+
const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
|
|
667
|
+
if (modelGuard) {
|
|
668
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
669
|
+
}
|
|
560
670
|
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
561
|
-
const
|
|
671
|
+
const handleCodexEvent = (event) => {
|
|
562
672
|
session.lastActivity = Date.now();
|
|
563
673
|
if (event.type === 'thread.started') {
|
|
564
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode);
|
|
674
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
|
|
565
675
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
|
|
566
676
|
return;
|
|
567
677
|
}
|
|
@@ -589,13 +699,32 @@ export async function main() {
|
|
|
589
699
|
if (event.type === 'turn.completed') {
|
|
590
700
|
writeState(session);
|
|
591
701
|
}
|
|
592
|
-
}
|
|
702
|
+
};
|
|
703
|
+
const logCodexLine = (line) => {
|
|
593
704
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
|
|
594
|
-
}
|
|
705
|
+
};
|
|
706
|
+
const clearStoredThread = () => {
|
|
707
|
+
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
708
|
+
session.adapter.clearThreadId();
|
|
709
|
+
};
|
|
710
|
+
const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
|
|
711
|
+
let result = await runTurnOnce();
|
|
712
|
+
if (!result.interrupted
|
|
713
|
+
&& !result.finalMessage
|
|
714
|
+
&& result.exitCode
|
|
715
|
+
&& result.exitCode !== 0
|
|
716
|
+
&& isRecoverableCodexThreadError(result.errorText)) {
|
|
717
|
+
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Stored thread was not found; clearing and retrying once`);
|
|
718
|
+
clearStoredThread();
|
|
719
|
+
result = await runTurnOnce();
|
|
720
|
+
}
|
|
595
721
|
if (result.threadId) {
|
|
596
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode);
|
|
722
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
597
723
|
}
|
|
598
724
|
if (!result.interrupted && result.finalMessage) {
|
|
725
|
+
if (isRecoverableCodexThreadError(result.errorText)) {
|
|
726
|
+
clearStoredThread();
|
|
727
|
+
}
|
|
599
728
|
await client.sendMessage(session.conversationId, result.finalMessage, {
|
|
600
729
|
metadata: {
|
|
601
730
|
turnId: session.currentTurnId,
|
|
@@ -608,7 +737,7 @@ export async function main() {
|
|
|
608
737
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent reply (${result.finalMessage.length} chars)`);
|
|
609
738
|
}
|
|
610
739
|
else if (!result.interrupted && result.exitCode && result.exitCode !== 0) {
|
|
611
|
-
const userVisibleError =
|
|
740
|
+
const userVisibleError = formatCodexTurnFailure(result.errorText);
|
|
612
741
|
session.state.lastError = userVisibleError;
|
|
613
742
|
writeState(session);
|
|
614
743
|
if (result.errorText) {
|
|
@@ -637,7 +766,9 @@ export async function main() {
|
|
|
637
766
|
}
|
|
638
767
|
}
|
|
639
768
|
catch (error) {
|
|
640
|
-
const message =
|
|
769
|
+
const message = error instanceof ExecutionEnvironmentError
|
|
770
|
+
? error.userMessage
|
|
771
|
+
: `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
|
|
641
772
|
session.state.lastError = message;
|
|
642
773
|
writeState(session);
|
|
643
774
|
await client.sendMessage(session.conversationId, message, {
|
|
@@ -649,7 +780,7 @@ export async function main() {
|
|
|
649
780
|
},
|
|
650
781
|
}).catch(() => { });
|
|
651
782
|
await handoffFinalMessage(session.conversationId);
|
|
652
|
-
if (error instanceof Error &&
|
|
783
|
+
if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
|
|
653
784
|
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
654
785
|
}
|
|
655
786
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
@@ -678,6 +809,7 @@ export async function main() {
|
|
|
678
809
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
679
810
|
];
|
|
680
811
|
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
812
|
+
const codexModelOptions = buildCodexModelOptions(args.model);
|
|
681
813
|
let runtimeDescriptor = {
|
|
682
814
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
683
815
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
@@ -688,7 +820,7 @@ export async function main() {
|
|
|
688
820
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
689
821
|
: {}),
|
|
690
822
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
691
|
-
models:
|
|
823
|
+
models: codexModelOptions,
|
|
692
824
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
693
825
|
workspaceRoots: workspaceRootMetadata,
|
|
694
826
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -707,6 +839,18 @@ export async function main() {
|
|
|
707
839
|
lastSeenSignal.set(conversationId, timestamp);
|
|
708
840
|
}
|
|
709
841
|
}
|
|
842
|
+
async function baselineSessionControl(conversationId) {
|
|
843
|
+
if (lastSeenControl.has(conversationId))
|
|
844
|
+
return;
|
|
845
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
|
|
846
|
+
if (!raw || typeof raw !== 'object')
|
|
847
|
+
return;
|
|
848
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
849
|
+
if (timestamp > 0) {
|
|
850
|
+
lastSeenControl.set(conversationId, timestamp);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
let publishRuntimeDetailsInFlight = false;
|
|
710
854
|
const publishRuntimeHeartbeat = async () => {
|
|
711
855
|
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
712
856
|
agentId,
|
|
@@ -716,84 +860,99 @@ export async function main() {
|
|
|
716
860
|
});
|
|
717
861
|
if (!streamConnected)
|
|
718
862
|
return;
|
|
719
|
-
await refreshKnownConversationIds().catch((error) => {
|
|
720
|
-
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
721
|
-
});
|
|
722
863
|
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
723
864
|
console.error('[canon-codex] Failed to publish agent runtime:', error);
|
|
724
865
|
});
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
866
|
+
if (publishRuntimeDetailsInFlight)
|
|
867
|
+
return;
|
|
868
|
+
publishRuntimeDetailsInFlight = true;
|
|
869
|
+
try {
|
|
870
|
+
await refreshKnownConversationIds().catch((error) => {
|
|
871
|
+
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
872
|
+
});
|
|
873
|
+
await publishHostSessionSnapshots({
|
|
874
|
+
conversationIds: Array.from(knownConversationIds),
|
|
875
|
+
agentId,
|
|
876
|
+
clientType: 'codex',
|
|
877
|
+
runtime: runtimeDescriptor,
|
|
878
|
+
workspaceOptions,
|
|
879
|
+
defaultCwd: workingDir,
|
|
880
|
+
extraSessionConfigFields: ['permissionMode'],
|
|
881
|
+
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
882
|
+
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
883
|
+
return [
|
|
884
|
+
session.conversationId,
|
|
885
|
+
{
|
|
886
|
+
...(session.state.model ? { model: session.state.model } : {}),
|
|
887
|
+
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
888
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
889
|
+
executionMode: session.environment.mode,
|
|
890
|
+
executionBranch: session.environment.branch ?? null,
|
|
891
|
+
},
|
|
892
|
+
];
|
|
893
|
+
})),
|
|
894
|
+
}).catch((error) => {
|
|
895
|
+
console.error('[canon-codex] Failed to publish session snapshots:', error);
|
|
896
|
+
});
|
|
897
|
+
await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
|
|
898
|
+
const session = sessions.get(conversationId);
|
|
899
|
+
const workspaceId = session
|
|
900
|
+
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
901
|
+
: runtimeDescriptor.defaultWorkspaceId;
|
|
902
|
+
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
903
|
+
const descriptor = runtimeDescriptor.runtimeDescriptor;
|
|
904
|
+
if (!descriptor)
|
|
905
|
+
return;
|
|
906
|
+
const payload = {
|
|
907
|
+
descriptor,
|
|
908
|
+
surfaceMode: 'host',
|
|
909
|
+
statusItems: [
|
|
910
|
+
{
|
|
911
|
+
id: 'transport',
|
|
912
|
+
label: 'Transport',
|
|
913
|
+
value: 'exec --json',
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: 'streaming',
|
|
917
|
+
label: 'Live output',
|
|
918
|
+
value: 'Thinking, tools, and completed-message previews',
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: 'codex-cli',
|
|
922
|
+
label: 'Codex CLI',
|
|
923
|
+
value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
|
|
924
|
+
tone: codexCliStatus.version ? 'default' : 'warning',
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
id: 'nativeActions',
|
|
928
|
+
label: 'Native actions',
|
|
929
|
+
value: 'Limited until app-server transport',
|
|
930
|
+
tone: 'warning',
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
execution: {
|
|
934
|
+
resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
|
|
935
|
+
resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
|
|
936
|
+
workspaceRootId: workspace?.workspaceRootId ?? null,
|
|
937
|
+
workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
|
|
938
|
+
executionMode: session?.environment.mode ?? null,
|
|
939
|
+
executionBranch: session?.environment.branch ?? null,
|
|
940
|
+
worktreePath: session?.environment.worktreePath ?? null,
|
|
941
|
+
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
776
942
|
},
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
'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.',
|
|
791
|
-
],
|
|
792
|
-
};
|
|
793
|
-
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
794
|
-
})).catch((error) => {
|
|
795
|
-
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
796
|
-
});
|
|
943
|
+
notes: [
|
|
944
|
+
'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.',
|
|
945
|
+
'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.',
|
|
946
|
+
],
|
|
947
|
+
};
|
|
948
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
949
|
+
})).catch((error) => {
|
|
950
|
+
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
publishRuntimeDetailsInFlight = false;
|
|
955
|
+
}
|
|
797
956
|
};
|
|
798
957
|
const stream = new CanonStream({
|
|
799
958
|
apiKey,
|
|
@@ -843,7 +1002,7 @@ export async function main() {
|
|
|
843
1002
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
844
1003
|
: {}),
|
|
845
1004
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
846
|
-
models:
|
|
1005
|
+
models: codexModelOptions,
|
|
847
1006
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
848
1007
|
workspaceRoots: workspaceRootMetadata,
|
|
849
1008
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -862,7 +1021,7 @@ export async function main() {
|
|
|
862
1021
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
863
1022
|
: {}),
|
|
864
1023
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
865
|
-
models:
|
|
1024
|
+
models: codexModelOptions,
|
|
866
1025
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
867
1026
|
workspaceRoots: workspaceRootMetadata,
|
|
868
1027
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -932,7 +1091,7 @@ export async function main() {
|
|
|
932
1091
|
catch (error) {
|
|
933
1092
|
console.error('[canon-codex] Failed to load startup conversations:', error);
|
|
934
1093
|
}
|
|
935
|
-
|
|
1094
|
+
startCodexStreamInBackground(stream, (error) => {
|
|
936
1095
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
937
1096
|
});
|
|
938
1097
|
const pollControl = async () => {
|
|
@@ -948,6 +1107,14 @@ export async function main() {
|
|
|
948
1107
|
const session = sessions.get(conversationId);
|
|
949
1108
|
if (session && !session.closed) {
|
|
950
1109
|
if (control.model && control.model !== session.state.model) {
|
|
1110
|
+
const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
|
|
1111
|
+
if (modelGuard) {
|
|
1112
|
+
session.state.lastError = modelGuard;
|
|
1113
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${modelGuard}`);
|
|
1114
|
+
writeState(session);
|
|
1115
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
951
1118
|
session.adapter.setModel(control.model);
|
|
952
1119
|
session.state.model = control.model;
|
|
953
1120
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
|
|
@@ -960,6 +1127,7 @@ export async function main() {
|
|
|
960
1127
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] effort control is not mapped yet (${control.effort})`);
|
|
961
1128
|
}
|
|
962
1129
|
}
|
|
1130
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
963
1131
|
}
|
|
964
1132
|
}
|
|
965
1133
|
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`);
|
|
@@ -980,14 +1148,14 @@ export async function main() {
|
|
|
980
1148
|
continue;
|
|
981
1149
|
}
|
|
982
1150
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${signal.type} signal`);
|
|
983
|
-
if (session.running) {
|
|
984
|
-
await session.adapter.interrupt();
|
|
985
|
-
}
|
|
986
|
-
session.turnState = 'interrupted';
|
|
987
1151
|
if (signal.type === 'stop_and_drop') {
|
|
988
1152
|
const droppedPrompts = session.queue.splice(0);
|
|
989
1153
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
990
1154
|
}
|
|
1155
|
+
if (session.running) {
|
|
1156
|
+
await session.adapter.interrupt();
|
|
1157
|
+
}
|
|
1158
|
+
session.turnState = 'interrupted';
|
|
991
1159
|
writeTurn(session);
|
|
992
1160
|
clearStreaming(conversationId);
|
|
993
1161
|
client.setTyping(conversationId, false).catch(() => { });
|
|
@@ -1042,7 +1210,11 @@ export async function main() {
|
|
|
1042
1210
|
await new Promise(() => { });
|
|
1043
1211
|
}
|
|
1044
1212
|
runCli(import.meta.url, main, (error) => {
|
|
1045
|
-
|
|
1213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1214
|
+
console.error(`[canon-codex] ${message}`);
|
|
1046
1215
|
getActiveProfileLock()?.release();
|
|
1047
1216
|
process.exit(1);
|
|
1217
|
+
}, {
|
|
1218
|
+
name: 'canon-codex',
|
|
1219
|
+
help: HELP,
|
|
1048
1220
|
});
|
package/dist/permission-mode.js
CHANGED
|
@@ -22,10 +22,6 @@ function codexOptionsThrough(mode) {
|
|
|
22
22
|
const index = CODEX_PERMISSION_OPTIONS.findIndex((option) => option.value === mode);
|
|
23
23
|
return index >= 0 ? CODEX_PERMISSION_OPTIONS.slice(0, index + 1) : [];
|
|
24
24
|
}
|
|
25
|
-
function isLegacyFullAuto(args) {
|
|
26
|
-
return args['ask-for-approval'] === 'never'
|
|
27
|
-
&& (args.sandbox === 'workspace-write' || args.sandbox == null);
|
|
28
|
-
}
|
|
29
25
|
export function deriveCodexPermissionEnvelope(args) {
|
|
30
26
|
if (args.sandbox === 'read-only') {
|
|
31
27
|
return {
|
|
@@ -46,7 +42,7 @@ export function deriveCodexPermissionEnvelope(args) {
|
|
|
46
42
|
availablePermissionModes: [...CODEX_PERMISSION_OPTIONS],
|
|
47
43
|
};
|
|
48
44
|
}
|
|
49
|
-
if (args['full-auto']
|
|
45
|
+
if (args['full-auto']) {
|
|
50
46
|
return {
|
|
51
47
|
defaultPermissionMode: 'full-auto',
|
|
52
48
|
availablePermissionModes: codexOptionsThrough('full-auto'),
|
package/dist/register.js
CHANGED
|
@@ -4,6 +4,27 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
|
|
6
6
|
import { runCli } from './cli-entry.js';
|
|
7
|
+
const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
|
|
8
|
+
|
|
9
|
+
USAGE
|
|
10
|
+
canon-codex-register --name <name> --description <text> --phone <e164> [flags]
|
|
11
|
+
|
|
12
|
+
REQUIRED
|
|
13
|
+
--name <name> Agent display name shown in Canon
|
|
14
|
+
--description <text> Short profile description
|
|
15
|
+
--phone <e164> Owner phone number, for example +15551234567
|
|
16
|
+
|
|
17
|
+
FLAGS
|
|
18
|
+
--profile <name> Local profile name in ~/.canon/agents.json
|
|
19
|
+
--base-url <url> Canon API base URL override
|
|
20
|
+
--help, -h Show this help
|
|
21
|
+
--version, -V Show package version
|
|
22
|
+
|
|
23
|
+
EXAMPLES
|
|
24
|
+
canon-codex-register --name "My Codex" --description "Local coding agent" --phone "+15551234567"
|
|
25
|
+
canon-codex-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
26
|
+
|
|
27
|
+
After approval, start it with CANON_AGENT=<profile> canon-codex --cwd /path/to/project.`;
|
|
7
28
|
export async function main() {
|
|
8
29
|
setDefaultResultOrder('ipv4first');
|
|
9
30
|
const { values } = parseArgs({
|
|
@@ -92,4 +113,7 @@ export async function main() {
|
|
|
92
113
|
runCli(import.meta.url, main, (error) => {
|
|
93
114
|
console.error('[canon-codex-register] Fatal error:', error);
|
|
94
115
|
process.exit(1);
|
|
116
|
+
}, {
|
|
117
|
+
name: 'canon-codex-register',
|
|
118
|
+
help: HELP,
|
|
95
119
|
});
|
package/dist/session-store.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { type ExecutionEnvironmentMode } from '@canonmsg/core';
|
|
2
|
-
export declare function
|
|
3
|
-
|
|
2
|
+
export declare function buildCodexThreadPolicyFingerprint(input: {
|
|
3
|
+
baseCwd: string;
|
|
4
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
5
|
+
permissionMode?: string | null;
|
|
6
|
+
sandbox?: string | null;
|
|
7
|
+
approvalPolicy?: string | null;
|
|
8
|
+
fullAuto?: boolean;
|
|
9
|
+
bypassApprovalsAndSandbox?: boolean;
|
|
10
|
+
}): string;
|
|
11
|
+
export declare function loadStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): string | null;
|
|
12
|
+
export declare function saveStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): void;
|
|
4
13
|
export declare function clearStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
|
package/dist/session-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
4
5
|
const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
|
|
@@ -14,29 +15,60 @@ function saveStore(store) {
|
|
|
14
15
|
mkdirSync(CANON_DIR, { recursive: true });
|
|
15
16
|
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
16
17
|
}
|
|
17
|
-
export function
|
|
18
|
+
export function buildCodexThreadPolicyFingerprint(input) {
|
|
19
|
+
return createHash('sha256').update(JSON.stringify({
|
|
20
|
+
version: 1,
|
|
21
|
+
baseCwd: input.baseCwd,
|
|
22
|
+
executionMode: input.executionMode ?? null,
|
|
23
|
+
permissionMode: input.permissionMode ?? null,
|
|
24
|
+
sandbox: input.sandbox ?? null,
|
|
25
|
+
approvalPolicy: input.approvalPolicy ?? null,
|
|
26
|
+
fullAuto: input.fullAuto === true,
|
|
27
|
+
bypassApprovalsAndSandbox: input.bypassApprovalsAndSandbox === true,
|
|
28
|
+
})).digest('hex').slice(0, 24);
|
|
29
|
+
}
|
|
30
|
+
export function loadStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode, policyFingerprint) {
|
|
18
31
|
if (runtimeId) {
|
|
19
32
|
const state = loadRuntimeSessionState(runtimeId, {
|
|
20
33
|
conversationId,
|
|
21
34
|
baseCwd,
|
|
22
35
|
executionMode,
|
|
23
36
|
});
|
|
24
|
-
if (state?.threadId)
|
|
37
|
+
if (state?.threadId) {
|
|
38
|
+
if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
|
|
39
|
+
clearRuntimeSessionState(runtimeId, {
|
|
40
|
+
conversationId,
|
|
41
|
+
baseCwd,
|
|
42
|
+
executionMode,
|
|
43
|
+
});
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
25
46
|
return state.threadId;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
26
49
|
}
|
|
27
50
|
const store = loadStore();
|
|
28
51
|
const record = store.agents[agentId]?.[conversationId];
|
|
29
52
|
if (!record || record.cwd !== baseCwd)
|
|
30
53
|
return null;
|
|
54
|
+
if (policyFingerprint && record.policyFingerprint !== policyFingerprint) {
|
|
55
|
+
delete store.agents[agentId][conversationId];
|
|
56
|
+
if (Object.keys(store.agents[agentId]).length === 0) {
|
|
57
|
+
delete store.agents[agentId];
|
|
58
|
+
}
|
|
59
|
+
saveStore(store);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
31
62
|
return record.threadId;
|
|
32
63
|
}
|
|
33
|
-
export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode) {
|
|
64
|
+
export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
|
|
34
65
|
if (runtimeId) {
|
|
35
66
|
saveRuntimeSessionState(runtimeId, {
|
|
36
67
|
conversationId,
|
|
37
68
|
baseCwd,
|
|
38
69
|
executionMode,
|
|
39
70
|
threadId,
|
|
71
|
+
...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
|
|
40
72
|
});
|
|
41
73
|
return;
|
|
42
74
|
}
|
|
@@ -45,6 +77,7 @@ export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd,
|
|
|
45
77
|
store.agents[agentId][conversationId] = {
|
|
46
78
|
threadId,
|
|
47
79
|
cwd: baseCwd,
|
|
80
|
+
...(policyFingerprint ? { policyFingerprint } : {}),
|
|
48
81
|
updatedAt: new Date().toISOString(),
|
|
49
82
|
};
|
|
50
83
|
saveStore(store);
|
package/dist/setup.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { runCli } from './cli-entry.js';
|
|
4
|
+
const HELP = `canon-codex-setup — print Canon Codex setup guidance
|
|
5
|
+
|
|
6
|
+
USAGE
|
|
7
|
+
canon-codex-setup
|
|
8
|
+
|
|
9
|
+
FLAGS
|
|
10
|
+
--help, -h Show this help
|
|
11
|
+
--version, -V Show package version
|
|
12
|
+
|
|
13
|
+
This command does not register an agent. It checks for the Codex CLI and prints
|
|
14
|
+
the registration and host commands to run next.`;
|
|
4
15
|
export function main() {
|
|
5
16
|
console.log('Canon Codex Plugin Setup');
|
|
6
17
|
console.log('========================\n');
|
|
@@ -38,4 +49,7 @@ export function main() {
|
|
|
38
49
|
runCli(import.meta.url, main, (error) => {
|
|
39
50
|
console.error('[canon-codex-setup] Fatal error:', error);
|
|
40
51
|
process.exit(1);
|
|
52
|
+
}, {
|
|
53
|
+
name: 'canon-codex-setup',
|
|
54
|
+
help: HELP,
|
|
41
55
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@canonmsg/agent-sdk": "^1.1.0",
|
|
33
|
-
"@canonmsg/core": "^0.15.
|
|
33
|
+
"@canonmsg/core": "^0.15.2"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|