@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.
Files changed (55) hide show
  1. package/ARCHITECTURE.md +14 -15
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/openapi.yaml +4 -2
  4. package/package.json +1 -1
  5. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -0
  6. package/src/__tests__/approval-interception-trust-gates.test.ts +151 -0
  7. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -0
  8. package/src/__tests__/channel-approval-routes.test.ts +72 -1118
  9. package/src/__tests__/channel-guardian.test.ts +245 -641
  10. package/src/__tests__/channel-inbound-disk-pressure.test.ts +0 -1
  11. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  12. package/src/__tests__/conversation-fork-retrospective.test.ts +250 -0
  13. package/src/__tests__/guardian-outbound-http.test.ts +0 -5
  14. package/src/__tests__/guardian-routing-invariants.test.ts +1 -3
  15. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  16. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  17. package/src/__tests__/non-member-access-request.test.ts +0 -1
  18. package/src/__tests__/provider-platform-proxy-integration.test.ts +25 -5
  19. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  20. package/src/__tests__/server-history-render.test.ts +39 -0
  21. package/src/__tests__/slack-inbound-verification.test.ts +0 -1
  22. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +2 -0
  23. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +33 -32
  24. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  25. package/src/approvals/AGENTS.md +1 -2
  26. package/src/approvals/guardian-decision-primitive.ts +13 -210
  27. package/src/approvals/guardian-request-resolvers.ts +2 -6
  28. package/src/cli/commands/memory/index.ts +2 -0
  29. package/src/cli/commands/memory/memory-retrospective.ts +129 -0
  30. package/src/config/schemas/llm.ts +1 -0
  31. package/src/daemon/handlers/shared.ts +7 -0
  32. package/src/memory/__tests__/fork-message-copy.test.ts +232 -0
  33. package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -1
  34. package/src/memory/conversation-crud.ts +409 -139
  35. package/src/memory/fork-message-copy.ts +170 -0
  36. package/src/memory/memory-retrospective-job.ts +10 -5
  37. package/src/notifications/approval-card-builder.ts +13 -2
  38. package/src/notifications/trusted-contact-payloads.ts +5 -7
  39. package/src/providers/inference/adapter-factory.ts +6 -0
  40. package/src/providers/inference/connections.ts +6 -1
  41. package/src/providers/model-catalog.ts +37 -0
  42. package/src/providers/platform-proxy/constants.ts +5 -0
  43. package/src/providers/retry.ts +1 -0
  44. package/src/providers/together/client.ts +35 -0
  45. package/src/runtime/guardian-decision-types.ts +3 -22
  46. package/src/runtime/http-server.ts +1 -15
  47. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -9
  48. package/src/runtime/routes/guardian-approval-interception.ts +10 -273
  49. package/src/__tests__/access-request-decision.test.ts +0 -375
  50. package/src/__tests__/guardian-grant-minting.test.ts +0 -607
  51. package/src/memory/guardian-approvals.ts +0 -361
  52. package/src/runtime/routes/access-request-decision.ts +0 -297
  53. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -973
  54. package/src/runtime/routes/channel-guardian-routes.ts +0 -19
  55. 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 (Dual-Mode Approval)
108
+ ### Guardian Decision Primitive
109
109
 
110
- All guardian approval decisions — regardless of how they arrive — route through a single unified primitive in `src/approvals/guardian-decision-primitive.ts`. This centralizes decision logic that was previously duplicated across callback button handlers, the conversational approval engine, and the requester self-cancel path.
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 | Purpose |
115
- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | `applyGuardianDecision(params)` | Apply a guardian decision atomically: downgrade `approve_always` for guardian-on-behalf requests, capture approval info, resolve the pending interaction, update the approval record, and mint a scoped grant on approve. Returns `{ applied, reason?, requestId? }`. |
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-like stale protection via `handleChannelDecision`).
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` | Unified decision primitive: `applyCanonicalGuardianDecision` (canonical) and `applyGuardianDecision` (legacy) |
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, openrouter, openai-compatible,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.0-dev.202606222007.e9d01e2",
3
+ "version": "0.10.0-dev.202606222147.354e06a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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",