@canonmsg/codex-plugin 0.9.4 → 0.9.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/dist/adapter.d.ts +1 -0
- package/dist/adapter.js +3 -0
- 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 +256 -116
- 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
package/dist/adapter.js
CHANGED
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);
|
|
@@ -112,14 +162,6 @@ function summarizeCommand(command) {
|
|
|
112
162
|
const shortened = trimmed.length > 140 ? `${trimmed.slice(0, 137)}...` : trimmed;
|
|
113
163
|
return `Running: ${shortened}`;
|
|
114
164
|
}
|
|
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
165
|
function sleep(ms) {
|
|
124
166
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
125
167
|
}
|
|
@@ -157,6 +199,14 @@ export async function main() {
|
|
|
157
199
|
if (typeof args['ask-for-approval'] === 'string') {
|
|
158
200
|
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.');
|
|
159
201
|
}
|
|
202
|
+
const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
|
|
203
|
+
const codexCliStatus = detectCodexCliVersion(codexBin);
|
|
204
|
+
if (codexCliStatus.version) {
|
|
205
|
+
console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
|
|
209
|
+
}
|
|
160
210
|
const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
|
|
161
211
|
console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
162
212
|
const client = new CanonClient(apiKey, baseUrl);
|
|
@@ -264,6 +314,7 @@ export async function main() {
|
|
|
264
314
|
runtimeState.writeSessionState(session.conversationId, {
|
|
265
315
|
lastError: session.state.lastError,
|
|
266
316
|
model: session.state.model,
|
|
317
|
+
permissionMode: session.state.permissionMode,
|
|
267
318
|
cwd: session.cwd,
|
|
268
319
|
executionMode: session.environment.mode,
|
|
269
320
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
@@ -383,15 +434,37 @@ export async function main() {
|
|
|
383
434
|
try {
|
|
384
435
|
const sessionCwd = environment.cwd;
|
|
385
436
|
const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
437
|
+
const modelGuard = buildCodexModelGuardMessage(sessionModel, codexCliStatus);
|
|
438
|
+
if (modelGuard) {
|
|
439
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
440
|
+
}
|
|
441
|
+
const effectivePermissionMode = config?.permissionMode ?? codexPermissionEnvelope.defaultPermissionMode;
|
|
442
|
+
if (effectivePermissionMode
|
|
443
|
+
&& !codexPermissionEnvelope.availablePermissionModes.some((option) => option.value === effectivePermissionMode)) {
|
|
444
|
+
throw new ExecutionEnvironmentError(`Permission mode "${effectivePermissionMode}" 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.');
|
|
390
445
|
}
|
|
391
|
-
const approvalOverride = mapCanonPermissionToCodex(
|
|
446
|
+
const approvalOverride = mapCanonPermissionToCodex(effectivePermissionMode);
|
|
392
447
|
const defaultSandbox = (typeof args.sandbox === 'string' ? args.sandbox : null);
|
|
393
448
|
const defaultFullAuto = Boolean(args['full-auto']);
|
|
394
449
|
const defaultBypass = Boolean(args['dangerously-bypass-approvals-and-sandbox']);
|
|
450
|
+
const legacyApprovalPolicy = (typeof args['ask-for-approval'] === 'string'
|
|
451
|
+
? args['ask-for-approval']
|
|
452
|
+
: null);
|
|
453
|
+
const effectiveSandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
|
|
454
|
+
const effectiveFullAuto = approvalOverride ? approvalOverride.fullAuto : defaultFullAuto;
|
|
455
|
+
const effectiveBypass = approvalOverride
|
|
456
|
+
? approvalOverride.bypassApprovalsAndSandbox
|
|
457
|
+
: defaultBypass;
|
|
458
|
+
const policyFingerprint = buildCodexThreadPolicyFingerprint({
|
|
459
|
+
baseCwd: environment.baseCwd,
|
|
460
|
+
executionMode: environment.mode,
|
|
461
|
+
permissionMode: effectivePermissionMode ?? null,
|
|
462
|
+
sandbox: effectiveSandbox,
|
|
463
|
+
approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
|
|
464
|
+
fullAuto: effectiveFullAuto,
|
|
465
|
+
bypassApprovalsAndSandbox: effectiveBypass,
|
|
466
|
+
});
|
|
467
|
+
const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policyFingerprint);
|
|
395
468
|
const session = {
|
|
396
469
|
conversationId,
|
|
397
470
|
cwd: sessionCwd,
|
|
@@ -399,26 +472,24 @@ export async function main() {
|
|
|
399
472
|
adapter: new CodexConversationAdapter({
|
|
400
473
|
cwd: sessionCwd,
|
|
401
474
|
threadId: storedThreadId,
|
|
402
|
-
codexBin
|
|
475
|
+
codexBin,
|
|
403
476
|
model: sessionModel ?? null,
|
|
404
|
-
sandbox:
|
|
405
|
-
approvalPolicy:
|
|
406
|
-
? args['ask-for-approval']
|
|
407
|
-
: null),
|
|
477
|
+
sandbox: effectiveSandbox,
|
|
478
|
+
approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
|
|
408
479
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
409
480
|
addDirs: args['add-dir'] ?? [],
|
|
410
481
|
configOverrides: args.config ?? [],
|
|
411
|
-
fullAuto:
|
|
412
|
-
bypassApprovalsAndSandbox:
|
|
413
|
-
? approvalOverride.bypassApprovalsAndSandbox
|
|
414
|
-
: defaultBypass,
|
|
482
|
+
fullAuto: effectiveFullAuto,
|
|
483
|
+
bypassApprovalsAndSandbox: effectiveBypass,
|
|
415
484
|
}),
|
|
416
485
|
queue: [],
|
|
417
486
|
running: false,
|
|
418
487
|
state: {
|
|
419
488
|
model: sessionModel,
|
|
489
|
+
permissionMode: effectivePermissionMode,
|
|
420
490
|
state: 'idle',
|
|
421
491
|
},
|
|
492
|
+
policyFingerprint,
|
|
422
493
|
turnState: 'idle',
|
|
423
494
|
currentTurnId: null,
|
|
424
495
|
currentTurnOpenedAt: null,
|
|
@@ -428,7 +499,10 @@ export async function main() {
|
|
|
428
499
|
closed: false,
|
|
429
500
|
};
|
|
430
501
|
sessions.set(conversationId, session);
|
|
431
|
-
await
|
|
502
|
+
await Promise.all([
|
|
503
|
+
baselineControlSignal(conversationId),
|
|
504
|
+
baselineSessionControl(conversationId),
|
|
505
|
+
]);
|
|
432
506
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
|
|
433
507
|
writeState(session);
|
|
434
508
|
writeTurn(session);
|
|
@@ -557,11 +631,15 @@ export async function main() {
|
|
|
557
631
|
status: 'thinking',
|
|
558
632
|
}).catch(() => { });
|
|
559
633
|
try {
|
|
634
|
+
const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
|
|
635
|
+
if (modelGuard) {
|
|
636
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
637
|
+
}
|
|
560
638
|
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
561
|
-
const
|
|
639
|
+
const handleCodexEvent = (event) => {
|
|
562
640
|
session.lastActivity = Date.now();
|
|
563
641
|
if (event.type === 'thread.started') {
|
|
564
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode);
|
|
642
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
|
|
565
643
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
|
|
566
644
|
return;
|
|
567
645
|
}
|
|
@@ -589,13 +667,32 @@ export async function main() {
|
|
|
589
667
|
if (event.type === 'turn.completed') {
|
|
590
668
|
writeState(session);
|
|
591
669
|
}
|
|
592
|
-
}
|
|
670
|
+
};
|
|
671
|
+
const logCodexLine = (line) => {
|
|
593
672
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
|
|
594
|
-
}
|
|
673
|
+
};
|
|
674
|
+
const clearStoredThread = () => {
|
|
675
|
+
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
676
|
+
session.adapter.clearThreadId();
|
|
677
|
+
};
|
|
678
|
+
const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
|
|
679
|
+
let result = await runTurnOnce();
|
|
680
|
+
if (!result.interrupted
|
|
681
|
+
&& !result.finalMessage
|
|
682
|
+
&& result.exitCode
|
|
683
|
+
&& result.exitCode !== 0
|
|
684
|
+
&& isRecoverableCodexThreadError(result.errorText)) {
|
|
685
|
+
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Stored thread was not found; clearing and retrying once`);
|
|
686
|
+
clearStoredThread();
|
|
687
|
+
result = await runTurnOnce();
|
|
688
|
+
}
|
|
595
689
|
if (result.threadId) {
|
|
596
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode);
|
|
690
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
597
691
|
}
|
|
598
692
|
if (!result.interrupted && result.finalMessage) {
|
|
693
|
+
if (isRecoverableCodexThreadError(result.errorText)) {
|
|
694
|
+
clearStoredThread();
|
|
695
|
+
}
|
|
599
696
|
await client.sendMessage(session.conversationId, result.finalMessage, {
|
|
600
697
|
metadata: {
|
|
601
698
|
turnId: session.currentTurnId,
|
|
@@ -608,7 +705,7 @@ export async function main() {
|
|
|
608
705
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent reply (${result.finalMessage.length} chars)`);
|
|
609
706
|
}
|
|
610
707
|
else if (!result.interrupted && result.exitCode && result.exitCode !== 0) {
|
|
611
|
-
const userVisibleError =
|
|
708
|
+
const userVisibleError = formatCodexTurnFailure(result.errorText);
|
|
612
709
|
session.state.lastError = userVisibleError;
|
|
613
710
|
writeState(session);
|
|
614
711
|
if (result.errorText) {
|
|
@@ -637,7 +734,9 @@ export async function main() {
|
|
|
637
734
|
}
|
|
638
735
|
}
|
|
639
736
|
catch (error) {
|
|
640
|
-
const message =
|
|
737
|
+
const message = error instanceof ExecutionEnvironmentError
|
|
738
|
+
? error.userMessage
|
|
739
|
+
: `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
|
|
641
740
|
session.state.lastError = message;
|
|
642
741
|
writeState(session);
|
|
643
742
|
await client.sendMessage(session.conversationId, message, {
|
|
@@ -649,7 +748,7 @@ export async function main() {
|
|
|
649
748
|
},
|
|
650
749
|
}).catch(() => { });
|
|
651
750
|
await handoffFinalMessage(session.conversationId);
|
|
652
|
-
if (error instanceof Error &&
|
|
751
|
+
if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
|
|
653
752
|
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
654
753
|
}
|
|
655
754
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
@@ -678,6 +777,7 @@ export async function main() {
|
|
|
678
777
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
679
778
|
];
|
|
680
779
|
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
780
|
+
const codexModelOptions = buildCodexModelOptions(args.model);
|
|
681
781
|
let runtimeDescriptor = {
|
|
682
782
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
683
783
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
@@ -688,7 +788,7 @@ export async function main() {
|
|
|
688
788
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
689
789
|
: {}),
|
|
690
790
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
691
|
-
models:
|
|
791
|
+
models: codexModelOptions,
|
|
692
792
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
693
793
|
workspaceRoots: workspaceRootMetadata,
|
|
694
794
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -707,6 +807,18 @@ export async function main() {
|
|
|
707
807
|
lastSeenSignal.set(conversationId, timestamp);
|
|
708
808
|
}
|
|
709
809
|
}
|
|
810
|
+
async function baselineSessionControl(conversationId) {
|
|
811
|
+
if (lastSeenControl.has(conversationId))
|
|
812
|
+
return;
|
|
813
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
|
|
814
|
+
if (!raw || typeof raw !== 'object')
|
|
815
|
+
return;
|
|
816
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
817
|
+
if (timestamp > 0) {
|
|
818
|
+
lastSeenControl.set(conversationId, timestamp);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
let publishRuntimeDetailsInFlight = false;
|
|
710
822
|
const publishRuntimeHeartbeat = async () => {
|
|
711
823
|
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
712
824
|
agentId,
|
|
@@ -716,84 +828,99 @@ export async function main() {
|
|
|
716
828
|
});
|
|
717
829
|
if (!streamConnected)
|
|
718
830
|
return;
|
|
719
|
-
await refreshKnownConversationIds().catch((error) => {
|
|
720
|
-
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
721
|
-
});
|
|
722
831
|
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
723
832
|
console.error('[canon-codex] Failed to publish agent runtime:', error);
|
|
724
833
|
});
|
|
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
|
-
|
|
834
|
+
if (publishRuntimeDetailsInFlight)
|
|
835
|
+
return;
|
|
836
|
+
publishRuntimeDetailsInFlight = true;
|
|
837
|
+
try {
|
|
838
|
+
await refreshKnownConversationIds().catch((error) => {
|
|
839
|
+
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
840
|
+
});
|
|
841
|
+
await publishHostSessionSnapshots({
|
|
842
|
+
conversationIds: Array.from(knownConversationIds),
|
|
843
|
+
agentId,
|
|
844
|
+
clientType: 'codex',
|
|
845
|
+
runtime: runtimeDescriptor,
|
|
846
|
+
workspaceOptions,
|
|
847
|
+
defaultCwd: workingDir,
|
|
848
|
+
extraSessionConfigFields: ['permissionMode'],
|
|
849
|
+
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
850
|
+
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
851
|
+
return [
|
|
852
|
+
session.conversationId,
|
|
853
|
+
{
|
|
854
|
+
...(session.state.model ? { model: session.state.model } : {}),
|
|
855
|
+
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
856
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
857
|
+
executionMode: session.environment.mode,
|
|
858
|
+
executionBranch: session.environment.branch ?? null,
|
|
859
|
+
},
|
|
860
|
+
];
|
|
861
|
+
})),
|
|
862
|
+
}).catch((error) => {
|
|
863
|
+
console.error('[canon-codex] Failed to publish session snapshots:', error);
|
|
864
|
+
});
|
|
865
|
+
await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
|
|
866
|
+
const session = sessions.get(conversationId);
|
|
867
|
+
const workspaceId = session
|
|
868
|
+
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
869
|
+
: runtimeDescriptor.defaultWorkspaceId;
|
|
870
|
+
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
871
|
+
const descriptor = runtimeDescriptor.runtimeDescriptor;
|
|
872
|
+
if (!descriptor)
|
|
873
|
+
return;
|
|
874
|
+
const payload = {
|
|
875
|
+
descriptor,
|
|
876
|
+
surfaceMode: 'host',
|
|
877
|
+
statusItems: [
|
|
878
|
+
{
|
|
879
|
+
id: 'transport',
|
|
880
|
+
label: 'Transport',
|
|
881
|
+
value: 'exec --json',
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: 'streaming',
|
|
885
|
+
label: 'Live output',
|
|
886
|
+
value: 'Thinking, tools, and completed-message previews',
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: 'codex-cli',
|
|
890
|
+
label: 'Codex CLI',
|
|
891
|
+
value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
|
|
892
|
+
tone: codexCliStatus.version ? 'default' : 'warning',
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: 'nativeActions',
|
|
896
|
+
label: 'Native actions',
|
|
897
|
+
value: 'Limited until app-server transport',
|
|
898
|
+
tone: 'warning',
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
execution: {
|
|
902
|
+
resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
|
|
903
|
+
resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
|
|
904
|
+
workspaceRootId: workspace?.workspaceRootId ?? null,
|
|
905
|
+
workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
|
|
906
|
+
executionMode: session?.environment.mode ?? null,
|
|
907
|
+
executionBranch: session?.environment.branch ?? null,
|
|
908
|
+
worktreePath: session?.environment.worktreePath ?? null,
|
|
909
|
+
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
776
910
|
},
|
|
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
|
-
});
|
|
911
|
+
notes: [
|
|
912
|
+
'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.',
|
|
913
|
+
'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.',
|
|
914
|
+
],
|
|
915
|
+
};
|
|
916
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
917
|
+
})).catch((error) => {
|
|
918
|
+
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
finally {
|
|
922
|
+
publishRuntimeDetailsInFlight = false;
|
|
923
|
+
}
|
|
797
924
|
};
|
|
798
925
|
const stream = new CanonStream({
|
|
799
926
|
apiKey,
|
|
@@ -843,7 +970,7 @@ export async function main() {
|
|
|
843
970
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
844
971
|
: {}),
|
|
845
972
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
846
|
-
models:
|
|
973
|
+
models: codexModelOptions,
|
|
847
974
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
848
975
|
workspaceRoots: workspaceRootMetadata,
|
|
849
976
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -862,7 +989,7 @@ export async function main() {
|
|
|
862
989
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
863
990
|
: {}),
|
|
864
991
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
865
|
-
models:
|
|
992
|
+
models: codexModelOptions,
|
|
866
993
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
867
994
|
workspaceRoots: workspaceRootMetadata,
|
|
868
995
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -932,7 +1059,7 @@ export async function main() {
|
|
|
932
1059
|
catch (error) {
|
|
933
1060
|
console.error('[canon-codex] Failed to load startup conversations:', error);
|
|
934
1061
|
}
|
|
935
|
-
|
|
1062
|
+
startCodexStreamInBackground(stream, (error) => {
|
|
936
1063
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
937
1064
|
});
|
|
938
1065
|
const pollControl = async () => {
|
|
@@ -948,6 +1075,14 @@ export async function main() {
|
|
|
948
1075
|
const session = sessions.get(conversationId);
|
|
949
1076
|
if (session && !session.closed) {
|
|
950
1077
|
if (control.model && control.model !== session.state.model) {
|
|
1078
|
+
const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
|
|
1079
|
+
if (modelGuard) {
|
|
1080
|
+
session.state.lastError = modelGuard;
|
|
1081
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${modelGuard}`);
|
|
1082
|
+
writeState(session);
|
|
1083
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
951
1086
|
session.adapter.setModel(control.model);
|
|
952
1087
|
session.state.model = control.model;
|
|
953
1088
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
|
|
@@ -960,6 +1095,7 @@ export async function main() {
|
|
|
960
1095
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] effort control is not mapped yet (${control.effort})`);
|
|
961
1096
|
}
|
|
962
1097
|
}
|
|
1098
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
963
1099
|
}
|
|
964
1100
|
}
|
|
965
1101
|
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`);
|
|
@@ -980,14 +1116,14 @@ export async function main() {
|
|
|
980
1116
|
continue;
|
|
981
1117
|
}
|
|
982
1118
|
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
1119
|
if (signal.type === 'stop_and_drop') {
|
|
988
1120
|
const droppedPrompts = session.queue.splice(0);
|
|
989
1121
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
990
1122
|
}
|
|
1123
|
+
if (session.running) {
|
|
1124
|
+
await session.adapter.interrupt();
|
|
1125
|
+
}
|
|
1126
|
+
session.turnState = 'interrupted';
|
|
991
1127
|
writeTurn(session);
|
|
992
1128
|
clearStreaming(conversationId);
|
|
993
1129
|
client.setTyping(conversationId, false).catch(() => { });
|
|
@@ -1042,7 +1178,11 @@ export async function main() {
|
|
|
1042
1178
|
await new Promise(() => { });
|
|
1043
1179
|
}
|
|
1044
1180
|
runCli(import.meta.url, main, (error) => {
|
|
1045
|
-
|
|
1181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1182
|
+
console.error(`[canon-codex] ${message}`);
|
|
1046
1183
|
getActiveProfileLock()?.release();
|
|
1047
1184
|
process.exit(1);
|
|
1185
|
+
}, {
|
|
1186
|
+
name: 'canon-codex',
|
|
1187
|
+
help: HELP,
|
|
1048
1188
|
});
|
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.5",
|
|
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.1"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|