@vellumai/vellum-gateway 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/AGENTS.md +10 -0
- package/Dockerfile +2 -0
- package/bun.lock +12 -1
- package/package.json +5 -2
- package/src/auth/token-service.ts +6 -1
- package/src/config-file-cache.ts +2 -2
- package/src/config.ts +15 -2
- package/src/credential-reader.ts +96 -12
- package/src/feature-flag-registry.json +8 -0
- package/src/http/routes/channel-verification-session-proxy.ts +2 -1
- package/src/http/routes/trust-rules.ts +338 -0
- package/src/index.ts +60 -0
- package/src/schema.ts +190 -0
- package/src/telegram/normalize.test.ts +122 -0
- package/src/telegram/normalize.ts +46 -2
- package/src/trust-store.ts +518 -0
package/AGENTS.md
CHANGED
|
@@ -31,6 +31,16 @@ All assistant API requests from clients, CLI, skills, and user-facing tooling **
|
|
|
31
31
|
|
|
32
32
|
**SKILL.md gateway URL pattern:** For gateway control-plane writes/actions that are not exposed through a CLI read command, use `$INTERNAL_GATEWAY_BASE_URL` (injected by `bash` and `host_bash`). Do not hardcode `localhost`/ports in skill examples, and do not instruct users/agents to manually export the variable from Settings. For public ingress URLs (e.g. OAuth redirect URIs, webhook registration), use `assistant config get ingress.publicBaseUrl` or load the `public-ingress` skill — do not inject public URLs as environment variables.
|
|
33
33
|
|
|
34
|
+
### Trust Management in Docker Mode
|
|
35
|
+
|
|
36
|
+
In Docker mode, the gateway is the sole owner of trust rule storage. Trust files (`trust.json`, `actor-token-signing-key`) live on the gateway security volume (`/gateway-security`), configured via `GATEWAY_SECURITY_DIR`. No other container has access to this volume.
|
|
37
|
+
|
|
38
|
+
The assistant reads and writes trust rules via the gateway's HTTP trust API instead of accessing the filesystem directly. This ensures the security boundary is enforced at the container level — even if the assistant container is compromised, it cannot tamper with trust rules without going through the gateway's API.
|
|
39
|
+
|
|
40
|
+
### Credential Access in Docker Mode
|
|
41
|
+
|
|
42
|
+
In Docker mode, the gateway accesses stored credentials via the CES HTTP API (`CES_CREDENTIAL_URL`), authenticated with `CES_SERVICE_TOKEN`. The gateway does not have direct filesystem access to credential encryption keys (`keys.enc`, `store.key`), which reside on the CES security volume.
|
|
43
|
+
|
|
34
44
|
### Channel Identity Vocabulary
|
|
35
45
|
|
|
36
46
|
Gateway inbound events use a channel-discriminated union model (`GatewayInboundEvent`) with explicit identity fields:
|
package/Dockerfile
CHANGED
package/bun.lock
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
"name": "vellum-gateway",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"file-type": "^21.3.0",
|
|
9
|
+
"minimatch": "^10.2.4",
|
|
9
10
|
"pino": "^9.6.0",
|
|
10
11
|
"pino-pretty": "^13.1.3",
|
|
12
|
+
"uuid": "^13.0.0",
|
|
11
13
|
},
|
|
12
14
|
"devDependencies": {
|
|
13
15
|
"@types/bun": "^1.2.4",
|
|
16
|
+
"@types/uuid": "^11.0.0",
|
|
14
17
|
"eslint": "^10.0.0",
|
|
15
18
|
"knip": "^5.83.1",
|
|
16
19
|
"prettier": "^3.8.1",
|
|
@@ -118,6 +121,8 @@
|
|
|
118
121
|
|
|
119
122
|
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
120
123
|
|
|
124
|
+
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
|
125
|
+
|
|
121
126
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="],
|
|
122
127
|
|
|
123
128
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="],
|
|
@@ -262,7 +267,7 @@
|
|
|
262
267
|
|
|
263
268
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
|
264
269
|
|
|
265
|
-
"minimatch": ["minimatch@10.2.
|
|
270
|
+
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
|
266
271
|
|
|
267
272
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
|
268
273
|
|
|
@@ -362,6 +367,8 @@
|
|
|
362
367
|
|
|
363
368
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
|
364
369
|
|
|
370
|
+
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
|
371
|
+
|
|
365
372
|
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
|
|
366
373
|
|
|
367
374
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
@@ -376,10 +383,14 @@
|
|
|
376
383
|
|
|
377
384
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
378
385
|
|
|
386
|
+
"@eslint/config-array/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
|
387
|
+
|
|
379
388
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
|
380
389
|
|
|
381
390
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
|
382
391
|
|
|
392
|
+
"eslint/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
|
393
|
+
|
|
383
394
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
384
395
|
|
|
385
396
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/vellum-gateway",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./twilio/verify": "./src/twilio/verify.ts",
|
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"file-type": "^21.3.0",
|
|
26
|
+
"minimatch": "^10.2.4",
|
|
26
27
|
"pino": "^9.6.0",
|
|
27
|
-
"pino-pretty": "^13.1.3"
|
|
28
|
+
"pino-pretty": "^13.1.3",
|
|
29
|
+
"uuid": "^13.0.0"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@types/bun": "^1.2.4",
|
|
33
|
+
"@types/uuid": "^11.0.0",
|
|
31
34
|
"eslint": "^10.0.0",
|
|
32
35
|
"knip": "^5.83.1",
|
|
33
36
|
"prettier": "^3.8.1",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* JWT token service for the gateway's auth system.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors the assistant's token-service but manages its own signing key
|
|
5
|
-
* using the same loadOrCreateSigningKey pattern
|
|
5
|
+
* using the same loadOrCreateSigningKey pattern. When GATEWAY_SECURITY_DIR
|
|
6
|
+
* is set the key is read from that directory; otherwise falls back to
|
|
6
7
|
* ~/.vellum/protected/actor-token-signing-key.
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -32,6 +33,10 @@ const log = getLogger("auth-token-service");
|
|
|
32
33
|
let signingKey: Buffer | null = null;
|
|
33
34
|
|
|
34
35
|
function getSigningKeyPath(): string {
|
|
36
|
+
const securityDir = process.env.GATEWAY_SECURITY_DIR;
|
|
37
|
+
if (securityDir) {
|
|
38
|
+
return join(securityDir, "actor-token-signing-key");
|
|
39
|
+
}
|
|
35
40
|
return join(getRootDir(), "protected", "actor-token-signing-key");
|
|
36
41
|
}
|
|
37
42
|
|
package/src/config-file-cache.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { getWorkspaceDir } from "./credential-reader.js";
|
|
12
12
|
|
|
13
13
|
const DEFAULT_TTL_MS = 1000;
|
|
14
14
|
|
|
@@ -55,7 +55,7 @@ export class ConfigFileCache {
|
|
|
55
55
|
|
|
56
56
|
constructor(opts?: { ttlMs?: number }) {
|
|
57
57
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
58
|
-
this.configPath = join(
|
|
58
|
+
this.configPath = join(getWorkspaceDir(), "config.json");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Read the config file if the cached snapshot is stale or force is set. */
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { getLogger, type LogFileConfig } from "./logger.js";
|
|
5
|
-
import { getRootDir } from "./credential-reader.js";
|
|
5
|
+
import { getRootDir, getWorkspaceDir } from "./credential-reader.js";
|
|
6
6
|
|
|
7
7
|
const log = getLogger("config");
|
|
8
8
|
|
|
@@ -43,7 +43,7 @@ type RoutingEntry = {
|
|
|
43
43
|
*/
|
|
44
44
|
function readWorkspaceConfig(): Record<string, unknown> {
|
|
45
45
|
try {
|
|
46
|
-
const configPath = join(
|
|
46
|
+
const configPath = join(getWorkspaceDir(), "config.json");
|
|
47
47
|
if (!existsSync(configPath)) return {};
|
|
48
48
|
const raw = readFileSync(configPath, "utf-8");
|
|
49
49
|
const data = JSON.parse(raw);
|
|
@@ -54,6 +54,19 @@ function readWorkspaceConfig(): Record<string, unknown> {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Directory containing gateway security files (trust.json, actor-token-signing-key).
|
|
59
|
+
*
|
|
60
|
+
* In Docker, this is a dedicated volume mounted at /gateway-security via the
|
|
61
|
+
* GATEWAY_SECURITY_DIR env var. In local (non-Docker) mode, falls back to
|
|
62
|
+
* ~/.vellum/protected/ for backwards compatibility.
|
|
63
|
+
*/
|
|
64
|
+
export function getGatewaySecurityDir(): string {
|
|
65
|
+
const override = process.env.GATEWAY_SECURITY_DIR?.trim();
|
|
66
|
+
if (override) return override;
|
|
67
|
+
return join(getRootDir(), "protected");
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function parseRoutingEntries(raw: unknown): RoutingEntry[] {
|
|
58
71
|
if (!Array.isArray(raw)) return [];
|
|
59
72
|
const entries: RoutingEntry[] = [];
|
package/src/credential-reader.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-only reader for the assistant's credential stores.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. CES HTTP API (when CES_CREDENTIAL_URL is set — containerized mode)
|
|
6
|
+
* 2. Keychain broker (UDS — native macOS/Linux)
|
|
7
|
+
* 3. Encrypted-at-rest file (~/.vellum/protected/keys.enc)
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { createDecipheriv, pbkdf2Sync, randomUUID } from "node:crypto";
|
|
@@ -137,18 +139,25 @@ export function getRootDir(): string {
|
|
|
137
139
|
);
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Returns the workspace root for user-facing state.
|
|
144
|
+
*
|
|
145
|
+
* When the WORKSPACE_DIR env var is set, returns that value (used in
|
|
146
|
+
* containerized deployments where the workspace is a separate volume).
|
|
147
|
+
* Otherwise falls back to ~/.vellum/workspace.
|
|
148
|
+
*/
|
|
149
|
+
export function getWorkspaceDir(): string {
|
|
150
|
+
const override = process.env.WORKSPACE_DIR?.trim();
|
|
151
|
+
if (override) return override;
|
|
152
|
+
return join(getRootDir(), "workspace");
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
export function getEncryptedStorePath(): string {
|
|
141
156
|
return join(getRootDir(), "protected", "keys.enc");
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
export function getMetadataPath(): string {
|
|
145
|
-
return join(
|
|
146
|
-
getRootDir(),
|
|
147
|
-
"workspace",
|
|
148
|
-
"data",
|
|
149
|
-
"credentials",
|
|
150
|
-
"metadata.json",
|
|
151
|
-
);
|
|
160
|
+
return join(getWorkspaceDir(), "data", "credentials", "metadata.json");
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
// ---------------------------------------------------------------------------
|
|
@@ -308,19 +317,94 @@ async function readBrokerCredential(
|
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
// ---------------------------------------------------------------------------
|
|
311
|
-
//
|
|
320
|
+
// CES HTTP credential reader (containerized mode)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
const CES_HTTP_TIMEOUT_MS = 5_000;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Try to read a credential from the CES managed service over HTTP.
|
|
327
|
+
*
|
|
328
|
+
* Activated when `CES_CREDENTIAL_URL` is set (e.g. `http://ces-host:8090`).
|
|
329
|
+
* Requires `CES_SERVICE_TOKEN` for bearer auth.
|
|
330
|
+
*
|
|
331
|
+
* Returns `undefined` if the env vars are not set, the CES is unreachable,
|
|
332
|
+
* or the credential doesn't exist (404).
|
|
333
|
+
*/
|
|
334
|
+
async function readCesCredential(
|
|
335
|
+
account: string,
|
|
336
|
+
): Promise<string | undefined> {
|
|
337
|
+
const baseUrl = process.env.CES_CREDENTIAL_URL?.trim();
|
|
338
|
+
if (!baseUrl) return undefined;
|
|
339
|
+
|
|
340
|
+
const serviceToken = process.env.CES_SERVICE_TOKEN?.trim();
|
|
341
|
+
if (!serviceToken) {
|
|
342
|
+
log.warn("CES_CREDENTIAL_URL is set but CES_SERVICE_TOKEN is missing");
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const url = `${baseUrl}/v1/credentials/${encodeURIComponent(account)}`;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const controller = new AbortController();
|
|
350
|
+
const timer = setTimeout(() => controller.abort(), CES_HTTP_TIMEOUT_MS);
|
|
351
|
+
|
|
352
|
+
const resp = await fetch(url, {
|
|
353
|
+
method: "GET",
|
|
354
|
+
headers: {
|
|
355
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
356
|
+
Accept: "application/json",
|
|
357
|
+
},
|
|
358
|
+
signal: controller.signal,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
|
|
363
|
+
if (resp.status === 404) return undefined;
|
|
364
|
+
|
|
365
|
+
if (!resp.ok) {
|
|
366
|
+
log.warn(
|
|
367
|
+
{ account, status: resp.status },
|
|
368
|
+
"CES credential read returned non-OK status",
|
|
369
|
+
);
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const body = (await resp.json()) as { account?: string; value?: string };
|
|
374
|
+
if (typeof body.value === "string") return body.value;
|
|
375
|
+
|
|
376
|
+
log.debug({ account }, "CES credential response missing 'value' field");
|
|
377
|
+
return undefined;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
log.debug({ err, account }, "Failed to read credential from CES");
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Public credential reader — tries CES, broker, then encrypted store
|
|
312
386
|
// ---------------------------------------------------------------------------
|
|
313
387
|
|
|
314
388
|
/**
|
|
315
389
|
* Read a single credential by account key.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
390
|
+
*
|
|
391
|
+
* Resolution order:
|
|
392
|
+
* 1. CES HTTP API (when CES_CREDENTIAL_URL is set)
|
|
393
|
+
* 2. Keychain broker (UDS)
|
|
394
|
+
* 3. Encrypted-at-rest store (keys.enc)
|
|
318
395
|
*/
|
|
319
396
|
export async function readCredential(
|
|
320
397
|
account: string,
|
|
321
398
|
): Promise<string | undefined> {
|
|
399
|
+
// CES HTTP backend (containerized mode)
|
|
400
|
+
const cesValue = await readCesCredential(account);
|
|
401
|
+
if (cesValue !== undefined) return cesValue;
|
|
402
|
+
|
|
403
|
+
// Keychain broker (native mode)
|
|
322
404
|
const brokerValue = await readBrokerCredential(account);
|
|
323
405
|
if (brokerValue !== undefined) return brokerValue;
|
|
406
|
+
|
|
407
|
+
// Encrypted file fallback
|
|
324
408
|
return readEncryptedCredential(account);
|
|
325
409
|
}
|
|
326
410
|
|
|
@@ -288,6 +288,14 @@
|
|
|
288
288
|
"label": "Inline Skill Command Expansion",
|
|
289
289
|
"description": "Enable secure inline skill command expansion via !`command` syntax, with version-pinned approval and sandboxed execution at skill load time",
|
|
290
290
|
"defaultEnabled": true
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"id": "channel-voice-transcription",
|
|
294
|
+
"scope": "assistant",
|
|
295
|
+
"key": "feature_flags.channel-voice-transcription.enabled",
|
|
296
|
+
"label": "Channel Voice Transcription",
|
|
297
|
+
"description": "Auto-transcribe voice/audio messages received from channels (Telegram, WhatsApp) before processing",
|
|
298
|
+
"defaultEnabled": true
|
|
291
299
|
}
|
|
292
300
|
]
|
|
293
301
|
}
|
|
@@ -150,7 +150,8 @@ export function createChannelVerificationSessionProxyHandler(
|
|
|
150
150
|
req: Request,
|
|
151
151
|
clientIp?: string,
|
|
152
152
|
): Promise<Response> {
|
|
153
|
-
const
|
|
153
|
+
const lockDir = process.env.GATEWAY_SECURITY_DIR || getRootDir();
|
|
154
|
+
const lockPath = join(lockDir, "guardian-init.lock");
|
|
154
155
|
if (existsSync(lockPath) || guardianInitInFlight) {
|
|
155
156
|
log.warn("Guardian init rejected — already bootstrapped");
|
|
156
157
|
return Response.json(
|