@vellumai/assistant 0.4.43 → 0.4.44

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.
Files changed (88) hide show
  1. package/ARCHITECTURE.md +13 -14
  2. package/README.md +11 -12
  3. package/docs/architecture/integrations.md +75 -93
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-routes-http.test.ts +0 -2
  6. package/src/__tests__/bundled-asset.test.ts +1 -1
  7. package/src/__tests__/checker.test.ts +31 -28
  8. package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  10. package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
  11. package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
  12. package/src/__tests__/onboarding-template-contract.test.ts +0 -10
  13. package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
  14. package/src/__tests__/send-endpoint-busy.test.ts +0 -3
  15. package/src/__tests__/session-confirmation-signals.test.ts +7 -45
  16. package/src/__tests__/starter-task-flow.test.ts +9 -19
  17. package/src/__tests__/system-prompt.test.ts +3 -4
  18. package/src/__tests__/trust-store.test.ts +4 -4
  19. package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
  20. package/src/cli/commands/amazon/index.ts +4 -39
  21. package/src/cli/commands/amazon/session.ts +18 -26
  22. package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
  23. package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
  24. package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
  25. package/src/cli/commands/twitter/index.ts +95 -835
  26. package/src/cli/commands/twitter/oauth-client.ts +1 -35
  27. package/src/cli/commands/twitter/router.ts +70 -115
  28. package/src/cli/commands/twitter/types.ts +30 -0
  29. package/src/cli/reference.ts +2 -2
  30. package/src/config/bundled-skills/amazon/SKILL.md +0 -1
  31. package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
  32. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
  33. package/src/config/bundled-skills/doordash/SKILL.md +0 -1
  34. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
  35. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
  36. package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
  37. package/src/config/bundled-skills/twitter/SKILL.md +53 -166
  38. package/src/config/feature-flag-registry.json +8 -0
  39. package/src/daemon/handlers/session-history.ts +41 -9
  40. package/src/daemon/lifecycle.ts +4 -17
  41. package/src/daemon/message-types/apps.ts +0 -25
  42. package/src/daemon/message-types/integrations.ts +1 -7
  43. package/src/daemon/message-types/sessions.ts +6 -1
  44. package/src/daemon/message-types/surfaces.ts +2 -0
  45. package/src/daemon/ride-shotgun-handler.ts +33 -1
  46. package/src/daemon/seed-files.ts +3 -27
  47. package/src/daemon/server.ts +2 -18
  48. package/src/daemon/session-agent-loop-handlers.ts +24 -2
  49. package/src/daemon/session-runtime-assembly.ts +0 -7
  50. package/src/daemon/session-surfaces.ts +185 -33
  51. package/src/daemon/session.ts +2 -28
  52. package/src/memory/app-store.ts +0 -18
  53. package/src/memory/schema/infrastructure.ts +0 -8
  54. package/src/permissions/defaults.ts +3 -3
  55. package/src/prompts/system-prompt.ts +4 -5
  56. package/src/prompts/templates/BOOTSTRAP.md +0 -3
  57. package/src/providers/registry.ts +2 -4
  58. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  59. package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
  60. package/src/runtime/auth/route-policy.ts +0 -4
  61. package/src/runtime/auth/scopes.ts +1 -0
  62. package/src/runtime/auth/token-service.ts +1 -1
  63. package/src/runtime/http-types.ts +10 -0
  64. package/src/runtime/middleware/error-handler.ts +14 -1
  65. package/src/runtime/routes/app-management-routes.ts +61 -64
  66. package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
  67. package/src/runtime/routes/brain-graph-routes.ts +4 -42
  68. package/src/runtime/routes/conversation-routes.ts +9 -6
  69. package/src/runtime/routes/diagnostics-routes.ts +91 -14
  70. package/src/runtime/routes/settings-routes.ts +3 -93
  71. package/src/tools/AGENTS.md +38 -0
  72. package/src/tools/apps/executors.ts +0 -6
  73. package/src/tools/document/editor-template.ts +10 -8
  74. package/src/twitter/platform-proxy-client.ts +6 -3
  75. package/src/util/errors.ts +12 -0
  76. package/src/__tests__/home-base-bootstrap.test.ts +0 -84
  77. package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
  78. package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
  79. package/src/cli/commands/twitter/client.ts +0 -989
  80. package/src/cli/commands/twitter/session.ts +0 -121
  81. package/src/home-base/app-link-store.ts +0 -78
  82. package/src/home-base/bootstrap.ts +0 -74
  83. package/src/home-base/prebuilt/brain-graph.html +0 -1483
  84. package/src/home-base/prebuilt/index.html +0 -702
  85. package/src/home-base/prebuilt/seed-metadata.json +0 -21
  86. package/src/home-base/prebuilt/seed.ts +0 -122
  87. package/src/home-base/prebuilt-home-base-updater.ts +0 -36
  88. package/src/util/cookie-session.ts +0 -98
package/ARCHITECTURE.md CHANGED
@@ -5,12 +5,11 @@ This document owns assistant-runtime architecture details. The repo-level archit
5
5
  ### Channel Onboarding Playbook Bootstrap
6
6
 
7
7
  - Transport metadata arrives via `session_create.transport` (HTTP) or `/channels/inbound` (`channelId`, optional `hints`, optional `uxBrief`).
8
- - Telegram webhook ingress now injects deterministic channel-safe transport metadata (`hints` + `uxBrief`) so non-dashboard channels defer Home Base-only UI tasks cleanly.
8
+ - Telegram webhook ingress injects deterministic channel-safe transport metadata (`hints` + `uxBrief`) so non-dashboard channels defer dashboard-only UI tasks cleanly.
9
9
  - `OnboardingPlaybookManager` resolves `<channel>_onboarding.md`, checks `onboarding/playbooks/registry.json`, and applies per-channel first-time fast-path onboarding.
10
- - `OnboardingOrchestrator` derives onboarding-mode guidance (post-hatch sequence, USER.md capture, Home Base handoff) from playbook + transport context.
10
+ - `OnboardingOrchestrator` derives onboarding-mode guidance (post-hatch sequence, USER.md capture) from playbook + transport context.
11
11
  - Session runtime assembly injects both `<channel_onboarding_playbook>` and `<onboarding_mode>` context before provider calls, then strips both from persisted conversation history.
12
- - Daemon startup runs `ensurePrebuiltHomeBaseSeeded()` to provision one idempotent prebuilt Home Base app in `~/.vellum/workspace/data/apps`.
13
- - Home Base onboarding buttons relay prefilled natural-language prompts to the main assistant; permission setup remains user-initiated and hatch + first-conversation flows avoid proactive permission asks.
12
+ - Permission setup remains user-initiated and hatch + first-conversation flows avoid proactive permission asks.
14
13
 
15
14
  ### Guardian Actor Context (Unified Across Channels)
16
15
 
@@ -46,7 +45,7 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
46
45
  | `actor:<assistantId>:<actorPrincipalId>` | `actor` | Desktop, iOS, or CLI client |
47
46
  | `svc:gateway:<assistantId>` | `svc_gateway` | Gateway service (ingress, webhooks) |
48
47
  | `svc:internal:<assistantId>:<sessionId>` | `svc_internal` | Internal service connections |
49
- | `svc:daemon:<identifier>` | `svc_daemon` | Daemon service token (CLI bootstrap, local) |
48
+ | `svc:daemon:<identifier>` | `svc_daemon` | Daemon service token (local) |
50
49
 
51
50
  **Scope profiles:**
52
51
 
@@ -59,7 +58,7 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
59
58
 
60
59
  **Identity lifecycle:**
61
60
 
62
- 1. **Bootstrap (loopback-only, macOS/CLI)** — On first launch, the client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. The endpoint is loopback-only and mints a JWT access token + refresh token pair. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`.
61
+ 1. **Bootstrap (loopback-only, macOS)** — On first launch, the macOS client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. The endpoint is loopback-only and mints a JWT access token + refresh token pair. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. The CLI obtains its bearer token during `hatch` and does not perform a separate bootstrap step.
63
62
 
64
63
  2. **iOS pairing** — iOS devices obtain JWTs through the QR pairing flow. The pairing response includes `accessToken` and `refreshToken` credentials.
65
64
 
@@ -719,7 +718,7 @@ graph LR
719
718
  CONFIG["config files<br/>Hot-reloaded by daemon<br/>(includes assistantFeatureFlagValues)"]
720
719
  ONBOARD_PLAYBOOKS["onboarding/playbooks/<br/>[channel]_onboarding.md<br/>assistant-updatable checklists"]
721
720
  ONBOARD_REGISTRY["onboarding/playbooks/registry.json<br/>channel-start index for fast-path + reconciliation"]
722
- APPS_STORE["data/apps/<br/><app-id>.json + pages/*.html<br/>prebuilt Home Base seeded here"]
721
+ APPS_STORE["data/apps/<br/><app-id>.json + pages/*.html<br/>User-created apps stored here"]
723
722
  SKILLS_DIR["skills/<br/>managed skill directories<br/>SKILL.md + TOOLS.json + tools/"]
724
723
  end
725
724
 
@@ -1736,7 +1735,7 @@ Every event published through the hub is wrapped in an `AssistantEvent` (defined
1736
1735
  | `assistantId` | `string` | Logical assistant identifier (`"self"` for HTTP runs) |
1737
1736
  | `sessionId` | `string?` | Resolved conversation ID when available |
1738
1737
  | `emittedAt` | `string` (ISO-8601) | Server-side timestamp |
1739
- | `message` | `ServerMessage` | The outbound message payload |
1738
+ | `message` | `ServerMessage` | The outbound message payload |
1740
1739
 
1741
1740
  ### SSE Frame Format
1742
1741
 
@@ -1766,12 +1765,12 @@ Keep-alive heartbeats (every 30 s by default):
1766
1765
 
1767
1766
  ### Key Source Files
1768
1767
 
1769
- | File | Role |
1770
- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
1771
- | `assistant/src/runtime/assistant-event.ts` | `AssistantEvent` type, `buildAssistantEvent()` factory, SSE framing helpers |
1772
- | `assistant/src/runtime/assistant-event-hub.ts` | `AssistantEventHub` class and process-level singleton |
1773
- | `assistant/src/runtime/routes/events-routes.ts` | `handleSubscribeAssistantEvents()` — SSE route handler |
1774
- | `assistant/src/daemon/server.ts` | Session event paths that publish to the hub (`send` → `publishAssistantEvent`) |
1768
+ | File | Role |
1769
+ | ----------------------------------------------- | ------------------------------------------------------------------------------ |
1770
+ | `assistant/src/runtime/assistant-event.ts` | `AssistantEvent` type, `buildAssistantEvent()` factory, SSE framing helpers |
1771
+ | `assistant/src/runtime/assistant-event-hub.ts` | `AssistantEventHub` class and process-level singleton |
1772
+ | `assistant/src/runtime/routes/events-routes.ts` | `handleSubscribeAssistantEvents()` — SSE route handler |
1773
+ | `assistant/src/daemon/server.ts` | Session event paths that publish to the hub (`send` → `publishAssistantEvent`) |
1775
1774
 
1776
1775
  ---
1777
1776
 
package/README.md CHANGED
@@ -36,15 +36,15 @@ cp .env.example .env
36
36
 
37
37
  ## Configuration
38
38
 
39
- | Variable | Required | Default | Description |
40
- | ---------------------- | -------- | --------------------------- | ------------------------------------------------- |
41
- | `ANTHROPIC_API_KEY` | Yes | — | Anthropic Claude API key |
42
- | `OPENAI_API_KEY` | No | — | OpenAI API key |
43
- | `GEMINI_API_KEY` | No | — | Google Gemini API key |
44
- | `OLLAMA_API_KEY` | No | — | API key for authenticated Ollama deployments |
45
- | `OLLAMA_BASE_URL` | No | `http://127.0.0.1:11434/v1` | Ollama base URL |
46
- | `RUNTIME_HTTP_PORT` | No | — | Enable the HTTP server (required for gateway/web) |
47
- | `RUNTIME_HTTP_HOST` | No | `127.0.0.1` | HTTP server bind address |
39
+ | Variable | Required | Default | Description |
40
+ | ------------------- | -------- | --------------------------- | ------------------------------------------------- |
41
+ | `ANTHROPIC_API_KEY` | Yes | — | Anthropic Claude API key |
42
+ | `OPENAI_API_KEY` | No | — | OpenAI API key |
43
+ | `GEMINI_API_KEY` | No | — | Google Gemini API key |
44
+ | `OLLAMA_API_KEY` | No | — | API key for authenticated Ollama deployments |
45
+ | `OLLAMA_BASE_URL` | No | `http://127.0.0.1:11434/v1` | Ollama base URL |
46
+ | `RUNTIME_HTTP_PORT` | No | — | Enable the HTTP server (required for gateway/web) |
47
+ | `RUNTIME_HTTP_HOST` | No | `127.0.0.1` | HTTP server bind address |
48
48
 
49
49
  ## Update Bulletin
50
50
 
@@ -112,7 +112,6 @@ assistant/
112
112
  │ ├── messaging/ # Message processing pipeline
113
113
  │ ├── context/ # Context assembly and compaction
114
114
  │ ├── playbooks/ # Channel onboarding playbooks
115
- │ ├── home-base/ # Home Base app-link bootstrap
116
115
  │ ├── hooks/ # Git-style lifecycle hooks
117
116
  │ ├── media/ # Media processing and attachments
118
117
  │ ├── schedule/ # Reminders and recurrence scheduling (cron + RRULE)
@@ -265,9 +264,9 @@ The channel guardian service generates verification challenge instructions with
265
264
 
266
265
  ### Vellum Guardian Identity (Actor Tokens)
267
266
 
268
- The vellum channel (macOS, iOS, CLI) uses JWTs to bind guardian identity to HTTP requests. This enables identity-based authentication for the local desktop/mobile channel, paralleling how external channels (Telegram) use `actorExternalId` for guardian identity.
267
+ The vellum channel (macOS, iOS) uses JWTs to bind guardian identity to HTTP requests. This enables identity-based authentication for the local desktop/mobile channel, paralleling how external channels (Telegram) use `actorExternalId` for guardian identity. The CLI authenticates using its bearer token obtained during `hatch`.
269
268
 
270
- - **Bootstrap**: After hatch, the macOS client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. The endpoint is idempotent -- repeated calls with the same device return the same principal but mint fresh credentials.
269
+ - **Bootstrap**: After hatch, the macOS client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. The endpoint is idempotent -- repeated calls with the same device return the same principal but mint fresh credentials. The CLI does not bootstrap separately; it uses the bearer token minted during `hatch`.
271
270
  - **iOS pairing**: The pairing response includes `accessToken` and `refreshToken` credentials automatically when a vellum guardian binding exists.
272
271
  - **Local identity**: Local connections resolve identity server-side via `resolveLocalGuardianContext()` without requiring a JWT.
273
272
  - **HTTP enforcement**: All vellum HTTP routes require a valid JWT via the `Authorization: Bearer <jwt>` header. The JWT carries identity claims (`sub` with principal type and ID) and scope permissions. Route-level enforcement in `route-policy.ts` checks scopes and principal types.
@@ -9,7 +9,7 @@ The integration framework lets Vellum connect to third-party services via OAuth2
9
9
  - **Secrets never reach the LLM** — OAuth tokens are stored in the credential vault and accessed exclusively through the `TokenManager`, which provides tokens to tool executors via `withValidToken()`. The LLM never sees raw tokens.
10
10
  - **PKCE or client_secret flows** — Desktop apps use PKCE by default (S256). Providers that require a client secret (e.g. Slack) pass it during the OAuth2 flow and store it in credential metadata for autonomous refresh. Twitter uses PKCE with an optional client secret in `local_byo` mode.
11
11
  - **Unified messaging layer** — All messaging platforms implement the `MessagingProvider` interface. Generic tools delegate to the provider, so adding a new platform is just implementing one adapter + an OAuth setup skill.
12
- - **Standalone integrations** — Not all integrations fit the messaging model. Twitter has its own OAuth2 flow and HTTP handlers (`twitter_auth_start`, `twitter_auth_status`) separate from the unified messaging layer.
12
+ - **Standalone integrations** — Not all integrations fit the messaging model. Twitter has its own OAuth2 flow via the shared connect orchestrator, plus a managed mode that routes through the platform proxy. It sits outside the unified messaging layer.
13
13
  - **Provider registry** — Messaging providers register at daemon startup. The registry tracks which providers have stored credentials, enabling auto-selection when only one is connected.
14
14
 
15
15
  ### Unified Messaging Architecture
@@ -139,7 +139,7 @@ sequenceDiagram
139
139
 
140
140
  ### Twitter Integration Architecture
141
141
 
142
- Twitter uses a standalone OAuth2 flow separate from the unified messaging layer. It supports a dual-path operation architecture: an **OAuth path** that calls X API v2 directly for posting and replying, and a **Browser path** that uses Chrome DevTools Protocol (CDP) for all operations including read-only ones. A strategy router (`router.ts`) selects the appropriate path based on user preference and capability.
142
+ Twitter uses a standalone OAuth2 flow separate from the unified messaging layer. It supports a two-mode operation architecture determined by the `twitter.integrationMode` config field: **managed** mode routes all API calls through the Vellum platform proxy (which holds the OAuth credentials), while **OAuth** mode uses locally-stored OAuth2 tokens to call X API v2 directly. A mode router (`router.ts`) selects the appropriate path based on the caller-provided mode.
143
143
 
144
144
  #### Twitter OAuth2 Flow
145
145
 
@@ -184,46 +184,36 @@ sequenceDiagram
184
184
  IPC->>UI: show connected state
185
185
  ```
186
186
 
187
- #### Dual-Path Operation Architecture
187
+ #### Two-Mode Operation Architecture
188
188
 
189
- The strategy router (`router.ts`) determines whether to use the OAuth or browser path for each operation. The preferred strategy is read from the `twitter.operationStrategy` config field (default: `auto`).
189
+ The mode router (`router.ts`) determines whether to use the managed or OAuth path for each operation. The mode is determined by the `twitter.integrationMode` config field: `"managed"` routes through the platform proxy, everything else uses OAuth directly.
190
190
 
191
191
  ```mermaid
192
192
  flowchart TD
193
- CLI["vellum x post / reply"] --> Router["Strategy Router (router.ts)"]
194
- Router --> StratCheck{Preferred strategy?}
193
+ CLI["assistant x post / reply / timeline / search"] --> Router["Mode Router (router.ts)"]
194
+ Router --> ModeCheck{Integration mode?}
195
195
 
196
- StratCheck -->|oauth| OAuthOnly["OAuth Client (oauth-client.ts)"]
197
- OAuthOnly --> XAPI["X API v2 POST /tweets"]
196
+ ModeCheck -->|managed| ManagedPath["Platform Proxy Client (platform-proxy-client.ts)"]
197
+ ManagedPath --> PlatformAPI["Platform → X API v2"]
198
198
 
199
- StratCheck -->|browser| BrowserOnly["Browser Client (client.ts)"]
200
- BrowserOnly --> CDP["Chrome CDP GraphQL mutation"]
201
-
202
- StratCheck -->|auto| AutoCheck{"OAuth available &\noperation supported?"}
203
- AutoCheck -->|yes| TryOAuth["Try OAuth Client"]
204
- TryOAuth -->|success| XAPI
205
- TryOAuth -->|failure| Fallback["Fallback to Browser Client"]
206
- Fallback --> CDP
207
- AutoCheck -->|no| BrowserOnly
199
+ ModeCheck -->|oauth| OAuthPath["OAuth Client (oauth-client.ts)"]
200
+ OAuthPath --> XAPI["X API v2 POST /tweets"]
208
201
  ```
209
202
 
210
- - **`auto`** (default): Checks `oauthIsAvailable()` (access token stored) and `oauthSupportsOperation()` (currently `post` and `reply`). If both pass, tries OAuth first. On OAuth failure, falls back to browser. If OAuth is not available or the operation is unsupported, uses browser directly.
211
- - **`oauth`**: Uses OAuth exclusively. Fails with an actionable error if credentials are not configured.
212
- - **`browser`**: Uses CDP exclusively. Fails with an actionable error if the browser session has expired.
213
-
214
- The strategy is persisted in the Vellum config file as `twitter.operationStrategy` and can be changed via `vellum x strategy set <oauth|browser|auto>`.
203
+ - **`managed`**: Routes all API calls through the Vellum platform proxy. The platform holds the OAuth credentials and forwards requests on behalf of the assistant. Supports both write operations (post, reply) and read operations (timeline, tweet detail, search, user lookup). This is the default when the user has a managed assistant.
204
+ - **`oauth`**: Uses locally-stored OAuth2 Bearer tokens to call X API v2 directly. Supports only write operations (post, reply). Read operations throw an error directing the user to use managed mode.
215
205
 
216
206
  #### Twitter OAuth2 Specifics
217
207
 
218
- | Aspect | Detail |
219
- | --------------------- | ------------------------------------------------------------------------------------------------------------------ |
220
- | Auth URL | `https://twitter.com/i/oauth2/authorize` (from provider profile) |
221
- | Token URL | `https://api.x.com/2/oauth2/token` (from provider profile) |
222
- | Flow | PKCE (S256), optional client secret, via connect orchestrator |
223
- | Default scopes | `tweet.read`, `tweet.write`, `users.read`, `offline.access` (from provider profile) |
224
- | Identity verification | Provider profile `identityVerifier` → `GET https://api.x.com/2/users/me` with Bearer token |
225
- | Credential names | `client_id`, `client_secret` |
226
- | HTTP endpoints | `oauth_connect_start` / `oauth_connect_result` (generic), plus legacy `twitter_auth_start` / `twitter_auth_status` |
208
+ | Aspect | Detail |
209
+ | --------------------- | ------------------------------------------------------------------------------------------ |
210
+ | Auth URL | `https://twitter.com/i/oauth2/authorize` (from provider profile) |
211
+ | Token URL | `https://api.x.com/2/oauth2/token` (from provider profile) |
212
+ | Flow | PKCE (S256), optional client secret, via connect orchestrator |
213
+ | Default scopes | `tweet.read`, `tweet.write`, `users.read`, `offline.access` (from provider profile) |
214
+ | Identity verification | Provider profile `identityVerifier` → `GET https://api.x.com/2/users/me` with Bearer token |
215
+ | Credential names | `client_id`, `client_secret` |
216
+ | HTTP endpoints | `oauth_connect_start` / `oauth_connect_result` (generic) |
227
217
 
228
218
  #### Twitter Credential Metadata Structure
229
219
 
@@ -244,78 +234,70 @@ When the OAuth2 flow completes, the handler stores credential metadata at `integ
244
234
 
245
235
  #### Twitter Operation Paths
246
236
 
247
- **OAuth path** (`oauth-client.ts`): The `oauthPostTweet` function calls X API v2 (`POST https://api.x.com/2/tweets`) with a Bearer token obtained via `withValidToken('integration:twitter', ...)`. The token manager handles automatic refresh if the stored token is expired. Supports `post` and `reply` (by including `reply.in_reply_to_tweet_id` in the request body). All other operations (timeline, search, etc.) throw `UnsupportedOAuthOperationError` and are not available via this path.
237
+ **Managed path** (`platform-proxy-client.ts`): Routes API calls through the Vellum platform proxy at `${platformBaseUrl}/api/v1/assistants/${assistantId}/integrations/twitter/proxy/*`. The platform holds the OAuth credentials and forwards requests to X API v2 on behalf of the assistant. Supports all operations: post, reply, user lookup, user tweets, tweet detail, and search. Errors from the proxy surface as `TwitterProxyError` with structured error codes and retryability hints.
248
238
 
249
- **Browser path** (`client.ts`): Connects to Chrome via CDP (`localhost:9222`), finds an authenticated x.com tab, and executes GraphQL mutations/queries through the browser's session cookies. Supports all operations including read-only ones (timeline, search, home, notifications, bookmarks, likes, followers, following, media). Session management is handled by Ride Shotgun recordings (`vellum x refresh`).
239
+ **OAuth path** (`oauth-client.ts`): The `oauthPostTweet` function calls X API v2 (`POST https://api.x.com/2/tweets`) with a Bearer token provided by the caller. Supports `post` and `reply` (by including `reply.in_reply_to_tweet_id` in the request body). Read operations are not supported via this path and will throw an error directing the user to use managed mode.
250
240
 
251
241
  #### Available Twitter Tools
252
242
 
253
- | Tool / Command | Mechanism | Description |
254
- | ------------------------ | ------------------------------ | ------------------------------------------------------------------- |
255
- | `vellum x post` | Strategy router (OAuth or CDP) | Post a tweet. Uses the configured strategy (`auto` by default). |
256
- | `vellum x reply` | Strategy router (OAuth or CDP) | Reply to a tweet. Uses the configured strategy (`auto` by default). |
257
- | `vellum x timeline` | CDP | Fetch a user's recent tweets. Browser path only. |
258
- | `vellum x search` | CDP | Search tweets. Browser path only. |
259
- | `vellum x home` | CDP | Fetch home timeline. Browser path only. |
260
- | `vellum x notifications` | CDP | Fetch notifications. Browser path only. |
261
- | `vellum x bookmarks` | CDP | Fetch bookmarks. Browser path only. |
262
- | `vellum x likes` | CDP | Fetch a user's liked tweets. Browser path only. |
263
- | `vellum x followers` | CDP | Fetch a user's followers. Browser path only. |
264
- | `vellum x following` | CDP | Fetch who a user follows. Browser path only. |
265
- | `vellum x media` | CDP | Fetch a user's media tweets. Browser path only. |
266
- | `vellum x strategy` | Config | Get or set the operation strategy (`oauth`, `browser`, `auto`). |
267
- | `vellum x status` | IPC + local | Check browser session, OAuth connection, and strategy status. |
268
-
269
- Note: OAuth2 scopes (`tweet.read`, `tweet.write`, `users.read`, `offline.access`) are requested during the auth flow. The `post` and `reply` operations use these tokens when the OAuth path is selected. Read operations require the browser path.
243
+ | Tool / Command | Mechanism | Description |
244
+ | ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
245
+ | `assistant x post` | Mode router (OAuth or managed) | Post a tweet. Defaults to OAuth; pass `--managed` to route through the platform proxy. |
246
+ | `assistant x reply` | Mode router (OAuth or managed) | Reply to a tweet. Defaults to OAuth; pass `--managed` to route through the platform proxy. |
247
+ | `assistant x timeline` | Managed only | Fetch a user's recent tweets. Resolves screen name to user ID, then fetches timeline. |
248
+ | `assistant x tweet` | Managed only | Fetch a single tweet and its reply thread via conversation ID search. |
249
+ | `assistant x search` | Managed only | Search tweets. Supports `Top`, `Latest`, `People`, and `Media` product types. |
250
+ | `assistant x status` | HTTP (daemon) | Check OAuth connection and managed mode availability. |
251
+
252
+ Note: Write operations (post, reply) support both OAuth and managed modes. Read operations (timeline, tweet, search) require managed mode because the OAuth path only supports `post` and `reply`.
270
253
 
271
254
  ### Key Design Decisions
272
255
 
273
- | Decision | Rationale |
274
- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
275
- | PKCE by default, optional client_secret | Desktop apps prefer PKCE; some providers (Slack) require a secret, which is stored in credential metadata for autonomous refresh |
276
- | Shared connect orchestrator | All OAuth providers route through `orchestrateOAuthConnect()`, which resolves profiles, enforces scope policy, runs the flow, stores tokens, and verifies identity. Adding a provider is a declarative profile entry, not new orchestration code |
277
- | Canonical credential naming | All reads and writes use `client_id`/`client_secret` as canonical field names |
278
- | Gateway callback transport | OAuth callbacks are now routed through the gateway at `${ingress.publicBaseUrl}/webhooks/oauth/callback` instead of a loopback redirect URI. This enables OAuth flows to work in remote and tunneled deployments. |
279
- | Unified `MessagingProvider` interface | All platforms implement the same contract; generic tools work immediately for new providers |
280
- | Twitter outside unified messaging | Twitter is a broadcast/read platform, not a conversation platform — it doesn't fit the `MessagingProvider` contract |
281
- | Dual-path Twitter strategy | OAuth is more reliable for posting (no browser session dependency) but only supports post/reply. Browser path supports all operations. `auto` strategy gives the best of both: OAuth when possible, browser as fallback. User can override via `vellum x strategy set`. |
282
- | Provider auto-selection | If only one provider is connected, tools skip the `platform` parameter — seamless single-platform UX |
283
- | Token expiry in credential metadata | Reuses existing `CredentialMetadata` store; `expiresAt` field enables proactive refresh with 5min buffer |
284
- | Confidence scores on medium-risk tools | LLM self-reports confidence (0-1); enables future trust calibration without blocking execution |
285
- | Platform-specific extension tools | Operations unique to one platform (e.g. Gmail labels, Slack reactions) are separate tools, not forced into the generic interface |
286
- | Twitter identity verification before token storage | OAuth2 tokens are only persisted after a successful `GET /2/users/me` call, preventing storage of invalid or mismatched credentials |
256
+ | Decision | Rationale |
257
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
+ | PKCE by default, optional client_secret | Desktop apps prefer PKCE; some providers (Slack) require a secret, which is stored in credential metadata for autonomous refresh |
259
+ | Shared connect orchestrator | All OAuth providers route through `orchestrateOAuthConnect()`, which resolves profiles, enforces scope policy, runs the flow, stores tokens, and verifies identity. Adding a provider is a declarative profile entry, not new orchestration code |
260
+ | Canonical credential naming | All reads and writes use `client_id`/`client_secret` as canonical field names |
261
+ | Gateway callback transport | OAuth callbacks are now routed through the gateway at `${ingress.publicBaseUrl}/webhooks/oauth/callback` instead of a loopback redirect URI. This enables OAuth flows to work in remote and tunneled deployments. |
262
+ | Unified `MessagingProvider` interface | All platforms implement the same contract; generic tools work immediately for new providers |
263
+ | Twitter outside unified messaging | Twitter is a broadcast/read platform, not a conversation platform — it doesn't fit the `MessagingProvider` contract |
264
+ | Two-mode Twitter architecture (managed + OAuth) | Managed mode delegates to the platform proxy which holds credentials no local browser or session management needed. OAuth mode provides direct API access for users with their own developer credentials. Read operations require managed mode since OAuth only supports post/reply. |
265
+ | Provider auto-selection | If only one provider is connected, tools skip the `platform` parameter — seamless single-platform UX |
266
+ | Token expiry in credential metadata | Reuses existing `CredentialMetadata` store; `expiresAt` field enables proactive refresh with 5min buffer |
267
+ | Confidence scores on medium-risk tools | LLM self-reports confidence (0-1); enables future trust calibration without blocking execution |
268
+ | Platform-specific extension tools | Operations unique to one platform (e.g. Gmail labels, Slack reactions) are separate tools, not forced into the generic interface |
269
+ | Twitter identity verification before token storage | OAuth2 tokens are only persisted after a successful `GET /2/users/me` call, preventing storage of invalid or mismatched credentials |
287
270
 
288
271
  ### Source Files
289
272
 
290
- | File | Role |
291
- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
292
- | `assistant/src/security/oauth2.ts` | OAuth2 flow: PKCE or client_secret, Bun.serve callback, token exchange |
293
- | `assistant/src/security/token-manager.ts` | `withValidToken()` — auto-refresh, 401 retry, expiry buffer |
294
- | `assistant/src/messaging/provider.ts` | `MessagingProvider` interface |
295
- | `assistant/src/messaging/provider-types.ts` | Platform-agnostic types (Conversation, Message, SearchResult) |
296
- | `assistant/src/messaging/registry.ts` | Provider registry: register, lookup, list connected |
297
- | `assistant/src/messaging/activity-analyzer.ts` | Activity classification for conversations |
298
- | `assistant/src/messaging/style-analyzer.ts` | Writing style extraction from message corpus |
299
- | `assistant/src/messaging/draft-store.ts` | Local draft storage (platform/id JSON files) |
300
- | `assistant/src/messaging/providers/slack/` | Slack adapter, client, types |
301
- | `assistant/src/messaging/providers/gmail/` | Gmail adapter, client, types |
302
- | `assistant/src/config/bundled-skills/messaging/` | Unified messaging skill (SKILL.md, TOOLS.json, tools/) |
303
- | `assistant/src/watcher/providers/gmail.ts` | Gmail watcher using History API |
304
- | `assistant/src/watcher/providers/github.ts` | GitHub watcher for PRs, issues, review requests, and mentions |
305
- | `assistant/src/watcher/providers/linear.ts` | Linear watcher for assigned issues, status changes, and @mentions |
306
- | `assistant/src/oauth/provider-profiles.ts` | Provider profile registry: auth URLs, token URLs, scopes, policies, identity verifiers |
307
- | `assistant/src/oauth/connect-orchestrator.ts` | Shared OAuth connect orchestrator: profile resolution, scope policy, flow execution, token storage |
308
- | `assistant/src/oauth/scope-policy.ts` | Deterministic scope resolution and policy enforcement |
309
- | `assistant/src/oauth/connect-types.ts` | Shared types: `OAuthProviderProfile`, `OAuthScopePolicy`, `OAuthConnectResult` |
310
- | `assistant/src/oauth/token-persistence.ts` | Token storage helper: persists tokens, metadata, and runs post-connect hooks |
311
- | `assistant/src/daemon/handlers/oauth-connect.ts` | Generic OAuth connect handler (`oauth_connect_start` / `oauth_connect_result`) |
312
- | `assistant/src/daemon/handlers/twitter-auth.ts` | Legacy Twitter OAuth2 flow handlers (`twitter_auth_start`, `twitter_auth_status`) |
313
- | `assistant/src/cli/commands/twitter/client.ts` | Twitter CDP client: GraphQL mutations/queries via Chrome DevTools Protocol |
314
- | `assistant/src/cli/commands/twitter/oauth-client.ts` | OAuth-backed Twitter client: X API v2 post/reply via stored tokens using `withValidToken()` |
315
- | `assistant/src/cli/commands/twitter/router.ts` | Strategy router: selects OAuth or browser path based on `twitter.operationStrategy` config |
316
- | `assistant/src/cli/commands/twitter/session.ts` | Twitter browser session persistence (cookie import/export) |
317
- | `assistant/src/cli/commands/twitter/index.ts` | `vellum x` CLI command group (post, reply, strategy, refresh, status, login, logout, and read operations) |
318
- | `assistant/src/config/bundled-skills/twitter/SKILL.md` | X (Twitter) bundled skill instructions |
273
+ | File | Role |
274
+ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
275
+ | `assistant/src/security/oauth2.ts` | OAuth2 flow: PKCE or client_secret, Bun.serve callback, token exchange |
276
+ | `assistant/src/security/token-manager.ts` | `withValidToken()` — auto-refresh, 401 retry, expiry buffer |
277
+ | `assistant/src/messaging/provider.ts` | `MessagingProvider` interface |
278
+ | `assistant/src/messaging/provider-types.ts` | Platform-agnostic types (Conversation, Message, SearchResult) |
279
+ | `assistant/src/messaging/registry.ts` | Provider registry: register, lookup, list connected |
280
+ | `assistant/src/messaging/activity-analyzer.ts` | Activity classification for conversations |
281
+ | `assistant/src/messaging/style-analyzer.ts` | Writing style extraction from message corpus |
282
+ | `assistant/src/messaging/draft-store.ts` | Local draft storage (platform/id JSON files) |
283
+ | `assistant/src/messaging/providers/slack/` | Slack adapter, client, types |
284
+ | `assistant/src/messaging/providers/gmail/` | Gmail adapter, client, types |
285
+ | `assistant/src/config/bundled-skills/messaging/` | Unified messaging skill (SKILL.md, TOOLS.json, tools/) |
286
+ | `assistant/src/watcher/providers/gmail.ts` | Gmail watcher using History API |
287
+ | `assistant/src/watcher/providers/github.ts` | GitHub watcher for PRs, issues, review requests, and mentions |
288
+ | `assistant/src/watcher/providers/linear.ts` | Linear watcher for assigned issues, status changes, and @mentions |
289
+ | `assistant/src/oauth/provider-profiles.ts` | Provider profile registry: auth URLs, token URLs, scopes, policies, identity verifiers |
290
+ | `assistant/src/oauth/connect-orchestrator.ts` | Shared OAuth connect orchestrator: profile resolution, scope policy, flow execution, token storage |
291
+ | `assistant/src/oauth/scope-policy.ts` | Deterministic scope resolution and policy enforcement |
292
+ | `assistant/src/oauth/connect-types.ts` | Shared types: `OAuthProviderProfile`, `OAuthScopePolicy`, `OAuthConnectResult` |
293
+ | `assistant/src/oauth/token-persistence.ts` | Token storage helper: persists tokens, metadata, and runs post-connect hooks |
294
+ | `assistant/src/daemon/handlers/oauth-connect.ts` | Generic OAuth connect handler (`oauth_connect_start` / `oauth_connect_result`) |
295
+ | `assistant/src/cli/commands/twitter/oauth-client.ts` | OAuth-backed Twitter client: X API v2 post/reply via Bearer token |
296
+ | `assistant/src/cli/commands/twitter/router.ts` | Mode router: selects managed or OAuth path based on caller-provided `TwitterMode` |
297
+ | `assistant/src/cli/commands/twitter/types.ts` | Shared types: `PostTweetResult`, `UserInfo`, `TweetEntry`, `NotificationEntry` |
298
+ | `assistant/src/cli/commands/twitter/index.ts` | `assistant x` CLI command group (post, reply, timeline, tweet, search, status) |
299
+ | `assistant/src/twitter/platform-proxy-client.ts` | Platform-managed Twitter proxy client: routes API calls through the Vellum platform |
300
+ | `assistant/src/config/bundled-skills/twitter/SKILL.md` | X (Twitter) bundled skill instructions |
319
301
 
320
302
  ---
321
303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.43",
3
+ "version": "0.4.44",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "assistant": "./src/index.ts"
@@ -112,7 +112,6 @@ function makeIdleSession(opts?: {
112
112
  setCommandIntent: () => {},
113
113
  setTurnChannelContext: () => {},
114
114
  setTurnInterfaceContext: () => {},
115
- setStateSignalListener: () => {},
116
115
  updateClient: () => {},
117
116
  enqueueMessage: () => ({ queued: false, requestId: "noop" }),
118
117
  hasAnyPendingConfirmation: () => false,
@@ -171,7 +170,6 @@ function makeConfirmationEmittingSession(opts?: {
171
170
  setCommandIntent: () => {},
172
171
  setTurnChannelContext: () => {},
173
172
  setTurnInterfaceContext: () => {},
174
- setStateSignalListener: () => {},
175
173
  updateClient: () => {},
176
174
  enqueueMessage: () => ({ queued: false, requestId: "noop" }),
177
175
  hasAnyPendingConfirmation: () => false,
@@ -124,7 +124,7 @@ describe("resolveBundledDir", () => {
124
124
  process.execPath = join(macosDir, "vellum-daemon");
125
125
 
126
126
  const result = resolveBundledDir(
127
- "/$bunfs/root/src/home-base/prebuilt",
127
+ "/$bunfs/root/src/widgets/prebuilt",
128
128
  ".",
129
129
  "prebuilt",
130
130
  );
@@ -637,27 +637,26 @@ describe("Permission Checker", () => {
637
637
  expect(result.decision).toBe("prompt");
638
638
  });
639
639
 
640
- test("host_bash rm is always high risk prompt", async () => {
640
+ test("host_bash rm is always prompted via default ask rule", async () => {
641
641
  const result = await check(
642
642
  "host_bash",
643
643
  { command: "rm file.txt" },
644
644
  "/tmp",
645
645
  );
646
646
  expect(result.decision).toBe("prompt");
647
- expect(result.reason).toContain("High risk");
647
+ expect(result.reason).toContain("ask rule");
648
648
  });
649
649
 
650
- test("plain rm (without -rf) is high risk and prompts despite default allow rule", async () => {
651
- // Validates that ALL rm commands are escalated to High risk, not just rm -rf.
652
- // The default allow rule for host_bash auto-approves Low/Medium risk but
653
- // High risk always prompts.
650
+ test("plain rm (without -rf) prompts via default ask rule", async () => {
651
+ // The default ask rule for host_bash prompts ALL commands regardless
652
+ // of risk level rm commands are no exception.
654
653
  const result = await check(
655
654
  "host_bash",
656
655
  { command: "rm single-file.txt" },
657
656
  "/tmp",
658
657
  );
659
658
  expect(result.decision).toBe("prompt");
660
- expect(result.reason).toContain("High risk");
659
+ expect(result.reason).toContain("ask rule");
661
660
 
662
661
  // Also verify rm -rf still prompts
663
662
  const rfResult = await check(
@@ -666,7 +665,7 @@ describe("Permission Checker", () => {
666
665
  "/tmp",
667
666
  );
668
667
  expect(rfResult.decision).toBe("prompt");
669
- expect(rfResult.reason).toContain("High risk");
668
+ expect(rfResult.reason).toContain("ask rule");
670
669
  });
671
670
 
672
671
  test("rm is high risk even with matching trust rule → prompt", async () => {
@@ -807,11 +806,11 @@ describe("Permission Checker", () => {
807
806
  expect(result.matchedRule?.id).toBe("default:ask-host_file_edit-global");
808
807
  });
809
808
 
810
- test("host_bash auto-allows low risk via default allow rule", async () => {
809
+ test("host_bash prompts low risk via default ask rule", async () => {
811
810
  const result = await check("host_bash", { command: "ls" }, "/tmp");
812
- expect(result.decision).toBe("allow");
813
- expect(result.reason).toContain("Matched trust rule");
814
- expect(result.matchedRule?.id).toBe("default:allow-host_bash-global");
811
+ expect(result.decision).toBe("prompt");
812
+ expect(result.reason).toContain("ask rule");
813
+ expect(result.matchedRule?.id).toBe("default:ask-host_bash-global");
815
814
  });
816
815
 
817
816
  test("scaffold_managed_skill prompts by default via managed skill ask rule", async () => {
@@ -2232,11 +2231,12 @@ describe("Permission Checker", () => {
2232
2231
  expect(result.matchedRule?.id).toBe("default:allow-bash-global");
2233
2232
  });
2234
2233
 
2235
- test("host_bash auto-allows low risk in strict mode (default allow rule is a matching rule)", async () => {
2234
+ test("host_bash prompts low risk in strict mode (default ask rule matches)", async () => {
2236
2235
  testConfig.permissions.mode = "strict";
2237
2236
  const result = await check("host_bash", { command: "ls" }, "/tmp");
2238
- expect(result.decision).toBe("allow");
2239
- expect(result.matchedRule?.id).toBe("default:allow-host_bash-global");
2237
+ expect(result.decision).toBe("prompt");
2238
+ expect(result.reason).toContain("ask rule");
2239
+ expect(result.matchedRule?.id).toBe("default:ask-host_bash-global");
2240
2240
  });
2241
2241
 
2242
2242
  test("high-risk host_bash (rm) with no matching rule returns prompt in strict mode", async () => {
@@ -3570,15 +3570,16 @@ describe("Permission Checker", () => {
3570
3570
  expect(result.matchedRule?.id).toBe("default:allow-bash-global");
3571
3571
  });
3572
3572
 
3573
- test("low-risk host_bash auto-allows in strict mode (default allow rule is a matching rule)", async () => {
3573
+ test("low-risk host_bash prompts in strict mode (default ask rule matches)", async () => {
3574
3574
  testConfig.permissions.mode = "strict";
3575
3575
  const result = await check(
3576
3576
  "host_bash",
3577
3577
  { command: "echo hello" },
3578
3578
  "/tmp",
3579
3579
  );
3580
- expect(result.decision).toBe("allow");
3581
- expect(result.matchedRule?.id).toBe("default:allow-host_bash-global");
3580
+ expect(result.decision).toBe("prompt");
3581
+ expect(result.reason).toContain("ask rule");
3582
+ expect(result.matchedRule?.id).toBe("default:ask-host_bash-global");
3582
3583
  });
3583
3584
 
3584
3585
  test("low-risk file_read with no rule prompts in strict mode", async () => {
@@ -3660,10 +3661,11 @@ describe("Permission Checker", () => {
3660
3661
  // target-scoped. ───────────────────────────────────────────────
3661
3662
 
3662
3663
  describe("Invariant 4: host execution approvals are explicit and target-scoped", () => {
3663
- test("host_bash auto-allows low risk via default allow rule", async () => {
3664
+ test("host_bash prompts low risk via default ask rule", async () => {
3664
3665
  const result = await check("host_bash", { command: "ls" }, "/tmp");
3665
- expect(result.decision).toBe("allow");
3666
- expect(result.matchedRule?.id).toBe("default:allow-host_bash-global");
3666
+ expect(result.decision).toBe("prompt");
3667
+ expect(result.reason).toContain("ask rule");
3668
+ expect(result.matchedRule?.id).toBe("default:ask-host_bash-global");
3667
3669
  });
3668
3670
 
3669
3671
  test("host_file_read prompts by default (no implicit allow)", async () => {
@@ -3740,7 +3742,7 @@ describe("Permission Checker", () => {
3740
3742
  expect(matchResult.matchedRule?.id).toBe("inv4-target-scoped");
3741
3743
 
3742
3744
  // Different target — the target-scoped rule should NOT match;
3743
- // falls back to the default host_bash allow rule (auto-allows medium risk)
3745
+ // falls back to the default host_bash ask rule (prompts)
3744
3746
  const noMatchResult = await check(
3745
3747
  "host_bash",
3746
3748
  { command: "run script.js" },
@@ -3749,8 +3751,9 @@ describe("Permission Checker", () => {
3749
3751
  executionTarget: "/usr/local/bin/bun",
3750
3752
  },
3751
3753
  );
3752
- expect(noMatchResult.decision).toBe("allow");
3753
- expect(noMatchResult.matchedRule?.id).not.toBe("inv4-target-scoped");
3754
+ expect(noMatchResult.decision).toBe("prompt");
3755
+ expect(noMatchResult.reason).toContain("ask rule");
3756
+ expect(noMatchResult.matchedRule?.id).toBe("default:ask-host_bash-global");
3754
3757
  });
3755
3758
  });
3756
3759
 
@@ -4310,7 +4313,7 @@ describe("bash network_mode=proxied — no special-casing", () => {
4310
4313
 
4311
4314
  test("proxied bash follows normal rules (auto-allowed by default rule)", async () => {
4312
4315
  // Proxied bash is no longer force-prompted — the default allow-bash rule
4313
- // auto-allows low/medium risk commands regardless of network_mode.
4316
+ // prompts low/medium risk commands regardless of network_mode.
4314
4317
  const result = await check(
4315
4318
  "bash",
4316
4319
  { command: "curl https://api.example.com", network_mode: "proxied" },
@@ -4722,10 +4725,10 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
4722
4725
  expect(result.reason).toContain("ask rule");
4723
4726
  });
4724
4727
 
4725
- test("host_bash → allow (default allow rule matches)", async () => {
4728
+ test("host_bash → prompt (default ask rule matches)", async () => {
4726
4729
  const result = await check("host_bash", { command: "ls" }, workspaceDir);
4727
- expect(result.decision).toBe("allow");
4728
- expect(result.reason).toContain("Matched trust rule");
4730
+ expect(result.decision).toBe("prompt");
4731
+ expect(result.reason).toContain("ask rule");
4729
4732
  });
4730
4733
 
4731
4734
  // ── explicit rules still take precedence in workspace mode ──