@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.
- package/ARCHITECTURE.md +13 -14
- package/README.md +11 -12
- package/docs/architecture/integrations.md +75 -93
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -2
- package/src/__tests__/bundled-asset.test.ts +1 -1
- package/src/__tests__/checker.test.ts +31 -28
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
- package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
- package/src/__tests__/onboarding-template-contract.test.ts +0 -10
- package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
- package/src/__tests__/send-endpoint-busy.test.ts +0 -3
- package/src/__tests__/session-confirmation-signals.test.ts +7 -45
- package/src/__tests__/starter-task-flow.test.ts +9 -19
- package/src/__tests__/system-prompt.test.ts +3 -4
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
- package/src/cli/commands/amazon/index.ts +4 -39
- package/src/cli/commands/amazon/session.ts +18 -26
- package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
- package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
- package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
- package/src/cli/commands/twitter/index.ts +95 -835
- package/src/cli/commands/twitter/oauth-client.ts +1 -35
- package/src/cli/commands/twitter/router.ts +70 -115
- package/src/cli/commands/twitter/types.ts +30 -0
- package/src/cli/reference.ts +2 -2
- package/src/config/bundled-skills/amazon/SKILL.md +0 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
- package/src/config/bundled-skills/doordash/SKILL.md +0 -1
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
- package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
- package/src/config/bundled-skills/twitter/SKILL.md +53 -166
- package/src/config/feature-flag-registry.json +8 -0
- package/src/daemon/handlers/session-history.ts +41 -9
- package/src/daemon/lifecycle.ts +4 -17
- package/src/daemon/message-types/apps.ts +0 -25
- package/src/daemon/message-types/integrations.ts +1 -7
- package/src/daemon/message-types/sessions.ts +6 -1
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +33 -1
- package/src/daemon/seed-files.ts +3 -27
- package/src/daemon/server.ts +2 -18
- package/src/daemon/session-agent-loop-handlers.ts +24 -2
- package/src/daemon/session-runtime-assembly.ts +0 -7
- package/src/daemon/session-surfaces.ts +185 -33
- package/src/daemon/session.ts +2 -28
- package/src/memory/app-store.ts +0 -18
- package/src/memory/schema/infrastructure.ts +0 -8
- package/src/permissions/defaults.ts +3 -3
- package/src/prompts/system-prompt.ts +4 -5
- package/src/prompts/templates/BOOTSTRAP.md +0 -3
- package/src/providers/registry.ts +2 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/auth/scopes.ts +1 -0
- package/src/runtime/auth/token-service.ts +1 -1
- package/src/runtime/http-types.ts +10 -0
- package/src/runtime/middleware/error-handler.ts +14 -1
- package/src/runtime/routes/app-management-routes.ts +61 -64
- package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
- package/src/runtime/routes/brain-graph-routes.ts +4 -42
- package/src/runtime/routes/conversation-routes.ts +9 -6
- package/src/runtime/routes/diagnostics-routes.ts +91 -14
- package/src/runtime/routes/settings-routes.ts +3 -93
- package/src/tools/AGENTS.md +38 -0
- package/src/tools/apps/executors.ts +0 -6
- package/src/tools/document/editor-template.ts +10 -8
- package/src/twitter/platform-proxy-client.ts +6 -3
- package/src/util/errors.ts +12 -0
- package/src/__tests__/home-base-bootstrap.test.ts +0 -84
- package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
- package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
- package/src/cli/commands/twitter/client.ts +0 -989
- package/src/cli/commands/twitter/session.ts +0 -121
- package/src/home-base/app-link-store.ts +0 -78
- package/src/home-base/bootstrap.ts +0 -74
- package/src/home-base/prebuilt/brain-graph.html +0 -1483
- package/src/home-base/prebuilt/index.html +0 -702
- package/src/home-base/prebuilt/seed-metadata.json +0 -21
- package/src/home-base/prebuilt/seed.ts +0 -122
- package/src/home-base/prebuilt-home-base-updater.ts +0 -36
- 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
|
|
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
|
|
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
|
-
-
|
|
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 (
|
|
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
|
|
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/>
|
|
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
|
|
40
|
-
|
|
|
41
|
-
| `ANTHROPIC_API_KEY`
|
|
42
|
-
| `OPENAI_API_KEY`
|
|
43
|
-
| `GEMINI_API_KEY`
|
|
44
|
-
| `OLLAMA_API_KEY`
|
|
45
|
-
| `OLLAMA_BASE_URL`
|
|
46
|
-
| `RUNTIME_HTTP_PORT`
|
|
47
|
-
| `RUNTIME_HTTP_HOST`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
####
|
|
187
|
+
#### Two-Mode Operation Architecture
|
|
188
188
|
|
|
189
|
-
The
|
|
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["
|
|
194
|
-
Router -->
|
|
193
|
+
CLI["assistant x post / reply / timeline / search"] --> Router["Mode Router (router.ts)"]
|
|
194
|
+
Router --> ModeCheck{Integration mode?}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
ModeCheck -->|managed| ManagedPath["Platform Proxy Client (platform-proxy-client.ts)"]
|
|
197
|
+
ManagedPath --> PlatformAPI["Platform → X API v2"]
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
- **`
|
|
211
|
-
- **`oauth`**: Uses
|
|
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)
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
254
|
-
|
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
|
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/
|
|
313
|
-
| `assistant/src/cli/commands/twitter/
|
|
314
|
-
| `assistant/src/cli/commands/twitter/
|
|
315
|
-
| `assistant/src/cli/commands/twitter/
|
|
316
|
-
| `assistant/src/
|
|
317
|
-
| `assistant/src/
|
|
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
|
@@ -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,
|
|
@@ -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
|
|
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("
|
|
647
|
+
expect(result.reason).toContain("ask rule");
|
|
648
648
|
});
|
|
649
649
|
|
|
650
|
-
test("plain rm (without -rf)
|
|
651
|
-
//
|
|
652
|
-
//
|
|
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("
|
|
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("
|
|
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
|
|
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("
|
|
813
|
-
expect(result.reason).toContain("
|
|
814
|
-
expect(result.matchedRule?.id).toBe("default:
|
|
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
|
|
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("
|
|
2239
|
-
expect(result.
|
|
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
|
|
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("
|
|
3581
|
-
expect(result.
|
|
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
|
|
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("
|
|
3666
|
-
expect(result.
|
|
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
|
|
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("
|
|
3753
|
-
expect(noMatchResult.
|
|
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
|
-
//
|
|
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 →
|
|
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("
|
|
4728
|
-
expect(result.reason).toContain("
|
|
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 ──
|