@vellumai/assistant 0.4.29 → 0.4.30
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 +39 -37
- package/README.md +5 -6
- package/docs/runbook-trusted-contacts.md +79 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/calls/relay-server.ts +39 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +42 -35
- package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-skills/weather/TOOLS.json +4 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/contacts/contact-store.ts +195 -4
- package/src/contacts/types.ts +26 -0
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/guardian-actions.ts +20 -87
- package/src/daemon/handlers/sessions.ts +0 -1
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +1 -0
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +12 -0
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +127 -0
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +20 -49
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +196 -28
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +76 -0
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
|
@@ -9,13 +9,39 @@
|
|
|
9
9
|
"input_schema": {
|
|
10
10
|
"type": "object",
|
|
11
11
|
"properties": {
|
|
12
|
-
"calendar_id": {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
"calendar_id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Calendar ID to query (default \"primary\")"
|
|
15
|
+
},
|
|
16
|
+
"time_min": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Start of time range in ISO 8601 format (e.g. \"2024-01-15T00:00:00Z\"). Defaults to now."
|
|
19
|
+
},
|
|
20
|
+
"time_max": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "End of time range in ISO 8601 format (e.g. \"2024-01-22T00:00:00Z\"). Defaults to 7 days from now."
|
|
23
|
+
},
|
|
24
|
+
"max_results": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"description": "Maximum number of events to return (default 25, max 250)"
|
|
27
|
+
},
|
|
28
|
+
"query": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Free text search terms to filter events"
|
|
31
|
+
},
|
|
32
|
+
"single_events": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "Whether to expand recurring events into individual instances (default true)"
|
|
35
|
+
},
|
|
36
|
+
"order_by": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["startTime", "updated"],
|
|
39
|
+
"description": "Sort order (default \"startTime\"). Only valid when single_events is true."
|
|
40
|
+
},
|
|
41
|
+
"reason": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
44
|
+
}
|
|
19
45
|
}
|
|
20
46
|
},
|
|
21
47
|
"executor": "tools/calendar-list-events.ts",
|
|
@@ -29,8 +55,18 @@
|
|
|
29
55
|
"input_schema": {
|
|
30
56
|
"type": "object",
|
|
31
57
|
"properties": {
|
|
32
|
-
"event_id": {
|
|
33
|
-
|
|
58
|
+
"event_id": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "The calendar event ID"
|
|
61
|
+
},
|
|
62
|
+
"calendar_id": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Calendar ID (default \"primary\")"
|
|
65
|
+
},
|
|
66
|
+
"reason": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
69
|
+
}
|
|
34
70
|
},
|
|
35
71
|
"required": ["event_id"]
|
|
36
72
|
},
|
|
@@ -45,19 +81,49 @@
|
|
|
45
81
|
"input_schema": {
|
|
46
82
|
"type": "object",
|
|
47
83
|
"properties": {
|
|
48
|
-
"summary": {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
84
|
+
"summary": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "Event title"
|
|
87
|
+
},
|
|
88
|
+
"start": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Start time in ISO 8601 format (e.g. \"2024-01-15T09:00:00-05:00\") or date for all-day events (\"2024-01-15\")"
|
|
91
|
+
},
|
|
92
|
+
"end": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "End time in ISO 8601 format or date for all-day events"
|
|
95
|
+
},
|
|
96
|
+
"description": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Event description or notes"
|
|
99
|
+
},
|
|
100
|
+
"location": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Event location"
|
|
103
|
+
},
|
|
53
104
|
"attendees": {
|
|
54
105
|
"type": "array",
|
|
55
|
-
"items": {
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
56
109
|
"description": "Email addresses of attendees to invite"
|
|
57
110
|
},
|
|
58
|
-
"timezone": {
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
"timezone": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "IANA timezone (e.g. \"America/New_York\"). Required when start/end don't include timezone offset."
|
|
114
|
+
},
|
|
115
|
+
"calendar_id": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Calendar ID (default \"primary\")"
|
|
118
|
+
},
|
|
119
|
+
"confidence": {
|
|
120
|
+
"type": "number",
|
|
121
|
+
"description": "Confidence score (0-1) for this action"
|
|
122
|
+
},
|
|
123
|
+
"reason": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
126
|
+
}
|
|
61
127
|
},
|
|
62
128
|
"required": ["summary", "start", "end", "confidence"]
|
|
63
129
|
},
|
|
@@ -72,14 +138,29 @@
|
|
|
72
138
|
"input_schema": {
|
|
73
139
|
"type": "object",
|
|
74
140
|
"properties": {
|
|
75
|
-
"time_min": {
|
|
76
|
-
|
|
141
|
+
"time_min": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Start of time range in ISO 8601 format"
|
|
144
|
+
},
|
|
145
|
+
"time_max": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"description": "End of time range in ISO 8601 format"
|
|
148
|
+
},
|
|
77
149
|
"calendar_ids": {
|
|
78
150
|
"type": "array",
|
|
79
|
-
"items": {
|
|
151
|
+
"items": {
|
|
152
|
+
"type": "string"
|
|
153
|
+
},
|
|
80
154
|
"description": "Calendar IDs to check (default [\"primary\"])"
|
|
81
155
|
},
|
|
82
|
-
"timezone": {
|
|
156
|
+
"timezone": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "IANA timezone for interpreting results (e.g. \"America/New_York\")"
|
|
159
|
+
},
|
|
160
|
+
"reason": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
163
|
+
}
|
|
83
164
|
},
|
|
84
165
|
"required": ["time_min", "time_max"]
|
|
85
166
|
},
|
|
@@ -94,10 +175,27 @@
|
|
|
94
175
|
"input_schema": {
|
|
95
176
|
"type": "object",
|
|
96
177
|
"properties": {
|
|
97
|
-
"event_id": {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
178
|
+
"event_id": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "The calendar event ID"
|
|
181
|
+
},
|
|
182
|
+
"response": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"enum": ["accepted", "declined", "tentative"],
|
|
185
|
+
"description": "RSVP response"
|
|
186
|
+
},
|
|
187
|
+
"calendar_id": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Calendar ID (default \"primary\")"
|
|
190
|
+
},
|
|
191
|
+
"confidence": {
|
|
192
|
+
"type": "number",
|
|
193
|
+
"description": "Confidence score (0-1) for this action"
|
|
194
|
+
},
|
|
195
|
+
"reason": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
198
|
+
}
|
|
101
199
|
},
|
|
102
200
|
"required": ["event_id", "response", "confidence"]
|
|
103
201
|
},
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Guardian Verify Setup"
|
|
3
|
-
description: "Set up guardian verification for voice or
|
|
3
|
+
description: "Set up guardian verification for voice, Telegram, or Slack channels via outbound verification flow"
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata: { "vellum": { "emoji": "\ud83d\udd10" } }
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are helping your user set up guardian verification for a messaging channel (voice or
|
|
8
|
+
You are helping your user set up guardian verification for a messaging channel (voice, Telegram, or Slack). This links their identity as the trusted guardian for the chosen channel. Use gateway control-plane APIs for outbound actions, and use `vellum integrations guardian status` for status reads.
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
12
12
|
- Use the injected `INTERNAL_GATEWAY_BASE_URL` for gateway API calls.
|
|
13
|
-
- Never call the
|
|
13
|
+
- Never call the daemon runtime port directly; always call the gateway URL.
|
|
14
14
|
- The bearer token is available as the `$GATEWAY_AUTH_TOKEN` environment variable for control-plane `curl` requests.
|
|
15
15
|
- Run shell commands for this skill with `bash`.
|
|
16
16
|
- Keep narration minimal: execute required calls first, then provide a concise status update. Do not narrate internal install/check/load chatter unless something fails.
|
|
@@ -21,8 +21,9 @@ Ask the user which channel they want to verify:
|
|
|
21
21
|
|
|
22
22
|
- **voice** -- verify a phone number for voice calls
|
|
23
23
|
- **telegram** -- verify a Telegram account
|
|
24
|
+
- **slack** -- verify a Slack account
|
|
24
25
|
|
|
25
|
-
If the user's intent already specifies a channel (e.g. "verify my phone number for voice calls"), skip the prompt and proceed.
|
|
26
|
+
If the user's intent already specifies a channel (e.g. "verify my phone number for voice calls", "verify me on Slack"), skip the prompt and proceed.
|
|
26
27
|
|
|
27
28
|
## Step 2: Collect Destination
|
|
28
29
|
|
|
@@ -32,6 +33,7 @@ Based on the chosen channel, ask for the required destination:
|
|
|
32
33
|
- **Telegram**: Ask for their Telegram chat ID (numeric) or @handle. Explain:
|
|
33
34
|
- If they know their numeric chat ID, provide it directly. The bot will send the code to that chat.
|
|
34
35
|
- If they only know their @handle, the flow uses a bootstrap deep-link that they must click first.
|
|
36
|
+
- **Slack**: Ask for their Slack user ID. Explain that this is their Slack member ID (e.g. U01ABCDEF), not their display name or email. They can find it in their Slack profile under "More" > "Copy member ID". The bot will send a verification code via Slack DM.
|
|
35
37
|
|
|
36
38
|
## Step 3: Start Outbound Verification
|
|
37
39
|
|
|
@@ -44,7 +46,7 @@ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/outbound/st
|
|
|
44
46
|
-d '{"channel": "<channel>", "destination": "<destination>"}'
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
Replace `<channel>` with `voice` or `
|
|
49
|
+
Replace `<channel>` with `voice`, `telegram`, or `slack`, and `<destination>` with the phone number, Telegram destination, or Slack user ID.
|
|
48
50
|
|
|
49
51
|
### On success (`success: true`)
|
|
50
52
|
|
|
@@ -53,6 +55,7 @@ Report the exact next action based on the channel:
|
|
|
53
55
|
- **Voice**: The response includes a `secret` field with the verification code. Tell the user the code BEFORE the call connects: "I'm calling [number] now. Your verification code is [secret]. When you answer the call, enter this code using your phone's keypad." The `/outbound/start` API call already initiates the voice call. Do NOT place a separate `call_start` call. **After delivering the code, immediately begin the voice auto-check polling loop** (see [Voice Auto-Check Polling](#voice-auto-check-polling) below).
|
|
54
56
|
- **Telegram with chat ID** (no `telegramBootstrapUrl` in response): The response includes a `secret` field. Show it in the current chat: "Your verification code is **[secret]**. I've also sent it to your Telegram. Open the Telegram bot chat and reply with that 6-digit code to complete verification." If the response does not contain a `secret` field, treat this as a control-plane error: tell the user something went wrong and ask them to retry from Step 3 or resend (Step 4).
|
|
55
57
|
- **Telegram with handle** (`telegramBootstrapUrl` present in response): "Tap this deep-link first: [telegramBootstrapUrl]. After Telegram binds your identity, I'll send your verification code."
|
|
58
|
+
- **Slack**: The response includes a `secret` field with the verification code. Show it in the current chat: "Your verification code is **[secret]**. I've also sent it to you as a Slack DM. Open the DM from the Vellum bot in Slack and reply with that 6-digit code to complete verification." The DM channel ID is captured automatically during this process for future message delivery. If the response does not contain a `secret` field, treat this as a control-plane error: tell the user something went wrong and ask them to retry from Step 3 or resend (Step 4). **After delivering the code, immediately begin the Slack auto-check polling loop** (see [Slack Auto-Check Polling](#slack-auto-check-polling) below).
|
|
56
59
|
|
|
57
60
|
After reporting the bootstrap URL for Telegram handle flows, wait for the user to confirm they clicked the link. Then check guardian status (Step 6) to see if the bootstrap completed and a code was sent.
|
|
58
61
|
|
|
@@ -60,14 +63,14 @@ After reporting the bootstrap URL for Telegram handle flows, wait for the user t
|
|
|
60
63
|
|
|
61
64
|
Handle each error code:
|
|
62
65
|
|
|
63
|
-
| Error code | Action
|
|
64
|
-
| --------------------- |
|
|
65
|
-
| `missing_destination` | Ask the user to provide their phone number or
|
|
66
|
-
| `invalid_destination` | Tell the user the format is invalid. For phone: suggest E.164 format (+15551234567). For Telegram: explain that group chat IDs (negative numbers) are not supported.
|
|
67
|
-
| `already_bound` | Tell the user a guardian is already bound for this channel. Ask if they want to replace it. If yes, re-run the start request with `"rebind": true` added to the JSON body.
|
|
68
|
-
| `rate_limited` | Tell the user they have sent too many verification attempts to this destination. Ask them to wait and try again later.
|
|
69
|
-
| `unsupported_channel` | Tell the user the channel is not supported. Only voice and
|
|
70
|
-
| `no_bot_username` | Telegram bot is not configured. Load and run the `telegram-setup` skill first.
|
|
66
|
+
| Error code | Action |
|
|
67
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `missing_destination` | Ask the user to provide their phone number, Telegram destination, or Slack user ID. |
|
|
69
|
+
| `invalid_destination` | Tell the user the format is invalid. For phone: suggest E.164 format (+15551234567). For Telegram: explain that group chat IDs (negative numbers) are not supported. For Slack: explain that the value must be a Slack member ID (e.g. U01ABCDEF). |
|
|
70
|
+
| `already_bound` | Tell the user a guardian is already bound for this channel. Ask if they want to replace it. If yes, re-run the start request with `"rebind": true` added to the JSON body. |
|
|
71
|
+
| `rate_limited` | Tell the user they have sent too many verification attempts to this destination. Ask them to wait and try again later. |
|
|
72
|
+
| `unsupported_channel` | Tell the user the channel is not supported. Only voice, telegram, and slack are valid. |
|
|
73
|
+
| `no_bot_username` | Telegram bot is not configured. Load and run the `telegram-setup` skill first. |
|
|
71
74
|
|
|
72
75
|
## Step 4: Handle Resend
|
|
73
76
|
|
|
@@ -84,6 +87,7 @@ On success, report the next action based on the channel:
|
|
|
84
87
|
|
|
85
88
|
- **Voice**: The resend response includes a fresh `secret` field with a new verification code. Tell the user the new code BEFORE the call connects — just like the initial start flow: "I'm calling [number] again. Your new verification code is [secret]. When you answer the call, enter this code using your phone's keypad." The `/outbound/resend` API call already initiates the voice call. Do NOT place a separate `call_start` call. **After delivering the code, immediately begin the voice auto-check polling loop** (see [Voice Auto-Check Polling](#voice-auto-check-polling) below).
|
|
86
89
|
- **Telegram**: The resend response includes a fresh `secret` field. Show the new code in the current chat: "Your new verification code is **[secret]**. I've also sent it to your Telegram. Open the Telegram bot chat and reply with that 6-digit code to complete verification." If the response does not contain a `secret` field, treat this as a control-plane error: tell the user something went wrong and ask them to retry from Step 3.
|
|
90
|
+
- **Slack**: The resend response includes a fresh `secret` field. Show the new code in the current chat: "Your new verification code is **[secret]**. I've also sent it to you as a Slack DM. Reply to the DM with that 6-digit code to complete verification. (resent)" If the response does not contain a `secret` field, treat this as a control-plane error: tell the user something went wrong and ask them to retry from Step 3. **After delivering the code, immediately begin the Slack auto-check polling loop** (see [Slack Auto-Check Polling](#slack-auto-check-polling) below).
|
|
87
91
|
|
|
88
92
|
### Resend errors
|
|
89
93
|
|
|
@@ -129,19 +133,48 @@ vellum integrations guardian status --channel voice --json
|
|
|
129
133
|
6. If the 2-minute timeout is reached without `bound: true`: proactively tell the user — "I've been checking for about 2 minutes but verification hasn't completed yet. The code may have expired or wasn't entered. Would you like me to resend a new code (Step 4) or start a new session (Step 3)?"
|
|
130
134
|
|
|
131
135
|
**Rebind guard:**
|
|
132
|
-
When in a **rebind flow** (i.e., the `start_outbound` request included `"rebind": true` because a binding already existed), do NOT treat
|
|
136
|
+
When in a **rebind flow** (i.e., the `start_outbound` request included `"rebind": true` because a binding already existed), do NOT treat `bound: true` alone as success. The pre-existing binding will show `bound: true` before the user has entered the new code, which would be a false positive. To guard against this:
|
|
133
137
|
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
- Non-rebind flows (fresh verification with no prior binding) are unaffected — the first `bound: true` is trustworthy.
|
|
138
|
+
- Only report success when BOTH conditions are met: `bound: true` AND `verificationSessionId` is **absent** from the status response. The `verificationSessionId` field is present while a verification session is still active (pending). When the user enters the correct code, the session is consumed and `verificationSessionId` disappears from subsequent status responses. This proves the new outbound session was consumed and the binding is fresh.
|
|
139
|
+
- If a poll shows `bound: true` but `verificationSessionId` is still present, the old binding is still active and the new code has not yet been consumed — continue polling.
|
|
140
|
+
- Non-rebind flows (fresh verification with no prior binding) are unaffected — the first `bound: true` is trustworthy because there was no prior binding to confuse the result.
|
|
138
141
|
|
|
139
142
|
**Important polling rules:**
|
|
140
143
|
|
|
141
|
-
- This polling loop is voice-only. Do NOT poll for Telegram channels (Telegram has its own bot-driven flow).
|
|
144
|
+
- This polling loop is voice-only. Do NOT poll for Telegram channels (Telegram has its own bot-driven flow). For Slack, use the separate Slack Auto-Check Polling loop below.
|
|
142
145
|
- Do NOT require the user to ask "did it work?" — the whole point is proactive confirmation.
|
|
143
146
|
- If the user sends a message while polling is in progress, handle their message normally. If their message is about verification status, the next poll iteration will provide the answer.
|
|
144
147
|
|
|
148
|
+
## Slack Auto-Check Polling
|
|
149
|
+
|
|
150
|
+
For **Slack** verification: after telling the user their code and instructing them to reply in the Slack DM (in Step 3 or Step 4), proactively poll for completion so the user gets instant confirmation.
|
|
151
|
+
|
|
152
|
+
**Polling procedure:**
|
|
153
|
+
|
|
154
|
+
1. Wait ~15 seconds after delivering the code (to give the user time to open the Slack DM and reply with the code).
|
|
155
|
+
2. Check the binding status via Vellum CLI:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
vellum integrations guardian status --channel slack --json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
3. If the response shows `bound: true`: immediately send a proactive success message in the current chat — "Slack verification complete! Your Slack account is now the trusted guardian. The DM channel has been captured for future message delivery." Stop polling.
|
|
162
|
+
4. If not yet bound: wait ~15 seconds and poll again.
|
|
163
|
+
5. Continue polling for up to **2 minutes** (approximately 8 attempts).
|
|
164
|
+
6. If the 2-minute timeout is reached without `bound: true`: proactively tell the user — "I've been checking for about 2 minutes but verification hasn't completed yet. The code may have expired or wasn't entered. Would you like me to resend a new code (Step 4) or start a new session (Step 3)?"
|
|
165
|
+
|
|
166
|
+
**Rebind guard:**
|
|
167
|
+
When in a **rebind flow** (i.e., the `start_outbound` request included `"rebind": true` because a binding already existed), do NOT treat `bound: true` alone as success. The pre-existing binding will show `bound: true` before the user has entered the new code, which would be a false positive. To guard against this:
|
|
168
|
+
|
|
169
|
+
- Only report success when BOTH conditions are met: `bound: true` AND `verificationSessionId` is **absent** from the status response. The `verificationSessionId` field is present while a verification session is still active (pending). When the user enters the correct code, the session is consumed and `verificationSessionId` disappears from subsequent status responses. This proves the new outbound session was consumed and the binding is fresh.
|
|
170
|
+
- If a poll shows `bound: true` but `verificationSessionId` is still present, the old binding is still active and the new code has not yet been consumed — continue polling.
|
|
171
|
+
- Non-rebind flows (fresh verification with no prior binding) are unaffected — the first `bound: true` is trustworthy because there was no prior binding to confuse the result.
|
|
172
|
+
|
|
173
|
+
**Important polling rules:**
|
|
174
|
+
|
|
175
|
+
- Do NOT require the user to ask "did it work?" — the whole point is proactive confirmation.
|
|
176
|
+
- If the user sends a message while polling is in progress, handle their message normally.
|
|
177
|
+
|
|
145
178
|
## Step 6: Check Guardian Status
|
|
146
179
|
|
|
147
180
|
After the user reports entering the code, verify the binding was created:
|
|
@@ -166,7 +199,7 @@ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/guardian/revoke" \
|
|
|
166
199
|
-d '{"channel": "<channel>"}'
|
|
167
200
|
```
|
|
168
201
|
|
|
169
|
-
Replace `<channel>` with the channel to unbind from (e.g. `voice`, `telegram`).
|
|
202
|
+
Replace `<channel>` with the channel to unbind from (e.g. `voice`, `telegram`, `slack`).
|
|
170
203
|
|
|
171
204
|
### On success (`success: true`)
|
|
172
205
|
|
|
@@ -181,5 +214,5 @@ The response includes `bound: false` after the operation completes. Check the pr
|
|
|
181
214
|
- The resend cooldown is 15 seconds between sends, with a maximum of 5 sends per session.
|
|
182
215
|
- Per-destination rate limiting allows up to 10 sends within a 1-hour rolling window.
|
|
183
216
|
- Guardian verification is identity-bound: the code can only be consumed by the identity matching the destination provided at start time.
|
|
184
|
-
- **Missing `secret` guardrail**: For voice
|
|
217
|
+
- **Missing `secret` guardrail**: For voice, Telegram chat-ID, and Slack flows, the API response MUST include a `secret` field. If `secret` is unexpectedly absent from a start or resend response that otherwise indicates success, treat this as a control-plane error. Do NOT fabricate a code or tell the user to proceed without one. Instead, tell the user something went wrong and ask them to retry the start (Step 3) or resend (Step 4).
|
|
185
218
|
- **Revoking a guardian**: To remove the current guardian from a channel, use the revoke API (Step 7). This revokes the binding AND revokes the guardian's contact record, so they lose access to the channel. A new guardian can then be verified for that channel.
|
|
@@ -20,17 +20,27 @@
|
|
|
20
20
|
},
|
|
21
21
|
"attachment_ids": {
|
|
22
22
|
"type": "array",
|
|
23
|
-
"items": {
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "string"
|
|
25
|
+
},
|
|
24
26
|
"description": "IDs of attached source images to edit (required for edit mode)"
|
|
25
27
|
},
|
|
26
28
|
"model": {
|
|
27
29
|
"type": "string",
|
|
28
|
-
"enum": [
|
|
30
|
+
"enum": [
|
|
31
|
+
"gemini-2.5-flash-image",
|
|
32
|
+
"gemini-3-pro-image",
|
|
33
|
+
"gemini-3-pro-image-preview"
|
|
34
|
+
],
|
|
29
35
|
"description": "Which model to use for generation. If omitted, uses the user's configured preference."
|
|
30
36
|
},
|
|
31
37
|
"variants": {
|
|
32
38
|
"type": "number",
|
|
33
39
|
"description": "Number of image variants to generate (1-4, default: 1)"
|
|
40
|
+
},
|
|
41
|
+
"reason": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
34
44
|
}
|
|
35
45
|
},
|
|
36
46
|
"required": ["prompt"]
|
|
@@ -109,14 +109,20 @@ export async function run(
|
|
|
109
109
|
content += `\n\n${result.text}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const contentBlocks: ImageContent[] = result.images.map((img) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
const contentBlocks: ImageContent[] = result.images.map((img) => {
|
|
113
|
+
const block: ImageContent = {
|
|
114
|
+
type: "image" as const,
|
|
115
|
+
source: {
|
|
116
|
+
type: "base64" as const,
|
|
117
|
+
media_type: img.mimeType,
|
|
118
|
+
data: img.dataBase64,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
if (img.title) {
|
|
122
|
+
(block as unknown as Record<string, unknown>)._title = img.title;
|
|
123
|
+
}
|
|
124
|
+
return block;
|
|
125
|
+
});
|
|
120
126
|
|
|
121
127
|
return {
|
|
122
128
|
content,
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"seeds": {
|
|
18
18
|
"type": "array",
|
|
19
|
-
"items": {
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
20
22
|
"description": "Entity names or aliases to use as starting points"
|
|
21
23
|
},
|
|
22
24
|
"steps": {
|
|
@@ -26,12 +28,16 @@
|
|
|
26
28
|
"properties": {
|
|
27
29
|
"relation_types": {
|
|
28
30
|
"type": "array",
|
|
29
|
-
"items": {
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
30
34
|
"description": "Relation types to follow (e.g., 'uses', 'works_on', 'depends_on')"
|
|
31
35
|
},
|
|
32
36
|
"entity_types": {
|
|
33
37
|
"type": "array",
|
|
34
|
-
"items": {
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
},
|
|
35
41
|
"description": "Entity types to include (e.g., 'person', 'project', 'tool')"
|
|
36
42
|
}
|
|
37
43
|
}
|
|
@@ -45,6 +51,10 @@
|
|
|
45
51
|
"include_items": {
|
|
46
52
|
"type": "boolean",
|
|
47
53
|
"description": "Include associated memory items for each found entity (default: true)"
|
|
54
|
+
},
|
|
55
|
+
"reason": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
48
58
|
}
|
|
49
59
|
},
|
|
50
60
|
"required": ["query_type", "seeds"]
|
|
@@ -200,7 +200,7 @@ Be specific and factual. Describe what you see, not what you infer happened betw
|
|
|
200
200
|
|
|
201
201
|
### Clip Delivery
|
|
202
202
|
|
|
203
|
-
The `generate_clip` tool automatically opens clips in the user's default video player after extraction. Clips are saved persistently in the asset's pipeline directory (`pipeline/<assetId>/clips/`). The `clipPath` field in the tool response contains the absolute file path.
|
|
203
|
+
The `generate_clip` tool automatically opens clips in the user's default video player after extraction (handled internally — do **not** run `open` via `host_bash`). Clips are saved persistently in the asset's pipeline directory (`pipeline/<assetId>/clips/`), falling back to a temp directory when the source location is read-only. Each clip gets a unique filename so concurrent or repeated extractions at the same range never collide. The `clipPath` field in the tool response contains the absolute file path.
|
|
204
204
|
|
|
205
205
|
The tool handles high-bitrate and incompatible codec sources automatically — it tries stream copy first for speed, then falls back to H.264 re-encoding if needed. **Always use `generate_clip` rather than manual ffmpeg commands.**
|
|
206
206
|
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"metadata": {
|
|
21
21
|
"type": "object",
|
|
22
22
|
"description": "Optional JSON metadata to attach to the asset (e.g., pipeline config, source info)"
|
|
23
|
+
},
|
|
24
|
+
"reason": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"required": ["file_path"]
|
|
@@ -47,6 +51,10 @@
|
|
|
47
51
|
"type": "string",
|
|
48
52
|
"enum": ["registered", "processing", "indexed", "failed"],
|
|
49
53
|
"description": "Filter assets by processing status"
|
|
54
|
+
},
|
|
55
|
+
"reason": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
},
|
|
@@ -97,6 +105,10 @@
|
|
|
97
105
|
"type": "string",
|
|
98
106
|
"enum": ["api", "local"],
|
|
99
107
|
"description": "Transcription backend: 'api' (OpenAI Whisper cloud) or 'local' (whisper.cpp). Default: 'local'."
|
|
108
|
+
},
|
|
109
|
+
"reason": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
100
112
|
}
|
|
101
113
|
},
|
|
102
114
|
"required": ["asset_id"]
|
|
@@ -146,6 +158,10 @@
|
|
|
146
158
|
"type": "number",
|
|
147
159
|
"minimum": 0,
|
|
148
160
|
"description": "Maximum retry attempts per segment on failure. Default: 3"
|
|
161
|
+
},
|
|
162
|
+
"reason": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
149
165
|
}
|
|
150
166
|
},
|
|
151
167
|
"required": ["asset_id", "system_prompt", "output_schema"]
|
|
@@ -176,6 +192,10 @@
|
|
|
176
192
|
"model": {
|
|
177
193
|
"type": "string",
|
|
178
194
|
"description": "LLM model to use for analysis. Default: 'claude-sonnet-4-6'"
|
|
195
|
+
},
|
|
196
|
+
"reason": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
179
199
|
}
|
|
180
200
|
},
|
|
181
201
|
"required": ["asset_id", "query"]
|
|
@@ -219,6 +239,10 @@
|
|
|
219
239
|
"title": {
|
|
220
240
|
"type": "string",
|
|
221
241
|
"description": "Short descriptive title for the clip (e.g. 'snow-dive-closeup', 'goal-celebration'). Used as the filename. If omitted, falls back to timestamp-based naming."
|
|
242
|
+
},
|
|
243
|
+
"reason": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
222
246
|
}
|
|
223
247
|
},
|
|
224
248
|
"required": ["asset_id", "start_time", "end_time"]
|
|
@@ -237,6 +261,10 @@
|
|
|
237
261
|
"asset_id": {
|
|
238
262
|
"type": "string",
|
|
239
263
|
"description": "ID of the media asset to diagnose"
|
|
264
|
+
},
|
|
265
|
+
"reason": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"description": "Brief non-technical explanation of why this tool is being called"
|
|
240
268
|
}
|
|
241
269
|
},
|
|
242
270
|
"required": ["asset_id"]
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* This is a generic media-processing primitive with no domain-specific logic.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { access, constants, mkdir, stat } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
10
12
|
import { dirname, join } from "node:path";
|
|
11
13
|
|
|
12
14
|
import { uploadFileBackedAttachment } from "../../../../memory/attachments-store.js";
|
|
@@ -132,13 +134,31 @@ export async function run(
|
|
|
132
134
|
|
|
133
135
|
// Save clips to the asset's pipeline directory so they persist for
|
|
134
136
|
// attachment delivery (tmpdir files get cleaned up before the sandbox
|
|
135
|
-
// attachment system can serve them).
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
// attachment system can serve them). Fall back to a temp directory when the
|
|
138
|
+
// source directory is read-only (external mounts, protected folders, etc.).
|
|
139
|
+
const preferredDir = join(
|
|
140
|
+
dirname(asset.filePath),
|
|
141
|
+
"pipeline",
|
|
142
|
+
assetId,
|
|
143
|
+
"clips",
|
|
144
|
+
);
|
|
145
|
+
let clipDir: string;
|
|
146
|
+
try {
|
|
147
|
+
await mkdir(preferredDir, { recursive: true });
|
|
148
|
+
await access(preferredDir, constants.W_OK);
|
|
149
|
+
clipDir = preferredDir;
|
|
150
|
+
} catch {
|
|
151
|
+
clipDir = join(tmpdir(), "vellum-clips", assetId);
|
|
152
|
+
await mkdir(clipDir, { recursive: true });
|
|
153
|
+
}
|
|
138
154
|
|
|
155
|
+
// Include a short unique suffix to prevent filename collisions when
|
|
156
|
+
// concurrent or repeated calls target the same time range.
|
|
157
|
+
const timestampSuffix = `${formatTimestamp(startTime).replace(/:/g, "")}-${formatTimestamp(endTime).replace(/:/g, "")}`;
|
|
158
|
+
const uniqueSuffix = randomUUID().slice(0, 8);
|
|
139
159
|
const clipFilename = title
|
|
140
|
-
? `${sanitizeFilename(title)}.${outputFormat}`
|
|
141
|
-
: `clip-${
|
|
160
|
+
? `${sanitizeFilename(title)}-${timestampSuffix}-${uniqueSuffix}.${outputFormat}`
|
|
161
|
+
: `clip-${timestampSuffix}-${uniqueSuffix}.${outputFormat}`;
|
|
142
162
|
const clipPath = join(clipDir, clipFilename);
|
|
143
163
|
|
|
144
164
|
try {
|