@vellumai/assistant 0.4.13 → 0.4.15
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 +77 -38
- package/README.md +10 -12
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +108 -522
- package/src/__tests__/channel-approval-routes.test.ts +92 -239
- package/src/__tests__/channel-approval.test.ts +100 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +13 -6
- package/src/__tests__/conversation-routes.test.ts +11 -4
- package/src/__tests__/guardian-actions-endpoint.test.ts +26 -19
- package/src/__tests__/mcp-health-check.test.ts +65 -0
- package/src/__tests__/permission-types.test.ts +33 -0
- package/src/__tests__/scan-result-store.test.ts +121 -0
- package/src/__tests__/session-agent-loop.test.ts +120 -0
- package/src/__tests__/session-approval-overrides.test.ts +205 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +38 -0
- package/src/amazon/client.ts +8 -5
- package/src/approvals/guardian-decision-primitive.ts +14 -9
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/call-controller.ts +2 -2
- package/src/calls/twilio-routes.ts +2 -2
- package/src/cli/mcp.ts +3 -3
- package/src/cli.ts +24 -0
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +19 -130
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +8 -6
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +49 -14
- package/src/config/bundled-skills/messaging/TOOLS.json +52 -9
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +35 -11
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +3 -1
- package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +5 -6
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +10 -2
- package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +20 -0
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +3 -4
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +10 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +11 -3
- package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +86 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/skills-catalog/SKILL.md +31 -8
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +79 -24
- package/src/config/bundled-skills/sms-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/approval-generators.ts +6 -3
- package/src/daemon/handlers/config-ingress.ts +2 -6
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/sessions.ts +4 -1
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +32 -0
- package/src/daemon/ipc-contract/messages.ts +3 -1
- package/src/daemon/ipc-handler.ts +24 -0
- package/src/daemon/ipc-validate.ts +1 -1
- package/src/daemon/lifecycle.ts +6 -8
- package/src/daemon/server.ts +8 -3
- package/src/daemon/session-agent-loop.ts +19 -1
- package/src/daemon/session-attachments.ts +2 -1
- package/src/daemon/session-history.ts +2 -2
- package/src/daemon/session-process.ts +5 -9
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session-tool-setup.ts +216 -69
- package/src/daemon/session.ts +24 -1
- package/src/events/domain-events.ts +1 -1
- package/src/events/tool-domain-event-publisher.ts +5 -10
- package/src/influencer/client.ts +8 -7
- package/src/messaging/providers/gmail/client.ts +33 -1
- package/src/messaging/providers/gmail/mime-builder.ts +5 -1
- package/src/messaging/providers/sms/adapter.ts +3 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +3 -7
- package/src/messaging/providers/whatsapp/adapter.ts +3 -7
- package/src/notifications/adapters/sms.ts +2 -2
- package/src/notifications/adapters/telegram.ts +2 -2
- package/src/permissions/prompter.ts +2 -0
- package/src/permissions/types.ts +11 -1
- package/src/runtime/approval-conversation-turn.ts +4 -0
- package/src/runtime/auth/__tests__/context.test.ts +130 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +277 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +289 -0
- package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +71 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +239 -0
- package/src/runtime/auth/__tests__/policy.test.ts +29 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +166 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +109 -0
- package/src/runtime/auth/__tests__/subject.test.ts +149 -0
- package/src/runtime/auth/__tests__/token-service.test.ts +263 -0
- package/src/runtime/auth/context.ts +62 -0
- package/src/runtime/{actor-refresh-token-service.ts → auth/credential-service.ts} +112 -79
- package/src/runtime/auth/external-assistant-id.ts +69 -0
- package/src/runtime/auth/index.ts +37 -0
- package/src/runtime/auth/middleware.ts +127 -0
- package/src/runtime/auth/policy.ts +17 -0
- package/src/runtime/auth/route-policy.ts +261 -0
- package/src/runtime/auth/scopes.ts +64 -0
- package/src/runtime/auth/subject.ts +68 -0
- package/src/runtime/auth/token-service.ts +275 -0
- package/src/runtime/auth/types.ts +79 -0
- package/src/runtime/channel-approval-parser.ts +11 -5
- package/src/runtime/channel-approval-types.ts +1 -1
- package/src/runtime/channel-approvals.ts +22 -1
- package/src/runtime/guardian-action-followup-executor.ts +2 -2
- package/src/runtime/guardian-context-resolver.ts +15 -0
- package/src/runtime/guardian-decision-types.ts +23 -6
- package/src/runtime/guardian-outbound-actions.ts +4 -22
- package/src/runtime/guardian-reply-router.ts +5 -3
- package/src/runtime/http-server.ts +210 -182
- package/src/runtime/http-types.ts +11 -1
- package/src/runtime/local-actor-identity.ts +25 -0
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/approval-routes.ts +42 -59
- package/src/runtime/routes/channel-route-shared.ts +9 -41
- package/src/runtime/routes/channel-routes.ts +0 -2
- package/src/runtime/routes/conversation-routes.ts +39 -49
- package/src/runtime/routes/events-routes.ts +15 -22
- package/src/runtime/routes/guardian-action-routes.ts +46 -51
- package/src/runtime/routes/guardian-approval-interception.ts +6 -5
- package/src/runtime/routes/guardian-bootstrap-routes.ts +12 -8
- package/src/runtime/routes/guardian-refresh-routes.ts +2 -2
- package/src/runtime/routes/inbound-message-handler.ts +39 -45
- package/src/runtime/routes/pairing-routes.ts +9 -9
- package/src/runtime/routes/secret-routes.ts +90 -45
- package/src/runtime/routes/surface-action-routes.ts +12 -2
- package/src/runtime/routes/trust-rules-routes.ts +13 -0
- package/src/runtime/routes/twilio-routes.ts +3 -3
- package/src/runtime/session-approval-overrides.ts +86 -0
- package/src/security/keychain-to-encrypted-migration.ts +8 -1
- package/src/skills/frontmatter.ts +44 -1
- package/src/tools/permission-checker.ts +226 -74
- package/src/runtime/actor-token-service.ts +0 -234
- package/src/runtime/middleware/actor-token.ts +0 -265
|
@@ -4,28 +4,29 @@
|
|
|
4
4
|
* These endpoints let desktop clients fetch pending guardian prompts and
|
|
5
5
|
* submit button decisions without relying on text parsing.
|
|
6
6
|
*
|
|
7
|
-
* All guardian action endpoints require a valid
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* All guardian action endpoints require a valid JWT bearer token.
|
|
8
|
+
* Auth is verified upstream by JWT middleware; the AuthContext is
|
|
9
|
+
* threaded through from the HTTP server layer.
|
|
10
|
+
*
|
|
11
|
+
* Guardian decisions additionally verify the actor is the bound guardian
|
|
12
|
+
* via the AuthContext's actorPrincipalId.
|
|
10
13
|
*/
|
|
11
14
|
import {
|
|
12
15
|
applyCanonicalGuardianDecision,
|
|
13
16
|
} from '../../approvals/guardian-decision-primitive.js';
|
|
17
|
+
import { isHttpAuthDisabled } from '../../config/env.js';
|
|
14
18
|
import {
|
|
15
19
|
type CanonicalGuardianRequest,
|
|
16
20
|
getCanonicalGuardianRequest,
|
|
17
21
|
listCanonicalGuardianRequests,
|
|
18
22
|
} from '../../memory/canonical-guardian-store.js';
|
|
23
|
+
import { getActiveBinding } from '../../memory/guardian-bindings.js';
|
|
24
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
25
|
+
import type { AuthContext } from '../auth/types.js';
|
|
19
26
|
import type { ApprovalAction } from '../channel-approval-types.js';
|
|
20
27
|
import type { GuardianDecisionPrompt } from '../guardian-decision-types.js';
|
|
21
28
|
import { buildDecisionActions } from '../guardian-decision-types.js';
|
|
22
29
|
import { httpError } from '../http-errors.js';
|
|
23
|
-
import {
|
|
24
|
-
isActorBoundGuardian,
|
|
25
|
-
isLocalFallbackBoundGuardian,
|
|
26
|
-
type ServerWithRequestIP,
|
|
27
|
-
verifyHttpActorTokenWithLocalFallback,
|
|
28
|
-
} from '../middleware/actor-token.js';
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// GET /v1/guardian-actions/pending?conversationId=...
|
|
@@ -33,23 +34,13 @@ import {
|
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* List pending guardian decision prompts for a conversation.
|
|
36
|
-
*
|
|
37
|
+
* Auth is verified upstream by JWT middleware.
|
|
37
38
|
*
|
|
38
39
|
* Returns guardian approval requests (from the channel guardian store) that
|
|
39
40
|
* are still pending, mapped to the GuardianDecisionPrompt shape so clients
|
|
40
41
|
* can render structured button UIs.
|
|
41
42
|
*/
|
|
42
|
-
export function handleGuardianActionsPending(
|
|
43
|
-
const tokenResult = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
44
|
-
if (!tokenResult.ok) {
|
|
45
|
-
return httpError(
|
|
46
|
-
tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
47
|
-
tokenResult.message,
|
|
48
|
-
tokenResult.status,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const url = new URL(req.url);
|
|
43
|
+
export function handleGuardianActionsPending(url: URL, _authContext: AuthContext): Response {
|
|
53
44
|
const conversationId = url.searchParams.get('conversationId');
|
|
54
45
|
|
|
55
46
|
if (!conversationId) {
|
|
@@ -64,29 +55,41 @@ export function handleGuardianActionsPending(req: Request, server: ServerWithReq
|
|
|
64
55
|
// POST /v1/guardian-actions/decision
|
|
65
56
|
// ---------------------------------------------------------------------------
|
|
66
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Verify that the actor from AuthContext is the bound guardian for the
|
|
60
|
+
* vellum channel. Returns an error Response if not, or null if allowed.
|
|
61
|
+
*/
|
|
62
|
+
function requireBoundGuardian(authContext: AuthContext): Response | null {
|
|
63
|
+
// Dev bypass: when auth is disabled, skip guardian binding check
|
|
64
|
+
// (mirrors enforcePolicy dev bypass in route-policy.ts)
|
|
65
|
+
if (isHttpAuthDisabled()) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!authContext.actorPrincipalId) {
|
|
69
|
+
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
70
|
+
}
|
|
71
|
+
const binding = getActiveBinding(DAEMON_INTERNAL_ASSISTANT_ID, 'vellum');
|
|
72
|
+
if (!binding) {
|
|
73
|
+
// No binding yet -- in pre-bootstrap state, allow through
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (binding.guardianExternalUserId !== authContext.actorPrincipalId) {
|
|
77
|
+
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
83
|
* Submit a guardian action decision.
|
|
69
|
-
* Requires
|
|
84
|
+
* Requires AuthContext with a bound guardian actor.
|
|
70
85
|
*
|
|
71
86
|
* Routes all decisions through the unified canonical guardian decision
|
|
72
87
|
* primitive which handles CAS resolution, resolver dispatch, and grant
|
|
73
88
|
* minting.
|
|
74
89
|
*/
|
|
75
|
-
export async function handleGuardianActionDecision(req: Request,
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
return httpError(
|
|
79
|
-
tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
80
|
-
tokenResult.message,
|
|
81
|
-
tokenResult.status,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const isBoundGuardian = tokenResult.claims
|
|
85
|
-
? isActorBoundGuardian(tokenResult.claims)
|
|
86
|
-
: isLocalFallbackBoundGuardian();
|
|
87
|
-
if (!isBoundGuardian) {
|
|
88
|
-
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
89
|
-
}
|
|
90
|
+
export async function handleGuardianActionDecision(req: Request, authContext: AuthContext): Promise<Response> {
|
|
91
|
+
const guardianError = requireBoundGuardian(authContext);
|
|
92
|
+
if (guardianError) return guardianError;
|
|
90
93
|
|
|
91
94
|
const body = await req.json() as {
|
|
92
95
|
requestId?: string;
|
|
@@ -104,9 +107,9 @@ export async function handleGuardianActionDecision(req: Request, server: ServerW
|
|
|
104
107
|
return httpError('BAD_REQUEST', 'action is required', 400);
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_always', 'reject']);
|
|
110
|
+
const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_10m', 'approve_thread', 'approve_always', 'reject']);
|
|
108
111
|
if (!VALID_ACTIONS.has(action)) {
|
|
109
|
-
return httpError('BAD_REQUEST', `Invalid action: ${action}. Must be one of: approve_once, approve_always, reject`, 400);
|
|
112
|
+
return httpError('BAD_REQUEST', `Invalid action: ${action}. Must be one of: approve_once, approve_10m, approve_thread, approve_always, reject`, 400);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// Verify conversationId scoping before applying the canonical decision.
|
|
@@ -118,17 +121,9 @@ export async function handleGuardianActionDecision(req: Request, server: ServerW
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
// Resolve
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
? tokenResult.claims.guardianPrincipalId
|
|
125
|
-
: tokenResult.guardianContext.guardianExternalUserId;
|
|
126
|
-
|
|
127
|
-
// Resolve the actor's principal ID: from the token claims if present,
|
|
128
|
-
// otherwise from the vellum guardian binding (local fallback).
|
|
129
|
-
const actorPrincipalId = tokenResult.claims
|
|
130
|
-
? tokenResult.claims.guardianPrincipalId
|
|
131
|
-
: tokenResult.guardianContext.guardianPrincipalId ?? undefined;
|
|
124
|
+
// Resolve actor identity from the AuthContext (set by JWT middleware).
|
|
125
|
+
const actorExternalUserId = authContext.actorPrincipalId ?? undefined;
|
|
126
|
+
const actorPrincipalId = authContext.actorPrincipalId ?? undefined;
|
|
132
127
|
|
|
133
128
|
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
134
129
|
requestId,
|
|
@@ -223,7 +218,7 @@ function mapCanonicalRequestToPrompt(
|
|
|
223
218
|
?? (req.toolName ? `Approve tool: ${req.toolName}` : `Guardian request: ${req.kind}`);
|
|
224
219
|
|
|
225
220
|
// pending_question requests are typically voice-originated and need
|
|
226
|
-
// approve/reject only (no approve_always
|
|
221
|
+
// approve/reject only (no approve_always -- guardian-on-behalf invariant).
|
|
227
222
|
const actions = buildDecisionActions({ forGuardianOnBehalf: true });
|
|
228
223
|
|
|
229
224
|
const expiresAt = req.expiresAt
|
|
@@ -18,6 +18,7 @@ import { runApprovalConversationTurn } from '../approval-conversation-turn.js';
|
|
|
18
18
|
import { composeApprovalMessageGenerative } from '../approval-message-composer.js';
|
|
19
19
|
import { parseApprovalDecision } from '../channel-approval-parser.js';
|
|
20
20
|
import type {
|
|
21
|
+
ApprovalAction,
|
|
21
22
|
ApprovalDecisionResult,
|
|
22
23
|
} from '../channel-approval-types.js';
|
|
23
24
|
import {
|
|
@@ -275,12 +276,12 @@ export async function handleApprovalInterception(
|
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
// Decision-bearing disposition from the engine
|
|
278
|
-
let decisionAction = engineResult.disposition as
|
|
279
|
+
let decisionAction = engineResult.disposition as ApprovalAction;
|
|
279
280
|
|
|
280
|
-
// Belt-and-suspenders: guardians cannot
|
|
281
|
-
// engine returns
|
|
281
|
+
// Belt-and-suspenders: guardians cannot use broad allow modes even if
|
|
282
|
+
// the engine returns them (the engine's allowedActions validation should
|
|
282
283
|
// already prevent this, but enforce it here too).
|
|
283
|
-
if (decisionAction === 'approve_always') {
|
|
284
|
+
if (decisionAction === 'approve_always' || decisionAction === 'approve_10m' || decisionAction === 'approve_thread') {
|
|
284
285
|
decisionAction = 'approve_once';
|
|
285
286
|
}
|
|
286
287
|
|
|
@@ -838,7 +839,7 @@ export async function handleApprovalInterception(
|
|
|
838
839
|
}
|
|
839
840
|
|
|
840
841
|
// Decision-bearing disposition — map to ApprovalDecisionResult and apply
|
|
841
|
-
const decisionAction = engineResult.disposition as
|
|
842
|
+
const decisionAction = engineResult.disposition as ApprovalAction;
|
|
842
843
|
const engineDecision: ApprovalDecisionResult = {
|
|
843
844
|
action: decisionAction,
|
|
844
845
|
source: 'plain_text',
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Idempotent bootstrap endpoint for the vellum guardian channel.
|
|
5
5
|
* Creates or confirms a guardianPrincipalId and channel='vellum'
|
|
6
|
-
* guardian binding, then mints and returns
|
|
7
|
-
* to (assistantId, guardianPrincipalId
|
|
6
|
+
* guardian binding, then mints and returns a JWT access token bound
|
|
7
|
+
* to (assistantId, guardianPrincipalId) with a paired refresh token.
|
|
8
8
|
*
|
|
9
|
-
* Only the hashed
|
|
9
|
+
* Only the hashed tokens are persisted.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
@@ -18,10 +18,14 @@ import {
|
|
|
18
18
|
getActiveBinding,
|
|
19
19
|
} from '../../memory/guardian-bindings.js';
|
|
20
20
|
import { getLogger } from '../../util/logger.js';
|
|
21
|
-
import { mintCredentialPair } from '../actor-refresh-token-service.js';
|
|
22
21
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
22
|
+
import { mintCredentialPair } from '../auth/credential-service.js';
|
|
23
23
|
import { httpError } from '../http-errors.js';
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
/** Bun server shape needed for requestIP -- avoids importing the full Bun type. */
|
|
26
|
+
type ServerWithRequestIP = {
|
|
27
|
+
requestIP(req: Request): { address: string; family: string; port: number } | null;
|
|
28
|
+
};
|
|
25
29
|
|
|
26
30
|
const log = getLogger('guardian-bootstrap');
|
|
27
31
|
|
|
@@ -68,7 +72,7 @@ const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
|
68
72
|
* Handle POST /v1/integrations/guardian/vellum/bootstrap
|
|
69
73
|
*
|
|
70
74
|
* Body: { platform: 'macos', deviceId: string }
|
|
71
|
-
* Returns: { guardianPrincipalId,
|
|
75
|
+
* Returns: { guardianPrincipalId, accessToken, isNew }
|
|
72
76
|
*
|
|
73
77
|
* This endpoint is loopback-only (macOS local use only). iOS devices
|
|
74
78
|
* obtain actor tokens exclusively through the QR pairing flow.
|
|
@@ -118,8 +122,8 @@ export async function handleGuardianBootstrap(req: Request, server: ServerWithRe
|
|
|
118
122
|
|
|
119
123
|
return Response.json({
|
|
120
124
|
guardianPrincipalId,
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
accessToken: credentials.accessToken,
|
|
126
|
+
accessTokenExpiresAt: credentials.accessTokenExpiresAt,
|
|
123
127
|
refreshToken: credentials.refreshToken,
|
|
124
128
|
refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
|
|
125
129
|
refreshAfter: credentials.refreshAfter,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getLogger } from '../../util/logger.js';
|
|
9
|
-
import { rotateCredentials } from '../
|
|
9
|
+
import { rotateCredentials } from '../auth/credential-service.js';
|
|
10
10
|
import { httpError } from '../http-errors.js';
|
|
11
11
|
|
|
12
12
|
const log = getLogger('guardian-refresh');
|
|
@@ -15,7 +15,7 @@ const log = getLogger('guardian-refresh');
|
|
|
15
15
|
* Handle POST /v1/integrations/guardian/vellum/refresh
|
|
16
16
|
*
|
|
17
17
|
* Body: { platform: 'ios' | 'macos', deviceId: string, refreshToken: string }
|
|
18
|
-
* Returns: { guardianPrincipalId,
|
|
18
|
+
* Returns: { guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter }
|
|
19
19
|
*/
|
|
20
20
|
export async function handleGuardianRefresh(req: Request): Promise<Response> {
|
|
21
21
|
try {
|
|
@@ -26,9 +26,9 @@ import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
|
26
26
|
import { canonicalizeInboundIdentity } from '../../util/canonicalize-identity.js';
|
|
27
27
|
import { IngressBlockedError } from '../../util/errors.js';
|
|
28
28
|
import { getLogger } from '../../util/logger.js';
|
|
29
|
-
import { readHttpToken } from '../../util/platform.js';
|
|
30
29
|
import { notifyGuardianOfAccessRequest } from '../access-request-helper.js';
|
|
31
30
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
31
|
+
import { mintDaemonDeliveryToken } from '../auth/token-service.js';
|
|
32
32
|
import {
|
|
33
33
|
buildApprovalUIMetadata,
|
|
34
34
|
getApprovalInfoByConversation,
|
|
@@ -70,7 +70,6 @@ import {
|
|
|
70
70
|
GUARDIAN_APPROVAL_TTL_MS,
|
|
71
71
|
type GuardianContext,
|
|
72
72
|
stripVerificationFailurePrefix,
|
|
73
|
-
verifyGatewayOrigin,
|
|
74
73
|
} from './channel-route-shared.js';
|
|
75
74
|
import { handleApprovalInterception } from './guardian-approval-interception.js';
|
|
76
75
|
import { deliverGeneratedApprovalPrompt } from './guardian-approval-prompt.js';
|
|
@@ -96,24 +95,22 @@ function parseGuardianVerifyCode(content: string): string | undefined {
|
|
|
96
95
|
export async function handleChannelInbound(
|
|
97
96
|
req: Request,
|
|
98
97
|
processMessage?: MessageProcessor,
|
|
99
|
-
bearerToken?: string,
|
|
100
98
|
assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
101
|
-
gatewayOriginSecret?: string,
|
|
102
99
|
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
103
100
|
approvalConversationGenerator?: ApprovalConversationGenerator,
|
|
104
101
|
_guardianActionCopyGenerator?: GuardianActionCopyGenerator,
|
|
105
102
|
_guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator,
|
|
106
103
|
): Promise<Response> {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
// Gateway-origin proof is enforced by route-policy middleware (svc_gateway
|
|
105
|
+
// principal type required) before this handler runs. The exchange JWT
|
|
106
|
+
// itself proves gateway origin.
|
|
107
|
+
|
|
108
|
+
// Factory that mints a fresh short-lived JWT for each daemon-to-gateway
|
|
109
|
+
// delivery callback. The JWT has a 60-second TTL, so long-running
|
|
110
|
+
// background operations (typing heartbeats, approval watchers, reply
|
|
111
|
+
// delivery) must call this at each delivery attempt rather than reusing
|
|
112
|
+
// a single token from request start.
|
|
113
|
+
const mintBearerToken = (): string => mintDaemonDeliveryToken();
|
|
117
114
|
|
|
118
115
|
const body = await req.json() as {
|
|
119
116
|
sourceChannel?: string;
|
|
@@ -308,7 +305,7 @@ export async function handleChannelInbound(
|
|
|
308
305
|
senderName: body.actorDisplayName,
|
|
309
306
|
senderUsername: body.actorUsername,
|
|
310
307
|
replyCallbackUrl: body.replyCallbackUrl,
|
|
311
|
-
bearerToken,
|
|
308
|
+
bearerToken: mintBearerToken(),
|
|
312
309
|
assistantId,
|
|
313
310
|
canonicalAssistantId,
|
|
314
311
|
});
|
|
@@ -345,7 +342,7 @@ export async function handleChannelInbound(
|
|
|
345
342
|
chatId: conversationExternalId,
|
|
346
343
|
text: replyText,
|
|
347
344
|
assistantId,
|
|
348
|
-
},
|
|
345
|
+
}, mintBearerToken());
|
|
349
346
|
} catch (err) {
|
|
350
347
|
log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
|
|
351
348
|
}
|
|
@@ -394,7 +391,7 @@ export async function handleChannelInbound(
|
|
|
394
391
|
senderName: body.actorDisplayName,
|
|
395
392
|
senderUsername: body.actorUsername,
|
|
396
393
|
replyCallbackUrl: body.replyCallbackUrl,
|
|
397
|
-
bearerToken,
|
|
394
|
+
bearerToken: mintBearerToken(),
|
|
398
395
|
assistantId,
|
|
399
396
|
canonicalAssistantId,
|
|
400
397
|
});
|
|
@@ -434,7 +431,7 @@ export async function handleChannelInbound(
|
|
|
434
431
|
chatId: conversationExternalId,
|
|
435
432
|
text: replyText,
|
|
436
433
|
assistantId,
|
|
437
|
-
},
|
|
434
|
+
}, mintBearerToken());
|
|
438
435
|
} catch (err) {
|
|
439
436
|
log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
|
|
440
437
|
}
|
|
@@ -451,7 +448,7 @@ export async function handleChannelInbound(
|
|
|
451
448
|
chatId: conversationExternalId,
|
|
452
449
|
text: "Sorry, you haven't been approved to message this assistant.",
|
|
453
450
|
assistantId,
|
|
454
|
-
},
|
|
451
|
+
}, mintBearerToken());
|
|
455
452
|
} catch (err) {
|
|
456
453
|
log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
|
|
457
454
|
}
|
|
@@ -567,7 +564,7 @@ export async function handleChannelInbound(
|
|
|
567
564
|
chatId: pendingReply.chatId,
|
|
568
565
|
text: pendingReply.text,
|
|
569
566
|
assistantId: pendingReply.assistantId,
|
|
570
|
-
},
|
|
567
|
+
}, mintBearerToken());
|
|
571
568
|
channelDeliveryStore.clearPendingVerificationReply(result.eventId);
|
|
572
569
|
log.info({ eventId: result.eventId }, 'Retried pending verification reply: delivered');
|
|
573
570
|
} catch (retryErr) {
|
|
@@ -864,7 +861,7 @@ export async function handleChannelInbound(
|
|
|
864
861
|
chatId: conversationExternalId,
|
|
865
862
|
text: replyText,
|
|
866
863
|
assistantId,
|
|
867
|
-
},
|
|
864
|
+
}, mintBearerToken());
|
|
868
865
|
} catch (err) {
|
|
869
866
|
// The challenge is already consumed and side effects applied, so
|
|
870
867
|
// we cannot simply re-throw and let the gateway retry the full
|
|
@@ -887,7 +884,7 @@ export async function handleChannelInbound(
|
|
|
887
884
|
chatId: conversationExternalId,
|
|
888
885
|
text: replyText,
|
|
889
886
|
assistantId,
|
|
890
|
-
},
|
|
887
|
+
}, mintBearerToken());
|
|
891
888
|
log.info({ eventId: result.eventId }, 'Verification reply delivered on self-retry');
|
|
892
889
|
channelDeliveryStore.clearPendingVerificationReply(result.eventId);
|
|
893
890
|
} catch (retryErr) {
|
|
@@ -992,7 +989,7 @@ export async function handleChannelInbound(
|
|
|
992
989
|
replyCallbackUrl,
|
|
993
990
|
guardianChatId: conversationExternalId,
|
|
994
991
|
assistantId: canonicalAssistantId,
|
|
995
|
-
bearerToken,
|
|
992
|
+
bearerToken: mintBearerToken(),
|
|
996
993
|
},
|
|
997
994
|
});
|
|
998
995
|
|
|
@@ -1004,7 +1001,7 @@ export async function handleChannelInbound(
|
|
|
1004
1001
|
chatId: conversationExternalId,
|
|
1005
1002
|
text: routerResult.replyText,
|
|
1006
1003
|
assistantId: canonicalAssistantId,
|
|
1007
|
-
},
|
|
1004
|
+
}, mintBearerToken());
|
|
1008
1005
|
} catch (err) {
|
|
1009
1006
|
log.error({ err, conversationExternalId }, 'Failed to deliver canonical router reply');
|
|
1010
1007
|
}
|
|
@@ -1042,7 +1039,7 @@ export async function handleChannelInbound(
|
|
|
1042
1039
|
sourceChannel,
|
|
1043
1040
|
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
1044
1041
|
replyCallbackUrl,
|
|
1045
|
-
bearerToken,
|
|
1042
|
+
bearerToken: mintBearerToken(),
|
|
1046
1043
|
guardianCtx,
|
|
1047
1044
|
assistantId: canonicalAssistantId,
|
|
1048
1045
|
approvalCopyGenerator,
|
|
@@ -1200,7 +1197,7 @@ export async function handleChannelInbound(
|
|
|
1200
1197
|
commandIntent,
|
|
1201
1198
|
sourceLanguageCode,
|
|
1202
1199
|
replyCallbackUrl,
|
|
1203
|
-
|
|
1200
|
+
mintBearerToken,
|
|
1204
1201
|
assistantId: canonicalAssistantId,
|
|
1205
1202
|
approvalCopyGenerator,
|
|
1206
1203
|
});
|
|
@@ -1348,7 +1345,8 @@ interface BackgroundProcessingParams {
|
|
|
1348
1345
|
metadataHints: string[];
|
|
1349
1346
|
metadataUxBrief?: string;
|
|
1350
1347
|
replyCallbackUrl?: string;
|
|
1351
|
-
|
|
1348
|
+
/** Factory that mints a fresh delivery JWT for each HTTP attempt. */
|
|
1349
|
+
mintBearerToken: () => string;
|
|
1352
1350
|
assistantId?: string;
|
|
1353
1351
|
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
1354
1352
|
commandIntent?: Record<string, unknown>;
|
|
@@ -1384,7 +1382,7 @@ function shouldEmitTelegramTyping(
|
|
|
1384
1382
|
function startTelegramTypingHeartbeat(
|
|
1385
1383
|
callbackUrl: string,
|
|
1386
1384
|
chatId: string,
|
|
1387
|
-
|
|
1385
|
+
mintBearerToken: () => string,
|
|
1388
1386
|
assistantId?: string,
|
|
1389
1387
|
): () => void {
|
|
1390
1388
|
let active = true;
|
|
@@ -1396,7 +1394,7 @@ function startTelegramTypingHeartbeat(
|
|
|
1396
1394
|
void deliverChannelReply(
|
|
1397
1395
|
callbackUrl,
|
|
1398
1396
|
{ chatId, chatAction: 'typing', assistantId },
|
|
1399
|
-
|
|
1397
|
+
mintBearerToken(),
|
|
1400
1398
|
).catch((err) => {
|
|
1401
1399
|
log.debug({ err, chatId }, 'Failed to deliver Telegram typing indicator');
|
|
1402
1400
|
}).finally(() => {
|
|
@@ -1423,7 +1421,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1423
1421
|
guardianExternalUserId?: string;
|
|
1424
1422
|
requesterExternalUserId?: string;
|
|
1425
1423
|
replyCallbackUrl: string;
|
|
1426
|
-
|
|
1424
|
+
mintBearerToken: () => string;
|
|
1427
1425
|
assistantId?: string;
|
|
1428
1426
|
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
1429
1427
|
}): () => void {
|
|
@@ -1435,7 +1433,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1435
1433
|
guardianExternalUserId,
|
|
1436
1434
|
requesterExternalUserId,
|
|
1437
1435
|
replyCallbackUrl,
|
|
1438
|
-
|
|
1436
|
+
mintBearerToken,
|
|
1439
1437
|
assistantId,
|
|
1440
1438
|
approvalCopyGenerator,
|
|
1441
1439
|
} = params;
|
|
@@ -1467,7 +1465,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1467
1465
|
chatId: externalChatId,
|
|
1468
1466
|
sourceChannel,
|
|
1469
1467
|
assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1470
|
-
bearerToken,
|
|
1468
|
+
bearerToken: mintBearerToken(),
|
|
1471
1469
|
prompt,
|
|
1472
1470
|
uiMetadata: buildApprovalUIMetadata(prompt, info),
|
|
1473
1471
|
messageContext: {
|
|
@@ -1535,7 +1533,7 @@ function startTrustedContactApprovalNotifier(params: {
|
|
|
1535
1533
|
guardianTrustClass: GuardianContext['trustClass'];
|
|
1536
1534
|
guardianExternalUserId?: string;
|
|
1537
1535
|
replyCallbackUrl: string;
|
|
1538
|
-
|
|
1536
|
+
mintBearerToken: () => string;
|
|
1539
1537
|
assistantId?: string;
|
|
1540
1538
|
}): () => void {
|
|
1541
1539
|
const {
|
|
@@ -1545,7 +1543,7 @@ function startTrustedContactApprovalNotifier(params: {
|
|
|
1545
1543
|
guardianTrustClass,
|
|
1546
1544
|
guardianExternalUserId,
|
|
1547
1545
|
replyCallbackUrl,
|
|
1548
|
-
|
|
1546
|
+
mintBearerToken,
|
|
1549
1547
|
assistantId,
|
|
1550
1548
|
} = params;
|
|
1551
1549
|
|
|
@@ -1588,7 +1586,7 @@ function startTrustedContactApprovalNotifier(params: {
|
|
|
1588
1586
|
chatId: externalChatId,
|
|
1589
1587
|
text: waitingText,
|
|
1590
1588
|
assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1591
|
-
},
|
|
1589
|
+
}, mintBearerToken());
|
|
1592
1590
|
} catch (err) {
|
|
1593
1591
|
log.warn({ err, conversationId }, 'Failed to deliver trusted-contact pending-approval notification');
|
|
1594
1592
|
// Remove from notified set so delivery is retried on next poll
|
|
@@ -1630,7 +1628,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1630
1628
|
metadataHints,
|
|
1631
1629
|
metadataUxBrief,
|
|
1632
1630
|
replyCallbackUrl,
|
|
1633
|
-
|
|
1631
|
+
mintBearerToken,
|
|
1634
1632
|
assistantId,
|
|
1635
1633
|
approvalCopyGenerator,
|
|
1636
1634
|
commandIntent,
|
|
@@ -1642,7 +1640,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1642
1640
|
? replyCallbackUrl
|
|
1643
1641
|
: undefined;
|
|
1644
1642
|
const stopTypingHeartbeat = typingCallbackUrl
|
|
1645
|
-
? startTelegramTypingHeartbeat(typingCallbackUrl, externalChatId,
|
|
1643
|
+
? startTelegramTypingHeartbeat(typingCallbackUrl, externalChatId, mintBearerToken, assistantId)
|
|
1646
1644
|
: undefined;
|
|
1647
1645
|
const stopApprovalWatcher = replyCallbackUrl
|
|
1648
1646
|
? startPendingApprovalPromptWatcher({
|
|
@@ -1653,7 +1651,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1653
1651
|
guardianExternalUserId: guardianCtx.guardianExternalUserId,
|
|
1654
1652
|
requesterExternalUserId: guardianCtx.requesterExternalUserId,
|
|
1655
1653
|
replyCallbackUrl,
|
|
1656
|
-
|
|
1654
|
+
mintBearerToken,
|
|
1657
1655
|
assistantId,
|
|
1658
1656
|
approvalCopyGenerator,
|
|
1659
1657
|
})
|
|
@@ -1666,7 +1664,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1666
1664
|
guardianTrustClass: guardianCtx.trustClass,
|
|
1667
1665
|
guardianExternalUserId: guardianCtx.guardianExternalUserId,
|
|
1668
1666
|
replyCallbackUrl,
|
|
1669
|
-
|
|
1667
|
+
mintBearerToken,
|
|
1670
1668
|
assistantId,
|
|
1671
1669
|
})
|
|
1672
1670
|
: undefined;
|
|
@@ -1701,7 +1699,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1701
1699
|
conversationId,
|
|
1702
1700
|
externalChatId,
|
|
1703
1701
|
replyCallbackUrl,
|
|
1704
|
-
|
|
1702
|
+
mintBearerToken(),
|
|
1705
1703
|
assistantId,
|
|
1706
1704
|
{
|
|
1707
1705
|
onSegmentDelivered: (count) =>
|
|
@@ -1735,11 +1733,7 @@ function deliverBootstrapVerificationTelegram(
|
|
|
1735
1733
|
): void {
|
|
1736
1734
|
const attemptDelivery = async (): Promise<boolean> => {
|
|
1737
1735
|
const gatewayUrl = getGatewayInternalBaseUrl();
|
|
1738
|
-
const bearerToken =
|
|
1739
|
-
if (!bearerToken) {
|
|
1740
|
-
log.error('Cannot deliver bootstrap verification Telegram message: no runtime HTTP token available');
|
|
1741
|
-
return false;
|
|
1742
|
-
}
|
|
1736
|
+
const bearerToken = mintDaemonDeliveryToken();
|
|
1743
1737
|
const url = `${gatewayUrl}/deliver/telegram`;
|
|
1744
1738
|
const resp = await fetch(url, {
|
|
1745
1739
|
method: 'POST',
|
|
@@ -10,16 +10,16 @@ import {
|
|
|
10
10
|
import type { ServerMessage } from '../../daemon/ipc-contract.js';
|
|
11
11
|
import { PairingStore } from '../../daemon/pairing-store.js';
|
|
12
12
|
import { getLogger } from '../../util/logger.js';
|
|
13
|
-
import { mintCredentialPair } from '../actor-refresh-token-service.js';
|
|
14
13
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
14
|
+
import { mintCredentialPair } from '../auth/credential-service.js';
|
|
15
15
|
import { ensureVellumGuardianBinding } from '../guardian-vellum-migration.js';
|
|
16
16
|
import { httpError } from '../http-errors.js';
|
|
17
17
|
|
|
18
18
|
const log = getLogger('runtime-http');
|
|
19
19
|
|
|
20
20
|
interface PairingCredentials {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
accessToken: string;
|
|
22
|
+
accessTokenExpiresAt: number;
|
|
23
23
|
refreshToken: string;
|
|
24
24
|
refreshTokenExpiresAt: number;
|
|
25
25
|
refreshAfter: number;
|
|
@@ -50,8 +50,8 @@ function mintPairingCredentials(deviceId: string, platform: string): PairingCred
|
|
|
50
50
|
|
|
51
51
|
log.info({ assistantId, platform }, 'Minted credentials during pairing');
|
|
52
52
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
accessToken: credentials.accessToken,
|
|
54
|
+
accessTokenExpiresAt: credentials.accessTokenExpiresAt,
|
|
55
55
|
refreshToken: credentials.refreshToken,
|
|
56
56
|
refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
|
|
57
57
|
refreshAfter: credentials.refreshAfter,
|
|
@@ -213,8 +213,8 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
|
|
|
213
213
|
localLanUrl: entry.localLanUrl,
|
|
214
214
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
215
215
|
...(credentials ? {
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
accessToken: credentials.accessToken,
|
|
217
|
+
accessTokenExpiresAt: credentials.accessTokenExpiresAt,
|
|
218
218
|
refreshToken: credentials.refreshToken,
|
|
219
219
|
refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
|
|
220
220
|
refreshAfter: credentials.refreshAfter,
|
|
@@ -312,8 +312,8 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
312
312
|
localLanUrl: entry.localLanUrl,
|
|
313
313
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
314
314
|
...(credentialEntry ? {
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
accessToken: credentialEntry.credentials.accessToken,
|
|
316
|
+
accessTokenExpiresAt: credentialEntry.credentials.accessTokenExpiresAt,
|
|
317
317
|
refreshToken: credentialEntry.credentials.refreshToken,
|
|
318
318
|
refreshTokenExpiresAt: credentialEntry.credentials.refreshTokenExpiresAt,
|
|
319
319
|
refreshAfter: credentialEntry.credentials.refreshAfter,
|