@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +126 -59
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -59,7 +59,6 @@ import type { ServerMessage } from './ipc-protocol.js';
|
|
|
59
59
|
import { initializeProvidersAndTools, registerMessagingProviders,registerWatcherProviders } from './providers-setup.js';
|
|
60
60
|
import { seedInterfaceFiles } from './seed-files.js';
|
|
61
61
|
import { DaemonServer } from './server.js';
|
|
62
|
-
import { setApprovalConversationGenerator, setGuardianActionCopyGenerator, setGuardianFollowUpConversationGenerator } from './session-process.js';
|
|
63
62
|
import { initSlashPairingContext } from './session-slash.js';
|
|
64
63
|
import { installShutdownHandlers } from './shutdown-handlers.js';
|
|
65
64
|
|
|
@@ -320,21 +319,9 @@ export async function runDaemon(): Promise<void> {
|
|
|
320
319
|
server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
|
|
321
320
|
interfacesDir: getInterfacesDir(),
|
|
322
321
|
approvalCopyGenerator: createApprovalCopyGenerator(),
|
|
323
|
-
approvalConversationGenerator: (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return gen;
|
|
327
|
-
})(),
|
|
328
|
-
guardianActionCopyGenerator: (() => {
|
|
329
|
-
const gen = createGuardianActionCopyGenerator();
|
|
330
|
-
setGuardianActionCopyGenerator(gen);
|
|
331
|
-
return gen;
|
|
332
|
-
})(),
|
|
333
|
-
guardianFollowUpConversationGenerator: (() => {
|
|
334
|
-
const gen = createGuardianFollowUpConversationGenerator();
|
|
335
|
-
setGuardianFollowUpConversationGenerator(gen);
|
|
336
|
-
return gen;
|
|
337
|
-
})(),
|
|
322
|
+
approvalConversationGenerator: createApprovalConversationGenerator(),
|
|
323
|
+
guardianActionCopyGenerator: createGuardianActionCopyGenerator(),
|
|
324
|
+
guardianFollowUpConversationGenerator: createGuardianFollowUpConversationGenerator(),
|
|
338
325
|
sendMessageDeps: {
|
|
339
326
|
getOrCreateSession: (conversationId) =>
|
|
340
327
|
server.getSessionForMessages(conversationId),
|
|
@@ -390,6 +377,25 @@ export async function runDaemon(): Promise<void> {
|
|
|
390
377
|
socketPath: getSocketPath(),
|
|
391
378
|
});
|
|
392
379
|
|
|
380
|
+
// Download embedding runtime in background (non-blocking).
|
|
381
|
+
// If download fails, local embeddings gracefully fall back to cloud backends.
|
|
382
|
+
void (async () => {
|
|
383
|
+
try {
|
|
384
|
+
const { EmbeddingRuntimeManager } = await import('../memory/embedding-runtime-manager.js');
|
|
385
|
+
const runtimeManager = new EmbeddingRuntimeManager();
|
|
386
|
+
if (!runtimeManager.isReady()) {
|
|
387
|
+
log.info('Downloading embedding runtime in background...');
|
|
388
|
+
await runtimeManager.ensureInstalled();
|
|
389
|
+
// Reset the localBackendBroken flag so auto mode retries local embeddings
|
|
390
|
+
const { clearEmbeddingBackendCache } = await import('../memory/embedding-backend.js');
|
|
391
|
+
clearEmbeddingBackendCache();
|
|
392
|
+
log.info('Embedding runtime download complete');
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
log.warn({ err }, 'Embedding runtime download failed — local embeddings will use cloud fallback');
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
|
|
393
399
|
if (config.auditLog.retentionDays > 0) {
|
|
394
400
|
try {
|
|
395
401
|
rotateToolInvocations(config.auditLog.retentionDays);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pairing request store with TTL and disk persistence.
|
|
3
3
|
*
|
|
4
4
|
* Each pairing request lives for at most TTL_MS (5 minutes) before
|
|
5
5
|
* being swept as expired. Status transitions:
|
|
6
6
|
* registered → pending → approved | denied | expired
|
|
7
|
+
*
|
|
8
|
+
* Entries are persisted to ~/.vellum/protected/pairing-requests.json
|
|
9
|
+
* using the same atomic-write pattern as approved-devices-store.ts
|
|
10
|
+
* so that device bindings survive daemon restarts.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
10
16
|
|
|
11
17
|
import { getLogger } from '../util/logger.js';
|
|
18
|
+
import { getRootDir } from '../util/platform.js';
|
|
12
19
|
|
|
13
20
|
const log = getLogger('pairing-store');
|
|
14
21
|
|
|
@@ -40,11 +47,60 @@ function timingSafeCompare(a: string, b: string): boolean {
|
|
|
40
47
|
return timingSafeEqual(bufA, bufB);
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
interface PairingStoreFile {
|
|
51
|
+
version: 1;
|
|
52
|
+
requests: PairingRequest[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStorePath(): string {
|
|
56
|
+
return join(getRootDir(), 'protected', 'pairing-requests.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadFromDisk(): Map<string, PairingRequest> {
|
|
60
|
+
const path = getStorePath();
|
|
61
|
+
if (!existsSync(path)) {
|
|
62
|
+
return new Map();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(path, 'utf-8');
|
|
66
|
+
const data = JSON.parse(raw) as PairingStoreFile;
|
|
67
|
+
if (data.version !== 1 || !Array.isArray(data.requests)) {
|
|
68
|
+
log.warn('Invalid pairing-requests.json format, starting fresh');
|
|
69
|
+
return new Map();
|
|
70
|
+
}
|
|
71
|
+
const map = new Map<string, PairingRequest>();
|
|
72
|
+
for (const entry of data.requests) {
|
|
73
|
+
map.set(entry.pairingRequestId, entry);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log.error({ err }, 'Failed to load pairing-requests.json');
|
|
78
|
+
return new Map();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveToDisk(requests: Map<string, PairingRequest>): void {
|
|
83
|
+
const path = getStorePath();
|
|
84
|
+
const dir = dirname(path);
|
|
85
|
+
if (!existsSync(dir)) {
|
|
86
|
+
mkdirSync(dir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
const data: PairingStoreFile = {
|
|
89
|
+
version: 1,
|
|
90
|
+
requests: Array.from(requests.values()),
|
|
91
|
+
};
|
|
92
|
+
const tmpPath = path + '.tmp.' + process.pid;
|
|
93
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
94
|
+
renameSync(tmpPath, path);
|
|
95
|
+
chmodSync(path, 0o600);
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
export class PairingStore {
|
|
44
99
|
private requests = new Map<string, PairingRequest>();
|
|
45
100
|
private sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
46
101
|
|
|
47
102
|
start(): void {
|
|
103
|
+
this.requests = loadFromDisk();
|
|
48
104
|
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
|
|
49
105
|
}
|
|
50
106
|
|
|
@@ -84,6 +140,7 @@ export class PairingStore {
|
|
|
84
140
|
localLanUrl: params.localLanUrl ?? null,
|
|
85
141
|
createdAt: Date.now(),
|
|
86
142
|
});
|
|
143
|
+
this.persist();
|
|
87
144
|
|
|
88
145
|
log.info({ pairingRequestId: params.pairingRequestId }, 'Pairing request registered');
|
|
89
146
|
return { ok: true };
|
|
@@ -98,7 +155,7 @@ export class PairingStore {
|
|
|
98
155
|
pairingSecret: string;
|
|
99
156
|
deviceId: string;
|
|
100
157
|
deviceName: string;
|
|
101
|
-
}): { ok: true; entry: PairingRequest } | { ok: false; reason: 'not_found' | 'invalid_secret' | 'expired' } {
|
|
158
|
+
}): { ok: true; entry: PairingRequest } | { ok: false; reason: 'not_found' | 'invalid_secret' | 'expired' | 'already_paired' } {
|
|
102
159
|
const entry = this.requests.get(params.pairingRequestId);
|
|
103
160
|
if (!entry) {
|
|
104
161
|
return { ok: false, reason: 'not_found' };
|
|
@@ -113,11 +170,21 @@ export class PairingStore {
|
|
|
113
170
|
return { ok: false, reason: 'invalid_secret' };
|
|
114
171
|
}
|
|
115
172
|
|
|
116
|
-
|
|
173
|
+
const hashedDeviceId = hashValue(params.deviceId);
|
|
174
|
+
|
|
175
|
+
// If a device has already been bound to this pairing request, reject
|
|
176
|
+
// attempts from a different device to prevent hijacking.
|
|
177
|
+
if (entry.hashedDeviceId && !timingSafeCompare(entry.hashedDeviceId, hashedDeviceId)) {
|
|
178
|
+
log.warn({ pairingRequestId: params.pairingRequestId }, 'Pairing request already bound to a different device');
|
|
179
|
+
return { ok: false, reason: 'already_paired' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
entry.hashedDeviceId = hashedDeviceId;
|
|
117
183
|
entry.deviceName = params.deviceName;
|
|
118
184
|
if (entry.status === 'registered') {
|
|
119
185
|
entry.status = 'pending';
|
|
120
186
|
}
|
|
187
|
+
this.persist();
|
|
121
188
|
|
|
122
189
|
return { ok: true, entry };
|
|
123
190
|
}
|
|
@@ -130,6 +197,7 @@ export class PairingStore {
|
|
|
130
197
|
if (!entry) return null;
|
|
131
198
|
entry.status = 'approved';
|
|
132
199
|
entry.bearerToken = bearerToken;
|
|
200
|
+
this.persist();
|
|
133
201
|
return entry;
|
|
134
202
|
}
|
|
135
203
|
|
|
@@ -140,6 +208,7 @@ export class PairingStore {
|
|
|
140
208
|
const entry = this.requests.get(pairingRequestId);
|
|
141
209
|
if (!entry) return null;
|
|
142
210
|
entry.status = 'denied';
|
|
211
|
+
this.persist();
|
|
143
212
|
return entry;
|
|
144
213
|
}
|
|
145
214
|
|
|
@@ -160,19 +229,33 @@ export class PairingStore {
|
|
|
160
229
|
return timingSafeCompare(entry.hashedPairingSecret, hashedSecret);
|
|
161
230
|
}
|
|
162
231
|
|
|
232
|
+
private persist(): void {
|
|
233
|
+
try {
|
|
234
|
+
saveToDisk(this.requests);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
log.error({ err }, 'Failed to persist pairing requests to disk');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
163
240
|
private sweep(): void {
|
|
164
241
|
const now = Date.now();
|
|
242
|
+
let changed = false;
|
|
165
243
|
for (const [id, entry] of this.requests) {
|
|
166
244
|
if (now - entry.createdAt > TTL_MS) {
|
|
167
245
|
if (entry.status !== 'approved') {
|
|
168
246
|
entry.status = 'expired';
|
|
247
|
+
changed = true;
|
|
169
248
|
}
|
|
170
249
|
// Remove entries older than 2x TTL regardless of status
|
|
171
250
|
if (now - entry.createdAt > TTL_MS * 2) {
|
|
172
251
|
this.requests.delete(id);
|
|
252
|
+
changed = true;
|
|
173
253
|
log.debug({ pairingRequestId: id }, 'Pairing request swept');
|
|
174
254
|
}
|
|
175
255
|
}
|
|
176
256
|
}
|
|
257
|
+
if (changed) {
|
|
258
|
+
this.persist();
|
|
259
|
+
}
|
|
177
260
|
}
|
|
178
261
|
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { buildSystemPrompt } from '../config/system-prompt.js';
|
|
|
10
10
|
import type { HeartbeatService } from '../heartbeat/heartbeat-service.js';
|
|
11
11
|
import { bootstrapHomeBaseAppLink } from '../home-base/bootstrap.js';
|
|
12
12
|
import * as attachmentsStore from '../memory/attachments-store.js';
|
|
13
|
+
import {
|
|
14
|
+
createCanonicalGuardianRequest,
|
|
15
|
+
generateCanonicalRequestCode,
|
|
16
|
+
} from '../memory/canonical-guardian-store.js';
|
|
13
17
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
14
18
|
import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
|
|
15
19
|
import { RateLimitProvider } from '../providers/ratelimit.js';
|
|
@@ -114,6 +118,20 @@ function makePendingInteractionRegistrar(
|
|
|
114
118
|
persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
|
|
115
119
|
},
|
|
116
120
|
});
|
|
121
|
+
|
|
122
|
+
// Create a canonical guardian request so IPC/HTTP handlers can find it
|
|
123
|
+
// via applyCanonicalGuardianDecision.
|
|
124
|
+
createCanonicalGuardianRequest({
|
|
125
|
+
id: msg.requestId,
|
|
126
|
+
kind: 'tool_approval',
|
|
127
|
+
sourceType: 'desktop',
|
|
128
|
+
sourceChannel: 'vellum',
|
|
129
|
+
conversationId,
|
|
130
|
+
toolName: msg.toolName,
|
|
131
|
+
status: 'pending',
|
|
132
|
+
requestCode: generateCanonicalRequestCode(),
|
|
133
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
134
|
+
});
|
|
117
135
|
} else if (msg.type === 'secret_request') {
|
|
118
136
|
pendingInteractions.register(msg.requestId, {
|
|
119
137
|
session,
|
|
@@ -330,6 +330,7 @@ export async function handleMessageComplete(
|
|
|
330
330
|
// Clean assistant content and accumulate directives
|
|
331
331
|
const { cleanedContent, directives: msgDirectives, warnings: msgWarnings } =
|
|
332
332
|
cleanAssistantContent(event.message.content);
|
|
333
|
+
const cleanedBlocks = cleanedContent as ContentBlock[];
|
|
333
334
|
state.accumulatedDirectives.push(...msgDirectives);
|
|
334
335
|
state.directiveWarnings.push(...msgWarnings);
|
|
335
336
|
if (msgDirectives.length > 0) {
|
|
@@ -340,7 +341,7 @@ export async function handleMessageComplete(
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// Build content with UI surfaces
|
|
343
|
-
const contentWithSurfaces: ContentBlock[] = [...
|
|
344
|
+
const contentWithSurfaces: ContentBlock[] = [...cleanedBlocks];
|
|
344
345
|
for (const surface of deps.ctx.currentTurnSurfaces) {
|
|
345
346
|
contentWithSurfaces.push({
|
|
346
347
|
type: 'ui_surface',
|
|
@@ -371,9 +372,9 @@ export async function handleMessageComplete(
|
|
|
371
372
|
deps.ctx.currentTurnSurfaces = [];
|
|
372
373
|
|
|
373
374
|
// Emit trace event
|
|
374
|
-
const charCount =
|
|
375
|
-
.filter((b)
|
|
376
|
-
.reduce((sum
|
|
375
|
+
const charCount = cleanedBlocks
|
|
376
|
+
.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
|
|
377
|
+
.reduce((sum, b) => sum + b.text.length, 0);
|
|
377
378
|
const toolUseCount = event.message.content
|
|
378
379
|
.filter((b) => b.type === 'tool_use')
|
|
379
380
|
.length;
|
|
@@ -25,6 +25,7 @@ import { stripMemoryRecallMessages } from '../memory/retriever.js';
|
|
|
25
25
|
import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
26
26
|
import type { ContentBlock,Message } from '../providers/types.js';
|
|
27
27
|
import type { Provider } from '../providers/types.js';
|
|
28
|
+
import { resolveActorTrust } from '../runtime/actor-trust-resolver.js';
|
|
28
29
|
import type { UsageActor } from '../usage/actors.js';
|
|
29
30
|
import { getLogger } from '../util/logger.js';
|
|
30
31
|
import { truncate } from '../util/truncate.js';
|
|
@@ -55,9 +56,11 @@ import { raceWithTimeout,stripMediaPayloadsForRetry } from './session-media-retr
|
|
|
55
56
|
import { prepareMemoryContext } from './session-memory.js';
|
|
56
57
|
import type { MessageQueue } from './session-queue-manager.js';
|
|
57
58
|
import type { QueueDrainReason } from './session-queue-manager.js';
|
|
58
|
-
import type { ActiveSurfaceContext, ChannelCapabilities, ChannelTurnContextParams, GuardianRuntimeContext,InterfaceTurnContextParams } from './session-runtime-assembly.js';
|
|
59
|
+
import type { ActiveSurfaceContext, ChannelCapabilities, ChannelTurnContextParams, GuardianRuntimeContext, InboundActorContext, InterfaceTurnContextParams } from './session-runtime-assembly.js';
|
|
59
60
|
import {
|
|
60
61
|
applyRuntimeInjections,
|
|
62
|
+
inboundActorContextFromGuardian,
|
|
63
|
+
inboundActorContextFromTrust,
|
|
61
64
|
stripInjectedContext,
|
|
62
65
|
} from './session-runtime-assembly.js';
|
|
63
66
|
import type { SkillProjectionCache } from './session-skill-tools.js';
|
|
@@ -102,6 +105,7 @@ export interface AgentLoopSessionContext {
|
|
|
102
105
|
channelCapabilities?: ChannelCapabilities;
|
|
103
106
|
commandIntent?: { type: string; payload?: string; languageCode?: string };
|
|
104
107
|
guardianContext?: GuardianRuntimeContext;
|
|
108
|
+
assistantId?: string;
|
|
105
109
|
voiceCallControlPrompt?: string;
|
|
106
110
|
|
|
107
111
|
readonly coreToolNames: Set<string>;
|
|
@@ -254,7 +258,7 @@ export async function runAgentLoopImpl(
|
|
|
254
258
|
conflictGate: ctx.conflictGate,
|
|
255
259
|
scopeId: ctx.memoryPolicy.scopeId,
|
|
256
260
|
includeDefaultFallback: ctx.memoryPolicy.includeDefaultFallback,
|
|
257
|
-
|
|
261
|
+
guardianTrustClass: ctx.guardianContext?.trustClass,
|
|
258
262
|
isInteractive: options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock),
|
|
259
263
|
},
|
|
260
264
|
content,
|
|
@@ -349,6 +353,28 @@ export async function runAgentLoopImpl(
|
|
|
349
353
|
conversationOriginInterface: getConversationOriginInterface(ctx.conversationId),
|
|
350
354
|
};
|
|
351
355
|
|
|
356
|
+
// Resolve the inbound actor context for the model's <inbound_actor_context>
|
|
357
|
+
// block. When the session carries enough identity info, use the unified
|
|
358
|
+
// actor trust resolver so member status/policy and guardian binding details
|
|
359
|
+
// are fresh for this turn. The session runtime context remains the source
|
|
360
|
+
// for policy gating; this block is model-facing grounding metadata.
|
|
361
|
+
let resolvedInboundActorContext: InboundActorContext | null = null;
|
|
362
|
+
if (ctx.guardianContext) {
|
|
363
|
+
const gc = ctx.guardianContext;
|
|
364
|
+
if (gc.requesterExternalUserId && gc.requesterChatId) {
|
|
365
|
+
const actorTrust = resolveActorTrust({
|
|
366
|
+
assistantId: ctx.assistantId ?? 'self',
|
|
367
|
+
sourceChannel: gc.sourceChannel,
|
|
368
|
+
externalChatId: gc.requesterChatId,
|
|
369
|
+
senderExternalUserId: gc.requesterExternalUserId,
|
|
370
|
+
senderDisplayName: gc.requesterSenderDisplayName,
|
|
371
|
+
});
|
|
372
|
+
resolvedInboundActorContext = inboundActorContextFromTrust(actorTrust);
|
|
373
|
+
} else {
|
|
374
|
+
resolvedInboundActorContext = inboundActorContextFromGuardian(gc);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
352
378
|
const isInteractiveResolved = options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock);
|
|
353
379
|
runMessages = applyRuntimeInjections(runMessages, {
|
|
354
380
|
softConflictInstruction,
|
|
@@ -358,7 +384,7 @@ export async function runAgentLoopImpl(
|
|
|
358
384
|
channelCommandContext: ctx.commandIntent ?? null,
|
|
359
385
|
channelTurnContext,
|
|
360
386
|
interfaceTurnContext,
|
|
361
|
-
|
|
387
|
+
inboundActorContext: resolvedInboundActorContext,
|
|
362
388
|
temporalContext,
|
|
363
389
|
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
364
390
|
isNonInteractive: !isInteractiveResolved,
|
|
@@ -477,7 +503,7 @@ export async function runAgentLoopImpl(
|
|
|
477
503
|
channelCommandContext: ctx.commandIntent ?? null,
|
|
478
504
|
channelTurnContext,
|
|
479
505
|
interfaceTurnContext,
|
|
480
|
-
|
|
506
|
+
inboundActorContext: resolvedInboundActorContext,
|
|
481
507
|
temporalContext,
|
|
482
508
|
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
483
509
|
isNonInteractive: !isInteractiveResolved,
|
|
@@ -515,7 +541,7 @@ export async function runAgentLoopImpl(
|
|
|
515
541
|
channelCommandContext: ctx.commandIntent ?? null,
|
|
516
542
|
channelTurnContext,
|
|
517
543
|
interfaceTurnContext,
|
|
518
|
-
|
|
544
|
+
inboundActorContext: resolvedInboundActorContext,
|
|
519
545
|
temporalContext,
|
|
520
546
|
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
521
547
|
isNonInteractive: !isInteractiveResolved,
|
|
@@ -604,7 +630,8 @@ export async function runAgentLoopImpl(
|
|
|
604
630
|
const newMessages = updatedHistory.slice(preRunHistoryLength).map((msg) => {
|
|
605
631
|
if (msg.role !== 'assistant') return msg;
|
|
606
632
|
const { cleanedContent } = cleanAssistantContent(msg.content);
|
|
607
|
-
|
|
633
|
+
const cleanedBlocks = cleanedContent as ContentBlock[];
|
|
634
|
+
return { ...msg, content: cleanedBlocks };
|
|
608
635
|
});
|
|
609
636
|
|
|
610
637
|
const hasAssistantResponse = newMessages.some((msg) => msg.role === 'assistant');
|
|
@@ -24,30 +24,38 @@ import { resetSkillToolProjection } from './session-skill-tools.js';
|
|
|
24
24
|
|
|
25
25
|
const log = getLogger('session-lifecycle');
|
|
26
26
|
|
|
27
|
-
type
|
|
27
|
+
type GuardianTrustClass = GuardianRuntimeContext['trustClass'];
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function parseProvenanceTrustClass(metadata: string | null): GuardianTrustClass | undefined {
|
|
30
30
|
if (!metadata) return undefined;
|
|
31
31
|
try {
|
|
32
|
-
const parsed = JSON.parse(metadata) as {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const parsed = JSON.parse(metadata) as {
|
|
33
|
+
provenanceTrustClass?: unknown;
|
|
34
|
+
provenanceActorRole?: unknown;
|
|
35
|
+
};
|
|
36
|
+
const trustClass = parsed?.provenanceTrustClass;
|
|
37
|
+
if (trustClass === 'guardian' || trustClass === 'trusted_contact' || trustClass === 'unknown') {
|
|
38
|
+
return trustClass;
|
|
36
39
|
}
|
|
40
|
+
// Legacy fallback for rows persisted before provenanceTrustClass existed.
|
|
41
|
+
const legacyRole = parsed?.provenanceActorRole;
|
|
42
|
+
if (legacyRole === 'guardian') return 'guardian';
|
|
43
|
+
if (legacyRole === 'non-guardian') return 'trusted_contact';
|
|
44
|
+
if (legacyRole === 'unverified_channel') return 'unknown';
|
|
37
45
|
} catch {
|
|
38
46
|
// Ignore malformed metadata and treat as unknown provenance.
|
|
39
47
|
}
|
|
40
48
|
return undefined;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
function
|
|
44
|
-
return
|
|
51
|
+
function isUntrustedTrustClass(trustClass: GuardianTrustClass | undefined): boolean {
|
|
52
|
+
return trustClass === 'trusted_contact' || trustClass === 'unknown';
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function filterMessagesForUntrustedActor(messages: conversationStore.MessageRow[]): conversationStore.MessageRow[] {
|
|
48
56
|
return messages.filter((m) => {
|
|
49
|
-
const
|
|
50
|
-
return
|
|
57
|
+
const provenanceTrustClass = parseProvenanceTrustClass(m.metadata);
|
|
58
|
+
return provenanceTrustClass === 'trusted_contact' || provenanceTrustClass === 'unknown';
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -59,8 +67,8 @@ export interface LoadFromDbContext {
|
|
|
59
67
|
usageStats: UsageStats;
|
|
60
68
|
contextCompactedMessageCount: number;
|
|
61
69
|
contextCompactedAt: number | null;
|
|
62
|
-
guardianContext?: {
|
|
63
|
-
|
|
70
|
+
guardianContext?: { trustClass: GuardianTrustClass };
|
|
71
|
+
loadedHistoryTrustClass?: GuardianTrustClass;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
export interface AbortContext {
|
|
@@ -89,17 +97,17 @@ export interface DisposeContext extends AbortContext {
|
|
|
89
97
|
// ── loadFromDb ───────────────────────────────────────────────────────
|
|
90
98
|
|
|
91
99
|
export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
|
|
92
|
-
const
|
|
100
|
+
const trustClass = ctx.guardianContext?.trustClass;
|
|
93
101
|
const allDbMessages = conversationStore.getMessages(ctx.conversationId);
|
|
94
|
-
const dbMessages =
|
|
102
|
+
const dbMessages = isUntrustedTrustClass(trustClass)
|
|
95
103
|
? filterMessagesForUntrustedActor(allDbMessages)
|
|
96
104
|
: allDbMessages;
|
|
97
105
|
|
|
98
106
|
const conv = conversationStore.getConversation(ctx.conversationId);
|
|
99
|
-
const contextSummary = !
|
|
107
|
+
const contextSummary = !isUntrustedTrustClass(trustClass)
|
|
100
108
|
? conv?.contextSummary?.trim() || null
|
|
101
109
|
: null;
|
|
102
|
-
if (
|
|
110
|
+
if (isUntrustedTrustClass(trustClass)) {
|
|
103
111
|
// Compacted summaries may include trusted/guardian-only details, so we
|
|
104
112
|
// disable summary-based context for untrusted actor views.
|
|
105
113
|
ctx.contextCompactedMessageCount = 0;
|
|
@@ -145,7 +153,7 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
|
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
ctx.
|
|
156
|
+
ctx.loadedHistoryTrustClass = trustClass;
|
|
149
157
|
|
|
150
158
|
log.info({ conversationId: ctx.conversationId, count: ctx.messages.length }, 'Loaded messages from DB');
|
|
151
159
|
}
|
|
@@ -34,7 +34,7 @@ export interface MemoryPrepareContext {
|
|
|
34
34
|
conflictGate: ConflictGate;
|
|
35
35
|
scopeId: string;
|
|
36
36
|
includeDefaultFallback: boolean;
|
|
37
|
-
|
|
37
|
+
guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
|
|
38
38
|
/** When false (e.g. scheduled tasks), skip conflict clarification prompts. */
|
|
39
39
|
isInteractive?: boolean;
|
|
40
40
|
}
|
|
@@ -64,7 +64,7 @@ export async function prepareMemoryContext(
|
|
|
64
64
|
// Provenance-based trust gating: untrusted actors skip all memory operations
|
|
65
65
|
// (recall, dynamic profile, conflict gate) to prevent untrusted content from
|
|
66
66
|
// influencing memory-augmented responses.
|
|
67
|
-
const isTrustedActor = ctx.
|
|
67
|
+
const isTrustedActor = ctx.guardianTrustClass === 'guardian' || ctx.guardianTrustClass === undefined;
|
|
68
68
|
|
|
69
69
|
if (!isTrustedActor) {
|
|
70
70
|
return {
|