@vellumai/assistant 0.4.13 → 0.4.15
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 +77 -38
- package/README.md +10 -12
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +108 -522
- package/src/__tests__/channel-approval-routes.test.ts +92 -239
- package/src/__tests__/channel-approval.test.ts +100 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +13 -6
- package/src/__tests__/conversation-routes.test.ts +11 -4
- package/src/__tests__/guardian-actions-endpoint.test.ts +26 -19
- package/src/__tests__/mcp-health-check.test.ts +65 -0
- package/src/__tests__/permission-types.test.ts +33 -0
- package/src/__tests__/scan-result-store.test.ts +121 -0
- package/src/__tests__/session-agent-loop.test.ts +120 -0
- package/src/__tests__/session-approval-overrides.test.ts +205 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +38 -0
- package/src/amazon/client.ts +8 -5
- package/src/approvals/guardian-decision-primitive.ts +14 -9
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/call-controller.ts +2 -2
- package/src/calls/twilio-routes.ts +2 -2
- package/src/cli/mcp.ts +3 -3
- package/src/cli.ts +24 -0
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +19 -130
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +8 -6
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +49 -14
- package/src/config/bundled-skills/messaging/TOOLS.json +52 -9
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +35 -11
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +3 -1
- package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +5 -6
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +10 -2
- package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +20 -0
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +3 -4
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +10 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +11 -3
- package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +86 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/skills-catalog/SKILL.md +31 -8
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +79 -24
- package/src/config/bundled-skills/sms-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/approval-generators.ts +6 -3
- package/src/daemon/handlers/config-ingress.ts +2 -6
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/sessions.ts +4 -1
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +32 -0
- package/src/daemon/ipc-contract/messages.ts +3 -1
- package/src/daemon/ipc-handler.ts +24 -0
- package/src/daemon/ipc-validate.ts +1 -1
- package/src/daemon/lifecycle.ts +6 -8
- package/src/daemon/server.ts +8 -3
- package/src/daemon/session-agent-loop.ts +19 -1
- package/src/daemon/session-attachments.ts +2 -1
- package/src/daemon/session-history.ts +2 -2
- package/src/daemon/session-process.ts +5 -9
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session-tool-setup.ts +216 -69
- package/src/daemon/session.ts +24 -1
- package/src/events/domain-events.ts +1 -1
- package/src/events/tool-domain-event-publisher.ts +5 -10
- package/src/influencer/client.ts +8 -7
- package/src/messaging/providers/gmail/client.ts +33 -1
- package/src/messaging/providers/gmail/mime-builder.ts +5 -1
- package/src/messaging/providers/sms/adapter.ts +3 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +3 -7
- package/src/messaging/providers/whatsapp/adapter.ts +3 -7
- package/src/notifications/adapters/sms.ts +2 -2
- package/src/notifications/adapters/telegram.ts +2 -2
- package/src/permissions/prompter.ts +2 -0
- package/src/permissions/types.ts +11 -1
- package/src/runtime/approval-conversation-turn.ts +4 -0
- package/src/runtime/auth/__tests__/context.test.ts +130 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +277 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +289 -0
- package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +71 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +239 -0
- package/src/runtime/auth/__tests__/policy.test.ts +29 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +166 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +109 -0
- package/src/runtime/auth/__tests__/subject.test.ts +149 -0
- package/src/runtime/auth/__tests__/token-service.test.ts +263 -0
- package/src/runtime/auth/context.ts +62 -0
- package/src/runtime/{actor-refresh-token-service.ts → auth/credential-service.ts} +112 -79
- package/src/runtime/auth/external-assistant-id.ts +69 -0
- package/src/runtime/auth/index.ts +37 -0
- package/src/runtime/auth/middleware.ts +127 -0
- package/src/runtime/auth/policy.ts +17 -0
- package/src/runtime/auth/route-policy.ts +261 -0
- package/src/runtime/auth/scopes.ts +64 -0
- package/src/runtime/auth/subject.ts +68 -0
- package/src/runtime/auth/token-service.ts +275 -0
- package/src/runtime/auth/types.ts +79 -0
- package/src/runtime/channel-approval-parser.ts +11 -5
- package/src/runtime/channel-approval-types.ts +1 -1
- package/src/runtime/channel-approvals.ts +22 -1
- package/src/runtime/guardian-action-followup-executor.ts +2 -2
- package/src/runtime/guardian-context-resolver.ts +15 -0
- package/src/runtime/guardian-decision-types.ts +23 -6
- package/src/runtime/guardian-outbound-actions.ts +4 -22
- package/src/runtime/guardian-reply-router.ts +5 -3
- package/src/runtime/http-server.ts +210 -182
- package/src/runtime/http-types.ts +11 -1
- package/src/runtime/local-actor-identity.ts +25 -0
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/approval-routes.ts +42 -59
- package/src/runtime/routes/channel-route-shared.ts +9 -41
- package/src/runtime/routes/channel-routes.ts +0 -2
- package/src/runtime/routes/conversation-routes.ts +39 -49
- package/src/runtime/routes/events-routes.ts +15 -22
- package/src/runtime/routes/guardian-action-routes.ts +46 -51
- package/src/runtime/routes/guardian-approval-interception.ts +6 -5
- package/src/runtime/routes/guardian-bootstrap-routes.ts +12 -8
- package/src/runtime/routes/guardian-refresh-routes.ts +2 -2
- package/src/runtime/routes/inbound-message-handler.ts +39 -45
- package/src/runtime/routes/pairing-routes.ts +9 -9
- package/src/runtime/routes/secret-routes.ts +90 -45
- package/src/runtime/routes/surface-action-routes.ts +12 -2
- package/src/runtime/routes/trust-rules-routes.ts +13 -0
- package/src/runtime/routes/twilio-routes.ts +3 -3
- package/src/runtime/session-approval-overrides.ts +86 -0
- package/src/security/keychain-to-encrypted-migration.ts +8 -1
- package/src/skills/frontmatter.ts +44 -1
- package/src/tools/permission-checker.ts +226 -74
- package/src/runtime/actor-token-service.ts +0 -234
- package/src/runtime/middleware/actor-token.ts +0 -265
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Actor-token mint/verify service.
|
|
3
|
-
*
|
|
4
|
-
* Mints HMAC-signed actor tokens bound to (assistantId, guardianPrincipalId,
|
|
5
|
-
* deviceId, platform). Only the SHA-256 hash of the token is persisted —
|
|
6
|
-
* the raw plaintext is returned to the caller once and never stored.
|
|
7
|
-
*
|
|
8
|
-
* Token format: base64url(JSON claims) + '.' + base64url(HMAC-SHA256 signature)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
12
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { dirname, join } from 'node:path';
|
|
14
|
-
|
|
15
|
-
import { getLogger } from '../util/logger.js';
|
|
16
|
-
import { getRootDir } from '../util/platform.js';
|
|
17
|
-
|
|
18
|
-
const log = getLogger('actor-token-service');
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Types
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export interface ActorTokenClaims {
|
|
25
|
-
/** The assistant this token is scoped to. */
|
|
26
|
-
assistantId: string;
|
|
27
|
-
/** Platform: 'macos' | 'ios' */
|
|
28
|
-
platform: string;
|
|
29
|
-
/** Opaque device identifier (hashed for storage). */
|
|
30
|
-
deviceId: string;
|
|
31
|
-
/** The guardian principal this token is bound to. */
|
|
32
|
-
guardianPrincipalId: string;
|
|
33
|
-
/** Token issuance timestamp (epoch ms). */
|
|
34
|
-
iat: number;
|
|
35
|
-
/** Token expiration timestamp (epoch ms). Null means non-expiring. */
|
|
36
|
-
exp: number | null;
|
|
37
|
-
/** Random jti (JWT ID) for uniqueness. */
|
|
38
|
-
jti: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface MintResult {
|
|
42
|
-
/** The raw actor token string — returned once, never persisted. */
|
|
43
|
-
token: string;
|
|
44
|
-
/** SHA-256 hex hash of the token (for storage/lookup). */
|
|
45
|
-
tokenHash: string;
|
|
46
|
-
/** The decoded claims. */
|
|
47
|
-
claims: ActorTokenClaims;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type VerifyResult =
|
|
51
|
-
| { ok: true; claims: ActorTokenClaims }
|
|
52
|
-
| { ok: false; reason: string };
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Signing key management
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
let signingKey: Buffer | null = null;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Path to the persisted signing key file.
|
|
62
|
-
* Stored in the protected directory alongside other sensitive material.
|
|
63
|
-
*/
|
|
64
|
-
function getSigningKeyPath(): string {
|
|
65
|
-
return join(getRootDir(), 'protected', 'actor-token-signing-key');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Load a signing key from disk or generate and persist a new one.
|
|
70
|
-
* Uses the same atomic-write + chmod 0o600 pattern as approved-devices-store.ts.
|
|
71
|
-
*/
|
|
72
|
-
export function loadOrCreateSigningKey(): Buffer {
|
|
73
|
-
const keyPath = getSigningKeyPath();
|
|
74
|
-
|
|
75
|
-
// Try to load existing key
|
|
76
|
-
if (existsSync(keyPath)) {
|
|
77
|
-
try {
|
|
78
|
-
const raw = readFileSync(keyPath);
|
|
79
|
-
if (raw.length === 32) {
|
|
80
|
-
log.info('Actor-token signing key loaded from disk');
|
|
81
|
-
return raw;
|
|
82
|
-
}
|
|
83
|
-
log.warn('Signing key file has unexpected length, regenerating');
|
|
84
|
-
} catch (err) {
|
|
85
|
-
log.warn({ err }, 'Failed to read signing key file, regenerating');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Generate and persist a new key
|
|
90
|
-
const newKey = randomBytes(32);
|
|
91
|
-
const dir = dirname(keyPath);
|
|
92
|
-
if (!existsSync(dir)) {
|
|
93
|
-
mkdirSync(dir, { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
const tmpPath = keyPath + '.tmp.' + process.pid;
|
|
96
|
-
writeFileSync(tmpPath, newKey, { mode: 0o600 });
|
|
97
|
-
renameSync(tmpPath, keyPath);
|
|
98
|
-
chmodSync(keyPath, 0o600);
|
|
99
|
-
|
|
100
|
-
log.info('Actor-token signing key generated and persisted');
|
|
101
|
-
return newKey;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Initialize (or reinitialize) the signing key. Called at daemon startup
|
|
106
|
-
* with a key loaded from disk via loadOrCreateSigningKey(), or by tests
|
|
107
|
-
* with a deterministic key.
|
|
108
|
-
*/
|
|
109
|
-
export function initSigningKey(key: Buffer): void {
|
|
110
|
-
signingKey = key;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function getSigningKey(): Buffer {
|
|
114
|
-
if (!signingKey) {
|
|
115
|
-
throw new Error('Actor-token signing key not initialized — call initSigningKey() during startup');
|
|
116
|
-
}
|
|
117
|
-
return signingKey;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Base64url helpers
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
function base64urlEncode(data: Buffer | string): string {
|
|
125
|
-
const buf = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
|
|
126
|
-
return buf.toString('base64url');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function base64urlDecode(str: string): Buffer {
|
|
130
|
-
return Buffer.from(str, 'base64url');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Hashing
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
/** SHA-256 hex digest of a raw token string. */
|
|
138
|
-
export function hashToken(token: string): string {
|
|
139
|
-
return createHash('sha256').update(token).digest('hex');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Mint
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
/** Default TTL for actor tokens: 30 days in milliseconds. */
|
|
147
|
-
const DEFAULT_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Mint a new actor token.
|
|
151
|
-
*
|
|
152
|
-
* @param params Token claims (assistantId, platform, deviceId, guardianPrincipalId).
|
|
153
|
-
* @param ttlMs Optional TTL in milliseconds. Defaults to 30 days.
|
|
154
|
-
* Pass `null` explicitly for a non-expiring token.
|
|
155
|
-
* @returns The raw token, its hash, and the embedded claims.
|
|
156
|
-
*/
|
|
157
|
-
export function mintActorToken(params: {
|
|
158
|
-
assistantId: string;
|
|
159
|
-
platform: string;
|
|
160
|
-
deviceId: string;
|
|
161
|
-
guardianPrincipalId: string;
|
|
162
|
-
ttlMs?: number | null;
|
|
163
|
-
}): MintResult {
|
|
164
|
-
const now = Date.now();
|
|
165
|
-
const effectiveTtl = params.ttlMs === undefined ? DEFAULT_TOKEN_TTL_MS : params.ttlMs;
|
|
166
|
-
const claims: ActorTokenClaims = {
|
|
167
|
-
assistantId: params.assistantId,
|
|
168
|
-
platform: params.platform,
|
|
169
|
-
deviceId: params.deviceId,
|
|
170
|
-
guardianPrincipalId: params.guardianPrincipalId,
|
|
171
|
-
iat: now,
|
|
172
|
-
exp: effectiveTtl != null ? now + effectiveTtl : null,
|
|
173
|
-
jti: randomBytes(16).toString('hex'),
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const payload = base64urlEncode(JSON.stringify(claims));
|
|
177
|
-
const sig = createHmac('sha256', getSigningKey())
|
|
178
|
-
.update(payload)
|
|
179
|
-
.digest();
|
|
180
|
-
const token = payload + '.' + base64urlEncode(sig);
|
|
181
|
-
const tokenHash = hashToken(token);
|
|
182
|
-
|
|
183
|
-
return { token, tokenHash, claims };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
// Verify
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Verify an actor token's structural integrity and signature.
|
|
192
|
-
*
|
|
193
|
-
* Does NOT check revocation — callers must additionally verify the
|
|
194
|
-
* token hash exists in the actor-token store with status='active'.
|
|
195
|
-
*/
|
|
196
|
-
export function verifyActorToken(token: string): VerifyResult {
|
|
197
|
-
const dotIndex = token.indexOf('.');
|
|
198
|
-
if (dotIndex < 0) {
|
|
199
|
-
return { ok: false, reason: 'malformed_token' };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const payload = token.slice(0, dotIndex);
|
|
203
|
-
const sigPart = token.slice(dotIndex + 1);
|
|
204
|
-
|
|
205
|
-
// Recompute HMAC
|
|
206
|
-
const expectedSig = createHmac('sha256', getSigningKey())
|
|
207
|
-
.update(payload)
|
|
208
|
-
.digest();
|
|
209
|
-
const actualSig = base64urlDecode(sigPart);
|
|
210
|
-
|
|
211
|
-
if (expectedSig.length !== actualSig.length) {
|
|
212
|
-
return { ok: false, reason: 'invalid_signature' };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Constant-time comparison
|
|
216
|
-
if (!timingSafeEqual(expectedSig, actualSig)) {
|
|
217
|
-
return { ok: false, reason: 'invalid_signature' };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let claims: ActorTokenClaims;
|
|
221
|
-
try {
|
|
222
|
-
const decoded = base64urlDecode(payload).toString('utf-8');
|
|
223
|
-
claims = JSON.parse(decoded) as ActorTokenClaims;
|
|
224
|
-
} catch {
|
|
225
|
-
return { ok: false, reason: 'malformed_claims' };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Expiration check
|
|
229
|
-
if (claims.exp != null && Date.now() > claims.exp) {
|
|
230
|
-
return { ok: false, reason: 'token_expired' };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return { ok: true, claims };
|
|
234
|
-
}
|
|
@@ -1,265 +0,0 @@
|
|
|
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 { GuardianRuntimeContext } from '../../daemon/session-runtime-assembly.js';
|
|
18
|
-
import { getActiveBinding } from '../../memory/guardian-bindings.js';
|
|
19
|
-
import { getLogger } from '../../util/logger.js';
|
|
20
|
-
import { type ActorTokenClaims, hashToken, verifyActorToken } from '../actor-token-service.js';
|
|
21
|
-
import { findActiveByTokenHash } from '../actor-token-store.js';
|
|
22
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
23
|
-
import { resolveGuardianContext } from '../guardian-context-resolver.js';
|
|
24
|
-
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
25
|
-
|
|
26
|
-
const log = getLogger('actor-token-middleware');
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Types
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
export interface ActorTokenResult {
|
|
33
|
-
ok: true;
|
|
34
|
-
claims: ActorTokenClaims;
|
|
35
|
-
guardianContext: GuardianRuntimeContext;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ActorTokenError {
|
|
39
|
-
ok: false;
|
|
40
|
-
status: number;
|
|
41
|
-
message: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ActorTokenVerification = ActorTokenResult | ActorTokenError;
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Header extraction
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
const ACTOR_TOKEN_HEADER = 'x-actor-token';
|
|
51
|
-
|
|
52
|
-
export function extractActorToken(req: Request): string | null {
|
|
53
|
-
return req.headers.get(ACTOR_TOKEN_HEADER) || null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Full verification pipeline
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Verify the X-Actor-Token header and resolve a guardian runtime context.
|
|
62
|
-
*
|
|
63
|
-
* Steps:
|
|
64
|
-
* 1. Extract the header value.
|
|
65
|
-
* 2. Verify HMAC signature and expiration.
|
|
66
|
-
* 3. Check the token hash is active in the actor-token store.
|
|
67
|
-
* 4. Resolve a guardian context through the standard trust pipeline using
|
|
68
|
-
* the claims' guardianPrincipalId as the sender identity.
|
|
69
|
-
*
|
|
70
|
-
* Returns an ok result with claims and guardianContext, or an error with
|
|
71
|
-
* the HTTP status code and message to return.
|
|
72
|
-
*/
|
|
73
|
-
export function verifyHttpActorToken(req: Request): ActorTokenVerification {
|
|
74
|
-
const rawToken = extractActorToken(req);
|
|
75
|
-
if (!rawToken) {
|
|
76
|
-
return {
|
|
77
|
-
ok: false,
|
|
78
|
-
status: 401,
|
|
79
|
-
message: 'Missing X-Actor-Token header. Vellum HTTP requests require actor identity.',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Structural + signature verification
|
|
84
|
-
const verifyResult = verifyActorToken(rawToken);
|
|
85
|
-
if (!verifyResult.ok) {
|
|
86
|
-
log.warn({ reason: verifyResult.reason }, 'Actor token verification failed');
|
|
87
|
-
return {
|
|
88
|
-
ok: false,
|
|
89
|
-
status: 401,
|
|
90
|
-
message: `Invalid actor token: ${verifyResult.reason}`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Check the token is active in the store (not revoked)
|
|
95
|
-
const tokenHash = hashToken(rawToken);
|
|
96
|
-
const record = findActiveByTokenHash(tokenHash);
|
|
97
|
-
if (!record) {
|
|
98
|
-
log.warn('Actor token not found in active store (possibly revoked)');
|
|
99
|
-
return {
|
|
100
|
-
ok: false,
|
|
101
|
-
status: 401,
|
|
102
|
-
message: 'Actor token is no longer active',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const { claims } = verifyResult;
|
|
107
|
-
|
|
108
|
-
// Resolve guardian context through the shared trust pipeline.
|
|
109
|
-
// The guardianPrincipalId from the token is used as the sender identity,
|
|
110
|
-
// and 'vellum' is used as the channel for binding lookup.
|
|
111
|
-
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
112
|
-
const guardianCtx = resolveGuardianContext({
|
|
113
|
-
assistantId,
|
|
114
|
-
sourceChannel: 'vellum',
|
|
115
|
-
conversationExternalId: 'local',
|
|
116
|
-
actorExternalId: claims.guardianPrincipalId,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
ok: true,
|
|
121
|
-
claims,
|
|
122
|
-
guardianContext: guardianCtx,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Verify that the actor identity from a verified token matches the bound
|
|
128
|
-
* guardian for the vellum channel. Used for guardian-decision endpoints
|
|
129
|
-
* where only the guardian should be able to approve/reject.
|
|
130
|
-
*
|
|
131
|
-
* Returns true if the actor is the bound guardian, false otherwise.
|
|
132
|
-
*/
|
|
133
|
-
export function isActorBoundGuardian(claims: ActorTokenClaims): boolean {
|
|
134
|
-
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
135
|
-
const binding = getActiveBinding(assistantId, 'vellum');
|
|
136
|
-
if (!binding) return false;
|
|
137
|
-
return binding.guardianExternalUserId === claims.guardianPrincipalId;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Bearer-auth fallback variants
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
/** Loopback addresses — used to gate the local identity fallback. */
|
|
145
|
-
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
146
|
-
|
|
147
|
-
/** Bun server shape needed for requestIP — avoids importing the full Bun type. */
|
|
148
|
-
export type ServerWithRequestIP = {
|
|
149
|
-
requestIP(req: Request): { address: string; family: string; port: number } | null;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Result for the fallback verification path where the actor token is absent
|
|
154
|
-
* but the request is bearer-authenticated (local trusted client like CLI).
|
|
155
|
-
*/
|
|
156
|
-
export interface ActorTokenLocalFallbackResult {
|
|
157
|
-
ok: true;
|
|
158
|
-
claims: null;
|
|
159
|
-
guardianContext: GuardianRuntimeContext;
|
|
160
|
-
localFallback: true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type ActorTokenVerificationWithFallback =
|
|
164
|
-
| ActorTokenResult
|
|
165
|
-
| ActorTokenLocalFallbackResult
|
|
166
|
-
| ActorTokenError;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Verify the actor token with fallback to local IPC identity resolution.
|
|
170
|
-
*
|
|
171
|
-
* When an actor token is present, the full verification pipeline runs.
|
|
172
|
-
* When absent AND the request originates from a loopback address, the
|
|
173
|
-
* request is treated as a trusted local client (e.g. CLI) and we fall
|
|
174
|
-
* back to `resolveLocalIpcGuardianContext()` which produces the same
|
|
175
|
-
* guardian context as the IPC pathway.
|
|
176
|
-
*
|
|
177
|
-
* Two conditions must BOTH be met for the local fallback:
|
|
178
|
-
* 1. No X-Forwarded-For header (rules out gateway-proxied requests).
|
|
179
|
-
* 2. The peer remote address is a loopback address (rules out LAN/container
|
|
180
|
-
* connections when the runtime binds to 0.0.0.0).
|
|
181
|
-
*
|
|
182
|
-
* The peer address is checked via `server.requestIP(req)`.
|
|
183
|
-
*
|
|
184
|
-
* --- CLI compatibility note ---
|
|
185
|
-
*
|
|
186
|
-
* The local fallback is an intentional CLI compatibility path, not a
|
|
187
|
-
* security gap. The CLI currently sends only `Authorization: Bearer <token>`
|
|
188
|
-
* without `X-Actor-Token`. This fallback allows the CLI to function until
|
|
189
|
-
* it is migrated to actor tokens in a future milestone.
|
|
190
|
-
*
|
|
191
|
-
* The fallback is gated by three conditions that together ensure only
|
|
192
|
-
* genuinely local connections receive guardian identity:
|
|
193
|
-
* (1) Absence of X-Forwarded-For header — the gateway always injects
|
|
194
|
-
* this header when proxying, so its presence indicates a remote client.
|
|
195
|
-
* (2) Loopback origin check — verifies the peer IP is 127.0.0.1/::1,
|
|
196
|
-
* preventing LAN or container peers.
|
|
197
|
-
* (3) Valid bearer authentication — already enforced upstream by the
|
|
198
|
-
* HTTP server's auth gate before this function is called.
|
|
199
|
-
*
|
|
200
|
-
* Once the CLI adopts actor tokens, this fallback can be removed.
|
|
201
|
-
*/
|
|
202
|
-
export function verifyHttpActorTokenWithLocalFallback(
|
|
203
|
-
req: Request,
|
|
204
|
-
server: ServerWithRequestIP,
|
|
205
|
-
): ActorTokenVerificationWithFallback {
|
|
206
|
-
const rawToken = extractActorToken(req);
|
|
207
|
-
|
|
208
|
-
// If an actor token is present, use the strict verification pipeline.
|
|
209
|
-
if (rawToken) {
|
|
210
|
-
return verifyHttpActorToken(req);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Gate the local fallback on provably-local origin. The gateway runtime
|
|
214
|
-
// proxy always injects X-Forwarded-For with the real client IP when
|
|
215
|
-
// forwarding requests. Direct local connections (CLI, macOS app) never
|
|
216
|
-
// set this header. If X-Forwarded-For is present, the request was
|
|
217
|
-
// proxied through the gateway on behalf of a potentially remote client
|
|
218
|
-
// and must not receive local guardian identity.
|
|
219
|
-
if (req.headers.get('x-forwarded-for')) {
|
|
220
|
-
log.warn('Rejecting local identity fallback: request has X-Forwarded-For (proxied through gateway)');
|
|
221
|
-
return {
|
|
222
|
-
ok: false,
|
|
223
|
-
status: 401,
|
|
224
|
-
message: 'Missing X-Actor-Token header. Proxied requests require actor identity.',
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Verify the peer address is actually loopback. This prevents LAN or
|
|
229
|
-
// container peers from receiving local guardian identity when the
|
|
230
|
-
// runtime binds to 0.0.0.0.
|
|
231
|
-
const peerIp = server.requestIP(req)?.address;
|
|
232
|
-
if (!peerIp || !LOOPBACK_ADDRESSES.has(peerIp)) {
|
|
233
|
-
log.warn({ peerIp }, 'Rejecting local identity fallback: peer is not loopback');
|
|
234
|
-
return {
|
|
235
|
-
ok: false,
|
|
236
|
-
status: 401,
|
|
237
|
-
message: 'Missing X-Actor-Token header. Non-loopback requests require actor identity.',
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// No actor token, no forwarding header, and the peer is on loopback
|
|
242
|
-
// — this is a direct local connection that passed bearer auth at the
|
|
243
|
-
// HTTP server layer. Resolve identity the same way as IPC.
|
|
244
|
-
log.debug('No actor token present on direct local request; using local IPC identity fallback');
|
|
245
|
-
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
246
|
-
return {
|
|
247
|
-
ok: true,
|
|
248
|
-
claims: null,
|
|
249
|
-
guardianContext,
|
|
250
|
-
localFallback: true,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Check whether the local fallback identity is the bound guardian.
|
|
256
|
-
*
|
|
257
|
-
* When no actor token is present (local fallback), the local user is
|
|
258
|
-
* treated as the guardian of their own machine — equivalent to IPC.
|
|
259
|
-
* This returns true when either the resolved trust class is 'guardian'
|
|
260
|
-
* or no vellum binding exists yet (pre-bootstrap).
|
|
261
|
-
*/
|
|
262
|
-
export function isLocalFallbackBoundGuardian(): boolean {
|
|
263
|
-
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
264
|
-
return guardianContext.trustClass === 'guardian';
|
|
265
|
-
}
|