@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -15,17 +15,29 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ChannelId } from '../channels/types.js';
|
|
17
17
|
import {
|
|
18
|
+
createCanonicalGuardianDelivery,
|
|
18
19
|
createCanonicalGuardianRequest,
|
|
19
20
|
listCanonicalGuardianRequests,
|
|
21
|
+
updateCanonicalGuardianDelivery,
|
|
20
22
|
} from '../memory/canonical-guardian-store.js';
|
|
21
23
|
import { listActiveBindingsByAssistant } from '../memory/channel-guardian-store.js';
|
|
24
|
+
import type { MemberStatus } from '../memory/ingress-member-store.js';
|
|
22
25
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
26
|
+
import type { NotificationDeliveryResult } from '../notifications/types.js';
|
|
23
27
|
import { getLogger } from '../util/logger.js';
|
|
24
28
|
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
25
29
|
import { GUARDIAN_APPROVAL_TTL_MS } from './routes/channel-route-shared.js';
|
|
26
30
|
|
|
27
31
|
const log = getLogger('access-request-helper');
|
|
28
32
|
|
|
33
|
+
function applyDeliveryStatus(deliveryId: string, result: NotificationDeliveryResult): void {
|
|
34
|
+
if (result.status === 'sent') {
|
|
35
|
+
updateCanonicalGuardianDelivery(deliveryId, { status: 'sent' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
updateCanonicalGuardianDelivery(deliveryId, { status: 'failed' });
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
// ---------------------------------------------------------------------------
|
|
30
42
|
// Types
|
|
31
43
|
// ---------------------------------------------------------------------------
|
|
@@ -37,6 +49,7 @@ export interface AccessRequestParams {
|
|
|
37
49
|
senderExternalUserId?: string;
|
|
38
50
|
senderName?: string;
|
|
39
51
|
senderUsername?: string;
|
|
52
|
+
previousMemberStatus?: MemberStatus;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export type AccessRequestResult =
|
|
@@ -71,6 +84,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
71
84
|
senderExternalUserId,
|
|
72
85
|
senderName,
|
|
73
86
|
senderUsername,
|
|
87
|
+
previousMemberStatus,
|
|
74
88
|
} = params;
|
|
75
89
|
|
|
76
90
|
if (!senderExternalUserId) {
|
|
@@ -105,14 +119,22 @@ export function notifyGuardianOfAccessRequest(
|
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
// The conversationId is assistant-scoped so the dedupe query below only
|
|
123
|
+
// matches requests for the same assistant. Without this, a pending request
|
|
124
|
+
// from assistant A could be returned for assistant B, allowing the caller
|
|
125
|
+
// to piggyback on A's guardian approval.
|
|
126
|
+
const conversationId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}`;
|
|
127
|
+
|
|
108
128
|
// Deduplicate: skip creation if there is already a pending canonical request
|
|
109
|
-
// for the same requester on this channel. Still return
|
|
110
|
-
// the existing request ID so callers know the guardian
|
|
129
|
+
// for the same requester on this channel *and* assistant. Still return
|
|
130
|
+
// notified: true with the existing request ID so callers know the guardian
|
|
131
|
+
// was already notified.
|
|
111
132
|
const existingCanonical = listCanonicalGuardianRequests({
|
|
112
133
|
status: 'pending',
|
|
113
134
|
requesterExternalUserId: senderExternalUserId,
|
|
114
135
|
sourceChannel,
|
|
115
136
|
kind: 'access_request',
|
|
137
|
+
conversationId,
|
|
116
138
|
});
|
|
117
139
|
if (existingCanonical.length > 0) {
|
|
118
140
|
log.debug(
|
|
@@ -130,7 +152,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
130
152
|
kind: 'access_request',
|
|
131
153
|
sourceType: 'channel',
|
|
132
154
|
sourceChannel,
|
|
133
|
-
conversationId
|
|
155
|
+
conversationId,
|
|
134
156
|
requesterExternalUserId: senderExternalUserId,
|
|
135
157
|
requesterChatId: externalChatId,
|
|
136
158
|
guardianExternalUserId: guardianExternalUserId ?? undefined,
|
|
@@ -139,6 +161,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
139
161
|
expiresAt: new Date(Date.now() + GUARDIAN_APPROVAL_TTL_MS).toISOString(),
|
|
140
162
|
});
|
|
141
163
|
|
|
164
|
+
let vellumDeliveryId: string | null = null;
|
|
142
165
|
void emitNotificationSignal({
|
|
143
166
|
sourceEventName: 'ingress.access_request',
|
|
144
167
|
sourceChannel,
|
|
@@ -160,9 +183,64 @@ export function notifyGuardianOfAccessRequest(
|
|
|
160
183
|
senderUsername: senderUsername ?? null,
|
|
161
184
|
senderIdentifier,
|
|
162
185
|
guardianBindingChannel,
|
|
186
|
+
previousMemberStatus: previousMemberStatus ?? null,
|
|
163
187
|
},
|
|
164
188
|
dedupeKey: `access-request:${canonicalRequest.id}`,
|
|
165
|
-
|
|
189
|
+
onThreadCreated: (info) => {
|
|
190
|
+
if (info.sourceEventName !== 'ingress.access_request' || vellumDeliveryId) return;
|
|
191
|
+
const delivery = createCanonicalGuardianDelivery({
|
|
192
|
+
requestId: canonicalRequest.id,
|
|
193
|
+
destinationChannel: 'vellum',
|
|
194
|
+
destinationConversationId: info.conversationId,
|
|
195
|
+
});
|
|
196
|
+
vellumDeliveryId = delivery.id;
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
.then((signalResult) => {
|
|
200
|
+
for (const result of signalResult.deliveryResults) {
|
|
201
|
+
if (result.channel === 'vellum') {
|
|
202
|
+
if (!vellumDeliveryId) {
|
|
203
|
+
const delivery = createCanonicalGuardianDelivery({
|
|
204
|
+
requestId: canonicalRequest.id,
|
|
205
|
+
destinationChannel: 'vellum',
|
|
206
|
+
destinationConversationId: result.conversationId,
|
|
207
|
+
});
|
|
208
|
+
vellumDeliveryId = delivery.id;
|
|
209
|
+
}
|
|
210
|
+
applyDeliveryStatus(vellumDeliveryId, result);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (result.channel !== 'telegram' && result.channel !== 'sms') {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const delivery = createCanonicalGuardianDelivery({
|
|
219
|
+
requestId: canonicalRequest.id,
|
|
220
|
+
destinationChannel: result.channel,
|
|
221
|
+
destinationChatId: result.destination.length > 0 ? result.destination : undefined,
|
|
222
|
+
});
|
|
223
|
+
applyDeliveryStatus(delivery.id, result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!vellumDeliveryId) {
|
|
227
|
+
const fallback = createCanonicalGuardianDelivery({
|
|
228
|
+
requestId: canonicalRequest.id,
|
|
229
|
+
destinationChannel: 'vellum',
|
|
230
|
+
});
|
|
231
|
+
updateCanonicalGuardianDelivery(fallback.id, { status: 'failed' });
|
|
232
|
+
log.warn(
|
|
233
|
+
{ requestId: canonicalRequest.id, reason: signalResult.reason },
|
|
234
|
+
'Notification pipeline did not produce a vellum delivery result for access request',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
.catch((err) => {
|
|
239
|
+
log.error(
|
|
240
|
+
{ err, requestId: canonicalRequest.id, sourceChannel, senderExternalUserId },
|
|
241
|
+
'Failed to persist access request delivery rows from notification pipeline',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
166
244
|
|
|
167
245
|
log.info(
|
|
168
246
|
{ sourceChannel, senderExternalUserId, senderIdentifier, guardianBindingChannel },
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor-token mint/verify service.
|
|
3
|
+
*
|
|
4
|
+
* Mints HMAC-signed actor tokens bound to (assistantId, guardianPrincipalId,
|
|
5
|
+
* deviceId, platform). Only the SHA-256 hash of the token is persisted —
|
|
6
|
+
* the raw plaintext is returned to the caller once and never stored.
|
|
7
|
+
*
|
|
8
|
+
* Token format: base64url(JSON claims) + '.' + base64url(HMAC-SHA256 signature)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { getLogger } from '../util/logger.js';
|
|
16
|
+
import { getRootDir } from '../util/platform.js';
|
|
17
|
+
|
|
18
|
+
const log = getLogger('actor-token-service');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface ActorTokenClaims {
|
|
25
|
+
/** The assistant this token is scoped to. */
|
|
26
|
+
assistantId: string;
|
|
27
|
+
/** Platform: 'macos' | 'ios' */
|
|
28
|
+
platform: string;
|
|
29
|
+
/** Opaque device identifier (hashed for storage). */
|
|
30
|
+
deviceId: string;
|
|
31
|
+
/** The guardian principal this token is bound to. */
|
|
32
|
+
guardianPrincipalId: string;
|
|
33
|
+
/** Token issuance timestamp (epoch ms). */
|
|
34
|
+
iat: number;
|
|
35
|
+
/** Token expiration timestamp (epoch ms). Null means non-expiring. */
|
|
36
|
+
exp: number | null;
|
|
37
|
+
/** Random jti (JWT ID) for uniqueness. */
|
|
38
|
+
jti: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MintResult {
|
|
42
|
+
/** The raw actor token string — returned once, never persisted. */
|
|
43
|
+
token: string;
|
|
44
|
+
/** SHA-256 hex hash of the token (for storage/lookup). */
|
|
45
|
+
tokenHash: string;
|
|
46
|
+
/** The decoded claims. */
|
|
47
|
+
claims: ActorTokenClaims;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type VerifyResult =
|
|
51
|
+
| { ok: true; claims: ActorTokenClaims }
|
|
52
|
+
| { ok: false; reason: string };
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Signing key management
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
let signingKey: Buffer | null = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Path to the persisted signing key file.
|
|
62
|
+
* Stored in the protected directory alongside other sensitive material.
|
|
63
|
+
*/
|
|
64
|
+
function getSigningKeyPath(): string {
|
|
65
|
+
return join(getRootDir(), 'protected', 'actor-token-signing-key');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load a signing key from disk or generate and persist a new one.
|
|
70
|
+
* Uses the same atomic-write + chmod 0o600 pattern as approved-devices-store.ts.
|
|
71
|
+
*/
|
|
72
|
+
export function loadOrCreateSigningKey(): Buffer {
|
|
73
|
+
const keyPath = getSigningKeyPath();
|
|
74
|
+
|
|
75
|
+
// Try to load existing key
|
|
76
|
+
if (existsSync(keyPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = readFileSync(keyPath);
|
|
79
|
+
if (raw.length === 32) {
|
|
80
|
+
log.info('Actor-token signing key loaded from disk');
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
log.warn('Signing key file has unexpected length, regenerating');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
log.warn({ err }, 'Failed to read signing key file, regenerating');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Generate and persist a new key
|
|
90
|
+
const newKey = randomBytes(32);
|
|
91
|
+
const dir = dirname(keyPath);
|
|
92
|
+
if (!existsSync(dir)) {
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
const tmpPath = keyPath + '.tmp.' + process.pid;
|
|
96
|
+
writeFileSync(tmpPath, newKey, { mode: 0o600 });
|
|
97
|
+
renameSync(tmpPath, keyPath);
|
|
98
|
+
chmodSync(keyPath, 0o600);
|
|
99
|
+
|
|
100
|
+
log.info('Actor-token signing key generated and persisted');
|
|
101
|
+
return newKey;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize (or reinitialize) the signing key. Called at daemon startup
|
|
106
|
+
* with a key loaded from disk via loadOrCreateSigningKey(), or by tests
|
|
107
|
+
* with a deterministic key.
|
|
108
|
+
*/
|
|
109
|
+
export function initSigningKey(key: Buffer): void {
|
|
110
|
+
signingKey = key;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getSigningKey(): Buffer {
|
|
114
|
+
if (!signingKey) {
|
|
115
|
+
throw new Error('Actor-token signing key not initialized — call initSigningKey() during startup');
|
|
116
|
+
}
|
|
117
|
+
return signingKey;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Base64url helpers
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function base64urlEncode(data: Buffer | string): string {
|
|
125
|
+
const buf = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
|
|
126
|
+
return buf.toString('base64url');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function base64urlDecode(str: string): Buffer {
|
|
130
|
+
return Buffer.from(str, 'base64url');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Hashing
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/** SHA-256 hex digest of a raw token string. */
|
|
138
|
+
export function hashToken(token: string): string {
|
|
139
|
+
return createHash('sha256').update(token).digest('hex');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Mint
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/** Default TTL for actor tokens: 90 days in milliseconds. */
|
|
147
|
+
const DEFAULT_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1000;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Mint a new actor token.
|
|
151
|
+
*
|
|
152
|
+
* @param params Token claims (assistantId, platform, deviceId, guardianPrincipalId).
|
|
153
|
+
* @param ttlMs Optional TTL in milliseconds. Defaults to 90 days.
|
|
154
|
+
* Pass `null` explicitly for a non-expiring token.
|
|
155
|
+
* @returns The raw token, its hash, and the embedded claims.
|
|
156
|
+
*/
|
|
157
|
+
export function mintActorToken(params: {
|
|
158
|
+
assistantId: string;
|
|
159
|
+
platform: string;
|
|
160
|
+
deviceId: string;
|
|
161
|
+
guardianPrincipalId: string;
|
|
162
|
+
ttlMs?: number | null;
|
|
163
|
+
}): MintResult {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const effectiveTtl = params.ttlMs === undefined ? DEFAULT_TOKEN_TTL_MS : params.ttlMs;
|
|
166
|
+
const claims: ActorTokenClaims = {
|
|
167
|
+
assistantId: params.assistantId,
|
|
168
|
+
platform: params.platform,
|
|
169
|
+
deviceId: params.deviceId,
|
|
170
|
+
guardianPrincipalId: params.guardianPrincipalId,
|
|
171
|
+
iat: now,
|
|
172
|
+
exp: effectiveTtl != null ? now + effectiveTtl : null,
|
|
173
|
+
jti: randomBytes(16).toString('hex'),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const payload = base64urlEncode(JSON.stringify(claims));
|
|
177
|
+
const sig = createHmac('sha256', getSigningKey())
|
|
178
|
+
.update(payload)
|
|
179
|
+
.digest();
|
|
180
|
+
const token = payload + '.' + base64urlEncode(sig);
|
|
181
|
+
const tokenHash = hashToken(token);
|
|
182
|
+
|
|
183
|
+
return { token, tokenHash, claims };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Verify
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Verify an actor token's structural integrity and signature.
|
|
192
|
+
*
|
|
193
|
+
* Does NOT check revocation — callers must additionally verify the
|
|
194
|
+
* token hash exists in the actor-token store with status='active'.
|
|
195
|
+
*/
|
|
196
|
+
export function verifyActorToken(token: string): VerifyResult {
|
|
197
|
+
const dotIndex = token.indexOf('.');
|
|
198
|
+
if (dotIndex < 0) {
|
|
199
|
+
return { ok: false, reason: 'malformed_token' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const payload = token.slice(0, dotIndex);
|
|
203
|
+
const sigPart = token.slice(dotIndex + 1);
|
|
204
|
+
|
|
205
|
+
// Recompute HMAC
|
|
206
|
+
const expectedSig = createHmac('sha256', getSigningKey())
|
|
207
|
+
.update(payload)
|
|
208
|
+
.digest();
|
|
209
|
+
const actualSig = base64urlDecode(sigPart);
|
|
210
|
+
|
|
211
|
+
if (expectedSig.length !== actualSig.length) {
|
|
212
|
+
return { ok: false, reason: 'invalid_signature' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Constant-time comparison
|
|
216
|
+
if (!timingSafeEqual(expectedSig, actualSig)) {
|
|
217
|
+
return { ok: false, reason: 'invalid_signature' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let claims: ActorTokenClaims;
|
|
221
|
+
try {
|
|
222
|
+
const decoded = base64urlDecode(payload).toString('utf-8');
|
|
223
|
+
claims = JSON.parse(decoded) as ActorTokenClaims;
|
|
224
|
+
} catch {
|
|
225
|
+
return { ok: false, reason: 'malformed_claims' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Expiration check
|
|
229
|
+
if (claims.exp != null && Date.now() > claims.exp) {
|
|
230
|
+
return { ok: false, reason: 'token_expired' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { ok: true, claims };
|
|
234
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-only actor token persistence.
|
|
3
|
+
*
|
|
4
|
+
* Stores only the SHA-256 hash of each actor token alongside metadata
|
|
5
|
+
* (assistantId, guardianPrincipalId, deviceId hash, platform, status).
|
|
6
|
+
* The raw token plaintext is never stored.
|
|
7
|
+
*
|
|
8
|
+
* Uses the assistant SQLite database via drizzle-orm.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { and, eq } from 'drizzle-orm';
|
|
12
|
+
import { v4 as uuid } from 'uuid';
|
|
13
|
+
|
|
14
|
+
import { getDb } from '../memory/db.js';
|
|
15
|
+
import { actorTokenRecords } from '../memory/schema.js';
|
|
16
|
+
import { getLogger } from '../util/logger.js';
|
|
17
|
+
|
|
18
|
+
const log = getLogger('actor-token-store');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type ActorTokenStatus = 'active' | 'revoked';
|
|
25
|
+
|
|
26
|
+
export interface ActorTokenRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
tokenHash: string;
|
|
29
|
+
assistantId: string;
|
|
30
|
+
guardianPrincipalId: string;
|
|
31
|
+
hashedDeviceId: string;
|
|
32
|
+
platform: string;
|
|
33
|
+
status: ActorTokenStatus;
|
|
34
|
+
issuedAt: number;
|
|
35
|
+
expiresAt: number | null;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Operations
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Store a new actor token record (hash-only).
|
|
46
|
+
*/
|
|
47
|
+
export function createActorTokenRecord(params: {
|
|
48
|
+
tokenHash: string;
|
|
49
|
+
assistantId: string;
|
|
50
|
+
guardianPrincipalId: string;
|
|
51
|
+
hashedDeviceId: string;
|
|
52
|
+
platform: string;
|
|
53
|
+
issuedAt: number;
|
|
54
|
+
expiresAt?: number | null;
|
|
55
|
+
}): ActorTokenRecord {
|
|
56
|
+
const db = getDb();
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const id = uuid();
|
|
59
|
+
|
|
60
|
+
const row = {
|
|
61
|
+
id,
|
|
62
|
+
tokenHash: params.tokenHash,
|
|
63
|
+
assistantId: params.assistantId,
|
|
64
|
+
guardianPrincipalId: params.guardianPrincipalId,
|
|
65
|
+
hashedDeviceId: params.hashedDeviceId,
|
|
66
|
+
platform: params.platform,
|
|
67
|
+
status: 'active' as const,
|
|
68
|
+
issuedAt: params.issuedAt,
|
|
69
|
+
expiresAt: params.expiresAt ?? null,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
db.insert(actorTokenRecords).values(row).run();
|
|
75
|
+
log.info({ id, assistantId: params.assistantId, platform: params.platform }, 'Actor token record created');
|
|
76
|
+
|
|
77
|
+
return row;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Look up an active actor token record by its hash.
|
|
82
|
+
*/
|
|
83
|
+
export function findActiveByTokenHash(tokenHash: string): ActorTokenRecord | null {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
const row = db
|
|
86
|
+
.select()
|
|
87
|
+
.from(actorTokenRecords)
|
|
88
|
+
.where(
|
|
89
|
+
and(
|
|
90
|
+
eq(actorTokenRecords.tokenHash, tokenHash),
|
|
91
|
+
eq(actorTokenRecords.status, 'active'),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
.get();
|
|
95
|
+
|
|
96
|
+
return row ? rowToRecord(row) : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find an active token for a specific (assistantId, guardianPrincipalId, deviceId).
|
|
101
|
+
* Used for idempotent bootstrap — if an active token already exists for this
|
|
102
|
+
* device binding, we can revoke-and-remint or return the existing record.
|
|
103
|
+
*/
|
|
104
|
+
export function findActiveByDeviceBinding(
|
|
105
|
+
assistantId: string,
|
|
106
|
+
guardianPrincipalId: string,
|
|
107
|
+
hashedDeviceId: string,
|
|
108
|
+
): ActorTokenRecord | null {
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const row = db
|
|
111
|
+
.select()
|
|
112
|
+
.from(actorTokenRecords)
|
|
113
|
+
.where(
|
|
114
|
+
and(
|
|
115
|
+
eq(actorTokenRecords.assistantId, assistantId),
|
|
116
|
+
eq(actorTokenRecords.guardianPrincipalId, guardianPrincipalId),
|
|
117
|
+
eq(actorTokenRecords.hashedDeviceId, hashedDeviceId),
|
|
118
|
+
eq(actorTokenRecords.status, 'active'),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
.get();
|
|
122
|
+
|
|
123
|
+
return row ? rowToRecord(row) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Revoke all active tokens for a given device binding.
|
|
128
|
+
* Called before minting a new token to ensure one-active-per-device.
|
|
129
|
+
*/
|
|
130
|
+
export function revokeByDeviceBinding(
|
|
131
|
+
assistantId: string,
|
|
132
|
+
guardianPrincipalId: string,
|
|
133
|
+
hashedDeviceId: string,
|
|
134
|
+
): number {
|
|
135
|
+
const db = getDb();
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
|
|
138
|
+
const condition = and(
|
|
139
|
+
eq(actorTokenRecords.assistantId, assistantId),
|
|
140
|
+
eq(actorTokenRecords.guardianPrincipalId, guardianPrincipalId),
|
|
141
|
+
eq(actorTokenRecords.hashedDeviceId, hashedDeviceId),
|
|
142
|
+
eq(actorTokenRecords.status, 'active'),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Count matching rows before the update since drizzle's bun-sqlite
|
|
146
|
+
// .run() does not expose the underlying changes count in its types.
|
|
147
|
+
const matching = db
|
|
148
|
+
.select({ id: actorTokenRecords.id })
|
|
149
|
+
.from(actorTokenRecords)
|
|
150
|
+
.where(condition)
|
|
151
|
+
.all();
|
|
152
|
+
|
|
153
|
+
if (matching.length === 0) return 0;
|
|
154
|
+
|
|
155
|
+
db.update(actorTokenRecords)
|
|
156
|
+
.set({ status: 'revoked', updatedAt: now })
|
|
157
|
+
.where(condition)
|
|
158
|
+
.run();
|
|
159
|
+
|
|
160
|
+
return matching.length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Find all active actor token records for a given (assistantId, guardianPrincipalId).
|
|
165
|
+
* Used for multi-device guardian fanout — returns all bound devices (macOS, iOS, etc.)
|
|
166
|
+
* so notification targeting can reach every device for the same guardian identity.
|
|
167
|
+
*/
|
|
168
|
+
export function findActiveByGuardianPrincipalId(
|
|
169
|
+
assistantId: string,
|
|
170
|
+
guardianPrincipalId: string,
|
|
171
|
+
): ActorTokenRecord[] {
|
|
172
|
+
const db = getDb();
|
|
173
|
+
const rows = db
|
|
174
|
+
.select()
|
|
175
|
+
.from(actorTokenRecords)
|
|
176
|
+
.where(
|
|
177
|
+
and(
|
|
178
|
+
eq(actorTokenRecords.assistantId, assistantId),
|
|
179
|
+
eq(actorTokenRecords.guardianPrincipalId, guardianPrincipalId),
|
|
180
|
+
eq(actorTokenRecords.status, 'active'),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
.all();
|
|
184
|
+
|
|
185
|
+
return rows.map(rowToRecord);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Revoke a single token by its hash.
|
|
190
|
+
*/
|
|
191
|
+
export function revokeByTokenHash(tokenHash: string): boolean {
|
|
192
|
+
const db = getDb();
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
|
|
195
|
+
const condition = and(
|
|
196
|
+
eq(actorTokenRecords.tokenHash, tokenHash),
|
|
197
|
+
eq(actorTokenRecords.status, 'active'),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Check existence before update since drizzle's bun-sqlite .run()
|
|
201
|
+
// does not expose the underlying changes count in its types.
|
|
202
|
+
const existing = db
|
|
203
|
+
.select({ id: actorTokenRecords.id })
|
|
204
|
+
.from(actorTokenRecords)
|
|
205
|
+
.where(condition)
|
|
206
|
+
.get();
|
|
207
|
+
|
|
208
|
+
if (!existing) return false;
|
|
209
|
+
|
|
210
|
+
db.update(actorTokenRecords)
|
|
211
|
+
.set({ status: 'revoked', updatedAt: now })
|
|
212
|
+
.where(condition)
|
|
213
|
+
.run();
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Helpers
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function rowToRecord(row: typeof actorTokenRecords.$inferSelect): ActorTokenRecord {
|
|
223
|
+
return {
|
|
224
|
+
id: row.id,
|
|
225
|
+
tokenHash: row.tokenHash,
|
|
226
|
+
assistantId: row.assistantId,
|
|
227
|
+
guardianPrincipalId: row.guardianPrincipalId,
|
|
228
|
+
hashedDeviceId: row.hashedDeviceId,
|
|
229
|
+
platform: row.platform,
|
|
230
|
+
status: row.status as ActorTokenStatus,
|
|
231
|
+
issuedAt: row.issuedAt,
|
|
232
|
+
expiresAt: row.expiresAt,
|
|
233
|
+
createdAt: row.createdAt,
|
|
234
|
+
updatedAt: row.updatedAt,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -17,7 +17,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
|
|
|
17
17
|
import type { IngressMember } from '../memory/ingress-member-store.js';
|
|
18
18
|
import { findMember } from '../memory/ingress-member-store.js';
|
|
19
19
|
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
20
|
-
import {
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
21
21
|
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -76,7 +76,7 @@ export interface ResolveActorTrustInput {
|
|
|
76
76
|
* 5. Classify: guardian > trusted_contact (active member) > unknown.
|
|
77
77
|
*/
|
|
78
78
|
export function resolveActorTrust(input: ResolveActorTrustInput): ActorTrustContext {
|
|
79
|
-
const assistantId =
|
|
79
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
80
80
|
|
|
81
81
|
const rawUserId = typeof input.senderExternalUserId === 'string' && input.senderExternalUserId.trim().length > 0
|
|
82
82
|
? input.senderExternalUserId.trim()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical internal scope ID for all daemon-side assistant-scoped storage.
|
|
3
|
+
*
|
|
4
|
+
* The daemon uses a single fixed identity (`'self'`) for its own assistant
|
|
5
|
+
* scope. Public/external assistant IDs are an edge concern owned by the
|
|
6
|
+
* gateway and platform layers (hatch, invite links, etc.). Daemon code
|
|
7
|
+
* should never derive scoping decisions from externally-provided assistant
|
|
8
|
+
* IDs — use this constant instead.
|
|
9
|
+
*/
|
|
10
|
+
export const DAEMON_INTERNAL_ASSISTANT_ID = 'self' as const;
|
|
@@ -151,11 +151,13 @@ export function handleChannelDecision(
|
|
|
151
151
|
if (
|
|
152
152
|
details &&
|
|
153
153
|
details.persistentDecisionsAllowed !== false &&
|
|
154
|
-
details.allowlistOptions?.length
|
|
155
|
-
details.scopeOptions?.length
|
|
154
|
+
details.allowlistOptions?.length
|
|
156
155
|
) {
|
|
157
156
|
const pattern = details.allowlistOptions[0].pattern;
|
|
158
|
-
|
|
157
|
+
// Non-scoped tools (web_fetch, network_request, etc.) have empty
|
|
158
|
+
// scopeOptions — default to 'everywhere' so approve_always still
|
|
159
|
+
// persists a trust rule instead of silently degrading to one-time.
|
|
160
|
+
const scope = details.scopeOptions?.length ? details.scopeOptions[0].scope : 'everywhere';
|
|
159
161
|
// Only persist executionTarget for skill-origin tools — core tools don't
|
|
160
162
|
// set it in their PolicyContext, so a persisted value would prevent the
|
|
161
163
|
// rule from ever matching on subsequent permission checks.
|