@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.
- package/dist/{agent-hooks-SNRBZYAU.js → agent-hooks-XTZVALGQ.js} +11 -11
- package/dist/api/acp/auth.d.ts +46 -0
- package/dist/api/acp/authorization-page.d.ts +11 -0
- package/dist/api/acp/conversations.d.ts +70 -0
- package/dist/api/acp/errors.d.ts +6 -0
- package/dist/api/acp/route.d.ts +16 -0
- package/dist/api/acp/schema.d.ts +97 -0
- package/dist/api/acp/sleep.d.ts +2 -0
- package/dist/api/acp/state.d.ts +22 -0
- package/dist/api/acp/transport.d.ts +113 -0
- package/dist/api.js +29 -29
- package/dist/app.d.ts +0 -4
- package/dist/app.js +2010 -82
- package/dist/{catalog-runtime-3I3JOIIH.js → catalog-runtime-5357XR4F.js} +4 -4
- package/dist/{chunk-24OJZ5OU.js → chunk-2ZQPVX7C.js} +6 -6
- package/dist/{chunk-LZBPGX5A.js → chunk-3S7ULUWW.js} +1 -1
- package/dist/{chunk-AHMWPFIW.js → chunk-4CNDGK25.js} +6 -6
- package/dist/{chunk-3HJ4F4L7.js → chunk-4RONS3WR.js} +4 -4
- package/dist/{chunk-LMNMMESF.js → chunk-6UDPINOX.js} +2 -2
- package/dist/{chunk-W46NIB5H.js → chunk-6VOYP4M6.js} +2 -3
- package/dist/{chunk-W5JMPEBO.js → chunk-6WGC3SO4.js} +9 -9
- package/dist/{chunk-T5MTGX2E.js → chunk-ACPDVEJO.js} +22 -20
- package/dist/{chunk-6UXDTPEN.js → chunk-BCHYQS4B.js} +1 -1
- package/dist/{chunk-2ADFGSUS.js → chunk-BHYRYYLY.js} +1 -1
- package/dist/{chunk-IO6KFXYE.js → chunk-BT53RIGJ.js} +7 -7
- package/dist/{chunk-G2B2XZQW.js → chunk-DUKIDPF6.js} +1 -1
- package/dist/{chunk-PLZT45VC.js → chunk-E57WPXWY.js} +5 -5
- package/dist/{chunk-2EPYYVJC.js → chunk-FKEZZMXS.js} +8 -8
- package/dist/{chunk-R767Y7QV.js → chunk-HIPUIPJM.js} +5 -5
- package/dist/{chunk-3L7CWDQQ.js → chunk-HUXFMDBZ.js} +1 -1
- package/dist/{chunk-AX3UDIMA.js → chunk-IZDYYYGP.js} +3 -3
- package/dist/{chunk-56XONJPF.js → chunk-JA43DV4Y.js} +1 -1
- package/dist/{chunk-QI5JW5AG.js → chunk-JM2COLGC.js} +6 -6
- package/dist/{chunk-QZV76CM4.js → chunk-JNO2GUDS.js} +21 -21
- package/dist/{chunk-35PM4YY5.js → chunk-K7VDC2QW.js} +6 -6
- package/dist/{chunk-4WGFWHWP.js → chunk-LCP4EDKR.js} +1 -1
- package/dist/{chunk-EENQGEIP.js → chunk-LHXIZLFF.js} +1 -1
- package/dist/{chunk-WOKJRLM2.js → chunk-M5YWNMTE.js} +5 -5
- package/dist/{chunk-37IQ6DGU.js → chunk-MAPJFUMV.js} +3 -3
- package/dist/{chunk-FJMTMGBC.js → chunk-MLSQX2EX.js} +1 -1
- package/dist/{chunk-E3OBGRHN.js → chunk-PJPRXO2T.js} +1 -1
- package/dist/{chunk-VOV3SCM3.js → chunk-QABCY33S.js} +1 -1
- package/dist/{chunk-X52ESOBP.js → chunk-SHJK75Z2.js} +3 -3
- package/dist/{chunk-AYW7J6K4.js → chunk-UBOIHNTH.js} +13 -13
- package/dist/{chunk-V64JCZUV.js → chunk-UF6BUA5W.js} +3 -3
- package/dist/{chunk-KTXOGLNE.js → chunk-WQFEWULF.js} +2 -2
- package/dist/{chunk-SQHFVIMZ.js → chunk-YKX6MTLU.js} +1 -1
- package/dist/{chunk-HUK5XIZL.js → chunk-Z3QAXJR7.js} +4 -4
- package/dist/cli/chat.js +32 -32
- package/dist/cli/check.js +5 -5
- package/dist/cli/plugins.js +12 -12
- package/dist/cli/snapshot-create.js +9 -9
- package/dist/cli/upgrade.js +6 -6
- package/dist/{db-K2VMNLHQ.js → db-MA52H62H.js} +6 -6
- package/dist/instrumentation.js +3 -3
- package/dist/nitro.js +6 -6
- package/dist/{oauth-callback-server-DZZMTKSS.js → oauth-callback-server-DXGHKN6S.js} +25 -25
- package/dist/{runner-IIAKWDXI.js → runner-4IXUAQNK.js} +18 -18
- package/dist/{skills-EXIQ7BP5.js → skills-X3DY7AXO.js} +5 -5
- package/dist/spawn-43WGS6CH.js +37 -0
- package/dist/task-runner-VDRKITPV.js +32 -0
- package/dist/{validation-XUGIWBJF.js → validation-Z6JXSTJS.js} +5 -5
- package/dist/version.js +1 -1
- package/dist/{work-G4RLJ634.js → work-OKWRFBDW.js} +18 -18
- package/dist/worker-B236AE72.js +22 -0
- package/package.json +5 -6
- package/dist/app-adapter.d.ts +0 -85
- package/dist/chat/api-turns/adapter-conversations.d.ts +0 -15
- package/dist/spawn-ZR6OOEAN.js +0 -37
- package/dist/task-runner-C52HYCVD.js +0 -32
- 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-
|
|
3
|
+
} from "./chunk-4RONS3WR.js";
|
|
4
4
|
import {
|
|
5
5
|
bindSpawnAgent
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QABCY33S.js";
|
|
7
7
|
import {
|
|
8
8
|
createAgentInvocationWorker,
|
|
9
9
|
enqueueAgentInvocation,
|
|
10
10
|
listPendingAgentInvocationMailboxAppends,
|
|
11
11
|
routeAgentInvocationWork
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
57
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
106
|
+
} from "./chunk-UF6BUA5W.js";
|
|
107
107
|
import {
|
|
108
108
|
validatePluginEgressCredentialHooks,
|
|
109
109
|
validatePluginRegistrations
|
|
110
|
-
} from "./chunk-
|
|
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-
|
|
158
|
+
} from "./chunk-JNO2GUDS.js";
|
|
159
159
|
import {
|
|
160
160
|
AuthorizationFlowDisabledError,
|
|
161
161
|
PluginCredentialFailureError
|
|
162
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
200
|
+
} from "./chunk-HIPUIPJM.js";
|
|
201
201
|
import {
|
|
202
202
|
parseSlackThreadId
|
|
203
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
272
|
+
} from "./chunk-2ZQPVX7C.js";
|
|
273
273
|
import {
|
|
274
274
|
getSandboxResourceConfig,
|
|
275
275
|
setSandboxResourceConfig
|
|
276
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
343
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
370
|
-
import "./chunk-
|
|
371
|
-
import "./chunk-
|
|
369
|
+
} from "./chunk-6UDPINOX.js";
|
|
370
|
+
import "./chunk-BHYRYYLY.js";
|
|
371
|
+
import "./chunk-WQFEWULF.js";
|
|
372
372
|
import {
|
|
373
373
|
pluginCatalogRuntime
|
|
374
|
-
} from "./chunk-
|
|
375
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
701
|
+
const errorMessage3 = storedSession?.errorMessage;
|
|
702
702
|
if (dispatchOutcome) {
|
|
703
703
|
return {
|
|
704
|
-
...
|
|
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
|
|
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" ?
|
|
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
|
|
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
|
-
|
|
7176
|
+
errorMessage3 = result.errorMessage;
|
|
7177
7177
|
},
|
|
7178
7178
|
shouldYield: hooks.shouldYield,
|
|
7179
7179
|
skipBackfill: true
|
|
7180
7180
|
});
|
|
7181
7181
|
return {
|
|
7182
|
-
...
|
|
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,
|
|
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/
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
|
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
|
|
8656
|
-
const explicitMethods =
|
|
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 (
|
|
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
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
const
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
(
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
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({
|