@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
|
@@ -1107,6 +1107,126 @@ describe("session-agent-loop", () => {
|
|
|
1107
1107
|
});
|
|
1108
1108
|
});
|
|
1109
1109
|
|
|
1110
|
+
describe("stale pending surface cleanup", () => {
|
|
1111
|
+
test("auto-completes non-dynamic_page pending surfaces on regular user message", async () => {
|
|
1112
|
+
const events: ServerMessage[] = [];
|
|
1113
|
+
|
|
1114
|
+
const ctx = makeCtx();
|
|
1115
|
+
// Pre-populate a stale pending table surface
|
|
1116
|
+
ctx.pendingSurfaceActions.set("stale-table-1", { surfaceType: "table" });
|
|
1117
|
+
ctx.pendingSurfaceActions.set("stale-form-1", { surfaceType: "form" });
|
|
1118
|
+
// dynamic_page should be preserved
|
|
1119
|
+
ctx.pendingSurfaceActions.set("page-1", { surfaceType: "dynamic_page" });
|
|
1120
|
+
|
|
1121
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg), {
|
|
1122
|
+
isUserMessage: true,
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// The stale table and form surfaces should have been auto-completed
|
|
1126
|
+
const completeEvents = events.filter(
|
|
1127
|
+
(e) => e.type === "ui_surface_complete",
|
|
1128
|
+
);
|
|
1129
|
+
expect(completeEvents).toHaveLength(2);
|
|
1130
|
+
for (const evt of completeEvents) {
|
|
1131
|
+
const typed = evt as { surfaceId: string; summary: string };
|
|
1132
|
+
expect(typed.summary).toBe("Dismissed");
|
|
1133
|
+
expect(["stale-table-1", "stale-form-1"]).toContain(typed.surfaceId);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// dynamic_page should still be pending
|
|
1137
|
+
expect(ctx.pendingSurfaceActions.has("page-1")).toBe(true);
|
|
1138
|
+
expect(ctx.pendingSurfaceActions.has("stale-table-1")).toBe(false);
|
|
1139
|
+
expect(ctx.pendingSurfaceActions.has("stale-form-1")).toBe(false);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test("does not auto-complete surfaces when request is a surface action", async () => {
|
|
1143
|
+
const events: ServerMessage[] = [];
|
|
1144
|
+
|
|
1145
|
+
const ctx = makeCtx();
|
|
1146
|
+
ctx.pendingSurfaceActions.set("active-table-1", { surfaceType: "table" });
|
|
1147
|
+
// Mark the request ID as a surface action response
|
|
1148
|
+
ctx.currentRequestId = "surface-action-req";
|
|
1149
|
+
ctx.surfaceActionRequestIds.add("surface-action-req");
|
|
1150
|
+
|
|
1151
|
+
await runAgentLoopImpl(
|
|
1152
|
+
ctx,
|
|
1153
|
+
"[User action on table surface]",
|
|
1154
|
+
"msg-1",
|
|
1155
|
+
(msg) => events.push(msg),
|
|
1156
|
+
{ isUserMessage: true },
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
// No ui_surface_complete should have been emitted
|
|
1160
|
+
const completeEvents = events.filter(
|
|
1161
|
+
(e) => e.type === "ui_surface_complete",
|
|
1162
|
+
);
|
|
1163
|
+
expect(completeEvents).toHaveLength(0);
|
|
1164
|
+
// The pending surface should still be there
|
|
1165
|
+
expect(ctx.pendingSurfaceActions.has("active-table-1")).toBe(true);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test("no-op when no pending surfaces exist", async () => {
|
|
1169
|
+
const events: ServerMessage[] = [];
|
|
1170
|
+
|
|
1171
|
+
const ctx = makeCtx();
|
|
1172
|
+
// No pending surfaces
|
|
1173
|
+
|
|
1174
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg), {
|
|
1175
|
+
isUserMessage: true,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
const completeEvents = events.filter(
|
|
1179
|
+
(e) => e.type === "ui_surface_complete",
|
|
1180
|
+
);
|
|
1181
|
+
expect(completeEvents).toHaveLength(0);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
test("does not auto-complete surfaces for internal/subagent turns (no isUserMessage)", async () => {
|
|
1185
|
+
const events: ServerMessage[] = [];
|
|
1186
|
+
|
|
1187
|
+
const ctx = makeCtx();
|
|
1188
|
+
ctx.pendingSurfaceActions.set("active-table-1", { surfaceType: "table" });
|
|
1189
|
+
ctx.pendingSurfaceActions.set("active-form-1", { surfaceType: "form" });
|
|
1190
|
+
|
|
1191
|
+
// Internal turn: no isUserMessage option
|
|
1192
|
+
await runAgentLoopImpl(ctx, "subagent notification", "msg-1", (msg) =>
|
|
1193
|
+
events.push(msg),
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// No ui_surface_complete should have been emitted
|
|
1197
|
+
const completeEvents = events.filter(
|
|
1198
|
+
(e) => e.type === "ui_surface_complete",
|
|
1199
|
+
);
|
|
1200
|
+
expect(completeEvents).toHaveLength(0);
|
|
1201
|
+
// Pending surfaces should still be there
|
|
1202
|
+
expect(ctx.pendingSurfaceActions.has("active-table-1")).toBe(true);
|
|
1203
|
+
expect(ctx.pendingSurfaceActions.has("active-form-1")).toBe(true);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("finally block still runs if onEvent throws during stale surface dismissal", async () => {
|
|
1207
|
+
let _eventCount = 0;
|
|
1208
|
+
const ctx = makeCtx();
|
|
1209
|
+
ctx.pendingSurfaceActions.set("stale-table-1", { surfaceType: "table" });
|
|
1210
|
+
|
|
1211
|
+
const throwingOnEvent = (msg: ServerMessage) => {
|
|
1212
|
+
_eventCount++;
|
|
1213
|
+
if (msg.type === "ui_surface_complete") {
|
|
1214
|
+
throw new Error("onEvent sink failed");
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// The error from onEvent should be caught by the try/catch,
|
|
1219
|
+
// and the finally block should still clean up session state
|
|
1220
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", throwingOnEvent, {
|
|
1221
|
+
isUserMessage: true,
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
expect(ctx.processing).toBe(false);
|
|
1225
|
+
expect(ctx.abortController).toBeNull();
|
|
1226
|
+
expect(ctx.currentRequestId).toBeUndefined();
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1110
1230
|
describe("error-only response with no assistant text", () => {
|
|
1111
1231
|
test("synthesizes error assistant message when provider returns no response", async () => {
|
|
1112
1232
|
const events: ServerMessage[] = [];
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clearAll,
|
|
5
|
+
clearMode,
|
|
6
|
+
getEffectiveMode,
|
|
7
|
+
hasActiveOverride,
|
|
8
|
+
setThreadMode,
|
|
9
|
+
setTimedMode,
|
|
10
|
+
} from '../runtime/session-approval-overrides.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Tests
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe('session-approval-overrides', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
clearAll();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// -----------------------------------------------------------------------
|
|
22
|
+
// setThreadMode / getEffectiveMode
|
|
23
|
+
// -----------------------------------------------------------------------
|
|
24
|
+
describe('thread mode', () => {
|
|
25
|
+
test('setThreadMode stores a thread override', () => {
|
|
26
|
+
setThreadMode('conv-1');
|
|
27
|
+
const mode = getEffectiveMode('conv-1');
|
|
28
|
+
expect(mode).not.toBeNull();
|
|
29
|
+
expect(mode!.kind).toBe('thread');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('thread mode persists across multiple reads', () => {
|
|
33
|
+
setThreadMode('conv-1');
|
|
34
|
+
expect(getEffectiveMode('conv-1')).not.toBeNull();
|
|
35
|
+
expect(getEffectiveMode('conv-1')).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('thread mode is scoped to a specific conversationId', () => {
|
|
39
|
+
setThreadMode('conv-1');
|
|
40
|
+
expect(getEffectiveMode('conv-1')).not.toBeNull();
|
|
41
|
+
expect(getEffectiveMode('conv-2')).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
// setTimedMode / getEffectiveMode
|
|
47
|
+
// -----------------------------------------------------------------------
|
|
48
|
+
describe('timed mode', () => {
|
|
49
|
+
test('setTimedMode stores a timed override with default 10-minute TTL', () => {
|
|
50
|
+
const before = Date.now();
|
|
51
|
+
setTimedMode('conv-1');
|
|
52
|
+
const after = Date.now();
|
|
53
|
+
|
|
54
|
+
const mode = getEffectiveMode('conv-1');
|
|
55
|
+
expect(mode).not.toBeNull();
|
|
56
|
+
expect(mode!.kind).toBe('timed');
|
|
57
|
+
|
|
58
|
+
const timed = mode!;
|
|
59
|
+
if (timed.kind === 'timed') {
|
|
60
|
+
const expectedMin = before + 10 * 60 * 1000;
|
|
61
|
+
const expectedMax = after + 10 * 60 * 1000;
|
|
62
|
+
expect(timed.expiresAt).toBeGreaterThanOrEqual(expectedMin);
|
|
63
|
+
expect(timed.expiresAt).toBeLessThanOrEqual(expectedMax);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('setTimedMode accepts a custom duration', () => {
|
|
68
|
+
const before = Date.now();
|
|
69
|
+
setTimedMode('conv-1', 5000);
|
|
70
|
+
const after = Date.now();
|
|
71
|
+
|
|
72
|
+
const mode = getEffectiveMode('conv-1');
|
|
73
|
+
expect(mode).not.toBeNull();
|
|
74
|
+
const timed = mode!;
|
|
75
|
+
if (timed.kind === 'timed') {
|
|
76
|
+
expect(timed.expiresAt).toBeGreaterThanOrEqual(before + 5000);
|
|
77
|
+
expect(timed.expiresAt).toBeLessThanOrEqual(after + 5000);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('timed mode expires after TTL elapses', async () => {
|
|
82
|
+
setTimedMode('conv-1', 1); // 1ms TTL
|
|
83
|
+
// Small delay to ensure expiry
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
85
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('expired timed mode is lazily cleaned up on read', async () => {
|
|
89
|
+
setTimedMode('conv-1', 1);
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
91
|
+
|
|
92
|
+
// First read triggers cleanup and returns null
|
|
93
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
94
|
+
// Subsequent read also returns null (entry was removed)
|
|
95
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
// Mode replacement
|
|
101
|
+
// -----------------------------------------------------------------------
|
|
102
|
+
describe('mode replacement', () => {
|
|
103
|
+
test('setTimedMode replaces an existing thread mode', () => {
|
|
104
|
+
setThreadMode('conv-1');
|
|
105
|
+
setTimedMode('conv-1', 60_000);
|
|
106
|
+
|
|
107
|
+
const mode = getEffectiveMode('conv-1');
|
|
108
|
+
expect(mode).not.toBeNull();
|
|
109
|
+
expect(mode!.kind).toBe('timed');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('setThreadMode replaces an existing timed mode', () => {
|
|
113
|
+
setTimedMode('conv-1', 60_000);
|
|
114
|
+
setThreadMode('conv-1');
|
|
115
|
+
|
|
116
|
+
const mode = getEffectiveMode('conv-1');
|
|
117
|
+
expect(mode).not.toBeNull();
|
|
118
|
+
expect(mode!.kind).toBe('thread');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
// clearMode
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
describe('clearMode', () => {
|
|
126
|
+
test('clearMode removes a thread override', () => {
|
|
127
|
+
setThreadMode('conv-1');
|
|
128
|
+
clearMode('conv-1');
|
|
129
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('clearMode removes a timed override', () => {
|
|
133
|
+
setTimedMode('conv-1', 60_000);
|
|
134
|
+
clearMode('conv-1');
|
|
135
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('clearMode is a no-op for unknown conversationId', () => {
|
|
139
|
+
// Should not throw
|
|
140
|
+
clearMode('nonexistent');
|
|
141
|
+
expect(getEffectiveMode('nonexistent')).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
// hasActiveOverride
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
describe('hasActiveOverride', () => {
|
|
149
|
+
test('returns false when no override is set', () => {
|
|
150
|
+
expect(hasActiveOverride('conv-1')).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('returns true for active thread override', () => {
|
|
154
|
+
setThreadMode('conv-1');
|
|
155
|
+
expect(hasActiveOverride('conv-1')).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('returns true for active timed override', () => {
|
|
159
|
+
setTimedMode('conv-1', 60_000);
|
|
160
|
+
expect(hasActiveOverride('conv-1')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('returns false after timed override expires', async () => {
|
|
164
|
+
setTimedMode('conv-1', 1);
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
166
|
+
expect(hasActiveOverride('conv-1')).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('returns false after clearMode', () => {
|
|
170
|
+
setThreadMode('conv-1');
|
|
171
|
+
clearMode('conv-1');
|
|
172
|
+
expect(hasActiveOverride('conv-1')).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// -----------------------------------------------------------------------
|
|
177
|
+
// clearAll
|
|
178
|
+
// -----------------------------------------------------------------------
|
|
179
|
+
describe('clearAll', () => {
|
|
180
|
+
test('clearAll removes all overrides', () => {
|
|
181
|
+
setThreadMode('conv-1');
|
|
182
|
+
setTimedMode('conv-2', 60_000);
|
|
183
|
+
setThreadMode('conv-3');
|
|
184
|
+
|
|
185
|
+
clearAll();
|
|
186
|
+
|
|
187
|
+
expect(getEffectiveMode('conv-1')).toBeNull();
|
|
188
|
+
expect(getEffectiveMode('conv-2')).toBeNull();
|
|
189
|
+
expect(getEffectiveMode('conv-3')).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('clearAll is safe to call on empty store', () => {
|
|
193
|
+
// Should not throw
|
|
194
|
+
clearAll();
|
|
195
|
+
expect(hasActiveOverride('anything')).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
// getEffectiveMode with no override
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
test('getEffectiveMode returns null for unknown conversationId', () => {
|
|
203
|
+
expect(getEffectiveMode('nonexistent')).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -180,4 +180,42 @@ describe('task_progress surface compatibility', () => {
|
|
|
180
180
|
expect(templateData.status).toBe('completed');
|
|
181
181
|
expect(Array.isArray(templateData.steps)).toBe(true);
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
test('ui_show rejects new interactive surface when a non-dynamic_page pending surface exists', async () => {
|
|
185
|
+
const sent: ServerMessage[] = [];
|
|
186
|
+
const ctx = makeContext(sent);
|
|
187
|
+
|
|
188
|
+
// Pre-populate a pending table surface (simulates a previously shown interactive surface)
|
|
189
|
+
ctx.pendingSurfaceActions.set('stale-surface-1', { surfaceType: 'table' });
|
|
190
|
+
|
|
191
|
+
const result = await surfaceProxyResolver(ctx, 'ui_show', {
|
|
192
|
+
surface_type: 'table',
|
|
193
|
+
title: 'New Table',
|
|
194
|
+
data: { columns: [], rows: [] },
|
|
195
|
+
actions: [{ id: 'archive', label: 'Archive' }],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.isError).toBe(true);
|
|
199
|
+
expect(result.content).toContain('Another interactive surface is already awaiting user input');
|
|
200
|
+
// The stale entry should still be present (guard only rejects, doesn't clean up)
|
|
201
|
+
expect(ctx.pendingSurfaceActions.has('stale-surface-1')).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('ui_show allows new interactive surface when only dynamic_page surfaces are pending', async () => {
|
|
205
|
+
const sent: ServerMessage[] = [];
|
|
206
|
+
const ctx = makeContext(sent);
|
|
207
|
+
|
|
208
|
+
// dynamic_page pending entries should not block new interactive surfaces
|
|
209
|
+
ctx.pendingSurfaceActions.set('page-1', { surfaceType: 'dynamic_page' });
|
|
210
|
+
|
|
211
|
+
const result = await surfaceProxyResolver(ctx, 'ui_show', {
|
|
212
|
+
surface_type: 'table',
|
|
213
|
+
title: 'Email Table',
|
|
214
|
+
data: { columns: [], rows: [] },
|
|
215
|
+
actions: [{ id: 'archive', label: 'Archive' }],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.isError).toBe(false);
|
|
219
|
+
expect(sent.some(m => m.type === 'ui_surface_show')).toBe(true);
|
|
220
|
+
});
|
|
183
221
|
});
|
package/src/amazon/client.ts
CHANGED
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
|
|
52
52
|
import { extensionRelayServer } from '../browser-extension-relay/server.js';
|
|
53
53
|
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
54
|
+
import { isSigningKeyInitialized, mintEdgeRelayToken } from '../runtime/auth/token-service.js';
|
|
54
55
|
import type { ExtractedCredential } from '../tools/browser/network-recording-types.js';
|
|
55
|
-
import { readHttpToken } from '../util/platform.js';
|
|
56
56
|
import {
|
|
57
57
|
type AmazonSession,
|
|
58
58
|
loadSession,
|
|
@@ -75,11 +75,14 @@ export async function sendRelayCommand(command: Record<string, unknown>): Promis
|
|
|
75
75
|
return extensionRelayServer.sendCommand(command as Omit<ExtensionCommand, 'id'>);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Fall back to HTTP relay endpoint
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Fall back to HTTP relay endpoint via the gateway.
|
|
79
|
+
// The gateway validates edge JWTs (aud=vellum-gateway) and mints an
|
|
80
|
+
// exchange token for the runtime. Without the signing key (CLI
|
|
81
|
+
// out-of-process), we cannot mint JWTs at all.
|
|
82
|
+
if (!isSigningKeyInitialized()) {
|
|
83
|
+
throw new Error('Auth signing key not initialized — browser-relay commands require the daemon to be running');
|
|
82
84
|
}
|
|
85
|
+
const token = mintEdgeRelayToken();
|
|
83
86
|
|
|
84
87
|
const resp = await fetch(`${getGatewayInternalBaseUrl()}/v1/browser-relay/command`, {
|
|
85
88
|
method: 'POST',
|
|
@@ -162,10 +162,11 @@ export interface ApplyGuardianDecisionParams {
|
|
|
162
162
|
export function applyGuardianDecision(params: ApplyGuardianDecisionParams): ApplyGuardianDecisionResult {
|
|
163
163
|
const { approval, decision, actorExternalUserId, actorChannel, decisionContext } = params;
|
|
164
164
|
|
|
165
|
-
// Guardians cannot
|
|
166
|
-
const effectiveDecision: ApprovalDecisionResult =
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
// Guardians cannot grant broad allow modes on behalf of requesters -- downgrade.
|
|
166
|
+
const effectiveDecision: ApprovalDecisionResult =
|
|
167
|
+
decision.action === 'approve_always' || decision.action === 'approve_10m' || decision.action === 'approve_thread'
|
|
168
|
+
? { ...decision, action: 'approve_once' }
|
|
169
|
+
: decision;
|
|
169
170
|
|
|
170
171
|
// Capture pending approval info before handleChannelDecision resolves
|
|
171
172
|
// (and removes) the pending interaction. Needed for grant minting.
|
|
@@ -282,6 +283,8 @@ export function mintCanonicalRequestGrant(params: {
|
|
|
282
283
|
/** Valid actions for canonical guardian decisions. */
|
|
283
284
|
const VALID_CANONICAL_ACTIONS: ReadonlySet<ApprovalAction> = new Set([
|
|
284
285
|
'approve_once',
|
|
286
|
+
'approve_10m',
|
|
287
|
+
'approve_thread',
|
|
285
288
|
'approve_always',
|
|
286
289
|
'reject',
|
|
287
290
|
]);
|
|
@@ -404,11 +407,13 @@ export async function applyCanonicalGuardianDecision(
|
|
|
404
407
|
return { applied: false, reason: 'expired' };
|
|
405
408
|
}
|
|
406
409
|
|
|
407
|
-
// 3. Downgrade approve_always to approve_once for
|
|
408
|
-
// Guardians cannot
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
// 3. Downgrade approve_always and temporary modes to approve_once for
|
|
411
|
+
// guardian-on-behalf requests. Guardians cannot grant broad allow modes
|
|
412
|
+
// (permanent, timed, or thread-scoped) on behalf of requesters.
|
|
413
|
+
const effectiveAction: ApprovalAction =
|
|
414
|
+
action === 'approve_always' || action === 'approve_10m' || action === 'approve_thread'
|
|
415
|
+
? 'approve_once'
|
|
416
|
+
: action;
|
|
412
417
|
|
|
413
418
|
// 4. CAS resolve: atomically transition from 'pending' to terminal status
|
|
414
419
|
const targetStatus: CanonicalRequestStatus = effectiveAction === 'reject'
|
|
@@ -18,6 +18,7 @@ import { upsertMember } from '../memory/ingress-member-store.js';
|
|
|
18
18
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
19
19
|
import { addRule } from '../permissions/trust-store.js';
|
|
20
20
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
21
|
+
import { mintDaemonDeliveryToken } from '../runtime/auth/token-service.js';
|
|
21
22
|
import type { ApprovalAction } from '../runtime/channel-approval-types.js';
|
|
22
23
|
import { createOutboundSession } from '../runtime/channel-guardian-service.js';
|
|
23
24
|
import { deliverChannelReply } from '../runtime/gateway-client.js';
|
|
@@ -25,7 +26,6 @@ import * as pendingInteractions from '../runtime/pending-interactions.js';
|
|
|
25
26
|
import { getTool } from '../tools/registry.js';
|
|
26
27
|
import { TC_GRANT_WAIT_MAX_MS } from '../tools/tool-approval-handler.js';
|
|
27
28
|
import { getLogger } from '../util/logger.js';
|
|
28
|
-
import { readHttpToken } from '../util/platform.js';
|
|
29
29
|
|
|
30
30
|
const log = getLogger('guardian-request-resolvers');
|
|
31
31
|
|
|
@@ -313,7 +313,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
313
313
|
const decidedByExternalUserId = ctx.actor.externalUserId ?? '';
|
|
314
314
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
315
315
|
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
316
|
-
const desktopBearerToken =
|
|
316
|
+
const desktopBearerToken = mintDaemonDeliveryToken();
|
|
317
317
|
|
|
318
318
|
if (decision.action === 'reject') {
|
|
319
319
|
log.info(
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
} from '../memory/canonical-guardian-store.js';
|
|
20
20
|
import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
|
|
21
21
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
22
|
+
import { mintDaemonDeliveryToken } from '../runtime/auth/token-service.js';
|
|
22
23
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
23
24
|
import { getLogger } from '../util/logger.js';
|
|
24
|
-
import { readHttpToken } from '../util/platform.js';
|
|
25
25
|
import { getMaxCallDurationMs, getSilenceTimeoutMs, getUserConsultationTimeoutMs } from './call-constants.js';
|
|
26
26
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
27
27
|
import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
|
|
@@ -949,7 +949,7 @@ export class CallController {
|
|
|
949
949
|
canonicalDeliveries,
|
|
950
950
|
this.assistantId,
|
|
951
951
|
getGatewayInternalBaseUrl(),
|
|
952
|
-
|
|
952
|
+
mintDaemonDeliveryToken(),
|
|
953
953
|
).catch((err) => {
|
|
954
954
|
log.error(
|
|
955
955
|
{ err, callSessionId: this.callSessionId, requestId: pendingActionRequest.id },
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { getCallWelcomeGreeting, getRuntimeProxyBearerToken } from '../config/env.js';
|
|
10
10
|
import { loadConfig } from '../config/loader.js';
|
|
11
11
|
import { getTwilioRelayUrl } from '../inbound/public-ingress-urls.js';
|
|
12
|
+
import { mintEdgeRelayToken } from '../runtime/auth/token-service.js';
|
|
12
13
|
import { getLogger } from '../util/logger.js';
|
|
13
|
-
import { readHttpToken } from '../util/platform.js';
|
|
14
14
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
15
15
|
import { createInboundVoiceSession } from './call-domain.js';
|
|
16
16
|
import { logDeadLetterEvent } from './call-recovery.js';
|
|
@@ -264,7 +264,7 @@ function buildVoiceWebhookTwiml(
|
|
|
264
264
|
|
|
265
265
|
// Use the same token resolution the gateway uses for runtimeProxyBearerToken:
|
|
266
266
|
// env var override first, then the on-disk http-token file.
|
|
267
|
-
const relayToken = getRuntimeProxyBearerToken() ??
|
|
267
|
+
const relayToken = getRuntimeProxyBearerToken() ?? mintEdgeRelayToken();
|
|
268
268
|
|
|
269
269
|
// Propagate guardianVerificationSessionId as a TwiML <Parameter> for
|
|
270
270
|
// observability. This is not the sole source of truth; the relay
|
package/src/cli/mcp.ts
CHANGED
|
@@ -12,15 +12,15 @@ import { getCliLogger } from '../util/logger.js';
|
|
|
12
12
|
|
|
13
13
|
const log = getCliLogger('cli');
|
|
14
14
|
|
|
15
|
-
const HEALTH_CHECK_TIMEOUT_MS = 10_000;
|
|
15
|
+
export const HEALTH_CHECK_TIMEOUT_MS = 10_000;
|
|
16
16
|
|
|
17
|
-
async function checkServerHealth(serverId: string, config: McpServerConfig): Promise<string> {
|
|
17
|
+
export async function checkServerHealth(serverId: string, config: McpServerConfig, timeoutMs = HEALTH_CHECK_TIMEOUT_MS): Promise<string> {
|
|
18
18
|
const client = new McpClient(serverId, { quiet: true });
|
|
19
19
|
try {
|
|
20
20
|
await Promise.race([
|
|
21
21
|
client.connect(config.transport),
|
|
22
22
|
new Promise<never>((_, reject) => {
|
|
23
|
-
const t = setTimeout(() => reject(new Error('timeout')),
|
|
23
|
+
const t = setTimeout(() => reject(new Error('timeout')), timeoutMs);
|
|
24
24
|
if (typeof t === 'object' && 'unref' in t) t.unref();
|
|
25
25
|
}),
|
|
26
26
|
]);
|
package/src/cli.ts
CHANGED
|
@@ -232,6 +232,12 @@ export async function startCli(): Promise<void> {
|
|
|
232
232
|
}
|
|
233
233
|
process.stdout.write(`\u2502\n`);
|
|
234
234
|
process.stdout.write(`\u2502 [a] Allow once\n`);
|
|
235
|
+
if (req.temporaryOptionsAvailable?.includes('allow_10m')) {
|
|
236
|
+
process.stdout.write(`\u2502 [t] Allow 10m\n`);
|
|
237
|
+
}
|
|
238
|
+
if (req.temporaryOptionsAvailable?.includes('allow_thread')) {
|
|
239
|
+
process.stdout.write(`\u2502 [T] Allow Thread\n`);
|
|
240
|
+
}
|
|
235
241
|
process.stdout.write(`\u2502 [d] Deny once\n`);
|
|
236
242
|
if (req.allowlistOptions.length > 0 && req.scopeOptions.length > 0) {
|
|
237
243
|
process.stdout.write(`\u2502 [A] Allowlist...\n`);
|
|
@@ -277,6 +283,24 @@ export async function startCli(): Promise<void> {
|
|
|
277
283
|
return;
|
|
278
284
|
}
|
|
279
285
|
|
|
286
|
+
if (choice === 't' && trimmed === 't' && req.temporaryOptionsAvailable?.includes('allow_10m')) {
|
|
287
|
+
send({
|
|
288
|
+
type: 'confirmation_response',
|
|
289
|
+
requestId: req.requestId,
|
|
290
|
+
decision: 'allow_10m',
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (trimmed === 'T' && req.temporaryOptionsAvailable?.includes('allow_thread')) {
|
|
296
|
+
send({
|
|
297
|
+
type: 'confirmation_response',
|
|
298
|
+
requestId: req.requestId,
|
|
299
|
+
decision: 'allow_thread',
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
if (choice === 'd') {
|
|
281
305
|
send({
|
|
282
306
|
type: 'confirmation_response',
|