@vellumai/vellum-gateway 0.8.2 → 0.8.4
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/ARCHITECTURE.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/config-file-watcher.test.ts +57 -0
- package/src/__tests__/ipc-slack-thread-routes.test.ts +157 -0
- package/src/__tests__/route-schema-guard.test.ts +4 -0
- package/src/__tests__/slack-display-name.test.ts +218 -0
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +98 -4
- package/src/__tests__/twilio-webhooks.test.ts +47 -0
- package/src/auth/ipc-route-policy.ts +6 -0
- package/src/channels/inbound-event.ts +8 -2
- package/src/channels/types.ts +2 -0
- package/src/config-file-watcher.ts +44 -1
- package/src/db/slack-store.ts +10 -0
- package/src/feature-flag-registry.json +111 -23
- package/src/handlers/handle-inbound.ts +6 -4
- package/src/http/routes/a2a-routes.test.ts +129 -0
- package/src/http/routes/a2a-routes.ts +121 -0
- package/src/http/routes/twilio-voice-verify-callback.ts +41 -12
- package/src/http/routes/twilio-voice-webhook.test.ts +55 -0
- package/src/http/routes/twilio-voice-webhook.ts +10 -2
- package/src/index.ts +16 -0
- package/src/ipc/slack-thread-handlers.ts +39 -0
- package/src/risk/bash-risk-classifier.test.ts +24 -0
- package/src/risk/command-registry/commands/assistant.ts +33 -0
- package/src/risk/command-registry.test.ts +5 -0
- package/src/runtime/client.ts +66 -14
- package/src/slack/normalize.ts +78 -26
- package/src/slack/socket-mode.ts +2 -2
- package/src/twilio/validate-webhook.ts +7 -1
- package/src/types.ts +1 -0
- package/src/velay/client.test.ts +100 -0
- package/src/velay/client.ts +73 -0
|
@@ -4,6 +4,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
|
4
4
|
import type { GatewayConfig } from "../config.js";
|
|
5
5
|
import { SlackStore } from "../db/slack-store.js";
|
|
6
6
|
import * as schema from "../db/schema.js";
|
|
7
|
+
import type { RuntimeInboundPayload } from "../runtime/client.js";
|
|
7
8
|
import type { NormalizedSlackEvent } from "../slack/normalize.js";
|
|
8
9
|
|
|
9
10
|
type FetchFn = (
|
|
@@ -27,14 +28,42 @@ function makeSlackUserResponse(): Response {
|
|
|
27
28
|
let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () =>
|
|
28
29
|
makeSlackUserResponse(),
|
|
29
30
|
);
|
|
31
|
+
const runtimePayloads: RuntimeInboundPayload[] = [];
|
|
30
32
|
|
|
31
33
|
mock.module("../fetch.js", () => ({
|
|
32
34
|
fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
|
|
33
35
|
}));
|
|
34
36
|
|
|
37
|
+
mock.module("../runtime/client.js", () => ({
|
|
38
|
+
CircuitBreakerOpenError: class CircuitBreakerOpenError extends Error {
|
|
39
|
+
readonly retryAfterSecs: number;
|
|
40
|
+
|
|
41
|
+
constructor(retryAfterSecs: number) {
|
|
42
|
+
super("Circuit breaker is open");
|
|
43
|
+
this.name = "CircuitBreakerOpenError";
|
|
44
|
+
this.retryAfterSecs = retryAfterSecs;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
forwardToRuntime: mock(
|
|
48
|
+
async (_config: GatewayConfig, payload: RuntimeInboundPayload) => {
|
|
49
|
+
runtimePayloads.push(payload);
|
|
50
|
+
return {
|
|
51
|
+
accepted: true,
|
|
52
|
+
duplicate: false,
|
|
53
|
+
eventId: "runtime-event-1",
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
mock.module("../verification/text-verification.js", () => ({
|
|
60
|
+
tryTextVerificationIntercept: mock(async () => ({ intercepted: false })),
|
|
61
|
+
}));
|
|
62
|
+
|
|
35
63
|
const { SlackSocketModeClient } = await import("../slack/socket-mode.js");
|
|
36
64
|
const { clearChannelInfoCache, clearUserInfoCache, resolveSlackUser } =
|
|
37
65
|
await import("../slack/normalize.js");
|
|
66
|
+
const { handleInbound } = await import("../handlers/handle-inbound.js");
|
|
38
67
|
import type { SlackSocketModeConfig } from "../slack/socket-mode.js";
|
|
39
68
|
|
|
40
69
|
type SocketModeHarness = {
|
|
@@ -149,6 +178,7 @@ function flushAsyncEventEmission(): Promise<void> {
|
|
|
149
178
|
}
|
|
150
179
|
|
|
151
180
|
beforeEach(() => {
|
|
181
|
+
runtimePayloads.length = 0;
|
|
152
182
|
clearUserInfoCache();
|
|
153
183
|
clearChannelInfoCache();
|
|
154
184
|
fetchMock = mock(async () => makeSlackUserResponse());
|
|
@@ -706,21 +736,85 @@ describe("SlackSocketModeClient thread tracking", () => {
|
|
|
706
736
|
}
|
|
707
737
|
});
|
|
708
738
|
|
|
739
|
+
test("emits a plain app mention without resolving the event channel name", async () => {
|
|
740
|
+
const { rawDb, store } = createSlackStore();
|
|
741
|
+
const config = makeConfig();
|
|
742
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
743
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
744
|
+
const ws = makeOpenSocket();
|
|
745
|
+
const conversationInfoChannels: string[] = [];
|
|
746
|
+
|
|
747
|
+
fetchMock = mock(async (input) => {
|
|
748
|
+
const url = new URL(String(input));
|
|
749
|
+
if (url.pathname.endsWith("/conversations.info")) {
|
|
750
|
+
const channelId = url.searchParams.get("channel");
|
|
751
|
+
if (channelId) {
|
|
752
|
+
conversationInfoChannels.push(channelId);
|
|
753
|
+
}
|
|
754
|
+
return new Response(
|
|
755
|
+
JSON.stringify({
|
|
756
|
+
ok: true,
|
|
757
|
+
channel: { id: channelId, name: "support-triage" },
|
|
758
|
+
}),
|
|
759
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
return makeSlackUserResponse();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
client.handleMessage(
|
|
767
|
+
JSON.stringify({
|
|
768
|
+
envelope_id: "env-channel-name",
|
|
769
|
+
type: "events_api",
|
|
770
|
+
payload: {
|
|
771
|
+
event_id: "Ev-channel-name",
|
|
772
|
+
event: {
|
|
773
|
+
type: "app_mention",
|
|
774
|
+
user: "U-actor",
|
|
775
|
+
text: "<@UBOT> please summarize this",
|
|
776
|
+
ts: "1700000000.000950",
|
|
777
|
+
channel: "C-thread",
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
}),
|
|
781
|
+
ws,
|
|
782
|
+
);
|
|
783
|
+
await flushAsyncEventEmission();
|
|
784
|
+
|
|
785
|
+
expect(emitted).toHaveLength(1);
|
|
786
|
+
expect(emitted[0].event.source.channelName).toBeUndefined();
|
|
787
|
+
expect(conversationInfoChannels).toEqual([]);
|
|
788
|
+
|
|
789
|
+
await handleInbound(config, emitted[0].event, {
|
|
790
|
+
routingOverride: emitted[0].routing,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
expect(runtimePayloads).toHaveLength(1);
|
|
794
|
+
expect(runtimePayloads[0].sourceMetadata?.channelName).toBeUndefined();
|
|
795
|
+
} finally {
|
|
796
|
+
rawDb.close();
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
709
800
|
test("keeps embedded Slack channel labels without conversations.info lookup", async () => {
|
|
710
801
|
const { rawDb, store } = createSlackStore();
|
|
711
802
|
const emitted: NormalizedSlackEvent[] = [];
|
|
712
803
|
const client = createHarness(store, (event) => emitted.push(event));
|
|
713
804
|
const ws = makeOpenSocket();
|
|
714
|
-
|
|
805
|
+
const conversationInfoChannels: string[] = [];
|
|
715
806
|
|
|
716
807
|
fetchMock = mock(async (input) => {
|
|
717
808
|
const url = new URL(String(input));
|
|
718
809
|
if (url.pathname.endsWith("/conversations.info")) {
|
|
719
|
-
|
|
810
|
+
const channelId = url.searchParams.get("channel");
|
|
811
|
+
if (channelId) {
|
|
812
|
+
conversationInfoChannels.push(channelId);
|
|
813
|
+
}
|
|
720
814
|
return new Response(
|
|
721
815
|
JSON.stringify({
|
|
722
816
|
ok: true,
|
|
723
|
-
channel: { id:
|
|
817
|
+
channel: { id: channelId, name: "private-name" },
|
|
724
818
|
}),
|
|
725
819
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
726
820
|
);
|
|
@@ -752,7 +846,7 @@ describe("SlackSocketModeClient thread tracking", () => {
|
|
|
752
846
|
expect(emitted[0].event.message.content).toBe(
|
|
753
847
|
"@Example User continue in #visible-name",
|
|
754
848
|
);
|
|
755
|
-
expect(
|
|
849
|
+
expect(conversationInfoChannels).toEqual([]);
|
|
756
850
|
} finally {
|
|
757
851
|
rawDb.close();
|
|
758
852
|
}
|
|
@@ -826,6 +826,53 @@ describe("Twilio webhook signature with canonical ingress base URL", () => {
|
|
|
826
826
|
});
|
|
827
827
|
});
|
|
828
828
|
|
|
829
|
+
test("uses platform callback assistant ID for Twilio websocket URL", async () => {
|
|
830
|
+
const assistantId = "019e2d0d-f355-744c-a12c-d7e7dcefcf1e";
|
|
831
|
+
fetchMock = mock(
|
|
832
|
+
async () =>
|
|
833
|
+
new Response(
|
|
834
|
+
'<?xml version="1.0" encoding="UTF-8"?><Response><Connect>' +
|
|
835
|
+
'<ConversationRelay url="wss://__VELLUM_PUBLIC_BASE_URL__/webhooks/twilio/relay?token=__VELLUM_RELAY_TOKEN__"/>' +
|
|
836
|
+
"</Connect></Response>",
|
|
837
|
+
{
|
|
838
|
+
status: 200,
|
|
839
|
+
headers: { "Content-Type": "text/xml" },
|
|
840
|
+
},
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const handler = createTwilioVoiceWebhookHandler(
|
|
845
|
+
makeConfig({ velayBaseUrl: "https://velay-staging.vellum.ai" }),
|
|
846
|
+
makeCaches(),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const platformCallbackUrl =
|
|
850
|
+
`https://staging-platform.vellum.ai/v1/gateway/callbacks/${assistantId}` +
|
|
851
|
+
"/webhooks/twilio/voice?callSessionId=platform-wss-test";
|
|
852
|
+
const localUrl =
|
|
853
|
+
"http://localhost:7830/webhooks/twilio/voice?callSessionId=platform-wss-test";
|
|
854
|
+
const params = { CallSid: "CA-platform-wss" };
|
|
855
|
+
const signature = computeSignature(platformCallbackUrl, params, AUTH_TOKEN);
|
|
856
|
+
const req = new Request(localUrl, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: {
|
|
859
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
860
|
+
"X-Twilio-Signature": signature,
|
|
861
|
+
"X-Vellum-Ingress-URL": platformCallbackUrl,
|
|
862
|
+
},
|
|
863
|
+
body: new URLSearchParams(params).toString(),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const res = await handler(req);
|
|
867
|
+
expect(res.status).toBe(200);
|
|
868
|
+
const body = await res.text();
|
|
869
|
+
expect(body).toContain(
|
|
870
|
+
`url="wss://velay-staging.vellum.ai/${assistantId}/webhooks/twilio/relay?`,
|
|
871
|
+
);
|
|
872
|
+
expect(body).not.toContain("__VELLUM_PUBLIC_BASE_URL__");
|
|
873
|
+
expect(body).not.toContain("staging-platform.vellum.ai");
|
|
874
|
+
});
|
|
875
|
+
|
|
829
876
|
test("platform proxy URL takes priority over configured ingress", async () => {
|
|
830
877
|
fetchMock = mock(
|
|
831
878
|
async () =>
|
|
@@ -223,6 +223,12 @@ const POLICY_TABLE: PolicyEntry[] = [
|
|
|
223
223
|
["updateHeartbeatConfig", ["settings.write"]],
|
|
224
224
|
|
|
225
225
|
// Integrations / ingress
|
|
226
|
+
[
|
|
227
|
+
"integrations_a2a_invite_complete_post",
|
|
228
|
+
["internal.write"],
|
|
229
|
+
["svc_gateway"],
|
|
230
|
+
],
|
|
231
|
+
["integrations_a2a_invite_redeem_post", ["internal.write"], ["svc_gateway"]],
|
|
226
232
|
["integrations_ingress_config_get", ["settings.read"]],
|
|
227
233
|
["integrations_ingress_config_put", ["settings.write"]],
|
|
228
234
|
["integrations_oauth_start_post", ["settings.write"]],
|
|
@@ -10,7 +10,7 @@ import type { ChannelId } from "./types.js";
|
|
|
10
10
|
|
|
11
11
|
export type InboundChannelId = Extract<
|
|
12
12
|
ChannelId,
|
|
13
|
-
"telegram" | "whatsapp" | "slack" | "email"
|
|
13
|
+
"telegram" | "whatsapp" | "slack" | "email" | "a2a"
|
|
14
14
|
>;
|
|
15
15
|
|
|
16
16
|
interface InboundEventBase<C extends InboundChannelId> {
|
|
@@ -40,6 +40,9 @@ interface InboundEventBase<C extends InboundChannelId> {
|
|
|
40
40
|
lastName?: string;
|
|
41
41
|
languageCode?: string;
|
|
42
42
|
isBot?: boolean;
|
|
43
|
+
timezone?: string;
|
|
44
|
+
timezoneLabel?: string;
|
|
45
|
+
timezoneOffsetSeconds?: number;
|
|
43
46
|
};
|
|
44
47
|
source: {
|
|
45
48
|
updateId: string;
|
|
@@ -51,6 +54,7 @@ interface InboundEventBase<C extends InboundChannelId> {
|
|
|
51
54
|
* `In-Reply-To`, etc.) can reuse the field later.
|
|
52
55
|
*/
|
|
53
56
|
threadId?: string;
|
|
57
|
+
channelName?: string;
|
|
54
58
|
};
|
|
55
59
|
raw: Record<string, unknown>;
|
|
56
60
|
}
|
|
@@ -59,9 +63,11 @@ export type TelegramInboundEvent = InboundEventBase<"telegram">;
|
|
|
59
63
|
export type WhatsAppInboundEvent = InboundEventBase<"whatsapp">;
|
|
60
64
|
export type SlackInboundEvent = InboundEventBase<"slack">;
|
|
61
65
|
export type EmailInboundEvent = InboundEventBase<"email">;
|
|
66
|
+
export type A2aInboundEvent = InboundEventBase<"a2a">;
|
|
62
67
|
|
|
63
68
|
export type GatewayInboundEvent =
|
|
64
69
|
| TelegramInboundEvent
|
|
65
70
|
| WhatsAppInboundEvent
|
|
66
71
|
| SlackInboundEvent
|
|
67
|
-
| EmailInboundEvent
|
|
72
|
+
| EmailInboundEvent
|
|
73
|
+
| A2aInboundEvent;
|
package/src/channels/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const CHANNEL_IDS = [
|
|
|
5
5
|
"whatsapp",
|
|
6
6
|
"slack",
|
|
7
7
|
"email",
|
|
8
|
+
"a2a",
|
|
8
9
|
] as const;
|
|
9
10
|
|
|
10
11
|
export type ChannelId = (typeof CHANNEL_IDS)[number];
|
|
@@ -30,6 +31,7 @@ export const INTERFACE_IDS = [
|
|
|
30
31
|
"whatsapp",
|
|
31
32
|
"slack",
|
|
32
33
|
"email",
|
|
34
|
+
"a2a",
|
|
33
35
|
] as const;
|
|
34
36
|
|
|
35
37
|
export type InterfaceId = (typeof INTERFACE_IDS)[number];
|
|
@@ -15,6 +15,14 @@ import { getLogger } from "./logger.js";
|
|
|
15
15
|
const log = getLogger("config-file-watcher");
|
|
16
16
|
|
|
17
17
|
const DEBOUNCE_MS = 500;
|
|
18
|
+
const FALLBACK_POLL_MS = 1_000;
|
|
19
|
+
|
|
20
|
+
type IntervalHandle = ReturnType<typeof setInterval>;
|
|
21
|
+
|
|
22
|
+
type TimerApi = {
|
|
23
|
+
setInterval: (fn: () => void, delayMs: number) => IntervalHandle;
|
|
24
|
+
clearInterval: (timer: IntervalHandle) => void;
|
|
25
|
+
};
|
|
18
26
|
|
|
19
27
|
export type ConfigChangeEvent = {
|
|
20
28
|
/** Full parsed config.json data. */
|
|
@@ -32,16 +40,27 @@ export type ConfigChangeCallback = (event: ConfigChangeEvent) => void;
|
|
|
32
40
|
|
|
33
41
|
export class ConfigFileWatcher {
|
|
34
42
|
private watcher: FSWatcher | null = null;
|
|
43
|
+
private pollTimer: IntervalHandle | null = null;
|
|
35
44
|
private watchingDirectory = false;
|
|
36
45
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
37
46
|
private lastSerialized: Map<string, string> = new Map();
|
|
38
47
|
private lastValues: Map<string, unknown> = new Map();
|
|
39
48
|
private callback: ConfigChangeCallback;
|
|
40
49
|
private configPath: string;
|
|
50
|
+
private pollIntervalMs: number;
|
|
51
|
+
private timerApi: TimerApi;
|
|
41
52
|
|
|
42
|
-
constructor(
|
|
53
|
+
constructor(
|
|
54
|
+
callback: ConfigChangeCallback,
|
|
55
|
+
opts?: { pollIntervalMs?: number; timerApi?: TimerApi },
|
|
56
|
+
) {
|
|
43
57
|
this.callback = callback;
|
|
44
58
|
this.configPath = getConfigPath();
|
|
59
|
+
this.pollIntervalMs = opts?.pollIntervalMs ?? FALLBACK_POLL_MS;
|
|
60
|
+
this.timerApi = opts?.timerApi ?? {
|
|
61
|
+
setInterval,
|
|
62
|
+
clearInterval,
|
|
63
|
+
};
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
start(): void {
|
|
@@ -77,6 +96,8 @@ export class ConfigFileWatcher {
|
|
|
77
96
|
"Failed to start config file watcher",
|
|
78
97
|
);
|
|
79
98
|
}
|
|
99
|
+
|
|
100
|
+
this.startFallbackPolling();
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
stop(): void {
|
|
@@ -84,12 +105,34 @@ export class ConfigFileWatcher {
|
|
|
84
105
|
clearTimeout(this.debounceTimer);
|
|
85
106
|
this.debounceTimer = null;
|
|
86
107
|
}
|
|
108
|
+
if (this.pollTimer) {
|
|
109
|
+
this.timerApi.clearInterval(this.pollTimer);
|
|
110
|
+
this.pollTimer = null;
|
|
111
|
+
}
|
|
87
112
|
if (this.watcher) {
|
|
88
113
|
this.watcher.close();
|
|
89
114
|
this.watcher = null;
|
|
90
115
|
}
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
private startFallbackPolling(): void {
|
|
119
|
+
if (this.pollIntervalMs <= 0 || this.pollTimer) return;
|
|
120
|
+
|
|
121
|
+
this.pollTimer = this.timerApi.setInterval(() => {
|
|
122
|
+
this.pollOnce();
|
|
123
|
+
|
|
124
|
+
if (this.watchingDirectory && existsSync(this.configPath)) {
|
|
125
|
+
this.upgradeWatcher();
|
|
126
|
+
}
|
|
127
|
+
}, this.pollIntervalMs);
|
|
128
|
+
(this.pollTimer as { unref?: () => void }).unref?.();
|
|
129
|
+
|
|
130
|
+
log.info(
|
|
131
|
+
{ intervalMs: this.pollIntervalMs },
|
|
132
|
+
"Polling config file for missed change events",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
private scheduleCheck(): void {
|
|
94
137
|
if (this.debounceTimer) {
|
|
95
138
|
clearTimeout(this.debounceTimer);
|
package/src/db/slack-store.ts
CHANGED
|
@@ -67,6 +67,16 @@ export class SlackStore {
|
|
|
67
67
|
return row !== undefined;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
detachThread(threadTs: string, channelId: string): boolean {
|
|
71
|
+
const raw = (this.db as unknown as { $client: Database }).$client;
|
|
72
|
+
const changes = raw
|
|
73
|
+
.prepare(
|
|
74
|
+
"DELETE FROM slack_active_threads WHERE thread_ts = ? AND channel_id = ?",
|
|
75
|
+
)
|
|
76
|
+
.run(threadTs, channelId).changes;
|
|
77
|
+
return changes > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
/**
|
|
71
81
|
* Returns all unexpired active threads with a known channel for reconnect
|
|
72
82
|
* catch-up. Rows with a NULL `channel_id` (legacy rows from before the
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"defaultEnabled": false
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
-
"id": "memory-retrospective",
|
|
13
|
+
"id": "memory-retrospective-fork",
|
|
14
14
|
"scope": "assistant",
|
|
15
|
-
"key": "memory-retrospective",
|
|
16
|
-
"label": "
|
|
17
|
-
"description": "
|
|
15
|
+
"key": "memory-retrospective-fork",
|
|
16
|
+
"label": "Fork-based memory retrospective",
|
|
17
|
+
"description": "Fork the source conversation through its latest message for memory retrospectives, instead of rendering the slice into a transcript and waking an empty background conversation. Lets the retrospective hit the provider prompt cache and read compaction summary + tail messages natively.",
|
|
18
18
|
"defaultEnabled": false
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
"description": "When enabled, the Local hosting option uses Docker under the hood for sandboxed execution, hiding the separate Docker card",
|
|
34
34
|
"defaultEnabled": false
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
"id": "a2a-channel",
|
|
38
|
+
"scope": "assistant",
|
|
39
|
+
"key": "a2a-channel",
|
|
40
|
+
"label": "A2A Channel",
|
|
41
|
+
"description": "Enable the A2A (Agent-to-Agent) channel for inter-assistant communication via the open A2A protocol",
|
|
42
|
+
"defaultEnabled": false
|
|
43
|
+
},
|
|
36
44
|
{
|
|
37
45
|
"id": "email-channel",
|
|
38
46
|
"scope": "assistant",
|
|
@@ -43,7 +51,7 @@
|
|
|
43
51
|
},
|
|
44
52
|
{
|
|
45
53
|
"id": "settings-developer-nav",
|
|
46
|
-
"scope": "
|
|
54
|
+
"scope": "assistant",
|
|
47
55
|
"key": "settings-developer-nav",
|
|
48
56
|
"label": "Settings Developer Nav",
|
|
49
57
|
"description": "Control Developer nav visibility in macOS settings",
|
|
@@ -89,6 +97,14 @@
|
|
|
89
97
|
"description": "Surface credential grant and audit inspection endpoints for reviewing active grants and access logs",
|
|
90
98
|
"defaultEnabled": false
|
|
91
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"id": "chatgpt-subscription-auth",
|
|
102
|
+
"scope": "assistant",
|
|
103
|
+
"key": "chatgpt-subscription-auth",
|
|
104
|
+
"label": "ChatGPT Subscription Auth",
|
|
105
|
+
"description": "Enable ChatGPT subscription OAuth as a provider auth type for OpenAI models, using the Codex device-code flow.",
|
|
106
|
+
"defaultEnabled": false
|
|
107
|
+
},
|
|
92
108
|
{
|
|
93
109
|
"id": "deploy-to-vercel",
|
|
94
110
|
"scope": "assistant",
|
|
@@ -145,6 +161,14 @@
|
|
|
145
161
|
"description": "Show a speaker button on assistant messages to generate and play the message as audio via Fish Audio TTS",
|
|
146
162
|
"defaultEnabled": false
|
|
147
163
|
},
|
|
164
|
+
{
|
|
165
|
+
"id": "openai-compatible-endpoints",
|
|
166
|
+
"scope": "assistant",
|
|
167
|
+
"key": "openai-compatible-endpoints",
|
|
168
|
+
"label": "OpenAI-Compatible Endpoints",
|
|
169
|
+
"description": "Enable user-configured OpenAI-compatible inference endpoints with custom base URLs and model identifiers",
|
|
170
|
+
"defaultEnabled": false
|
|
171
|
+
},
|
|
148
172
|
{
|
|
149
173
|
"id": "backward-releases",
|
|
150
174
|
"scope": "assistant",
|
|
@@ -267,7 +291,7 @@
|
|
|
267
291
|
},
|
|
268
292
|
{
|
|
269
293
|
"id": "account-deletion",
|
|
270
|
-
"scope": "
|
|
294
|
+
"scope": "assistant",
|
|
271
295
|
"key": "account-deletion",
|
|
272
296
|
"label": "Account Deletion",
|
|
273
297
|
"description": "Surfaces the user-initiated account deletion flow in client settings.",
|
|
@@ -281,14 +305,6 @@
|
|
|
281
305
|
"description": "Enable the app-control skill (per-app screenshot + raw input bypassing AX tree)",
|
|
282
306
|
"defaultEnabled": false
|
|
283
307
|
},
|
|
284
|
-
{
|
|
285
|
-
"id": "species-migration",
|
|
286
|
-
"scope": "assistant",
|
|
287
|
-
"key": "species-migration",
|
|
288
|
-
"label": "Species Migration",
|
|
289
|
-
"description": "Enable the Species Migration skill for migrating from OpenClaw, Hermes, Manus, and other assistant species into Vellum.",
|
|
290
|
-
"defaultEnabled": false
|
|
291
|
-
},
|
|
292
308
|
{
|
|
293
309
|
"id": "analyze-conversation",
|
|
294
310
|
"scope": "assistant",
|
|
@@ -299,7 +315,7 @@
|
|
|
299
315
|
},
|
|
300
316
|
{
|
|
301
317
|
"id": "pro-plan-adjust",
|
|
302
|
-
"scope": "
|
|
318
|
+
"scope": "client",
|
|
303
319
|
"key": "pro-plan-adjust",
|
|
304
320
|
"label": "Pro Plan Adjust",
|
|
305
321
|
"description": "Show the rich Plan card (current plan, features, Manage/Upgrade CTA) at the top of the macOS Settings \u2192 Billing tab.",
|
|
@@ -322,19 +338,91 @@
|
|
|
322
338
|
"defaultEnabled": false
|
|
323
339
|
},
|
|
324
340
|
{
|
|
325
|
-
"id": "
|
|
341
|
+
"id": "velvet-theme",
|
|
342
|
+
"scope": "client",
|
|
343
|
+
"key": "velvet-theme",
|
|
344
|
+
"label": "Velvet Theme",
|
|
345
|
+
"description": "Show the Velvet theme option in the macOS appearance settings. Velvet is a dark-mode variant with red/pink accent colors.",
|
|
346
|
+
"defaultEnabled": false
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"id": "query-complexity-routing",
|
|
350
|
+
"scope": "assistant",
|
|
351
|
+
"key": "query-complexity-routing",
|
|
352
|
+
"label": "Query Complexity Routing",
|
|
353
|
+
"description": "Automatically route user messages to the most appropriate inference profile based on query complexity. Simple queries use the speed profile, complex queries escalate to the quality profile. The user is notified of each switch and can opt out by pinning a profile on the conversation.",
|
|
354
|
+
"defaultEnabled": false
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"id": "queue-steering",
|
|
326
358
|
"scope": "assistant",
|
|
327
|
-
"key": "
|
|
328
|
-
"label": "
|
|
329
|
-
"description": "Enable the
|
|
359
|
+
"key": "queue-steering",
|
|
360
|
+
"label": "Queue Steering",
|
|
361
|
+
"description": "Enable the 'Push to agent' button on queued messages, allowing users to steer the assistant to a specific queued message by aborting the current generation and promoting the message to the head of the queue.",
|
|
362
|
+
"defaultEnabled": false
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "chat-pull-to-refresh-enabled",
|
|
366
|
+
"scope": "client",
|
|
367
|
+
"key": "chat-pull-to-refresh-enabled",
|
|
368
|
+
"label": "Chat Pull to Refresh",
|
|
369
|
+
"description": "Enable pull-to-refresh gesture in the chat view.",
|
|
330
370
|
"defaultEnabled": false
|
|
331
371
|
},
|
|
332
372
|
{
|
|
333
|
-
"id": "
|
|
373
|
+
"id": "doctor",
|
|
374
|
+
"scope": "client",
|
|
375
|
+
"key": "doctor",
|
|
376
|
+
"label": "Doctor",
|
|
377
|
+
"description": "Enable the Doctor diagnostic tab in Debug settings.",
|
|
378
|
+
"defaultEnabled": false
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "home-page",
|
|
382
|
+
"scope": "client",
|
|
383
|
+
"key": "home-page",
|
|
384
|
+
"label": "Home Page",
|
|
385
|
+
"description": "Enable the Home page as the default landing view.",
|
|
386
|
+
"defaultEnabled": false
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"id": "platform-notifications",
|
|
390
|
+
"scope": "client",
|
|
391
|
+
"key": "platform-notifications",
|
|
392
|
+
"label": "Platform Notifications",
|
|
393
|
+
"description": "Enable the Notifications tab in settings.",
|
|
394
|
+
"defaultEnabled": false
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"id": "rollback-enabled",
|
|
398
|
+
"scope": "assistant",
|
|
399
|
+
"key": "rollback-enabled",
|
|
400
|
+
"label": "Rollback Enabled",
|
|
401
|
+
"description": "Show older versions in the version picker, allowing rollback to previous releases.",
|
|
402
|
+
"defaultEnabled": false
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"id": "self-hosted-assistant",
|
|
406
|
+
"scope": "client",
|
|
407
|
+
"key": "self-hosted-assistant",
|
|
408
|
+
"label": "Self-Hosted Assistant",
|
|
409
|
+
"description": "Enable self-hosted assistant configuration.",
|
|
410
|
+
"defaultEnabled": false
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"id": "settings-sleep-policy",
|
|
334
414
|
"scope": "assistant",
|
|
335
|
-
"key": "
|
|
336
|
-
"label": "
|
|
337
|
-
"description": "Enable
|
|
415
|
+
"key": "settings-sleep-policy",
|
|
416
|
+
"label": "Settings Sleep Policy",
|
|
417
|
+
"description": "Enable sleep policy settings.",
|
|
418
|
+
"defaultEnabled": false
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"id": "velvet",
|
|
422
|
+
"scope": "client",
|
|
423
|
+
"key": "velvet",
|
|
424
|
+
"label": "Velvet",
|
|
425
|
+
"description": "Enable the Velvet design theme.",
|
|
338
426
|
"defaultEnabled": false
|
|
339
427
|
}
|
|
340
428
|
]
|
|
@@ -15,7 +15,6 @@ import type { RuntimeInboundResponse } from "../runtime/client.js";
|
|
|
15
15
|
import type { GatewayInboundEvent } from "../types.js";
|
|
16
16
|
import { tryTextVerificationIntercept } from "../verification/text-verification.js";
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
const log = getLogger("handle-inbound");
|
|
20
19
|
|
|
21
20
|
export type InboundResult = {
|
|
@@ -119,6 +118,7 @@ export async function handleInbound(
|
|
|
119
118
|
options?.transportMetadata?.hints,
|
|
120
119
|
);
|
|
121
120
|
const transportUxBrief = options?.transportMetadata?.uxBrief?.trim();
|
|
121
|
+
const sourceChannelName = event.source.channelName?.trim();
|
|
122
122
|
|
|
123
123
|
try {
|
|
124
124
|
const response = await forwardToRuntime(
|
|
@@ -144,8 +144,12 @@ export async function handleInbound(
|
|
|
144
144
|
messageId: event.source.messageId,
|
|
145
145
|
chatType: event.source.chatType,
|
|
146
146
|
...(event.source.threadId ? { threadId: event.source.threadId } : {}),
|
|
147
|
+
...(sourceChannelName ? { channelName: sourceChannelName } : {}),
|
|
147
148
|
languageCode: event.actor.languageCode,
|
|
148
149
|
isBot: event.actor.isBot,
|
|
150
|
+
timezone: event.actor.timezone,
|
|
151
|
+
timezoneLabel: event.actor.timezoneLabel,
|
|
152
|
+
timezoneOffsetSeconds: event.actor.timezoneOffsetSeconds,
|
|
149
153
|
...(transportHints.length > 0 ? { hints: transportHints } : {}),
|
|
150
154
|
...(transportUxBrief ? { uxBrief: transportUxBrief } : {}),
|
|
151
155
|
...(options?.sourceMetadata ?? {}),
|
|
@@ -176,9 +180,7 @@ export async function handleInbound(
|
|
|
176
180
|
// writes to both assistant DB and gateway DB. Fire-and-forget so
|
|
177
181
|
// IPC failures here cannot leak as unhandled rejections.
|
|
178
182
|
if (!response.denied) {
|
|
179
|
-
void touchContactChannelStats(event, response.duplicate).catch(
|
|
180
|
-
() => {},
|
|
181
|
-
);
|
|
183
|
+
void touchContactChannelStats(event, response.duplicate).catch(() => {});
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
return { forwarded: true, rejected: false, runtimeResponse: response };
|