@vellumai/credential-executor 0.4.55
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 +55 -0
- package/bun.lock +37 -0
- package/package.json +32 -0
- package/src/__tests__/command-executor.test.ts +1333 -0
- package/src/__tests__/command-validator.test.ts +708 -0
- package/src/__tests__/command-workspace.test.ts +997 -0
- package/src/__tests__/grant-store.test.ts +467 -0
- package/src/__tests__/http-executor.test.ts +1251 -0
- package/src/__tests__/http-policy.test.ts +970 -0
- package/src/__tests__/local-materializers.test.ts +826 -0
- package/src/__tests__/managed-materializers.test.ts +961 -0
- package/src/__tests__/toolstore.test.ts +539 -0
- package/src/__tests__/transport.test.ts +388 -0
- package/src/audit/store.ts +188 -0
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/executor.ts +840 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +282 -0
- package/src/commands/validator.ts +438 -0
- package/src/commands/workspace.ts +512 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +247 -0
- package/src/grants/rpc-handlers.ts +269 -0
- package/src/grants/temporary-store.ts +219 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +540 -0
- package/src/http/path-template.ts +179 -0
- package/src/http/policy.ts +256 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +106 -0
- package/src/main.ts +263 -0
- package/src/managed-main.ts +420 -0
- package/src/materializers/local.ts +300 -0
- package/src/materializers/managed-platform.ts +270 -0
- package/src/paths.ts +137 -0
- package/src/server.ts +636 -0
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +290 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +342 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CES in-memory temporary grant store.
|
|
3
|
+
*
|
|
4
|
+
* Manages grants for `allow_once`, `allow_10m`, and `allow_thread` decisions.
|
|
5
|
+
* All state is in-memory — temporary grants never survive a process restart,
|
|
6
|
+
* which is the desired behaviour for ephemeral approvals.
|
|
7
|
+
*
|
|
8
|
+
* Keying:
|
|
9
|
+
* - `allow_once`: Keyed by proposal hash. Consumed (deleted) on first use.
|
|
10
|
+
* - `allow_10m`: Keyed by proposal hash. Checked for expiry on every read;
|
|
11
|
+
* expired entries are lazily purged.
|
|
12
|
+
* - `allow_thread`: Keyed by proposal hash + conversation ID. Scoped to a
|
|
13
|
+
* single conversation thread.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export type TemporaryGrantKind = "allow_once" | "allow_10m" | "allow_thread";
|
|
21
|
+
|
|
22
|
+
export interface TemporaryGrant {
|
|
23
|
+
/** The kind of temporary grant. */
|
|
24
|
+
kind: TemporaryGrantKind;
|
|
25
|
+
/** Canonical proposal hash identifying the operation being granted. */
|
|
26
|
+
proposalHash: string;
|
|
27
|
+
/** Conversation ID — required for `allow_thread`, ignored otherwise. */
|
|
28
|
+
conversationId?: string;
|
|
29
|
+
/** When the grant was created (epoch ms). */
|
|
30
|
+
createdAt: number;
|
|
31
|
+
/** When the grant expires (epoch ms). Only set for `allow_10m`. */
|
|
32
|
+
expiresAt?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Default TTL for timed grants (10 minutes). */
|
|
36
|
+
const DEFAULT_TIMED_DURATION_MS = 10 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Store implementation
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute the storage key for a temporary grant.
|
|
44
|
+
*
|
|
45
|
+
* - `allow_once` / `allow_10m`: keyed by proposal hash alone.
|
|
46
|
+
* - `allow_thread`: keyed by proposal hash + conversation ID.
|
|
47
|
+
*/
|
|
48
|
+
function storageKey(
|
|
49
|
+
kind: TemporaryGrantKind,
|
|
50
|
+
proposalHash: string,
|
|
51
|
+
conversationId?: string,
|
|
52
|
+
): string {
|
|
53
|
+
if (kind === "allow_thread") {
|
|
54
|
+
if (!conversationId) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"allow_thread grants require a conversationId",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return `thread:${conversationId}:${proposalHash}`;
|
|
60
|
+
}
|
|
61
|
+
return `${kind}:${proposalHash}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class TemporaryGrantStore {
|
|
65
|
+
private readonly store = new Map<string, TemporaryGrant>();
|
|
66
|
+
|
|
67
|
+
// -----------------------------------------------------------------------
|
|
68
|
+
// Public API
|
|
69
|
+
// -----------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record a temporary grant.
|
|
73
|
+
*
|
|
74
|
+
* For `allow_once` and `allow_10m`, if a grant with the same proposal
|
|
75
|
+
* hash already exists, it is replaced (last-write-wins).
|
|
76
|
+
*
|
|
77
|
+
* @param kind - The type of temporary grant.
|
|
78
|
+
* @param proposalHash - Canonical hash of the operation proposal.
|
|
79
|
+
* @param options - Additional options (conversationId for thread grants,
|
|
80
|
+
* custom duration for timed grants).
|
|
81
|
+
*/
|
|
82
|
+
add(
|
|
83
|
+
kind: TemporaryGrantKind,
|
|
84
|
+
proposalHash: string,
|
|
85
|
+
options?: {
|
|
86
|
+
conversationId?: string;
|
|
87
|
+
durationMs?: number;
|
|
88
|
+
},
|
|
89
|
+
): void {
|
|
90
|
+
const key = storageKey(kind, proposalHash, options?.conversationId);
|
|
91
|
+
|
|
92
|
+
const grant: TemporaryGrant = {
|
|
93
|
+
kind,
|
|
94
|
+
proposalHash,
|
|
95
|
+
createdAt: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (options?.conversationId) {
|
|
99
|
+
grant.conversationId = options.conversationId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (kind === "allow_10m") {
|
|
103
|
+
grant.expiresAt =
|
|
104
|
+
Date.now() + (options?.durationMs ?? DEFAULT_TIMED_DURATION_MS);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.store.set(key, grant);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check whether an active temporary grant exists for the given proposal.
|
|
112
|
+
*
|
|
113
|
+
* - `allow_once`: Returns `true` and **consumes** the grant (deletes it).
|
|
114
|
+
* - `allow_10m`: Returns `true` only if the grant has not expired.
|
|
115
|
+
* Expired grants are lazily purged.
|
|
116
|
+
* - `allow_thread`: Returns `true` only if a grant exists for the given
|
|
117
|
+
* proposal hash scoped to the specified conversation ID.
|
|
118
|
+
*
|
|
119
|
+
* Returns `false` if no matching grant exists.
|
|
120
|
+
*/
|
|
121
|
+
check(
|
|
122
|
+
kind: TemporaryGrantKind,
|
|
123
|
+
proposalHash: string,
|
|
124
|
+
conversationId?: string,
|
|
125
|
+
): boolean {
|
|
126
|
+
const key = storageKey(kind, proposalHash, conversationId);
|
|
127
|
+
const grant = this.store.get(key);
|
|
128
|
+
if (!grant) return false;
|
|
129
|
+
|
|
130
|
+
if (grant.kind === "allow_once") {
|
|
131
|
+
// Consume on first use
|
|
132
|
+
this.store.delete(key);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (grant.kind === "allow_10m") {
|
|
137
|
+
if (grant.expiresAt !== undefined && Date.now() >= grant.expiresAt) {
|
|
138
|
+
// Expired — purge and deny
|
|
139
|
+
this.store.delete(key);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// allow_thread — no expiry, just existence check
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check whether any kind of active temporary grant exists for the given
|
|
151
|
+
* proposal hash and optional conversation ID.
|
|
152
|
+
*
|
|
153
|
+
* Checks `allow_once`, `allow_10m`, and `allow_thread` in order.
|
|
154
|
+
* Returns the kind of the matched grant, or `undefined` if none match.
|
|
155
|
+
*
|
|
156
|
+
* Note: If an `allow_once` grant matches, it is consumed.
|
|
157
|
+
*/
|
|
158
|
+
checkAny(
|
|
159
|
+
proposalHash: string,
|
|
160
|
+
conversationId?: string,
|
|
161
|
+
): TemporaryGrantKind | undefined {
|
|
162
|
+
// Check allow_once first (most specific / single-use)
|
|
163
|
+
if (this.check("allow_once", proposalHash)) return "allow_once";
|
|
164
|
+
|
|
165
|
+
// Check allow_10m
|
|
166
|
+
if (this.check("allow_10m", proposalHash)) return "allow_10m";
|
|
167
|
+
|
|
168
|
+
// Check allow_thread (requires conversationId)
|
|
169
|
+
if (conversationId && this.check("allow_thread", proposalHash, conversationId)) {
|
|
170
|
+
return "allow_thread";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Remove a specific temporary grant.
|
|
178
|
+
*
|
|
179
|
+
* Returns `true` if the grant existed and was removed.
|
|
180
|
+
*/
|
|
181
|
+
remove(
|
|
182
|
+
kind: TemporaryGrantKind,
|
|
183
|
+
proposalHash: string,
|
|
184
|
+
conversationId?: string,
|
|
185
|
+
): boolean {
|
|
186
|
+
const key = storageKey(kind, proposalHash, conversationId);
|
|
187
|
+
return this.store.delete(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove all temporary grants for a given conversation ID.
|
|
192
|
+
*
|
|
193
|
+
* Useful when a conversation/thread ends. Only removes `allow_thread`
|
|
194
|
+
* grants scoped to that conversation.
|
|
195
|
+
*/
|
|
196
|
+
clearConversation(conversationId: string): void {
|
|
197
|
+
const prefix = `thread:${conversationId}:`;
|
|
198
|
+
for (const key of this.store.keys()) {
|
|
199
|
+
if (key.startsWith(prefix)) {
|
|
200
|
+
this.store.delete(key);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove all temporary grants. Useful for testing or full reset.
|
|
207
|
+
*/
|
|
208
|
+
clear(): void {
|
|
209
|
+
this.store.clear();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Return the number of currently stored grants (including expired ones
|
|
214
|
+
* that haven't been lazily purged yet).
|
|
215
|
+
*/
|
|
216
|
+
get size(): number {
|
|
217
|
+
return this.store.size;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP audit summary generation for the Credential Execution Service.
|
|
3
|
+
*
|
|
4
|
+
* Produces token-free audit summaries of credentialed HTTP operations.
|
|
5
|
+
* These summaries are stored in the CES audit log and may be exposed
|
|
6
|
+
* to the assistant runtime for observability — they must never contain
|
|
7
|
+
* secret values, auth tokens, or raw credential material.
|
|
8
|
+
*
|
|
9
|
+
* Audit summaries capture:
|
|
10
|
+
* - What was accessed (method, URL template, status code)
|
|
11
|
+
* - Which credential and grant were used
|
|
12
|
+
* - Whether the operation succeeded
|
|
13
|
+
* - Timing metadata
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
|
|
18
|
+
import type { AuditRecordSummary } from "@vellumai/ces-contracts";
|
|
19
|
+
import { derivePathTemplate } from "./path-template.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface HttpAuditInput {
|
|
26
|
+
/** CES credential handle used for this request. */
|
|
27
|
+
credentialHandle: string;
|
|
28
|
+
/** Grant ID that authorised this request. */
|
|
29
|
+
grantId: string;
|
|
30
|
+
/** CES session ID. */
|
|
31
|
+
sessionId: string;
|
|
32
|
+
/** HTTP method. */
|
|
33
|
+
method: string;
|
|
34
|
+
/** Raw target URL (will be templated for the audit record). */
|
|
35
|
+
url: string;
|
|
36
|
+
/** Whether the HTTP operation succeeded. */
|
|
37
|
+
success: boolean;
|
|
38
|
+
/** HTTP status code (if available). */
|
|
39
|
+
statusCode?: number;
|
|
40
|
+
/** Error message if the operation failed (must not contain secrets). */
|
|
41
|
+
errorMessage?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Summary generation
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate a token-free audit record summary for an HTTP operation.
|
|
50
|
+
*
|
|
51
|
+
* The `target` field uses the path template (with placeholders) rather
|
|
52
|
+
* than the raw URL to avoid leaking path-level identifiers that might
|
|
53
|
+
* be sensitive (e.g. personal resource IDs). The method is prepended
|
|
54
|
+
* for readability: `GET https://api.example.com/users/{:num}`.
|
|
55
|
+
*/
|
|
56
|
+
export function generateHttpAuditSummary(
|
|
57
|
+
input: HttpAuditInput,
|
|
58
|
+
): AuditRecordSummary {
|
|
59
|
+
let target: string;
|
|
60
|
+
try {
|
|
61
|
+
const template = derivePathTemplate(input.url);
|
|
62
|
+
target = `${input.method.toUpperCase()} ${template}`;
|
|
63
|
+
} catch {
|
|
64
|
+
// If URL parsing fails, use a safe redacted placeholder
|
|
65
|
+
target = `${input.method.toUpperCase()} [invalid-url]`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Append status code if available
|
|
69
|
+
if (input.statusCode !== undefined) {
|
|
70
|
+
target += ` -> ${input.statusCode}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
auditId: randomUUID(),
|
|
75
|
+
grantId: input.grantId,
|
|
76
|
+
credentialHandle: input.credentialHandle,
|
|
77
|
+
toolName: "http",
|
|
78
|
+
target,
|
|
79
|
+
sessionId: input.sessionId,
|
|
80
|
+
success: input.success,
|
|
81
|
+
...(input.errorMessage ? { errorMessage: input.errorMessage } : {}),
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
}
|