@canonmsg/codex-plugin 0.18.10 → 0.18.12
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/app-server-adapter.d.ts +6 -0
- package/dist/app-server-adapter.js +45 -5
- package/dist/codex-app-tools.d.ts +42 -0
- package/dist/codex-app-tools.js +519 -0
- package/dist/control-channel.d.ts +10 -3
- package/dist/control-channel.js +10 -2
- package/dist/host.d.ts +20 -1
- package/dist/host.js +72 -5
- package/dist/session-store.js +1 -1
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CodexApprovalPolicy, CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
|
|
2
|
+
export type JsonRecord = Record<string, unknown>;
|
|
2
3
|
export interface CodexSkillMetadata {
|
|
3
4
|
name: string;
|
|
4
5
|
path: string;
|
|
@@ -18,6 +19,7 @@ export declare class CodexAppServerAdapter {
|
|
|
18
19
|
private readonly configOverrides;
|
|
19
20
|
private readonly fullAuto;
|
|
20
21
|
private readonly bypassApprovalsAndSandbox;
|
|
22
|
+
private readonly dynamicTools;
|
|
21
23
|
private child;
|
|
22
24
|
private threadId;
|
|
23
25
|
private loadedThreadId;
|
|
@@ -49,6 +51,7 @@ export declare class CodexAppServerAdapter {
|
|
|
49
51
|
configOverrides?: string[];
|
|
50
52
|
fullAuto?: boolean;
|
|
51
53
|
bypassApprovalsAndSandbox?: boolean;
|
|
54
|
+
dynamicTools?: readonly JsonRecord[];
|
|
52
55
|
});
|
|
53
56
|
getThreadId(): string | null;
|
|
54
57
|
clearThreadId(): void;
|
|
@@ -64,6 +67,8 @@ export declare class CodexAppServerAdapter {
|
|
|
64
67
|
setReasoningEffort(effort: string | null): void;
|
|
65
68
|
isRunning(): boolean;
|
|
66
69
|
interrupt(): Promise<void>;
|
|
70
|
+
compactThread(): Promise<void>;
|
|
71
|
+
requestAppServer(method: string, params: unknown): Promise<unknown>;
|
|
67
72
|
close(): void;
|
|
68
73
|
runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[], _extraAddDirs?: readonly string[], options?: CodexRunTurnOptions): Promise<CodexTurnResult>;
|
|
69
74
|
private resolveApprovalPolicy;
|
|
@@ -79,6 +84,7 @@ export declare class CodexAppServerAdapter {
|
|
|
79
84
|
private handleLine;
|
|
80
85
|
private handleServerRequest;
|
|
81
86
|
private handleNotification;
|
|
87
|
+
private isCurrentThreadNotification;
|
|
82
88
|
private resolveCurrentTurn;
|
|
83
89
|
private clearActiveTurn;
|
|
84
90
|
private sendRequest;
|
|
@@ -11,6 +11,7 @@ export class CodexAppServerAdapter {
|
|
|
11
11
|
configOverrides;
|
|
12
12
|
fullAuto;
|
|
13
13
|
bypassApprovalsAndSandbox;
|
|
14
|
+
dynamicTools;
|
|
14
15
|
child = null;
|
|
15
16
|
threadId;
|
|
16
17
|
loadedThreadId = null;
|
|
@@ -42,6 +43,7 @@ export class CodexAppServerAdapter {
|
|
|
42
43
|
this.configOverrides = opts.configOverrides ?? [];
|
|
43
44
|
this.fullAuto = opts.fullAuto ?? false;
|
|
44
45
|
this.bypassApprovalsAndSandbox = opts.bypassApprovalsAndSandbox ?? false;
|
|
46
|
+
this.dynamicTools = opts.dynamicTools ?? [];
|
|
45
47
|
}
|
|
46
48
|
getThreadId() {
|
|
47
49
|
return this.threadId;
|
|
@@ -83,6 +85,19 @@ export class CodexAppServerAdapter {
|
|
|
83
85
|
turnId: this.currentTurnId,
|
|
84
86
|
}).catch(() => { });
|
|
85
87
|
}
|
|
88
|
+
async compactThread() {
|
|
89
|
+
if (!this.threadId) {
|
|
90
|
+
throw new Error('No Codex thread to compact');
|
|
91
|
+
}
|
|
92
|
+
await this.ensureStarted();
|
|
93
|
+
await this.sendRequest('thread/compact/start', {
|
|
94
|
+
threadId: this.threadId,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async requestAppServer(method, params) {
|
|
98
|
+
await this.ensureStarted();
|
|
99
|
+
return await this.sendRequest(method, params);
|
|
100
|
+
}
|
|
86
101
|
close() {
|
|
87
102
|
this.child?.kill('SIGTERM');
|
|
88
103
|
this.child = null;
|
|
@@ -113,6 +128,7 @@ export class CodexAppServerAdapter {
|
|
|
113
128
|
approvalPolicy: this.resolveApprovalPolicy(),
|
|
114
129
|
excludeTurns: true,
|
|
115
130
|
persistExtendedHistory: true,
|
|
131
|
+
...(this.dynamicTools.length ? { dynamicTools: this.dynamicTools } : {}),
|
|
116
132
|
});
|
|
117
133
|
this.loadedThreadId = this.threadId;
|
|
118
134
|
this.rememberResolvedModel(resumed);
|
|
@@ -124,6 +140,7 @@ export class CodexAppServerAdapter {
|
|
|
124
140
|
...(this.sandbox ? { sandbox: this.sandbox } : {}),
|
|
125
141
|
...this.configPayload(),
|
|
126
142
|
approvalPolicy: this.resolveApprovalPolicy(),
|
|
143
|
+
...(this.dynamicTools.length ? { dynamicTools: this.dynamicTools } : {}),
|
|
127
144
|
experimentalRawEvents: false,
|
|
128
145
|
persistExtendedHistory: true,
|
|
129
146
|
});
|
|
@@ -349,16 +366,18 @@ export class CodexAppServerAdapter {
|
|
|
349
366
|
}
|
|
350
367
|
}
|
|
351
368
|
handleNotification(method, params) {
|
|
352
|
-
if (method === 'turn/started') {
|
|
353
|
-
this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
|
|
354
|
-
this.currentOnEvent?.({ type: 'turn.started' });
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
369
|
if (method === 'skills/changed') {
|
|
358
370
|
this.skillsCache = null;
|
|
359
371
|
this.currentOnEvent?.({ type: 'skills.changed' });
|
|
360
372
|
return;
|
|
361
373
|
}
|
|
374
|
+
if (!this.isCurrentThreadNotification(params))
|
|
375
|
+
return;
|
|
376
|
+
if (method === 'turn/started') {
|
|
377
|
+
this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
|
|
378
|
+
this.currentOnEvent?.({ type: 'turn.started' });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
362
381
|
if (method === 'thread/status/changed') {
|
|
363
382
|
const status = params.status;
|
|
364
383
|
if (status?.type === 'active' && Array.isArray(status.activeFlags)) {
|
|
@@ -467,6 +486,15 @@ export class CodexAppServerAdapter {
|
|
|
467
486
|
this.currentErrorText = stringifyPreview(params);
|
|
468
487
|
}
|
|
469
488
|
}
|
|
489
|
+
isCurrentThreadNotification(params) {
|
|
490
|
+
const threadId = readNotificationThreadId(params);
|
|
491
|
+
if (threadId && this.threadId && threadId !== this.threadId)
|
|
492
|
+
return false;
|
|
493
|
+
const turnId = readNotificationTurnId(params);
|
|
494
|
+
if (turnId && this.currentTurnId && turnId !== this.currentTurnId)
|
|
495
|
+
return false;
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
470
498
|
resolveCurrentTurn() {
|
|
471
499
|
const result = {
|
|
472
500
|
threadId: this.threadId,
|
|
@@ -529,6 +557,18 @@ function readRawString(record, key) {
|
|
|
529
557
|
const value = record?.[key];
|
|
530
558
|
return typeof value === 'string' ? value : undefined;
|
|
531
559
|
}
|
|
560
|
+
function readNotificationThreadId(params) {
|
|
561
|
+
return readString(params, 'threadId')
|
|
562
|
+
?? readString(params.thread, 'id')
|
|
563
|
+
?? readString(params.turn, 'threadId')
|
|
564
|
+
?? readString(params.item, 'threadId')
|
|
565
|
+
?? readString(params.status, 'threadId');
|
|
566
|
+
}
|
|
567
|
+
function readNotificationTurnId(params) {
|
|
568
|
+
return readString(params, 'turnId')
|
|
569
|
+
?? readString(params.turn, 'id')
|
|
570
|
+
?? readString(params.item, 'turnId');
|
|
571
|
+
}
|
|
532
572
|
function readNullableString(record, key) {
|
|
533
573
|
const value = record?.[key];
|
|
534
574
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
2
|
+
interface DynamicToolSpec {
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
namespace: 'codex_app';
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: JsonRecord;
|
|
8
|
+
deferLoading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexAppToolAdapter {
|
|
11
|
+
requestAppServer(method: string, params: unknown): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface CodexAppToolWorkspace {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
cwd: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CodexAppToolRuntime {
|
|
19
|
+
adapter: CodexAppToolAdapter;
|
|
20
|
+
currentThreadId: string | null;
|
|
21
|
+
currentCwd: string;
|
|
22
|
+
workspaces: ReadonlyArray<CodexAppToolWorkspace>;
|
|
23
|
+
model?: string | null;
|
|
24
|
+
effort?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface CodexAppToolCallParams {
|
|
27
|
+
arguments?: unknown;
|
|
28
|
+
namespace?: unknown;
|
|
29
|
+
tool?: unknown;
|
|
30
|
+
}
|
|
31
|
+
type DynamicToolCallResponse = {
|
|
32
|
+
success: boolean;
|
|
33
|
+
contentItems: Array<{
|
|
34
|
+
type: 'inputText';
|
|
35
|
+
text: string;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
export declare const CODEX_APP_DYNAMIC_TOOLS: ReadonlyArray<DynamicToolSpec>;
|
|
39
|
+
export declare function isCodexAppToolCall(params: Record<string, unknown>): boolean;
|
|
40
|
+
export declare function deniedCodexAppToolResult(reason: string): DynamicToolCallResponse;
|
|
41
|
+
export declare function handleCodexAppToolCall(runtime: CodexAppToolRuntime, params: CodexAppToolCallParams): Promise<DynamicToolCallResponse>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
const emptyObjectSchema = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
properties: {},
|
|
4
|
+
additionalProperties: false,
|
|
5
|
+
};
|
|
6
|
+
const modelProperties = {
|
|
7
|
+
model: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Optional model override. Omit unless the user explicitly asks for a specific model.',
|
|
10
|
+
},
|
|
11
|
+
thinking: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Optional reasoning effort override.',
|
|
14
|
+
enum: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const createThreadTargetSchema = {
|
|
18
|
+
description: 'Where to create the thread. Canon currently supports local project targets only.',
|
|
19
|
+
anyOf: [
|
|
20
|
+
{
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
properties: {
|
|
24
|
+
type: { type: 'string', enum: ['project'] },
|
|
25
|
+
projectId: { type: 'string', description: 'Canon workspace id.' },
|
|
26
|
+
environment: {
|
|
27
|
+
anyOf: [
|
|
28
|
+
{
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
type: { type: 'string', enum: ['local'] },
|
|
33
|
+
},
|
|
34
|
+
required: ['type'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: true,
|
|
39
|
+
properties: {
|
|
40
|
+
type: { type: 'string', enum: ['worktree'] },
|
|
41
|
+
},
|
|
42
|
+
required: ['type'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ['type', 'projectId', 'environment'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
type: { type: 'string', enum: ['projectless'] },
|
|
54
|
+
directoryName: { type: 'string' },
|
|
55
|
+
},
|
|
56
|
+
required: ['type'],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const forkEnvironmentSchema = {
|
|
61
|
+
description: 'Where the fork should run. Canon currently supports same-directory forks only.',
|
|
62
|
+
anyOf: [
|
|
63
|
+
{
|
|
64
|
+
type: 'object',
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
properties: {
|
|
67
|
+
type: { type: 'string', enum: ['same-directory'] },
|
|
68
|
+
},
|
|
69
|
+
required: ['type'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: true,
|
|
74
|
+
properties: {
|
|
75
|
+
type: { type: 'string', enum: ['worktree'] },
|
|
76
|
+
},
|
|
77
|
+
required: ['type'],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
function tool(name, description, inputSchema, deferLoading = true) {
|
|
82
|
+
return { namespace: 'codex_app', name, description, inputSchema, deferLoading };
|
|
83
|
+
}
|
|
84
|
+
export const CODEX_APP_DYNAMIC_TOOLS = [
|
|
85
|
+
tool('automation_update', 'Create, update, view, or delete Codex app automations. Canon exposes the name for compatibility, but does not manage Desktop automations.', {
|
|
86
|
+
type: 'object',
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
properties: {
|
|
89
|
+
id: { type: 'string' },
|
|
90
|
+
mode: { type: 'string' },
|
|
91
|
+
kind: { type: 'string' },
|
|
92
|
+
name: { type: 'string' },
|
|
93
|
+
prompt: { type: 'string' },
|
|
94
|
+
rrule: { type: 'string' },
|
|
95
|
+
cwds: {
|
|
96
|
+
anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
|
|
97
|
+
},
|
|
98
|
+
destination: { type: 'string' },
|
|
99
|
+
executionEnvironment: { type: 'string' },
|
|
100
|
+
localEnvironmentConfigPath: { type: ['string', 'null'] },
|
|
101
|
+
model: { type: 'string' },
|
|
102
|
+
reasoningEffort: { type: 'string' },
|
|
103
|
+
targetThreadId: { type: 'string' },
|
|
104
|
+
status: { type: 'string' },
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
tool('navigate_to_codex_page', 'Navigate the Codex Desktop UI. Canon exposes the name for compatibility, but has no Codex Desktop page to navigate.', { type: 'object', additionalProperties: true, properties: {} }),
|
|
108
|
+
tool('read_thread_terminal', 'Read the Codex Desktop terminal output for this thread. Canon exposes the name for compatibility, but has no Desktop terminal pane.', emptyObjectSchema, false),
|
|
109
|
+
tool('load_workspace_dependencies', 'Locate bundled Desktop workspace dependency runtimes. Canon exposes the name for compatibility, but does not provide Desktop bundle paths.', emptyObjectSchema, false),
|
|
110
|
+
tool('fork_thread', 'Fork a Codex thread. Omit threadId to fork the calling thread. Canon supports same-directory forks.', {
|
|
111
|
+
type: 'object',
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
properties: {
|
|
114
|
+
threadId: { type: 'string' },
|
|
115
|
+
environment: forkEnvironmentSchema,
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
tool('handoff_thread', 'Move a Codex thread between a checkout and worktree. Canon exposes the name for compatibility, but does not manage Desktop handoffs.', {
|
|
119
|
+
type: 'object',
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
properties: { threadId: { type: 'string' } },
|
|
122
|
+
required: ['threadId'],
|
|
123
|
+
}),
|
|
124
|
+
tool('get_handoff_status', 'Read Codex Desktop handoff status. Canon exposes the name for compatibility, but does not manage Desktop handoffs.', {
|
|
125
|
+
type: 'object',
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
properties: { threadId: { type: 'string' } },
|
|
128
|
+
}),
|
|
129
|
+
tool('list_projects', 'List Canon workspaces available to Codex app tools.', emptyObjectSchema),
|
|
130
|
+
tool('create_thread', 'Create a separate Codex thread only when the user explicitly asks for a new or separate thread. Canon supports local project targets.', {
|
|
131
|
+
type: 'object',
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
properties: {
|
|
134
|
+
prompt: { type: 'string', description: 'Initial prompt for the new thread.' },
|
|
135
|
+
target: createThreadTargetSchema,
|
|
136
|
+
...modelProperties,
|
|
137
|
+
},
|
|
138
|
+
required: ['prompt'],
|
|
139
|
+
}),
|
|
140
|
+
tool('list_threads', 'List recent Codex threads in Canon-configured workspaces.', {
|
|
141
|
+
type: 'object',
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
properties: {
|
|
144
|
+
query: { type: 'string' },
|
|
145
|
+
limit: { type: 'number' },
|
|
146
|
+
archived: { type: 'boolean' },
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
tool('read_thread', 'Read recent status and turn summaries for one Codex thread without opening it.', {
|
|
150
|
+
type: 'object',
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
properties: {
|
|
153
|
+
threadId: { type: 'string' },
|
|
154
|
+
},
|
|
155
|
+
required: ['threadId'],
|
|
156
|
+
}),
|
|
157
|
+
tool('send_message_to_thread', 'Send a follow-up prompt to an existing Codex thread in the background.', {
|
|
158
|
+
type: 'object',
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
properties: {
|
|
161
|
+
threadId: { type: 'string' },
|
|
162
|
+
prompt: { type: 'string' },
|
|
163
|
+
...modelProperties,
|
|
164
|
+
},
|
|
165
|
+
required: ['threadId', 'prompt'],
|
|
166
|
+
}),
|
|
167
|
+
tool('set_thread_pinned', 'Pin or unpin a Codex thread. Canon exposes the name for compatibility, but pinned state is Desktop-only.', {
|
|
168
|
+
type: 'object',
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
properties: {
|
|
171
|
+
threadId: { type: 'string' },
|
|
172
|
+
pinned: { type: 'boolean' },
|
|
173
|
+
},
|
|
174
|
+
required: ['threadId', 'pinned'],
|
|
175
|
+
}),
|
|
176
|
+
tool('set_thread_archived', 'Archive or unarchive a Codex thread.', {
|
|
177
|
+
type: 'object',
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
properties: {
|
|
180
|
+
threadId: { type: 'string' },
|
|
181
|
+
archived: { type: 'boolean' },
|
|
182
|
+
},
|
|
183
|
+
required: ['archived'],
|
|
184
|
+
}),
|
|
185
|
+
tool('set_thread_title', 'Rename a Codex thread.', {
|
|
186
|
+
type: 'object',
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
properties: {
|
|
189
|
+
threadId: { type: 'string' },
|
|
190
|
+
title: { type: 'string' },
|
|
191
|
+
},
|
|
192
|
+
required: ['threadId', 'title'],
|
|
193
|
+
}),
|
|
194
|
+
];
|
|
195
|
+
const CODEX_APP_TOOL_NAMES = new Set(CODEX_APP_DYNAMIC_TOOLS.map((entry) => String(entry.name)));
|
|
196
|
+
const UNSUPPORTED_TOOLS = new Map([
|
|
197
|
+
['automation_update', 'Canon does not manage Codex Desktop automations.'],
|
|
198
|
+
['navigate_to_codex_page', 'Canon has no Codex Desktop page to navigate.'],
|
|
199
|
+
['read_thread_terminal', 'Canon has no Codex Desktop terminal pane to read.'],
|
|
200
|
+
['load_workspace_dependencies', 'Canon does not provide Codex Desktop bundled dependency paths.'],
|
|
201
|
+
['handoff_thread', 'Canon does not manage Codex Desktop handoffs.'],
|
|
202
|
+
['get_handoff_status', 'Canon does not manage Codex Desktop handoffs.'],
|
|
203
|
+
['set_thread_pinned', 'Pinned thread state is Codex Desktop-only.'],
|
|
204
|
+
]);
|
|
205
|
+
export function isCodexAppToolCall(params) {
|
|
206
|
+
const namespace = typeof params.namespace === 'string' ? params.namespace : null;
|
|
207
|
+
const rawTool = typeof params.tool === 'string' ? params.tool.trim() : '';
|
|
208
|
+
const toolName = normalizeToolName(params.tool);
|
|
209
|
+
if (namespace && namespace !== 'codex_app')
|
|
210
|
+
return rawTool.startsWith('codex_app.');
|
|
211
|
+
return namespace === 'codex_app'
|
|
212
|
+
|| rawTool.startsWith('codex_app.')
|
|
213
|
+
|| (toolName ? CODEX_APP_TOOL_NAMES.has(toolName) : false);
|
|
214
|
+
}
|
|
215
|
+
export function deniedCodexAppToolResult(reason) {
|
|
216
|
+
return toolResult(false, { error: reason });
|
|
217
|
+
}
|
|
218
|
+
export async function handleCodexAppToolCall(runtime, params) {
|
|
219
|
+
const toolName = normalizeToolName(params.tool);
|
|
220
|
+
if (!toolName || !CODEX_APP_TOOL_NAMES.has(toolName)) {
|
|
221
|
+
return toolResult(false, { error: `Unsupported codex_app tool: ${String(params.tool ?? 'unknown')}` });
|
|
222
|
+
}
|
|
223
|
+
const unsupportedReason = UNSUPPORTED_TOOLS.get(toolName);
|
|
224
|
+
if (unsupportedReason) {
|
|
225
|
+
return toolResult(false, { tool: toolName, error: unsupportedReason });
|
|
226
|
+
}
|
|
227
|
+
const args = parseToolArguments(params.arguments);
|
|
228
|
+
try {
|
|
229
|
+
switch (toolName) {
|
|
230
|
+
case 'list_projects':
|
|
231
|
+
return toolResult(true, {
|
|
232
|
+
projects: listToolWorkspaces(runtime).map((workspace) => ({
|
|
233
|
+
id: workspace.id,
|
|
234
|
+
label: workspace.label,
|
|
235
|
+
cwd: workspace.cwd,
|
|
236
|
+
environments: ['local'],
|
|
237
|
+
})),
|
|
238
|
+
unsupported: {
|
|
239
|
+
worktreeEnvironments: 'Canon host app-server tools only create local project threads.',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
case 'create_thread':
|
|
243
|
+
return await createThread(runtime, args);
|
|
244
|
+
case 'fork_thread':
|
|
245
|
+
return await forkThread(runtime, args);
|
|
246
|
+
case 'list_threads':
|
|
247
|
+
return await listThreads(runtime, args);
|
|
248
|
+
case 'read_thread':
|
|
249
|
+
return await readThread(runtime, args);
|
|
250
|
+
case 'send_message_to_thread':
|
|
251
|
+
return await sendMessageToThread(runtime, args);
|
|
252
|
+
case 'set_thread_archived':
|
|
253
|
+
return await setThreadArchived(runtime, args);
|
|
254
|
+
case 'set_thread_title':
|
|
255
|
+
return await setThreadTitle(runtime, args);
|
|
256
|
+
default:
|
|
257
|
+
return toolResult(false, { tool: toolName, error: 'Tool is registered but has no Canon handler.' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return toolResult(false, {
|
|
262
|
+
tool: toolName,
|
|
263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function createThread(runtime, args) {
|
|
268
|
+
const prompt = readString(args, 'prompt');
|
|
269
|
+
if (!prompt)
|
|
270
|
+
throw new Error('create_thread requires a non-empty prompt.');
|
|
271
|
+
const cwd = resolveCreateThreadCwd(runtime, args);
|
|
272
|
+
const started = await runtime.adapter.requestAppServer('thread/start', {
|
|
273
|
+
cwd,
|
|
274
|
+
...(readString(args, 'model') ?? runtime.model ? { model: readString(args, 'model') ?? runtime.model } : {}),
|
|
275
|
+
dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
|
|
276
|
+
experimentalRawEvents: false,
|
|
277
|
+
persistExtendedHistory: true,
|
|
278
|
+
});
|
|
279
|
+
const threadId = readThreadId(started);
|
|
280
|
+
if (!threadId)
|
|
281
|
+
throw new Error('Codex app-server did not return a thread id for the new thread.');
|
|
282
|
+
const turnStarted = await runtime.adapter.requestAppServer('turn/start', buildTurnStartParams({
|
|
283
|
+
threadId,
|
|
284
|
+
prompt,
|
|
285
|
+
model: readString(args, 'model') ?? null,
|
|
286
|
+
effort: readString(args, 'thinking') ?? readString(args, 'effort') ?? runtime.effort ?? null,
|
|
287
|
+
}));
|
|
288
|
+
return toolResult(true, {
|
|
289
|
+
threadId,
|
|
290
|
+
turnId: readTurnId(turnStarted) ?? null,
|
|
291
|
+
status: 'submitted',
|
|
292
|
+
cwd,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function forkThread(runtime, args) {
|
|
296
|
+
const environment = isRecord(args.environment) ? args.environment : null;
|
|
297
|
+
const environmentType = readString(environment, 'type');
|
|
298
|
+
if (environmentType && environmentType !== 'same-directory') {
|
|
299
|
+
throw new Error('Canon only supports same-directory codex_app.fork_thread calls.');
|
|
300
|
+
}
|
|
301
|
+
const sourceThreadId = readString(args, 'threadId') ?? runtime.currentThreadId;
|
|
302
|
+
if (!sourceThreadId)
|
|
303
|
+
throw new Error('fork_thread requires a source thread id.');
|
|
304
|
+
const source = await assertThreadAllowed(runtime, sourceThreadId);
|
|
305
|
+
const forked = await runtime.adapter.requestAppServer('thread/fork', {
|
|
306
|
+
threadId: sourceThreadId,
|
|
307
|
+
cwd: source.cwd ?? runtime.currentCwd,
|
|
308
|
+
excludeTurns: true,
|
|
309
|
+
persistExtendedHistory: true,
|
|
310
|
+
});
|
|
311
|
+
const threadId = readThreadId(forked);
|
|
312
|
+
if (!threadId)
|
|
313
|
+
throw new Error('Codex app-server did not return a forked thread id.');
|
|
314
|
+
return toolResult(true, {
|
|
315
|
+
threadId,
|
|
316
|
+
sourceThreadId,
|
|
317
|
+
status: 'forked',
|
|
318
|
+
cwd: source.cwd ?? runtime.currentCwd,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async function listThreads(runtime, args) {
|
|
322
|
+
const limit = clampLimit(readNumber(args, 'limit'));
|
|
323
|
+
const result = await runtime.adapter.requestAppServer('thread/list', {
|
|
324
|
+
cwd: allowedCwds(runtime),
|
|
325
|
+
limit,
|
|
326
|
+
searchTerm: readString(args, 'query') ?? readString(args, 'searchTerm') ?? null,
|
|
327
|
+
archived: typeof args.archived === 'boolean' ? args.archived : false,
|
|
328
|
+
});
|
|
329
|
+
return toolResult(true, result);
|
|
330
|
+
}
|
|
331
|
+
async function readThread(runtime, args) {
|
|
332
|
+
const threadId = readString(args, 'threadId');
|
|
333
|
+
if (!threadId)
|
|
334
|
+
throw new Error('read_thread requires threadId.');
|
|
335
|
+
await assertThreadAllowed(runtime, threadId);
|
|
336
|
+
const result = await runtime.adapter.requestAppServer('thread/read', {
|
|
337
|
+
threadId,
|
|
338
|
+
includeTurns: true,
|
|
339
|
+
});
|
|
340
|
+
return toolResult(true, result);
|
|
341
|
+
}
|
|
342
|
+
async function sendMessageToThread(runtime, args) {
|
|
343
|
+
const threadId = readString(args, 'threadId');
|
|
344
|
+
const prompt = readString(args, 'prompt');
|
|
345
|
+
if (!threadId)
|
|
346
|
+
throw new Error('send_message_to_thread requires threadId.');
|
|
347
|
+
if (!prompt)
|
|
348
|
+
throw new Error('send_message_to_thread requires a non-empty prompt.');
|
|
349
|
+
await assertThreadAllowed(runtime, threadId);
|
|
350
|
+
const result = await runtime.adapter.requestAppServer('turn/start', buildTurnStartParams({
|
|
351
|
+
threadId,
|
|
352
|
+
prompt,
|
|
353
|
+
model: readString(args, 'model') ?? null,
|
|
354
|
+
effort: readString(args, 'thinking') ?? readString(args, 'effort') ?? null,
|
|
355
|
+
}));
|
|
356
|
+
return toolResult(true, {
|
|
357
|
+
threadId,
|
|
358
|
+
turnId: readTurnId(result) ?? null,
|
|
359
|
+
status: 'submitted',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function setThreadArchived(runtime, args) {
|
|
363
|
+
const threadId = readString(args, 'threadId') ?? runtime.currentThreadId;
|
|
364
|
+
if (!threadId)
|
|
365
|
+
throw new Error('set_thread_archived requires threadId outside the calling thread.');
|
|
366
|
+
if (typeof args.archived !== 'boolean')
|
|
367
|
+
throw new Error('set_thread_archived requires archived boolean.');
|
|
368
|
+
await assertThreadAllowed(runtime, threadId);
|
|
369
|
+
const result = await runtime.adapter.requestAppServer(args.archived ? 'thread/archive' : 'thread/unarchive', { threadId });
|
|
370
|
+
return toolResult(true, { threadId, archived: args.archived, result });
|
|
371
|
+
}
|
|
372
|
+
async function setThreadTitle(runtime, args) {
|
|
373
|
+
const threadId = readString(args, 'threadId');
|
|
374
|
+
const title = readString(args, 'title');
|
|
375
|
+
if (!threadId)
|
|
376
|
+
throw new Error('set_thread_title requires threadId.');
|
|
377
|
+
if (!title)
|
|
378
|
+
throw new Error('set_thread_title requires a non-empty title.');
|
|
379
|
+
await assertThreadAllowed(runtime, threadId);
|
|
380
|
+
const result = await runtime.adapter.requestAppServer('thread/name/set', {
|
|
381
|
+
threadId,
|
|
382
|
+
name: title,
|
|
383
|
+
});
|
|
384
|
+
return toolResult(true, { threadId, title, result });
|
|
385
|
+
}
|
|
386
|
+
function resolveCreateThreadCwd(runtime, args) {
|
|
387
|
+
const target = isRecord(args.target) ? args.target : null;
|
|
388
|
+
const environment = isRecord(target?.environment) ? target.environment : null;
|
|
389
|
+
const environmentType = readString(environment, 'type');
|
|
390
|
+
if (environmentType === 'worktree') {
|
|
391
|
+
throw new Error('Canon does not create codex_app threads in new worktrees yet.');
|
|
392
|
+
}
|
|
393
|
+
if (readString(target, 'type') === 'projectless') {
|
|
394
|
+
throw new Error('Canon does not support projectless codex_app threads yet.');
|
|
395
|
+
}
|
|
396
|
+
const projectId = readString(target, 'projectId')
|
|
397
|
+
?? readString(args, 'projectId')
|
|
398
|
+
?? readString(args, 'workspaceId');
|
|
399
|
+
if (!projectId)
|
|
400
|
+
return runtime.currentCwd;
|
|
401
|
+
const workspace = listToolWorkspaces(runtime).find((entry) => entry.id === projectId);
|
|
402
|
+
if (!workspace)
|
|
403
|
+
throw new Error(`Unknown Canon workspace/project id: ${projectId}`);
|
|
404
|
+
return workspace.cwd;
|
|
405
|
+
}
|
|
406
|
+
async function assertThreadAllowed(runtime, threadId) {
|
|
407
|
+
if (threadId === runtime.currentThreadId) {
|
|
408
|
+
return { cwd: runtime.currentCwd, raw: null };
|
|
409
|
+
}
|
|
410
|
+
const result = await runtime.adapter.requestAppServer('thread/read', {
|
|
411
|
+
threadId,
|
|
412
|
+
includeTurns: false,
|
|
413
|
+
});
|
|
414
|
+
const cwd = readThreadCwd(result);
|
|
415
|
+
if (!cwd || !allowedCwdSet(runtime).has(cwd)) {
|
|
416
|
+
throw new Error(`Thread ${threadId} is not in a Canon-configured workspace.`);
|
|
417
|
+
}
|
|
418
|
+
return { cwd, raw: result };
|
|
419
|
+
}
|
|
420
|
+
function buildTurnStartParams(input) {
|
|
421
|
+
return {
|
|
422
|
+
threadId: input.threadId,
|
|
423
|
+
input: [{ type: 'text', text: input.prompt, text_elements: [] }],
|
|
424
|
+
...(input.model ? { model: input.model } : {}),
|
|
425
|
+
...(input.effort ? { effort: input.effort } : {}),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function listToolWorkspaces(runtime) {
|
|
429
|
+
const seen = new Set();
|
|
430
|
+
const workspaces = [];
|
|
431
|
+
for (const workspace of runtime.workspaces) {
|
|
432
|
+
if (!workspace.cwd || seen.has(workspace.cwd))
|
|
433
|
+
continue;
|
|
434
|
+
seen.add(workspace.cwd);
|
|
435
|
+
workspaces.push(workspace);
|
|
436
|
+
}
|
|
437
|
+
if (runtime.currentCwd && !seen.has(runtime.currentCwd)) {
|
|
438
|
+
workspaces.unshift({
|
|
439
|
+
id: 'current',
|
|
440
|
+
label: 'Current session',
|
|
441
|
+
cwd: runtime.currentCwd,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return workspaces;
|
|
445
|
+
}
|
|
446
|
+
function allowedCwds(runtime) {
|
|
447
|
+
return listToolWorkspaces(runtime).map((workspace) => workspace.cwd);
|
|
448
|
+
}
|
|
449
|
+
function allowedCwdSet(runtime) {
|
|
450
|
+
return new Set(allowedCwds(runtime));
|
|
451
|
+
}
|
|
452
|
+
function clampLimit(value) {
|
|
453
|
+
if (!value || !Number.isFinite(value))
|
|
454
|
+
return 20;
|
|
455
|
+
return Math.max(1, Math.min(100, Math.floor(value)));
|
|
456
|
+
}
|
|
457
|
+
function parseToolArguments(value) {
|
|
458
|
+
if (isRecord(value))
|
|
459
|
+
return value;
|
|
460
|
+
if (typeof value === 'string' && value.trim()) {
|
|
461
|
+
try {
|
|
462
|
+
const parsed = JSON.parse(value);
|
|
463
|
+
return isRecord(parsed) ? parsed : {};
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return {};
|
|
470
|
+
}
|
|
471
|
+
function normalizeToolName(value) {
|
|
472
|
+
if (typeof value !== 'string' || !value.trim())
|
|
473
|
+
return null;
|
|
474
|
+
const trimmed = value.trim();
|
|
475
|
+
return trimmed.startsWith('codex_app.') ? trimmed.slice('codex_app.'.length) : trimmed;
|
|
476
|
+
}
|
|
477
|
+
function readThreadId(value) {
|
|
478
|
+
if (!isRecord(value))
|
|
479
|
+
return undefined;
|
|
480
|
+
return readString(value, 'threadId')
|
|
481
|
+
?? readString(value, 'id')
|
|
482
|
+
?? readString(isRecord(value.thread) ? value.thread : undefined, 'id');
|
|
483
|
+
}
|
|
484
|
+
function readTurnId(value) {
|
|
485
|
+
if (!isRecord(value))
|
|
486
|
+
return undefined;
|
|
487
|
+
return readString(value, 'turnId')
|
|
488
|
+
?? readString(value, 'id')
|
|
489
|
+
?? readString(isRecord(value.turn) ? value.turn : undefined, 'id');
|
|
490
|
+
}
|
|
491
|
+
function readThreadCwd(value) {
|
|
492
|
+
if (!isRecord(value))
|
|
493
|
+
return null;
|
|
494
|
+
const thread = isRecord(value.thread) ? value.thread : undefined;
|
|
495
|
+
return readString(thread, 'cwd')
|
|
496
|
+
?? readString(thread, 'workingDirectory')
|
|
497
|
+
?? readString(value, 'cwd')
|
|
498
|
+
?? null;
|
|
499
|
+
}
|
|
500
|
+
function readString(record, key) {
|
|
501
|
+
const value = record?.[key];
|
|
502
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
503
|
+
}
|
|
504
|
+
function readNumber(record, key) {
|
|
505
|
+
const value = record[key];
|
|
506
|
+
return typeof value === 'number' ? value : undefined;
|
|
507
|
+
}
|
|
508
|
+
function isRecord(value) {
|
|
509
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
510
|
+
}
|
|
511
|
+
function toolResult(success, payload) {
|
|
512
|
+
return {
|
|
513
|
+
success,
|
|
514
|
+
contentItems: [{
|
|
515
|
+
type: 'inputText',
|
|
516
|
+
text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2),
|
|
517
|
+
}],
|
|
518
|
+
};
|
|
519
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* codex host characterization profile (the "codex host profile" describe
|
|
6
6
|
* block in core's control-poller.test.ts is the contract):
|
|
7
7
|
*
|
|
8
|
-
* - Keys: `session` + `signal
|
|
9
|
-
* conversation, conversations polled one
|
|
8
|
+
* - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
|
|
9
|
+
* configured; read sequentially per conversation, conversations polled one
|
|
10
|
+
* at a time.
|
|
10
11
|
* - Cadence: immediate first cycle, then active/idle delays + jitter with
|
|
11
12
|
* the activity probe sampled BEFORE the cycle runs.
|
|
12
13
|
* - Session controls are always consumed once newer (default consume), even
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
* - Dedupe is primed eagerly via `poller.baseline([conversationId])` at
|
|
16
17
|
* session creation — the second legacy poll site.
|
|
17
18
|
*/
|
|
18
|
-
import { ControlChannelPoller, type ControlChannelRTDB, type ControlHandlerResult, type ControlPollerError, type ControlSessionEvent, type ControlSignalEvent } from '@canonmsg/core';
|
|
19
|
+
import { ControlChannelPoller, type ControlChannelRTDB, type ControlHandlerResult, type ControlPollerError, type ControlPrimitiveEvent, type ControlSessionEvent, type ControlSignalEvent } from '@canonmsg/core';
|
|
19
20
|
export declare const CONTROL_POLL_MS = 2000;
|
|
20
21
|
export declare const IDLE_CONTROL_POLL_MS = 10000;
|
|
21
22
|
export declare const CONTROL_POLL_JITTER_MS = 1000;
|
|
@@ -38,6 +39,12 @@ export interface CodexControlChannelInput {
|
|
|
38
39
|
* node in place; thrown errors also leave it (consumeOnError stays false).
|
|
39
40
|
*/
|
|
40
41
|
onSignal: (event: ControlSignalEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
|
|
42
|
+
/**
|
|
43
|
+
* Handles a newer `/primitive` node. Primitive commands are consumed by
|
|
44
|
+
* default after the handler resolves or throws, matching core's command
|
|
45
|
+
* semantics so one failed command cannot replay forever.
|
|
46
|
+
*/
|
|
47
|
+
onPrimitive?: (event: ControlPrimitiveEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
|
|
41
48
|
onError?: (error: ControlPollerError) => void;
|
|
42
49
|
/** Injectable jitter source (tests). */
|
|
43
50
|
random?: () => number;
|
package/dist/control-channel.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* codex host characterization profile (the "codex host profile" describe
|
|
6
6
|
* block in core's control-poller.test.ts is the contract):
|
|
7
7
|
*
|
|
8
|
-
* - Keys: `session` + `signal
|
|
9
|
-
* conversation, conversations polled one
|
|
8
|
+
* - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
|
|
9
|
+
* configured; read sequentially per conversation, conversations polled one
|
|
10
|
+
* at a time.
|
|
10
11
|
* - Cadence: immediate first cycle, then active/idle delays + jitter with
|
|
11
12
|
* the activity probe sampled BEFORE the cycle runs.
|
|
12
13
|
* - Session controls are always consumed once newer (default consume), even
|
|
@@ -41,6 +42,13 @@ export function createCodexControlPoller(input) {
|
|
|
41
42
|
signal: {
|
|
42
43
|
handle: input.onSignal,
|
|
43
44
|
},
|
|
45
|
+
...(input.onPrimitive
|
|
46
|
+
? {
|
|
47
|
+
primitive: {
|
|
48
|
+
handle: input.onPrimitive,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
44
52
|
},
|
|
45
53
|
...(input.onError ? { onError: input.onError } : {}),
|
|
46
54
|
...(input.random ? { random: input.random } : {}),
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type CanonRuntimeCommandDescriptor, type ExecutionEnvironmentMode } from '@canonmsg/core';
|
|
2
|
+
import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core';
|
|
3
3
|
import { type CodexSkillMetadata } from './app-server-adapter.js';
|
|
4
4
|
interface HostSessionState {
|
|
5
5
|
lastError?: string;
|
|
@@ -51,5 +51,24 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
|
|
|
51
51
|
}];
|
|
52
52
|
export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
|
|
53
53
|
export declare function buildCodexSkillCommands(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor[];
|
|
54
|
+
export declare function buildCodexRuntimeDescriptor(input: {
|
|
55
|
+
models: Array<{
|
|
56
|
+
value: string;
|
|
57
|
+
label: string;
|
|
58
|
+
}>;
|
|
59
|
+
workspaces: WorkspaceOption[];
|
|
60
|
+
workspaceRoots?: CanonWorkspaceRootMetadata[];
|
|
61
|
+
executionModes: ExecutionEnvironmentMode[];
|
|
62
|
+
permissionModes: Array<{
|
|
63
|
+
value: string;
|
|
64
|
+
label: string;
|
|
65
|
+
}>;
|
|
66
|
+
defaultPermissionMode?: string;
|
|
67
|
+
presentation?: CanonRuntimePresentationPolicy;
|
|
68
|
+
supportsPlanMode: boolean;
|
|
69
|
+
supportsCompact?: boolean;
|
|
70
|
+
supportsRichCards?: boolean;
|
|
71
|
+
skills?: ReadonlyArray<CodexSkillMetadata>;
|
|
72
|
+
}): CanonRuntimeDescriptor;
|
|
54
73
|
export declare function main(): Promise<void>;
|
|
55
74
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -10,6 +10,7 @@ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_
|
|
|
10
10
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
12
|
import { CodexAppServerAdapter } from './app-server-adapter.js';
|
|
13
|
+
import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
|
|
13
14
|
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
14
15
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
15
16
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
@@ -126,7 +127,7 @@ export function buildCodexSkillCommands(skills) {
|
|
|
126
127
|
};
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
|
-
function buildCodexRuntimeDescriptor(input) {
|
|
130
|
+
export function buildCodexRuntimeDescriptor(input) {
|
|
130
131
|
const commands = [
|
|
131
132
|
{
|
|
132
133
|
id: 'runtime-status',
|
|
@@ -146,6 +147,20 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
146
147
|
RUNTIME_STOP_ACTION,
|
|
147
148
|
RUNTIME_STOP_AND_DROP_ACTION,
|
|
148
149
|
];
|
|
150
|
+
if (input.supportsCompact) {
|
|
151
|
+
commands.push({
|
|
152
|
+
id: 'codex-compact-context',
|
|
153
|
+
label: 'Compact',
|
|
154
|
+
description: 'Ask Codex to compact the current thread context.',
|
|
155
|
+
primitive: 'context.compact',
|
|
156
|
+
aliases: ['compact'],
|
|
157
|
+
category: 'session',
|
|
158
|
+
placements: ['composer_slash', 'command_palette'],
|
|
159
|
+
availability: ['idle'],
|
|
160
|
+
ownerOnly: true,
|
|
161
|
+
dispatch: { kind: 'primitive', primitive: 'context.compact' },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
149
164
|
const skillCommands = input.skills?.length ? buildCodexSkillCommands(input.skills) : [];
|
|
150
165
|
if (skillCommands.length) {
|
|
151
166
|
commands.push(...skillCommands);
|
|
@@ -835,6 +850,7 @@ export async function main() {
|
|
|
835
850
|
session.currentTurnId = null;
|
|
836
851
|
session.currentTurnOpenedAt = null;
|
|
837
852
|
session.currentTurnUpdatedAt = null;
|
|
853
|
+
session.currentTurnCanUseCodexAppTools = false;
|
|
838
854
|
session.lastAcceptedIntent = null;
|
|
839
855
|
session.resetRequested = false;
|
|
840
856
|
}
|
|
@@ -909,6 +925,7 @@ export async function main() {
|
|
|
909
925
|
configOverrides: args.config ?? [],
|
|
910
926
|
fullAuto: policy.fullAuto,
|
|
911
927
|
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
928
|
+
dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
|
|
912
929
|
})
|
|
913
930
|
: new CodexConversationAdapter({
|
|
914
931
|
cwd: sessionCwd,
|
|
@@ -941,6 +958,7 @@ export async function main() {
|
|
|
941
958
|
currentTurnId: null,
|
|
942
959
|
currentTurnOpenedAt: null,
|
|
943
960
|
currentTurnUpdatedAt: null,
|
|
961
|
+
currentTurnCanUseCodexAppTools: false,
|
|
944
962
|
activeSelfContextId: null,
|
|
945
963
|
lastAcceptedIntent: null,
|
|
946
964
|
resetRequested: false,
|
|
@@ -971,7 +989,7 @@ export async function main() {
|
|
|
971
989
|
pendingSessionCreations.delete(conversationId);
|
|
972
990
|
}
|
|
973
991
|
}
|
|
974
|
-
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled') {
|
|
992
|
+
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled', canUseCodexAppTools = false) {
|
|
975
993
|
const nextPrompt = {
|
|
976
994
|
prompt,
|
|
977
995
|
intent,
|
|
@@ -981,6 +999,7 @@ export async function main() {
|
|
|
981
999
|
mediaAddDirs,
|
|
982
1000
|
planMode,
|
|
983
1001
|
artifactRoutingMode,
|
|
1002
|
+
canUseCodexAppTools,
|
|
984
1003
|
};
|
|
985
1004
|
if (toFront) {
|
|
986
1005
|
session.queue.unshift(nextPrompt);
|
|
@@ -1084,6 +1103,22 @@ export async function main() {
|
|
|
1084
1103
|
const requestId = String(request.id);
|
|
1085
1104
|
const params = request.params;
|
|
1086
1105
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
1106
|
+
if (request.method === 'item/tool/call' && isCodexAppToolCall(params)) {
|
|
1107
|
+
if (!(session.adapter instanceof CodexAppServerAdapter)) {
|
|
1108
|
+
return deniedCodexAppToolResult('codex_app tools require the Codex app-server transport.');
|
|
1109
|
+
}
|
|
1110
|
+
if (!session.currentTurnCanUseCodexAppTools) {
|
|
1111
|
+
return deniedCodexAppToolResult('Only the Canon owner can use codex_app tools.');
|
|
1112
|
+
}
|
|
1113
|
+
return await handleCodexAppToolCall({
|
|
1114
|
+
adapter: session.adapter,
|
|
1115
|
+
currentThreadId: session.adapter.getThreadId(),
|
|
1116
|
+
currentCwd: session.cwd,
|
|
1117
|
+
workspaces: workspaceOptions,
|
|
1118
|
+
model: session.state.model ?? null,
|
|
1119
|
+
effort: session.state.effort ?? null,
|
|
1120
|
+
}, params);
|
|
1121
|
+
}
|
|
1087
1122
|
const runtimeCardPayload = runtimeCardRequestPayload(request.method, params);
|
|
1088
1123
|
if (runtimeCardPayload) {
|
|
1089
1124
|
const card = parseRuntimeCardV1(runtimeCardPayload);
|
|
@@ -1273,7 +1308,7 @@ export async function main() {
|
|
|
1273
1308
|
: decision === 'reject'
|
|
1274
1309
|
? `The plan was declined — keep planning and wait for guidance before implementing.${feedback ? `\n\nNotes:\n${feedback}` : ''}`
|
|
1275
1310
|
: `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
|
|
1276
|
-
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve');
|
|
1311
|
+
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve', 'disabled', input.isOwner);
|
|
1277
1312
|
return;
|
|
1278
1313
|
}
|
|
1279
1314
|
let materialized = [];
|
|
@@ -1370,7 +1405,7 @@ export async function main() {
|
|
|
1370
1405
|
});
|
|
1371
1406
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
1372
1407
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1373
|
-
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
|
|
1408
|
+
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
|
|
1374
1409
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
1375
1410
|
await session.adapter.interrupt().catch(() => { });
|
|
1376
1411
|
clearStreaming(input.conversationId);
|
|
@@ -1378,7 +1413,7 @@ export async function main() {
|
|
|
1378
1413
|
return;
|
|
1379
1414
|
}
|
|
1380
1415
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1381
|
-
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
|
|
1416
|
+
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
|
|
1382
1417
|
}
|
|
1383
1418
|
function sendTurnArtifactFile(session, file) {
|
|
1384
1419
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
@@ -1436,6 +1471,7 @@ export async function main() {
|
|
|
1436
1471
|
session.turnCommandBlocks = createCommandBlockTracker();
|
|
1437
1472
|
session.currentTurnOpenedAt = Date.now();
|
|
1438
1473
|
session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
|
|
1474
|
+
session.currentTurnCanUseCodexAppTools = nextTurn.canUseCodexAppTools === true;
|
|
1439
1475
|
session.lastAcceptedIntent = nextTurn.intent;
|
|
1440
1476
|
session.turnState = 'thinking';
|
|
1441
1477
|
session.lastActivity = Date.now();
|
|
@@ -1701,6 +1737,7 @@ export async function main() {
|
|
|
1701
1737
|
session.currentTurnId = null;
|
|
1702
1738
|
session.currentTurnOpenedAt = null;
|
|
1703
1739
|
session.currentTurnUpdatedAt = null;
|
|
1740
|
+
session.currentTurnCanUseCodexAppTools = false;
|
|
1704
1741
|
session.lastAcceptedIntent = null;
|
|
1705
1742
|
session.resetRequested = false;
|
|
1706
1743
|
session.lastActivity = Date.now();
|
|
@@ -1742,6 +1779,7 @@ export async function main() {
|
|
|
1742
1779
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
1743
1780
|
presentation: runtimePresentation,
|
|
1744
1781
|
supportsPlanMode: useAppServer,
|
|
1782
|
+
supportsCompact: useAppServer,
|
|
1745
1783
|
supportsRichCards: useAppServer,
|
|
1746
1784
|
skills: codexSkills,
|
|
1747
1785
|
}),
|
|
@@ -1838,6 +1876,34 @@ export async function main() {
|
|
|
1838
1876
|
clearStreaming(conversationId);
|
|
1839
1877
|
typingSignals.clear(conversationId).catch(() => { });
|
|
1840
1878
|
}
|
|
1879
|
+
async function handleControlPrimitive(event) {
|
|
1880
|
+
const { conversationId, value } = event;
|
|
1881
|
+
const primitiveId = typeof value.id === 'string' ? value.id : '';
|
|
1882
|
+
if (primitiveId !== 'context.compact') {
|
|
1883
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring unsupported primitive (${primitiveId || 'unknown'})`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const session = sessions.get(conversationId);
|
|
1887
|
+
if (!session || session.closed) {
|
|
1888
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: no live Codex session`);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (!(session.adapter instanceof CodexAppServerAdapter)) {
|
|
1892
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: compact is only available for Codex app-server sessions`);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
try {
|
|
1896
|
+
await session.adapter.compactThread();
|
|
1897
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Compact requested`);
|
|
1898
|
+
writeState(session);
|
|
1899
|
+
}
|
|
1900
|
+
catch (error) {
|
|
1901
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1902
|
+
session.state.lastError = `Could not compact Codex context: ${message}`;
|
|
1903
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${session.state.lastError}`);
|
|
1904
|
+
writeState(session);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1841
1907
|
const controlPoller = createCodexControlPoller({
|
|
1842
1908
|
rtdb,
|
|
1843
1909
|
agentId,
|
|
@@ -1848,6 +1914,7 @@ export async function main() {
|
|
|
1848
1914
|
applySessionControl(conversationId, control);
|
|
1849
1915
|
},
|
|
1850
1916
|
onSignal: handleControlSignal,
|
|
1917
|
+
onPrimitive: handleControlPrimitive,
|
|
1851
1918
|
onError: (error) => {
|
|
1852
1919
|
// The legacy loop ignored transient RTDB failures; keep read/consume
|
|
1853
1920
|
// errors quiet but surface handler failures.
|
package/dist/session-store.js
CHANGED
|
@@ -17,7 +17,7 @@ function saveStore(store) {
|
|
|
17
17
|
}
|
|
18
18
|
export function buildCodexThreadPolicyFingerprint(input) {
|
|
19
19
|
return createHash('sha256').update(JSON.stringify({
|
|
20
|
-
version:
|
|
20
|
+
version: 2,
|
|
21
21
|
baseCwd: input.baseCwd,
|
|
22
22
|
executionMode: input.executionMode ?? null,
|
|
23
23
|
permissionMode: input.permissionMode ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.12",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-sdk": "^3.4.
|
|
32
|
+
"@canonmsg/agent-sdk": "^3.4.2",
|
|
33
33
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
34
|
-
"@canonmsg/core": "^
|
|
34
|
+
"@canonmsg/core": "^3.0.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.0.0"
|