@vellumai/assistant 0.4.2 → 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/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- 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-guardian.test.ts +6 -5
- package/src/__tests__/config-schema.test.ts +2 -0
- 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-outbound-http.test.ts +0 -1
- 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__/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 +2 -1
- 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 +11 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +3 -1
- package/src/daemon/server.ts +2 -1
- package/src/daemon/session-agent-loop.ts +2 -1
- package/src/daemon/session-runtime-assembly.ts +3 -1
- package/src/daemon/session-surfaces.ts +29 -1
- 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/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-outbound-actions.ts +5 -4
- package/src/runtime/http-server.ts +11 -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 +2 -1
- 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 +4 -3
- package/src/runtime/routes/ingress-routes.ts +2 -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
|
@@ -105,14 +105,22 @@ export function notifyGuardianOfAccessRequest(
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// The conversationId is assistant-scoped so the dedupe query below only
|
|
109
|
+
// matches requests for the same assistant. Without this, a pending request
|
|
110
|
+
// from assistant A could be returned for assistant B, allowing the caller
|
|
111
|
+
// to piggyback on A's guardian approval.
|
|
112
|
+
const conversationId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}`;
|
|
113
|
+
|
|
108
114
|
// Deduplicate: skip creation if there is already a pending canonical request
|
|
109
|
-
// for the same requester on this channel. Still return
|
|
110
|
-
// the existing request ID so callers know the guardian
|
|
115
|
+
// for the same requester on this channel *and* assistant. Still return
|
|
116
|
+
// notified: true with the existing request ID so callers know the guardian
|
|
117
|
+
// was already notified.
|
|
111
118
|
const existingCanonical = listCanonicalGuardianRequests({
|
|
112
119
|
status: 'pending',
|
|
113
120
|
requesterExternalUserId: senderExternalUserId,
|
|
114
121
|
sourceChannel,
|
|
115
122
|
kind: 'access_request',
|
|
123
|
+
conversationId,
|
|
116
124
|
});
|
|
117
125
|
if (existingCanonical.length > 0) {
|
|
118
126
|
log.debug(
|
|
@@ -130,7 +138,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
130
138
|
kind: 'access_request',
|
|
131
139
|
sourceType: 'channel',
|
|
132
140
|
sourceChannel,
|
|
133
|
-
conversationId
|
|
141
|
+
conversationId,
|
|
134
142
|
requesterExternalUserId: senderExternalUserId,
|
|
135
143
|
requesterChatId: externalChatId,
|
|
136
144
|
guardianExternalUserId: guardianExternalUserId ?? undefined,
|
|
@@ -17,7 +17,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
|
|
|
17
17
|
import type { IngressMember } from '../memory/ingress-member-store.js';
|
|
18
18
|
import { findMember } from '../memory/ingress-member-store.js';
|
|
19
19
|
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
20
|
-
import {
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
21
21
|
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -76,7 +76,7 @@ export interface ResolveActorTrustInput {
|
|
|
76
76
|
* 5. Classify: guardian > trusted_contact (active member) > unknown.
|
|
77
77
|
*/
|
|
78
78
|
export function resolveActorTrust(input: ResolveActorTrustInput): ActorTrustContext {
|
|
79
|
-
const assistantId =
|
|
79
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
80
80
|
|
|
81
81
|
const rawUserId = typeof input.senderExternalUserId === 'string' && input.senderExternalUserId.trim().length > 0
|
|
82
82
|
? input.senderExternalUserId.trim()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical internal scope ID for all daemon-side assistant-scoped storage.
|
|
3
|
+
*
|
|
4
|
+
* The daemon uses a single fixed identity (`'self'`) for its own assistant
|
|
5
|
+
* scope. Public/external assistant IDs are an edge concern owned by the
|
|
6
|
+
* gateway and platform layers (hatch, invite links, etc.). Daemon code
|
|
7
|
+
* should never derive scoping decisions from externally-provided assistant
|
|
8
|
+
* IDs — use this constant instead.
|
|
9
|
+
*/
|
|
10
|
+
export const DAEMON_INTERNAL_ASSISTANT_ID = 'self' as const;
|
|
@@ -16,7 +16,8 @@ import { sendMessage as sendSms } from '../messaging/providers/sms/client.js';
|
|
|
16
16
|
import { getCredentialMetadata } from '../tools/credentials/metadata-store.js';
|
|
17
17
|
import { getLogger } from '../util/logger.js';
|
|
18
18
|
import { normalizePhoneNumber } from '../util/phone.js';
|
|
19
|
-
import {
|
|
19
|
+
import { readHttpToken } from '../util/platform.js';
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
20
21
|
import {
|
|
21
22
|
countRecentSendsToDestination,
|
|
22
23
|
createOutboundSession,
|
|
@@ -243,7 +244,7 @@ function initiateGuardianVoiceCall(
|
|
|
243
244
|
// ---------------------------------------------------------------------------
|
|
244
245
|
|
|
245
246
|
export function startOutbound(params: StartOutboundParams): OutboundActionResult {
|
|
246
|
-
const assistantId =
|
|
247
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
247
248
|
const channel = params.channel;
|
|
248
249
|
const originConversationId = params.originConversationId;
|
|
249
250
|
|
|
@@ -541,7 +542,7 @@ function startOutboundVoice(
|
|
|
541
542
|
// ---------------------------------------------------------------------------
|
|
542
543
|
|
|
543
544
|
export function resendOutbound(params: ResendOutboundParams): OutboundActionResult {
|
|
544
|
-
const assistantId =
|
|
545
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
545
546
|
const channel = params.channel;
|
|
546
547
|
const originConversationId = params.originConversationId;
|
|
547
548
|
|
|
@@ -707,7 +708,7 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
707
708
|
// ---------------------------------------------------------------------------
|
|
708
709
|
|
|
709
710
|
export function cancelOutbound(params: CancelOutboundParams): OutboundActionResult {
|
|
710
|
-
const assistantId =
|
|
711
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
711
712
|
const channel = params.channel;
|
|
712
713
|
|
|
713
714
|
const session = findActiveSession(assistantId, channel);
|
|
@@ -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,
|
|
@@ -813,11 +804,11 @@ export class RuntimeHttpServer {
|
|
|
813
804
|
|
|
814
805
|
// Internal Twilio forwarding endpoints (gateway -> runtime)
|
|
815
806
|
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
816
|
-
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 };
|
|
817
808
|
const formBody = new URLSearchParams(json.params).toString();
|
|
818
809
|
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
819
810
|
const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
820
|
-
return await handleVoiceWebhook(fakeReq
|
|
811
|
+
return await handleVoiceWebhook(fakeReq);
|
|
821
812
|
}
|
|
822
813
|
|
|
823
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,6 +24,7 @@ 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 {
|
|
@@ -363,7 +364,7 @@ function makeHubPublisher(
|
|
|
363
364
|
? (msg as { sessionId: string }).sessionId
|
|
364
365
|
: undefined;
|
|
365
366
|
const resolvedSessionId = msgSessionId ?? conversationId;
|
|
366
|
-
const event = buildAssistantEvent(
|
|
367
|
+
const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
|
|
367
368
|
hubChain = (async () => {
|
|
368
369
|
await hubChain;
|
|
369
370
|
try {
|
|
@@ -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,
|
|
@@ -1447,7 +1448,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1447
1448
|
replyCallbackUrl,
|
|
1448
1449
|
chatId: externalChatId,
|
|
1449
1450
|
sourceChannel,
|
|
1450
|
-
assistantId: assistantId ??
|
|
1451
|
+
assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1451
1452
|
bearerToken,
|
|
1452
1453
|
prompt,
|
|
1453
1454
|
uiMetadata: buildApprovalUIMetadata(prompt, info),
|
|
@@ -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) {
|
|
@@ -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
|
|