@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895
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 +1 -1
- package/node_modules/@vellumai/service-contracts/package.json +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
- package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
- package/package.json +2 -3
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/local-standalone.test.ts +5 -36
- package/src/__tests__/managed-integration.test.ts +112 -91
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/transport.test.ts +23 -27
- package/src/cli.ts +1 -1
- package/src/index.ts +8 -88
- package/src/main.ts +228 -340
- package/src/paths.ts +4 -20
- package/src/server.ts +52 -469
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
- package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
- package/src/__tests__/command-executor.test.ts +0 -1879
- package/src/__tests__/command-validator.test.ts +0 -1405
- package/src/__tests__/command-workspace.test.ts +0 -1050
- package/src/__tests__/grant-store.test.ts +0 -689
- package/src/__tests__/http-executor.test.ts +0 -1336
- package/src/__tests__/http-policy.test.ts +0 -1069
- package/src/__tests__/local-materializers.test.ts +0 -860
- package/src/__tests__/local-token-refresh.test.ts +0 -361
- package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
- package/src/__tests__/managed-lazy-getters.test.ts +0 -359
- package/src/__tests__/managed-materializers.test.ts +0 -1028
- package/src/__tests__/managed-rejection.test.ts +0 -43
- package/src/__tests__/toolstore.test.ts +0 -773
- package/src/audit/store.ts +0 -188
- package/src/commands/auth-adapters.ts +0 -169
- package/src/commands/egress-hooks.ts +0 -203
- package/src/commands/executor.ts +0 -1155
- package/src/commands/output-scan.ts +0 -157
- package/src/commands/profiles.ts +0 -286
- package/src/commands/validator.ts +0 -702
- package/src/commands/workspace.ts +0 -550
- package/src/grants/index.ts +0 -17
- package/src/grants/persistent-store.ts +0 -309
- package/src/grants/rpc-handlers.ts +0 -293
- package/src/grants/temporary-store.ts +0 -289
- package/src/http/audit.ts +0 -84
- package/src/http/executor.ts +0 -684
- package/src/http/path-template.ts +0 -245
- package/src/http/policy.ts +0 -238
- package/src/http/response-filter.ts +0 -233
- package/src/managed-errors.ts +0 -9
- package/src/managed-lazy-getters.ts +0 -106
- package/src/managed-main.ts +0 -822
- package/src/materializers/local-oauth-lookup.ts +0 -98
- package/src/materializers/local-token-refresh.ts +0 -287
- package/src/materializers/local.ts +0 -316
- package/src/materializers/managed-platform.ts +0 -295
- package/src/subjects/local.ts +0 -177
- package/src/subjects/managed.ts +0 -311
- package/src/subjects/policy.ts +0 -79
- package/src/toolstore/integrity.ts +0 -94
- package/src/toolstore/manifest.ts +0 -154
- package/src/toolstore/publish.ts +0 -571
package/src/subjects/managed.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Managed subject resolution for platform OAuth handles.
|
|
3
|
-
*
|
|
4
|
-
* Resolves `platform_oauth:<connection_id>` handles into a normalized
|
|
5
|
-
* subject shape that the rest of CES can treat uniformly alongside local
|
|
6
|
-
* subjects. Managed subjects never carry raw tokens — they carry only
|
|
7
|
-
* the metadata needed to call the platform's token-materialization
|
|
8
|
-
* endpoint at execution time.
|
|
9
|
-
*
|
|
10
|
-
* Subject resolution is the first phase of a two-phase credential flow:
|
|
11
|
-
* 1. **Resolution** (this module) — parse the handle, validate the
|
|
12
|
-
* connection exists in the platform catalog, and return a subject
|
|
13
|
-
* descriptor with provider metadata.
|
|
14
|
-
* 2. **Materialization** (`materializers/managed-platform.ts`) — use
|
|
15
|
-
* the resolved subject to request a short-lived access token from
|
|
16
|
-
* the platform and inject it into the execution environment.
|
|
17
|
-
*
|
|
18
|
-
* The subject shape is intentionally slim and secret-free so it can be
|
|
19
|
-
* logged, cached in memory, and passed across internal boundaries without
|
|
20
|
-
* risk of leaking credentials.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
HandleType,
|
|
25
|
-
parseHandle,
|
|
26
|
-
type PlatformOAuthHandle,
|
|
27
|
-
} from "@vellumai/service-contracts/credential-rpc";
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Common subject interface
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Source discriminator shared by all subject types.
|
|
35
|
-
*
|
|
36
|
-
* - `"local"` — credential lives in the local secure-key backend.
|
|
37
|
-
* - `"managed"` — credential is managed by the platform; tokens are
|
|
38
|
-
* obtained via the platform's CES token-materialization endpoint.
|
|
39
|
-
*/
|
|
40
|
-
export type SubjectSource = "local" | "managed";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Common shape that all resolved subjects expose. CES execution paths
|
|
44
|
-
* (HTTP materializer, command materializer) can branch on `source`
|
|
45
|
-
* without knowing the full subject type.
|
|
46
|
-
*/
|
|
47
|
-
export interface ResolvedSubject {
|
|
48
|
-
/** Source of the credential. */
|
|
49
|
-
source: SubjectSource;
|
|
50
|
-
/** The raw handle string that was resolved. */
|
|
51
|
-
handle: string;
|
|
52
|
-
/** Provider identifier (e.g. "google", "slack", "github"). */
|
|
53
|
-
provider: string;
|
|
54
|
-
/** Connection identifier on the platform (managed) or locally. */
|
|
55
|
-
connectionId: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Managed subject shape
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* A resolved managed subject — the output of resolving a
|
|
64
|
-
* `platform_oauth:<connection_id>` handle against the platform catalog.
|
|
65
|
-
*
|
|
66
|
-
* This shape carries zero secret material. It is safe to log, serialize,
|
|
67
|
-
* and pass across internal boundaries.
|
|
68
|
-
*/
|
|
69
|
-
export interface ManagedSubject extends ResolvedSubject {
|
|
70
|
-
source: "managed";
|
|
71
|
-
/** Account info as reported by the platform catalog (e.g. email). */
|
|
72
|
-
accountInfo: string | null;
|
|
73
|
-
/** Granted OAuth scopes as reported by the platform catalog. */
|
|
74
|
-
grantedScopes: string[];
|
|
75
|
-
/** Connection status from the platform catalog (e.g. "active", "expired"). */
|
|
76
|
-
status: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Platform catalog entry (non-secret subset from the platform response)
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Shape of a single connection entry in the platform catalog response.
|
|
85
|
-
* Only non-secret fields are parsed; token values are never included.
|
|
86
|
-
*
|
|
87
|
-
* Field names match the platform's ManagedConnectionCatalogEntrySerializer:
|
|
88
|
-
* handle, connection_id, provider, account_label, scopes_granted, status
|
|
89
|
-
*/
|
|
90
|
-
export interface PlatformCatalogEntry {
|
|
91
|
-
handle: string;
|
|
92
|
-
connection_id: string;
|
|
93
|
-
provider: string;
|
|
94
|
-
account_label?: string | null;
|
|
95
|
-
scopes_granted?: string[];
|
|
96
|
-
status?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// Resolution errors
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
export class SubjectResolutionError extends Error {
|
|
104
|
-
readonly code: string;
|
|
105
|
-
|
|
106
|
-
constructor(code: string, message: string) {
|
|
107
|
-
super(message);
|
|
108
|
-
this.name = "SubjectResolutionError";
|
|
109
|
-
this.code = code;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// Resolution options
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
export interface ManagedSubjectResolverOptions {
|
|
118
|
-
/**
|
|
119
|
-
* Platform base URL (without trailing slash).
|
|
120
|
-
* e.g. "https://api.vellum.ai"
|
|
121
|
-
*/
|
|
122
|
-
platformBaseUrl: string;
|
|
123
|
-
/**
|
|
124
|
-
* Assistant API key for authenticating with the platform.
|
|
125
|
-
*/
|
|
126
|
-
assistantApiKey: string;
|
|
127
|
-
/**
|
|
128
|
-
* Platform-assigned assistant UUID. Required for building the
|
|
129
|
-
* platform catalog URL: /v1/assistants/<id>/oauth/managed/catalog/
|
|
130
|
-
*/
|
|
131
|
-
assistantId: string;
|
|
132
|
-
/**
|
|
133
|
-
* Optional custom fetch implementation (for testing).
|
|
134
|
-
*/
|
|
135
|
-
fetch?: typeof globalThis.fetch;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Resolution result
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
export type ResolveResult =
|
|
143
|
-
| { ok: true; subject: ManagedSubject }
|
|
144
|
-
| { ok: false; error: SubjectResolutionError };
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Resolver implementation
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve a `platform_oauth:<connection_id>` handle into a managed subject
|
|
152
|
-
* by looking up the connection in the platform's CES catalog.
|
|
153
|
-
*
|
|
154
|
-
* Fail-closed: if the platform cannot be reached, returns an error rather
|
|
155
|
-
* than proceeding without credential validation.
|
|
156
|
-
*/
|
|
157
|
-
export async function resolveManagedSubject(
|
|
158
|
-
handle: string,
|
|
159
|
-
options: ManagedSubjectResolverOptions,
|
|
160
|
-
): Promise<ResolveResult> {
|
|
161
|
-
// -- Parse handle ---------------------------------------------------------
|
|
162
|
-
const parsed = parseHandle(handle);
|
|
163
|
-
if (!parsed.ok) {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
error: new SubjectResolutionError("INVALID_HANDLE", parsed.error),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (parsed.handle.type !== HandleType.PlatformOAuth) {
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
error: new SubjectResolutionError(
|
|
174
|
-
"WRONG_HANDLE_TYPE",
|
|
175
|
-
`Expected platform_oauth handle, got ${parsed.handle.type}`,
|
|
176
|
-
),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const platformHandle = parsed.handle as PlatformOAuthHandle;
|
|
181
|
-
|
|
182
|
-
// -- Validate prerequisites -----------------------------------------------
|
|
183
|
-
if (!options.platformBaseUrl) {
|
|
184
|
-
return {
|
|
185
|
-
ok: false,
|
|
186
|
-
error: new SubjectResolutionError(
|
|
187
|
-
"MISSING_PLATFORM_URL",
|
|
188
|
-
"Platform base URL is required for managed subject resolution",
|
|
189
|
-
),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!options.assistantApiKey) {
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
error: new SubjectResolutionError(
|
|
197
|
-
"MISSING_API_KEY",
|
|
198
|
-
"Assistant API key is required for managed subject resolution",
|
|
199
|
-
),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!options.assistantId) {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
error: new SubjectResolutionError(
|
|
207
|
-
"MISSING_ASSISTANT_ID",
|
|
208
|
-
"Assistant ID is required for managed subject resolution",
|
|
209
|
-
),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// -- Fetch catalog entry --------------------------------------------------
|
|
214
|
-
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
215
|
-
const catalogUrl = `${options.platformBaseUrl}/v1/assistants/${encodeURIComponent(options.assistantId)}/oauth/managed/catalog/`;
|
|
216
|
-
|
|
217
|
-
let response: Response;
|
|
218
|
-
try {
|
|
219
|
-
response = await fetchFn(catalogUrl, {
|
|
220
|
-
method: "GET",
|
|
221
|
-
headers: {
|
|
222
|
-
Authorization: `Api-Key ${options.assistantApiKey}`,
|
|
223
|
-
Accept: "application/json",
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
} catch (err) {
|
|
227
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
228
|
-
return {
|
|
229
|
-
ok: false,
|
|
230
|
-
error: new SubjectResolutionError(
|
|
231
|
-
"PLATFORM_UNREACHABLE",
|
|
232
|
-
`Failed to reach platform CES catalog: ${sanitizeError(message)}`,
|
|
233
|
-
),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!response.ok) {
|
|
238
|
-
return {
|
|
239
|
-
ok: false,
|
|
240
|
-
error: new SubjectResolutionError(
|
|
241
|
-
`PLATFORM_HTTP_${response.status}`,
|
|
242
|
-
`Platform CES catalog returned HTTP ${response.status}`,
|
|
243
|
-
),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// -- Parse response -------------------------------------------------------
|
|
248
|
-
// The platform returns a flat JSON array of catalog entries
|
|
249
|
-
// (serialized with many=True), not a wrapper object.
|
|
250
|
-
let entries: PlatformCatalogEntry[];
|
|
251
|
-
try {
|
|
252
|
-
entries = (await response.json()) as PlatformCatalogEntry[];
|
|
253
|
-
} catch {
|
|
254
|
-
return {
|
|
255
|
-
ok: false,
|
|
256
|
-
error: new SubjectResolutionError(
|
|
257
|
-
"INVALID_CATALOG_RESPONSE",
|
|
258
|
-
"Platform CES catalog returned invalid JSON",
|
|
259
|
-
),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!Array.isArray(entries)) {
|
|
264
|
-
return {
|
|
265
|
-
ok: false,
|
|
266
|
-
error: new SubjectResolutionError(
|
|
267
|
-
"INVALID_CATALOG_RESPONSE",
|
|
268
|
-
"Platform CES catalog returned unexpected response format",
|
|
269
|
-
),
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// -- Find matching connection ---------------------------------------------
|
|
274
|
-
const entry = entries.find(
|
|
275
|
-
(c) => c.connection_id === platformHandle.connectionId,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
if (!entry) {
|
|
279
|
-
return {
|
|
280
|
-
ok: false,
|
|
281
|
-
error: new SubjectResolutionError(
|
|
282
|
-
"CONNECTION_NOT_FOUND",
|
|
283
|
-
`Connection ${platformHandle.connectionId} not found in platform catalog`,
|
|
284
|
-
),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -- Build managed subject ------------------------------------------------
|
|
289
|
-
const subject: ManagedSubject = {
|
|
290
|
-
source: "managed",
|
|
291
|
-
handle,
|
|
292
|
-
provider: entry.provider,
|
|
293
|
-
connectionId: entry.connection_id,
|
|
294
|
-
accountInfo: entry.account_label ?? null,
|
|
295
|
-
grantedScopes: entry.scopes_granted ?? [],
|
|
296
|
-
status: entry.status ?? "unknown",
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
return { ok: true, subject };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
// Helpers
|
|
304
|
-
// ---------------------------------------------------------------------------
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Sanitize error messages to avoid leaking secrets (defensive).
|
|
308
|
-
*/
|
|
309
|
-
function sanitizeError(message: string): string {
|
|
310
|
-
return message.replace(/Api-Key\s+\S+/gi, "Api-Key [REDACTED]");
|
|
311
|
-
}
|
package/src/subjects/policy.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CES credential policy enforcement.
|
|
3
|
-
*
|
|
4
|
-
* Enforces credential-level policies (allowedTools, allowedDomains) that were
|
|
5
|
-
* previously only checked by the pre-CES broker in the assistant daemon.
|
|
6
|
-
* Without these checks, CES would materialise credentials that the broker
|
|
7
|
-
* would have rejected — a security regression.
|
|
8
|
-
*
|
|
9
|
-
* Policy rules:
|
|
10
|
-
* - **allowedDomains** — credentials with domain restrictions are scoped to
|
|
11
|
-
* browser use on those domains and cannot be used server-side by CES.
|
|
12
|
-
* - **allowedTools** — if set (even if empty), only the listed tools may
|
|
13
|
-
* consume the credential. An empty array means deny-all. CES tool names
|
|
14
|
-
* ("make_authenticated_request", "run_authenticated_command") must be in
|
|
15
|
-
* the list.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { StaticCredentialRecord } from "@vellumai/credential-storage";
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Result type
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export interface CredentialPolicyCheckResult {
|
|
25
|
-
ok: boolean;
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Policy check
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check credential-level policies before materialisation.
|
|
35
|
-
*
|
|
36
|
-
* Returns `{ ok: true }` if the credential may be materialised for the
|
|
37
|
-
* given CES tool, or `{ ok: false, error }` with a human-readable
|
|
38
|
-
* rejection message.
|
|
39
|
-
*
|
|
40
|
-
* @param metadata Non-secret metadata record for a local static credential.
|
|
41
|
-
* @param cesToolName The CES tool requesting materialisation
|
|
42
|
-
* (e.g. "make_authenticated_request" or "run_authenticated_command").
|
|
43
|
-
*/
|
|
44
|
-
export function checkCredentialPolicy(
|
|
45
|
-
metadata: StaticCredentialRecord,
|
|
46
|
-
cesToolName: string,
|
|
47
|
-
): CredentialPolicyCheckResult {
|
|
48
|
-
// -- allowedDomains -------------------------------------------------------
|
|
49
|
-
// Credentials with domain restrictions are scoped to browser use on those
|
|
50
|
-
// domains. They cannot be used for server-side operations through CES.
|
|
51
|
-
if (metadata.allowedDomains && metadata.allowedDomains.length > 0) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
error:
|
|
55
|
-
`Credential ${metadata.service}/${metadata.field} has domain restrictions ` +
|
|
56
|
-
`(${metadata.allowedDomains.join(", ")}) and cannot be used for server-side ` +
|
|
57
|
-
`operations through CES. Remove domain restrictions or use a separate credential.`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// -- allowedTools ---------------------------------------------------------
|
|
62
|
-
// If set (even if empty), the CES tool must be in the allowed list.
|
|
63
|
-
// An empty allowedTools array means no tools are permitted (deny-all).
|
|
64
|
-
if (metadata.allowedTools) {
|
|
65
|
-
if (!metadata.allowedTools.includes(cesToolName)) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
error:
|
|
69
|
-
metadata.allowedTools.length === 0
|
|
70
|
-
? `Credential ${metadata.service}/${metadata.field} does not allow any tools.`
|
|
71
|
-
: `Tool "${cesToolName}" is not allowed to use credential ` +
|
|
72
|
-
`${metadata.service}/${metadata.field}. ` +
|
|
73
|
-
`Allowed tools: ${metadata.allowedTools.join(", ")}.`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { ok: true };
|
|
79
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bundle integrity verification.
|
|
3
|
-
*
|
|
4
|
-
* Provides SHA-256 digest computation and verification for secure command
|
|
5
|
-
* bundles. Digests are computed over the raw bundle bytes and compared
|
|
6
|
-
* against the expected digest declared in the toolstore manifest.
|
|
7
|
-
*
|
|
8
|
-
* All digests are lowercase hex-encoded SHA-256 hashes (64 characters).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Digest computation
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Compute the SHA-256 hex digest of arbitrary bytes.
|
|
19
|
-
*
|
|
20
|
-
* Returns a lowercase 64-character hex string.
|
|
21
|
-
*/
|
|
22
|
-
export function computeDigest(data: Buffer | Uint8Array): string {
|
|
23
|
-
return createHash("sha256").update(data).digest("hex");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Digest verification
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
export interface DigestVerificationResult {
|
|
31
|
-
/** Whether the computed digest matches the expected digest. */
|
|
32
|
-
valid: boolean;
|
|
33
|
-
/** The computed digest (always present). */
|
|
34
|
-
computedDigest: string;
|
|
35
|
-
/** The expected digest (always present). */
|
|
36
|
-
expectedDigest: string;
|
|
37
|
-
/** Human-readable error message when invalid (undefined when valid). */
|
|
38
|
-
error?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Verify that the SHA-256 digest of `data` matches `expectedDigest`.
|
|
43
|
-
*
|
|
44
|
-
* Uses constant-time comparison via `timingSafeEqual` to prevent
|
|
45
|
-
* timing side-channel attacks on digest values.
|
|
46
|
-
*/
|
|
47
|
-
export function verifyDigest(
|
|
48
|
-
data: Buffer | Uint8Array,
|
|
49
|
-
expectedDigest: string,
|
|
50
|
-
): DigestVerificationResult {
|
|
51
|
-
const computedDigest = computeDigest(data);
|
|
52
|
-
|
|
53
|
-
// Use timing-safe comparison to prevent timing attacks
|
|
54
|
-
const computedBuf = Buffer.from(computedDigest, "hex");
|
|
55
|
-
const expectedBuf = Buffer.from(expectedDigest, "hex");
|
|
56
|
-
|
|
57
|
-
// If the expected digest is not valid hex (wrong length), fail
|
|
58
|
-
if (computedBuf.length !== expectedBuf.length || expectedBuf.length !== 32) {
|
|
59
|
-
return {
|
|
60
|
-
valid: false,
|
|
61
|
-
computedDigest,
|
|
62
|
-
expectedDigest,
|
|
63
|
-
error: `Digest mismatch: expected "${expectedDigest}" but computed "${computedDigest}". ` +
|
|
64
|
-
`The bundle contents do not match the declared digest.`,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const match = safeTimingEqual(computedBuf, expectedBuf);
|
|
69
|
-
|
|
70
|
-
if (!match) {
|
|
71
|
-
return {
|
|
72
|
-
valid: false,
|
|
73
|
-
computedDigest,
|
|
74
|
-
expectedDigest,
|
|
75
|
-
error: `Digest mismatch: expected "${expectedDigest}" but computed "${computedDigest}". ` +
|
|
76
|
-
`The bundle contents do not match the declared digest.`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
valid: true,
|
|
82
|
-
computedDigest,
|
|
83
|
-
expectedDigest,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Constant-time buffer comparison. Wraps `crypto.timingSafeEqual`
|
|
89
|
-
* with a length guard (timingSafeEqual throws on length mismatch).
|
|
90
|
-
*/
|
|
91
|
-
function safeTimingEqual(a: Buffer, b: Buffer): boolean {
|
|
92
|
-
if (a.length !== b.length) return false;
|
|
93
|
-
return timingSafeEqual(a, b);
|
|
94
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toolstore manifest type definitions.
|
|
3
|
-
*
|
|
4
|
-
* Describes an approved secure command bundle that CES can publish into
|
|
5
|
-
* its private immutable toolstore. Each manifest records:
|
|
6
|
-
*
|
|
7
|
-
* - **sourceUrl** — The canonical URL the bundle was fetched from.
|
|
8
|
-
* - **expectedDigest** — SHA-256 hex digest that the downloaded bytes
|
|
9
|
-
* must match before publication.
|
|
10
|
-
* - **bundleId** — Unique identifier for the command bundle
|
|
11
|
-
* (e.g. "gh-cli", "aws-cli").
|
|
12
|
-
* - **version** — Semantic version of the bundle.
|
|
13
|
-
* - **commandProfiles** — Profile names from the secure command manifest
|
|
14
|
-
* that this bundle declares.
|
|
15
|
-
*
|
|
16
|
-
* ## Publishing rules
|
|
17
|
-
*
|
|
18
|
-
* 1. **Only CES can write** — bundles are published into the CES-private
|
|
19
|
-
* data root, which the assistant process cannot reach.
|
|
20
|
-
* 2. **Immutable once published** — a bundle directory keyed by its digest
|
|
21
|
-
* is never overwritten. Re-publishing the same digest is a no-op.
|
|
22
|
-
* 3. **Workspace-origin binaries are never publishable** — bundles must
|
|
23
|
-
* come from a known source URL; arbitrary assistant-provided bytes are
|
|
24
|
-
* rejected.
|
|
25
|
-
* 4. **Publication does not grant credentials** — writing a bundle into
|
|
26
|
-
* the toolstore is purely a content operation. Credential-use grants
|
|
27
|
-
* are managed by a separate subsystem.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import type { SecureCommandManifest } from "../commands/profiles.js";
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Bundle origin
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Describes the provenance of a bundle.
|
|
38
|
-
*
|
|
39
|
-
* `sourceUrl` must be an HTTPS URL. Workspace paths, file:// URLs, and
|
|
40
|
-
* data: URLs are structurally rejected.
|
|
41
|
-
*/
|
|
42
|
-
export interface BundleOrigin {
|
|
43
|
-
/** HTTPS URL from which the bundle was fetched. */
|
|
44
|
-
sourceUrl: string;
|
|
45
|
-
/** ISO-8601 timestamp of when the bundle was fetched. */
|
|
46
|
-
fetchedAt: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Toolstore manifest
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* A toolstore manifest describes a single approved secure command bundle.
|
|
55
|
-
*
|
|
56
|
-
* This is the metadata stored alongside the bundle contents in the
|
|
57
|
-
* content-addressed toolstore directory.
|
|
58
|
-
*/
|
|
59
|
-
export interface ToolstoreManifest {
|
|
60
|
-
/** SHA-256 hex digest of the bundle contents. Content-address key. */
|
|
61
|
-
digest: string;
|
|
62
|
-
|
|
63
|
-
/** Unique identifier for the command bundle. */
|
|
64
|
-
bundleId: string;
|
|
65
|
-
|
|
66
|
-
/** Semantic version of the bundle. */
|
|
67
|
-
version: string;
|
|
68
|
-
|
|
69
|
-
/** Provenance information — where the bundle came from. */
|
|
70
|
-
origin: BundleOrigin;
|
|
71
|
-
|
|
72
|
-
/** Profile names declared in the secure command manifest. */
|
|
73
|
-
declaredProfiles: string[];
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The full secure command manifest embedded in the toolstore manifest.
|
|
77
|
-
* Used for runtime validation without needing to re-parse the bundle.
|
|
78
|
-
*/
|
|
79
|
-
secureCommandManifest: SecureCommandManifest;
|
|
80
|
-
|
|
81
|
-
/** ISO-8601 timestamp of when the bundle was published to the toolstore. */
|
|
82
|
-
publishedAt: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Validation helpers
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
/** Regex for a valid SHA-256 hex digest. */
|
|
90
|
-
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Returns true if the given string is a valid SHA-256 hex digest.
|
|
94
|
-
*/
|
|
95
|
-
export function isValidSha256Hex(digest: string): boolean {
|
|
96
|
-
return SHA256_HEX_PATTERN.test(digest);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Schemes that are structurally rejected as bundle source URLs.
|
|
101
|
-
* Only HTTPS sources are accepted.
|
|
102
|
-
*/
|
|
103
|
-
const REJECTED_URL_SCHEMES = ["file:", "data:", "blob:", "javascript:"];
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Validate a source URL for bundle origin.
|
|
107
|
-
*
|
|
108
|
-
* Accepted: HTTPS URLs only.
|
|
109
|
-
* Rejected: file://, data://, workspace paths, non-URL strings.
|
|
110
|
-
*
|
|
111
|
-
* Returns an error message if invalid, or null if valid.
|
|
112
|
-
*/
|
|
113
|
-
export function validateSourceUrl(sourceUrl: string): string | null {
|
|
114
|
-
if (!sourceUrl || sourceUrl.trim().length === 0) {
|
|
115
|
-
return "sourceUrl is required and must be non-empty.";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Must be a valid URL
|
|
119
|
-
let parsed: URL;
|
|
120
|
-
try {
|
|
121
|
-
parsed = new URL(sourceUrl);
|
|
122
|
-
} catch {
|
|
123
|
-
return `sourceUrl "${sourceUrl}" is not a valid URL. Only HTTPS URLs are accepted.`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Must be HTTPS
|
|
127
|
-
if (parsed.protocol !== "https:") {
|
|
128
|
-
if (REJECTED_URL_SCHEMES.includes(parsed.protocol)) {
|
|
129
|
-
return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted as bundle sources.`;
|
|
130
|
-
}
|
|
131
|
-
return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted.`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Workspace-origin path patterns that are never publishable as bundle
|
|
139
|
-
* sources. These catch attempts to publish assistant-provided bytes
|
|
140
|
-
* directly from the workspace directory.
|
|
141
|
-
*/
|
|
142
|
-
const WORKSPACE_PATH_PATTERNS = [
|
|
143
|
-
/^~?\/?\.vellum\//,
|
|
144
|
-
/\/\.vellum\//,
|
|
145
|
-
/\/workspace\//i,
|
|
146
|
-
] as const;
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Returns true if the given path looks like a workspace-origin path
|
|
150
|
-
* that should never be accepted as a bundle source.
|
|
151
|
-
*/
|
|
152
|
-
export function isWorkspaceOriginPath(path: string): boolean {
|
|
153
|
-
return WORKSPACE_PATH_PATTERNS.some((pattern) => pattern.test(path));
|
|
154
|
-
}
|