@vellumai/assistant 0.3.28 → 0.4.0
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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* POST /v1/ingress/members/:id/block — block a member
|
|
9
9
|
*
|
|
10
10
|
* Invites:
|
|
11
|
-
* GET /v1/ingress/invites
|
|
12
|
-
* POST /v1/ingress/invites
|
|
13
|
-
* DELETE /v1/ingress/invites/:id
|
|
14
|
-
* POST /v1/ingress/invites/redeem
|
|
11
|
+
* GET /v1/ingress/invites — list invites
|
|
12
|
+
* POST /v1/ingress/invites — create an invite (supports voice)
|
|
13
|
+
* DELETE /v1/ingress/invites/:id — revoke an invite
|
|
14
|
+
* POST /v1/ingress/invites/redeem — redeem an invite (token or voice code)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
listIngressInvites,
|
|
21
21
|
listIngressMembers,
|
|
22
22
|
redeemIngressInvite,
|
|
23
|
+
redeemVoiceInviteCode,
|
|
23
24
|
revokeIngressInvite,
|
|
24
25
|
revokeIngressMember,
|
|
25
26
|
upsertIngressMember,
|
|
@@ -130,6 +131,11 @@ export function handleListInvites(url: URL): Response {
|
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
133
|
* POST /v1/ingress/invites
|
|
134
|
+
*
|
|
135
|
+
* For voice invites, pass `sourceChannel: "voice"` with required
|
|
136
|
+
* `expectedExternalUserId` (E.164 phone). Voice codes are always 6 digits.
|
|
137
|
+
* The response will include a one-time `voiceCode` field that must be
|
|
138
|
+
* communicated to the invited user out-of-band.
|
|
133
139
|
*/
|
|
134
140
|
export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
135
141
|
const body = (await req.json()) as Record<string, unknown>;
|
|
@@ -139,6 +145,8 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
|
139
145
|
note: body.note as string | undefined,
|
|
140
146
|
maxUses: body.maxUses as number | undefined,
|
|
141
147
|
expiresInMs: body.expiresInMs as number | undefined,
|
|
148
|
+
expectedExternalUserId: body.expectedExternalUserId as string | undefined,
|
|
149
|
+
voiceCodeDigits: body.voiceCodeDigits as number | undefined,
|
|
142
150
|
});
|
|
143
151
|
|
|
144
152
|
if (!result.ok) {
|
|
@@ -161,10 +169,50 @@ export function handleRevokeInvite(inviteId: string): Response {
|
|
|
161
169
|
|
|
162
170
|
/**
|
|
163
171
|
* POST /v1/ingress/invites/redeem
|
|
172
|
+
*
|
|
173
|
+
* Unified invite redemption endpoint. Supports two modes:
|
|
174
|
+
*
|
|
175
|
+
* 1. **Token-based** (existing): pass `token`, `sourceChannel`, `externalUserId`, etc.
|
|
176
|
+
* 2. **Voice code** (new): pass `code` and `callerExternalUserId` (E.164 phone).
|
|
177
|
+
* Optionally pass `assistantId`.
|
|
178
|
+
*
|
|
179
|
+
* The presence of `code` in the body selects voice-code redemption.
|
|
164
180
|
*/
|
|
165
181
|
export async function handleRedeemInvite(req: Request): Promise<Response> {
|
|
166
182
|
const body = (await req.json()) as Record<string, unknown>;
|
|
167
183
|
|
|
184
|
+
// Voice-code redemption path: triggered when `code` is present
|
|
185
|
+
if (body.code != null) {
|
|
186
|
+
const callerExternalUserId = body.callerExternalUserId as string | undefined;
|
|
187
|
+
const code = body.code as string | undefined;
|
|
188
|
+
|
|
189
|
+
if (!callerExternalUserId || !code) {
|
|
190
|
+
return Response.json(
|
|
191
|
+
{ ok: false, error: 'callerExternalUserId and code are required' },
|
|
192
|
+
{ status: 400 },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = redeemVoiceInviteCode({
|
|
197
|
+
assistantId: body.assistantId as string | undefined,
|
|
198
|
+
callerExternalUserId,
|
|
199
|
+
sourceChannel: 'voice',
|
|
200
|
+
code,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!result.ok) {
|
|
204
|
+
return Response.json({ ok: false, error: result.reason }, { status: 400 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Response.json({
|
|
208
|
+
ok: true,
|
|
209
|
+
type: result.type,
|
|
210
|
+
memberId: result.memberId,
|
|
211
|
+
...(result.type === 'redeemed' ? { inviteId: result.inviteId } : {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Token-based redemption path (default)
|
|
168
216
|
const result = redeemIngressInvite({
|
|
169
217
|
token: body.token as string | undefined,
|
|
170
218
|
externalUserId: body.externalUserId as string | undefined,
|
|
@@ -75,6 +75,9 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
|
|
|
75
75
|
|
|
76
76
|
const result = ctx.pairingStore.beginRequest({ pairingRequestId, pairingSecret, deviceId, deviceName });
|
|
77
77
|
if (!result.ok) {
|
|
78
|
+
if (result.reason === 'already_paired') {
|
|
79
|
+
return httpError('CONFLICT', 'This pairing request is already bound to another device', 409);
|
|
80
|
+
}
|
|
78
81
|
const statusCode = result.reason === 'invalid_secret' ? 403 : result.reason === 'not_found' ? 403 : 410;
|
|
79
82
|
return httpError('FORBIDDEN', 'Forbidden', statusCode);
|
|
80
83
|
}
|
|
@@ -124,13 +124,13 @@ export function isGuardianControlPlaneInvocation(
|
|
|
124
124
|
export function enforceGuardianOnlyPolicy(
|
|
125
125
|
toolName: string,
|
|
126
126
|
input: Record<string, unknown>,
|
|
127
|
-
|
|
127
|
+
trustClass: string | undefined,
|
|
128
128
|
): { denied: boolean; reason?: string } {
|
|
129
129
|
if (!isGuardianControlPlaneInvocation(toolName, input)) {
|
|
130
130
|
return { denied: false };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (
|
|
133
|
+
if (trustClass === 'guardian' || trustClass === undefined) {
|
|
134
134
|
return { denied: false };
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -10,8 +10,8 @@ import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifec
|
|
|
10
10
|
|
|
11
11
|
const log = getLogger('tool-approval-handler');
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
return role === '
|
|
13
|
+
function isUntrustedGuardianTrustClass(role: ToolContext['guardianTrustClass']): boolean {
|
|
14
|
+
return role === 'trusted_contact' || role === 'unknown';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function requiresGuardianApprovalForActor(
|
|
@@ -26,10 +26,10 @@ function requiresGuardianApprovalForActor(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function guardianApprovalDeniedMessage(
|
|
29
|
-
|
|
29
|
+
trustClass: ToolContext['guardianTrustClass'],
|
|
30
30
|
toolName: string,
|
|
31
31
|
): string {
|
|
32
|
-
if (
|
|
32
|
+
if (trustClass === 'unknown') {
|
|
33
33
|
return `Permission denied for "${toolName}": this action requires guardian approval from a verified channel identity.`;
|
|
34
34
|
}
|
|
35
35
|
return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
|
|
@@ -82,13 +82,13 @@ export class ToolApprovalHandler {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
|
|
85
|
-
const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.
|
|
85
|
+
const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianTrustClass);
|
|
86
86
|
if (guardianCheck.denied) {
|
|
87
87
|
log.warn({
|
|
88
88
|
toolName: name,
|
|
89
89
|
sessionId: context.sessionId,
|
|
90
90
|
conversationId: context.conversationId,
|
|
91
|
-
|
|
91
|
+
trustClass: context.guardianTrustClass,
|
|
92
92
|
reason: 'guardian_only_policy',
|
|
93
93
|
}, 'Guardian-only policy blocked tool invocation');
|
|
94
94
|
const durationMs = Date.now() - startTime;
|
|
@@ -118,7 +118,7 @@ export class ToolApprovalHandler {
|
|
|
118
118
|
let deferredConsumeParams: Parameters<typeof consumeGrantForInvocation>[0] | null = null;
|
|
119
119
|
|
|
120
120
|
if (
|
|
121
|
-
|
|
121
|
+
isUntrustedGuardianTrustClass(context.guardianTrustClass)
|
|
122
122
|
&& requiresGuardianApprovalForActor(name, input, executionTarget)
|
|
123
123
|
) {
|
|
124
124
|
const inputDigest = computeToolApprovalDigest(name, input);
|
|
@@ -233,7 +233,7 @@ export class ToolApprovalHandler {
|
|
|
233
233
|
toolName: name,
|
|
234
234
|
sessionId: context.sessionId,
|
|
235
235
|
conversationId: context.conversationId,
|
|
236
|
-
|
|
236
|
+
trustClass: context.guardianTrustClass,
|
|
237
237
|
executionTarget,
|
|
238
238
|
grantId: grantResult.grant.id,
|
|
239
239
|
}, 'Scoped grant consumed — allowing untrusted actor tool invocation');
|
|
@@ -273,7 +273,7 @@ export class ToolApprovalHandler {
|
|
|
273
273
|
// actors remain fail-closed with no escalation.
|
|
274
274
|
let escalationMessage: string | undefined;
|
|
275
275
|
if (
|
|
276
|
-
context.
|
|
276
|
+
context.guardianTrustClass === 'trusted_contact'
|
|
277
277
|
&& context.assistantId
|
|
278
278
|
&& context.executionChannel
|
|
279
279
|
&& context.requesterExternalUserId
|
|
@@ -308,12 +308,12 @@ export class ToolApprovalHandler {
|
|
|
308
308
|
// If escalation.failed, fall through to generic denial message.
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.
|
|
311
|
+
const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.guardianTrustClass, name);
|
|
312
312
|
log.warn({
|
|
313
313
|
toolName: name,
|
|
314
314
|
sessionId: context.sessionId,
|
|
315
315
|
conversationId: context.conversationId,
|
|
316
|
-
|
|
316
|
+
trustClass: context.guardianTrustClass,
|
|
317
317
|
executionTarget,
|
|
318
318
|
reason: 'guardian_approval_required',
|
|
319
319
|
grantMissReason: grantResult.reason,
|
package/src/tools/types.ts
CHANGED
|
@@ -137,8 +137,8 @@ export interface ToolContext {
|
|
|
137
137
|
proxyApprovalCallback?: import('./network/script-proxy/types.js').ProxyApprovalCallback;
|
|
138
138
|
/** Optional principal identifier propagated to sub-tool confirmation flows. */
|
|
139
139
|
principal?: string;
|
|
140
|
-
/**
|
|
141
|
-
|
|
140
|
+
/** Inbound trust classification for the session — used by trust/policy gates. */
|
|
141
|
+
guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
|
|
142
142
|
/** Channel through which the tool invocation originates (e.g. 'telegram', 'voice'). Used for scoped grant consumption. */
|
|
143
143
|
executionChannel?: string;
|
|
144
144
|
/** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
|
package/src/util/logger.ts
CHANGED
|
@@ -3,12 +3,22 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { Writable } from 'node:stream';
|
|
4
4
|
|
|
5
5
|
import pino from 'pino';
|
|
6
|
+
import type { PrettyOptions } from 'pino-pretty';
|
|
6
7
|
import pinoPretty from 'pino-pretty';
|
|
7
8
|
|
|
8
9
|
import { getDebugMode, getDebugStdoutLogs,getLogStderr } from '../config/env-registry.js';
|
|
9
10
|
import { logSerializers } from './log-redact.js';
|
|
10
11
|
import { getLogPath } from './platform.js';
|
|
11
12
|
|
|
13
|
+
/** Common pino-pretty options that inline [module] into the message prefix. */
|
|
14
|
+
function prettyOpts(extra?: PrettyOptions): PrettyOptions {
|
|
15
|
+
return {
|
|
16
|
+
messageFormat: '[{module}] {msg}',
|
|
17
|
+
ignore: 'module',
|
|
18
|
+
...extra,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export type LogFileConfig = {
|
|
13
23
|
dir: string | undefined;
|
|
14
24
|
retentionDays: number;
|
|
@@ -59,7 +69,7 @@ let activeLogFileConfig: LogFileConfig | null = null;
|
|
|
59
69
|
|
|
60
70
|
function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
61
71
|
if (!config.dir) {
|
|
62
|
-
return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty({ destination: 1 }));
|
|
72
|
+
return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 1 })));
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
if (!existsSync(config.dir)) {
|
|
@@ -68,9 +78,10 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
68
78
|
|
|
69
79
|
const today = formatDate(new Date());
|
|
70
80
|
const filePath = logFilePathForDate(config.dir, new Date());
|
|
71
|
-
const
|
|
81
|
+
const fileDest = pino.destination({ dest: filePath, sync: false, mkdir: true, mode: 0o600 });
|
|
72
82
|
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
73
83
|
try { chmodSync(filePath, 0o600); } catch { /* best-effort */ }
|
|
84
|
+
const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
|
|
74
85
|
|
|
75
86
|
activeLogDate = today;
|
|
76
87
|
activeLogFileConfig = config;
|
|
@@ -78,7 +89,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
78
89
|
const level = getDebugMode() ? 'debug' : 'info';
|
|
79
90
|
|
|
80
91
|
if (getDebugMode()) {
|
|
81
|
-
const prettyStream = pinoPretty({ destination: 2 });
|
|
92
|
+
const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
|
|
82
93
|
return pino(
|
|
83
94
|
{ name: 'assistant', level, serializers: logSerializers },
|
|
84
95
|
pino.multistream([
|
|
@@ -92,7 +103,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
92
103
|
{ name: 'assistant', level, serializers: logSerializers },
|
|
93
104
|
pino.multistream([
|
|
94
105
|
{ stream: fileStream, level: 'info' as const },
|
|
95
|
-
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
|
|
106
|
+
{ stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
|
|
96
107
|
]),
|
|
97
108
|
);
|
|
98
109
|
}
|
|
@@ -135,12 +146,13 @@ function getRootLogger(): pino.Logger {
|
|
|
135
146
|
|
|
136
147
|
try {
|
|
137
148
|
const logPath = getLogPath();
|
|
138
|
-
const
|
|
149
|
+
const fileDest = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
|
|
139
150
|
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
140
151
|
try { chmodSync(logPath, 0o600); } catch { /* best-effort */ }
|
|
152
|
+
const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
|
|
141
153
|
|
|
142
154
|
if (getDebugMode()) {
|
|
143
|
-
const prettyStream = pinoPretty({ destination: 2 });
|
|
155
|
+
const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
|
|
144
156
|
const multi = pino.multistream([
|
|
145
157
|
{ stream: fileStream, level: 'info' as const },
|
|
146
158
|
{ stream: prettyStream, level: 'debug' as const },
|
|
@@ -151,14 +163,14 @@ function getRootLogger(): pino.Logger {
|
|
|
151
163
|
{ level: 'info', serializers: logSerializers },
|
|
152
164
|
pino.multistream([
|
|
153
165
|
{ stream: fileStream, level: 'info' as const },
|
|
154
|
-
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
|
|
166
|
+
{ stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
|
|
155
167
|
]),
|
|
156
168
|
);
|
|
157
169
|
} else {
|
|
158
170
|
rootLogger = pino({ level: 'info', serializers: logSerializers }, fileStream);
|
|
159
171
|
}
|
|
160
172
|
} catch {
|
|
161
|
-
rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty({ destination: 2 }));
|
|
173
|
+
rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 2 })));
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
176
|
return rootLogger;
|
package/src/util/platform.ts
CHANGED
|
@@ -121,6 +121,15 @@ export function getDataDir(): string {
|
|
|
121
121
|
return join(getWorkspaceDir(), 'data');
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Returns the embedding models directory (~/.vellum/workspace/embedding-models).
|
|
126
|
+
* Downloaded embedding runtime (onnxruntime-node, transformers bundle, model weights)
|
|
127
|
+
* is stored here, downloaded post-hatch rather than shipped with the app.
|
|
128
|
+
*/
|
|
129
|
+
export function getEmbeddingModelsDir(): string {
|
|
130
|
+
return join(getWorkspaceDir(), 'embedding-models');
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
/**
|
|
125
134
|
* Returns the IPC blob directory (~/.vellum/workspace/data/ipc-blobs).
|
|
126
135
|
* Temporary blob files for zero-copy IPC payloads live here.
|
|
@@ -357,6 +366,7 @@ export function ensureDataDir(): void {
|
|
|
357
366
|
workspace,
|
|
358
367
|
join(workspace, 'hooks'),
|
|
359
368
|
join(workspace, 'skills'),
|
|
369
|
+
join(workspace, 'embedding-models'),
|
|
360
370
|
// Data sub-dirs under workspace
|
|
361
371
|
wsData,
|
|
362
372
|
join(wsData, 'db'),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic voice invite code generation and hashing.
|
|
3
|
+
*
|
|
4
|
+
* Generates short numeric codes (default 6 digits) for voice-channel invite
|
|
5
|
+
* redemption. The plaintext code is returned once at creation time and never
|
|
6
|
+
* stored — only its SHA-256 hash is persisted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash, randomInt } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a cryptographically random numeric code of the given length.
|
|
13
|
+
* Uses node:crypto randomInt for uniform distribution.
|
|
14
|
+
*/
|
|
15
|
+
export function generateVoiceCode(digits: number = 6): string {
|
|
16
|
+
if (digits < 4 || digits > 10) {
|
|
17
|
+
throw new Error(`Voice code digit count must be between 4 and 10, got ${digits}`);
|
|
18
|
+
}
|
|
19
|
+
const min = Math.pow(10, digits - 1); // e.g. 100000 for 6 digits
|
|
20
|
+
const max = Math.pow(10, digits); // e.g. 1000000 for 6 digits
|
|
21
|
+
return String(randomInt(min, max));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 hash a voice code for storage comparison.
|
|
26
|
+
*/
|
|
27
|
+
export function hashVoiceCode(code: string): string {
|
|
28
|
+
return createHash('sha256').update(code).digest('hex');
|
|
29
|
+
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Guardian invite intent resolution for deterministic first-turn routing.
|
|
2
|
-
// Exports `resolveGuardianInviteIntent` as the single public entry point.
|
|
3
|
-
// When a guardian invite management request is detected, the session pipeline
|
|
4
|
-
// rewrites the message to force immediate entry into the trusted-contacts
|
|
5
|
-
// skill flow, bypassing the normal agent loop's tendency to produce conceptual
|
|
6
|
-
// preambles before loading the skill.
|
|
7
|
-
|
|
8
|
-
export type GuardianInviteIntentResult =
|
|
9
|
-
| { kind: 'none' }
|
|
10
|
-
| { kind: 'invite_management'; rewrittenContent: string; action?: 'create' | 'list' | 'revoke' };
|
|
11
|
-
|
|
12
|
-
// ── Direct invite patterns ────────────────────────────────────────────────
|
|
13
|
-
// These capture imperative requests to manage Telegram invite links.
|
|
14
|
-
|
|
15
|
-
const CREATE_INVITE_PATTERNS: RegExp[] = [
|
|
16
|
-
/\bcreate\s+(?:an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
17
|
-
/\binvite\s+(?:someone|somebody|a\s+friend|a\s+person)\s+(?:on|to|via|through)\s+telegram\b/i,
|
|
18
|
-
/\b(?:make|generate|get)\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
19
|
-
/\btelegram\s+invite\s*(?:link)?\b/i,
|
|
20
|
-
/\bsend\s+(?:a\s+|an\s+)?invite\s+(?:link\s+)?(?:on|for|via|through)\s+telegram\b/i,
|
|
21
|
-
/\bshare\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
22
|
-
/\binvite\s+(?:link\s+)?for\s+telegram\b/i,
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const LIST_INVITE_PATTERNS: RegExp[] = [
|
|
26
|
-
/\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:active\s+)?invite(?:s|\s*links?)\b/i,
|
|
27
|
-
/\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:telegram\s+)?invite(?:s|\s*links?)\b/i,
|
|
28
|
-
/\bwhat\s+invite(?:s|\s*links?)\s+(?:do\s+I\s+have|are\s+active|exist)\b/i,
|
|
29
|
-
/\bhow\s+many\s+invite(?:s|\s*links?)\b/i,
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const REVOKE_INVITE_PATTERNS: RegExp[] = [
|
|
33
|
-
/\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?invite\s*(?:link)?\b/i,
|
|
34
|
-
/\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
35
|
-
/\binvite\s*(?:link)?\s+(?:revoke|cancel|disable|invalidate|delete|remove)\b/i,
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
// ── Conceptual / question patterns ──────────────────────────────────────
|
|
39
|
-
// These indicate the user is asking *about* invites rather than requesting
|
|
40
|
-
// to manage them. Return passthrough for these.
|
|
41
|
-
|
|
42
|
-
const CONCEPTUAL_PATTERNS: RegExp[] = [
|
|
43
|
-
/^\s*(?:how|what|why|when|where|who|which)\b.*\binvite/i,
|
|
44
|
-
/\bwhat\s+(?:is|are)\s+(?:an?\s+)?invite\s*(?:link)?\b/i,
|
|
45
|
-
/\bhow\s+(?:do|does|can)\s+(?:invite|invitation)s?\s+work\b/i,
|
|
46
|
-
/\bexplain\s+(?:the\s+)?invite\b/i,
|
|
47
|
-
/\btell\s+me\s+about\s+invite\b/i,
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
/** Common polite/filler words stripped before checking intent-only status. */
|
|
51
|
-
const FILLER_PATTERN =
|
|
52
|
-
/\b(please|pls|plz|can\s+you|could\s+you|would\s+you|now|right\s+now|thanks|thank\s+you|thx|ty|for\s+me|ok(ay)?|hey|hi|hello|just|i\s+want\s+to|i'd\s+like\s+to|i\s+need\s+to|let's|let\s+me)\b/gi;
|
|
53
|
-
|
|
54
|
-
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
function isConceptualQuestion(text: string): boolean {
|
|
57
|
-
const cleaned = text.replace(/^\s*(hey|hi|hello|please|pls|plz)[,\s]+/i, '');
|
|
58
|
-
// Allow actionable requests through even though they start with
|
|
59
|
-
// question-like words — these are imperative invite management requests.
|
|
60
|
-
if (LIST_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
61
|
-
if (CREATE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
62
|
-
if (REVOKE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
63
|
-
return CONCEPTUAL_PATTERNS.some((p) => p.test(cleaned));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function detectAction(text: string): 'create' | 'list' | 'revoke' | undefined {
|
|
67
|
-
// Check revoke and list before create — create patterns include the broad
|
|
68
|
-
// `telegram invite link` matcher that would otherwise swallow revoke/list inputs.
|
|
69
|
-
if (REVOKE_INVITE_PATTERNS.some((p) => p.test(text))) return 'revoke';
|
|
70
|
-
if (LIST_INVITE_PATTERNS.some((p) => p.test(text))) return 'list';
|
|
71
|
-
if (CREATE_INVITE_PATTERNS.some((p) => p.test(text))) return 'create';
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── Structured intent resolver ───────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Resolves guardian invite management intent from user text.
|
|
79
|
-
*
|
|
80
|
-
* Pipeline:
|
|
81
|
-
* 1. Skip slash commands entirely
|
|
82
|
-
* 2. Conceptual question gate -- questions return `none`
|
|
83
|
-
* 3. Detect create/list/revoke invite patterns
|
|
84
|
-
* 4. On match, build a deterministic model instruction to load trusted-contacts
|
|
85
|
-
*/
|
|
86
|
-
export function resolveGuardianInviteIntent(text: string): GuardianInviteIntentResult {
|
|
87
|
-
const trimmed = text.trim();
|
|
88
|
-
|
|
89
|
-
// Never intercept slash commands
|
|
90
|
-
if (trimmed.startsWith('/')) {
|
|
91
|
-
return { kind: 'none' };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Conceptual questions pass through to normal agent processing
|
|
95
|
-
if (isConceptualQuestion(trimmed)) {
|
|
96
|
-
return { kind: 'none' };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Strip fillers for pattern matching but keep original for context
|
|
100
|
-
const withoutFillers = trimmed.replace(FILLER_PATTERN, '').replace(/\s{2,}/g, ' ').trim();
|
|
101
|
-
|
|
102
|
-
const action = detectAction(withoutFillers);
|
|
103
|
-
if (!action) {
|
|
104
|
-
return { kind: 'none' };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Build the rewritten content that deterministically loads the skill
|
|
108
|
-
const actionDescriptions: Record<string, string> = {
|
|
109
|
-
create: 'The user wants to create a Telegram invite link. Create the invite, look up the bot username, and present the shareable deep link with copy-paste instructions.',
|
|
110
|
-
list: 'The user wants to see their invite links. List all invites (especially active ones for Telegram) and present them in a readable format.',
|
|
111
|
-
revoke: 'The user wants to revoke an invite link. List invites to identify the target, confirm with the user, then revoke it.',
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const rewrittenContent = [
|
|
115
|
-
actionDescriptions[action],
|
|
116
|
-
'Please invoke the "Trusted Contacts" skill (ID: trusted-contacts) immediately using skill_load.',
|
|
117
|
-
].join('\n');
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
kind: 'invite_management',
|
|
121
|
-
rewrittenContent,
|
|
122
|
-
action,
|
|
123
|
-
};
|
|
124
|
-
}
|