@openape/openclaw-plugin-grants 0.2.0

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/dist/index.js ADDED
@@ -0,0 +1,1424 @@
1
+ // src/types.ts
2
+ var DEFAULT_CONFIG = {
3
+ mode: "local",
4
+ audience: "openclaw",
5
+ defaultApproval: "once",
6
+ pollIntervalMs: 3e3,
7
+ pollTimeoutMs: 3e5
8
+ };
9
+
10
+ // src/adapters/loader.ts
11
+ import { createHash } from "crypto";
12
+ import { existsSync, readFileSync, readdirSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { dirname, join } from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ // src/adapters/toml.ts
18
+ function parseKeyValue(line) {
19
+ const eqIndex = line.indexOf("=");
20
+ if (eqIndex === -1)
21
+ return null;
22
+ const key = line.slice(0, eqIndex).trim();
23
+ const value = line.slice(eqIndex + 1).trim();
24
+ if (!key || !value)
25
+ return null;
26
+ return { key, value };
27
+ }
28
+ function splitTomlArray(inner) {
29
+ const elements = [];
30
+ let current = "";
31
+ let inQuote = false;
32
+ for (let i = 0; i < inner.length; i++) {
33
+ const char = inner[i];
34
+ if (char === '"') {
35
+ inQuote = !inQuote;
36
+ current += char;
37
+ } else if (char === "," && !inQuote) {
38
+ elements.push(current);
39
+ current = "";
40
+ } else {
41
+ current += char;
42
+ }
43
+ }
44
+ if (current.trim())
45
+ elements.push(current);
46
+ return elements;
47
+ }
48
+ function parseTomlValue(raw) {
49
+ const trimmed = raw.trim();
50
+ if (trimmed.startsWith('"') && trimmed.endsWith('"'))
51
+ return trimmed.slice(1, -1);
52
+ if (trimmed === "true")
53
+ return true;
54
+ if (trimmed === "false")
55
+ return false;
56
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
57
+ const inner = trimmed.slice(1, -1).trim();
58
+ if (!inner)
59
+ return [];
60
+ return splitTomlArray(inner).map((value) => value.trim().replace(/^"|"$/g, ""));
61
+ }
62
+ return trimmed;
63
+ }
64
+ function parseAdapterToml(content) {
65
+ const result = {};
66
+ const operations = [];
67
+ let currentSection = "root";
68
+ let currentEntry = {};
69
+ const flushOperation = () => {
70
+ if (currentSection === "operation" && Object.keys(currentEntry).length > 0) {
71
+ operations.push(currentEntry);
72
+ currentEntry = {};
73
+ }
74
+ };
75
+ for (const rawLine of content.split("\n")) {
76
+ const line = rawLine.trim();
77
+ if (!line || line.startsWith("#"))
78
+ continue;
79
+ if (line === "[cli]") {
80
+ flushOperation();
81
+ currentSection = "cli";
82
+ result.cli = {};
83
+ continue;
84
+ }
85
+ if (line === "[[operation]]") {
86
+ flushOperation();
87
+ currentSection = "operation";
88
+ currentEntry = {};
89
+ continue;
90
+ }
91
+ const kv = parseKeyValue(line);
92
+ if (!kv)
93
+ continue;
94
+ const value = parseTomlValue(kv.value);
95
+ if (currentSection === "root") {
96
+ ;
97
+ result[kv.key] = value;
98
+ } else if (currentSection === "cli") {
99
+ ;
100
+ result.cli[kv.key] = value;
101
+ } else {
102
+ currentEntry[kv.key] = value;
103
+ }
104
+ }
105
+ flushOperation();
106
+ result.operation = operations;
107
+ if (result.schema !== "openape-shapes/v1") {
108
+ throw new Error(`Unsupported adapter schema: ${result.schema ?? "missing"}`);
109
+ }
110
+ if (!result.cli?.id || !result.cli.executable) {
111
+ throw new Error("Adapter is missing cli.id or cli.executable");
112
+ }
113
+ if (!result.operation?.length) {
114
+ throw new Error("Adapter must define at least one [[operation]] entry");
115
+ }
116
+ return {
117
+ schema: result.schema,
118
+ cli: {
119
+ id: String(result.cli.id),
120
+ executable: String(result.cli.executable),
121
+ ...result.cli.audience ? { audience: String(result.cli.audience) } : {},
122
+ ...result.cli.version ? { version: String(result.cli.version) } : {}
123
+ },
124
+ operations: result.operation.map((operation) => {
125
+ if (!Array.isArray(operation.command) || operation.command.some((token) => typeof token !== "string")) {
126
+ throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing command[]`);
127
+ }
128
+ if (!Array.isArray(operation.resource_chain) || operation.resource_chain.some((token) => typeof token !== "string")) {
129
+ throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing resource_chain[]`);
130
+ }
131
+ if (typeof operation.id !== "string" || typeof operation.display !== "string" || typeof operation.action !== "string") {
132
+ throw new TypeError("Operation must define id, display, and action");
133
+ }
134
+ return {
135
+ id: operation.id,
136
+ command: operation.command,
137
+ ...Array.isArray(operation.positionals) ? { positionals: operation.positionals } : {},
138
+ ...Array.isArray(operation.required_options) ? { required_options: operation.required_options } : {},
139
+ display: operation.display,
140
+ action: operation.action,
141
+ risk: operation.risk || "low",
142
+ resource_chain: operation.resource_chain,
143
+ ...operation.exact_command !== void 0 ? { exact_command: Boolean(operation.exact_command) } : {}
144
+ };
145
+ })
146
+ };
147
+ }
148
+
149
+ // src/adapters/loader.ts
150
+ var PACKAGE_DIR = dirname(fileURLToPath(import.meta.url));
151
+ function digest(content) {
152
+ return `SHA-256:${createHash("sha256").update(content).digest("hex")}`;
153
+ }
154
+ function bundledAdapterDir() {
155
+ return join(PACKAGE_DIR, "..", "..", "adapters");
156
+ }
157
+ function getSearchDirs(options) {
158
+ const dirs = [];
159
+ if (options?.explicit) {
160
+ dirs.push(...options.explicit);
161
+ }
162
+ if (options?.workspaceDir) {
163
+ dirs.push(join(options.workspaceDir, ".openclaw", "adapters"));
164
+ }
165
+ dirs.push(join(homedir(), ".openclaw", "adapters"));
166
+ dirs.push(bundledAdapterDir());
167
+ return dirs;
168
+ }
169
+ function loadAdapterFromFile(filePath) {
170
+ const content = readFileSync(filePath, "utf-8");
171
+ const adapter = parseAdapterToml(content);
172
+ return {
173
+ adapter,
174
+ source: filePath,
175
+ digest: digest(content)
176
+ };
177
+ }
178
+ function loadAdapter(cliId, options) {
179
+ const dirs = getSearchDirs(options);
180
+ for (const dir of dirs) {
181
+ const filePath = join(dir, `${cliId}.toml`);
182
+ if (existsSync(filePath)) {
183
+ return loadAdapterFromFile(filePath);
184
+ }
185
+ }
186
+ throw new Error(`No adapter found for CLI: ${cliId}`);
187
+ }
188
+ function discoverAdapters(options) {
189
+ const dirs = getSearchDirs(options);
190
+ const seen = /* @__PURE__ */ new Set();
191
+ const adapters = [];
192
+ for (const dir of dirs) {
193
+ if (!existsSync(dir))
194
+ continue;
195
+ let entries;
196
+ try {
197
+ entries = readdirSync(dir);
198
+ } catch {
199
+ continue;
200
+ }
201
+ for (const entry of entries) {
202
+ if (!entry.endsWith(".toml"))
203
+ continue;
204
+ const cliId = entry.slice(0, -5);
205
+ if (seen.has(cliId))
206
+ continue;
207
+ try {
208
+ const loaded = loadAdapterFromFile(join(dir, entry));
209
+ seen.add(cliId);
210
+ adapters.push(loaded);
211
+ } catch {
212
+ }
213
+ }
214
+ }
215
+ return adapters;
216
+ }
217
+
218
+ // src/store/grant-store.ts
219
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
220
+ import { dirname as dirname2, join as join2 } from "path";
221
+ import { randomUUID } from "crypto";
222
+ var GrantStore = class {
223
+ grants = /* @__PURE__ */ new Map();
224
+ filePath;
225
+ constructor(stateDir) {
226
+ this.filePath = stateDir ? join2(stateDir, "grants", "store.json") : null;
227
+ this.load();
228
+ }
229
+ createGrant(input) {
230
+ const grant = {
231
+ id: randomUUID().slice(0, 8),
232
+ permission: input.permission,
233
+ approval: "once",
234
+ status: "pending",
235
+ command: input.command,
236
+ reason: input.reason,
237
+ risk: input.risk,
238
+ display: input.display,
239
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
240
+ };
241
+ this.grants.set(grant.id, grant);
242
+ this.save();
243
+ return grant;
244
+ }
245
+ approveGrant(id, approval, expiresAt) {
246
+ const grant = this.grants.get(id);
247
+ if (!grant || grant.status !== "pending")
248
+ return null;
249
+ grant.status = "approved";
250
+ grant.approval = approval;
251
+ grant.decidedAt = (/* @__PURE__ */ new Date()).toISOString();
252
+ if (expiresAt)
253
+ grant.expiresAt = expiresAt;
254
+ this.save();
255
+ return grant;
256
+ }
257
+ denyGrant(id) {
258
+ const grant = this.grants.get(id);
259
+ if (!grant || grant.status !== "pending")
260
+ return null;
261
+ grant.status = "denied";
262
+ grant.decidedAt = (/* @__PURE__ */ new Date()).toISOString();
263
+ this.save();
264
+ return grant;
265
+ }
266
+ consumeGrant(id) {
267
+ const grant = this.grants.get(id);
268
+ if (!grant || grant.status !== "approved")
269
+ return false;
270
+ if (grant.approval === "once") {
271
+ grant.status = "used";
272
+ grant.usedAt = (/* @__PURE__ */ new Date()).toISOString();
273
+ this.save();
274
+ }
275
+ return true;
276
+ }
277
+ revokeGrant(id) {
278
+ const grant = this.grants.get(id);
279
+ if (!grant || grant.status !== "approved" && grant.status !== "pending")
280
+ return false;
281
+ grant.status = "revoked";
282
+ this.save();
283
+ return true;
284
+ }
285
+ getGrant(id) {
286
+ return this.grants.get(id);
287
+ }
288
+ listGrants(filter) {
289
+ const grants = Array.from(this.grants.values());
290
+ if (filter?.status)
291
+ return grants.filter((g) => g.status === filter.status);
292
+ return grants;
293
+ }
294
+ getActiveGrantCount() {
295
+ return Array.from(this.grants.values()).filter((g) => g.status === "approved" || g.status === "pending").length;
296
+ }
297
+ load() {
298
+ if (!this.filePath || !existsSync2(this.filePath))
299
+ return;
300
+ try {
301
+ const data = JSON.parse(readFileSync2(this.filePath, "utf-8"));
302
+ if (Array.isArray(data)) {
303
+ for (const grant of data) {
304
+ this.grants.set(grant.id, grant);
305
+ }
306
+ }
307
+ } catch {
308
+ }
309
+ }
310
+ save() {
311
+ if (!this.filePath)
312
+ return;
313
+ const dir = dirname2(this.filePath);
314
+ if (!existsSync2(dir))
315
+ mkdirSync(dir, { recursive: true });
316
+ writeFileSync(this.filePath, JSON.stringify(Array.from(this.grants.values()), null, 2));
317
+ }
318
+ };
319
+
320
+ // src/store/grant-cache.ts
321
+ import { cliAuthorizationDetailCovers } from "@openape/core";
322
+ var GrantCache = class {
323
+ entries = /* @__PURE__ */ new Map();
324
+ put(grant, detail) {
325
+ if (grant.approval === "once")
326
+ return;
327
+ const expiresAt = grant.expiresAt ? new Date(grant.expiresAt).getTime() : null;
328
+ this.entries.set(grant.permission, {
329
+ grant,
330
+ detail,
331
+ expiresAt
332
+ });
333
+ }
334
+ lookup(permission, detail) {
335
+ const direct = this.entries.get(permission);
336
+ if (direct) {
337
+ if (this.isExpired(direct)) {
338
+ this.entries.delete(permission);
339
+ return null;
340
+ }
341
+ return direct.grant;
342
+ }
343
+ for (const [key, entry] of this.entries) {
344
+ if (this.isExpired(entry)) {
345
+ this.entries.delete(key);
346
+ continue;
347
+ }
348
+ if (cliAuthorizationDetailCovers(entry.detail, detail)) {
349
+ return entry.grant;
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ remove(permission) {
355
+ return this.entries.delete(permission);
356
+ }
357
+ clear() {
358
+ this.entries.clear();
359
+ }
360
+ size() {
361
+ return this.entries.size;
362
+ }
363
+ isExpired(entry) {
364
+ if (!entry.expiresAt)
365
+ return false;
366
+ return Date.now() > entry.expiresAt;
367
+ }
368
+ };
369
+
370
+ // src/store/audit-log.ts
371
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
372
+ import { dirname as dirname3, join as join3 } from "path";
373
+ var AuditLog = class {
374
+ filePath;
375
+ constructor(stateDir) {
376
+ this.filePath = stateDir ? join3(stateDir, "grants", "audit.jsonl") : null;
377
+ if (this.filePath) {
378
+ const dir = dirname3(this.filePath);
379
+ if (!existsSync3(dir))
380
+ mkdirSync2(dir, { recursive: true });
381
+ }
382
+ }
383
+ write(entry) {
384
+ const full = {
385
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
386
+ ...entry
387
+ };
388
+ if (this.filePath) {
389
+ appendFileSync(this.filePath, JSON.stringify(full) + "\n");
390
+ }
391
+ }
392
+ };
393
+
394
+ // src/local/local-jwt.ts
395
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
396
+ import { join as join4 } from "path";
397
+ import { randomUUID as randomUUID2 } from "crypto";
398
+ import * as jose from "jose";
399
+ var LOCAL_ISSUER = "local://openclaw-grants";
400
+ var KEY_ALGORITHM = "EdDSA";
401
+ var LocalJwtSigner = class {
402
+ privateKey = null;
403
+ publicKey = null;
404
+ kid = "";
405
+ stateDir;
406
+ constructor(stateDir) {
407
+ this.stateDir = stateDir;
408
+ }
409
+ async init() {
410
+ const keyDir = join4(this.stateDir, "grants", "keys");
411
+ const privatePath = join4(keyDir, "private.jwk");
412
+ const publicPath = join4(keyDir, "public.jwk");
413
+ if (existsSync4(privatePath) && existsSync4(publicPath)) {
414
+ const privData = JSON.parse(readFileSync3(privatePath, "utf-8"));
415
+ const pubData = JSON.parse(readFileSync3(publicPath, "utf-8"));
416
+ this.privateKey = await jose.importJWK(privData, KEY_ALGORITHM);
417
+ this.publicKey = await jose.importJWK(pubData, KEY_ALGORITHM);
418
+ this.kid = privData.kid ?? "local-1";
419
+ } else {
420
+ const { privateKey, publicKey } = await jose.generateKeyPair(KEY_ALGORITHM, { crv: "Ed25519" });
421
+ this.privateKey = privateKey;
422
+ this.publicKey = publicKey;
423
+ this.kid = `local-${Date.now()}`;
424
+ if (!existsSync4(keyDir))
425
+ mkdirSync3(keyDir, { recursive: true });
426
+ const privJwk = await jose.exportJWK(privateKey);
427
+ privJwk.kid = this.kid;
428
+ privJwk.alg = KEY_ALGORITHM;
429
+ writeFileSync2(privatePath, JSON.stringify(privJwk, null, 2));
430
+ const pubJwk = await jose.exportJWK(publicKey);
431
+ pubJwk.kid = this.kid;
432
+ pubJwk.alg = KEY_ALGORITHM;
433
+ pubJwk.use = "sig";
434
+ writeFileSync2(publicPath, JSON.stringify(pubJwk, null, 2));
435
+ }
436
+ }
437
+ async signGrant(options) {
438
+ if (!this.privateKey)
439
+ throw new Error("LocalJwtSigner not initialized");
440
+ const { grant, audience, detail, executionContext } = options;
441
+ const expirationTime = this.getExpiration(grant);
442
+ const jwt = await new jose.SignJWT({
443
+ grant_id: grant.id,
444
+ grant_type: grant.approval,
445
+ permissions: [grant.permission],
446
+ authorization_details: [detail],
447
+ cmd_hash: executionContext.argv_hash,
448
+ command: grant.command,
449
+ execution_context: executionContext
450
+ }).setProtectedHeader({ alg: KEY_ALGORITHM, kid: this.kid }).setIssuedAt().setIssuer(LOCAL_ISSUER).setSubject("local-agent").setAudience(audience).setExpirationTime(expirationTime).setJti(randomUUID2()).sign(this.privateKey);
451
+ return jwt;
452
+ }
453
+ async getJwks() {
454
+ if (!this.publicKey)
455
+ throw new Error("LocalJwtSigner not initialized");
456
+ const jwk = await jose.exportJWK(this.publicKey);
457
+ jwk.kid = this.kid;
458
+ jwk.alg = KEY_ALGORITHM;
459
+ jwk.use = "sig";
460
+ return { keys: [jwk] };
461
+ }
462
+ getIssuer() {
463
+ return LOCAL_ISSUER;
464
+ }
465
+ getExpiration(grant) {
466
+ switch (grant.approval) {
467
+ case "once":
468
+ return "5m";
469
+ case "timed":
470
+ if (grant.expiresAt) {
471
+ const secondsFromNow = Math.max(0, Math.floor((new Date(grant.expiresAt).getTime() - Date.now()) / 1e3));
472
+ return `${secondsFromNow}s`;
473
+ }
474
+ return "1h";
475
+ case "always":
476
+ return "1h";
477
+ default:
478
+ return "5m";
479
+ }
480
+ }
481
+ };
482
+
483
+ // src/approval/channel-approval.ts
484
+ var RISK_EMOJI = {
485
+ low: "\u{1F7E2}",
486
+ // green circle
487
+ medium: "\u{1F7E1}",
488
+ // yellow circle
489
+ high: "\u{1F534}",
490
+ // red circle
491
+ critical: "\u26D4"
492
+ // no entry
493
+ };
494
+ var ChannelApproval = class {
495
+ constructor(api, store, timeoutMs = 3e5) {
496
+ this.api = api;
497
+ this.store = store;
498
+ this.timeoutMs = timeoutMs;
499
+ this.registerCommands();
500
+ }
501
+ pending = /* @__PURE__ */ new Map();
502
+ async requestApproval(grant) {
503
+ const emoji = RISK_EMOJI[grant.risk] ?? "\u{1F50D}";
504
+ const message = [
505
+ `\u{1F510} Grant Request ${emoji} [${grant.risk} risk]`,
506
+ `Operation: ${grant.display}`,
507
+ `Permission: ${grant.permission}`,
508
+ grant.reason ? `Reason: ${grant.reason}` : "",
509
+ `ID: ${grant.id}`,
510
+ "",
511
+ "Reply: /grant-approve <id> [once|1h|4h|always] or /grant-deny <id>"
512
+ ].filter(Boolean).join("\n");
513
+ await this.api.sendChannelMessage({
514
+ text: message,
515
+ actions: [
516
+ { label: "Once", value: `grant-approve ${grant.id} once` },
517
+ { label: "1h", value: `grant-approve ${grant.id} 1h` },
518
+ { label: "4h", value: `grant-approve ${grant.id} 4h` },
519
+ { label: "Always", value: `grant-approve ${grant.id} always` },
520
+ { label: "Deny", value: `grant-deny ${grant.id}` }
521
+ ]
522
+ });
523
+ return new Promise((resolve) => {
524
+ const timeout = setTimeout(() => {
525
+ this.pending.delete(grant.id);
526
+ resolve({ approved: false });
527
+ }, this.timeoutMs);
528
+ this.pending.set(grant.id, { resolve, timeout });
529
+ });
530
+ }
531
+ registerCommands() {
532
+ this.api.onChannelCommand("grant-approve", async (args) => {
533
+ const [id, durationOrType] = args;
534
+ if (!id)
535
+ return;
536
+ let approval = "once";
537
+ let expiresAt;
538
+ if (durationOrType === "always") {
539
+ approval = "always";
540
+ } else if (durationOrType === "1h" || durationOrType === "4h") {
541
+ approval = "timed";
542
+ const hours = durationOrType === "1h" ? 1 : 4;
543
+ expiresAt = new Date(Date.now() + hours * 36e5).toISOString();
544
+ } else if (durationOrType === "once" || !durationOrType) {
545
+ approval = "once";
546
+ }
547
+ const grant = this.store.approveGrant(id, approval, expiresAt);
548
+ if (!grant) {
549
+ this.api.log.warn(`[grants] Cannot approve grant ${id}: not found or not pending`);
550
+ return;
551
+ }
552
+ const pending = this.pending.get(id);
553
+ if (pending) {
554
+ clearTimeout(pending.timeout);
555
+ this.pending.delete(id);
556
+ pending.resolve({ approved: true, approval });
557
+ }
558
+ });
559
+ this.api.onChannelCommand("grant-deny", async (args) => {
560
+ const [id] = args;
561
+ if (!id)
562
+ return;
563
+ this.store.denyGrant(id);
564
+ const pending = this.pending.get(id);
565
+ if (pending) {
566
+ clearTimeout(pending.timeout);
567
+ this.pending.delete(id);
568
+ pending.resolve({ approved: false });
569
+ }
570
+ });
571
+ }
572
+ };
573
+
574
+ // src/adapters/parser.ts
575
+ import { canonicalizeCliPermission, computeArgvHash } from "@openape/core";
576
+ function parseOptionArgs(tokens) {
577
+ const options = {};
578
+ const positionals = [];
579
+ for (let index = 0; index < tokens.length; index += 1) {
580
+ const token = tokens[index];
581
+ if (!token.startsWith("--")) {
582
+ positionals.push(token);
583
+ continue;
584
+ }
585
+ const stripped = token.slice(2);
586
+ const eqIndex = stripped.indexOf("=");
587
+ if (eqIndex >= 0) {
588
+ options[stripped.slice(0, eqIndex)] = stripped.slice(eqIndex + 1);
589
+ continue;
590
+ }
591
+ const next = tokens[index + 1];
592
+ if (next && !next.startsWith("--")) {
593
+ options[stripped] = next;
594
+ index += 1;
595
+ continue;
596
+ }
597
+ options[stripped] = "true";
598
+ }
599
+ return { options, positionals };
600
+ }
601
+ function resolveBindingToken(binding, bindings) {
602
+ const match = binding.match(/^\{([^}|]+)(?:\|([^}]+))?\}$/);
603
+ if (!match)
604
+ return binding;
605
+ const [, name, transform] = match;
606
+ const value = bindings[name];
607
+ if (!value)
608
+ throw new Error(`Missing binding: ${name}`);
609
+ if (!transform)
610
+ return value;
611
+ if (transform === "owner" || transform === "name") {
612
+ const [owner, repo] = value.split("/");
613
+ if (!owner || !repo)
614
+ throw new Error(`Binding ${name} must be in owner/name form`);
615
+ return transform === "owner" ? owner : repo;
616
+ }
617
+ throw new Error(`Unsupported binding transform: ${transform}`);
618
+ }
619
+ function renderTemplate(template, bindings) {
620
+ return template.replace(/\{([^}]+)\}/g, (_, expression) => resolveBindingToken(`{${expression}}`, bindings));
621
+ }
622
+ function parseResourceChain(chain, bindings) {
623
+ return chain.map((entry) => {
624
+ const [resource, selectorSpec = "*"] = entry.split(":", 2);
625
+ if (!resource)
626
+ throw new Error(`Invalid resource chain entry: ${entry}`);
627
+ if (selectorSpec === "*") {
628
+ return { resource };
629
+ }
630
+ const selector = Object.fromEntries(
631
+ selectorSpec.split(",").map((segment) => {
632
+ const [key, rawValue] = segment.split("=", 2);
633
+ if (!key || !rawValue)
634
+ throw new Error(`Invalid selector segment: ${segment}`);
635
+ return [key, renderTemplate(rawValue, bindings)];
636
+ })
637
+ );
638
+ return { resource, selector };
639
+ });
640
+ }
641
+ function matchOperation(operation, argv) {
642
+ if (argv.length < operation.command.length)
643
+ return null;
644
+ const prefix = argv.slice(0, operation.command.length);
645
+ if (prefix.join("\0") !== operation.command.join("\0"))
646
+ return null;
647
+ const remainder = argv.slice(operation.command.length);
648
+ const { options, positionals } = parseOptionArgs(remainder);
649
+ const expectedPositionals = operation.positionals ?? [];
650
+ if (positionals.length !== expectedPositionals.length)
651
+ return null;
652
+ for (const option of operation.required_options ?? []) {
653
+ if (!options[option])
654
+ return null;
655
+ }
656
+ const bindings = { ...options };
657
+ expectedPositionals.forEach((name, index) => {
658
+ bindings[name] = positionals[index];
659
+ });
660
+ return bindings;
661
+ }
662
+ async function resolveCommand(loaded, fullArgv) {
663
+ const [executable, ...commandArgv] = fullArgv;
664
+ if (!executable) {
665
+ throw new Error("Missing wrapped command");
666
+ }
667
+ if (executable !== loaded.adapter.cli.executable) {
668
+ throw new Error(`Adapter ${loaded.adapter.cli.id} expects executable ${loaded.adapter.cli.executable}, got ${executable}`);
669
+ }
670
+ const matches = loaded.adapter.operations.flatMap((operation2) => {
671
+ try {
672
+ const bindings2 = matchOperation(operation2, commandArgv);
673
+ return bindings2 ? [{ operation: operation2, bindings: bindings2 }] : [];
674
+ } catch {
675
+ return [];
676
+ }
677
+ });
678
+ if (matches.length === 0) {
679
+ throw new Error(`No adapter operation matched: ${fullArgv.join(" ")}`);
680
+ }
681
+ if (matches.length > 1) {
682
+ matches.sort((a, b) => {
683
+ const scoreA = (a.operation.required_options?.length ?? 0) + (a.operation.positionals?.length ?? 0) + (a.operation.exact_command ? 1 : 0);
684
+ const scoreB = (b.operation.required_options?.length ?? 0) + (b.operation.positionals?.length ?? 0) + (b.operation.exact_command ? 1 : 0);
685
+ return scoreB - scoreA;
686
+ });
687
+ }
688
+ const { operation, bindings } = matches[0];
689
+ const resource_chain = parseResourceChain(operation.resource_chain, bindings);
690
+ const detail = {
691
+ type: "openape_cli",
692
+ cli_id: loaded.adapter.cli.id,
693
+ operation_id: operation.id,
694
+ resource_chain,
695
+ action: operation.action,
696
+ permission: "",
697
+ display: renderTemplate(operation.display, bindings),
698
+ risk: operation.risk,
699
+ ...operation.exact_command ? { constraints: { exact_command: true } } : {}
700
+ };
701
+ detail.permission = canonicalizeCliPermission(detail);
702
+ return {
703
+ adapter: loaded.adapter,
704
+ source: loaded.source,
705
+ digest: loaded.digest,
706
+ executable,
707
+ commandArgv,
708
+ bindings,
709
+ detail,
710
+ executionContext: {
711
+ argv: fullArgv,
712
+ argv_hash: await computeArgvHash(fullArgv),
713
+ adapter_id: loaded.adapter.cli.id,
714
+ adapter_version: loaded.adapter.cli.version ?? loaded.adapter.schema,
715
+ adapter_digest: loaded.digest,
716
+ resolved_executable: executable,
717
+ context_bindings: bindings
718
+ },
719
+ permission: detail.permission
720
+ };
721
+ }
722
+ async function createFallbackCommand(commandString) {
723
+ const argv = parseCommandString(commandString);
724
+ const hash = await computeArgvHash(argv);
725
+ return {
726
+ command: commandString,
727
+ argv,
728
+ hash,
729
+ permission: `unknown.command[hash=${hash.slice(7, 19)}]#execute`,
730
+ display: `Execute: ${commandString.length > 60 ? `${commandString.slice(0, 57)}...` : commandString}`,
731
+ risk: "high"
732
+ };
733
+ }
734
+ function parseCommandString(command) {
735
+ const argv = [];
736
+ let current = "";
737
+ let inQuote = null;
738
+ for (let i = 0; i < command.length; i++) {
739
+ const char = command[i];
740
+ if (inQuote) {
741
+ if (char === inQuote) {
742
+ inQuote = null;
743
+ } else {
744
+ current += char;
745
+ }
746
+ } else if (char === "'" || char === '"') {
747
+ inQuote = char;
748
+ } else if (char === " " || char === " ") {
749
+ if (current) {
750
+ argv.push(current);
751
+ current = "";
752
+ }
753
+ } else {
754
+ current += char;
755
+ }
756
+ }
757
+ if (current)
758
+ argv.push(current);
759
+ return argv;
760
+ }
761
+ async function resolveCommandFromAdapters(adapters, commandString) {
762
+ const argv = parseCommandString(commandString);
763
+ if (argv.length === 0) {
764
+ throw new Error("Empty command");
765
+ }
766
+ const executable = argv[0];
767
+ for (const loaded of adapters) {
768
+ if (loaded.adapter.cli.executable !== executable)
769
+ continue;
770
+ try {
771
+ const resolved = await resolveCommand(loaded, argv);
772
+ return { resolved, fallback: null };
773
+ } catch {
774
+ }
775
+ }
776
+ const fallback = await createFallbackCommand(commandString);
777
+ return { resolved: null, fallback };
778
+ }
779
+
780
+ // src/execution/executor.ts
781
+ async function executeCommand(api, options) {
782
+ const { command, args, jwt, privileged, apesBinaryPath, timeout } = options;
783
+ if (privileged && jwt && apesBinaryPath) {
784
+ return executeWithApes(api, { command, args, jwt, binaryPath: apesBinaryPath, timeout });
785
+ }
786
+ return executeDirectly(api, { command, args, timeout });
787
+ }
788
+ async function executeDirectly(api, options) {
789
+ try {
790
+ const result = await api.runtime.system.runCommandWithTimeout(
791
+ options.command,
792
+ options.args,
793
+ { timeout: options.timeout ?? 3e4 }
794
+ );
795
+ if (result.exitCode !== 0) {
796
+ return {
797
+ success: false,
798
+ output: result.stdout || void 0,
799
+ error: result.stderr || `Command exited with code ${result.exitCode}`
800
+ };
801
+ }
802
+ return {
803
+ success: true,
804
+ output: result.stdout
805
+ };
806
+ } catch (error) {
807
+ return {
808
+ success: false,
809
+ error: error instanceof Error ? error.message : String(error)
810
+ };
811
+ }
812
+ }
813
+ async function executeWithApes(api, options) {
814
+ try {
815
+ const result = await api.runtime.system.runCommandWithTimeout(
816
+ options.binaryPath,
817
+ ["--grant", options.jwt, "--", options.command, ...options.args],
818
+ { timeout: options.timeout ?? 3e4 }
819
+ );
820
+ if (result.exitCode !== 0) {
821
+ return {
822
+ success: false,
823
+ output: result.stdout || void 0,
824
+ error: result.stderr || `apes exited with code ${result.exitCode}`
825
+ };
826
+ }
827
+ return {
828
+ success: true,
829
+ output: result.stdout
830
+ };
831
+ } catch (error) {
832
+ return {
833
+ success: false,
834
+ error: error instanceof Error ? error.message : String(error)
835
+ };
836
+ }
837
+ }
838
+
839
+ // src/idp/idp-grants.ts
840
+ import { hostname } from "os";
841
+ import { verifyAuthzJWT } from "@openape/grants";
842
+ import { cliAuthorizationDetailCovers as cliAuthorizationDetailCovers2 } from "@openape/core";
843
+
844
+ // src/idp/discovery.ts
845
+ import { resolveIdP } from "@openape/core";
846
+ var _discoveryCache = {};
847
+ function extractDomain(email) {
848
+ const parts = email.split("@");
849
+ if (parts.length !== 2 || !parts[1])
850
+ throw new Error(`Invalid email: ${email}`);
851
+ return parts[1];
852
+ }
853
+ async function discoverIdpUrl(email, pinnedUrl) {
854
+ if (pinnedUrl)
855
+ return pinnedUrl;
856
+ const domain = extractDomain(email);
857
+ const idpUrl = await resolveIdP(domain);
858
+ if (!idpUrl)
859
+ throw new Error(`No DDISA record found for domain: ${domain}`);
860
+ return idpUrl;
861
+ }
862
+ async function discoverEndpoints(idpUrl) {
863
+ if (_discoveryCache[idpUrl])
864
+ return _discoveryCache[idpUrl];
865
+ try {
866
+ const response = await fetch(`${idpUrl}/.well-known/openid-configuration`);
867
+ if (response.ok) {
868
+ const data = await response.json();
869
+ _discoveryCache[idpUrl] = data;
870
+ return data;
871
+ }
872
+ } catch {
873
+ }
874
+ _discoveryCache[idpUrl] = {};
875
+ return {};
876
+ }
877
+ async function getGrantsEndpoint(idpUrl) {
878
+ const disco = await discoverEndpoints(idpUrl);
879
+ return disco.openape_grants_endpoint || `${idpUrl}/api/grants`;
880
+ }
881
+ async function getAgentChallengeEndpoint(idpUrl) {
882
+ const disco = await discoverEndpoints(idpUrl);
883
+ return disco.ddisa_agent_challenge_endpoint || `${idpUrl}/api/agent/challenge`;
884
+ }
885
+ async function getAgentAuthenticateEndpoint(idpUrl) {
886
+ const disco = await discoverEndpoints(idpUrl);
887
+ return disco.ddisa_agent_authenticate_endpoint || `${idpUrl}/api/agent/authenticate`;
888
+ }
889
+ async function getJwksUri(idpUrl) {
890
+ const disco = await discoverEndpoints(idpUrl);
891
+ return disco.jwks_uri || `${idpUrl}/.well-known/jwks.json`;
892
+ }
893
+ function clearDiscoveryCache() {
894
+ for (const key of Object.keys(_discoveryCache))
895
+ delete _discoveryCache[key];
896
+ }
897
+
898
+ // src/idp/idp-grants.ts
899
+ async function handleIdpGrantExec(ctx, options) {
900
+ const { config, api, authState, store, cache, audit } = ctx;
901
+ const { resolved, fallback, command, reason, privileged } = options;
902
+ let permission;
903
+ let display;
904
+ let risk;
905
+ let detail = null;
906
+ let argv;
907
+ if (resolved) {
908
+ permission = resolved.permission;
909
+ display = resolved.detail.display;
910
+ risk = resolved.detail.risk;
911
+ detail = resolved.detail;
912
+ argv = [resolved.executable, ...resolved.commandArgv];
913
+ } else if (fallback) {
914
+ permission = fallback.permission;
915
+ display = fallback.display;
916
+ risk = fallback.risk;
917
+ argv = fallback.argv;
918
+ } else {
919
+ return { success: false, error: "Command resolution failed" };
920
+ }
921
+ if (detail) {
922
+ const cached = cache.lookup(permission, detail);
923
+ if (cached) {
924
+ api.log.info(`[grants] IdP cache hit for ${permission}`);
925
+ audit.write({ event: "grant_used", grantId: cached.id, permission, command, detail: "idp cache hit" });
926
+ return doExecute(api, config, argv, cached.jwt, privileged);
927
+ }
928
+ }
929
+ const grantsEndpoint = await getGrantsEndpoint(authState.idpUrl);
930
+ const grantBody = {
931
+ requester: authState.email,
932
+ target_host: hostname(),
933
+ audience: config.audience,
934
+ grant_type: config.defaultApproval,
935
+ command: argv,
936
+ reason: reason ?? display,
937
+ permissions: [permission]
938
+ };
939
+ if (resolved) {
940
+ grantBody.authorization_details = [resolved.detail];
941
+ grantBody.execution_context = resolved.executionContext;
942
+ }
943
+ const createResp = await fetch(grantsEndpoint, {
944
+ method: "POST",
945
+ headers: {
946
+ "Authorization": `Bearer ${authState.token}`,
947
+ "Content-Type": "application/json"
948
+ },
949
+ body: JSON.stringify(grantBody)
950
+ });
951
+ if (!createResp.ok) {
952
+ const text = await createResp.text();
953
+ return { success: false, error: `IdP grant creation failed: ${createResp.status} ${text}` };
954
+ }
955
+ const { id: grantId } = await createResp.json();
956
+ audit.write({ event: "grant_requested", grantId, permission, command });
957
+ const localGrant = store.createGrant({ permission, command, reason, risk, display });
958
+ const pollInterval = config.pollIntervalMs ?? 3e3;
959
+ const pollTimeout = config.pollTimeoutMs ?? 3e5;
960
+ const deadline = Date.now() + pollTimeout;
961
+ while (Date.now() < deadline) {
962
+ const statusResp = await fetch(`${grantsEndpoint}/${grantId}`, {
963
+ headers: { Authorization: `Bearer ${authState.token}` }
964
+ });
965
+ if (statusResp.ok) {
966
+ const { status } = await statusResp.json();
967
+ if (status === "approved")
968
+ break;
969
+ if (status === "denied" || status === "revoked") {
970
+ audit.write({ event: "grant_denied", grantId, permission });
971
+ store.denyGrant(localGrant.id);
972
+ return { success: false, error: `Grant ${status}: ${display}` };
973
+ }
974
+ }
975
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
976
+ }
977
+ if (Date.now() >= deadline) {
978
+ return { success: false, error: `Grant approval timed out for: ${display}` };
979
+ }
980
+ const tokenResp = await fetch(`${grantsEndpoint}/${grantId}/token`, {
981
+ method: "POST",
982
+ headers: { Authorization: `Bearer ${authState.token}` }
983
+ });
984
+ if (!tokenResp.ok) {
985
+ return { success: false, error: `Failed to fetch grant token: ${tokenResp.status}` };
986
+ }
987
+ const { authz_jwt: jwt } = await tokenResp.json();
988
+ if (resolved) {
989
+ const jwksUri = await getJwksUri(authState.idpUrl);
990
+ const verifyResult = await verifyAuthzJWT(jwt, {
991
+ expectedIss: authState.idpUrl,
992
+ expectedAud: config.audience,
993
+ jwksUri
994
+ });
995
+ if (!verifyResult.valid) {
996
+ return { success: false, error: `Grant JWT verification failed: ${verifyResult.error}` };
997
+ }
998
+ const claims = verifyResult.claims;
999
+ const grantedDetails = extractCliDetails(claims);
1000
+ if (grantedDetails.length > 0 && !grantedDetails.some((d) => cliAuthorizationDetailCovers2(d, resolved.detail))) {
1001
+ return { success: false, error: `Grant does not cover required permission: ${permission}` };
1002
+ }
1003
+ }
1004
+ const approval = config.defaultApproval;
1005
+ store.approveGrant(localGrant.id, approval);
1006
+ if (detail && approval !== "once") {
1007
+ const updatedGrant = store.getGrant(localGrant.id);
1008
+ updatedGrant.jwt = jwt;
1009
+ cache.put(updatedGrant, detail);
1010
+ }
1011
+ store.consumeGrant(localGrant.id);
1012
+ audit.write({ event: "grant_approved", grantId, permission, command });
1013
+ return doExecute(api, config, argv, jwt, privileged);
1014
+ }
1015
+ function extractCliDetails(claims) {
1016
+ const details = claims.authorization_details;
1017
+ if (!Array.isArray(details))
1018
+ return [];
1019
+ return details.filter(
1020
+ (d) => typeof d === "object" && d !== null && d.type === "openape_cli"
1021
+ );
1022
+ }
1023
+ async function doExecute(api, config, argv, jwt, privileged) {
1024
+ const [command, ...args] = argv;
1025
+ if (!command)
1026
+ return { success: false, error: "Empty command" };
1027
+ return executeCommand(api, {
1028
+ command,
1029
+ args,
1030
+ jwt,
1031
+ privileged: privileged && config.apes?.enabled,
1032
+ apesBinaryPath: config.apes?.binaryPath ?? "apes"
1033
+ });
1034
+ }
1035
+
1036
+ // src/tools/grant-exec.ts
1037
+ async function handleGrantExec(ctx, input) {
1038
+ const { config, api, adapters, store, cache, audit } = ctx;
1039
+ const resolution = await resolveCommandFromAdapters(adapters, input.command);
1040
+ let permission;
1041
+ let display;
1042
+ let risk;
1043
+ let detail = null;
1044
+ let executionContext = null;
1045
+ let argv;
1046
+ if (resolution.resolved) {
1047
+ const r = resolution.resolved;
1048
+ permission = r.permission;
1049
+ display = r.detail.display;
1050
+ risk = r.detail.risk;
1051
+ detail = r.detail;
1052
+ executionContext = r.executionContext;
1053
+ argv = [r.executable, ...r.commandArgv];
1054
+ } else if (resolution.fallback) {
1055
+ const f = resolution.fallback;
1056
+ permission = f.permission;
1057
+ display = f.display;
1058
+ risk = f.risk;
1059
+ argv = f.argv;
1060
+ } else {
1061
+ return { success: false, error: "Command resolution failed" };
1062
+ }
1063
+ if (detail) {
1064
+ const cached = cache.lookup(permission, detail);
1065
+ if (cached) {
1066
+ api.log.info(`[grants] Cache hit for ${permission}`);
1067
+ audit.write({ event: "grant_used", grantId: cached.id, permission, command: input.command, detail: "cache hit" });
1068
+ return doExecute2(ctx, argv, input, cached.jwt);
1069
+ }
1070
+ }
1071
+ const grant = store.createGrant({
1072
+ permission,
1073
+ command: input.command,
1074
+ reason: input.reason,
1075
+ risk,
1076
+ display
1077
+ });
1078
+ audit.write({ event: "grant_requested", grantId: grant.id, permission, command: input.command });
1079
+ if (config.mode === "local") {
1080
+ if (!ctx.channelApproval) {
1081
+ return { success: false, error: "Channel approval not available" };
1082
+ }
1083
+ const result = await ctx.channelApproval.requestApproval(grant);
1084
+ if (!result.approved) {
1085
+ audit.write({ event: "grant_denied", grantId: grant.id, permission });
1086
+ return { success: false, error: `Grant denied for: ${display}` };
1087
+ }
1088
+ const approvedGrant = store.getGrant(grant.id);
1089
+ if (!approvedGrant || approvedGrant.status !== "approved") {
1090
+ return { success: false, error: "Grant not in approved state" };
1091
+ }
1092
+ let jwt;
1093
+ if (ctx.localJwt && detail && executionContext) {
1094
+ jwt = await ctx.localJwt.signGrant({
1095
+ grant: approvedGrant,
1096
+ audience: config.audience,
1097
+ detail,
1098
+ executionContext
1099
+ });
1100
+ approvedGrant.jwt = jwt;
1101
+ }
1102
+ if (detail && approvedGrant.approval !== "once") {
1103
+ cache.put(approvedGrant, detail);
1104
+ }
1105
+ store.consumeGrant(approvedGrant.id);
1106
+ audit.write({ event: "grant_approved", grantId: approvedGrant.id, permission, command: input.command });
1107
+ return doExecute2(ctx, argv, input, jwt);
1108
+ }
1109
+ if (config.mode === "idp") {
1110
+ if (!ctx.idpAuthState) {
1111
+ return { success: false, error: "IdP authentication not available. Check agentEmail and agentKeyPath config." };
1112
+ }
1113
+ return handleIdpGrantExec(
1114
+ { config, api, authState: ctx.idpAuthState, store, cache, audit },
1115
+ { resolved: resolution.resolved, fallback: resolution.fallback, command: input.command, reason: input.reason, privileged: input.privileged }
1116
+ );
1117
+ }
1118
+ return { success: false, error: `Unknown mode: ${config.mode}` };
1119
+ }
1120
+ async function doExecute2(ctx, argv, input, jwt) {
1121
+ const { config, api, audit } = ctx;
1122
+ const [command, ...args] = argv;
1123
+ if (!command) {
1124
+ return { success: false, error: "Empty command" };
1125
+ }
1126
+ const privileged = input.privileged && config.apes?.enabled;
1127
+ const result = await executeCommand(api, {
1128
+ command,
1129
+ args,
1130
+ jwt,
1131
+ privileged,
1132
+ apesBinaryPath: config.apes?.binaryPath ?? "apes"
1133
+ });
1134
+ audit.write({
1135
+ event: result.success ? "exec_success" : "exec_failed",
1136
+ command: input.command,
1137
+ detail: result.error
1138
+ });
1139
+ return result;
1140
+ }
1141
+
1142
+ // src/idp/auth.ts
1143
+ import { readFileSync as readFileSync4 } from "fs";
1144
+ import { sign } from "crypto";
1145
+ async function authenticateAgent(options) {
1146
+ const { idpUrl, email, keyPath } = options;
1147
+ const keyContent = readFileSync4(keyPath, "utf-8");
1148
+ const privateKey = loadEd25519PrivateKey(keyContent);
1149
+ const challengeUrl = await getAgentChallengeEndpoint(idpUrl);
1150
+ const challengeResp = await fetch(challengeUrl, {
1151
+ method: "POST",
1152
+ headers: { "Content-Type": "application/json" },
1153
+ body: JSON.stringify({ agent_id: email })
1154
+ });
1155
+ if (!challengeResp.ok)
1156
+ throw new Error(`Agent challenge failed: ${challengeResp.status} ${await challengeResp.text()}`);
1157
+ const { challenge } = await challengeResp.json();
1158
+ const signature = sign(null, Buffer.from(challenge), privateKey).toString("base64");
1159
+ const authenticateUrl = await getAgentAuthenticateEndpoint(idpUrl);
1160
+ const authResp = await fetch(authenticateUrl, {
1161
+ method: "POST",
1162
+ headers: { "Content-Type": "application/json" },
1163
+ body: JSON.stringify({ agent_id: email, challenge, signature })
1164
+ });
1165
+ if (!authResp.ok)
1166
+ throw new Error(`Agent authentication failed: ${authResp.status} ${await authResp.text()}`);
1167
+ const { token, expires_in } = await authResp.json();
1168
+ return {
1169
+ idpUrl,
1170
+ token,
1171
+ email,
1172
+ expiresAt: Math.floor(Date.now() / 1e3) + (expires_in || 3600)
1173
+ };
1174
+ }
1175
+ function isTokenExpired(state) {
1176
+ return Math.floor(Date.now() / 1e3) > state.expiresAt - 30;
1177
+ }
1178
+ function loadEd25519PrivateKey(content) {
1179
+ if (content.includes("BEGIN PRIVATE KEY"))
1180
+ return content;
1181
+ if (content.includes("BEGIN OPENSSH PRIVATE KEY"))
1182
+ return content;
1183
+ throw new Error("Unsupported key format. Expected PKCS8 PEM or OpenSSH format.");
1184
+ }
1185
+
1186
+ // src/execution/apes.ts
1187
+ import { existsSync as existsSync5 } from "fs";
1188
+ import { execFileSync } from "child_process";
1189
+ function detectApes(binaryPath = "apes") {
1190
+ if (binaryPath !== "apes" && existsSync5(binaryPath)) {
1191
+ return { available: true, path: binaryPath, version: getApesVersion(binaryPath) };
1192
+ }
1193
+ try {
1194
+ const result = execFileSync("which", [binaryPath], { encoding: "utf-8", timeout: 5e3 }).trim();
1195
+ if (result) {
1196
+ return { available: true, path: result, version: getApesVersion(result) };
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ return { available: false, path: binaryPath };
1201
+ }
1202
+ function getApesVersion(binaryPath) {
1203
+ try {
1204
+ return execFileSync(binaryPath, ["--version"], { encoding: "utf-8", timeout: 5e3 }).trim();
1205
+ } catch {
1206
+ return void 0;
1207
+ }
1208
+ }
1209
+ function buildApesArgs(jwt, command, args) {
1210
+ return ["--grant", jwt, "--", command, ...args];
1211
+ }
1212
+
1213
+ // src/index.ts
1214
+ var BLOCKED_TOOLS = /* @__PURE__ */ new Set(["exec", "bash", "shell", "run_command"]);
1215
+ function register(api, userConfig) {
1216
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
1217
+ api.log.info(`[grants] Initializing in ${config.mode} mode`);
1218
+ const stateDir = api.runtime.config.getStateDir();
1219
+ const workspaceDir = api.runtime.config.getWorkspaceDir();
1220
+ const store = new GrantStore(stateDir);
1221
+ const cache = new GrantCache();
1222
+ const audit = new AuditLog(stateDir);
1223
+ const adapters = discoverAdapters({
1224
+ explicit: config.adapterPaths,
1225
+ workspaceDir
1226
+ });
1227
+ api.log.info(`[grants] Loaded ${adapters.length} adapters: ${adapters.map((a) => a.adapter.cli.id).join(", ")}`);
1228
+ let localJwt = null;
1229
+ let channelApproval = null;
1230
+ if (config.mode === "local") {
1231
+ localJwt = new LocalJwtSigner(stateDir);
1232
+ channelApproval = new ChannelApproval(api, store, config.pollTimeoutMs);
1233
+ localJwt.init().catch((error) => {
1234
+ api.log.error(`[grants] Failed to init local JWT: ${error}`);
1235
+ });
1236
+ }
1237
+ let idpAuthState = null;
1238
+ if (config.mode === "idp") {
1239
+ if (!config.agentEmail || !config.agentKeyPath) {
1240
+ api.log.error("[grants] IdP mode requires agentEmail and agentKeyPath");
1241
+ } else {
1242
+ discoverIdpUrl(config.agentEmail, config.idpUrl).then((idpUrl) => authenticateAgent({
1243
+ idpUrl,
1244
+ email: config.agentEmail,
1245
+ keyPath: config.agentKeyPath
1246
+ })).then((state) => {
1247
+ idpAuthState = state;
1248
+ api.log.info(`[grants] IdP auth OK: ${state.email} @ ${state.idpUrl}`);
1249
+ }).catch((error) => {
1250
+ api.log.error(`[grants] IdP auth failed: ${error}`);
1251
+ });
1252
+ }
1253
+ }
1254
+ api.registerTool({
1255
+ name: "grant_exec",
1256
+ description: [
1257
+ "Execute a CLI command with grant-based authorization.",
1258
+ "Commands are resolved against adapters for granular permissions.",
1259
+ "The owner must approve the grant before execution proceeds."
1260
+ ].join(" "),
1261
+ inputSchema: {
1262
+ type: "object",
1263
+ properties: {
1264
+ command: {
1265
+ type: "string",
1266
+ description: 'The full CLI command to execute (e.g. "gh pr merge 42 --repo openape/core")'
1267
+ },
1268
+ reason: {
1269
+ type: "string",
1270
+ description: "Why this command needs to be executed"
1271
+ },
1272
+ privileged: {
1273
+ type: "boolean",
1274
+ description: "Whether this command requires elevated privileges (via apes)"
1275
+ }
1276
+ },
1277
+ required: ["command"]
1278
+ },
1279
+ handler: async (input) => {
1280
+ api.log.info(`[grants] grant_exec called: ${input.command}`);
1281
+ return handleGrantExec(
1282
+ { config, api, adapters, store, cache, audit, localJwt, channelApproval, idpAuthState },
1283
+ input
1284
+ );
1285
+ }
1286
+ });
1287
+ api.on("before_tool_call", async (context) => {
1288
+ if (BLOCKED_TOOLS.has(context.toolName)) {
1289
+ api.log.warn(`[grants] Blocked tool: ${context.toolName} \u2014 use grant_exec instead`);
1290
+ audit.write({ event: "exec_blocked", command: String(context.toolInput.command ?? context.toolName) });
1291
+ return {
1292
+ allow: false,
1293
+ message: `Direct command execution via "${context.toolName}" is disabled. Use grant_exec instead for authorized command execution.`
1294
+ };
1295
+ }
1296
+ return { allow: true };
1297
+ }, { priority: 100 });
1298
+ api.registerHttpRoute({
1299
+ path: "/grants/.well-known/jwks.json",
1300
+ method: "GET",
1301
+ handler: async () => {
1302
+ if (!localJwt) {
1303
+ return {
1304
+ status: 404,
1305
+ headers: { "Content-Type": "application/json" },
1306
+ body: { error: "JWKS not available in IdP mode" }
1307
+ };
1308
+ }
1309
+ try {
1310
+ const jwks = await localJwt.getJwks();
1311
+ return {
1312
+ status: 200,
1313
+ headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600" },
1314
+ body: jwks
1315
+ };
1316
+ } catch {
1317
+ return {
1318
+ status: 500,
1319
+ headers: { "Content-Type": "application/json" },
1320
+ body: { error: "JWKS not ready" }
1321
+ };
1322
+ }
1323
+ }
1324
+ });
1325
+ api.registerCli({
1326
+ name: "grants",
1327
+ description: "Manage grant-based command execution",
1328
+ subcommands: [
1329
+ {
1330
+ name: "status",
1331
+ description: "Show grant system status (mode, auth, active grants)",
1332
+ handler: async () => {
1333
+ console.log(`Mode: ${config.mode}`);
1334
+ console.log(`Adapters: ${adapters.length} loaded`);
1335
+ console.log(`Active grants: ${store.getActiveGrantCount()}`);
1336
+ console.log(`Cache entries: ${cache.size()}`);
1337
+ }
1338
+ },
1339
+ {
1340
+ name: "list",
1341
+ description: "List all grants",
1342
+ handler: async () => {
1343
+ const grants = store.listGrants();
1344
+ if (grants.length === 0) {
1345
+ console.log("No grants");
1346
+ return;
1347
+ }
1348
+ for (const g of grants) {
1349
+ console.log(`${g.id} [${g.status}] ${g.permission} (${g.approval})`);
1350
+ }
1351
+ }
1352
+ },
1353
+ {
1354
+ name: "revoke",
1355
+ description: "Revoke a grant by ID",
1356
+ handler: async (args) => {
1357
+ const id = args[0];
1358
+ if (!id) {
1359
+ console.error("Usage: openclaw grants revoke <grant-id>");
1360
+ return;
1361
+ }
1362
+ const success = store.revokeGrant(id);
1363
+ if (success) {
1364
+ cache.remove(store.getGrant(id)?.permission ?? "");
1365
+ audit.write({ event: "grant_revoked", grantId: id });
1366
+ console.log(`Grant ${id} revoked`);
1367
+ } else {
1368
+ console.error(`Grant ${id} not found or cannot be revoked`);
1369
+ }
1370
+ }
1371
+ },
1372
+ {
1373
+ name: "adapters",
1374
+ description: "List loaded adapters and their operations",
1375
+ handler: async () => {
1376
+ if (adapters.length === 0) {
1377
+ console.log("No adapters loaded");
1378
+ return;
1379
+ }
1380
+ for (const a of adapters) {
1381
+ console.log(`
1382
+ ${a.adapter.cli.id} (${a.adapter.cli.executable})`);
1383
+ for (const op of a.adapter.operations) {
1384
+ console.log(` ${op.id}: ${op.display} [${op.risk}]`);
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ ],
1390
+ handler: async (args) => {
1391
+ console.log(`Unknown subcommand: ${args.join(" ")}`);
1392
+ console.log("Available: status, list, revoke, adapters");
1393
+ }
1394
+ });
1395
+ api.log.info("[grants] Plugin registered successfully");
1396
+ }
1397
+ export {
1398
+ AuditLog,
1399
+ ChannelApproval,
1400
+ GrantCache,
1401
+ GrantStore,
1402
+ LocalJwtSigner,
1403
+ authenticateAgent,
1404
+ buildApesArgs,
1405
+ clearDiscoveryCache,
1406
+ createFallbackCommand,
1407
+ detectApes,
1408
+ discoverAdapters,
1409
+ discoverEndpoints,
1410
+ discoverIdpUrl,
1411
+ executeCommand,
1412
+ getGrantsEndpoint,
1413
+ handleGrantExec,
1414
+ handleIdpGrantExec,
1415
+ isTokenExpired,
1416
+ loadAdapter,
1417
+ loadAdapterFromFile,
1418
+ parseAdapterToml,
1419
+ parseCommandString,
1420
+ register,
1421
+ resolveCommand,
1422
+ resolveCommandFromAdapters
1423
+ };
1424
+ //# sourceMappingURL=index.js.map