@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.
- package/dist/{agent-hooks-ZGTDOXQY.js → agent-hooks-EEWWWIS2.js} +5 -5
- package/dist/api-reference.d.ts +1 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +3763 -4132
- package/dist/chat/agent-dispatch/runner.d.ts +1 -1
- package/dist/chat/app/services.d.ts +1 -1
- package/dist/chat/conversations/store.d.ts +1 -1
- package/dist/chat/egress/credentialed.d.ts +57 -0
- package/dist/chat/egress/plugin.d.ts +24 -0
- package/dist/chat/plugins/agent-hooks.d.ts +3 -3
- package/dist/chat/plugins/credential-hooks.d.ts +2 -0
- package/dist/chat/resource-events/ingest.d.ts +23 -0
- package/dist/chat/resource-events/notification.d.ts +20 -0
- package/dist/chat/resource-events/store.d.ts +79 -0
- package/dist/chat/respond.d.ts +1 -1
- package/dist/chat/sandbox/{egress-credentials.d.ts → egress/credentials.d.ts} +28 -5
- package/dist/chat/sandbox/egress/policy.d.ts +32 -0
- package/dist/chat/sandbox/egress/proxy.d.ts +35 -0
- package/dist/chat/sandbox/{egress-session.d.ts → egress/session.d.ts} +41 -11
- package/dist/chat/sandbox/sandbox.d.ts +2 -2
- package/dist/chat/services/pending-auth.d.ts +5 -0
- package/dist/chat/slack/client.d.ts +2 -0
- package/dist/chat/task-execution/slack-work.d.ts +22 -0
- package/dist/chat/task-execution/state.d.ts +7 -1
- package/dist/chat/task-execution/store.d.ts +30 -8
- package/dist/chat/tools/resource-events.d.ts +19 -0
- package/dist/chat/tools/types.d.ts +2 -1
- package/dist/{chunk-Y3YUOEAZ.js → chunk-5C3RY6IZ.js} +20 -11
- package/dist/{chunk-IOBSRZK5.js → chunk-AK7T7XRQ.js} +1 -1
- package/dist/{chunk-LX5GBMEP.js → chunk-BFQ7IUTE.js} +1 -1
- package/dist/{chunk-YA2JCC7G.js → chunk-GK5CITCY.js} +4 -2
- package/dist/{chunk-4XHCVBXH.js → chunk-KZLRUQAA.js} +77 -12
- package/dist/{chunk-BOMLWLTF.js → chunk-LK53AHO5.js} +1759 -175
- package/dist/{chunk-2AJ4TEKE.js → chunk-ORRLK22J.js} +24 -2
- package/dist/{chunk-PQ2U2AO3.js → chunk-QVMLDCVR.js} +10 -2
- package/dist/{chunk-RV5RYIJW.js → chunk-UXPG6ZIN.js} +28 -0
- package/dist/cli/chat.js +4 -4
- package/dist/cli/env.js +7 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/main.js +1 -1
- package/dist/cli/plugins.js +3 -3
- package/dist/cli/upgrade.js +3 -3
- package/dist/{db-GQJKBX5W.js → db-TRIVS6BW.js} +2 -2
- package/dist/handlers/agent-dispatch.d.ts +1 -1
- package/dist/handlers/github-webhook/check-suite.d.ts +3 -0
- package/dist/handlers/github-webhook/pull-request-review.d.ts +3 -0
- package/dist/handlers/github-webhook/pull-request.d.ts +3 -0
- package/dist/handlers/github-webhook/resource.d.ts +10 -0
- package/dist/handlers/github-webhook.d.ts +15 -0
- package/dist/handlers/sandbox-egress-proxy.d.ts +2 -2
- package/dist/nitro.js +2 -2
- package/dist/reporting/conversations.d.ts +2 -2
- package/dist/reporting.d.ts +2 -2
- package/dist/reporting.js +16 -22
- package/dist/{runner-LNBN7BIN.js → runner-KS4XDMHF.js} +5 -5
- package/package.json +6 -5
- package/dist/chat/sandbox/egress-policy.d.ts +0 -15
- package/dist/chat/sandbox/egress-proxy.d.ts +0 -19
- /package/dist/chat/sandbox/{egress-oidc.d.ts → egress/oidc.d.ts} +0 -0
- /package/dist/chat/sandbox/{egress-schemas.d.ts → egress/schemas.d.ts} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
780
|
-
await this.state.set(tokenKey(userId, provider), parsed,
|
|
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/
|
|
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:
|
|
3865
|
-
limit:
|
|
3866
|
-
|
|
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:
|
|
3873
|
-
|
|
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:
|
|
3879
|
-
|
|
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:
|
|
3885
|
-
|
|
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:
|
|
3891
|
-
|
|
4358
|
+
inclusive: Type14.Optional(
|
|
4359
|
+
Type14.Boolean({
|
|
3892
4360
|
description: "Whether oldest/latest bounds should be inclusive."
|
|
3893
4361
|
})
|
|
3894
4362
|
),
|
|
3895
|
-
max_pages:
|
|
3896
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
4209
|
-
text:
|
|
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
|
|
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:
|
|
4254
|
-
emoji:
|
|
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
|
|
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 =
|
|
5064
|
+
var editReplacementSchema2 = Type17.Object(
|
|
4552
5065
|
{
|
|
4553
|
-
oldText:
|
|
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:
|
|
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:
|
|
4567
|
-
title:
|
|
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:
|
|
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:
|
|
5151
|
+
inputSchema: Type17.Object(
|
|
4639
5152
|
{
|
|
4640
|
-
canvas:
|
|
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:
|
|
4645
|
-
|
|
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:
|
|
4651
|
-
|
|
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:
|
|
5225
|
+
inputSchema: Type17.Object(
|
|
4713
5226
|
{
|
|
4714
|
-
canvas:
|
|
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:
|
|
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:
|
|
5311
|
+
inputSchema: Type17.Object(
|
|
4799
5312
|
{
|
|
4800
|
-
canvas:
|
|
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:
|
|
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
|
|
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:
|
|
5040
|
-
name:
|
|
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:
|
|
5076
|
-
items:
|
|
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:
|
|
5082
|
-
|
|
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:
|
|
5088
|
-
|
|
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:
|
|
5139
|
-
limit:
|
|
5140
|
-
|
|
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:
|
|
5686
|
+
inputSchema: Type18.Object(
|
|
5166
5687
|
{
|
|
5167
|
-
item_id:
|
|
5688
|
+
item_id: Type18.String({
|
|
5168
5689
|
minLength: 1,
|
|
5169
5690
|
description: "ID of the Slack list item to update."
|
|
5170
5691
|
}),
|
|
5171
|
-
completed:
|
|
5172
|
-
|
|
5692
|
+
completed: Type18.Optional(
|
|
5693
|
+
Type18.Boolean({
|
|
5173
5694
|
description: "Optional completion status update."
|
|
5174
5695
|
})
|
|
5175
5696
|
),
|
|
5176
|
-
title:
|
|
5177
|
-
|
|
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
|
|
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:
|
|
5410
|
-
url:
|
|
5411
|
-
|
|
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:
|
|
5417
|
-
|
|
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:
|
|
5423
|
-
|
|
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:
|
|
5429
|
-
|
|
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:
|
|
5436
|
-
|
|
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
|
|
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:
|
|
5678
|
-
user_id:
|
|
5679
|
-
|
|
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:
|
|
5685
|
-
|
|
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:
|
|
5691
|
-
|
|
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:
|
|
5697
|
-
|
|
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:
|
|
5704
|
-
|
|
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:
|
|
5711
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
5902
|
-
question:
|
|
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:
|
|
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
|
|
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:
|
|
6528
|
-
url:
|
|
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:
|
|
6533
|
-
|
|
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
|
|
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:
|
|
6646
|
-
query:
|
|
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:
|
|
6652
|
-
|
|
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
|
|
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:
|
|
7250
|
+
inputSchema: Type25.Object(
|
|
6730
7251
|
{
|
|
6731
|
-
path:
|
|
7252
|
+
path: Type25.String({
|
|
6732
7253
|
minLength: 1,
|
|
6733
7254
|
description: "Path to write in the sandbox workspace."
|
|
6734
7255
|
}),
|
|
6735
|
-
content:
|
|
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
|
|
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
|
|
7698
|
+
// src/chat/sandbox/egress/session.ts
|
|
7166
7699
|
import { createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
7167
7700
|
|
|
7168
|
-
// src/chat/sandbox/egress
|
|
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 =
|
|
7177
|
-
var httpStatusSchema =
|
|
7178
|
-
var providerNameSchema =
|
|
7179
|
-
var credentialSignalKindSchema =
|
|
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 =
|
|
7714
|
+
var sandboxEgressCredentialContextSchema = z2.object({
|
|
7182
7715
|
credentials: credentialContextSchema,
|
|
7183
|
-
egressId:
|
|
7716
|
+
egressId: z2.string().min(1),
|
|
7184
7717
|
expiresAtMs: finiteNumberSchema,
|
|
7185
|
-
contextId:
|
|
7718
|
+
contextId: z2.string().min(1)
|
|
7186
7719
|
}).strict();
|
|
7187
|
-
var sandboxEgressCredentialLeaseSchema =
|
|
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:
|
|
7193
|
-
headerTransforms:
|
|
7725
|
+
expiresAt: z2.string().min(1),
|
|
7726
|
+
headerTransforms: z2.array(pluginCredentialHeaderTransformSchema).min(1)
|
|
7194
7727
|
}).strict();
|
|
7195
|
-
var sandboxEgressAuthRequiredSignalSchema =
|
|
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:
|
|
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:
|
|
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 =
|
|
7744
|
+
var sandboxEgressPermissionDeniedSignalSchema = z2.object({
|
|
7212
7745
|
account: pluginProviderAccountSchema.optional(),
|
|
7213
|
-
acceptedPermissions:
|
|
7746
|
+
acceptedPermissions: z2.string().optional(),
|
|
7214
7747
|
grant: sandboxEgressGrantSchema,
|
|
7215
|
-
message:
|
|
7748
|
+
message: z2.string().min(1),
|
|
7216
7749
|
provider: providerNameSchema,
|
|
7217
|
-
source:
|
|
7218
|
-
sso:
|
|
7750
|
+
source: z2.literal("upstream"),
|
|
7751
|
+
sso: z2.string().optional(),
|
|
7219
7752
|
status: httpStatusSchema,
|
|
7220
|
-
upstreamHost:
|
|
7221
|
-
upstreamPath:
|
|
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
|
|
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
|
|
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 +
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7904
|
+
ttlMs2
|
|
7372
7905
|
);
|
|
7373
7906
|
}
|
|
7374
7907
|
async function setSandboxEgressPermissionDeniedSignal(context, signal) {
|
|
7375
|
-
const
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
10242
|
-
thinking_level:
|
|
10243
|
-
confidence:
|
|
10789
|
+
var turnExecutionProfileSchema = z3.object({
|
|
10790
|
+
thinking_level: z3.enum(TURN_THINKING_LEVELS),
|
|
10791
|
+
confidence: z3.preprocess(
|
|
10244
10792
|
coerceClassifierConfidence,
|
|
10245
|
-
|
|
10793
|
+
z3.number().min(0).max(1)
|
|
10246
10794
|
),
|
|
10247
|
-
reason:
|
|
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
|
-
|
|
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,
|