@vellumai/assistant 0.4.1 → 0.4.3
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 +84 -7
- package/bun.lock +0 -83
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +2 -3
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/assistant-id-boundary-guard.test.ts +290 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/channel-approval-routes.test.ts +55 -5
- package/src/__tests__/channel-guardian.test.ts +6 -5
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/daemon-server-session-init.test.ts +54 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +21 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +4 -2
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +50 -9
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +161 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/non-member-access-request.test.ts +28 -1
- package/src/__tests__/notification-decision-strategy.test.ts +44 -0
- package/src/__tests__/relay-server.test.ts +644 -4
- package/src/__tests__/send-endpoint-busy.test.ts +129 -3
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +43 -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-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/approvals/guardian-decision-primitive.ts +24 -2
- package/src/approvals/guardian-request-resolvers.ts +42 -3
- package/src/calls/call-constants.ts +8 -0
- package/src/calls/call-controller.ts +2 -1
- package/src/calls/call-domain.ts +5 -4
- package/src/calls/relay-server.ts +513 -116
- package/src/calls/twilio-routes.ts +3 -5
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +164 -1
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +214 -0
- package/src/config/calls-schema.ts +12 -0
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -2
- package/src/daemon/handlers/config-channels.ts +5 -7
- package/src/daemon/handlers/config-inbox.ts +2 -0
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +136 -13
- 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 +3 -1
- package/src/daemon/server.ts +19 -3
- package/src/daemon/session-agent-loop.ts +35 -23
- package/src/daemon/session-runtime-assembly.ts +3 -1
- package/src/daemon/session-surfaces.ts +29 -1
- package/src/memory/app-store.ts +6 -0
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/embedding-local.ts +25 -13
- package/src/memory/embedding-runtime-manager.ts +24 -6
- 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/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +10 -5
- package/src/notifications/copy-composer.ts +11 -1
- package/src/notifications/emit-signal.ts +2 -1
- package/src/runtime/access-request-helper.ts +11 -3
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/guardian-context-resolver.ts +5 -1
- package/src/runtime/guardian-outbound-actions.ts +5 -4
- package/src/runtime/guardian-reply-router.ts +12 -0
- package/src/runtime/http-server.ts +12 -20
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +2 -1
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/call-routes.ts +2 -1
- 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 +33 -11
- package/src/runtime/routes/events-routes.ts +2 -3
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +16 -4
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/tools/apps/executors.ts +15 -0
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +2 -1
- package/src/workspace/git-service.ts +19 -0
|
@@ -40,6 +40,7 @@ import { consumeCallback, consumeCallbackError } from '../security/oauth-callbac
|
|
|
40
40
|
import { getLogger } from '../util/logger.js';
|
|
41
41
|
import { buildAssistantEvent } from './assistant-event.js';
|
|
42
42
|
import { assistantEventHub } from './assistant-event-hub.js';
|
|
43
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
43
44
|
import { sweepFailedEvents } from './channel-retry-sweep.js';
|
|
44
45
|
import { httpError } from './http-errors.js';
|
|
45
46
|
// Middleware
|
|
@@ -97,7 +98,6 @@ import {
|
|
|
97
98
|
startCanonicalGuardianExpirySweep,
|
|
98
99
|
stopCanonicalGuardianExpirySweep,
|
|
99
100
|
} from './routes/canonical-guardian-expiry-sweep.js';
|
|
100
|
-
import { canonicalChannelAssistantId } from './routes/channel-route-shared.js';
|
|
101
101
|
import {
|
|
102
102
|
handleChannelDeliveryAck,
|
|
103
103
|
handleChannelInbound,
|
|
@@ -270,7 +270,7 @@ export class RuntimeHttpServer {
|
|
|
270
270
|
ipcBroadcast(msg);
|
|
271
271
|
// Also publish to the event hub so HTTP/SSE clients (e.g. macOS
|
|
272
272
|
// app with localHttpEnabled) receive pairing approval requests.
|
|
273
|
-
void assistantEventHub.publish(buildAssistantEvent(
|
|
273
|
+
void assistantEventHub.publish(buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg));
|
|
274
274
|
}
|
|
275
275
|
: undefined,
|
|
276
276
|
};
|
|
@@ -521,22 +521,13 @@ export class RuntimeHttpServer {
|
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
//
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
return this.dispatchEndpoint(
|
|
524
|
+
// Runtime routes: /v1/<endpoint>
|
|
525
|
+
const routeMatch = path.match(/^\/v1\/(.+)$/);
|
|
526
|
+
if (routeMatch) {
|
|
527
|
+
return this.dispatchEndpoint(routeMatch[1], req, url);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
|
|
531
|
-
const match = path.match(/^\/v1\/assistants\/([^/]+)\/(.+)$/);
|
|
532
|
-
if (!match) {
|
|
533
|
-
return httpError('NOT_FOUND', 'Not found', 404);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const assistantId = canonicalChannelAssistantId(match[1]);
|
|
537
|
-
const endpoint = match[2];
|
|
538
|
-
log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
|
|
539
|
-
return this.dispatchEndpoint(endpoint, req, url, assistantId);
|
|
530
|
+
return httpError('NOT_FOUND', 'Not found', 404);
|
|
540
531
|
}
|
|
541
532
|
|
|
542
533
|
private handleBrowserRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
|
|
@@ -616,8 +607,8 @@ export class RuntimeHttpServer {
|
|
|
616
607
|
endpoint: string,
|
|
617
608
|
req: Request,
|
|
618
609
|
url: URL,
|
|
619
|
-
assistantId: string = 'self',
|
|
620
610
|
): Promise<Response> {
|
|
611
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
621
612
|
return withErrorHandling(endpoint, async () => {
|
|
622
613
|
if (endpoint === 'health' && req.method === 'GET') return handleHealth();
|
|
623
614
|
if (endpoint === 'debug' && req.method === 'GET') return handleDebug();
|
|
@@ -690,7 +681,7 @@ export class RuntimeHttpServer {
|
|
|
690
681
|
try {
|
|
691
682
|
recordConversationSeenSignal({
|
|
692
683
|
conversationId,
|
|
693
|
-
assistantId:
|
|
684
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
694
685
|
sourceChannel: (body.sourceChannel as string) ?? 'vellum',
|
|
695
686
|
signalType: (body.signalType as string ?? 'macos_conversation_opened') as SignalType,
|
|
696
687
|
confidence: (body.confidence as string ?? 'explicit') as Confidence,
|
|
@@ -714,6 +705,7 @@ export class RuntimeHttpServer {
|
|
|
714
705
|
processMessage: this.processMessage,
|
|
715
706
|
persistAndProcessMessage: this.persistAndProcessMessage,
|
|
716
707
|
sendMessageDeps: this.sendMessageDeps,
|
|
708
|
+
approvalConversationGenerator: this.approvalConversationGenerator,
|
|
717
709
|
});
|
|
718
710
|
}
|
|
719
711
|
|
|
@@ -812,11 +804,11 @@ export class RuntimeHttpServer {
|
|
|
812
804
|
|
|
813
805
|
// Internal Twilio forwarding endpoints (gateway -> runtime)
|
|
814
806
|
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
815
|
-
const json = await req.json() as { params: Record<string, string>; originalUrl?: string
|
|
807
|
+
const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
|
|
816
808
|
const formBody = new URLSearchParams(json.params).toString();
|
|
817
809
|
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
818
810
|
const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
819
|
-
return await handleVoiceWebhook(fakeReq
|
|
811
|
+
return await handleVoiceWebhook(fakeReq);
|
|
820
812
|
}
|
|
821
813
|
|
|
822
814
|
if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
|
|
@@ -57,6 +57,8 @@ export interface InviteResponseData {
|
|
|
57
57
|
expectedExternalUserId?: string;
|
|
58
58
|
voiceCode?: string;
|
|
59
59
|
voiceCodeDigits?: number;
|
|
60
|
+
friendName?: string;
|
|
61
|
+
guardianName?: string;
|
|
60
62
|
createdAt: number;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -110,6 +112,8 @@ function inviteToResponse(inv: IngressInvite, opts?: { rawToken?: string; voiceC
|
|
|
110
112
|
...(inv.expectedExternalUserId ? { expectedExternalUserId: inv.expectedExternalUserId } : {}),
|
|
111
113
|
...(opts?.voiceCode ? { voiceCode: opts.voiceCode } : {}),
|
|
112
114
|
...(inv.voiceCodeDigits != null ? { voiceCodeDigits: inv.voiceCodeDigits } : {}),
|
|
115
|
+
...(inv.friendName ? { friendName: inv.friendName } : {}),
|
|
116
|
+
...(inv.guardianName ? { guardianName: inv.guardianName } : {}),
|
|
113
117
|
createdAt: inv.createdAt,
|
|
114
118
|
};
|
|
115
119
|
}
|
|
@@ -149,6 +153,8 @@ export function createIngressInvite(params: {
|
|
|
149
153
|
// Voice invite parameters
|
|
150
154
|
expectedExternalUserId?: string;
|
|
151
155
|
voiceCodeDigits?: number;
|
|
156
|
+
friendName?: string;
|
|
157
|
+
guardianName?: string;
|
|
152
158
|
}): IngressResult<InviteResponseData> {
|
|
153
159
|
if (!params.sourceChannel) {
|
|
154
160
|
return { ok: false, error: 'sourceChannel is required for create' };
|
|
@@ -168,6 +174,12 @@ export function createIngressInvite(params: {
|
|
|
168
174
|
if (!isValidE164(params.expectedExternalUserId)) {
|
|
169
175
|
return { ok: false, error: 'expectedExternalUserId must be in E.164 format (e.g., +15551234567)' };
|
|
170
176
|
}
|
|
177
|
+
if (typeof params.friendName !== 'string' || !params.friendName.trim()) {
|
|
178
|
+
return { ok: false, error: 'friendName is required for voice invites' };
|
|
179
|
+
}
|
|
180
|
+
if (typeof params.guardianName !== 'string' || !params.guardianName.trim()) {
|
|
181
|
+
return { ok: false, error: 'guardianName is required for voice invites' };
|
|
182
|
+
}
|
|
171
183
|
voiceCode = generateVoiceCode(6);
|
|
172
184
|
voiceCodeHash = hashVoiceCode(voiceCode);
|
|
173
185
|
}
|
|
@@ -181,6 +193,8 @@ export function createIngressInvite(params: {
|
|
|
181
193
|
expectedExternalUserId: params.expectedExternalUserId,
|
|
182
194
|
voiceCodeHash,
|
|
183
195
|
voiceCodeDigits: 6,
|
|
196
|
+
friendName: params.friendName,
|
|
197
|
+
guardianName: params.guardianName,
|
|
184
198
|
} : {}),
|
|
185
199
|
});
|
|
186
200
|
// Voice invites must not expose the token — callers must redeem via the
|
|
@@ -13,6 +13,7 @@ import { findActiveVoiceInvites,findByTokenHash, hashToken, markInviteExpired, r
|
|
|
13
13
|
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
14
14
|
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
15
15
|
import { hashVoiceCode } from '../util/voice-code.js';
|
|
16
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Outcome type
|
|
@@ -223,7 +224,7 @@ export function redeemVoiceInviteCode(params: {
|
|
|
223
224
|
sourceChannel: 'voice';
|
|
224
225
|
code: string;
|
|
225
226
|
}): VoiceRedemptionOutcome {
|
|
226
|
-
const { assistantId =
|
|
227
|
+
const { assistantId = DAEMON_INTERNAL_ASSISTANT_ID, callerExternalUserId, code } = params;
|
|
227
228
|
|
|
228
229
|
if (!callerExternalUserId) {
|
|
229
230
|
return { ok: false, reason: 'invalid_or_expired' };
|
|
@@ -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:
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
|
|
12
12
|
import { getConfig } from '../../config/loader.js';
|
|
13
13
|
import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
|
|
14
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
14
15
|
import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
|
|
15
16
|
|
|
16
17
|
// ── Idempotency cache ─────────────────────────────────────────────────────────
|
|
@@ -41,7 +42,7 @@ function pruneIdempotencyCache(): void {
|
|
|
41
42
|
* Optional `idempotencyKey`: if supplied, duplicate requests with the same key
|
|
42
43
|
* within 5 minutes return the cached 201 response without starting a second call.
|
|
43
44
|
*/
|
|
44
|
-
export async function handleStartCall(req: Request, assistantId: string =
|
|
45
|
+
export async function handleStartCall(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
|
|
45
46
|
if (!getConfig().calls.enabled) {
|
|
46
47
|
return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
|
|
47
48
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
5
5
|
|
|
6
6
|
import type { ChannelId } from '../../channels/types.js';
|
|
7
|
-
import {
|
|
7
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
8
8
|
import type {
|
|
9
9
|
ApprovalAction,
|
|
10
10
|
ApprovalDecisionResult,
|
|
@@ -15,8 +15,8 @@ export type { ActorTrustClass, DenialReason, GuardianContext } from '../guardian
|
|
|
15
15
|
export { toGuardianRuntimeContext } from '../guardian-context-resolver.js';
|
|
16
16
|
|
|
17
17
|
/** Canonicalize assistantId for channel ingress paths. */
|
|
18
|
-
export function canonicalChannelAssistantId(
|
|
19
|
-
return
|
|
18
|
+
export function canonicalChannelAssistantId(_assistantId: string): string {
|
|
19
|
+
return DAEMON_INTERNAL_ASSISTANT_ID;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../memory/conversation-attention-store.js';
|
|
11
11
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
12
12
|
import { truncate } from '../../util/truncate.js';
|
|
13
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
13
14
|
import { httpError } from '../http-errors.js';
|
|
14
15
|
|
|
15
16
|
export function handleListConversationAttention(url: URL): Response {
|
|
@@ -27,7 +28,7 @@ export function handleListConversationAttention(url: URL): Response {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const attentionStates = listConversationAttention({
|
|
30
|
-
assistantId:
|
|
31
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
31
32
|
state: stateParam as AttentionFilterState,
|
|
32
33
|
sourceChannel: channel,
|
|
33
34
|
source: sourceParam !== 'all' ? sourceParam : undefined,
|
|
@@ -24,9 +24,11 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
|
|
|
24
24
|
import type { Provider } from '../../providers/types.js';
|
|
25
25
|
import { getLogger } from '../../util/logger.js';
|
|
26
26
|
import { buildAssistantEvent } from '../assistant-event.js';
|
|
27
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
27
28
|
import { routeGuardianReply } from '../guardian-reply-router.js';
|
|
28
29
|
import { httpError } from '../http-errors.js';
|
|
29
30
|
import type {
|
|
31
|
+
ApprovalConversationGenerator,
|
|
30
32
|
MessageProcessor,
|
|
31
33
|
NonBlockingMessageProcessor,
|
|
32
34
|
RuntimeAttachmentMetadata,
|
|
@@ -87,6 +89,7 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
87
89
|
}>;
|
|
88
90
|
session: import('../../daemon/session.js').Session;
|
|
89
91
|
onEvent: (msg: ServerMessage) => void;
|
|
92
|
+
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
90
93
|
}): Promise<{ consumed: boolean; messageId?: string }> {
|
|
91
94
|
const {
|
|
92
95
|
conversationId,
|
|
@@ -96,15 +99,16 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
96
99
|
attachments,
|
|
97
100
|
session,
|
|
98
101
|
onEvent,
|
|
102
|
+
approvalConversationGenerator,
|
|
99
103
|
} = params;
|
|
100
104
|
const trimmedContent = content.trim();
|
|
101
105
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
106
|
+
// Try inline approval interception whenever a pending confirmation exists.
|
|
107
|
+
// We intentionally do not block on queue depth: after an auto-deny, users
|
|
108
|
+
// often retry with "approve"/"yes" while the queue is still draining, and
|
|
109
|
+
// requiring an empty queue can create a deny/retry cascade.
|
|
105
110
|
if (
|
|
106
111
|
!session.hasAnyPendingConfirmation()
|
|
107
|
-
|| session.getQueueDepth() > 0
|
|
108
112
|
|| trimmedContent.length === 0
|
|
109
113
|
) {
|
|
110
114
|
return { consumed: false };
|
|
@@ -125,6 +129,7 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
125
129
|
},
|
|
126
130
|
conversationId,
|
|
127
131
|
pendingRequestIds,
|
|
132
|
+
approvalConversationGenerator,
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
if (!routerResult.consumed || routerResult.type === 'nl_keep_pending') {
|
|
@@ -176,6 +181,16 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
176
181
|
return { consumed: true, messageId };
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
function resolveCanonicalRequestSourceType(sourceChannel: string | undefined): 'desktop' | 'channel' | 'voice' {
|
|
185
|
+
if (sourceChannel === 'voice') {
|
|
186
|
+
return 'voice';
|
|
187
|
+
}
|
|
188
|
+
if (sourceChannel === 'vellum') {
|
|
189
|
+
return 'desktop';
|
|
190
|
+
}
|
|
191
|
+
return 'channel';
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
function getInterfaceFilesWithMtimes(interfacesDir: string | null): Array<{ path: string; mtimeMs: number }> {
|
|
180
195
|
if (!interfacesDir || !existsSync(interfacesDir)) return [];
|
|
181
196
|
const results: Array<{ path: string; mtimeMs: number }> = [];
|
|
@@ -319,12 +334,17 @@ function makeHubPublisher(
|
|
|
319
334
|
|
|
320
335
|
// Create a canonical guardian request so IPC/HTTP handlers can find it
|
|
321
336
|
// via applyCanonicalGuardianDecision.
|
|
337
|
+
const guardianContext = session.guardianContext;
|
|
338
|
+
const sourceChannel = guardianContext?.sourceChannel ?? 'vellum';
|
|
322
339
|
createCanonicalGuardianRequest({
|
|
323
340
|
id: msg.requestId,
|
|
324
341
|
kind: 'tool_approval',
|
|
325
|
-
sourceType:
|
|
326
|
-
sourceChannel
|
|
342
|
+
sourceType: resolveCanonicalRequestSourceType(sourceChannel),
|
|
343
|
+
sourceChannel,
|
|
327
344
|
conversationId,
|
|
345
|
+
requesterExternalUserId: guardianContext?.requesterExternalUserId,
|
|
346
|
+
requesterChatId: guardianContext?.requesterChatId,
|
|
347
|
+
guardianExternalUserId: guardianContext?.guardianExternalUserId,
|
|
328
348
|
toolName: msg.toolName,
|
|
329
349
|
status: 'pending',
|
|
330
350
|
requestCode: generateCanonicalRequestCode(),
|
|
@@ -344,7 +364,7 @@ function makeHubPublisher(
|
|
|
344
364
|
? (msg as { sessionId: string }).sessionId
|
|
345
365
|
: undefined;
|
|
346
366
|
const resolvedSessionId = msgSessionId ?? conversationId;
|
|
347
|
-
const event = buildAssistantEvent(
|
|
367
|
+
const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
|
|
348
368
|
hubChain = (async () => {
|
|
349
369
|
await hubChain;
|
|
350
370
|
try {
|
|
@@ -362,6 +382,7 @@ export async function handleSendMessage(
|
|
|
362
382
|
processMessage?: MessageProcessor;
|
|
363
383
|
persistAndProcessMessage?: NonBlockingMessageProcessor;
|
|
364
384
|
sendMessageDeps?: SendMessageDeps;
|
|
385
|
+
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
365
386
|
},
|
|
366
387
|
): Promise<Response> {
|
|
367
388
|
const body = await req.json() as {
|
|
@@ -440,10 +461,11 @@ export async function handleSendMessage(
|
|
|
440
461
|
sourceChannel,
|
|
441
462
|
sourceInterface,
|
|
442
463
|
content: content ?? '',
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
464
|
+
attachments,
|
|
465
|
+
session,
|
|
466
|
+
onEvent,
|
|
467
|
+
approvalConversationGenerator: deps.approvalConversationGenerator,
|
|
468
|
+
});
|
|
447
469
|
if (inlineReplyResult.consumed) {
|
|
448
470
|
return Response.json(
|
|
449
471
|
{ accepted: true, ...(inlineReplyResult.messageId ? { messageId: inlineReplyResult.messageId } : {}) },
|
|
@@ -11,6 +11,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
|
|
|
11
11
|
import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
|
|
12
12
|
import type { AssistantEventSubscription } from '../assistant-event-hub.js';
|
|
13
13
|
import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
|
|
14
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
14
15
|
import { httpError } from '../http-errors.js';
|
|
15
16
|
|
|
16
17
|
/** Keep-alive comment sent to idle clients every 30 s by default. */
|
|
@@ -50,8 +51,6 @@ export function handleSubscribeAssistantEvents(
|
|
|
50
51
|
// closures are in place before events can arrive. `controllerRef` is set
|
|
51
52
|
// synchronously inside ReadableStream's start(), so it is non-null by the
|
|
52
53
|
// time any event or eviction fires.
|
|
53
|
-
// 'self' is the assistantId used by buildAssistantEvent('self', ...) for
|
|
54
|
-
// all HTTP and voice session events.
|
|
55
54
|
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
56
55
|
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
57
56
|
let sub!: AssistantEventSubscription;
|
|
@@ -63,7 +62,7 @@ export function handleSubscribeAssistantEvents(
|
|
|
63
62
|
|
|
64
63
|
try {
|
|
65
64
|
sub = hub.subscribe(
|
|
66
|
-
{ assistantId:
|
|
65
|
+
{ assistantId: DAEMON_INTERNAL_ASSISTANT_ID, sessionId: mapping.conversationId },
|
|
67
66
|
(event) => {
|
|
68
67
|
const controller = controllerRef;
|
|
69
68
|
if (!controller) return;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { deleteConversationKey } from '../../memory/conversation-key-store.js';
|
|
5
5
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
6
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
6
7
|
import { httpError } from '../http-errors.js';
|
|
7
8
|
|
|
8
|
-
export async function handleDeleteConversation(req: Request, assistantId: string =
|
|
9
|
+
export async function handleDeleteConversation(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
|
|
9
10
|
const body = await req.json() as {
|
|
10
11
|
sourceChannel?: string;
|
|
11
12
|
externalChatId?: string;
|
|
@@ -26,14 +27,14 @@ export async function handleDeleteConversation(req: Request, assistantId: string
|
|
|
26
27
|
const legacyKey = `${sourceChannel}:${externalChatId}`;
|
|
27
28
|
const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
|
|
28
29
|
deleteConversationKey(scopedKey);
|
|
29
|
-
if (assistantId ===
|
|
30
|
+
if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
|
|
30
31
|
deleteConversationKey(legacyKey);
|
|
31
32
|
}
|
|
32
33
|
// external_conversation_bindings is currently assistant-agnostic
|
|
33
34
|
// (unique by sourceChannel + externalChatId). Restrict mutations to the
|
|
34
35
|
// canonical self-assistant route so multi-assistant legacy routes do not
|
|
35
36
|
// clobber each other's bindings.
|
|
36
|
-
if (assistantId ===
|
|
37
|
+
if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
|
|
37
38
|
externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -28,6 +28,7 @@ import { IngressBlockedError } from '../../util/errors.js';
|
|
|
28
28
|
import { getLogger } from '../../util/logger.js';
|
|
29
29
|
import { readHttpToken } from '../../util/platform.js';
|
|
30
30
|
import { notifyGuardianOfAccessRequest } from '../access-request-helper.js';
|
|
31
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
31
32
|
import {
|
|
32
33
|
buildApprovalUIMetadata,
|
|
33
34
|
getApprovalInfoByConversation,
|
|
@@ -97,7 +98,7 @@ export async function handleChannelInbound(
|
|
|
97
98
|
req: Request,
|
|
98
99
|
processMessage?: MessageProcessor,
|
|
99
100
|
bearerToken?: string,
|
|
100
|
-
assistantId: string =
|
|
101
|
+
assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
101
102
|
gatewayOriginSecret?: string,
|
|
102
103
|
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
103
104
|
approvalConversationGenerator?: ApprovalConversationGenerator,
|
|
@@ -580,7 +581,7 @@ export async function handleChannelInbound(
|
|
|
580
581
|
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
581
582
|
// self so assistant-scoped legacy routes do not overwrite each other's
|
|
582
583
|
// channel binding metadata for the same chat.
|
|
583
|
-
if (canonicalAssistantId ===
|
|
584
|
+
if (canonicalAssistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
|
|
584
585
|
externalConversationStore.upsertBinding({
|
|
585
586
|
conversationId: result.conversationId,
|
|
586
587
|
sourceChannel,
|
|
@@ -1401,6 +1402,8 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1401
1402
|
sourceChannel: ChannelId;
|
|
1402
1403
|
externalChatId: string;
|
|
1403
1404
|
guardianTrustClass: GuardianContext['trustClass'];
|
|
1405
|
+
guardianExternalUserId?: string;
|
|
1406
|
+
requesterExternalUserId?: string;
|
|
1404
1407
|
replyCallbackUrl: string;
|
|
1405
1408
|
bearerToken?: string;
|
|
1406
1409
|
assistantId?: string;
|
|
@@ -1411,6 +1414,8 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1411
1414
|
sourceChannel,
|
|
1412
1415
|
externalChatId,
|
|
1413
1416
|
guardianTrustClass,
|
|
1417
|
+
guardianExternalUserId,
|
|
1418
|
+
requesterExternalUserId,
|
|
1414
1419
|
replyCallbackUrl,
|
|
1415
1420
|
bearerToken,
|
|
1416
1421
|
assistantId,
|
|
@@ -1419,7 +1424,12 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1419
1424
|
|
|
1420
1425
|
// Approval prompt delivery is guardian-only. Non-guardian and unverified
|
|
1421
1426
|
// actors must never receive approval prompt broadcasts for the conversation.
|
|
1422
|
-
|
|
1427
|
+
// We also require an explicit identity match against the bound guardian to
|
|
1428
|
+
// avoid broadcasting prompts when trustClass is stale/mis-scoped.
|
|
1429
|
+
const isBoundGuardianActor = guardianTrustClass === 'guardian'
|
|
1430
|
+
&& !!guardianExternalUserId
|
|
1431
|
+
&& requesterExternalUserId === guardianExternalUserId;
|
|
1432
|
+
if (!isBoundGuardianActor) {
|
|
1423
1433
|
return () => {};
|
|
1424
1434
|
}
|
|
1425
1435
|
|
|
@@ -1438,7 +1448,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1438
1448
|
replyCallbackUrl,
|
|
1439
1449
|
chatId: externalChatId,
|
|
1440
1450
|
sourceChannel,
|
|
1441
|
-
assistantId: assistantId ??
|
|
1451
|
+
assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1442
1452
|
bearerToken,
|
|
1443
1453
|
prompt,
|
|
1444
1454
|
uiMetadata: buildApprovalUIMetadata(prompt, info),
|
|
@@ -1502,6 +1512,8 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1502
1512
|
sourceChannel,
|
|
1503
1513
|
externalChatId,
|
|
1504
1514
|
guardianTrustClass: guardianCtx.trustClass,
|
|
1515
|
+
guardianExternalUserId: guardianCtx.guardianExternalUserId,
|
|
1516
|
+
requesterExternalUserId: guardianCtx.requesterExternalUserId,
|
|
1505
1517
|
replyCallbackUrl,
|
|
1506
1518
|
bearerToken,
|
|
1507
1519
|
assistantId,
|
|
@@ -147,6 +147,8 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
|
147
147
|
expiresInMs: body.expiresInMs as number | undefined,
|
|
148
148
|
expectedExternalUserId: body.expectedExternalUserId as string | undefined,
|
|
149
149
|
voiceCodeDigits: body.voiceCodeDigits as number | undefined,
|
|
150
|
+
friendName: body.friendName as string | undefined,
|
|
151
|
+
guardianName: body.guardianName as string | undefined,
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
if (!result.ok) {
|
|
@@ -102,6 +102,21 @@ export async function executeAppCreate(
|
|
|
102
102
|
const preview = input.preview;
|
|
103
103
|
const appType = input.type === 'site' ? 'site' as const : 'app' as const;
|
|
104
104
|
|
|
105
|
+
// Validate required fields — LLM input is not type-checked at runtime
|
|
106
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
107
|
+
return { content: JSON.stringify({ error: 'name is required and must be a non-empty string' }), isError: true };
|
|
108
|
+
}
|
|
109
|
+
if (typeof htmlDefinition !== 'string') {
|
|
110
|
+
return { content: JSON.stringify({ error: 'html is required and must be a string containing the HTML definition' }), isError: true };
|
|
111
|
+
}
|
|
112
|
+
if (pages) {
|
|
113
|
+
for (const [filename, content] of Object.entries(pages)) {
|
|
114
|
+
if (typeof content !== 'string') {
|
|
115
|
+
return { content: JSON.stringify({ error: `pages["${filename}"] must be a string, got ${typeof content}` }), isError: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
const app = store.createApp({ name, description, schemaJson, htmlDefinition, pages, appType });
|
|
106
121
|
|
|
107
122
|
if (input.set_as_home_base) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { startCall } from '../../calls/call-domain.js';
|
|
2
2
|
import { getConfig } from '../../config/loader.js';
|
|
3
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
|
|
3
4
|
import { findActiveSession } from '../../runtime/channel-guardian-service.js';
|
|
4
5
|
import { normalizePhoneNumber } from '../../util/phone.js';
|
|
5
6
|
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
@@ -16,7 +17,7 @@ export async function executeCallStart(
|
|
|
16
17
|
? normalizePhoneNumber(input.phone_number)
|
|
17
18
|
: null;
|
|
18
19
|
if (requestedPhone) {
|
|
19
|
-
const assistantId = context.assistantId ??
|
|
20
|
+
const assistantId = context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
20
21
|
const activeVoiceVerification = findActiveSession(assistantId, 'voice');
|
|
21
22
|
const verificationDestination = activeVoiceVerification?.destinationAddress ?? activeVoiceVerification?.expectedPhoneE164;
|
|
22
23
|
if (verificationDestination === requestedPhone) {
|
|
@@ -132,6 +132,18 @@ function findWasmPath(pkg: string, file: string): string {
|
|
|
132
132
|
return execDirPath;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Use module resolution to find the package. This handles hoisted
|
|
136
|
+
// dependencies (e.g. global bun installs where web-tree-sitter is at the
|
|
137
|
+
// top-level node_modules rather than nested under @vellumai/assistant).
|
|
138
|
+
try {
|
|
139
|
+
const resolved = require.resolve(`${pkg}/package.json`);
|
|
140
|
+
const pkgDir = dirname(resolved);
|
|
141
|
+
const resolvedPath = join(pkgDir, file);
|
|
142
|
+
if (existsSync(resolvedPath)) return resolvedPath;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
log.warn({ err, pkg, file }, 'require.resolve failed for WASM package, falling back to manual resolution');
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
|
|
136
148
|
|
|
137
149
|
if (existsSync(sourcePath)) return sourcePath;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
|
|
2
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
2
3
|
import { createOrReuseToolGrantRequest } from '../runtime/tool-grant-request-helper.js';
|
|
3
4
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
4
5
|
import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
|
|
@@ -128,7 +129,7 @@ export class ToolApprovalHandler {
|
|
|
128
129
|
toolName: name,
|
|
129
130
|
inputDigest,
|
|
130
131
|
consumingRequestId: context.requestId ?? `preexec-${context.sessionId}-${Date.now()}`,
|
|
131
|
-
assistantId: context.assistantId ??
|
|
132
|
+
assistantId: context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
132
133
|
executionChannel: context.executionChannel,
|
|
133
134
|
conversationId: context.conversationId,
|
|
134
135
|
callSessionId: context.callSessionId,
|
|
@@ -16,6 +16,11 @@ const log = getLogger('workspace-git');
|
|
|
16
16
|
* Strips all GIT_* env vars (e.g. GIT_DIR, GIT_WORK_TREE) that CI runners
|
|
17
17
|
* or parent processes may set, then adds GIT_CEILING_DIRECTORIES to prevent
|
|
18
18
|
* walking up to a parent repo.
|
|
19
|
+
*
|
|
20
|
+
* On macOS, augments PATH with common binary directories so the real git
|
|
21
|
+
* binary is found even when the daemon is launched from a .app bundle with
|
|
22
|
+
* a minimal PATH. Without this, the macOS /usr/bin/git shim triggers an
|
|
23
|
+
* "Install Command Line Developer Tools" popup on every git invocation.
|
|
19
24
|
*/
|
|
20
25
|
function cleanGitEnv(workspaceDir: string): Record<string, string> {
|
|
21
26
|
const env: Record<string, string> = {};
|
|
@@ -25,6 +30,20 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
env.GIT_CEILING_DIRECTORIES = workspaceDir;
|
|
33
|
+
|
|
34
|
+
const home = process.env.HOME ?? '';
|
|
35
|
+
const extraDirs = [
|
|
36
|
+
'/opt/homebrew/bin',
|
|
37
|
+
'/usr/local/bin',
|
|
38
|
+
`${home}/.local/bin`,
|
|
39
|
+
];
|
|
40
|
+
const currentPath = env.PATH ?? '';
|
|
41
|
+
const pathDirs = currentPath.split(':');
|
|
42
|
+
const missing = extraDirs.filter(d => !pathDirs.includes(d));
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
env.PATH = [...missing, currentPath].filter(Boolean).join(':');
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
return env;
|
|
29
48
|
}
|
|
30
49
|
|