@vellumai/assistant 0.10.0-dev.202606222007.e9d01e2 → 0.10.0-dev.202606222147.354e06a
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 +14 -15
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/openapi.yaml +4 -2
- package/package.json +1 -1
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +151 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +72 -1118
- package/src/__tests__/channel-guardian.test.ts +245 -641
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-fork-retrospective.test.ts +250 -0
- package/src/__tests__/guardian-outbound-http.test.ts +0 -5
- package/src/__tests__/guardian-routing-invariants.test.ts +1 -3
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -1
- package/src/__tests__/provider-platform-proxy-integration.test.ts +25 -5
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +33 -32
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/approvals/AGENTS.md +1 -2
- package/src/approvals/guardian-decision-primitive.ts +13 -210
- package/src/approvals/guardian-request-resolvers.ts +2 -6
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-retrospective.ts +129 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/daemon/handlers/shared.ts +7 -0
- package/src/memory/__tests__/fork-message-copy.test.ts +232 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -1
- package/src/memory/conversation-crud.ts +409 -139
- package/src/memory/fork-message-copy.ts +170 -0
- package/src/memory/memory-retrospective-job.ts +10 -5
- package/src/notifications/approval-card-builder.ts +13 -2
- package/src/notifications/trusted-contact-payloads.ts +5 -7
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/inference/connections.ts +6 -1
- package/src/providers/model-catalog.ts +37 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +1 -0
- package/src/providers/together/client.ts +35 -0
- package/src/runtime/guardian-decision-types.ts +3 -22
- package/src/runtime/http-server.ts +1 -15
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -9
- package/src/runtime/routes/guardian-approval-interception.ts +10 -273
- package/src/__tests__/access-request-decision.test.ts +0 -375
- package/src/__tests__/guardian-grant-minting.test.ts +0 -607
- package/src/memory/guardian-approvals.ts +0 -361
- package/src/runtime/routes/access-request-decision.ts +0 -297
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -973
- package/src/runtime/routes/channel-guardian-routes.ts +0 -19
- package/src/runtime/routes/guardian-expiry-sweep.ts +0 -132
package/ARCHITECTURE.md
CHANGED
|
@@ -105,21 +105,20 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
|
|
|
105
105
|
|
|
106
106
|
Scoped approval grants allow a guardian's approval decision on one channel (e.g., Telegram) to authorize a tool execution on a different channel (e.g., voice). Two scope modes exist: `request_id` (bound to a specific pending request) and `tool_signature` (bound to `toolName` + canonical `inputDigest`). Grants are one-time-use, exact-match, fail-closed, and TTL-bound. Full architecture details (lifecycle flow, security invariants, key files) live in [`docs/architecture/security.md`](docs/architecture/security.md#channel-agnostic-scoped-approval-grants).
|
|
107
107
|
|
|
108
|
-
### Guardian Decision Primitive
|
|
108
|
+
### Guardian Decision Primitive
|
|
109
109
|
|
|
110
|
-
All guardian approval decisions — regardless of how they arrive — route through a single
|
|
110
|
+
All guardian approval decisions — regardless of how they arrive — route through a single canonical primitive in `src/approvals/guardian-decision-primitive.ts`, which centralizes decision logic for callback button handlers, the conversational approval engine, and channel reactions/text.
|
|
111
111
|
|
|
112
112
|
**Core API:**
|
|
113
113
|
|
|
114
|
-
| Function
|
|
115
|
-
|
|
|
116
|
-
| `
|
|
117
|
-
| `listGuardianDecisionPrompts({ conversationId })` | List pending prompts for a conversation, aggregating channel guardian approval requests and pending confirmation interactions into a uniform `GuardianDecisionPrompt` shape. |
|
|
114
|
+
| Function | Purpose |
|
|
115
|
+
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `applyCanonicalGuardianDecision(params)` | Apply a guardian decision against the canonical store: validate status/identity/expiry, CAS-resolve the request (first-writer-wins), dispatch to the kind-specific resolver, mint a scoped grant on approve, and withdraw the request's approval cards. Returns a structured applied/failed result. |
|
|
118
117
|
|
|
119
118
|
**Security invariants enforced by the primitive:**
|
|
120
119
|
|
|
121
120
|
- Decision application is identity-bound to the expected guardian identity.
|
|
122
|
-
- Decisions are first-response-wins (CAS
|
|
121
|
+
- Decisions are first-response-wins (CAS stale protection via `resolveCanonicalGuardianRequest`).
|
|
123
122
|
- `approve_always` is downgraded to `approve_once` for guardian-on-behalf requests (guardians cannot permanently allowlist tools for requesters).
|
|
124
123
|
- Scoped grant minting only fires on explicit approve for requests with tool metadata.
|
|
125
124
|
|
|
@@ -199,14 +198,14 @@ The canonical guardian request system provides a channel-agnostic, unified domai
|
|
|
199
198
|
|
|
200
199
|
**Key source files:**
|
|
201
200
|
|
|
202
|
-
| File | Purpose
|
|
203
|
-
| ------------------------------------------------------- |
|
|
204
|
-
| `src/memory/canonical-guardian-store.ts` | Canonical request and delivery persistence (CRUD, CAS resolve, list with filters)
|
|
205
|
-
| `src/approvals/guardian-decision-primitive.ts` |
|
|
206
|
-
| `src/approvals/guardian-request-resolvers.ts` | Resolver registry: kind-specific side-effect dispatch after CAS resolution
|
|
207
|
-
| `src/runtime/guardian-reply-router.ts` | Shared inbound router: callback -> code -> NL classification pipeline
|
|
208
|
-
| `src/runtime/routes/guardian-action-routes.ts` | HTTP endpoints for prompt listing and decision submission
|
|
209
|
-
| `src/runtime/routes/canonical-guardian-expiry-sweep.ts` | Canonical request expiry sweep
|
|
201
|
+
| File | Purpose |
|
|
202
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
203
|
+
| `src/memory/canonical-guardian-store.ts` | Canonical request and delivery persistence (CRUD, CAS resolve, list with filters) |
|
|
204
|
+
| `src/approvals/guardian-decision-primitive.ts` | Canonical decision primitive: `applyCanonicalGuardianDecision` + scoped grant minting |
|
|
205
|
+
| `src/approvals/guardian-request-resolvers.ts` | Resolver registry: kind-specific side-effect dispatch after CAS resolution |
|
|
206
|
+
| `src/runtime/guardian-reply-router.ts` | Shared inbound router: callback -> code -> NL classification pipeline |
|
|
207
|
+
| `src/runtime/routes/guardian-action-routes.ts` | HTTP endpoints for prompt listing and decision submission |
|
|
208
|
+
| `src/runtime/routes/canonical-guardian-expiry-sweep.ts` | Canonical request expiry sweep |
|
|
210
209
|
|
|
211
210
|
### Outbound Channel Verification (HTTP Endpoints)
|
|
212
211
|
|
|
@@ -47,9 +47,15 @@ const ALLOWED_PREFIXES = {
|
|
|
47
47
|
"./",
|
|
48
48
|
// Config schema/loader at depth-1 and depth-2.
|
|
49
49
|
"../../config/loader",
|
|
50
|
+
"../../../config/loader",
|
|
50
51
|
"../../config/schema",
|
|
51
52
|
"../../config/env",
|
|
52
53
|
"../../util/platform",
|
|
54
|
+
// Memory retrospective — the retrospective CLI runs the fork-based
|
|
55
|
+
// retrospective in-process (no daemon, no IPC), so it imports the
|
|
56
|
+
// job handler directly. Depth-2 for commands/memory/ nesting.
|
|
57
|
+
"../../memory/memory-retrospective-job",
|
|
58
|
+
"../../../memory/memory-retrospective-job",
|
|
53
59
|
"../logger",
|
|
54
60
|
"../output",
|
|
55
61
|
"../../logger",
|
package/openapi.yaml
CHANGED
|
@@ -13317,8 +13317,8 @@ paths:
|
|
|
13317
13317
|
schema:
|
|
13318
13318
|
type: string
|
|
13319
13319
|
description:
|
|
13320
|
-
"Filter by provider. One of: anthropic, openai, gemini, ollama, fireworks,
|
|
13321
|
-
minimax, atlascloud"
|
|
13320
|
+
"Filter by provider. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
|
|
13321
|
+
openai-compatible, minimax, atlascloud"
|
|
13322
13322
|
responses:
|
|
13323
13323
|
"200":
|
|
13324
13324
|
description: Successful response
|
|
@@ -29216,6 +29216,7 @@ components:
|
|
|
29216
29216
|
- openai-compatible
|
|
29217
29217
|
- minimax
|
|
29218
29218
|
- atlascloud
|
|
29219
|
+
- together
|
|
29219
29220
|
ProfilePatchEntry:
|
|
29220
29221
|
type: object
|
|
29221
29222
|
properties:
|
|
@@ -29592,6 +29593,7 @@ components:
|
|
|
29592
29593
|
- gemini
|
|
29593
29594
|
- ollama
|
|
29594
29595
|
- fireworks
|
|
29596
|
+
- together
|
|
29595
29597
|
- openrouter
|
|
29596
29598
|
- openai-compatible
|
|
29597
29599
|
- minimax
|
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@ describe("buildAccessRequestSeedContentBlocks", () => {
|
|
|
22
22
|
expect(blocks).toHaveLength(2);
|
|
23
23
|
expect((blocks[0] as Record<string, unknown>).type).toBe("ui_surface");
|
|
24
24
|
expect((blocks[1] as Record<string, unknown>).type).toBe("text");
|
|
25
|
+
// The fallback block is flagged so surface-capable clients skip it.
|
|
26
|
+
expect((blocks[1] as Record<string, unknown>)._surfaceFallback).toBe(true);
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
test("card surface has correct surfaceType and surfaceId", () => {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust-class gates in `handleApprovalInterception`.
|
|
3
|
+
*
|
|
4
|
+
* The interception dispatcher applies identity-based gates before any decision
|
|
5
|
+
* is resolved:
|
|
6
|
+
* - An unverified sender (no established identity) auto-denies a pending
|
|
7
|
+
* approval — a missing-identity actor must not be able to leave a sensitive
|
|
8
|
+
* request actionable.
|
|
9
|
+
* - An identity-known non-guardian must NOT auto-deny; it waits for the
|
|
10
|
+
* guardian and receives a "pending" notice.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
mock.module("../util/logger.js", () => ({
|
|
16
|
+
getLogger: () =>
|
|
17
|
+
new Proxy({} as Record<string, unknown>, {
|
|
18
|
+
get: () => () => {},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const _conversationMocks = new Map<string, unknown>();
|
|
23
|
+
mock.module("../daemon/conversation-registry.js", () => ({
|
|
24
|
+
findConversation: (id: string) => _conversationMocks.get(id),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
28
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
29
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
30
|
+
import * as approvalMessageComposer from "../runtime/approval-message-composer.js";
|
|
31
|
+
import * as gatewayClient from "../runtime/gateway-client.js";
|
|
32
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
33
|
+
import { handleApprovalInterception } from "../runtime/routes/guardian-approval-interception.js";
|
|
34
|
+
|
|
35
|
+
await initializeDb();
|
|
36
|
+
|
|
37
|
+
const ASSISTANT_ID = "self";
|
|
38
|
+
const CONVERSATION_ID = "conv-1";
|
|
39
|
+
const REQUESTER_CHAT = "requester-chat-1";
|
|
40
|
+
const TOOL_NAME = "execute_shell";
|
|
41
|
+
const TOOL_INPUT = { command: "rm -rf /tmp/test" };
|
|
42
|
+
|
|
43
|
+
function registerPendingInteraction(
|
|
44
|
+
requestId: string,
|
|
45
|
+
conversationId: string,
|
|
46
|
+
toolName: string,
|
|
47
|
+
input: Record<string, unknown> = TOOL_INPUT,
|
|
48
|
+
): ReturnType<typeof mock> {
|
|
49
|
+
const handleConfirmationResponse = mock(() => {});
|
|
50
|
+
const _mockSession = {
|
|
51
|
+
handleConfirmationResponse,
|
|
52
|
+
ensureActorScopedHistory: async () => {},
|
|
53
|
+
} as unknown as Conversation;
|
|
54
|
+
_conversationMocks.set(conversationId, _mockSession);
|
|
55
|
+
|
|
56
|
+
pendingInteractions.register(requestId, {
|
|
57
|
+
conversationId,
|
|
58
|
+
kind: "confirmation",
|
|
59
|
+
confirmationDetails: {
|
|
60
|
+
toolName,
|
|
61
|
+
input,
|
|
62
|
+
riskLevel: "high",
|
|
63
|
+
allowlistOptions: [
|
|
64
|
+
{ label: "test", description: "test", pattern: "test" },
|
|
65
|
+
],
|
|
66
|
+
scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return handleConfirmationResponse;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("approval interception trust-class gates", () => {
|
|
74
|
+
let deliverSpy: ReturnType<typeof spyOn>;
|
|
75
|
+
let composeSpy: ReturnType<typeof spyOn>;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
pendingInteractions.clear();
|
|
79
|
+
deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue({
|
|
80
|
+
ok: true,
|
|
81
|
+
});
|
|
82
|
+
composeSpy = spyOn(
|
|
83
|
+
approvalMessageComposer,
|
|
84
|
+
"composeApprovalMessageGenerative",
|
|
85
|
+
).mockResolvedValue("test message");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("identity-known unknown sender does not auto-deny pending approval", async () => {
|
|
89
|
+
const sessionMock = registerPendingInteraction(
|
|
90
|
+
"req-unknown-no-auto-deny-1",
|
|
91
|
+
CONVERSATION_ID,
|
|
92
|
+
TOOL_NAME,
|
|
93
|
+
TOOL_INPUT,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await handleApprovalInterception({
|
|
97
|
+
conversationId: CONVERSATION_ID,
|
|
98
|
+
content: "approve",
|
|
99
|
+
conversationExternalId: REQUESTER_CHAT,
|
|
100
|
+
sourceChannel: "telegram",
|
|
101
|
+
actorExternalId: "intruder-user-1",
|
|
102
|
+
replyCallbackUrl: "https://gateway.test/deliver",
|
|
103
|
+
trustCtx: {
|
|
104
|
+
sourceChannel: "telegram",
|
|
105
|
+
trustClass: "unknown",
|
|
106
|
+
requesterExternalUserId: "intruder-user-1",
|
|
107
|
+
guardianExternalUserId: "guardian-1",
|
|
108
|
+
} as TrustContext,
|
|
109
|
+
assistantId: ASSISTANT_ID,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.handled).toBe(true);
|
|
113
|
+
expect(result.type).toBe("assistant_turn");
|
|
114
|
+
expect(sessionMock).not.toHaveBeenCalled();
|
|
115
|
+
|
|
116
|
+
deliverSpy.mockRestore();
|
|
117
|
+
composeSpy.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("unverified sender (no identity) auto-denies pending approval", async () => {
|
|
121
|
+
const sessionMock = registerPendingInteraction(
|
|
122
|
+
"req-unknown-auto-deny-1",
|
|
123
|
+
CONVERSATION_ID,
|
|
124
|
+
TOOL_NAME,
|
|
125
|
+
TOOL_INPUT,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result = await handleApprovalInterception({
|
|
129
|
+
conversationId: CONVERSATION_ID,
|
|
130
|
+
content: "approve",
|
|
131
|
+
conversationExternalId: REQUESTER_CHAT,
|
|
132
|
+
sourceChannel: "telegram",
|
|
133
|
+
actorExternalId: undefined,
|
|
134
|
+
replyCallbackUrl: "https://gateway.test/deliver",
|
|
135
|
+
trustCtx: {
|
|
136
|
+
sourceChannel: "telegram",
|
|
137
|
+
trustClass: "unknown",
|
|
138
|
+
} as TrustContext,
|
|
139
|
+
assistantId: ASSISTANT_ID,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.handled).toBe(true);
|
|
143
|
+
expect(result.type).toBe("decision_applied");
|
|
144
|
+
expect(sessionMock).toHaveBeenCalled();
|
|
145
|
+
expect(sessionMock.mock.calls[0]?.[0]).toBe("req-unknown-auto-deny-1");
|
|
146
|
+
expect(sessionMock.mock.calls[0]?.[1]).toBe("deny");
|
|
147
|
+
|
|
148
|
+
deliverSpy.mockRestore();
|
|
149
|
+
composeSpy.mockRestore();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -116,6 +116,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
116
116
|
findAnalysisConversationFor: mock(() => null),
|
|
117
117
|
findMostRecentRetrospectiveFor: mock(() => null),
|
|
118
118
|
forkConversation: mock(() => ({ id: "conv-fork" })),
|
|
119
|
+
forkConversationForRetrospective: mock(async () => ({ id: "conv-fork" })),
|
|
119
120
|
getConversationOverrideProfile: () => undefined,
|
|
120
121
|
resolveOverrideProfile: () => undefined,
|
|
121
122
|
getConversationMemoryScopeId: () => "default",
|