@sentry/junior 0.183.0 → 0.184.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 (71) hide show
  1. package/dist/{agent-hooks-SNRBZYAU.js → agent-hooks-XTZVALGQ.js} +11 -11
  2. package/dist/api/acp/auth.d.ts +46 -0
  3. package/dist/api/acp/authorization-page.d.ts +11 -0
  4. package/dist/api/acp/conversations.d.ts +70 -0
  5. package/dist/api/acp/errors.d.ts +6 -0
  6. package/dist/api/acp/route.d.ts +16 -0
  7. package/dist/api/acp/schema.d.ts +97 -0
  8. package/dist/api/acp/sleep.d.ts +2 -0
  9. package/dist/api/acp/state.d.ts +22 -0
  10. package/dist/api/acp/transport.d.ts +113 -0
  11. package/dist/api.js +29 -29
  12. package/dist/app.d.ts +0 -4
  13. package/dist/app.js +2010 -82
  14. package/dist/{catalog-runtime-3I3JOIIH.js → catalog-runtime-5357XR4F.js} +4 -4
  15. package/dist/{chunk-24OJZ5OU.js → chunk-2ZQPVX7C.js} +6 -6
  16. package/dist/{chunk-LZBPGX5A.js → chunk-3S7ULUWW.js} +1 -1
  17. package/dist/{chunk-AHMWPFIW.js → chunk-4CNDGK25.js} +6 -6
  18. package/dist/{chunk-3HJ4F4L7.js → chunk-4RONS3WR.js} +4 -4
  19. package/dist/{chunk-LMNMMESF.js → chunk-6UDPINOX.js} +2 -2
  20. package/dist/{chunk-W46NIB5H.js → chunk-6VOYP4M6.js} +2 -3
  21. package/dist/{chunk-W5JMPEBO.js → chunk-6WGC3SO4.js} +9 -9
  22. package/dist/{chunk-T5MTGX2E.js → chunk-ACPDVEJO.js} +22 -20
  23. package/dist/{chunk-6UXDTPEN.js → chunk-BCHYQS4B.js} +1 -1
  24. package/dist/{chunk-2ADFGSUS.js → chunk-BHYRYYLY.js} +1 -1
  25. package/dist/{chunk-IO6KFXYE.js → chunk-BT53RIGJ.js} +7 -7
  26. package/dist/{chunk-G2B2XZQW.js → chunk-DUKIDPF6.js} +1 -1
  27. package/dist/{chunk-PLZT45VC.js → chunk-E57WPXWY.js} +5 -5
  28. package/dist/{chunk-2EPYYVJC.js → chunk-FKEZZMXS.js} +8 -8
  29. package/dist/{chunk-R767Y7QV.js → chunk-HIPUIPJM.js} +5 -5
  30. package/dist/{chunk-3L7CWDQQ.js → chunk-HUXFMDBZ.js} +1 -1
  31. package/dist/{chunk-AX3UDIMA.js → chunk-IZDYYYGP.js} +3 -3
  32. package/dist/{chunk-56XONJPF.js → chunk-JA43DV4Y.js} +1 -1
  33. package/dist/{chunk-QI5JW5AG.js → chunk-JM2COLGC.js} +6 -6
  34. package/dist/{chunk-QZV76CM4.js → chunk-JNO2GUDS.js} +21 -21
  35. package/dist/{chunk-35PM4YY5.js → chunk-K7VDC2QW.js} +6 -6
  36. package/dist/{chunk-4WGFWHWP.js → chunk-LCP4EDKR.js} +1 -1
  37. package/dist/{chunk-EENQGEIP.js → chunk-LHXIZLFF.js} +1 -1
  38. package/dist/{chunk-WOKJRLM2.js → chunk-M5YWNMTE.js} +5 -5
  39. package/dist/{chunk-37IQ6DGU.js → chunk-MAPJFUMV.js} +3 -3
  40. package/dist/{chunk-FJMTMGBC.js → chunk-MLSQX2EX.js} +1 -1
  41. package/dist/{chunk-E3OBGRHN.js → chunk-PJPRXO2T.js} +1 -1
  42. package/dist/{chunk-VOV3SCM3.js → chunk-QABCY33S.js} +1 -1
  43. package/dist/{chunk-X52ESOBP.js → chunk-SHJK75Z2.js} +3 -3
  44. package/dist/{chunk-AYW7J6K4.js → chunk-UBOIHNTH.js} +13 -13
  45. package/dist/{chunk-V64JCZUV.js → chunk-UF6BUA5W.js} +3 -3
  46. package/dist/{chunk-KTXOGLNE.js → chunk-WQFEWULF.js} +2 -2
  47. package/dist/{chunk-SQHFVIMZ.js → chunk-YKX6MTLU.js} +1 -1
  48. package/dist/{chunk-HUK5XIZL.js → chunk-Z3QAXJR7.js} +4 -4
  49. package/dist/cli/chat.js +32 -32
  50. package/dist/cli/check.js +5 -5
  51. package/dist/cli/plugins.js +12 -12
  52. package/dist/cli/snapshot-create.js +9 -9
  53. package/dist/cli/upgrade.js +6 -6
  54. package/dist/{db-K2VMNLHQ.js → db-MA52H62H.js} +6 -6
  55. package/dist/instrumentation.js +3 -3
  56. package/dist/nitro.js +6 -6
  57. package/dist/{oauth-callback-server-DZZMTKSS.js → oauth-callback-server-DXGHKN6S.js} +25 -25
  58. package/dist/{runner-IIAKWDXI.js → runner-4IXUAQNK.js} +18 -18
  59. package/dist/{skills-EXIQ7BP5.js → skills-X3DY7AXO.js} +5 -5
  60. package/dist/spawn-43WGS6CH.js +37 -0
  61. package/dist/task-runner-VDRKITPV.js +32 -0
  62. package/dist/{validation-XUGIWBJF.js → validation-Z6JXSTJS.js} +5 -5
  63. package/dist/version.js +1 -1
  64. package/dist/{work-G4RLJ634.js → work-OKWRFBDW.js} +18 -18
  65. package/dist/worker-B236AE72.js +22 -0
  66. package/package.json +5 -6
  67. package/dist/app-adapter.d.ts +0 -85
  68. package/dist/chat/api-turns/adapter-conversations.d.ts +0 -15
  69. package/dist/spawn-ZR6OOEAN.js +0 -37
  70. package/dist/task-runner-C52HYCVD.js +0 -32
  71. package/dist/worker-R2JEHK67.js +0 -22
package/dist/app.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  processConversationWork
3
- } from "./chunk-3HJ4F4L7.js";
3
+ } from "./chunk-4RONS3WR.js";
4
4
  import {
5
5
  bindSpawnAgent
6
- } from "./chunk-VOV3SCM3.js";
6
+ } from "./chunk-QABCY33S.js";
7
7
  import {
8
8
  createAgentInvocationWorker,
9
9
  enqueueAgentInvocation,
10
10
  listPendingAgentInvocationMailboxAppends,
11
11
  routeAgentInvocationWork
12
- } from "./chunk-IO6KFXYE.js";
12
+ } from "./chunk-BT53RIGJ.js";
13
13
  import {
14
14
  GET as GET2,
15
15
  GET2 as GET3,
@@ -36,7 +36,7 @@ import {
36
36
  startSlackProcessingReaction,
37
37
  stripLeadingBotMention,
38
38
  wakePausedTurn
39
- } from "./chunk-T5MTGX2E.js";
39
+ } from "./chunk-ACPDVEJO.js";
40
40
  import "./chunk-FA6POGLH.js";
41
41
  import {
42
42
  GET,
@@ -53,8 +53,8 @@ import {
53
53
  resolveSlackConversationContext,
54
54
  routeApiTurnWork,
55
55
  webActorFromEmail
56
- } from "./chunk-AYW7J6K4.js";
57
- import "./chunk-4WGFWHWP.js";
56
+ } from "./chunk-UBOIHNTH.js";
57
+ import "./chunk-LCP4EDKR.js";
58
58
  import {
59
59
  buildDeliveredTurnStatePatch,
60
60
  completeAuthPauseTurn,
@@ -65,7 +65,7 @@ import {
65
65
  getTurnUserSlackMessageTs,
66
66
  persistAuthPauseTurnState,
67
67
  requireTurnFailureEventId
68
- } from "./chunk-WOKJRLM2.js";
68
+ } from "./chunk-M5YWNMTE.js";
69
69
  import {
70
70
  claimDispatchMailboxAppend,
71
71
  confirmDispatchMailboxAppend,
@@ -85,7 +85,7 @@ import {
85
85
  recordTaskExecution,
86
86
  resolveTurnSessionRouting,
87
87
  scheduleSessionCompletedPluginTasks
88
- } from "./chunk-W5JMPEBO.js";
88
+ } from "./chunk-6WGC3SO4.js";
89
89
  import {
90
90
  ConversationTurnLifecycleService
91
91
  } from "./chunk-M3QYTBQ6.js";
@@ -97,17 +97,17 @@ import {
97
97
  persistThreadRuntimeState,
98
98
  persistThreadState,
99
99
  persistThreadStateById
100
- } from "./chunk-PLZT45VC.js";
100
+ } from "./chunk-E57WPXWY.js";
101
101
  import "./chunk-UTL3MHPC.js";
102
102
  import {
103
103
  createVercelPluginTaskCallback,
104
104
  queueCallback,
105
105
  registerVercelPluginTaskDevConsumer
106
- } from "./chunk-V64JCZUV.js";
106
+ } from "./chunk-UF6BUA5W.js";
107
107
  import {
108
108
  validatePluginEgressCredentialHooks,
109
109
  validatePluginRegistrations
110
- } from "./chunk-3L7CWDQQ.js";
110
+ } from "./chunk-HUXFMDBZ.js";
111
111
  import {
112
112
  defineJuniorPlugins,
113
113
  pluginCatalogConfigFromEnv,
@@ -155,11 +155,11 @@ import {
155
155
  unpublishedArtifactHeaders,
156
156
  verifySlackSignature,
157
157
  withNormalizedThreadId
158
- } from "./chunk-QZV76CM4.js";
158
+ } from "./chunk-JNO2GUDS.js";
159
159
  import {
160
160
  AuthorizationFlowDisabledError,
161
161
  PluginCredentialFailureError
162
- } from "./chunk-35PM4YY5.js";
162
+ } from "./chunk-K7VDC2QW.js";
163
163
  import "./chunk-6Y5B5PEX.js";
164
164
  import "./chunk-DIB4GX3G.js";
165
165
  import {
@@ -168,7 +168,7 @@ import {
168
168
  deleteMcpAuthSessionsForUserProvider,
169
169
  deleteMcpServerSessionId,
170
170
  deleteMcpStoredOAuthCredentials
171
- } from "./chunk-2EPYYVJC.js";
171
+ } from "./chunk-FKEZZMXS.js";
172
172
  import {
173
173
  RetryableDeliveryError
174
174
  } from "./chunk-RF6J3DIO.js";
@@ -176,7 +176,7 @@ import {
176
176
  formatProviderLabel,
177
177
  postSlackMessage,
178
178
  startOAuthFlow
179
- } from "./chunk-QI5JW5AG.js";
179
+ } from "./chunk-JM2COLGC.js";
180
180
  import {
181
181
  getExperimentalFeatures,
182
182
  isExperimentalFeatureEnabled,
@@ -197,10 +197,10 @@ import {
197
197
  scheduledRunId,
198
198
  scheduledRunSchema,
199
199
  withScheduledTaskLock
200
- } from "./chunk-R767Y7QV.js";
200
+ } from "./chunk-HIPUIPJM.js";
201
201
  import {
202
202
  parseSlackThreadId
203
- } from "./chunk-SQHFVIMZ.js";
203
+ } from "./chunk-YKX6MTLU.js";
204
204
  import {
205
205
  CONVERSATION_WORK_STALE_ENQUEUE_MS,
206
206
  appendAndEnqueueInboundMessage,
@@ -215,7 +215,7 @@ import {
215
215
  removeActiveConversation,
216
216
  requestConversationWork,
217
217
  stopConversationWork
218
- } from "./chunk-HUK5XIZL.js";
218
+ } from "./chunk-Z3QAXJR7.js";
219
219
  import {
220
220
  ConversationTurnBoundaryError,
221
221
  CooperativeTurnYieldError,
@@ -233,7 +233,7 @@ import {
233
233
  markTurnCompleted,
234
234
  markTurnFailed,
235
235
  startActiveTurn
236
- } from "./chunk-56XONJPF.js";
236
+ } from "./chunk-JA43DV4Y.js";
237
237
  import {
238
238
  abandonTurnRecord,
239
239
  appendThreadContextMessages,
@@ -269,11 +269,11 @@ import {
269
269
  trimTrailingAssistantMessages,
270
270
  turnHasReply,
271
271
  upsertConversationMessage
272
- } from "./chunk-24OJZ5OU.js";
272
+ } from "./chunk-2ZQPVX7C.js";
273
273
  import {
274
274
  getSandboxResourceConfig,
275
275
  setSandboxResourceConfig
276
- } from "./chunk-37IQ6DGU.js";
276
+ } from "./chunk-MAPJFUMV.js";
277
277
  import "./chunk-PDO5BLNM.js";
278
278
  import "./chunk-UIE3R5XU.js";
279
279
  import {
@@ -292,7 +292,7 @@ import {
292
292
  import "./chunk-7H2VZTWN.js";
293
293
  import {
294
294
  getTurnRequestDeadline
295
- } from "./chunk-FJMTMGBC.js";
295
+ } from "./chunk-MLSQX2EX.js";
296
296
  import {
297
297
  SlackActionError,
298
298
  bindEventTaskCredentialSubject,
@@ -318,7 +318,7 @@ import {
318
318
  verifyEventTaskCredentialSubject,
319
319
  verifyScheduledTaskCredentialSubject,
320
320
  verifySlackDirectCredentialSubject
321
- } from "./chunk-AHMWPFIW.js";
321
+ } from "./chunk-4CNDGK25.js";
322
322
  import "./chunk-3Y47VMHV.js";
323
323
  import {
324
324
  contextProvenance,
@@ -331,7 +331,7 @@ import {
331
331
  requireSlackDestination,
332
332
  withConversationEventLock,
333
333
  withConversationMutationLock
334
- } from "./chunk-AX3UDIMA.js";
334
+ } from "./chunk-IZDYYYGP.js";
335
335
  import {
336
336
  juniorAgentBindings,
337
337
  juniorAgentInvocations,
@@ -339,13 +339,13 @@ import {
339
339
  juniorConversations,
340
340
  juniorSchedulerRuns,
341
341
  juniorSchedulerTasks
342
- } from "./chunk-6UXDTPEN.js";
343
- import "./chunk-X52ESOBP.js";
342
+ } from "./chunk-BCHYQS4B.js";
343
+ import "./chunk-SHJK75Z2.js";
344
344
  import {
345
345
  acquireActiveLock,
346
346
  getStateAdapter,
347
347
  withActiveLock
348
- } from "./chunk-G2B2XZQW.js";
348
+ } from "./chunk-DUKIDPF6.js";
349
349
  import {
350
350
  juniorDestinations
351
351
  } from "./chunk-VFUK3X5B.js";
@@ -366,13 +366,13 @@ import {
366
366
  modelIdForProfile,
367
367
  setProfiles,
368
368
  setSlackReactionConfig
369
- } from "./chunk-LMNMMESF.js";
370
- import "./chunk-2ADFGSUS.js";
371
- import "./chunk-KTXOGLNE.js";
369
+ } from "./chunk-6UDPINOX.js";
370
+ import "./chunk-BHYRYYLY.js";
371
+ import "./chunk-WQFEWULF.js";
372
372
  import {
373
373
  pluginCatalogRuntime
374
- } from "./chunk-E3OBGRHN.js";
375
- import "./chunk-EENQGEIP.js";
374
+ } from "./chunk-PJPRXO2T.js";
375
+ import "./chunk-LHXIZLFF.js";
376
376
  import {
377
377
  credentialContextForActor
378
378
  } from "./chunk-CEMMLV5L.js";
@@ -407,13 +407,13 @@ import {
407
407
  withContext,
408
408
  withLogContext,
409
409
  withSpan
410
- } from "./chunk-LZBPGX5A.js";
410
+ } from "./chunk-3S7ULUWW.js";
411
411
  import {
412
412
  continueTrace
413
413
  } from "./chunk-PEQXYSSE.js";
414
414
  import {
415
415
  JUNIOR_VERSION
416
- } from "./chunk-W46NIB5H.js";
416
+ } from "./chunk-6VOYP4M6.js";
417
417
 
418
418
  // src/app.ts
419
419
  import { createRequire } from "module";
@@ -698,10 +698,10 @@ async function readDispatchTurnResult(dispatch2) {
698
698
  const session = storedSession ?? summary;
699
699
  const dispatchOutcome = summary?.dispatchOutcome ?? storedSession?.dispatchOutcome;
700
700
  const resultMessageTs = summary?.resultMessageId ?? storedSession?.resultMessageId;
701
- const errorMessage = storedSession?.errorMessage;
701
+ const errorMessage3 = storedSession?.errorMessage;
702
702
  if (dispatchOutcome) {
703
703
  return {
704
- ...errorMessage ? { errorMessage } : void 0,
704
+ ...errorMessage3 ? { errorMessage: errorMessage3 } : void 0,
705
705
  outcome: dispatchOutcome,
706
706
  ...resultMessageTs ? { resultMessageTs } : void 0
707
707
  };
@@ -1274,13 +1274,13 @@ async function skipMissedRun(db, args) {
1274
1274
  return void 0;
1275
1275
  }
1276
1276
  const duplicateOf = await findStaleRecoveryCanonicalTask(db, current);
1277
- const errorMessage = duplicateOf ? `Duplicate stale scheduled task was skipped without dispatch. Canonical task: ${duplicateOf.id}.` : "Scheduled occurrence was more than 24 hours late and was skipped without dispatch.";
1277
+ const errorMessage3 = duplicateOf ? `Duplicate stale scheduled task was skipped without dispatch. Canonical task: ${duplicateOf.id}.` : "Scheduled occurrence was more than 24 hours late and was skipped without dispatch.";
1278
1278
  const skipped = {
1279
1279
  id: scheduledRunId(current.id, args.scheduledForMs),
1280
1280
  attempt: 1,
1281
1281
  claimedAtMs: args.nowMs,
1282
1282
  completedAtMs: args.nowMs,
1283
- errorMessage,
1283
+ errorMessage: errorMessage3,
1284
1284
  scheduledForMs: args.scheduledForMs,
1285
1285
  status: "skipped",
1286
1286
  taskId: current.id
@@ -1299,12 +1299,12 @@ async function skipMissedRun(db, args) {
1299
1299
  nextRunAtMs,
1300
1300
  runNowAtMs: isRunNow ? void 0 : current.runNowAtMs,
1301
1301
  status,
1302
- statusReason: status === "deleted" ? errorMessage : void 0,
1302
+ statusReason: status === "deleted" ? errorMessage3 : void 0,
1303
1303
  updatedAtMs: args.nowMs
1304
1304
  },
1305
1305
  current
1306
1306
  );
1307
- return { errorMessage, run: skipped, task: current };
1307
+ return { errorMessage: errorMessage3, run: skipped, task: current };
1308
1308
  }
1309
1309
  async function claimDueScheduledRun(db, args) {
1310
1310
  const skippedRuns = [];
@@ -7147,7 +7147,7 @@ function createSlackDispatchTurnRunner(options) {
7147
7147
  isSubscribedContext: true
7148
7148
  });
7149
7149
  let outcome;
7150
- let errorMessage;
7150
+ let errorMessage3;
7151
7151
  let resultMessageTs;
7152
7152
  const routing = buildDispatchRoutingContext(dispatch2);
7153
7153
  await options.executeSlackTurn(thread, message, {
@@ -7173,13 +7173,13 @@ function createSlackDispatchTurnRunner(options) {
7173
7173
  },
7174
7174
  onTurnOutcome: (result) => {
7175
7175
  outcome = result.outcome;
7176
- errorMessage = result.errorMessage;
7176
+ errorMessage3 = result.errorMessage;
7177
7177
  },
7178
7178
  shouldYield: hooks.shouldYield,
7179
7179
  skipBackfill: true
7180
7180
  });
7181
7181
  return {
7182
- ...errorMessage ? { errorMessage } : void 0,
7182
+ ...errorMessage3 ? { errorMessage: errorMessage3 } : void 0,
7183
7183
  ...outcome ? { outcome } : void 0,
7184
7184
  ...resultMessageTs ? { resultMessageTs } : void 0
7185
7185
  };
@@ -7411,7 +7411,7 @@ async function failTurnBestEffort(args) {
7411
7411
  );
7412
7412
  }
7413
7413
  }
7414
- async function persistFailedReplyState(turn, errorMessage = "Paused agent run failed while continuing") {
7414
+ async function persistFailedReplyState(turn, errorMessage3 = "Paused agent run failed while continuing") {
7415
7415
  const currentState = await getPersistedThreadState(turn.conversationId);
7416
7416
  const conversation = coerceThreadConversationState(currentState);
7417
7417
  await hydrateConversationMessages({
@@ -7428,7 +7428,7 @@ async function persistFailedReplyState(turn, errorMessage = "Paused agent run fa
7428
7428
  });
7429
7429
  await failTurnBestEffort({
7430
7430
  turn,
7431
- errorMessage
7431
+ errorMessage: errorMessage3
7432
7432
  });
7433
7433
  await persistThreadStateById(turn.conversationId, {
7434
7434
  conversation
@@ -8189,7 +8189,7 @@ async function stopApiConversationTurn(args) {
8189
8189
  return result;
8190
8190
  }
8191
8191
 
8192
- // src/chat/api-turns/adapter-conversations.ts
8192
+ // src/api/acp/conversations.ts
8193
8193
  var EVENT_PAGE_SIZE = 50;
8194
8194
  function actorFromUser(user) {
8195
8195
  return webActorFromEmail(
@@ -8227,7 +8227,7 @@ async function latestEventCursor(eventStore, conversationId) {
8227
8227
  const page = await eventStore.query(conversationId, { limit: 1 });
8228
8228
  return page.events.at(-1)?.seq ?? 0;
8229
8229
  }
8230
- function createAdapterConversations(options) {
8230
+ function createAcpConversations(options) {
8231
8231
  return {
8232
8232
  async cancel({ conversationId, user }) {
8233
8233
  if (!await hasConversationAccess(conversationId, user)) {
@@ -8340,8 +8340,1928 @@ function createAdapterConversations(options) {
8340
8340
  };
8341
8341
  }
8342
8342
 
8343
+ // src/api/acp/route.ts
8344
+ import { createHash as createHash4 } from "crypto";
8345
+ import * as acp4 from "@agentclientprotocol/sdk";
8346
+
8347
+ // src/api/acp/auth.ts
8348
+ import {
8349
+ createHash as createHash3,
8350
+ randomBytes,
8351
+ randomUUID as randomUUID2,
8352
+ timingSafeEqual as timingSafeEqual3
8353
+ } from "crypto";
8354
+ import * as acp3 from "@agentclientprotocol/sdk";
8355
+ import { userSchema as userSchema2 } from "@sentry/junior-plugin-api";
8356
+ import { z as z7 } from "zod";
8357
+
8358
+ // src/api/acp/state.ts
8359
+ import { setTimeout as wait } from "timers/promises";
8360
+ var MUTATION_LOCK_TTL_MS = 1e4;
8361
+ async function withLock(state, key, run, options = {}) {
8362
+ const ttlMs = options.ttlMs ?? MUTATION_LOCK_TTL_MS;
8363
+ const startedAtMs = Date.now();
8364
+ let lock;
8365
+ while (true) {
8366
+ lock = await state.acquireLock(key, ttlMs);
8367
+ if (lock) break;
8368
+ if (Date.now() - startedAtMs >= (options.waitMs ?? 0)) {
8369
+ return { acquired: false };
8370
+ }
8371
+ await wait(options.retryMs ?? 25, void 0, { ref: false });
8372
+ }
8373
+ const heartbeat = options.keepAlive ? setInterval(
8374
+ () => void state.extendLock(lock, ttlMs).catch(() => void 0),
8375
+ Math.floor(ttlMs / 3)
8376
+ ) : void 0;
8377
+ heartbeat?.unref();
8378
+ try {
8379
+ return { acquired: true, value: await run(lock) };
8380
+ } finally {
8381
+ if (heartbeat) clearInterval(heartbeat);
8382
+ await state.releaseLock(lock);
8383
+ }
8384
+ }
8385
+ async function fenceLock(state, lock, ttlMs = MUTATION_LOCK_TTL_MS) {
8386
+ if (!await state.extendLock(lock, ttlMs)) {
8387
+ throw new Error(
8388
+ `ACP lock ownership was lost before write for ${lock.threadId}`
8389
+ );
8390
+ }
8391
+ }
8392
+
8393
+ // src/api/acp/transport.ts
8394
+ import { createHash as createHash2, randomUUID } from "crypto";
8395
+ import * as acp2 from "@agentclientprotocol/sdk";
8396
+ import { userSchema } from "@sentry/junior-plugin-api";
8397
+ import { z as z6 } from "zod";
8398
+
8399
+ // src/api/acp/schema.ts
8400
+ import * as acp from "@agentclientprotocol/sdk";
8401
+ import { z as z5 } from "zod";
8402
+ var ACP_SESSION_ID_PATTERN = /^local:acp:[a-f0-9]{32}$/;
8403
+ var metaSchema = z5.record(z5.string(), z5.unknown()).nullable();
8404
+ var jsonRpcIdSchema = z5.union([z5.string(), z5.number().finite(), z5.null()]);
8405
+ var SESSION_HEADER_METHODS = /* @__PURE__ */ new Set([
8406
+ acp.methods.agent.session.cancel,
8407
+ acp.methods.agent.session.close,
8408
+ acp.methods.agent.session.load,
8409
+ acp.methods.agent.session.prompt,
8410
+ acp.methods.agent.session.resume,
8411
+ acp.methods.agent.session.setConfigOption,
8412
+ acp.methods.agent.session.setMode
8413
+ ]);
8414
+ var acpConnectionIdSchema = z5.string().uuid();
8415
+ var acpSessionIdSchema = z5.string().regex(ACP_SESSION_ID_PATTERN);
8416
+ var acpCallSchema = z5.object({
8417
+ id: jsonRpcIdSchema.optional(),
8418
+ jsonrpc: z5.literal("2.0"),
8419
+ method: z5.string().min(1),
8420
+ params: z5.unknown().optional()
8421
+ }).strict();
8422
+ var jsonRpcErrorSchema = z5.object({
8423
+ code: z5.number().int(),
8424
+ data: z5.unknown().optional(),
8425
+ message: z5.string()
8426
+ }).strict();
8427
+ var acpResponseSchema = z5.union([
8428
+ z5.object({
8429
+ id: jsonRpcIdSchema,
8430
+ jsonrpc: z5.literal("2.0"),
8431
+ result: z5.unknown()
8432
+ }).strict(),
8433
+ z5.object({
8434
+ error: jsonRpcErrorSchema,
8435
+ id: jsonRpcIdSchema,
8436
+ jsonrpc: z5.literal("2.0")
8437
+ }).strict()
8438
+ ]);
8439
+ var acpInboundMessageSchema = z5.union([
8440
+ acpCallSchema,
8441
+ acpResponseSchema
8442
+ ]);
8443
+ var implementationSchema = z5.object({
8444
+ _meta: metaSchema.optional(),
8445
+ name: z5.string().min(1),
8446
+ title: z5.string().nullable().optional(),
8447
+ version: z5.string()
8448
+ }).strict();
8449
+ var initializeParamsSchema = z5.object({
8450
+ _meta: metaSchema.optional(),
8451
+ clientCapabilities: z5.record(z5.string(), z5.unknown()).optional(),
8452
+ clientInfo: implementationSchema.nullable().optional(),
8453
+ protocolVersion: z5.number().int().min(0).max(65535)
8454
+ }).strict();
8455
+ var authenticateParamsSchema = z5.object({
8456
+ _meta: metaSchema.optional(),
8457
+ methodId: z5.string().min(1)
8458
+ }).strict();
8459
+ var newSessionParamsSchema = z5.object({
8460
+ _meta: metaSchema.optional(),
8461
+ additionalDirectories: z5.array(z5.string()).optional(),
8462
+ cwd: z5.string().min(1),
8463
+ mcpServers: z5.array(z5.unknown())
8464
+ }).strict();
8465
+ var loadSessionParamsSchema = newSessionParamsSchema.extend({
8466
+ sessionId: acpSessionIdSchema
8467
+ });
8468
+ var annotationsSchema = z5.object({
8469
+ _meta: metaSchema.optional(),
8470
+ audience: z5.array(z5.enum(["assistant", "user"])).nullable().optional(),
8471
+ lastModified: z5.string().nullable().optional(),
8472
+ priority: z5.number().finite().nullable().optional()
8473
+ }).strict();
8474
+ var textContentBlockSchema = z5.object({
8475
+ _meta: metaSchema.optional(),
8476
+ annotations: annotationsSchema.nullable().optional(),
8477
+ text: z5.string(),
8478
+ type: z5.literal("text")
8479
+ }).strict();
8480
+ var promptParamsSchema = z5.object({
8481
+ _meta: metaSchema.optional(),
8482
+ prompt: z5.array(textContentBlockSchema),
8483
+ sessionId: acpSessionIdSchema
8484
+ }).strict();
8485
+ var cancelParamsSchema = z5.object({
8486
+ _meta: metaSchema.optional(),
8487
+ sessionId: acpSessionIdSchema
8488
+ }).strict();
8489
+
8490
+ // src/api/acp/sleep.ts
8491
+ function sleep(ms, signal) {
8492
+ return new Promise((resolve, reject) => {
8493
+ if (signal.aborted) {
8494
+ reject(signal.reason);
8495
+ return;
8496
+ }
8497
+ const timeoutId = setTimeout(() => {
8498
+ signal.removeEventListener("abort", onAbort);
8499
+ resolve();
8500
+ }, ms);
8501
+ const onAbort = () => {
8502
+ clearTimeout(timeoutId);
8503
+ reject(signal.reason);
8504
+ };
8505
+ signal.addEventListener("abort", onAbort, { once: true });
8506
+ });
8507
+ }
8508
+
8509
+ // src/api/acp/transport.ts
8510
+ var ACP_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
8511
+ var ACP_SSE_LEASE_TTL_MS = 6e4;
8512
+ var ACP_SSE_LEASE_RENEW_INTERVAL_MS = 2e4;
8513
+ var EVENT_POLL_INTERVAL_MS = 100;
8514
+ var LOCK_WAIT_MS = 5e3;
8515
+ var MAX_STREAM_ITEMS = 1024;
8516
+ var MUTATION_LOCK_OPTIONS = {
8517
+ keepAlive: true,
8518
+ ttlMs: MUTATION_LOCK_TTL_MS,
8519
+ waitMs: LOCK_WAIT_MS
8520
+ };
8521
+ var SSE_KEEP_ALIVE_MS = 15e3;
8522
+ var SSE_MAINTENANCE_INTERVAL_MS = 1e3;
8523
+ var jsonRpcIdSchema2 = z6.union([z6.string(), z6.number().finite(), z6.null()]);
8524
+ var acpConnectionSchema = z6.object({
8525
+ credentialHash: z6.string().length(64),
8526
+ nonce: z6.string().min(1),
8527
+ user: userSchema.optional()
8528
+ }).strict();
8529
+ var acpStreamRouteSchema = z6.object({
8530
+ connectionId: z6.string().min(1),
8531
+ sessionId: z6.string().min(1).optional()
8532
+ }).strict();
8533
+ var acpPromptStreamOutputSchema = z6.object({
8534
+ afterSeq: z6.number().int().nonnegative(),
8535
+ kind: z6.literal("prompt"),
8536
+ messageId: z6.string().min(1),
8537
+ requestId: jsonRpcIdSchema2,
8538
+ turnId: z6.string().min(1)
8539
+ }).strict();
8540
+ var acpStreamOutputSchema = z6.discriminatedUnion("kind", [
8541
+ z6.object({
8542
+ kind: z6.literal("message"),
8543
+ message: acpInboundMessageSchema
8544
+ }).strict(),
8545
+ z6.object({ kind: z6.literal("replay") }).strict(),
8546
+ acpPromptStreamOutputSchema
8547
+ ]);
8548
+ var storedAcpStreamItemSchema = z6.object({
8549
+ id: z6.string().min(1),
8550
+ output: acpStreamOutputSchema
8551
+ }).strict();
8552
+ var acpRequestReceiptSchema = z6.object({
8553
+ deliveryKey: z6.string().min(1).optional(),
8554
+ outputs: z6.array(acpStreamOutputSchema),
8555
+ sessionId: z6.string().min(1).optional()
8556
+ }).strict();
8557
+ var acpStreamCursorSchema = z6.object({ itemId: z6.string().min(1) }).strict();
8558
+ function stableHex(value) {
8559
+ return createHash2("sha256").update(value).digest("hex").slice(0, 32);
8560
+ }
8561
+ function connectionKey(connectionId) {
8562
+ return `junior:acp:v1:connection:${connectionId}`;
8563
+ }
8564
+ function receiptKey(connectionId, requestKey2) {
8565
+ return `${connectionKey(connectionId)}:request:${requestKey2}`;
8566
+ }
8567
+ function receiptLockKey(connectionId, requestKey2) {
8568
+ return `${receiptKey(connectionId, requestKey2)}:lock`;
8569
+ }
8570
+ function connectionLockKey(connectionId) {
8571
+ return `${connectionKey(connectionId)}:lock`;
8572
+ }
8573
+ function streamKey(route) {
8574
+ const suffix = route.sessionId ? `session:${stableHex(route.sessionId)}` : "connection";
8575
+ return `${connectionKey(route.connectionId)}:stream:${suffix}`;
8576
+ }
8577
+ function streamListKey(route) {
8578
+ return `${streamKey(route)}:items`;
8579
+ }
8580
+ function streamCursorKey(route) {
8581
+ return `${streamKey(route)}:cursor`;
8582
+ }
8583
+ function streamLockKey(route) {
8584
+ return `${streamKey(route)}:subscriber`;
8585
+ }
8586
+ function streamAppendLockKey(route) {
8587
+ return `${streamKey(route)}:append`;
8588
+ }
8589
+ function requestStreamItemId(requestKey2, index) {
8590
+ return `acp-item:${stableHex(`${requestKey2}:${index}`)}`;
8591
+ }
8592
+ function parseStreamItem(value) {
8593
+ return storedAcpStreamItemSchema.parse(value);
8594
+ }
8595
+ function parseReceipt(value) {
8596
+ return acpRequestReceiptSchema.parse(value);
8597
+ }
8598
+ async function readStreamItems(state, route) {
8599
+ return (await state.getList(streamListKey(route))).map(parseStreamItem);
8600
+ }
8601
+ async function readStreamCursor(state, route) {
8602
+ const value = await state.get(streamCursorKey(route));
8603
+ return value === null || value === void 0 ? void 0 : acpStreamCursorSchema.parse(value).itemId;
8604
+ }
8605
+ async function connectionIsLive(state, connectionId) {
8606
+ return Boolean(await readAcpConnection(state, connectionId));
8607
+ }
8608
+ async function createAcpConnection(state, credentialHash2) {
8609
+ const connectionId = randomUUID();
8610
+ const connection = acpConnectionSchema.parse({
8611
+ credentialHash: credentialHash2,
8612
+ nonce: randomUUID()
8613
+ });
8614
+ await state.set(connectionKey(connectionId), connection, ACP_STATE_TTL_MS);
8615
+ return { connection, connectionId };
8616
+ }
8617
+ async function readAcpConnection(state, connectionId) {
8618
+ const value = await state.get(connectionKey(connectionId));
8619
+ return value === null || value === void 0 ? void 0 : acpConnectionSchema.parse(value);
8620
+ }
8621
+ async function deleteAcpConnection(state, connectionId) {
8622
+ const result = await withLock(
8623
+ state,
8624
+ connectionLockKey(connectionId),
8625
+ async (lock) => {
8626
+ await fenceLock(state, lock, MUTATION_LOCK_TTL_MS);
8627
+ await state.delete(connectionKey(connectionId));
8628
+ await fenceLock(state, lock, MUTATION_LOCK_TTL_MS);
8629
+ await state.delete(streamCursorKey({ connectionId }));
8630
+ },
8631
+ MUTATION_LOCK_OPTIONS
8632
+ );
8633
+ if (!result.acquired) {
8634
+ throw new Error("Timed out acquiring ACP connection control");
8635
+ }
8636
+ }
8637
+ async function bindAcpConnectionUser(args) {
8638
+ const result = await withLock(
8639
+ args.state,
8640
+ connectionLockKey(args.connectionId),
8641
+ async (lock) => {
8642
+ const connection = await readAcpConnection(args.state, args.connectionId);
8643
+ if (!connection || connection.credentialHash !== args.credentialHash) {
8644
+ return "expired";
8645
+ }
8646
+ if (connection.user && connection.user.id !== args.user.id) {
8647
+ return "conflict";
8648
+ }
8649
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8650
+ await args.state.set(
8651
+ connectionKey(args.connectionId),
8652
+ acpConnectionSchema.parse({ ...connection, user: args.user }),
8653
+ ACP_STATE_TTL_MS
8654
+ );
8655
+ return "completed";
8656
+ },
8657
+ MUTATION_LOCK_OPTIONS
8658
+ );
8659
+ if (!result.acquired) {
8660
+ throw new Error("Timed out acquiring ACP connection control");
8661
+ }
8662
+ return result.value;
8663
+ }
8664
+ async function prepareStreamAppend(args) {
8665
+ const existing = await readStreamItems(args.state, args.route);
8666
+ const cursor = await readStreamCursor(args.state, args.route);
8667
+ const cursorIndex = cursor ? existing.findIndex((item) => item.id === cursor) : -1;
8668
+ const pending = existing.slice(cursorIndex >= 0 ? cursorIndex + 1 : 0);
8669
+ const presentIds = /* @__PURE__ */ new Set();
8670
+ const pendingIds = /* @__PURE__ */ new Set();
8671
+ for (const id of args.ids) {
8672
+ const matches = (item) => item.id === id || item.id.startsWith(`${id}:delivery:`);
8673
+ if (existing.some(matches)) presentIds.add(id);
8674
+ if (pending.some(matches)) pendingIds.add(id);
8675
+ }
8676
+ const retainedIds = pendingIds.size === 0 ? /* @__PURE__ */ new Set() : presentIds;
8677
+ const missingCount = args.ids.length - retainedIds.size;
8678
+ if (missingCount > MAX_STREAM_ITEMS) throw new AcpStreamFullError();
8679
+ if (missingCount === 0 || existing.length + missingCount <= MAX_STREAM_ITEMS) {
8680
+ return {
8681
+ existingIds: new Set(existing.map((item) => item.id)),
8682
+ retainedIds
8683
+ };
8684
+ }
8685
+ if (existing.length === 0 || cursorIndex !== existing.length - 1) {
8686
+ throw new AcpStreamFullError();
8687
+ }
8688
+ await fenceLock(args.state, args.lock, MUTATION_LOCK_TTL_MS);
8689
+ await args.state.delete(streamListKey(args.route));
8690
+ await args.state.delete(streamCursorKey(args.route));
8691
+ return { existingIds: /* @__PURE__ */ new Set(), retainedIds: /* @__PURE__ */ new Set() };
8692
+ }
8693
+ async function appendStreamOutputWithLock(args) {
8694
+ const route = acpStreamRouteSchema.parse(args.route);
8695
+ const output = acpStreamOutputSchema.parse(args.output);
8696
+ let id = args.id;
8697
+ for (let delivery = 1; args.existingIds.has(id); delivery += 1) {
8698
+ id = `${args.id}:delivery:${delivery}`;
8699
+ }
8700
+ await fenceLock(args.state, args.lock, MUTATION_LOCK_TTL_MS);
8701
+ if (output.kind !== "message" && !route.sessionId) {
8702
+ throw new Error(`ACP ${output.kind} output requires a session stream`);
8703
+ }
8704
+ await args.state.appendToList(
8705
+ streamListKey(route),
8706
+ { id, output },
8707
+ { ttlMs: ACP_STATE_TTL_MS }
8708
+ );
8709
+ args.existingIds.add(id);
8710
+ }
8711
+ var AcpStreamFullError = class extends Error {
8712
+ constructor() {
8713
+ super("ACP stream has too much undelivered output");
8714
+ this.name = "AcpStreamFullError";
8715
+ }
8716
+ };
8717
+ async function streamSessionReplay(args) {
8718
+ const messages = await args.conversations.readMessages(args.sessionId);
8719
+ for (const message of messages) {
8720
+ if (args.signal.aborted) return false;
8721
+ const emitted = await args.emit({
8722
+ jsonrpc: "2.0",
8723
+ method: acp2.methods.client.session.update,
8724
+ params: {
8725
+ sessionId: args.sessionId,
8726
+ update: {
8727
+ sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
8728
+ content: { type: "text", text: message.text },
8729
+ messageId: message.id
8730
+ }
8731
+ }
8732
+ });
8733
+ if (!emitted) return false;
8734
+ }
8735
+ return true;
8736
+ }
8737
+ async function queueReceipt(args) {
8738
+ const receipt = parseReceipt(args.receipt);
8739
+ const deliveryKey = receipt.deliveryKey ?? args.requestKey;
8740
+ const route = { connectionId: args.connectionId };
8741
+ if (receipt.sessionId) route.sessionId = receipt.sessionId;
8742
+ const ids = receipt.outputs.map(
8743
+ (_, index) => requestStreamItemId(deliveryKey, index)
8744
+ );
8745
+ const appendAll = async (lock) => {
8746
+ const prepared = await prepareStreamAppend({
8747
+ ids,
8748
+ lock,
8749
+ route,
8750
+ state: args.state
8751
+ });
8752
+ for (const [index, output] of receipt.outputs.entries()) {
8753
+ const id = ids[index];
8754
+ if (!id || prepared.retainedIds.has(id)) continue;
8755
+ await appendStreamOutputWithLock({
8756
+ existingIds: prepared.existingIds,
8757
+ id,
8758
+ lock,
8759
+ output,
8760
+ route,
8761
+ state: args.state
8762
+ });
8763
+ }
8764
+ };
8765
+ if (args.stream) {
8766
+ if (streamAppendLockKey(args.stream.route) !== streamAppendLockKey(route)) {
8767
+ throw new Error("Reserved ACP output changed streams");
8768
+ }
8769
+ await appendAll(args.stream.lock);
8770
+ return;
8771
+ }
8772
+ const result = await withLock(
8773
+ args.state,
8774
+ streamAppendLockKey(route),
8775
+ appendAll,
8776
+ MUTATION_LOCK_OPTIONS
8777
+ );
8778
+ if (!result.acquired) {
8779
+ throw new Error("Timed out acquiring ACP stream append control");
8780
+ }
8781
+ }
8782
+ async function completeAcpRequest(args) {
8783
+ try {
8784
+ const result = await withLock(
8785
+ args.state,
8786
+ receiptLockKey(args.connectionId, args.requestKey),
8787
+ async (lock) => {
8788
+ if (!await readAcpConnection(args.state, args.connectionId)) {
8789
+ return "expired";
8790
+ }
8791
+ const receipt = parseReceipt(args.receipt);
8792
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8793
+ await args.state.set(
8794
+ receiptKey(args.connectionId, args.requestKey),
8795
+ receipt,
8796
+ ACP_STATE_TTL_MS
8797
+ );
8798
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8799
+ await queueReceipt({
8800
+ connectionId: args.connectionId,
8801
+ receipt,
8802
+ requestKey: args.requestKey,
8803
+ state: args.state
8804
+ });
8805
+ return "completed";
8806
+ },
8807
+ MUTATION_LOCK_OPTIONS
8808
+ );
8809
+ if (!result.acquired) {
8810
+ throw new Error("Timed out acquiring ACP request completion control");
8811
+ }
8812
+ return result.value;
8813
+ } catch (error) {
8814
+ if (error instanceof AcpStreamFullError) return "full";
8815
+ throw error;
8816
+ }
8817
+ }
8818
+ async function acceptAcpRequest(args) {
8819
+ try {
8820
+ const result = await withLock(
8821
+ args.state,
8822
+ receiptLockKey(args.connectionId, args.requestKey),
8823
+ async (lock) => {
8824
+ if (!await readAcpConnection(args.state, args.connectionId)) {
8825
+ return "expired";
8826
+ }
8827
+ const stored = await args.state.get(
8828
+ receiptKey(args.connectionId, args.requestKey)
8829
+ );
8830
+ const receipt = stored === null || stored === void 0 ? void 0 : parseReceipt(stored);
8831
+ if (!receipt) {
8832
+ const createAndQueue = async (stream2) => {
8833
+ const created = parseReceipt(await args.createReceipt());
8834
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8835
+ await args.state.set(
8836
+ receiptKey(args.connectionId, args.requestKey),
8837
+ created,
8838
+ ACP_STATE_TTL_MS
8839
+ );
8840
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8841
+ const queue = {
8842
+ connectionId: args.connectionId,
8843
+ receipt: created,
8844
+ requestKey: args.requestKey,
8845
+ state: args.state
8846
+ };
8847
+ if (stream2) queue.stream = stream2;
8848
+ await queueReceipt(queue);
8849
+ };
8850
+ if (!args.reserveRoute) {
8851
+ if (!await readAcpConnection(args.state, args.connectionId)) {
8852
+ return "expired";
8853
+ }
8854
+ await createAndQueue();
8855
+ return "accepted";
8856
+ }
8857
+ const route = acpStreamRouteSchema.parse(args.reserveRoute);
8858
+ const stream = await withLock(
8859
+ args.state,
8860
+ streamAppendLockKey(route),
8861
+ async (streamLock) => {
8862
+ await prepareStreamAppend({
8863
+ ids: [requestStreamItemId(args.requestKey, 0)],
8864
+ lock: streamLock,
8865
+ route,
8866
+ state: args.state
8867
+ });
8868
+ if (!await readAcpConnection(args.state, args.connectionId)) {
8869
+ return "expired";
8870
+ }
8871
+ await createAndQueue({ lock: streamLock, route });
8872
+ return "accepted";
8873
+ },
8874
+ MUTATION_LOCK_OPTIONS
8875
+ );
8876
+ if (!stream.acquired) {
8877
+ throw new Error("Timed out acquiring ACP stream append control");
8878
+ }
8879
+ return stream.value;
8880
+ }
8881
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
8882
+ await queueReceipt({
8883
+ connectionId: args.connectionId,
8884
+ receipt,
8885
+ requestKey: args.requestKey,
8886
+ state: args.state
8887
+ });
8888
+ return "accepted";
8889
+ },
8890
+ MUTATION_LOCK_OPTIONS
8891
+ );
8892
+ return result.acquired ? result.value : "busy";
8893
+ } catch (error) {
8894
+ if (error instanceof AcpStreamFullError) return "full";
8895
+ throw error;
8896
+ }
8897
+ }
8898
+ function resultMessage(requestId, result) {
8899
+ return { jsonrpc: "2.0", id: requestId, result };
8900
+ }
8901
+ function errorMessage(requestId, error) {
8902
+ return {
8903
+ jsonrpc: "2.0",
8904
+ id: requestId,
8905
+ error: error.toErrorResponse()
8906
+ };
8907
+ }
8908
+ async function streamPrompt(args) {
8909
+ let cursor = args.output.afterSeq;
8910
+ const sentMessageIds = /* @__PURE__ */ new Set();
8911
+ while (!args.signal.aborted) {
8912
+ if (!await connectionIsLive(args.state, args.connectionId)) return false;
8913
+ const page = await args.conversations.readTurn({
8914
+ afterCursor: cursor,
8915
+ conversationId: args.sessionId,
8916
+ messageId: args.output.messageId,
8917
+ turnId: args.output.turnId
8918
+ });
8919
+ cursor = page.cursor;
8920
+ if (page.messages.length === 0 && !page.terminal) {
8921
+ await sleep(EVENT_POLL_INTERVAL_MS, args.signal);
8922
+ continue;
8923
+ }
8924
+ for (const message of page.messages) {
8925
+ if (sentMessageIds.has(message.id)) continue;
8926
+ sentMessageIds.add(message.id);
8927
+ if (!await args.emit({
8928
+ jsonrpc: "2.0",
8929
+ method: acp2.methods.client.session.update,
8930
+ params: {
8931
+ sessionId: args.sessionId,
8932
+ update: {
8933
+ sessionUpdate: "agent_message_chunk",
8934
+ content: { type: "text", text: message.text },
8935
+ messageId: message.id
8936
+ }
8937
+ }
8938
+ })) {
8939
+ return false;
8940
+ }
8941
+ }
8942
+ if (page.terminal?.status === "completed") {
8943
+ return await args.emit(
8944
+ resultMessage(args.output.requestId, {
8945
+ stopReason: page.terminal.outcome === "cancelled" ? "cancelled" : "end_turn"
8946
+ })
8947
+ );
8948
+ }
8949
+ if (page.terminal?.status === "failed") {
8950
+ return await args.emit(
8951
+ errorMessage(
8952
+ args.output.requestId,
8953
+ acp2.RequestError.internalError(
8954
+ { failureCode: page.terminal.failureCode },
8955
+ "Junior Turn failed"
8956
+ )
8957
+ )
8958
+ );
8959
+ }
8960
+ }
8961
+ return false;
8962
+ }
8963
+ function serializeSseMessage(message) {
8964
+ return new TextEncoder().encode(`data: ${JSON.stringify(message)}
8965
+
8966
+ `);
8967
+ }
8968
+ function serializeSseKeepAlive() {
8969
+ return new TextEncoder().encode(":\n\n");
8970
+ }
8971
+ async function openAcpSse(args) {
8972
+ const route = acpStreamRouteSchema.parse(args.route);
8973
+ const lock = await args.state.acquireLock(
8974
+ streamLockKey(route),
8975
+ ACP_SSE_LEASE_TTL_MS
8976
+ );
8977
+ if (!lock) {
8978
+ return new Response("ACP SSE stream already connected", { status: 409 });
8979
+ }
8980
+ return new Response(createSseBody({ ...args, lock, route }), {
8981
+ status: 200,
8982
+ headers: {
8983
+ "Cache-Control": "no-cache",
8984
+ Connection: "keep-alive",
8985
+ "Content-Type": "text/event-stream"
8986
+ }
8987
+ });
8988
+ }
8989
+ function createSseBody(args) {
8990
+ const abort = new AbortController();
8991
+ let cleaned = false;
8992
+ let heartbeat;
8993
+ let keepAlive;
8994
+ let leaseExpiresAt = args.lock.expiresAt;
8995
+ let nextMaintenanceAtMs = 0;
8996
+ let retainingLease;
8997
+ let closeBody;
8998
+ const correlation = {
8999
+ connectionId: args.route.connectionId
9000
+ };
9001
+ if (args.route.sessionId) correlation.conversationId = args.route.sessionId;
9002
+ if (args.userId) correlation.userId = args.userId;
9003
+ const cleanup = async () => {
9004
+ if (cleaned) return;
9005
+ cleaned = true;
9006
+ abort.abort();
9007
+ closeBody?.();
9008
+ if (keepAlive) clearInterval(keepAlive);
9009
+ if (heartbeat) clearInterval(heartbeat);
9010
+ args.requestSignal.removeEventListener("abort", onRequestAbort);
9011
+ try {
9012
+ await args.state.releaseLock(args.lock);
9013
+ } catch (error) {
9014
+ args.onError?.(error, "acp.sse.lock_cleanup.exception", correlation);
9015
+ }
9016
+ };
9017
+ const onRequestAbort = () => void cleanup();
9018
+ const retainLease = async () => {
9019
+ if (retainingLease) return await retainingLease;
9020
+ retainingLease = (async () => {
9021
+ let extended;
9022
+ try {
9023
+ extended = await args.state.extendLock(args.lock, ACP_SSE_LEASE_TTL_MS);
9024
+ } catch (error) {
9025
+ if (Date.now() < leaseExpiresAt) return;
9026
+ throw new Error("ACP SSE stream lease could not be renewed", {
9027
+ cause: error
9028
+ });
9029
+ }
9030
+ if (!extended) {
9031
+ throw new Error("ACP SSE stream lease expired");
9032
+ }
9033
+ leaseExpiresAt = Date.now() + ACP_SSE_LEASE_TTL_MS;
9034
+ })();
9035
+ try {
9036
+ await retainingLease;
9037
+ } finally {
9038
+ retainingLease = void 0;
9039
+ }
9040
+ };
9041
+ return new ReadableStream({
9042
+ start(controller) {
9043
+ let failureCaptured = false;
9044
+ closeBody = () => {
9045
+ try {
9046
+ controller.close();
9047
+ } catch {
9048
+ }
9049
+ };
9050
+ const fail = (error) => {
9051
+ if (cleaned) return;
9052
+ const failure = error instanceof Error ? error : new Error("ACP SSE stream failed", { cause: error });
9053
+ if (!failureCaptured) {
9054
+ failureCaptured = true;
9055
+ args.onError?.(failure, "acp.sse.exception", correlation);
9056
+ }
9057
+ abort.abort(failure);
9058
+ try {
9059
+ controller.error(failure);
9060
+ } catch {
9061
+ }
9062
+ void cleanup();
9063
+ };
9064
+ args.requestSignal.addEventListener("abort", onRequestAbort, {
9065
+ once: true
9066
+ });
9067
+ if (args.requestSignal.aborted) {
9068
+ void cleanup();
9069
+ return;
9070
+ }
9071
+ keepAlive = setInterval(() => {
9072
+ if (cleaned || (controller.desiredSize ?? 0) <= 0) return;
9073
+ try {
9074
+ controller.enqueue(serializeSseKeepAlive());
9075
+ } catch (error) {
9076
+ fail(error);
9077
+ }
9078
+ }, SSE_KEEP_ALIVE_MS);
9079
+ keepAlive.unref?.();
9080
+ heartbeat = setInterval(() => {
9081
+ if (cleaned) return;
9082
+ void retainLease().catch(fail);
9083
+ }, ACP_SSE_LEASE_RENEW_INTERVAL_MS);
9084
+ heartbeat.unref?.();
9085
+ const emit = async (message) => {
9086
+ while (!abort.signal.aborted && (controller.desiredSize ?? 0) <= 0) {
9087
+ await sleep(EVENT_POLL_INTERVAL_MS, abort.signal);
9088
+ }
9089
+ if (abort.signal.aborted || !await connectionIsLive(args.state, args.route.connectionId)) {
9090
+ abort.abort();
9091
+ return false;
9092
+ }
9093
+ await retainLease();
9094
+ controller.enqueue(serializeSseMessage(message));
9095
+ return true;
9096
+ };
9097
+ const pump = async () => {
9098
+ let cursor = await readStreamCursor(args.state, args.route);
9099
+ while (!abort.signal.aborted) {
9100
+ if (!await connectionIsLive(args.state, args.route.connectionId)) {
9101
+ return;
9102
+ }
9103
+ if (args.maintain && Date.now() >= nextMaintenanceAtMs) {
9104
+ nextMaintenanceAtMs = Date.now() + SSE_MAINTENANCE_INTERVAL_MS;
9105
+ await args.maintain();
9106
+ }
9107
+ const items = await readStreamItems(args.state, args.route);
9108
+ const cursorIndex = cursor ? items.findIndex((item) => item.id === cursor) : -1;
9109
+ const pending = items.slice(cursorIndex >= 0 ? cursorIndex + 1 : 0);
9110
+ if (pending.length === 0) {
9111
+ await sleep(EVENT_POLL_INTERVAL_MS, abort.signal);
9112
+ continue;
9113
+ }
9114
+ for (const item of pending) {
9115
+ if (abort.signal.aborted) return;
9116
+ await retainLease();
9117
+ await args.state.set(
9118
+ streamCursorKey(args.route),
9119
+ acpStreamCursorSchema.parse({ itemId: item.id }),
9120
+ ACP_STATE_TTL_MS
9121
+ );
9122
+ cursor = item.id;
9123
+ let delivered;
9124
+ if (item.output.kind === "message") {
9125
+ delivered = await emit(item.output.message);
9126
+ } else {
9127
+ const sessionId = args.route.sessionId;
9128
+ if (!sessionId) {
9129
+ throw new Error(
9130
+ `ACP ${item.output.kind} output requires a session stream`
9131
+ );
9132
+ }
9133
+ delivered = item.output.kind === "replay" ? await streamSessionReplay({
9134
+ conversations: args.conversations,
9135
+ emit,
9136
+ sessionId,
9137
+ signal: abort.signal
9138
+ }) : await streamPrompt({
9139
+ conversations: args.conversations,
9140
+ connectionId: args.route.connectionId,
9141
+ emit,
9142
+ output: item.output,
9143
+ sessionId,
9144
+ signal: abort.signal,
9145
+ state: args.state
9146
+ });
9147
+ }
9148
+ if (!delivered || abort.signal.aborted) return;
9149
+ }
9150
+ }
9151
+ };
9152
+ void pump().catch((error) => {
9153
+ if (!cleaned && !args.requestSignal.aborted) fail(error);
9154
+ }).finally(cleanup);
9155
+ },
9156
+ async cancel() {
9157
+ await cleanup();
9158
+ }
9159
+ });
9160
+ }
9161
+
9162
+ // src/api/acp/auth.ts
9163
+ var ACP_AUTHORIZATION_TTL_MS = 10 * 60 * 1e3;
9164
+ var ACP_CONNECTION_COOKIE = "junior_acp_connection";
9165
+ var LOCK_WAIT_MS2 = 5e3;
9166
+ var jsonRpcIdSchema3 = z7.union([z7.string(), z7.number().finite(), z7.null()]);
9167
+ var acpAuthorizationSchema = z7.object({
9168
+ connectionId: z7.string().uuid(),
9169
+ credentialHash: z7.string().length(64),
9170
+ elicitationId: z7.string().uuid(),
9171
+ expiresAtMs: z7.number().int().positive(),
9172
+ requestId: jsonRpcIdSchema3,
9173
+ requestKey: z7.string().min(1),
9174
+ userCodeHash: z7.string().length(64)
9175
+ }).strict();
9176
+ var acpAuthorizationPointerSchema = z7.object({
9177
+ expiresAtMs: z7.number().int().positive(),
9178
+ transactionId: z7.string().uuid()
9179
+ }).strict();
9180
+ var ACP_AUTH_METHOD_ID = "junior";
9181
+ var ACP_AUTH_METHOD = {
9182
+ id: ACP_AUTH_METHOD_ID,
9183
+ name: "Sign in to Junior",
9184
+ description: "Sign in with the Google account allowed by this Junior app."
9185
+ };
9186
+ function authorizationKey(transactionId) {
9187
+ return `junior:acp:v1:authorization:${transactionId}`;
9188
+ }
9189
+ function authorizationLockKey(transactionId) {
9190
+ return `${authorizationKey(transactionId)}:lock`;
9191
+ }
9192
+ function authorizationPointerKey(connectionId) {
9193
+ return `junior:acp:v1:connection:${connectionId}:authorization`;
9194
+ }
9195
+ function authorizationPointerLockKey(connectionId) {
9196
+ return `${authorizationPointerKey(connectionId)}:lock`;
9197
+ }
9198
+ function credentialHash(value) {
9199
+ return createHash3("sha256").update(value).digest("hex");
9200
+ }
9201
+ function createUserCode() {
9202
+ const value = randomBytes(6).toString("hex").toUpperCase();
9203
+ return `${value.slice(0, 4)}-${value.slice(4, 8)}-${value.slice(8, 12)}`;
9204
+ }
9205
+ function userCodeHash(value) {
9206
+ return credentialHash(value.trim().replaceAll("-", "").toUpperCase());
9207
+ }
9208
+ function userCodeMatches(value, expectedHash) {
9209
+ const actual = Buffer.from(userCodeHash(value), "hex");
9210
+ const expected = Buffer.from(expectedHash, "hex");
9211
+ return actual.length === expected.length && timingSafeEqual3(actual, expected);
9212
+ }
9213
+ function cookieValue(request) {
9214
+ const header = request.headers.get("cookie");
9215
+ if (!header) return void 0;
9216
+ for (const part of header.split(";")) {
9217
+ const separator = part.indexOf("=");
9218
+ if (separator <= 0) continue;
9219
+ if (part.slice(0, separator).trim() !== ACP_CONNECTION_COOKIE) continue;
9220
+ const value = part.slice(separator + 1).trim();
9221
+ if (value) return value;
9222
+ }
9223
+ return void 0;
9224
+ }
9225
+ function cookieAttributes(request) {
9226
+ return [
9227
+ "Path=/api/acp",
9228
+ "HttpOnly",
9229
+ "SameSite=Strict",
9230
+ ...new URL(request.url).protocol === "https:" ? ["Secure"] : []
9231
+ ].join("; ");
9232
+ }
9233
+ function createAcpConnectionCredential(request) {
9234
+ const credential = randomBytes(32).toString("base64url");
9235
+ return {
9236
+ cookie: `${ACP_CONNECTION_COOKIE}=${credential}; ${cookieAttributes(request)}; Max-Age=86400`,
9237
+ credentialHash: credentialHash(credential)
9238
+ };
9239
+ }
9240
+ function hasAcpConnectionCredential(request, expectedHash) {
9241
+ const credential = cookieValue(request);
9242
+ if (!credential) return false;
9243
+ const actual = Buffer.from(credentialHash(credential), "hex");
9244
+ const expected = Buffer.from(expectedHash, "hex");
9245
+ return actual.length === expected.length && timingSafeEqual3(actual, expected);
9246
+ }
9247
+ function authorizationBaseURL(request, configured) {
9248
+ const value = configured?.trim();
9249
+ let origin;
9250
+ if (value) {
9251
+ origin = /^https?:\/\//.test(value) ? value : `https://${value}`;
9252
+ } else {
9253
+ const requestURL = new URL(request.url);
9254
+ const hostname = requestURL.hostname.replace(/^\[|\]$/g, "");
9255
+ if (!["localhost", "127.0.0.1", "::1"].includes(hostname)) {
9256
+ throw acp3.RequestError.internalError(
9257
+ void 0,
9258
+ "Junior ACP sign-in requires a configured public base URL"
9259
+ );
9260
+ }
9261
+ origin = requestURL.origin;
9262
+ }
9263
+ const url = new URL(origin);
9264
+ url.pathname = "/";
9265
+ url.search = "";
9266
+ url.hash = "";
9267
+ return url;
9268
+ }
9269
+ async function readAuthorization(state, transactionId) {
9270
+ const value = await state.get(authorizationKey(transactionId));
9271
+ return value === null || value === void 0 ? void 0 : acpAuthorizationSchema.parse(value);
9272
+ }
9273
+ async function readAuthorizationPointer(state, connectionId) {
9274
+ const value = await state.get(authorizationPointerKey(connectionId));
9275
+ return value === null || value === void 0 ? void 0 : acpAuthorizationPointerSchema.parse(value);
9276
+ }
9277
+ async function deleteAuthorizationPointer(state, connectionId, transactionId) {
9278
+ const result = await withLock(
9279
+ state,
9280
+ authorizationPointerLockKey(connectionId),
9281
+ async (lock) => {
9282
+ const pointer = await readAuthorizationPointer(state, connectionId);
9283
+ if (pointer?.transactionId !== transactionId) return;
9284
+ await fenceLock(state, lock, MUTATION_LOCK_TTL_MS);
9285
+ await state.delete(authorizationPointerKey(connectionId));
9286
+ },
9287
+ {
9288
+ keepAlive: true,
9289
+ ttlMs: MUTATION_LOCK_TTL_MS,
9290
+ waitMs: LOCK_WAIT_MS2
9291
+ }
9292
+ );
9293
+ if (!result.acquired) {
9294
+ throw new Error("Timed out acquiring ACP authorization pointer control");
9295
+ }
9296
+ }
9297
+ async function deleteAuthorization(state, authorization, transactionId) {
9298
+ await deleteAuthorizationPointer(
9299
+ state,
9300
+ authorization.connectionId,
9301
+ transactionId
9302
+ );
9303
+ await state.delete(authorizationKey(transactionId));
9304
+ }
9305
+ function completionReceipt(authorization, response) {
9306
+ return {
9307
+ deliveryKey: `${authorization.requestKey}:authorization-complete`,
9308
+ outputs: [
9309
+ {
9310
+ kind: "message",
9311
+ message: {
9312
+ jsonrpc: "2.0",
9313
+ method: acp3.methods.client.elicitation.complete,
9314
+ params: { elicitationId: authorization.elicitationId }
9315
+ }
9316
+ },
9317
+ { kind: "message", message: response }
9318
+ ]
9319
+ };
9320
+ }
9321
+ function resultMessage2(requestId) {
9322
+ return { jsonrpc: "2.0", id: requestId, result: {} };
9323
+ }
9324
+ function cancelledMessage(requestId, message = "Junior sign-in was cancelled") {
9325
+ return {
9326
+ jsonrpc: "2.0",
9327
+ id: requestId,
9328
+ error: acp3.RequestError.requestCancelled(
9329
+ void 0,
9330
+ message
9331
+ ).toErrorResponse()
9332
+ };
9333
+ }
9334
+ async function beginAcpAuthorization(args) {
9335
+ const url = authorizationBaseURL(args.request, args.baseURL);
9336
+ const result = await withLock(
9337
+ args.state,
9338
+ authorizationPointerLockKey(args.connectionId),
9339
+ async (lock) => {
9340
+ const pointer = await readAuthorizationPointer(
9341
+ args.state,
9342
+ args.connectionId
9343
+ );
9344
+ if (pointer) {
9345
+ const pending = await readAuthorization(
9346
+ args.state,
9347
+ pointer.transactionId
9348
+ );
9349
+ if (pending?.connectionId === args.connectionId) {
9350
+ throw acp3.RequestError.invalidRequest(
9351
+ void 0,
9352
+ "Junior sign-in is already in progress"
9353
+ );
9354
+ }
9355
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9356
+ await args.state.delete(authorizationPointerKey(args.connectionId));
9357
+ }
9358
+ const transactionId2 = randomUUID2();
9359
+ const userCode2 = createUserCode();
9360
+ const authorization = acpAuthorizationSchema.parse({
9361
+ connectionId: args.connectionId,
9362
+ credentialHash: args.credentialHash,
9363
+ elicitationId: transactionId2,
9364
+ expiresAtMs: Date.now() + ACP_AUTHORIZATION_TTL_MS,
9365
+ requestId: args.requestId,
9366
+ requestKey: args.requestKey,
9367
+ userCodeHash: userCodeHash(userCode2)
9368
+ });
9369
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9370
+ await args.state.set(
9371
+ authorizationKey(transactionId2),
9372
+ authorization,
9373
+ ACP_STATE_TTL_MS
9374
+ );
9375
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9376
+ await args.state.set(
9377
+ authorizationPointerKey(args.connectionId),
9378
+ { expiresAtMs: authorization.expiresAtMs, transactionId: transactionId2 },
9379
+ ACP_STATE_TTL_MS
9380
+ );
9381
+ return { transactionId: transactionId2, userCode: userCode2 };
9382
+ },
9383
+ {
9384
+ keepAlive: true,
9385
+ ttlMs: MUTATION_LOCK_TTL_MS,
9386
+ waitMs: LOCK_WAIT_MS2
9387
+ }
9388
+ );
9389
+ if (!result.acquired) {
9390
+ throw new Error("Timed out acquiring ACP authorization pointer control");
9391
+ }
9392
+ const { transactionId, userCode } = result.value;
9393
+ url.pathname = `/_junior/acp/auth/${transactionId}`;
9394
+ const params = {
9395
+ mode: "url",
9396
+ message: `Sign in to Junior in your browser. Enter verification code ${userCode}.`,
9397
+ elicitationId: transactionId,
9398
+ requestId: args.requestId,
9399
+ url: url.toString()
9400
+ };
9401
+ return {
9402
+ deliveryKey: `${args.requestKey}:authorization-start`,
9403
+ outputs: [
9404
+ {
9405
+ kind: "message",
9406
+ message: {
9407
+ jsonrpc: "2.0",
9408
+ id: transactionId,
9409
+ method: acp3.methods.client.elicitation.create,
9410
+ params
9411
+ }
9412
+ }
9413
+ ]
9414
+ };
9415
+ }
9416
+ async function finishAuthorization(args) {
9417
+ const result = await completeAcpRequest({
9418
+ connectionId: args.authorization.connectionId,
9419
+ receipt: completionReceipt(args.authorization, args.response),
9420
+ requestKey: args.authorization.requestKey,
9421
+ state: args.state
9422
+ });
9423
+ return result === "full" ? "busy" : result;
9424
+ }
9425
+ async function completeAcpAuthorization(args) {
9426
+ const result = await withLock(
9427
+ args.state,
9428
+ authorizationLockKey(args.transactionId),
9429
+ async (lock) => {
9430
+ const authorization = await readAuthorization(
9431
+ args.state,
9432
+ args.transactionId
9433
+ );
9434
+ if (!authorization) {
9435
+ return "expired";
9436
+ }
9437
+ if (authorization.expiresAtMs <= Date.now()) {
9438
+ const completion2 = await finishAuthorization({
9439
+ authorization,
9440
+ response: cancelledMessage(
9441
+ authorization.requestId,
9442
+ "Junior sign-in request expired"
9443
+ ),
9444
+ state: args.state
9445
+ });
9446
+ if (completion2 === "busy") return completion2;
9447
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9448
+ await deleteAuthorization(
9449
+ args.state,
9450
+ authorization,
9451
+ args.transactionId
9452
+ );
9453
+ return "expired";
9454
+ }
9455
+ if (!userCodeMatches(args.userCode, authorization.userCodeHash)) {
9456
+ return "invalid";
9457
+ }
9458
+ const binding = await bindAcpConnectionUser({
9459
+ connectionId: authorization.connectionId,
9460
+ credentialHash: authorization.credentialHash,
9461
+ state: args.state,
9462
+ user: userSchema2.parse(args.user)
9463
+ });
9464
+ if (binding !== "completed") {
9465
+ const completion2 = await finishAuthorization({
9466
+ authorization,
9467
+ response: cancelledMessage(
9468
+ authorization.requestId,
9469
+ "Junior sign-in could not bind this connection"
9470
+ ),
9471
+ state: args.state
9472
+ });
9473
+ if (completion2 !== "completed") return completion2;
9474
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9475
+ await deleteAuthorization(
9476
+ args.state,
9477
+ authorization,
9478
+ args.transactionId
9479
+ );
9480
+ return binding;
9481
+ }
9482
+ const completion = await finishAuthorization({
9483
+ authorization,
9484
+ response: resultMessage2(authorization.requestId),
9485
+ state: args.state
9486
+ });
9487
+ if (completion !== "completed") return completion;
9488
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9489
+ await deleteAuthorization(args.state, authorization, args.transactionId);
9490
+ return "completed";
9491
+ },
9492
+ {
9493
+ keepAlive: true,
9494
+ ttlMs: MUTATION_LOCK_TTL_MS,
9495
+ waitMs: LOCK_WAIT_MS2
9496
+ }
9497
+ );
9498
+ if (!result.acquired) {
9499
+ throw new Error("Timed out acquiring ACP authorization control");
9500
+ }
9501
+ return result.value;
9502
+ }
9503
+ async function handleAcpAuthorizationResponse(args) {
9504
+ if (typeof args.response.id !== "string") return "ignored";
9505
+ const parsedId = z7.string().uuid().safeParse(args.response.id);
9506
+ if (!parsedId.success) return "ignored";
9507
+ const authorization = await readAuthorization(args.state, parsedId.data);
9508
+ if (!authorization || authorization.connectionId !== args.connectionId) {
9509
+ return "ignored";
9510
+ }
9511
+ const action = "result" in args.response && typeof args.response.result === "object" && args.response.result !== null && "action" in args.response.result && typeof args.response.result.action === "string" ? args.response.result.action : void 0;
9512
+ if (action === "accept") return "accepted";
9513
+ const result = await withLock(
9514
+ args.state,
9515
+ authorizationLockKey(parsedId.data),
9516
+ async (lock) => {
9517
+ const current = await readAuthorization(args.state, parsedId.data);
9518
+ if (!current || current.connectionId !== args.connectionId) return false;
9519
+ const completion = await finishAuthorization({
9520
+ authorization: current,
9521
+ response: cancelledMessage(current.requestId),
9522
+ state: args.state
9523
+ });
9524
+ if (completion === "busy") return false;
9525
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9526
+ await deleteAuthorization(args.state, current, parsedId.data);
9527
+ return true;
9528
+ },
9529
+ {
9530
+ keepAlive: true,
9531
+ ttlMs: MUTATION_LOCK_TTL_MS,
9532
+ waitMs: LOCK_WAIT_MS2
9533
+ }
9534
+ );
9535
+ if (!result.acquired) {
9536
+ throw new Error("Timed out acquiring ACP authorization control");
9537
+ }
9538
+ return result.value ? "cancelled" : "ignored";
9539
+ }
9540
+ async function expireAcpAuthorization(args) {
9541
+ const pointer = await readAuthorizationPointer(args.state, args.connectionId);
9542
+ if (!pointer || pointer.expiresAtMs > Date.now()) return false;
9543
+ const result = await withLock(
9544
+ args.state,
9545
+ authorizationLockKey(pointer.transactionId),
9546
+ async (lock) => {
9547
+ const authorization = await readAuthorization(
9548
+ args.state,
9549
+ pointer.transactionId
9550
+ );
9551
+ if (!authorization || authorization.connectionId !== args.connectionId) {
9552
+ await deleteAuthorizationPointer(
9553
+ args.state,
9554
+ args.connectionId,
9555
+ pointer.transactionId
9556
+ );
9557
+ return false;
9558
+ }
9559
+ if (authorization.expiresAtMs > Date.now()) return false;
9560
+ const completion = await finishAuthorization({
9561
+ authorization,
9562
+ response: cancelledMessage(
9563
+ authorization.requestId,
9564
+ "Junior sign-in request expired"
9565
+ ),
9566
+ state: args.state
9567
+ });
9568
+ if (completion === "busy") return false;
9569
+ await fenceLock(args.state, lock, MUTATION_LOCK_TTL_MS);
9570
+ await deleteAuthorization(
9571
+ args.state,
9572
+ authorization,
9573
+ pointer.transactionId
9574
+ );
9575
+ return true;
9576
+ },
9577
+ {
9578
+ keepAlive: true,
9579
+ ttlMs: MUTATION_LOCK_TTL_MS,
9580
+ waitMs: LOCK_WAIT_MS2
9581
+ }
9582
+ );
9583
+ if (!result.acquired) return false;
9584
+ return result.value;
9585
+ }
9586
+
9587
+ // src/api/acp/route.ts
9588
+ var ACP_CONNECTION_ID_HEADER = "Acp-Connection-Id";
9589
+ var ACP_SESSION_ID_HEADER = "Acp-Session-Id";
9590
+ var ACP_CONVERSATION_PREFIX = "local:acp:";
9591
+ var ACP_EVENT_STREAM_MIME_TYPE = "text/event-stream";
9592
+ var ACP_JSON_MIME_TYPE = "application/json";
9593
+ var MAX_PROMPT_TEXT_LENGTH = 32e3;
9594
+ function isRecord2(value) {
9595
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9596
+ }
9597
+ function authenticatedAcpUser(connection) {
9598
+ return connection.user;
9599
+ }
9600
+ function supportsUrlElicitation(params) {
9601
+ const parsed = initializeParamsSchema.safeParse(params);
9602
+ if (!parsed.success) return false;
9603
+ const elicitation = parsed.data.clientCapabilities?.elicitation;
9604
+ return isRecord2(elicitation) && isRecord2(elicitation.url);
9605
+ }
9606
+ function parseParams(schema, params) {
9607
+ const parsed = schema.safeParse(params);
9608
+ if (parsed.success) return parsed.data;
9609
+ const firstPath = parsed.error.issues[0]?.path[0];
9610
+ throw acp4.RequestError.invalidParams({
9611
+ field: typeof firstPath === "string" || typeof firstPath === "number" ? String(firstPath) : "params"
9612
+ });
9613
+ }
9614
+ function rejectUnsupportedMcpServers(mcpServers) {
9615
+ if (mcpServers.length > 0) {
9616
+ throw acp4.RequestError.invalidParams(
9617
+ { field: "mcpServers" },
9618
+ "Junior does not accept client MCP servers"
9619
+ );
9620
+ }
9621
+ }
9622
+ function promptText(prompt) {
9623
+ if (prompt.length === 0) {
9624
+ throw acp4.RequestError.invalidParams(
9625
+ { field: "prompt" },
9626
+ "Junior accepts one or more text blocks only"
9627
+ );
9628
+ }
9629
+ const blocks = [];
9630
+ for (const block of prompt) {
9631
+ if (!block.text.trim()) {
9632
+ throw acp4.RequestError.invalidParams(
9633
+ { field: "prompt" },
9634
+ "Junior accepts non-empty text blocks only"
9635
+ );
9636
+ }
9637
+ blocks.push(block.text);
9638
+ }
9639
+ const text = blocks.join("\n");
9640
+ if (text.length > MAX_PROMPT_TEXT_LENGTH) {
9641
+ throw acp4.RequestError.invalidParams(
9642
+ { field: "prompt" },
9643
+ `Junior accepts at most ${MAX_PROMPT_TEXT_LENGTH} prompt characters`
9644
+ );
9645
+ }
9646
+ return text;
9647
+ }
9648
+ function stableHex2(value, length = 32) {
9649
+ return createHash4("sha256").update(value).digest("hex").slice(0, length);
9650
+ }
9651
+ function canonicalJson(value) {
9652
+ if (Array.isArray(value)) return value.map(canonicalJson);
9653
+ if (!isRecord2(value)) return value;
9654
+ return Object.fromEntries(
9655
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, canonicalJson(child)])
9656
+ );
9657
+ }
9658
+ function requestKey(call) {
9659
+ const id = call.id === null ? "null" : `${typeof call.id}:${stableHex2(String(call.id))}`;
9660
+ const payload = JSON.stringify(
9661
+ canonicalJson({ method: call.method, params: call.params ?? null })
9662
+ );
9663
+ return `${id}:${stableHex2(payload)}`;
9664
+ }
9665
+ function sessionIdFromCall(call) {
9666
+ if (!call) return void 0;
9667
+ const schema = call.method === acp4.methods.agent.session.load ? loadSessionParamsSchema : call.method === acp4.methods.agent.session.prompt ? promptParamsSchema : call.method === acp4.methods.agent.session.cancel ? cancelParamsSchema : void 0;
9668
+ if (!schema) return void 0;
9669
+ const parsed = schema.safeParse(call.params);
9670
+ return parsed.success ? parsed.data.sessionId : void 0;
9671
+ }
9672
+ function determineRoute(connectionId, call, headerSessionId) {
9673
+ const paramsSessionId = sessionIdFromCall(call);
9674
+ if (SESSION_HEADER_METHODS.has(call.method) && !headerSessionId) {
9675
+ return textResponse("Missing Acp-Session-Id", 400);
9676
+ }
9677
+ if (headerSessionId !== void 0 && paramsSessionId !== void 0 && headerSessionId !== paramsSessionId) {
9678
+ return textResponse("Mismatched Acp-Session-Id", 400);
9679
+ }
9680
+ return {
9681
+ connectionId,
9682
+ ...headerSessionId ? { sessionId: headerSessionId } : paramsSessionId ? { sessionId: paramsSessionId } : {}
9683
+ };
9684
+ }
9685
+ function resultMessage3(requestId, result) {
9686
+ return { jsonrpc: "2.0", id: requestId, result };
9687
+ }
9688
+ function errorMessage2(requestId, error) {
9689
+ return {
9690
+ jsonrpc: "2.0",
9691
+ id: requestId,
9692
+ error: error.toErrorResponse()
9693
+ };
9694
+ }
9695
+ function textResponse(body, status) {
9696
+ return new Response(body, {
9697
+ status,
9698
+ headers: { "Content-Type": "text/plain" }
9699
+ });
9700
+ }
9701
+ function jsonResponse(value, status, headers) {
9702
+ return new Response(JSON.stringify(value), {
9703
+ status,
9704
+ headers: { "Content-Type": ACP_JSON_MIME_TYPE, ...headers }
9705
+ });
9706
+ }
9707
+ function emptyResponse(status) {
9708
+ return new Response(null, { status });
9709
+ }
9710
+ async function createSession(args) {
9711
+ const params = parseParams(
9712
+ newSessionParamsSchema,
9713
+ args.call.params
9714
+ );
9715
+ rejectUnsupportedMcpServers(params.mcpServers);
9716
+ const sessionId = `${ACP_CONVERSATION_PREFIX}${stableHex2(
9717
+ `${args.connection.nonce}:${args.requestKey}`
9718
+ )}`;
9719
+ await args.conversations.create({
9720
+ conversationId: sessionId,
9721
+ user: args.user
9722
+ });
9723
+ return sessionId;
9724
+ }
9725
+ async function acceptPrompt(args) {
9726
+ const params = parseParams(
9727
+ promptParamsSchema,
9728
+ args.call.params
9729
+ );
9730
+ const admission = await args.conversations.prompt({
9731
+ conversationId: params.sessionId,
9732
+ idempotencyKey: `${args.connection.nonce}:${args.requestKey}`,
9733
+ text: promptText(params.prompt),
9734
+ user: args.user
9735
+ });
9736
+ if (admission.status === "not_found") {
9737
+ throw acp4.RequestError.resourceNotFound(params.sessionId);
9738
+ }
9739
+ if (admission.status === "active") {
9740
+ throw acp4.RequestError.invalidParams(
9741
+ { field: "sessionId" },
9742
+ "This ACP session already has an active prompt"
9743
+ );
9744
+ }
9745
+ return {
9746
+ output: {
9747
+ afterSeq: admission.afterCursor,
9748
+ kind: "prompt",
9749
+ messageId: admission.messageId,
9750
+ requestId: args.requestId,
9751
+ turnId: admission.turnId
9752
+ },
9753
+ sessionId: params.sessionId
9754
+ };
9755
+ }
9756
+ async function createRequestReceipt(args) {
9757
+ if (args.call.method === acp4.methods.agent.session.new) {
9758
+ const sessionId = await createSession(args);
9759
+ return {
9760
+ outputs: [
9761
+ {
9762
+ kind: "message",
9763
+ message: resultMessage3(args.call.id, { sessionId })
9764
+ }
9765
+ ]
9766
+ };
9767
+ }
9768
+ if (args.call.method === acp4.methods.agent.session.load) {
9769
+ const params = parseParams(
9770
+ loadSessionParamsSchema,
9771
+ args.call.params
9772
+ );
9773
+ rejectUnsupportedMcpServers(params.mcpServers);
9774
+ if (!await args.conversations.hasAccess({
9775
+ conversationId: params.sessionId,
9776
+ user: args.user
9777
+ })) {
9778
+ throw acp4.RequestError.resourceNotFound(params.sessionId);
9779
+ }
9780
+ return {
9781
+ sessionId: params.sessionId,
9782
+ outputs: [
9783
+ { kind: "replay" },
9784
+ { kind: "message", message: resultMessage3(args.call.id, {}) }
9785
+ ]
9786
+ };
9787
+ }
9788
+ if (args.call.method === acp4.methods.agent.session.prompt) {
9789
+ const prompt = await acceptPrompt({
9790
+ ...args,
9791
+ requestId: args.call.id
9792
+ });
9793
+ return {
9794
+ outputs: [prompt.output],
9795
+ sessionId: prompt.sessionId
9796
+ };
9797
+ }
9798
+ throw acp4.RequestError.methodNotFound(args.call.method);
9799
+ }
9800
+ async function handleCancelNotification(args) {
9801
+ const params = parseParams(
9802
+ cancelParamsSchema,
9803
+ args.call.params
9804
+ );
9805
+ const result = await args.conversations.cancel({
9806
+ conversationId: params.sessionId,
9807
+ user: args.user
9808
+ });
9809
+ if (result === "not_found") {
9810
+ throw acp4.RequestError.resourceNotFound(params.sessionId);
9811
+ }
9812
+ }
9813
+ async function requireConnection(args) {
9814
+ const value = await readAcpConnection(args.state, args.connectionId);
9815
+ if (!value || !hasAcpConnectionCredential(args.request, value.credentialHash)) {
9816
+ return void 0;
9817
+ }
9818
+ return value;
9819
+ }
9820
+ function isJsonContentType(contentType) {
9821
+ return contentType?.split(";", 1)[0]?.trim().toLowerCase() === ACP_JSON_MIME_TYPE;
9822
+ }
9823
+ async function handleInitialize(args) {
9824
+ const requestId = args.call.id;
9825
+ if (requestId === void 0) {
9826
+ return textResponse("Initialize request must include an ID", 400);
9827
+ }
9828
+ try {
9829
+ parseParams(initializeParamsSchema, args.call.params);
9830
+ if (!supportsUrlElicitation(args.call.params)) {
9831
+ throw acp4.RequestError.invalidParams(
9832
+ { field: "clientCapabilities.elicitation.url" },
9833
+ "Remote Junior authentication requires URL elicitation support"
9834
+ );
9835
+ }
9836
+ const credential = createAcpConnectionCredential(args.request);
9837
+ const result = {
9838
+ protocolVersion: acp4.PROTOCOL_VERSION,
9839
+ agentCapabilities: {
9840
+ loadSession: true,
9841
+ promptCapabilities: {
9842
+ image: false,
9843
+ audio: false,
9844
+ embeddedContext: false
9845
+ }
9846
+ },
9847
+ authMethods: args.browserAuthAvailable ? [ACP_AUTH_METHOD] : [],
9848
+ agentInfo: { name: "junior", version: args.version }
9849
+ };
9850
+ const connection = await createAcpConnection(
9851
+ args.state,
9852
+ credential.credentialHash
9853
+ );
9854
+ return jsonResponse(resultMessage3(requestId, result), 200, {
9855
+ [ACP_CONNECTION_ID_HEADER]: connection.connectionId,
9856
+ "Set-Cookie": credential.cookie
9857
+ });
9858
+ } catch (error) {
9859
+ if (!(error instanceof acp4.RequestError)) throw error;
9860
+ return jsonResponse(errorMessage2(requestId, error), 200);
9861
+ }
9862
+ }
9863
+ async function handleConnectedPost(args) {
9864
+ const requestId = args.call.id;
9865
+ if (requestId === void 0) {
9866
+ if (args.authenticated && args.call.method === acp4.methods.agent.session.cancel) {
9867
+ try {
9868
+ await handleCancelNotification({
9869
+ call: args.call,
9870
+ conversations: args.options.conversations,
9871
+ user: args.authenticated
9872
+ });
9873
+ } catch (error) {
9874
+ if (!(error instanceof acp4.RequestError)) throw error;
9875
+ }
9876
+ }
9877
+ return emptyResponse(202);
9878
+ }
9879
+ const identifiedCall = { ...args.call, id: requestId };
9880
+ const identifiedRequestKey = requestKey(identifiedCall);
9881
+ const acceptance = {
9882
+ connectionId: args.connectionId,
9883
+ requestKey: identifiedRequestKey,
9884
+ state: args.state,
9885
+ createReceipt: async () => {
9886
+ try {
9887
+ if (identifiedCall.method === acp4.methods.agent.authenticate) {
9888
+ const params = parseParams(
9889
+ authenticateParamsSchema,
9890
+ identifiedCall.params
9891
+ );
9892
+ if (params.methodId !== ACP_AUTH_METHOD_ID) {
9893
+ throw acp4.RequestError.invalidParams(
9894
+ { field: "methodId" },
9895
+ "Junior did not advertise this authentication method"
9896
+ );
9897
+ }
9898
+ if (args.authenticated) {
9899
+ return {
9900
+ outputs: [
9901
+ {
9902
+ kind: "message",
9903
+ message: resultMessage3(requestId, {})
9904
+ }
9905
+ ]
9906
+ };
9907
+ }
9908
+ if (!args.options.browserAuth) {
9909
+ throw acp4.RequestError.internalError(
9910
+ void 0,
9911
+ "Junior ACP authentication requires an enabled dashboard"
9912
+ );
9913
+ }
9914
+ return await beginAcpAuthorization({
9915
+ baseURL: args.options.browserAuth.baseURL,
9916
+ connectionId: args.connectionId,
9917
+ credentialHash: args.connection.credentialHash,
9918
+ request: args.request,
9919
+ requestId,
9920
+ requestKey: identifiedRequestKey,
9921
+ state: args.state
9922
+ });
9923
+ }
9924
+ if (!args.authenticated) {
9925
+ throw acp4.RequestError.authRequired();
9926
+ }
9927
+ return await createRequestReceipt({
9928
+ call: identifiedCall,
9929
+ connection: args.connection,
9930
+ conversations: args.options.conversations,
9931
+ requestKey: identifiedRequestKey,
9932
+ user: args.authenticated
9933
+ });
9934
+ } catch (error) {
9935
+ if (!(error instanceof acp4.RequestError)) throw error;
9936
+ const receipt = {
9937
+ outputs: [
9938
+ {
9939
+ kind: "message",
9940
+ message: errorMessage2(requestId, error)
9941
+ }
9942
+ ]
9943
+ };
9944
+ if (args.route.sessionId) receipt.sessionId = args.route.sessionId;
9945
+ return receipt;
9946
+ }
9947
+ }
9948
+ };
9949
+ if (identifiedCall.method === acp4.methods.agent.session.prompt) {
9950
+ acceptance.reserveRoute = args.route;
9951
+ }
9952
+ const status = await acceptAcpRequest(acceptance);
9953
+ if (status === "busy") {
9954
+ return textResponse("ACP request is already being accepted", 503);
9955
+ }
9956
+ if (status === "expired") {
9957
+ return textResponse("Unknown Acp-Connection-Id", 404);
9958
+ }
9959
+ if (status === "full") {
9960
+ return textResponse("ACP stream has too much undelivered output", 503);
9961
+ }
9962
+ return emptyResponse(202);
9963
+ }
9964
+ async function handleConnectedResponse(args) {
9965
+ await handleAcpAuthorizationResponse({
9966
+ connectionId: args.connectionId,
9967
+ response: args.response,
9968
+ state: args.state
9969
+ });
9970
+ return emptyResponse(202);
9971
+ }
9972
+ function createAcpHttpHandler(options) {
9973
+ const state = options.state;
9974
+ return async (request) => {
9975
+ let authenticated;
9976
+ let call;
9977
+ let connectionId;
9978
+ let sessionId;
9979
+ try {
9980
+ if (request.headers.has("Authorization")) {
9981
+ return new Response("Unauthorized", { status: 401 });
9982
+ }
9983
+ const connectionHeader = request.headers.get(ACP_CONNECTION_ID_HEADER);
9984
+ if (connectionHeader !== null) {
9985
+ const parsed = acpConnectionIdSchema.safeParse(connectionHeader);
9986
+ if (!parsed.success) {
9987
+ return textResponse("Invalid Acp-Connection-Id", 400);
9988
+ }
9989
+ connectionId = parsed.data;
9990
+ }
9991
+ const sessionHeader = request.headers.get(ACP_SESSION_ID_HEADER);
9992
+ if (sessionHeader !== null) {
9993
+ const parsed = acpSessionIdSchema.safeParse(sessionHeader);
9994
+ if (!parsed.success) {
9995
+ return textResponse("Invalid Acp-Session-Id", 400);
9996
+ }
9997
+ sessionId = parsed.data;
9998
+ }
9999
+ if (request.method === "POST") {
10000
+ if (!isJsonContentType(request.headers.get("Content-Type"))) {
10001
+ return textResponse("Unsupported Media Type", 415);
10002
+ }
10003
+ let value;
10004
+ try {
10005
+ value = await request.json();
10006
+ } catch {
10007
+ return textResponse("Invalid JSON", 400);
10008
+ }
10009
+ if (Array.isArray(value)) {
10010
+ return textResponse(
10011
+ "Batch JSON-RPC requests are not implemented",
10012
+ 501
10013
+ );
10014
+ }
10015
+ const parsedMessage = acpInboundMessageSchema.safeParse(value);
10016
+ if (!parsedMessage.success) {
10017
+ return textResponse("Invalid JSON-RPC message", 400);
10018
+ }
10019
+ const message = parsedMessage.data;
10020
+ if ("method" in message && message.method === acp4.methods.agent.initialize) {
10021
+ call = message;
10022
+ if (connectionId) {
10023
+ return textResponse(
10024
+ "Initialize not allowed on existing connection",
10025
+ 400
10026
+ );
10027
+ }
10028
+ return await handleInitialize({
10029
+ browserAuthAvailable: options.browserAuth !== void 0,
10030
+ call,
10031
+ request,
10032
+ state,
10033
+ version: options.version
10034
+ });
10035
+ }
10036
+ if (!connectionId) {
10037
+ return textResponse("Missing Acp-Connection-Id", 400);
10038
+ }
10039
+ const connection2 = await requireConnection({
10040
+ connectionId,
10041
+ request,
10042
+ state
10043
+ });
10044
+ if (!connection2) {
10045
+ return textResponse("Unauthorized", 401);
10046
+ }
10047
+ authenticated = authenticatedAcpUser(connection2);
10048
+ if (!("method" in message)) {
10049
+ return await handleConnectedResponse({
10050
+ connectionId,
10051
+ response: message,
10052
+ state
10053
+ });
10054
+ }
10055
+ call = message;
10056
+ const route = determineRoute(connectionId, call, sessionId);
10057
+ if (route instanceof Response) return route;
10058
+ return await handleConnectedPost({
10059
+ authenticated,
10060
+ call,
10061
+ connection: connection2,
10062
+ connectionId,
10063
+ options,
10064
+ request,
10065
+ route,
10066
+ state
10067
+ });
10068
+ }
10069
+ if (!connectionId) {
10070
+ return textResponse("Missing Acp-Connection-Id", 400);
10071
+ }
10072
+ const connection = await requireConnection({
10073
+ connectionId,
10074
+ request,
10075
+ state
10076
+ });
10077
+ if (!connection) {
10078
+ return textResponse("Unauthorized", 401);
10079
+ }
10080
+ authenticated = authenticatedAcpUser(connection);
10081
+ if (request.method === "GET") {
10082
+ if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
10083
+ return textResponse("WebSocket upgrade is not implemented", 426);
10084
+ }
10085
+ const accept = request.headers.get("Accept")?.toLowerCase();
10086
+ if (!accept?.includes(ACP_EVENT_STREAM_MIME_TYPE)) {
10087
+ return textResponse("Not Acceptable", 406);
10088
+ }
10089
+ const route = { connectionId };
10090
+ if (sessionId) route.sessionId = sessionId;
10091
+ const stream = {
10092
+ conversations: options.conversations,
10093
+ onError: options.onError,
10094
+ requestSignal: request.signal,
10095
+ route,
10096
+ state
10097
+ };
10098
+ if (!sessionId) {
10099
+ stream.maintain = async () => {
10100
+ await expireAcpAuthorization({
10101
+ connectionId: route.connectionId,
10102
+ state
10103
+ });
10104
+ };
10105
+ }
10106
+ if (authenticated) stream.userId = authenticated.id;
10107
+ return await openAcpSse(stream);
10108
+ }
10109
+ if (request.method === "DELETE") {
10110
+ await deleteAcpConnection(state, connectionId);
10111
+ return emptyResponse(202);
10112
+ }
10113
+ return textResponse("Method Not Allowed", 405);
10114
+ } catch (error) {
10115
+ const conversationId = sessionIdFromCall(call) ?? sessionId;
10116
+ const context = {};
10117
+ if (authenticated) context.userId = authenticated.id;
10118
+ if (connectionId) context.connectionId = connectionId;
10119
+ if (conversationId) context.conversationId = conversationId;
10120
+ options.onError?.(error, "acp.transport.exception", context);
10121
+ return textResponse("ACP transport failed", 500);
10122
+ }
10123
+ };
10124
+ }
10125
+
10126
+ // src/api/acp/authorization-page.ts
10127
+ var ACP_AUTHORIZATION_PATH = "/_junior/acp/auth/:transactionId";
10128
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10129
+ function escapeHtml(value) {
10130
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
10131
+ }
10132
+ function renderResult(agentName, result) {
10133
+ const content = {
10134
+ blocked: {
10135
+ status: 403,
10136
+ title: "Request blocked",
10137
+ message: "Open the sign-in link again and confirm the connection."
10138
+ },
10139
+ busy: {
10140
+ status: 503,
10141
+ title: "Your ACP client has pending output",
10142
+ message: "Reconnect the client, then confirm this connection again."
10143
+ },
10144
+ completed: {
10145
+ status: 200,
10146
+ title: `${agentName} is connected`,
10147
+ message: "You can close this window and return to your ACP client."
10148
+ },
10149
+ conflict: {
10150
+ status: 409,
10151
+ title: "This connection uses another account",
10152
+ message: "Start a new connection in your ACP client and sign in again."
10153
+ },
10154
+ expired: {
10155
+ status: 410,
10156
+ title: "This sign-in request expired",
10157
+ message: "Return to your ACP client and start sign-in again."
10158
+ },
10159
+ invalid: {
10160
+ status: 403,
10161
+ title: "Verification code did not match",
10162
+ message: "Return to your ACP client and enter the code shown there."
10163
+ }
10164
+ }[result];
10165
+ return new Response(
10166
+ `<!DOCTYPE html>
10167
+ <html lang="en">
10168
+ <head>
10169
+ <meta charset="utf-8" />
10170
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10171
+ <title>${escapeHtml(content.title)}</title>
10172
+ </head>
10173
+ <body style="margin:0;background:#000;color:#fff;font-family:ui-sans-serif,system-ui,sans-serif">
10174
+ <main style="min-height:100vh;display:grid;place-items:center;padding:2rem">
10175
+ <section style="max-width:32rem;border-left:4px solid #a78bfa;padding-left:1rem">
10176
+ <h1 style="margin:0 0 .75rem;font-size:2rem">${escapeHtml(content.title)}</h1>
10177
+ <p style="margin:0;color:#b8b8b8;line-height:1.5">${escapeHtml(content.message)}</p>
10178
+ </section>
10179
+ </main>
10180
+ </body>
10181
+ </html>`,
10182
+ {
10183
+ status: content.status,
10184
+ headers: {
10185
+ "cache-control": "no-store",
10186
+ "content-security-policy": "frame-ancestors 'none'; form-action 'self'",
10187
+ "content-type": "text/html; charset=utf-8",
10188
+ "x-frame-options": "DENY"
10189
+ }
10190
+ }
10191
+ );
10192
+ }
10193
+ function renderConfirmation(agentName) {
10194
+ return new Response(
10195
+ `<!DOCTYPE html>
10196
+ <html lang="en">
10197
+ <head>
10198
+ <meta charset="utf-8" />
10199
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10200
+ <title>Connect ${escapeHtml(agentName)}</title>
10201
+ </head>
10202
+ <body style="margin:0;background:#000;color:#fff;font-family:ui-sans-serif,system-ui,sans-serif">
10203
+ <main style="min-height:100vh;display:grid;place-items:center;padding:2rem">
10204
+ <section style="max-width:32rem;border-left:4px solid #a78bfa;padding-left:1rem">
10205
+ <h1 style="margin:0 0 .75rem;font-size:2rem">Connect ${escapeHtml(agentName)}</h1>
10206
+ <p style="margin:0 0 1rem;color:#b8b8b8;line-height:1.5">Only continue if you started sign-in from your ACP client. Enter the verification code shown by the client.</p>
10207
+ <form method="post">
10208
+ <label style="display:block;margin:0 0 1rem">Verification code<br><input name="code" required autocomplete="one-time-code" inputmode="text" style="box-sizing:border-box;margin-top:.5rem;padding:.625rem;width:100%"></label>
10209
+ <button type="submit" style="border:0;border-radius:.375rem;background:#a78bfa;color:#000;cursor:pointer;font:inherit;font-weight:700;padding:.625rem 1rem">Connect</button>
10210
+ </form>
10211
+ </section>
10212
+ </main>
10213
+ </body>
10214
+ </html>`,
10215
+ {
10216
+ headers: {
10217
+ "cache-control": "no-store",
10218
+ "content-security-policy": "frame-ancestors 'none'; form-action 'self'",
10219
+ "content-type": "text/html; charset=utf-8",
10220
+ "x-frame-options": "DENY"
10221
+ }
10222
+ }
10223
+ );
10224
+ }
10225
+ function requestHasSameOrigin(request) {
10226
+ const origin = request.headers.get("origin");
10227
+ if (!origin) return false;
10228
+ try {
10229
+ return new URL(origin).origin === new URL(request.url).origin;
10230
+ } catch {
10231
+ return false;
10232
+ }
10233
+ }
10234
+ function transactionIdFromRequest(request) {
10235
+ const transactionId = new URL(request.url).pathname.split("/").at(-1);
10236
+ return transactionId && UUID_PATTERN.test(transactionId) ? transactionId : void 0;
10237
+ }
10238
+ async function handleAcpAuthorizationPage(args) {
10239
+ const transactionId = transactionIdFromRequest(args.request);
10240
+ if (!transactionId) return renderResult(args.agentName, "expired");
10241
+ if (args.request.method === "GET") {
10242
+ return renderConfirmation(args.agentName);
10243
+ }
10244
+ if (args.request.method !== "POST" || !requestHasSameOrigin(args.request)) {
10245
+ return renderResult(args.agentName, "blocked");
10246
+ }
10247
+ let userCode;
10248
+ try {
10249
+ const value = (await args.request.formData()).get("code");
10250
+ userCode = typeof value === "string" ? value : void 0;
10251
+ } catch {
10252
+ }
10253
+ if (!userCode?.trim()) return renderResult(args.agentName, "invalid");
10254
+ const result = await completeAcpAuthorization({
10255
+ state: args.state,
10256
+ transactionId,
10257
+ user: args.user,
10258
+ userCode
10259
+ });
10260
+ return renderResult(args.agentName, result);
10261
+ }
10262
+
8343
10263
  // src/app.ts
8344
- function resolveAdapterBaseURL(dashboard) {
10264
+ function resolveAcpBaseURL(dashboard) {
8345
10265
  const configured = dashboard?.baseURL?.trim() || process.env.JUNIOR_BASE_URL?.trim();
8346
10266
  if (configured) return configured;
8347
10267
  const vercelURL = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim() || process.env.VERCEL_URL?.trim();
@@ -8652,11 +10572,11 @@ function validateDashboardRouteOwnership(args) {
8652
10572
  function mountRoutes(app, routes) {
8653
10573
  for (const route of routes) {
8654
10574
  const handler = (c) => route.handler(c.req.raw);
8655
- const methods = typeof route.method === "string" ? [route.method] : route.method ?? ["ALL"];
8656
- const explicitMethods = methods.filter(
10575
+ const methods5 = typeof route.method === "string" ? [route.method] : route.method ?? ["ALL"];
10576
+ const explicitMethods = methods5.filter(
8657
10577
  (method) => method !== "ALL"
8658
10578
  );
8659
- if (methods.includes("ALL")) {
10579
+ if (methods5.includes("ALL")) {
8660
10580
  app.all(route.path, handler);
8661
10581
  } else if (explicitMethods.length > 0) {
8662
10582
  app.on(explicitMethods, route.path, handler);
@@ -8801,37 +10721,41 @@ async function createApp(options) {
8801
10721
  });
8802
10722
  return conversationWorkOptions;
8803
10723
  };
8804
- let adapterRoutes = [];
8805
- let authenticatedRoutes = [];
8806
- try {
8807
- const adapterOutputs = await Promise.all(
8808
- (options?.adapters ?? []).map(async (adapter) => {
8809
- const work = getConversationWorkOptions();
8810
- const state = work.state ?? getStateAdapter();
8811
- return await adapter({
8812
- agentName: botConfig.userName,
8813
- baseURL: resolveAdapterBaseURL(dashboard),
8814
- conversations: createAdapterConversations({
8815
- conversationStore: work.conversationStore,
8816
- eventStore: getConversationEventStore(),
8817
- queue: work.queue ?? getVercelConversationWorkQueue(),
8818
- state
8819
- }),
8820
- reportError: (error, event, attributes) => void logException(error, event, attributes),
8821
- state,
8822
- version: JUNIOR_VERSION
8823
- });
8824
- })
8825
- );
8826
- adapterRoutes = adapterOutputs.flatMap((output) => output.routes ?? []);
8827
- authenticatedRoutes = adapterOutputs.flatMap(
8828
- (output) => output.authenticatedRoutes ?? []
8829
- );
8830
- if (authenticatedRoutes.length > 0 && (!dashboard || dashboard.disabled)) {
8831
- throw new Error(
8832
- "createApp() adapters with authenticated routes require an enabled dashboard"
8833
- );
10724
+ let acpRuntime;
10725
+ const getAcpRuntime = () => {
10726
+ if (acpRuntime) return acpRuntime;
10727
+ const work = getConversationWorkOptions();
10728
+ const state = work.state ?? getStateAdapter();
10729
+ acpRuntime = {
10730
+ handleRequest: createAcpHttpHandler({
10731
+ ...dashboard && !dashboard.disabled ? { browserAuth: { baseURL: resolveAcpBaseURL(dashboard) } } : void 0,
10732
+ conversations: createAcpConversations({
10733
+ conversationStore: work.conversationStore,
10734
+ eventStore: getConversationEventStore(),
10735
+ queue: work.queue ?? getVercelConversationWorkQueue(),
10736
+ state
10737
+ }),
10738
+ onError: (error, event, attributes) => void logException(error, event, { ...attributes, platform: "acp" }),
10739
+ state,
10740
+ version: JUNIOR_VERSION
10741
+ }),
10742
+ state
10743
+ };
10744
+ return acpRuntime;
10745
+ };
10746
+ const authenticatedRoutes = dashboard && !dashboard.disabled ? [
10747
+ {
10748
+ handler: (request, user) => handleAcpAuthorizationPage({
10749
+ agentName: botConfig.userName,
10750
+ request,
10751
+ state: getAcpRuntime().state,
10752
+ user
10753
+ }),
10754
+ method: ["GET", "POST"],
10755
+ path: ACP_AUTHORIZATION_PATH
8834
10756
  }
10757
+ ] : [];
10758
+ try {
8835
10759
  validateDashboardRouteOwnership({
8836
10760
  authenticatedRoutes,
8837
10761
  dashboard,
@@ -8856,8 +10780,12 @@ async function createApp(options) {
8856
10780
  next
8857
10781
  );
8858
10782
  });
10783
+ app.on(
10784
+ ["GET", "POST", "DELETE"],
10785
+ "/api/acp",
10786
+ (c) => getAcpRuntime().handleRequest(c.req.raw)
10787
+ );
8859
10788
  mountRoutes(app, pluginRoutes);
8860
- mountRoutes(app, adapterRoutes);
8861
10789
  mountRoutes(
8862
10790
  app,
8863
10791
  await createDashboardRouteRegistrations({