@vellumai/credential-executor 0.10.7-dev.202607102035.64f07ea → 0.10.7-staging.1
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 +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +686 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +5 -3
- package/node_modules/@vellumai/service-contracts/src/grants.ts +184 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +4 -2
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +447 -4
- package/package.json +3 -2
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/command-executor.test.ts +1879 -0
- package/src/__tests__/command-validator.test.ts +1405 -0
- package/src/__tests__/command-workspace.test.ts +1050 -0
- package/src/__tests__/grant-store.test.ts +689 -0
- package/src/__tests__/http-executor.test.ts +1336 -0
- package/src/__tests__/http-policy.test.ts +1069 -0
- package/src/__tests__/local-materializers.test.ts +860 -0
- package/src/__tests__/local-standalone.test.ts +36 -5
- package/src/__tests__/local-token-refresh.test.ts +361 -0
- package/src/__tests__/manage-secure-command-tool.test.ts +134 -0
- package/src/__tests__/managed-integration.test.ts +91 -112
- package/src/__tests__/managed-lazy-getters.test.ts +359 -0
- package/src/__tests__/managed-materializers.test.ts +1028 -0
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/managed-rejection.test.ts +43 -0
- package/src/__tests__/toolstore.test.ts +773 -0
- package/src/__tests__/transport.test.ts +27 -23
- package/src/audit/store.ts +188 -0
- package/src/cli.ts +1 -1
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/egress-hooks.ts +203 -0
- package/src/commands/executor.ts +1155 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +286 -0
- package/src/commands/validator.ts +702 -0
- package/src/commands/workspace.ts +550 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +309 -0
- package/src/grants/rpc-handlers.ts +293 -0
- package/src/grants/temporary-store.ts +289 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +684 -0
- package/src/http/path-template.ts +245 -0
- package/src/http/policy.ts +238 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +88 -8
- package/src/main.ts +340 -228
- package/src/managed-errors.ts +9 -0
- package/src/managed-lazy-getters.ts +106 -0
- package/src/managed-main.ts +822 -0
- package/src/materializers/local-oauth-lookup.ts +98 -0
- package/src/materializers/local-token-refresh.ts +287 -0
- package/src/materializers/local.ts +316 -0
- package/src/materializers/managed-platform.ts +295 -0
- package/src/paths.ts +20 -4
- package/src/server.ts +469 -52
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +311 -0
- package/src/subjects/policy.ts +79 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +571 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +0 -104
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +0 -118
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP response filtering for the Credential Execution Service.
|
|
3
|
+
*
|
|
4
|
+
* Sanitises raw HTTP responses before returning them to the untrusted
|
|
5
|
+
* assistant runtime. The assistant must never receive:
|
|
6
|
+
* - Raw auth-bearing response headers (e.g. `set-cookie`, `www-authenticate`)
|
|
7
|
+
* - Echoed secret values in response bodies (defense-in-depth scrubbing)
|
|
8
|
+
* - Unbounded response bodies that could exhaust memory
|
|
9
|
+
*
|
|
10
|
+
* The filter also produces a token-free audit summary of every HTTP
|
|
11
|
+
* interaction for the CES audit log.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Configuration
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Maximum response body size returned to the assistant (256 KB).
|
|
20
|
+
*
|
|
21
|
+
* Responses larger than this are truncated with a suffix indicating
|
|
22
|
+
* the original size. The full body is never stored — this is a hard
|
|
23
|
+
* clamp, not a soft limit.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Response headers that are safe to pass through to the assistant.
|
|
29
|
+
*
|
|
30
|
+
* Only these headers are included in the sanitised response.
|
|
31
|
+
* Everything else is stripped — especially `set-cookie`,
|
|
32
|
+
* `www-authenticate`, and any custom auth headers.
|
|
33
|
+
*/
|
|
34
|
+
const ALLOWED_RESPONSE_HEADERS = new Set([
|
|
35
|
+
"content-type",
|
|
36
|
+
"content-length",
|
|
37
|
+
"content-encoding",
|
|
38
|
+
"content-language",
|
|
39
|
+
"content-disposition",
|
|
40
|
+
"cache-control",
|
|
41
|
+
"etag",
|
|
42
|
+
"last-modified",
|
|
43
|
+
"date",
|
|
44
|
+
"x-request-id",
|
|
45
|
+
"x-ratelimit-limit",
|
|
46
|
+
"x-ratelimit-remaining",
|
|
47
|
+
"x-ratelimit-reset",
|
|
48
|
+
"retry-after",
|
|
49
|
+
"link",
|
|
50
|
+
"location",
|
|
51
|
+
"vary",
|
|
52
|
+
"accept-ranges",
|
|
53
|
+
"access-control-allow-origin",
|
|
54
|
+
"access-control-allow-methods",
|
|
55
|
+
"access-control-allow-headers",
|
|
56
|
+
"access-control-expose-headers",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Types
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** Raw HTTP response from the outbound call. */
|
|
64
|
+
export interface RawHttpResponse {
|
|
65
|
+
/** HTTP status code. */
|
|
66
|
+
statusCode: number;
|
|
67
|
+
/** Raw response headers (key-value pairs, header names may be mixed case). */
|
|
68
|
+
headers: Record<string, string>;
|
|
69
|
+
/** Response body as a string. */
|
|
70
|
+
body: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Sanitised HTTP response safe for the assistant runtime. */
|
|
74
|
+
export interface SanitisedHttpResponse {
|
|
75
|
+
/** HTTP status code (passed through). */
|
|
76
|
+
statusCode: number;
|
|
77
|
+
/** Whitelisted response headers (lowercased keys). */
|
|
78
|
+
headers: Record<string, string>;
|
|
79
|
+
/** Body clamped to MAX_BODY_BYTES with secrets scrubbed. */
|
|
80
|
+
body: string;
|
|
81
|
+
/** Whether the body was truncated. */
|
|
82
|
+
truncated: boolean;
|
|
83
|
+
/** Original body size in bytes (before truncation). */
|
|
84
|
+
originalBodyBytes: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Header filtering
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Filter response headers to only include whitelisted safe headers.
|
|
93
|
+
*
|
|
94
|
+
* All header names are lowercased for consistent comparison.
|
|
95
|
+
*/
|
|
96
|
+
export function filterResponseHeaders(
|
|
97
|
+
headers: Record<string, string>,
|
|
98
|
+
): Record<string, string> {
|
|
99
|
+
const filtered: Record<string, string> = {};
|
|
100
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
101
|
+
if (ALLOWED_RESPONSE_HEADERS.has(key.toLowerCase())) {
|
|
102
|
+
filtered[key.toLowerCase()] = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return filtered;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Body clamping
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Clamp the response body to the maximum allowed size.
|
|
114
|
+
*
|
|
115
|
+
* Returns the (possibly truncated) body and metadata about truncation.
|
|
116
|
+
*/
|
|
117
|
+
export function clampBody(body: string): {
|
|
118
|
+
clampedBody: string;
|
|
119
|
+
truncated: boolean;
|
|
120
|
+
originalBytes: number;
|
|
121
|
+
} {
|
|
122
|
+
const bodyBytes = Buffer.byteLength(body, "utf-8");
|
|
123
|
+
|
|
124
|
+
if (bodyBytes <= MAX_BODY_BYTES) {
|
|
125
|
+
return {
|
|
126
|
+
clampedBody: body,
|
|
127
|
+
truncated: false,
|
|
128
|
+
originalBytes: bodyBytes,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Truncate to MAX_BODY_BYTES. We use Buffer to handle multi-byte characters
|
|
133
|
+
// correctly — slice at byte boundaries and convert back to string.
|
|
134
|
+
const buf = Buffer.from(body, "utf-8");
|
|
135
|
+
const truncatedBuf = buf.subarray(0, MAX_BODY_BYTES);
|
|
136
|
+
|
|
137
|
+
// Decode back to string; incomplete multi-byte sequences at the end are
|
|
138
|
+
// replaced with the Unicode replacement character, which is acceptable
|
|
139
|
+
// for a truncated preview.
|
|
140
|
+
const truncatedBody = truncatedBuf.toString("utf-8");
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
clampedBody:
|
|
144
|
+
truncatedBody +
|
|
145
|
+
`\n\n[CES: Response truncated from ${bodyBytes} bytes to ${MAX_BODY_BYTES} bytes]`,
|
|
146
|
+
truncated: true,
|
|
147
|
+
originalBytes: bodyBytes,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Secret scrubbing (defense-in-depth)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Scrub exact occurrences of known secret values from a response body.
|
|
157
|
+
*
|
|
158
|
+
* This is a defense-in-depth measure for APIs that echo back auth tokens
|
|
159
|
+
* or API keys in their response bodies. The scrubbing replaces exact
|
|
160
|
+
* matches of the secret with a redacted placeholder.
|
|
161
|
+
*
|
|
162
|
+
* Limitations:
|
|
163
|
+
* - Only scrubs exact matches (no partial or encoded variants).
|
|
164
|
+
* - Short secrets (< 8 characters) are skipped to avoid false positives.
|
|
165
|
+
* - This is NOT a primary security control — the grant system and
|
|
166
|
+
* credential isolation are the real boundaries.
|
|
167
|
+
*/
|
|
168
|
+
export function scrubSecrets(body: string, secrets: string[]): string {
|
|
169
|
+
let result = body;
|
|
170
|
+
for (const secret of secrets) {
|
|
171
|
+
// Skip short secrets to avoid false positives with common substrings
|
|
172
|
+
if (secret.length < 8) continue;
|
|
173
|
+
// Use a simple global replace — secrets are treated as literal strings
|
|
174
|
+
result = replaceAll(result, secret, "[CES:REDACTED]");
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Replace all occurrences of `search` in `str` with `replacement`.
|
|
181
|
+
*
|
|
182
|
+
* Uses a simple loop to avoid regex special-character escaping issues
|
|
183
|
+
* with secret values that may contain regex metacharacters.
|
|
184
|
+
*/
|
|
185
|
+
function replaceAll(str: string, search: string, replacement: string): string {
|
|
186
|
+
if (search.length === 0) return str;
|
|
187
|
+
|
|
188
|
+
let result = "";
|
|
189
|
+
let idx = 0;
|
|
190
|
+
while (idx < str.length) {
|
|
191
|
+
const foundAt = str.indexOf(search, idx);
|
|
192
|
+
if (foundAt === -1) {
|
|
193
|
+
result += str.slice(idx);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
result += str.slice(idx, foundAt) + replacement;
|
|
197
|
+
idx = foundAt + search.length;
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Full response filter
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Apply the full sanitisation pipeline to a raw HTTP response.
|
|
208
|
+
*
|
|
209
|
+
* Pipeline:
|
|
210
|
+
* 1. Filter response headers to the whitelist.
|
|
211
|
+
* 2. Clamp the body to MAX_BODY_BYTES.
|
|
212
|
+
* 3. Scrub known secrets from the (already clamped) body.
|
|
213
|
+
*
|
|
214
|
+
* @param raw - The raw HTTP response from the outbound call.
|
|
215
|
+
* @param secrets - Known secret values to scrub from the body.
|
|
216
|
+
* @returns Sanitised response safe for the assistant runtime.
|
|
217
|
+
*/
|
|
218
|
+
export function filterHttpResponse(
|
|
219
|
+
raw: RawHttpResponse,
|
|
220
|
+
secrets: string[] = [],
|
|
221
|
+
): SanitisedHttpResponse {
|
|
222
|
+
const filteredHeaders = filterResponseHeaders(raw.headers);
|
|
223
|
+
const { clampedBody, truncated, originalBytes } = clampBody(raw.body);
|
|
224
|
+
const scrubbedBody = scrubSecrets(clampedBody, secrets);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
statusCode: raw.statusCode,
|
|
228
|
+
headers: filteredHeaders,
|
|
229
|
+
body: scrubbedBody,
|
|
230
|
+
truncated,
|
|
231
|
+
originalBodyBytes: originalBytes,
|
|
232
|
+
};
|
|
233
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,28 +2,108 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @vellumai/credential-executor
|
|
4
4
|
*
|
|
5
|
-
* Credential Execution Service (CES) — an isolated runtime that
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Credential Execution Service (CES) — an isolated runtime that executes
|
|
6
|
+
* credential-bearing tool operations on behalf of untrusted agents. The CES
|
|
7
|
+
* receives RPC requests from the assistant daemon, materialises credentials
|
|
8
|
+
* from the local credential store, executes the requested operation through
|
|
9
|
+
* the egress proxy, and returns sanitised results.
|
|
9
10
|
*
|
|
10
|
-
* This module re-exports the public API surface. For
|
|
11
|
-
* `main.ts` —
|
|
12
|
-
*
|
|
11
|
+
* This module re-exports the public API surface. For entrypoints see:
|
|
12
|
+
* - `main.ts` — local mode (stdio transport, child process)
|
|
13
|
+
* - `managed-main.ts` — managed mode (Unix socket transport, sidecar)
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
CesRpcServer,
|
|
18
|
+
createCesServer,
|
|
19
|
+
createRunAuthenticatedCommandHandler,
|
|
20
|
+
registerCommandExecutionHandler,
|
|
21
|
+
createMakeAuthenticatedRequestHandler,
|
|
22
|
+
createManageSecureCommandToolHandler,
|
|
23
|
+
registerManageSecureCommandToolHandler,
|
|
24
|
+
buildHandlersWithHttp,
|
|
25
|
+
} from "./server.js";
|
|
16
26
|
export type {
|
|
17
27
|
CesServerOptions,
|
|
28
|
+
ManageSecureCommandToolHandlerDeps,
|
|
18
29
|
RpcHandlerRegistry,
|
|
19
30
|
RpcMethodHandler,
|
|
31
|
+
RunAuthenticatedCommandHandlerOptions,
|
|
20
32
|
SessionContext,
|
|
21
33
|
} from "./server.js";
|
|
22
34
|
|
|
23
35
|
export {
|
|
24
36
|
getCesDataRoot,
|
|
37
|
+
getCesGrantsDir,
|
|
38
|
+
getCesAuditDir,
|
|
39
|
+
getCesToolStoreDir,
|
|
25
40
|
getCesMode,
|
|
26
41
|
getBootstrapSocketPath,
|
|
27
42
|
getHealthPort,
|
|
28
43
|
} from "./paths.js";
|
|
29
44
|
export type { CesMode } from "./paths.js";
|
|
45
|
+
|
|
46
|
+
export { PersistentGrantStore, TemporaryGrantStore } from "./grants/index.js";
|
|
47
|
+
export type {
|
|
48
|
+
PersistentGrant,
|
|
49
|
+
TemporaryGrant,
|
|
50
|
+
TemporaryGrantKind,
|
|
51
|
+
} from "./grants/index.js";
|
|
52
|
+
|
|
53
|
+
export { computeDigest, verifyDigest } from "./toolstore/integrity.js";
|
|
54
|
+
export type { DigestVerificationResult } from "./toolstore/integrity.js";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
isValidSha256Hex,
|
|
58
|
+
validateSourceUrl,
|
|
59
|
+
isWorkspaceOriginPath,
|
|
60
|
+
} from "./toolstore/manifest.js";
|
|
61
|
+
export type {
|
|
62
|
+
BundleOrigin,
|
|
63
|
+
ToolstoreManifest,
|
|
64
|
+
} from "./toolstore/manifest.js";
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
publishBundle,
|
|
68
|
+
readPublishedManifest,
|
|
69
|
+
isBundlePublished,
|
|
70
|
+
getBundleDir,
|
|
71
|
+
getBundleManifestPath,
|
|
72
|
+
getBundleContentPath,
|
|
73
|
+
} from "./toolstore/publish.js";
|
|
74
|
+
export type {
|
|
75
|
+
PublishRequest,
|
|
76
|
+
PublishResult,
|
|
77
|
+
} from "./toolstore/publish.js";
|
|
78
|
+
|
|
79
|
+
export { resolveLocalSubject } from "./subjects/local.js";
|
|
80
|
+
export type {
|
|
81
|
+
ResolvedStaticSubject,
|
|
82
|
+
ResolvedOAuthSubject,
|
|
83
|
+
ResolvedLocalSubject,
|
|
84
|
+
SubjectResolutionResult,
|
|
85
|
+
OAuthConnectionLookup,
|
|
86
|
+
LocalSubjectResolverDeps,
|
|
87
|
+
} from "./subjects/local.js";
|
|
88
|
+
|
|
89
|
+
export { LocalMaterialiser } from "./materializers/local.js";
|
|
90
|
+
export type {
|
|
91
|
+
MaterialisedCredential,
|
|
92
|
+
MaterialisationResult,
|
|
93
|
+
TokenRefreshFn,
|
|
94
|
+
LocalMaterialiserDeps,
|
|
95
|
+
} from "./materializers/local.js";
|
|
96
|
+
|
|
97
|
+
export { executeAuthenticatedCommand } from "./commands/executor.js";
|
|
98
|
+
export type {
|
|
99
|
+
ExecuteCommandRequest,
|
|
100
|
+
ExecuteCommandResult,
|
|
101
|
+
CommandExecutorDeps,
|
|
102
|
+
MaterializeCredentialFn,
|
|
103
|
+
MaterializeCredentialResult,
|
|
104
|
+
} from "./commands/executor.js";
|
|
105
|
+
|
|
106
|
+
export { executeAuthenticatedHttpRequest } from "./http/executor.js";
|
|
107
|
+
export type { HttpExecutorDeps } from "./http/executor.js";
|
|
108
|
+
|
|
109
|
+
export { MANAGED_LOCAL_STATIC_REJECTION_ERROR } from "./managed-errors.js";
|