@sentry/junior 0.81.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/{agent-hooks-ZGTDOXQY.js → agent-hooks-EEWWWIS2.js} +5 -5
  2. package/dist/api-reference.d.ts +1 -1
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +3763 -4132
  5. package/dist/chat/agent-dispatch/runner.d.ts +1 -1
  6. package/dist/chat/app/services.d.ts +1 -1
  7. package/dist/chat/conversations/store.d.ts +1 -1
  8. package/dist/chat/egress/credentialed.d.ts +57 -0
  9. package/dist/chat/egress/plugin.d.ts +24 -0
  10. package/dist/chat/plugins/agent-hooks.d.ts +3 -3
  11. package/dist/chat/plugins/credential-hooks.d.ts +2 -0
  12. package/dist/chat/resource-events/ingest.d.ts +23 -0
  13. package/dist/chat/resource-events/notification.d.ts +20 -0
  14. package/dist/chat/resource-events/store.d.ts +79 -0
  15. package/dist/chat/respond.d.ts +1 -1
  16. package/dist/chat/sandbox/{egress-credentials.d.ts → egress/credentials.d.ts} +28 -5
  17. package/dist/chat/sandbox/egress/policy.d.ts +32 -0
  18. package/dist/chat/sandbox/egress/proxy.d.ts +35 -0
  19. package/dist/chat/sandbox/{egress-session.d.ts → egress/session.d.ts} +41 -11
  20. package/dist/chat/sandbox/sandbox.d.ts +2 -2
  21. package/dist/chat/services/pending-auth.d.ts +5 -0
  22. package/dist/chat/slack/client.d.ts +2 -0
  23. package/dist/chat/task-execution/slack-work.d.ts +22 -0
  24. package/dist/chat/task-execution/state.d.ts +7 -1
  25. package/dist/chat/task-execution/store.d.ts +30 -8
  26. package/dist/chat/tools/resource-events.d.ts +19 -0
  27. package/dist/chat/tools/types.d.ts +2 -1
  28. package/dist/{chunk-Y3YUOEAZ.js → chunk-5C3RY6IZ.js} +20 -11
  29. package/dist/{chunk-IOBSRZK5.js → chunk-AK7T7XRQ.js} +1 -1
  30. package/dist/{chunk-LX5GBMEP.js → chunk-BFQ7IUTE.js} +1 -1
  31. package/dist/{chunk-YA2JCC7G.js → chunk-GK5CITCY.js} +4 -2
  32. package/dist/{chunk-4XHCVBXH.js → chunk-KZLRUQAA.js} +77 -12
  33. package/dist/{chunk-BOMLWLTF.js → chunk-LK53AHO5.js} +1759 -175
  34. package/dist/{chunk-2AJ4TEKE.js → chunk-ORRLK22J.js} +24 -2
  35. package/dist/{chunk-PQ2U2AO3.js → chunk-QVMLDCVR.js} +10 -2
  36. package/dist/{chunk-RV5RYIJW.js → chunk-UXPG6ZIN.js} +28 -0
  37. package/dist/cli/chat.js +4 -4
  38. package/dist/cli/env.js +7 -1
  39. package/dist/cli/init.js +1 -0
  40. package/dist/cli/main.js +1 -1
  41. package/dist/cli/plugins.js +3 -3
  42. package/dist/cli/upgrade.js +3 -3
  43. package/dist/{db-GQJKBX5W.js → db-TRIVS6BW.js} +2 -2
  44. package/dist/handlers/agent-dispatch.d.ts +1 -1
  45. package/dist/handlers/github-webhook/check-suite.d.ts +3 -0
  46. package/dist/handlers/github-webhook/pull-request-review.d.ts +3 -0
  47. package/dist/handlers/github-webhook/pull-request.d.ts +3 -0
  48. package/dist/handlers/github-webhook/resource.d.ts +10 -0
  49. package/dist/handlers/github-webhook.d.ts +15 -0
  50. package/dist/handlers/sandbox-egress-proxy.d.ts +2 -2
  51. package/dist/nitro.js +2 -2
  52. package/dist/reporting/conversations.d.ts +2 -2
  53. package/dist/reporting.d.ts +2 -2
  54. package/dist/reporting.js +16 -22
  55. package/dist/{runner-LNBN7BIN.js → runner-KS4XDMHF.js} +5 -5
  56. package/package.json +6 -5
  57. package/dist/chat/sandbox/egress-policy.d.ts +0 -15
  58. package/dist/chat/sandbox/egress-proxy.d.ts +0 -19
  59. /package/dist/chat/sandbox/{egress-oidc.d.ts → egress/oidc.d.ts} +0 -0
  60. /package/dist/chat/sandbox/{egress-schemas.d.ts → egress/schemas.d.ts} +0 -0
  61. /package/dist/chat/sandbox/{egress-tracing.d.ts → egress/tracing.d.ts} +0 -0
@@ -31,7 +31,7 @@ import {
31
31
  recordSubagentStarted,
32
32
  recordToolExecutionStarted,
33
33
  upsertAgentTurnSessionRecord
34
- } from "./chunk-YA2JCC7G.js";
34
+ } from "./chunk-GK5CITCY.js";
35
35
  import {
36
36
  createPluginEmbedder,
37
37
  createPluginHookRunner,
@@ -42,14 +42,14 @@ import {
42
42
  getPlugins,
43
43
  getSlackToolContext,
44
44
  resolveChannelCapabilities
45
- } from "./chunk-Y3YUOEAZ.js";
45
+ } from "./chunk-5C3RY6IZ.js";
46
46
  import {
47
47
  createPluginLogger,
48
48
  createPluginState
49
49
  } from "./chunk-GUO4EE7L.js";
50
50
  import {
51
51
  getDb
52
- } from "./chunk-PQ2U2AO3.js";
52
+ } from "./chunk-QVMLDCVR.js";
53
53
  import {
54
54
  SANDBOX_DATA_ROOT,
55
55
  SANDBOX_SKILLS_ROOT,
@@ -71,7 +71,7 @@ import {
71
71
  normalizeSlackConversationId,
72
72
  parseDestination,
73
73
  withSlackRetries
74
- } from "./chunk-4XHCVBXH.js";
74
+ } from "./chunk-KZLRUQAA.js";
75
75
  import {
76
76
  GEN_AI_PROVIDER_NAME,
77
77
  GEN_AI_SERVER_ADDRESS,
@@ -122,6 +122,7 @@ import {
122
122
  parseSkillInvocation
123
123
  } from "./chunk-WCXVEQPI.js";
124
124
  import {
125
+ CredentialUnavailableError,
125
126
  credentialContextSchema,
126
127
  pluginCatalogRuntime,
127
128
  resolveAuthTokenPlaceholder,
@@ -191,7 +192,7 @@ function getConfigDefaults() {
191
192
  return cloneDefaults(installDefaults);
192
193
  }
193
194
 
194
- // src/chat/sandbox/egress-tracing.ts
195
+ // src/chat/sandbox/egress/tracing.ts
195
196
  function isValidDomainPattern(domain) {
196
197
  if (domain.includes("*")) {
197
198
  return domain.startsWith("*.") && domain.indexOf("*", 1) === -1;
@@ -776,8 +777,8 @@ var StateAdapterTokenStore = class {
776
777
  async set(userId, provider, tokens) {
777
778
  const parsed = storedTokensSchema.parse(tokens);
778
779
  const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
779
- const ttlMs = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
780
- await this.state.set(tokenKey(userId, provider), parsed, ttlMs);
780
+ const ttlMs2 = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
781
+ await this.state.set(tokenKey(userId, provider), parsed, ttlMs2);
781
782
  }
782
783
  async delete(userId, provider) {
783
784
  await this.state.delete(tokenKey(userId, provider));
@@ -3769,9 +3770,476 @@ function createReportProgressTool() {
3769
3770
  });
3770
3771
  }
3771
3772
 
3772
- // src/chat/tools/slack/channel-list-messages.ts
3773
+ // src/chat/tools/resource-events.ts
3773
3774
  import { Type as Type13 } from "@sinclair/typebox";
3774
3775
 
3776
+ // src/chat/resource-events/store.ts
3777
+ import { createHash } from "crypto";
3778
+ import { destinationSchema } from "@sentry/junior-plugin-api";
3779
+ import { z } from "zod";
3780
+ var RESOURCE_EVENT_PREFIX = "junior:resource_event_subscription";
3781
+ var INDEX_LOCK_TTL_MS = 1e4;
3782
+ var SUBSCRIPTION_LOCK_TTL_MS = 1e4;
3783
+ var subscriptionStatusSchema = z.enum(["active", "cancelled", "completed"]);
3784
+ var subscriptionIdIndexSchema = z.array(z.string().min(1));
3785
+ var subscriptionSchema = z.object({
3786
+ conversationId: z.string().min(1),
3787
+ createdAtMs: z.number().finite(),
3788
+ destination: destinationSchema,
3789
+ events: z.array(z.string().min(1)).min(1),
3790
+ expiresAtMs: z.number().finite(),
3791
+ id: z.string().min(1),
3792
+ intent: z.string().min(1),
3793
+ label: z.string().min(1),
3794
+ provider: z.string().min(1),
3795
+ resourceRef: z.string().min(1),
3796
+ resourceType: z.string().min(1),
3797
+ status: subscriptionStatusSchema,
3798
+ updatedAtMs: z.number().finite()
3799
+ }).strict();
3800
+ function digest(value) {
3801
+ return createHash("sha256").update(value).digest("hex").slice(0, 32);
3802
+ }
3803
+ function subscriptionKey(id) {
3804
+ return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
3805
+ }
3806
+ function subscriptionLockKey(id) {
3807
+ return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
3808
+ }
3809
+ function resourceIndexKey(provider, resourceRef) {
3810
+ return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${provider}\0${resourceRef}`)}`;
3811
+ }
3812
+ function conversationIndexKey(conversationId) {
3813
+ return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
3814
+ }
3815
+ function indexLockKey(key) {
3816
+ return `${key}:lock`;
3817
+ }
3818
+ function ttlUntil(expiresAtMs, nowMs) {
3819
+ return Math.max(1, expiresAtMs - nowMs);
3820
+ }
3821
+ async function readSubscriptionIdIndex(state, key) {
3822
+ const value = await state.get(key);
3823
+ if (value === void 0 || value === null) {
3824
+ return [];
3825
+ }
3826
+ return subscriptionIdIndexSchema.parse(value);
3827
+ }
3828
+ function buildSubscriptionId(input) {
3829
+ const eventKey = [...new Set(input.events)].sort().join("\0");
3830
+ return `resub_${digest(
3831
+ `${input.provider}\0${input.resourceRef}\0${input.conversationId}\0${eventKey}`
3832
+ )}`;
3833
+ }
3834
+ async function withIndexLock(state, key, callback) {
3835
+ const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
3836
+ if (!lock) {
3837
+ throw new Error(`Could not acquire resource event index lock for ${key}`);
3838
+ }
3839
+ try {
3840
+ return await callback();
3841
+ } finally {
3842
+ await state.releaseLock(lock);
3843
+ }
3844
+ }
3845
+ async function addToIndex(state, key, subscriptionId, nowMs) {
3846
+ await withIndexLock(state, key, async () => {
3847
+ const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
3848
+ const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
3849
+ await state.set(key, next, await indexTtlMs(state, next, nowMs));
3850
+ });
3851
+ }
3852
+ async function removeFromIndex(state, key, subscriptionId, nowMs) {
3853
+ await withIndexLock(state, key, async () => {
3854
+ const existing = await readSubscriptionIdIndex(state, key);
3855
+ const next = existing.filter((id) => id !== subscriptionId);
3856
+ await state.set(key, next, await indexTtlMs(state, next, nowMs));
3857
+ });
3858
+ }
3859
+ async function indexTtlMs(state, subscriptionIds, nowMs) {
3860
+ const records = await Promise.all(
3861
+ subscriptionIds.map(
3862
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
3863
+ )
3864
+ );
3865
+ const latestExpiresAtMs = Math.max(
3866
+ nowMs,
3867
+ ...records.filter(
3868
+ (record) => record !== void 0 && activeAt(record, nowMs)
3869
+ ).map((record) => record.expiresAtMs)
3870
+ );
3871
+ return ttlUntil(latestExpiresAtMs, nowMs);
3872
+ }
3873
+ function parseSubscription(value) {
3874
+ if (value === void 0 || value === null) {
3875
+ return void 0;
3876
+ }
3877
+ return subscriptionSchema.parse(value);
3878
+ }
3879
+ function activeAt(subscription, nowMs) {
3880
+ return subscription.status === "active" && subscription.expiresAtMs > nowMs;
3881
+ }
3882
+ function matchesEvent(subscription, input) {
3883
+ return subscription.provider === input.provider && subscription.resourceRef === input.resourceRef && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs);
3884
+ }
3885
+ async function createResourceEventSubscription(input, options = {}) {
3886
+ const state = options.state ?? getStateAdapter();
3887
+ await state.connect();
3888
+ const nowMs = options.nowMs ?? Date.now();
3889
+ const events = [...new Set(input.events.map((event) => event.trim()))].filter(
3890
+ Boolean
3891
+ );
3892
+ if (events.length === 0) {
3893
+ throw new Error("Resource event subscription requires at least one event");
3894
+ }
3895
+ if (input.expiresAtMs <= nowMs) {
3896
+ throw new Error("Resource event subscription expiry must be in the future");
3897
+ }
3898
+ const id = buildSubscriptionId({
3899
+ conversationId: input.conversationId,
3900
+ events,
3901
+ provider: input.provider,
3902
+ resourceRef: input.resourceRef
3903
+ });
3904
+ const record = {
3905
+ conversationId: input.conversationId,
3906
+ createdAtMs: nowMs,
3907
+ destination: input.destination,
3908
+ events,
3909
+ expiresAtMs: input.expiresAtMs,
3910
+ id,
3911
+ intent: input.intent,
3912
+ label: input.label,
3913
+ provider: input.provider,
3914
+ resourceRef: input.resourceRef,
3915
+ resourceType: input.resourceType,
3916
+ status: "active",
3917
+ updatedAtMs: nowMs
3918
+ };
3919
+ const parsed = subscriptionSchema.parse(record);
3920
+ await state.set(
3921
+ subscriptionKey(id),
3922
+ parsed,
3923
+ ttlUntil(parsed.expiresAtMs, nowMs)
3924
+ );
3925
+ await addToIndex(
3926
+ state,
3927
+ resourceIndexKey(input.provider, input.resourceRef),
3928
+ id,
3929
+ nowMs
3930
+ );
3931
+ await addToIndex(
3932
+ state,
3933
+ conversationIndexKey(input.conversationId),
3934
+ id,
3935
+ nowMs
3936
+ );
3937
+ return parsed;
3938
+ }
3939
+ async function listResourceEventSubscriptions(input) {
3940
+ const state = input.state ?? getStateAdapter();
3941
+ await state.connect();
3942
+ const nowMs = input.nowMs ?? Date.now();
3943
+ const ids = await readSubscriptionIdIndex(
3944
+ state,
3945
+ conversationIndexKey(input.conversationId)
3946
+ );
3947
+ const records = await Promise.all(
3948
+ ids.map(
3949
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
3950
+ )
3951
+ );
3952
+ return records.filter(
3953
+ (record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
3954
+ ).sort((left, right) => left.createdAtMs - right.createdAtMs);
3955
+ }
3956
+ async function cancelResourceEventSubscription(input) {
3957
+ const state = input.state ?? getStateAdapter();
3958
+ await state.connect();
3959
+ const lock = await state.acquireLock(
3960
+ subscriptionLockKey(input.id),
3961
+ SUBSCRIPTION_LOCK_TTL_MS
3962
+ );
3963
+ if (!lock) {
3964
+ throw new Error(`Could not acquire subscription lock for ${input.id}`);
3965
+ }
3966
+ try {
3967
+ const current = parseSubscription(
3968
+ await state.get(subscriptionKey(input.id))
3969
+ );
3970
+ if (!current || current.conversationId !== input.conversationId) {
3971
+ return void 0;
3972
+ }
3973
+ const nowMs = input.nowMs ?? Date.now();
3974
+ const next = {
3975
+ ...current,
3976
+ status: "cancelled",
3977
+ updatedAtMs: nowMs
3978
+ };
3979
+ await state.set(
3980
+ subscriptionKey(input.id),
3981
+ next,
3982
+ JUNIOR_THREAD_STATE_TTL_MS
3983
+ );
3984
+ await removeFromIndex(
3985
+ state,
3986
+ resourceIndexKey(current.provider, current.resourceRef),
3987
+ input.id,
3988
+ nowMs
3989
+ );
3990
+ await removeFromIndex(
3991
+ state,
3992
+ conversationIndexKey(current.conversationId),
3993
+ input.id,
3994
+ nowMs
3995
+ );
3996
+ return next;
3997
+ } finally {
3998
+ await state.releaseLock(lock);
3999
+ }
4000
+ }
4001
+ async function findMatchingResourceEventSubscriptions(input) {
4002
+ const state = input.state ?? getStateAdapter();
4003
+ await state.connect();
4004
+ const nowMs = input.nowMs ?? Date.now();
4005
+ const ids = await readSubscriptionIdIndex(
4006
+ state,
4007
+ resourceIndexKey(input.provider, input.resourceRef)
4008
+ );
4009
+ const records = await Promise.all(
4010
+ ids.map(
4011
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
4012
+ )
4013
+ );
4014
+ return records.filter(
4015
+ (record) => record !== void 0 && matchesEvent(record, {
4016
+ eventType: input.eventType,
4017
+ nowMs,
4018
+ provider: input.provider,
4019
+ resourceRef: input.resourceRef
4020
+ })
4021
+ );
4022
+ }
4023
+ async function deliverResourceEventSubscription(input) {
4024
+ const state = input.state ?? getStateAdapter();
4025
+ await state.connect();
4026
+ const lock = await state.acquireLock(
4027
+ subscriptionLockKey(input.subscription.id),
4028
+ SUBSCRIPTION_LOCK_TTL_MS
4029
+ );
4030
+ if (!lock) {
4031
+ throw new Error(
4032
+ `Resource event subscription delivery lock busy: ${input.subscription.id}`
4033
+ );
4034
+ }
4035
+ try {
4036
+ const nowMs = input.nowMs ?? Date.now();
4037
+ const current = parseSubscription(
4038
+ await state.get(subscriptionKey(input.subscription.id))
4039
+ );
4040
+ if (!current || !matchesEvent(current, {
4041
+ eventType: input.eventType,
4042
+ nowMs,
4043
+ provider: input.provider,
4044
+ resourceRef: input.resourceRef
4045
+ })) {
4046
+ return false;
4047
+ }
4048
+ const delivered = await input.deliver(current);
4049
+ if (input.terminal) {
4050
+ const latest = parseSubscription(
4051
+ await state.get(subscriptionKey(current.id))
4052
+ );
4053
+ if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) {
4054
+ return delivered;
4055
+ }
4056
+ const next = {
4057
+ ...current,
4058
+ status: "completed",
4059
+ updatedAtMs: nowMs
4060
+ };
4061
+ await state.set(
4062
+ subscriptionKey(current.id),
4063
+ next,
4064
+ JUNIOR_THREAD_STATE_TTL_MS
4065
+ );
4066
+ await removeFromIndex(
4067
+ state,
4068
+ resourceIndexKey(current.provider, current.resourceRef),
4069
+ current.id,
4070
+ nowMs
4071
+ );
4072
+ await removeFromIndex(
4073
+ state,
4074
+ conversationIndexKey(current.conversationId),
4075
+ current.id,
4076
+ nowMs
4077
+ );
4078
+ }
4079
+ return delivered;
4080
+ } finally {
4081
+ await state.releaseLock(lock);
4082
+ }
4083
+ }
4084
+
4085
+ // src/chat/tools/resource-events.ts
4086
+ var DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
4087
+ var MAX_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
4088
+ var subscribeInputSchema = Type13.Object(
4089
+ {
4090
+ resourceRef: Type13.String({
4091
+ description: "Opaque resource ref copied from a subscribable tool result."
4092
+ }),
4093
+ provider: Type13.String({
4094
+ description: "Provider that owns the resource ref."
4095
+ }),
4096
+ resourceType: Type13.String({
4097
+ description: "Provider-defined resource type from the subscribable hint."
4098
+ }),
4099
+ label: Type13.String({
4100
+ description: "Human-readable resource label from the subscribable hint."
4101
+ }),
4102
+ events: Type13.Array(Type13.String(), {
4103
+ description: "High-signal event names to deliver to this conversation when they occur.",
4104
+ minItems: 1
4105
+ }),
4106
+ intent: Type13.String({
4107
+ description: "Concise reason this conversation wants these events, used when an event arrives."
4108
+ }),
4109
+ ttlMs: Type13.Optional(
4110
+ Type13.Number({
4111
+ description: "How long to keep the subscription active. Defaults to 14 days and is capped at 30 days."
4112
+ })
4113
+ )
4114
+ },
4115
+ { additionalProperties: false }
4116
+ );
4117
+ var cancelInputSchema = Type13.Object(
4118
+ {
4119
+ subscriptionId: Type13.String({
4120
+ description: "Subscription id returned by subscribeToResourceEvents or listResourceEventSubscriptions."
4121
+ })
4122
+ },
4123
+ { additionalProperties: false }
4124
+ );
4125
+ function requireConversationContext(context) {
4126
+ if (!context.conversationId) {
4127
+ throw new Error("Resource event subscriptions require a conversation");
4128
+ }
4129
+ if (context.destination.platform !== "slack") {
4130
+ throw new Error(
4131
+ "Resource event subscriptions currently require Slack delivery"
4132
+ );
4133
+ }
4134
+ if (!isSlackThreadConversationId(context.conversationId)) {
4135
+ throw new Error(
4136
+ "Resource event subscriptions require a Slack thread conversation"
4137
+ );
4138
+ }
4139
+ return context.conversationId;
4140
+ }
4141
+ function canUseResourceEventSubscriptionTools(context) {
4142
+ return context.destination.platform === "slack" && Boolean(
4143
+ context.conversationId && isSlackThreadConversationId(context.conversationId)
4144
+ );
4145
+ }
4146
+ function isSlackThreadConversationId(conversationId) {
4147
+ const parts = conversationId.split(":");
4148
+ return parts.length === 3 && parts[0] === "slack" && Boolean(parts[1]) && Boolean(parts[2]);
4149
+ }
4150
+ function cleanStrings(values) {
4151
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
4152
+ }
4153
+ function ttlMs(input) {
4154
+ if (input.ttlMs === void 0) {
4155
+ return DEFAULT_TTL_MS;
4156
+ }
4157
+ if (!Number.isFinite(input.ttlMs) || input.ttlMs <= 0) {
4158
+ throw new Error("ttlMs must be a positive finite number");
4159
+ }
4160
+ return Math.min(input.ttlMs, MAX_TTL_MS);
4161
+ }
4162
+ function createSubscribeToResourceEventsTool(context) {
4163
+ return tool({
4164
+ description: "Subscribe the current conversation to high-signal events for a resource returned by a subscribable tool result. Matching events are queued as normal conversation messages; they do not interrupt active work.",
4165
+ inputSchema: subscribeInputSchema,
4166
+ async execute(input) {
4167
+ const conversationId = requireConversationContext(context);
4168
+ const events = cleanStrings(input.events);
4169
+ const intent = input.intent.trim();
4170
+ if (!intent) {
4171
+ throw new Error("intent is required");
4172
+ }
4173
+ const nowMs = Date.now();
4174
+ const subscription = await createResourceEventSubscription({
4175
+ conversationId,
4176
+ destination: context.destination,
4177
+ events,
4178
+ expiresAtMs: nowMs + ttlMs(input),
4179
+ intent,
4180
+ label: input.label.trim(),
4181
+ provider: input.provider.trim(),
4182
+ resourceRef: input.resourceRef.trim(),
4183
+ resourceType: input.resourceType.trim()
4184
+ });
4185
+ return {
4186
+ id: subscription.id,
4187
+ status: subscription.status,
4188
+ resourceRef: subscription.resourceRef,
4189
+ events: subscription.events,
4190
+ expiresAtMs: subscription.expiresAtMs
4191
+ };
4192
+ }
4193
+ });
4194
+ }
4195
+ function createListResourceEventSubscriptionsTool(context) {
4196
+ return tool({
4197
+ description: "List active resource event subscriptions for the current conversation.",
4198
+ inputSchema: Type13.Object({}, { additionalProperties: false }),
4199
+ async execute() {
4200
+ const conversationId = requireConversationContext(context);
4201
+ const subscriptions = await listResourceEventSubscriptions({
4202
+ conversationId
4203
+ });
4204
+ return {
4205
+ subscriptions: subscriptions.map((subscription) => ({
4206
+ id: subscription.id,
4207
+ label: subscription.label,
4208
+ resourceRef: subscription.resourceRef,
4209
+ provider: subscription.provider,
4210
+ resourceType: subscription.resourceType,
4211
+ events: subscription.events,
4212
+ intent: subscription.intent,
4213
+ expiresAtMs: subscription.expiresAtMs
4214
+ }))
4215
+ };
4216
+ }
4217
+ });
4218
+ }
4219
+ function createCancelResourceEventSubscriptionTool(context) {
4220
+ return tool({
4221
+ description: "Cancel a resource event subscription for the current conversation.",
4222
+ inputSchema: cancelInputSchema,
4223
+ async execute(input) {
4224
+ const conversationId = requireConversationContext(context);
4225
+ const subscription = await cancelResourceEventSubscription({
4226
+ conversationId,
4227
+ id: input.subscriptionId
4228
+ });
4229
+ if (!subscription) {
4230
+ throw new Error("Resource event subscription was not found");
4231
+ }
4232
+ return {
4233
+ id: subscription.id,
4234
+ status: subscription.status
4235
+ };
4236
+ }
4237
+ });
4238
+ }
4239
+
4240
+ // src/chat/tools/slack/channel-list-messages.ts
4241
+ import { Type as Type14 } from "@sinclair/typebox";
4242
+
3775
4243
  // src/chat/slack/channel.ts
3776
4244
  async function listChannelMessages(input) {
3777
4245
  const client = getSlackClient();
@@ -3861,39 +4329,39 @@ function createSlackChannelListMessagesTool(context) {
3861
4329
  return tool({
3862
4330
  description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
3863
4331
  annotations: { readOnlyHint: true, destructiveHint: false },
3864
- inputSchema: Type13.Object({
3865
- limit: Type13.Optional(
3866
- Type13.Integer({
4332
+ inputSchema: Type14.Object({
4333
+ limit: Type14.Optional(
4334
+ Type14.Integer({
3867
4335
  minimum: 1,
3868
4336
  maximum: 1e3,
3869
4337
  description: "Maximum number of messages to return across pages."
3870
4338
  })
3871
4339
  ),
3872
- cursor: Type13.Optional(
3873
- Type13.String({
4340
+ cursor: Type14.Optional(
4341
+ Type14.String({
3874
4342
  minLength: 1,
3875
4343
  description: "Optional cursor to continue from a prior call."
3876
4344
  })
3877
4345
  ),
3878
- oldest: Type13.Optional(
3879
- Type13.String({
4346
+ oldest: Type14.Optional(
4347
+ Type14.String({
3880
4348
  minLength: 1,
3881
4349
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
3882
4350
  })
3883
4351
  ),
3884
- latest: Type13.Optional(
3885
- Type13.String({
4352
+ latest: Type14.Optional(
4353
+ Type14.String({
3886
4354
  minLength: 1,
3887
4355
  description: "Optional latest message timestamp (Slack ts) for range filtering."
3888
4356
  })
3889
4357
  ),
3890
- inclusive: Type13.Optional(
3891
- Type13.Boolean({
4358
+ inclusive: Type14.Optional(
4359
+ Type14.Boolean({
3892
4360
  description: "Whether oldest/latest bounds should be inclusive."
3893
4361
  })
3894
4362
  ),
3895
- max_pages: Type13.Optional(
3896
- Type13.Integer({
4363
+ max_pages: Type14.Optional(
4364
+ Type14.Integer({
3897
4365
  minimum: 1,
3898
4366
  maximum: 10,
3899
4367
  description: "Maximum number of API pages to traverse in a single call."
@@ -3953,7 +4421,7 @@ function createSlackChannelListMessagesTool(context) {
3953
4421
  }
3954
4422
 
3955
4423
  // src/chat/tools/slack/channel-post-message.ts
3956
- import { Type as Type14 } from "@sinclair/typebox";
4424
+ import { Type as Type15 } from "@sinclair/typebox";
3957
4425
 
3958
4426
  // src/chat/slack/outbound.ts
3959
4427
  var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
@@ -3997,7 +4465,13 @@ async function getPermalinkBestEffort(args) {
3997
4465
  message_ts: args.messageTs
3998
4466
  }),
3999
4467
  3,
4000
- { action: "chat.getPermalink" }
4468
+ {
4469
+ action: "chat.getPermalink",
4470
+ spanAttributes: {
4471
+ "app.slack.channel_id": args.channelId,
4472
+ "app.slack.message_ts": args.messageTs
4473
+ }
4474
+ }
4001
4475
  );
4002
4476
  return response.permalink;
4003
4477
  } catch {
@@ -4024,7 +4498,13 @@ async function postSlackMessage(input) {
4024
4498
  ...threadTs ? { thread_ts: threadTs } : {}
4025
4499
  }),
4026
4500
  3,
4027
- { action: "chat.postMessage" }
4501
+ {
4502
+ action: "chat.postMessage",
4503
+ spanAttributes: {
4504
+ "app.slack.channel_id": channelId,
4505
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : {}
4506
+ }
4507
+ }
4028
4508
  );
4029
4509
  if (!response.ts) {
4030
4510
  throw new Error("Slack message posted without ts");
@@ -4054,7 +4534,13 @@ async function deleteSlackMessage(input) {
4054
4534
  ts: timestamp
4055
4535
  }),
4056
4536
  3,
4057
- { action: "chat.delete" }
4537
+ {
4538
+ action: "chat.delete",
4539
+ spanAttributes: {
4540
+ "app.slack.channel_id": channelId,
4541
+ "app.slack.message_ts": timestamp
4542
+ }
4543
+ }
4058
4544
  );
4059
4545
  }
4060
4546
  async function postSlackEphemeralMessage(input) {
@@ -4082,7 +4568,14 @@ async function postSlackEphemeralMessage(input) {
4082
4568
  ...threadTs ? { thread_ts: threadTs } : {}
4083
4569
  }),
4084
4570
  3,
4085
- { action: "chat.postEphemeral" }
4571
+ {
4572
+ action: "chat.postEphemeral",
4573
+ spanAttributes: {
4574
+ "app.slack.channel_id": channelId,
4575
+ "app.slack.user_id": userId,
4576
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : {}
4577
+ }
4578
+ }
4086
4579
  );
4087
4580
  return {
4088
4581
  messageTs: response.message_ts
@@ -4119,7 +4612,13 @@ async function uploadFilesToThread(input) {
4119
4612
  file_uploads: fileUploads
4120
4613
  }),
4121
4614
  3,
4122
- { action: "filesUploadV2" }
4615
+ {
4616
+ action: "filesUploadV2",
4617
+ spanAttributes: {
4618
+ "app.slack.channel_id": channelId,
4619
+ "app.slack.thread_ts": threadTs
4620
+ }
4621
+ }
4123
4622
  );
4124
4623
  }
4125
4624
  async function addReactionToMessage(input) {
@@ -4143,7 +4642,14 @@ async function addReactionToMessage(input) {
4143
4642
  name: emoji
4144
4643
  }),
4145
4644
  3,
4146
- { action: "reactions.add" }
4645
+ {
4646
+ action: "reactions.add",
4647
+ spanAttributes: {
4648
+ "app.slack.channel_id": channelId,
4649
+ "app.slack.message_ts": timestamp,
4650
+ "app.slack.reaction": emoji
4651
+ }
4652
+ }
4147
4653
  );
4148
4654
  } catch (error) {
4149
4655
  if (error instanceof SlackActionError && error.code === "already_reacted") {
@@ -4174,7 +4680,14 @@ async function removeReactionFromMessage(input) {
4174
4680
  name: emoji
4175
4681
  }),
4176
4682
  3,
4177
- { action: "reactions.remove" }
4683
+ {
4684
+ action: "reactions.remove",
4685
+ spanAttributes: {
4686
+ "app.slack.channel_id": channelId,
4687
+ "app.slack.message_ts": timestamp,
4688
+ "app.slack.reaction": emoji
4689
+ }
4690
+ }
4178
4691
  );
4179
4692
  } catch (error) {
4180
4693
  if (error instanceof SlackActionError && error.code === "no_reaction") {
@@ -4205,8 +4718,8 @@ function createOperationKey(toolName, input) {
4205
4718
  function createSlackChannelPostMessageTool(context, state) {
4206
4719
  return tool({
4207
4720
  description: "Post a new top-level message to the current Slack channel. Use only when the user explicitly asks to post/send/share/say something to the channel. Do not use for thread replies, inline @mentions, or pinging mentioned users.",
4208
- inputSchema: Type14.Object({
4209
- text: Type14.String({
4721
+ inputSchema: Type15.Object({
4722
+ text: Type15.String({
4210
4723
  minLength: 1,
4211
4724
  maxLength: 4e4,
4212
4725
  description: "Slack mrkdwn text to post."
@@ -4246,12 +4759,12 @@ function createSlackChannelPostMessageTool(context, state) {
4246
4759
  }
4247
4760
 
4248
4761
  // src/chat/tools/slack/message-add-reaction.ts
4249
- import { Type as Type15 } from "@sinclair/typebox";
4762
+ import { Type as Type16 } from "@sinclair/typebox";
4250
4763
  function createSlackMessageAddReactionTool(context, state) {
4251
4764
  return tool({
4252
4765
  description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
4253
- inputSchema: Type15.Object({
4254
- emoji: Type15.String({
4766
+ inputSchema: Type16.Object({
4767
+ emoji: Type16.String({
4255
4768
  minLength: 1,
4256
4769
  maxLength: 64,
4257
4770
  description: "Slack emoji alias name to react with (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`). Optional surrounding colons are allowed."
@@ -4303,7 +4816,7 @@ function createSlackMessageAddReactionTool(context, state) {
4303
4816
  }
4304
4817
 
4305
4818
  // src/chat/tools/slack/canvas-tools.ts
4306
- import { Type as Type16 } from "@sinclair/typebox";
4819
+ import { Type as Type17 } from "@sinclair/typebox";
4307
4820
 
4308
4821
  // src/chat/slack/canvas-references.ts
4309
4822
  var SLACK_FILE_ID_PATTERN = /^F[A-Z0-9]{4,}$/i;
@@ -4548,13 +5061,13 @@ function resolveCanvasTarget(canvas) {
4548
5061
  }
4549
5062
  return { ok: true, canvasId };
4550
5063
  }
4551
- var editReplacementSchema2 = Type16.Object(
5064
+ var editReplacementSchema2 = Type17.Object(
4552
5065
  {
4553
- oldText: Type16.String({
5066
+ oldText: Type17.String({
4554
5067
  minLength: 1,
4555
5068
  description: "Exact Canvas markdown to replace. It must be unique in the current Canvas body and must not overlap another edit."
4556
5069
  }),
4557
- newText: Type16.String({
5070
+ newText: Type17.String({
4558
5071
  description: "Replacement Canvas markdown for this edit."
4559
5072
  })
4560
5073
  },
@@ -4563,13 +5076,13 @@ var editReplacementSchema2 = Type16.Object(
4563
5076
  function createSlackCanvasCreateTool(context, state) {
4564
5077
  return tool({
4565
5078
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when the answer is better as a reusable document than a thread reply: long-form research, timelines, bios/profiles, structured notes, plans, comparisons, or anything likely to exceed one compact Slack reply. After creating it, reply with one or two short sentences plus the canvas link; do not recap the canvas contents. Do not use for short answers that fit cleanly in one normal thread reply.",
4566
- inputSchema: Type16.Object({
4567
- title: Type16.String({
5079
+ inputSchema: Type17.Object({
5080
+ title: Type17.String({
4568
5081
  minLength: 1,
4569
5082
  maxLength: 160,
4570
5083
  description: "Canvas title."
4571
5084
  }),
4572
- markdown: Type16.String({
5085
+ markdown: Type17.String({
4573
5086
  minLength: 1,
4574
5087
  description: "Canvas markdown body content."
4575
5088
  })
@@ -4635,20 +5148,20 @@ function createSlackCanvasReadTool() {
4635
5148
  return tool({
4636
5149
  description: "Read a bounded line range from a Slack canvas as markdown. Use when you need exact Canvas contents to verify facts or make edits safely. Do not use for generic web pages \u2014 use webFetch for those.",
4637
5150
  annotations: { readOnlyHint: true, destructiveHint: false },
4638
- inputSchema: Type16.Object(
5151
+ inputSchema: Type17.Object(
4639
5152
  {
4640
- canvas: Type16.String({
5153
+ canvas: Type17.String({
4641
5154
  minLength: 1,
4642
5155
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
4643
5156
  }),
4644
- offset: Type16.Optional(
4645
- Type16.Integer({
5157
+ offset: Type17.Optional(
5158
+ Type17.Integer({
4646
5159
  minimum: 1,
4647
5160
  description: "1-indexed line number to start reading from."
4648
5161
  })
4649
5162
  ),
4650
- limit: Type16.Optional(
4651
- Type16.Integer({
5163
+ limit: Type17.Optional(
5164
+ Type17.Integer({
4652
5165
  minimum: 1,
4653
5166
  description: "Maximum number of lines to read. Defaults to 1000."
4654
5167
  })
@@ -4709,13 +5222,13 @@ function createSlackCanvasEditTool(state) {
4709
5222
  description: "Edit one Slack canvas with exact markdown replacements. Use for precise changes to existing Canvas content; prefer this over slackCanvasWrite for targeted changes. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff. Multiple changes to the same canvas: use one edits[] call.",
4710
5223
  prepareArguments: prepareCanvasEditArguments,
4711
5224
  executionMode: "sequential",
4712
- inputSchema: Type16.Object(
5225
+ inputSchema: Type17.Object(
4713
5226
  {
4714
- canvas: Type16.String({
5227
+ canvas: Type17.String({
4715
5228
  minLength: 1,
4716
5229
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
4717
5230
  }),
4718
- edits: Type16.Array(editReplacementSchema2, {
5231
+ edits: Type17.Array(editReplacementSchema2, {
4719
5232
  minItems: 1,
4720
5233
  description: "Exact replacements matched against the current Canvas body, not incrementally."
4721
5234
  })
@@ -4795,13 +5308,13 @@ function createSlackCanvasWriteTool(state) {
4795
5308
  return tool({
4796
5309
  description: "Write UTF-8 markdown content to a Slack canvas. Use for deliberate full-Canvas replacement after validation; use slackCanvasEdit for targeted changes to existing canvas content.",
4797
5310
  executionMode: "sequential",
4798
- inputSchema: Type16.Object(
5311
+ inputSchema: Type17.Object(
4799
5312
  {
4800
- canvas: Type16.String({
5313
+ canvas: Type17.String({
4801
5314
  minLength: 1,
4802
5315
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
4803
5316
  }),
4804
- content: Type16.String({
5317
+ content: Type17.String({
4805
5318
  description: "UTF-8 markdown content to write."
4806
5319
  })
4807
5320
  },
@@ -4862,7 +5375,7 @@ function createSlackCanvasWriteTool(state) {
4862
5375
  }
4863
5376
 
4864
5377
  // src/chat/tools/slack/list-tools.ts
4865
- import { Type as Type17 } from "@sinclair/typebox";
5378
+ import { Type as Type18 } from "@sinclair/typebox";
4866
5379
 
4867
5380
  // src/chat/tools/slack/lists.ts
4868
5381
  function normalizeKey(value) {
@@ -4918,7 +5431,9 @@ async function createTodoList(name) {
4918
5431
  name,
4919
5432
  schema: DEFAULT_TODO_SCHEMA,
4920
5433
  todo_mode: true
4921
- })
5434
+ }),
5435
+ 3,
5436
+ { action: "slackLists.create" }
4922
5437
  );
4923
5438
  if (!result.list_id) {
4924
5439
  throw new Error("Slack list was created without list_id");
@@ -4964,7 +5479,9 @@ async function addListItems(input) {
4964
5479
  () => client.slackLists.items.create({
4965
5480
  list_id: input.listId,
4966
5481
  initial_fields: initialFields
4967
- })
5482
+ }),
5483
+ 3,
5484
+ { action: "slackLists.items.create" }
4968
5485
  );
4969
5486
  if (response.item?.id) {
4970
5487
  createdItemIds.push(response.item.id);
@@ -4986,7 +5503,9 @@ async function listItems(listId, limit = 100) {
4986
5503
  list_id: listId,
4987
5504
  limit: cappedLimit,
4988
5505
  cursor
4989
- })
5506
+ }),
5507
+ 3,
5508
+ { action: "slackLists.items.list" }
4990
5509
  );
4991
5510
  const remaining = cappedLimit - items.length;
4992
5511
  if (remaining <= 0) {
@@ -5028,7 +5547,9 @@ async function updateListItem(input) {
5028
5547
  () => client.slackLists.items.update({
5029
5548
  list_id: input.listId,
5030
5549
  cells
5031
- })
5550
+ }),
5551
+ 3,
5552
+ { action: "slackLists.items.update" }
5032
5553
  );
5033
5554
  }
5034
5555
 
@@ -5036,8 +5557,8 @@ async function updateListItem(input) {
5036
5557
  function createSlackListCreateTool(state) {
5037
5558
  return tool({
5038
5559
  description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
5039
- inputSchema: Type17.Object({
5040
- name: Type17.String({
5560
+ inputSchema: Type18.Object({
5561
+ name: Type18.String({
5041
5562
  minLength: 1,
5042
5563
  maxLength: 160,
5043
5564
  description: "Name for the new Slack list."
@@ -5072,20 +5593,20 @@ function createSlackListCreateTool(state) {
5072
5593
  function createSlackListAddItemsTool(state) {
5073
5594
  return tool({
5074
5595
  description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
5075
- inputSchema: Type17.Object({
5076
- items: Type17.Array(Type17.String({ minLength: 1 }), {
5596
+ inputSchema: Type18.Object({
5597
+ items: Type18.Array(Type18.String({ minLength: 1 }), {
5077
5598
  minItems: 1,
5078
5599
  maxItems: 25,
5079
5600
  description: "List item titles to create."
5080
5601
  }),
5081
- assignee_user_id: Type17.Optional(
5082
- Type17.String({
5602
+ assignee_user_id: Type18.Optional(
5603
+ Type18.String({
5083
5604
  minLength: 1,
5084
5605
  description: "Optional Slack user ID assigned to all created items."
5085
5606
  })
5086
5607
  ),
5087
- due_date: Type17.Optional(
5088
- Type17.String({
5608
+ due_date: Type18.Optional(
5609
+ Type18.String({
5089
5610
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
5090
5611
  description: "Optional due date in YYYY-MM-DD format."
5091
5612
  })
@@ -5135,9 +5656,9 @@ function createSlackListGetItemsTool(state) {
5135
5656
  return tool({
5136
5657
  description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
5137
5658
  annotations: { readOnlyHint: true, destructiveHint: false },
5138
- inputSchema: Type17.Object({
5139
- limit: Type17.Optional(
5140
- Type17.Integer({
5659
+ inputSchema: Type18.Object({
5660
+ limit: Type18.Optional(
5661
+ Type18.Integer({
5141
5662
  minimum: 1,
5142
5663
  maximum: 200,
5143
5664
  description: "Maximum number of list items to return."
@@ -5162,19 +5683,19 @@ function createSlackListGetItemsTool(state) {
5162
5683
  function createSlackListUpdateItemTool(state) {
5163
5684
  return tool({
5164
5685
  description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
5165
- inputSchema: Type17.Object(
5686
+ inputSchema: Type18.Object(
5166
5687
  {
5167
- item_id: Type17.String({
5688
+ item_id: Type18.String({
5168
5689
  minLength: 1,
5169
5690
  description: "ID of the Slack list item to update."
5170
5691
  }),
5171
- completed: Type17.Optional(
5172
- Type17.Boolean({
5692
+ completed: Type18.Optional(
5693
+ Type18.Boolean({
5173
5694
  description: "Optional completion status update."
5174
5695
  })
5175
5696
  ),
5176
- title: Type17.Optional(
5177
- Type17.String({
5697
+ title: Type18.Optional(
5698
+ Type18.String({
5178
5699
  minLength: 1,
5179
5700
  description: "Optional new item title."
5180
5701
  })
@@ -5224,7 +5745,7 @@ function createSlackListUpdateItemTool(state) {
5224
5745
  }
5225
5746
 
5226
5747
  // src/chat/tools/slack/thread-read.ts
5227
- import { Type as Type18 } from "@sinclair/typebox";
5748
+ import { Type as Type19 } from "@sinclair/typebox";
5228
5749
 
5229
5750
  // src/chat/tools/slack/slack-message-url.ts
5230
5751
  var SLACK_HOST_PATTERN = /^[a-z0-9-]+\.slack(?:-gov)?\.com$/;
@@ -5406,34 +5927,34 @@ function createSlackThreadReadTool(context) {
5406
5927
  return tool({
5407
5928
  description: "Read a Slack thread from a shared Slack message archive URL or explicit channel + timestamp. Use when the user shares a Slack message link (https://*.slack.com/archives/...) and you need the referenced message and its thread context. Public channel links can be read if the bot has access; private channels and DMs are only readable when they are the current conversation.",
5408
5929
  annotations: { readOnlyHint: true, destructiveHint: false },
5409
- inputSchema: Type18.Object({
5410
- url: Type18.Optional(
5411
- Type18.String({
5930
+ inputSchema: Type19.Object({
5931
+ url: Type19.Optional(
5932
+ Type19.String({
5412
5933
  minLength: 1,
5413
5934
  description: "Slack message archive URL, e.g. https://workspace.slack.com/archives/C123/p1700000000123456"
5414
5935
  })
5415
5936
  ),
5416
- channel_id: Type18.Optional(
5417
- Type18.String({
5937
+ channel_id: Type19.Optional(
5938
+ Type19.String({
5418
5939
  minLength: 1,
5419
5940
  description: "Slack channel/conversation ID (e.g. C123). Use with `ts` as an alternative to `url`."
5420
5941
  })
5421
5942
  ),
5422
- ts: Type18.Optional(
5423
- Type18.String({
5943
+ ts: Type19.Optional(
5944
+ Type19.String({
5424
5945
  minLength: 1,
5425
5946
  description: "Slack message timestamp (e.g. 1700000000.123456). May be the thread root or any message in the thread."
5426
5947
  })
5427
5948
  ),
5428
- limit: Type18.Optional(
5429
- Type18.Integer({
5949
+ limit: Type19.Optional(
5950
+ Type19.Integer({
5430
5951
  minimum: 1,
5431
5952
  maximum: 1e3,
5432
5953
  description: "Maximum number of thread messages to fetch."
5433
5954
  })
5434
5955
  ),
5435
- max_pages: Type18.Optional(
5436
- Type18.Integer({
5956
+ max_pages: Type19.Optional(
5957
+ Type19.Integer({
5437
5958
  minimum: 1,
5438
5959
  maximum: 10,
5439
5960
  description: "Maximum number of Slack API pages to traverse."
@@ -5528,7 +6049,7 @@ function createSlackThreadReadTool(context) {
5528
6049
  }
5529
6050
 
5530
6051
  // src/chat/tools/slack/user-lookup.ts
5531
- import { Type as Type19 } from "@sinclair/typebox";
6052
+ import { Type as Type20 } from "@sinclair/typebox";
5532
6053
 
5533
6054
  // src/chat/slack/users.ts
5534
6055
  function normalizeUser(raw) {
@@ -5674,41 +6195,41 @@ function createSlackUserLookupTool() {
5674
6195
  return tool({
5675
6196
  description: "Look up Slack user profiles by user ID, email, or name search. Use when you need to identify a user, resolve cross-platform identity, or look up profile details like title or status. Returns profile fields including custom fields. For user ID lookup, pass a Slack user ID (e.g. U039RR91S). For search, pass a name query.",
5676
6197
  annotations: { readOnlyHint: true, destructiveHint: false },
5677
- inputSchema: Type19.Object({
5678
- user_id: Type19.Optional(
5679
- Type19.String({
6198
+ inputSchema: Type20.Object({
6199
+ user_id: Type20.Optional(
6200
+ Type20.String({
5680
6201
  minLength: 1,
5681
6202
  description: "Slack user ID to look up (e.g. U039RR91S). Mutually exclusive with email and query."
5682
6203
  })
5683
6204
  ),
5684
- email: Type19.Optional(
5685
- Type19.String({
6205
+ email: Type20.Optional(
6206
+ Type20.String({
5686
6207
  minLength: 3,
5687
6208
  description: "Email address to look up. Mutually exclusive with user_id and query."
5688
6209
  })
5689
6210
  ),
5690
- query: Type19.Optional(
5691
- Type19.String({
6211
+ query: Type20.Optional(
6212
+ Type20.String({
5692
6213
  minLength: 2,
5693
6214
  description: "Name to search for (matches against username, display name, real name). Mutually exclusive with user_id and email."
5694
6215
  })
5695
6216
  ),
5696
- limit: Type19.Optional(
5697
- Type19.Integer({
6217
+ limit: Type20.Optional(
6218
+ Type20.Integer({
5698
6219
  minimum: 1,
5699
6220
  maximum: 20,
5700
6221
  description: "Maximum number of results to return for name search. Defaults to 10."
5701
6222
  })
5702
6223
  ),
5703
- max_pages: Type19.Optional(
5704
- Type19.Integer({
6224
+ max_pages: Type20.Optional(
6225
+ Type20.Integer({
5705
6226
  minimum: 1,
5706
6227
  maximum: 5,
5707
6228
  description: "Maximum number of Slack API pages to scan for name search. Defaults to 3."
5708
6229
  })
5709
6230
  ),
5710
- include_bots: Type19.Optional(
5711
- Type19.Boolean({
6231
+ include_bots: Type20.Optional(
6232
+ Type20.Boolean({
5712
6233
  description: "Include bot accounts in name search results. Defaults to false."
5713
6234
  })
5714
6235
  )
@@ -5787,12 +6308,12 @@ function createSlackUserLookupTool() {
5787
6308
  }
5788
6309
 
5789
6310
  // src/chat/tools/system-time.ts
5790
- import { Type as Type20 } from "@sinclair/typebox";
6311
+ import { Type as Type21 } from "@sinclair/typebox";
5791
6312
  function createSystemTimeTool() {
5792
6313
  return tool({
5793
6314
  description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
5794
6315
  annotations: { readOnlyHint: true, destructiveHint: false },
5795
- inputSchema: Type20.Object({}),
6316
+ inputSchema: Type21.Object({}),
5796
6317
  execute: async () => {
5797
6318
  const now = /* @__PURE__ */ new Date();
5798
6319
  return {
@@ -5810,7 +6331,7 @@ function createSystemTimeTool() {
5810
6331
  import {
5811
6332
  Agent
5812
6333
  } from "@earendil-works/pi-agent-core";
5813
- import { Type as Type21 } from "@sinclair/typebox";
6334
+ import { Type as Type22 } from "@sinclair/typebox";
5814
6335
  import { THREAD_STATE_TTL_MS } from "chat";
5815
6336
 
5816
6337
  // src/chat/tools/advisor/session-store.ts
@@ -5898,12 +6419,12 @@ function createAdvisorTool(context) {
5898
6419
  const spanContext = context.logContext ?? {};
5899
6420
  return tool({
5900
6421
  description: ADVISOR_TOOL_DESCRIPTION,
5901
- inputSchema: Type21.Object({
5902
- question: Type21.String({
6422
+ inputSchema: Type22.Object({
6423
+ question: Type22.String({
5903
6424
  minLength: 1,
5904
6425
  description: "Focused advisor question or decision point."
5905
6426
  }),
5906
- context: Type21.String({
6427
+ context: Type22.String({
5907
6428
  minLength: 1,
5908
6429
  description: "Curated evidence packet: relevant requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions."
5909
6430
  })
@@ -6079,7 +6600,7 @@ function createAdvisorTool(context) {
6079
6600
  }
6080
6601
 
6081
6602
  // src/chat/tools/web/fetch-tool.ts
6082
- import { Type as Type22 } from "@sinclair/typebox";
6603
+ import { Type as Type23 } from "@sinclair/typebox";
6083
6604
 
6084
6605
  // src/chat/tools/web/constants.ts
6085
6606
  var USER_AGENT = "junior-bot/0.1";
@@ -6524,13 +7045,13 @@ function createWebFetchTool(hooks) {
6524
7045
  destructiveHint: false,
6525
7046
  openWorldHint: true
6526
7047
  },
6527
- inputSchema: Type22.Object({
6528
- url: Type22.String({
7048
+ inputSchema: Type23.Object({
7049
+ url: Type23.String({
6529
7050
  minLength: 1,
6530
7051
  description: "HTTP(S) URL to fetch."
6531
7052
  }),
6532
- max_chars: Type22.Optional(
6533
- Type22.Integer({
7053
+ max_chars: Type23.Optional(
7054
+ Type23.Integer({
6534
7055
  minimum: 500,
6535
7056
  maximum: MAX_FETCH_CHARS,
6536
7057
  description: "Optional maximum number of extracted characters to return."
@@ -6594,7 +7115,7 @@ function createWebFetchTool(hooks) {
6594
7115
  import { generateText } from "ai";
6595
7116
  import { createGatewayProvider } from "@ai-sdk/gateway";
6596
7117
  import { getModel } from "@earendil-works/pi-ai";
6597
- import { Type as Type23 } from "@sinclair/typebox";
7118
+ import { Type as Type24 } from "@sinclair/typebox";
6598
7119
  var SEARCH_TIMEOUT_MS = 6e4;
6599
7120
  var MAX_RESULTS2 = 5;
6600
7121
  var DEFAULT_SEARCH_MODEL = getModel("vercel-ai-gateway", "openai/gpt-5.4").id;
@@ -6642,14 +7163,14 @@ function createWebSearchTool(override) {
6642
7163
  destructiveHint: false,
6643
7164
  openWorldHint: true
6644
7165
  },
6645
- inputSchema: Type23.Object({
6646
- query: Type23.String({
7166
+ inputSchema: Type24.Object({
7167
+ query: Type24.String({
6647
7168
  minLength: 1,
6648
7169
  maxLength: 500,
6649
7170
  description: "Search query."
6650
7171
  }),
6651
- max_results: Type23.Optional(
6652
- Type23.Integer({
7172
+ max_results: Type24.Optional(
7173
+ Type24.Integer({
6653
7174
  minimum: 1,
6654
7175
  maximum: MAX_RESULTS2,
6655
7176
  description: "Max results to return."
@@ -6721,18 +7242,18 @@ function createWebSearchTool(override) {
6721
7242
  }
6722
7243
 
6723
7244
  // src/chat/tools/sandbox/write-file.ts
6724
- import { Type as Type24 } from "@sinclair/typebox";
7245
+ import { Type as Type25 } from "@sinclair/typebox";
6725
7246
  function createWriteFileTool() {
6726
7247
  return tool({
6727
7248
  description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or deliberate full-file replacement after validation; use editFile instead for targeted changes to existing files. Do not use for exploratory analysis-only turns.",
6728
7249
  executionMode: "sequential",
6729
- inputSchema: Type24.Object(
7250
+ inputSchema: Type25.Object(
6730
7251
  {
6731
- path: Type24.String({
7252
+ path: Type25.String({
6732
7253
  minLength: 1,
6733
7254
  description: "Path to write in the sandbox workspace."
6734
7255
  }),
6735
- content: Type24.String({
7256
+ content: Type25.String({
6736
7257
  description: "UTF-8 file content to write."
6737
7258
  })
6738
7259
  },
@@ -6801,6 +7322,11 @@ function createTools(availableSkills, hooks = {}, context) {
6801
7322
  if (context.advisor) {
6802
7323
  tools.advisor = createAdvisorTool(context.advisor);
6803
7324
  }
7325
+ if (canUseResourceEventSubscriptionTools(context)) {
7326
+ tools.subscribeToResourceEvents = createSubscribeToResourceEventsTool(context);
7327
+ tools.listResourceEventSubscriptions = createListResourceEventSubscriptionsTool(context);
7328
+ tools.cancelResourceEventSubscription = createCancelResourceEventSubscriptionTool(context);
7329
+ }
6804
7330
  if (context.mcpToolManager) {
6805
7331
  tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
6806
7332
  tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
@@ -7062,7 +7588,14 @@ async function deliverPrivateMessage(input) {
7062
7588
  }
7063
7589
  }
7064
7590
  try {
7065
- const dmChannelId = (await client.conversations.open({ users: input.userId })).channel?.id;
7591
+ const dmChannelId = (await withSlackRetries(
7592
+ () => client.conversations.open({ users: input.userId }),
7593
+ 3,
7594
+ {
7595
+ action: "conversations.open",
7596
+ spanAttributes: { "app.slack.user_id": input.userId }
7597
+ }
7598
+ )).channel?.id;
7066
7599
  if (!dmChannelId) {
7067
7600
  logWarn(
7068
7601
  "oauth_dm_fallback_failed",
@@ -7162,63 +7695,63 @@ async function startOAuthFlow(provider, input) {
7162
7695
  };
7163
7696
  }
7164
7697
 
7165
- // src/chat/sandbox/egress-session.ts
7698
+ // src/chat/sandbox/egress/session.ts
7166
7699
  import { createHmac, randomUUID, timingSafeEqual } from "crypto";
7167
7700
 
7168
- // src/chat/sandbox/egress-schemas.ts
7169
- import { z } from "zod";
7701
+ // src/chat/sandbox/egress/schemas.ts
7702
+ import { z as z2 } from "zod";
7170
7703
  import {
7171
7704
  pluginAuthorizationSchema,
7172
7705
  pluginCredentialHeaderTransformSchema,
7173
7706
  pluginGrantSchema,
7174
7707
  pluginProviderAccountSchema
7175
7708
  } from "@sentry/junior-plugin-api";
7176
- var finiteNumberSchema = z.number().refine(Number.isFinite);
7177
- var httpStatusSchema = z.number().int().min(100).max(599);
7178
- var providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
7179
- var credentialSignalKindSchema = z.enum(["auth_required", "unavailable"]);
7709
+ var finiteNumberSchema = z2.number().refine(Number.isFinite);
7710
+ var httpStatusSchema = z2.number().int().min(100).max(599);
7711
+ var providerNameSchema = z2.string().regex(/^[a-z][a-z0-9-]*$/);
7712
+ var credentialSignalKindSchema = z2.enum(["auth_required", "unavailable"]);
7180
7713
  var sandboxEgressGrantSchema = pluginGrantSchema;
7181
- var sandboxEgressCredentialContextSchema = z.object({
7714
+ var sandboxEgressCredentialContextSchema = z2.object({
7182
7715
  credentials: credentialContextSchema,
7183
- egressId: z.string().min(1),
7716
+ egressId: z2.string().min(1),
7184
7717
  expiresAtMs: finiteNumberSchema,
7185
- contextId: z.string().min(1)
7718
+ contextId: z2.string().min(1)
7186
7719
  }).strict();
7187
- var sandboxEgressCredentialLeaseSchema = z.object({
7720
+ var sandboxEgressCredentialLeaseSchema = z2.object({
7188
7721
  account: pluginProviderAccountSchema.optional(),
7189
7722
  authorization: pluginAuthorizationSchema.optional(),
7190
7723
  grant: sandboxEgressGrantSchema,
7191
7724
  provider: providerNameSchema,
7192
- expiresAt: z.string().min(1),
7193
- headerTransforms: z.array(pluginCredentialHeaderTransformSchema).min(1)
7725
+ expiresAt: z2.string().min(1),
7726
+ headerTransforms: z2.array(pluginCredentialHeaderTransformSchema).min(1)
7194
7727
  }).strict();
7195
- var sandboxEgressAuthRequiredSignalSchema = z.object({
7728
+ var sandboxEgressAuthRequiredSignalSchema = z2.object({
7196
7729
  authorization: pluginAuthorizationSchema.optional(),
7197
7730
  grant: sandboxEgressGrantSchema,
7198
7731
  kind: credentialSignalKindSchema.default("auth_required"),
7199
7732
  provider: providerNameSchema,
7200
- message: z.string().optional(),
7733
+ message: z2.string().optional(),
7201
7734
  createdAtMs: finiteNumberSchema
7202
7735
  }).strict().superRefine((signal, ctx) => {
7203
7736
  if (signal.authorization && signal.authorization.provider !== signal.provider) {
7204
7737
  ctx.addIssue({
7205
- code: z.ZodIssueCode.custom,
7738
+ code: z2.ZodIssueCode.custom,
7206
7739
  message: "Auth signal authorization provider must match provider",
7207
7740
  path: ["authorization", "provider"]
7208
7741
  });
7209
7742
  }
7210
7743
  });
7211
- var sandboxEgressPermissionDeniedSignalSchema = z.object({
7744
+ var sandboxEgressPermissionDeniedSignalSchema = z2.object({
7212
7745
  account: pluginProviderAccountSchema.optional(),
7213
- acceptedPermissions: z.string().optional(),
7746
+ acceptedPermissions: z2.string().optional(),
7214
7747
  grant: sandboxEgressGrantSchema,
7215
- message: z.string().min(1),
7748
+ message: z2.string().min(1),
7216
7749
  provider: providerNameSchema,
7217
- source: z.literal("upstream"),
7218
- sso: z.string().optional(),
7750
+ source: z2.literal("upstream"),
7751
+ sso: z2.string().optional(),
7219
7752
  status: httpStatusSchema,
7220
- upstreamHost: z.string().min(1),
7221
- upstreamPath: z.string().min(1),
7753
+ upstreamHost: z2.string().min(1),
7754
+ upstreamPath: z2.string().min(1),
7222
7755
  createdAtMs: finiteNumberSchema
7223
7756
  }).strict();
7224
7757
  function parseSandboxEgressAuthRequiredSignal(value) {
@@ -7230,7 +7763,7 @@ function parseSandboxEgressPermissionDeniedSignal(value) {
7230
7763
  return result.success ? result.data : void 0;
7231
7764
  }
7232
7765
 
7233
- // src/chat/sandbox/egress-session.ts
7766
+ // src/chat/sandbox/egress/session.ts
7234
7767
  var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
7235
7768
  var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
7236
7769
  var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
@@ -7295,12 +7828,12 @@ function getSandboxEgressSecret() {
7295
7828
  throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
7296
7829
  }
7297
7830
  function createSandboxEgressCredentialToken(input) {
7298
- const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
7831
+ const ttlMs2 = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
7299
7832
  const now = Date.now();
7300
7833
  const context = {
7301
7834
  credentials: input.credentials,
7302
7835
  egressId: input.egressId,
7303
- expiresAtMs: now + ttlMs,
7836
+ expiresAtMs: now + ttlMs2,
7304
7837
  contextId: randomUUID()
7305
7838
  };
7306
7839
  const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(
@@ -7336,7 +7869,7 @@ async function setSandboxEgressCredentialLease(context, lease) {
7336
7869
  if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
7337
7870
  return;
7338
7871
  }
7339
- const ttlMs = Math.max(
7872
+ const ttlMs2 = Math.max(
7340
7873
  1,
7341
7874
  Math.min(leaseExpiresAtMs, context.expiresAtMs) - Date.now()
7342
7875
  );
@@ -7345,7 +7878,7 @@ async function setSandboxEgressCredentialLease(context, lease) {
7345
7878
  await state.set(
7346
7879
  leaseKey(lease.provider, lease.grant.name, context),
7347
7880
  lease,
7348
- ttlMs
7881
+ ttlMs2
7349
7882
  );
7350
7883
  }
7351
7884
  async function getSandboxEgressCredentialLease(provider, grantName, context) {
@@ -7359,7 +7892,7 @@ async function clearSandboxEgressCredentialLease(provider, grantName, context) {
7359
7892
  await state.delete(leaseKey(provider, grantName, context));
7360
7893
  }
7361
7894
  async function setSandboxEgressAuthRequiredSignal(context, signal) {
7362
- const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
7895
+ const ttlMs2 = Math.max(1, context.expiresAtMs - Date.now());
7363
7896
  const state = getStateAdapter();
7364
7897
  await state.connect();
7365
7898
  await state.set(
@@ -7368,11 +7901,11 @@ async function setSandboxEgressAuthRequiredSignal(context, signal) {
7368
7901
  ...signal,
7369
7902
  createdAtMs: Date.now()
7370
7903
  },
7371
- ttlMs
7904
+ ttlMs2
7372
7905
  );
7373
7906
  }
7374
7907
  async function setSandboxEgressPermissionDeniedSignal(context, signal) {
7375
- const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
7908
+ const ttlMs2 = Math.max(1, context.expiresAtMs - Date.now());
7376
7909
  const state = getStateAdapter();
7377
7910
  await state.connect();
7378
7911
  await state.set(
@@ -7381,7 +7914,7 @@ async function setSandboxEgressPermissionDeniedSignal(context, signal) {
7381
7914
  ...signal,
7382
7915
  createdAtMs: Date.now()
7383
7916
  },
7384
- ttlMs
7917
+ ttlMs2
7385
7918
  );
7386
7919
  }
7387
7920
  async function clearSandboxEgressSignals(egressId) {
@@ -7432,7 +7965,7 @@ async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
7432
7965
  return signal;
7433
7966
  }
7434
7967
 
7435
- // src/chat/sandbox/egress-policy.ts
7968
+ // src/chat/sandbox/egress/policy.ts
7436
7969
  function matchesSandboxEgressDomain(host, domain) {
7437
7970
  return host.toLowerCase() === domain.toLowerCase();
7438
7971
  }
@@ -9382,12 +9915,12 @@ async function getMcpAuthSession(authSessionId) {
9382
9915
  const stateAdapter = await getConnectedStateAdapter();
9383
9916
  return parseMcpAuthSession(await stateAdapter.get(sessionKey(authSessionId)));
9384
9917
  }
9385
- async function putMcpAuthSession(session, ttlMs = MCP_AUTH_SESSION_TTL_MS) {
9918
+ async function putMcpAuthSession(session, ttlMs2 = MCP_AUTH_SESSION_TTL_MS) {
9386
9919
  const stateAdapter = await getConnectedStateAdapter();
9387
9920
  await stateAdapter.set(
9388
9921
  sessionKey(session.authSessionId),
9389
9922
  JSON.stringify(session),
9390
- ttlMs
9923
+ ttlMs2
9391
9924
  );
9392
9925
  const nextIndex = parseSessionIndex(
9393
9926
  await stateAdapter.get(sessionIndexKey(session.userId, session.provider))
@@ -9398,7 +9931,7 @@ async function putMcpAuthSession(session, ttlMs = MCP_AUTH_SESSION_TTL_MS) {
9398
9931
  await stateAdapter.set(
9399
9932
  sessionIndexKey(session.userId, session.provider),
9400
9933
  JSON.stringify(nextIndex),
9401
- ttlMs
9934
+ ttlMs2
9402
9935
  );
9403
9936
  }
9404
9937
  async function patchMcpAuthSession(authSessionId, patch) {
@@ -9478,12 +10011,12 @@ async function getMcpStoredOAuthCredentials(userId, provider) {
9478
10011
  await stateAdapter.get(credentialsKey(userId, provider))
9479
10012
  );
9480
10013
  }
9481
- async function putMcpStoredOAuthCredentials(userId, provider, value, ttlMs = MCP_AUTH_CREDENTIALS_TTL_MS) {
10014
+ async function putMcpStoredOAuthCredentials(userId, provider, value, ttlMs2 = MCP_AUTH_CREDENTIALS_TTL_MS) {
9482
10015
  const stateAdapter = await getConnectedStateAdapter();
9483
10016
  await stateAdapter.set(
9484
10017
  credentialsKey(userId, provider),
9485
10018
  JSON.stringify(value),
9486
- ttlMs
10019
+ ttlMs2
9487
10020
  );
9488
10021
  }
9489
10022
  async function deleteMcpStoredOAuthCredentials(userId, provider) {
@@ -9496,7 +10029,7 @@ async function getMcpServerSessionId(userId, provider) {
9496
10029
  await stateAdapter.get(serverSessionKey(userId, provider))
9497
10030
  )?.sessionId;
9498
10031
  }
9499
- async function putMcpServerSessionId(userId, provider, sessionId, ttlMs = MCP_SERVER_SESSION_TTL_MS) {
10032
+ async function putMcpServerSessionId(userId, provider, sessionId, ttlMs2 = MCP_SERVER_SESSION_TTL_MS) {
9500
10033
  const stateAdapter = await getConnectedStateAdapter();
9501
10034
  await stateAdapter.set(
9502
10035
  serverSessionKey(userId, provider),
@@ -9504,7 +10037,7 @@ async function putMcpServerSessionId(userId, provider, sessionId, ttlMs = MCP_SE
9504
10037
  sessionId,
9505
10038
  updatedAtMs: Date.now()
9506
10039
  }),
9507
- ttlMs
10040
+ ttlMs2
9508
10041
  );
9509
10042
  }
9510
10043
  async function deleteMcpServerSessionId(userId, provider) {
@@ -9524,6 +10057,15 @@ async function unlinkProvider(userId, provider, userTokenStore) {
9524
10057
 
9525
10058
  // src/chat/services/pending-auth.ts
9526
10059
  var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
10060
+ var NON_REQUEST_SKIPPED_REASON_PREFIXES = [
10061
+ "directed_to_other_party:",
10062
+ "side_conversation:"
10063
+ ];
10064
+ function isSkippedNonRequest(reason) {
10065
+ return NON_REQUEST_SKIPPED_REASON_PREFIXES.some(
10066
+ (prefix) => reason?.startsWith(prefix)
10067
+ );
10068
+ }
9527
10069
  function canReusePendingAuthLink(args) {
9528
10070
  const { pendingAuth } = args;
9529
10071
  if (!pendingAuth) {
@@ -9567,6 +10109,12 @@ function isPendingAuthLatestRequest(conversation, pendingAuth) {
9567
10109
  if (message?.role !== "user") {
9568
10110
  continue;
9569
10111
  }
10112
+ if (message.author?.isBot) {
10113
+ continue;
10114
+ }
10115
+ if (isSkippedNonRequest(message.meta?.skippedReason)) {
10116
+ continue;
10117
+ }
9570
10118
  return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
9571
10119
  }
9572
10120
  return false;
@@ -10206,7 +10754,7 @@ function buildTurnResult(input) {
10206
10754
  }
10207
10755
 
10208
10756
  // src/chat/services/turn-thinking-level.ts
10209
- import { z as z2 } from "zod";
10757
+ import { z as z3 } from "zod";
10210
10758
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
10211
10759
  var MAX_ROUTER_CONTEXT_CHARS = 8e3;
10212
10760
  var ROUTER_CONTEXT_HEAD_CHARS = 3e3;
@@ -10238,13 +10786,13 @@ function coerceClassifierConfidence(value) {
10238
10786
  }
10239
10787
  return CONFIDENCE_LABELS[trimmed] ?? value;
10240
10788
  }
10241
- var turnExecutionProfileSchema = z2.object({
10242
- thinking_level: z2.enum(TURN_THINKING_LEVELS),
10243
- confidence: z2.preprocess(
10789
+ var turnExecutionProfileSchema = z3.object({
10790
+ thinking_level: z3.enum(TURN_THINKING_LEVELS),
10791
+ confidence: z3.preprocess(
10244
10792
  coerceClassifierConfidence,
10245
- z2.number().min(0).max(1)
10793
+ z3.number().min(0).max(1)
10246
10794
  ),
10247
- reason: z2.string().min(1)
10795
+ reason: z3.string().min(1)
10248
10796
  });
10249
10797
  var DEFAULT_THINKING_LEVEL = "medium";
10250
10798
  var THINKING_LEVEL_RANK = {
@@ -11212,6 +11760,1028 @@ function createMcpAuthOrchestration(input) {
11212
11760
  };
11213
11761
  }
11214
11762
 
11763
+ // src/chat/egress/plugin.ts
11764
+ import { randomUUID as randomUUID4 } from "crypto";
11765
+
11766
+ // src/chat/plugins/credential-hooks.ts
11767
+ import {
11768
+ pluginAuthorizationSchema as pluginAuthorizationSchema2,
11769
+ pluginCredentialResultSchema,
11770
+ pluginGrantSchema as pluginGrantSchema2,
11771
+ pluginProviderAccountSchema as pluginProviderAccountSchema2
11772
+ } from "@sentry/junior-plugin-api";
11773
+ function parseSchema(schema, value, message) {
11774
+ const result = schema.safeParse(value);
11775
+ if (!result.success) {
11776
+ throw new Error(message);
11777
+ }
11778
+ return result.data;
11779
+ }
11780
+ function parseAuthorization(value, pluginName) {
11781
+ if (value === void 0) {
11782
+ return void 0;
11783
+ }
11784
+ const authorization = parseSchema(
11785
+ pluginAuthorizationSchema2,
11786
+ value,
11787
+ `Plugin "${pluginName}" grant authorization is invalid`
11788
+ );
11789
+ if (authorization.provider !== pluginName) {
11790
+ throw new Error(
11791
+ `Plugin "${pluginName}" grant authorization provider must match the issuing plugin`
11792
+ );
11793
+ }
11794
+ return authorization;
11795
+ }
11796
+ function parseGrant(value, pluginName) {
11797
+ return parseSchema(
11798
+ pluginGrantSchema2,
11799
+ value,
11800
+ `Plugin "${pluginName}" grantForEgress returned an invalid grant`
11801
+ );
11802
+ }
11803
+ function pluginFor(provider) {
11804
+ return getPlugins().find((candidate) => candidate.manifest.name === provider);
11805
+ }
11806
+ function basePluginContext(plugin) {
11807
+ const pluginName = plugin.manifest.name;
11808
+ return {
11809
+ plugin: { name: pluginName },
11810
+ log: createPluginLogger(pluginName),
11811
+ db: getDb()
11812
+ };
11813
+ }
11814
+ function parseCredentialResult(value, pluginName) {
11815
+ const result = parseSchema(
11816
+ pluginCredentialResultSchema,
11817
+ value,
11818
+ `Plugin "${pluginName}" issueCredential result is invalid`
11819
+ );
11820
+ if (result.type === "lease") {
11821
+ parseAuthorization(result.lease.authorization, pluginName);
11822
+ return result;
11823
+ }
11824
+ if (result.type === "unavailable") {
11825
+ return result;
11826
+ }
11827
+ parseAuthorization(result.authorization, pluginName);
11828
+ return result;
11829
+ }
11830
+ async function selectPluginGrant(input) {
11831
+ const plugin = pluginFor(input.provider);
11832
+ const hook = plugin?.hooks?.grantForEgress;
11833
+ if (!plugin || !hook) {
11834
+ return void 0;
11835
+ }
11836
+ const result = await hook({
11837
+ ...basePluginContext(plugin),
11838
+ request: {
11839
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
11840
+ method: input.method,
11841
+ ...input.operation ? { operation: input.operation } : {},
11842
+ url: input.upstreamUrl.toString()
11843
+ }
11844
+ });
11845
+ return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
11846
+ }
11847
+ async function onPluginEgressResponse(input) {
11848
+ const plugin = pluginFor(input.provider);
11849
+ const hook = plugin?.hooks?.onEgressResponse;
11850
+ if (!plugin || !hook) {
11851
+ return {};
11852
+ }
11853
+ let permissionDenied;
11854
+ await hook({
11855
+ ...basePluginContext(plugin),
11856
+ grant: input.grant,
11857
+ permissionDenied(message) {
11858
+ const trimmed = message.trim();
11859
+ if (!trimmed) {
11860
+ throw new Error(
11861
+ `Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`
11862
+ );
11863
+ }
11864
+ permissionDenied = { message: trimmed };
11865
+ },
11866
+ request: {
11867
+ method: input.method,
11868
+ ...input.operation ? { operation: input.operation } : {},
11869
+ url: input.upstreamUrl.toString()
11870
+ },
11871
+ response: input.response
11872
+ });
11873
+ return permissionDenied ? { permissionDenied } : {};
11874
+ }
11875
+ function hasEgressCredentialHooks(provider) {
11876
+ const hooks = pluginFor(provider)?.hooks;
11877
+ return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
11878
+ }
11879
+ async function resolvePluginOAuthAccount(input) {
11880
+ const plugin = pluginFor(input.provider);
11881
+ const hook = plugin?.hooks?.resolveOAuthAccount;
11882
+ if (!plugin || !hook) {
11883
+ return void 0;
11884
+ }
11885
+ const account = await hook({
11886
+ ...basePluginContext(plugin),
11887
+ tokens: input.tokens
11888
+ });
11889
+ return account === void 0 ? void 0 : parseSchema(
11890
+ pluginProviderAccountSchema2,
11891
+ account,
11892
+ `Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`
11893
+ );
11894
+ }
11895
+ async function issuePluginCredential(input) {
11896
+ const plugin = pluginFor(input.provider);
11897
+ const hook = plugin?.hooks?.issueCredential;
11898
+ if (!plugin || !hook) {
11899
+ throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
11900
+ }
11901
+ const currentUserId = input.actor.type === "user" ? input.actor.userId : void 0;
11902
+ const credentialSubjectUserId = input.credentialSubject?.userId;
11903
+ const result = await hook({
11904
+ ...basePluginContext(plugin),
11905
+ actor: input.actor,
11906
+ grant: input.grant,
11907
+ ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : {},
11908
+ tokens: {
11909
+ ...currentUserId ? {
11910
+ currentUser: {
11911
+ userId: currentUserId,
11912
+ get: async () => await input.userTokenStore.get(
11913
+ currentUserId,
11914
+ plugin.manifest.name
11915
+ ),
11916
+ set: async (tokens) => {
11917
+ await input.userTokenStore.set(
11918
+ currentUserId,
11919
+ plugin.manifest.name,
11920
+ tokens
11921
+ );
11922
+ },
11923
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(
11924
+ currentUserId,
11925
+ plugin.manifest.name,
11926
+ callback
11927
+ )
11928
+ }
11929
+ } : {},
11930
+ ...credentialSubjectUserId ? {
11931
+ credentialSubject: {
11932
+ userId: credentialSubjectUserId,
11933
+ get: async () => await input.userTokenStore.get(
11934
+ credentialSubjectUserId,
11935
+ plugin.manifest.name
11936
+ ),
11937
+ set: async (tokens) => {
11938
+ await input.userTokenStore.set(
11939
+ credentialSubjectUserId,
11940
+ plugin.manifest.name,
11941
+ tokens
11942
+ );
11943
+ },
11944
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(
11945
+ credentialSubjectUserId,
11946
+ plugin.manifest.name,
11947
+ callback
11948
+ )
11949
+ }
11950
+ } : {}
11951
+ }
11952
+ });
11953
+ return parseCredentialResult(result, plugin.manifest.name);
11954
+ }
11955
+
11956
+ // src/chat/sandbox/egress/credentials.ts
11957
+ var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
11958
+ var SandboxEgressCredentialError = class extends Error {
11959
+ authorization;
11960
+ grant;
11961
+ kind;
11962
+ provider;
11963
+ constructor(input) {
11964
+ super(input.message);
11965
+ this.name = "SandboxEgressCredentialError";
11966
+ this.authorization = input.authorization;
11967
+ this.grant = input.grant;
11968
+ this.kind = input.kind;
11969
+ this.provider = input.provider;
11970
+ }
11971
+ };
11972
+ function defaultGrantForProvider(input) {
11973
+ const access = HTTP_READ_METHODS.has(
11974
+ input.method.toUpperCase()
11975
+ ) ? "read" : "write";
11976
+ return {
11977
+ source: "broker",
11978
+ grant: {
11979
+ name: "default",
11980
+ access,
11981
+ reason: `sandbox-egress:${input.provider}:${access}`
11982
+ }
11983
+ };
11984
+ }
11985
+ function oauthAuthorizationForProvider(provider) {
11986
+ const oauth = pluginCatalogRuntime.getOAuthConfig(provider);
11987
+ return oauth ? {
11988
+ type: "oauth",
11989
+ provider,
11990
+ ...oauth.scope ? { scope: oauth.scope } : {}
11991
+ } : void 0;
11992
+ }
11993
+ function credentialSubjectFromContext(context) {
11994
+ return "subject" in context.credentials && context.credentials.subject ? { type: "user", userId: context.credentials.subject.userId } : void 0;
11995
+ }
11996
+ function assertLeaseTransformsOwnedByProvider(provider, lease) {
11997
+ for (const transform of lease.headerTransforms) {
11998
+ if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) {
11999
+ throw new Error(
12000
+ `Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`
12001
+ );
12002
+ }
12003
+ }
12004
+ }
12005
+ async function selectSandboxEgressGrant(input) {
12006
+ if (!hasEgressCredentialHooks(input.provider)) {
12007
+ return defaultGrantForProvider(input);
12008
+ }
12009
+ const pluginGrant = await selectPluginGrant({
12010
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
12011
+ ...input.operation ? { operation: input.operation } : {},
12012
+ provider: input.provider,
12013
+ method: input.method,
12014
+ upstreamUrl: input.upstreamUrl
12015
+ });
12016
+ if (!pluginGrant) {
12017
+ throw new Error(
12018
+ `Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`
12019
+ );
12020
+ }
12021
+ return { source: "plugin", grant: pluginGrant };
12022
+ }
12023
+ function authorizationForSandboxEgressGrant(provider, selection) {
12024
+ return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
12025
+ }
12026
+ async function sandboxEgressCredentialLease(provider, selection, context) {
12027
+ const { grant } = selection;
12028
+ const cached = await getSandboxEgressCredentialLease(
12029
+ provider,
12030
+ grant.name,
12031
+ context
12032
+ );
12033
+ if (cached) {
12034
+ if (selection.source === "plugin" && cached.grant.access !== grant.access) {
12035
+ throw new Error(
12036
+ `Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`
12037
+ );
12038
+ }
12039
+ return {
12040
+ ...cached,
12041
+ grant
12042
+ };
12043
+ }
12044
+ let lease;
12045
+ if (selection.source === "plugin") {
12046
+ const credentialSubject = credentialSubjectFromContext(context);
12047
+ const pluginResult = await issuePluginCredential({
12048
+ provider,
12049
+ grant,
12050
+ actor: context.credentials.actor,
12051
+ ...credentialSubject ? { credentialSubject } : {},
12052
+ userTokenStore: createUserTokenStore()
12053
+ });
12054
+ if (pluginResult.type === "needed") {
12055
+ throw new SandboxEgressCredentialError({
12056
+ provider,
12057
+ grant,
12058
+ kind: "auth_required",
12059
+ authorization: pluginResult.authorization,
12060
+ message: pluginResult.message
12061
+ });
12062
+ }
12063
+ if (pluginResult.type === "unavailable") {
12064
+ throw new SandboxEgressCredentialError({
12065
+ provider,
12066
+ grant,
12067
+ kind: "unavailable",
12068
+ message: pluginResult.message
12069
+ });
12070
+ }
12071
+ lease = pluginResult.lease;
12072
+ } else {
12073
+ try {
12074
+ lease = await issueProviderCredentialLease({
12075
+ context: context.credentials,
12076
+ provider,
12077
+ reason: grant.reason ?? `sandbox-egress:${provider}:default`
12078
+ });
12079
+ } catch (error) {
12080
+ if (error instanceof CredentialUnavailableError) {
12081
+ throw new SandboxEgressCredentialError({
12082
+ provider,
12083
+ grant,
12084
+ kind: "auth_required",
12085
+ authorization: authorizationForSandboxEgressGrant(
12086
+ provider,
12087
+ selection
12088
+ ),
12089
+ message: error.message
12090
+ });
12091
+ }
12092
+ throw error;
12093
+ }
12094
+ }
12095
+ const headerTransforms = lease.headerTransforms ?? [];
12096
+ if (headerTransforms.length === 0) {
12097
+ throw new Error(
12098
+ `Credential lease for ${provider} did not include header transforms`
12099
+ );
12100
+ }
12101
+ const leaseExpiresAtMs = Date.parse(lease.expiresAt);
12102
+ if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
12103
+ throw new Error(`Credential lease for ${provider} is expired`);
12104
+ }
12105
+ const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
12106
+ const cachedLease = {
12107
+ provider,
12108
+ grant,
12109
+ ...lease.account ? { account: lease.account } : {},
12110
+ ...authorization ? { authorization } : {},
12111
+ expiresAt: lease.expiresAt,
12112
+ headerTransforms
12113
+ };
12114
+ assertLeaseTransformsOwnedByProvider(provider, cachedLease);
12115
+ await setSandboxEgressCredentialLease(context, cachedLease);
12116
+ return cachedLease;
12117
+ }
12118
+ function hasSandboxEgressLeaseTransformForHost(lease, host) {
12119
+ return lease.headerTransforms.some(
12120
+ (transform) => matchesSandboxEgressDomain(host, transform.domain)
12121
+ );
12122
+ }
12123
+
12124
+ // src/chat/egress/credentialed.ts
12125
+ import {
12126
+ EgressAuthRequired,
12127
+ EgressPolicyDenied
12128
+ } from "@sentry/junior-plugin-api";
12129
+ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
12130
+ "connection",
12131
+ "host",
12132
+ "keep-alive",
12133
+ "proxy-authenticate",
12134
+ "proxy-authorization",
12135
+ "te",
12136
+ "trailer",
12137
+ "transfer-encoding",
12138
+ "upgrade"
12139
+ ]);
12140
+ var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
12141
+ "vercel-sandbox-oidc-token",
12142
+ "vercel-forwarded-host",
12143
+ "vercel-forwarded-scheme",
12144
+ "vercel-forwarded-port",
12145
+ "vercel-forwarded-path"
12146
+ ]);
12147
+ var TRACE_PROPAGATION_HEADERS = /* @__PURE__ */ new Set([
12148
+ "baggage",
12149
+ "sentry-trace",
12150
+ "traceparent"
12151
+ ]);
12152
+ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
12153
+ "content-encoding",
12154
+ "content-length"
12155
+ ]);
12156
+ var UPSTREAM_TOKEN_REJECTION_STATUS = 401;
12157
+ var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
12158
+ var GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
12159
+ var RESPONSE_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
12160
+ function authRequiredResponse(input) {
12161
+ return new Response(
12162
+ `junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized
12163
+ ${input.message}`,
12164
+ {
12165
+ status: 401,
12166
+ headers: {
12167
+ "content-type": "text/plain; charset=utf-8",
12168
+ "cache-control": "no-store"
12169
+ }
12170
+ }
12171
+ );
12172
+ }
12173
+ function policyDeniedResponse(error) {
12174
+ return Response.json(
12175
+ { error: error.message },
12176
+ {
12177
+ status: 403,
12178
+ headers: {
12179
+ "cache-control": "no-store"
12180
+ }
12181
+ }
12182
+ );
12183
+ }
12184
+ function shouldLogSandboxEgressInfo() {
12185
+ const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
12186
+ return environment !== "production";
12187
+ }
12188
+ function egressAttributes(input) {
12189
+ return {
12190
+ ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
12191
+ ...input.provider ? { "app.provider.name": input.provider } : {},
12192
+ ...input.grantName ? { "app.grant.name": input.grantName } : {},
12193
+ ...input.grantAccess ? { "app.grant.access": input.grantAccess } : {},
12194
+ ...input.grantReason ? { "app.grant.reason": input.grantReason } : {},
12195
+ ...input.host ? { "server.address": input.host } : {},
12196
+ ...input.method ? { "http.request.method": input.method } : {},
12197
+ ...input.path ? { "url.path": input.path } : {},
12198
+ ...input.status ? { "http.response.status_code": input.status } : {}
12199
+ };
12200
+ }
12201
+ function redactedProxyPath(pathname) {
12202
+ if (pathname.startsWith(`${SANDBOX_EGRESS_PROXY_PATH}/`)) {
12203
+ return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
12204
+ }
12205
+ return pathname;
12206
+ }
12207
+ function routingAttributes(request, upstreamUrl) {
12208
+ const proxyUrl = new URL(request.url);
12209
+ const attributes = {
12210
+ "app.sandbox.egress.proxy_path": redactedProxyPath(proxyUrl.pathname)
12211
+ };
12212
+ if (upstreamUrl) {
12213
+ attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
12214
+ const gitService = upstreamUrl.searchParams.get("service");
12215
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
12216
+ attributes["app.sandbox.egress.git_service"] = gitService;
12217
+ }
12218
+ }
12219
+ return attributes;
12220
+ }
12221
+ function displayedUpstreamPath(upstreamUrl) {
12222
+ const gitService = upstreamUrl.searchParams.get("service");
12223
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
12224
+ return `${upstreamUrl.pathname}?service=${gitService}`;
12225
+ }
12226
+ return upstreamUrl.pathname;
12227
+ }
12228
+ function upstreamPermissionAttributes(provider, upstream) {
12229
+ if (provider !== "github") {
12230
+ return {};
12231
+ }
12232
+ return {
12233
+ "app.github.accepted_permissions": upstream.headers.get("x-accepted-github-permissions") ?? void 0,
12234
+ "app.github.sso": upstream.headers.get("x-github-sso") ?? void 0
12235
+ };
12236
+ }
12237
+ function githubPermissionHeaders(upstream) {
12238
+ const acceptedPermissions = upstream.headers.get(
12239
+ "x-accepted-github-permissions"
12240
+ );
12241
+ const sso = upstream.headers.get("x-github-sso");
12242
+ return {
12243
+ ...acceptedPermissions ? { acceptedPermissions } : {},
12244
+ ...sso ? { sso } : {}
12245
+ };
12246
+ }
12247
+ function permissionDeniedMessage(provider, grant) {
12248
+ return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
12249
+ }
12250
+ function isEgressAuthRequired(error) {
12251
+ return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
12252
+ }
12253
+ function isEgressPolicyDenied(error) {
12254
+ return error instanceof EgressPolicyDenied || error instanceof Error && error.name === "EgressPolicyDenied";
12255
+ }
12256
+ function logSandboxEgressUpstreamRequest(input) {
12257
+ if (!shouldLogSandboxEgressInfo()) {
12258
+ return;
12259
+ }
12260
+ logInfo(
12261
+ "sandbox_egress_upstream_request",
12262
+ {},
12263
+ {
12264
+ ...egressAttributes({
12265
+ egressId: input.egressId,
12266
+ grantAccess: input.grantAccess,
12267
+ grantName: input.grantName,
12268
+ grantReason: input.grantReason,
12269
+ host: input.upstreamUrl.hostname,
12270
+ method: input.request.method,
12271
+ path: input.upstreamUrl.pathname,
12272
+ provider: input.provider,
12273
+ status: input.upstream.status
12274
+ }),
12275
+ ...routingAttributes(input.request, input.upstreamUrl),
12276
+ "app.sandbox.egress.upstream_ok": input.upstream.ok
12277
+ },
12278
+ `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${displayedUpstreamPath(input.upstreamUrl)} -> ${input.upstream.status}`
12279
+ );
12280
+ }
12281
+ async function requestBodyBytes(request) {
12282
+ if (request.method === "GET" || request.method === "HEAD" || request.body === null) {
12283
+ return void 0;
12284
+ }
12285
+ return await request.arrayBuffer();
12286
+ }
12287
+ function isGrantSelectionBodyVisible(input) {
12288
+ return input.provider === "github" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
12289
+ }
12290
+ function grantSelectionBodyText(input) {
12291
+ if (input.body === void 0) {
12292
+ return void 0;
12293
+ }
12294
+ if (input.body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
12295
+ if (!input.operation && input.provider === "github" && input.request.method.toUpperCase() === "POST" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql")) {
12296
+ throw new EgressPolicyDenied(
12297
+ "GitHub GraphQL request body is too large for Junior to inspect before issuing credentials."
12298
+ );
12299
+ }
12300
+ return void 0;
12301
+ }
12302
+ return new TextDecoder().decode(input.body);
12303
+ }
12304
+ function responseContentLength(upstream) {
12305
+ const raw = upstream.headers.get("content-length");
12306
+ if (!raw) {
12307
+ return void 0;
12308
+ }
12309
+ const parsed = Number(raw);
12310
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
12311
+ }
12312
+ async function responseTextWithinLimit(upstream, maxBytes) {
12313
+ const limit = Math.min(
12314
+ Math.max(0, Math.floor(maxBytes)),
12315
+ RESPONSE_BODY_TEXT_LIMIT_BYTES
12316
+ );
12317
+ if (limit <= 0) {
12318
+ return void 0;
12319
+ }
12320
+ const contentLength = responseContentLength(upstream);
12321
+ if (contentLength !== void 0 && contentLength > limit) {
12322
+ return void 0;
12323
+ }
12324
+ let clone;
12325
+ try {
12326
+ clone = upstream.clone();
12327
+ } catch {
12328
+ return void 0;
12329
+ }
12330
+ const body = clone.body;
12331
+ if (!body) {
12332
+ return "";
12333
+ }
12334
+ const reader = body.getReader();
12335
+ const chunks = [];
12336
+ let bytes = 0;
12337
+ try {
12338
+ while (true) {
12339
+ const { done, value } = await reader.read();
12340
+ if (done) {
12341
+ break;
12342
+ }
12343
+ if (!value) {
12344
+ continue;
12345
+ }
12346
+ bytes += value.byteLength;
12347
+ if (bytes > limit) {
12348
+ await reader.cancel().catch(() => void 0);
12349
+ return void 0;
12350
+ }
12351
+ chunks.push(value);
12352
+ }
12353
+ } catch {
12354
+ await reader.cancel().catch(() => void 0);
12355
+ return void 0;
12356
+ } finally {
12357
+ reader.releaseLock();
12358
+ }
12359
+ const combined = new Uint8Array(bytes);
12360
+ let offset = 0;
12361
+ for (const chunk of chunks) {
12362
+ combined.set(chunk, offset);
12363
+ offset += chunk.byteLength;
12364
+ }
12365
+ return new TextDecoder().decode(combined);
12366
+ }
12367
+ function requestHeaders(request, lease, upstreamHost, tracePropagation) {
12368
+ const headers = new Headers();
12369
+ const mayPropagateTrace = shouldPropagateSandboxEgressTrace(
12370
+ upstreamHost,
12371
+ tracePropagation
12372
+ );
12373
+ request.headers.forEach((value, key) => {
12374
+ const normalized = key.toLowerCase();
12375
+ if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) {
12376
+ return;
12377
+ }
12378
+ if (TRACE_PROPAGATION_HEADERS.has(normalized) && !mayPropagateTrace) {
12379
+ return;
12380
+ }
12381
+ headers.append(key, value);
12382
+ });
12383
+ for (const transform of lease.headerTransforms) {
12384
+ if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) {
12385
+ continue;
12386
+ }
12387
+ for (const [key, value] of Object.entries(transform.headers)) {
12388
+ if (TRACE_PROPAGATION_HEADERS.has(key.toLowerCase()) && !mayPropagateTrace) {
12389
+ continue;
12390
+ }
12391
+ headers.set(key, value);
12392
+ }
12393
+ }
12394
+ return headers;
12395
+ }
12396
+ function responseHeaders(upstream) {
12397
+ const headers = new Headers();
12398
+ upstream.headers.forEach((value, key) => {
12399
+ const normalized = key.toLowerCase();
12400
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) {
12401
+ headers.append(key, value);
12402
+ }
12403
+ });
12404
+ return headers;
12405
+ }
12406
+ function leaseLogAttributes(input) {
12407
+ return {
12408
+ ...egressAttributes({
12409
+ egressId: input.egressId,
12410
+ grantAccess: input.lease.grant.access,
12411
+ grantName: input.lease.grant.name,
12412
+ grantReason: input.lease.grant.reason,
12413
+ host: input.upstreamUrl.hostname,
12414
+ method: input.request.method,
12415
+ path: input.upstreamUrl.pathname,
12416
+ provider: input.provider,
12417
+ status: input.status
12418
+ }),
12419
+ ...routingAttributes(input.request, input.upstreamUrl),
12420
+ ...input.upstream ? upstreamPermissionAttributes(input.provider, input.upstream) : {}
12421
+ };
12422
+ }
12423
+ async function recordSandboxAuthRequired(input) {
12424
+ await setSandboxEgressAuthRequiredSignal(input.credentialContext, {
12425
+ provider: input.provider,
12426
+ grant: input.grant,
12427
+ kind: input.kind ?? "auth_required",
12428
+ ...input.authorization ? { authorization: input.authorization } : {},
12429
+ message: input.message
12430
+ });
12431
+ }
12432
+ async function recordSandboxPermissionDenied(input) {
12433
+ await setSandboxEgressPermissionDeniedSignal(input.credentialContext, {
12434
+ provider: input.provider,
12435
+ grant: input.lease.grant,
12436
+ ...input.lease.account ? { account: input.lease.account } : {},
12437
+ message: input.message,
12438
+ source: "upstream",
12439
+ status: input.upstream.status,
12440
+ upstreamHost: input.upstreamUrl.hostname,
12441
+ upstreamPath: displayedUpstreamPath(input.upstreamUrl),
12442
+ ...input.provider === "github" ? githubPermissionHeaders(input.upstream) : {}
12443
+ });
12444
+ }
12445
+ async function executeCredentialedEgressRequest(input) {
12446
+ const {
12447
+ activeEgressId,
12448
+ credentialContext,
12449
+ deps,
12450
+ operation,
12451
+ provider,
12452
+ request,
12453
+ upstreamUrl
12454
+ } = input;
12455
+ const bodyForGrantSelection = isGrantSelectionBodyVisible({
12456
+ provider,
12457
+ upstreamUrl
12458
+ }) ? await requestBodyBytes(request) : void 0;
12459
+ let grantSelection;
12460
+ try {
12461
+ grantSelection = await selectSandboxEgressGrant({
12462
+ bodyText: grantSelectionBodyText({
12463
+ body: bodyForGrantSelection,
12464
+ ...operation ? { operation } : {},
12465
+ provider,
12466
+ request,
12467
+ upstreamUrl
12468
+ }),
12469
+ ...operation ? { operation } : {},
12470
+ provider,
12471
+ method: request.method,
12472
+ upstreamUrl
12473
+ });
12474
+ } catch (error) {
12475
+ if (isEgressPolicyDenied(error)) {
12476
+ logWarn(
12477
+ "sandbox_egress_policy_denied",
12478
+ {},
12479
+ {
12480
+ ...egressAttributes({
12481
+ egressId: activeEgressId,
12482
+ host: upstreamUrl.hostname,
12483
+ method: request.method,
12484
+ path: upstreamUrl.pathname,
12485
+ provider,
12486
+ status: 403
12487
+ }),
12488
+ ...routingAttributes(request, upstreamUrl)
12489
+ },
12490
+ error.message
12491
+ );
12492
+ return policyDeniedResponse(error);
12493
+ }
12494
+ throw error;
12495
+ }
12496
+ const issueCredentialLease = deps.issueCredentialLease ?? sandboxEgressCredentialLease;
12497
+ const clearCredentialLease = deps.clearCredentialLease ?? clearSandboxEgressCredentialLease;
12498
+ const recordAuthRequired = deps.recordAuthRequired ?? recordSandboxAuthRequired;
12499
+ const recordPermissionDenied = deps.recordPermissionDenied ?? recordSandboxPermissionDenied;
12500
+ let lease;
12501
+ try {
12502
+ lease = await issueCredentialLease(
12503
+ provider,
12504
+ grantSelection,
12505
+ credentialContext
12506
+ );
12507
+ } catch (error) {
12508
+ if (error instanceof SandboxEgressCredentialError) {
12509
+ await recordAuthRequired({
12510
+ credentialContext,
12511
+ provider: error.provider,
12512
+ grant: error.grant,
12513
+ kind: error.kind,
12514
+ authorization: error.authorization,
12515
+ message: error.message
12516
+ });
12517
+ const isAuthRequired = error.kind === "auth_required";
12518
+ logWarn(
12519
+ isAuthRequired ? "sandbox_egress_credential_needed" : "sandbox_egress_credential_unavailable",
12520
+ {},
12521
+ {
12522
+ ...egressAttributes({
12523
+ egressId: activeEgressId,
12524
+ grantAccess: error.grant.access,
12525
+ grantName: error.grant.name,
12526
+ grantReason: error.grant.reason,
12527
+ host: upstreamUrl.hostname,
12528
+ method: request.method,
12529
+ path: upstreamUrl.pathname,
12530
+ provider: error.provider,
12531
+ status: 401
12532
+ }),
12533
+ ...routingAttributes(request, upstreamUrl)
12534
+ },
12535
+ isAuthRequired ? "Sandbox egress grant needs user authorization before issuing a credential lease" : "Sandbox egress credential lease is unavailable for selected grant"
12536
+ );
12537
+ return authRequiredResponse({
12538
+ provider: error.provider,
12539
+ grant: error.grant,
12540
+ message: error.message
12541
+ });
12542
+ }
12543
+ throw error;
12544
+ }
12545
+ const attributes = (status, upstream2) => leaseLogAttributes({
12546
+ egressId: activeEgressId,
12547
+ lease,
12548
+ provider,
12549
+ request,
12550
+ status,
12551
+ ...upstream2 ? { upstream: upstream2 } : {},
12552
+ upstreamUrl
12553
+ });
12554
+ if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
12555
+ logWarn(
12556
+ "sandbox_egress_transform_missing",
12557
+ {},
12558
+ {
12559
+ ...attributes(403),
12560
+ "app.sandbox.egress.transform_domains": lease.headerTransforms.map(
12561
+ (transform) => transform.domain
12562
+ )
12563
+ },
12564
+ "Sandbox egress credential lease does not cover forwarded host"
12565
+ );
12566
+ return Response.json(
12567
+ { error: "Credential lease does not cover forwarded host" },
12568
+ { status: 403 }
12569
+ );
12570
+ }
12571
+ const fetchImpl = deps.fetch ?? fetch;
12572
+ const headers = requestHeaders(
12573
+ request,
12574
+ lease,
12575
+ upstreamUrl.hostname,
12576
+ deps.tracePropagation ?? {}
12577
+ );
12578
+ const body = bodyForGrantSelection ?? await requestBodyBytes(request);
12579
+ const intercepted = await deps.interceptHttp?.({
12580
+ provider,
12581
+ request: new Request(upstreamUrl, {
12582
+ method: request.method,
12583
+ headers,
12584
+ ...body !== void 0 ? { body } : {}
12585
+ }),
12586
+ upstreamUrl
12587
+ });
12588
+ if (intercepted) {
12589
+ return intercepted;
12590
+ }
12591
+ const upstream = await fetchImpl(upstreamUrl, {
12592
+ method: request.method,
12593
+ headers,
12594
+ ...body !== void 0 ? { body } : {},
12595
+ redirect: "manual"
12596
+ });
12597
+ try {
12598
+ const effects = await onPluginEgressResponse({
12599
+ provider,
12600
+ grant: lease.grant,
12601
+ method: request.method,
12602
+ ...operation ? { operation } : {},
12603
+ upstreamUrl,
12604
+ response: {
12605
+ headers: new Headers(upstream.headers),
12606
+ readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
12607
+ status: upstream.status
12608
+ }
12609
+ });
12610
+ if (effects.permissionDenied) {
12611
+ await recordPermissionDenied({
12612
+ credentialContext,
12613
+ provider,
12614
+ lease,
12615
+ message: effects.permissionDenied.message,
12616
+ upstream,
12617
+ upstreamUrl
12618
+ });
12619
+ logWarn(
12620
+ "sandbox_egress_upstream_permission_classified",
12621
+ {},
12622
+ {
12623
+ ...attributes(upstream.status, upstream)
12624
+ },
12625
+ "Sandbox egress plugin classified upstream response as permission denied"
12626
+ );
12627
+ }
12628
+ } catch (error) {
12629
+ if (!isEgressAuthRequired(error)) {
12630
+ throw error;
12631
+ }
12632
+ await clearCredentialLease(provider, lease.grant.name, credentialContext);
12633
+ await recordAuthRequired({
12634
+ credentialContext,
12635
+ provider,
12636
+ grant: lease.grant,
12637
+ authorization: error.authorization ?? lease.authorization,
12638
+ message: error.message
12639
+ });
12640
+ logWarn(
12641
+ "sandbox_egress_upstream_auth_required_classified",
12642
+ {},
12643
+ {
12644
+ ...attributes(upstream.status, upstream)
12645
+ },
12646
+ "Sandbox egress plugin classified upstream response as auth required"
12647
+ );
12648
+ await upstream.body?.cancel().catch(() => void 0);
12649
+ return authRequiredResponse({
12650
+ provider,
12651
+ grant: lease.grant,
12652
+ message: error.message
12653
+ });
12654
+ }
12655
+ logSandboxEgressUpstreamRequest({
12656
+ egressId: activeEgressId,
12657
+ grantAccess: lease.grant.access,
12658
+ grantName: lease.grant.name,
12659
+ grantReason: lease.grant.reason,
12660
+ provider,
12661
+ request,
12662
+ upstream,
12663
+ upstreamUrl
12664
+ });
12665
+ if (upstream.status >= 400) {
12666
+ logWarn(
12667
+ "sandbox_egress_upstream_error_response",
12668
+ {},
12669
+ {
12670
+ ...attributes(upstream.status, upstream),
12671
+ "error.type": `http_${upstream.status}`
12672
+ },
12673
+ `Sandbox egress upstream returned HTTP ${upstream.status}`
12674
+ );
12675
+ }
12676
+ if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS || upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS) {
12677
+ logWarn(
12678
+ "sandbox_egress_upstream_auth_rejected",
12679
+ {},
12680
+ {
12681
+ ...attributes(upstream.status, upstream),
12682
+ ...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
12683
+ "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
12684
+ } : {}
12685
+ },
12686
+ upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
12687
+ );
12688
+ if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
12689
+ await clearCredentialLease(provider, lease.grant.name, credentialContext);
12690
+ await recordAuthRequired({
12691
+ credentialContext,
12692
+ provider,
12693
+ grant: lease.grant,
12694
+ authorization: lease.authorization,
12695
+ message: `Provider rejected the injected ${provider} credential.`
12696
+ });
12697
+ await upstream.body?.cancel().catch(() => void 0);
12698
+ return authRequiredResponse({
12699
+ provider,
12700
+ grant: lease.grant,
12701
+ message: `Provider rejected the injected ${provider} credential.
12702
+ `
12703
+ });
12704
+ } else {
12705
+ await clearCredentialLease(provider, lease.grant.name, credentialContext);
12706
+ await recordPermissionDenied({
12707
+ credentialContext,
12708
+ provider,
12709
+ lease,
12710
+ message: permissionDeniedMessage(provider, lease.grant),
12711
+ upstream,
12712
+ upstreamUrl
12713
+ });
12714
+ }
12715
+ }
12716
+ return new Response(upstream.body, {
12717
+ status: upstream.status,
12718
+ statusText: upstream.statusText,
12719
+ headers: responseHeaders(upstream)
12720
+ });
12721
+ }
12722
+
12723
+ // src/chat/egress/plugin.ts
12724
+ function credentialContextForPluginEgress(credentialContext) {
12725
+ return {
12726
+ credentials: credentialContext,
12727
+ contextId: randomUUID4(),
12728
+ egressId: `plugin-egress:${randomUUID4()}`,
12729
+ expiresAtMs: Date.now() + 30 * 60 * 1e3
12730
+ };
12731
+ }
12732
+ function createPluginEgress(deps) {
12733
+ const credentialContext = deps.credentialContext ? credentialContextForPluginEgress(deps.credentialContext) : void 0;
12734
+ return {
12735
+ async fetch(input) {
12736
+ const operation = input.operation.trim();
12737
+ if (!operation) {
12738
+ throw new Error("Plugin egress operation is required");
12739
+ }
12740
+ if (!credentialContext) {
12741
+ throw new PluginCredentialFailureError(
12742
+ input.provider,
12743
+ `Cannot issue ${input.provider} credentials without a credential context.`
12744
+ );
12745
+ }
12746
+ const upstreamUrl = new URL(input.request.url);
12747
+ if (upstreamUrl.protocol !== "https:") {
12748
+ throw new Error("Plugin egress requires HTTPS provider URLs");
12749
+ }
12750
+ const resolvedProvider = resolveSandboxEgressProviderForHost(
12751
+ upstreamUrl.hostname
12752
+ );
12753
+ if (resolvedProvider !== input.provider) {
12754
+ throw new Error(
12755
+ `Plugin egress provider "${input.provider}" does not own ${upstreamUrl.hostname}`
12756
+ );
12757
+ }
12758
+ return await executeCredentialedEgressRequest({
12759
+ activeEgressId: credentialContext.egressId,
12760
+ credentialContext,
12761
+ deps: {
12762
+ ...deps.fetch ? { fetch: deps.fetch } : {},
12763
+ recordAuthRequired: async (signal) => {
12764
+ await deps.pluginAuth.handleAuthRequired({
12765
+ ...signal.authorization ? { authorization: signal.authorization } : {},
12766
+ grant: signal.grant,
12767
+ kind: signal.kind ?? "auth_required",
12768
+ message: signal.message,
12769
+ provider: signal.provider
12770
+ });
12771
+ },
12772
+ recordPermissionDenied: async () => {
12773
+ },
12774
+ tracePropagation: {}
12775
+ },
12776
+ operation,
12777
+ provider: input.provider,
12778
+ request: input.request,
12779
+ upstreamUrl
12780
+ });
12781
+ }
12782
+ };
12783
+ }
12784
+
11215
12785
  // src/chat/respond.ts
11216
12786
  var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
11217
12787
  function sleep3(ms) {
@@ -11824,6 +13394,23 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
11824
13394
  userText: userInput,
11825
13395
  artifactState: context.artifactState,
11826
13396
  configuration: configurationValues,
13397
+ egress: createPluginEgress({
13398
+ credentialContext: context.credentialContext,
13399
+ pluginAuth: {
13400
+ async handleAuthRequired(signal) {
13401
+ await pluginAuth.maybeHandleAuthSignal({
13402
+ auth_required: {
13403
+ ...signal.authorization ? { authorization: signal.authorization } : {},
13404
+ createdAtMs: Date.now(),
13405
+ grant: signal.grant,
13406
+ kind: signal.kind,
13407
+ message: signal.message,
13408
+ provider: signal.provider
13409
+ }
13410
+ });
13411
+ }
13412
+ }
13413
+ }),
11827
13414
  mcpToolManager: turnMcpToolManager,
11828
13415
  sandbox,
11829
13416
  surface,
@@ -13175,7 +14762,8 @@ export {
13175
14762
  setConfigDefaults,
13176
14763
  getConfigDefaults,
13177
14764
  createUserTokenStore,
13178
- issueProviderCredentialLease,
14765
+ findMatchingResourceEventSubscriptions,
14766
+ deliverResourceEventSubscription,
13179
14767
  listThreadReplies,
13180
14768
  postSlackMessage,
13181
14769
  deleteSlackMessage,
@@ -13190,14 +14778,8 @@ export {
13190
14778
  startOAuthFlow,
13191
14779
  SANDBOX_EGRESS_PROXY_PATH,
13192
14780
  parseSandboxEgressCredentialToken,
13193
- setSandboxEgressCredentialLease,
13194
- getSandboxEgressCredentialLease,
13195
- clearSandboxEgressCredentialLease,
13196
- setSandboxEgressAuthRequiredSignal,
13197
- setSandboxEgressPermissionDeniedSignal,
13198
14781
  normalizeSandboxEgressTracePropagationDomains,
13199
14782
  shouldPropagateSandboxEgressTrace,
13200
- matchesSandboxEgressDomain,
13201
14783
  resolveSandboxEgressProviderForHost,
13202
14784
  shouldEmitDevAgentTrace,
13203
14785
  AuthorizationFlowDisabledError,
@@ -13228,6 +14810,8 @@ export {
13228
14810
  markTurnFailed,
13229
14811
  isRedundantReactionAckText,
13230
14812
  finalizeMcpAuthorization,
14813
+ resolvePluginOAuthAccount,
14814
+ executeCredentialedEgressRequest,
13231
14815
  generateAssistantReply,
13232
14816
  estimateTextTokens,
13233
14817
  getAgentContextCompactionTriggerTokens,