@vellumai/assistant 0.11.5 → 0.11.6-staging.1
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/AGENTS.md +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -86,95 +86,155 @@ export const SlackStreamTaskSchema = z.object({
|
|
|
86
86
|
|
|
87
87
|
export type SlackStreamTask = z.infer<typeof SlackStreamTaskSchema>;
|
|
88
88
|
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Audience
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
89
93
|
/**
|
|
90
|
-
*
|
|
91
|
-
* `chat.startStream` / `chat.appendStream` / `chat.stopStream` Web API methods.
|
|
94
|
+
* Restricts a message to a single reader in a room that has more.
|
|
92
95
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
96
|
+
* A single object rather than a flag beside an id, because the two are only
|
|
97
|
+
* meaningful together: a channel cannot address one reader without knowing
|
|
98
|
+
* which. Slack reads this as `chat.postEphemeral`; a channel whose rooms hold
|
|
99
|
+
* one reader already has nothing to do.
|
|
100
|
+
*/
|
|
101
|
+
export const MessageAudienceSchema = z.object({
|
|
102
|
+
kind: z.literal("oneReader"),
|
|
103
|
+
/** The reader, in the channel's own id space. */
|
|
104
|
+
userId: z.string(),
|
|
105
|
+
/**
|
|
106
|
+
* The organization the reader belongs to, for a channel whose user ids are
|
|
107
|
+
* only unique within one.
|
|
108
|
+
*
|
|
109
|
+
* Not every channel federates. Telegram and Discord user ids are global and
|
|
110
|
+
* ignore this; Slack ids are scoped to a workspace, and a guest reaching a
|
|
111
|
+
* shared channel from another one is only identified by the pair. A channel
|
|
112
|
+
* that needs it and does not get it cannot address the reader, so it treats
|
|
113
|
+
* the audience as unexpressible rather than guessing.
|
|
114
|
+
*/
|
|
115
|
+
userOrgId: z.string().optional(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type MessageAudience = z.infer<typeof MessageAudienceSchema>;
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Streaming a reply that grows
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One step of a plan carried alongside a growing reply: an ordered,
|
|
126
|
+
* status-bearing unit of work the assistant reports while a turn runs.
|
|
98
127
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
128
|
+
* The vocabulary is the assistant's own `task_progress` surface rather than any
|
|
129
|
+
* channel's, so a channel that renders plans natively converts on its way out
|
|
130
|
+
* and a channel that cannot writes them as text. Slack's `task_update` chunk
|
|
131
|
+
* spells two of these statuses differently and its transport translates.
|
|
132
|
+
*/
|
|
133
|
+
export const StreamPlanStepSchema = z.object({
|
|
134
|
+
label: z.string(),
|
|
135
|
+
status: z.enum(["pending", "in_progress", "completed", "failed"]),
|
|
136
|
+
detail: z.string().optional(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export type StreamPlanStep = z.infer<typeof StreamPlanStepSchema>;
|
|
140
|
+
|
|
141
|
+
/** A plan shown with a growing reply: an optional title over ordered steps. */
|
|
142
|
+
export const StreamPlanSchema = z.object({
|
|
143
|
+
title: z.string().optional(),
|
|
144
|
+
steps: z.array(StreamPlanStepSchema),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export type StreamPlan = z.infer<typeof StreamPlanSchema>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One operation on a reply that grows while the turn runs.
|
|
151
|
+
*
|
|
152
|
+
* Channels that can do this disagree on what the growing thing *is*, and the
|
|
153
|
+
* split is preview versus persist. Slack streams the reply itself:
|
|
154
|
+
* `chat.startStream` opens the message that will remain, and `chat.stopStream`
|
|
155
|
+
* finalizes it in place. Telegram streams a live draft, which is ephemeral,
|
|
156
|
+
* expires on its own, and never becomes the reply; the reply is persisted
|
|
157
|
+
* afterwards by an ordinary send. Discord has no primitive for either and
|
|
158
|
+
* omits `streamReply`, so the caller sends the finished reply and nothing is
|
|
159
|
+
* simulated on its behalf.
|
|
160
|
+
*
|
|
161
|
+
* That is why every operation carries both `text`, the whole of what the reply
|
|
162
|
+
* now reads as, and `appended`, only what is new since the last operation.
|
|
163
|
+
* Slack's append takes the delta; Telegram's draft takes the partial text and
|
|
164
|
+
* lets its clients animate the difference. Neither channel has to know which
|
|
165
|
+
* kind the other is, and a third can read whichever it needs.
|
|
166
|
+
*
|
|
167
|
+
* `text` is required on every operation because a channel that rewrites needs
|
|
168
|
+
* it on every one, and it is what the reply reads as rather than what this
|
|
169
|
+
* operation changed: absent is never the right answer, where empty can be.
|
|
170
|
+
* `appended` is what moved, so it is absent when only the plan did.
|
|
171
|
+
*
|
|
172
|
+
* `start` opens the growing reply and returns a `streamId` in the channel's
|
|
173
|
+
* own id space; `append` advances it; `stop` ends it. What `stop` leaves
|
|
174
|
+
* behind differs by channel, which is why it carries the complete reply rather
|
|
175
|
+
* than a remainder: a channel that finalized in place has already shown it,
|
|
176
|
+
* and a channel whose preview evaporates needs it to send the real message.
|
|
102
177
|
*/
|
|
103
|
-
export const
|
|
178
|
+
export const StreamOpSchema = z
|
|
104
179
|
.discriminatedUnion("action", [
|
|
105
180
|
z.object({
|
|
106
181
|
action: z.literal("start"),
|
|
107
|
-
threadTs: z.string(),
|
|
108
|
-
markdownText: z.string().optional(),
|
|
109
|
-
taskDisplayMode: z.literal("plan").optional(),
|
|
110
|
-
/** Title of the plan block, serialized as a `plan_update` chunk. */
|
|
111
|
-
planTitle: z.string().optional(),
|
|
112
|
-
tasks: z.array(SlackStreamTaskSchema).optional(),
|
|
113
182
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
183
|
+
* The message this reply grows under, in the channel's own id space.
|
|
184
|
+
*
|
|
185
|
+
* Slack requires one: `chat.startStream` streams into a thread, and
|
|
186
|
+
* omitting it is only accepted where a whole channel is one session. A
|
|
187
|
+
* channel that threads nothing ignores it.
|
|
116
188
|
*/
|
|
117
|
-
|
|
189
|
+
anchorMessageId: z.string().optional(),
|
|
190
|
+
text: z.string(),
|
|
191
|
+
appended: z.string().optional(),
|
|
192
|
+
plan: StreamPlanSchema.optional(),
|
|
118
193
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
194
|
+
* Who may see the growing reply. Absent means everyone in the room, the
|
|
195
|
+
* same reading the finished reply gives it.
|
|
121
196
|
*/
|
|
122
|
-
|
|
197
|
+
audience: MessageAudienceSchema.optional(),
|
|
123
198
|
}),
|
|
124
199
|
z.object({
|
|
125
200
|
action: z.literal("append"),
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
201
|
+
/** The open reply, in the channel's own id space. */
|
|
202
|
+
streamId: z.string(),
|
|
203
|
+
text: z.string(),
|
|
204
|
+
appended: z.string().optional(),
|
|
205
|
+
plan: StreamPlanSchema.optional(),
|
|
131
206
|
}),
|
|
132
207
|
z.object({
|
|
133
208
|
action: z.literal("stop"),
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
209
|
+
streamId: z.string(),
|
|
210
|
+
/**
|
|
211
|
+
* The complete reply, not the remainder. A channel whose preview
|
|
212
|
+
* evaporates sends this as the message that stays; one that finalizes
|
|
213
|
+
* its stream in place has already shown it and appends `appended`.
|
|
214
|
+
*/
|
|
215
|
+
text: z.string(),
|
|
216
|
+
appended: z.string().optional(),
|
|
217
|
+
plan: StreamPlanSchema.optional(),
|
|
140
218
|
}),
|
|
141
219
|
])
|
|
142
220
|
.superRefine((op, ctx) => {
|
|
143
|
-
//
|
|
144
|
-
//
|
|
221
|
+
// An operation has to move something: new words, or the plan beside them.
|
|
222
|
+
// `text` alone cannot, since it restates what the reply already reads as.
|
|
223
|
+
// Slack rejects a start or append carrying neither, and a channel that
|
|
224
|
+
// rewrites would spend an edit redrawing what is already on screen.
|
|
145
225
|
if (
|
|
146
226
|
(op.action === "start" || op.action === "append") &&
|
|
147
|
-
op.
|
|
148
|
-
op.
|
|
227
|
+
op.appended === undefined &&
|
|
228
|
+
op.plan === undefined
|
|
149
229
|
) {
|
|
150
230
|
ctx.addIssue({
|
|
151
231
|
code: z.ZodIssueCode.custom,
|
|
152
|
-
message: `${op.action} requires
|
|
232
|
+
message: `${op.action} requires appended or plan`,
|
|
153
233
|
});
|
|
154
234
|
}
|
|
155
235
|
});
|
|
156
236
|
|
|
157
|
-
export type
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Audience
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Restricts a message to a single reader in a room that has more.
|
|
165
|
-
*
|
|
166
|
-
* A single object rather than a flag beside an id, because the two are only
|
|
167
|
-
* meaningful together: a channel cannot address one reader without knowing
|
|
168
|
-
* which. Slack reads this as `chat.postEphemeral`; a channel whose rooms hold
|
|
169
|
-
* one reader already has nothing to do.
|
|
170
|
-
*/
|
|
171
|
-
export const MessageAudienceSchema = z.object({
|
|
172
|
-
kind: z.literal("oneReader"),
|
|
173
|
-
/** The reader, in the channel's own id space. */
|
|
174
|
-
userId: z.string(),
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
export type MessageAudience = z.infer<typeof MessageAudienceSchema>;
|
|
237
|
+
export type StreamOp = z.infer<typeof StreamOpSchema>;
|
|
178
238
|
|
|
179
239
|
// ---------------------------------------------------------------------------
|
|
180
240
|
// Channel reply payload — the full outbound wire format
|
|
@@ -197,7 +257,7 @@ export const ChannelReplyPayloadSchema = z.object({
|
|
|
197
257
|
* Each channel decides what that means, and one that cannot ignores it.
|
|
198
258
|
* Slack is the only channel acting on it today, where it becomes Block Kit.
|
|
199
259
|
*/
|
|
200
|
-
|
|
260
|
+
renderRichly: z.boolean().optional(),
|
|
201
261
|
});
|
|
202
262
|
|
|
203
263
|
export type ChannelReplyPayload = z.infer<typeof ChannelReplyPayloadSchema>;
|
|
@@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeHttpPublicBaseUrl,
|
|
5
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash,
|
|
5
6
|
normalizePublicBaseUrl,
|
|
7
|
+
parseRecordedAssistantId,
|
|
8
|
+
parseTunnelRecord,
|
|
9
|
+
trimmedNonEmptyString,
|
|
10
|
+
TUNNEL_PROVIDERS,
|
|
6
11
|
velayHostForPlatformHost,
|
|
7
12
|
} from "../ingress.js";
|
|
8
13
|
import {
|
|
@@ -85,6 +90,35 @@ describe("normalizeHttpPublicBaseUrl", () => {
|
|
|
85
90
|
});
|
|
86
91
|
});
|
|
87
92
|
|
|
93
|
+
describe("normalizeHttpPublicBaseUrlWithoutTrailingSlash", () => {
|
|
94
|
+
test("drops the root path its sibling always emits", () => {
|
|
95
|
+
expect(
|
|
96
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test"),
|
|
97
|
+
).toBe("https://x.test");
|
|
98
|
+
expect(
|
|
99
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test/"),
|
|
100
|
+
).toBe("https://x.test");
|
|
101
|
+
expect(
|
|
102
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(" https://x.test/api/// "),
|
|
103
|
+
).toBe("https://x.test/api");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("rejects everything its sibling rejects", () => {
|
|
107
|
+
for (const value of [
|
|
108
|
+
"",
|
|
109
|
+
" ",
|
|
110
|
+
"notaurl",
|
|
111
|
+
"ftp://x.test",
|
|
112
|
+
"https://x.test?a=b",
|
|
113
|
+
42,
|
|
114
|
+
]) {
|
|
115
|
+
expect(
|
|
116
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(value),
|
|
117
|
+
).toBeUndefined();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
describe("Twilio ingress helpers", () => {
|
|
89
123
|
test("resolves public base URL with fallback", () => {
|
|
90
124
|
expect(
|
|
@@ -123,3 +157,87 @@ describe("Twilio ingress helpers", () => {
|
|
|
123
157
|
});
|
|
124
158
|
});
|
|
125
159
|
});
|
|
160
|
+
|
|
161
|
+
describe("parseTunnelRecord", () => {
|
|
162
|
+
const TUNNEL_URL = "https://assistant-1.example.ts.net";
|
|
163
|
+
|
|
164
|
+
test("accepts every provider in the registry", () => {
|
|
165
|
+
for (const provider of TUNNEL_PROVIDERS) {
|
|
166
|
+
expect(
|
|
167
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
168
|
+
).toEqual({ provider, publicBaseUrl: TUNNEL_URL });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("returns the URL in the shape its own validator produces", () => {
|
|
173
|
+
// Padding and trailing slashes are normalized away rather than handed
|
|
174
|
+
// back, so readers never re-normalize what they were given.
|
|
175
|
+
for (const publicBaseUrl of [
|
|
176
|
+
` ${TUNNEL_URL} `,
|
|
177
|
+
`${TUNNEL_URL}/`,
|
|
178
|
+
`${TUNNEL_URL}///`,
|
|
179
|
+
]) {
|
|
180
|
+
expect(
|
|
181
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
182
|
+
).toEqual({ provider: "ngrok", publicBaseUrl: TUNNEL_URL });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("rejects values that are not a record", () => {
|
|
187
|
+
for (const value of [undefined, null, "nonsense", 42, []]) {
|
|
188
|
+
expect(parseTunnelRecord(value)).toBeNull();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects a record missing either field", () => {
|
|
193
|
+
expect(parseTunnelRecord({ provider: "ngrok" })).toBeNull();
|
|
194
|
+
expect(parseTunnelRecord({ publicBaseUrl: TUNNEL_URL })).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects a provider outside the registry", () => {
|
|
198
|
+
// Readers render the provider into a `vellum tunnel --provider <name>`
|
|
199
|
+
// command, so a hand-edited config must not reach a terminal.
|
|
200
|
+
for (const provider of ["wireguard", "; rm -rf /", "", 42]) {
|
|
201
|
+
expect(
|
|
202
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
203
|
+
).toBeNull();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects a URL that is not absolute HTTP(S)", () => {
|
|
208
|
+
for (const publicBaseUrl of [
|
|
209
|
+
"",
|
|
210
|
+
" ",
|
|
211
|
+
"not-a-url",
|
|
212
|
+
"example.ts.net",
|
|
213
|
+
"ftp://x.test",
|
|
214
|
+
"https://one.ngrok.app?token=x",
|
|
215
|
+
42,
|
|
216
|
+
]) {
|
|
217
|
+
expect(
|
|
218
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
219
|
+
).toBeNull();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("trimmedNonEmptyString", () => {
|
|
225
|
+
test("trims and rejects blank or non-string values", () => {
|
|
226
|
+
expect(trimmedNonEmptyString(" assistant-1 ")).toBe("assistant-1");
|
|
227
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
228
|
+
expect(trimmedNonEmptyString(value)).toBeUndefined();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("parseRecordedAssistantId", () => {
|
|
234
|
+
test("trims a recorded id", () => {
|
|
235
|
+
expect(parseRecordedAssistantId(" assistant-1 ")).toBe("assistant-1");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("rejects blank and non-string values", () => {
|
|
239
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
240
|
+
expect(parseRecordedAssistantId(value)).toBeNull();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -48,3 +48,106 @@ export function normalizeHttpPublicBaseUrl(value: unknown): string | undefined {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The same validation as `normalizeHttpPublicBaseUrl`, minus the root path it
|
|
54
|
+
* always emits (`https://x` becomes `https://x/`). Callers that append a path
|
|
55
|
+
* or persist the result want this shape, so they neither join through a double
|
|
56
|
+
* slash nor re-strip what the normalizer just added.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeHttpPublicBaseUrlWithoutTrailingSlash(
|
|
59
|
+
value: unknown,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
return normalizePublicBaseUrl(normalizeHttpPublicBaseUrl(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A trimmed string, or undefined when the value is not a non-blank string.
|
|
66
|
+
*
|
|
67
|
+
* Exported because the identity check in the daemon's tunnel probe compares an
|
|
68
|
+
* id recorded here against one an edge serves, and the two sides have to trim
|
|
69
|
+
* alike: a padded served id must read as a match, not as a different
|
|
70
|
+
* assistant.
|
|
71
|
+
*/
|
|
72
|
+
export function trimmedNonEmptyString(value: unknown): string | undefined {
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Records `vellum tunnel` leaves in the workspace config's `ingress` section.
|
|
81
|
+
// The CLI writes them and the daemon reads them, and the two are separate
|
|
82
|
+
// build units that cannot import each other, so the key names, the provider
|
|
83
|
+
// allowlist, and the validation all live here rather than in a copy on each
|
|
84
|
+
// side.
|
|
85
|
+
|
|
86
|
+
/** Key under `ingress` holding the tunnel that most recently ran. */
|
|
87
|
+
export const INGRESS_LAST_TUNNEL_KEY = "lastTunnel";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Key under `ingress` holding a tunnel published for device pairing alone.
|
|
91
|
+
*
|
|
92
|
+
* A tailnet-only tunnel started while webhook integrations are configured
|
|
93
|
+
* cannot carry their callbacks, so it leaves `publicBaseUrl` (the callback
|
|
94
|
+
* base) as it is and records itself here. Readers that answer the pairing card
|
|
95
|
+
* prefer this address over `publicBaseUrl`; teardown drops the key.
|
|
96
|
+
*/
|
|
97
|
+
export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
|
|
98
|
+
|
|
99
|
+
/** Key under `ingress` holding the assistant id that tunnel fronted. */
|
|
100
|
+
export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The local tunnel providers the CLI can start, in the order they are offered.
|
|
104
|
+
* `vellum tunnel`'s accepted `--provider` values and the persisted-record
|
|
105
|
+
* validation below both derive from it.
|
|
106
|
+
*/
|
|
107
|
+
export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
|
|
108
|
+
|
|
109
|
+
export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
|
|
110
|
+
|
|
111
|
+
/** A recorded tunnel, kept after teardown so UIs can name the command to restart. */
|
|
112
|
+
export interface TunnelRecord {
|
|
113
|
+
provider: TunnelProviderName;
|
|
114
|
+
publicBaseUrl: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isTunnelProviderName(value: unknown): value is TunnelProviderName {
|
|
118
|
+
return (
|
|
119
|
+
typeof value === "string" &&
|
|
120
|
+
(TUNNEL_PROVIDERS as readonly string[]).includes(value)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse an `ingress.lastTunnel` or `ingress.pairingTunnel` value, or null when
|
|
126
|
+
* it is absent or malformed.
|
|
127
|
+
*
|
|
128
|
+
* A hand-edited config must not hand callers an unusable address or a provider
|
|
129
|
+
* name no command accepts: the provider is checked against the allowlist
|
|
130
|
+
* because readers render it into a restart command, and the URL faces the same
|
|
131
|
+
* absolute HTTP(S) constraint as every other public base URL. The URL comes
|
|
132
|
+
* back in the shape the validator produced, so readers never re-normalize it.
|
|
133
|
+
*/
|
|
134
|
+
export function parseTunnelRecord(value: unknown): TunnelRecord | null {
|
|
135
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const { provider, publicBaseUrl } = value as Record<string, unknown>;
|
|
139
|
+
if (!isTunnelProviderName(provider)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const normalized =
|
|
143
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
|
|
144
|
+
if (normalized === undefined) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return { provider, publicBaseUrl: normalized };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Parse an `ingress.assistantId` value, or null when it is absent or blank. */
|
|
151
|
+
export function parseRecordedAssistantId(value: unknown): string | null {
|
|
152
|
+
return trimmedNonEmptyString(value) ?? null;
|
|
153
|
+
}
|