@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
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CES persistent grant store.
|
|
3
|
-
*
|
|
4
|
-
* Stores canonical validated grants by stable ID in a `grants.json` file
|
|
5
|
-
* inside the CES-private data root. This file is never co-mingled with
|
|
6
|
-
* assistant trust files or credential metadata.
|
|
7
|
-
*
|
|
8
|
-
* Design principles:
|
|
9
|
-
* - **Fail closed**: If the store file is unreadable or malformed, all
|
|
10
|
-
* reads return empty and all writes throw. The CES must never fall back
|
|
11
|
-
* to a permissive default when the persistent state is corrupt.
|
|
12
|
-
* - **Atomic writes**: Uses rename-over-tmp to prevent partial writes.
|
|
13
|
-
* - **Deduplication**: Grants are keyed by a canonical hash (the `id`
|
|
14
|
-
* field) — adding an active grant with an existing ID is a no-op.
|
|
15
|
-
* Revoked grants with the same ID are reactivated (upsert).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
chmodSync,
|
|
20
|
-
existsSync,
|
|
21
|
-
mkdirSync,
|
|
22
|
-
readFileSync,
|
|
23
|
-
renameSync,
|
|
24
|
-
writeFileSync,
|
|
25
|
-
} from "node:fs";
|
|
26
|
-
import { dirname, join } from "node:path";
|
|
27
|
-
import { randomUUID } from "node:crypto";
|
|
28
|
-
|
|
29
|
-
import { getCesGrantsDir } from "../paths.js";
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Types
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/** A canonical persistent grant stored on disk. */
|
|
36
|
-
export interface PersistentGrant {
|
|
37
|
-
/** Stable canonical hash identifying this grant. */
|
|
38
|
-
id: string;
|
|
39
|
-
/** The tool or command pattern this grant authorises. */
|
|
40
|
-
tool: string;
|
|
41
|
-
/** Glob pattern scoping the grant. */
|
|
42
|
-
pattern: string;
|
|
43
|
-
/** Scope constraint (directory path or "everywhere"). */
|
|
44
|
-
scope: string;
|
|
45
|
-
/** When the grant was created (epoch ms). */
|
|
46
|
-
createdAt: number;
|
|
47
|
-
/** The agent session that created this grant. Backfilled to "unknown" on legacy grants. */
|
|
48
|
-
sessionId: string;
|
|
49
|
-
/** When the grant was revoked (epoch ms), or undefined if active. */
|
|
50
|
-
revokedAt?: number;
|
|
51
|
-
/** Human-readable reason for revocation. */
|
|
52
|
-
revokedReason?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** On-disk format for the grants file. */
|
|
56
|
-
interface GrantsFile {
|
|
57
|
-
version: number;
|
|
58
|
-
grants: PersistentGrant[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const GRANTS_FILE_VERSION = 1;
|
|
62
|
-
const GRANTS_FILENAME = "grants.json";
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Store implementation
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
export class PersistentGrantStore {
|
|
69
|
-
private readonly filePath: string;
|
|
70
|
-
/** Set to true when the store detects corruption; blocks all operations. */
|
|
71
|
-
private corrupt = false;
|
|
72
|
-
|
|
73
|
-
constructor(grantsDir?: string) {
|
|
74
|
-
const dir = grantsDir ?? getCesGrantsDir();
|
|
75
|
-
this.filePath = join(dir, GRANTS_FILENAME);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// -----------------------------------------------------------------------
|
|
79
|
-
// Public API
|
|
80
|
-
// -----------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Initialise the store, ensuring the parent directory exists and the
|
|
84
|
-
* grants file is readable. If the file does not exist it is created
|
|
85
|
-
* with an empty grant list.
|
|
86
|
-
*
|
|
87
|
-
* Throws if the directory cannot be created or an existing file is
|
|
88
|
-
* malformed (fail-closed).
|
|
89
|
-
*/
|
|
90
|
-
init(): void {
|
|
91
|
-
const dir = dirname(this.filePath);
|
|
92
|
-
if (!existsSync(dir)) {
|
|
93
|
-
mkdirSync(dir, { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!existsSync(this.filePath)) {
|
|
97
|
-
this.writeToDisk([]);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Validate the existing file is readable and well-formed.
|
|
102
|
-
// If it isn't, mark corrupt and throw (fail closed).
|
|
103
|
-
const grants = this.loadFromDisk();
|
|
104
|
-
|
|
105
|
-
// Migration: backfill sessionId on legacy grants that pre-date the field.
|
|
106
|
-
let migrated = false;
|
|
107
|
-
for (const grant of grants) {
|
|
108
|
-
if (grant.sessionId == null) {
|
|
109
|
-
(grant as { sessionId: string }).sessionId = "unknown";
|
|
110
|
-
migrated = true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (migrated) {
|
|
114
|
-
this.writeToDisk(grants);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Return all persisted grants that are not revoked.
|
|
120
|
-
*
|
|
121
|
-
* Returns an empty array if the store has never been initialised
|
|
122
|
-
* (no file on disk). Throws if the store is corrupt.
|
|
123
|
-
*/
|
|
124
|
-
getAll(): PersistentGrant[] {
|
|
125
|
-
return this.getAllIncludingRevoked().filter((g) => g.revokedAt == null);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Return all persisted grants including revoked ones.
|
|
130
|
-
*
|
|
131
|
-
* Used by the listing handler to expose the full audit trail.
|
|
132
|
-
*/
|
|
133
|
-
getAllIncludingRevoked(): PersistentGrant[] {
|
|
134
|
-
this.assertNotCorrupt();
|
|
135
|
-
if (!existsSync(this.filePath)) return [];
|
|
136
|
-
return [...this.loadFromDisk()];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Look up a grant by its canonical ID (active grants only).
|
|
141
|
-
*
|
|
142
|
-
* Returns `undefined` if not found or revoked. Throws if the store is corrupt.
|
|
143
|
-
*/
|
|
144
|
-
getById(id: string): PersistentGrant | undefined {
|
|
145
|
-
return this.getAll().find((g) => g.id === id);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Add a grant. If an active grant with the same `id` already exists,
|
|
150
|
-
* this is a no-op (idempotent deduplication by canonical hash).
|
|
151
|
-
*
|
|
152
|
-
* If a revoked grant with the same `id` exists, it is reactivated
|
|
153
|
-
* with the new grant's fields — this supports the revoke-then-re-approve
|
|
154
|
-
* workflow where the same proposal hash is re-granted.
|
|
155
|
-
*
|
|
156
|
-
* Returns `true` if the grant was newly added or reactivated, `false`
|
|
157
|
-
* if it was a duplicate of an already-active grant.
|
|
158
|
-
*/
|
|
159
|
-
add(grant: PersistentGrant): boolean {
|
|
160
|
-
this.assertNotCorrupt();
|
|
161
|
-
const grants = this.loadFromDisk();
|
|
162
|
-
const existing = grants.find((g) => g.id === grant.id);
|
|
163
|
-
if (existing) {
|
|
164
|
-
// Already active — deduplicate as before.
|
|
165
|
-
if (existing.revokedAt == null) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
// Revoked — reactivate with fresh fields.
|
|
169
|
-
existing.tool = grant.tool;
|
|
170
|
-
existing.pattern = grant.pattern;
|
|
171
|
-
existing.scope = grant.scope;
|
|
172
|
-
existing.createdAt = grant.createdAt;
|
|
173
|
-
existing.sessionId = grant.sessionId;
|
|
174
|
-
existing.revokedAt = undefined;
|
|
175
|
-
existing.revokedReason = undefined;
|
|
176
|
-
this.writeToDisk(grants);
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
grants.push(grant);
|
|
180
|
-
this.writeToDisk(grants);
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Remove a grant by its canonical ID (hard delete).
|
|
186
|
-
*
|
|
187
|
-
* Returns `true` if the grant was found and removed, `false` otherwise.
|
|
188
|
-
*
|
|
189
|
-
* Prefer `markRevoked()` for audit-preserving revocation.
|
|
190
|
-
*/
|
|
191
|
-
remove(id: string): boolean {
|
|
192
|
-
this.assertNotCorrupt();
|
|
193
|
-
const grants = this.loadFromDisk();
|
|
194
|
-
const index = grants.findIndex((g) => g.id === id);
|
|
195
|
-
if (index === -1) return false;
|
|
196
|
-
grants.splice(index, 1);
|
|
197
|
-
this.writeToDisk(grants);
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Mark a grant as revoked by its canonical ID. The grant remains
|
|
203
|
-
* on disk for audit purposes but is excluded from `getAll()` and
|
|
204
|
-
* `getById()` lookups.
|
|
205
|
-
*
|
|
206
|
-
* Returns `true` if the grant was found and marked revoked,
|
|
207
|
-
* `false` if the grant does not exist or is already revoked.
|
|
208
|
-
*/
|
|
209
|
-
markRevoked(id: string, reason?: string): boolean {
|
|
210
|
-
this.assertNotCorrupt();
|
|
211
|
-
const grants = this.loadFromDisk();
|
|
212
|
-
const grant = grants.find((g) => g.id === id);
|
|
213
|
-
if (!grant || grant.revokedAt != null) return false;
|
|
214
|
-
grant.revokedAt = Date.now();
|
|
215
|
-
grant.revokedReason = reason;
|
|
216
|
-
this.writeToDisk(grants);
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Check whether a grant with the given ID exists.
|
|
222
|
-
*/
|
|
223
|
-
has(id: string): boolean {
|
|
224
|
-
return this.getById(id) !== undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Remove all grants and reset the store to an empty state.
|
|
229
|
-
*/
|
|
230
|
-
clear(): void {
|
|
231
|
-
this.assertNotCorrupt();
|
|
232
|
-
this.writeToDisk([]);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// -----------------------------------------------------------------------
|
|
236
|
-
// Internals
|
|
237
|
-
// -----------------------------------------------------------------------
|
|
238
|
-
|
|
239
|
-
private assertNotCorrupt(): void {
|
|
240
|
-
if (this.corrupt) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
"CES persistent grant store is corrupt — refusing to operate (fail closed)",
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private loadFromDisk(): PersistentGrant[] {
|
|
248
|
-
try {
|
|
249
|
-
const raw = readFileSync(this.filePath, "utf-8");
|
|
250
|
-
const data = JSON.parse(raw) as unknown;
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
typeof data !== "object" ||
|
|
254
|
-
data === null ||
|
|
255
|
-
!("version" in data) ||
|
|
256
|
-
!("grants" in data)
|
|
257
|
-
) {
|
|
258
|
-
this.corrupt = true;
|
|
259
|
-
throw new Error(
|
|
260
|
-
"CES grants file is malformed: missing version or grants field",
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const file = data as GrantsFile;
|
|
265
|
-
|
|
266
|
-
if (file.version !== GRANTS_FILE_VERSION) {
|
|
267
|
-
this.corrupt = true;
|
|
268
|
-
throw new Error(
|
|
269
|
-
`CES grants file has unsupported version ${file.version} (expected ${GRANTS_FILE_VERSION})`,
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!Array.isArray(file.grants)) {
|
|
274
|
-
this.corrupt = true;
|
|
275
|
-
throw new Error("CES grants file is malformed: grants is not an array");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return [...file.grants];
|
|
279
|
-
} catch (err) {
|
|
280
|
-
if (this.corrupt) throw err;
|
|
281
|
-
// Any other read/parse error → fail closed
|
|
282
|
-
this.corrupt = true;
|
|
283
|
-
throw new Error(
|
|
284
|
-
`CES persistent grant store failed to read ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private writeToDisk(grants: PersistentGrant[]): void {
|
|
290
|
-
const dir = dirname(this.filePath);
|
|
291
|
-
if (!existsSync(dir)) {
|
|
292
|
-
mkdirSync(dir, { recursive: true });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const data: GrantsFile = {
|
|
296
|
-
version: GRANTS_FILE_VERSION,
|
|
297
|
-
grants,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
301
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
|
|
302
|
-
mode: 0o600,
|
|
303
|
-
});
|
|
304
|
-
renameSync(tmpPath, this.filePath);
|
|
305
|
-
// Enforce owner-only permissions even if the file already existed
|
|
306
|
-
// with wider permissions.
|
|
307
|
-
chmodSync(this.filePath, 0o600);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CES RPC handlers for grant and audit management.
|
|
3
|
-
*
|
|
4
|
-
* Implements the server-side handlers for:
|
|
5
|
-
* - `record_grant` — Record a grant decision after guardian approval.
|
|
6
|
-
* - `list_grants` — List grants filtered by session, handle, or status.
|
|
7
|
-
* - `revoke_grant` — Revoke a specific grant by its stable ID.
|
|
8
|
-
* - `list_audit_records` — List audit records with filtering and pagination.
|
|
9
|
-
*
|
|
10
|
-
* All handlers operate strictly on CES-owned state and never expose raw
|
|
11
|
-
* secret material, raw tokens, or raw headers/bodies. Grant records returned
|
|
12
|
-
* to the assistant contain only metadata (handle, proposal type, status,
|
|
13
|
-
* timestamps).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
ListGrants,
|
|
18
|
-
ListGrantsResponse,
|
|
19
|
-
RecordGrant,
|
|
20
|
-
RecordGrantResponse,
|
|
21
|
-
RevokeGrant,
|
|
22
|
-
RevokeGrantResponse,
|
|
23
|
-
ListAuditRecords,
|
|
24
|
-
ListAuditRecordsResponse,
|
|
25
|
-
PersistentGrantRecord,
|
|
26
|
-
} from "@vellumai/service-contracts/credential-rpc";
|
|
27
|
-
|
|
28
|
-
import type { PersistentGrantStore, PersistentGrant } from "./persistent-store.js";
|
|
29
|
-
import type { TemporaryGrantStore } from "./temporary-store.js";
|
|
30
|
-
import type { AuditStore } from "../audit/store.js";
|
|
31
|
-
import type { RpcMethodHandler } from "../server.js";
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Grant → PersistentGrantRecord projection
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Project a CES internal PersistentGrant into the wire-format
|
|
39
|
-
* PersistentGrantRecord. Maps real fields from the persistent store
|
|
40
|
-
* schema into the wire contract.
|
|
41
|
-
*/
|
|
42
|
-
function projectGrant(grant: PersistentGrant): PersistentGrantRecord {
|
|
43
|
-
return {
|
|
44
|
-
grantId: grant.id,
|
|
45
|
-
sessionId: grant.sessionId,
|
|
46
|
-
credentialHandle: grant.scope,
|
|
47
|
-
proposalType:
|
|
48
|
-
grant.tool === "http" || grant.tool === "command"
|
|
49
|
-
? grant.tool
|
|
50
|
-
: "command",
|
|
51
|
-
proposalHash: grant.id,
|
|
52
|
-
allowedPurposes: [grant.pattern],
|
|
53
|
-
status: grant.revokedAt != null ? "revoked" : "active",
|
|
54
|
-
grantedBy: "user",
|
|
55
|
-
createdAt: new Date(grant.createdAt).toISOString(),
|
|
56
|
-
expiresAt: null,
|
|
57
|
-
consumedAt: null,
|
|
58
|
-
revokedAt:
|
|
59
|
-
grant.revokedAt != null
|
|
60
|
-
? new Date(grant.revokedAt).toISOString()
|
|
61
|
-
: null,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// record_grant handler
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
export interface RecordGrantHandlerDeps {
|
|
70
|
-
persistentGrantStore: PersistentGrantStore;
|
|
71
|
-
temporaryGrantStore: TemporaryGrantStore;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create an RPC handler for the `record_grant` method.
|
|
76
|
-
*
|
|
77
|
-
* Receives a `TemporaryGrantDecision` from the approval bridge and persists
|
|
78
|
-
* it as a `PersistentGrant` (for approved decisions) or returns a success
|
|
79
|
-
* acknowledgement (for denied decisions). The handler also adds an
|
|
80
|
-
* in-memory temporary grant so the caller can immediately retry the
|
|
81
|
-
* original tool invocation.
|
|
82
|
-
*
|
|
83
|
-
* For approved decisions with a TTL of "PT10M", the grant is stored as
|
|
84
|
-
* a timed temporary grant. Otherwise it is persisted as a permanent grant.
|
|
85
|
-
*/
|
|
86
|
-
export function createRecordGrantHandler(
|
|
87
|
-
deps: RecordGrantHandlerDeps,
|
|
88
|
-
): RpcMethodHandler<RecordGrant, RecordGrantResponse> {
|
|
89
|
-
return (request) => {
|
|
90
|
-
const { decision, sessionId } = request;
|
|
91
|
-
|
|
92
|
-
// Denied decisions are acknowledged but produce no grant record.
|
|
93
|
-
if (decision.decision === "denied") {
|
|
94
|
-
return { success: true };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const proposal = decision.proposal;
|
|
98
|
-
const now = Date.now();
|
|
99
|
-
const grantId = decision.proposalHash;
|
|
100
|
-
|
|
101
|
-
// Determine the grant type. When omitted (backwards compat), default
|
|
102
|
-
// to `always_allow` so existing callers that don't send `grantType`
|
|
103
|
-
// continue to create persistent grants.
|
|
104
|
-
const grantType = decision.grantType ?? "always_allow";
|
|
105
|
-
|
|
106
|
-
// Only `always_allow` creates a persistent grant. All other approved
|
|
107
|
-
// decisions create only a temporary grant — this prevents allow_once,
|
|
108
|
-
// allow_10m, and allow_conversation from becoming effectively permanent.
|
|
109
|
-
if (grantType === "always_allow") {
|
|
110
|
-
let pattern: string;
|
|
111
|
-
if (proposal.type === "http") {
|
|
112
|
-
// Use the templated allowedUrlPatterns (e.g. "https://api.example.com/repos/{:uuid}/pulls")
|
|
113
|
-
// so the persistent grant covers future requests with different IDs but the same URL structure.
|
|
114
|
-
// Falls back to the exact URL only if allowedUrlPatterns is missing.
|
|
115
|
-
const urlPattern = proposal.allowedUrlPatterns?.[0] ?? proposal.url;
|
|
116
|
-
pattern = `${proposal.method} ${urlPattern}`;
|
|
117
|
-
} else {
|
|
118
|
-
pattern = proposal.allowedCommandPatterns?.[0] ?? proposal.command;
|
|
119
|
-
}
|
|
120
|
-
const persistentGrant: PersistentGrant = {
|
|
121
|
-
id: grantId,
|
|
122
|
-
tool: proposal.type,
|
|
123
|
-
pattern,
|
|
124
|
-
scope: proposal.credentialHandle,
|
|
125
|
-
createdAt: now,
|
|
126
|
-
sessionId,
|
|
127
|
-
};
|
|
128
|
-
deps.persistentGrantStore.add(persistentGrant);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Record a temporary grant so the caller can use it immediately.
|
|
132
|
-
// For `always_allow`, an `allow_once` temp grant bridges the gap until
|
|
133
|
-
// the next policy check hits the persistent store.
|
|
134
|
-
if (grantType === "allow_10m") {
|
|
135
|
-
deps.temporaryGrantStore.add("allow_10m", decision.proposalHash);
|
|
136
|
-
} else if (grantType === "allow_conversation") {
|
|
137
|
-
deps.temporaryGrantStore.add("allow_conversation", decision.proposalHash, {
|
|
138
|
-
conversationId: request.conversationId ?? sessionId,
|
|
139
|
-
});
|
|
140
|
-
} else {
|
|
141
|
-
// allow_once and always_allow both get a single-use temp grant
|
|
142
|
-
// for immediate retry.
|
|
143
|
-
deps.temporaryGrantStore.add("allow_once", decision.proposalHash);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Compute expiry from TTL if present.
|
|
147
|
-
let expiresAt: string | null = null;
|
|
148
|
-
if (decision.ttl === "PT10M") {
|
|
149
|
-
expiresAt = new Date(now + 10 * 60 * 1000).toISOString();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const grantRecord: PersistentGrantRecord = {
|
|
153
|
-
grantId,
|
|
154
|
-
sessionId,
|
|
155
|
-
credentialHandle: proposal.credentialHandle,
|
|
156
|
-
proposalType: proposal.type,
|
|
157
|
-
proposalHash: decision.proposalHash,
|
|
158
|
-
allowedPurposes:
|
|
159
|
-
proposal.type === "http"
|
|
160
|
-
? proposal.allowedUrlPatterns ?? [`${proposal.method} ${proposal.url}`]
|
|
161
|
-
: proposal.allowedCommandPatterns ?? [proposal.command],
|
|
162
|
-
status: "active",
|
|
163
|
-
grantedBy: decision.decidedBy,
|
|
164
|
-
createdAt: new Date(now).toISOString(),
|
|
165
|
-
expiresAt,
|
|
166
|
-
consumedAt: null,
|
|
167
|
-
revokedAt: null,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
success: true,
|
|
172
|
-
grant: grantRecord,
|
|
173
|
-
};
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// list_grants handler
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
|
|
181
|
-
export interface ListGrantsHandlerDeps {
|
|
182
|
-
persistentGrantStore: PersistentGrantStore;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Create an RPC handler for the `list_grants` method.
|
|
187
|
-
*
|
|
188
|
-
* Lists all persistent grants (including revoked, for audit trail),
|
|
189
|
-
* optionally filtered by session ID, credential handle, or status.
|
|
190
|
-
* Returns wire-format PersistentGrantRecords that never include raw
|
|
191
|
-
* secret material.
|
|
192
|
-
*/
|
|
193
|
-
export function createListGrantsHandler(
|
|
194
|
-
deps: ListGrantsHandlerDeps,
|
|
195
|
-
): RpcMethodHandler<ListGrants, ListGrantsResponse> {
|
|
196
|
-
return (request) => {
|
|
197
|
-
// Include revoked grants in the listing for audit visibility.
|
|
198
|
-
const allGrants = deps.persistentGrantStore.getAllIncludingRevoked();
|
|
199
|
-
const projected = allGrants.map((g) => projectGrant(g));
|
|
200
|
-
|
|
201
|
-
let filtered = projected;
|
|
202
|
-
|
|
203
|
-
if (request.sessionId) {
|
|
204
|
-
filtered = filtered.filter((g) => g.sessionId === request.sessionId);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (request.credentialHandle) {
|
|
208
|
-
filtered = filtered.filter(
|
|
209
|
-
(g) => g.credentialHandle === request.credentialHandle,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (request.status) {
|
|
214
|
-
filtered = filtered.filter((g) => g.status === request.status);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { grants: filtered };
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
// revoke_grant handler
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
export interface RevokeGrantHandlerDeps {
|
|
226
|
-
persistentGrantStore: PersistentGrantStore;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Create an RPC handler for the `revoke_grant` method.
|
|
231
|
-
*
|
|
232
|
-
* Marks a grant as revoked in the persistent store by its stable ID,
|
|
233
|
-
* preserving the record for audit trail. Returns success/failure.
|
|
234
|
-
*/
|
|
235
|
-
export function createRevokeGrantHandler(
|
|
236
|
-
deps: RevokeGrantHandlerDeps,
|
|
237
|
-
): RpcMethodHandler<RevokeGrant, RevokeGrantResponse> {
|
|
238
|
-
return (request) => {
|
|
239
|
-
const revoked = deps.persistentGrantStore.markRevoked(
|
|
240
|
-
request.grantId,
|
|
241
|
-
request.reason,
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
if (!revoked) {
|
|
245
|
-
return {
|
|
246
|
-
success: false,
|
|
247
|
-
error: {
|
|
248
|
-
code: "GRANT_NOT_FOUND",
|
|
249
|
-
message: `No grant found with ID "${request.grantId}" (or already revoked)`,
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return { success: true };
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
// list_audit_records handler
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
export interface ListAuditRecordsHandlerDeps {
|
|
263
|
-
auditStore: AuditStore;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Create an RPC handler for the `list_audit_records` method.
|
|
268
|
-
*
|
|
269
|
-
* Lists audit records with optional filtering by session, credential
|
|
270
|
-
* handle, or grant ID. Supports limit and cursor-based pagination.
|
|
271
|
-
*
|
|
272
|
-
* Audit records never contain raw secrets, raw tokens, or raw
|
|
273
|
-
* headers/bodies — they are token-free summaries generated at
|
|
274
|
-
* execution time.
|
|
275
|
-
*/
|
|
276
|
-
export function createListAuditRecordsHandler(
|
|
277
|
-
deps: ListAuditRecordsHandlerDeps,
|
|
278
|
-
): RpcMethodHandler<ListAuditRecords, ListAuditRecordsResponse> {
|
|
279
|
-
return (request) => {
|
|
280
|
-
const result = deps.auditStore.list({
|
|
281
|
-
sessionId: request.sessionId,
|
|
282
|
-
credentialHandle: request.credentialHandle,
|
|
283
|
-
grantId: request.grantId,
|
|
284
|
-
limit: request.limit,
|
|
285
|
-
cursor: request.cursor,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
records: result.records,
|
|
290
|
-
nextCursor: result.nextCursor,
|
|
291
|
-
};
|
|
292
|
-
};
|
|
293
|
-
}
|