@vellumai/assistant 0.4.15 → 0.4.17
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/Dockerfile +6 -6
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +383 -254
- package/src/__tests__/call-controller.test.ts +1074 -751
- package/src/__tests__/call-routes-http.test.ts +329 -279
- package/src/__tests__/channel-approval-routes.test.ts +2 -13
- package/src/__tests__/channel-approvals.test.ts +227 -182
- package/src/__tests__/channel-guardian.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
- package/src/__tests__/conversation-routes.test.ts +71 -41
- package/src/__tests__/daemon-server-session-init.test.ts +258 -191
- package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
- package/src/__tests__/extract-email.test.ts +42 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
- package/src/__tests__/gateway-only-guard.test.ts +54 -55
- package/src/__tests__/gmail-integration.test.ts +48 -46
- package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
- package/src/__tests__/guardian-outbound-http.test.ts +334 -208
- package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
- package/src/__tests__/guardian-routing-state.test.ts +257 -209
- package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
- package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
- package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
- package/src/__tests__/ingress-reconcile.test.ts +184 -142
- package/src/__tests__/non-member-access-request.test.ts +291 -247
- package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
- package/src/__tests__/pairing-concurrent.test.ts +78 -0
- package/src/__tests__/recording-intent-handler.test.ts +422 -291
- package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
- package/src/__tests__/runtime-events-sse.test.ts +67 -50
- package/src/__tests__/send-endpoint-busy.test.ts +314 -232
- package/src/__tests__/session-approval-overrides.test.ts +93 -91
- package/src/__tests__/sms-messaging-provider.test.ts +74 -47
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
- package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
- package/src/__tests__/twilio-config.test.ts +49 -41
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
- package/src/__tests__/twilio-routes.test.ts +389 -280
- package/src/calls/call-controller.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +6 -6
- package/src/calls/twilio-routes.ts +2 -4
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -4
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
- package/src/config/env.ts +39 -29
- package/src/daemon/handlers/config-inbox.ts +5 -5
- package/src/daemon/handlers/skills.ts +18 -10
- package/src/daemon/ipc-contract/messages.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +7 -1
- package/src/daemon/pairing-store.ts +15 -2
- package/src/daemon/session-agent-loop-handlers.ts +5 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-process.ts +1 -1
- package/src/daemon/session-slash.ts +4 -4
- package/src/daemon/session-surfaces.ts +42 -2
- package/src/runtime/auth/token-service.ts +95 -45
- package/src/runtime/channel-retry-sweep.ts +2 -2
- package/src/runtime/http-server.ts +8 -7
- package/src/runtime/http-types.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
- package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
- package/src/runtime/routes/pairing-routes.ts +4 -1
- package/src/sequence/reply-matcher.ts +14 -4
- package/src/skills/frontmatter.ts +9 -6
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/util/platform.ts +0 -12
- package/docs/architecture/http-token-refresh.md +0 -274
|
@@ -4,42 +4,45 @@
|
|
|
4
4
|
* Tests POST /v1/confirm, POST /v1/secret, and POST /v1/trust-rules
|
|
5
5
|
* through RuntimeHttpServer with pending-interactions tracking.
|
|
6
6
|
*/
|
|
7
|
-
import { mkdtempSync, realpathSync,rmSync } from
|
|
8
|
-
import { tmpdir } from
|
|
9
|
-
import { join } from
|
|
7
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
-
import { afterAll, beforeEach, describe, expect, mock,test } from
|
|
11
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
12
|
|
|
13
|
-
import type { ServerMessage } from
|
|
14
|
-
import type { Session } from
|
|
13
|
+
import type { ServerMessage } from "../daemon/ipc-protocol.js";
|
|
14
|
+
import type { Session } from "../daemon/session.js";
|
|
15
15
|
|
|
16
|
-
const testDir = realpathSync(
|
|
16
|
+
const testDir = realpathSync(
|
|
17
|
+
mkdtempSync(join(tmpdir(), "approval-routes-http-test-")),
|
|
18
|
+
);
|
|
17
19
|
|
|
18
|
-
mock.module(
|
|
20
|
+
mock.module("../util/platform.js", () => ({
|
|
19
21
|
getRootDir: () => testDir,
|
|
20
22
|
getDataDir: () => testDir,
|
|
21
|
-
isMacOS: () => process.platform ===
|
|
22
|
-
isLinux: () => process.platform ===
|
|
23
|
-
isWindows: () => process.platform ===
|
|
24
|
-
getSocketPath: () => join(testDir,
|
|
25
|
-
getPidPath: () => join(testDir,
|
|
26
|
-
getDbPath: () => join(testDir,
|
|
27
|
-
getLogPath: () => join(testDir,
|
|
23
|
+
isMacOS: () => process.platform === "darwin",
|
|
24
|
+
isLinux: () => process.platform === "linux",
|
|
25
|
+
isWindows: () => process.platform === "win32",
|
|
26
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
27
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
28
30
|
ensureDataDir: () => {},
|
|
29
31
|
}));
|
|
30
32
|
|
|
31
|
-
mock.module(
|
|
32
|
-
getLogger: () =>
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
mock.module("../util/logger.js", () => ({
|
|
34
|
+
getLogger: () =>
|
|
35
|
+
new Proxy({} as Record<string, unknown>, {
|
|
36
|
+
get: () => () => {},
|
|
37
|
+
}),
|
|
35
38
|
}));
|
|
36
39
|
|
|
37
|
-
mock.module(
|
|
40
|
+
mock.module("../config/loader.js", () => ({
|
|
38
41
|
getConfig: () => ({
|
|
39
42
|
ui: {},
|
|
40
|
-
|
|
41
|
-
model:
|
|
42
|
-
provider:
|
|
43
|
+
|
|
44
|
+
model: "test",
|
|
45
|
+
provider: "test",
|
|
43
46
|
apiKeys: {},
|
|
44
47
|
memory: { enabled: false },
|
|
45
48
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
@@ -48,16 +51,36 @@ mock.module('../config/loader.js', () => ({
|
|
|
48
51
|
}),
|
|
49
52
|
}));
|
|
50
53
|
|
|
54
|
+
mock.module("../config/env.js", () => ({
|
|
55
|
+
isHttpAuthDisabled: () => true,
|
|
56
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
57
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
58
|
+
getGatewayPort: () => 7830,
|
|
59
|
+
getRuntimeHttpPort: () => 7821,
|
|
60
|
+
getRuntimeHttpHost: () => "127.0.0.1",
|
|
61
|
+
getRuntimeProxyBearerToken: () => undefined,
|
|
62
|
+
getRuntimeGatewayOriginSecret: () => undefined,
|
|
63
|
+
getIngressPublicBaseUrl: () => undefined,
|
|
64
|
+
setIngressPublicBaseUrl: () => {},
|
|
65
|
+
}));
|
|
66
|
+
|
|
51
67
|
// Mock the trust store so addRule doesn't touch disk or require full config
|
|
52
|
-
mock.module(
|
|
53
|
-
addRule: () => ({
|
|
68
|
+
mock.module("../permissions/trust-store.js", () => ({
|
|
69
|
+
addRule: () => ({
|
|
70
|
+
id: "test-rule",
|
|
71
|
+
tool: "test",
|
|
72
|
+
pattern: "*",
|
|
73
|
+
scope: "everywhere",
|
|
74
|
+
decision: "allow",
|
|
75
|
+
priority: 100,
|
|
76
|
+
}),
|
|
54
77
|
getRules: () => [],
|
|
55
78
|
}));
|
|
56
79
|
|
|
57
|
-
import { getDb, initializeDb, resetDb } from
|
|
58
|
-
import { AssistantEventHub } from
|
|
59
|
-
import { RuntimeHttpServer } from
|
|
60
|
-
import * as pendingInteractions from
|
|
80
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
81
|
+
import { AssistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
82
|
+
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
83
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
61
84
|
|
|
62
85
|
initializeDb();
|
|
63
86
|
|
|
@@ -72,11 +95,19 @@ function makeIdleSession(opts?: {
|
|
|
72
95
|
let processing = false;
|
|
73
96
|
return {
|
|
74
97
|
isProcessing: () => processing,
|
|
75
|
-
persistUserMessage: (
|
|
98
|
+
persistUserMessage: (
|
|
99
|
+
_content: string,
|
|
100
|
+
_attachments: unknown[],
|
|
101
|
+
requestId?: string,
|
|
102
|
+
) => {
|
|
76
103
|
processing = true;
|
|
77
|
-
return requestId ??
|
|
104
|
+
return requestId ?? "msg-1";
|
|
105
|
+
},
|
|
106
|
+
memoryPolicy: {
|
|
107
|
+
scopeId: "default",
|
|
108
|
+
includeDefaultFallback: false,
|
|
109
|
+
strictSideEffects: false,
|
|
78
110
|
},
|
|
79
|
-
memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
|
|
80
111
|
setChannelCapabilities: () => {},
|
|
81
112
|
setAssistantId: () => {},
|
|
82
113
|
setGuardianContext: () => {},
|
|
@@ -85,17 +116,25 @@ function makeIdleSession(opts?: {
|
|
|
85
116
|
setTurnInterfaceContext: () => {},
|
|
86
117
|
setStateSignalListener: () => {},
|
|
87
118
|
updateClient: () => {},
|
|
88
|
-
enqueueMessage: () => ({ queued: false, requestId:
|
|
119
|
+
enqueueMessage: () => ({ queued: false, requestId: "noop" }),
|
|
89
120
|
hasAnyPendingConfirmation: () => false,
|
|
90
|
-
runAgentLoop: async (
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
runAgentLoop: async (
|
|
122
|
+
_content: string,
|
|
123
|
+
_messageId: string,
|
|
124
|
+
onEvent: (msg: ServerMessage) => void,
|
|
125
|
+
) => {
|
|
126
|
+
onEvent({ type: "assistant_text_delta", text: "Hello!" });
|
|
127
|
+
onEvent({ type: "message_complete", sessionId: "test-session" });
|
|
93
128
|
processing = false;
|
|
94
129
|
},
|
|
95
130
|
handleConfirmationResponse: (requestId: string, decision: string) => {
|
|
96
131
|
opts?.onConfirmation?.(requestId, decision);
|
|
97
132
|
},
|
|
98
|
-
handleSecretResponse: (
|
|
133
|
+
handleSecretResponse: (
|
|
134
|
+
requestId: string,
|
|
135
|
+
value?: string,
|
|
136
|
+
delivery?: string,
|
|
137
|
+
) => {
|
|
99
138
|
opts?.onSecret?.(requestId, value, delivery);
|
|
100
139
|
},
|
|
101
140
|
} as unknown as Session;
|
|
@@ -111,15 +150,23 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
111
150
|
toolName?: string;
|
|
112
151
|
}): Session {
|
|
113
152
|
let processing = false;
|
|
114
|
-
const reqId = opts?.confirmRequestId ??
|
|
115
|
-
const tool = opts?.toolName ??
|
|
153
|
+
const reqId = opts?.confirmRequestId ?? "confirm-req-1";
|
|
154
|
+
const tool = opts?.toolName ?? "shell_command";
|
|
116
155
|
return {
|
|
117
156
|
isProcessing: () => processing,
|
|
118
|
-
persistUserMessage: (
|
|
157
|
+
persistUserMessage: (
|
|
158
|
+
_content: string,
|
|
159
|
+
_attachments: unknown[],
|
|
160
|
+
requestId?: string,
|
|
161
|
+
) => {
|
|
119
162
|
processing = true;
|
|
120
|
-
return requestId ??
|
|
163
|
+
return requestId ?? "msg-1";
|
|
164
|
+
},
|
|
165
|
+
memoryPolicy: {
|
|
166
|
+
scopeId: "default",
|
|
167
|
+
includeDefaultFallback: false,
|
|
168
|
+
strictSideEffects: false,
|
|
121
169
|
},
|
|
122
|
-
memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
|
|
123
170
|
setChannelCapabilities: () => {},
|
|
124
171
|
setAssistantId: () => {},
|
|
125
172
|
setGuardianContext: () => {},
|
|
@@ -128,23 +175,25 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
128
175
|
setTurnInterfaceContext: () => {},
|
|
129
176
|
setStateSignalListener: () => {},
|
|
130
177
|
updateClient: () => {},
|
|
131
|
-
enqueueMessage: () => ({ queued: false, requestId:
|
|
178
|
+
enqueueMessage: () => ({ queued: false, requestId: "noop" }),
|
|
132
179
|
hasAnyPendingConfirmation: () => false,
|
|
133
|
-
runAgentLoop: async (
|
|
180
|
+
runAgentLoop: async (
|
|
181
|
+
_content: string,
|
|
182
|
+
_messageId: string,
|
|
183
|
+
onEvent: (msg: ServerMessage) => void,
|
|
184
|
+
) => {
|
|
134
185
|
// Emit confirmation_request — this triggers the hub publisher to register
|
|
135
186
|
// the pending interaction
|
|
136
187
|
onEvent({
|
|
137
|
-
type:
|
|
188
|
+
type: "confirmation_request",
|
|
138
189
|
requestId: reqId,
|
|
139
190
|
toolName: tool,
|
|
140
|
-
input: { command:
|
|
141
|
-
riskLevel:
|
|
191
|
+
input: { command: "ls" },
|
|
192
|
+
riskLevel: "medium",
|
|
142
193
|
allowlistOptions: [
|
|
143
|
-
{ label:
|
|
144
|
-
],
|
|
145
|
-
scopeOptions: [
|
|
146
|
-
{ label: 'This session', scope: 'session' },
|
|
194
|
+
{ label: "Allow ls", description: "Allow ls command", pattern: "ls" },
|
|
147
195
|
],
|
|
196
|
+
scopeOptions: [{ label: "This session", scope: "session" }],
|
|
148
197
|
persistentDecisionsAllowed: true,
|
|
149
198
|
});
|
|
150
199
|
// Hang to simulate waiting for decision
|
|
@@ -161,26 +210,30 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
161
210
|
// Tests
|
|
162
211
|
// ---------------------------------------------------------------------------
|
|
163
212
|
|
|
164
|
-
const TEST_TOKEN =
|
|
213
|
+
const TEST_TOKEN = "test-bearer-token-approvals";
|
|
165
214
|
const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
|
|
166
215
|
|
|
167
|
-
describe(
|
|
216
|
+
describe("standalone approval endpoints — HTTP layer", () => {
|
|
168
217
|
let server: RuntimeHttpServer;
|
|
169
218
|
let port: number;
|
|
170
219
|
let eventHub: AssistantEventHub;
|
|
171
220
|
|
|
172
221
|
beforeEach(() => {
|
|
173
222
|
const db = getDb();
|
|
174
|
-
db.run(
|
|
175
|
-
db.run(
|
|
176
|
-
db.run(
|
|
223
|
+
db.run("DELETE FROM messages");
|
|
224
|
+
db.run("DELETE FROM conversations");
|
|
225
|
+
db.run("DELETE FROM conversation_keys");
|
|
177
226
|
pendingInteractions.clear();
|
|
178
227
|
eventHub = new AssistantEventHub();
|
|
179
228
|
});
|
|
180
229
|
|
|
181
230
|
afterAll(() => {
|
|
182
231
|
resetDb();
|
|
183
|
-
try {
|
|
232
|
+
try {
|
|
233
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
234
|
+
} catch {
|
|
235
|
+
/* best effort */
|
|
236
|
+
}
|
|
184
237
|
});
|
|
185
238
|
|
|
186
239
|
async function startServer(sessionFactory: () => Session): Promise<void> {
|
|
@@ -207,8 +260,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
207
260
|
|
|
208
261
|
// ── POST /v1/confirm ─────────────────────────────────────────────────
|
|
209
262
|
|
|
210
|
-
describe(
|
|
211
|
-
test(
|
|
263
|
+
describe("POST /v1/confirm", () => {
|
|
264
|
+
test("resolves a pending confirmation by requestId", async () => {
|
|
212
265
|
let confirmedRequestId: string | undefined;
|
|
213
266
|
let confirmedDecision: string | undefined;
|
|
214
267
|
|
|
@@ -222,44 +275,44 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
222
275
|
await startServer(() => session);
|
|
223
276
|
|
|
224
277
|
// Manually register a pending interaction
|
|
225
|
-
pendingInteractions.register(
|
|
278
|
+
pendingInteractions.register("req-abc", {
|
|
226
279
|
session,
|
|
227
|
-
conversationId:
|
|
228
|
-
kind:
|
|
280
|
+
conversationId: "conv-1",
|
|
281
|
+
kind: "confirmation",
|
|
229
282
|
confirmationDetails: {
|
|
230
|
-
toolName:
|
|
231
|
-
input: { command:
|
|
232
|
-
riskLevel:
|
|
283
|
+
toolName: "shell_command",
|
|
284
|
+
input: { command: "ls" },
|
|
285
|
+
riskLevel: "medium",
|
|
233
286
|
allowlistOptions: [],
|
|
234
287
|
scopeOptions: [],
|
|
235
288
|
},
|
|
236
289
|
});
|
|
237
290
|
|
|
238
|
-
const res = await fetch(url(
|
|
239
|
-
method:
|
|
240
|
-
headers: {
|
|
241
|
-
body: JSON.stringify({ requestId:
|
|
291
|
+
const res = await fetch(url("confirm"), {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
294
|
+
body: JSON.stringify({ requestId: "req-abc", decision: "allow" }),
|
|
242
295
|
});
|
|
243
|
-
const body = await res.json() as { accepted: boolean };
|
|
296
|
+
const body = (await res.json()) as { accepted: boolean };
|
|
244
297
|
|
|
245
298
|
expect(res.status).toBe(200);
|
|
246
299
|
expect(body.accepted).toBe(true);
|
|
247
|
-
expect(confirmedRequestId).toBe(
|
|
248
|
-
expect(confirmedDecision).toBe(
|
|
300
|
+
expect(confirmedRequestId).toBe("req-abc");
|
|
301
|
+
expect(confirmedDecision).toBe("allow");
|
|
249
302
|
|
|
250
303
|
// Interaction should be removed after resolution
|
|
251
|
-
expect(pendingInteractions.get(
|
|
304
|
+
expect(pendingInteractions.get("req-abc")).toBeUndefined();
|
|
252
305
|
|
|
253
306
|
await stopServer();
|
|
254
307
|
});
|
|
255
308
|
|
|
256
|
-
test(
|
|
309
|
+
test("returns 404 for unknown requestId", async () => {
|
|
257
310
|
await startServer(() => makeIdleSession());
|
|
258
311
|
|
|
259
|
-
const res = await fetch(url(
|
|
260
|
-
method:
|
|
261
|
-
headers: {
|
|
262
|
-
body: JSON.stringify({ requestId:
|
|
312
|
+
const res = await fetch(url("confirm"), {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
315
|
+
body: JSON.stringify({ requestId: "nonexistent", decision: "allow" }),
|
|
263
316
|
});
|
|
264
317
|
|
|
265
318
|
expect(res.status).toBe(404);
|
|
@@ -267,42 +320,42 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
267
320
|
await stopServer();
|
|
268
321
|
});
|
|
269
322
|
|
|
270
|
-
test(
|
|
323
|
+
test("returns 404 for already-resolved requestId", async () => {
|
|
271
324
|
const session = makeIdleSession();
|
|
272
325
|
await startServer(() => session);
|
|
273
326
|
|
|
274
|
-
pendingInteractions.register(
|
|
327
|
+
pendingInteractions.register("req-once", {
|
|
275
328
|
session,
|
|
276
|
-
conversationId:
|
|
277
|
-
kind:
|
|
329
|
+
conversationId: "conv-1",
|
|
330
|
+
kind: "confirmation",
|
|
278
331
|
});
|
|
279
332
|
|
|
280
333
|
// First resolution succeeds
|
|
281
|
-
const res1 = await fetch(url(
|
|
282
|
-
method:
|
|
283
|
-
headers: {
|
|
284
|
-
body: JSON.stringify({ requestId:
|
|
334
|
+
const res1 = await fetch(url("confirm"), {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
337
|
+
body: JSON.stringify({ requestId: "req-once", decision: "allow" }),
|
|
285
338
|
});
|
|
286
339
|
expect(res1.status).toBe(200);
|
|
287
340
|
|
|
288
341
|
// Second resolution fails (already consumed)
|
|
289
|
-
const res2 = await fetch(url(
|
|
290
|
-
method:
|
|
291
|
-
headers: {
|
|
292
|
-
body: JSON.stringify({ requestId:
|
|
342
|
+
const res2 = await fetch(url("confirm"), {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
345
|
+
body: JSON.stringify({ requestId: "req-once", decision: "deny" }),
|
|
293
346
|
});
|
|
294
347
|
expect(res2.status).toBe(404);
|
|
295
348
|
|
|
296
349
|
await stopServer();
|
|
297
350
|
});
|
|
298
351
|
|
|
299
|
-
test(
|
|
352
|
+
test("returns 400 for missing requestId", async () => {
|
|
300
353
|
await startServer(() => makeIdleSession());
|
|
301
354
|
|
|
302
|
-
const res = await fetch(url(
|
|
303
|
-
method:
|
|
304
|
-
headers: {
|
|
305
|
-
body: JSON.stringify({ decision:
|
|
355
|
+
const res = await fetch(url("confirm"), {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
358
|
+
body: JSON.stringify({ decision: "allow" }),
|
|
306
359
|
});
|
|
307
360
|
|
|
308
361
|
expect(res.status).toBe(400);
|
|
@@ -310,13 +363,13 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
310
363
|
await stopServer();
|
|
311
364
|
});
|
|
312
365
|
|
|
313
|
-
test(
|
|
366
|
+
test("returns 400 for invalid decision", async () => {
|
|
314
367
|
await startServer(() => makeIdleSession());
|
|
315
368
|
|
|
316
|
-
const res = await fetch(url(
|
|
317
|
-
method:
|
|
318
|
-
headers: {
|
|
319
|
-
body: JSON.stringify({ requestId:
|
|
369
|
+
const res = await fetch(url("confirm"), {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
372
|
+
body: JSON.stringify({ requestId: "req-1", decision: "maybe" }),
|
|
320
373
|
});
|
|
321
374
|
|
|
322
375
|
expect(res.status).toBe(400);
|
|
@@ -327,8 +380,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
327
380
|
|
|
328
381
|
// ── POST /v1/secret ──────────────────────────────────────────────────
|
|
329
382
|
|
|
330
|
-
describe(
|
|
331
|
-
test(
|
|
383
|
+
describe("POST /v1/secret", () => {
|
|
384
|
+
test("resolves a pending secret request by requestId", async () => {
|
|
332
385
|
let secretRequestId: string | undefined;
|
|
333
386
|
let secretValue: string | undefined;
|
|
334
387
|
let secretDelivery: string | undefined;
|
|
@@ -343,38 +396,42 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
343
396
|
|
|
344
397
|
await startServer(() => session);
|
|
345
398
|
|
|
346
|
-
pendingInteractions.register(
|
|
399
|
+
pendingInteractions.register("secret-req-1", {
|
|
347
400
|
session,
|
|
348
|
-
conversationId:
|
|
349
|
-
kind:
|
|
401
|
+
conversationId: "conv-1",
|
|
402
|
+
kind: "secret",
|
|
350
403
|
});
|
|
351
404
|
|
|
352
|
-
const res = await fetch(url(
|
|
353
|
-
method:
|
|
354
|
-
headers: {
|
|
355
|
-
body: JSON.stringify({
|
|
405
|
+
const res = await fetch(url("secret"), {
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
408
|
+
body: JSON.stringify({
|
|
409
|
+
requestId: "secret-req-1",
|
|
410
|
+
value: "my-secret-key",
|
|
411
|
+
delivery: "store",
|
|
412
|
+
}),
|
|
356
413
|
});
|
|
357
|
-
const body = await res.json() as { accepted: boolean };
|
|
414
|
+
const body = (await res.json()) as { accepted: boolean };
|
|
358
415
|
|
|
359
416
|
expect(res.status).toBe(200);
|
|
360
417
|
expect(body.accepted).toBe(true);
|
|
361
|
-
expect(secretRequestId).toBe(
|
|
362
|
-
expect(secretValue).toBe(
|
|
363
|
-
expect(secretDelivery).toBe(
|
|
418
|
+
expect(secretRequestId).toBe("secret-req-1");
|
|
419
|
+
expect(secretValue).toBe("my-secret-key");
|
|
420
|
+
expect(secretDelivery).toBe("store");
|
|
364
421
|
|
|
365
422
|
// Interaction should be removed after resolution
|
|
366
|
-
expect(pendingInteractions.get(
|
|
423
|
+
expect(pendingInteractions.get("secret-req-1")).toBeUndefined();
|
|
367
424
|
|
|
368
425
|
await stopServer();
|
|
369
426
|
});
|
|
370
427
|
|
|
371
|
-
test(
|
|
428
|
+
test("returns 404 for unknown requestId", async () => {
|
|
372
429
|
await startServer(() => makeIdleSession());
|
|
373
430
|
|
|
374
|
-
const res = await fetch(url(
|
|
375
|
-
method:
|
|
376
|
-
headers: {
|
|
377
|
-
body: JSON.stringify({ requestId:
|
|
431
|
+
const res = await fetch(url("secret"), {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
434
|
+
body: JSON.stringify({ requestId: "nonexistent", value: "test" }),
|
|
378
435
|
});
|
|
379
436
|
|
|
380
437
|
expect(res.status).toBe(404);
|
|
@@ -382,13 +439,13 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
382
439
|
await stopServer();
|
|
383
440
|
});
|
|
384
441
|
|
|
385
|
-
test(
|
|
442
|
+
test("returns 400 for missing requestId", async () => {
|
|
386
443
|
await startServer(() => makeIdleSession());
|
|
387
444
|
|
|
388
|
-
const res = await fetch(url(
|
|
389
|
-
method:
|
|
390
|
-
headers: {
|
|
391
|
-
body: JSON.stringify({ value:
|
|
445
|
+
const res = await fetch(url("secret"), {
|
|
446
|
+
method: "POST",
|
|
447
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
448
|
+
body: JSON.stringify({ value: "test" }),
|
|
392
449
|
});
|
|
393
450
|
|
|
394
451
|
expect(res.status).toBe(400);
|
|
@@ -396,13 +453,17 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
396
453
|
await stopServer();
|
|
397
454
|
});
|
|
398
455
|
|
|
399
|
-
test(
|
|
456
|
+
test("returns 400 for invalid delivery", async () => {
|
|
400
457
|
await startServer(() => makeIdleSession());
|
|
401
458
|
|
|
402
|
-
const res = await fetch(url(
|
|
403
|
-
method:
|
|
404
|
-
headers: {
|
|
405
|
-
body: JSON.stringify({
|
|
459
|
+
const res = await fetch(url("secret"), {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
requestId: "req-1",
|
|
464
|
+
value: "test",
|
|
465
|
+
delivery: "invalid",
|
|
466
|
+
}),
|
|
406
467
|
});
|
|
407
468
|
|
|
408
469
|
expect(res.status).toBe(400);
|
|
@@ -413,14 +474,19 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
413
474
|
|
|
414
475
|
// ── POST /v1/trust-rules ─────────────────────────────────────────────
|
|
415
476
|
|
|
416
|
-
describe(
|
|
417
|
-
test(
|
|
477
|
+
describe("POST /v1/trust-rules", () => {
|
|
478
|
+
test("returns 404 for unknown requestId", async () => {
|
|
418
479
|
await startServer(() => makeIdleSession());
|
|
419
480
|
|
|
420
|
-
const res = await fetch(url(
|
|
421
|
-
method:
|
|
422
|
-
headers: {
|
|
423
|
-
body: JSON.stringify({
|
|
481
|
+
const res = await fetch(url("trust-rules"), {
|
|
482
|
+
method: "POST",
|
|
483
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
484
|
+
body: JSON.stringify({
|
|
485
|
+
requestId: "nonexistent",
|
|
486
|
+
pattern: "ls",
|
|
487
|
+
scope: "session",
|
|
488
|
+
decision: "allow",
|
|
489
|
+
}),
|
|
424
490
|
});
|
|
425
491
|
|
|
426
492
|
expect(res.status).toBe(404);
|
|
@@ -428,13 +494,17 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
428
494
|
await stopServer();
|
|
429
495
|
});
|
|
430
496
|
|
|
431
|
-
test(
|
|
497
|
+
test("returns 400 for missing requestId", async () => {
|
|
432
498
|
await startServer(() => makeIdleSession());
|
|
433
499
|
|
|
434
|
-
const res = await fetch(url(
|
|
435
|
-
method:
|
|
436
|
-
headers: {
|
|
437
|
-
body: JSON.stringify({
|
|
500
|
+
const res = await fetch(url("trust-rules"), {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
503
|
+
body: JSON.stringify({
|
|
504
|
+
pattern: "ls",
|
|
505
|
+
scope: "session",
|
|
506
|
+
decision: "allow",
|
|
507
|
+
}),
|
|
438
508
|
});
|
|
439
509
|
|
|
440
510
|
expect(res.status).toBe(400);
|
|
@@ -442,13 +512,17 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
442
512
|
await stopServer();
|
|
443
513
|
});
|
|
444
514
|
|
|
445
|
-
test(
|
|
515
|
+
test("returns 400 for missing pattern", async () => {
|
|
446
516
|
await startServer(() => makeIdleSession());
|
|
447
517
|
|
|
448
|
-
const res = await fetch(url(
|
|
449
|
-
method:
|
|
450
|
-
headers: {
|
|
451
|
-
body: JSON.stringify({
|
|
518
|
+
const res = await fetch(url("trust-rules"), {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
521
|
+
body: JSON.stringify({
|
|
522
|
+
requestId: "req-1",
|
|
523
|
+
scope: "session",
|
|
524
|
+
decision: "allow",
|
|
525
|
+
}),
|
|
452
526
|
});
|
|
453
527
|
|
|
454
528
|
expect(res.status).toBe(400);
|
|
@@ -456,13 +530,17 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
456
530
|
await stopServer();
|
|
457
531
|
});
|
|
458
532
|
|
|
459
|
-
test(
|
|
533
|
+
test("returns 400 for missing scope", async () => {
|
|
460
534
|
await startServer(() => makeIdleSession());
|
|
461
535
|
|
|
462
|
-
const res = await fetch(url(
|
|
463
|
-
method:
|
|
464
|
-
headers: {
|
|
465
|
-
body: JSON.stringify({
|
|
536
|
+
const res = await fetch(url("trust-rules"), {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
539
|
+
body: JSON.stringify({
|
|
540
|
+
requestId: "req-1",
|
|
541
|
+
pattern: "ls",
|
|
542
|
+
decision: "allow",
|
|
543
|
+
}),
|
|
466
544
|
});
|
|
467
545
|
|
|
468
546
|
expect(res.status).toBe(400);
|
|
@@ -470,13 +548,18 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
470
548
|
await stopServer();
|
|
471
549
|
});
|
|
472
550
|
|
|
473
|
-
test(
|
|
551
|
+
test("returns 400 for invalid decision", async () => {
|
|
474
552
|
await startServer(() => makeIdleSession());
|
|
475
553
|
|
|
476
|
-
const res = await fetch(url(
|
|
477
|
-
method:
|
|
478
|
-
headers: {
|
|
479
|
-
body: JSON.stringify({
|
|
554
|
+
const res = await fetch(url("trust-rules"), {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
requestId: "req-1",
|
|
559
|
+
pattern: "ls",
|
|
560
|
+
scope: "session",
|
|
561
|
+
decision: "maybe",
|
|
562
|
+
}),
|
|
480
563
|
});
|
|
481
564
|
|
|
482
565
|
expect(res.status).toBe(400);
|
|
@@ -484,21 +567,26 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
484
567
|
await stopServer();
|
|
485
568
|
});
|
|
486
569
|
|
|
487
|
-
test(
|
|
570
|
+
test("returns 409 when no confirmation details available", async () => {
|
|
488
571
|
const session = makeIdleSession();
|
|
489
572
|
await startServer(() => session);
|
|
490
573
|
|
|
491
574
|
// Register without confirmationDetails
|
|
492
|
-
pendingInteractions.register(
|
|
575
|
+
pendingInteractions.register("req-no-details", {
|
|
493
576
|
session,
|
|
494
|
-
conversationId:
|
|
495
|
-
kind:
|
|
577
|
+
conversationId: "conv-1",
|
|
578
|
+
kind: "secret",
|
|
496
579
|
});
|
|
497
580
|
|
|
498
|
-
const res = await fetch(url(
|
|
499
|
-
method:
|
|
500
|
-
headers: {
|
|
501
|
-
body: JSON.stringify({
|
|
581
|
+
const res = await fetch(url("trust-rules"), {
|
|
582
|
+
method: "POST",
|
|
583
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
584
|
+
body: JSON.stringify({
|
|
585
|
+
requestId: "req-no-details",
|
|
586
|
+
pattern: "ls",
|
|
587
|
+
scope: "session",
|
|
588
|
+
decision: "allow",
|
|
589
|
+
}),
|
|
502
590
|
});
|
|
503
591
|
|
|
504
592
|
expect(res.status).toBe(409);
|
|
@@ -506,28 +594,35 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
506
594
|
await stopServer();
|
|
507
595
|
});
|
|
508
596
|
|
|
509
|
-
test(
|
|
597
|
+
test("returns 403 when persistent decisions are not allowed", async () => {
|
|
510
598
|
const session = makeIdleSession();
|
|
511
599
|
await startServer(() => session);
|
|
512
600
|
|
|
513
|
-
pendingInteractions.register(
|
|
601
|
+
pendingInteractions.register("req-no-persist", {
|
|
514
602
|
session,
|
|
515
|
-
conversationId:
|
|
516
|
-
kind:
|
|
603
|
+
conversationId: "conv-1",
|
|
604
|
+
kind: "confirmation",
|
|
517
605
|
confirmationDetails: {
|
|
518
|
-
toolName:
|
|
519
|
-
input: { command:
|
|
520
|
-
riskLevel:
|
|
521
|
-
allowlistOptions: [
|
|
522
|
-
|
|
606
|
+
toolName: "shell_command",
|
|
607
|
+
input: { command: "rm -rf" },
|
|
608
|
+
riskLevel: "high",
|
|
609
|
+
allowlistOptions: [
|
|
610
|
+
{ label: "Allow", description: "test", pattern: "rm" },
|
|
611
|
+
],
|
|
612
|
+
scopeOptions: [{ label: "Session", scope: "session" }],
|
|
523
613
|
persistentDecisionsAllowed: false,
|
|
524
614
|
},
|
|
525
615
|
});
|
|
526
616
|
|
|
527
|
-
const res = await fetch(url(
|
|
528
|
-
method:
|
|
529
|
-
headers: {
|
|
530
|
-
body: JSON.stringify({
|
|
617
|
+
const res = await fetch(url("trust-rules"), {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
620
|
+
body: JSON.stringify({
|
|
621
|
+
requestId: "req-no-persist",
|
|
622
|
+
pattern: "rm",
|
|
623
|
+
scope: "session",
|
|
624
|
+
decision: "allow",
|
|
625
|
+
}),
|
|
531
626
|
});
|
|
532
627
|
|
|
533
628
|
expect(res.status).toBe(403);
|
|
@@ -535,93 +630,118 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
535
630
|
await stopServer();
|
|
536
631
|
});
|
|
537
632
|
|
|
538
|
-
test(
|
|
633
|
+
test("returns 403 when pattern does not match allowlist", async () => {
|
|
539
634
|
const session = makeIdleSession();
|
|
540
635
|
await startServer(() => session);
|
|
541
636
|
|
|
542
|
-
pendingInteractions.register(
|
|
637
|
+
pendingInteractions.register("req-bad-pattern", {
|
|
543
638
|
session,
|
|
544
|
-
conversationId:
|
|
545
|
-
kind:
|
|
639
|
+
conversationId: "conv-1",
|
|
640
|
+
kind: "confirmation",
|
|
546
641
|
confirmationDetails: {
|
|
547
|
-
toolName:
|
|
548
|
-
input: { command:
|
|
549
|
-
riskLevel:
|
|
550
|
-
allowlistOptions: [
|
|
551
|
-
|
|
642
|
+
toolName: "shell_command",
|
|
643
|
+
input: { command: "ls" },
|
|
644
|
+
riskLevel: "medium",
|
|
645
|
+
allowlistOptions: [
|
|
646
|
+
{ label: "Allow ls", description: "test", pattern: "ls" },
|
|
647
|
+
],
|
|
648
|
+
scopeOptions: [{ label: "Session", scope: "session" }],
|
|
552
649
|
},
|
|
553
650
|
});
|
|
554
651
|
|
|
555
|
-
const res = await fetch(url(
|
|
556
|
-
method:
|
|
557
|
-
headers: {
|
|
558
|
-
body: JSON.stringify({
|
|
652
|
+
const res = await fetch(url("trust-rules"), {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
655
|
+
body: JSON.stringify({
|
|
656
|
+
requestId: "req-bad-pattern",
|
|
657
|
+
pattern: "rm",
|
|
658
|
+
scope: "session",
|
|
659
|
+
decision: "allow",
|
|
660
|
+
}),
|
|
559
661
|
});
|
|
560
662
|
|
|
561
663
|
expect(res.status).toBe(403);
|
|
562
|
-
const body = await res.json() as {
|
|
563
|
-
|
|
664
|
+
const body = (await res.json()) as {
|
|
665
|
+
error: { message: string; code?: string };
|
|
666
|
+
};
|
|
667
|
+
expect(body.error.message).toContain("pattern");
|
|
564
668
|
|
|
565
669
|
await stopServer();
|
|
566
670
|
});
|
|
567
671
|
|
|
568
|
-
test(
|
|
672
|
+
test("returns 403 when scope does not match scope options", async () => {
|
|
569
673
|
const session = makeIdleSession();
|
|
570
674
|
await startServer(() => session);
|
|
571
675
|
|
|
572
|
-
pendingInteractions.register(
|
|
676
|
+
pendingInteractions.register("req-bad-scope", {
|
|
573
677
|
session,
|
|
574
|
-
conversationId:
|
|
575
|
-
kind:
|
|
678
|
+
conversationId: "conv-1",
|
|
679
|
+
kind: "confirmation",
|
|
576
680
|
confirmationDetails: {
|
|
577
|
-
toolName:
|
|
578
|
-
input: { command:
|
|
579
|
-
riskLevel:
|
|
580
|
-
allowlistOptions: [
|
|
581
|
-
|
|
681
|
+
toolName: "shell_command",
|
|
682
|
+
input: { command: "ls" },
|
|
683
|
+
riskLevel: "medium",
|
|
684
|
+
allowlistOptions: [
|
|
685
|
+
{ label: "Allow ls", description: "test", pattern: "ls" },
|
|
686
|
+
],
|
|
687
|
+
scopeOptions: [{ label: "Session", scope: "session" }],
|
|
582
688
|
},
|
|
583
689
|
});
|
|
584
690
|
|
|
585
|
-
const res = await fetch(url(
|
|
586
|
-
method:
|
|
587
|
-
headers: {
|
|
588
|
-
body: JSON.stringify({
|
|
691
|
+
const res = await fetch(url("trust-rules"), {
|
|
692
|
+
method: "POST",
|
|
693
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
694
|
+
body: JSON.stringify({
|
|
695
|
+
requestId: "req-bad-scope",
|
|
696
|
+
pattern: "ls",
|
|
697
|
+
scope: "global",
|
|
698
|
+
decision: "allow",
|
|
699
|
+
}),
|
|
589
700
|
});
|
|
590
701
|
|
|
591
702
|
expect(res.status).toBe(403);
|
|
592
|
-
const body = await res.json() as {
|
|
593
|
-
|
|
703
|
+
const body = (await res.json()) as {
|
|
704
|
+
error: { message: string; code?: string };
|
|
705
|
+
};
|
|
706
|
+
expect(body.error.message).toContain("scope");
|
|
594
707
|
|
|
595
708
|
await stopServer();
|
|
596
709
|
});
|
|
597
710
|
|
|
598
|
-
test(
|
|
711
|
+
test("does not remove the pending interaction after adding trust rule", async () => {
|
|
599
712
|
const session = makeIdleSession();
|
|
600
713
|
await startServer(() => session);
|
|
601
714
|
|
|
602
|
-
pendingInteractions.register(
|
|
715
|
+
pendingInteractions.register("req-keep", {
|
|
603
716
|
session,
|
|
604
|
-
conversationId:
|
|
605
|
-
kind:
|
|
717
|
+
conversationId: "conv-1",
|
|
718
|
+
kind: "confirmation",
|
|
606
719
|
confirmationDetails: {
|
|
607
|
-
toolName:
|
|
608
|
-
input: { command:
|
|
609
|
-
riskLevel:
|
|
610
|
-
allowlistOptions: [
|
|
611
|
-
|
|
720
|
+
toolName: "shell_command",
|
|
721
|
+
input: { command: "ls" },
|
|
722
|
+
riskLevel: "medium",
|
|
723
|
+
allowlistOptions: [
|
|
724
|
+
{ label: "Allow ls", description: "test", pattern: "ls" },
|
|
725
|
+
],
|
|
726
|
+
scopeOptions: [{ label: "Session", scope: "session" }],
|
|
612
727
|
},
|
|
613
728
|
});
|
|
614
729
|
|
|
615
|
-
const res = await fetch(url(
|
|
616
|
-
method:
|
|
617
|
-
headers: {
|
|
618
|
-
body: JSON.stringify({
|
|
730
|
+
const res = await fetch(url("trust-rules"), {
|
|
731
|
+
method: "POST",
|
|
732
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
733
|
+
body: JSON.stringify({
|
|
734
|
+
requestId: "req-keep",
|
|
735
|
+
pattern: "ls",
|
|
736
|
+
scope: "session",
|
|
737
|
+
decision: "allow",
|
|
738
|
+
}),
|
|
619
739
|
});
|
|
620
740
|
|
|
621
741
|
expect(res.status).toBe(200);
|
|
622
742
|
|
|
623
743
|
// Interaction should still be present (not consumed)
|
|
624
|
-
expect(pendingInteractions.get(
|
|
744
|
+
expect(pendingInteractions.get("req-keep")).toBeDefined();
|
|
625
745
|
|
|
626
746
|
await stopServer();
|
|
627
747
|
});
|
|
@@ -629,13 +749,14 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
629
749
|
|
|
630
750
|
// ── Hub publisher integration ────────────────────────────────────────
|
|
631
751
|
|
|
632
|
-
describe(
|
|
633
|
-
test(
|
|
634
|
-
const confirmReceived: Array<{ requestId: string; decision: string }> =
|
|
752
|
+
describe("hub publisher registers pending interactions", () => {
|
|
753
|
+
test("confirmation_request events register pending interactions", async () => {
|
|
754
|
+
const confirmReceived: Array<{ requestId: string; decision: string }> =
|
|
755
|
+
[];
|
|
635
756
|
|
|
636
757
|
const session = makeConfirmationEmittingSession({
|
|
637
|
-
confirmRequestId:
|
|
638
|
-
toolName:
|
|
758
|
+
confirmRequestId: "auto-req-1",
|
|
759
|
+
toolName: "shell_command",
|
|
639
760
|
onConfirmation: (reqId, dec) => {
|
|
640
761
|
confirmReceived.push({ requestId: reqId, decision: dec });
|
|
641
762
|
},
|
|
@@ -644,10 +765,15 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
644
765
|
await startServer(() => session);
|
|
645
766
|
|
|
646
767
|
// Send a message that triggers a confirmation_request
|
|
647
|
-
const res = await fetch(url(
|
|
648
|
-
method:
|
|
649
|
-
headers: {
|
|
650
|
-
body: JSON.stringify({
|
|
768
|
+
const res = await fetch(url("messages"), {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
771
|
+
body: JSON.stringify({
|
|
772
|
+
conversationKey: "conv-auto",
|
|
773
|
+
content: "Run ls",
|
|
774
|
+
sourceChannel: "vellum",
|
|
775
|
+
interface: "macos",
|
|
776
|
+
}),
|
|
651
777
|
});
|
|
652
778
|
expect(res.status).toBe(202);
|
|
653
779
|
|
|
@@ -655,22 +781,22 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
655
781
|
await new Promise((r) => setTimeout(r, 100));
|
|
656
782
|
|
|
657
783
|
// The pending interaction should have been auto-registered
|
|
658
|
-
const interaction = pendingInteractions.get(
|
|
784
|
+
const interaction = pendingInteractions.get("auto-req-1");
|
|
659
785
|
expect(interaction).toBeDefined();
|
|
660
|
-
expect(interaction!.kind).toBe(
|
|
661
|
-
expect(interaction!.confirmationDetails?.toolName).toBe(
|
|
786
|
+
expect(interaction!.kind).toBe("confirmation");
|
|
787
|
+
expect(interaction!.confirmationDetails?.toolName).toBe("shell_command");
|
|
662
788
|
|
|
663
789
|
// Now resolve it via the confirm endpoint
|
|
664
|
-
const confirmRes = await fetch(url(
|
|
665
|
-
method:
|
|
666
|
-
headers: {
|
|
667
|
-
body: JSON.stringify({ requestId:
|
|
790
|
+
const confirmRes = await fetch(url("confirm"), {
|
|
791
|
+
method: "POST",
|
|
792
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
793
|
+
body: JSON.stringify({ requestId: "auto-req-1", decision: "allow" }),
|
|
668
794
|
});
|
|
669
795
|
expect(confirmRes.status).toBe(200);
|
|
670
796
|
|
|
671
797
|
expect(confirmReceived).toHaveLength(1);
|
|
672
|
-
expect(confirmReceived[0].requestId).toBe(
|
|
673
|
-
expect(confirmReceived[0].decision).toBe(
|
|
798
|
+
expect(confirmReceived[0].requestId).toBe("auto-req-1");
|
|
799
|
+
expect(confirmReceived[0].decision).toBe("allow");
|
|
674
800
|
|
|
675
801
|
await stopServer();
|
|
676
802
|
});
|
|
@@ -678,36 +804,39 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
678
804
|
|
|
679
805
|
// ── getByConversation ────────────────────────────────────────────────
|
|
680
806
|
|
|
681
|
-
describe(
|
|
682
|
-
test(
|
|
807
|
+
describe("getByConversation", () => {
|
|
808
|
+
test("returns all pending interactions for a conversation", async () => {
|
|
683
809
|
const session = makeIdleSession();
|
|
684
810
|
await startServer(() => session);
|
|
685
811
|
|
|
686
|
-
pendingInteractions.register(
|
|
812
|
+
pendingInteractions.register("req-a", {
|
|
687
813
|
session,
|
|
688
|
-
conversationId:
|
|
689
|
-
kind:
|
|
814
|
+
conversationId: "conv-x",
|
|
815
|
+
kind: "confirmation",
|
|
690
816
|
});
|
|
691
|
-
pendingInteractions.register(
|
|
817
|
+
pendingInteractions.register("req-b", {
|
|
692
818
|
session,
|
|
693
|
-
conversationId:
|
|
694
|
-
kind:
|
|
819
|
+
conversationId: "conv-x",
|
|
820
|
+
kind: "secret",
|
|
695
821
|
});
|
|
696
|
-
pendingInteractions.register(
|
|
822
|
+
pendingInteractions.register("req-c", {
|
|
697
823
|
session,
|
|
698
|
-
conversationId:
|
|
699
|
-
kind:
|
|
824
|
+
conversationId: "conv-y",
|
|
825
|
+
kind: "confirmation",
|
|
700
826
|
});
|
|
701
827
|
|
|
702
|
-
const results = pendingInteractions.getByConversation(
|
|
828
|
+
const results = pendingInteractions.getByConversation("conv-x");
|
|
703
829
|
expect(results).toHaveLength(2);
|
|
704
|
-
expect(results.map((r) => r.requestId).sort()).toEqual([
|
|
830
|
+
expect(results.map((r) => r.requestId).sort()).toEqual([
|
|
831
|
+
"req-a",
|
|
832
|
+
"req-b",
|
|
833
|
+
]);
|
|
705
834
|
|
|
706
|
-
const resultsY = pendingInteractions.getByConversation(
|
|
835
|
+
const resultsY = pendingInteractions.getByConversation("conv-y");
|
|
707
836
|
expect(resultsY).toHaveLength(1);
|
|
708
|
-
expect(resultsY[0].requestId).toBe(
|
|
837
|
+
expect(resultsY[0].requestId).toBe("req-c");
|
|
709
838
|
|
|
710
|
-
const resultsZ = pendingInteractions.getByConversation(
|
|
839
|
+
const resultsZ = pendingInteractions.getByConversation("conv-z");
|
|
711
840
|
expect(resultsZ).toHaveLength(0);
|
|
712
841
|
|
|
713
842
|
await stopServer();
|