@canonmsg/codex-plugin 0.29.3 → 0.30.1

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/host.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { type TurnArtifactRoutingDecision, type TurnArtifactRoutingMode, type RecoveryCheckpointTracker } from '@canonmsg/coding-agent-host';
3
- import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type PreparedExecutionEnvironment, type WorkspaceOption, type CanonWorkspaceRootMetadata, type RuntimeStreamingPayload, type TurnLifecycleState, type TurnOutputBlock, type TurnVerbosity, type TurnVerbosityConfig } from '@canonmsg/core';
4
- import { type CodexSandboxMode } from './adapter.js';
5
- import { type CodexSkillMetadata } from './app-server-adapter.js';
3
+ import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimeFact, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type PreparedExecutionEnvironment, type WorkspaceOption, type CanonWorkspaceRootMetadata, type DeliveryIntent, type RuntimeStreamingPayload, type TurnLifecycleState, type TurnOutputBlock, type TurnVerbosity, type TurnVerbosityConfig } from '@canonmsg/core';
4
+ import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
5
+ import { CodexConversationAdapter, type CodexSandboxMode } from './adapter.js';
6
+ import { CodexAppServerAdapter, type CodexSkillMetadata } from './app-server-adapter.js';
6
7
  import { deriveCodexPermissionEnvelope } from './permission-mode.js';
8
+ import { type CommandBlockTracker } from './turn-activity.js';
7
9
  import { type CodexControlOption } from './model-catalog.js';
10
+ type CodexAdapter = CodexConversationAdapter | CodexAppServerAdapter;
8
11
  interface HostSessionState {
9
12
  lastError?: string;
10
13
  model?: string;
@@ -21,6 +24,63 @@ export declare function buildCodexInitialSessionState(input: {
21
24
  permissionMode?: string;
22
25
  effort?: string | null;
23
26
  }): HostSessionState;
27
+ type ArtifactRoutingMode = 'workspace-generated' | 'disabled';
28
+ interface Session {
29
+ conversationId: string;
30
+ cwd: string;
31
+ environment: PreparedExecutionEnvironment;
32
+ adapter: CodexAdapter;
33
+ queue: Array<{
34
+ prompt: string;
35
+ skillInvocationText?: string;
36
+ planMode?: boolean;
37
+ intent: DeliveryIntent;
38
+ sourceMessageId?: string | null;
39
+ markAccepted?: boolean;
40
+ imagePaths?: string[];
41
+ mediaAddDirs?: string[];
42
+ artifactRoutingMode?: ArtifactRoutingMode;
43
+ canUseCodexAppTools?: boolean;
44
+ /**
45
+ * Resolved when this message arrived and carried here, so a prompt that
46
+ * waits in the queue still runs under the answer it arrived with. Absent on
47
+ * a continuation prompt (a plan-review result), which inherits whatever the
48
+ * conversation last resolved.
49
+ */
50
+ turnVerbosity?: TurnVerbosity;
51
+ requestingUserId: string | null;
52
+ replyAuthority: AgentReplyAuthorityV1 | null;
53
+ }>;
54
+ running: boolean;
55
+ state: HostSessionState;
56
+ policyFingerprint: string;
57
+ turnState: TurnLifecycleState;
58
+ currentTurnId: string | null;
59
+ currentTurnOpenedAt: number | null;
60
+ currentTurnUpdatedAt: number | null;
61
+ currentTurnCanUseCodexAppTools: boolean;
62
+ currentReplyAuthority: AgentReplyAuthorityV1 | null;
63
+ /** Cancels Canon interactions created by the currently running Codex turn. */
64
+ currentTurnAbortController: AbortController | null;
65
+ /**
66
+ * The verbosity the RUNNING turn was opened with. Promoted off the queue
67
+ * entry at `runNextTurn` and never re-read mid-turn, so the live writer, the
68
+ * turn-state writer and the final's trail cannot disagree with each other.
69
+ */
70
+ turnVerbosity: TurnVerbosity;
71
+ /** The running turn called `no_reply`: end it without posting anything. */
72
+ currentTurnSilenced: boolean;
73
+ activeSelfContextId: string | null;
74
+ lastAcceptedIntent: DeliveryIntent | null;
75
+ resetRequested: boolean;
76
+ lastActivity: number;
77
+ typingKeepaliveTimer: ReturnType<typeof setInterval> | null;
78
+ closed: boolean;
79
+ turnLiveText: string;
80
+ turnBlocks: TurnOutputBlock[];
81
+ turnCommandBlocks: CommandBlockTracker;
82
+ }
83
+ export declare function buildCodexSessionFacts(session?: Pick<Session, 'closed' | 'adapter'>): CanonRuntimeFact[];
24
84
  export declare function createCodexRecoveryCheckpointTracker(persist: (messageId: string) => boolean): RecoveryCheckpointTracker;
25
85
  /** Conservative fallback used only when native app-server discovery is unavailable. */
26
86
  export declare const CODEX_EFFORT_OPTIONS: readonly CodexControlOption[];
package/dist/host.js CHANGED
@@ -18,11 +18,16 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
18
18
  import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
19
19
  import { startCodexStreamInBackground } from './host-lifecycle.js';
20
20
  import { createCodexControlPoller } from './control-channel.js';
21
- import { runCli } from '@canonmsg/core';
21
+ import { runCli, acquireLock } from '@canonmsg/core';
22
+ import { runCodexWorkSessionHost } from './work-session-cli.js';
23
+ import { CODEX_WORK_SESSION_HELP, codexAccountLockKey, isSharedWorkSessionCommand } from './work-session-options.js';
22
24
  import { applyTextSegmentBlock, beginCommandBlock, buildCodexFinalTurnTrail, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
23
25
  import { FALLBACK_CODEX_EFFORT_OPTIONS, buildCodexEffortOptions, buildCodexModelOptions, readCodexConfiguredEffort, resolveCodexDefaultModel, resolveCodexEffortForModel, } from './model-catalog.js';
26
+ let managedAccountLock;
24
27
  const HELP = `canon-codex — run a local Codex agent host for Canon
25
28
 
29
+ ${CODEX_WORK_SESSION_HELP}
30
+
26
31
  USAGE
27
32
  canon-codex [flags]
28
33
 
@@ -69,6 +74,23 @@ export function buildCodexInitialSessionState(input) {
69
74
  state: 'idle',
70
75
  };
71
76
  }
77
+ export function buildCodexSessionFacts(session) {
78
+ if (!session || session.closed)
79
+ return [];
80
+ const adapter = session.adapter;
81
+ const appServer = adapter instanceof CodexAppServerAdapter;
82
+ const facts = [{
83
+ id: 'runtime', label: 'Runtime', value: appServer ? 'Codex app-server' : 'Codex CLI', group: 'runtime',
84
+ }];
85
+ if (appServer) {
86
+ const observed = adapter.getObservedSettings();
87
+ if (observed.model)
88
+ facts.push({ id: 'model', label: 'Model', value: observed.model, group: 'model' });
89
+ if (observed.effort)
90
+ facts.push({ id: 'reasoning', label: 'Reasoning', value: observed.effort, group: 'model' });
91
+ }
92
+ return facts;
93
+ }
72
94
  const MAX_SESSIONS = 12;
73
95
  export function createCodexRecoveryCheckpointTracker(persist) {
74
96
  return createRecoveryCheckpointTracker(persist);
@@ -133,10 +155,10 @@ export function buildCodexRuntimeDescriptor(input) {
133
155
  const commands = [
134
156
  {
135
157
  id: 'runtime-status',
136
- label: 'Runtime status',
137
- description: 'Open Codex runtime details.',
158
+ label: 'Session info',
159
+ description: 'Open Canon session information.',
138
160
  primitive: 'runtime.status',
139
- aliases: ['status'],
161
+ aliases: ['session-info'],
140
162
  category: 'details',
141
163
  placements: ['composer_slash', 'command_palette'],
142
164
  availability: ['always'],
@@ -584,6 +606,10 @@ export function shouldRouteCodexTurnArtifacts(input) {
584
606
  }
585
607
  export async function main() {
586
608
  setDefaultResultOrder('ipv4first');
609
+ if (isSharedWorkSessionCommand(process.argv.slice(2))) {
610
+ await runCodexWorkSessionHost(process.argv.slice(2));
611
+ return;
612
+ }
587
613
  const { values: args } = parseArgs({
588
614
  options: {
589
615
  cwd: { type: 'string' },
@@ -687,6 +713,9 @@ export async function main() {
687
713
  }
688
714
  console.error(`[canon-codex] Authenticated as ${agentId}`);
689
715
  }
716
+ // One local owner across managed, standalone attach and multi-room shared modes,
717
+ // including profile aliases and directly supplied credentials for this account.
718
+ managedAccountLock = acquireLock(codexAccountLockKey({ environmentId: resolvedAgent.environmentId, agentId }));
690
719
  let codexDynamicTools = filterCodexCommunicationTools(baseCodexDynamicTools, outboundPolicy);
691
720
  // Shared poll/timeout engine. Built-in `input`/`card` descriptors own
692
721
  // create+poll (codex passes native/responder policy via payload/options).
@@ -1338,6 +1367,7 @@ export async function main() {
1338
1367
  function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, turn = {}) {
1339
1368
  const nextPrompt = {
1340
1369
  prompt,
1370
+ skillInvocationText: turn.skillInvocationText,
1341
1371
  intent,
1342
1372
  sourceMessageId,
1343
1373
  markAccepted,
@@ -1884,6 +1914,7 @@ export async function main() {
1884
1914
  if (session.running && deliveryIntent === 'interrupt') {
1885
1915
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, {
1886
1916
  ...resolveCodexTurnModes(participantContext, input.message),
1917
+ skillInvocationText: input.message.senderType === 'human' ? input.message.text ?? undefined : undefined,
1887
1918
  replyAuthority: input.replyAuthority ?? null,
1888
1919
  });
1889
1920
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
@@ -1895,6 +1926,7 @@ export async function main() {
1895
1926
  }
1896
1927
  enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, {
1897
1928
  ...resolveCodexTurnModes(participantContext, input.message),
1929
+ skillInvocationText: input.message.senderType === 'human' ? input.message.text ?? undefined : undefined,
1898
1930
  replyAuthority: input.replyAuthority ?? null,
1899
1931
  });
1900
1932
  }
@@ -2126,6 +2158,7 @@ export async function main() {
2126
2158
  };
2127
2159
  const runTurnOnce = () => session.adapter.runTurn(turnPrompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
2128
2160
  planMode: nextTurn.planMode,
2161
+ skillInvocationText: nextTurn.skillInvocationText,
2129
2162
  onServerRequest: (request) => handleCodexServerRequest(session, request, nextTurn.requestingUserId ?? null, nextTurn.sourceMessageId ?? null),
2130
2163
  });
2131
2164
  let result = await runTurnOnce();
@@ -2584,12 +2617,9 @@ export async function main() {
2584
2617
  return;
2585
2618
  await runtimeState.writeRuntimeInfo(conversationId, {
2586
2619
  surfaceMode: 'host',
2587
- descriptor: {
2588
- coreControls: [],
2589
- supportsInterrupt: descriptor.supportsInterrupt,
2590
- supportsInputInterrupt: descriptor.supportsInputInterrupt,
2591
- streamingTextMode: descriptor.streamingTextMode,
2592
- },
2620
+ // The shared publisher sanitizes configuration while retaining commands.
2621
+ descriptor,
2622
+ facts: buildCodexSessionFacts(sessions.get(conversationId)),
2593
2623
  });
2594
2624
  }));
2595
2625
  const failure = results.find((result) => result.status === 'rejected');
@@ -2802,6 +2832,7 @@ export async function main() {
2802
2832
  closeSession(session.conversationId);
2803
2833
  }
2804
2834
  markLocalRuntimeStopped(runtimeId);
2835
+ managedAccountLock?.release();
2805
2836
  (lockHandle ?? getActiveProfileLock())?.release();
2806
2837
  process.exit(0);
2807
2838
  };
@@ -2814,6 +2845,7 @@ export async function main() {
2814
2845
  runCli(import.meta.url, main, (error) => {
2815
2846
  const message = error instanceof Error ? error.message : String(error);
2816
2847
  console.error(`[canon-codex] ${message}`);
2848
+ managedAccountLock?.release();
2817
2849
  getActiveProfileLock()?.release();
2818
2850
  process.exit(1);
2819
2851
  }, {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  export { main as hostMain } from './host.js';
2
2
  export { main as registerMain } from './register.js';
3
3
  export { main as setupMain } from './setup.js';
4
+ export { main as attachMain } from './attach.js';
5
+ export { CodexAttachedSessionAdapter, listLoadedCodexSessions } from './attached-session-adapter.js';
6
+ export type { CodexAttachedSessionOptions } from './attached-session-adapter.js';
7
+ export { CodexWorkSessionProvider } from './work-session-provider.js';
8
+ export type { CodexWorkSessionProviderOptions } from './work-session-provider.js';
4
9
  export { extractCodexApprovalDiff, mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
5
10
  export type { CanonCodexApprovalRequest, CodexAppServerApprovalMethod, CodexApprovalDecision, CodexNativeApprovalRequest, } from './app-server-approval.js';
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { main as hostMain } from './host.js';
2
2
  export { main as registerMain } from './register.js';
3
3
  export { main as setupMain } from './setup.js';
4
+ export { main as attachMain } from './attach.js';
5
+ export { CodexAttachedSessionAdapter, listLoadedCodexSessions } from './attached-session-adapter.js';
6
+ export { CodexWorkSessionProvider } from './work-session-provider.js';
4
7
  export { extractCodexApprovalDiff, mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
package/dist/setup.js CHANGED
@@ -37,6 +37,11 @@ export function main() {
37
37
  console.log(' A git repo is not required; any readable directory works.');
38
38
  console.log(' Use --workspace-root to let Canon offer discovered projects inside an approved root.');
39
39
  console.log('');
40
+ console.log(' To share an existing native CLI thread instead, see:');
41
+ console.log(' canon-codex-attach --help');
42
+ console.log(' This experimental mode requires an explicitly shared local app-server.');
43
+ console.log(' Private Codex Desktop servers cannot currently be attached.');
44
+ console.log('');
40
45
  console.log('Optional flags:');
41
46
  console.log(' --model gpt-5.4');
42
47
  console.log(' --sandbox workspace-write');
@@ -0,0 +1,2 @@
1
+ /** Opt-in shared native runtime; never starts or takes ownership of the native server. */
2
+ export declare function runCodexWorkSessionHost(argv: string[]): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import { CANON_DIR, CanonClient, acquireLock, createFileAttachedSessionStore, resolveCanonAgent, resolveCanonProfile, verifyResolvedAgentEnvironment, } from '@canonmsg/core';
2
+ import { createCanonWorkSessionHost, createFileWorkSessionHostStore } from '@canonmsg/agent-sdk';
3
+ import { attachedSessionStatePath } from './attach-options.js';
4
+ import { CodexWorkSessionProvider } from './work-session-provider.js';
5
+ import { codexAccountLockKey, codexNativeSessionLockKey, parseCodexWorkSessionOptions, resolveWorkSessionInstallation, } from './work-session-options.js';
6
+ /** Opt-in shared native runtime; never starts or takes ownership of the native server. */
7
+ export async function runCodexWorkSessionHost(argv) {
8
+ const options = parseCodexWorkSessionOptions(argv);
9
+ const resolution = { logPrefix: '[canon-codex]', expectedClientType: 'codex', lock: true };
10
+ const profile = options.profile ? resolveCanonProfile(options.profile, resolution) : resolveCanonAgent(resolution);
11
+ let accountLock;
12
+ let provider;
13
+ let host;
14
+ let stopping = false;
15
+ let resolveShutdown = () => { };
16
+ const shutdown = new Promise((resolve) => { resolveShutdown = resolve; });
17
+ const stop = () => {
18
+ stopping = true;
19
+ void host?.stop().catch((error) => console.error('[canon-codex]', error));
20
+ resolveShutdown();
21
+ };
22
+ process.once('SIGINT', stop);
23
+ process.once('SIGTERM', stop);
24
+ process.once('SIGHUP', stop);
25
+ try {
26
+ await verifyResolvedAgentEnvironment(profile);
27
+ const agentId = profile.agentId ?? (await new CanonClient(profile.apiKey, profile.baseUrl).getAuthToken()).agentId;
28
+ accountLock = acquireLock(codexAccountLockKey({ environmentId: profile.environmentId, agentId }));
29
+ const installation = resolveWorkSessionInstallation(CANON_DIR, { environmentId: profile.environmentId, agentId });
30
+ provider = new CodexWorkSessionProvider({ ...options,
31
+ onError: (error) => console.error('[canon-codex] Native server:', error.message),
32
+ });
33
+ host = createCanonWorkSessionHost({ connection: { ...profile, agentId },
34
+ hostId: installation.hostId, displayName: profile.agentName ?? 'Codex work sessions', provider,
35
+ store: createFileWorkSessionHostStore(installation.journalPath),
36
+ createSessionStore: (binding) => createFileAttachedSessionStore(attachedSessionStatePath(CANON_DIR, binding)),
37
+ acquireNativeSession: (binding) => {
38
+ const lock = acquireLock(codexNativeSessionLockKey(binding.nativeSessionId));
39
+ return () => lock.release();
40
+ },
41
+ onStatus: (event) => {
42
+ if (event.status === 'available')
43
+ console.error('[canon-codex] Ready. In Canon choose Start or add work session. Keep this terminal and the shared Codex server running.');
44
+ else if (event.status === 'stopped') {
45
+ stopping = true;
46
+ resolveShutdown();
47
+ }
48
+ else
49
+ console.error('[canon-codex]', event.conversationId, event.session?.status);
50
+ },
51
+ onError: (error, operation) => console.error(`[canon-codex] ${operation}:`, error instanceof Error ? error.message : String(error)),
52
+ });
53
+ if (!stopping)
54
+ await host.start();
55
+ if (!stopping)
56
+ await shutdown;
57
+ }
58
+ finally {
59
+ process.removeListener('SIGINT', stop);
60
+ process.removeListener('SIGTERM', stop);
61
+ process.removeListener('SIGHUP', stop);
62
+ try {
63
+ await host?.stop();
64
+ }
65
+ finally {
66
+ try {
67
+ await provider?.close();
68
+ }
69
+ finally {
70
+ accountLock?.release();
71
+ profile.lockHandle?.release();
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,33 @@
1
+ export declare const CODEX_WORK_SESSION_HELP = "SHARED WORK SESSIONS (experimental; Codex 0.154 or newer)\n canon-codex --shared-server ws://127.0.0.1:4500 --cwd /path/to/project\n\n --shared-server <url> Explicit local WebSocket app-server\n --profile <name> Canon profile (or use CANON_AGENT)\n --cwd <path> Default permitted project\n --workspace <path> Additional permitted project; repeatable\n --workspace-root <path> Discover projects under a permitted root\n --model <id> Default model from native model/list\n --reasoning-effort <id> Default effort supported by that model\n --default-permission-mode <mode> readonly or workspace (default workspace)\n --permission-mode <mode> Allowed permission choices; repeatable\n --default-execution-mode <mode> locked or worktree (default worktree)\n --execution-mode <mode> Allowed execution choices; repeatable\n\nIn Canon choose Start or add work session. New sessions use on-request approval;\nthe account owner can answer native approvals and questions in Canon. Existing\nloaded sessions preserve their native settings. Native clients can keep using\nthe same thread. Ctrl-C stops sharing without stopping the native server.\nDesktop's private server is unsupported. Managed-host flags do not apply here.";
2
+ export declare function isSharedWorkSessionCommand(argv: string[]): boolean;
3
+ export declare function parseCodexWorkSessionOptions(argv: string[]): {
4
+ endpoint: string;
5
+ profile: string | undefined;
6
+ cwd: string;
7
+ workspaces: string[] | undefined;
8
+ workspaceRoots: string[] | undefined;
9
+ defaultModel: string | undefined;
10
+ defaultReasoningEffort: string | undefined;
11
+ defaultPermissionMode: "workspace" | "readonly";
12
+ defaultExecutionMode: "locked" | "worktree";
13
+ permissionModes: ("workspace" | "readonly")[] | undefined;
14
+ executionModes: ("locked" | "worktree")[] | undefined;
15
+ };
16
+ export declare function codexAccountKey(identity: {
17
+ environmentId: string;
18
+ agentId: string;
19
+ }): string;
20
+ /** Preserve the standalone attach key so older attachment processes also exclude a new host. */
21
+ export declare function codexAccountLockKey(identity: {
22
+ environmentId: string;
23
+ agentId: string;
24
+ }): string;
25
+ export declare function codexNativeSessionLockKey(nativeSessionId: string): string;
26
+ /** Call while owning the account lock; malformed existing state must never get a new identity. */
27
+ export declare function resolveWorkSessionInstallation(directory: string, identity: {
28
+ environmentId: string;
29
+ agentId: string;
30
+ }): {
31
+ hostId: string;
32
+ journalPath: string;
33
+ };
@@ -0,0 +1,100 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { validateAttachedSessionEndpoint } from './attached-session-adapter.js';
6
+ export const CODEX_WORK_SESSION_HELP = `SHARED WORK SESSIONS (experimental; Codex 0.154 or newer)
7
+ canon-codex --shared-server ws://127.0.0.1:4500 --cwd /path/to/project
8
+
9
+ --shared-server <url> Explicit local WebSocket app-server
10
+ --profile <name> Canon profile (or use CANON_AGENT)
11
+ --cwd <path> Default permitted project
12
+ --workspace <path> Additional permitted project; repeatable
13
+ --workspace-root <path> Discover projects under a permitted root
14
+ --model <id> Default model from native model/list
15
+ --reasoning-effort <id> Default effort supported by that model
16
+ --default-permission-mode <mode> readonly or workspace (default workspace)
17
+ --permission-mode <mode> Allowed permission choices; repeatable
18
+ --default-execution-mode <mode> locked or worktree (default worktree)
19
+ --execution-mode <mode> Allowed execution choices; repeatable
20
+
21
+ In Canon choose Start or add work session. New sessions use on-request approval;
22
+ the account owner can answer native approvals and questions in Canon. Existing
23
+ loaded sessions preserve their native settings. Native clients can keep using
24
+ the same thread. Ctrl-C stops sharing without stopping the native server.
25
+ Desktop's private server is unsupported. Managed-host flags do not apply here.`;
26
+ export function isSharedWorkSessionCommand(argv) {
27
+ return argv.some((arg) => arg === '--shared-server' || arg.startsWith('--shared-server='));
28
+ }
29
+ export function parseCodexWorkSessionOptions(argv) {
30
+ const { values } = parseArgs({ args: argv, strict: true, allowPositionals: false, options: {
31
+ 'shared-server': { type: 'string' }, profile: { type: 'string' }, cwd: { type: 'string' },
32
+ workspace: { type: 'string', multiple: true }, 'workspace-root': { type: 'string', multiple: true },
33
+ model: { type: 'string' }, 'reasoning-effort': { type: 'string' },
34
+ 'default-permission-mode': { type: 'string' }, 'permission-mode': { type: 'string', multiple: true },
35
+ 'default-execution-mode': { type: 'string' }, 'execution-mode': { type: 'string', multiple: true },
36
+ } });
37
+ function clean(value, label) {
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (!value.trim() || /[\u0000-\u001f\u007f]/.test(value))
41
+ throw new Error(`${label} must be non-empty and contain no control characters.`);
42
+ return value.trim();
43
+ }
44
+ function choices(raw, allowed, label) {
45
+ if (!raw)
46
+ return undefined;
47
+ return [...new Set(raw.map((value) => {
48
+ if (!allowed.includes(value))
49
+ throw new Error(`${label} must be ${allowed.join(' or ')}.`);
50
+ return value;
51
+ }))];
52
+ }
53
+ const permissionModes = choices(values['permission-mode'], ['readonly', 'workspace'], '--permission-mode');
54
+ const executionModes = choices(values['execution-mode'], ['locked', 'worktree'], '--execution-mode');
55
+ const defaultPermissionMode = choices([values['default-permission-mode'] ?? 'workspace'], ['readonly', 'workspace'], '--default-permission-mode')[0];
56
+ const defaultExecutionMode = choices([values['default-execution-mode'] ?? 'worktree'], ['locked', 'worktree'], '--default-execution-mode')[0];
57
+ if ((permissionModes && !permissionModes.includes(defaultPermissionMode)) || (executionModes && !executionModes.includes(defaultExecutionMode))) {
58
+ throw new Error('Default work-session policies must be included in the allowed choices.');
59
+ }
60
+ return {
61
+ endpoint: validateAttachedSessionEndpoint(clean(values['shared-server'], '--shared-server') ?? ''),
62
+ profile: clean(values.profile, '--profile'), cwd: clean(values.cwd, '--cwd') ?? process.cwd(),
63
+ workspaces: values.workspace?.map((value) => clean(value, '--workspace')),
64
+ workspaceRoots: values['workspace-root']?.map((value) => clean(value, '--workspace-root')),
65
+ defaultModel: clean(values.model, '--model'), defaultReasoningEffort: clean(values['reasoning-effort'], '--reasoning-effort'),
66
+ defaultPermissionMode, defaultExecutionMode, permissionModes, executionModes,
67
+ };
68
+ }
69
+ export function codexAccountKey(identity) {
70
+ return createHash('sha256').update(JSON.stringify([identity.environmentId, identity.agentId])).digest('hex');
71
+ }
72
+ /** Preserve the standalone attach key so older attachment processes also exclude a new host. */
73
+ export function codexAccountLockKey(identity) {
74
+ return `attached-${codexAccountKey(identity)}`;
75
+ }
76
+ export function codexNativeSessionLockKey(nativeSessionId) {
77
+ return `attached-native-${createHash('sha256').update(JSON.stringify(['codex', nativeSessionId])).digest('hex')}`;
78
+ }
79
+ /** Call while owning the account lock; malformed existing state must never get a new identity. */
80
+ export function resolveWorkSessionInstallation(directory, identity) {
81
+ const stateDirectory = join(directory, 'work-session-hosts', codexAccountKey(identity));
82
+ mkdirSync(stateDirectory, { recursive: true, mode: 0o700 });
83
+ const path = join(stateDirectory, 'installation.json');
84
+ let value;
85
+ try {
86
+ value = JSON.parse(readFileSync(path, 'utf8'));
87
+ }
88
+ catch (error) {
89
+ if (error.code !== 'ENOENT')
90
+ throw new Error('Work-session installation identity cannot be read; retain its state for recovery.', { cause: error });
91
+ value = { schema: 'canon.codex-work-session-installation.v1', ...identity, hostId: randomUUID() };
92
+ writeFileSync(path, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
93
+ }
94
+ const saved = value;
95
+ if (saved?.schema !== 'canon.codex-work-session-installation.v1' || saved.environmentId !== identity.environmentId
96
+ || saved.agentId !== identity.agentId || typeof saved.hostId !== 'string' || !/^[a-f0-9-]{36}$/.test(saved.hostId)) {
97
+ throw new Error('Work-session installation identity is invalid; retain its state for recovery.');
98
+ }
99
+ return { hostId: saved.hostId, journalPath: join(stateDirectory, 'journal.json') };
100
+ }
@@ -0,0 +1,40 @@
1
+ import { prepareConversationEnvironment, type NativeWorkSessionProvider, type NativeWorkSessionCreateContext, type NativeWorkSessionCreated, type NativeSessionInteractionBridge, type WorkSessionCatalog, type WorkSessionSelection } from '@canonmsg/core';
2
+ import { CodexAttachedSessionAdapter, type CodexSharedServerOptions } from './attached-session-adapter.js';
3
+ export interface CodexWorkSessionProviderOptions extends CodexSharedServerOptions {
4
+ cwd: string;
5
+ workspaces?: string[];
6
+ workspaceRoots?: string[];
7
+ defaultModel?: string;
8
+ defaultReasoningEffort?: string;
9
+ defaultPermissionMode?: 'readonly' | 'workspace';
10
+ defaultExecutionMode?: 'locked' | 'worktree';
11
+ /** Local policy ceiling. Never populated from a remote session request. */
12
+ permissionModes?: Array<'readonly' | 'workspace'>;
13
+ executionModes?: Array<'locked' | 'worktree'>;
14
+ /** Native worktrees remain on disk after the provider closes. */
15
+ prepareEnvironment?: typeof prepareConversationEnvironment;
16
+ }
17
+ /** One shared native server; all selections are resolved locally from the advertised catalog. */
18
+ export declare class CodexWorkSessionProvider implements NativeWorkSessionProvider {
19
+ private readonly options;
20
+ private readonly connection;
21
+ private readonly projects;
22
+ private readonly permissionModes;
23
+ private readonly executionModes;
24
+ private readonly observers;
25
+ private readonly environments;
26
+ private readonly createdSettings;
27
+ private readonly creationAttempts;
28
+ private readonly creationFingerprints;
29
+ private closed;
30
+ constructor(options: CodexWorkSessionProviderOptions);
31
+ catalog(): Promise<WorkSessionCatalog>;
32
+ create(selection: Extract<WorkSessionSelection, {
33
+ mode: 'create';
34
+ }>, context: NativeWorkSessionCreateContext): Promise<NativeWorkSessionCreated>;
35
+ private createOnce;
36
+ open(nativeSessionId: string, interactionBridge?: NativeSessionInteractionBridge): Promise<CodexAttachedSessionAdapter>;
37
+ close(): Promise<void>;
38
+ private assertOpen;
39
+ private models;
40
+ }