@vellumai/assistant 0.5.3 → 0.5.5
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 +18 -27
- package/docs/architecture/memory.md +105 -0
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +51 -2
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +24 -30
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/secret-routes.ts +5 -1
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +22 -20
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import type { VellumPlatformClient } from "../platform/client.js";
|
|
3
4
|
import { BackendError, VellumError } from "../util/errors.js";
|
|
4
5
|
import {
|
|
5
6
|
CredentialRequiredError,
|
|
@@ -7,40 +8,53 @@ import {
|
|
|
7
8
|
ProviderUnreachableError,
|
|
8
9
|
} from "./platform-connection.js";
|
|
9
10
|
|
|
11
|
+
function makeMockClient(
|
|
12
|
+
fetchImpl?: typeof globalThis.fetch,
|
|
13
|
+
): VellumPlatformClient {
|
|
14
|
+
const mockFetchFn =
|
|
15
|
+
fetchImpl ??
|
|
16
|
+
(mock(async () => {
|
|
17
|
+
return new Response(
|
|
18
|
+
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
19
|
+
{ status: 200 },
|
|
20
|
+
);
|
|
21
|
+
}) as unknown as typeof globalThis.fetch);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
baseUrl: "https://platform.example.com",
|
|
25
|
+
assistantApiKey: "test-api-key",
|
|
26
|
+
platformAssistantId: "asst-abc",
|
|
27
|
+
fetch: mock(async (path: string, init?: RequestInit) => {
|
|
28
|
+
const url = `https://platform.example.com${path}`;
|
|
29
|
+
const headers = new Headers(init?.headers);
|
|
30
|
+
headers.set("Authorization", "Api-Key test-api-key");
|
|
31
|
+
return mockFetchFn(url, { ...init, headers });
|
|
32
|
+
}),
|
|
33
|
+
} as unknown as VellumPlatformClient;
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
const DEFAULT_OPTIONS = {
|
|
11
37
|
id: "conn-1",
|
|
12
38
|
providerKey: "integration:google",
|
|
13
39
|
externalId: "ext-123",
|
|
14
40
|
accountInfo: "user@example.com",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
apiKey: "test-api-key",
|
|
41
|
+
client: makeMockClient(),
|
|
42
|
+
connectionId: "platform-conn-123",
|
|
18
43
|
};
|
|
19
44
|
|
|
20
45
|
describe("PlatformOAuthConnection", () => {
|
|
21
|
-
let originalFetch: typeof globalThis.fetch;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
originalFetch = globalThis.fetch;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
globalThis.fetch = originalFetch;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
46
|
test("successful proxied request", async () => {
|
|
32
47
|
const upstreamBody = { messages: [{ id: "msg-1", snippet: "Hello" }] };
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
async (url: string | URL | Request, init?: RequestInit) => {
|
|
36
|
-
expect(url).toBe(
|
|
37
|
-
"https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/
|
|
49
|
+
const client = makeMockClient(
|
|
50
|
+
mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
51
|
+
expect(String(url)).toBe(
|
|
52
|
+
"https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/platform-conn-123/",
|
|
38
53
|
);
|
|
39
54
|
expect(init?.method).toBe("POST");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
55
|
+
const headers = new Headers(init?.headers);
|
|
56
|
+
expect(headers.get("Authorization")).toBe("Api-Key test-api-key");
|
|
57
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
44
58
|
|
|
45
59
|
const parsed = JSON.parse(init?.body as string);
|
|
46
60
|
expect(parsed).toEqual({
|
|
@@ -61,10 +75,13 @@ describe("PlatformOAuthConnection", () => {
|
|
|
61
75
|
}),
|
|
62
76
|
{ status: 200 },
|
|
63
77
|
);
|
|
64
|
-
},
|
|
65
|
-
)
|
|
78
|
+
}) as unknown as typeof globalThis.fetch,
|
|
79
|
+
);
|
|
66
80
|
|
|
67
|
-
const conn = new PlatformOAuthConnection(
|
|
81
|
+
const conn = new PlatformOAuthConnection({
|
|
82
|
+
...DEFAULT_OPTIONS,
|
|
83
|
+
client,
|
|
84
|
+
});
|
|
68
85
|
const result = await conn.request({
|
|
69
86
|
method: "GET",
|
|
70
87
|
path: "/gmail/v1/users/me/messages",
|
|
@@ -77,8 +94,8 @@ describe("PlatformOAuthConnection", () => {
|
|
|
77
94
|
});
|
|
78
95
|
|
|
79
96
|
test("forwards baseUrl when provided", async () => {
|
|
80
|
-
|
|
81
|
-
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
97
|
+
const client = makeMockClient(
|
|
98
|
+
mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
82
99
|
const parsed = JSON.parse(init?.body as string);
|
|
83
100
|
expect(parsed.request.baseUrl).toBe(
|
|
84
101
|
"https://www.googleapis.com/calendar/v3",
|
|
@@ -88,10 +105,10 @@ describe("PlatformOAuthConnection", () => {
|
|
|
88
105
|
JSON.stringify({ status: 200, headers: {}, body: {} }),
|
|
89
106
|
{ status: 200 },
|
|
90
107
|
);
|
|
91
|
-
},
|
|
92
|
-
)
|
|
108
|
+
}) as unknown as typeof globalThis.fetch,
|
|
109
|
+
);
|
|
93
110
|
|
|
94
|
-
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
111
|
+
const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
|
|
95
112
|
await conn.request({
|
|
96
113
|
method: "GET",
|
|
97
114
|
path: "/calendars/primary/events",
|
|
@@ -100,8 +117,8 @@ describe("PlatformOAuthConnection", () => {
|
|
|
100
117
|
});
|
|
101
118
|
|
|
102
119
|
test("omits baseUrl from envelope when not provided", async () => {
|
|
103
|
-
|
|
104
|
-
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
120
|
+
const client = makeMockClient(
|
|
121
|
+
mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
105
122
|
const parsed = JSON.parse(init?.body as string);
|
|
106
123
|
expect("baseUrl" in parsed.request).toBe(false);
|
|
107
124
|
|
|
@@ -109,10 +126,10 @@ describe("PlatformOAuthConnection", () => {
|
|
|
109
126
|
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
110
127
|
{ status: 200 },
|
|
111
128
|
);
|
|
112
|
-
},
|
|
113
|
-
)
|
|
129
|
+
}) as unknown as typeof globalThis.fetch,
|
|
130
|
+
);
|
|
114
131
|
|
|
115
|
-
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
132
|
+
const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
|
|
116
133
|
await conn.request({ method: "GET", path: "/some/path" });
|
|
117
134
|
});
|
|
118
135
|
|
|
@@ -127,22 +144,26 @@ describe("PlatformOAuthConnection", () => {
|
|
|
127
144
|
});
|
|
128
145
|
|
|
129
146
|
test("424 response throws CredentialRequiredError", async () => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
const client = makeMockClient(
|
|
148
|
+
mock(
|
|
149
|
+
async () => new Response("", { status: 424 }),
|
|
150
|
+
) as unknown as typeof globalThis.fetch,
|
|
151
|
+
);
|
|
133
152
|
|
|
134
|
-
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
153
|
+
const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
|
|
135
154
|
await expect(
|
|
136
155
|
conn.request({ method: "GET", path: "/test" }),
|
|
137
156
|
).rejects.toThrow(CredentialRequiredError);
|
|
138
157
|
});
|
|
139
158
|
|
|
140
159
|
test("502 response throws ProviderUnreachableError", async () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
const client = makeMockClient(
|
|
161
|
+
mock(
|
|
162
|
+
async () => new Response("", { status: 502 }),
|
|
163
|
+
) as unknown as typeof globalThis.fetch,
|
|
164
|
+
);
|
|
144
165
|
|
|
145
|
-
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
166
|
+
const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
|
|
146
167
|
await expect(
|
|
147
168
|
conn.request({ method: "GET", path: "/test" }),
|
|
148
169
|
).rejects.toThrow(ProviderUnreachableError);
|
|
@@ -155,36 +176,24 @@ describe("PlatformOAuthConnection", () => {
|
|
|
155
176
|
);
|
|
156
177
|
});
|
|
157
178
|
|
|
158
|
-
test("
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
...DEFAULT_OPTIONS,
|
|
171
|
-
platformBaseUrl: "https://platform.example.com/",
|
|
172
|
-
});
|
|
173
|
-
await conn.request({ method: "GET", path: "/test" });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("strips integration: prefix from providerKey for slug", async () => {
|
|
177
|
-
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
178
|
-
expect(String(url)).toContain("/external-provider-proxy/slack/");
|
|
179
|
-
return new Response(
|
|
180
|
-
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
181
|
-
{ status: 200 },
|
|
182
|
-
);
|
|
183
|
-
}) as unknown as typeof globalThis.fetch;
|
|
179
|
+
test("uses connectionId in proxy URL regardless of providerKey format", async () => {
|
|
180
|
+
const client = makeMockClient(
|
|
181
|
+
mock(async (url: string | URL | Request) => {
|
|
182
|
+
expect(String(url)).toContain(
|
|
183
|
+
"/external-provider-proxy/slack-conn-456/",
|
|
184
|
+
);
|
|
185
|
+
return new Response(
|
|
186
|
+
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
187
|
+
{ status: 200 },
|
|
188
|
+
);
|
|
189
|
+
}) as unknown as typeof globalThis.fetch,
|
|
190
|
+
);
|
|
184
191
|
|
|
185
192
|
const conn = new PlatformOAuthConnection({
|
|
186
193
|
...DEFAULT_OPTIONS,
|
|
194
|
+
client,
|
|
187
195
|
providerKey: "integration:slack",
|
|
196
|
+
connectionId: "slack-conn-456",
|
|
188
197
|
});
|
|
189
198
|
await conn.request({ method: "GET", path: "/test" });
|
|
190
199
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VellumPlatformClient } from "../platform/client.js";
|
|
1
2
|
import { BackendError } from "../util/errors.js";
|
|
2
3
|
import type {
|
|
3
4
|
OAuthConnection,
|
|
@@ -24,9 +25,9 @@ export interface PlatformOAuthConnectionOptions {
|
|
|
24
25
|
providerKey: string;
|
|
25
26
|
externalId: string;
|
|
26
27
|
accountInfo: string | null;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
client: VellumPlatformClient;
|
|
29
|
+
/** Platform-side connection ID used in the proxy URL path. */
|
|
30
|
+
connectionId: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class PlatformOAuthConnection implements OAuthConnection {
|
|
@@ -35,18 +36,13 @@ export class PlatformOAuthConnection implements OAuthConnection {
|
|
|
35
36
|
readonly externalId: string;
|
|
36
37
|
readonly accountInfo: string | null;
|
|
37
38
|
|
|
38
|
-
private readonly
|
|
39
|
-
private readonly
|
|
40
|
-
private readonly apiKey: string;
|
|
39
|
+
private readonly client: VellumPlatformClient;
|
|
40
|
+
private readonly connectionId: string;
|
|
41
41
|
|
|
42
42
|
constructor(options: PlatformOAuthConnectionOptions) {
|
|
43
|
-
|
|
44
|
-
if (!options.platformBaseUrl) missing.push("platform base URL");
|
|
45
|
-
if (!options.apiKey) missing.push("assistant API key");
|
|
46
|
-
if (!options.assistantId) missing.push("assistant ID");
|
|
47
|
-
if (missing.length > 0) {
|
|
43
|
+
if (!options.connectionId) {
|
|
48
44
|
throw new BackendError(
|
|
49
|
-
`Platform-managed connection for "${options.providerKey}" cannot be created: missing
|
|
45
|
+
`Platform-managed connection for "${options.providerKey}" cannot be created: missing connection ID. ` +
|
|
50
46
|
`Log in to the Vellum platform or switch to using your own OAuth app.`,
|
|
51
47
|
);
|
|
52
48
|
}
|
|
@@ -55,14 +51,12 @@ export class PlatformOAuthConnection implements OAuthConnection {
|
|
|
55
51
|
this.providerKey = options.providerKey;
|
|
56
52
|
this.externalId = options.externalId;
|
|
57
53
|
this.accountInfo = options.accountInfo;
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
this.apiKey = options.apiKey;
|
|
54
|
+
this.client = options.client;
|
|
55
|
+
this.connectionId = options.connectionId;
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
async request(req: OAuthConnectionRequest): Promise<OAuthConnectionResponse> {
|
|
64
|
-
const
|
|
65
|
-
const proxyUrl = `${this.platformBaseUrl}/v1/assistants/${this.assistantId}/external-provider-proxy/${providerSlug}/`;
|
|
59
|
+
const proxyPath = `/v1/assistants/${this.client.platformAssistantId}/external-provider-proxy/${this.connectionId}/`;
|
|
66
60
|
|
|
67
61
|
const body: Record<string, unknown> = {
|
|
68
62
|
request: {
|
|
@@ -75,10 +69,9 @@ export class PlatformOAuthConnection implements OAuthConnection {
|
|
|
75
69
|
},
|
|
76
70
|
};
|
|
77
71
|
|
|
78
|
-
const response = await fetch(
|
|
72
|
+
const response = await this.client.fetch(proxyPath, {
|
|
79
73
|
method: "POST",
|
|
80
74
|
headers: {
|
|
81
|
-
Authorization: `Api-Key ${this.apiKey}`,
|
|
82
75
|
"Content-Type": "application/json",
|
|
83
76
|
},
|
|
84
77
|
body: JSON.stringify(body),
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the gateway's trust rule endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Provides CRUD operations over trust rules stored in the gateway,
|
|
5
|
+
* replacing direct filesystem access to trust.json when the assistant
|
|
6
|
+
* runs in a containerized environment.
|
|
7
|
+
*
|
|
8
|
+
* Both async and synchronous variants are exported. The sync variants
|
|
9
|
+
* use `Bun.spawnSync` + `curl` to make blocking HTTP calls — acceptable
|
|
10
|
+
* for user-initiated write operations that are infrequent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TrustRule } from "@vellumai/ces-contracts";
|
|
14
|
+
|
|
15
|
+
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
16
|
+
import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
|
|
17
|
+
import { getLogger } from "../util/logger.js";
|
|
18
|
+
|
|
19
|
+
const log = getLogger("trust-client");
|
|
20
|
+
|
|
21
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Result types (not in ces-contracts — local to the client)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface AcceptStarterBundleResult {
|
|
28
|
+
accepted: boolean;
|
|
29
|
+
rulesAdded: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the gateway base URL for trust rule requests.
|
|
38
|
+
*
|
|
39
|
+
* Prefers the `GATEWAY_INTERNAL_URL` env var (set in Docker environments
|
|
40
|
+
* where the gateway runs in a separate container), falling back to the
|
|
41
|
+
* existing `getGatewayInternalBaseUrl()` helper for local deployments.
|
|
42
|
+
*/
|
|
43
|
+
function getBaseUrl(): string {
|
|
44
|
+
return process.env.GATEWAY_INTERNAL_URL ?? getGatewayInternalBaseUrl();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function authHeaders(): Record<string, string> {
|
|
48
|
+
return {
|
|
49
|
+
Authorization: `Bearer ${mintDaemonDeliveryToken()}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute a fetch request with standard timeout and error handling.
|
|
56
|
+
* Throws a descriptive error on non-OK responses or network failures.
|
|
57
|
+
*/
|
|
58
|
+
async function request<T>(
|
|
59
|
+
method: string,
|
|
60
|
+
path: string,
|
|
61
|
+
body?: unknown,
|
|
62
|
+
): Promise<T> {
|
|
63
|
+
const url = `${getBaseUrl()}${path}`;
|
|
64
|
+
const options: RequestInit = {
|
|
65
|
+
method,
|
|
66
|
+
headers: authHeaders(),
|
|
67
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
68
|
+
};
|
|
69
|
+
if (body !== undefined) {
|
|
70
|
+
options.body = JSON.stringify(body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let response: Response;
|
|
74
|
+
try {
|
|
75
|
+
response = await fetch(url, options);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log.error({ err, method, path }, "Trust rule request failed (network)");
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Trust rule request failed: ${method} ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
85
|
+
log.error(
|
|
86
|
+
{ status: response.status, body: text, method, path },
|
|
87
|
+
"Trust rule request failed",
|
|
88
|
+
);
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Trust rule request failed (${response.status}): ${method} ${path}: ${text}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (await response.json()) as T;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Synchronous HTTP request via `Bun.spawnSync` + `curl`.
|
|
99
|
+
*
|
|
100
|
+
* Used by the gateway trust store adapter for write operations that must
|
|
101
|
+
* return synchronously to satisfy the `TrustStoreBackend` interface.
|
|
102
|
+
* Write operations are user-initiated and infrequent, so blocking is acceptable.
|
|
103
|
+
*/
|
|
104
|
+
function requestSync<T>(method: string, path: string, body?: unknown): T {
|
|
105
|
+
const url = `${getBaseUrl()}${path}`;
|
|
106
|
+
const headers = authHeaders();
|
|
107
|
+
const args: string[] = [
|
|
108
|
+
"curl",
|
|
109
|
+
"-s",
|
|
110
|
+
"-S",
|
|
111
|
+
"-X",
|
|
112
|
+
method,
|
|
113
|
+
"--max-time",
|
|
114
|
+
String(Math.ceil(REQUEST_TIMEOUT_MS / 1000)),
|
|
115
|
+
"-H",
|
|
116
|
+
`Authorization: ${headers.Authorization}`,
|
|
117
|
+
"-H",
|
|
118
|
+
"Content-Type: application/json",
|
|
119
|
+
"-w",
|
|
120
|
+
"\n%{http_code}",
|
|
121
|
+
];
|
|
122
|
+
if (body !== undefined) {
|
|
123
|
+
args.push("-d", JSON.stringify(body));
|
|
124
|
+
}
|
|
125
|
+
args.push(url);
|
|
126
|
+
|
|
127
|
+
const proc = Bun.spawnSync(args, {
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "pipe",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (proc.exitCode !== 0) {
|
|
133
|
+
const stderr = proc.stderr.toString().trim();
|
|
134
|
+
log.error(
|
|
135
|
+
{ exitCode: proc.exitCode, stderr, method, path },
|
|
136
|
+
"Trust rule sync request failed (curl)",
|
|
137
|
+
);
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Trust rule sync request failed: ${method} ${path}: curl exit ${proc.exitCode}: ${stderr}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const output = proc.stdout.toString().trim();
|
|
144
|
+
// curl -w "\n%{http_code}" appends the HTTP status code on the last line
|
|
145
|
+
const lastNewline = output.lastIndexOf("\n");
|
|
146
|
+
const responseBody = lastNewline >= 0 ? output.slice(0, lastNewline) : "";
|
|
147
|
+
const statusCode = parseInt(
|
|
148
|
+
lastNewline >= 0 ? output.slice(lastNewline + 1) : output,
|
|
149
|
+
10,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
153
|
+
log.error(
|
|
154
|
+
{ status: statusCode, body: responseBody, method, path },
|
|
155
|
+
"Trust rule sync request failed",
|
|
156
|
+
);
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Trust rule sync request failed (${statusCode}): ${method} ${path}: ${responseBody}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!responseBody) {
|
|
163
|
+
return {} as T;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(responseBody) as T;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
log.error(
|
|
170
|
+
{ err, responseBody, method, path },
|
|
171
|
+
"Failed to parse sync response JSON",
|
|
172
|
+
);
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Trust rule sync request: failed to parse response: ${method} ${path}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Public API
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/** Fetch all trust rules from the gateway. */
|
|
184
|
+
export async function getAllRules(): Promise<TrustRule[]> {
|
|
185
|
+
const data = await request<{ rules: TrustRule[] }>("GET", "/v1/trust-rules");
|
|
186
|
+
return data.rules;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Create a new trust rule. */
|
|
190
|
+
export async function addRule(params: {
|
|
191
|
+
tool: string;
|
|
192
|
+
pattern: string;
|
|
193
|
+
scope: string;
|
|
194
|
+
decision?: TrustRule["decision"];
|
|
195
|
+
priority?: number;
|
|
196
|
+
allowHighRisk?: boolean;
|
|
197
|
+
executionTarget?: string;
|
|
198
|
+
}): Promise<TrustRule> {
|
|
199
|
+
const data = await request<{ rule: TrustRule }>(
|
|
200
|
+
"POST",
|
|
201
|
+
"/v1/trust-rules",
|
|
202
|
+
params,
|
|
203
|
+
);
|
|
204
|
+
return data.rule;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Update an existing trust rule by ID. */
|
|
208
|
+
export async function updateRule(
|
|
209
|
+
id: string,
|
|
210
|
+
updates: {
|
|
211
|
+
tool?: string;
|
|
212
|
+
pattern?: string;
|
|
213
|
+
scope?: string;
|
|
214
|
+
decision?: TrustRule["decision"];
|
|
215
|
+
priority?: number;
|
|
216
|
+
allowHighRisk?: boolean;
|
|
217
|
+
executionTarget?: string;
|
|
218
|
+
},
|
|
219
|
+
): Promise<TrustRule> {
|
|
220
|
+
const data = await request<{ rule: TrustRule }>(
|
|
221
|
+
"PATCH",
|
|
222
|
+
`/v1/trust-rules/${encodeURIComponent(id)}`,
|
|
223
|
+
updates,
|
|
224
|
+
);
|
|
225
|
+
return data.rule;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Remove a trust rule by ID. Returns true if the rule was found and deleted. */
|
|
229
|
+
export async function removeRule(id: string): Promise<boolean> {
|
|
230
|
+
const data = await request<{ success: boolean }>(
|
|
231
|
+
"DELETE",
|
|
232
|
+
`/v1/trust-rules/${encodeURIComponent(id)}`,
|
|
233
|
+
);
|
|
234
|
+
return data.success;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Clear all user trust rules (default rules are preserved by the gateway). */
|
|
238
|
+
export async function clearRules(): Promise<void> {
|
|
239
|
+
await request<{ success: boolean }>("POST", "/v1/trust-rules/clear");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Find the highest-priority matching rule for a tool invocation.
|
|
244
|
+
*
|
|
245
|
+
* @param tool Tool name (e.g. "host_bash")
|
|
246
|
+
* @param candidates Command candidates to match against rule patterns
|
|
247
|
+
* @param scope Working directory scope
|
|
248
|
+
*/
|
|
249
|
+
export async function findMatchingRule(
|
|
250
|
+
tool: string,
|
|
251
|
+
candidates: string[],
|
|
252
|
+
scope: string,
|
|
253
|
+
): Promise<TrustRule | null> {
|
|
254
|
+
const params = new URLSearchParams({
|
|
255
|
+
tool,
|
|
256
|
+
commands: candidates.join(","),
|
|
257
|
+
scope,
|
|
258
|
+
});
|
|
259
|
+
const data = await request<{ rule: TrustRule | null }>(
|
|
260
|
+
"GET",
|
|
261
|
+
`/v1/trust-rules/match?${params.toString()}`,
|
|
262
|
+
);
|
|
263
|
+
return data.rule;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Accept the starter approval bundle, seeding common low-risk allow rules. */
|
|
267
|
+
export async function acceptStarterBundle(): Promise<AcceptStarterBundleResult> {
|
|
268
|
+
return request<AcceptStarterBundleResult>(
|
|
269
|
+
"POST",
|
|
270
|
+
"/v1/trust-rules/starter-bundle",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Synchronous API — used by the gateway trust store adapter
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/** Fetch all trust rules from the gateway (synchronous). */
|
|
279
|
+
export function getAllRulesSync(): TrustRule[] {
|
|
280
|
+
const data = requestSync<{ rules: TrustRule[] }>("GET", "/v1/trust-rules");
|
|
281
|
+
return data.rules;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Create a new trust rule (synchronous). */
|
|
285
|
+
export function addRuleSync(params: {
|
|
286
|
+
tool: string;
|
|
287
|
+
pattern: string;
|
|
288
|
+
scope: string;
|
|
289
|
+
decision?: TrustRule["decision"];
|
|
290
|
+
priority?: number;
|
|
291
|
+
allowHighRisk?: boolean;
|
|
292
|
+
executionTarget?: string;
|
|
293
|
+
}): TrustRule {
|
|
294
|
+
const data = requestSync<{ rule: TrustRule }>(
|
|
295
|
+
"POST",
|
|
296
|
+
"/v1/trust-rules",
|
|
297
|
+
params,
|
|
298
|
+
);
|
|
299
|
+
return data.rule;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Update an existing trust rule by ID (synchronous). */
|
|
303
|
+
export function updateRuleSync(
|
|
304
|
+
id: string,
|
|
305
|
+
updates: {
|
|
306
|
+
tool?: string;
|
|
307
|
+
pattern?: string;
|
|
308
|
+
scope?: string;
|
|
309
|
+
decision?: TrustRule["decision"];
|
|
310
|
+
priority?: number;
|
|
311
|
+
allowHighRisk?: boolean;
|
|
312
|
+
executionTarget?: string;
|
|
313
|
+
},
|
|
314
|
+
): TrustRule {
|
|
315
|
+
const data = requestSync<{ rule: TrustRule }>(
|
|
316
|
+
"PATCH",
|
|
317
|
+
`/v1/trust-rules/${encodeURIComponent(id)}`,
|
|
318
|
+
updates,
|
|
319
|
+
);
|
|
320
|
+
return data.rule;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Remove a trust rule by ID (synchronous). Returns true if deleted. */
|
|
324
|
+
export function removeRuleSync(id: string): boolean {
|
|
325
|
+
const data = requestSync<{ success: boolean }>(
|
|
326
|
+
"DELETE",
|
|
327
|
+
`/v1/trust-rules/${encodeURIComponent(id)}`,
|
|
328
|
+
);
|
|
329
|
+
return data.success;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Clear all user trust rules (synchronous). */
|
|
333
|
+
export function clearRulesSync(): void {
|
|
334
|
+
requestSync<{ success: boolean }>("POST", "/v1/trust-rules/clear");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Accept the starter approval bundle (synchronous). */
|
|
338
|
+
export function acceptStarterBundleSync(): AcceptStarterBundleResult {
|
|
339
|
+
return requestSync<AcceptStarterBundleResult>(
|
|
340
|
+
"POST",
|
|
341
|
+
"/v1/trust-rules/starter-bundle",
|
|
342
|
+
);
|
|
343
|
+
}
|