@vellumai/assistant 0.4.29 → 0.4.31
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/Dockerfile +14 -8
- package/README.md +7 -8
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +76 -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__/config-schema.test.ts +0 -9
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- 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-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
- 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__/memory-lifecycle-e2e.test.ts +11 -10
- 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__/registry.test.ts +0 -10
- 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__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- 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__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -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/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/relay-server.ts +44 -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 +49 -53
- package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
- 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-tool-registry.ts +2 -5
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/config/memory-schema.ts +0 -10
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +221 -56
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +35 -4
- 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-heartbeat.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +21 -88
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- 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 +2 -45
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-conflict-gate.ts +21 -82
- 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-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- 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/heartbeat/heartbeat-service.ts +5 -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/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +0 -7
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +12 -17
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/index.ts +0 -1
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/messaging/types.ts +0 -38
- 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 +128 -0
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +29 -46
- 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/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +494 -47
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/global-search-routes.ts +2 -2
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +78 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/routes/migration-routes.ts +17 -17
- 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/workspace/git-service.ts +6 -4
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -32
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
- /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
|
@@ -140,6 +140,7 @@ import {
|
|
|
140
140
|
handleMergeContacts,
|
|
141
141
|
handleUpdateContactChannel,
|
|
142
142
|
handleUpsertContact,
|
|
143
|
+
handleVerifyContactChannel,
|
|
143
144
|
} from "./routes/contact-routes.js";
|
|
144
145
|
import { handleListConversationAttention } from "./routes/conversation-attention-routes.js";
|
|
145
146
|
// Route handlers — grouped by domain
|
|
@@ -159,16 +160,6 @@ import {
|
|
|
159
160
|
import { handleGuardianBootstrap } from "./routes/guardian-bootstrap-routes.js";
|
|
160
161
|
import { handleGuardianRefresh } from "./routes/guardian-refresh-routes.js";
|
|
161
162
|
import { handleGetIdentity, handleHealth } from "./routes/identity-routes.js";
|
|
162
|
-
import {
|
|
163
|
-
handleBlockMember,
|
|
164
|
-
handleCreateInvite,
|
|
165
|
-
handleListInvites,
|
|
166
|
-
handleListMembers,
|
|
167
|
-
handleRedeemInvite,
|
|
168
|
-
handleRevokeInvite,
|
|
169
|
-
handleRevokeMember,
|
|
170
|
-
handleUpsertMember,
|
|
171
|
-
} from "./routes/ingress-routes.js";
|
|
172
163
|
import {
|
|
173
164
|
handleCancelOutbound,
|
|
174
165
|
handleClearSlackChannelConfig,
|
|
@@ -185,6 +176,12 @@ import {
|
|
|
185
176
|
handleSetupTelegram,
|
|
186
177
|
handleStartOutbound,
|
|
187
178
|
} from "./routes/integration-routes.js";
|
|
179
|
+
import {
|
|
180
|
+
handleCreateInvite,
|
|
181
|
+
handleListInvites,
|
|
182
|
+
handleRedeemInvite,
|
|
183
|
+
handleRevokeInvite,
|
|
184
|
+
} from "./routes/invite-routes.js";
|
|
188
185
|
import {
|
|
189
186
|
handleMigrationExport,
|
|
190
187
|
handleMigrationImport,
|
|
@@ -1133,64 +1130,50 @@ export class RuntimeHttpServer {
|
|
|
1133
1130
|
handleUpdateContactChannel(req, params.id, authContext.assistantId),
|
|
1134
1131
|
},
|
|
1135
1132
|
{
|
|
1136
|
-
endpoint: "contacts/:
|
|
1137
|
-
method: "GET",
|
|
1138
|
-
policyKey: "contacts",
|
|
1139
|
-
handler: ({ params, authContext }) =>
|
|
1140
|
-
handleGetContact(params.id, authContext.assistantId),
|
|
1141
|
-
},
|
|
1142
|
-
|
|
1143
|
-
// ------------------------------------------------------------------
|
|
1144
|
-
// Ingress contacts
|
|
1145
|
-
// ------------------------------------------------------------------
|
|
1146
|
-
{
|
|
1147
|
-
endpoint: "ingress/members",
|
|
1148
|
-
method: "GET",
|
|
1149
|
-
handler: ({ url }) => handleListMembers(url),
|
|
1150
|
-
},
|
|
1151
|
-
{
|
|
1152
|
-
endpoint: "ingress/members",
|
|
1153
|
-
method: "POST",
|
|
1154
|
-
handler: async ({ req }) => handleUpsertMember(req),
|
|
1155
|
-
},
|
|
1156
|
-
{
|
|
1157
|
-
endpoint: "ingress/members/:id/block",
|
|
1133
|
+
endpoint: "contacts/:contactId/channels/:channelId/verify",
|
|
1158
1134
|
method: "POST",
|
|
1159
|
-
policyKey: "
|
|
1160
|
-
handler: async ({
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
handler: async ({ req, params }) => handleRevokeMember(req, params.id),
|
|
1135
|
+
policyKey: "contacts/channels",
|
|
1136
|
+
handler: async ({ params, authContext }) =>
|
|
1137
|
+
handleVerifyContactChannel(
|
|
1138
|
+
params.contactId,
|
|
1139
|
+
params.channelId,
|
|
1140
|
+
authContext.assistantId,
|
|
1141
|
+
),
|
|
1167
1142
|
},
|
|
1168
1143
|
|
|
1169
1144
|
// ------------------------------------------------------------------
|
|
1170
|
-
//
|
|
1145
|
+
// Contacts invites — must precede contacts/:id to avoid shadowing
|
|
1171
1146
|
// ------------------------------------------------------------------
|
|
1172
1147
|
{
|
|
1173
|
-
endpoint: "
|
|
1148
|
+
endpoint: "contacts/invites",
|
|
1174
1149
|
method: "GET",
|
|
1175
1150
|
handler: ({ url }) => handleListInvites(url),
|
|
1176
1151
|
},
|
|
1177
1152
|
{
|
|
1178
|
-
endpoint: "
|
|
1153
|
+
endpoint: "contacts/invites",
|
|
1179
1154
|
method: "POST",
|
|
1180
1155
|
handler: async ({ req }) => handleCreateInvite(req),
|
|
1181
1156
|
},
|
|
1182
1157
|
{
|
|
1183
|
-
endpoint: "
|
|
1158
|
+
endpoint: "contacts/invites/redeem",
|
|
1184
1159
|
method: "POST",
|
|
1185
1160
|
handler: async ({ req }) => handleRedeemInvite(req),
|
|
1186
1161
|
},
|
|
1187
1162
|
{
|
|
1188
|
-
endpoint: "
|
|
1163
|
+
endpoint: "contacts/invites/:id",
|
|
1189
1164
|
method: "DELETE",
|
|
1190
|
-
policyKey: "
|
|
1165
|
+
policyKey: "contacts/invites",
|
|
1191
1166
|
handler: ({ params }) => handleRevokeInvite(params.id),
|
|
1192
1167
|
},
|
|
1193
1168
|
|
|
1169
|
+
{
|
|
1170
|
+
endpoint: "contacts/:id",
|
|
1171
|
+
method: "GET",
|
|
1172
|
+
policyKey: "contacts",
|
|
1173
|
+
handler: ({ params, authContext }) =>
|
|
1174
|
+
handleGetContact(params.id, authContext.assistantId),
|
|
1175
|
+
},
|
|
1176
|
+
|
|
1194
1177
|
// ------------------------------------------------------------------
|
|
1195
1178
|
// Integrations — Telegram
|
|
1196
1179
|
// ------------------------------------------------------------------
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
hashToken,
|
|
18
18
|
markInviteExpired,
|
|
19
19
|
recordInviteUse,
|
|
20
|
-
} from "../memory/
|
|
20
|
+
} from "../memory/invite-store.js";
|
|
21
21
|
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
22
22
|
import { hashVoiceCode } from "../util/voice-code.js";
|
|
23
23
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared business logic for
|
|
2
|
+
* Shared business logic for invite management.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from the IPC handlers in daemon/handlers/config-inbox.ts so that
|
|
5
5
|
* both the HTTP routes and the IPC handlers call the same logic.
|
|
6
|
+
*
|
|
7
|
+
* Member/contact operations have been migrated to the /v1/contacts and
|
|
8
|
+
* /v1/contacts/channels endpoints.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { isChannelId } from "../channels/types.js";
|
|
9
|
-
import { listContacts } from "../contacts/contact-store.js";
|
|
10
|
-
import {
|
|
11
|
-
blockMember,
|
|
12
|
-
revokeMember,
|
|
13
|
-
upsertMember,
|
|
14
|
-
} from "../contacts/contacts-write.js";
|
|
15
|
-
import type {
|
|
16
|
-
ContactWithChannels,
|
|
17
|
-
ContactWriteResult,
|
|
18
|
-
MemberPolicy,
|
|
19
|
-
MemberStatus,
|
|
20
|
-
} from "../contacts/types.js";
|
|
21
12
|
import {
|
|
22
13
|
createInvite,
|
|
23
14
|
findByTokenHash,
|
|
@@ -26,10 +17,9 @@ import {
|
|
|
26
17
|
type InviteStatus,
|
|
27
18
|
listInvites,
|
|
28
19
|
revokeInvite,
|
|
29
|
-
} from "../memory/
|
|
20
|
+
} from "../memory/invite-store.js";
|
|
30
21
|
import { isValidE164 } from "../util/phone.js";
|
|
31
22
|
import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
|
|
32
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
33
23
|
import { getTransport } from "./channel-invite-transport.js";
|
|
34
24
|
import {
|
|
35
25
|
type InviteRedemptionOutcome,
|
|
@@ -67,19 +57,6 @@ export interface InviteResponseData {
|
|
|
67
57
|
createdAt: number;
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
export interface MemberResponseData {
|
|
71
|
-
id: string;
|
|
72
|
-
sourceChannel: string;
|
|
73
|
-
externalUserId?: string;
|
|
74
|
-
externalChatId?: string;
|
|
75
|
-
displayName?: string;
|
|
76
|
-
username?: string;
|
|
77
|
-
status: string;
|
|
78
|
-
policy: string;
|
|
79
|
-
lastSeenAt?: number;
|
|
80
|
-
createdAt: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
60
|
// ---------------------------------------------------------------------------
|
|
84
61
|
// Mappers
|
|
85
62
|
// ---------------------------------------------------------------------------
|
|
@@ -133,41 +110,6 @@ function inviteToResponse(
|
|
|
133
110
|
};
|
|
134
111
|
}
|
|
135
112
|
|
|
136
|
-
function writeResultToResponse(result: ContactWriteResult): MemberResponseData {
|
|
137
|
-
return {
|
|
138
|
-
id: `${result.contact.id}:${result.channel.id}`,
|
|
139
|
-
sourceChannel: result.channel.type,
|
|
140
|
-
externalUserId: result.channel.externalUserId ?? undefined,
|
|
141
|
-
externalChatId: result.channel.externalChatId ?? undefined,
|
|
142
|
-
displayName: result.contact.displayName,
|
|
143
|
-
username: undefined,
|
|
144
|
-
status:
|
|
145
|
-
result.channel.status === "unverified"
|
|
146
|
-
? "pending"
|
|
147
|
-
: result.channel.status,
|
|
148
|
-
policy: result.channel.policy,
|
|
149
|
-
lastSeenAt: result.channel.lastSeenAt ?? undefined,
|
|
150
|
-
createdAt: result.channel.createdAt,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function contactToMemberResponse(
|
|
155
|
-
contact: ContactWithChannels,
|
|
156
|
-
): MemberResponseData[] {
|
|
157
|
-
return contact.channels.map((ch) => ({
|
|
158
|
-
id: `${contact.id}:${ch.id}`,
|
|
159
|
-
sourceChannel: ch.type,
|
|
160
|
-
externalUserId: ch.externalUserId ?? undefined,
|
|
161
|
-
externalChatId: ch.externalChatId ?? undefined,
|
|
162
|
-
displayName: contact.displayName,
|
|
163
|
-
username: undefined,
|
|
164
|
-
status: ch.status,
|
|
165
|
-
policy: ch.policy,
|
|
166
|
-
lastSeenAt: ch.lastSeenAt ?? undefined,
|
|
167
|
-
createdAt: ch.createdAt,
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
113
|
// ---------------------------------------------------------------------------
|
|
172
114
|
// Result types
|
|
173
115
|
// ---------------------------------------------------------------------------
|
|
@@ -347,97 +289,3 @@ export function redeemVoiceInviteCode(params: {
|
|
|
347
289
|
}): VoiceRedemptionOutcome {
|
|
348
290
|
return redeemVoiceInviteCodeTyped(params);
|
|
349
291
|
}
|
|
350
|
-
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
// Contact operations
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
|
|
355
|
-
export function listIngressContacts(params: {
|
|
356
|
-
assistantId?: string;
|
|
357
|
-
sourceChannel?: string;
|
|
358
|
-
status?: string;
|
|
359
|
-
policy?: string;
|
|
360
|
-
}): IngressResult<MemberResponseData[]> {
|
|
361
|
-
// Use uncapped: true since this internal path needs the full dataset
|
|
362
|
-
const allContacts = listContacts(
|
|
363
|
-
params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
364
|
-
Number.MAX_SAFE_INTEGER,
|
|
365
|
-
"contact",
|
|
366
|
-
{ uncapped: true },
|
|
367
|
-
);
|
|
368
|
-
const members = allContacts.flatMap((c) => contactToMemberResponse(c));
|
|
369
|
-
|
|
370
|
-
const filtered = members.filter((m) => {
|
|
371
|
-
if (params.sourceChannel && m.sourceChannel !== params.sourceChannel)
|
|
372
|
-
return false;
|
|
373
|
-
if (params.status && m.status !== params.status) return false;
|
|
374
|
-
if (params.policy && m.policy !== params.policy) return false;
|
|
375
|
-
return true;
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
return { ok: true, data: filtered };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function upsertIngressContact(params: {
|
|
382
|
-
sourceChannel?: string;
|
|
383
|
-
externalUserId?: string;
|
|
384
|
-
externalChatId?: string;
|
|
385
|
-
displayName?: string;
|
|
386
|
-
username?: string;
|
|
387
|
-
policy?: string;
|
|
388
|
-
status?: string;
|
|
389
|
-
assistantId?: string;
|
|
390
|
-
}): IngressResult<MemberResponseData> {
|
|
391
|
-
if (!params.sourceChannel) {
|
|
392
|
-
return { ok: false, error: "sourceChannel is required for upsert" };
|
|
393
|
-
}
|
|
394
|
-
if (!params.externalUserId && !params.externalChatId) {
|
|
395
|
-
return {
|
|
396
|
-
ok: false,
|
|
397
|
-
error:
|
|
398
|
-
"At least one of externalUserId or externalChatId is required for upsert",
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
const result = upsertMember({
|
|
402
|
-
assistantId: params.assistantId,
|
|
403
|
-
sourceChannel: params.sourceChannel,
|
|
404
|
-
externalUserId: params.externalUserId,
|
|
405
|
-
externalChatId: params.externalChatId,
|
|
406
|
-
displayName: params.displayName,
|
|
407
|
-
username: params.username,
|
|
408
|
-
policy: params.policy as MemberPolicy | undefined,
|
|
409
|
-
status: params.status as MemberStatus | undefined,
|
|
410
|
-
});
|
|
411
|
-
if (!result) {
|
|
412
|
-
return { ok: false, error: "Failed to upsert member" };
|
|
413
|
-
}
|
|
414
|
-
return { ok: true, data: writeResultToResponse(result) };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function revokeIngressContact(
|
|
418
|
-
memberId?: string,
|
|
419
|
-
reason?: string,
|
|
420
|
-
): IngressResult<MemberResponseData> {
|
|
421
|
-
if (!memberId) {
|
|
422
|
-
return { ok: false, error: "memberId is required for revoke" };
|
|
423
|
-
}
|
|
424
|
-
const result = revokeMember(memberId, reason);
|
|
425
|
-
if (!result) {
|
|
426
|
-
return { ok: false, error: "Member not found or cannot be revoked" };
|
|
427
|
-
}
|
|
428
|
-
return { ok: true, data: writeResultToResponse(result) };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export function blockIngressContact(
|
|
432
|
-
memberId?: string,
|
|
433
|
-
reason?: string,
|
|
434
|
-
): IngressResult<MemberResponseData> {
|
|
435
|
-
if (!memberId) {
|
|
436
|
-
return { ok: false, error: "memberId is required for block" };
|
|
437
|
-
}
|
|
438
|
-
const result = blockMember(memberId, reason);
|
|
439
|
-
if (!result) {
|
|
440
|
-
return { ok: false, error: "Member not found or already blocked" };
|
|
441
|
-
}
|
|
442
|
-
return { ok: true, data: writeResultToResponse(result) };
|
|
443
|
-
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Natural language approval intent parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses short inbound messages (e.g. from Slack) to determine whether the
|
|
5
|
+
* entire message expresses an approval, rejection, or timed-approval intent.
|
|
6
|
+
* Only matches when the full message is an approval/rejection phrase -- does
|
|
7
|
+
* NOT match partial intent inside longer sentences like "yes but also do X".
|
|
8
|
+
*
|
|
9
|
+
* This parser complements the existing `channel-approval-parser.ts`
|
|
10
|
+
* (deterministic phrase-map) by covering a broader set of colloquial
|
|
11
|
+
* patterns, emoji, and timed-approval variants.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Public types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type ApprovalDecision = "approve" | "reject" | "approve_10m";
|
|
19
|
+
|
|
20
|
+
export interface ApprovalIntent {
|
|
21
|
+
decision: ApprovalDecision;
|
|
22
|
+
confidence: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Pattern definitions
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Exact-match phrases (after normalization) for approval. */
|
|
30
|
+
const APPROVE_EXACT: ReadonlySet<string> = new Set([
|
|
31
|
+
"yes",
|
|
32
|
+
"yep",
|
|
33
|
+
"yeah",
|
|
34
|
+
"yea",
|
|
35
|
+
"yup",
|
|
36
|
+
"approved",
|
|
37
|
+
"approve",
|
|
38
|
+
"go ahead",
|
|
39
|
+
"do it",
|
|
40
|
+
"sure",
|
|
41
|
+
"ok",
|
|
42
|
+
"okay",
|
|
43
|
+
"k",
|
|
44
|
+
"lgtm",
|
|
45
|
+
"sounds good",
|
|
46
|
+
"go for it",
|
|
47
|
+
"please",
|
|
48
|
+
"pls",
|
|
49
|
+
"y",
|
|
50
|
+
"\u{1F44D}", // 👍
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Exact-match phrases for rejection. */
|
|
54
|
+
const REJECT_EXACT: ReadonlySet<string> = new Set([
|
|
55
|
+
"no",
|
|
56
|
+
"nope",
|
|
57
|
+
"nah",
|
|
58
|
+
"reject",
|
|
59
|
+
"rejected",
|
|
60
|
+
"denied",
|
|
61
|
+
"deny",
|
|
62
|
+
"don't",
|
|
63
|
+
"dont",
|
|
64
|
+
"cancel",
|
|
65
|
+
"stop",
|
|
66
|
+
"n",
|
|
67
|
+
"\u{1F44E}", // 👎
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Patterns for timed approval (e.g. "approve for 10 minutes", "yes for now").
|
|
72
|
+
* Matched after normalization.
|
|
73
|
+
*/
|
|
74
|
+
const TIMED_PATTERNS: readonly RegExp[] = [
|
|
75
|
+
/^(?:approve|yes|ok|okay|sure|yep|yeah|go ahead)\s+for\s+10\s*(?:min(?:utes?)?|m)$/,
|
|
76
|
+
/^(?:approve|yes|ok|okay|sure|yep|yeah)\s+for\s+now$/,
|
|
77
|
+
/^approve\s+10\s*(?:min(?:utes?)?|m)$/,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Normalization
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize input: lowercase, trim, strip `[ref:...]` disambiguation tags,
|
|
86
|
+
* strip trailing punctuation (except emoji), collapse internal whitespace.
|
|
87
|
+
*/
|
|
88
|
+
function normalize(text: string): string {
|
|
89
|
+
return text
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/\[ref:[^\]]*\]/g, "")
|
|
93
|
+
.replace(/\s+/g, " ")
|
|
94
|
+
.replace(/[.!?,;:]+$/, "")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Public API
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse a message for approval/rejection intent.
|
|
104
|
+
*
|
|
105
|
+
* Returns an `ApprovalIntent` when the entire message clearly expresses a
|
|
106
|
+
* single approval or rejection decision. Returns `null` when no intent is
|
|
107
|
+
* detected or when the message contains additional content beyond the
|
|
108
|
+
* decision phrase.
|
|
109
|
+
*/
|
|
110
|
+
export function parseApprovalIntent(text: string): ApprovalIntent | null {
|
|
111
|
+
const normalized = normalize(text);
|
|
112
|
+
|
|
113
|
+
if (normalized.length === 0) return null;
|
|
114
|
+
|
|
115
|
+
// Reject messages that are too long to be a simple decision phrase.
|
|
116
|
+
// This prevents matching approval words buried inside longer messages.
|
|
117
|
+
// The longest timed-approval phrase is ~30 chars; allow some padding.
|
|
118
|
+
if (normalized.length > 40) return null;
|
|
119
|
+
|
|
120
|
+
// Check timed approval patterns first (more specific).
|
|
121
|
+
for (const pattern of TIMED_PATTERNS) {
|
|
122
|
+
if (pattern.test(normalized)) {
|
|
123
|
+
return { decision: "approve_10m", confidence: 0.95 };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Exact approval match.
|
|
128
|
+
if (APPROVE_EXACT.has(normalized)) {
|
|
129
|
+
return { decision: "approve", confidence: 0.95 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Exact rejection match.
|
|
133
|
+
if (REJECT_EXACT.has(normalized)) {
|
|
134
|
+
return { decision: "reject", confidence: 0.95 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -8,57 +8,18 @@
|
|
|
8
8
|
* header. Guardian decisions additionally verify that the actor is the
|
|
9
9
|
* bound guardian.
|
|
10
10
|
*/
|
|
11
|
-
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
12
|
-
import { findGuardianForChannel } from "../../contacts/contact-store.js";
|
|
13
11
|
import { getConversationByKey } from "../../memory/conversation-key-store.js";
|
|
14
12
|
import { addRule } from "../../permissions/trust-store.js";
|
|
15
13
|
import type { UserDecision } from "../../permissions/types.js";
|
|
16
14
|
import { getTool } from "../../tools/registry.js";
|
|
17
15
|
import { getLogger } from "../../util/logger.js";
|
|
16
|
+
import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
|
|
18
17
|
import type { AuthContext } from "../auth/types.js";
|
|
19
18
|
import { httpError } from "../http-errors.js";
|
|
20
19
|
import * as pendingInteractions from "../pending-interactions.js";
|
|
21
20
|
|
|
22
21
|
const log = getLogger("approval-routes");
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* Verify the actor from AuthContext is the bound guardian for the vellum channel.
|
|
26
|
-
* Returns an error Response if not, or null if allowed.
|
|
27
|
-
*/
|
|
28
|
-
function requireBoundGuardian(authContext: AuthContext): Response | null {
|
|
29
|
-
// Dev bypass: when auth is disabled, skip guardian binding check
|
|
30
|
-
// (mirrors enforcePolicy dev bypass in route-policy.ts)
|
|
31
|
-
if (isHttpAuthDisabled()) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
if (!authContext.actorPrincipalId) {
|
|
35
|
-
return httpError(
|
|
36
|
-
"FORBIDDEN",
|
|
37
|
-
"Actor is not the bound guardian for this channel",
|
|
38
|
-
403,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
const guardianResult = findGuardianForChannel(
|
|
42
|
-
"vellum",
|
|
43
|
-
authContext.assistantId,
|
|
44
|
-
);
|
|
45
|
-
if (!guardianResult) {
|
|
46
|
-
// No guardian yet — in pre-bootstrap state, allow through
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
if (
|
|
50
|
-
(guardianResult.channel.externalUserId ??
|
|
51
|
-
guardianResult.contact.principalId) !== authContext.actorPrincipalId
|
|
52
|
-
) {
|
|
53
|
-
return httpError(
|
|
54
|
-
"FORBIDDEN",
|
|
55
|
-
"Actor is not the bound guardian for this channel",
|
|
56
|
-
403,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
23
|
/**
|
|
63
24
|
* POST /v1/confirm — resolve a pending confirmation by requestId.
|
|
64
25
|
* Requires AuthContext with guardian-bound actor.
|
|
@@ -297,7 +297,8 @@ async function handleCallbackDecision(params: {
|
|
|
297
297
|
const result = applyGuardianDecision({
|
|
298
298
|
approval: guardianApproval,
|
|
299
299
|
decision: callbackDecision,
|
|
300
|
-
|
|
300
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
301
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
301
302
|
actorChannel: sourceChannel,
|
|
302
303
|
});
|
|
303
304
|
|
|
@@ -491,7 +492,8 @@ async function handleConversationalDecision(params: {
|
|
|
491
492
|
const result = applyGuardianDecision({
|
|
492
493
|
approval: targetApproval,
|
|
493
494
|
decision: engineDecision,
|
|
494
|
-
|
|
495
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
496
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
495
497
|
actorChannel: sourceChannel,
|
|
496
498
|
});
|
|
497
499
|
|
|
@@ -675,7 +677,8 @@ async function handleLegacyDecision(params: {
|
|
|
675
677
|
const result = applyGuardianDecision({
|
|
676
678
|
approval: targetLegacyApproval,
|
|
677
679
|
decision: legacyGuardianDecision,
|
|
678
|
-
|
|
680
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
681
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
679
682
|
actorChannel: sourceChannel,
|
|
680
683
|
});
|
|
681
684
|
|
|
@@ -64,10 +64,44 @@ export function parseCallbackData(
|
|
|
64
64
|
const source =
|
|
65
65
|
sourceChannel === "whatsapp"
|
|
66
66
|
? ("whatsapp_button" as const)
|
|
67
|
-
:
|
|
67
|
+
: sourceChannel === "slack"
|
|
68
|
+
? ("slack_button" as const)
|
|
69
|
+
: ("telegram_button" as const);
|
|
68
70
|
return { action: action as ApprovalAction, source, requestId };
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Reaction callback data parser — format: "reaction:<emoji_name>"
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Map of Slack emoji names to approval actions. Multiple emoji names can
|
|
79
|
+
* map to the same action to handle Slack's aliasing (e.g. `+1` and `thumbsup`
|
|
80
|
+
* both represent the thumbs-up emoji).
|
|
81
|
+
*/
|
|
82
|
+
const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
|
|
83
|
+
["+1", "approve_once"],
|
|
84
|
+
["thumbsup", "approve_once"],
|
|
85
|
+
["-1", "reject"],
|
|
86
|
+
["thumbsdown", "reject"],
|
|
87
|
+
["alarm_clock", "approve_10m"],
|
|
88
|
+
["white_check_mark", "approve_always"],
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse a `reaction:<emoji_name>` callback data string into an approval
|
|
93
|
+
* decision. Returns null if the emoji is not mapped to any action.
|
|
94
|
+
*/
|
|
95
|
+
export function parseReactionCallbackData(
|
|
96
|
+
data: string,
|
|
97
|
+
): ApprovalDecisionResult | null {
|
|
98
|
+
if (!data.startsWith("reaction:")) return null;
|
|
99
|
+
const emoji = data.slice("reaction:".length);
|
|
100
|
+
const action = REACTION_EMOJI_MAP.get(emoji);
|
|
101
|
+
if (!action) return null;
|
|
102
|
+
return { action, source: "slack_reaction" };
|
|
103
|
+
}
|
|
104
|
+
|
|
71
105
|
// ---------------------------------------------------------------------------
|
|
72
106
|
// Context builders
|
|
73
107
|
// ---------------------------------------------------------------------------
|