@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic local actor identity for IPC connections.
|
|
3
|
+
*
|
|
4
|
+
* IPC (Unix domain socket) connections come from the local macOS native app.
|
|
5
|
+
* No actor token is sent over the socket; instead, the daemon assigns a
|
|
6
|
+
* deterministic local actor identity server-side by looking up the vellum
|
|
7
|
+
* channel guardian binding.
|
|
8
|
+
*
|
|
9
|
+
* This routes IPC connections through the same `resolveGuardianContext`
|
|
10
|
+
* pathway used by HTTP channel ingress, producing equivalent
|
|
11
|
+
* guardian-context behavior for the vellum channel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChannelId } from '../channels/types.js';
|
|
15
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
16
|
+
import { getActiveBinding } from '../memory/guardian-bindings.js';
|
|
17
|
+
import { getLogger } from '../util/logger.js';
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
19
|
+
import {
|
|
20
|
+
resolveGuardianContext,
|
|
21
|
+
toGuardianRuntimeContext,
|
|
22
|
+
} from './guardian-context-resolver.js';
|
|
23
|
+
|
|
24
|
+
const log = getLogger('local-actor-identity');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the guardian runtime context for a local IPC connection.
|
|
28
|
+
*
|
|
29
|
+
* Looks up the vellum guardian binding to obtain the `guardianPrincipalId`,
|
|
30
|
+
* then passes it as the sender identity through `resolveGuardianContext` --
|
|
31
|
+
* the same pathway HTTP channel routes use. This ensures IPC and HTTP
|
|
32
|
+
* produce equivalent trust classification for the vellum channel.
|
|
33
|
+
*
|
|
34
|
+
* When no vellum guardian binding exists (e.g. fresh install before
|
|
35
|
+
* bootstrap), falls back to a minimal guardian context so the local
|
|
36
|
+
* user is not incorrectly denied.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveLocalIpcGuardianContext(
|
|
39
|
+
sourceChannel: ChannelId = 'vellum',
|
|
40
|
+
): GuardianRuntimeContext {
|
|
41
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
42
|
+
const binding = getActiveBinding(assistantId, 'vellum');
|
|
43
|
+
|
|
44
|
+
if (!binding) {
|
|
45
|
+
// No vellum binding yet (pre-bootstrap). The local user is
|
|
46
|
+
// inherently the guardian of their own machine, so produce a
|
|
47
|
+
// guardian context without a binding match. The trust resolver
|
|
48
|
+
// would classify this as 'unknown' due to no_binding, but for
|
|
49
|
+
// the local IPC case that is incorrect -- the local macOS user
|
|
50
|
+
// is always the guardian.
|
|
51
|
+
log.debug('No vellum guardian binding found; using fallback guardian context for IPC');
|
|
52
|
+
return {
|
|
53
|
+
sourceChannel,
|
|
54
|
+
trustClass: 'guardian',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const guardianPrincipalId = binding.guardianExternalUserId;
|
|
59
|
+
|
|
60
|
+
// Route through the shared trust resolution pipeline using 'vellum'
|
|
61
|
+
// as the channel for binding lookup. The guardianPrincipalId comes
|
|
62
|
+
// from the vellum binding, so the binding lookup must also target
|
|
63
|
+
// 'vellum' — otherwise resolveActorTrust would look up a different
|
|
64
|
+
// channel's binding (e.g. telegram/sms) and the IDs wouldn't match,
|
|
65
|
+
// causing a 'unknown' trust classification.
|
|
66
|
+
const guardianCtx = resolveGuardianContext({
|
|
67
|
+
assistantId,
|
|
68
|
+
sourceChannel: 'vellum',
|
|
69
|
+
externalChatId: 'local',
|
|
70
|
+
senderExternalUserId: guardianPrincipalId,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Overlay the caller's actual sourceChannel onto the resolved context
|
|
74
|
+
// so downstream consumers see the correct channel provenance.
|
|
75
|
+
return toGuardianRuntimeContext(sourceChannel, guardianCtx);
|
|
76
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor-token verification middleware for HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the X-Actor-Token header, verifies the HMAC signature,
|
|
5
|
+
* checks that the token is active in the store, and returns the
|
|
6
|
+
* verified claims and resolved guardian runtime context.
|
|
7
|
+
*
|
|
8
|
+
* Used by vellum-channel HTTP routes (POST /v1/messages, POST /v1/confirm,
|
|
9
|
+
* POST /v1/guardian-actions/decision, etc.) to enforce identity-based
|
|
10
|
+
* authentication.
|
|
11
|
+
*
|
|
12
|
+
* For backward compatibility with bearer-authenticated local clients (CLI),
|
|
13
|
+
* provides fallback functions that resolve identity through the local IPC
|
|
14
|
+
* guardian context pathway when no actor token is present.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
18
|
+
import type { GuardianRuntimeContext } from '../../daemon/session-runtime-assembly.js';
|
|
19
|
+
import { getActiveBinding } from '../../memory/guardian-bindings.js';
|
|
20
|
+
import { getLogger } from '../../util/logger.js';
|
|
21
|
+
import { type ActorTokenClaims, hashToken, verifyActorToken } from '../actor-token-service.js';
|
|
22
|
+
import { findActiveByTokenHash } from '../actor-token-store.js';
|
|
23
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
24
|
+
import {
|
|
25
|
+
resolveGuardianContext,
|
|
26
|
+
toGuardianRuntimeContext,
|
|
27
|
+
} from '../guardian-context-resolver.js';
|
|
28
|
+
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
29
|
+
|
|
30
|
+
const log = getLogger('actor-token-middleware');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface ActorTokenResult {
|
|
37
|
+
ok: true;
|
|
38
|
+
claims: ActorTokenClaims;
|
|
39
|
+
guardianContext: GuardianRuntimeContext;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActorTokenError {
|
|
43
|
+
ok: false;
|
|
44
|
+
status: number;
|
|
45
|
+
message: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ActorTokenVerification = ActorTokenResult | ActorTokenError;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Header extraction
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const ACTOR_TOKEN_HEADER = 'x-actor-token';
|
|
55
|
+
|
|
56
|
+
export function extractActorToken(req: Request): string | null {
|
|
57
|
+
return req.headers.get(ACTOR_TOKEN_HEADER) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Full verification pipeline
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verify the X-Actor-Token header and resolve a guardian runtime context.
|
|
66
|
+
*
|
|
67
|
+
* Steps:
|
|
68
|
+
* 1. Extract the header value.
|
|
69
|
+
* 2. Verify HMAC signature and expiration.
|
|
70
|
+
* 3. Check the token hash is active in the actor-token store.
|
|
71
|
+
* 4. Resolve a guardian context through the standard trust pipeline using
|
|
72
|
+
* the claims' guardianPrincipalId as the sender identity.
|
|
73
|
+
*
|
|
74
|
+
* Returns an ok result with claims and guardianContext, or an error with
|
|
75
|
+
* the HTTP status code and message to return.
|
|
76
|
+
*/
|
|
77
|
+
export function verifyHttpActorToken(req: Request): ActorTokenVerification {
|
|
78
|
+
const rawToken = extractActorToken(req);
|
|
79
|
+
if (!rawToken) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
status: 401,
|
|
83
|
+
message: 'Missing X-Actor-Token header. Vellum HTTP requests require actor identity.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Structural + signature verification
|
|
88
|
+
const verifyResult = verifyActorToken(rawToken);
|
|
89
|
+
if (!verifyResult.ok) {
|
|
90
|
+
log.warn({ reason: verifyResult.reason }, 'Actor token verification failed');
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: 401,
|
|
94
|
+
message: `Invalid actor token: ${verifyResult.reason}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check the token is active in the store (not revoked)
|
|
99
|
+
const tokenHash = hashToken(rawToken);
|
|
100
|
+
const record = findActiveByTokenHash(tokenHash);
|
|
101
|
+
if (!record) {
|
|
102
|
+
log.warn('Actor token not found in active store (possibly revoked)');
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 401,
|
|
106
|
+
message: 'Actor token is no longer active',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { claims } = verifyResult;
|
|
111
|
+
|
|
112
|
+
// Resolve guardian context through the shared trust pipeline.
|
|
113
|
+
// The guardianPrincipalId from the token is used as the sender identity,
|
|
114
|
+
// and 'vellum' is used as the channel for binding lookup.
|
|
115
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
116
|
+
const guardianCtx = resolveGuardianContext({
|
|
117
|
+
assistantId,
|
|
118
|
+
sourceChannel: 'vellum',
|
|
119
|
+
externalChatId: 'local',
|
|
120
|
+
senderExternalUserId: claims.guardianPrincipalId,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const guardianContext = toGuardianRuntimeContext('vellum' as ChannelId, guardianCtx);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
claims,
|
|
128
|
+
guardianContext,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify that the actor identity from a verified token matches the bound
|
|
134
|
+
* guardian for the vellum channel. Used for guardian-decision endpoints
|
|
135
|
+
* where only the guardian should be able to approve/reject.
|
|
136
|
+
*
|
|
137
|
+
* Returns true if the actor is the bound guardian, false otherwise.
|
|
138
|
+
*/
|
|
139
|
+
export function isActorBoundGuardian(claims: ActorTokenClaims): boolean {
|
|
140
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
141
|
+
const binding = getActiveBinding(assistantId, 'vellum');
|
|
142
|
+
if (!binding) return false;
|
|
143
|
+
return binding.guardianExternalUserId === claims.guardianPrincipalId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Bearer-auth fallback variants
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** Loopback addresses — used to gate the local identity fallback. */
|
|
151
|
+
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
152
|
+
|
|
153
|
+
/** Bun server shape needed for requestIP — avoids importing the full Bun type. */
|
|
154
|
+
export type ServerWithRequestIP = {
|
|
155
|
+
requestIP(req: Request): { address: string; family: string; port: number } | null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Result for the fallback verification path where the actor token is absent
|
|
160
|
+
* but the request is bearer-authenticated (local trusted client like CLI).
|
|
161
|
+
*/
|
|
162
|
+
export interface ActorTokenLocalFallbackResult {
|
|
163
|
+
ok: true;
|
|
164
|
+
claims: null;
|
|
165
|
+
guardianContext: GuardianRuntimeContext;
|
|
166
|
+
localFallback: true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ActorTokenVerificationWithFallback =
|
|
170
|
+
| ActorTokenResult
|
|
171
|
+
| ActorTokenLocalFallbackResult
|
|
172
|
+
| ActorTokenError;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Verify the actor token with fallback to local IPC identity resolution.
|
|
176
|
+
*
|
|
177
|
+
* When an actor token is present, the full verification pipeline runs.
|
|
178
|
+
* When absent AND the request originates from a loopback address, the
|
|
179
|
+
* request is treated as a trusted local client (e.g. CLI) and we fall
|
|
180
|
+
* back to `resolveLocalIpcGuardianContext()` which produces the same
|
|
181
|
+
* guardian context as the IPC pathway.
|
|
182
|
+
*
|
|
183
|
+
* Two conditions must BOTH be met for the local fallback:
|
|
184
|
+
* 1. No X-Forwarded-For header (rules out gateway-proxied requests).
|
|
185
|
+
* 2. The peer remote address is a loopback address (rules out LAN/container
|
|
186
|
+
* connections when the runtime binds to 0.0.0.0).
|
|
187
|
+
*
|
|
188
|
+
* The peer address is checked via `server.requestIP(req)`.
|
|
189
|
+
*
|
|
190
|
+
* --- CLI compatibility note ---
|
|
191
|
+
*
|
|
192
|
+
* The local fallback is an intentional CLI compatibility path, not a
|
|
193
|
+
* security gap. The CLI currently sends only `Authorization: Bearer <token>`
|
|
194
|
+
* without `X-Actor-Token`. This fallback allows the CLI to function until
|
|
195
|
+
* it is migrated to actor tokens in a future milestone.
|
|
196
|
+
*
|
|
197
|
+
* The fallback is gated by three conditions that together ensure only
|
|
198
|
+
* genuinely local connections receive guardian identity:
|
|
199
|
+
* (1) Absence of X-Forwarded-For header — the gateway always injects
|
|
200
|
+
* this header when proxying, so its presence indicates a remote client.
|
|
201
|
+
* (2) Loopback origin check — verifies the peer IP is 127.0.0.1/::1,
|
|
202
|
+
* preventing LAN or container peers.
|
|
203
|
+
* (3) Valid bearer authentication — already enforced upstream by the
|
|
204
|
+
* HTTP server's auth gate before this function is called.
|
|
205
|
+
*
|
|
206
|
+
* Once the CLI adopts actor tokens, this fallback can be removed.
|
|
207
|
+
*/
|
|
208
|
+
export function verifyHttpActorTokenWithLocalFallback(
|
|
209
|
+
req: Request,
|
|
210
|
+
server: ServerWithRequestIP,
|
|
211
|
+
): ActorTokenVerificationWithFallback {
|
|
212
|
+
const rawToken = extractActorToken(req);
|
|
213
|
+
|
|
214
|
+
// If an actor token is present, use the strict verification pipeline.
|
|
215
|
+
if (rawToken) {
|
|
216
|
+
return verifyHttpActorToken(req);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Gate the local fallback on provably-local origin. The gateway runtime
|
|
220
|
+
// proxy always injects X-Forwarded-For with the real client IP when
|
|
221
|
+
// forwarding requests. Direct local connections (CLI, macOS app) never
|
|
222
|
+
// set this header. If X-Forwarded-For is present, the request was
|
|
223
|
+
// proxied through the gateway on behalf of a potentially remote client
|
|
224
|
+
// and must not receive local guardian identity.
|
|
225
|
+
if (req.headers.get('x-forwarded-for')) {
|
|
226
|
+
log.warn('Rejecting local identity fallback: request has X-Forwarded-For (proxied through gateway)');
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 401,
|
|
230
|
+
message: 'Missing X-Actor-Token header. Proxied requests require actor identity.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Verify the peer address is actually loopback. This prevents LAN or
|
|
235
|
+
// container peers from receiving local guardian identity when the
|
|
236
|
+
// runtime binds to 0.0.0.0.
|
|
237
|
+
const peerIp = server.requestIP(req)?.address;
|
|
238
|
+
if (!peerIp || !LOOPBACK_ADDRESSES.has(peerIp)) {
|
|
239
|
+
log.warn({ peerIp }, 'Rejecting local identity fallback: peer is not loopback');
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
status: 401,
|
|
243
|
+
message: 'Missing X-Actor-Token header. Non-loopback requests require actor identity.',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// No actor token, no forwarding header, and the peer is on loopback
|
|
248
|
+
// — this is a direct local connection that passed bearer auth at the
|
|
249
|
+
// HTTP server layer. Resolve identity the same way as IPC.
|
|
250
|
+
log.debug('No actor token present on direct local request; using local IPC identity fallback');
|
|
251
|
+
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
claims: null,
|
|
255
|
+
guardianContext,
|
|
256
|
+
localFallback: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check whether the local fallback identity is the bound guardian.
|
|
262
|
+
*
|
|
263
|
+
* When no actor token is present (local fallback), the local user is
|
|
264
|
+
* treated as the guardian of their own machine — equivalent to IPC.
|
|
265
|
+
* This returns true when either the resolved trust class is 'guardian'
|
|
266
|
+
* or no vellum binding exists yet (pre-bootstrap).
|
|
267
|
+
*/
|
|
268
|
+
export function isLocalFallbackBoundGuardian(): boolean {
|
|
269
|
+
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
270
|
+
return guardianContext.trustClass === 'guardian';
|
|
271
|
+
}
|
|
@@ -12,12 +12,10 @@ import { httpError } from '../http-errors.js';
|
|
|
12
12
|
const log = getLogger('runtime-http');
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Regex to extract the Twilio webhook subpath
|
|
16
|
-
* assistant-scoped route shapes:
|
|
15
|
+
* Regex to extract the Twilio webhook subpath:
|
|
17
16
|
* /v1/calls/twilio/<subpath>
|
|
18
|
-
* /v1/assistants/<id>/calls/twilio/<subpath>
|
|
19
17
|
*/
|
|
20
|
-
export const TWILIO_WEBHOOK_RE = /^\/v1\/
|
|
18
|
+
export const TWILIO_WEBHOOK_RE = /^\/v1\/calls\/twilio\/(.+)$/;
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* Gateway-compatible Twilio webhook paths:
|
|
@@ -3,20 +3,77 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These endpoints resolve pending confirmations, secrets, and trust rules
|
|
5
5
|
* by requestId — orthogonal to message sending.
|
|
6
|
+
*
|
|
7
|
+
* All approval endpoints require a valid actor token via the X-Actor-Token
|
|
8
|
+
* header (with local CLI fallback). Guardian decisions additionally verify
|
|
9
|
+
* that the actor is the bound guardian.
|
|
6
10
|
*/
|
|
7
11
|
import { getConversationByKey } from '../../memory/conversation-key-store.js';
|
|
8
12
|
import { addRule } from '../../permissions/trust-store.js';
|
|
9
13
|
import { getTool } from '../../tools/registry.js';
|
|
10
14
|
import { getLogger } from '../../util/logger.js';
|
|
11
15
|
import { httpError } from '../http-errors.js';
|
|
16
|
+
import {
|
|
17
|
+
isActorBoundGuardian,
|
|
18
|
+
isLocalFallbackBoundGuardian,
|
|
19
|
+
type ServerWithRequestIP,
|
|
20
|
+
verifyHttpActorTokenWithLocalFallback,
|
|
21
|
+
} from '../middleware/actor-token.js';
|
|
12
22
|
import * as pendingInteractions from '../pending-interactions.js';
|
|
13
23
|
|
|
14
24
|
const log = getLogger('approval-routes');
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Verify the actor token from the request with local fallback.
|
|
28
|
+
* Returns an error Response if verification fails, or null if
|
|
29
|
+
* the actor is authenticated (via actor token or local identity).
|
|
30
|
+
*/
|
|
31
|
+
function requireActorToken(req: Request, server: ServerWithRequestIP): Response | null {
|
|
32
|
+
const result = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
return httpError(
|
|
35
|
+
result.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
36
|
+
result.message,
|
|
37
|
+
result.status,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verify the actor token and confirm the actor is the bound guardian.
|
|
45
|
+
* When no actor token is present (bearer-authenticated local client),
|
|
46
|
+
* falls back to local IPC identity resolution and checks the local
|
|
47
|
+
* identity is the bound guardian.
|
|
48
|
+
*/
|
|
49
|
+
function requireBoundGuardian(req: Request, server: ServerWithRequestIP): Response | null {
|
|
50
|
+
const result = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
return httpError(
|
|
53
|
+
result.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
54
|
+
result.message,
|
|
55
|
+
result.status,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
// For actor-token-authenticated requests, check the token's identity.
|
|
59
|
+
// For local fallback (bearer-auth only), check the local identity.
|
|
60
|
+
const isBoundGuardian = result.claims
|
|
61
|
+
? isActorBoundGuardian(result.claims)
|
|
62
|
+
: isLocalFallbackBoundGuardian();
|
|
63
|
+
if (!isBoundGuardian) {
|
|
64
|
+
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
16
69
|
/**
|
|
17
70
|
* POST /v1/confirm — resolve a pending confirmation by requestId.
|
|
71
|
+
* Requires a valid actor token (guardian-bound).
|
|
18
72
|
*/
|
|
19
|
-
export async function handleConfirm(req: Request): Promise<Response> {
|
|
73
|
+
export async function handleConfirm(req: Request, server: ServerWithRequestIP): Promise<Response> {
|
|
74
|
+
const authError = requireBoundGuardian(req, server);
|
|
75
|
+
if (authError) return authError;
|
|
76
|
+
|
|
20
77
|
const body = await req.json() as {
|
|
21
78
|
requestId?: string;
|
|
22
79
|
decision?: string;
|
|
@@ -37,14 +94,20 @@ export async function handleConfirm(req: Request): Promise<Response> {
|
|
|
37
94
|
return httpError('NOT_FOUND', 'No pending interaction found for this requestId', 404);
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
interaction.session.handleConfirmationResponse(requestId, decision
|
|
97
|
+
interaction.session.handleConfirmationResponse(requestId, decision, undefined, undefined, undefined, {
|
|
98
|
+
source: 'button',
|
|
99
|
+
});
|
|
41
100
|
return Response.json({ accepted: true });
|
|
42
101
|
}
|
|
43
102
|
|
|
44
103
|
/**
|
|
45
104
|
* POST /v1/secret — resolve a pending secret request by requestId.
|
|
105
|
+
* Requires a valid actor token (guardian-bound).
|
|
46
106
|
*/
|
|
47
|
-
export async function handleSecret(req: Request): Promise<Response> {
|
|
107
|
+
export async function handleSecret(req: Request, server: ServerWithRequestIP): Promise<Response> {
|
|
108
|
+
const authError = requireBoundGuardian(req, server);
|
|
109
|
+
if (authError) return authError;
|
|
110
|
+
|
|
48
111
|
const body = await req.json() as {
|
|
49
112
|
requestId?: string;
|
|
50
113
|
value?: string;
|
|
@@ -76,13 +139,17 @@ export async function handleSecret(req: Request): Promise<Response> {
|
|
|
76
139
|
|
|
77
140
|
/**
|
|
78
141
|
* POST /v1/trust-rules — add a trust rule for a pending confirmation.
|
|
142
|
+
* Requires a valid actor token (guardian-bound).
|
|
79
143
|
*
|
|
80
144
|
* Does NOT resolve the confirmation itself (the client still needs to
|
|
81
145
|
* POST /v1/confirm to approve/deny). Validates the pattern and scope
|
|
82
146
|
* against the server-provided allowlist options from the original
|
|
83
147
|
* confirmation_request.
|
|
84
148
|
*/
|
|
85
|
-
export async function handleTrustRule(req: Request): Promise<Response> {
|
|
149
|
+
export async function handleTrustRule(req: Request, server: ServerWithRequestIP): Promise<Response> {
|
|
150
|
+
const authError = requireBoundGuardian(req, server);
|
|
151
|
+
if (authError) return authError;
|
|
152
|
+
|
|
86
153
|
const body = await req.json() as {
|
|
87
154
|
requestId?: string;
|
|
88
155
|
pattern?: string;
|
|
@@ -130,9 +197,14 @@ export async function handleTrustRule(req: Request): Promise<Response> {
|
|
|
130
197
|
return httpError('FORBIDDEN', 'pattern does not match any server-provided allowlist option', 403);
|
|
131
198
|
}
|
|
132
199
|
|
|
133
|
-
// Validate scope against server-provided scope options
|
|
200
|
+
// Validate scope against server-provided scope options.
|
|
201
|
+
// Non-scoped tools have empty scopeOptions — only "everywhere" is valid for them.
|
|
134
202
|
const validScopes = (confirmation.scopeOptions ?? []).map((o) => o.scope);
|
|
135
|
-
if (
|
|
203
|
+
if (validScopes.length === 0) {
|
|
204
|
+
if (scope !== 'everywhere') {
|
|
205
|
+
return httpError('FORBIDDEN', 'non-scoped tools only accept scope "everywhere"', 403);
|
|
206
|
+
}
|
|
207
|
+
} else if (!validScopes.includes(scope)) {
|
|
136
208
|
return httpError('FORBIDDEN', 'scope does not match any server-provided scope option', 403);
|
|
137
209
|
}
|
|
138
210
|
|
|
@@ -155,12 +227,15 @@ export async function handleTrustRule(req: Request): Promise<Response> {
|
|
|
155
227
|
|
|
156
228
|
/**
|
|
157
229
|
* GET /v1/pending-interactions?conversationKey=...
|
|
230
|
+
* Requires a valid actor token.
|
|
158
231
|
*
|
|
159
232
|
* Returns pending confirmations and secrets for a conversation, allowing
|
|
160
233
|
* polling-based clients (like the CLI) to discover approval requests
|
|
161
234
|
* without SSE.
|
|
162
235
|
*/
|
|
163
|
-
export function handleListPendingInteractions(url: URL): Response {
|
|
236
|
+
export function handleListPendingInteractions(url: URL, req: Request, server: ServerWithRequestIP): Response {
|
|
237
|
+
const authError = requireActorToken(req, server);
|
|
238
|
+
if (authError) return authError;
|
|
164
239
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
165
240
|
const conversationId = url.searchParams.get('conversationId');
|
|
166
241
|
|