@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/README.md +151 -1
- package/dist/adapter.d.ts +2 -0
- package/dist/app-server-adapter.d.ts +5 -0
- package/dist/app-server-adapter.js +26 -7
- package/dist/attach-options.d.ts +21 -0
- package/dist/attach-options.js +71 -0
- package/dist/attach.d.ts +2 -0
- package/dist/attach.js +101 -0
- package/dist/attached-session-adapter.d.ts +112 -0
- package/dist/attached-session-adapter.js +769 -0
- package/dist/host.d.ts +63 -3
- package/dist/host.js +42 -10
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/setup.js +5 -0
- package/dist/work-session-cli.d.ts +2 -0
- package/dist/work-session-cli.js +75 -0
- package/dist/work-session-options.d.ts +33 -0
- package/dist/work-session-options.js +100 -0
- package/dist/work-session-provider.d.ts +40 -0
- package/dist/work-session-provider.js +234 -0
- package/package.json +14 -13
- package/scripts/smoke-attach.mjs +222 -0
- package/scripts/smoke-work-sessions.mjs +218 -0
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 {
|
|
5
|
-
import { type
|
|
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: '
|
|
137
|
-
description: 'Open
|
|
158
|
+
label: 'Session info',
|
|
159
|
+
description: 'Open Canon session information.',
|
|
138
160
|
primitive: 'runtime.status',
|
|
139
|
-
aliases: ['
|
|
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
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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,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
|
+
}
|