@canonmsg/codex-plugin 0.18.11 → 0.19.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 +2 -2
- package/dist/app-server-adapter.d.ts +5 -0
- package/dist/app-server-adapter.js +36 -5
- package/dist/app-server-approval.d.ts +1 -1
- package/dist/app-server-approval.js +1 -1
- package/dist/bridge-bin.d.ts +14 -0
- package/dist/bridge-bin.js +27 -0
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -1
- package/dist/codex-app-tools.d.ts +42 -0
- package/dist/codex-app-tools.js +519 -0
- package/dist/control-channel.d.ts +26 -46
- package/dist/control-channel.js +37 -50
- package/dist/host.d.ts +1 -1
- package/dist/host.js +840 -929
- package/dist/register.js +73 -25
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.js +2 -2
- package/dist/turn-activity.d.ts +7 -11
- package/dist/turn-activity.js +7 -41
- package/package.json +10 -7
- package/dist/host-lifecycle.d.ts +0 -4
- package/dist/host-lifecycle.js +0 -3
- package/dist/inbound-policy.d.ts +0 -22
- package/dist/inbound-policy.js +0 -46
- package/dist/startup-recovery.d.ts +0 -46
- package/dist/startup-recovery.js +0 -59
|
@@ -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
|
+
}
|
|
@@ -1,52 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex host
|
|
2
|
+
* Codex host control-channel wiring (bridge notifications).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Phase 6: the host-side RTDB `/control` poller is DELETED — the Bridge is
|
|
5
|
+
* the single authoritative consumer for this identity (one adaptive poller
|
|
6
|
+
* per daemon; cadence, jitter, baselines and consume semantics live there)
|
|
7
|
+
* and pushes `onSessionControl` / `onControlSignal` / `onControlPrimitive`
|
|
8
|
+
* notifications instead. This module keeps the codex-shaped handler wiring:
|
|
9
|
+
* session + signal ride the shared agent-host router; the primitive channel
|
|
10
|
+
* (context.compact) subscribes alongside them.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* the activity probe sampled BEFORE the cycle runs.
|
|
13
|
-
* - Session controls are always consumed once newer (default consume), even
|
|
14
|
-
* when no live session applied them; signals are NOT consumed when the
|
|
15
|
-
* handler throws (consumeOnError stays false).
|
|
16
|
-
* - Dedupe is primed eagerly via `poller.baseline([conversationId])` at
|
|
17
|
-
* session creation — the second legacy poll site.
|
|
12
|
+
* Consume semantics note (deliberate Phase-6 change, plan §2): the node is
|
|
13
|
+
* consumed bridge-side BEFORE dispatch, so handler return values are
|
|
14
|
+
* advisory-only and a throwing handler is logged, never retried — the legacy
|
|
15
|
+
* leave-in-place-on-error behavior is gone with the host poller.
|
|
18
16
|
*/
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
export declare const IDLE_CONTROL_POLL_MS = 10000;
|
|
22
|
-
export declare const CONTROL_POLL_JITTER_MS = 1000;
|
|
17
|
+
import type { ControlPrimitiveEventRecord, ControlSessionEventRecord, ControlSignalEventRecord } from '@canonmsg/backend-contracts';
|
|
18
|
+
import { type ControlNotificationSource, type HostControlHandlerResult } from '@canonmsg/agent-host';
|
|
23
19
|
export interface CodexControlChannelInput {
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* dedupe already advanced, so it is never retried).
|
|
35
|
-
*/
|
|
36
|
-
onSessionControl: (event: ControlSessionEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
|
|
37
|
-
/**
|
|
38
|
-
* Handles a newer `/signal` node. Return `{ consume: false }` to leave the
|
|
39
|
-
* node in place; thrown errors also leave it (consumeOnError stays false).
|
|
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;
|
|
48
|
-
onError?: (error: ControlPollerError) => void;
|
|
49
|
-
/** Injectable jitter source (tests). */
|
|
50
|
-
random?: () => number;
|
|
20
|
+
/** Buffered notification source (registered before the hello completes). */
|
|
21
|
+
source: ControlNotificationSource;
|
|
22
|
+
/** Applies a `/session` control (model/effort; permission is start-only). */
|
|
23
|
+
onSessionControl: (event: ControlSessionEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
|
|
24
|
+
/** Handles a `/signal` node (interrupt / stop_and_drop / new_session). */
|
|
25
|
+
onSignal: (event: ControlSignalEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
|
|
26
|
+
/** Handles a `/primitive` command (context.compact). */
|
|
27
|
+
onPrimitive?: (event: ControlPrimitiveEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
|
|
28
|
+
/** Error sink for throwing handlers (default: console.error). */
|
|
29
|
+
onError?: (error: unknown, kind: 'session' | 'signal' | 'primitive', conversationId: string | null) => void;
|
|
51
30
|
}
|
|
52
|
-
|
|
31
|
+
/** Wire the codex control handlers to the bridge notifications; returns detach. */
|
|
32
|
+
export declare function attachCodexControlNotifications(input: CodexControlChannelInput): () => void;
|