@vellumai/assistant 0.12.6-dev.202609301018.08b7375 → 0.12.6-dev.202609301115.aafbefc
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
CHANGED
|
@@ -83,6 +83,7 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
|
|
|
83
83
|
| Profile | Scopes | Used by |
|
|
84
84
|
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
85
85
|
| `actor_client_v1` | `chat.{read,write}`, `approval.{read,write}`, `settings.{read,write}`, `attachments.{read,write}`, `calls.{read,write}`, `feature_flags.{read,write}` | Desktop, CLI clients |
|
|
86
|
+
| `contact_client_v1` | `chat.write`, `approval.write`, `attachments.write` | Non-guardian contact clients |
|
|
86
87
|
| `gateway_ingress_v1` | `ingress.write`, `internal.write` | Gateway channel inbound + webhook forwarding |
|
|
87
88
|
| `gateway_service_v1` | `chat.{read,write}`, `settings.{read,write}`, `attachments.{read,write}`, `internal.write` | Gateway service-to-daemon calls |
|
|
88
89
|
| `local_v1` | `local.all` | Local (loopback) conversation sessions |
|
|
@@ -98,6 +99,8 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
|
|
|
98
99
|
|
|
99
100
|
3. **Local identity** — Local connections use deterministic identity resolution without tokens.
|
|
100
101
|
|
|
102
|
+
**Token role:** Each access and refresh token records the principal it belongs to, `guardian` or `contact`. The role picks the scope profile at mint time, and rotation carries it forward.
|
|
103
|
+
|
|
101
104
|
**Route policy enforcement:** Every protected endpoint declares required scopes and allowed principal types in `src/runtime/auth/route-policy.ts`. The `enforcePolicy()` function checks the AuthContext against these requirements and returns 403 when access is denied. A guard test ensures every dispatched endpoint has a corresponding policy entry.
|
|
102
105
|
|
|
103
106
|
**Credential storage:** Only hashed tokens are persisted. Access token hashes go in `credential_records`; refresh token hashes in `refresh_token_records`. Raw tokens are returned once and never stored server-side.
|
package/package.json
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* 2. No X-Actor-Token references in production code.
|
|
9
9
|
* 3. No legacy gateway-origin proof in production code.
|
|
10
10
|
* 4. Scope profile contract — every profile resolves to the expected scopes.
|
|
11
|
+
* 5. Contact profile agreement: the gateway mints what this runtime honors.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { execSync } from "node:child_process";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
14
16
|
import { resolve } from "node:path";
|
|
15
17
|
import { describe, expect, test } from "bun:test";
|
|
16
18
|
|
|
17
|
-
import { resolveScopeProfile } from "../scopes.js";
|
|
19
|
+
import { isNarrowScopeProfile, resolveScopeProfile } from "../scopes.js";
|
|
18
20
|
import type { Scope, ScopeProfile } from "../types.js";
|
|
19
21
|
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
@@ -253,6 +255,7 @@ describe("scope profile contract", () => {
|
|
|
253
255
|
"feature_flags.read",
|
|
254
256
|
"feature_flags.write",
|
|
255
257
|
],
|
|
258
|
+
contact_client_v1: ["chat.write", "approval.write", "attachments.write"],
|
|
256
259
|
gateway_ingress_v1: ["ingress.write", "internal.write"],
|
|
257
260
|
gateway_service_v1: [
|
|
258
261
|
"chat.read",
|
|
@@ -290,3 +293,50 @@ describe("scope profile contract", () => {
|
|
|
290
293
|
}
|
|
291
294
|
});
|
|
292
295
|
});
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// 5. Contact profile agreement with the gateway
|
|
299
|
+
//
|
|
300
|
+
// The gateway mints `contact_client_v1` and this runtime decides what it may
|
|
301
|
+
// reach. The two registries are separate tables, so a contact token carries
|
|
302
|
+
// whichever scopes the narrower one grants: a scope missing here 403s a route
|
|
303
|
+
// the gateway believes it opened.
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
describe("contact_client_v1 agrees with the gateway registry", () => {
|
|
307
|
+
const GATEWAY_SCOPES_PATH = resolve(
|
|
308
|
+
PROJECT_ROOT,
|
|
309
|
+
"gateway/src/auth/scopes.ts",
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
function gatewaySource(): string {
|
|
313
|
+
return readFileSync(GATEWAY_SCOPES_PATH, "utf8");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
test("grants the same scopes on both sides", () => {
|
|
317
|
+
const block = /contact_client_v1: new Set<Scope>\(\[([^\]]*)\]\)/.exec(
|
|
318
|
+
gatewaySource(),
|
|
319
|
+
);
|
|
320
|
+
expect(
|
|
321
|
+
block,
|
|
322
|
+
`contact_client_v1 not found in ${GATEWAY_SCOPES_PATH}`,
|
|
323
|
+
).not.toBeNull();
|
|
324
|
+
|
|
325
|
+
const gatewayScopes = [...block![1].matchAll(/"([^"]+)"/g)]
|
|
326
|
+
.map((m) => m[1])
|
|
327
|
+
.sort();
|
|
328
|
+
const runtimeScopes: string[] = [
|
|
329
|
+
...resolveScopeProfile("contact_client_v1"),
|
|
330
|
+
].sort();
|
|
331
|
+
|
|
332
|
+
expect(gatewayScopes.length).toBeGreaterThan(0);
|
|
333
|
+
expect(runtimeScopes).toEqual(gatewayScopes);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("is narrow on both sides", () => {
|
|
337
|
+
const broad = /contact_client_v1: (true|false)/.exec(gatewaySource());
|
|
338
|
+
expect(broad).not.toBeNull();
|
|
339
|
+
expect(broad![1]).toBe("false");
|
|
340
|
+
expect(isNarrowScopeProfile("contact_client_v1")).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -11,6 +11,7 @@ import type { AuthContext, Scope, ScopeProfile } from "../types.js";
|
|
|
11
11
|
/** Every profile name; the Record keeps this list exhaustive. */
|
|
12
12
|
const KNOWN_PROFILES = Object.keys({
|
|
13
13
|
actor_client_v1: true,
|
|
14
|
+
contact_client_v1: true,
|
|
14
15
|
gateway_ingress_v1: true,
|
|
15
16
|
gateway_service_v1: true,
|
|
16
17
|
local_v1: true,
|
|
@@ -129,6 +130,7 @@ describe("isNarrowScopeProfile", () => {
|
|
|
129
130
|
// flipped onto a single-route grant would open every unscoped route to it.
|
|
130
131
|
const narrow: Record<ScopeProfile, boolean> = {
|
|
131
132
|
actor_client_v1: false,
|
|
133
|
+
contact_client_v1: true,
|
|
132
134
|
gateway_ingress_v1: false,
|
|
133
135
|
gateway_service_v1: false,
|
|
134
136
|
local_v1: false,
|
|
@@ -27,6 +27,18 @@ const PROFILE_SCOPES: Record<ScopeProfile, ReadonlySet<Scope>> = {
|
|
|
27
27
|
"feature_flags.read",
|
|
28
28
|
"feature_flags.write",
|
|
29
29
|
]),
|
|
30
|
+
// A non-guardian principal. Excludes the settings, feature-flag and call
|
|
31
|
+
// scopes `actor_client_v1` carries: those reach the control plane, which
|
|
32
|
+
// only the guardian may write.
|
|
33
|
+
// Carries no read scope. The routes behind `chat.read`, `approval.read` and
|
|
34
|
+
// `attachments.read` take a caller-supplied conversation or attachment id
|
|
35
|
+
// and never check who is asking, so they return the guardian's data to any
|
|
36
|
+
// holder. Each read is added back once its routes filter by caller.
|
|
37
|
+
contact_client_v1: new Set<Scope>([
|
|
38
|
+
"chat.write",
|
|
39
|
+
"approval.write",
|
|
40
|
+
"attachments.write",
|
|
41
|
+
]),
|
|
30
42
|
gateway_ingress_v1: new Set<Scope>(["ingress.write", "internal.write"]),
|
|
31
43
|
gateway_service_v1: new Set<Scope>([
|
|
32
44
|
"chat.read",
|
|
@@ -91,6 +103,9 @@ export function hasAllScopes(ctx: AuthContext, ...scopes: Scope[]): boolean {
|
|
|
91
103
|
*/
|
|
92
104
|
const BROAD_SCOPE_PROFILES: Record<ScopeProfile, boolean> = {
|
|
93
105
|
actor_client_v1: true,
|
|
106
|
+
// Narrow: a route naming no scope is a route no one has judged a contact
|
|
107
|
+
// against, so it refuses them rather than admitting any valid token.
|
|
108
|
+
contact_client_v1: false,
|
|
94
109
|
gateway_ingress_v1: true,
|
|
95
110
|
gateway_service_v1: true,
|
|
96
111
|
local_v1: true,
|