@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.
Files changed (42) hide show
  1. package/Dockerfile +55 -0
  2. package/bun.lock +37 -0
  3. package/package.json +32 -0
  4. package/src/__tests__/command-executor.test.ts +1333 -0
  5. package/src/__tests__/command-validator.test.ts +708 -0
  6. package/src/__tests__/command-workspace.test.ts +997 -0
  7. package/src/__tests__/grant-store.test.ts +467 -0
  8. package/src/__tests__/http-executor.test.ts +1251 -0
  9. package/src/__tests__/http-policy.test.ts +970 -0
  10. package/src/__tests__/local-materializers.test.ts +826 -0
  11. package/src/__tests__/managed-materializers.test.ts +961 -0
  12. package/src/__tests__/toolstore.test.ts +539 -0
  13. package/src/__tests__/transport.test.ts +388 -0
  14. package/src/audit/store.ts +188 -0
  15. package/src/commands/auth-adapters.ts +169 -0
  16. package/src/commands/executor.ts +840 -0
  17. package/src/commands/output-scan.ts +157 -0
  18. package/src/commands/profiles.ts +282 -0
  19. package/src/commands/validator.ts +438 -0
  20. package/src/commands/workspace.ts +512 -0
  21. package/src/grants/index.ts +17 -0
  22. package/src/grants/persistent-store.ts +247 -0
  23. package/src/grants/rpc-handlers.ts +269 -0
  24. package/src/grants/temporary-store.ts +219 -0
  25. package/src/http/audit.ts +84 -0
  26. package/src/http/executor.ts +540 -0
  27. package/src/http/path-template.ts +179 -0
  28. package/src/http/policy.ts +256 -0
  29. package/src/http/response-filter.ts +233 -0
  30. package/src/index.ts +106 -0
  31. package/src/main.ts +263 -0
  32. package/src/managed-main.ts +420 -0
  33. package/src/materializers/local.ts +300 -0
  34. package/src/materializers/managed-platform.ts +270 -0
  35. package/src/paths.ts +137 -0
  36. package/src/server.ts +636 -0
  37. package/src/subjects/local.ts +177 -0
  38. package/src/subjects/managed.ts +290 -0
  39. package/src/toolstore/integrity.ts +94 -0
  40. package/src/toolstore/manifest.ts +154 -0
  41. package/src/toolstore/publish.ts +342 -0
  42. 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
+ }