@sentry/junior 0.263.0 → 0.265.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{adapter-BIacI0Bt.js → adapter-cUkPH3Om.js} +1 -1
- package/dist/{admin-C7vUk7s9.js → admin-Cf0rDmWV.js} +1 -1
- package/dist/{agent-runner-BUBas7Yy.js → agent-runner-C6MwHJEs.js} +296 -238
- package/dist/api/schema.d.ts +213 -16
- package/dist/api/schema.js +3 -3
- package/dist/api.js +107 -40
- package/dist/{app-Bo0kB987.d.ts → app-Csbi9dW7.d.ts} +4 -3
- package/dist/app.d.ts +1 -1
- package/dist/app.js +101 -393
- package/dist/{assistant-reply-BFmjgJjD.js → assistant-reply-UPA8vKlC.js} +1 -1
- package/dist/{automation-CWX0qL8U.js → automation-2RbKzpMT.js} +30 -9
- package/dist/{briefs-h55v5lX9.js → briefs-DRjx8kQ9.js} +3 -3
- package/dist/{catalog-runtime-Suf-aLlh.js → catalog-runtime-D3qbJlYz.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +4 -18
- package/dist/cli/main.js +4 -4
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-DZBCBj1M.js → client-DX34VNtH.js} +3 -8
- package/dist/{config-Dnv8KmNs.js → config-BayQ9hHH.js} +100 -3
- package/dist/{connections-Br0_xv27.js → connections-mpN5Lsnj.js} +11 -10
- package/dist/{conversation-privacy-FY8yc2q3.js → conversation-privacy-frjQCJa9.js} +1 -1
- package/dist/{credential-sync-BvkiyWF5.js → credential-sync-CZmyDNdR.js} +20 -20
- package/dist/{agent-hooks-CqnZfuCt.js → credentialed-C6pKlB1p.js} +3197 -66
- package/dist/{executor-J9k2sp9m.js → executor-CAr7xQr2.js} +10 -300
- package/dist/{images-DV2FlEGL.js → images-B2yA3HWE.js} +2 -2
- package/dist/instrumentation.js +41 -25
- package/dist/{job-queue-B4yTgklN.js → job-queue-Cjo6Yg_b.js} +53 -14
- package/dist/{logging-Dk_FpF36.js → logging-D2QBriUo.js} +19 -32
- package/dist/{message-BzseAFH1.js → message-C9ymnpBS.js} +4 -4
- package/dist/nitro.d.ts +17 -2
- package/dist/nitro.js +26 -27
- package/dist/{oauth-callback-server-bTspTMCC.js → oauth-callback-server-BtMf3TPZ.js} +2 -2
- package/dist/{prompt-CxatQVaj.js → prompt-CvaDdSX3.js} +1 -1
- package/dist/{registry-BFzQFV_y.js → registry-COTeZ2cV.js} +1 -1
- package/dist/{resolve-CnRHib0U.js → resolve-g9PKz9pH.js} +7 -106
- package/dist/{result-DczfmNBa.js → result-COnC42GR.js} +8 -8
- package/dist/{runner-CJd9Wec-.js → runner-BnTNS7x_.js} +10 -10
- package/dist/{sandbox-egress-signals-DUmVkOx4.js → sandbox-egress-signals-C_IIdLFk.js} +1 -1
- package/dist/{schema-QidbykYl.js → schema-D5Amlzkp.js} +44 -3
- package/dist/{sentry-BBUlHih9.js → sentry-MDkYAVPV.js} +3 -6
- package/dist/{service-account-BI2kQDrb.js → service-account-CwvToidT.js} +3 -3
- package/dist/{service-account-x2QZXm4X.js → service-account-YdYftwV2.js} +5 -5
- package/dist/{session-stop-CtyJsnEj.js → session-stop-CpCUy1ZJ.js} +121 -81
- package/dist/{skills-Dbzx9-rS.js → skills-Bb2KaJ3m.js} +2 -2
- package/dist/{spawn-D8WU99UV.js → spawn-mbTcrLb3.js} +2 -2
- package/dist/{store-TKg1HVKx.js → store-CqRnaZSq.js} +3 -3
- package/dist/{store-UlOrW6JC.js → store-DdT74Lvq.js} +18 -9
- package/dist/{task-queue-BRB49H-J.js → task-queue-8XjDIORF.js} +14 -19
- package/dist/{task-runner-C575f2Dd.js → task-runner-6mLmG6g_.js} +46 -28
- package/dist/{tasks-LOItiYgL.js → tasks-DGbrur6x.js} +14 -2075
- package/dist/{thread-state-IDHdZA7R.js → thread-state-D45lz-oC.js} +5 -5
- package/dist/{turn-failure-response-h_LSXJ-M.js → turn-failure-response-CaEPLSBn.js} +7 -7
- package/dist/{turn-lifecycle-Cvi6Sb8Y.js → turn-lifecycle-C35IyRGp.js} +1 -1
- package/dist/{turn-session-routing-5Nyv4fso.js → turn-session-routing-BKx6rfVd.js} +1 -1
- package/dist/{validation-BeTriWC2.js → validation-C0X3CX51.js} +1 -1
- package/dist/{version-BQX_hLDC.js → version-BPHgJwnm.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-DmNRM88V.js → viewer-B2wXNspU.js} +21 -5
- package/dist/{web-input-BHcGXSox.js → web-input-Y6aXvmfa.js} +145 -46
- package/dist/{work-DLJMV02b.js → work-DMbqK0kT.js} +10 -10
- package/dist/{worker-vfG2ESg1.js → worker-B-rDJ3XN.js} +8 -6
- package/package.json +10 -9
- package/dist/profile-Dq5I7TV1.js +0 -582
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { C as addAgentTurnUsage, R as parseDestination, c as getConversationEventStore, d as getDb, f as getSqlExecutor, m as withConversationEventLock, p as createSqlConversationEventStore, u as getConversationStore, x as bindProviderConversation, y as startLocationThread } from "./viewer-DmNRM88V.js";
|
|
1
|
+
import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-DX34VNtH.js";
|
|
2
|
+
import { n as botConfig } from "./config-BayQ9hHH.js";
|
|
3
|
+
import { V as toOptionalString, f as logException, m as logWarn, o as getActiveTraceId, t as buildTurnFailureResponse, y as setSpanAttributes, z as isRecord } from "./logging-D2QBriUo.js";
|
|
4
|
+
import { c as toStoredSlackActor, g as parseSlackMessageTs } from "./actor-tlxzqaRe.js";
|
|
5
|
+
import { S as bindProviderConversation, b as startLocationThread, d as getConversationStore, f as getDb, h as withConversationEventLock, l as getConversationEventStore, m as createSqlConversationEventStore, p as getSqlExecutor, w as addAgentTurnUsage } from "./viewer-B2wXNspU.js";
|
|
7
6
|
import { d as juniorDestinations } from "./agent-invocations-CxKldAeB.js";
|
|
8
|
-
import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-
|
|
7
|
+
import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-CAr7xQr2.js";
|
|
9
8
|
import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-AY8rs-FR.js";
|
|
10
|
-
import {
|
|
11
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
9
|
+
import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-COTeZ2cV.js";
|
|
10
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-D3qbJlYz.js";
|
|
11
|
+
import { An as createPluginLogger, Q as parseSandboxEgressAuthRequiredSignal, ar as agentsInstructionsUpdatedEvent, at as formatProviderLabel, ct as startOAuthFlow, ir as JUNIOR_NATIVE_EVENT_NAMESPACE, mn as ToolInputError, nr as createPluginCache, nt as resolveSandboxEgressProviderForHost, on as sleep, or as authenticationLinkedEvent, p as getPlugins, sr as authenticationUnlinkedEvent, t as executeCredentialedEgressRequest } from "./credentialed-C6pKlB1p.js";
|
|
12
|
+
import { i as MUTATION_LOCK_TTL_MS, o as fenceLock, r as getStateAdapter, s as withLock } from "./adapter-cUkPH3Om.js";
|
|
14
13
|
import { N as messageCardRef } from "./conversation-B0baEwoA.js";
|
|
15
|
-
import {
|
|
16
|
-
import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-CWX0qL8U.js";
|
|
14
|
+
import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-2RbKzpMT.js";
|
|
17
15
|
import { isDeepStrictEqual } from "node:util";
|
|
18
16
|
import { z } from "zod";
|
|
19
|
-
import { createHash,
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
18
|
import { and, asc, desc, eq, inArray, ne, or, sql } from "drizzle-orm";
|
|
21
|
-
import {
|
|
19
|
+
import { actorSchema, eventMatches, sourceSchema } from "@sentry/junior-plugin-api";
|
|
22
20
|
// src/chat/configuration/defaults.ts
|
|
23
21
|
let installDefaults = {};
|
|
24
22
|
function cloneDefaults(defaults) {
|
|
@@ -397,2065 +395,6 @@ function createDurableLocationConfigurationService(args) {
|
|
|
397
395
|
}
|
|
398
396
|
});
|
|
399
397
|
}
|
|
400
|
-
// src/chat/plugins/credential-hooks.ts
|
|
401
|
-
function parseSchema(schema, value, message) {
|
|
402
|
-
const result = schema.safeParse(value);
|
|
403
|
-
if (!result.success) throw new Error(message);
|
|
404
|
-
return result.data;
|
|
405
|
-
}
|
|
406
|
-
function parseAuthorization(value, pluginName) {
|
|
407
|
-
if (value === void 0) return;
|
|
408
|
-
const authorization = parseSchema(pluginAuthorizationSchema, value, `Plugin "${pluginName}" grant authorization is invalid`);
|
|
409
|
-
if (authorization.provider !== pluginName) throw new Error(`Plugin "${pluginName}" grant authorization provider must match the issuing plugin`);
|
|
410
|
-
return authorization;
|
|
411
|
-
}
|
|
412
|
-
function parseGrant(value, pluginName) {
|
|
413
|
-
return parseSchema(pluginGrantSchema, value, `Plugin "${pluginName}" grantForEgress returned an invalid grant`);
|
|
414
|
-
}
|
|
415
|
-
function pluginFor(provider) {
|
|
416
|
-
return getPlugins().find((candidate) => candidate.manifest.name === provider);
|
|
417
|
-
}
|
|
418
|
-
function basePluginContext(plugin) {
|
|
419
|
-
const pluginName = plugin.manifest.name;
|
|
420
|
-
return {
|
|
421
|
-
plugin: { name: pluginName },
|
|
422
|
-
cache: createPluginCache(pluginName),
|
|
423
|
-
log: createPluginLogger(pluginName),
|
|
424
|
-
db: getDb()
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
function parseCredentialResult(value, pluginName) {
|
|
428
|
-
const result = parseSchema(pluginCredentialResultSchema, value, `Plugin "${pluginName}" issueCredential result is invalid`);
|
|
429
|
-
if (result.type === "lease") {
|
|
430
|
-
parseAuthorization(result.lease.authorization, pluginName);
|
|
431
|
-
return result;
|
|
432
|
-
}
|
|
433
|
-
if (result.type === "unavailable") return result;
|
|
434
|
-
parseAuthorization(result.authorization, pluginName);
|
|
435
|
-
return result;
|
|
436
|
-
}
|
|
437
|
-
/** Ask a plugin which grant an outbound request needs. */
|
|
438
|
-
async function selectPluginGrant(input) {
|
|
439
|
-
const plugin = pluginFor(input.provider);
|
|
440
|
-
const hook = plugin?.hooks?.grantForEgress;
|
|
441
|
-
if (!plugin || !hook) return;
|
|
442
|
-
const result = await hook({
|
|
443
|
-
...basePluginContext(plugin),
|
|
444
|
-
request: {
|
|
445
|
-
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
|
|
446
|
-
method: input.method,
|
|
447
|
-
...input.operation ? { operation: input.operation } : void 0,
|
|
448
|
-
url: input.upstreamUrl.toString()
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
|
|
452
|
-
}
|
|
453
|
-
/** Let the owning plugin inspect an upstream response without changing pass-through behavior. */
|
|
454
|
-
async function onPluginEgressResponse(input) {
|
|
455
|
-
const plugin = pluginFor(input.provider);
|
|
456
|
-
const hook = plugin?.hooks?.onEgressResponse;
|
|
457
|
-
if (!plugin || !hook) return {};
|
|
458
|
-
let permissionDenied;
|
|
459
|
-
await hook({
|
|
460
|
-
...basePluginContext(plugin),
|
|
461
|
-
grant: input.grant,
|
|
462
|
-
permissionDenied(message) {
|
|
463
|
-
const trimmed = message.trim();
|
|
464
|
-
if (!trimmed) throw new Error(`Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`);
|
|
465
|
-
permissionDenied = { message: trimmed };
|
|
466
|
-
},
|
|
467
|
-
request: {
|
|
468
|
-
method: input.method,
|
|
469
|
-
...input.operation ? { operation: input.operation } : void 0,
|
|
470
|
-
url: input.upstreamUrl.toString()
|
|
471
|
-
},
|
|
472
|
-
response: input.response
|
|
473
|
-
});
|
|
474
|
-
return permissionDenied ? { permissionDenied } : {};
|
|
475
|
-
}
|
|
476
|
-
/** Return whether a plugin owns credential issuance for egress. */
|
|
477
|
-
function hasEgressCredentialHooks(provider) {
|
|
478
|
-
const hooks = pluginFor(provider)?.hooks;
|
|
479
|
-
return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
|
|
480
|
-
}
|
|
481
|
-
/** Return providers that can verify an OAuth account for identity storage. */
|
|
482
|
-
function getOAuthAccountProviders() {
|
|
483
|
-
return getPlugins().filter((plugin) => Boolean(plugin.hooks?.resolveOAuthAccount)).map((plugin) => plugin.manifest.name);
|
|
484
|
-
}
|
|
485
|
-
/** Ask a plugin which provider account belongs to an OAuth token. */
|
|
486
|
-
async function resolvePluginOAuthAccount(input) {
|
|
487
|
-
const plugin = pluginFor(input.provider);
|
|
488
|
-
const hook = plugin?.hooks?.resolveOAuthAccount;
|
|
489
|
-
if (!plugin || !hook) return;
|
|
490
|
-
const account = await hook({
|
|
491
|
-
...basePluginContext(plugin),
|
|
492
|
-
tokenResponse: input.tokenResponse,
|
|
493
|
-
tokens: input.tokens
|
|
494
|
-
});
|
|
495
|
-
return account === void 0 ? void 0 : parseSchema(pluginProviderAccountSchema, account, `Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`);
|
|
496
|
-
}
|
|
497
|
-
/** Ask a plugin to issue headers or describe why the selected grant is unavailable. */
|
|
498
|
-
async function issuePluginCredential(input) {
|
|
499
|
-
const plugin = pluginFor(input.provider);
|
|
500
|
-
const hook = plugin?.hooks?.issueCredential;
|
|
501
|
-
if (!plugin || !hook) throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
|
|
502
|
-
const currentUserId = "type" in input.actor ? input.actor.userId : void 0;
|
|
503
|
-
const credentialSubjectUserId = input.credentialSubject?.userId;
|
|
504
|
-
return parseCredentialResult(await hook({
|
|
505
|
-
...basePluginContext(plugin),
|
|
506
|
-
actor: input.actor,
|
|
507
|
-
grant: input.grant,
|
|
508
|
-
...input.credentialSubject ? { credentialSubject: input.credentialSubject } : void 0,
|
|
509
|
-
tokens: {
|
|
510
|
-
...currentUserId ? { currentUser: {
|
|
511
|
-
userId: currentUserId,
|
|
512
|
-
get: async () => await input.userTokenStore.get(currentUserId, plugin.manifest.name),
|
|
513
|
-
set: async (tokens) => {
|
|
514
|
-
await input.userTokenStore.set(currentUserId, plugin.manifest.name, tokens);
|
|
515
|
-
},
|
|
516
|
-
withRefresh: async (callback) => await input.userTokenStore.withRefresh(currentUserId, plugin.manifest.name, callback)
|
|
517
|
-
} } : void 0,
|
|
518
|
-
...credentialSubjectUserId ? { credentialSubject: {
|
|
519
|
-
userId: credentialSubjectUserId,
|
|
520
|
-
get: async () => await input.userTokenStore.get(credentialSubjectUserId, plugin.manifest.name),
|
|
521
|
-
set: async (tokens) => {
|
|
522
|
-
await input.userTokenStore.set(credentialSubjectUserId, plugin.manifest.name, tokens);
|
|
523
|
-
},
|
|
524
|
-
withRefresh: async (callback) => await input.userTokenStore.withRefresh(credentialSubjectUserId, plugin.manifest.name, callback)
|
|
525
|
-
} } : void 0
|
|
526
|
-
}
|
|
527
|
-
}), plugin.manifest.name);
|
|
528
|
-
}
|
|
529
|
-
// src/chat/slack/post-warning.ts
|
|
530
|
-
const codes = /* @__PURE__ */ new Set([
|
|
531
|
-
"invalid_metadata_format",
|
|
532
|
-
"invalid_metadata_schema",
|
|
533
|
-
"metadata_too_large",
|
|
534
|
-
"invalid_arguments",
|
|
535
|
-
"invalid_blocks",
|
|
536
|
-
"missing_scope",
|
|
537
|
-
"missing_charset",
|
|
538
|
-
"superfluous_charset",
|
|
539
|
-
"message_truncated"
|
|
540
|
-
]);
|
|
541
|
-
const pathFields = /* @__PURE__ */ new Set([
|
|
542
|
-
"metadata",
|
|
543
|
-
"event_type",
|
|
544
|
-
"event_payload",
|
|
545
|
-
"entities",
|
|
546
|
-
"entity_type",
|
|
547
|
-
"entity_payload",
|
|
548
|
-
"external_ref",
|
|
549
|
-
"id",
|
|
550
|
-
"type",
|
|
551
|
-
"url",
|
|
552
|
-
"attributes",
|
|
553
|
-
"title",
|
|
554
|
-
"text",
|
|
555
|
-
"display_id",
|
|
556
|
-
"display_type",
|
|
557
|
-
"product_name",
|
|
558
|
-
"fields",
|
|
559
|
-
"status",
|
|
560
|
-
"custom_fields",
|
|
561
|
-
"key",
|
|
562
|
-
"label",
|
|
563
|
-
"value",
|
|
564
|
-
"long",
|
|
565
|
-
"format",
|
|
566
|
-
"link",
|
|
567
|
-
"display_order",
|
|
568
|
-
"app_unfurl_url",
|
|
569
|
-
"app_id",
|
|
570
|
-
"created_by",
|
|
571
|
-
"created_at",
|
|
572
|
-
"updated_by",
|
|
573
|
-
"updated_at",
|
|
574
|
-
"full_size_preview",
|
|
575
|
-
"preview_url",
|
|
576
|
-
"mime_type"
|
|
577
|
-
]);
|
|
578
|
-
const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
|
|
579
|
-
ignored as a result For for event entity refer to following errors error
|
|
580
|
-
missing required field fields property properties additional unexpected unknown
|
|
581
|
-
unsupported invalid expected received actual type types must should match matches
|
|
582
|
-
matching does not is are has have of at in on and or an only allowed allow
|
|
583
|
-
value values string number integer boolean object array null minimum maximum
|
|
584
|
-
length min max items item empty nonempty too long short large small size limit
|
|
585
|
-
exceeded exceeds less greater than equal valid format schema validation failed
|
|
586
|
-
failure cannot contain contains found one any all none enum pattern definition
|
|
587
|
-
defined constraint constraints satisfy satisfies with without provided specified
|
|
588
|
-
unrecognized unrecognised recognized recognised extraneous disallowed duplicate
|
|
589
|
-
unique malformed nested root exactly needs need requires requirement supported
|
|
590
|
-
json-pointer input different schema_uri schema_id schemas additionalProperties
|
|
591
|
-
minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
|
|
592
|
-
function summarizeDiagnostic(value) {
|
|
593
|
-
return value.slice(0, 2e3).replace(/\b(?:https?:\/\/|www\.)[^\s"'`<>]+/gi, "[value]").replace(/\/[^\s"'`[\],;()]*|[\p{L}\p{N}\p{M}_]+(?:[-./:@=+%~][\p{L}\p{N}\p{M}_]+)*|[^\s\w[\]{}():,;."'`/*=<>!?+-]/gu, (token) => {
|
|
594
|
-
if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
|
|
595
|
-
return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
|
|
596
|
-
}).replace(/\s+/g, " ").slice(0, 2e3);
|
|
597
|
-
}
|
|
598
|
-
/** Report Slack warnings once, on the request span, without retrying the post. */
|
|
599
|
-
function captureSlackPostWarning(response, attributes) {
|
|
600
|
-
const warnings = response.response_metadata?.warnings ?? [];
|
|
601
|
-
const messages = response.response_metadata?.messages ?? [];
|
|
602
|
-
const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
|
|
603
|
-
const diagnostics = {
|
|
604
|
-
"app.slack.warning_count": warnings.length,
|
|
605
|
-
"app.slack.response_message_count": messages.length,
|
|
606
|
-
"app.slack.diagnostic_codes": knownCodes
|
|
607
|
-
};
|
|
608
|
-
setSpanAttributes(diagnostics);
|
|
609
|
-
if (!warnings.length && !messages.length) return;
|
|
610
|
-
captureMessage("Slack chat.postMessage returned warnings", {
|
|
611
|
-
level: "warning",
|
|
612
|
-
fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
|
|
613
|
-
tags: { "app.slack.method": "chat.postMessage" },
|
|
614
|
-
extra: {
|
|
615
|
-
...getLogContextAttributes(),
|
|
616
|
-
...attributes,
|
|
617
|
-
...diagnostics,
|
|
618
|
-
"app.slack.response_diagnostics": {
|
|
619
|
-
warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
|
|
620
|
-
messages: messages.slice(0, 20).map(summarizeDiagnostic)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
// src/chat/slack/work-object.ts
|
|
626
|
-
const url = z.url({ protocol: /^https?$/ });
|
|
627
|
-
const textField = z.strictObject({
|
|
628
|
-
type: z.literal("string"),
|
|
629
|
-
key: z.string().min(1),
|
|
630
|
-
label: z.string().min(1),
|
|
631
|
-
value: z.string(),
|
|
632
|
-
long: z.boolean().optional(),
|
|
633
|
-
format: z.literal("markdown").optional(),
|
|
634
|
-
link: url.optional()
|
|
635
|
-
}).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
|
|
636
|
-
const customField = z.discriminatedUnion("type", [textField, z.strictObject({
|
|
637
|
-
type: z.literal("slack#/types/timestamp"),
|
|
638
|
-
key: z.string().min(1),
|
|
639
|
-
label: z.string().min(1),
|
|
640
|
-
value: z.number().int(),
|
|
641
|
-
link: url.optional()
|
|
642
|
-
})]);
|
|
643
|
-
const payload = z.strictObject({
|
|
644
|
-
attributes: z.strictObject({
|
|
645
|
-
title: z.strictObject({ text: z.string().min(1) }),
|
|
646
|
-
display_id: z.string().optional(),
|
|
647
|
-
display_type: z.string().optional(),
|
|
648
|
-
product_name: z.string().optional(),
|
|
649
|
-
product_icon: z.strictObject({
|
|
650
|
-
url,
|
|
651
|
-
alt_text: z.string().min(1)
|
|
652
|
-
}).optional()
|
|
653
|
-
}),
|
|
654
|
-
custom_fields: z.array(customField).optional(),
|
|
655
|
-
display_order: z.array(z.string()).optional()
|
|
656
|
-
});
|
|
657
|
-
/** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
|
|
658
|
-
const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
|
|
659
|
-
const entity = z.strictObject({
|
|
660
|
-
external_ref: z.strictObject({
|
|
661
|
-
id: slackExternalRefIdSchema,
|
|
662
|
-
type: z.string().min(1).optional()
|
|
663
|
-
}),
|
|
664
|
-
url
|
|
665
|
-
});
|
|
666
|
-
/** Validate notification and detail metadata before Slack can ignore bad fields. */
|
|
667
|
-
const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
|
|
668
|
-
entity_type: z.literal("slack#/entities/item"),
|
|
669
|
-
entity_payload: payload.extend({ fields: z.never().optional() })
|
|
670
|
-
}), entity.extend({
|
|
671
|
-
entity_type: z.literal("slack#/entities/task"),
|
|
672
|
-
entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
|
|
673
|
-
})]);
|
|
674
|
-
// src/chat/slack/outbound.ts
|
|
675
|
-
const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
|
|
676
|
-
function requireSlackConversationId(channelId, action) {
|
|
677
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
678
|
-
if (!normalized) throw new Error(`${action} requires a valid channel ID`);
|
|
679
|
-
return normalized;
|
|
680
|
-
}
|
|
681
|
-
function requireSlackThreadTimestamp(threadTs, action) {
|
|
682
|
-
const normalized = threadTs.trim();
|
|
683
|
-
if (!normalized) throw new Error(`${action} requires a thread timestamp`);
|
|
684
|
-
return normalized;
|
|
685
|
-
}
|
|
686
|
-
function requireSlackMessageTimestamp(timestamp, action) {
|
|
687
|
-
const normalized = parseSlackMessageTs(timestamp);
|
|
688
|
-
if (!normalized) throw new Error(`${action} requires a target message timestamp`);
|
|
689
|
-
return normalized;
|
|
690
|
-
}
|
|
691
|
-
function requireSlackMessageText(text, action) {
|
|
692
|
-
if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
|
|
693
|
-
if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
|
|
694
|
-
return text;
|
|
695
|
-
}
|
|
696
|
-
/** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
|
|
697
|
-
async function getSlackMessagePermalink(args) {
|
|
698
|
-
try {
|
|
699
|
-
return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
|
|
700
|
-
channel: args.channelId,
|
|
701
|
-
message_ts: args.messageTs
|
|
702
|
-
}), 3, {
|
|
703
|
-
action: "chat.getPermalink",
|
|
704
|
-
idempotent: true,
|
|
705
|
-
spanAttributes: {
|
|
706
|
-
"app.slack.channel_id": args.channelId,
|
|
707
|
-
"app.slack.message_ts": args.messageTs
|
|
708
|
-
}
|
|
709
|
-
})).permalink;
|
|
710
|
-
} catch {
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
/** Open the bot's direct message with one Slack user and return its channel ID. */
|
|
715
|
-
async function openSlackDirectMessage(userId) {
|
|
716
|
-
const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
|
|
717
|
-
if (!channelId) throw new Error("Slack did not return a direct message channel.");
|
|
718
|
-
return channelId;
|
|
719
|
-
}
|
|
720
|
-
/** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
|
|
721
|
-
async function postSlackMessage(input) {
|
|
722
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
|
|
723
|
-
const text = requireSlackMessageText(input.text, "Slack message posting");
|
|
724
|
-
const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
|
|
725
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
|
|
726
|
-
const attributes = {
|
|
727
|
-
"app.slack.channel_id": channelId,
|
|
728
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
|
|
729
|
-
"app.slack.work_object.count": entities?.length ?? 0
|
|
730
|
-
};
|
|
731
|
-
const response = await withSlackRetries(async () => {
|
|
732
|
-
const response = await getSlackClient().chat.postMessage({
|
|
733
|
-
channel: channelId,
|
|
734
|
-
text,
|
|
735
|
-
unfurl_links: false,
|
|
736
|
-
unfurl_media: false,
|
|
737
|
-
...input.blocks?.length ? { blocks: input.blocks } : void 0,
|
|
738
|
-
...entities ? { metadata: { entities } } : void 0,
|
|
739
|
-
...threadTs ? { thread_ts: threadTs } : void 0
|
|
740
|
-
});
|
|
741
|
-
const messageId = parseSlackMessageTs(response.ts);
|
|
742
|
-
setSpanAttributes({ "messaging.message.id": messageId });
|
|
743
|
-
captureSlackPostWarning(response, {
|
|
744
|
-
...attributes,
|
|
745
|
-
"messaging.message.id": messageId
|
|
746
|
-
});
|
|
747
|
-
return response;
|
|
748
|
-
}, 3, {
|
|
749
|
-
action: "chat.postMessage",
|
|
750
|
-
attributes
|
|
751
|
-
});
|
|
752
|
-
const messageTs = parseSlackMessageTs(response.ts);
|
|
753
|
-
if (!messageTs) throw new Error("Slack message posted without ts");
|
|
754
|
-
return {
|
|
755
|
-
ts: messageTs,
|
|
756
|
-
...input.includePermalink ? { permalink: await getSlackMessagePermalink({
|
|
757
|
-
channelId,
|
|
758
|
-
messageTs
|
|
759
|
-
}) } : void 0
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
|
|
764
|
-
* request validation and Web API behavior are centralized here.
|
|
765
|
-
*/
|
|
766
|
-
async function postSlackEphemeralMessage(input) {
|
|
767
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
|
|
768
|
-
const userId = parseSlackUserId(input.userId);
|
|
769
|
-
if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
|
|
770
|
-
const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
|
|
771
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
|
|
772
|
-
return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
|
|
773
|
-
channel: channelId,
|
|
774
|
-
user: userId,
|
|
775
|
-
text,
|
|
776
|
-
...input.blocks?.length ? { blocks: input.blocks } : void 0,
|
|
777
|
-
...threadTs ? { thread_ts: threadTs } : void 0
|
|
778
|
-
}), 3, {
|
|
779
|
-
action: "chat.postEphemeral",
|
|
780
|
-
spanAttributes: {
|
|
781
|
-
"app.slack.channel_id": channelId,
|
|
782
|
-
"app.slack.user_id": userId,
|
|
783
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
|
|
784
|
-
}
|
|
785
|
-
})).message_ts };
|
|
786
|
-
}
|
|
787
|
-
/** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
|
|
788
|
-
async function uploadFilesToConversation(input) {
|
|
789
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
|
|
790
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
|
|
791
|
-
if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
|
|
792
|
-
const fileUploads = input.files.map((file) => {
|
|
793
|
-
const filename = file.filename.trim();
|
|
794
|
-
if (!filename) throw new Error("Slack file upload requires every file to have a filename");
|
|
795
|
-
return {
|
|
796
|
-
file: file.data,
|
|
797
|
-
filename
|
|
798
|
-
};
|
|
799
|
-
});
|
|
800
|
-
return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
|
|
801
|
-
channel_id: channelId,
|
|
802
|
-
...threadTs ? { thread_ts: threadTs } : void 0,
|
|
803
|
-
file_uploads: fileUploads
|
|
804
|
-
}), 3, {
|
|
805
|
-
action: "filesUploadV2",
|
|
806
|
-
spanAttributes: {
|
|
807
|
-
"app.slack.channel_id": channelId,
|
|
808
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
|
|
809
|
-
}
|
|
810
|
-
})).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
|
|
811
|
-
}
|
|
812
|
-
/** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
|
|
813
|
-
async function addReactionToMessage(input) {
|
|
814
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
|
|
815
|
-
const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
|
|
816
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
817
|
-
if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
|
|
818
|
-
try {
|
|
819
|
-
await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
|
|
820
|
-
channel: channelId,
|
|
821
|
-
timestamp,
|
|
822
|
-
name: emoji
|
|
823
|
-
}), input.timeoutMs === void 0 ? 3 : 1, {
|
|
824
|
-
action: "reactions.add",
|
|
825
|
-
idempotent: true,
|
|
826
|
-
spanAttributes: {
|
|
827
|
-
"app.slack.channel_id": channelId,
|
|
828
|
-
"app.slack.message_ts": timestamp,
|
|
829
|
-
"app.slack.reaction": emoji
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
} catch (error) {
|
|
833
|
-
if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
|
|
834
|
-
throw error;
|
|
835
|
-
}
|
|
836
|
-
return { ok: true };
|
|
837
|
-
}
|
|
838
|
-
/** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
|
|
839
|
-
async function removeReactionFromMessage(input) {
|
|
840
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
|
|
841
|
-
const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
|
|
842
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
843
|
-
if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
|
|
844
|
-
try {
|
|
845
|
-
await withSlackRetries(() => getSlackClient().reactions.remove({
|
|
846
|
-
channel: channelId,
|
|
847
|
-
timestamp,
|
|
848
|
-
name: emoji
|
|
849
|
-
}), 3, {
|
|
850
|
-
action: "reactions.remove",
|
|
851
|
-
idempotent: true,
|
|
852
|
-
spanAttributes: {
|
|
853
|
-
"app.slack.channel_id": channelId,
|
|
854
|
-
"app.slack.message_ts": timestamp,
|
|
855
|
-
"app.slack.reaction": emoji
|
|
856
|
-
}
|
|
857
|
-
});
|
|
858
|
-
} catch (error) {
|
|
859
|
-
if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
|
|
860
|
-
throw error;
|
|
861
|
-
}
|
|
862
|
-
return { ok: true };
|
|
863
|
-
}
|
|
864
|
-
// src/chat/slack/status-format.ts
|
|
865
|
-
const SLACK_STATUS_MAX_LENGTH = 50;
|
|
866
|
-
/** Truncate a status string to fit Slack's 50-char assistant status limit. */
|
|
867
|
-
function truncateStatusText(text) {
|
|
868
|
-
const trimmed = text.trim();
|
|
869
|
-
if (!trimmed) return "";
|
|
870
|
-
if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
|
|
871
|
-
return `${trimmed.slice(0, 47).trimEnd()}...`;
|
|
872
|
-
}
|
|
873
|
-
// src/chat/slack/mrkdwn.ts
|
|
874
|
-
/** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
|
|
875
|
-
function escapeSlackMrkdwnText(text) {
|
|
876
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
877
|
-
}
|
|
878
|
-
/** Escape a URL for Slack explicit link syntax while preserving query semantics. */
|
|
879
|
-
function escapeSlackLinkUrl(url) {
|
|
880
|
-
return url.replaceAll("&", "&").replaceAll("<", "%3C").replaceAll(">", "%3E");
|
|
881
|
-
}
|
|
882
|
-
/** Build a Slack explicit link from dynamic URL and label values. */
|
|
883
|
-
function formatSlackLink(url, label) {
|
|
884
|
-
return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
|
|
885
|
-
}
|
|
886
|
-
function readInlineCodeSpan(line, start) {
|
|
887
|
-
if (line[start] !== "`") return;
|
|
888
|
-
let n = 1;
|
|
889
|
-
while (line[start + n] === "`") n++;
|
|
890
|
-
const marker = "`".repeat(n);
|
|
891
|
-
let search = start + n;
|
|
892
|
-
while (search < line.length) {
|
|
893
|
-
const close = line.indexOf(marker, search);
|
|
894
|
-
if (close === -1) return;
|
|
895
|
-
const after = close + n;
|
|
896
|
-
if (line[after] !== "`") return {
|
|
897
|
-
text: line.slice(start, after),
|
|
898
|
-
end: after
|
|
899
|
-
};
|
|
900
|
-
search = after + 1;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
function readExistingSlackAngleToken(line, start) {
|
|
904
|
-
if (line[start] !== "<") return;
|
|
905
|
-
const close = line.indexOf(">", start + 1);
|
|
906
|
-
if (close === -1) return;
|
|
907
|
-
const body = line.slice(start + 1, close);
|
|
908
|
-
if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
|
|
909
|
-
text: line.slice(start, close + 1),
|
|
910
|
-
end: close + 1
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function readMarkdownLink(line, start) {
|
|
914
|
-
if (line[start] !== "[") return;
|
|
915
|
-
const labelEnd = line.indexOf("](", start + 1);
|
|
916
|
-
if (labelEnd === -1) return;
|
|
917
|
-
const destStart = labelEnd + 2;
|
|
918
|
-
if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
|
|
919
|
-
const closeParens = line.indexOf(")", destStart);
|
|
920
|
-
if (closeParens === -1) return;
|
|
921
|
-
return {
|
|
922
|
-
text: line.slice(start, closeParens + 1),
|
|
923
|
-
end: closeParens + 1
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
function hasUnmatchedClosingParen(text) {
|
|
927
|
-
let balance = 0;
|
|
928
|
-
for (const ch of text) if (ch === "(") balance++;
|
|
929
|
-
else if (ch === ")") balance--;
|
|
930
|
-
return balance < 0;
|
|
931
|
-
}
|
|
932
|
-
function readBareUrl(line, start) {
|
|
933
|
-
let end = start;
|
|
934
|
-
while (end < line.length) {
|
|
935
|
-
const ch = line.charAt(end);
|
|
936
|
-
if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
|
|
937
|
-
end++;
|
|
938
|
-
}
|
|
939
|
-
if (end === start) return;
|
|
940
|
-
let raw = line.slice(start, end);
|
|
941
|
-
let suffix = "";
|
|
942
|
-
const peel = () => {
|
|
943
|
-
suffix = raw.slice(-1) + suffix;
|
|
944
|
-
raw = raw.slice(0, -1);
|
|
945
|
-
};
|
|
946
|
-
const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
|
|
947
|
-
while (raw.length > 0 && shouldPeel()) peel();
|
|
948
|
-
if (!/^https?:\/\/.+/.test(raw)) return;
|
|
949
|
-
return {
|
|
950
|
-
url: raw,
|
|
951
|
-
suffix,
|
|
952
|
-
end
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
const BARE_SLACK_USER_ID_RE = /^@([UW](?=[A-Z0-9]*\d)[A-Z0-9]{8,})(?![\w])/;
|
|
956
|
-
function readBareSlackUserMention(line, start) {
|
|
957
|
-
if (line[start] !== "@" || /\w/.test(line[start - 1] ?? "")) return;
|
|
958
|
-
const match = BARE_SLACK_USER_ID_RE.exec(line.slice(start));
|
|
959
|
-
if (!match) return;
|
|
960
|
-
return {
|
|
961
|
-
text: `<@${match[1]}>`,
|
|
962
|
-
end: start + match[0].length
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Wrap bare http(s) URLs and bare `@U123` Slack user IDs on a single line as
|
|
967
|
-
* Slack explicit `<url>` links and `<@U123>` mentions.
|
|
968
|
-
*/
|
|
969
|
-
function wrapBareReferencesOnLine(line) {
|
|
970
|
-
let result = "";
|
|
971
|
-
let i = 0;
|
|
972
|
-
while (i < line.length) {
|
|
973
|
-
const codeSpan = readInlineCodeSpan(line, i);
|
|
974
|
-
if (codeSpan) {
|
|
975
|
-
result += codeSpan.text;
|
|
976
|
-
i = codeSpan.end;
|
|
977
|
-
continue;
|
|
978
|
-
}
|
|
979
|
-
const angleToken = readExistingSlackAngleToken(line, i);
|
|
980
|
-
if (angleToken) {
|
|
981
|
-
result += angleToken.text;
|
|
982
|
-
i = angleToken.end;
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
const mdLink = readMarkdownLink(line, i);
|
|
986
|
-
if (mdLink) {
|
|
987
|
-
result += mdLink.text;
|
|
988
|
-
i = mdLink.end;
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
const userMention = readBareSlackUserMention(line, i);
|
|
992
|
-
if (userMention) {
|
|
993
|
-
result += userMention.text;
|
|
994
|
-
i = userMention.end;
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
|
|
998
|
-
const parsed = readBareUrl(line, i);
|
|
999
|
-
if (parsed) {
|
|
1000
|
-
result += `<${parsed.url}>${parsed.suffix}`;
|
|
1001
|
-
i = parsed.end;
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
result += line[i];
|
|
1006
|
-
i++;
|
|
1007
|
-
}
|
|
1008
|
-
return result;
|
|
1009
|
-
}
|
|
1010
|
-
/**
|
|
1011
|
-
* Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
|
|
1012
|
-
* links, preventing Slack's auto-linker from consuming adjacent formatting
|
|
1013
|
-
* markers into the URL. Bare `@U123` user IDs become `<@U123>` mentions,
|
|
1014
|
-
* because Slack shows a bare ID as literal text.
|
|
1015
|
-
*
|
|
1016
|
-
* Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
|
|
1017
|
-
* agree on which lines are code.
|
|
1018
|
-
*/
|
|
1019
|
-
function wrapBareReferences(text) {
|
|
1020
|
-
const lines = text.split("\n");
|
|
1021
|
-
const out = [];
|
|
1022
|
-
let inCodeBlock = false;
|
|
1023
|
-
for (const line of lines) {
|
|
1024
|
-
if (line.trimStart().startsWith("```")) {
|
|
1025
|
-
inCodeBlock = !inCodeBlock;
|
|
1026
|
-
out.push(line);
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
out.push(inCodeBlock ? line : wrapBareReferencesOnLine(line));
|
|
1030
|
-
}
|
|
1031
|
-
return out.join("\n");
|
|
1032
|
-
}
|
|
1033
|
-
const LIST_ITEM_PATTERN = /^[-*•]\s|^\d+\.\s/;
|
|
1034
|
-
/**
|
|
1035
|
-
* Turn a follow-up paragraph inside a list item into a bullet at the same
|
|
1036
|
-
* indent.
|
|
1037
|
-
*
|
|
1038
|
-
* Slack's `markdown` block keeps only one paragraph per list item. It joins
|
|
1039
|
-
* the other paragraphs to the top-level item's text with no space and renders
|
|
1040
|
-
* nested lists after them. As a bullet, the paragraph keeps its place and
|
|
1041
|
-
* nesting.
|
|
1042
|
-
*/
|
|
1043
|
-
function bulletListFollowUpParagraphs(text) {
|
|
1044
|
-
const lines = text.split("\n");
|
|
1045
|
-
let inCodeBlock = false;
|
|
1046
|
-
let inList = false;
|
|
1047
|
-
for (const [i, line] of lines.entries()) {
|
|
1048
|
-
const trimmed = line.trimStart();
|
|
1049
|
-
const isFence = trimmed.startsWith("```");
|
|
1050
|
-
if (!inCodeBlock && trimmed && trimmed.length === line.length) inList = LIST_ITEM_PATTERN.test(line);
|
|
1051
|
-
else if (!inCodeBlock && inList && trimmed && !isFence && lines[i - 1]?.trim() === "" && !LIST_ITEM_PATTERN.test(trimmed)) lines[i] = `${line.slice(0, line.length - trimmed.length)}- ${trimmed}`;
|
|
1052
|
-
if (isFence) inCodeBlock = !inCodeBlock;
|
|
1053
|
-
}
|
|
1054
|
-
return lines.join("\n");
|
|
1055
|
-
}
|
|
1056
|
-
/** Insert blank lines between content blocks so Slack renders them with visual separation. */
|
|
1057
|
-
function ensureBlockSpacing(text) {
|
|
1058
|
-
const codeBlockPattern = /^```/;
|
|
1059
|
-
const lines = text.split("\n");
|
|
1060
|
-
const result = [];
|
|
1061
|
-
let inCodeBlock = false;
|
|
1062
|
-
for (const line of lines) {
|
|
1063
|
-
if (codeBlockPattern.test(line.trimStart())) {
|
|
1064
|
-
if (!inCodeBlock) {
|
|
1065
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
1066
|
-
if (prev !== void 0 && prev.trim() !== "") result.push("");
|
|
1067
|
-
}
|
|
1068
|
-
inCodeBlock = !inCodeBlock;
|
|
1069
|
-
result.push(line);
|
|
1070
|
-
continue;
|
|
1071
|
-
}
|
|
1072
|
-
if (inCodeBlock) {
|
|
1073
|
-
result.push(line);
|
|
1074
|
-
continue;
|
|
1075
|
-
}
|
|
1076
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
1077
|
-
const isListItem = LIST_ITEM_PATTERN.test(line.trimStart());
|
|
1078
|
-
const isListContinuation = prev !== void 0 && !isListItem && /^\s/.test(line) && (/^\s/.test(prev) || LIST_ITEM_PATTERN.test(prev));
|
|
1079
|
-
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !isListContinuation && !(LIST_ITEM_PATTERN.test(prev.trimStart()) && isListItem)) result.push("");
|
|
1080
|
-
result.push(line);
|
|
1081
|
-
}
|
|
1082
|
-
return result.join("\n");
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Normalize model-authored Slack markdown for delivery via `markdown_text`
|
|
1086
|
-
* or `{ type: "markdown" }` blocks.
|
|
1087
|
-
*
|
|
1088
|
-
* Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
|
|
1089
|
-
* delivery shaping: bare URL wrapping, block spacing, and list follow-up
|
|
1090
|
-
* paragraph bullets. Slack reply delivery owns chunking and continuation
|
|
1091
|
-
* markers separately.
|
|
1092
|
-
*/
|
|
1093
|
-
function normalizeSlackReplyMarkdown(text) {
|
|
1094
|
-
let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
1095
|
-
normalized = wrapBareReferences(normalized);
|
|
1096
|
-
normalized = ensureBlockSpacing(normalized);
|
|
1097
|
-
normalized = bulletListFollowUpParagraphs(normalized);
|
|
1098
|
-
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
1099
|
-
}
|
|
1100
|
-
/** Normalize assistant status text before handing it to Slack. */
|
|
1101
|
-
function normalizeSlackStatusText(text) {
|
|
1102
|
-
const trimmed = text.trim();
|
|
1103
|
-
if (!trimmed) return "";
|
|
1104
|
-
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
1105
|
-
}
|
|
1106
|
-
/** Render a Slack user mention, which Slack shows as the person's name. */
|
|
1107
|
-
function slackMention(userId) {
|
|
1108
|
-
return `<@${userId}>`;
|
|
1109
|
-
}
|
|
1110
|
-
// src/chat/slack/oauth-authorization-message.ts
|
|
1111
|
-
const ACCOUNT_SCOPE_NOTE = "This connects only your account. Junior sees only what you can see.";
|
|
1112
|
-
/** Present private OAuth authorization with a clear action and text fallback. */
|
|
1113
|
-
function buildSlackOAuthAuthorizationMessage(args) {
|
|
1114
|
-
return {
|
|
1115
|
-
text: `${formatSlackLink(args.authorizationUrl, args.label)}. ${ACCOUNT_SCOPE_NOTE} ${args.completionText}`,
|
|
1116
|
-
blocks: [{
|
|
1117
|
-
type: "section",
|
|
1118
|
-
text: {
|
|
1119
|
-
type: "mrkdwn",
|
|
1120
|
-
text: `*${escapeSlackMrkdwnText(args.label)}*\n${ACCOUNT_SCOPE_NOTE}\n${escapeSlackMrkdwnText(args.completionText)}`
|
|
1121
|
-
},
|
|
1122
|
-
accessory: {
|
|
1123
|
-
type: "button",
|
|
1124
|
-
text: {
|
|
1125
|
-
type: "plain_text",
|
|
1126
|
-
text: "Connect"
|
|
1127
|
-
},
|
|
1128
|
-
style: "primary",
|
|
1129
|
-
url: args.authorizationUrl,
|
|
1130
|
-
action_id: "oauth_connect"
|
|
1131
|
-
}
|
|
1132
|
-
}]
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
// src/chat/oauth-flow.ts
|
|
1136
|
-
const OAUTH_STATE_TTL_MS = 6e5;
|
|
1137
|
-
function optionalString(value) {
|
|
1138
|
-
return typeof value === "string" ? value : void 0;
|
|
1139
|
-
}
|
|
1140
|
-
/** Parse OAuth callback state that was persisted before a provider redirect. */
|
|
1141
|
-
function parseOAuthStatePayload(value) {
|
|
1142
|
-
if (!isRecord(value)) return;
|
|
1143
|
-
if (typeof value.userId !== "string" || typeof value.provider !== "string") return;
|
|
1144
|
-
const actor = value.actor === void 0 ? void 0 : actorSchema.safeParse(value.actor);
|
|
1145
|
-
if (value.actor !== void 0 && (!actor || !actor.success)) return;
|
|
1146
|
-
const destination = parseDestination(value.destination);
|
|
1147
|
-
if (value.destination !== void 0 && !destination) return;
|
|
1148
|
-
const source = value.source === void 0 ? void 0 : sourceSchema.safeParse(value.source);
|
|
1149
|
-
if (value.source !== void 0 && (!source || !source.success)) return;
|
|
1150
|
-
return {
|
|
1151
|
-
userId: value.userId,
|
|
1152
|
-
provider: value.provider,
|
|
1153
|
-
...actor?.success ? { actor: actor.data } : void 0,
|
|
1154
|
-
...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : void 0,
|
|
1155
|
-
...destination ? { destination } : void 0,
|
|
1156
|
-
...source?.success ? { source: source.data } : void 0,
|
|
1157
|
-
...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : void 0,
|
|
1158
|
-
...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : void 0,
|
|
1159
|
-
...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : void 0,
|
|
1160
|
-
...optionalString(value.scope) ? { scope: optionalString(value.scope) } : void 0
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
/** Return the manifest-owned display label for a provider. */
|
|
1164
|
-
function formatProviderLabel(provider) {
|
|
1165
|
-
const displayName = pluginCatalogRuntime.getDisplayName(provider);
|
|
1166
|
-
if (!displayName) throw new Error(`Unknown plugin provider display name: "${provider}"`);
|
|
1167
|
-
return displayName;
|
|
1168
|
-
}
|
|
1169
|
-
/** Resolve the public base URL from environment variables (JUNIOR_BASE_URL or Vercel). */
|
|
1170
|
-
function resolveBaseUrl() {
|
|
1171
|
-
const explicit = process.env.JUNIOR_BASE_URL?.trim();
|
|
1172
|
-
if (explicit) return explicit;
|
|
1173
|
-
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
1174
|
-
if (vercelProd) return `https://${vercelProd}`;
|
|
1175
|
-
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
1176
|
-
if (vercelUrl) return `https://${vercelUrl}`;
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Authorization links must only be visible to the requesting user.
|
|
1180
|
-
* Try in-context private delivery first, then fall back to a DM.
|
|
1181
|
-
*/
|
|
1182
|
-
async function deliverPrivateMessage(input) {
|
|
1183
|
-
let client;
|
|
1184
|
-
try {
|
|
1185
|
-
client = getSlackClient();
|
|
1186
|
-
} catch {
|
|
1187
|
-
logWarn("oauth.private_delivery.skipped", { "app.reason": "missing_bot_token" });
|
|
1188
|
-
return false;
|
|
1189
|
-
}
|
|
1190
|
-
if (input.channelId) try {
|
|
1191
|
-
if (isDmChannel(input.channelId)) await postSlackMessage({
|
|
1192
|
-
channelId: input.channelId,
|
|
1193
|
-
text: input.text,
|
|
1194
|
-
blocks: input.blocks,
|
|
1195
|
-
threadTs: input.threadTs
|
|
1196
|
-
});
|
|
1197
|
-
else await postSlackEphemeralMessage({
|
|
1198
|
-
channelId: input.channelId,
|
|
1199
|
-
userId: input.userId,
|
|
1200
|
-
text: input.text,
|
|
1201
|
-
blocks: input.blocks,
|
|
1202
|
-
threadTs: input.threadTs
|
|
1203
|
-
});
|
|
1204
|
-
return "in_context";
|
|
1205
|
-
} catch (error) {
|
|
1206
|
-
logWarn("oauth.private_delivery.failed", {
|
|
1207
|
-
"app.slack.error": error instanceof Error ? error.message : String(error),
|
|
1208
|
-
"app.slack.channel": input.channelId
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
try {
|
|
1212
|
-
const dmChannelId = (await withSlackRetries(() => client.conversations.open({ users: input.userId }), 3, {
|
|
1213
|
-
action: "conversations.open",
|
|
1214
|
-
spanAttributes: { "app.slack.user_id": input.userId }
|
|
1215
|
-
})).channel?.id;
|
|
1216
|
-
if (!dmChannelId) {
|
|
1217
|
-
logWarn("oauth.dm.fallback.failed", { "app.reason": "no_dm_channel_id" });
|
|
1218
|
-
return false;
|
|
1219
|
-
}
|
|
1220
|
-
await postSlackMessage({
|
|
1221
|
-
channelId: dmChannelId,
|
|
1222
|
-
text: input.text,
|
|
1223
|
-
blocks: input.blocks
|
|
1224
|
-
});
|
|
1225
|
-
return "fallback_dm";
|
|
1226
|
-
} catch (error) {
|
|
1227
|
-
logWarn("oauth.dm.fallback.failed", { "app.slack.error": error instanceof Error ? error.message : String(error) });
|
|
1228
|
-
return false;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
/** Present one OAuth request through the active surface's private delivery. */
|
|
1232
|
-
async function deliverOAuthAuthorization(request, input) {
|
|
1233
|
-
if (input.authorization) {
|
|
1234
|
-
await input.authorization.deliver(request);
|
|
1235
|
-
return "in_context";
|
|
1236
|
-
}
|
|
1237
|
-
return await deliverPrivateMessage({
|
|
1238
|
-
channelId: input.channelId,
|
|
1239
|
-
threadTs: input.threadTs,
|
|
1240
|
-
userId: input.userId,
|
|
1241
|
-
...buildSlackOAuthAuthorizationMessage(request)
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
/** Initiate an OAuth authorization code flow for a provider and deliver the auth link to the user. */
|
|
1245
|
-
async function startOAuthFlow(provider, input) {
|
|
1246
|
-
const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
|
|
1247
|
-
if (!providerConfig) return {
|
|
1248
|
-
ok: false,
|
|
1249
|
-
error: `Provider "${provider}" does not support OAuth authorization`
|
|
1250
|
-
};
|
|
1251
|
-
const clientId = process.env[providerConfig.clientIdEnv]?.trim();
|
|
1252
|
-
if (!clientId) return {
|
|
1253
|
-
ok: false,
|
|
1254
|
-
error: `Missing ${providerConfig.clientIdEnv} environment variable`
|
|
1255
|
-
};
|
|
1256
|
-
const baseUrl = resolveBaseUrl();
|
|
1257
|
-
if (!baseUrl) return {
|
|
1258
|
-
ok: false,
|
|
1259
|
-
error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
|
|
1260
|
-
};
|
|
1261
|
-
const state = input.authorization ? await input.authorization.createState() : randomBytes(32).toString("hex");
|
|
1262
|
-
const requestedScope = input.scope ?? providerConfig.scope;
|
|
1263
|
-
await getStateAdapter().set(`oauth-state:${state}`, {
|
|
1264
|
-
userId: input.actorId,
|
|
1265
|
-
provider,
|
|
1266
|
-
...input.actor ? { actor: input.actor } : void 0,
|
|
1267
|
-
...input.channelId ? { channelId: input.channelId } : void 0,
|
|
1268
|
-
...input.destination ? { destination: input.destination } : void 0,
|
|
1269
|
-
...input.source ? { source: input.source } : void 0,
|
|
1270
|
-
...input.threadTs ? { threadTs: input.threadTs } : void 0,
|
|
1271
|
-
...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : void 0,
|
|
1272
|
-
...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : void 0,
|
|
1273
|
-
...requestedScope ? { scope: requestedScope } : void 0
|
|
1274
|
-
}, OAUTH_STATE_TTL_MS);
|
|
1275
|
-
const authorizeParams = new URLSearchParams({
|
|
1276
|
-
client_id: clientId,
|
|
1277
|
-
state,
|
|
1278
|
-
redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
|
|
1279
|
-
response_type: "code"
|
|
1280
|
-
});
|
|
1281
|
-
if (requestedScope) authorizeParams.set("scope", requestedScope);
|
|
1282
|
-
for (const [key, value] of Object.entries(providerConfig.authorizeParams ?? {})) authorizeParams.set(key, value);
|
|
1283
|
-
logInfo("jr_rpc.oauth.started", {
|
|
1284
|
-
"app.credential.provider": provider,
|
|
1285
|
-
...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : void 0
|
|
1286
|
-
});
|
|
1287
|
-
return {
|
|
1288
|
-
ok: true,
|
|
1289
|
-
delivery: await deliverOAuthAuthorization({
|
|
1290
|
-
authorizationUrl: `${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}`,
|
|
1291
|
-
label: `Connect your ${formatProviderLabel(provider)} account`,
|
|
1292
|
-
completionText: input.resumeSessionId ? "Once you've authorized, Junior will continue automatically." : "Once you've authorized, you'll see a confirmation in Slack."
|
|
1293
|
-
}, {
|
|
1294
|
-
authorization: input.authorization,
|
|
1295
|
-
channelId: input.channelId,
|
|
1296
|
-
threadTs: input.threadTs,
|
|
1297
|
-
userId: input.actorId
|
|
1298
|
-
})
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
// src/chat/sandbox/egress/schemas.ts
|
|
1302
|
-
const finiteNumberSchema = z.number().refine(Number.isFinite);
|
|
1303
|
-
const httpStatusSchema = z.number().int().min(100).max(599);
|
|
1304
|
-
const providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
1305
|
-
const credentialSignalKindSchema = z.enum(["auth_required", "unavailable"]);
|
|
1306
|
-
const sandboxEgressGrantSchema = pluginGrantSchema;
|
|
1307
|
-
const sandboxEgressCredentialContextSchema = z.object({
|
|
1308
|
-
credentials: credentialContextSchema,
|
|
1309
|
-
/** Conversation that the credentials act for. Telemetry only; it grants nothing. */
|
|
1310
|
-
conversationId: z.string().min(1).optional(),
|
|
1311
|
-
egressId: z.string().min(1),
|
|
1312
|
-
expiresAtMs: finiteNumberSchema,
|
|
1313
|
-
contextId: z.string().min(1)
|
|
1314
|
-
}).strict();
|
|
1315
|
-
const sandboxEgressCredentialLeaseSchema = z.object({
|
|
1316
|
-
account: pluginProviderAccountSchema.optional(),
|
|
1317
|
-
authorization: pluginAuthorizationSchema.optional(),
|
|
1318
|
-
grant: sandboxEgressGrantSchema,
|
|
1319
|
-
provider: providerNameSchema,
|
|
1320
|
-
expiresAt: z.string().min(1),
|
|
1321
|
-
headerTransforms: z.array(pluginCredentialHeaderTransformSchema).min(1)
|
|
1322
|
-
}).strict();
|
|
1323
|
-
const sandboxEgressAuthRequiredSignalSchema = z.object({
|
|
1324
|
-
authorization: pluginAuthorizationSchema.optional(),
|
|
1325
|
-
grant: sandboxEgressGrantSchema,
|
|
1326
|
-
kind: credentialSignalKindSchema.default("auth_required"),
|
|
1327
|
-
provider: providerNameSchema,
|
|
1328
|
-
message: z.string().optional(),
|
|
1329
|
-
createdAtMs: finiteNumberSchema
|
|
1330
|
-
}).strict().superRefine((signal, ctx) => {
|
|
1331
|
-
if (signal.authorization && signal.authorization.provider !== signal.provider) ctx.addIssue({
|
|
1332
|
-
code: z.ZodIssueCode.custom,
|
|
1333
|
-
message: "Auth signal authorization provider must match provider",
|
|
1334
|
-
path: ["authorization", "provider"]
|
|
1335
|
-
});
|
|
1336
|
-
});
|
|
1337
|
-
const sandboxEgressPermissionDeniedSignalSchema = z.object({
|
|
1338
|
-
account: pluginProviderAccountSchema.optional(),
|
|
1339
|
-
grant: sandboxEgressGrantSchema,
|
|
1340
|
-
message: z.string().min(1),
|
|
1341
|
-
provider: providerNameSchema,
|
|
1342
|
-
source: z.literal("upstream"),
|
|
1343
|
-
status: httpStatusSchema,
|
|
1344
|
-
upstreamHost: z.string().min(1),
|
|
1345
|
-
upstreamPath: z.string().min(1),
|
|
1346
|
-
createdAtMs: finiteNumberSchema
|
|
1347
|
-
}).strict();
|
|
1348
|
-
const sandboxEgressSignalsResponseSchema = z.object({
|
|
1349
|
-
auth_required: sandboxEgressAuthRequiredSignalSchema.optional(),
|
|
1350
|
-
permission_denied: sandboxEgressPermissionDeniedSignalSchema.optional()
|
|
1351
|
-
}).strict();
|
|
1352
|
-
/** Parse a host-owned sandbox egress auth signal from state or tool results. */
|
|
1353
|
-
function parseSandboxEgressAuthRequiredSignal(value) {
|
|
1354
|
-
const result = sandboxEgressAuthRequiredSignalSchema.safeParse(value);
|
|
1355
|
-
return result.success ? result.data : void 0;
|
|
1356
|
-
}
|
|
1357
|
-
/** Parse a host-owned sandbox egress permission-denied signal from state or tool results. */
|
|
1358
|
-
function parseSandboxEgressPermissionDeniedSignal(value) {
|
|
1359
|
-
const result = sandboxEgressPermissionDeniedSignalSchema.safeParse(value);
|
|
1360
|
-
return result.success ? result.data : void 0;
|
|
1361
|
-
}
|
|
1362
|
-
// src/chat/sandbox/egress/session.ts
|
|
1363
|
-
const SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
|
|
1364
|
-
const SANDBOX_EGRESS_TOKEN_VERSION = "v1";
|
|
1365
|
-
const SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
|
|
1366
|
-
const SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX = "sandbox-egress-auth-required";
|
|
1367
|
-
const SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX = "sandbox-egress-permission-denied";
|
|
1368
|
-
const SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
|
|
1369
|
-
const DEFAULT_SESSION_TTL_MS = 18e5;
|
|
1370
|
-
/** Treat a cached lease as expired this long before its provider expiry. */
|
|
1371
|
-
const LEASE_REFRESH_BUFFER_MS = 3e5;
|
|
1372
|
-
/** True when this grant may keep shared auth headers on the host. */
|
|
1373
|
-
function isSharedInstallationGrant(grant) {
|
|
1374
|
-
return grant.name.startsWith("installation-");
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Build the host key for a shared installation grant.
|
|
1378
|
-
*
|
|
1379
|
-
* Only installation grants are remembered. User and other grants issue live
|
|
1380
|
-
* headers each time, so they never write a host key.
|
|
1381
|
-
*/
|
|
1382
|
-
function leaseKey(provider, grant) {
|
|
1383
|
-
return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${grant.name}:shared`;
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Build the command signal key for credential issuance failures by access level.
|
|
1387
|
-
*/
|
|
1388
|
-
function authSignalKey(egressId, access) {
|
|
1389
|
-
return `${SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX}:${egressId}:${access}`;
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Build the command signal key for upstream permission denials by access level.
|
|
1393
|
-
*/
|
|
1394
|
-
function permissionSignalKey(egressId, access) {
|
|
1395
|
-
return `${SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX}:${egressId}:${access}`;
|
|
1396
|
-
}
|
|
1397
|
-
function base64Url(input) {
|
|
1398
|
-
return Buffer.from(input, "utf8").toString("base64url");
|
|
1399
|
-
}
|
|
1400
|
-
function fromBase64Url(input) {
|
|
1401
|
-
return Buffer.from(input, "base64url").toString("utf8");
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Sign a versioned context payload in the sandbox egress HMAC domain.
|
|
1405
|
-
*
|
|
1406
|
-
* `JUNIOR_SECRET` is the shared secret so proxy URLs cannot be forged by code
|
|
1407
|
-
* running inside the sandbox.
|
|
1408
|
-
*/
|
|
1409
|
-
function signPayload(payload) {
|
|
1410
|
-
return createHmac("sha256", getSandboxEgressSecret()).update(`${SANDBOX_EGRESS_HMAC_CONTEXT}:${payload}`).digest("base64url");
|
|
1411
|
-
}
|
|
1412
|
-
function timingSafeMatch(expected, actual) {
|
|
1413
|
-
const expectedBuffer = Buffer.from(expected);
|
|
1414
|
-
const actualBuffer = Buffer.from(actual);
|
|
1415
|
-
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
1416
|
-
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
1417
|
-
}
|
|
1418
|
-
function parseSandboxEgressContext(value) {
|
|
1419
|
-
const result = sandboxEgressCredentialContextSchema.safeParse(value);
|
|
1420
|
-
if (!result.success) return;
|
|
1421
|
-
if (result.data.expiresAtMs <= Date.now()) return;
|
|
1422
|
-
return result.data;
|
|
1423
|
-
}
|
|
1424
|
-
function parseLease(value) {
|
|
1425
|
-
const result = sandboxEgressCredentialLeaseSchema.safeParse(value);
|
|
1426
|
-
if (!result.success) return;
|
|
1427
|
-
const expiresAtMs = Date.parse(result.data.expiresAt);
|
|
1428
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now() + LEASE_REFRESH_BUFFER_MS) return;
|
|
1429
|
-
return result.data;
|
|
1430
|
-
}
|
|
1431
|
-
function getSandboxEgressSecret() {
|
|
1432
|
-
const secret = process.env.JUNIOR_SECRET?.trim();
|
|
1433
|
-
if (secret) return secret;
|
|
1434
|
-
throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Create the signed Junior credential context embedded in the proxy URL.
|
|
1438
|
-
*
|
|
1439
|
-
* The token binds actor credentials to a specific sandbox egress id and a
|
|
1440
|
-
* short expiry. Vercel OIDC proves which VM sent a request; this token proves
|
|
1441
|
-
* which Junior actor and credential context that VM is allowed to use.
|
|
1442
|
-
*/
|
|
1443
|
-
function createSandboxEgressCredentialToken(input) {
|
|
1444
|
-
const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
|
|
1445
|
-
const now = Date.now();
|
|
1446
|
-
const context = {
|
|
1447
|
-
...input.conversationId ? { conversationId: input.conversationId } : void 0,
|
|
1448
|
-
credentials: input.credentials,
|
|
1449
|
-
egressId: input.egressId,
|
|
1450
|
-
expiresAtMs: now + ttlMs,
|
|
1451
|
-
contextId: randomUUID()
|
|
1452
|
-
};
|
|
1453
|
-
const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(JSON.stringify(context))}`;
|
|
1454
|
-
return `${payload}.${signPayload(payload)}`;
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Verify the signed Junior credential context from the proxy URL.
|
|
1458
|
-
*
|
|
1459
|
-
* Returning `undefined` means the token is missing, expired, malformed, or not
|
|
1460
|
-
* signed with the current `JUNIOR_SECRET`.
|
|
1461
|
-
*/
|
|
1462
|
-
function parseSandboxEgressCredentialToken(token) {
|
|
1463
|
-
if (!token) return;
|
|
1464
|
-
const parts = token.split(".");
|
|
1465
|
-
if (parts.length !== 3 || parts[0] !== SANDBOX_EGRESS_TOKEN_VERSION) return;
|
|
1466
|
-
const encodedSession = parts[1];
|
|
1467
|
-
const signature = parts[2];
|
|
1468
|
-
if (!encodedSession || !signature) return;
|
|
1469
|
-
if (!timingSafeMatch(signPayload(`${parts[0]}.${encodedSession}`), signature)) return;
|
|
1470
|
-
try {
|
|
1471
|
-
return parseSandboxEgressContext(JSON.parse(fromBase64Url(encodedSession)));
|
|
1472
|
-
} catch {
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Remember auth headers for a shared installation grant.
|
|
1478
|
-
*
|
|
1479
|
-
* User and other grants are ignored here. TTL follows the provider expiry.
|
|
1480
|
-
* Sandbox context expiry still decides whether a request may use the headers;
|
|
1481
|
-
* it does not shorten a shared installation grant for other sandboxes.
|
|
1482
|
-
*/
|
|
1483
|
-
async function setSandboxEgressCredentialLease(_context, lease) {
|
|
1484
|
-
if (!isSharedInstallationGrant(lease.grant)) return;
|
|
1485
|
-
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
1486
|
-
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) return;
|
|
1487
|
-
const ttlMs = Math.max(1, leaseExpiresAtMs - Date.now());
|
|
1488
|
-
const state = getStateAdapter();
|
|
1489
|
-
await state.connect();
|
|
1490
|
-
await state.set(leaseKey(lease.provider, lease.grant), lease, ttlMs);
|
|
1491
|
-
}
|
|
1492
|
-
/** Load remembered auth headers for a shared installation grant. */
|
|
1493
|
-
async function getSandboxEgressCredentialLease(provider, grant, _context) {
|
|
1494
|
-
if (!isSharedInstallationGrant(grant)) return;
|
|
1495
|
-
const state = getStateAdapter();
|
|
1496
|
-
await state.connect();
|
|
1497
|
-
return parseLease(await state.get(leaseKey(provider, grant)));
|
|
1498
|
-
}
|
|
1499
|
-
/** Drop remembered installation headers after the upstream rejects them. */
|
|
1500
|
-
async function clearSandboxEgressCredentialLease(provider, grant, _context) {
|
|
1501
|
-
if (!isSharedInstallationGrant(grant)) return;
|
|
1502
|
-
const state = getStateAdapter();
|
|
1503
|
-
await state.connect();
|
|
1504
|
-
await state.delete(leaseKey(provider, grant));
|
|
1505
|
-
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Record that credential issuance needs user authorization for this command.
|
|
1508
|
-
*
|
|
1509
|
-
* The command runner consumes this after the sandbox tool finishes so the agent
|
|
1510
|
-
* can present a normal authorization-required response instead of a raw proxy
|
|
1511
|
-
* failure.
|
|
1512
|
-
*/
|
|
1513
|
-
async function setSandboxEgressAuthRequiredSignal(context, signal) {
|
|
1514
|
-
const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
|
|
1515
|
-
const state = getStateAdapter();
|
|
1516
|
-
await state.connect();
|
|
1517
|
-
await state.set(authSignalKey(context.egressId, signal.grant.access), {
|
|
1518
|
-
...signal,
|
|
1519
|
-
createdAtMs: Date.now()
|
|
1520
|
-
}, ttlMs);
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Record that the provider rejected an issued credential with permission denial.
|
|
1524
|
-
*/
|
|
1525
|
-
async function setSandboxEgressPermissionDeniedSignal(context, signal) {
|
|
1526
|
-
const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
|
|
1527
|
-
const state = getStateAdapter();
|
|
1528
|
-
await state.connect();
|
|
1529
|
-
await state.set(permissionSignalKey(context.egressId, signal.grant.access), {
|
|
1530
|
-
...signal,
|
|
1531
|
-
createdAtMs: Date.now()
|
|
1532
|
-
}, ttlMs);
|
|
1533
|
-
}
|
|
1534
|
-
/** Remove pending auth/permission signals before or after a sandbox command. */
|
|
1535
|
-
async function clearSandboxEgressSignals(egressId) {
|
|
1536
|
-
if (!egressId) return;
|
|
1537
|
-
const state = getStateAdapter();
|
|
1538
|
-
await state.connect();
|
|
1539
|
-
await Promise.all([
|
|
1540
|
-
state.delete(authSignalKey(egressId, "read")),
|
|
1541
|
-
state.delete(authSignalKey(egressId, "write")),
|
|
1542
|
-
state.delete(permissionSignalKey(egressId, "read")),
|
|
1543
|
-
state.delete(permissionSignalKey(egressId, "write"))
|
|
1544
|
-
]);
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Consume the auth signal produced during a sandbox command, preferring writes.
|
|
1548
|
-
*/
|
|
1549
|
-
async function consumeSandboxEgressAuthRequiredSignal(egressId) {
|
|
1550
|
-
if (!egressId) return;
|
|
1551
|
-
const state = getStateAdapter();
|
|
1552
|
-
await state.connect();
|
|
1553
|
-
const [writeSignal, readSignal] = await Promise.all([state.get(authSignalKey(egressId, "write")), state.get(authSignalKey(egressId, "read"))]);
|
|
1554
|
-
const signal = parseSandboxEgressAuthRequiredSignal(writeSignal) ?? parseSandboxEgressAuthRequiredSignal(readSignal);
|
|
1555
|
-
await Promise.all([state.delete(authSignalKey(egressId, "read")), state.delete(authSignalKey(egressId, "write"))]);
|
|
1556
|
-
return signal;
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Consume the permission signal produced during a sandbox command, preferring writes.
|
|
1560
|
-
*/
|
|
1561
|
-
async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
|
|
1562
|
-
if (!egressId) return;
|
|
1563
|
-
const state = getStateAdapter();
|
|
1564
|
-
await state.connect();
|
|
1565
|
-
const [writeSignal, readSignal] = await Promise.all([state.get(permissionSignalKey(egressId, "write")), state.get(permissionSignalKey(egressId, "read"))]);
|
|
1566
|
-
const signal = parseSandboxEgressPermissionDeniedSignal(writeSignal) ?? parseSandboxEgressPermissionDeniedSignal(readSignal);
|
|
1567
|
-
await Promise.all([state.delete(permissionSignalKey(egressId, "read")), state.delete(permissionSignalKey(egressId, "write"))]);
|
|
1568
|
-
return signal;
|
|
1569
|
-
}
|
|
1570
|
-
// src/chat/sandbox/egress/tracing.ts
|
|
1571
|
-
function isValidDomainPattern(domain) {
|
|
1572
|
-
if (domain.includes("*")) return domain.startsWith("*.") && domain.indexOf("*", 1) === -1;
|
|
1573
|
-
return true;
|
|
1574
|
-
}
|
|
1575
|
-
/** Normalize exact and leading-wildcard sandbox egress trace domains. */
|
|
1576
|
-
function normalizeSandboxEgressTracePropagationDomains(domains) {
|
|
1577
|
-
if (domains === void 0) return [];
|
|
1578
|
-
if (!Array.isArray(domains)) throw new Error("sandbox.egressTracePropagationDomains must be an array");
|
|
1579
|
-
return [...new Set(domains.map((domain) => {
|
|
1580
|
-
if (typeof domain !== "string") throw new Error("sandbox.egressTracePropagationDomains entries must be strings");
|
|
1581
|
-
const normalized = domain.trim().toLowerCase();
|
|
1582
|
-
if (!normalized) throw new Error("sandbox.egressTracePropagationDomains entries must be non-empty");
|
|
1583
|
-
if (!isValidDomainPattern(normalized)) throw new Error("sandbox.egressTracePropagationDomains entries must be exact domains or leading wildcard domains");
|
|
1584
|
-
return normalized;
|
|
1585
|
-
}))].sort((left, right) => left.localeCompare(right));
|
|
1586
|
-
}
|
|
1587
|
-
/** Return whether a host may carry sandbox egress trace propagation headers. */
|
|
1588
|
-
function shouldPropagateSandboxEgressTrace(host, config = {}) {
|
|
1589
|
-
const normalizedHost = host.trim().toLowerCase();
|
|
1590
|
-
return (config.domains ?? []).some((domain) => {
|
|
1591
|
-
if (domain.startsWith("*.")) {
|
|
1592
|
-
const suffix = domain.slice(1);
|
|
1593
|
-
return normalizedHost.endsWith(suffix) && normalizedHost !== domain.slice(2);
|
|
1594
|
-
}
|
|
1595
|
-
return domain === normalizedHost;
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
// src/chat/sandbox/egress/policy.ts
|
|
1599
|
-
/** Return whether an outbound host is covered by a provider domain rule. */
|
|
1600
|
-
function matchesSandboxEgressDomain(host, domain) {
|
|
1601
|
-
return host.toLowerCase() === domain.toLowerCase();
|
|
1602
|
-
}
|
|
1603
|
-
function manifestDomains(manifest) {
|
|
1604
|
-
return [.../* @__PURE__ */ new Set([...manifest.credentials?.domains ?? [], ...manifest.domains ?? []])].sort((left, right) => left.localeCompare(right));
|
|
1605
|
-
}
|
|
1606
|
-
function providerEntries() {
|
|
1607
|
-
return pluginCatalogRuntime.getProviders().map((plugin) => ({
|
|
1608
|
-
provider: plugin.manifest.name,
|
|
1609
|
-
domains: manifestDomains(plugin.manifest)
|
|
1610
|
-
})).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Resolve the plugin provider responsible for an outbound sandbox host.
|
|
1614
|
-
*
|
|
1615
|
-
* Only plugin-declared provider domains are eligible for credentialed sandbox
|
|
1616
|
-
* egress. Hosts without a matching provider are blocked by the proxy even if a
|
|
1617
|
-
* malformed or stale network policy tried to forward them.
|
|
1618
|
-
*/
|
|
1619
|
-
function resolveSandboxEgressProviderForHost(host) {
|
|
1620
|
-
return providerEntries().find((entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain)))?.provider;
|
|
1621
|
-
}
|
|
1622
|
-
function sandboxProxyUrl(credentialToken) {
|
|
1623
|
-
const baseUrl = resolveBaseUrl();
|
|
1624
|
-
if (!baseUrl) throw new Error("Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)");
|
|
1625
|
-
const path = credentialToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${credentialToken}` : SANDBOX_EGRESS_PROXY_PATH;
|
|
1626
|
-
return new URL(path, baseUrl).toString();
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Build the policy that forwards provider domains through the egress proxy.
|
|
1630
|
-
*
|
|
1631
|
-
* The signed credential token is embedded in the forward URL so the proxy can
|
|
1632
|
-
* bind each forwarded request back to the actor and sandbox VM that created the
|
|
1633
|
-
* command. The sandbox receives placeholder env vars, not provider credentials.
|
|
1634
|
-
*/
|
|
1635
|
-
function buildSandboxEgressNetworkPolicy(input) {
|
|
1636
|
-
const allow = { "*": [] };
|
|
1637
|
-
const entries = providerEntries();
|
|
1638
|
-
const traceHeaders = Object.fromEntries(Object.entries(input?.traceHeaders ?? {}).filter(([, value]) => typeof value === "string" && value.trim()));
|
|
1639
|
-
const hasTraceHeaders = Object.keys(traceHeaders).length > 0;
|
|
1640
|
-
if (entries.length === 0 && (!hasTraceHeaders || (input?.traceConfig?.domains ?? []).length === 0)) return { allow };
|
|
1641
|
-
const forwardURL = input?.credentialToken ? sandboxProxyUrl(input.credentialToken) : void 0;
|
|
1642
|
-
const domains = /* @__PURE__ */ new Map();
|
|
1643
|
-
if (forwardURL) for (const entry of entries) for (const domain of entry.domains) domains.set(domain, { forward: true });
|
|
1644
|
-
if (hasTraceHeaders) for (const domain of input?.traceConfig?.domains ?? []) domains.set(domain, { forward: domains.get(domain)?.forward ?? false });
|
|
1645
|
-
for (const [domain, policy] of [...domains.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1646
|
-
const shouldPropagateTrace = shouldPropagateSandboxEgressTrace(domain, input?.traceConfig);
|
|
1647
|
-
if (policy.forward && forwardURL) allow[domain] = [{ forwardURL }];
|
|
1648
|
-
else if (shouldPropagateTrace && hasTraceHeaders) allow[domain] = [{ transform: [{ headers: traceHeaders }] }];
|
|
1649
|
-
}
|
|
1650
|
-
return { allow };
|
|
1651
|
-
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Resolve non-secret command environment values for registered sandbox providers.
|
|
1654
|
-
*
|
|
1655
|
-
* Credential env vars are placeholders that tell tools which auth variable name
|
|
1656
|
-
* exists; real credential headers are issued lazily by host egress.
|
|
1657
|
-
*/
|
|
1658
|
-
async function resolveSandboxCommandEnvironment() {
|
|
1659
|
-
const env = {};
|
|
1660
|
-
for (const plugin of pluginCatalogRuntime.getProviders().sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))) {
|
|
1661
|
-
Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
|
|
1662
|
-
const credentials = plugin.manifest.credentials;
|
|
1663
|
-
if (credentials?.authTokenEnv) env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
|
|
1664
|
-
}
|
|
1665
|
-
return env;
|
|
1666
|
-
}
|
|
1667
|
-
// src/chat/capabilities/router.ts
|
|
1668
|
-
var ProviderCredentialRouter = class {
|
|
1669
|
-
brokersByProvider;
|
|
1670
|
-
constructor(input) {
|
|
1671
|
-
this.brokersByProvider = input.brokersByProvider;
|
|
1672
|
-
}
|
|
1673
|
-
async issue(input) {
|
|
1674
|
-
const broker = this.brokersByProvider[input.provider];
|
|
1675
|
-
if (!broker) throw new Error(`No credential broker registered for provider: ${input.provider}`);
|
|
1676
|
-
return await broker.issue({
|
|
1677
|
-
context: input.context,
|
|
1678
|
-
reason: input.reason
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
};
|
|
1682
|
-
// src/chat/credentials/user-token-store.ts
|
|
1683
|
-
const storedTokensSchema = pluginStoredTokensSchema;
|
|
1684
|
-
// src/chat/credentials/state-adapter-token-store.ts
|
|
1685
|
-
const KEY_PREFIX = "oauth-token";
|
|
1686
|
-
const BUFFER_MS = 864e5;
|
|
1687
|
-
const LONG_LIVED_TTL_MS = 31536e6;
|
|
1688
|
-
const REFRESH_LOCK_WAIT_MS = 3e4;
|
|
1689
|
-
const REFRESH_LOCK_RETRY_MS = 100;
|
|
1690
|
-
function tokenKey(userId, provider) {
|
|
1691
|
-
return `${KEY_PREFIX}:${userId}:${provider}`;
|
|
1692
|
-
}
|
|
1693
|
-
function refreshLockKey(userId, provider) {
|
|
1694
|
-
return `${tokenKey(userId, provider)}:refresh`;
|
|
1695
|
-
}
|
|
1696
|
-
var StateAdapterTokenStore = class {
|
|
1697
|
-
state;
|
|
1698
|
-
constructor(stateAdapter) {
|
|
1699
|
-
this.state = stateAdapter;
|
|
1700
|
-
}
|
|
1701
|
-
async get(userId, provider) {
|
|
1702
|
-
const stored = await this.state.get(tokenKey(userId, provider));
|
|
1703
|
-
return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
|
|
1704
|
-
}
|
|
1705
|
-
async set(userId, provider, tokens) {
|
|
1706
|
-
const parsed = storedTokensSchema.parse(tokens);
|
|
1707
|
-
const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
|
|
1708
|
-
const ttlMs = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
|
|
1709
|
-
await this.state.set(tokenKey(userId, provider), parsed, ttlMs);
|
|
1710
|
-
}
|
|
1711
|
-
async delete(userId, provider) {
|
|
1712
|
-
await this.state.delete(tokenKey(userId, provider));
|
|
1713
|
-
}
|
|
1714
|
-
/** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
|
|
1715
|
-
async withRefresh(userId, provider, callback) {
|
|
1716
|
-
const lockKey = refreshLockKey(userId, provider);
|
|
1717
|
-
const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
|
|
1718
|
-
while (true) {
|
|
1719
|
-
const lock = await acquireActiveLock(this.state, lockKey);
|
|
1720
|
-
if (lock) try {
|
|
1721
|
-
return await callback();
|
|
1722
|
-
} finally {
|
|
1723
|
-
await this.state.releaseLock(lock);
|
|
1724
|
-
}
|
|
1725
|
-
if (Date.now() >= deadline) throw new Error(`Could not acquire OAuth token refresh lock`);
|
|
1726
|
-
await sleep(REFRESH_LOCK_RETRY_MS);
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
};
|
|
1730
|
-
// src/chat/capabilities/factory.ts
|
|
1731
|
-
const sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
|
|
1732
|
-
/** Create the user token store used by OAuth-backed credential brokers. */
|
|
1733
|
-
function createUserTokenStore() {
|
|
1734
|
-
return new StateAdapterTokenStore(getStateAdapter());
|
|
1735
|
-
}
|
|
1736
|
-
function createProviderCredentialRouter(userTokenStore) {
|
|
1737
|
-
const brokersByProvider = {};
|
|
1738
|
-
for (const plugin of pluginCatalogRuntime.getProviders()) {
|
|
1739
|
-
const { name } = plugin.manifest;
|
|
1740
|
-
if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) continue;
|
|
1741
|
-
brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, { userTokenStore });
|
|
1742
|
-
}
|
|
1743
|
-
return new ProviderCredentialRouter({ brokersByProvider });
|
|
1744
|
-
}
|
|
1745
|
-
function getSandboxEgressRouter() {
|
|
1746
|
-
const stateAdapter = getStateAdapter();
|
|
1747
|
-
let router = sandboxEgressRouters.get(stateAdapter);
|
|
1748
|
-
if (!router) {
|
|
1749
|
-
router = createProviderCredentialRouter(new StateAdapterTokenStore(stateAdapter));
|
|
1750
|
-
sandboxEgressRouters.set(stateAdapter, router);
|
|
1751
|
-
}
|
|
1752
|
-
return router;
|
|
1753
|
-
}
|
|
1754
|
-
/** Issue one provider credential lease for host-side sandbox egress proxying. */
|
|
1755
|
-
async function issueProviderCredentialLease(input) {
|
|
1756
|
-
return await getSandboxEgressRouter().issue(input);
|
|
1757
|
-
}
|
|
1758
|
-
// src/chat/sandbox/egress/credentials.ts
|
|
1759
|
-
const HTTP_READ_METHODS = /* @__PURE__ */ new Set([
|
|
1760
|
-
"GET",
|
|
1761
|
-
"HEAD",
|
|
1762
|
-
"OPTIONS"
|
|
1763
|
-
]);
|
|
1764
|
-
/**
|
|
1765
|
-
* Signals that egress selected a grant but could not issue credential headers.
|
|
1766
|
-
*
|
|
1767
|
-
* Callers convert this into command-level auth-required state so the agent can
|
|
1768
|
-
* request authorization without exposing provider-specific lease internals.
|
|
1769
|
-
*/
|
|
1770
|
-
var SandboxEgressCredentialError = class extends Error {
|
|
1771
|
-
authorization;
|
|
1772
|
-
grant;
|
|
1773
|
-
kind;
|
|
1774
|
-
provider;
|
|
1775
|
-
constructor(input) {
|
|
1776
|
-
super(input.message);
|
|
1777
|
-
this.name = "SandboxEgressCredentialError";
|
|
1778
|
-
this.authorization = input.authorization;
|
|
1779
|
-
this.grant = input.grant;
|
|
1780
|
-
this.kind = input.kind;
|
|
1781
|
-
this.provider = input.provider;
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
function defaultGrantForProvider(input) {
|
|
1785
|
-
const access = HTTP_READ_METHODS.has(input.method.toUpperCase()) ? "read" : "write";
|
|
1786
|
-
return {
|
|
1787
|
-
source: "broker",
|
|
1788
|
-
grant: {
|
|
1789
|
-
name: "default",
|
|
1790
|
-
access,
|
|
1791
|
-
reason: `sandbox-egress:${input.provider}:${access}`
|
|
1792
|
-
}
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
function hasGenericCredentials(provider) {
|
|
1796
|
-
const manifest = pluginCatalogRuntime.getDefinition(provider)?.manifest;
|
|
1797
|
-
return Boolean(manifest?.credentials || manifest?.apiHeaders);
|
|
1798
|
-
}
|
|
1799
|
-
function oauthAuthorizationForProvider(provider) {
|
|
1800
|
-
const oauth = pluginCatalogRuntime.getOAuthConfig(provider);
|
|
1801
|
-
return oauth ? {
|
|
1802
|
-
type: "oauth",
|
|
1803
|
-
provider,
|
|
1804
|
-
...oauth.scope ? { scope: oauth.scope } : void 0
|
|
1805
|
-
} : void 0;
|
|
1806
|
-
}
|
|
1807
|
-
function credentialSubjectFromContext(context) {
|
|
1808
|
-
return "subject" in context.credentials && context.credentials.subject ? {
|
|
1809
|
-
type: "user",
|
|
1810
|
-
userId: context.credentials.subject.userId
|
|
1811
|
-
} : void 0;
|
|
1812
|
-
}
|
|
1813
|
-
function assertLeaseTransformsOwnedByProvider(provider, lease) {
|
|
1814
|
-
for (const transform of lease.headerTransforms) if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) throw new Error(`Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`);
|
|
1815
|
-
}
|
|
1816
|
-
/**
|
|
1817
|
-
* Select the grant needed for one outbound request.
|
|
1818
|
-
*
|
|
1819
|
-
* GitHub GraphQL and other plugin-owned APIs may need body-aware grant choices;
|
|
1820
|
-
* providers without hooks use a simple read/write default based on HTTP method.
|
|
1821
|
-
* A hook plugin that also declares generic credentials can return no grant to
|
|
1822
|
-
* use that default for one request.
|
|
1823
|
-
*/
|
|
1824
|
-
async function selectSandboxEgressGrant(input) {
|
|
1825
|
-
if (!hasEgressCredentialHooks(input.provider)) return defaultGrantForProvider(input);
|
|
1826
|
-
const pluginGrant = await selectPluginGrant({
|
|
1827
|
-
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
|
|
1828
|
-
...input.operation ? { operation: input.operation } : void 0,
|
|
1829
|
-
provider: input.provider,
|
|
1830
|
-
method: input.method,
|
|
1831
|
-
upstreamUrl: input.upstreamUrl
|
|
1832
|
-
});
|
|
1833
|
-
if (!pluginGrant) {
|
|
1834
|
-
if (hasGenericCredentials(input.provider)) return defaultGrantForProvider(input);
|
|
1835
|
-
throw new Error(`Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`);
|
|
1836
|
-
}
|
|
1837
|
-
return {
|
|
1838
|
-
source: "plugin",
|
|
1839
|
-
grant: pluginGrant
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
/**
|
|
1843
|
-
* Resolve the authorization flow attached to a broker-selected egress grant.
|
|
1844
|
-
*
|
|
1845
|
-
* Plugin-selected grants can return their own authorization metadata when
|
|
1846
|
-
* issuing credentials; broker defaults use the provider OAuth config.
|
|
1847
|
-
*/
|
|
1848
|
-
function authorizationForSandboxEgressGrant(provider, selection) {
|
|
1849
|
-
return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
|
|
1850
|
-
}
|
|
1851
|
-
/**
|
|
1852
|
-
* Return credential header transforms for a selected grant.
|
|
1853
|
-
*
|
|
1854
|
-
* Only shared installation grants are remembered on the host. User and other
|
|
1855
|
-
* grants always issue from the live credential source so a saved login change
|
|
1856
|
-
* cannot leave stale auth headers in place. The sandbox context only authorizes
|
|
1857
|
-
* the request.
|
|
1858
|
-
*/
|
|
1859
|
-
async function sandboxEgressCredentialLease(provider, selection, context) {
|
|
1860
|
-
const { grant } = selection;
|
|
1861
|
-
const cached = await getSandboxEgressCredentialLease(provider, grant, context);
|
|
1862
|
-
if (cached) {
|
|
1863
|
-
if (selection.source === "plugin" && cached.grant.access !== grant.access) throw new Error(`Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`);
|
|
1864
|
-
return {
|
|
1865
|
-
...cached,
|
|
1866
|
-
grant
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
let lease;
|
|
1870
|
-
if (selection.source === "plugin") {
|
|
1871
|
-
const credentialSubject = credentialSubjectFromContext(context);
|
|
1872
|
-
const pluginResult = await issuePluginCredential({
|
|
1873
|
-
provider,
|
|
1874
|
-
grant,
|
|
1875
|
-
actor: context.credentials.actor,
|
|
1876
|
-
...credentialSubject ? { credentialSubject } : void 0,
|
|
1877
|
-
userTokenStore: createUserTokenStore()
|
|
1878
|
-
});
|
|
1879
|
-
if (pluginResult.type === "needed") throw new SandboxEgressCredentialError({
|
|
1880
|
-
provider,
|
|
1881
|
-
grant,
|
|
1882
|
-
kind: "auth_required",
|
|
1883
|
-
authorization: pluginResult.authorization,
|
|
1884
|
-
message: pluginResult.message
|
|
1885
|
-
});
|
|
1886
|
-
if (pluginResult.type === "unavailable") throw new SandboxEgressCredentialError({
|
|
1887
|
-
provider,
|
|
1888
|
-
grant,
|
|
1889
|
-
kind: "unavailable",
|
|
1890
|
-
message: pluginResult.message
|
|
1891
|
-
});
|
|
1892
|
-
lease = pluginResult.lease;
|
|
1893
|
-
} else try {
|
|
1894
|
-
lease = await issueProviderCredentialLease({
|
|
1895
|
-
context: context.credentials,
|
|
1896
|
-
provider,
|
|
1897
|
-
reason: grant.reason ?? `sandbox-egress:${provider}:default`
|
|
1898
|
-
});
|
|
1899
|
-
} catch (error) {
|
|
1900
|
-
if (error instanceof CredentialUnavailableError) throw new SandboxEgressCredentialError({
|
|
1901
|
-
provider,
|
|
1902
|
-
grant,
|
|
1903
|
-
kind: "auth_required",
|
|
1904
|
-
authorization: authorizationForSandboxEgressGrant(provider, selection),
|
|
1905
|
-
message: error.message
|
|
1906
|
-
});
|
|
1907
|
-
throw error;
|
|
1908
|
-
}
|
|
1909
|
-
const headerTransforms = lease.headerTransforms ?? [];
|
|
1910
|
-
if (headerTransforms.length === 0) throw new Error(`Credential lease for ${provider} did not include header transforms`);
|
|
1911
|
-
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
1912
|
-
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) throw new Error(`Credential lease for ${provider} is expired`);
|
|
1913
|
-
const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
|
|
1914
|
-
const issuedLease = {
|
|
1915
|
-
provider,
|
|
1916
|
-
grant,
|
|
1917
|
-
...lease.account ? { account: lease.account } : void 0,
|
|
1918
|
-
...authorization ? { authorization } : void 0,
|
|
1919
|
-
expiresAt: lease.expiresAt,
|
|
1920
|
-
headerTransforms
|
|
1921
|
-
};
|
|
1922
|
-
assertLeaseTransformsOwnedByProvider(provider, issuedLease);
|
|
1923
|
-
await setSandboxEgressCredentialLease(context, issuedLease);
|
|
1924
|
-
return issuedLease;
|
|
1925
|
-
}
|
|
1926
|
-
/** Return whether a credential lease can modify requests to the target host. */
|
|
1927
|
-
function hasSandboxEgressLeaseTransformForHost(lease, host) {
|
|
1928
|
-
return lease.headerTransforms.some((transform) => matchesSandboxEgressDomain(host, transform.domain));
|
|
1929
|
-
}
|
|
1930
|
-
// src/chat/egress/credentialed.ts
|
|
1931
|
-
const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1932
|
-
"connection",
|
|
1933
|
-
"host",
|
|
1934
|
-
"keep-alive",
|
|
1935
|
-
"proxy-authenticate",
|
|
1936
|
-
"proxy-authorization",
|
|
1937
|
-
"te",
|
|
1938
|
-
"trailer",
|
|
1939
|
-
"transfer-encoding",
|
|
1940
|
-
"upgrade"
|
|
1941
|
-
]);
|
|
1942
|
-
const PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
|
|
1943
|
-
"vercel-sandbox-oidc-token",
|
|
1944
|
-
"vercel-forwarded-host",
|
|
1945
|
-
"vercel-forwarded-scheme",
|
|
1946
|
-
"vercel-forwarded-port",
|
|
1947
|
-
"vercel-forwarded-path"
|
|
1948
|
-
]);
|
|
1949
|
-
const TRACE_PROPAGATION_HEADERS = /* @__PURE__ */ new Set([
|
|
1950
|
-
"baggage",
|
|
1951
|
-
"sentry-trace",
|
|
1952
|
-
"traceparent"
|
|
1953
|
-
]);
|
|
1954
|
-
const DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length"]);
|
|
1955
|
-
const UPSTREAM_TOKEN_REJECTION_STATUS = 401;
|
|
1956
|
-
const UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
|
|
1957
|
-
const GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 65536;
|
|
1958
|
-
const RESPONSE_BODY_TEXT_LIMIT_BYTES = 65536;
|
|
1959
|
-
function authRequiredResponse(input) {
|
|
1960
|
-
return new Response(`junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized\n${input.message}`, {
|
|
1961
|
-
status: 401,
|
|
1962
|
-
headers: {
|
|
1963
|
-
"content-type": "text/plain; charset=utf-8",
|
|
1964
|
-
"cache-control": "no-store"
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
function policyDeniedResponse(error) {
|
|
1969
|
-
return Response.json({ error: error.message }, {
|
|
1970
|
-
status: 403,
|
|
1971
|
-
headers: { "cache-control": "no-store" }
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
function shouldLogSandboxEgressInfo() {
|
|
1975
|
-
return (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase() !== "production";
|
|
1976
|
-
}
|
|
1977
|
-
function egressAttributes(input) {
|
|
1978
|
-
return {
|
|
1979
|
-
...input.egressId ? { "app.sandbox.egress_id": input.egressId } : void 0,
|
|
1980
|
-
...input.provider ? { "app.provider.name": input.provider } : void 0,
|
|
1981
|
-
...input.grantName ? { "app.grant.name": input.grantName } : void 0,
|
|
1982
|
-
...input.grantAccess ? { "app.grant.access": input.grantAccess } : void 0,
|
|
1983
|
-
...input.grantReason ? { "app.grant.reason": input.grantReason } : void 0,
|
|
1984
|
-
...input.host ? { "server.address": input.host } : void 0,
|
|
1985
|
-
...input.method ? { "http.request.method": input.method } : void 0,
|
|
1986
|
-
...input.path ? { "url.path": input.path } : void 0,
|
|
1987
|
-
...input.status ? { "http.response.status_code": input.status } : void 0
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
function redactedProxyPath(pathname) {
|
|
1991
|
-
if (pathname.startsWith(`/api/internal/sandbox-egress/`)) return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
|
|
1992
|
-
return pathname;
|
|
1993
|
-
}
|
|
1994
|
-
function routingAttributes(request, upstreamUrl) {
|
|
1995
|
-
const attributes = { "app.sandbox.egress.proxy_path": redactedProxyPath(new URL(request.url).pathname) };
|
|
1996
|
-
if (upstreamUrl) {
|
|
1997
|
-
attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
|
|
1998
|
-
const gitService = upstreamUrl.searchParams.get("service");
|
|
1999
|
-
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) attributes["app.sandbox.egress.git_service"] = gitService;
|
|
2000
|
-
}
|
|
2001
|
-
return attributes;
|
|
2002
|
-
}
|
|
2003
|
-
function displayedUpstreamPath(upstreamUrl) {
|
|
2004
|
-
const gitService = upstreamUrl.searchParams.get("service");
|
|
2005
|
-
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) return `${upstreamUrl.pathname}?service=${gitService}`;
|
|
2006
|
-
return upstreamUrl.pathname;
|
|
2007
|
-
}
|
|
2008
|
-
function permissionDeniedMessage(provider, grant) {
|
|
2009
|
-
return `${provider} returned HTTP 403 after the runtime injected the ${grant.name} grant. The request was forwarded; this is not a local runtime block.`;
|
|
2010
|
-
}
|
|
2011
|
-
function isEgressAuthRequired(error) {
|
|
2012
|
-
return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
|
|
2013
|
-
}
|
|
2014
|
-
function isEgressPolicyDenied(error) {
|
|
2015
|
-
return error instanceof EgressPolicyDenied || error instanceof Error && error.name === "EgressPolicyDenied";
|
|
2016
|
-
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Record one forwarded provider request. Write-grant requests are audit
|
|
2019
|
-
* events, so they reach Sentry in production. Other requests log only outside
|
|
2020
|
-
* production.
|
|
2021
|
-
*/
|
|
2022
|
-
function logSandboxEgressUpstreamRequest(input) {
|
|
2023
|
-
const isWrite = input.grantAccess === "write";
|
|
2024
|
-
if (!isWrite && !shouldLogSandboxEgressInfo()) return;
|
|
2025
|
-
(isWrite ? logAudit : logInfo)("sandbox.egress.upstream.requested", {
|
|
2026
|
-
...egressAttributes({
|
|
2027
|
-
egressId: input.egressId,
|
|
2028
|
-
grantAccess: input.grantAccess,
|
|
2029
|
-
grantName: input.grantName,
|
|
2030
|
-
grantReason: input.grantReason,
|
|
2031
|
-
host: input.upstreamUrl.hostname,
|
|
2032
|
-
method: input.request.method,
|
|
2033
|
-
path: input.upstreamUrl.pathname,
|
|
2034
|
-
provider: input.provider,
|
|
2035
|
-
status: input.upstream.status
|
|
2036
|
-
}),
|
|
2037
|
-
...routingAttributes(input.request, input.upstreamUrl),
|
|
2038
|
-
"app.sandbox.egress.upstream_ok": input.upstream.ok
|
|
2039
|
-
});
|
|
2040
|
-
}
|
|
2041
|
-
async function requestBodyBytes(request) {
|
|
2042
|
-
if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS" || request.body === null) return;
|
|
2043
|
-
return await request.arrayBuffer();
|
|
2044
|
-
}
|
|
2045
|
-
/**
|
|
2046
|
-
* Explain a request body that the runtime delivered already used.
|
|
2047
|
-
*
|
|
2048
|
-
* The forwarded headers cannot show whether the body was empty, so the proxy
|
|
2049
|
-
* does not send a credentialed write upstream without its payload.
|
|
2050
|
-
*/
|
|
2051
|
-
function unreadableBodyResponse(eventId) {
|
|
2052
|
-
return Response.json({
|
|
2053
|
-
source: "junior-egress",
|
|
2054
|
-
error: "Junior's sandbox egress proxy could not read the request body, so it did not send the request upstream.",
|
|
2055
|
-
...eventId ? { sentryEventId: eventId } : void 0
|
|
2056
|
-
}, {
|
|
2057
|
-
status: 500,
|
|
2058
|
-
headers: { "cache-control": "no-store" }
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
/** Identify GitHub writes whose body determines whether a grant is safe. */
|
|
2062
|
-
function githubBodyInspection(input) {
|
|
2063
|
-
if (input.provider !== "github" || input.requestMethod.toUpperCase() !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") return;
|
|
2064
|
-
const pathname = input.upstreamUrl.pathname.toLowerCase();
|
|
2065
|
-
if (pathname.endsWith("/graphql")) return "graphql";
|
|
2066
|
-
return /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/events)?$/.test(pathname) ? "pull-request-review" : void 0;
|
|
2067
|
-
}
|
|
2068
|
-
function grantSelectionBodyTooLargeMessage(inspection) {
|
|
2069
|
-
return inspection === "graphql" ? "GitHub GraphQL request body is too large for Junior to inspect before issuing credentials." : "GitHub pull request review request body is too large for Junior to inspect before issuing credentials.";
|
|
2070
|
-
}
|
|
2071
|
-
function grantSelectionBodyText(input) {
|
|
2072
|
-
if (input.body === void 0) return;
|
|
2073
|
-
if (input.body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
|
|
2074
|
-
const inspection = githubBodyInspection({
|
|
2075
|
-
provider: input.provider,
|
|
2076
|
-
requestMethod: input.request.method,
|
|
2077
|
-
upstreamUrl: input.upstreamUrl
|
|
2078
|
-
});
|
|
2079
|
-
if (inspection) throw new EgressPolicyDenied(grantSelectionBodyTooLargeMessage(inspection));
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
return new TextDecoder().decode(input.body);
|
|
2083
|
-
}
|
|
2084
|
-
function responseContentLength(upstream) {
|
|
2085
|
-
const raw = upstream.headers.get("content-length");
|
|
2086
|
-
if (!raw) return;
|
|
2087
|
-
const parsed = Number(raw);
|
|
2088
|
-
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
2089
|
-
}
|
|
2090
|
-
async function responseTextWithinLimit(upstream, maxBytes) {
|
|
2091
|
-
const limit = Math.min(Math.max(0, Math.floor(maxBytes)), RESPONSE_BODY_TEXT_LIMIT_BYTES);
|
|
2092
|
-
if (limit <= 0) return;
|
|
2093
|
-
const contentLength = responseContentLength(upstream);
|
|
2094
|
-
if (contentLength !== void 0 && contentLength > limit) return;
|
|
2095
|
-
let clone;
|
|
2096
|
-
try {
|
|
2097
|
-
clone = upstream.clone();
|
|
2098
|
-
} catch {
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
const body = clone.body;
|
|
2102
|
-
if (!body) return "";
|
|
2103
|
-
const reader = body.getReader();
|
|
2104
|
-
const chunks = [];
|
|
2105
|
-
let bytes = 0;
|
|
2106
|
-
try {
|
|
2107
|
-
while (true) {
|
|
2108
|
-
const { done, value } = await reader.read();
|
|
2109
|
-
if (done) break;
|
|
2110
|
-
if (!value) continue;
|
|
2111
|
-
bytes += value.byteLength;
|
|
2112
|
-
if (bytes > limit) {
|
|
2113
|
-
await reader.cancel().catch(() => void 0);
|
|
2114
|
-
return;
|
|
2115
|
-
}
|
|
2116
|
-
chunks.push(value);
|
|
2117
|
-
}
|
|
2118
|
-
} catch {
|
|
2119
|
-
await reader.cancel().catch(() => void 0);
|
|
2120
|
-
return;
|
|
2121
|
-
} finally {
|
|
2122
|
-
reader.releaseLock();
|
|
2123
|
-
}
|
|
2124
|
-
const combined = new Uint8Array(bytes);
|
|
2125
|
-
let offset = 0;
|
|
2126
|
-
for (const chunk of chunks) {
|
|
2127
|
-
combined.set(chunk, offset);
|
|
2128
|
-
offset += chunk.byteLength;
|
|
2129
|
-
}
|
|
2130
|
-
return new TextDecoder().decode(combined);
|
|
2131
|
-
}
|
|
2132
|
-
/**
|
|
2133
|
-
* Build the upstream request headers.
|
|
2134
|
-
*
|
|
2135
|
-
* Sandbox/proxy-only headers are stripped so they cannot leak to providers, and
|
|
2136
|
-
* trace headers are preserved only for hosts opted into trace propagation.
|
|
2137
|
-
* Credential lease transforms win last because they are the host-issued
|
|
2138
|
-
* authority for provider auth headers.
|
|
2139
|
-
*/
|
|
2140
|
-
function requestHeaders(request, lease, upstreamHost, tracePropagation) {
|
|
2141
|
-
const headers = new Headers();
|
|
2142
|
-
const mayPropagateTrace = shouldPropagateSandboxEgressTrace(upstreamHost, tracePropagation);
|
|
2143
|
-
request.headers.forEach((value, key) => {
|
|
2144
|
-
const normalized = key.toLowerCase();
|
|
2145
|
-
if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) return;
|
|
2146
|
-
if (TRACE_PROPAGATION_HEADERS.has(normalized) && !mayPropagateTrace) return;
|
|
2147
|
-
headers.append(key, value);
|
|
2148
|
-
});
|
|
2149
|
-
for (const transform of lease.headerTransforms) {
|
|
2150
|
-
if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) continue;
|
|
2151
|
-
for (const [key, value] of Object.entries(transform.headers)) {
|
|
2152
|
-
if (TRACE_PROPAGATION_HEADERS.has(key.toLowerCase()) && !mayPropagateTrace) continue;
|
|
2153
|
-
headers.set(key, value);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
return headers;
|
|
2157
|
-
}
|
|
2158
|
-
function responseHeaders(upstream) {
|
|
2159
|
-
const headers = new Headers();
|
|
2160
|
-
upstream.headers.forEach((value, key) => {
|
|
2161
|
-
const normalized = key.toLowerCase();
|
|
2162
|
-
if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) headers.append(key, value);
|
|
2163
|
-
});
|
|
2164
|
-
return headers;
|
|
2165
|
-
}
|
|
2166
|
-
function leaseLogAttributes(input) {
|
|
2167
|
-
return {
|
|
2168
|
-
...egressAttributes({
|
|
2169
|
-
egressId: input.egressId,
|
|
2170
|
-
grantAccess: input.lease.grant.access,
|
|
2171
|
-
grantName: input.lease.grant.name,
|
|
2172
|
-
grantReason: input.lease.grant.reason,
|
|
2173
|
-
host: input.upstreamUrl.hostname,
|
|
2174
|
-
method: input.request.method,
|
|
2175
|
-
path: input.upstreamUrl.pathname,
|
|
2176
|
-
provider: input.provider,
|
|
2177
|
-
status: input.status
|
|
2178
|
-
}),
|
|
2179
|
-
...routingAttributes(input.request, input.upstreamUrl)
|
|
2180
|
-
};
|
|
2181
|
-
}
|
|
2182
|
-
async function recordSandboxAuthRequired(input) {
|
|
2183
|
-
await setSandboxEgressAuthRequiredSignal(input.credentialContext, {
|
|
2184
|
-
provider: input.provider,
|
|
2185
|
-
grant: input.grant,
|
|
2186
|
-
kind: input.kind ?? "auth_required",
|
|
2187
|
-
...input.authorization ? { authorization: input.authorization } : void 0,
|
|
2188
|
-
message: input.message
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
async function recordSandboxPermissionDenied(input) {
|
|
2192
|
-
await setSandboxEgressPermissionDeniedSignal(input.credentialContext, {
|
|
2193
|
-
provider: input.provider,
|
|
2194
|
-
grant: input.lease.grant,
|
|
2195
|
-
...input.lease.account ? { account: input.lease.account } : void 0,
|
|
2196
|
-
message: input.message,
|
|
2197
|
-
source: "upstream",
|
|
2198
|
-
status: input.upstream.status,
|
|
2199
|
-
upstreamHost: input.upstreamUrl.hostname,
|
|
2200
|
-
upstreamPath: displayedUpstreamPath(input.upstreamUrl)
|
|
2201
|
-
});
|
|
2202
|
-
}
|
|
2203
|
-
/**
|
|
2204
|
-
* Forward one verified sandbox egress request with host-managed credentials.
|
|
2205
|
-
*
|
|
2206
|
-
* The caller must already have authenticated the sandbox VM, checked the signed
|
|
2207
|
-
* credential context, and resolved the provider for `upstreamUrl`. This function
|
|
2208
|
-
* then selects the read/write grant, issues or reuses a short-lived credential
|
|
2209
|
-
* lease, applies its header transforms, and maps provider auth failures into
|
|
2210
|
-
* command-level signals instead of throwing raw upstream details at the agent.
|
|
2211
|
-
*/
|
|
2212
|
-
async function executeCredentialedEgressRequest(input) {
|
|
2213
|
-
const { activeEgressId, credentialContext, deps, operation, provider, request, upstreamUrl } = input;
|
|
2214
|
-
let body;
|
|
2215
|
-
try {
|
|
2216
|
-
body = await requestBodyBytes(request);
|
|
2217
|
-
} catch (error) {
|
|
2218
|
-
if (!(error instanceof TypeError)) throw error;
|
|
2219
|
-
return unreadableBodyResponse(logException(error, "sandbox.egress.request_body.unreadable", {
|
|
2220
|
-
...egressAttributes({
|
|
2221
|
-
egressId: activeEgressId,
|
|
2222
|
-
host: upstreamUrl.hostname,
|
|
2223
|
-
method: request.method,
|
|
2224
|
-
path: upstreamUrl.pathname,
|
|
2225
|
-
provider,
|
|
2226
|
-
status: 500
|
|
2227
|
-
}),
|
|
2228
|
-
...routingAttributes(request, upstreamUrl)
|
|
2229
|
-
}));
|
|
2230
|
-
}
|
|
2231
|
-
const bodyForGrantSelection = githubBodyInspection({
|
|
2232
|
-
provider,
|
|
2233
|
-
requestMethod: request.method,
|
|
2234
|
-
upstreamUrl
|
|
2235
|
-
}) ? body : void 0;
|
|
2236
|
-
let grantSelection;
|
|
2237
|
-
try {
|
|
2238
|
-
grantSelection = await selectSandboxEgressGrant({
|
|
2239
|
-
bodyText: grantSelectionBodyText({
|
|
2240
|
-
body: bodyForGrantSelection,
|
|
2241
|
-
provider,
|
|
2242
|
-
request,
|
|
2243
|
-
upstreamUrl
|
|
2244
|
-
}),
|
|
2245
|
-
...operation ? { operation } : void 0,
|
|
2246
|
-
provider,
|
|
2247
|
-
method: request.method,
|
|
2248
|
-
upstreamUrl
|
|
2249
|
-
});
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
if (isEgressPolicyDenied(error)) {
|
|
2252
|
-
logWarn("sandbox.egress.policy.denied", {
|
|
2253
|
-
...egressAttributes({
|
|
2254
|
-
egressId: activeEgressId,
|
|
2255
|
-
host: upstreamUrl.hostname,
|
|
2256
|
-
method: request.method,
|
|
2257
|
-
path: upstreamUrl.pathname,
|
|
2258
|
-
provider,
|
|
2259
|
-
status: 403
|
|
2260
|
-
}),
|
|
2261
|
-
...routingAttributes(request, upstreamUrl),
|
|
2262
|
-
"app.sandbox.egress.policy.reason": error.message
|
|
2263
|
-
});
|
|
2264
|
-
return policyDeniedResponse(error);
|
|
2265
|
-
}
|
|
2266
|
-
throw error;
|
|
2267
|
-
}
|
|
2268
|
-
const issueCredentialLease = deps.issueCredentialLease ?? sandboxEgressCredentialLease;
|
|
2269
|
-
const clearCredentialLease = deps.clearCredentialLease ?? clearSandboxEgressCredentialLease;
|
|
2270
|
-
const recordAuthRequired = deps.recordAuthRequired ?? recordSandboxAuthRequired;
|
|
2271
|
-
const recordPermissionDenied = deps.recordPermissionDenied ?? recordSandboxPermissionDenied;
|
|
2272
|
-
async function resolveLease() {
|
|
2273
|
-
try {
|
|
2274
|
-
return await issueCredentialLease(provider, grantSelection, credentialContext);
|
|
2275
|
-
} catch (error) {
|
|
2276
|
-
if (!(error instanceof SandboxEgressCredentialError)) throw error;
|
|
2277
|
-
await recordAuthRequired({
|
|
2278
|
-
credentialContext,
|
|
2279
|
-
provider: error.provider,
|
|
2280
|
-
grant: error.grant,
|
|
2281
|
-
kind: error.kind,
|
|
2282
|
-
authorization: error.authorization,
|
|
2283
|
-
message: error.message
|
|
2284
|
-
});
|
|
2285
|
-
const isAuthRequired = error.kind === "auth_required";
|
|
2286
|
-
logWarn(isAuthRequired ? "sandbox.egress.credential.needed" : "sandbox.egress.credential.unavailable", {
|
|
2287
|
-
...egressAttributes({
|
|
2288
|
-
egressId: activeEgressId,
|
|
2289
|
-
grantAccess: error.grant.access,
|
|
2290
|
-
grantName: error.grant.name,
|
|
2291
|
-
grantReason: error.grant.reason,
|
|
2292
|
-
host: upstreamUrl.hostname,
|
|
2293
|
-
method: request.method,
|
|
2294
|
-
path: upstreamUrl.pathname,
|
|
2295
|
-
provider: error.provider,
|
|
2296
|
-
status: 401
|
|
2297
|
-
}),
|
|
2298
|
-
...routingAttributes(request, upstreamUrl)
|
|
2299
|
-
});
|
|
2300
|
-
return authRequiredResponse({
|
|
2301
|
-
provider: error.provider,
|
|
2302
|
-
grant: error.grant,
|
|
2303
|
-
message: error.message
|
|
2304
|
-
});
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
let leaseOrResponse = await resolveLease();
|
|
2308
|
-
if (leaseOrResponse instanceof Response) return leaseOrResponse;
|
|
2309
|
-
let lease = leaseOrResponse;
|
|
2310
|
-
const attributes = (activeLease, status) => leaseLogAttributes({
|
|
2311
|
-
egressId: activeEgressId,
|
|
2312
|
-
lease: activeLease,
|
|
2313
|
-
provider,
|
|
2314
|
-
request,
|
|
2315
|
-
status,
|
|
2316
|
-
upstreamUrl
|
|
2317
|
-
});
|
|
2318
|
-
if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
|
|
2319
|
-
logWarn("sandbox.egress.transform.missing", {
|
|
2320
|
-
...attributes(lease, 403),
|
|
2321
|
-
"app.sandbox.egress.transform_domains": lease.headerTransforms.map((transform) => transform.domain)
|
|
2322
|
-
});
|
|
2323
|
-
return Response.json({ error: "Credential lease does not cover forwarded host" }, { status: 403 });
|
|
2324
|
-
}
|
|
2325
|
-
const fetchImpl = deps.fetch ?? fetch;
|
|
2326
|
-
let retriedAfter403 = false;
|
|
2327
|
-
while (true) {
|
|
2328
|
-
const headers = requestHeaders(request, lease, upstreamUrl.hostname, deps.tracePropagation ?? {});
|
|
2329
|
-
const intercepted = await deps.interceptHttp?.({
|
|
2330
|
-
provider,
|
|
2331
|
-
request: new Request(upstreamUrl, {
|
|
2332
|
-
method: request.method,
|
|
2333
|
-
headers,
|
|
2334
|
-
...body !== void 0 ? { body } : void 0
|
|
2335
|
-
}),
|
|
2336
|
-
upstreamUrl
|
|
2337
|
-
});
|
|
2338
|
-
if (intercepted) return intercepted;
|
|
2339
|
-
const requestBody = body instanceof ArrayBuffer ? body.slice(0) : body;
|
|
2340
|
-
const upstream = await fetchImpl(upstreamUrl, {
|
|
2341
|
-
method: request.method,
|
|
2342
|
-
headers,
|
|
2343
|
-
...requestBody !== void 0 ? { body: requestBody } : void 0,
|
|
2344
|
-
redirect: "manual"
|
|
2345
|
-
});
|
|
2346
|
-
let pluginPermissionDenied;
|
|
2347
|
-
try {
|
|
2348
|
-
pluginPermissionDenied = (await onPluginEgressResponse({
|
|
2349
|
-
provider,
|
|
2350
|
-
grant: lease.grant,
|
|
2351
|
-
method: request.method,
|
|
2352
|
-
...operation ? { operation } : void 0,
|
|
2353
|
-
upstreamUrl,
|
|
2354
|
-
response: {
|
|
2355
|
-
headers: new Headers(upstream.headers),
|
|
2356
|
-
readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
|
|
2357
|
-
status: upstream.status
|
|
2358
|
-
}
|
|
2359
|
-
})).permissionDenied;
|
|
2360
|
-
} catch (error) {
|
|
2361
|
-
if (!isEgressAuthRequired(error)) throw error;
|
|
2362
|
-
await clearCredentialLease(provider, lease.grant, credentialContext);
|
|
2363
|
-
await recordAuthRequired({
|
|
2364
|
-
credentialContext,
|
|
2365
|
-
provider,
|
|
2366
|
-
grant: lease.grant,
|
|
2367
|
-
authorization: error.authorization ?? lease.authorization,
|
|
2368
|
-
message: error.message
|
|
2369
|
-
});
|
|
2370
|
-
logWarn("sandbox.egress.upstream_auth_requirement.classified", { ...attributes(lease, upstream.status) });
|
|
2371
|
-
await upstream.body?.cancel().catch(() => void 0);
|
|
2372
|
-
return authRequiredResponse({
|
|
2373
|
-
provider,
|
|
2374
|
-
grant: lease.grant,
|
|
2375
|
-
message: error.message
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
logSandboxEgressUpstreamRequest({
|
|
2379
|
-
egressId: activeEgressId,
|
|
2380
|
-
grantAccess: lease.grant.access,
|
|
2381
|
-
grantName: lease.grant.name,
|
|
2382
|
-
grantReason: lease.grant.reason,
|
|
2383
|
-
provider,
|
|
2384
|
-
request,
|
|
2385
|
-
upstream,
|
|
2386
|
-
upstreamUrl
|
|
2387
|
-
});
|
|
2388
|
-
if (upstream.status >= 400) logWarn("sandbox.egress.upstream_response.failed", {
|
|
2389
|
-
...attributes(lease, upstream.status),
|
|
2390
|
-
"error.type": `http_${upstream.status}`
|
|
2391
|
-
});
|
|
2392
|
-
if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
|
|
2393
|
-
logWarn("sandbox.egress.upstream_auth.rejected", {
|
|
2394
|
-
...attributes(lease, upstream.status),
|
|
2395
|
-
"app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
|
|
2396
|
-
});
|
|
2397
|
-
await clearCredentialLease(provider, lease.grant, credentialContext);
|
|
2398
|
-
await recordAuthRequired({
|
|
2399
|
-
credentialContext,
|
|
2400
|
-
provider,
|
|
2401
|
-
grant: lease.grant,
|
|
2402
|
-
authorization: lease.authorization,
|
|
2403
|
-
message: `Provider rejected the injected ${provider} credential.`
|
|
2404
|
-
});
|
|
2405
|
-
await upstream.body?.cancel().catch(() => void 0);
|
|
2406
|
-
return authRequiredResponse({
|
|
2407
|
-
provider,
|
|
2408
|
-
grant: lease.grant,
|
|
2409
|
-
message: `Provider rejected the injected ${provider} credential.\n`
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS && !retriedAfter403) {
|
|
2413
|
-
logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
|
|
2414
|
-
await clearCredentialLease(provider, lease.grant, credentialContext);
|
|
2415
|
-
await upstream.body?.cancel().catch(() => void 0);
|
|
2416
|
-
logWarn("sandbox.egress.upstream_auth.retrying", { ...attributes(lease, upstream.status) });
|
|
2417
|
-
leaseOrResponse = await resolveLease();
|
|
2418
|
-
if (leaseOrResponse instanceof Response) return leaseOrResponse;
|
|
2419
|
-
lease = leaseOrResponse;
|
|
2420
|
-
retriedAfter403 = true;
|
|
2421
|
-
continue;
|
|
2422
|
-
}
|
|
2423
|
-
if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS) {
|
|
2424
|
-
logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
|
|
2425
|
-
await clearCredentialLease(provider, lease.grant, credentialContext);
|
|
2426
|
-
await recordPermissionDenied({
|
|
2427
|
-
credentialContext,
|
|
2428
|
-
provider,
|
|
2429
|
-
lease,
|
|
2430
|
-
message: pluginPermissionDenied?.message ?? permissionDeniedMessage(provider, lease.grant),
|
|
2431
|
-
upstream,
|
|
2432
|
-
upstreamUrl
|
|
2433
|
-
});
|
|
2434
|
-
if (pluginPermissionDenied) logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
|
|
2435
|
-
return new Response(upstream.body, {
|
|
2436
|
-
status: upstream.status,
|
|
2437
|
-
statusText: upstream.statusText,
|
|
2438
|
-
headers: responseHeaders(upstream)
|
|
2439
|
-
});
|
|
2440
|
-
}
|
|
2441
|
-
if (pluginPermissionDenied) {
|
|
2442
|
-
await recordPermissionDenied({
|
|
2443
|
-
credentialContext,
|
|
2444
|
-
provider,
|
|
2445
|
-
lease,
|
|
2446
|
-
message: pluginPermissionDenied.message,
|
|
2447
|
-
upstream,
|
|
2448
|
-
upstreamUrl
|
|
2449
|
-
});
|
|
2450
|
-
logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
|
|
2451
|
-
}
|
|
2452
|
-
return new Response(upstream.body, {
|
|
2453
|
-
status: upstream.status,
|
|
2454
|
-
statusText: upstream.statusText,
|
|
2455
|
-
headers: responseHeaders(upstream)
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
398
|
// src/chat/experimental.ts
|
|
2460
399
|
/**
|
|
2461
400
|
* Known unstable product features that must be opted into explicitly.
|
|
@@ -5814,4 +3753,4 @@ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
|
|
|
5814
3753
|
async function saveScheduledAutomationInLock(db, task, current) {
|
|
5815
3754
|
await writeScheduledAutomation(db, requireStoredTask(task), current);
|
|
5816
3755
|
}
|
|
5817
|
-
export { persistConversationMessageSummaries as $,
|
|
3756
|
+
export { persistConversationMessageSummaries as $, isToolResultMessage as $t, commitAcceptedReply as A, abandonTurnRecord as At, recordAttachmentsDelivered as B, buildTurnErrorResponse as Bt, automationVersionOf as C, getConfigDefaults as Cn, canReusePendingAuthLink as Ct, AutomationConflictError as D, continuableMessages as Dt, sameDefinitionValue as E, wasPendingAuthStopped as Et, loadProjection as F, recordTurnSummary as Ft, recordMcpProviderConnected as G, getPiMessageRole as Gt, recordAuthorizationCompleted as H, isTurnExecutionLimitExceededError as Ht, loadTurnProjection as I, TURN_LIMIT_DISABLED_REASON as It, recordTurnRoute as J, hasRuntimeTurnContext as Jt, recordSubscribedReplyRoute as K, getTerminalAssistantMessages as Kt, loadTurnRoute as L, TurnBudgetExceededError as Lt, loadConnectedMcpProviders as M, failTurnRecord as Mt, loadConversationProjection as N, getTurnRecord as Nt, automationRevision as O, loadTurnCheckpoint as Ot, loadLastModelProfile as P, listTurnSummaries as Pt, projectConversationMessages as Q, isToolResultError as Qt, openConversationProjection as R, TurnSliceLimitExceededError as Rt, automationDefinition as S, piMessageFromHistoryItem as Sn, abandonReplacedPendingAuth as St, readAutomationVersion as T, getConversationPendingAuth as Tt, recordAuthorizationRequested as U, persistWithRetry as Ut, recordAuthenticationLinked as V, createTurnLimitGate as Vt, recordGuardianActionReviewed as W, extractAssistantText as Wt, hydrateConversationMessages as X, isAssistantMessage as Xt, appendConversationMessages as Y, instructionTextForProjection as Yt, persistConversationMessages as Z, isContinuablePiBoundary as Zt, parseEventAutomationRow as _, isExperimentalFeatureEnabled as _n, createPluginAuthOrchestration as _t, saveScheduledAutomation as a, SESSION_CONTEXT_SECTION_TAG as an, buildConversationContext as at, admitEventAutomationRun as b, configValueSchema as bn, AuthorizationFlowDisabledError as bt, withScheduledAutomationLock as c, buildDeterministicTurnId as cn, getConversationMessageSlackTs as ct, createEventAutomation as d, getSubscribedReplyPreflightDecision as dn, normalizeConversationText as dt, normalizeToolNameFromResult as en, resolveConversationTitle as et, deleteEventAutomation as f, getThreadStopDecision as fn, recordDeliveredAssistantMessage as ft, listEventAutomationsForTeam as g, getExperimentalFeatures as gn, PluginCredentialFailureError as gt, listDeletedEventAutomationsCreatedBy as h, getAgentContextInputLimitTokens as hn, createPluginEgress as ht, readScheduledAutomation as i, REQUEST_CONTEXT_SECTION_TAG as in, appendThreadContextMessages as it, commitMessages as j, completeTurnRecord as jt, requireAutomationRevision as k, saveTurnCheckpoint as kt, blockEventAutomation as l, SubscribedReplyReason as ln, isHumanConversationMessage as lt, getEventAutomation as m, getAgentContextCompactionTriggerTokens as mn, upsertConversationMessage as mt, listScheduledAutomationsForTeam as n, stripRuntimeTurnContext as nn, fallbackShortTitle as nt, saveScheduledAutomationInLock as o, TURN_CONTEXT_TAG as on, createConversationMemoryService as ot, findMatchingEventAutomations as p, estimateTextTokens as pn, turnHasReply as pt, recordToolExecutionStarted as q, getUserMessageInstructionText as qt, parseScheduledAutomationRow as r, trimTrailingAssistantMessages as rn, resolveTaskTitle as rt, scheduledRunId as s, buildDeterministicAssistantMessageId as sn, estimateConversationContextTokens as st, createScheduledAutomation as t, retainRuntimeTurnContext as tn, settleConversationTitleWork as tt, collectEventAutomationMatchKeys as u, decideSubscribedThreadReply as un, markConversationMessage as ut, saveEventAutomation as v, setExperimentalFeatures as vn, authorizationId as vt, listAutomationVersions as w, setConfigDefaults as wn, clearPendingAuth as wt, eventAutomationRunLimitReason as x, historyItemFromPiMessage as xn, AuthorizationPauseError as xt, setEventAutomationStatus as y, createDurableLocationConfigurationService as yn, AuthPausePersistenceError as yt, recordAgentsInstructionsUpdated as z, TurnToolCallLimitExceededError as zt };
|