@vellumai/credential-executor 0.4.56 → 0.5.0
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.
|
@@ -15,10 +15,11 @@ export type RpcError = z.infer<typeof RpcErrorSchema>;
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Error returned when a local_static credential handle is used in managed
|
|
18
|
-
* mode.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* mode. v2 stores use a UID-independent `store.key` file that removes the
|
|
19
|
+
* technical barrier (legacy v1 relied on PBKDF2 key derivation from user
|
|
20
|
+
* identity, which broke across container users). The restriction is now a
|
|
21
|
+
* policy choice: managed deployments use platform_oauth handles exclusively
|
|
22
|
+
* for simpler lifecycle and centralized token management.
|
|
22
23
|
*/
|
|
23
24
|
export const MANAGED_LOCAL_STATIC_REJECTION_ERROR =
|
|
24
25
|
"local_static credential handles are not supported in managed mode. " +
|
package/package.json
CHANGED
package/src/managed-main.ts
CHANGED
|
@@ -133,14 +133,16 @@ function buildHandlers(sessionIdRef: SessionIdRef, apiKeyRef: ApiKeyRef): RpcHan
|
|
|
133
133
|
|
|
134
134
|
// -- Build handler registry ------------------------------------------------
|
|
135
135
|
// NOTE: local_static credential handles are NOT supported in managed mode.
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
136
|
+
// v2 stores use a UID-independent `store.key` file that removes the
|
|
137
|
+
// technical barrier (legacy v1 stores relied on PBKDF2 key derivation
|
|
138
|
+
// from user identity, which broke across container users). The managed-
|
|
139
|
+
// mode restriction is now a policy choice: managed deployments use
|
|
140
|
+
// platform_oauth handles exclusively for simpler lifecycle and
|
|
141
|
+
// centralized token management.
|
|
140
142
|
//
|
|
141
143
|
// We provide error-returning stubs for localMaterialiser/localSubjectDeps
|
|
142
144
|
// so the HTTP handler compiles but any local_static request gets a clear
|
|
143
|
-
//
|
|
145
|
+
// rejection message.
|
|
144
146
|
|
|
145
147
|
const localMaterialiserStub = {
|
|
146
148
|
materialise: async () => ({
|
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
* encrypted store so subsequent reads (by both CES and the assistant)
|
|
13
13
|
* see the updated value.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* specific entropy via PBKDF2. The derivation includes `userInfo().username`
|
|
17
|
-
* and `userInfo().homedir`, so the key is only correct when CES runs as the
|
|
18
|
-
* same OS user as the assistant.
|
|
15
|
+
* Two store formats are supported:
|
|
19
16
|
*
|
|
20
|
-
* **
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* - **v2 (primary):** AES-256-GCM with a random 32-byte key stored at
|
|
18
|
+
* `<vellumRoot>/protected/store.key`. The key is machine-independent —
|
|
19
|
+
* any process that can read the key file can decrypt the store.
|
|
20
|
+
*
|
|
21
|
+
* - **v1 (legacy):** AES-256-GCM with a key derived from machine-specific
|
|
22
|
+
* entropy via PBKDF2. The derivation includes `userInfo().username` and
|
|
23
|
+
* `userInfo().homedir`, so the key is only correct when CES runs as the
|
|
24
|
+
* same OS user as the assistant.
|
|
25
|
+
*
|
|
26
|
+
* **Managed-mode restriction (v1 only):** For legacy v1 stores, the
|
|
27
|
+
* different container user identity produces a different PBKDF2-derived
|
|
28
|
+
* key, causing silent decryption failures. v2 stores use a
|
|
29
|
+
* UID-independent `store.key` file that can be shared via volume mount,
|
|
30
|
+
* removing this technical barrier. Managed deployments currently use
|
|
31
|
+
* `platform_oauth` handles exclusively as a policy choice (simpler
|
|
32
|
+
* lifecycle, centralized token management).
|
|
25
33
|
*/
|
|
26
34
|
|
|
27
35
|
import {
|
|
@@ -30,7 +38,7 @@ import {
|
|
|
30
38
|
pbkdf2Sync,
|
|
31
39
|
randomBytes,
|
|
32
40
|
} from "node:crypto";
|
|
33
|
-
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
34
42
|
import { hostname, userInfo } from "node:os";
|
|
35
43
|
import { dirname, join } from "node:path";
|
|
36
44
|
|
|
@@ -60,12 +68,42 @@ interface EncryptedEntry {
|
|
|
60
68
|
data: string;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
interface
|
|
71
|
+
interface StoreFileV1 {
|
|
64
72
|
version: 1;
|
|
65
73
|
salt: string;
|
|
66
74
|
entries: Record<string, EncryptedEntry>;
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
interface StoreFileV2 {
|
|
78
|
+
version: 2;
|
|
79
|
+
entries: Record<string, EncryptedEntry>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type StoreFile = StoreFileV1 | StoreFileV2;
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Store key file (v2 format)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const STORE_KEY_FILENAME = "store.key";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the v2 store key file from `<vellumRoot>/protected/store.key`.
|
|
92
|
+
* Returns the raw 32-byte key buffer, or null if the file is missing,
|
|
93
|
+
* wrong size, or unreadable.
|
|
94
|
+
*/
|
|
95
|
+
function readStoreKey(vellumRoot: string): Buffer | null {
|
|
96
|
+
try {
|
|
97
|
+
const keyPath = join(vellumRoot, "protected", STORE_KEY_FILENAME);
|
|
98
|
+
if (!existsSync(keyPath)) return null;
|
|
99
|
+
const buf = readFileSync(keyPath);
|
|
100
|
+
if (buf.length !== KEY_LENGTH) return null;
|
|
101
|
+
return buf;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
// ---------------------------------------------------------------------------
|
|
70
108
|
// Machine entropy (must match assistant/src/security/encrypted-store.ts)
|
|
71
109
|
// ---------------------------------------------------------------------------
|
|
@@ -152,14 +190,15 @@ function readStore(storePath: string): StoreFile | null {
|
|
|
152
190
|
try {
|
|
153
191
|
const raw = readFileSync(storePath, "utf-8");
|
|
154
192
|
const parsed = JSON.parse(raw);
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
if (typeof parsed.entries !== "object") return null;
|
|
194
|
+
|
|
195
|
+
if (parsed.version === 1 && typeof parsed.salt === "string") {
|
|
196
|
+
return parsed as StoreFileV1;
|
|
197
|
+
}
|
|
198
|
+
if (parsed.version === 2) {
|
|
199
|
+
return parsed as StoreFileV2;
|
|
161
200
|
}
|
|
162
|
-
return
|
|
201
|
+
return null;
|
|
163
202
|
} catch {
|
|
164
203
|
return null;
|
|
165
204
|
}
|
|
@@ -204,12 +243,19 @@ export function createLocalSecureKeyBackend(
|
|
|
204
243
|
const entry = store.entries[key];
|
|
205
244
|
if (!entry) return undefined;
|
|
206
245
|
|
|
207
|
-
|
|
208
|
-
|
|
246
|
+
let aesKey: Buffer;
|
|
247
|
+
if (store.version === 2) {
|
|
248
|
+
const storeKey = readStoreKey(vellumRoot);
|
|
249
|
+
if (!storeKey) return undefined;
|
|
250
|
+
aesKey = storeKey;
|
|
251
|
+
} else {
|
|
252
|
+
// v1: derive key from machine entropy via PBKDF2
|
|
253
|
+
const entropy = entropyGetter?.() ?? staticEntropy;
|
|
254
|
+
const salt = Buffer.from(store.salt, "hex");
|
|
255
|
+
aesKey = deriveKey(salt, entropy);
|
|
256
|
+
}
|
|
209
257
|
|
|
210
|
-
|
|
211
|
-
const derivedKey = deriveKey(salt, entropy);
|
|
212
|
-
return decrypt(entry, derivedKey);
|
|
258
|
+
return decrypt(entry, aesKey);
|
|
213
259
|
} catch {
|
|
214
260
|
return undefined;
|
|
215
261
|
}
|
|
@@ -225,10 +271,19 @@ export function createLocalSecureKeyBackend(
|
|
|
225
271
|
const store = readStore(storePath);
|
|
226
272
|
if (!store) return false;
|
|
227
273
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
274
|
+
let aesKey: Buffer;
|
|
275
|
+
if (store.version === 2) {
|
|
276
|
+
const storeKey = readStoreKey(vellumRoot);
|
|
277
|
+
if (!storeKey) return false;
|
|
278
|
+
aesKey = storeKey;
|
|
279
|
+
} else {
|
|
280
|
+
// v1: derive key from machine entropy via PBKDF2
|
|
281
|
+
const entropy = entropyGetter?.() ?? staticEntropy;
|
|
282
|
+
const salt = Buffer.from(store.salt, "hex");
|
|
283
|
+
aesKey = deriveKey(salt, entropy);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
store.entries[key] = encrypt(value, aesKey);
|
|
232
287
|
writeStore(store, storePath);
|
|
233
288
|
return true;
|
|
234
289
|
} catch {
|