@canonmsg/codex-plugin 0.29.4 → 0.31.0
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/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 +5 -0
- package/dist/host.js +47 -14
- 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
|
@@ -247,6 +247,11 @@ export declare function shouldRouteCodexTurnArtifacts(input: {
|
|
|
247
247
|
artifactRoutingMode: TurnArtifactRoutingMode | undefined;
|
|
248
248
|
silenced: boolean;
|
|
249
249
|
finalText: string | null | undefined;
|
|
250
|
+
currentAudience?: {
|
|
251
|
+
memberIds?: readonly string[];
|
|
252
|
+
agentId: string;
|
|
253
|
+
ownerId: string | null | undefined;
|
|
254
|
+
};
|
|
250
255
|
}): TurnArtifactRoutingDecision;
|
|
251
256
|
export declare function main(): Promise<void>;
|
|
252
257
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
8
8
|
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, 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, createRuntimeHeartbeat, 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, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
9
|
+
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildCanonGroupContext, buildCompactGroupContextLines, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createRuntimeHeartbeat, 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, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
10
|
import { validateCard } from '@canonmsg/rich-cards';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
12
|
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
@@ -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
|
|
|
@@ -594,13 +599,21 @@ export function selectCodexNativeImagePaths(imagePaths, nativeVisionEnabled) {
|
|
|
594
599
|
* does not currently carry into the catch without a race.
|
|
595
600
|
*/
|
|
596
601
|
export function shouldRouteCodexTurnArtifacts(input) {
|
|
602
|
+
const audience = input.currentAudience;
|
|
603
|
+
const ownerPair = !audience || (audience.ownerId !== audience.agentId
|
|
604
|
+
&& Boolean(audience.ownerId) && audience.memberIds?.length === 2
|
|
605
|
+
&& audience.memberIds.includes(audience.agentId) && audience.memberIds.includes(audience.ownerId));
|
|
597
606
|
return resolveTurnArtifactRouting({
|
|
598
|
-
artifactRoutingMode: input.artifactRoutingMode,
|
|
607
|
+
artifactRoutingMode: ownerPair ? input.artifactRoutingMode : 'disabled',
|
|
599
608
|
silenced: isSilentTurnSuppressed({ silenced: input.silenced, finalText: input.finalText }),
|
|
600
609
|
});
|
|
601
610
|
}
|
|
602
611
|
export async function main() {
|
|
603
612
|
setDefaultResultOrder('ipv4first');
|
|
613
|
+
if (isSharedWorkSessionCommand(process.argv.slice(2))) {
|
|
614
|
+
await runCodexWorkSessionHost(process.argv.slice(2));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
604
617
|
const { values: args } = parseArgs({
|
|
605
618
|
options: {
|
|
606
619
|
cwd: { type: 'string' },
|
|
@@ -704,6 +717,9 @@ export async function main() {
|
|
|
704
717
|
}
|
|
705
718
|
console.error(`[canon-codex] Authenticated as ${agentId}`);
|
|
706
719
|
}
|
|
720
|
+
// One local owner across managed, standalone attach and multi-room shared modes,
|
|
721
|
+
// including profile aliases and directly supplied credentials for this account.
|
|
722
|
+
managedAccountLock = acquireLock(codexAccountLockKey({ environmentId: resolvedAgent.environmentId, agentId }));
|
|
707
723
|
let codexDynamicTools = filterCodexCommunicationTools(baseCodexDynamicTools, outboundPolicy);
|
|
708
724
|
// Shared poll/timeout engine. Built-in `input`/`card` descriptors own
|
|
709
725
|
// create+poll (codex passes native/responder policy via payload/options).
|
|
@@ -842,6 +858,7 @@ export async function main() {
|
|
|
842
858
|
if (cached) {
|
|
843
859
|
conversationCache.set(payload.conversationId, {
|
|
844
860
|
...cached,
|
|
861
|
+
...payload.changes,
|
|
845
862
|
memberIds,
|
|
846
863
|
});
|
|
847
864
|
}
|
|
@@ -850,11 +867,12 @@ export async function main() {
|
|
|
850
867
|
}
|
|
851
868
|
if (!memberIds.includes(agentId)) {
|
|
852
869
|
knownConversationIds.delete(payload.conversationId);
|
|
853
|
-
|
|
870
|
+
pendingMembershipChanges.delete(payload.conversationId);
|
|
871
|
+
closeSession(payload.conversationId);
|
|
854
872
|
}
|
|
855
873
|
}
|
|
856
874
|
function getGroupContextMode(conversationId, conversation) {
|
|
857
|
-
if (conversation
|
|
875
|
+
if (!conversation)
|
|
858
876
|
return undefined;
|
|
859
877
|
if (pendingMembershipChanges.has(conversationId))
|
|
860
878
|
return 'membership_change';
|
|
@@ -872,7 +890,7 @@ export async function main() {
|
|
|
872
890
|
}
|
|
873
891
|
async function loadHydratedInboundContext(input) {
|
|
874
892
|
const [conversation, page] = await Promise.all([
|
|
875
|
-
getConversationMeta(input.conversationId),
|
|
893
|
+
getConversationMeta(input.conversationId, { refreshIfMemberMissing: agentId }),
|
|
876
894
|
input.hydratedPage
|
|
877
895
|
? Promise.resolve(input.hydratedPage)
|
|
878
896
|
: client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
@@ -881,6 +899,7 @@ export async function main() {
|
|
|
881
899
|
agentId,
|
|
882
900
|
conversationId: input.conversationId,
|
|
883
901
|
conversation,
|
|
902
|
+
requireAgentMembership: true,
|
|
884
903
|
page,
|
|
885
904
|
activeSelfContextId: input.activeSelfContextId,
|
|
886
905
|
selfContexts: input.selfContexts,
|
|
@@ -1192,6 +1211,10 @@ export async function main() {
|
|
|
1192
1211
|
}
|
|
1193
1212
|
}
|
|
1194
1213
|
async function getOrCreateSession(conversationId) {
|
|
1214
|
+
const current = conversationCache.get(conversationId);
|
|
1215
|
+
if (current && !current.memberIds.includes(agentId)) {
|
|
1216
|
+
throw new Error('Agent is no longer a member of this conversation');
|
|
1217
|
+
}
|
|
1195
1218
|
knownConversationIds.add(conversationId);
|
|
1196
1219
|
const existing = sessions.get(conversationId);
|
|
1197
1220
|
if (existing && !existing.closed) {
|
|
@@ -1918,8 +1941,9 @@ export async function main() {
|
|
|
1918
1941
|
replyAuthority: input.replyAuthority ?? null,
|
|
1919
1942
|
});
|
|
1920
1943
|
}
|
|
1921
|
-
function sendTurnArtifactFile(session, file) {
|
|
1944
|
+
function sendTurnArtifactFile(session, file, canPublish) {
|
|
1922
1945
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
1946
|
+
canPublish,
|
|
1923
1947
|
...(session.currentReplyAuthority ? { replyAuthority: session.currentReplyAuthority } : {}),
|
|
1924
1948
|
...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
|
|
1925
1949
|
metadata: {
|
|
@@ -1989,22 +2013,29 @@ export async function main() {
|
|
|
1989
2013
|
// the collect-and-post loop, so there is no un-gated path left to the
|
|
1990
2014
|
// workspace: the failure branches still send Canon's notice, and none of
|
|
1991
2015
|
// them can post the artifacts of a turn that chose silence.
|
|
2016
|
+
const decideArtifactRouting = () => shouldRouteCodexTurnArtifacts({
|
|
2017
|
+
artifactRoutingMode: nextTurn.artifactRoutingMode,
|
|
2018
|
+
silenced: session.currentTurnSilenced,
|
|
2019
|
+
finalText: turnFinalText,
|
|
2020
|
+
currentAudience: { memberIds: conversationCache.get(session.conversationId)?.memberIds, agentId, ownerId },
|
|
2021
|
+
});
|
|
1992
2022
|
const artifactRouter = createTurnArtifactRouter({
|
|
1993
|
-
decide:
|
|
1994
|
-
artifactRoutingMode: nextTurn.artifactRoutingMode,
|
|
1995
|
-
silenced: session.currentTurnSilenced,
|
|
1996
|
-
finalText: turnFinalText,
|
|
1997
|
-
}),
|
|
2023
|
+
decide: decideArtifactRouting,
|
|
1998
2024
|
baseline: () => artifactBaseline,
|
|
1999
2025
|
cwd: () => session.cwd,
|
|
2000
|
-
send: (file) => sendTurnArtifactFile(session, file),
|
|
2026
|
+
send: (file) => sendTurnArtifactFile(session, file, () => decideArtifactRouting().route),
|
|
2001
2027
|
log: (line) => console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`),
|
|
2002
2028
|
});
|
|
2003
2029
|
const routeArtifactsOnce = artifactRouter.route;
|
|
2004
2030
|
try {
|
|
2005
2031
|
const turnId = session.currentTurnId ?? randomUUID();
|
|
2006
2032
|
session.currentTurnId = turnId;
|
|
2007
|
-
|
|
2033
|
+
// Queueing does not freeze the audience: refresh at the native turn
|
|
2034
|
+
// boundary while retaining the same thread and its existing memory.
|
|
2035
|
+
const roster = buildCanonGroupContext({
|
|
2036
|
+
conversation: conversationCache.get(session.conversationId), messages: [], agentId, ownerId, ownerName,
|
|
2037
|
+
});
|
|
2038
|
+
let turnPrompt = [nextTurn.prompt, ...(roster ? buildCompactGroupContextLines(roster, 'initial') : [])].join('\n\n');
|
|
2008
2039
|
if (nextTurn.artifactRoutingMode === 'workspace-generated') {
|
|
2009
2040
|
artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
|
|
2010
2041
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
|
|
@@ -2820,6 +2851,7 @@ export async function main() {
|
|
|
2820
2851
|
closeSession(session.conversationId);
|
|
2821
2852
|
}
|
|
2822
2853
|
markLocalRuntimeStopped(runtimeId);
|
|
2854
|
+
managedAccountLock?.release();
|
|
2823
2855
|
(lockHandle ?? getActiveProfileLock())?.release();
|
|
2824
2856
|
process.exit(0);
|
|
2825
2857
|
};
|
|
@@ -2832,6 +2864,7 @@ export async function main() {
|
|
|
2832
2864
|
runCli(import.meta.url, main, (error) => {
|
|
2833
2865
|
const message = error instanceof Error ? error.message : String(error);
|
|
2834
2866
|
console.error(`[canon-codex] ${message}`);
|
|
2867
|
+
managedAccountLock?.release();
|
|
2835
2868
|
getActiveProfileLock()?.release();
|
|
2836
2869
|
process.exit(1);
|
|
2837
2870
|
}, {
|
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
|
+
}
|