@canonmsg/codex-plugin 0.26.1 → 0.26.2
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 +4 -0
- package/dist/host.d.ts +28 -0
- package/dist/host.js +107 -30
- package/dist/session-store.d.ts +2 -0
- package/dist/session-store.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,10 @@ model cannot select another user, conversation, or responder. It can display a
|
|
|
79
79
|
card with `send_card`, wait for an action card with `request_card`, or ask one
|
|
80
80
|
standalone question with `request_input`.
|
|
81
81
|
|
|
82
|
+
Service-agent sessions use the configured shared workspace and read-only local
|
|
83
|
+
filesystem automatically. They do not ask conversation members to choose a
|
|
84
|
+
coding workspace, execution mode, model, reasoning level, or permission mode.
|
|
85
|
+
|
|
82
86
|
Authorized conversation members may use a service agent even when they do not
|
|
83
87
|
own its Canon identity. Normal coding-host sessions keep their existing
|
|
84
88
|
owner-only execution boundary.
|
package/dist/host.d.ts
CHANGED
|
@@ -36,6 +36,19 @@ export declare function buildCodexLiveSessionConfig(input: {
|
|
|
36
36
|
permissionMode?: string | undefined;
|
|
37
37
|
model?: string | undefined;
|
|
38
38
|
};
|
|
39
|
+
export declare function buildCodexServiceSnapshotConfig(input: {
|
|
40
|
+
model?: string;
|
|
41
|
+
permissionMode?: string;
|
|
42
|
+
effort?: string;
|
|
43
|
+
workspaceId?: string;
|
|
44
|
+
}): {
|
|
45
|
+
model: string | undefined;
|
|
46
|
+
permissionMode: string | undefined;
|
|
47
|
+
effort: string | undefined;
|
|
48
|
+
workspaceId: string | undefined;
|
|
49
|
+
executionMode: "locked";
|
|
50
|
+
executionBranch: null;
|
|
51
|
+
};
|
|
39
52
|
/** Conservative fallback used only when native app-server discovery is unavailable. */
|
|
40
53
|
export declare const CODEX_EFFORT_OPTIONS: readonly CodexControlOption[];
|
|
41
54
|
export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
|
|
@@ -57,6 +70,7 @@ export declare function buildCodexRuntimeDescriptor(input: {
|
|
|
57
70
|
supportsCompact?: boolean;
|
|
58
71
|
supportsRichCards?: boolean;
|
|
59
72
|
skills?: ReadonlyArray<CodexSkillMetadata>;
|
|
73
|
+
serviceAgentMode?: boolean;
|
|
60
74
|
}): CanonRuntimeDescriptor;
|
|
61
75
|
export declare function buildCodexTurnResponseRouting(input: {
|
|
62
76
|
requestingUserId?: string | null;
|
|
@@ -73,6 +87,7 @@ export declare function getCodexRequestingUserId(message: {
|
|
|
73
87
|
export declare function resolveSessionExecutionMode(config: {
|
|
74
88
|
executionMode?: ExecutionEnvironmentMode;
|
|
75
89
|
} | null | undefined, serviceAgentMode?: boolean): ExecutionEnvironmentMode;
|
|
90
|
+
export declare function resolveCodexSessionConfig<T extends Record<string, unknown>>(config: T | null | undefined, serviceAgentMode?: boolean): T | null;
|
|
76
91
|
export declare function resolveWorkspaceCwd(config: {
|
|
77
92
|
workspaceId?: string;
|
|
78
93
|
retiredWorkspaceConfig?: boolean;
|
|
@@ -95,6 +110,19 @@ export declare function resolveCodexEffectiveRuntimePolicy(input: {
|
|
|
95
110
|
environment: Pick<PreparedExecutionEnvironment, 'baseCwd' | 'mode'>;
|
|
96
111
|
serviceAgentMode?: boolean;
|
|
97
112
|
}): CodexEffectiveRuntimePolicy;
|
|
113
|
+
export declare function resolveCodexPlanCommand(input: {
|
|
114
|
+
content: string;
|
|
115
|
+
requestedPlanMode: boolean;
|
|
116
|
+
useAppServer: boolean;
|
|
117
|
+
serviceAgentMode: boolean;
|
|
118
|
+
}): {
|
|
119
|
+
planMode: boolean;
|
|
120
|
+
content: string;
|
|
121
|
+
};
|
|
122
|
+
export declare function isCodexPlanApprovalReply(metadata: unknown, serviceAgentMode?: boolean): metadata is Record<string, unknown> & {
|
|
123
|
+
type: 'plan_approval_reply';
|
|
124
|
+
decision: string;
|
|
125
|
+
};
|
|
98
126
|
/**
|
|
99
127
|
* `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
|
|
100
128
|
* per-conversation-type default".
|
package/dist/host.js
CHANGED
|
@@ -78,6 +78,20 @@ export function buildCodexLiveSessionConfig(input) {
|
|
|
78
78
|
executionBranch: input.executionBranch ?? null,
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
export function buildCodexServiceSnapshotConfig(input) {
|
|
82
|
+
// Keep every setup key present, including keys whose host value is
|
|
83
|
+
// undefined. `publishHostSessionSnapshots` merges this over any persisted
|
|
84
|
+
// coding config, so an absent host value clears rather than resurrects a
|
|
85
|
+
// stale member-selected value.
|
|
86
|
+
return {
|
|
87
|
+
model: input.model,
|
|
88
|
+
permissionMode: input.permissionMode,
|
|
89
|
+
effort: input.effort,
|
|
90
|
+
workspaceId: input.workspaceId,
|
|
91
|
+
executionMode: 'locked',
|
|
92
|
+
executionBranch: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
81
95
|
const MAX_SESSIONS = 12;
|
|
82
96
|
// IDLE_TIMEOUT_MS (30 minutes) is shared with the other coding-agent hosts.
|
|
83
97
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -138,6 +152,7 @@ export function buildCodexSkillCommands(skills) {
|
|
|
138
152
|
});
|
|
139
153
|
}
|
|
140
154
|
export function buildCodexRuntimeDescriptor(input) {
|
|
155
|
+
const serviceAgentMode = input.serviceAgentMode === true;
|
|
141
156
|
const commands = [
|
|
142
157
|
{
|
|
143
158
|
id: 'runtime-status',
|
|
@@ -157,7 +172,7 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
157
172
|
RUNTIME_STOP_ACTION,
|
|
158
173
|
RUNTIME_STOP_AND_DROP_ACTION,
|
|
159
174
|
];
|
|
160
|
-
if (input.supportsCompact) {
|
|
175
|
+
if (input.supportsCompact && !serviceAgentMode) {
|
|
161
176
|
commands.push({
|
|
162
177
|
id: 'codex-compact-context',
|
|
163
178
|
label: 'Compact',
|
|
@@ -177,20 +192,22 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
177
192
|
}
|
|
178
193
|
const descriptor = buildFirstPartyCodingRuntimeDescriptor({
|
|
179
194
|
clientType: 'codex',
|
|
180
|
-
models: input.models,
|
|
181
|
-
workspaces: input.workspaces,
|
|
182
|
-
workspaceRoots: input.workspaceRoots,
|
|
183
|
-
executionModes: input.executionModes,
|
|
184
|
-
permissionModes: input.permissionModes,
|
|
185
|
-
defaultPermissionMode: input.defaultPermissionMode,
|
|
195
|
+
models: serviceAgentMode ? [] : input.models,
|
|
196
|
+
workspaces: serviceAgentMode ? [] : input.workspaces,
|
|
197
|
+
workspaceRoots: serviceAgentMode ? undefined : input.workspaceRoots,
|
|
198
|
+
executionModes: serviceAgentMode ? [] : input.executionModes,
|
|
199
|
+
permissionModes: serviceAgentMode ? [] : input.permissionModes,
|
|
200
|
+
defaultPermissionMode: serviceAgentMode ? undefined : input.defaultPermissionMode,
|
|
186
201
|
permissionModeLabel: 'Execution policy',
|
|
187
202
|
modelLiveBehavior: 'next_turn',
|
|
188
|
-
effortOptions:
|
|
189
|
-
|
|
203
|
+
effortOptions: serviceAgentMode
|
|
204
|
+
? []
|
|
205
|
+
: input.effortOptions ?? [...CODEX_EFFORT_OPTIONS],
|
|
206
|
+
defaultEffort: serviceAgentMode ? undefined : input.defaultEffort ?? 'medium',
|
|
190
207
|
effortLiveBehavior: 'next_turn',
|
|
191
208
|
presentation: input.presentation,
|
|
192
209
|
streamingTextMode: 'snapshot',
|
|
193
|
-
...(input.supportsPlanMode
|
|
210
|
+
...(input.supportsPlanMode && !serviceAgentMode
|
|
194
211
|
? {
|
|
195
212
|
turnModes: [
|
|
196
213
|
{
|
|
@@ -220,7 +237,7 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
220
237
|
rich: {
|
|
221
238
|
schema: 'canon.card.v1',
|
|
222
239
|
lifecycle: 'blocking_requires_action',
|
|
223
|
-
responder: 'agent_owner',
|
|
240
|
+
...(serviceAgentMode ? {} : { responder: 'agent_owner' }),
|
|
224
241
|
result: 'action_or_values',
|
|
225
242
|
maxTimeoutMs: 30 * 60_000,
|
|
226
243
|
blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
|
|
@@ -231,6 +248,13 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
231
248
|
}
|
|
232
249
|
: {}),
|
|
233
250
|
});
|
|
251
|
+
if (serviceAgentMode) {
|
|
252
|
+
const { runtimeControls: _runtimeControls, ...serviceDescriptor } = descriptor;
|
|
253
|
+
return {
|
|
254
|
+
...serviceDescriptor,
|
|
255
|
+
coreControls: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
234
258
|
if (input.models.length > 0) {
|
|
235
259
|
return descriptor;
|
|
236
260
|
}
|
|
@@ -269,6 +293,9 @@ export function resolveSessionExecutionMode(config, serviceAgentMode = false) {
|
|
|
269
293
|
return config.executionMode;
|
|
270
294
|
throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
|
|
271
295
|
}
|
|
296
|
+
export function resolveCodexSessionConfig(config, serviceAgentMode = false) {
|
|
297
|
+
return serviceAgentMode ? null : config ?? null;
|
|
298
|
+
}
|
|
272
299
|
export function resolveWorkspaceCwd(config, serviceAgentMode = false) {
|
|
273
300
|
return resolveHostWorkspaceCwd({
|
|
274
301
|
workspaceOptions,
|
|
@@ -311,6 +338,9 @@ export function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
311
338
|
baseCwd: input.environment.baseCwd,
|
|
312
339
|
executionMode: input.environment.mode,
|
|
313
340
|
permissionMode: permissionMode ?? null,
|
|
341
|
+
...(input.serviceAgentMode
|
|
342
|
+
? { serviceAgentMode: true, model: model ?? null }
|
|
343
|
+
: {}),
|
|
314
344
|
sandbox,
|
|
315
345
|
// `--ask-for-approval` is rejected at startup, so this was always null.
|
|
316
346
|
// The key must stay: it is hashed into every persisted thread fingerprint.
|
|
@@ -400,6 +430,14 @@ function parsePlanCommand(content) {
|
|
|
400
430
|
content: rest || 'Please inspect the request and propose a plan before making changes.',
|
|
401
431
|
};
|
|
402
432
|
}
|
|
433
|
+
export function resolveCodexPlanCommand(input) {
|
|
434
|
+
if (!input.useAppServer || input.serviceAgentMode) {
|
|
435
|
+
return { planMode: false, content: input.content };
|
|
436
|
+
}
|
|
437
|
+
return input.requestedPlanMode
|
|
438
|
+
? { planMode: true, content: input.content }
|
|
439
|
+
: parsePlanCommand(input.content);
|
|
440
|
+
}
|
|
403
441
|
function mapCodexQuestions(value) {
|
|
404
442
|
if (!Array.isArray(value))
|
|
405
443
|
return undefined;
|
|
@@ -453,6 +491,12 @@ function mapCodexQuestions(value) {
|
|
|
453
491
|
function isRecord(value) {
|
|
454
492
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
455
493
|
}
|
|
494
|
+
export function isCodexPlanApprovalReply(metadata, serviceAgentMode = false) {
|
|
495
|
+
return !serviceAgentMode
|
|
496
|
+
&& isRecord(metadata)
|
|
497
|
+
&& metadata.type === 'plan_approval_reply'
|
|
498
|
+
&& typeof metadata.decision === 'string';
|
|
499
|
+
}
|
|
456
500
|
function readString(record, key) {
|
|
457
501
|
const value = record[key];
|
|
458
502
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -1205,7 +1249,7 @@ export async function main() {
|
|
|
1205
1249
|
evictOldestIdle();
|
|
1206
1250
|
}
|
|
1207
1251
|
const creation = (async () => {
|
|
1208
|
-
const config = await loadSessionConfig(conversationId, agentId, rtdb);
|
|
1252
|
+
const config = resolveCodexSessionConfig(await loadSessionConfig(conversationId, agentId, rtdb), serviceAgentMode);
|
|
1209
1253
|
const sessionExecutionMode = resolveSessionExecutionMode(config, serviceAgentMode);
|
|
1210
1254
|
const workspaceCwd = resolveWorkspaceCwd(config, serviceAgentMode);
|
|
1211
1255
|
const environment = prepareConversationEnvironment({
|
|
@@ -1718,9 +1762,14 @@ export async function main() {
|
|
|
1718
1762
|
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1719
1763
|
return;
|
|
1720
1764
|
}
|
|
1721
|
-
if (
|
|
1722
|
-
|
|
1723
|
-
|
|
1765
|
+
if (input.message.metadata?.type === 'plan_approval_reply') {
|
|
1766
|
+
if (!isCodexPlanApprovalReply(input.message.metadata, serviceAgentMode)) {
|
|
1767
|
+
// A service agent never advertises or enters plan mode. Consume stale
|
|
1768
|
+
// replies left by an older coding descriptor instead of turning them
|
|
1769
|
+
// into hidden plan/implementation prompts after an upgrade or restart.
|
|
1770
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1724
1773
|
const planId = readString(input.message.metadata, 'planId');
|
|
1725
1774
|
if (planId && runtimeRequests.handleMessage(input.conversationId, {
|
|
1726
1775
|
senderId: input.message.senderId,
|
|
@@ -1758,11 +1807,12 @@ export async function main() {
|
|
|
1758
1807
|
const renderedContent = renderInboundContent(input.message, materialized);
|
|
1759
1808
|
const turnMetadata = normalizeTurnMetadata(input.message.metadata);
|
|
1760
1809
|
const requestedPlanMode = turnMetadata?.requestedTurnMode === 'plan';
|
|
1761
|
-
const planCommand =
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1810
|
+
const planCommand = resolveCodexPlanCommand({
|
|
1811
|
+
content: renderedContent,
|
|
1812
|
+
requestedPlanMode,
|
|
1813
|
+
useAppServer,
|
|
1814
|
+
serviceAgentMode,
|
|
1815
|
+
});
|
|
1766
1816
|
const content = planCommand.content;
|
|
1767
1817
|
const hydrated = await loadHydratedInboundContext({
|
|
1768
1818
|
conversationId: input.conversationId,
|
|
@@ -2354,14 +2404,18 @@ export async function main() {
|
|
|
2354
2404
|
});
|
|
2355
2405
|
let codexSkills = [];
|
|
2356
2406
|
const buildCurrentRuntimeDescriptor = () => ({
|
|
2357
|
-
|
|
2407
|
+
...(serviceAgentMode
|
|
2408
|
+
? {}
|
|
2409
|
+
: {
|
|
2410
|
+
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2411
|
+
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2412
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
2413
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
2414
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
2415
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
2416
|
+
: {}),
|
|
2417
|
+
}),
|
|
2358
2418
|
...(codexDefaultModel ? { defaultModel: codexDefaultModel } : {}),
|
|
2359
|
-
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2360
|
-
availableExecutionModes: hostAvailableExecutionModes,
|
|
2361
|
-
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
2362
|
-
...(codexPermissionEnvelope.defaultPermissionMode
|
|
2363
|
-
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
2364
|
-
: {}),
|
|
2365
2419
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
2366
2420
|
models: codexModelOptions,
|
|
2367
2421
|
effortOptions: codexEffortOptions,
|
|
@@ -2376,6 +2430,7 @@ export async function main() {
|
|
|
2376
2430
|
supportsCompact: useAppServer,
|
|
2377
2431
|
supportsRichCards: useAppServer,
|
|
2378
2432
|
skills: codexSkills,
|
|
2433
|
+
serviceAgentMode,
|
|
2379
2434
|
}),
|
|
2380
2435
|
});
|
|
2381
2436
|
let runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
@@ -2421,6 +2476,11 @@ export async function main() {
|
|
|
2421
2476
|
const session = sessions.get(conversationId);
|
|
2422
2477
|
if (!session || session.closed)
|
|
2423
2478
|
return;
|
|
2479
|
+
if (serviceAgentMode) {
|
|
2480
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring user session controls for service-agent runtime`);
|
|
2481
|
+
writeState(session);
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2424
2484
|
let modelChanged = false;
|
|
2425
2485
|
if (control.model && control.model !== session.state.model) {
|
|
2426
2486
|
if (codexModelOptions.length > 0 && !codexModelOptions.some((option) => option.value === control.model)) {
|
|
@@ -2577,10 +2637,27 @@ export async function main() {
|
|
|
2577
2637
|
workspaceOptions,
|
|
2578
2638
|
defaultCwd: workingDir,
|
|
2579
2639
|
extraSessionConfigFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
2580
|
-
liveSessionConfigByConversation: new Map(Array.from(sessions.
|
|
2640
|
+
liveSessionConfigByConversation: new Map(Array.from(serviceAgentMode ? knownConversationIds : sessions.keys()).map((conversationId) => {
|
|
2641
|
+
const session = sessions.get(conversationId);
|
|
2642
|
+
if (serviceAgentMode) {
|
|
2643
|
+
return [
|
|
2644
|
+
conversationId,
|
|
2645
|
+
buildCodexServiceSnapshotConfig({
|
|
2646
|
+
model: codexDefaultModel ?? session?.state.model ?? undefined,
|
|
2647
|
+
permissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
2648
|
+
effort: codexDefaultEffort ?? session?.state.effort ?? undefined,
|
|
2649
|
+
workspaceId: resolveWorkspaceIdForBaseCwd(workingDir) ?? undefined,
|
|
2650
|
+
}),
|
|
2651
|
+
];
|
|
2652
|
+
}
|
|
2653
|
+
if (!session) {
|
|
2654
|
+
// Non-service snapshots are published only for live sessions,
|
|
2655
|
+
// but keep this branch total if that invariant changes.
|
|
2656
|
+
return [conversationId, {}];
|
|
2657
|
+
}
|
|
2581
2658
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
2582
2659
|
return [
|
|
2583
|
-
|
|
2660
|
+
conversationId,
|
|
2584
2661
|
buildCodexLiveSessionConfig({
|
|
2585
2662
|
model: session.state.model,
|
|
2586
2663
|
permissionMode: session.state.permissionMode,
|
|
@@ -2749,7 +2826,7 @@ export async function main() {
|
|
|
2749
2826
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovery cursor was not found within ${STARTUP_RECOVERY_MAX_MESSAGES} messages; replaying the bounded recent window`);
|
|
2750
2827
|
}
|
|
2751
2828
|
for (const message of recovered.messages) {
|
|
2752
|
-
const isPlanReply = message.metadata
|
|
2829
|
+
const isPlanReply = isCodexPlanApprovalReply(message.metadata, serviceAgentMode);
|
|
2753
2830
|
if (!isPlanReply && !shouldTriggerAgentTurn({
|
|
2754
2831
|
senderType: message.senderType,
|
|
2755
2832
|
metadata: message.metadata,
|
package/dist/session-store.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function buildCodexThreadPolicyFingerprint(input: {
|
|
|
3
3
|
baseCwd: string;
|
|
4
4
|
executionMode?: ExecutionEnvironmentMode;
|
|
5
5
|
permissionMode?: string | null;
|
|
6
|
+
model?: string | null;
|
|
7
|
+
serviceAgentMode?: boolean;
|
|
6
8
|
sandbox?: string | null;
|
|
7
9
|
approvalPolicy?: string | null;
|
|
8
10
|
fullAuto?: boolean;
|
package/dist/session-store.js
CHANGED
|
@@ -6,6 +6,14 @@ export function buildCodexThreadPolicyFingerprint(input) {
|
|
|
6
6
|
baseCwd: input.baseCwd,
|
|
7
7
|
executionMode: input.executionMode ?? null,
|
|
8
8
|
permissionMode: input.permissionMode ?? null,
|
|
9
|
+
// Service agents make the model a host-owned runtime choice. Include it,
|
|
10
|
+
// plus an explicit service-mode marker, so the first service-agent
|
|
11
|
+
// release drops coding threads that may retain a member-selected model
|
|
12
|
+
// and later host model changes start a clean thread. Ordinary Codex host
|
|
13
|
+
// fingerprints intentionally remain byte-for-byte compatible.
|
|
14
|
+
...(input.serviceAgentMode
|
|
15
|
+
? { serviceAgentMode: true, model: input.model ?? null }
|
|
16
|
+
: {}),
|
|
9
17
|
sandbox: input.sandbox ?? null,
|
|
10
18
|
approvalPolicy: input.approvalPolicy ?? null,
|
|
11
19
|
fullAuto: input.fullAuto === true,
|