@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -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-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -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 +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -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/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- 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 +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared request-kind and instruction-mode resolver for guardian.question signals.
|
|
3
|
+
*
|
|
4
|
+
* Explicit request kinds provide a stable contract between producers and
|
|
5
|
+
* notification rendering logic, avoiding implicit inference from incidental
|
|
6
|
+
* fields like `toolName`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const GUARDIAN_QUESTION_REQUEST_KINDS = {
|
|
10
|
+
pending_question: 'pending_question',
|
|
11
|
+
tool_approval: 'tool_approval',
|
|
12
|
+
tool_grant_request: 'tool_grant_request',
|
|
13
|
+
access_request: 'access_request',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type GuardianQuestionRequestKind = keyof typeof GUARDIAN_QUESTION_REQUEST_KINDS;
|
|
17
|
+
export type GuardianQuestionInstructionMode = 'approval' | 'answer';
|
|
18
|
+
|
|
19
|
+
interface GuardianRequestKindModeConfig {
|
|
20
|
+
defaultMode: GuardianQuestionInstructionMode;
|
|
21
|
+
modeWhenToolNamePresent?: GuardianQuestionInstructionMode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const REQUEST_KIND_MODE_CONFIG: Record<GuardianQuestionRequestKind, GuardianRequestKindModeConfig> = {
|
|
25
|
+
pending_question: {
|
|
26
|
+
defaultMode: 'answer',
|
|
27
|
+
modeWhenToolNamePresent: 'approval',
|
|
28
|
+
},
|
|
29
|
+
tool_approval: {
|
|
30
|
+
defaultMode: 'approval',
|
|
31
|
+
},
|
|
32
|
+
tool_grant_request: {
|
|
33
|
+
defaultMode: 'approval',
|
|
34
|
+
},
|
|
35
|
+
access_request: {
|
|
36
|
+
defaultMode: 'approval',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface GuardianQuestionPayloadBase {
|
|
41
|
+
requestId: string;
|
|
42
|
+
requestCode: string;
|
|
43
|
+
questionText: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface GuardianQuestionPayloadBaseWithDiscriminator extends GuardianQuestionPayloadBase {
|
|
47
|
+
requestKind: GuardianQuestionRequestKind;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GuardianRequestModeInput {
|
|
52
|
+
kind: unknown;
|
|
53
|
+
toolName?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GuardianRequestTextInput {
|
|
57
|
+
requestCode: string;
|
|
58
|
+
questionText?: string | null;
|
|
59
|
+
toolName?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type GuardianDisambiguationCategory = 'questions' | 'approvals';
|
|
63
|
+
|
|
64
|
+
interface GuardianModeTextConfig {
|
|
65
|
+
invalidActionWithCode: (requestCode: string) => string;
|
|
66
|
+
invalidActionWithoutCode: string;
|
|
67
|
+
buildCodeOnlyHeader: (request: GuardianRequestTextInput) => string;
|
|
68
|
+
buildCodeOnlyDetailLine: (request: GuardianRequestTextInput) => string | null;
|
|
69
|
+
buildDisambiguationLabel: (request: Pick<GuardianRequestTextInput, 'questionText' | 'toolName'>) => string;
|
|
70
|
+
disambiguationCategory: GuardianDisambiguationCategory;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MODE_TEXT_CONFIG: Record<GuardianQuestionInstructionMode, GuardianModeTextConfig> = {
|
|
74
|
+
answer: {
|
|
75
|
+
invalidActionWithCode: (requestCode) =>
|
|
76
|
+
`I found request ${requestCode}, but I still need your answer. Reply "${requestCode} <your answer>".`,
|
|
77
|
+
invalidActionWithoutCode:
|
|
78
|
+
"I couldn't determine your answer. Reply with the request code followed by your answer (e.g., \"ABC123 3pm works\").",
|
|
79
|
+
buildCodeOnlyHeader: (request) => `I found question ${request.requestCode}.`,
|
|
80
|
+
buildCodeOnlyDetailLine: (request) => request.questionText ? `Question: ${request.questionText}` : null,
|
|
81
|
+
buildDisambiguationLabel: (request) => request.questionText ?? 'question',
|
|
82
|
+
disambiguationCategory: 'questions',
|
|
83
|
+
},
|
|
84
|
+
approval: {
|
|
85
|
+
invalidActionWithCode: (requestCode) =>
|
|
86
|
+
`I found request ${requestCode}, but I need to know your decision. Reply "${requestCode} approve" or "${requestCode} reject".`,
|
|
87
|
+
invalidActionWithoutCode:
|
|
88
|
+
"I couldn't determine your intended action. Reply with the request code followed by 'approve' or 'reject' (e.g., \"ABC123 approve\").",
|
|
89
|
+
buildCodeOnlyHeader: (request) => `I found request ${request.requestCode} for ${request.toolName ?? 'an action'}.`,
|
|
90
|
+
buildCodeOnlyDetailLine: (request) => request.questionText ? `Details: ${request.questionText}` : null,
|
|
91
|
+
buildDisambiguationLabel: (request) => request.toolName ?? request.questionText ?? 'action',
|
|
92
|
+
disambiguationCategory: 'approvals',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export interface PendingQuestionGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
|
|
97
|
+
requestKind: 'pending_question';
|
|
98
|
+
callSessionId: string;
|
|
99
|
+
activeGuardianRequestCount: number;
|
|
100
|
+
/**
|
|
101
|
+
* Voice tool-approval requests are persisted as pending_question with tool
|
|
102
|
+
* metadata so they still route through pending-question resolution.
|
|
103
|
+
*/
|
|
104
|
+
toolName?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ToolApprovalGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
|
|
108
|
+
requestKind: 'tool_approval';
|
|
109
|
+
toolName: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ToolGrantGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
|
|
113
|
+
requestKind: 'tool_grant_request';
|
|
114
|
+
toolName: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AccessRequestGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
|
|
118
|
+
requestKind: 'access_request';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type GuardianQuestionPayload =
|
|
122
|
+
| PendingQuestionGuardianPayload
|
|
123
|
+
| ToolApprovalGuardianPayload
|
|
124
|
+
| ToolGrantGuardianPayload
|
|
125
|
+
| AccessRequestGuardianPayload;
|
|
126
|
+
|
|
127
|
+
export interface GuardianQuestionModeResolution {
|
|
128
|
+
mode: GuardianQuestionInstructionMode;
|
|
129
|
+
requestKind: GuardianQuestionRequestKind | null;
|
|
130
|
+
legacyFallbackUsed: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function nonEmptyString(value: unknown): string | null {
|
|
134
|
+
if (typeof value !== 'string') return null;
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseGuardianQuestionRequestKind(
|
|
140
|
+
payload: Record<string, unknown>,
|
|
141
|
+
): GuardianQuestionRequestKind | null {
|
|
142
|
+
const raw = nonEmptyString(payload.requestKind);
|
|
143
|
+
if (!raw) return null;
|
|
144
|
+
|
|
145
|
+
switch (raw) {
|
|
146
|
+
case 'pending_question':
|
|
147
|
+
case 'tool_approval':
|
|
148
|
+
case 'tool_grant_request':
|
|
149
|
+
case 'access_request':
|
|
150
|
+
return raw;
|
|
151
|
+
default:
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseBasePayload(payload: Record<string, unknown>): GuardianQuestionPayloadBase | null {
|
|
157
|
+
const requestId = nonEmptyString(payload.requestId);
|
|
158
|
+
const requestCode = nonEmptyString(payload.requestCode);
|
|
159
|
+
const questionText = nonEmptyString(payload.questionText);
|
|
160
|
+
if (!requestId || !requestCode || !questionText) return null;
|
|
161
|
+
return { requestId, requestCode, questionText };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse a guardian.question context payload into a strict discriminated union.
|
|
166
|
+
*
|
|
167
|
+
* Returns null when required fields for the declared requestKind are missing,
|
|
168
|
+
* or when requestKind is absent/unknown.
|
|
169
|
+
*/
|
|
170
|
+
export function parseGuardianQuestionPayload(
|
|
171
|
+
payload: Record<string, unknown>,
|
|
172
|
+
): GuardianQuestionPayload | null {
|
|
173
|
+
const requestKind = parseGuardianQuestionRequestKind(payload);
|
|
174
|
+
if (!requestKind) return null;
|
|
175
|
+
|
|
176
|
+
const base = parseBasePayload(payload);
|
|
177
|
+
if (!base) return null;
|
|
178
|
+
|
|
179
|
+
switch (requestKind) {
|
|
180
|
+
case 'pending_question': {
|
|
181
|
+
const callSessionId = nonEmptyString(payload.callSessionId);
|
|
182
|
+
const activeGuardianRequestCount = typeof payload.activeGuardianRequestCount === 'number'
|
|
183
|
+
? payload.activeGuardianRequestCount
|
|
184
|
+
: undefined;
|
|
185
|
+
const toolName = nonEmptyString(payload.toolName);
|
|
186
|
+
if (!callSessionId || activeGuardianRequestCount === undefined || Number.isNaN(activeGuardianRequestCount)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const pendingQuestionPayload: PendingQuestionGuardianPayload = {
|
|
190
|
+
requestKind,
|
|
191
|
+
...base,
|
|
192
|
+
callSessionId,
|
|
193
|
+
activeGuardianRequestCount,
|
|
194
|
+
};
|
|
195
|
+
if (toolName) {
|
|
196
|
+
pendingQuestionPayload.toolName = toolName;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
...pendingQuestionPayload,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
case 'tool_approval':
|
|
203
|
+
case 'tool_grant_request': {
|
|
204
|
+
const toolName = nonEmptyString(payload.toolName);
|
|
205
|
+
if (!toolName) return null;
|
|
206
|
+
return {
|
|
207
|
+
requestKind,
|
|
208
|
+
...base,
|
|
209
|
+
toolName,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case 'access_request':
|
|
213
|
+
return {
|
|
214
|
+
requestKind,
|
|
215
|
+
...base,
|
|
216
|
+
};
|
|
217
|
+
default:
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function resolveGuardianInstructionModeForRequestKind(
|
|
223
|
+
requestKind: GuardianQuestionRequestKind,
|
|
224
|
+
toolName?: string | null,
|
|
225
|
+
): GuardianQuestionInstructionMode {
|
|
226
|
+
const config = REQUEST_KIND_MODE_CONFIG[requestKind];
|
|
227
|
+
const normalizedToolName = nonEmptyString(toolName);
|
|
228
|
+
if (normalizedToolName && config.modeWhenToolNamePresent) {
|
|
229
|
+
return config.modeWhenToolNamePresent;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return config.defaultMode;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function resolveGuardianInstructionModeFromFields(
|
|
236
|
+
requestKindValue: unknown,
|
|
237
|
+
toolNameValue: unknown,
|
|
238
|
+
): { requestKind: GuardianQuestionRequestKind; mode: GuardianQuestionInstructionMode } | null {
|
|
239
|
+
const requestKind = parseGuardianQuestionRequestKind({ requestKind: requestKindValue });
|
|
240
|
+
if (!requestKind) return null;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
requestKind,
|
|
244
|
+
mode: resolveGuardianInstructionModeForRequestKind(requestKind, nonEmptyString(toolNameValue)),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function resolveGuardianInstructionModeForRequest(
|
|
249
|
+
request?: GuardianRequestModeInput | null,
|
|
250
|
+
): GuardianQuestionInstructionMode {
|
|
251
|
+
if (!request) return 'approval';
|
|
252
|
+
const modeResolution = resolveGuardianInstructionModeFromFields(request.kind, request.toolName);
|
|
253
|
+
if (!modeResolution) return 'approval';
|
|
254
|
+
return modeResolution.mode;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getModeTextConfig(mode: GuardianQuestionInstructionMode): GuardianModeTextConfig {
|
|
258
|
+
return MODE_TEXT_CONFIG[mode];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildGuardianReplyDirective(
|
|
262
|
+
requestCode: string,
|
|
263
|
+
mode: GuardianQuestionInstructionMode,
|
|
264
|
+
): string {
|
|
265
|
+
switch (mode) {
|
|
266
|
+
case 'approval':
|
|
267
|
+
return `Reply "${requestCode} approve" or "${requestCode} reject".`;
|
|
268
|
+
case 'answer':
|
|
269
|
+
return `Reply "${requestCode} <your answer>".`;
|
|
270
|
+
default: {
|
|
271
|
+
const _never: never = mode;
|
|
272
|
+
return _never;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function buildGuardianRequestCodeInstruction(
|
|
278
|
+
requestCode: string,
|
|
279
|
+
mode: GuardianQuestionInstructionMode,
|
|
280
|
+
): string {
|
|
281
|
+
return `Reference code: ${requestCode}. ${buildGuardianReplyDirective(requestCode, mode)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function buildGuardianInvalidActionReply(
|
|
285
|
+
mode: GuardianQuestionInstructionMode,
|
|
286
|
+
requestCode?: string,
|
|
287
|
+
): string {
|
|
288
|
+
const config = getModeTextConfig(mode);
|
|
289
|
+
if (requestCode) return config.invalidActionWithCode(requestCode);
|
|
290
|
+
return config.invalidActionWithoutCode;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function buildGuardianCodeOnlyClarification(
|
|
294
|
+
mode: GuardianQuestionInstructionMode,
|
|
295
|
+
request: GuardianRequestTextInput,
|
|
296
|
+
): string {
|
|
297
|
+
const config = getModeTextConfig(mode);
|
|
298
|
+
const lines = [
|
|
299
|
+
config.buildCodeOnlyHeader(request),
|
|
300
|
+
];
|
|
301
|
+
const detailLine = config.buildCodeOnlyDetailLine(request);
|
|
302
|
+
if (detailLine) {
|
|
303
|
+
lines.push(detailLine);
|
|
304
|
+
}
|
|
305
|
+
lines.push(buildGuardianReplyDirective(request.requestCode, mode));
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function buildGuardianDisambiguationLabel(
|
|
310
|
+
mode: GuardianQuestionInstructionMode,
|
|
311
|
+
request: Pick<GuardianRequestTextInput, 'questionText' | 'toolName'>,
|
|
312
|
+
): string {
|
|
313
|
+
return getModeTextConfig(mode).buildDisambiguationLabel(request);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function buildGuardianDisambiguationExample(
|
|
317
|
+
mode: GuardianQuestionInstructionMode,
|
|
318
|
+
requestCode: string,
|
|
319
|
+
): string {
|
|
320
|
+
const category = getModeTextConfig(mode).disambiguationCategory;
|
|
321
|
+
const replyDirective = buildGuardianReplyDirective(requestCode, mode);
|
|
322
|
+
return `For ${category}: ${replyDirective.replace(/^Reply/, 'reply')}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function hasGuardianRequestCodeInstruction(
|
|
326
|
+
text: string | undefined,
|
|
327
|
+
requestCode: string,
|
|
328
|
+
mode: GuardianQuestionInstructionMode,
|
|
329
|
+
): boolean {
|
|
330
|
+
if (typeof text !== 'string') return false;
|
|
331
|
+
const upper = text.toUpperCase();
|
|
332
|
+
const normalizedCode = requestCode.toUpperCase();
|
|
333
|
+
|
|
334
|
+
switch (mode) {
|
|
335
|
+
case 'approval':
|
|
336
|
+
return upper.includes(`${normalizedCode} APPROVE`) && upper.includes(`${normalizedCode} REJECT`);
|
|
337
|
+
case 'answer': {
|
|
338
|
+
const hasAnswerInstruction = upper.includes(`${normalizedCode} <YOUR ANSWER>`);
|
|
339
|
+
const hasApprovalInstruction = upper.includes(`${normalizedCode} APPROVE`) || upper.includes(`${normalizedCode} REJECT`);
|
|
340
|
+
return hasAnswerInstruction && !hasApprovalInstruction;
|
|
341
|
+
}
|
|
342
|
+
default: {
|
|
343
|
+
const _never: never = mode;
|
|
344
|
+
return _never;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function escapeRegExp(value: string): string {
|
|
350
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeInstructionText(value: string): string {
|
|
354
|
+
return value
|
|
355
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
356
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
357
|
+
.trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function stripConflictingGuardianRequestInstructions(
|
|
361
|
+
text: string,
|
|
362
|
+
requestCode: string,
|
|
363
|
+
mode: GuardianQuestionInstructionMode,
|
|
364
|
+
): string {
|
|
365
|
+
const escapedCode = escapeRegExp(requestCode);
|
|
366
|
+
const approvalInstructionPattern = new RegExp(
|
|
367
|
+
`(?:Reference\\s+code:\\s*${escapedCode}\\.?\\s*)?Reply\\s+"${escapedCode}\\s+approve"\\s+or\\s+"${escapedCode}\\s+reject"\\.?`,
|
|
368
|
+
'ig',
|
|
369
|
+
);
|
|
370
|
+
const answerInstructionPattern = new RegExp(
|
|
371
|
+
`(?:Reference\\s+code:\\s*${escapedCode}\\.?\\s*)?Reply\\s+"${escapedCode}\\s+<your\\s+answer>"\\.?`,
|
|
372
|
+
'ig',
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const next = mode === 'answer'
|
|
376
|
+
? text.replace(approvalInstructionPattern, '')
|
|
377
|
+
: text.replace(answerInstructionPattern, '');
|
|
378
|
+
|
|
379
|
+
return normalizeInstructionText(next);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Resolve guardian reply instruction mode from request kind.
|
|
384
|
+
*
|
|
385
|
+
* Backward compatibility: if requestKind is missing/unknown, fall back to
|
|
386
|
+
* toolName presence so previously persisted payloads keep working.
|
|
387
|
+
*/
|
|
388
|
+
export function resolveGuardianQuestionInstructionMode(
|
|
389
|
+
payload: Record<string, unknown>,
|
|
390
|
+
): GuardianQuestionModeResolution {
|
|
391
|
+
const parsed = parseGuardianQuestionPayload(payload);
|
|
392
|
+
if (parsed) {
|
|
393
|
+
const parsedToolName = nonEmptyString('toolName' in parsed ? parsed.toolName : null);
|
|
394
|
+
return {
|
|
395
|
+
mode: resolveGuardianInstructionModeForRequestKind(parsed.requestKind, parsedToolName),
|
|
396
|
+
requestKind: parsed.requestKind,
|
|
397
|
+
legacyFallbackUsed: false,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const requestKindResolution = resolveGuardianInstructionModeFromFields(
|
|
402
|
+
payload.requestKind,
|
|
403
|
+
payload.toolName,
|
|
404
|
+
);
|
|
405
|
+
if (requestKindResolution) {
|
|
406
|
+
return {
|
|
407
|
+
mode: requestKindResolution.mode,
|
|
408
|
+
requestKind: requestKindResolution.requestKind,
|
|
409
|
+
legacyFallbackUsed: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const toolName = nonEmptyString(payload.toolName);
|
|
414
|
+
return {
|
|
415
|
+
mode: toolName ? 'approval' : 'answer',
|
|
416
|
+
requestKind: null,
|
|
417
|
+
legacyFallbackUsed: true,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* decision engine route contextually.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { GuardianQuestionPayload } from './guardian-question-mode.js';
|
|
8
|
+
|
|
7
9
|
export interface AttentionHints {
|
|
8
10
|
requiresAction: boolean;
|
|
9
11
|
urgency: 'low' | 'medium' | 'high';
|
|
@@ -14,14 +16,23 @@ export interface AttentionHints {
|
|
|
14
16
|
|
|
15
17
|
export type RoutingIntent = 'single_channel' | 'multi_channel' | 'all_channels';
|
|
16
18
|
|
|
17
|
-
export interface
|
|
19
|
+
export interface NotificationEventContextPayloadMap {
|
|
20
|
+
'guardian.question': GuardianQuestionPayload;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type NotificationContextPayload<TEventName extends string = string> =
|
|
24
|
+
TEventName extends keyof NotificationEventContextPayloadMap
|
|
25
|
+
? NotificationEventContextPayloadMap[TEventName]
|
|
26
|
+
: Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export interface NotificationSignal<TEventName extends string = string> {
|
|
18
29
|
signalId: string;
|
|
19
30
|
assistantId: string;
|
|
20
31
|
createdAt: number; // epoch ms
|
|
21
32
|
sourceChannel: string; // free-form: 'vellum', 'telegram', 'voice', 'scheduler', etc.
|
|
22
33
|
sourceSessionId: string;
|
|
23
|
-
sourceEventName:
|
|
24
|
-
contextPayload:
|
|
34
|
+
sourceEventName: TEventName; // free-form: 'reminder_fired', 'schedule_complete', 'guardian_question', etc.
|
|
35
|
+
contextPayload: NotificationContextPayload<TEventName>;
|
|
25
36
|
attentionHints: AttentionHints;
|
|
26
37
|
/** Routing intent from the source (e.g. reminder). Controls post-decision channel enforcement. */
|
|
27
38
|
routingIntent?: RoutingIntent;
|
|
@@ -776,7 +776,19 @@ export async function generateAllowlistOptions(toolName: string, input: Record<s
|
|
|
776
776
|
return [{ label: '*', description: 'Everything', pattern: '*' }];
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
-
|
|
779
|
+
// Directory-based scope only applies to filesystem and shell tools.
|
|
780
|
+
// All other tools auto-use "everywhere" (the client handles this).
|
|
781
|
+
export const SCOPE_AWARE_TOOLS = new Set([
|
|
782
|
+
'bash', 'host_bash',
|
|
783
|
+
'file_read', 'file_write', 'file_edit',
|
|
784
|
+
'host_file_read', 'host_file_write', 'host_file_edit',
|
|
785
|
+
]);
|
|
786
|
+
|
|
787
|
+
export function generateScopeOptions(workingDir: string, toolName?: string): ScopeOption[] {
|
|
788
|
+
if (toolName && !SCOPE_AWARE_TOOLS.has(toolName)) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
|
|
780
792
|
const home = homedir();
|
|
781
793
|
const options: ScopeOption[] = [];
|
|
782
794
|
|
|
@@ -21,14 +21,25 @@ interface PendingPrompt {
|
|
|
21
21
|
timer: ReturnType<typeof setTimeout>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export type ConfirmationStateCallback = (
|
|
25
|
+
requestId: string,
|
|
26
|
+
state: 'pending' | 'approved' | 'denied' | 'timed_out' | 'resolved_stale',
|
|
27
|
+
source: 'button' | 'inline_nl' | 'auto_deny' | 'timeout' | 'system',
|
|
28
|
+
) => void;
|
|
29
|
+
|
|
24
30
|
export class PermissionPrompter {
|
|
25
31
|
private pending = new Map<string, PendingPrompt>();
|
|
26
32
|
private sendToClient: (msg: ServerMessage) => void;
|
|
33
|
+
private onStateChanged?: ConfirmationStateCallback;
|
|
27
34
|
|
|
28
35
|
constructor(sendToClient: (msg: ServerMessage) => void) {
|
|
29
36
|
this.sendToClient = sendToClient;
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
setOnStateChanged(cb: ConfirmationStateCallback): void {
|
|
40
|
+
this.onStateChanged = cb;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
updateSender(sendToClient: (msg: ServerMessage) => void): void {
|
|
33
44
|
this.sendToClient = sendToClient;
|
|
34
45
|
}
|
|
@@ -60,6 +71,7 @@ export class PermissionPrompter {
|
|
|
60
71
|
const timer = setTimeout(() => {
|
|
61
72
|
this.pending.delete(requestId);
|
|
62
73
|
log.warn({ requestId, toolName }, 'Permission prompt timed out, defaulting to deny');
|
|
74
|
+
this.onStateChanged?.(requestId, 'timed_out', 'timeout');
|
|
63
75
|
resolve({ decision: 'deny' });
|
|
64
76
|
}, timeoutMs);
|
|
65
77
|
|
|
@@ -90,6 +102,8 @@ export class PermissionPrompter {
|
|
|
90
102
|
executionTarget,
|
|
91
103
|
persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
|
|
92
104
|
});
|
|
105
|
+
|
|
106
|
+
this.onStateChanged?.(requestId, 'pending', 'system');
|
|
93
107
|
});
|
|
94
108
|
}
|
|
95
109
|
|
|
@@ -512,6 +512,10 @@ export class AnthropicProvider implements Provider {
|
|
|
512
512
|
)
|
|
513
513
|
: this.client.messages.stream(params, { signal: timeoutSignal });
|
|
514
514
|
|
|
515
|
+
// Track whether we've seen a text content block so we can insert a
|
|
516
|
+
// separator between consecutive text blocks in the same response.
|
|
517
|
+
let hasSeenTextBlock = false;
|
|
518
|
+
|
|
515
519
|
stream.on("text", (text) => {
|
|
516
520
|
onEvent?.({ type: "text_delta", text });
|
|
517
521
|
});
|
|
@@ -527,6 +531,22 @@ export class AnthropicProvider implements Provider {
|
|
|
527
531
|
let pendingInputJsonFlush: ReturnType<typeof setTimeout> | undefined;
|
|
528
532
|
|
|
529
533
|
stream.on("streamEvent", (event) => {
|
|
534
|
+
// Insert a space separator when a new text content block starts
|
|
535
|
+
// after a previous one, so consecutive text blocks don't get
|
|
536
|
+
// concatenated without whitespace (e.g. "sentence.NextSentence").
|
|
537
|
+
// Uses a space instead of \n because the client's MarkdownRenderer
|
|
538
|
+
// can collapse soft line breaks (\n) within a paragraph.
|
|
539
|
+
if (event.type === 'content_block_start' && event.content_block.type === 'text') {
|
|
540
|
+
if (hasSeenTextBlock) {
|
|
541
|
+
onEvent?.({ type: "text_delta", text: " " });
|
|
542
|
+
}
|
|
543
|
+
hasSeenTextBlock = true;
|
|
544
|
+
} else if (event.type === 'content_block_start') {
|
|
545
|
+
// Reset on non-text blocks so that text separated by tool_use
|
|
546
|
+
// (text -> tool_use -> text) doesn't get a spurious leading space
|
|
547
|
+
// in the second text segment.
|
|
548
|
+
hasSeenTextBlock = false;
|
|
549
|
+
}
|
|
530
550
|
if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
|
|
531
551
|
currentStreamingToolName = event.content_block.name;
|
|
532
552
|
accumulatedInputJson = '';
|
|
@@ -107,10 +107,22 @@ export function extractText(response: ProviderResponse): string {
|
|
|
107
107
|
* Extract all text blocks from a ProviderResponse and join them.
|
|
108
108
|
*/
|
|
109
109
|
export function extractAllText(response: ProviderResponse): string {
|
|
110
|
-
|
|
110
|
+
const parts = response.content
|
|
111
111
|
.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
|
|
112
|
-
.map((b) => b.text)
|
|
113
|
-
|
|
112
|
+
.map((b) => b.text);
|
|
113
|
+
// Join consecutive text blocks with a space, but skip the separator when
|
|
114
|
+
// either side already has whitespace (avoids double-spacing).
|
|
115
|
+
let result = parts[0] ?? '';
|
|
116
|
+
for (let i = 1; i < parts.length; i++) {
|
|
117
|
+
const prev = result[result.length - 1];
|
|
118
|
+
const next = parts[i][0];
|
|
119
|
+
if (prev && next && prev !== ' ' && prev !== '\n' && prev !== '\t' &&
|
|
120
|
+
next !== ' ' && next !== '\n' && next !== '\t') {
|
|
121
|
+
result += ' ';
|
|
122
|
+
}
|
|
123
|
+
result += parts[i];
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
/**
|