@canonmsg/codex-plugin 0.23.5 → 0.23.7

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 CHANGED
@@ -61,7 +61,6 @@ export declare class CodexConversationAdapter {
61
61
  private model;
62
62
  private reasoningEffort;
63
63
  private readonly sandbox;
64
- private readonly legacyApprovalPolicy;
65
64
  private readonly codexProfile;
66
65
  private readonly addDirs;
67
66
  private readonly configOverrides;
@@ -78,7 +77,6 @@ export declare class CodexConversationAdapter {
78
77
  model?: string | null;
79
78
  reasoningEffort?: string | null;
80
79
  sandbox?: CodexSandboxMode | null;
81
- approvalPolicy?: CodexApprovalPolicy | null;
82
80
  codexProfile?: string | null;
83
81
  addDirs?: string[];
84
82
  configOverrides?: string[];
package/dist/adapter.js CHANGED
@@ -7,7 +7,6 @@ export class CodexConversationAdapter {
7
7
  model;
8
8
  reasoningEffort;
9
9
  sandbox;
10
- legacyApprovalPolicy;
11
10
  codexProfile;
12
11
  addDirs;
13
12
  configOverrides;
@@ -24,7 +23,6 @@ export class CodexConversationAdapter {
24
23
  this.model = opts.model ?? null;
25
24
  this.reasoningEffort = opts.reasoningEffort ?? null;
26
25
  this.sandbox = opts.sandbox ?? null;
27
- this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
28
26
  this.codexProfile = opts.codexProfile ?? null;
29
27
  this.addDirs = opts.addDirs ?? [];
30
28
  this.configOverrides = opts.configOverrides ?? [];
@@ -1,4 +1,4 @@
1
- import type { CodexApprovalPolicy, CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
1
+ import type { CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
2
2
  export type JsonRecord = Record<string, unknown>;
3
3
  export interface CodexSkillMetadata {
4
4
  name: string;
@@ -27,7 +27,6 @@ export declare class CodexAppServerAdapter {
27
27
  private model;
28
28
  private reasoningEffort;
29
29
  private readonly sandbox;
30
- private readonly legacyApprovalPolicy;
31
30
  private readonly addDirs;
32
31
  private readonly configOverrides;
33
32
  private readonly fullAuto;
@@ -60,7 +59,6 @@ export declare class CodexAppServerAdapter {
60
59
  model?: string | null;
61
60
  reasoningEffort?: string | null;
62
61
  sandbox?: CodexSandboxMode | null;
63
- approvalPolicy?: CodexApprovalPolicy | null;
64
62
  addDirs?: string[];
65
63
  configOverrides?: string[];
66
64
  fullAuto?: boolean;
@@ -8,7 +8,6 @@ export class CodexAppServerAdapter {
8
8
  model;
9
9
  reasoningEffort;
10
10
  sandbox;
11
- legacyApprovalPolicy;
12
11
  addDirs;
13
12
  configOverrides;
14
13
  fullAuto;
@@ -42,7 +41,6 @@ export class CodexAppServerAdapter {
42
41
  this.reasoningEffort = opts.reasoningEffort ?? null;
43
42
  this.resolvedReasoningEffort = this.reasoningEffort;
44
43
  this.sandbox = opts.sandbox ?? null;
45
- this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
46
44
  this.addDirs = opts.addDirs ?? [];
47
45
  this.configOverrides = opts.configOverrides ?? [];
48
46
  this.fullAuto = opts.fullAuto ?? false;
@@ -198,9 +196,7 @@ export class CodexAppServerAdapter {
198
196
  }
199
197
  }
200
198
  resolveApprovalPolicy() {
201
- if (this.bypassApprovalsAndSandbox || this.fullAuto)
202
- return 'never';
203
- return this.legacyApprovalPolicy;
199
+ return (this.bypassApprovalsAndSandbox || this.fullAuto) ? 'never' : null;
204
200
  }
205
201
  configPayload() {
206
202
  const config = {};
package/dist/host.js CHANGED
@@ -5,9 +5,8 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
- import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, } from '@canonmsg/coding-agent-host';
9
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
- import { decideAutoReply, } from './inbound-policy.js';
8
+ import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
11
10
  import { CodexConversationAdapter, } from './adapter.js';
12
11
  import { CodexAppServerAdapter, } from './app-server-adapter.js';
13
12
  import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
@@ -18,8 +17,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
18
17
  import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
19
18
  import { startCodexStreamInBackground } from './host-lifecycle.js';
20
19
  import { createCodexControlPoller } from './control-channel.js';
21
- import { runCli } from './cli-entry.js';
22
- import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
20
+ import { runCli } from '@canonmsg/core';
23
21
  import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
24
22
  import { FALLBACK_CODEX_EFFORT_OPTIONS, buildCodexEffortOptions, buildCodexModelOptions, readCodexConfiguredEffort, resolveCodexDefaultModel, resolveCodexEffortForModel, } from './model-catalog.js';
25
23
  const HELP = `canon-codex — run a local Codex agent host for Canon
@@ -240,13 +238,14 @@ export function buildCodexTurnResponseRouting(input) {
240
238
  export function getCodexRequestingUserId(message) {
241
239
  return message.senderType === 'human' ? message.senderId : null;
242
240
  }
243
- async function publishAgentRuntime(agentId, runtime) {
244
- await publishHostAgentRuntime(agentId, 'codex', runtime);
241
+ async function publishAgentRuntime(agentId, runtime, rtdb) {
242
+ await publishHostAgentRuntime(agentId, 'codex', runtime, rtdb);
245
243
  }
246
- async function loadSessionConfig(conversationId, agentId) {
244
+ async function loadSessionConfig(conversationId, agentId, rtdb) {
247
245
  return loadHostSessionConfig({
248
246
  conversationId,
249
247
  agentId,
248
+ rtdb,
250
249
  extraStringFields: CODEX_SESSION_CONFIG_FIELDS,
251
250
  });
252
251
  }
@@ -286,9 +285,7 @@ function resolveCodexEffectiveRuntimePolicy(input) {
286
285
  }
287
286
  const approvalOverride = mapCanonPermissionToCodex(permissionMode);
288
287
  const defaultSandbox = (stringArg(input.args, 'sandbox') ?? null);
289
- const defaultApprovalPolicy = (stringArg(input.args, 'ask-for-approval') ?? null);
290
288
  const sandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
291
- const approvalPolicy = approvalOverride ? null : defaultApprovalPolicy;
292
289
  const fullAuto = approvalOverride ? approvalOverride.fullAuto : boolArg(input.args, 'full-auto');
293
290
  const bypassApprovalsAndSandbox = approvalOverride
294
291
  ? approvalOverride.bypassApprovalsAndSandbox
@@ -298,7 +295,9 @@ function resolveCodexEffectiveRuntimePolicy(input) {
298
295
  executionMode: input.environment.mode,
299
296
  permissionMode: permissionMode ?? null,
300
297
  sandbox,
301
- approvalPolicy,
298
+ // `--ask-for-approval` is rejected at startup, so this was always null.
299
+ // The key must stay: it is hashed into every persisted thread fingerprint.
300
+ approvalPolicy: null,
302
301
  fullAuto,
303
302
  bypassApprovalsAndSandbox,
304
303
  });
@@ -306,7 +305,6 @@ function resolveCodexEffectiveRuntimePolicy(input) {
306
305
  ...(model ? { model } : {}),
307
306
  ...(permissionMode ? { permissionMode } : {}),
308
307
  sandbox,
309
- approvalPolicy,
310
308
  fullAuto,
311
309
  bypassApprovalsAndSandbox,
312
310
  fingerprint,
@@ -620,6 +618,7 @@ export async function main() {
620
618
  agentId,
621
619
  clientType: 'codex',
622
620
  hostMode: true,
621
+ rtdb,
623
622
  });
624
623
  const sessions = new Map();
625
624
  const pendingSessionCreations = new Map();
@@ -939,7 +938,7 @@ export async function main() {
939
938
  const droppedPrompts = session.queue.splice(0);
940
939
  await markQueuedPromptsRejected(conversationId, droppedPrompts);
941
940
  rememberDroppedRecoveryCursor(session, droppedPrompts);
942
- clearStoredThreadId(runtimeId, agentId, conversationId, session.environment.baseCwd, session.environment.mode);
941
+ clearStoredThreadId(runtimeId, conversationId, session.environment.baseCwd, session.environment.mode);
943
942
  session.adapter.clearThreadId();
944
943
  session.activeSelfContextId = null;
945
944
  session.state.lastError = undefined;
@@ -989,7 +988,7 @@ export async function main() {
989
988
  evictOldestIdle();
990
989
  }
991
990
  const creation = (async () => {
992
- const config = await loadSessionConfig(conversationId, agentId);
991
+ const config = await loadSessionConfig(conversationId, agentId, rtdb);
993
992
  const sessionExecutionMode = resolveSessionExecutionMode(config);
994
993
  const workspaceCwd = resolveWorkspaceCwd(config);
995
994
  const environment = prepareConversationEnvironment({
@@ -1010,7 +1009,7 @@ export async function main() {
1010
1009
  if (modelGuard) {
1011
1010
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
1012
1011
  }
1013
- const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
1012
+ const storedThreadId = loadStoredThreadId(runtimeId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
1014
1013
  const effectiveModel = policy.model ?? codexDefaultModel;
1015
1014
  const initialEffortResolution = resolveCodexEffortForModel({
1016
1015
  models: codexModels,
@@ -1026,7 +1025,6 @@ export async function main() {
1026
1025
  model: policy.model ?? null,
1027
1026
  reasoningEffort: initialEffort,
1028
1027
  sandbox: policy.sandbox,
1029
- approvalPolicy: policy.approvalPolicy,
1030
1028
  addDirs: args['add-dir'] ?? [],
1031
1029
  configOverrides: args.config ?? [],
1032
1030
  fullAuto: policy.fullAuto,
@@ -1040,7 +1038,6 @@ export async function main() {
1040
1038
  model: policy.model ?? null,
1041
1039
  reasoningEffort: initialEffort,
1042
1040
  sandbox: policy.sandbox,
1043
- approvalPolicy: policy.approvalPolicy,
1044
1041
  codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
1045
1042
  addDirs: args['add-dir'] ?? [],
1046
1043
  configOverrides: args.config ?? [],
@@ -1565,7 +1562,7 @@ export async function main() {
1565
1562
  if (session.resetRequested) {
1566
1563
  return;
1567
1564
  }
1568
- saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
1565
+ saveStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
1569
1566
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
1570
1567
  return;
1571
1568
  }
@@ -1669,7 +1666,7 @@ export async function main() {
1669
1666
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
1670
1667
  };
1671
1668
  const clearStoredThread = () => {
1672
- clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
1669
+ clearStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, session.environment.mode);
1673
1670
  session.adapter.clearThreadId();
1674
1671
  };
1675
1672
  const runTurnOnce = () => session.adapter.runTurn(turnPrompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
@@ -1698,7 +1695,7 @@ export async function main() {
1698
1695
  }
1699
1696
  }
1700
1697
  if (result.threadId && !session.resetRequested) {
1701
- saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
1698
+ saveStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
1702
1699
  }
1703
1700
  if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
1704
1701
  await routeArtifactsOnce();
@@ -1831,7 +1828,7 @@ export async function main() {
1831
1828
  }).catch(() => { });
1832
1829
  await handoffFinalMessage(session.conversationId);
1833
1830
  if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
1834
- clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
1831
+ clearStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, session.environment.mode);
1835
1832
  }
1836
1833
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
1837
1834
  }
@@ -2109,7 +2106,7 @@ export async function main() {
2109
2106
  });
2110
2107
  if (!streamConnected)
2111
2108
  return;
2112
- await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
2109
+ await publishAgentRuntime(agentId, runtimeDescriptor, rtdb).catch((error) => {
2113
2110
  console.error('[canon-codex] Failed to publish agent runtime:', error);
2114
2111
  });
2115
2112
  if (publishRuntimeDetailsInFlight)
@@ -2122,6 +2119,7 @@ export async function main() {
2122
2119
  await publishHostSessionSnapshots({
2123
2120
  conversationIds: Array.from(knownConversationIds),
2124
2121
  agentId,
2122
+ rtdb,
2125
2123
  clientType: 'codex',
2126
2124
  runtime: runtimeDescriptor,
2127
2125
  workspaceOptions,
package/dist/register.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { setDefaultResultOrder } from 'node:dns';
3
- import { parseArgs } from 'node:util';
4
- import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, resolveCanonRuntimeConnection, loadProfiles, updatePendingRegistration, upsertAgentProfile, verifyCanonRuntimeConnection, } from '@canonmsg/core';
5
- import { runCli } from './cli-entry.js';
2
+ import { registerRegistrationCli, runRegistrationCli, } from '@canonmsg/core';
6
3
  const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
7
4
 
8
5
  USAGE
@@ -15,7 +12,7 @@ REQUIRED
15
12
 
16
13
  FLAGS
17
14
  --profile <name> Local profile name in ~/.canon/agents.json
18
- --environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID)
15
+ --environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID; default: canon-prod-v1)
19
16
  --base-url <url> Canon API base URL override
20
17
  --stream-url <url> Canon stream URL override
21
18
  --rtdb-url <url> Canon RTDB URL override
@@ -28,113 +25,17 @@ EXAMPLES
28
25
  canon-codex-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
29
26
 
30
27
  After approval, start it with CANON_AGENT=<profile> canon-codex --cwd /path/to/project.`;
28
+ const OPTIONS = {
29
+ moduleUrl: import.meta.url,
30
+ clientType: 'codex',
31
+ cliName: 'canon-codex-register',
32
+ hostBinName: 'canon-codex',
33
+ developerInfo: 'Codex host plugin',
34
+ registeringLabel: 'Codex agent',
35
+ usage: 'Usage: canon-codex-register --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]',
36
+ help: HELP,
37
+ };
31
38
  export async function main() {
32
- setDefaultResultOrder('ipv4first');
33
- const { values } = parseArgs({
34
- options: {
35
- name: { type: 'string' },
36
- description: { type: 'string' },
37
- phone: { type: 'string' },
38
- profile: { type: 'string' },
39
- environment: { type: 'string' },
40
- 'base-url': { type: 'string' },
41
- 'stream-url': { type: 'string' },
42
- 'rtdb-url': { type: 'string' },
43
- 'firebase-api-key': { type: 'string' },
44
- },
45
- strict: true,
46
- });
47
- if (!values.name || !values.description || !values.phone) {
48
- console.error('Usage: canon-codex-register --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]');
49
- process.exit(1);
50
- }
51
- const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
52
- const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
53
- if (!environmentId) {
54
- throw new Error('--environment or CANON_ENVIRONMENT_ID is required');
55
- }
56
- const connection = resolveCanonRuntimeConnection({
57
- environmentId,
58
- apiBaseUrl: values['base-url'],
59
- streamUrl: values['stream-url'],
60
- rtdbUrl: values['rtdb-url'],
61
- firebaseWebApiKey: values['firebase-api-key'],
62
- });
63
- await verifyCanonRuntimeConnection(connection);
64
- const existingProfile = loadProfiles()[profileName];
65
- const existingAgentId = existingProfile?.environmentId === connection.environmentId
66
- ? existingProfile.agentId
67
- : undefined;
68
- console.log(`Registering Codex agent "${values.name}" (profile: ${profileName})...`);
69
- const pending = getOrCreatePendingRegistration(profileName, 'codex', connection);
70
- const result = await registerAndWaitForApproval({
71
- name: values.name,
72
- description: values.description,
73
- ownerPhone: values.phone,
74
- developerInfo: 'Codex host plugin',
75
- clientType: 'codex',
76
- baseUrl: connection.apiBaseUrl,
77
- requestedAgentId: existingAgentId,
78
- localRegistrationId: pending.localRegistrationId,
79
- }, {
80
- onSubmitted: (requestId, pollToken) => {
81
- updatePendingRegistration(profileName, {
82
- requestId,
83
- pollToken,
84
- clientType: 'codex',
85
- });
86
- console.log(`Registration submitted (request ID: ${requestId}).`);
87
- console.log('Waiting for approval in Canon app...');
88
- },
89
- onPollUpdate: () => {
90
- process.stdout.write('.');
91
- },
92
- });
93
- console.log('');
94
- switch (result.status) {
95
- case 'approved': {
96
- if (!result.apiKey || !result.agentId || !result.agentName) {
97
- console.error('Approval completed but Canon did not return a usable API key. Run this command again to resume key pickup.');
98
- process.exit(1);
99
- }
100
- upsertAgentProfile(profileName, {
101
- apiKey: result.apiKey,
102
- agentId: result.agentId,
103
- agentName: result.agentName,
104
- registeredAt: new Date().toISOString(),
105
- environmentId: connection.environmentId,
106
- baseUrl: connection.apiBaseUrl,
107
- streamUrl: connection.streamUrl,
108
- rtdbUrl: connection.rtdbUrl,
109
- firebaseApiKey: connection.firebaseWebApiKey,
110
- clientType: 'codex',
111
- });
112
- if (result.requestId) {
113
- await ackRegistrationApproval(connection.apiBaseUrl, result.requestId, result.pollToken);
114
- }
115
- clearPendingRegistration(profileName);
116
- console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
117
- console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
118
- console.log('Start it with: CANON_AGENT=' + profileName + ' canon-codex --cwd /path/to/project');
119
- console.log('Keep that terminal open while you want Canon to reach the agent.');
120
- console.log('Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.');
121
- console.log('Docs: https://canonmail.com/agents/integrations');
122
- break;
123
- }
124
- case 'rejected':
125
- console.log('Registration was rejected.');
126
- process.exit(1);
127
- break;
128
- case 'timeout':
129
- console.log('Registration timed out (5 minutes). Try again later.');
130
- process.exit(1);
131
- break;
132
- }
39
+ await runRegistrationCli(OPTIONS);
133
40
  }
134
- runCli(import.meta.url, main, (error) => {
135
- console.error('[canon-codex-register] Fatal error:', error);
136
- process.exit(1);
137
- }, {
138
- name: 'canon-codex-register',
139
- help: HELP,
140
- });
41
+ registerRegistrationCli(OPTIONS);
@@ -8,6 +8,6 @@ export declare function buildCodexThreadPolicyFingerprint(input: {
8
8
  fullAuto?: boolean;
9
9
  bypassApprovalsAndSandbox?: boolean;
10
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;
13
- export declare function clearStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
11
+ export declare function loadStoredThreadId(runtimeId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): string | null;
12
+ export declare function saveStoredThreadId(runtimeId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): void;
13
+ export declare function clearStoredThreadId(runtimeId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
@@ -1,20 +1,5 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
1
  import { createHash } from 'node:crypto';
3
- import { join } from 'node:path';
4
- import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
5
- const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
6
- function loadStore() {
7
- try {
8
- return JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
9
- }
10
- catch {
11
- return { agents: {} };
12
- }
13
- }
14
- function saveStore(store) {
15
- mkdirSync(CANON_DIR, { recursive: true });
16
- writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
17
- }
2
+ import { clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
18
3
  export function buildCodexThreadPolicyFingerprint(input) {
19
4
  return createHash('sha256').update(JSON.stringify({
20
5
  version: 2,
@@ -27,91 +12,53 @@ export function buildCodexThreadPolicyFingerprint(input) {
27
12
  bypassApprovalsAndSandbox: input.bypassApprovalsAndSandbox === true,
28
13
  })).digest('hex').slice(0, 24);
29
14
  }
30
- export function loadStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode, policyFingerprint) {
31
- if (runtimeId) {
32
- const state = loadRuntimeSessionState(runtimeId, {
33
- conversationId,
34
- baseCwd,
35
- executionMode,
36
- });
37
- if (state?.threadId) {
38
- if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
39
- clearRuntimeSessionState(runtimeId, {
40
- conversationId,
41
- baseCwd,
42
- executionMode,
43
- });
44
- return null;
45
- }
46
- return state.threadId;
47
- }
48
- return null;
49
- }
50
- const store = loadStore();
51
- const record = store.agents[agentId]?.[conversationId];
52
- if (!record || record.cwd !== baseCwd)
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];
15
+ export function loadStoredThreadId(runtimeId, conversationId, baseCwd, executionMode, policyFingerprint) {
16
+ const state = loadRuntimeSessionState(runtimeId, {
17
+ conversationId,
18
+ baseCwd,
19
+ executionMode,
20
+ });
21
+ if (state?.threadId) {
22
+ if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
23
+ clearRuntimeSessionState(runtimeId, {
24
+ conversationId,
25
+ baseCwd,
26
+ executionMode,
27
+ });
28
+ return null;
58
29
  }
59
- saveStore(store);
60
- return null;
30
+ return state.threadId;
61
31
  }
62
- return record.threadId;
32
+ return null;
63
33
  }
64
- export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
65
- if (runtimeId) {
66
- saveRuntimeSessionState(runtimeId, {
67
- conversationId,
68
- baseCwd,
69
- executionMode,
70
- threadId,
71
- ...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
72
- });
73
- return;
74
- }
75
- const store = loadStore();
76
- store.agents[agentId] ??= {};
77
- store.agents[agentId][conversationId] = {
34
+ export function saveStoredThreadId(runtimeId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
35
+ saveRuntimeSessionState(runtimeId, {
36
+ conversationId,
37
+ baseCwd,
38
+ executionMode,
78
39
  threadId,
79
- cwd: baseCwd,
80
- ...(policyFingerprint ? { policyFingerprint } : {}),
81
- updatedAt: new Date().toISOString(),
82
- };
83
- saveStore(store);
40
+ ...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
41
+ });
84
42
  }
85
- export function clearStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode) {
86
- if (runtimeId) {
87
- const existing = baseCwd
88
- ? loadRuntimeSessionState(runtimeId, {
89
- conversationId,
90
- baseCwd,
91
- executionMode,
92
- })
93
- : null;
94
- clearRuntimeSessionState(runtimeId, {
43
+ export function clearStoredThreadId(runtimeId, conversationId, baseCwd, executionMode) {
44
+ const existing = baseCwd
45
+ ? loadRuntimeSessionState(runtimeId, {
95
46
  conversationId,
96
47
  baseCwd,
97
48
  executionMode,
49
+ })
50
+ : null;
51
+ clearRuntimeSessionState(runtimeId, {
52
+ conversationId,
53
+ baseCwd,
54
+ executionMode,
55
+ });
56
+ if (existing?.lastInboundMessageId && baseCwd) {
57
+ saveRuntimeSessionState(runtimeId, {
58
+ conversationId,
59
+ baseCwd,
60
+ ...(executionMode ? { executionMode } : {}),
61
+ lastInboundMessageId: existing.lastInboundMessageId,
98
62
  });
99
- if (existing?.lastInboundMessageId && baseCwd) {
100
- saveRuntimeSessionState(runtimeId, {
101
- conversationId,
102
- baseCwd,
103
- ...(executionMode ? { executionMode } : {}),
104
- lastInboundMessageId: existing.lastInboundMessageId,
105
- });
106
- }
107
- return;
108
- }
109
- const store = loadStore();
110
- if (!store.agents[agentId]?.[conversationId])
111
- return;
112
- delete store.agents[agentId][conversationId];
113
- if (Object.keys(store.agents[agentId]).length === 0) {
114
- delete store.agents[agentId];
115
63
  }
116
- saveStore(store);
117
64
  }
package/dist/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
- import { runCli } from './cli-entry.js';
3
+ import { runCli } from '@canonmsg/core';
4
4
  const HELP = `canon-codex-setup — print Canon Codex setup guidance
5
5
 
6
6
  USAGE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.23.5",
3
+ "version": "0.23.7",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^7.1.3",
33
- "@canonmsg/coding-agent-host": "^0.3.1",
34
- "@canonmsg/core": "^8.2.0"
32
+ "@canonmsg/agent-sdk": "^8.1.0",
33
+ "@canonmsg/coding-agent-host": "^0.4.0",
34
+ "@canonmsg/core": "^9.2.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"
@@ -1,2 +0,0 @@
1
- export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
2
- export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
package/dist/cli-entry.js DELETED
@@ -1 +0,0 @@
1
- export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
@@ -1,22 +0,0 @@
1
- import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
2
- export interface InboundParticipantContext {
3
- conversationType: CanonConversation['type'] | 'unknown';
4
- memberCount: number | null;
5
- senderType: 'human' | 'ai_agent';
6
- senderName: string;
7
- isOwner: boolean;
8
- mentionedAgent: boolean;
9
- groupContext?: CanonGroupContext;
10
- groupContextMode?: CanonGroupContextMode;
11
- recentSenderTypes: Array<'human' | 'ai_agent'>;
12
- recentHumanCount: number;
13
- recentAgentCount: number;
14
- consecutiveAgentTurns: number;
15
- currentAgentStreakStartedByHuman: boolean;
16
- }
17
- export interface AutoReplyDecision {
18
- allow: boolean;
19
- reason: string;
20
- }
21
- export declare function buildInboundContextLines(context: InboundParticipantContext): string[];
22
- export declare function decideAutoReply(context: InboundParticipantContext, behavior?: ResolvedAgentBehaviorPolicy | null): AutoReplyDecision;
@@ -1,46 +0,0 @@
1
- import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
2
- function formatRecentSenders(senderTypes) {
3
- if (senderTypes.length === 0)
4
- return 'none';
5
- return senderTypes.map((senderType) => (senderType === 'ai_agent' ? 'agent' : 'human')).join(' -> ');
6
- }
7
- export function buildInboundContextLines(context) {
8
- const conversationTypeLabel = context.conversationType === 'unknown'
9
- ? 'unknown'
10
- : `${context.conversationType}${context.memberCount ? ` (${context.memberCount} members)` : ''}`;
11
- const senderRole = context.isOwner
12
- ? 'The latest sender is the verified human owner of this Canon agent.'
13
- : context.senderType === 'ai_agent'
14
- ? 'The latest sender is another AI agent in Canon.'
15
- : 'The latest sender is a human Canon participant.';
16
- return [
17
- senderRole,
18
- `Latest sender name: ${context.senderName}`,
19
- `Latest sender type: ${context.senderType}`,
20
- `Conversation type: ${conversationTypeLabel}`,
21
- ...(context.groupContext && context.groupContextMode
22
- ? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
23
- : []),
24
- `Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
25
- `Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
26
- `Recent human messages: ${context.recentHumanCount}`,
27
- `Recent agent messages: ${context.recentAgentCount}`,
28
- `Consecutive recent agent turns: ${context.consecutiveAgentTurns}`,
29
- `Current agent streak started after a human message: ${context.currentAgentStreakStartedByHuman ? 'yes' : 'no'}`,
30
- ];
31
- }
32
- export function decideAutoReply(context, behavior) {
33
- const decision = evaluateParticipationPolicy(behavior ?? resolveAgentBehaviorPolicy(), {
34
- conversationType: context.conversationType,
35
- senderType: context.senderType,
36
- isOwner: context.isOwner,
37
- mentionedAgent: context.mentionedAgent,
38
- recentHumanCount: context.recentHumanCount,
39
- consecutiveAgentTurns: context.consecutiveAgentTurns,
40
- currentAgentStreakStartedByHuman: context.currentAgentStreakStartedByHuman,
41
- });
42
- return {
43
- allow: decision.allow,
44
- reason: decision.reason,
45
- };
46
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Startup recovery for inbound messages missed while the host was offline.
3
- *
4
- * The host persists a `lastInboundMessageId` cursor per conversation. On
5
- * startup we paginate `getMessagesPage` (newest-first pages, older pages via
6
- * its `before` message-id parameter) until the cursor is found or a hard
7
- * per-conversation bound is hit, then replay everything after the cursor.
8
- *
9
- * This module is intentionally identical in packages/claude-code-plugin and
10
- * packages/codex-plugin — keep both copies in sync (future consolidation
11
- * candidate).
12
- */
13
- export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
14
- export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
15
- export interface StartupRecoveryMessage {
16
- id: string;
17
- senderId: string;
18
- createdAt?: string;
19
- metadata?: Record<string, unknown>;
20
- }
21
- export interface StartupRecoveryPage {
22
- messages: StartupRecoveryMessage[];
23
- }
24
- export type StartupRecoveryMode =
25
- /** Cursor found — `messages` is everything strictly after it. */
26
- 'after-cursor'
27
- /** Cursor present but not found within the bound — `messages` is the bounded recent window. */
28
- | 'truncated-window'
29
- /**
30
- * No usable cursor (fresh runtime file, or the cursor message no longer
31
- * exists in history) — only the newest inbound message is recovered, since
32
- * a full-history replay could fire mass duplicate turns.
33
- */
34
- | 'latest-only';
35
- export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
36
- mode: StartupRecoveryMode;
37
- /** Missed inbound messages (own messages excluded), oldest first. */
38
- messages: TPage['messages'];
39
- /** First page fetched — reusable as hydration context for recovered turns. */
40
- newestPage: TPage;
41
- }
42
- export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
43
- fetchPage: (before?: string) => Promise<TPage>;
44
- cursor: string | null | undefined;
45
- agentId: string;
46
- maxMessages?: number;
47
- }): Promise<StartupRecoveryResult<TPage>>;
@@ -1,72 +0,0 @@
1
- /**
2
- * Startup recovery for inbound messages missed while the host was offline.
3
- *
4
- * The host persists a `lastInboundMessageId` cursor per conversation. On
5
- * startup we paginate `getMessagesPage` (newest-first pages, older pages via
6
- * its `before` message-id parameter) until the cursor is found or a hard
7
- * per-conversation bound is hit, then replay everything after the cursor.
8
- *
9
- * This module is intentionally identical in packages/claude-code-plugin and
10
- * packages/codex-plugin — keep both copies in sync (future consolidation
11
- * candidate).
12
- */
13
- export const STARTUP_RECOVERY_PAGE_SIZE = 25;
14
- export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
15
- export async function collectMissedInboundMessages(input) {
16
- const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
17
- const newestPage = await input.fetchPage();
18
- const collected = [...newestPage.messages];
19
- const seenIds = new Set(collected.map((message) => message.id));
20
- const hasCursor = (messages) => input.cursor != null && messages.some((message) => message.id === input.cursor);
21
- let cursorFound = hasCursor(collected);
22
- if (input.cursor != null) {
23
- while (!cursorFound && collected.length < maxMessages) {
24
- // Pages are newest-first, so the last collected message is the oldest.
25
- const before = collected[collected.length - 1]?.id;
26
- if (!before)
27
- break;
28
- const page = await input.fetchPage(before);
29
- const fresh = page.messages.filter((message) => !seenIds.has(message.id));
30
- // No pagination progress (history exhausted, or the server ignored the
31
- // `before` cursor because that message was hard-deleted) — stop here.
32
- if (fresh.length === 0)
33
- break;
34
- for (const message of fresh)
35
- seenIds.add(message.id);
36
- collected.push(...fresh);
37
- cursorFound = hasCursor(fresh);
38
- }
39
- }
40
- // The REST API already gives us a deterministic newest-first sequence,
41
- // including messages with equal or missing timestamps. Reverse that wire
42
- // order rather than re-sorting on a lossy display timestamp.
43
- const ascending = [...collected].reverse();
44
- const completedSourceMessageIds = new Set(collected.flatMap((message) => {
45
- if (message.senderId !== input.agentId)
46
- return [];
47
- const metadata = message.metadata;
48
- if (metadata?.turnSemantics !== 'turn_complete')
49
- return [];
50
- return typeof metadata.sourceMessageId === 'string'
51
- ? [metadata.sourceMessageId]
52
- : [];
53
- }));
54
- const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId && !completedSourceMessageIds.has(message.id));
55
- let mode;
56
- let missed;
57
- if (cursorFound) {
58
- const cursorIndex = ascending.findIndex((message) => message.id === input.cursor);
59
- mode = 'after-cursor';
60
- missed = inboundOnly(ascending.slice(cursorIndex + 1));
61
- }
62
- else if (input.cursor != null && collected.length >= maxMessages) {
63
- mode = 'truncated-window';
64
- missed = inboundOnly(ascending.slice(-maxMessages));
65
- }
66
- else {
67
- mode = 'latest-only';
68
- missed = inboundOnly(ascending).slice(-1);
69
- }
70
- // Safe: `missed` only holds elements of pages returned by `fetchPage`.
71
- return { mode, messages: missed, newestPage };
72
- }