@canonmsg/codex-plugin 0.21.0 → 0.22.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.
Files changed (2) hide show
  1. package/dist/host.js +102 -146
  2. package/package.json +3 -3
package/dist/host.js CHANGED
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
8
  import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
9
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
12
  import { CodexAppServerAdapter } from './app-server-adapter.js';
@@ -522,6 +522,58 @@ export async function main() {
522
522
  }
523
523
  console.error(`[canon-codex] Authenticated as ${agentId}`);
524
524
  }
525
+ // Shared poll/timeout engine. Built-in `input`/`card` descriptors own
526
+ // create+poll (codex passes native/responder policy via payload/options).
527
+ // Approval keeps codex's own resolution shape (no owner-authored outcome
528
+ // messages / session rules — that is the SDK ApprovalManager's job).
529
+ const runtimeRequests = new RuntimeRequestManager(client, {
530
+ agentId,
531
+ ownerId: ownerId ?? '',
532
+ });
533
+ runtimeRequests.register('approval', {
534
+ kind: 'approval',
535
+ generateId: () => randomUUID(),
536
+ create: async ({ client: apiClient, ctx, conversationId, requestId, payload, expiresAt }) => {
537
+ await apiClient.createRuntimeApprovalRequest({
538
+ conversationId,
539
+ approvalId: requestId,
540
+ toolName: payload.toolName,
541
+ toolSummary: payload.toolSummary,
542
+ category: payload.category,
543
+ risk: payload.risk,
544
+ riskLevel: payload.riskLevel,
545
+ native: payload.native,
546
+ details: payload.details,
547
+ ...(payload.diff ? { diff: payload.diff } : {}),
548
+ responseUserId: ctx.ownerId || undefined,
549
+ allowSessionRule: true,
550
+ expiresAt,
551
+ ...(payload.turnId ? { turnId: payload.turnId } : {}),
552
+ });
553
+ return { requestId };
554
+ },
555
+ consume: async ({ client: apiClient, conversationId, requestId }) => {
556
+ const response = await apiClient
557
+ .consumeRuntimeApprovalResponse({ conversationId, approvalId: requestId })
558
+ .catch(() => null);
559
+ if (response?.status === 'allow') {
560
+ return {
561
+ state: 'resolved',
562
+ result: {
563
+ decision: 'allow',
564
+ ...(response.sessionRule ? { sessionRule: response.sessionRule } : {}),
565
+ },
566
+ };
567
+ }
568
+ if (response?.status === 'deny') {
569
+ return { state: 'resolved', result: { decision: 'deny' } };
570
+ }
571
+ if (response?.status === 'timeout')
572
+ return { state: 'timeout' };
573
+ return { state: 'pending' };
574
+ },
575
+ timeoutResult: () => ({ decision: 'deny' }),
576
+ });
525
577
  const launchArgs = [...process.argv.slice(2)];
526
578
  if (!launchArgs.some((arg) => arg === '--cwd' || arg.startsWith('--cwd='))) {
527
579
  launchArgs.push('--cwd', workingDir);
@@ -1015,80 +1067,6 @@ export async function main() {
1015
1067
  ? 'workspace-generated'
1016
1068
  : 'disabled';
1017
1069
  }
1018
- async function waitForRuntimeInputResponse(input) {
1019
- while (Date.now() < input.expiresAt) {
1020
- const response = await client.consumeRuntimeInputResponse({
1021
- conversationId: input.conversationId,
1022
- inputId: input.inputId,
1023
- }).catch(() => null);
1024
- if (response?.status === 'submitted') {
1025
- return { status: 'submitted', value: response.value, answers: response.answers };
1026
- }
1027
- if (response?.status === 'cancelled' || response?.status === 'timeout') {
1028
- return { status: response.status };
1029
- }
1030
- await sleep(1_000);
1031
- }
1032
- const response = await client.consumeRuntimeInputResponse({
1033
- conversationId: input.conversationId,
1034
- inputId: input.inputId,
1035
- }).catch(() => null);
1036
- if (response?.status === 'submitted') {
1037
- return { status: 'submitted', value: response.value, answers: response.answers };
1038
- }
1039
- return { status: 'timeout' };
1040
- }
1041
- async function waitForRuntimeApprovalResponse(input) {
1042
- while (Date.now() < input.expiresAt) {
1043
- const response = await client.consumeRuntimeApprovalResponse({
1044
- conversationId: input.conversationId,
1045
- approvalId: input.approvalId,
1046
- }).catch(() => null);
1047
- if (response?.status === 'allow') {
1048
- return { decision: 'allow', sessionRule: response.sessionRule };
1049
- }
1050
- if (response?.status === 'deny' || response?.status === 'timeout') {
1051
- return { decision: 'deny' };
1052
- }
1053
- await sleep(1_000);
1054
- }
1055
- await client.consumeRuntimeApprovalResponse({
1056
- conversationId: input.conversationId,
1057
- approvalId: input.approvalId,
1058
- }).catch(() => null);
1059
- return { decision: 'deny' };
1060
- }
1061
- async function waitForRuntimeCardResponse(input) {
1062
- while (Date.now() < input.expiresAt) {
1063
- const response = await client.consumeRuntimeCardResponse({
1064
- conversationId: input.conversationId,
1065
- cardId: input.cardId,
1066
- }).catch(() => null);
1067
- if (response?.status === 'submitted') {
1068
- return {
1069
- status: 'submitted',
1070
- ...(response.actionId ? { actionId: response.actionId } : {}),
1071
- ...(response.values ? { values: response.values } : {}),
1072
- };
1073
- }
1074
- if (response?.status === 'cancelled' || response?.status === 'timeout') {
1075
- return { status: response.status };
1076
- }
1077
- await sleep(1_000);
1078
- }
1079
- const response = await client.consumeRuntimeCardResponse({
1080
- conversationId: input.conversationId,
1081
- cardId: input.cardId,
1082
- }).catch(() => null);
1083
- if (response?.status === 'submitted') {
1084
- return {
1085
- status: 'submitted',
1086
- ...(response.actionId ? { actionId: response.actionId } : {}),
1087
- ...(response.values ? { values: response.values } : {}),
1088
- };
1089
- }
1090
- return { status: 'timeout' };
1091
- }
1092
1070
  function runtimeCardRequestPayload(method, params) {
1093
1071
  if (method !== 'item/runtimeCard/request'
1094
1072
  && method !== 'runtimeCard/request') {
@@ -1131,14 +1109,12 @@ export async function main() {
1131
1109
  let requestCreated = false;
1132
1110
  let requestResolved = false;
1133
1111
  try {
1134
- await client.createRuntimeCardRequest({
1135
- conversationId: session.conversationId,
1136
- cardId,
1137
- card: { ...card, cardId },
1138
- expiresAt,
1139
- // Omit responseUserId so the backend targets a reachable member (the
1140
- // owner if present, else the sole other member) — the owner is often
1141
- // not a member of agent-to-user DMs.
1112
+ // Built-in card descriptor owns create+poll; codex passes native/turnId
1113
+ // via payload, post-create turn side effects via onCreated. responseUserId
1114
+ // is omitted (default `infer` policy) so the backend targets a reachable
1115
+ // member (owner is often not a member of agent-to-user DMs).
1116
+ const cardResult = await runtimeRequests.request('card', session.conversationId, {
1117
+ card,
1142
1118
  native: {
1143
1119
  runtime: 'codex',
1144
1120
  method: request.method,
@@ -1150,25 +1126,32 @@ export async function main() {
1150
1126
  },
1151
1127
  },
1152
1128
  turnId: session.currentTurnId ?? undefined,
1153
- });
1154
- requestCreated = true;
1155
- session.turnState = 'waiting_input';
1156
- markTurnProgress(session);
1157
- upsertTurnBlock(session, {
1158
- id: `card:${cardId}`,
1159
- kind: 'input',
1160
- status: 'pending',
1161
- title: card.title,
1162
- summary: card.template ?? 'runtime card',
1163
- });
1164
- writeTurn(session);
1165
- stopVisibleWorkSignal(session);
1166
- writeCodexStreaming(session, null, 'waiting_input');
1167
- const response = await waitForRuntimeCardResponse({
1168
- conversationId: session.conversationId,
1169
- cardId,
1129
+ }, {
1130
+ requestId: cardId,
1170
1131
  expiresAt,
1132
+ onCreated: () => {
1133
+ requestCreated = true;
1134
+ session.turnState = 'waiting_input';
1135
+ markTurnProgress(session);
1136
+ upsertTurnBlock(session, {
1137
+ id: `card:${cardId}`,
1138
+ kind: 'input',
1139
+ status: 'pending',
1140
+ title: card.title,
1141
+ summary: card.template ?? 'runtime card',
1142
+ });
1143
+ writeTurn(session);
1144
+ stopVisibleWorkSignal(session);
1145
+ writeCodexStreaming(session, null, 'waiting_input');
1146
+ },
1171
1147
  });
1148
+ const response = cardResult.status === 'submitted'
1149
+ ? {
1150
+ status: 'submitted',
1151
+ ...(cardResult.actionId ? { actionId: cardResult.actionId } : {}),
1152
+ ...(cardResult.values ? { values: cardResult.values } : {}),
1153
+ }
1154
+ : { status: cardResult.status };
1172
1155
  requestResolved = true;
1173
1156
  const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
1174
1157
  await sendMessageWithRetry(client, session.conversationId, outcome.text, {
@@ -1216,12 +1199,10 @@ export async function main() {
1216
1199
  const paramsArguments = isRecord(params.arguments) ? params.arguments : null;
1217
1200
  const questions = mapCodexQuestions(params.questions ?? paramsInput?.questions ?? paramsArguments?.questions);
1218
1201
  const inputId = readString(params, 'itemId') ?? requestId;
1219
- await client.createRuntimeInputRequest({
1220
- conversationId: session.conversationId,
1221
- inputId,
1202
+ // Built-in input descriptor owns create+poll; default `owner` responder
1203
+ // policy resolves to ctx.ownerId (matching the former responseUserId).
1204
+ const response = await runtimeRequests.request('input', session.conversationId, {
1222
1205
  kind: 'clarify',
1223
- expiresAt,
1224
- responseUserId: ownerId ?? undefined,
1225
1206
  title: 'Codex needs input',
1226
1207
  prompt: questions?.length
1227
1208
  ? 'Codex needs your input to continue.'
@@ -1239,12 +1220,7 @@ export async function main() {
1239
1220
  },
1240
1221
  },
1241
1222
  turnId: session.currentTurnId ?? undefined,
1242
- });
1243
- const response = await waitForRuntimeInputResponse({
1244
- conversationId: session.conversationId,
1245
- inputId,
1246
- expiresAt,
1247
- });
1223
+ }, { requestId: inputId, expiresAt });
1248
1224
  return { answers: response.status === 'submitted' ? response.answers ?? {} : {} };
1249
1225
  }
1250
1226
  const mappedApproval = mapCodexAppServerApprovalRequest({
@@ -1253,31 +1229,12 @@ export async function main() {
1253
1229
  });
1254
1230
  if (mappedApproval) {
1255
1231
  const approvalId = readString(params, 'approvalId') ?? readString(params, 'itemId') ?? requestId;
1256
- await client.createRuntimeApprovalRequest({
1257
- conversationId: session.conversationId,
1258
- approvalId,
1259
- toolName: mappedApproval.toolName,
1260
- toolSummary: mappedApproval.toolSummary,
1261
- category: mappedApproval.category,
1262
- risk: mappedApproval.risk,
1263
- riskLevel: mappedApproval.riskLevel,
1264
- native: {
1265
- ...mappedApproval.native,
1266
- requestId,
1267
- method: request.method,
1268
- },
1269
- details: mappedApproval.details,
1270
- ...(mappedApproval.diff ? { diff: mappedApproval.diff } : {}),
1271
- responseUserId: ownerId ?? undefined,
1272
- allowSessionRule: true,
1273
- expiresAt,
1232
+ // Approval descriptor's create authors the server request (responder = ctx.ownerId).
1233
+ const response = await runtimeRequests.request('approval', session.conversationId, {
1234
+ ...mappedApproval,
1235
+ native: { ...mappedApproval.native, requestId, method: request.method },
1274
1236
  turnId: session.currentTurnId ?? undefined,
1275
- });
1276
- const response = await waitForRuntimeApprovalResponse({
1277
- conversationId: session.conversationId,
1278
- approvalId,
1279
- expiresAt,
1280
- });
1237
+ }, { requestId: approvalId, expiresAt });
1281
1238
  if (request.method === 'item/permissions/requestApproval') {
1282
1239
  return response.decision === 'allow'
1283
1240
  ? { permissions: isRecord(params.permissions) ? params.permissions : {}, scope: response.sessionRule ? 'session' : 'turn' }
@@ -1628,19 +1585,17 @@ export async function main() {
1628
1585
  }
1629
1586
  if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
1630
1587
  await routeArtifactsOnce();
1631
- const planApproval = buildPlanApprovalRequest(session.currentTurnId ?? randomUUID(), 'Plan ready for review.', {
1632
- responseUserId: ownerId ?? undefined,
1588
+ // Route plan-CREATE through the unified spine (`/runtime-plan/request`):
1589
+ // seeds the server-owned pending node/attention/state and authors the
1590
+ // `plan_approval` card. Resolution is UNCHANGED — Codex still re-queues a
1591
+ // fresh turn off the server-authored `plan_approval_reply`.
1592
+ await client.createRuntimePlanRequest({
1593
+ conversationId: session.conversationId,
1594
+ planId: session.currentTurnId ?? randomUUID(),
1633
1595
  title: 'Codex Plan',
1634
1596
  body: result.finalMessage,
1635
- });
1636
- await sendMessageWithRetry(client, session.conversationId, planApproval.text, {
1637
- messageId: buildCodexMessageId(session, 'plan'),
1638
- metadata: {
1639
- ...planApproval.metadata,
1640
- turnId: session.currentTurnId,
1641
- turnSemantics: 'control',
1642
- replyBehavior: 'suppress_auto_reply',
1643
- },
1597
+ ...(ownerId ? { responseUserId: ownerId } : {}),
1598
+ ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1644
1599
  });
1645
1600
  await handoffFinalMessage(session.conversationId);
1646
1601
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent plan approval card`);
@@ -2136,6 +2091,7 @@ export async function main() {
2136
2091
  controlPoller.stop();
2137
2092
  clearInterval(heartbeat);
2138
2093
  clearInterval(idleCheck);
2094
+ runtimeRequests.dispose();
2139
2095
  stream.stop();
2140
2096
  await runtimeState.clearAgentRuntime().catch(() => { });
2141
2097
  for (const session of [...sessions.values()]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
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": "^5.0.0",
32
+ "@canonmsg/agent-sdk": "^5.1.0",
33
33
  "@canonmsg/coding-agent-host": "^0.2.2",
34
- "@canonmsg/core": "^4.0.0"
34
+ "@canonmsg/core": "^4.2.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"