@vellumai/assistant 0.3.19 → 0.3.21
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
package/src/calls/call-domain.ts
CHANGED
|
@@ -72,6 +72,8 @@ export type CancelCallInput = {
|
|
|
72
72
|
export type AnswerCallInput = {
|
|
73
73
|
callSessionId: string;
|
|
74
74
|
answer: string;
|
|
75
|
+
/** When provided, the answer is matched to this specific pending question/consultation. */
|
|
76
|
+
pendingQuestionId?: string;
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
export type RelayInstructionInput = {
|
|
@@ -516,30 +518,66 @@ export async function cancelCall(input: CancelCallInput): Promise<{ ok: true; se
|
|
|
516
518
|
|
|
517
519
|
/**
|
|
518
520
|
* Answer a pending question for an active call.
|
|
521
|
+
*
|
|
522
|
+
* When `pendingQuestionId` is provided, the answer is matched to that specific
|
|
523
|
+
* pending question/consultation rather than relying on transient controller
|
|
524
|
+
* state. This allows answers to arrive while the call is active and not paused,
|
|
525
|
+
* as long as the referenced question is still pending.
|
|
519
526
|
*/
|
|
520
527
|
export async function answerCall(input: AnswerCallInput): Promise<{ ok: true; questionId: string } | CallError> {
|
|
521
|
-
const { callSessionId, answer } = input;
|
|
528
|
+
const { callSessionId, answer, pendingQuestionId } = input;
|
|
522
529
|
|
|
523
530
|
if (!answer || typeof answer !== 'string') {
|
|
524
531
|
return { ok: false, error: 'Missing answer', status: 400 };
|
|
525
532
|
}
|
|
526
533
|
|
|
534
|
+
const controller = getCallController(callSessionId);
|
|
535
|
+
if (!controller) {
|
|
536
|
+
log.warn({ callSessionId }, 'answerCall: no active controller for call session');
|
|
537
|
+
return { ok: false, error: 'No active controller for this call', status: 409 };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// When a specific question is targeted, validate it matches the active
|
|
541
|
+
// consultation. This prevents stale or duplicate answers from being
|
|
542
|
+
// applied to the wrong consultation.
|
|
543
|
+
if (pendingQuestionId) {
|
|
544
|
+
const activeQuestionId = controller.getPendingConsultationQuestionId();
|
|
545
|
+
if (!activeQuestionId) {
|
|
546
|
+
log.warn(
|
|
547
|
+
{ callSessionId, pendingQuestionId },
|
|
548
|
+
'answerCall: pendingQuestionId provided but no consultation is active',
|
|
549
|
+
);
|
|
550
|
+
return { ok: false, error: 'Referenced question is no longer pending', status: 409 };
|
|
551
|
+
}
|
|
552
|
+
if (activeQuestionId !== pendingQuestionId) {
|
|
553
|
+
log.warn(
|
|
554
|
+
{ callSessionId, pendingQuestionId, activeQuestionId },
|
|
555
|
+
'answerCall: pendingQuestionId does not match active consultation',
|
|
556
|
+
);
|
|
557
|
+
return { ok: false, error: 'Referenced question is stale — a newer consultation has superseded it', status: 409 };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Look up the pending question in the store for record-keeping
|
|
527
562
|
const question = getPendingQuestion(callSessionId);
|
|
528
563
|
if (!question) {
|
|
529
564
|
return { ok: false, error: 'No pending question found', status: 404 };
|
|
530
565
|
}
|
|
531
566
|
|
|
532
|
-
|
|
533
|
-
if (
|
|
534
|
-
log.warn(
|
|
535
|
-
|
|
567
|
+
// When pendingQuestionId is given, double-check it matches the store record
|
|
568
|
+
if (pendingQuestionId && question.id !== pendingQuestionId) {
|
|
569
|
+
log.warn(
|
|
570
|
+
{ callSessionId, pendingQuestionId, storeQuestionId: question.id },
|
|
571
|
+
'answerCall: store pending question does not match requested pendingQuestionId',
|
|
572
|
+
);
|
|
573
|
+
return { ok: false, error: 'Referenced question is stale', status: 409 };
|
|
536
574
|
}
|
|
537
575
|
|
|
538
576
|
const accepted = await controller.handleUserAnswer(answer);
|
|
539
577
|
if (!accepted) {
|
|
540
578
|
log.warn(
|
|
541
579
|
{ callSessionId },
|
|
542
|
-
'answerCall: controller rejected the answer (
|
|
580
|
+
'answerCall: controller rejected the answer (no pending consultation)',
|
|
543
581
|
);
|
|
544
582
|
return { ok: false, error: 'Controller is not waiting for an answer', status: 409 };
|
|
545
583
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
countPendingRequestsByCallSessionId,
|
|
13
13
|
createGuardianActionDelivery,
|
|
14
14
|
createGuardianActionRequest,
|
|
15
|
+
getGuardianConversationIdForCallSession,
|
|
15
16
|
updateDeliveryStatus,
|
|
16
17
|
} from '../memory/guardian-action-store.js';
|
|
17
18
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
@@ -22,6 +23,11 @@ import type { CallPendingQuestion } from './types.js';
|
|
|
22
23
|
|
|
23
24
|
const log = getLogger('guardian-dispatch');
|
|
24
25
|
|
|
26
|
+
// Per-callSessionId serialization lock. Ensures that concurrent dispatches for
|
|
27
|
+
// the same call session are serialized so the second dispatch always sees the
|
|
28
|
+
// delivery row (and thus the guardian conversation ID) persisted by the first.
|
|
29
|
+
const pendingDispatches = new Map<string, Promise<void>>();
|
|
30
|
+
|
|
25
31
|
export interface GuardianDispatchParams {
|
|
26
32
|
callSessionId: string;
|
|
27
33
|
conversationId: string;
|
|
@@ -47,6 +53,31 @@ function applyDeliveryStatus(deliveryId: string, result: NotificationDeliveryRes
|
|
|
47
53
|
* Fire-and-forget: errors are logged but do not propagate.
|
|
48
54
|
*/
|
|
49
55
|
export async function dispatchGuardianQuestion(params: GuardianDispatchParams): Promise<void> {
|
|
56
|
+
const { callSessionId } = params;
|
|
57
|
+
|
|
58
|
+
// Serialize concurrent dispatches for the same call session so the second
|
|
59
|
+
// dispatch always sees the guardian conversation ID persisted by the first.
|
|
60
|
+
const preceding = pendingDispatches.get(callSessionId);
|
|
61
|
+
const current = (preceding ?? Promise.resolve()).then(() =>
|
|
62
|
+
dispatchGuardianQuestionInner(params),
|
|
63
|
+
);
|
|
64
|
+
// Store a suppressed-error variant so the chain never rejects, and keep
|
|
65
|
+
// a stable reference for the cleanup identity check below.
|
|
66
|
+
const suppressed = current.catch(() => {});
|
|
67
|
+
pendingDispatches.set(callSessionId, suppressed);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await current;
|
|
71
|
+
} finally {
|
|
72
|
+
// Clean up the map entry only if it still points to our promise, to avoid
|
|
73
|
+
// removing a later dispatch's entry.
|
|
74
|
+
if (pendingDispatches.get(callSessionId) === suppressed) {
|
|
75
|
+
pendingDispatches.delete(callSessionId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Promise<void> {
|
|
50
81
|
const {
|
|
51
82
|
callSessionId,
|
|
52
83
|
conversationId,
|
|
@@ -84,6 +115,22 @@ export async function dispatchGuardianQuestion(params: GuardianDispatchParams):
|
|
|
84
115
|
// in the same call session.
|
|
85
116
|
const activeGuardianRequestCount = countPendingRequestsByCallSessionId(callSessionId);
|
|
86
117
|
|
|
118
|
+
// Look up the vellum conversation used for the first guardian question
|
|
119
|
+
// in this call session. When found, pass it as an affinity hint so the
|
|
120
|
+
// notification pipeline deterministically routes to the same conversation
|
|
121
|
+
// instead of letting the LLM choose a different thread.
|
|
122
|
+
const existingGuardianConversationId = getGuardianConversationIdForCallSession(callSessionId);
|
|
123
|
+
const conversationAffinityHint = existingGuardianConversationId
|
|
124
|
+
? { vellum: existingGuardianConversationId }
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
if (existingGuardianConversationId) {
|
|
128
|
+
log.info(
|
|
129
|
+
{ callSessionId, existingGuardianConversationId },
|
|
130
|
+
'Found existing guardian conversation for call session — enforcing thread affinity',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
87
134
|
// Route through the canonical notification pipeline. The paired vellum
|
|
88
135
|
// conversation from this pipeline is the canonical guardian thread.
|
|
89
136
|
let vellumDeliveryId: string | null = null;
|
|
@@ -107,6 +154,7 @@ export async function dispatchGuardianQuestion(params: GuardianDispatchParams):
|
|
|
107
154
|
pendingQuestionId: pendingQuestion.id,
|
|
108
155
|
activeGuardianRequestCount,
|
|
109
156
|
},
|
|
157
|
+
conversationAffinityHint,
|
|
110
158
|
dedupeKey: `guardian:${request.id}`,
|
|
111
159
|
onThreadCreated: (info) => {
|
|
112
160
|
if (info.sourceEventName !== 'guardian.question' || vellumDeliveryId) return;
|
package/src/calls/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
|
|
2
|
-
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped';
|
|
2
|
+
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped' | 'guardian_consult_deferred' | 'guardian_consult_coalesced';
|
|
3
3
|
export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* dependencies at startup via `setVoiceBridgeDeps()`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
|
|
13
14
|
import type { ChannelId } from '../channels/types.js';
|
|
14
15
|
import { getConfig } from '../config/loader.js';
|
|
15
16
|
import type { ServerMessage } from '../daemon/ipc-protocol.js';
|
|
@@ -18,7 +19,6 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
|
|
|
18
19
|
import { resolveChannelCapabilities } from '../daemon/session-runtime-assembly.js';
|
|
19
20
|
import { buildAssistantEvent } from '../runtime/assistant-event.js';
|
|
20
21
|
import { assistantEventHub } from '../runtime/assistant-event-hub.js';
|
|
21
|
-
import { consumeScopedApprovalGrantByToolSignature } from '../memory/scoped-approval-grants.js';
|
|
22
22
|
import { checkIngressForSecrets } from '../security/secret-ingress.js';
|
|
23
23
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
24
24
|
import { IngressBlockedError } from '../util/errors.js';
|
|
@@ -307,6 +307,7 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
|
|
|
307
307
|
strictSideEffects,
|
|
308
308
|
};
|
|
309
309
|
session.setAssistantId(opts.assistantId ?? 'self');
|
|
310
|
+
session.callSessionId = opts.callSessionId;
|
|
310
311
|
session.setGuardianContext(opts.guardianContext ?? null);
|
|
311
312
|
session.setCommandIntent(null);
|
|
312
313
|
session.setTurnChannelContext({
|
|
@@ -345,42 +346,56 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
|
|
|
345
346
|
const autoDeny = !isGuardian;
|
|
346
347
|
const autoAllow = isGuardian;
|
|
347
348
|
let lastError: string | null = null;
|
|
348
|
-
session.updateClient((msg: ServerMessage) => {
|
|
349
|
+
session.updateClient(async (msg: ServerMessage) => {
|
|
349
350
|
if (msg.type === 'confirmation_request') {
|
|
350
351
|
if (autoDeny) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
352
|
+
// Non-guardian voice callers have no interactive approval UI.
|
|
353
|
+
// The pre-exec gate (tool-approval-handler.ts) handles grant
|
|
354
|
+
// consumption with retry for tool execution confirmations, but
|
|
355
|
+
// some confirmation_request events originate from proxy/network
|
|
356
|
+
// paths (e.g. PermissionPrompter in createProxyApprovalCallback)
|
|
357
|
+
// that bypass the pre-exec gate. We do a single sync lookup here
|
|
358
|
+
// (maxWaitMs: 0) since the primary retry path is in the pre-exec
|
|
359
|
+
// gate; this secondary path just needs a quick check.
|
|
360
|
+
try {
|
|
361
|
+
const inputDigest = computeToolApprovalDigest(msg.toolName, msg.input);
|
|
362
|
+
const consumeResult = await consumeGrantForInvocation({
|
|
363
|
+
requestId: msg.requestId,
|
|
364
|
+
toolName: msg.toolName,
|
|
365
|
+
inputDigest,
|
|
366
|
+
consumingRequestId: msg.requestId,
|
|
367
|
+
assistantId: opts.assistantId ?? 'self',
|
|
368
|
+
executionChannel: 'voice',
|
|
369
|
+
conversationId: opts.conversationId,
|
|
370
|
+
callSessionId: opts.callSessionId,
|
|
371
|
+
requesterExternalUserId: opts.guardianContext?.requesterExternalUserId,
|
|
372
|
+
}, { maxWaitMs: 0 });
|
|
373
|
+
|
|
374
|
+
if (consumeResult.ok) {
|
|
375
|
+
log.info(
|
|
376
|
+
{ turnId, toolName: msg.toolName, grantId: consumeResult.grant.id },
|
|
377
|
+
'Consumed scoped grant — allowing non-guardian voice confirmation',
|
|
378
|
+
);
|
|
379
|
+
session.handleConfirmationResponse(
|
|
380
|
+
msg.requestId,
|
|
381
|
+
'allow',
|
|
382
|
+
undefined,
|
|
383
|
+
undefined,
|
|
384
|
+
`Permission approved for "${msg.toolName}": guardian pre-approved via scoped grant.`,
|
|
385
|
+
);
|
|
386
|
+
publishToHub(msg);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
log.error(
|
|
391
|
+
{ err, turnId, toolName: msg.toolName },
|
|
392
|
+
'Error consuming grant in voice confirmation handler — falling through to deny',
|
|
369
393
|
);
|
|
370
|
-
session.handleConfirmationResponse(
|
|
371
|
-
msg.requestId,
|
|
372
|
-
'allow',
|
|
373
|
-
undefined,
|
|
374
|
-
undefined,
|
|
375
|
-
`Permission approved for "${msg.toolName}": guardian pre-approved via scoped grant.`,
|
|
376
|
-
);
|
|
377
|
-
publishToHub(msg);
|
|
378
|
-
return;
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
log.info(
|
|
382
397
|
{ turnId, toolName: msg.toolName },
|
|
383
|
-
'Auto-denying confirmation request for voice turn (no matching scoped grant)',
|
|
398
|
+
'Auto-denying confirmation request for non-guardian voice turn (no matching scoped grant)',
|
|
384
399
|
);
|
|
385
400
|
session.handleConfirmationResponse(
|
|
386
401
|
msg.requestId,
|
|
@@ -429,6 +444,7 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
|
|
|
429
444
|
session.setCommandIntent(null);
|
|
430
445
|
session.setAssistantId('self');
|
|
431
446
|
session.setVoiceCallControlPrompt(null);
|
|
447
|
+
session.callSessionId = undefined;
|
|
432
448
|
// Reset the session's client callback to a no-op so the stale
|
|
433
449
|
// closure doesn't intercept events from future turns on the same session.
|
|
434
450
|
session.updateClient(() => {}, true);
|
package/src/cli/core-commands.ts
CHANGED
|
@@ -649,11 +649,7 @@ export function registerDoctorCommand(program: Command): void {
|
|
|
649
649
|
const { runSandboxDiagnostics } = await import('../tools/terminal/sandbox-diagnostics.js');
|
|
650
650
|
const sandbox = runSandboxDiagnostics();
|
|
651
651
|
log.info(`\n Sandbox: ${sandbox.config.enabled ? 'enabled' : 'disabled'}`);
|
|
652
|
-
log.info(` Backend: ${sandbox.config.backend}`);
|
|
653
652
|
log.info(` Reason: ${sandbox.activeBackendReason}`);
|
|
654
|
-
if (sandbox.config.backend === 'docker') {
|
|
655
|
-
log.info(` Image: ${sandbox.config.dockerImage}`);
|
|
656
|
-
}
|
|
657
653
|
log.info('');
|
|
658
654
|
for (const check of sandbox.checks) {
|
|
659
655
|
if (check.ok) {
|
package/src/cli/mcp.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
import { loadRawConfig } from '../config/loader.js';
|
|
4
|
+
import type { McpConfig, McpServerConfig } from '../config/mcp-schema.js';
|
|
5
|
+
import { getCliLogger } from '../util/logger.js';
|
|
6
|
+
|
|
7
|
+
const log = getCliLogger('cli');
|
|
8
|
+
|
|
9
|
+
export function registerMcpCommand(program: Command): void {
|
|
10
|
+
const mcp = program.command('mcp').description('Manage MCP (Model Context Protocol) servers');
|
|
11
|
+
|
|
12
|
+
mcp
|
|
13
|
+
.command('list')
|
|
14
|
+
.description('List configured MCP servers and their status')
|
|
15
|
+
.option('--json', 'Output as JSON')
|
|
16
|
+
.action((opts: { json?: boolean }) => {
|
|
17
|
+
const raw = loadRawConfig();
|
|
18
|
+
const mcpConfig = raw.mcp as Partial<McpConfig> | undefined;
|
|
19
|
+
const servers = mcpConfig?.servers ?? {};
|
|
20
|
+
const entries = Object.entries(servers) as [string, McpServerConfig][];
|
|
21
|
+
|
|
22
|
+
if (entries.length === 0) {
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
process.stdout.write(JSON.stringify([], null, 2) + '\n');
|
|
25
|
+
} else {
|
|
26
|
+
log.info('No MCP servers configured.');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
const result = entries.map(([id, config]) => ({ id, ...config }));
|
|
33
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log.info(`${entries.length} MCP server(s) configured:\n`);
|
|
38
|
+
for (const [id, cfg] of entries) {
|
|
39
|
+
const enabled = cfg.enabled !== false;
|
|
40
|
+
const transport = cfg.transport;
|
|
41
|
+
const risk = cfg.defaultRiskLevel ?? 'high';
|
|
42
|
+
const status = enabled ? '✓ enabled' : '✗ disabled';
|
|
43
|
+
|
|
44
|
+
log.info(` ${id}`);
|
|
45
|
+
log.info(` Status: ${status}`);
|
|
46
|
+
log.info(` Transport: ${transport?.type ?? 'unknown'}`);
|
|
47
|
+
if (transport?.type === 'stdio') {
|
|
48
|
+
log.info(` Command: ${transport.command} ${(transport.args ?? []).join(' ')}`);
|
|
49
|
+
} else if (transport && 'url' in transport) {
|
|
50
|
+
log.info(` URL: ${transport.url}`);
|
|
51
|
+
}
|
|
52
|
+
log.info(` Risk: ${risk}`);
|
|
53
|
+
if (cfg.allowedTools) log.info(` Allowed: ${cfg.allowedTools.join(', ')}`);
|
|
54
|
+
if (cfg.blockedTools) log.info(` Blocked: ${cfg.blockedTools.join(', ')}`);
|
|
55
|
+
log.info('');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -43,6 +43,72 @@ export function sanitizeUrlForDisplay(rawUrl: unknown): string {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function stringifyConfirmationInputValue(value: unknown): string {
|
|
47
|
+
if (typeof value === 'string') return value;
|
|
48
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
49
|
+
if (value == null) return 'null';
|
|
50
|
+
try {
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
} catch {
|
|
53
|
+
return String(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatConfirmationInputLines(input: Record<string, unknown>): string[] {
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
for (const key of Object.keys(input).sort()) {
|
|
60
|
+
const rawValue = input[key];
|
|
61
|
+
const value = key.toLowerCase().includes('url') && typeof rawValue === 'string'
|
|
62
|
+
? sanitizeUrlForDisplay(rawValue)
|
|
63
|
+
: rawValue;
|
|
64
|
+
const rendered = stringifyConfirmationInputValue(value);
|
|
65
|
+
const renderedLines = rendered.split('\n');
|
|
66
|
+
if (renderedLines.length === 0) {
|
|
67
|
+
lines.push(`${key}:`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
lines.push(`${key}: ${renderedLines[0]}`);
|
|
71
|
+
for (const continuation of renderedLines.slice(1)) {
|
|
72
|
+
lines.push(` ${continuation}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatConfirmationCommandPreview(req: Pick<ConfirmationRequest, 'toolName' | 'input'>): string {
|
|
79
|
+
if (req.toolName === 'bash' || req.toolName === 'host_bash') {
|
|
80
|
+
return String(req.input.command ?? '');
|
|
81
|
+
}
|
|
82
|
+
if (req.toolName === 'file_read' || req.toolName === 'host_file_read') {
|
|
83
|
+
return `read ${req.input.path ?? ''}`;
|
|
84
|
+
}
|
|
85
|
+
if (req.toolName === 'file_write' || req.toolName === 'host_file_write') {
|
|
86
|
+
return `write ${req.input.path ?? ''}`;
|
|
87
|
+
}
|
|
88
|
+
if (req.toolName === 'file_edit' || req.toolName === 'host_file_edit') {
|
|
89
|
+
return `edit ${req.input.path ?? ''}`;
|
|
90
|
+
}
|
|
91
|
+
if (req.toolName === 'web_fetch') {
|
|
92
|
+
return `fetch ${sanitizeUrlForDisplay(req.input.url ?? '')}`;
|
|
93
|
+
}
|
|
94
|
+
if (req.toolName === 'browser_navigate') {
|
|
95
|
+
return `navigate ${sanitizeUrlForDisplay(req.input.url ?? '')}`;
|
|
96
|
+
}
|
|
97
|
+
if (req.toolName === 'browser_close') {
|
|
98
|
+
return req.input.close_all_pages ? 'close all browser pages' : 'close browser page';
|
|
99
|
+
}
|
|
100
|
+
if (req.toolName === 'browser_click') {
|
|
101
|
+
return `click ${req.input.element_id ?? req.input.selector ?? ''}`;
|
|
102
|
+
}
|
|
103
|
+
if (req.toolName === 'browser_type') {
|
|
104
|
+
return `type into ${req.input.element_id ?? req.input.selector ?? ''}`;
|
|
105
|
+
}
|
|
106
|
+
if (req.toolName === 'browser_press_key') {
|
|
107
|
+
return `press "${req.input.key ?? ''}"`;
|
|
108
|
+
}
|
|
109
|
+
return req.toolName;
|
|
110
|
+
}
|
|
111
|
+
|
|
46
112
|
|
|
47
113
|
export async function startCli(): Promise<void> {
|
|
48
114
|
const socketPath = getSocketPath();
|
|
@@ -138,48 +204,24 @@ export async function startCli(): Promise<void> {
|
|
|
138
204
|
return false;
|
|
139
205
|
}
|
|
140
206
|
|
|
141
|
-
function formatCommandPreview(req: ConfirmationRequest): string {
|
|
142
|
-
if (req.toolName === 'bash') {
|
|
143
|
-
return String(req.input.command ?? '');
|
|
144
|
-
}
|
|
145
|
-
if (req.toolName === 'file_read') {
|
|
146
|
-
return `read ${req.input.path ?? ''}`;
|
|
147
|
-
}
|
|
148
|
-
if (req.toolName === 'file_write') {
|
|
149
|
-
return `write ${req.input.path ?? ''}`;
|
|
150
|
-
}
|
|
151
|
-
if (req.toolName === 'web_fetch') {
|
|
152
|
-
return `fetch ${sanitizeUrlForDisplay(req.input.url ?? '')}`;
|
|
153
|
-
}
|
|
154
|
-
if (req.toolName === 'browser_navigate') {
|
|
155
|
-
return `navigate ${sanitizeUrlForDisplay(req.input.url ?? '')}`;
|
|
156
|
-
}
|
|
157
|
-
if (req.toolName === 'browser_close') {
|
|
158
|
-
return req.input.close_all_pages ? 'close all browser pages' : 'close browser page';
|
|
159
|
-
}
|
|
160
|
-
if (req.toolName === 'browser_click') {
|
|
161
|
-
return `click ${req.input.element_id ?? req.input.selector ?? ''}`;
|
|
162
|
-
}
|
|
163
|
-
if (req.toolName === 'browser_type') {
|
|
164
|
-
return `type into ${req.input.element_id ?? req.input.selector ?? ''}`;
|
|
165
|
-
}
|
|
166
|
-
if (req.toolName === 'browser_press_key') {
|
|
167
|
-
return `press "${req.input.key ?? ''}"`;
|
|
168
|
-
}
|
|
169
|
-
return `${req.toolName}: ${truncate(JSON.stringify(req.input), 80)}`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
207
|
function renderConfirmationPrompt(req: ConfirmationRequest): void {
|
|
173
|
-
const preview =
|
|
208
|
+
const preview = formatConfirmationCommandPreview(req);
|
|
209
|
+
const inputLines = formatConfirmationInputLines(req.input);
|
|
174
210
|
process.stdout.write('\n');
|
|
175
211
|
process.stdout.write(`\u250C ${req.toolName}: ${preview}\n`);
|
|
176
212
|
process.stdout.write(`\u2502 Risk: ${req.riskLevel}${req.sandboxed ? ' [sandboxed]' : ''}\n`);
|
|
177
213
|
if (req.executionTarget) {
|
|
178
214
|
process.stdout.write(`\u2502 Target: ${req.executionTarget}\n`);
|
|
179
215
|
}
|
|
216
|
+
if (inputLines.length > 0) {
|
|
217
|
+
process.stdout.write(`\u2502\n`);
|
|
218
|
+
for (const line of inputLines) {
|
|
219
|
+
process.stdout.write(`\u2502 ${line}\n`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
180
222
|
if (req.diff) {
|
|
181
223
|
const diffOutput = req.diff.isNewFile
|
|
182
|
-
? formatNewFileDiff(req.diff.newContent, req.diff.filePath)
|
|
224
|
+
? formatNewFileDiff(req.diff.newContent, req.diff.filePath, null)
|
|
183
225
|
: formatDiff(req.diff.oldContent, req.diff.newContent, req.diff.filePath);
|
|
184
226
|
if (diffOutput) {
|
|
185
227
|
process.stdout.write(`\u2502\n`);
|
|
@@ -506,7 +548,7 @@ export async function startCli(): Promise<void> {
|
|
|
506
548
|
toolStreaming = false;
|
|
507
549
|
if (msg.diff) {
|
|
508
550
|
const diffOutput = msg.diff.isNewFile
|
|
509
|
-
? formatNewFileDiff(msg.diff.newContent, msg.diff.filePath)
|
|
551
|
+
? formatNewFileDiff(msg.diff.newContent, msg.diff.filePath, null)
|
|
510
552
|
: formatDiff(msg.diff.oldContent, msg.diff.newContent, msg.diff.filePath);
|
|
511
553
|
if (diffOutput) {
|
|
512
554
|
process.stdout.write(diffOutput);
|