@vellumai/assistant 0.4.32 → 0.4.34
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/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +85 -4
- package/src/__tests__/actor-token-service.test.ts +4 -12
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -346
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -8
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/relay-server.test.ts +151 -80
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -5
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
- package/src/__tests__/trusted-contact-verification.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +8 -66
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/channels/config.ts +41 -2
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/env.ts +0 -4
- package/src/config/feature-flag-registry.json +4 -4
- package/src/config/user-reference.ts +47 -9
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +19 -44
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +8 -12
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/lifecycle.ts +18 -26
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +76 -15
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +2 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +16 -49
- package/src/runtime/channel-invite-transport.ts +129 -34
- package/src/runtime/channel-invite-transports/email.ts +54 -0
- package/src/runtime/channel-invite-transports/slack.ts +87 -0
- package/src/runtime/channel-invite-transports/sms.ts +74 -0
- package/src/runtime/channel-invite-transports/telegram.ts +35 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +0 -1
- package/src/runtime/invite-redemption-service.ts +191 -11
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +52 -7
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +48 -46
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -6
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/invite-routes.ts +1 -0
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -12
- package/src/tools/browser/runtime-check.ts +110 -3
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/tools/tool-approval-handler.ts +0 -2
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
|
@@ -487,7 +487,7 @@ graph TB
|
|
|
487
487
|
|
|
488
488
|
### How it works
|
|
489
489
|
|
|
490
|
-
1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token
|
|
490
|
+
1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
|
|
491
491
|
|
|
492
492
|
2. **Turn-boundary commits**: After each conversation turn (user message + assistant response cycle), `session.ts` commits workspace changes via `commitTurnChanges(workspaceDir, sessionId, turnNumber)`. The commit runs in the `finally` block of `runAgentLoop`, guarded by a `turnStarted` flag that is set once the agent loop begins executing. This guarantees a commit attempt even when post-processing (e.g. `resolveAssistantAttachments`) throws, or when the user cancels mid-turn. The commit is raced against a configurable timeout (`workspaceGit.turnCommitMaxWaitMs`, default 4s) via `Promise.race`. If the commit exceeds the timeout, the turn proceeds immediately while the commit continues in the background. Note: the background commit is NOT awaited before the next turn starts, so brief cross-turn file attribution windows are possible but accepted as a tradeoff for responsiveness. Commit outcomes are logged with structured fields (`sessionId`, `turnNumber`, `filesChanged`, `durationMs`) for observability.
|
|
493
493
|
|
package/package.json
CHANGED
|
@@ -30,7 +30,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
30
30
|
getDbPath: () => join(testDir, "test.db"),
|
|
31
31
|
getLogPath: () => join(testDir, "test.log"),
|
|
32
32
|
ensureDataDir: () => {},
|
|
33
|
-
readHttpToken: () => "test-bearer-token",
|
|
34
33
|
}));
|
|
35
34
|
|
|
36
35
|
mock.module("../util/logger.js", () => ({
|
|
@@ -100,7 +99,6 @@ function createTestApproval(overrides: Record<string, unknown> = {}) {
|
|
|
100
99
|
return createApprovalRequest({
|
|
101
100
|
runId: `ingress-access-request-${Date.now()}`,
|
|
102
101
|
conversationId: `access-req-telegram-user-unknown-456`,
|
|
103
|
-
assistantId: "self",
|
|
104
102
|
channel: "telegram",
|
|
105
103
|
requesterExternalUserId: "user-unknown-456",
|
|
106
104
|
requesterChatId: "chat-123",
|
|
@@ -158,7 +156,7 @@ describe("access request decision handler", () => {
|
|
|
158
156
|
expect(result.type).toBe("approved");
|
|
159
157
|
|
|
160
158
|
// There should be an active session for this channel
|
|
161
|
-
const session = findActiveSession("
|
|
159
|
+
const session = findActiveSession("telegram");
|
|
162
160
|
expect(session).not.toBeNull();
|
|
163
161
|
expect(session!.expectedExternalUserId).toBe("user-unknown-456");
|
|
164
162
|
expect(session!.expectedChatId).toBe("chat-123");
|
|
@@ -187,7 +185,7 @@ describe("access request decision handler", () => {
|
|
|
187
185
|
expect(updated!.decidedByExternalUserId).toBe("guardian-user-789");
|
|
188
186
|
|
|
189
187
|
// No verification session should be created
|
|
190
|
-
const session = findActiveSession("
|
|
188
|
+
const session = findActiveSession("telegram");
|
|
191
189
|
expect(session).toBeNull();
|
|
192
190
|
});
|
|
193
191
|
|
|
@@ -331,4 +329,87 @@ describe("access request notification delivery", () => {
|
|
|
331
329
|
expect(text).toContain("unable to deliver");
|
|
332
330
|
expect(text).toContain("try again");
|
|
333
331
|
});
|
|
332
|
+
|
|
333
|
+
test("slack approval notification is sent as DM using requesterExternalUserId", async () => {
|
|
334
|
+
await notifyRequesterOfApproval({
|
|
335
|
+
replyCallbackUrl:
|
|
336
|
+
"http://localhost:7830/deliver/slack?threadTs=1234.5678",
|
|
337
|
+
requesterChatId: "C12345-channel",
|
|
338
|
+
requesterExternalUserId: "U98765-user",
|
|
339
|
+
channel: "slack",
|
|
340
|
+
assistantId: "self",
|
|
341
|
+
bearerToken: "test-token",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
345
|
+
const call = deliverReplyCalls[0];
|
|
346
|
+
// Should target the user ID (DM) not the channel
|
|
347
|
+
expect(call.payload.chatId).toBe("U98765-user");
|
|
348
|
+
// threadTs should be stripped — it belongs to the guardian's channel thread
|
|
349
|
+
expect(call.url).not.toContain("threadTs");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("slack denial notification is sent as DM using requesterExternalUserId", async () => {
|
|
353
|
+
await notifyRequesterOfDenial({
|
|
354
|
+
replyCallbackUrl:
|
|
355
|
+
"http://localhost:7830/deliver/slack?threadTs=1234.5678",
|
|
356
|
+
requesterChatId: "C12345-channel",
|
|
357
|
+
requesterExternalUserId: "U98765-user",
|
|
358
|
+
channel: "slack",
|
|
359
|
+
assistantId: "self",
|
|
360
|
+
bearerToken: "test-token",
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
364
|
+
const call = deliverReplyCalls[0];
|
|
365
|
+
expect(call.payload.chatId).toBe("U98765-user");
|
|
366
|
+
expect(call.url).not.toContain("threadTs");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("slack delivery failure notification is sent as DM using requesterExternalUserId", async () => {
|
|
370
|
+
await notifyRequesterOfDeliveryFailure({
|
|
371
|
+
replyCallbackUrl:
|
|
372
|
+
"http://localhost:7830/deliver/slack?threadTs=1234.5678",
|
|
373
|
+
requesterChatId: "C12345-channel",
|
|
374
|
+
requesterExternalUserId: "U98765-user",
|
|
375
|
+
channel: "slack",
|
|
376
|
+
assistantId: "self",
|
|
377
|
+
bearerToken: "test-token",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
381
|
+
const call = deliverReplyCalls[0];
|
|
382
|
+
expect(call.payload.chatId).toBe("U98765-user");
|
|
383
|
+
expect(call.url).not.toContain("threadTs");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("non-slack channels still use requesterChatId and preserve threadTs", async () => {
|
|
387
|
+
await notifyRequesterOfApproval({
|
|
388
|
+
replyCallbackUrl:
|
|
389
|
+
"http://localhost:7830/deliver/telegram?threadTs=1234.5678",
|
|
390
|
+
requesterChatId: "chat-123",
|
|
391
|
+
requesterExternalUserId: "user-456",
|
|
392
|
+
channel: "telegram",
|
|
393
|
+
assistantId: "self",
|
|
394
|
+
bearerToken: "test-token",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
398
|
+
expect(deliverReplyCalls[0].payload.chatId).toBe("chat-123");
|
|
399
|
+
// threadTs should be preserved for non-slack channels
|
|
400
|
+
expect(deliverReplyCalls[0].url).toContain("threadTs=1234.5678");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("slack without requesterExternalUserId falls back to requesterChatId", async () => {
|
|
404
|
+
await notifyRequesterOfApproval({
|
|
405
|
+
replyCallbackUrl: "http://localhost:7830/deliver/slack",
|
|
406
|
+
requesterChatId: "C12345-channel",
|
|
407
|
+
channel: "slack",
|
|
408
|
+
assistantId: "self",
|
|
409
|
+
bearerToken: "test-token",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
413
|
+
expect(deliverReplyCalls[0].payload.chatId).toBe("C12345-channel");
|
|
414
|
+
});
|
|
334
415
|
});
|
|
@@ -39,7 +39,6 @@ mock.module("../config/env.js", () => ({
|
|
|
39
39
|
isHttpAuthDisabled: () => false,
|
|
40
40
|
getInternalGatewayTarget: () => "http://localhost:7822",
|
|
41
41
|
getGatewayBaseUrl: () => "http://localhost:7822",
|
|
42
|
-
getRuntimeProxyBearerToken: () => undefined,
|
|
43
42
|
getRuntimeGatewayOriginSecret: () => undefined,
|
|
44
43
|
isHttpAuthDisabledWithoutSafetyGate: () => false,
|
|
45
44
|
getEnableMonitoring: () => false,
|
|
@@ -130,7 +129,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
130
129
|
|
|
131
130
|
const record = createActorTokenRecord({
|
|
132
131
|
tokenHash,
|
|
133
|
-
assistantId: "self",
|
|
134
132
|
guardianPrincipalId: "principal-store",
|
|
135
133
|
hashedDeviceId: "hashed-dev-store",
|
|
136
134
|
platform: "macos",
|
|
@@ -149,7 +147,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
149
147
|
|
|
150
148
|
createActorTokenRecord({
|
|
151
149
|
tokenHash,
|
|
152
|
-
assistantId: "self",
|
|
153
150
|
guardianPrincipalId: "principal-bind",
|
|
154
151
|
hashedDeviceId: "hashed-dev-bind",
|
|
155
152
|
platform: "ios",
|
|
@@ -157,7 +154,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
157
154
|
});
|
|
158
155
|
|
|
159
156
|
const found = findActiveByDeviceBinding(
|
|
160
|
-
"self",
|
|
161
157
|
"principal-bind",
|
|
162
158
|
"hashed-dev-bind",
|
|
163
159
|
);
|
|
@@ -170,7 +166,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
170
166
|
|
|
171
167
|
createActorTokenRecord({
|
|
172
168
|
tokenHash,
|
|
173
|
-
assistantId: "self",
|
|
174
169
|
guardianPrincipalId: "principal-revoke",
|
|
175
170
|
hashedDeviceId: "hashed-dev-revoke",
|
|
176
171
|
platform: "macos",
|
|
@@ -178,7 +173,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
178
173
|
});
|
|
179
174
|
|
|
180
175
|
const count = revokeByDeviceBinding(
|
|
181
|
-
"self",
|
|
182
176
|
"principal-revoke",
|
|
183
177
|
"hashed-dev-revoke",
|
|
184
178
|
);
|
|
@@ -193,7 +187,6 @@ describe("actor-token store (hash-only)", () => {
|
|
|
193
187
|
|
|
194
188
|
createActorTokenRecord({
|
|
195
189
|
tokenHash,
|
|
196
|
-
assistantId: "self",
|
|
197
190
|
guardianPrincipalId: "principal-single",
|
|
198
191
|
hashedDeviceId: "hashed-dev-single",
|
|
199
192
|
platform: "macos",
|
|
@@ -214,7 +207,7 @@ describe("guardian vellum migration", () => {
|
|
|
214
207
|
const principalId = ensureVellumGuardianBinding("self");
|
|
215
208
|
expect(principalId).toMatch(/^vellum-principal-/);
|
|
216
209
|
|
|
217
|
-
const guardianResult = findGuardianForChannel("vellum"
|
|
210
|
+
const guardianResult = findGuardianForChannel("vellum");
|
|
218
211
|
expect(guardianResult).not.toBeNull();
|
|
219
212
|
expect(guardianResult!.contact.principalId).toBe(principalId);
|
|
220
213
|
expect(guardianResult!.channel.verifiedVia).toBe("startup-migration");
|
|
@@ -228,7 +221,6 @@ describe("guardian vellum migration", () => {
|
|
|
228
221
|
|
|
229
222
|
test("ensureVellumGuardianBinding preserves existing bindings for other channels", () => {
|
|
230
223
|
createGuardianBinding({
|
|
231
|
-
assistantId: "self",
|
|
232
224
|
channel: "telegram",
|
|
233
225
|
guardianExternalUserId: "tg-user-123",
|
|
234
226
|
guardianDeliveryChatId: "tg-chat-456",
|
|
@@ -238,11 +230,11 @@ describe("guardian vellum migration", () => {
|
|
|
238
230
|
|
|
239
231
|
ensureVellumGuardianBinding("self");
|
|
240
232
|
|
|
241
|
-
const tgGuardian = findGuardianForChannel("telegram"
|
|
233
|
+
const tgGuardian = findGuardianForChannel("telegram");
|
|
242
234
|
expect(tgGuardian).not.toBeNull();
|
|
243
235
|
expect(tgGuardian!.channel.externalUserId).toBe("tg-user-123");
|
|
244
236
|
|
|
245
|
-
const vGuardian = findGuardianForChannel("vellum"
|
|
237
|
+
const vGuardian = findGuardianForChannel("vellum");
|
|
246
238
|
expect(vGuardian).not.toBeNull();
|
|
247
239
|
});
|
|
248
240
|
});
|
|
@@ -440,7 +432,7 @@ describe("resolveLocalIpcAuthContext", () => {
|
|
|
440
432
|
|
|
441
433
|
test("enriches actorPrincipalId from vellum guardian binding when present", () => {
|
|
442
434
|
ensureVellumGuardianBinding("self");
|
|
443
|
-
const guardianResult = findGuardianForChannel("vellum"
|
|
435
|
+
const guardianResult = findGuardianForChannel("vellum");
|
|
444
436
|
expect(guardianResult).toBeTruthy();
|
|
445
437
|
|
|
446
438
|
const ctx = resolveLocalIpcAuthContext("session-123");
|
|
@@ -60,7 +60,6 @@ afterAll(() => {
|
|
|
60
60
|
function mintParams(overrides: Partial<MintGrantParams> = {}): MintGrantParams {
|
|
61
61
|
const futureExpiry = new Date(Date.now() + 60_000).toISOString();
|
|
62
62
|
return {
|
|
63
|
-
assistantId: "self",
|
|
64
63
|
scopeMode: "request_id",
|
|
65
64
|
requestChannel: "telegram",
|
|
66
65
|
decisionChannel: "telegram",
|
|
@@ -177,7 +176,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
177
176
|
toolName: "shell",
|
|
178
177
|
inputDigest: computeToolApprovalDigest("shell", { command: "ls" }),
|
|
179
178
|
consumingRequestId: "consumer-1",
|
|
180
|
-
assistantId: "self",
|
|
181
179
|
});
|
|
182
180
|
|
|
183
181
|
expect(result.ok).toBe(true);
|
|
@@ -200,7 +198,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
200
198
|
toolName: "shell",
|
|
201
199
|
inputDigest: digest,
|
|
202
200
|
consumingRequestId: "consumer-2",
|
|
203
|
-
assistantId: "self",
|
|
204
201
|
});
|
|
205
202
|
|
|
206
203
|
expect(result.ok).toBe(true);
|
|
@@ -224,7 +221,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
224
221
|
toolName: "shell",
|
|
225
222
|
inputDigest: digest,
|
|
226
223
|
consumingRequestId: "consumer-3",
|
|
227
|
-
assistantId: "self",
|
|
228
224
|
});
|
|
229
225
|
|
|
230
226
|
expect(result.ok).toBe(true);
|
|
@@ -242,7 +238,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
242
238
|
toolName: "shell",
|
|
243
239
|
inputDigest: computeToolApprovalDigest("shell", { command: "ls" }),
|
|
244
240
|
consumingRequestId: "consumer-miss",
|
|
245
|
-
assistantId: "self",
|
|
246
241
|
},
|
|
247
242
|
{ maxWaitMs: 0 },
|
|
248
243
|
);
|
|
@@ -267,7 +262,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
267
262
|
toolName: "file_write",
|
|
268
263
|
inputDigest: digest,
|
|
269
264
|
consumingRequestId: "consumer-mismatch-tool",
|
|
270
|
-
assistantId: "self",
|
|
271
265
|
},
|
|
272
266
|
{ maxWaitMs: 0 },
|
|
273
267
|
);
|
|
@@ -293,32 +287,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
293
287
|
command: "rm -rf /",
|
|
294
288
|
}),
|
|
295
289
|
consumingRequestId: "consumer-mismatch-input",
|
|
296
|
-
assistantId: "self",
|
|
297
|
-
},
|
|
298
|
-
{ maxWaitMs: 0 },
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
expect(result.ok).toBe(false);
|
|
302
|
-
if (result.ok) return;
|
|
303
|
-
expect(result.reason).toBe("no_match");
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("miss: assistant ID mismatch", async () => {
|
|
307
|
-
mintGrantFromDecision(
|
|
308
|
-
mintParams({
|
|
309
|
-
scopeMode: "request_id",
|
|
310
|
-
requestId: "req-assist",
|
|
311
|
-
assistantId: "assistant-A",
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
const result = await consumeGrantForInvocation(
|
|
316
|
-
{
|
|
317
|
-
requestId: "req-assist",
|
|
318
|
-
toolName: "shell",
|
|
319
|
-
inputDigest: computeToolApprovalDigest("shell", {}),
|
|
320
|
-
consumingRequestId: "consumer-assist-mismatch",
|
|
321
|
-
assistantId: "assistant-B",
|
|
322
290
|
},
|
|
323
291
|
{ maxWaitMs: 0 },
|
|
324
292
|
);
|
|
@@ -344,7 +312,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
344
312
|
toolName: "shell",
|
|
345
313
|
inputDigest: computeToolApprovalDigest("shell", {}),
|
|
346
314
|
consumingRequestId: "consumer-expired",
|
|
347
|
-
assistantId: "self",
|
|
348
315
|
},
|
|
349
316
|
{ maxWaitMs: 0 },
|
|
350
317
|
);
|
|
@@ -368,7 +335,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
368
335
|
toolName: "shell",
|
|
369
336
|
inputDigest: computeToolApprovalDigest("shell", {}),
|
|
370
337
|
consumingRequestId: "consumer-first",
|
|
371
|
-
assistantId: "self",
|
|
372
338
|
});
|
|
373
339
|
expect(first.ok).toBe(true);
|
|
374
340
|
|
|
@@ -378,7 +344,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
378
344
|
toolName: "shell",
|
|
379
345
|
inputDigest: computeToolApprovalDigest("shell", {}),
|
|
380
346
|
consumingRequestId: "consumer-second",
|
|
381
|
-
assistantId: "self",
|
|
382
347
|
},
|
|
383
348
|
{ maxWaitMs: 0 },
|
|
384
349
|
);
|
|
@@ -401,7 +366,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
401
366
|
toolName: "shell",
|
|
402
367
|
inputDigest: digest,
|
|
403
368
|
consumingRequestId: "consumer-sig-first",
|
|
404
|
-
assistantId: "self",
|
|
405
369
|
});
|
|
406
370
|
expect(first.ok).toBe(true);
|
|
407
371
|
|
|
@@ -410,7 +374,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
410
374
|
toolName: "shell",
|
|
411
375
|
inputDigest: digest,
|
|
412
376
|
consumingRequestId: "consumer-sig-second",
|
|
413
|
-
assistantId: "self",
|
|
414
377
|
},
|
|
415
378
|
{ maxWaitMs: 0 },
|
|
416
379
|
);
|
|
@@ -439,7 +402,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
439
402
|
toolName: "shell",
|
|
440
403
|
inputDigest: digest,
|
|
441
404
|
consumingRequestId: "consumer-ctx",
|
|
442
|
-
assistantId: "self",
|
|
443
405
|
conversationId: "conv-ctx",
|
|
444
406
|
callSessionId: "call-ctx",
|
|
445
407
|
});
|
|
@@ -463,7 +425,6 @@ describe("approval-primitive / consumeGrantForInvocation", () => {
|
|
|
463
425
|
toolName: "shell",
|
|
464
426
|
inputDigest: digest,
|
|
465
427
|
consumingRequestId: "consumer-ctx-mismatch",
|
|
466
|
-
assistantId: "self",
|
|
467
428
|
conversationId: "conv-B",
|
|
468
429
|
},
|
|
469
430
|
{ maxWaitMs: 0 },
|
|
@@ -497,7 +458,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
497
458
|
toolName: "shell",
|
|
498
459
|
inputDigest: digest,
|
|
499
460
|
consumingRequestId: "consumer-async-immediate",
|
|
500
|
-
assistantId: "self",
|
|
501
461
|
});
|
|
502
462
|
const elapsed = Date.now() - start;
|
|
503
463
|
|
|
@@ -528,7 +488,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
528
488
|
toolName: "shell",
|
|
529
489
|
inputDigest: digest,
|
|
530
490
|
consumingRequestId: "consumer-async-delayed",
|
|
531
|
-
assistantId: "self",
|
|
532
491
|
},
|
|
533
492
|
{ maxWaitMs: 5_000, intervalMs: 100 },
|
|
534
493
|
);
|
|
@@ -553,7 +512,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
553
512
|
toolName: "shell",
|
|
554
513
|
inputDigest: digest,
|
|
555
514
|
consumingRequestId: "consumer-async-timeout",
|
|
556
|
-
assistantId: "self",
|
|
557
515
|
},
|
|
558
516
|
{ maxWaitMs: 500, intervalMs: 100 },
|
|
559
517
|
);
|
|
@@ -580,7 +538,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
580
538
|
toolName: "shell",
|
|
581
539
|
inputDigest: digest,
|
|
582
540
|
consumingRequestId: "consumer-aborted",
|
|
583
|
-
assistantId: "self",
|
|
584
541
|
},
|
|
585
542
|
{ maxWaitMs: 2_000, intervalMs: 50, signal: controller.signal },
|
|
586
543
|
);
|
|
@@ -606,7 +563,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
606
563
|
toolName: "shell",
|
|
607
564
|
inputDigest: digest,
|
|
608
565
|
consumingRequestId: "consumer-pre-aborted",
|
|
609
|
-
assistantId: "self",
|
|
610
566
|
},
|
|
611
567
|
{ maxWaitMs: 2_000, intervalMs: 50, signal: controller.signal },
|
|
612
568
|
);
|
|
@@ -628,7 +584,6 @@ describe("approval-primitive / consumeGrantForInvocation retry", () => {
|
|
|
628
584
|
toolName: "shell",
|
|
629
585
|
inputDigest: digest,
|
|
630
586
|
consumingRequestId: "consumer-no-retry",
|
|
631
|
-
assistantId: "self",
|
|
632
587
|
},
|
|
633
588
|
{ maxWaitMs: 0 },
|
|
634
589
|
);
|
|
@@ -57,7 +57,6 @@ mock.module("../config/env.js", () => ({
|
|
|
57
57
|
getGatewayPort: () => 7830,
|
|
58
58
|
getRuntimeHttpPort: () => 7821,
|
|
59
59
|
getRuntimeHttpHost: () => "127.0.0.1",
|
|
60
|
-
getRuntimeProxyBearerToken: () => undefined,
|
|
61
60
|
getRuntimeGatewayOriginSecret: () => undefined,
|
|
62
61
|
getIngressPublicBaseUrl: () => undefined,
|
|
63
62
|
setIngressPublicBaseUrl: () => {},
|
|
@@ -18,6 +18,8 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
|
18
18
|
* - No assistant-scoped route handlers in the daemon HTTP server
|
|
19
19
|
* - No hardcoded `'self'` string for assistant scoping (use the constant)
|
|
20
20
|
* - The constant itself equals `'self'`
|
|
21
|
+
* - No `assistantId` columns in daemon SQLite schema definitions
|
|
22
|
+
* - No `assistantId` parameter in daemon store function signatures
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -466,4 +468,152 @@ describe("assistant ID boundary", () => {
|
|
|
466
468
|
).not.toContain("assistantId");
|
|
467
469
|
}
|
|
468
470
|
});
|
|
471
|
+
|
|
472
|
+
// -------------------------------------------------------------------------
|
|
473
|
+
// Rule (f): No assistantId columns in daemon SQLite schema definitions
|
|
474
|
+
//
|
|
475
|
+
// The daemon is assistant-agnostic — it uses DAEMON_INTERNAL_ASSISTANT_ID
|
|
476
|
+
// implicitly. Schema files must not define assistantId columns, which would
|
|
477
|
+
// re-introduce assistant-scoped storage in the daemon layer.
|
|
478
|
+
// -------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
test("no assistantId columns in daemon SQLite schema definitions", () => {
|
|
481
|
+
const repoRoot = getRepoRoot();
|
|
482
|
+
|
|
483
|
+
// Scan all Drizzle schema files for assistantId column definitions.
|
|
484
|
+
// The Drizzle ORM pattern is `assistantId: text(` for defining a text
|
|
485
|
+
// column named assistantId.
|
|
486
|
+
const schemaGlobs = [
|
|
487
|
+
"assistant/src/memory/schema/*.ts",
|
|
488
|
+
"assistant/src/memory/schema/**/*.ts",
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
let grepOutput = "";
|
|
492
|
+
try {
|
|
493
|
+
grepOutput = execFileSync(
|
|
494
|
+
"git",
|
|
495
|
+
["grep", "-nE", "assistantId\\s*:\\s*text\\(", "--", ...schemaGlobs],
|
|
496
|
+
{ encoding: "utf-8", cwd: repoRoot },
|
|
497
|
+
).trim();
|
|
498
|
+
} catch (err) {
|
|
499
|
+
// Exit code 1 means no matches — happy path
|
|
500
|
+
if ((err as { status?: number }).status === 1) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const lines = grepOutput.split("\n").filter((l) => l.length > 0);
|
|
507
|
+
const violations = lines.filter((line) => {
|
|
508
|
+
// Allow comments
|
|
509
|
+
const parts = line.split(":");
|
|
510
|
+
const content = parts.slice(2).join(":").trim();
|
|
511
|
+
if (
|
|
512
|
+
content.startsWith("//") ||
|
|
513
|
+
content.startsWith("*") ||
|
|
514
|
+
content.startsWith("/*")
|
|
515
|
+
) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (violations.length > 0) {
|
|
522
|
+
const message = [
|
|
523
|
+
"Found `assistantId` column definitions in daemon SQLite schema files.",
|
|
524
|
+
"`assistantId` columns are not allowed in daemon schema — the daemon uses",
|
|
525
|
+
"`DAEMON_INTERNAL_ASSISTANT_ID` implicitly and is assistant-agnostic.",
|
|
526
|
+
"",
|
|
527
|
+
"Violations:",
|
|
528
|
+
...violations.map((v) => ` - ${v}`),
|
|
529
|
+
].join("\n");
|
|
530
|
+
|
|
531
|
+
expect(violations, message).toEqual([]);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// -------------------------------------------------------------------------
|
|
536
|
+
// Rule (g): No assistantId parameter in daemon store function signatures
|
|
537
|
+
//
|
|
538
|
+
// Store functions in the daemon layer must not accept assistantId as a
|
|
539
|
+
// parameter. The daemon is assistant-agnostic — all assistant scoping
|
|
540
|
+
// uses DAEMON_INTERNAL_ASSISTANT_ID internally.
|
|
541
|
+
// -------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
test("no assistantId parameter in daemon store function signatures", () => {
|
|
544
|
+
const repoRoot = getRepoRoot();
|
|
545
|
+
|
|
546
|
+
// Scan store files for exported function signatures that include
|
|
547
|
+
// assistantId as a parameter. This covers memory stores, contact stores,
|
|
548
|
+
// notification stores, credential/token stores, and call stores.
|
|
549
|
+
const storeGlobs = [
|
|
550
|
+
"assistant/src/memory/*.ts",
|
|
551
|
+
"assistant/src/contacts/*.ts",
|
|
552
|
+
"assistant/src/notifications/*.ts",
|
|
553
|
+
"assistant/src/runtime/auth/credential-service.ts",
|
|
554
|
+
"assistant/src/runtime/actor-token-store.ts",
|
|
555
|
+
"assistant/src/runtime/actor-refresh-token-store.ts",
|
|
556
|
+
"assistant/src/calls/call-store.ts",
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
// Match exported function declarations/expressions with assistantId in
|
|
560
|
+
// their parameter lists. Patterns:
|
|
561
|
+
// export function foo(assistantId
|
|
562
|
+
// export function foo(bar, assistantId
|
|
563
|
+
// export async function foo(assistantId
|
|
564
|
+
// export const foo = (assistantId
|
|
565
|
+
// export const foo = async (assistantId
|
|
566
|
+
// We use a broad pattern that catches assistantId appearing after an
|
|
567
|
+
// opening paren in an export context.
|
|
568
|
+
const pattern =
|
|
569
|
+
"export\\s+(async\\s+)?function\\s+\\w+\\s*\\([^)]*assistantId|export\\s+const\\s+\\w+\\s*=\\s*(async\\s+)?\\([^)]*assistantId";
|
|
570
|
+
|
|
571
|
+
let grepOutput = "";
|
|
572
|
+
try {
|
|
573
|
+
grepOutput = execFileSync(
|
|
574
|
+
"git",
|
|
575
|
+
["grep", "-nE", pattern, "--", ...storeGlobs],
|
|
576
|
+
{ encoding: "utf-8", cwd: repoRoot },
|
|
577
|
+
).trim();
|
|
578
|
+
} catch (err) {
|
|
579
|
+
// Exit code 1 means no matches — happy path
|
|
580
|
+
if ((err as { status?: number }).status === 1) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const allLines = grepOutput.split("\n").filter((l) => l.length > 0);
|
|
587
|
+
const violations = allLines.filter((line) => {
|
|
588
|
+
const filePath = line.split(":")[0];
|
|
589
|
+
if (isTestFile(filePath)) return false;
|
|
590
|
+
if (isMigrationFile(filePath)) return false;
|
|
591
|
+
|
|
592
|
+
// Allow comments
|
|
593
|
+
const parts = line.split(":");
|
|
594
|
+
const content = parts.slice(2).join(":").trim();
|
|
595
|
+
if (
|
|
596
|
+
content.startsWith("//") ||
|
|
597
|
+
content.startsWith("*") ||
|
|
598
|
+
content.startsWith("/*")
|
|
599
|
+
) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return true;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
if (violations.length > 0) {
|
|
607
|
+
const message = [
|
|
608
|
+
"Found daemon store functions with `assistantId` in their parameter signatures.",
|
|
609
|
+
"Store functions must not accept `assistantId` — the daemon is assistant-agnostic",
|
|
610
|
+
"and uses `DAEMON_INTERNAL_ASSISTANT_ID` implicitly.",
|
|
611
|
+
"",
|
|
612
|
+
"Violations:",
|
|
613
|
+
...violations.map((v) => ` - ${v}`),
|
|
614
|
+
].join("\n");
|
|
615
|
+
|
|
616
|
+
expect(violations, message).toEqual([]);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
469
619
|
});
|