@vellumai/assistant 0.4.29 → 0.4.30
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 +39 -37
- package/README.md +5 -6
- package/docs/runbook-trusted-contacts.md +79 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/calls/relay-server.ts +39 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +42 -35
- package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-skills/weather/TOOLS.json +4 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/contacts/contact-store.ts +195 -4
- package/src/contacts/types.ts +26 -0
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/guardian-actions.ts +20 -87
- package/src/daemon/handlers/sessions.ts +0 -1
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +1 -0
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +12 -0
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +127 -0
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +20 -49
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +196 -28
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +76 -0
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
|
@@ -9,22 +9,34 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
getAssistantContactMetadata,
|
|
13
|
+
getChannelById,
|
|
12
14
|
getContact,
|
|
13
15
|
listContacts,
|
|
14
16
|
mergeContacts,
|
|
15
17
|
searchContacts,
|
|
16
18
|
updateChannelStatus,
|
|
19
|
+
upsertAssistantContactMetadata,
|
|
17
20
|
upsertContact,
|
|
21
|
+
validateSpeciesMetadata,
|
|
18
22
|
} from "../../contacts/contact-store.js";
|
|
19
23
|
import type {
|
|
24
|
+
AssistantSpecies,
|
|
20
25
|
ChannelPolicy,
|
|
21
26
|
ChannelStatus,
|
|
22
27
|
ContactRole,
|
|
28
|
+
ContactType,
|
|
23
29
|
} from "../../contacts/types.js";
|
|
24
30
|
import { httpError } from "../http-errors.js";
|
|
25
31
|
|
|
32
|
+
const VALID_CONTACT_TYPES: readonly ContactType[] = ["human", "assistant"];
|
|
33
|
+
const VALID_ASSISTANT_SPECIES: readonly AssistantSpecies[] = [
|
|
34
|
+
"vellum",
|
|
35
|
+
"openclaw",
|
|
36
|
+
];
|
|
37
|
+
|
|
26
38
|
/**
|
|
27
|
-
* GET /v1/contacts?limit=50&role=guardian
|
|
39
|
+
* GET /v1/contacts?limit=50&role=guardian&contactType=human
|
|
28
40
|
*
|
|
29
41
|
* Also supports search query params: query, channelAddress, channelType, relationship.
|
|
30
42
|
* When any search param is provided, delegates to searchContacts() instead of listContacts().
|
|
@@ -32,12 +44,26 @@ import { httpError } from "../http-errors.js";
|
|
|
32
44
|
export function handleListContacts(url: URL, assistantId: string): Response {
|
|
33
45
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
34
46
|
const role = url.searchParams.get("role") as ContactRole | null;
|
|
47
|
+
const contactTypeParam = url.searchParams.get("contactType");
|
|
35
48
|
const query = url.searchParams.get("query");
|
|
36
49
|
const channelAddress = url.searchParams.get("channelAddress");
|
|
37
50
|
const channelType = url.searchParams.get("channelType");
|
|
38
51
|
const relationship = url.searchParams.get("relationship");
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
if (contactTypeParam && !isContactType(contactTypeParam)) {
|
|
54
|
+
return httpError(
|
|
55
|
+
"BAD_REQUEST",
|
|
56
|
+
`Invalid contactType "${contactTypeParam}". Must be one of: ${VALID_CONTACT_TYPES.join(", ")}`,
|
|
57
|
+
400,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hasSearchParams =
|
|
62
|
+
query || channelAddress || channelType || relationship;
|
|
63
|
+
|
|
64
|
+
const contactType = contactTypeParam
|
|
65
|
+
? (contactTypeParam as ContactType)
|
|
66
|
+
: undefined;
|
|
41
67
|
|
|
42
68
|
if (hasSearchParams) {
|
|
43
69
|
const contacts = searchContacts({
|
|
@@ -46,12 +72,19 @@ export function handleListContacts(url: URL, assistantId: string): Response {
|
|
|
46
72
|
channelAddress: channelAddress ?? undefined,
|
|
47
73
|
channelType: channelType ?? undefined,
|
|
48
74
|
relationship: relationship ?? undefined,
|
|
75
|
+
role: role ?? undefined,
|
|
76
|
+
contactType,
|
|
49
77
|
limit,
|
|
50
78
|
});
|
|
51
79
|
return Response.json({ ok: true, contacts });
|
|
52
80
|
}
|
|
53
81
|
|
|
54
|
-
const contacts = listContacts(
|
|
82
|
+
const contacts = listContacts(
|
|
83
|
+
assistantId,
|
|
84
|
+
limit,
|
|
85
|
+
role ?? undefined,
|
|
86
|
+
contactType,
|
|
87
|
+
);
|
|
55
88
|
return Response.json({ ok: true, contacts });
|
|
56
89
|
}
|
|
57
90
|
|
|
@@ -66,7 +99,15 @@ export function handleGetContact(
|
|
|
66
99
|
if (!contact) {
|
|
67
100
|
return httpError("NOT_FOUND", `Contact "${contactId}" not found`, 404);
|
|
68
101
|
}
|
|
69
|
-
|
|
102
|
+
const assistantMeta =
|
|
103
|
+
contact.contactType === "assistant"
|
|
104
|
+
? getAssistantContactMetadata(contact.id)
|
|
105
|
+
: undefined;
|
|
106
|
+
return Response.json({
|
|
107
|
+
ok: true,
|
|
108
|
+
contact,
|
|
109
|
+
assistantMetadata: assistantMeta ?? undefined,
|
|
110
|
+
});
|
|
70
111
|
}
|
|
71
112
|
|
|
72
113
|
/**
|
|
@@ -91,8 +132,37 @@ export async function handleMergeContacts(
|
|
|
91
132
|
}
|
|
92
133
|
}
|
|
93
134
|
|
|
135
|
+
const VALID_CHANNEL_STATUSES: readonly ChannelStatus[] = [
|
|
136
|
+
"active",
|
|
137
|
+
"pending",
|
|
138
|
+
"revoked",
|
|
139
|
+
"blocked",
|
|
140
|
+
"unverified",
|
|
141
|
+
];
|
|
142
|
+
const VALID_CHANNEL_POLICIES: readonly ChannelPolicy[] = [
|
|
143
|
+
"allow",
|
|
144
|
+
"deny",
|
|
145
|
+
"escalate",
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
function isContactType(value: string): value is ContactType {
|
|
149
|
+
return (VALID_CONTACT_TYPES as readonly string[]).includes(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isAssistantSpecies(value: string): value is AssistantSpecies {
|
|
153
|
+
return (VALID_ASSISTANT_SPECIES as readonly string[]).includes(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isChannelStatus(value: string): value is ChannelStatus {
|
|
157
|
+
return (VALID_CHANNEL_STATUSES as readonly string[]).includes(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isChannelPolicy(value: string): value is ChannelPolicy {
|
|
161
|
+
return (VALID_CHANNEL_POLICIES as readonly string[]).includes(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
94
164
|
/**
|
|
95
|
-
* POST /v1/contacts { displayName, id?, relationship?, importance?, ... }
|
|
165
|
+
* POST /v1/contacts { displayName, id?, relationship?, importance?, contactType?, assistantMetadata?, ... }
|
|
96
166
|
*/
|
|
97
167
|
export async function handleUpsertContact(
|
|
98
168
|
req: Request,
|
|
@@ -106,7 +176,20 @@ export async function handleUpsertContact(
|
|
|
106
176
|
responseExpectation?: string;
|
|
107
177
|
preferredTone?: string;
|
|
108
178
|
role?: string;
|
|
109
|
-
|
|
179
|
+
contactType?: string;
|
|
180
|
+
assistantMetadata?: {
|
|
181
|
+
species: string;
|
|
182
|
+
metadata?: Record<string, unknown>;
|
|
183
|
+
};
|
|
184
|
+
channels?: Array<{
|
|
185
|
+
type: string;
|
|
186
|
+
address: string;
|
|
187
|
+
isPrimary?: boolean;
|
|
188
|
+
status?: string;
|
|
189
|
+
policy?: string;
|
|
190
|
+
externalUserId?: string;
|
|
191
|
+
externalChatId?: string;
|
|
192
|
+
}>;
|
|
110
193
|
};
|
|
111
194
|
|
|
112
195
|
if (
|
|
@@ -135,6 +218,81 @@ export async function handleUpsertContact(
|
|
|
135
218
|
);
|
|
136
219
|
}
|
|
137
220
|
|
|
221
|
+
if (body.contactType !== undefined && !isContactType(body.contactType)) {
|
|
222
|
+
return httpError(
|
|
223
|
+
"BAD_REQUEST",
|
|
224
|
+
`Invalid contactType "${body.contactType}". Must be one of: ${VALID_CONTACT_TYPES.join(", ")}`,
|
|
225
|
+
400,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (body.contactType === "assistant") {
|
|
230
|
+
if (!body.assistantMetadata) {
|
|
231
|
+
return httpError(
|
|
232
|
+
"BAD_REQUEST",
|
|
233
|
+
'assistantMetadata is required when contactType is "assistant"',
|
|
234
|
+
400,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (!isAssistantSpecies(body.assistantMetadata.species)) {
|
|
238
|
+
return httpError(
|
|
239
|
+
"BAD_REQUEST",
|
|
240
|
+
`Invalid species "${body.assistantMetadata.species}". Must be one of: ${VALID_ASSISTANT_SPECIES.join(", ")}`,
|
|
241
|
+
400,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
validateSpeciesMetadata(
|
|
246
|
+
body.assistantMetadata.species as AssistantSpecies,
|
|
247
|
+
body.assistantMetadata.metadata ?? null,
|
|
248
|
+
);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return httpError(
|
|
251
|
+
"BAD_REQUEST",
|
|
252
|
+
err instanceof Error ? err.message : String(err),
|
|
253
|
+
400,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (body.contactType === "human" && body.assistantMetadata) {
|
|
259
|
+
return httpError(
|
|
260
|
+
"BAD_REQUEST",
|
|
261
|
+
'assistantMetadata must not be provided when contactType is "human"',
|
|
262
|
+
400,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (body.assistantMetadata && !body.contactType) {
|
|
267
|
+
return httpError(
|
|
268
|
+
"BAD_REQUEST",
|
|
269
|
+
'contactType must be "assistant" when assistantMetadata is provided',
|
|
270
|
+
400,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (body.channels) {
|
|
275
|
+
if (!Array.isArray(body.channels)) {
|
|
276
|
+
return httpError("BAD_REQUEST", "channels must be an array", 400);
|
|
277
|
+
}
|
|
278
|
+
for (const ch of body.channels) {
|
|
279
|
+
if (ch.status !== undefined && !isChannelStatus(ch.status)) {
|
|
280
|
+
return httpError(
|
|
281
|
+
"BAD_REQUEST",
|
|
282
|
+
`Invalid channel status "${ch.status}". Must be one of: ${VALID_CHANNEL_STATUSES.join(", ")}`,
|
|
283
|
+
400,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (ch.policy !== undefined && !isChannelPolicy(ch.policy)) {
|
|
287
|
+
return httpError(
|
|
288
|
+
"BAD_REQUEST",
|
|
289
|
+
`Invalid channel policy "${ch.policy}". Must be one of: ${VALID_CHANNEL_POLICIES.join(", ")}`,
|
|
290
|
+
400,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
138
296
|
try {
|
|
139
297
|
const contact = upsertContact({
|
|
140
298
|
id: body.id,
|
|
@@ -144,9 +302,23 @@ export async function handleUpsertContact(
|
|
|
144
302
|
responseExpectation: body.responseExpectation,
|
|
145
303
|
preferredTone: body.preferredTone,
|
|
146
304
|
role: body.role as ContactRole | undefined,
|
|
305
|
+
contactType: body.contactType as ContactType | undefined,
|
|
147
306
|
assistantId,
|
|
148
|
-
channels: body.channels
|
|
307
|
+
channels: body.channels?.map((ch) => ({
|
|
308
|
+
...ch,
|
|
309
|
+
status: ch.status as ChannelStatus | undefined,
|
|
310
|
+
policy: ch.policy as ChannelPolicy | undefined,
|
|
311
|
+
})),
|
|
149
312
|
});
|
|
313
|
+
|
|
314
|
+
if (body.assistantMetadata) {
|
|
315
|
+
upsertAssistantContactMetadata({
|
|
316
|
+
contactId: contact.id,
|
|
317
|
+
species: body.assistantMetadata.species as AssistantSpecies,
|
|
318
|
+
metadata: body.assistantMetadata.metadata ?? null,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
150
322
|
return Response.json(
|
|
151
323
|
{ ok: true, contact },
|
|
152
324
|
{ status: contact.created ? 201 : 200 },
|
|
@@ -157,27 +329,6 @@ export async function handleUpsertContact(
|
|
|
157
329
|
}
|
|
158
330
|
}
|
|
159
331
|
|
|
160
|
-
const VALID_CHANNEL_STATUSES: readonly ChannelStatus[] = [
|
|
161
|
-
"active",
|
|
162
|
-
"pending",
|
|
163
|
-
"revoked",
|
|
164
|
-
"blocked",
|
|
165
|
-
"unverified",
|
|
166
|
-
];
|
|
167
|
-
const VALID_CHANNEL_POLICIES: readonly ChannelPolicy[] = [
|
|
168
|
-
"allow",
|
|
169
|
-
"deny",
|
|
170
|
-
"escalate",
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
function isChannelStatus(value: string): value is ChannelStatus {
|
|
174
|
-
return (VALID_CHANNEL_STATUSES as readonly string[]).includes(value);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function isChannelPolicy(value: string): value is ChannelPolicy {
|
|
178
|
-
return (VALID_CHANNEL_POLICIES as readonly string[]).includes(value);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
332
|
/**
|
|
182
333
|
* PATCH /v1/contacts/channels/:channelId { status?, policy?, reason? }
|
|
183
334
|
*/
|
|
@@ -212,6 +363,23 @@ export async function handleUpdateContactChannel(
|
|
|
212
363
|
);
|
|
213
364
|
}
|
|
214
365
|
|
|
366
|
+
// Blocked-state guard: revoking a blocked channel is not allowed because
|
|
367
|
+
// blocking is a stronger action than revoking. The caller must explicitly
|
|
368
|
+
// unblock (set status to "active") before revoking.
|
|
369
|
+
if (body.status === "revoked") {
|
|
370
|
+
const existing = getChannelById(channelId);
|
|
371
|
+
if (!existing) {
|
|
372
|
+
return httpError("NOT_FOUND", `Channel "${channelId}" not found`, 404);
|
|
373
|
+
}
|
|
374
|
+
if (existing.status === "blocked") {
|
|
375
|
+
return httpError(
|
|
376
|
+
"CONFLICT",
|
|
377
|
+
"Cannot revoke a blocked channel. Unblock it first or leave it blocked.",
|
|
378
|
+
409,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
215
383
|
const updated = updateChannelStatus(channelId, {
|
|
216
384
|
status: body.status,
|
|
217
385
|
policy: body.policy,
|
|
@@ -11,17 +11,13 @@
|
|
|
11
11
|
* Guardian decisions additionally verify the actor is the bound guardian
|
|
12
12
|
* via the AuthContext's actorPrincipalId.
|
|
13
13
|
*/
|
|
14
|
-
import { applyCanonicalGuardianDecision } from "../../approvals/guardian-decision-primitive.js";
|
|
15
|
-
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
16
|
-
import { findGuardianForChannel } from "../../contacts/contact-store.js";
|
|
17
14
|
import {
|
|
18
15
|
type CanonicalGuardianRequest,
|
|
19
|
-
getCanonicalGuardianRequest,
|
|
20
|
-
isRequestInConversationScope,
|
|
21
16
|
listPendingRequestsByConversationScope,
|
|
22
17
|
} from "../../memory/canonical-guardian-store.js";
|
|
18
|
+
import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
|
|
23
19
|
import type { AuthContext } from "../auth/types.js";
|
|
24
|
-
import
|
|
20
|
+
import { processGuardianDecision } from "../guardian-action-service.js";
|
|
25
21
|
import type { GuardianDecisionPrompt } from "../guardian-decision-types.js";
|
|
26
22
|
import { buildDecisionActions } from "../guardian-decision-types.js";
|
|
27
23
|
import { httpError } from "../http-errors.js";
|
|
@@ -63,44 +59,6 @@ export function handleGuardianActionsPending(
|
|
|
63
59
|
// POST /v1/guardian-actions/decision
|
|
64
60
|
// ---------------------------------------------------------------------------
|
|
65
61
|
|
|
66
|
-
/**
|
|
67
|
-
* Verify that the actor from AuthContext is the bound guardian for the
|
|
68
|
-
* vellum channel. Returns an error Response if not, or null if allowed.
|
|
69
|
-
*/
|
|
70
|
-
function requireBoundGuardian(authContext: AuthContext): Response | null {
|
|
71
|
-
// Dev bypass: when auth is disabled, skip guardian binding check
|
|
72
|
-
// (mirrors enforcePolicy dev bypass in route-policy.ts)
|
|
73
|
-
if (isHttpAuthDisabled()) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
if (!authContext.actorPrincipalId) {
|
|
77
|
-
return httpError(
|
|
78
|
-
"FORBIDDEN",
|
|
79
|
-
"Actor is not the bound guardian for this channel",
|
|
80
|
-
403,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
const guardianResult = findGuardianForChannel(
|
|
84
|
-
"vellum",
|
|
85
|
-
authContext.assistantId,
|
|
86
|
-
);
|
|
87
|
-
if (!guardianResult) {
|
|
88
|
-
// No guardian yet — in pre-bootstrap state, allow through
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
if (
|
|
92
|
-
(guardianResult.channel.externalUserId ??
|
|
93
|
-
guardianResult.contact.principalId) !== authContext.actorPrincipalId
|
|
94
|
-
) {
|
|
95
|
-
return httpError(
|
|
96
|
-
"FORBIDDEN",
|
|
97
|
-
"Actor is not the bound guardian for this channel",
|
|
98
|
-
403,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
62
|
/**
|
|
105
63
|
* Submit a guardian action decision.
|
|
106
64
|
* Requires AuthContext with a bound guardian actor.
|
|
@@ -132,77 +90,24 @@ export async function handleGuardianActionDecision(
|
|
|
132
90
|
return httpError("BAD_REQUEST", "action is required", 400);
|
|
133
91
|
}
|
|
134
92
|
|
|
135
|
-
const
|
|
136
|
-
"approve_once",
|
|
137
|
-
"approve_10m",
|
|
138
|
-
"approve_thread",
|
|
139
|
-
"approve_always",
|
|
140
|
-
"reject",
|
|
141
|
-
]);
|
|
142
|
-
if (!VALID_ACTIONS.has(action)) {
|
|
143
|
-
return httpError(
|
|
144
|
-
"BAD_REQUEST",
|
|
145
|
-
`Invalid action: ${action}. Must be one of: approve_once, approve_10m, approve_thread, approve_always, reject`,
|
|
146
|
-
400,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Verify conversationId scoping before applying the canonical decision.
|
|
151
|
-
// The decision is allowed when the conversationId matches the request's
|
|
152
|
-
// source conversation OR a recorded delivery destination conversation.
|
|
153
|
-
// Channel is scoped to 'vellum' to prevent cross-channel approval when
|
|
154
|
-
// conversation ID namespaces overlap.
|
|
155
|
-
if (conversationId) {
|
|
156
|
-
const canonicalRequest = getCanonicalGuardianRequest(requestId);
|
|
157
|
-
if (
|
|
158
|
-
canonicalRequest &&
|
|
159
|
-
canonicalRequest.conversationId &&
|
|
160
|
-
!isRequestInConversationScope(requestId, conversationId, "vellum")
|
|
161
|
-
) {
|
|
162
|
-
return httpError(
|
|
163
|
-
"NOT_FOUND",
|
|
164
|
-
"No pending guardian action found for this requestId",
|
|
165
|
-
404,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Resolve actor identity from the AuthContext (set by JWT middleware).
|
|
171
|
-
const actorExternalUserId = authContext.actorPrincipalId ?? undefined;
|
|
172
|
-
const actorPrincipalId = authContext.actorPrincipalId ?? undefined;
|
|
173
|
-
|
|
174
|
-
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
93
|
+
const result = await processGuardianDecision({
|
|
175
94
|
requestId,
|
|
176
|
-
action
|
|
95
|
+
action,
|
|
96
|
+
conversationId,
|
|
97
|
+
channel: "vellum",
|
|
177
98
|
actorContext: {
|
|
178
|
-
externalUserId:
|
|
179
|
-
|
|
180
|
-
guardianPrincipalId: actorPrincipalId,
|
|
99
|
+
externalUserId: authContext.actorPrincipalId ?? undefined,
|
|
100
|
+
guardianPrincipalId: authContext.actorPrincipalId ?? undefined,
|
|
181
101
|
},
|
|
182
|
-
userText: undefined,
|
|
183
102
|
});
|
|
184
103
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
// (e.g. minting a verification session) did not happen. From the
|
|
188
|
-
// caller's perspective the decision was not truly applied.
|
|
189
|
-
if (canonicalResult.resolverFailed) {
|
|
190
|
-
return Response.json({
|
|
191
|
-
applied: false,
|
|
192
|
-
reason: "resolver_failed",
|
|
193
|
-
resolverFailureReason: canonicalResult.resolverFailureReason,
|
|
194
|
-
requestId: canonicalResult.requestId,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return Response.json({
|
|
199
|
-
applied: true,
|
|
200
|
-
requestId: canonicalResult.requestId,
|
|
201
|
-
});
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
return httpError("BAD_REQUEST", result.message, 400);
|
|
202
106
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
107
|
+
if (result.applied) {
|
|
108
|
+
return Response.json({ applied: true, requestId: result.requestId });
|
|
109
|
+
}
|
|
110
|
+
return result.reason === "not_found"
|
|
206
111
|
? httpError(
|
|
207
112
|
"NOT_FOUND",
|
|
208
113
|
"No pending guardian action found for this requestId",
|
|
@@ -210,8 +115,11 @@ export async function handleGuardianActionDecision(
|
|
|
210
115
|
)
|
|
211
116
|
: Response.json({
|
|
212
117
|
applied: false,
|
|
213
|
-
reason:
|
|
214
|
-
|
|
118
|
+
reason: result.reason,
|
|
119
|
+
...(result.resolverFailureReason
|
|
120
|
+
? { resolverFailureReason: result.resolverFailureReason }
|
|
121
|
+
: {}),
|
|
122
|
+
requestId: result.requestId ?? requestId,
|
|
215
123
|
});
|
|
216
124
|
}
|
|
217
125
|
|
|
@@ -30,12 +30,14 @@ import type {
|
|
|
30
30
|
ApprovalConversationGenerator,
|
|
31
31
|
ApprovalCopyGenerator,
|
|
32
32
|
} from "../http-types.js";
|
|
33
|
+
import { parseApprovalIntent } from "../nl-approval-parser.js";
|
|
33
34
|
import { handleGuardianCallbackDecision } from "./approval-strategies/guardian-callback-strategy.js";
|
|
34
35
|
import { handleGuardianLegacyFallback } from "./approval-strategies/guardian-legacy-fallback-strategy.js";
|
|
35
36
|
import { handleGuardianTextEngineDecision } from "./approval-strategies/guardian-text-engine-strategy.js";
|
|
36
37
|
import {
|
|
37
38
|
buildGuardianDenyContext,
|
|
38
39
|
parseCallbackData,
|
|
40
|
+
parseReactionCallbackData,
|
|
39
41
|
} from "./channel-route-shared.js";
|
|
40
42
|
import { deliverStaleApprovalReply } from "./guardian-approval-reply-helpers.js";
|
|
41
43
|
|
|
@@ -115,6 +117,33 @@ export async function handleApprovalInterception(
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// ── Slack reaction path ──
|
|
121
|
+
// Reactions produce `callbackData` of the form `reaction:<emoji_name>`.
|
|
122
|
+
// Handled before the pendingPrompt guard because guardian reactions arrive
|
|
123
|
+
// on the guardian's chat (guardianChatId), not the requester's conversation,
|
|
124
|
+
// so getChannelApprovalPrompt(conversationId) would return null.
|
|
125
|
+
// Only guardians can approve via reaction — non-guardian reactions are
|
|
126
|
+
// silently ignored to prevent self-approval.
|
|
127
|
+
if (callbackData?.startsWith("reaction:")) {
|
|
128
|
+
if (trustCtx.trustClass !== "guardian") {
|
|
129
|
+
return { handled: true, type: "stale_ignored" };
|
|
130
|
+
}
|
|
131
|
+
const reactionDecision = parseReactionCallbackData(callbackData);
|
|
132
|
+
if (!reactionDecision) {
|
|
133
|
+
// Unknown emoji — ignore silently
|
|
134
|
+
return { handled: true, type: "stale_ignored" };
|
|
135
|
+
}
|
|
136
|
+
const pending = getApprovalInfoByConversation(conversationId);
|
|
137
|
+
if (pending.length === 0) {
|
|
138
|
+
return { handled: true, type: "stale_ignored" };
|
|
139
|
+
}
|
|
140
|
+
const result = handleChannelDecision(conversationId, reactionDecision);
|
|
141
|
+
if (result.applied) {
|
|
142
|
+
return { handled: true, type: "decision_applied" };
|
|
143
|
+
}
|
|
144
|
+
return { handled: true, type: "stale_ignored" };
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
// ── Standard approval interception (existing flow) ──
|
|
119
148
|
const pendingPrompt = getChannelApprovalPrompt(conversationId);
|
|
120
149
|
if (!pendingPrompt) return { handled: false };
|
|
@@ -394,6 +423,30 @@ export async function handleApprovalInterception(
|
|
|
394
423
|
}
|
|
395
424
|
}
|
|
396
425
|
|
|
426
|
+
// ── Slack reaction path ──
|
|
427
|
+
// Reactions produce `callbackData` of the form `reaction:<emoji_name>`.
|
|
428
|
+
// Only guardians can approve via reaction — non-guardian reactions are
|
|
429
|
+
// silently ignored to prevent self-approval.
|
|
430
|
+
if (callbackData?.startsWith("reaction:")) {
|
|
431
|
+
if (trustCtx.trustClass !== "guardian") {
|
|
432
|
+
return { handled: true, type: "stale_ignored" };
|
|
433
|
+
}
|
|
434
|
+
const reactionDecision = parseReactionCallbackData(callbackData);
|
|
435
|
+
if (!reactionDecision) {
|
|
436
|
+
// Unknown emoji — ignore silently
|
|
437
|
+
return { handled: true, type: "stale_ignored" };
|
|
438
|
+
}
|
|
439
|
+
const pending = getApprovalInfoByConversation(conversationId);
|
|
440
|
+
if (pending.length === 0) {
|
|
441
|
+
return { handled: true, type: "stale_ignored" };
|
|
442
|
+
}
|
|
443
|
+
const result = handleChannelDecision(conversationId, reactionDecision);
|
|
444
|
+
if (result.applied) {
|
|
445
|
+
return { handled: true, type: "decision_applied" };
|
|
446
|
+
}
|
|
447
|
+
return { handled: true, type: "stale_ignored" };
|
|
448
|
+
}
|
|
449
|
+
|
|
397
450
|
// Try to extract a decision from callback data (button press) first.
|
|
398
451
|
// Callback/button path remains deterministic and takes priority.
|
|
399
452
|
if (callbackData) {
|
|
@@ -451,6 +504,29 @@ export async function handleApprovalInterception(
|
|
|
451
504
|
});
|
|
452
505
|
}
|
|
453
506
|
|
|
507
|
+
// ── Natural language approval intent parser ──
|
|
508
|
+
// Covers a broad set of colloquial approval/rejection phrases, emoji, and
|
|
509
|
+
// timed-approval variants. Runs before the legacy parser to provide wider
|
|
510
|
+
// coverage for channels (like Slack) that rely on plain-text responses.
|
|
511
|
+
if (pending.length > 0 && content) {
|
|
512
|
+
const nlIntent = parseApprovalIntent(content);
|
|
513
|
+
if (nlIntent && nlIntent.confidence >= 0.9) {
|
|
514
|
+
const nlDecision: ApprovalDecisionResult = {
|
|
515
|
+
action:
|
|
516
|
+
nlIntent.decision === "approve"
|
|
517
|
+
? "approve_once"
|
|
518
|
+
: nlIntent.decision === "approve_10m"
|
|
519
|
+
? "approve_10m"
|
|
520
|
+
: "reject",
|
|
521
|
+
source: "plain_text",
|
|
522
|
+
};
|
|
523
|
+
const nlResult = handleChannelDecision(conversationId, nlDecision);
|
|
524
|
+
if (nlResult.applied) {
|
|
525
|
+
return { handled: true, type: "decision_applied" };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
454
530
|
// ── Legacy deterministic fallback ──
|
|
455
531
|
// When no conversational engine is available, use the deterministic parser
|
|
456
532
|
// as a safety net for backward compatibility.
|