@vellumai/assistant 0.3.4 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +37 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +70 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -17
- package/src/__tests__/channel-approvals.test.ts +48 -1
- package/src/__tests__/channel-guardian.test.ts +74 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/handlers-twilio-config.test.ts +407 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +22 -11
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +21 -6
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/system-prompt.ts +24 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/daemon/handlers/config.ts +783 -9
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +108 -4
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +1 -1
- package/src/daemon/server.ts +6 -2
- package/src/daemon/session-agent-loop.ts +5 -1
- package/src/daemon/session-runtime-assembly.ts +55 -0
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +11 -1
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-init.ts +144 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/media-store.ts +759 -0
- package/src/memory/retriever.ts +6 -1
- package/src/memory/schema.ts +98 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +24 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +12 -4
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/http-server.ts +53 -27
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +67 -21
- package/src/runtime/run-orchestrator.ts +35 -2
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +35 -0
package/Dockerfile
CHANGED
|
@@ -42,11 +42,13 @@ RUN apt-get update && apt-get install -y \
|
|
|
42
42
|
fonts-freefont-ttf \
|
|
43
43
|
make \
|
|
44
44
|
g++ \
|
|
45
|
+
git \
|
|
45
46
|
sudo \
|
|
46
47
|
&& rm -rf /var/lib/apt/lists/*
|
|
47
48
|
|
|
48
49
|
# Copy bun binary from builder instead of re-installing
|
|
49
50
|
COPY --from=builder /root/.bun/bin/bun /usr/local/bin/bun
|
|
51
|
+
RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
|
|
50
52
|
|
|
51
53
|
# Create non-root user that also has sudo access so it can like install stuff
|
|
52
54
|
RUN groupadd --system --gid 1001 assistant && \
|
package/README.md
CHANGED
|
@@ -194,7 +194,7 @@ The `/channels/inbound` endpoint requires a valid `X-Gateway-Origin` header to p
|
|
|
194
194
|
|
|
195
195
|
## Twilio Setup Primitive
|
|
196
196
|
|
|
197
|
-
Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through the `twilio_config` IPC contract and the `twilio-setup` skill.
|
|
197
|
+
Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through the `twilio_config` IPC contract and the `twilio-setup` skill. For SMS-specific onboarding (including compliance verification and test sending), the `sms-setup` skill provides a guided conversational flow that layers on top of `twilio-setup`.
|
|
198
198
|
|
|
199
199
|
### `twilio_config` IPC Contract
|
|
200
200
|
|
|
@@ -208,8 +208,15 @@ The daemon handles `twilio_config` messages with the following actions:
|
|
|
208
208
|
| `provision_number` | Purchases a new phone number via the Twilio API. Accepts optional `areaCode` and `country` (ISO 3166-1 alpha-2, default `US`). Auto-assigns the number to the assistant (persists to config and secure storage) and configures Twilio webhooks (voice, status callback, SMS) when a public ingress URL is available. |
|
|
209
209
|
| `assign_number` | Assigns an existing Twilio phone number (E.164 format) to the assistant and auto-configures webhooks when ingress is available |
|
|
210
210
|
| `list_numbers` | Lists all incoming phone numbers on the Twilio account with their capabilities (voice, SMS) |
|
|
211
|
+
| `sms_compliance_status` | Returns the SMS compliance posture for the assigned phone number. Determines number type (toll-free vs local 10DLC) and retrieves toll-free verification status from Twilio. |
|
|
212
|
+
| `sms_submit_tollfree_verification` | Submits a new toll-free verification request to Twilio. Validates required fields and enum values. Defaults `businessType` to `SOLE_PROPRIETOR`. |
|
|
213
|
+
| `sms_update_tollfree_verification` | Updates an existing toll-free verification by SID. Requires `verificationSid`. |
|
|
214
|
+
| `sms_delete_tollfree_verification` | Deletes a toll-free verification by SID. Includes warning about queue priority reset. |
|
|
215
|
+
| `release_number` | Releases (deletes) a phone number from the Twilio account. Clears the number from config and secure storage. Includes warning about toll-free verification context loss. |
|
|
216
|
+
| `sms_send_test` | Sends a test SMS to the specified `phoneNumber` with the given `text`, polls Twilio for delivery status (up to 3 retries at 2-second intervals), and returns the result in `testResult`. Stores the last result in memory for use by `sms_doctor`. |
|
|
217
|
+
| `sms_doctor` | Runs a comprehensive SMS health diagnostic. Checks channel readiness, compliance/toll-free verification status, and the last `sms_send_test` result. Returns structured diagnostics in `diagnostics` with an overall `status` ("healthy", "degraded", or "unhealthy") and actionable `items`. |
|
|
211
218
|
|
|
212
|
-
Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`,
|
|
219
|
+
Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`, optional `warning` (for non-fatal webhook sync failures), optional `compliance` object (for compliance status actions, containing `numberType`, `verificationSid`, `verificationStatus`, `rejectionReason`, `rejectionReasons`, `errorCode`, `editAllowed`, `editExpiration`), optional `testResult` (for `sms_send_test`), and optional `diagnostics` (for `sms_doctor`).
|
|
213
220
|
|
|
214
221
|
### Ingress Webhook Reconciliation
|
|
215
222
|
|
|
@@ -251,6 +258,34 @@ Guardian bindings, verification challenges, and approval requests are all scoped
|
|
|
251
258
|
|
|
252
259
|
The channel guardian service generates verification challenge instructions with channel-appropriate wording. The `channelLabel()` function maps `sourceChannel` values to human-readable labels (e.g., `"telegram"` -> `"Telegram"`, `"sms"` -> `"SMS"`), so challenge prompts reference the correct channel name.
|
|
253
260
|
|
|
261
|
+
## Channel Readiness
|
|
262
|
+
|
|
263
|
+
The `channel_readiness` IPC contract provides a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. It runs local checks (credential presence, phone number assignment, ingress config) synchronously and optional remote checks (API reachability) asynchronously with a 5-minute TTL cache.
|
|
264
|
+
|
|
265
|
+
### `channel_readiness` IPC Contract
|
|
266
|
+
|
|
267
|
+
| Action | Description |
|
|
268
|
+
|--------|-------------|
|
|
269
|
+
| `get` | Returns readiness snapshots for the specified channel (or all channels if omitted). Local checks always run; remote checks run only when `includeRemote=true` and cache is stale. |
|
|
270
|
+
| `refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. |
|
|
271
|
+
|
|
272
|
+
Request fields: `action` (required), `channel` (optional filter), `assistantId` (optional), `includeRemote` (optional boolean).
|
|
273
|
+
|
|
274
|
+
Response type: `channel_readiness_response` with `success`, optional `snapshots` array (each with `channel`, `ready`, `checkedAt`, `stale`, `reasons`, `localChecks`, optional `remoteChecks`), and optional `error`.
|
|
275
|
+
|
|
276
|
+
### Built-in Channel Probes
|
|
277
|
+
|
|
278
|
+
- **SMS**: Checks Twilio credentials, phone number assignment, and public ingress URL.
|
|
279
|
+
- **Telegram**: Checks bot token, webhook secret, and public ingress URL.
|
|
280
|
+
|
|
281
|
+
### Key modules
|
|
282
|
+
|
|
283
|
+
| File | Purpose |
|
|
284
|
+
|------|---------|
|
|
285
|
+
| `src/runtime/channel-readiness-types.ts` | Shared types: `ChannelId`, `ReadinessCheckResult`, `ChannelReadinessSnapshot`, `ChannelProbe` |
|
|
286
|
+
| `src/runtime/channel-readiness-service.ts` | Service class with probe registration, cached readiness evaluation, and built-in SMS/Telegram probes |
|
|
287
|
+
| `src/daemon/handlers/config.ts` | `handleChannelReadiness` — IPC handler for `channel_readiness` messages |
|
|
288
|
+
|
|
254
289
|
## Database
|
|
255
290
|
|
|
256
291
|
SQLite via Drizzle ORM, stored at `~/.vellum/workspace/data/db/assistant.db`. Key tables include conversations, messages, tool invocations, attachments, memory segments (with FTS5), memory items, entities, reminders, and recurrence schedules (cron + RRULE).
|
package/package.json
CHANGED
|
@@ -405,6 +405,19 @@ function emitStruct(s: SwiftStruct): string {
|
|
|
405
405
|
lines.push(` public let ${p.swiftName}: ${p.swiftType}`);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// Emit public memberwise init (Swift only auto-generates internal inits for public structs)
|
|
409
|
+
if (s.properties.length > 0) {
|
|
410
|
+
lines.push('');
|
|
411
|
+
const params = s.properties
|
|
412
|
+
.map((p) => `${p.swiftName}: ${p.swiftType}${p.isOptional ? ' = nil' : ''}`)
|
|
413
|
+
.join(', ');
|
|
414
|
+
lines.push(` public init(${params}) {`);
|
|
415
|
+
for (const p of s.properties) {
|
|
416
|
+
lines.push(` self.${p.swiftName} = ${p.swiftName}`);
|
|
417
|
+
}
|
|
418
|
+
lines.push(' }');
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
if (needsCodingKeys(s.properties)) {
|
|
409
422
|
lines.push('');
|
|
410
423
|
lines.push(' private enum CodingKeys: String, CodingKey {');
|
|
@@ -601,6 +601,15 @@ exports[`IPC message snapshots ClientMessage types twilio_config serializes to e
|
|
|
601
601
|
}
|
|
602
602
|
`;
|
|
603
603
|
|
|
604
|
+
exports[`IPC message snapshots ClientMessage types channel_readiness serializes to expected JSON 1`] = `
|
|
605
|
+
{
|
|
606
|
+
"action": "get",
|
|
607
|
+
"channel": "sms",
|
|
608
|
+
"includeRemote": true,
|
|
609
|
+
"type": "channel_readiness",
|
|
610
|
+
}
|
|
611
|
+
`;
|
|
612
|
+
|
|
604
613
|
exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = `
|
|
605
614
|
{
|
|
606
615
|
"action": "create_challenge",
|
|
@@ -908,6 +917,20 @@ exports[`IPC message snapshots ClientMessage types tool_names_list serializes to
|
|
|
908
917
|
}
|
|
909
918
|
`;
|
|
910
919
|
|
|
920
|
+
exports[`IPC message snapshots ClientMessage types dictation_request serializes to expected JSON 1`] = `
|
|
921
|
+
{
|
|
922
|
+
"context": {
|
|
923
|
+
"appName": "Example App",
|
|
924
|
+
"bundleIdentifier": "com.example.app",
|
|
925
|
+
"cursorInTextField": true,
|
|
926
|
+
"selectedText": "some selected text",
|
|
927
|
+
"windowTitle": "Main Window",
|
|
928
|
+
},
|
|
929
|
+
"transcription": "Hello world",
|
|
930
|
+
"type": "dictation_request",
|
|
931
|
+
}
|
|
932
|
+
`;
|
|
933
|
+
|
|
911
934
|
exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
|
|
912
935
|
{
|
|
913
936
|
"success": true,
|
|
@@ -1323,6 +1346,14 @@ exports[`IPC message snapshots ServerMessage types task_routed serializes to exp
|
|
|
1323
1346
|
}
|
|
1324
1347
|
`;
|
|
1325
1348
|
|
|
1349
|
+
exports[`IPC message snapshots ServerMessage types ride_shotgun_progress serializes to expected JSON 1`] = `
|
|
1350
|
+
{
|
|
1351
|
+
"message": "Observing user activity...",
|
|
1352
|
+
"type": "ride_shotgun_progress",
|
|
1353
|
+
"watchId": "watch-shotgun-001",
|
|
1354
|
+
}
|
|
1355
|
+
`;
|
|
1356
|
+
|
|
1326
1357
|
exports[`IPC message snapshots ServerMessage types ride_shotgun_result serializes to expected JSON 1`] = `
|
|
1327
1358
|
{
|
|
1328
1359
|
"observationCount": 5,
|
|
@@ -1929,13 +1960,74 @@ exports[`IPC message snapshots ServerMessage types telegram_config_response seri
|
|
|
1929
1960
|
|
|
1930
1961
|
exports[`IPC message snapshots ServerMessage types twilio_config_response serializes to expected JSON 1`] = `
|
|
1931
1962
|
{
|
|
1963
|
+
"compliance": {
|
|
1964
|
+
"numberType": "toll_free",
|
|
1965
|
+
"verificationSid": "TF_VER_001",
|
|
1966
|
+
"verificationStatus": "TWILIO_APPROVED",
|
|
1967
|
+
},
|
|
1968
|
+
"diagnostics": {
|
|
1969
|
+
"actionItems": [],
|
|
1970
|
+
"compliance": {
|
|
1971
|
+
"detail": "Toll-free verification: TWILIO_APPROVED",
|
|
1972
|
+
"status": "TWILIO_APPROVED",
|
|
1973
|
+
},
|
|
1974
|
+
"overallStatus": "healthy",
|
|
1975
|
+
"readiness": {
|
|
1976
|
+
"issues": [],
|
|
1977
|
+
"ready": true,
|
|
1978
|
+
},
|
|
1979
|
+
},
|
|
1932
1980
|
"hasCredentials": true,
|
|
1933
1981
|
"phoneNumber": "+15551234567",
|
|
1934
1982
|
"success": true,
|
|
1983
|
+
"testResult": {
|
|
1984
|
+
"finalStatus": "delivered",
|
|
1985
|
+
"initialStatus": "queued",
|
|
1986
|
+
"messageSid": "SM-test-001",
|
|
1987
|
+
"to": "+15559876543",
|
|
1988
|
+
},
|
|
1935
1989
|
"type": "twilio_config_response",
|
|
1936
1990
|
}
|
|
1937
1991
|
`;
|
|
1938
1992
|
|
|
1993
|
+
exports[`IPC message snapshots ServerMessage types channel_readiness_response serializes to expected JSON 1`] = `
|
|
1994
|
+
{
|
|
1995
|
+
"snapshots": [
|
|
1996
|
+
{
|
|
1997
|
+
"channel": "sms",
|
|
1998
|
+
"checkedAt": 1700000000000,
|
|
1999
|
+
"localChecks": [
|
|
2000
|
+
{
|
|
2001
|
+
"message": "Twilio credentials are not configured",
|
|
2002
|
+
"name": "twilio_credentials",
|
|
2003
|
+
"passed": false,
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
"message": "Phone number is assigned",
|
|
2007
|
+
"name": "phone_number",
|
|
2008
|
+
"passed": true,
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
"message": "Public ingress URL is configured",
|
|
2012
|
+
"name": "ingress",
|
|
2013
|
+
"passed": true,
|
|
2014
|
+
},
|
|
2015
|
+
],
|
|
2016
|
+
"ready": false,
|
|
2017
|
+
"reasons": [
|
|
2018
|
+
{
|
|
2019
|
+
"code": "twilio_credentials",
|
|
2020
|
+
"text": "Twilio credentials are not configured",
|
|
2021
|
+
},
|
|
2022
|
+
],
|
|
2023
|
+
"stale": false,
|
|
2024
|
+
},
|
|
2025
|
+
],
|
|
2026
|
+
"success": true,
|
|
2027
|
+
"type": "channel_readiness_response",
|
|
2028
|
+
}
|
|
2029
|
+
`;
|
|
2030
|
+
|
|
1939
2031
|
exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = `
|
|
1940
2032
|
{
|
|
1941
2033
|
"instruction": "Send this code to the Telegram bot",
|
|
@@ -2499,3 +2591,11 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri
|
|
|
2499
2591
|
"type": "tool_names_list_response",
|
|
2500
2592
|
}
|
|
2501
2593
|
`;
|
|
2594
|
+
|
|
2595
|
+
exports[`IPC message snapshots ServerMessage types dictation_response serializes to expected JSON 1`] = `
|
|
2596
|
+
{
|
|
2597
|
+
"mode": "dictation",
|
|
2598
|
+
"text": "Hello world",
|
|
2599
|
+
"type": "dictation_response",
|
|
2600
|
+
}
|
|
2601
|
+
`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard test: ensures no production hard-coded approval lifecycle copy creeps
|
|
3
|
+
* back into route/service files outside of the composer module.
|
|
4
|
+
*
|
|
5
|
+
* The composer file (`approval-message-composer.ts`) is intentionally excluded
|
|
6
|
+
* since that is where deterministic fallback copy legitimately lives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { describe, test, expect } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
const SCANNED_FILES = [
|
|
14
|
+
'runtime/channel-approvals.ts',
|
|
15
|
+
'runtime/routes/channel-routes.ts',
|
|
16
|
+
'runtime/channel-guardian-service.ts',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const BANNED_PATTERNS: { pattern: RegExp; description: string }[] = [
|
|
20
|
+
{ pattern: /The assistant wants to use/i, description: 'old standard prompt' },
|
|
21
|
+
{ pattern: /Do you want to allow this/i, description: 'old approval question' },
|
|
22
|
+
{ pattern: /['"`]I'm still waiting/i, description: 'old reminder prefix (string literal)' },
|
|
23
|
+
{ pattern: /['"`].*is requesting to run/i, description: 'old guardian prompt (string literal)' },
|
|
24
|
+
{ pattern: /['"`]Sent to guardian/i, description: 'old forwarding notice (string literal)' },
|
|
25
|
+
{ pattern: /['"`]Guardian verified successfully/i, description: 'old verify success (string literal)' },
|
|
26
|
+
{ pattern: /['"`]Verification failed/i, description: 'old verify failure (string literal)' },
|
|
27
|
+
{ pattern: /['"`]Your request has been sent/i, description: 'old request forwarded notice (string literal)' },
|
|
28
|
+
{ pattern: /['"`]No guardian is configured/i, description: 'old no-binding notice (string literal)' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
describe('approval hardcoded copy guard', () => {
|
|
32
|
+
for (const file of SCANNED_FILES) {
|
|
33
|
+
test(`${file} does not contain banned approval copy literals`, () => {
|
|
34
|
+
const content = readFileSync(join(__dirname, '..', file), 'utf-8');
|
|
35
|
+
for (const { pattern, description: _description } of BANNED_PATTERNS) {
|
|
36
|
+
const match = content.match(pattern);
|
|
37
|
+
expect(match).toBeNull();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
composeApprovalMessage,
|
|
4
|
+
getFallbackMessage,
|
|
5
|
+
} from '../runtime/approval-message-composer.js';
|
|
6
|
+
import type {
|
|
7
|
+
ApprovalMessageScenario,
|
|
8
|
+
ApprovalMessageContext,
|
|
9
|
+
} from '../runtime/approval-message-composer.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Every scenario must produce a non-empty string
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const ALL_SCENARIOS: ApprovalMessageScenario[] = [
|
|
16
|
+
'standard_prompt',
|
|
17
|
+
'guardian_prompt',
|
|
18
|
+
'reminder_prompt',
|
|
19
|
+
'guardian_delivery_failed',
|
|
20
|
+
'guardian_request_forwarded',
|
|
21
|
+
'guardian_disambiguation',
|
|
22
|
+
'guardian_identity_mismatch',
|
|
23
|
+
'request_pending_guardian',
|
|
24
|
+
'guardian_decision_outcome',
|
|
25
|
+
'guardian_expired_requester',
|
|
26
|
+
'guardian_expired_guardian',
|
|
27
|
+
'guardian_verify_success',
|
|
28
|
+
'guardian_verify_failed',
|
|
29
|
+
'guardian_verify_challenge_setup',
|
|
30
|
+
'guardian_verify_status_bound',
|
|
31
|
+
'guardian_verify_status_unbound',
|
|
32
|
+
'guardian_deny_no_identity',
|
|
33
|
+
'guardian_deny_no_binding',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe('approval-message-composer', () => {
|
|
37
|
+
// -----------------------------------------------------------------------
|
|
38
|
+
// Fallback messages — every scenario produces non-empty output
|
|
39
|
+
// -----------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe('getFallbackMessage', () => {
|
|
42
|
+
for (const scenario of ALL_SCENARIOS) {
|
|
43
|
+
test(`scenario "${scenario}" produces a non-empty string`, () => {
|
|
44
|
+
const msg = getFallbackMessage({ scenario });
|
|
45
|
+
expect(typeof msg).toBe('string');
|
|
46
|
+
expect(msg.trim().length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('standard_prompt includes toolName when provided', () => {
|
|
51
|
+
const msg = getFallbackMessage({
|
|
52
|
+
scenario: 'standard_prompt',
|
|
53
|
+
toolName: 'execute_shell',
|
|
54
|
+
});
|
|
55
|
+
expect(msg).toContain('execute_shell');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('guardian_prompt includes requester identifier and toolName', () => {
|
|
59
|
+
const msg = getFallbackMessage({
|
|
60
|
+
scenario: 'guardian_prompt',
|
|
61
|
+
toolName: 'write_file',
|
|
62
|
+
requesterIdentifier: 'alice',
|
|
63
|
+
});
|
|
64
|
+
expect(msg).toContain('alice');
|
|
65
|
+
expect(msg).toContain('write_file');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('guardian_delivery_failed includes toolName when provided', () => {
|
|
69
|
+
const msg = getFallbackMessage({
|
|
70
|
+
scenario: 'guardian_delivery_failed',
|
|
71
|
+
toolName: 'execute_shell',
|
|
72
|
+
});
|
|
73
|
+
expect(msg).toContain('execute_shell');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('guardian_request_forwarded includes toolName', () => {
|
|
77
|
+
const msg = getFallbackMessage({
|
|
78
|
+
scenario: 'guardian_request_forwarded',
|
|
79
|
+
toolName: 'execute_shell',
|
|
80
|
+
});
|
|
81
|
+
expect(msg).toContain('execute_shell');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('guardian_disambiguation includes pendingCount', () => {
|
|
85
|
+
const msg = getFallbackMessage({
|
|
86
|
+
scenario: 'guardian_disambiguation',
|
|
87
|
+
pendingCount: 3,
|
|
88
|
+
});
|
|
89
|
+
expect(msg).toContain('3');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('guardian_decision_outcome includes decision and toolName', () => {
|
|
93
|
+
const msg = getFallbackMessage({
|
|
94
|
+
scenario: 'guardian_decision_outcome',
|
|
95
|
+
decision: 'approved',
|
|
96
|
+
toolName: 'read_file',
|
|
97
|
+
});
|
|
98
|
+
expect(msg).toContain('approved');
|
|
99
|
+
expect(msg).toContain('read_file');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('guardian_expired_requester includes toolName', () => {
|
|
103
|
+
const msg = getFallbackMessage({
|
|
104
|
+
scenario: 'guardian_expired_requester',
|
|
105
|
+
toolName: 'deploy',
|
|
106
|
+
});
|
|
107
|
+
expect(msg).toContain('deploy');
|
|
108
|
+
expect(msg).toContain('expired');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('guardian_expired_guardian includes requester and toolName', () => {
|
|
112
|
+
const msg = getFallbackMessage({
|
|
113
|
+
scenario: 'guardian_expired_guardian',
|
|
114
|
+
requesterIdentifier: 'bob',
|
|
115
|
+
toolName: 'delete_file',
|
|
116
|
+
});
|
|
117
|
+
expect(msg).toContain('bob');
|
|
118
|
+
expect(msg).toContain('delete_file');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('guardian_verify_failed includes failureReason', () => {
|
|
122
|
+
const msg = getFallbackMessage({
|
|
123
|
+
scenario: 'guardian_verify_failed',
|
|
124
|
+
failureReason: 'Code did not match.',
|
|
125
|
+
});
|
|
126
|
+
expect(msg).toContain('Code did not match.');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('guardian_verify_challenge_setup includes verifyCommand and ttlSeconds', () => {
|
|
130
|
+
const msg = getFallbackMessage({
|
|
131
|
+
scenario: 'guardian_verify_challenge_setup',
|
|
132
|
+
verifyCommand: '/verify abc123',
|
|
133
|
+
ttlSeconds: 30,
|
|
134
|
+
});
|
|
135
|
+
expect(msg).toContain('/verify abc123');
|
|
136
|
+
expect(msg).toContain('30');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
// composeApprovalMessage — layered source selection
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
describe('composeApprovalMessage', () => {
|
|
145
|
+
test('returns assistantPreface when provided (primary source)', () => {
|
|
146
|
+
const preface = 'The assistant already said something helpful.';
|
|
147
|
+
const msg = composeApprovalMessage({
|
|
148
|
+
scenario: 'standard_prompt',
|
|
149
|
+
toolName: 'execute_shell',
|
|
150
|
+
assistantPreface: preface,
|
|
151
|
+
});
|
|
152
|
+
expect(msg).toBe(preface);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('ignores empty assistantPreface and falls back to template', () => {
|
|
156
|
+
const msg = composeApprovalMessage({
|
|
157
|
+
scenario: 'standard_prompt',
|
|
158
|
+
toolName: 'execute_shell',
|
|
159
|
+
assistantPreface: '',
|
|
160
|
+
});
|
|
161
|
+
expect(msg).toContain('execute_shell');
|
|
162
|
+
expect(msg).not.toBe('');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('ignores whitespace-only assistantPreface', () => {
|
|
166
|
+
const msg = composeApprovalMessage({
|
|
167
|
+
scenario: 'standard_prompt',
|
|
168
|
+
toolName: 'execute_shell',
|
|
169
|
+
assistantPreface: ' ',
|
|
170
|
+
});
|
|
171
|
+
expect(msg).toContain('execute_shell');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('falls back to deterministic template when no assistantPreface', () => {
|
|
175
|
+
const msg = composeApprovalMessage({
|
|
176
|
+
scenario: 'guardian_prompt',
|
|
177
|
+
toolName: 'write_file',
|
|
178
|
+
requesterIdentifier: 'charlie',
|
|
179
|
+
});
|
|
180
|
+
expect(msg).toContain('charlie');
|
|
181
|
+
expect(msg).toContain('write_file');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('fallback matches getFallbackMessage output', () => {
|
|
185
|
+
const ctx: ApprovalMessageContext = {
|
|
186
|
+
scenario: 'reminder_prompt',
|
|
187
|
+
};
|
|
188
|
+
expect(composeApprovalMessage(ctx)).toBe(getFallbackMessage(ctx));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// -----------------------------------------------------------------------
|
|
193
|
+
// Verification scenario resilience — composed messages contain key facts
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe('verification scenario resilience', () => {
|
|
197
|
+
test('guardian_verify_challenge_setup includes verify command and TTL', () => {
|
|
198
|
+
const msg = composeApprovalMessage({
|
|
199
|
+
scenario: 'guardian_verify_challenge_setup',
|
|
200
|
+
verifyCommand: '/guardian_verify abc123def456',
|
|
201
|
+
ttlSeconds: 600,
|
|
202
|
+
});
|
|
203
|
+
expect(typeof msg).toBe('string');
|
|
204
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
205
|
+
expect(msg).toContain('/guardian_verify abc123def456');
|
|
206
|
+
expect(msg).toContain('600');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('guardian_verify_failed includes failure reason', () => {
|
|
210
|
+
const msg = composeApprovalMessage({
|
|
211
|
+
scenario: 'guardian_verify_failed',
|
|
212
|
+
failureReason: 'Too many attempts. Please try again later.',
|
|
213
|
+
});
|
|
214
|
+
expect(typeof msg).toBe('string');
|
|
215
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
216
|
+
expect(msg).toContain('Too many attempts');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('guardian_verify_failed with invalid-or-expired reason includes that reason', () => {
|
|
220
|
+
const msg = composeApprovalMessage({
|
|
221
|
+
scenario: 'guardian_verify_failed',
|
|
222
|
+
failureReason: 'The verification code is invalid or has expired.',
|
|
223
|
+
});
|
|
224
|
+
expect(typeof msg).toBe('string');
|
|
225
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
226
|
+
expect(msg).toContain('invalid or has expired');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('guardian_verify_success produces a non-empty success message', () => {
|
|
230
|
+
const msg = composeApprovalMessage({
|
|
231
|
+
scenario: 'guardian_verify_success',
|
|
232
|
+
});
|
|
233
|
+
expect(typeof msg).toBe('string');
|
|
234
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('guardian_verify_status_bound produces a non-empty message', () => {
|
|
238
|
+
const msg = composeApprovalMessage({
|
|
239
|
+
scenario: 'guardian_verify_status_bound',
|
|
240
|
+
});
|
|
241
|
+
expect(typeof msg).toBe('string');
|
|
242
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('guardian_verify_status_unbound produces a non-empty message', () => {
|
|
246
|
+
const msg = composeApprovalMessage({
|
|
247
|
+
scenario: 'guardian_verify_status_unbound',
|
|
248
|
+
});
|
|
249
|
+
expect(typeof msg).toBe('string');
|
|
250
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -34,10 +34,10 @@ mock.module('../util/logger.js', () => ({
|
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
36
|
mock.module('../calls/twilio-config.js', () => ({
|
|
37
|
-
getTwilioConfig: () => ({
|
|
37
|
+
getTwilioConfig: (assistantId?: string) => ({
|
|
38
38
|
accountSid: 'AC_test',
|
|
39
39
|
authToken: 'test_token',
|
|
40
|
-
phoneNumber: '+15550001111',
|
|
40
|
+
phoneNumber: assistantId === 'ast-alpha' ? '+15550003333' : '+15550001111',
|
|
41
41
|
webhookBaseUrl: 'https://test.example.com',
|
|
42
42
|
wssBaseUrl: 'wss://test.example.com',
|
|
43
43
|
}),
|
|
@@ -97,6 +97,16 @@ describe('resolveCallerIdentity — strict implicit-default policy', () => {
|
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
test('assistant_number resolves from assistant-scoped Twilio number when assistantId is provided', async () => {
|
|
101
|
+
const result = await resolveCallerIdentity(makeConfig(), undefined, 'ast-alpha');
|
|
102
|
+
expect(result.ok).toBe(true);
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
expect(result.mode).toBe('assistant_number');
|
|
105
|
+
expect(result.fromNumber).toBe('+15550003333');
|
|
106
|
+
expect(result.source).toBe('implicit_default');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
100
110
|
test('explicit user_number succeeds when eligible', async () => {
|
|
101
111
|
const result = await resolveCallerIdentity(
|
|
102
112
|
makeConfig({ userNumber: '+15550002222' }),
|
|
@@ -38,6 +38,7 @@ mock.module('../config/user-reference.js', () => ({
|
|
|
38
38
|
// ── Config mock ─────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
40
|
let mockCallModel: string | undefined = undefined;
|
|
41
|
+
let mockDisclosure: { enabled: boolean; text: string } = { enabled: false, text: '' };
|
|
41
42
|
|
|
42
43
|
mock.module('../config/loader.js', () => ({
|
|
43
44
|
getConfig: () => ({
|
|
@@ -49,7 +50,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
49
50
|
userConsultTimeoutSeconds: 90,
|
|
50
51
|
userConsultationTimeoutSeconds: 90,
|
|
51
52
|
silenceTimeoutSeconds: 30,
|
|
52
|
-
disclosure:
|
|
53
|
+
disclosure: mockDisclosure,
|
|
53
54
|
safety: { denyCategories: [] },
|
|
54
55
|
model: mockCallModel,
|
|
55
56
|
},
|
|
@@ -206,6 +207,7 @@ describe('call-orchestrator', () => {
|
|
|
206
207
|
resetTables();
|
|
207
208
|
mockCallModel = undefined;
|
|
208
209
|
mockUserReference = 'my human';
|
|
210
|
+
mockDisclosure = { enabled: false, text: '' };
|
|
209
211
|
// Reset the stream mock to default behaviour
|
|
210
212
|
mockStreamFn.mockImplementation(() => createMockStream(['Hello', ' there']));
|
|
211
213
|
});
|
|
@@ -289,6 +291,30 @@ describe('call-orchestrator', () => {
|
|
|
289
291
|
orchestrator.destroy();
|
|
290
292
|
});
|
|
291
293
|
|
|
294
|
+
test('strips USER_ANSWERED and USER_INSTRUCTION markers from spoken output', async () => {
|
|
295
|
+
mockStreamFn.mockImplementation(() =>
|
|
296
|
+
createMockStream([
|
|
297
|
+
'Thanks for waiting. ',
|
|
298
|
+
'[USER_ANSWERED: The guardian said 3 PM works.] ',
|
|
299
|
+
'[USER_INSTRUCTION: Keep this short.] ',
|
|
300
|
+
'I can confirm 3 PM works.',
|
|
301
|
+
]),
|
|
302
|
+
);
|
|
303
|
+
const { relay, orchestrator } = setupOrchestrator();
|
|
304
|
+
|
|
305
|
+
await orchestrator.handleCallerUtterance('Any update?');
|
|
306
|
+
|
|
307
|
+
const allText = relay.sentTokens.map((t) => t.token).join('');
|
|
308
|
+
expect(allText).toContain('Thanks for waiting.');
|
|
309
|
+
expect(allText).toContain('I can confirm 3 PM works.');
|
|
310
|
+
expect(allText).not.toContain('[USER_ANSWERED:');
|
|
311
|
+
expect(allText).not.toContain('[USER_INSTRUCTION:');
|
|
312
|
+
expect(allText).not.toContain('USER_ANSWERED');
|
|
313
|
+
expect(allText).not.toContain('USER_INSTRUCTION');
|
|
314
|
+
|
|
315
|
+
orchestrator.destroy();
|
|
316
|
+
});
|
|
317
|
+
|
|
292
318
|
// ── END_CALL pattern ──────────────────────────────────────────────
|
|
293
319
|
|
|
294
320
|
test('END_CALL pattern: detects marker, calls endSession, updates status to completed', async () => {
|
|
@@ -943,4 +969,47 @@ describe('call-orchestrator', () => {
|
|
|
943
969
|
await orchestrator.handleCallerUtterance('Test');
|
|
944
970
|
orchestrator.destroy();
|
|
945
971
|
});
|
|
972
|
+
|
|
973
|
+
test('system prompt uses disclosure text when disclosure is enabled', async () => {
|
|
974
|
+
mockDisclosure = {
|
|
975
|
+
enabled: true,
|
|
976
|
+
text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the person you represent. Do not say "AI assistant".',
|
|
977
|
+
};
|
|
978
|
+
mockStreamFn.mockImplementation((...args: unknown[]) => {
|
|
979
|
+
const firstArg = args[0] as { system: string };
|
|
980
|
+
expect(firstArg.system).toContain('introduce yourself as an assistant calling on behalf of the person you represent');
|
|
981
|
+
expect(firstArg.system).toContain('Do not say "AI assistant"');
|
|
982
|
+
return createMockStream(['Hello, I am calling on behalf of my human.']);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const { orchestrator } = setupOrchestrator();
|
|
986
|
+
await orchestrator.handleCallerUtterance('Who is this?');
|
|
987
|
+
orchestrator.destroy();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test('system prompt falls back to "Begin the conversation naturally" when disclosure is disabled', async () => {
|
|
991
|
+
mockDisclosure = { enabled: false, text: '' };
|
|
992
|
+
mockStreamFn.mockImplementation((...args: unknown[]) => {
|
|
993
|
+
const firstArg = args[0] as { system: string };
|
|
994
|
+
expect(firstArg.system).toContain('Begin the conversation naturally');
|
|
995
|
+
expect(firstArg.system).not.toContain('introduce yourself as an assistant calling on behalf of the person');
|
|
996
|
+
return createMockStream(['Hello there.']);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
const { orchestrator } = setupOrchestrator();
|
|
1000
|
+
await orchestrator.handleCallerUtterance('Hi');
|
|
1001
|
+
orchestrator.destroy();
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test('system prompt does not use "AI assistant" as a self-identity label', async () => {
|
|
1005
|
+
mockStreamFn.mockImplementation((...args: unknown[]) => {
|
|
1006
|
+
const firstArg = args[0] as { system: string };
|
|
1007
|
+
expect(firstArg.system).not.toMatch(/(?:you are|call yourself|introduce yourself as).*AI assistant/i);
|
|
1008
|
+
return createMockStream(['Got it.']);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const { orchestrator } = setupOrchestrator();
|
|
1012
|
+
await orchestrator.handleCallerUtterance('Hello');
|
|
1013
|
+
orchestrator.destroy();
|
|
1014
|
+
});
|
|
946
1015
|
});
|