@openape/apes 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1331 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var CONFIG_DIR = join(homedir(), ".config", "apes");
8
+ var AUTH_FILE = join(CONFIG_DIR, "auth.json");
9
+ var CONFIG_FILE = join(CONFIG_DIR, "config.toml");
10
+ function ensureDir() {
11
+ if (!existsSync(CONFIG_DIR)) {
12
+ mkdirSync(CONFIG_DIR, { recursive: true });
13
+ }
14
+ }
15
+ function loadAuth() {
16
+ if (!existsSync(AUTH_FILE))
17
+ return null;
18
+ try {
19
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function saveAuth(data) {
25
+ ensureDir();
26
+ writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
27
+ }
28
+ function clearAuth() {
29
+ if (existsSync(AUTH_FILE)) {
30
+ writeFileSync(AUTH_FILE, "", { mode: 384 });
31
+ }
32
+ }
33
+ function loadConfig() {
34
+ if (!existsSync(CONFIG_FILE))
35
+ return {};
36
+ try {
37
+ return parseTOML(readFileSync(CONFIG_FILE, "utf-8"));
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+ function parseTOML(content) {
43
+ const config = {};
44
+ let section = "";
45
+ for (const line of content.split("\n")) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed || trimmed.startsWith("#"))
48
+ continue;
49
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
50
+ if (sectionMatch) {
51
+ section = sectionMatch[1];
52
+ continue;
53
+ }
54
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*"(.+)"$/);
55
+ if (kvMatch) {
56
+ const [, key, value] = kvMatch;
57
+ if (section === "defaults") {
58
+ config.defaults = config.defaults || {};
59
+ config.defaults[key] = value;
60
+ } else if (section === "agent") {
61
+ config.agent = config.agent || {};
62
+ config.agent[key] = value;
63
+ }
64
+ }
65
+ }
66
+ return config;
67
+ }
68
+ function saveConfig(config) {
69
+ ensureDir();
70
+ const lines = [];
71
+ if (config.defaults) {
72
+ lines.push("[defaults]");
73
+ for (const [key, value] of Object.entries(config.defaults)) {
74
+ if (value)
75
+ lines.push(`${key} = "${value}"`);
76
+ }
77
+ lines.push("");
78
+ }
79
+ if (config.agent) {
80
+ lines.push("[agent]");
81
+ for (const [key, value] of Object.entries(config.agent)) {
82
+ if (value)
83
+ lines.push(`${key} = "${value}"`);
84
+ }
85
+ lines.push("");
86
+ }
87
+ writeFileSync(CONFIG_FILE, lines.join("\n"), { mode: 384 });
88
+ }
89
+ function getIdpUrl(explicit) {
90
+ if (explicit)
91
+ return explicit;
92
+ if (process.env.APES_IDP)
93
+ return process.env.APES_IDP;
94
+ const auth = loadAuth();
95
+ if (auth?.idp)
96
+ return auth.idp;
97
+ const config = loadConfig();
98
+ if (config.defaults?.idp)
99
+ return config.defaults.idp;
100
+ return null;
101
+ }
102
+ function getAuthToken() {
103
+ const auth = loadAuth();
104
+ if (!auth)
105
+ return null;
106
+ if (auth.expires_at && Date.now() / 1e3 > auth.expires_at - 30) {
107
+ return null;
108
+ }
109
+ return auth.access_token;
110
+ }
111
+ function getRequesterIdentity() {
112
+ return loadAuth()?.email ?? null;
113
+ }
114
+
115
+ // src/http.ts
116
+ import consola from "consola";
117
+ var debug = process.argv.includes("--debug");
118
+ var ApiError = class extends Error {
119
+ constructor(statusCode, message, problemDetails) {
120
+ super(message);
121
+ this.statusCode = statusCode;
122
+ this.problemDetails = problemDetails;
123
+ this.name = "ApiError";
124
+ }
125
+ };
126
+ var _discoveryCache = {};
127
+ async function discoverEndpoints(idpUrl) {
128
+ if (_discoveryCache[idpUrl]) {
129
+ return _discoveryCache[idpUrl];
130
+ }
131
+ try {
132
+ const response = await fetch(`${idpUrl}/.well-known/openid-configuration`);
133
+ if (response.ok) {
134
+ const data = await response.json();
135
+ _discoveryCache[idpUrl] = data;
136
+ return data;
137
+ }
138
+ } catch {
139
+ }
140
+ _discoveryCache[idpUrl] = {};
141
+ return {};
142
+ }
143
+ async function getGrantsEndpoint(idpUrl) {
144
+ const disco = await discoverEndpoints(idpUrl);
145
+ return disco.openape_grants_endpoint || `${idpUrl}/api/grants`;
146
+ }
147
+ async function getAgentChallengeEndpoint(idpUrl) {
148
+ const disco = await discoverEndpoints(idpUrl);
149
+ return disco.ddisa_agent_challenge_endpoint || `${idpUrl}/api/agent/challenge`;
150
+ }
151
+ async function getAgentAuthenticateEndpoint(idpUrl) {
152
+ const disco = await discoverEndpoints(idpUrl);
153
+ return disco.ddisa_agent_authenticate_endpoint || `${idpUrl}/api/agent/authenticate`;
154
+ }
155
+ async function getDelegationsEndpoint(idpUrl) {
156
+ const disco = await discoverEndpoints(idpUrl);
157
+ return disco.openape_delegations_endpoint || `${idpUrl}/api/delegations`;
158
+ }
159
+ async function refreshAgentToken() {
160
+ const auth = loadAuth();
161
+ if (!auth)
162
+ return null;
163
+ const config = loadConfig();
164
+ const keyPath = config.agent?.key;
165
+ if (!keyPath)
166
+ return null;
167
+ try {
168
+ const { readFileSync: readFileSync6 } = await import("fs");
169
+ const { sign } = await import("crypto");
170
+ const { homedir: homedir7 } = await import("os");
171
+ const { loadEd25519PrivateKey } = await import("./ssh-key-Q7KG4K25.js");
172
+ const resolved = keyPath.replace(/^~/, homedir7());
173
+ const keyContent = readFileSync6(resolved, "utf-8");
174
+ const privateKey = loadEd25519PrivateKey(keyContent);
175
+ const challengeUrl = await getAgentChallengeEndpoint(auth.idp);
176
+ const challengeResp = await fetch(challengeUrl, {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify({ agent_id: auth.email })
180
+ });
181
+ if (!challengeResp.ok)
182
+ return null;
183
+ const { challenge } = await challengeResp.json();
184
+ const { Buffer: Buffer2 } = await import("buffer");
185
+ const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
186
+ const authenticateUrl = await getAgentAuthenticateEndpoint(auth.idp);
187
+ const authResp = await fetch(authenticateUrl, {
188
+ method: "POST",
189
+ headers: { "Content-Type": "application/json" },
190
+ body: JSON.stringify({ agent_id: auth.email, challenge, signature })
191
+ });
192
+ if (!authResp.ok)
193
+ return null;
194
+ const { token, expires_in } = await authResp.json();
195
+ saveAuth({
196
+ ...auth,
197
+ access_token: token,
198
+ expires_at: Math.floor(Date.now() / 1e3) + (expires_in || 3600)
199
+ });
200
+ if (debug) {
201
+ consola.debug("Token refreshed via Ed25519 challenge-response");
202
+ }
203
+ return token;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ async function apiFetch(path, options = {}) {
209
+ let token = options.token || getAuthToken();
210
+ if (!token) {
211
+ token = await refreshAgentToken();
212
+ }
213
+ if (!token) {
214
+ throw new Error("Not authenticated (token expired). Run `apes login` first.");
215
+ }
216
+ let url;
217
+ if (path.startsWith("http")) {
218
+ url = path;
219
+ } else {
220
+ const idp = options.idp || getIdpUrl();
221
+ if (!idp) {
222
+ throw new Error("No IdP URL configured. Run `apes login` first or pass --idp.");
223
+ }
224
+ url = `${idp}${path}`;
225
+ }
226
+ const method = options.method || "GET";
227
+ const headers = {
228
+ "Authorization": `Bearer ${token}`,
229
+ "Content-Type": "application/json"
230
+ };
231
+ if (debug) {
232
+ consola.debug(`${method} ${url}`);
233
+ consola.debug(`Token: ${token.substring(0, 20)}...${token.substring(token.length - 10)}`);
234
+ }
235
+ const response = await fetch(url, {
236
+ method,
237
+ headers,
238
+ body: options.body ? JSON.stringify(options.body) : void 0
239
+ });
240
+ if (debug) {
241
+ consola.debug(`Response: ${response.status} ${response.statusText}`);
242
+ }
243
+ if (!response.ok) {
244
+ const contentType = response.headers.get("content-type") || "";
245
+ if (contentType.includes("application/problem+json") || contentType.includes("application/json")) {
246
+ try {
247
+ const problem = await response.json();
248
+ const message = problem.detail || problem.title || problem.statusMessage || problem.message || `${response.status} ${response.statusText}`;
249
+ throw new ApiError(response.status, message, problem);
250
+ } catch (e) {
251
+ if (e instanceof ApiError)
252
+ throw e;
253
+ }
254
+ }
255
+ const text = await response.text();
256
+ throw new ApiError(response.status, text || `${response.status} ${response.statusText}`);
257
+ }
258
+ return response.json();
259
+ }
260
+
261
+ // src/shapes/adapters.ts
262
+ import { createHash } from "crypto";
263
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
264
+ import { homedir as homedir2 } from "os";
265
+ import { basename, join as join2 } from "path";
266
+
267
+ // src/shapes/toml.ts
268
+ function parseKeyValue(line) {
269
+ const eqIndex = line.indexOf("=");
270
+ if (eqIndex === -1)
271
+ return null;
272
+ const key = line.slice(0, eqIndex).trim();
273
+ const value = line.slice(eqIndex + 1).trim();
274
+ if (!key || !value)
275
+ return null;
276
+ return { key, value };
277
+ }
278
+ function parseTomlValue(raw) {
279
+ const trimmed = raw.trim();
280
+ if (trimmed.startsWith('"') && trimmed.endsWith('"'))
281
+ return trimmed.slice(1, -1);
282
+ if (trimmed === "true")
283
+ return true;
284
+ if (trimmed === "false")
285
+ return false;
286
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
287
+ const inner = trimmed.slice(1, -1).trim();
288
+ if (!inner)
289
+ return [];
290
+ return inner.split(",").map((value) => value.trim().replace(/^"|"$/g, ""));
291
+ }
292
+ return trimmed;
293
+ }
294
+ function parseAdapterToml(content) {
295
+ const result = {};
296
+ const operations = [];
297
+ let currentSection = "root";
298
+ let currentEntry = {};
299
+ const flushOperation = () => {
300
+ if (currentSection === "operation" && Object.keys(currentEntry).length > 0) {
301
+ operations.push(currentEntry);
302
+ currentEntry = {};
303
+ }
304
+ };
305
+ for (const rawLine of content.split("\n")) {
306
+ const line = rawLine.trim();
307
+ if (!line || line.startsWith("#"))
308
+ continue;
309
+ if (line === "[cli]") {
310
+ flushOperation();
311
+ currentSection = "cli";
312
+ result.cli = {};
313
+ continue;
314
+ }
315
+ if (line === "[[operation]]") {
316
+ flushOperation();
317
+ currentSection = "operation";
318
+ currentEntry = {};
319
+ continue;
320
+ }
321
+ const kv = parseKeyValue(line);
322
+ if (!kv)
323
+ continue;
324
+ const value = parseTomlValue(kv.value);
325
+ if (currentSection === "root") {
326
+ ;
327
+ result[kv.key] = value;
328
+ } else if (currentSection === "cli") {
329
+ ;
330
+ result.cli[kv.key] = value;
331
+ } else {
332
+ currentEntry[kv.key] = value;
333
+ }
334
+ }
335
+ flushOperation();
336
+ result.operation = operations;
337
+ if (result.schema !== "openape-shapes/v1") {
338
+ throw new Error(`Unsupported adapter schema: ${result.schema ?? "missing"}`);
339
+ }
340
+ if (!result.cli?.id || !result.cli.executable) {
341
+ throw new Error("Adapter is missing cli.id or cli.executable");
342
+ }
343
+ if (!result.operation?.length) {
344
+ throw new Error("Adapter must define at least one [[operation]] entry");
345
+ }
346
+ return {
347
+ schema: result.schema,
348
+ cli: {
349
+ id: String(result.cli.id),
350
+ executable: String(result.cli.executable),
351
+ ...result.cli.audience ? { audience: String(result.cli.audience) } : {},
352
+ ...result.cli.version ? { version: String(result.cli.version) } : {}
353
+ },
354
+ operations: result.operation.map((operation) => {
355
+ if (!Array.isArray(operation.command) || operation.command.some((token) => typeof token !== "string")) {
356
+ throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing command[]`);
357
+ }
358
+ if (!Array.isArray(operation.resource_chain) || operation.resource_chain.some((token) => typeof token !== "string")) {
359
+ throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing resource_chain[]`);
360
+ }
361
+ if (typeof operation.id !== "string" || typeof operation.display !== "string" || typeof operation.action !== "string") {
362
+ throw new TypeError("Operation must define id, display, and action");
363
+ }
364
+ return {
365
+ id: operation.id,
366
+ command: operation.command,
367
+ ...Array.isArray(operation.positionals) ? { positionals: operation.positionals } : {},
368
+ ...Array.isArray(operation.required_options) ? { required_options: operation.required_options } : {},
369
+ display: operation.display,
370
+ action: operation.action,
371
+ risk: operation.risk || "low",
372
+ resource_chain: operation.resource_chain,
373
+ ...operation.exact_command !== void 0 ? { exact_command: Boolean(operation.exact_command) } : {}
374
+ };
375
+ })
376
+ };
377
+ }
378
+
379
+ // src/shapes/adapters.ts
380
+ function digest(content) {
381
+ return `SHA-256:${createHash("sha256").update(content).digest("hex")}`;
382
+ }
383
+ function adapterDirs() {
384
+ return [
385
+ join2(process.cwd(), ".openape", "shapes", "adapters"),
386
+ join2(homedir2(), ".openape", "shapes", "adapters"),
387
+ join2("/etc", "openape", "shapes", "adapters")
388
+ ];
389
+ }
390
+ function findByExecutable(executable) {
391
+ for (const dir of adapterDirs()) {
392
+ if (!existsSync2(dir))
393
+ continue;
394
+ try {
395
+ const files = readdirSync(dir).filter((f) => f.endsWith(".toml"));
396
+ for (const file of files) {
397
+ const path = join2(dir, file);
398
+ const content = readFileSync2(path, "utf-8");
399
+ const match = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
400
+ if (match && match[1] === executable)
401
+ return path;
402
+ }
403
+ } catch {
404
+ }
405
+ }
406
+ return void 0;
407
+ }
408
+ function resolveAdapterPath(cliId, explicitPath) {
409
+ if (explicitPath) {
410
+ if (existsSync2(explicitPath))
411
+ return explicitPath;
412
+ throw new Error(`Adapter file not found: ${explicitPath}`);
413
+ }
414
+ const candidates = adapterDirs().map((dir) => join2(dir, `${cliId}.toml`));
415
+ const match = candidates.find((path) => existsSync2(path));
416
+ if (match)
417
+ return match;
418
+ const byExec = findByExecutable(cliId);
419
+ if (byExec)
420
+ return byExec;
421
+ throw new Error(`No adapter found for ${cliId}`);
422
+ }
423
+ function loadAdapter(cliId, explicitPath) {
424
+ const source = resolveAdapterPath(cliId, explicitPath);
425
+ const content = readFileSync2(source, "utf-8");
426
+ const adapter = parseAdapterToml(content);
427
+ const idMatch = adapter.cli.id === cliId;
428
+ const fileMatch = basename(source) === `${cliId}.toml`;
429
+ const execMatch = adapter.cli.executable === cliId;
430
+ if (!idMatch && !fileMatch && !execMatch)
431
+ throw new Error(`Adapter ${source} does not match requested CLI ${cliId}`);
432
+ return {
433
+ adapter,
434
+ source,
435
+ digest: digest(content)
436
+ };
437
+ }
438
+ function tryLoadAdapter(cliId, explicitPath) {
439
+ try {
440
+ return loadAdapter(cliId, explicitPath);
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+
446
+ // src/shapes/audit.ts
447
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
448
+ import { homedir as homedir3 } from "os";
449
+ import { dirname, join as join3 } from "path";
450
+ function auditPath() {
451
+ return join3(homedir3(), ".config", "apes", "audit.jsonl");
452
+ }
453
+ function appendAuditLog(entry) {
454
+ const full = {
455
+ ...entry,
456
+ action: entry.action,
457
+ timestamp: entry.timestamp ?? Date.now()
458
+ };
459
+ const path = auditPath();
460
+ const dir = dirname(path);
461
+ try {
462
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
463
+ appendFileSync(path, `${JSON.stringify(full)}
464
+ `);
465
+ } catch {
466
+ }
467
+ }
468
+
469
+ // src/shapes/installer.ts
470
+ import { createHash as createHash2 } from "crypto";
471
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
472
+ import { homedir as homedir4 } from "os";
473
+ import { join as join4 } from "path";
474
+ function adapterDir(local) {
475
+ const base = local ? process.cwd() : homedir4();
476
+ return join4(base, ".openape", "shapes", "adapters");
477
+ }
478
+ function adapterPath(id, local) {
479
+ return join4(adapterDir(local), `${id}.toml`);
480
+ }
481
+ function sha256(content) {
482
+ return `SHA-256:${createHash2("sha256").update(content).digest("hex")}`;
483
+ }
484
+ async function installAdapter(entry, options = {}) {
485
+ const local = options.local ?? false;
486
+ const dest = adapterPath(entry.id, local);
487
+ const dir = adapterDir(local);
488
+ const response = await fetch(entry.download_url);
489
+ if (!response.ok)
490
+ throw new Error(`Failed to download adapter ${entry.id}: ${response.status} ${response.statusText}`);
491
+ const content = await response.text();
492
+ const digest2 = sha256(content);
493
+ if (digest2 !== entry.digest)
494
+ throw new Error(`Digest mismatch for ${entry.id}: expected ${entry.digest}, got ${digest2}`);
495
+ const updated = existsSync4(dest);
496
+ if (!existsSync4(dir))
497
+ mkdirSync3(dir, { recursive: true });
498
+ writeFileSync2(dest, content);
499
+ return { id: entry.id, path: dest, digest: digest2, updated };
500
+ }
501
+ function getInstalledDigest(id, local) {
502
+ const path = adapterPath(id, local);
503
+ if (!existsSync4(path))
504
+ return null;
505
+ const content = readFileSync3(path, "utf-8");
506
+ return sha256(content);
507
+ }
508
+ function isInstalled(id, local) {
509
+ return existsSync4(adapterPath(id, local));
510
+ }
511
+ function removeAdapter(id, local) {
512
+ const path = adapterPath(id, local);
513
+ if (!existsSync4(path))
514
+ return false;
515
+ unlinkSync(path);
516
+ return true;
517
+ }
518
+ function findConflictingAdapters(executable, excludeId) {
519
+ const conflicts = [];
520
+ const dirs = [
521
+ join4(process.cwd(), ".openape", "shapes", "adapters"),
522
+ join4(homedir4(), ".openape", "shapes", "adapters")
523
+ ];
524
+ for (const dir of dirs) {
525
+ if (!existsSync4(dir))
526
+ continue;
527
+ try {
528
+ for (const file of readdirSync2(dir).filter((f) => f.endsWith(".toml"))) {
529
+ const path = join4(dir, file);
530
+ const content = readFileSync3(path, "utf-8");
531
+ const execMatch = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
532
+ const idMatch = content.match(/^\s*id\s*=\s*"([^"]+)"/m);
533
+ if (execMatch?.[1] === executable && idMatch?.[1] !== excludeId) {
534
+ conflicts.push({ file, path, adapterId: idMatch?.[1] ?? file, executable });
535
+ }
536
+ }
537
+ } catch {
538
+ }
539
+ }
540
+ return conflicts;
541
+ }
542
+
543
+ // src/shapes/registry.ts
544
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
545
+ import { homedir as homedir5 } from "os";
546
+ import { join as join5 } from "path";
547
+ var REGISTRY_URL = process.env.SHAPES_REGISTRY_URL ?? "https://raw.githubusercontent.com/openape-ai/shapes-registry/main/registry.json";
548
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
549
+ function cacheDir() {
550
+ return join5(homedir5(), ".openape", "shapes", "cache");
551
+ }
552
+ function cachePath() {
553
+ return join5(cacheDir(), "registry.json");
554
+ }
555
+ function readCache() {
556
+ const path = cachePath();
557
+ if (!existsSync5(path))
558
+ return null;
559
+ try {
560
+ const raw = readFileSync4(path, "utf-8");
561
+ const stat = JSON.parse(raw);
562
+ if (stat._cached_at && Date.now() - stat._cached_at > CACHE_TTL_MS)
563
+ return null;
564
+ return stat;
565
+ } catch {
566
+ return null;
567
+ }
568
+ }
569
+ function writeCache(index) {
570
+ const dir = cacheDir();
571
+ if (!existsSync5(dir))
572
+ mkdirSync4(dir, { recursive: true });
573
+ writeFileSync3(cachePath(), JSON.stringify({ ...index, _cached_at: Date.now() }, null, 2));
574
+ }
575
+ async function fetchRegistry(forceRefresh = false) {
576
+ if (!forceRefresh) {
577
+ const cached = readCache();
578
+ if (cached)
579
+ return cached;
580
+ }
581
+ const response = await fetch(REGISTRY_URL);
582
+ if (!response.ok)
583
+ throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`);
584
+ const index = await response.json();
585
+ writeCache(index);
586
+ return index;
587
+ }
588
+ function searchAdapters(index, query) {
589
+ const q = query.toLowerCase();
590
+ return index.adapters.filter(
591
+ (a) => a.id.includes(q) || a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) || a.tags.some((t) => t.includes(q)) || a.category.includes(q)
592
+ );
593
+ }
594
+ function findAdapter(index, id) {
595
+ return index.adapters.find((a) => a.id === id);
596
+ }
597
+
598
+ // src/shapes/shell-parser.ts
599
+ import consola2 from "consola";
600
+ import { parse as shellParse } from "shell-quote";
601
+ var COMPOUND_OPERATORS = /* @__PURE__ */ new Set(["&&", "||", ";", "|", "&", ">", ">>", "<"]);
602
+ function parseShellCommand(raw) {
603
+ const trimmed = raw.trim();
604
+ if (trimmed.length === 0) return null;
605
+ let tokens;
606
+ try {
607
+ tokens = shellParse(trimmed);
608
+ } catch {
609
+ return null;
610
+ }
611
+ const hasShellExpansion = /\$\(|`/.test(trimmed);
612
+ const hasOperatorToken = tokens.some((t) => typeof t === "object" && t !== null && "op" in t && COMPOUND_OPERATORS.has(t.op));
613
+ const isCompound = hasShellExpansion || hasOperatorToken;
614
+ const stringTokens = [];
615
+ for (const t of tokens) {
616
+ if (typeof t === "string") {
617
+ stringTokens.push(t);
618
+ } else {
619
+ break;
620
+ }
621
+ }
622
+ if (stringTokens.length === 0) return null;
623
+ return {
624
+ executable: stringTokens[0],
625
+ argv: stringTokens.slice(1),
626
+ isCompound,
627
+ raw: trimmed
628
+ };
629
+ }
630
+ function extractShellCommandString(command) {
631
+ if (command.length < 3) return null;
632
+ if (command[0] !== "bash" && command[0] !== "sh") return null;
633
+ if (command[1] !== "-c") return null;
634
+ return command.slice(2).join(" ");
635
+ }
636
+ async function loadOrInstallAdapter(cliId) {
637
+ const local = tryLoadAdapter(cliId);
638
+ if (local) return local;
639
+ try {
640
+ const index = await fetchRegistry();
641
+ const entry = findAdapter(index, cliId);
642
+ if (!entry) return null;
643
+ consola2.info(`Installing shapes adapter for ${cliId} from registry...`);
644
+ await installAdapter(entry, { local: false });
645
+ appendAuditLog({
646
+ action: "adapter-auto-install",
647
+ cli_id: cliId,
648
+ digest: entry.digest,
649
+ source: "ape-shell"
650
+ });
651
+ return tryLoadAdapter(cliId);
652
+ } catch (err) {
653
+ consola2.debug(`ape-shell adapter auto-install failed for ${cliId}:`, err);
654
+ return null;
655
+ }
656
+ }
657
+
658
+ // src/shapes/capabilities.ts
659
+ import { canonicalizeCliPermission } from "@openape/grants";
660
+ function parseOperationChainEntry(entry) {
661
+ const [resource, selectorSpec = "*"] = entry.split(":", 2);
662
+ if (!resource) {
663
+ throw new Error(`Invalid resource chain entry: ${entry}`);
664
+ }
665
+ if (selectorSpec === "*") {
666
+ return { resource, selectorKeys: [] };
667
+ }
668
+ const selectorKeys = selectorSpec.split(",").map((segment) => {
669
+ const [key] = segment.split("=", 2);
670
+ if (!key)
671
+ throw new Error(`Invalid selector segment: ${segment}`);
672
+ return key;
673
+ });
674
+ return { resource, selectorKeys };
675
+ }
676
+ function operationChain(operation) {
677
+ return operation.resource_chain.map(parseOperationChainEntry);
678
+ }
679
+ function knownSelectorKeys(operations, resource) {
680
+ const keys = /* @__PURE__ */ new Set();
681
+ for (const operation of operations) {
682
+ for (const entry of operationChain(operation)) {
683
+ if (entry.resource !== resource)
684
+ continue;
685
+ for (const key of entry.selectorKeys) {
686
+ keys.add(key);
687
+ }
688
+ }
689
+ }
690
+ return Array.from(keys).sort();
691
+ }
692
+ function parseResourceSelector(raw) {
693
+ const [lhs, value] = raw.split("=", 2);
694
+ if (!lhs || !value) {
695
+ throw new Error(`Invalid selector: ${raw}`);
696
+ }
697
+ const [resource, key] = lhs.split(".", 2);
698
+ if (!resource || !key) {
699
+ throw new Error(`Selectors must be in resource.key=value form: ${raw}`);
700
+ }
701
+ return { resource, key, value };
702
+ }
703
+ function formatSelector(selector) {
704
+ if (!selector || Object.keys(selector).length === 0)
705
+ return "*";
706
+ return Object.entries(selector).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join(",");
707
+ }
708
+ function summarizeDetail(detail) {
709
+ const chain = detail.resource_chain.map((resource) => `${resource.resource}[${formatSelector(resource.selector)}]`).join(" -> ");
710
+ return `Allow ${detail.action} on ${detail.cli_id} ${chain}`;
711
+ }
712
+ function resolveCapabilityRequest(loaded, params) {
713
+ if (params.resources.length === 0) {
714
+ throw new Error("At least one --resource is required");
715
+ }
716
+ if (params.actions.length === 0) {
717
+ throw new Error("At least one --action is required");
718
+ }
719
+ const selectorMap = /* @__PURE__ */ new Map();
720
+ for (const rawSelector of params.selectors ?? []) {
721
+ const { resource, key, value } = parseResourceSelector(rawSelector);
722
+ const current = selectorMap.get(resource) ?? {};
723
+ current[key] = value;
724
+ selectorMap.set(resource, current);
725
+ }
726
+ const resource_chain = params.resources.map((resource) => {
727
+ const selector = selectorMap.get(resource);
728
+ const knownKeys = knownSelectorKeys(loaded.adapter.operations, resource);
729
+ if (selector) {
730
+ for (const key of Object.keys(selector)) {
731
+ if (!knownKeys.includes(key)) {
732
+ throw new Error(`Unknown selector ${resource}.${key} for adapter ${loaded.adapter.cli.id}`);
733
+ }
734
+ }
735
+ }
736
+ return selector && Object.keys(selector).length > 0 ? { resource, selector } : { resource };
737
+ });
738
+ const requestedSequence = params.resources.join("\0");
739
+ const matchingOperations = loaded.adapter.operations.filter((operation) => {
740
+ const sequence = operationChain(operation).map((entry) => entry.resource).join("\0");
741
+ return sequence === requestedSequence || sequence.startsWith(`${requestedSequence}\0`);
742
+ });
743
+ if (matchingOperations.length === 0) {
744
+ throw new Error(`No adapter operation supports resource chain: ${params.resources.join(" -> ")}`);
745
+ }
746
+ const details = params.actions.map((action) => {
747
+ const matchingActionOps = matchingOperations.filter((operation) => operation.action === action);
748
+ if (matchingActionOps.length === 0) {
749
+ throw new Error(`Action ${action} is not valid for resource chain: ${params.resources.join(" -> ")}`);
750
+ }
751
+ const exact_command = matchingActionOps.every((operation) => operation.exact_command === true);
752
+ const risks = ["low", "medium", "high", "critical"];
753
+ const risk = matchingActionOps.reduce((current, operation) => {
754
+ return risks.indexOf(operation.risk) > risks.indexOf(current) ? operation.risk : current;
755
+ }, "low");
756
+ const detail = {
757
+ type: "openape_cli",
758
+ cli_id: loaded.adapter.cli.id,
759
+ operation_id: `capability.${action}`,
760
+ resource_chain,
761
+ action,
762
+ permission: "",
763
+ display: "",
764
+ risk,
765
+ ...exact_command ? { constraints: { exact_command: true } } : {}
766
+ };
767
+ detail.permission = canonicalizeCliPermission(detail);
768
+ detail.display = summarizeDetail(detail);
769
+ return detail;
770
+ });
771
+ return {
772
+ adapter: loaded.adapter,
773
+ source: loaded.source,
774
+ digest: loaded.digest,
775
+ executable: loaded.adapter.cli.executable,
776
+ details,
777
+ executionContext: {
778
+ adapter_id: loaded.adapter.cli.id,
779
+ adapter_version: loaded.adapter.cli.version ?? loaded.adapter.schema,
780
+ adapter_digest: loaded.digest,
781
+ resolved_executable: loaded.adapter.cli.executable,
782
+ context_bindings: Object.fromEntries(
783
+ Array.from(selectorMap.entries()).flatMap(
784
+ ([resource, selector]) => Object.entries(selector).map(([key, value]) => [`${resource}.${key}`, value])
785
+ )
786
+ )
787
+ },
788
+ permissions: details.map((detail) => detail.permission),
789
+ summary: details.map((detail) => detail.display).join("; ")
790
+ };
791
+ }
792
+
793
+ // src/shapes/parser.ts
794
+ import { canonicalizeCliPermission as canonicalizeCliPermission2, computeArgvHash } from "@openape/grants";
795
+ function parseOptionArgs(tokens, valueOptions) {
796
+ const options = {};
797
+ const positionals = [];
798
+ const takesValue = new Set(valueOptions ?? []);
799
+ for (let index = 0; index < tokens.length; index += 1) {
800
+ const token = tokens[index];
801
+ if (token.startsWith("--")) {
802
+ const stripped = token.slice(2);
803
+ const eqIndex = stripped.indexOf("=");
804
+ if (eqIndex >= 0) {
805
+ options[stripped.slice(0, eqIndex)] = stripped.slice(eqIndex + 1);
806
+ continue;
807
+ }
808
+ const next = tokens[index + 1];
809
+ if (next && !next.startsWith("-")) {
810
+ options[stripped] = next;
811
+ index += 1;
812
+ continue;
813
+ }
814
+ options[stripped] = "true";
815
+ } else if (token.startsWith("-") && token.length > 1 && !/^-\d/.test(token)) {
816
+ const key = token.slice(1);
817
+ if (key.length === 1 && !takesValue.has(key)) {
818
+ options[key] = "true";
819
+ } else {
820
+ const next = tokens[index + 1];
821
+ if (next && !next.startsWith("-")) {
822
+ options[key] = next;
823
+ index += 1;
824
+ } else {
825
+ options[key] = "true";
826
+ }
827
+ }
828
+ } else {
829
+ positionals.push(token);
830
+ }
831
+ }
832
+ return { options, positionals };
833
+ }
834
+ function resolveBindingToken(binding, bindings) {
835
+ const match = binding.match(/^\{([^}|]+)(?:\|([^}]+))?\}$/);
836
+ if (!match)
837
+ return binding;
838
+ const [, name, transform] = match;
839
+ const value = bindings[name];
840
+ if (!value)
841
+ throw new Error(`Missing binding: ${name}`);
842
+ if (!transform)
843
+ return value;
844
+ if (transform === "owner" || transform === "name") {
845
+ const [owner, repo] = value.split("/");
846
+ if (!owner || !repo)
847
+ throw new Error(`Binding ${name} must be in owner/name form`);
848
+ return transform === "owner" ? owner : repo;
849
+ }
850
+ throw new Error(`Unsupported binding transform: ${transform}`);
851
+ }
852
+ function renderTemplate(template, bindings) {
853
+ return template.replace(/\{([^}]+)\}/g, (_, expression) => resolveBindingToken(`{${expression}}`, bindings));
854
+ }
855
+ function parseResourceChain(chain, bindings) {
856
+ return chain.map((entry) => {
857
+ const [resource, selectorSpec = "*"] = entry.split(":", 2);
858
+ if (!resource)
859
+ throw new Error(`Invalid resource chain entry: ${entry}`);
860
+ if (selectorSpec === "*") {
861
+ return { resource };
862
+ }
863
+ const selector = Object.fromEntries(
864
+ selectorSpec.split(",").map((segment) => {
865
+ const [key, rawValue] = segment.split("=", 2);
866
+ if (!key || !rawValue)
867
+ throw new Error(`Invalid selector segment: ${segment}`);
868
+ return [key, renderTemplate(rawValue, bindings)];
869
+ })
870
+ );
871
+ return { resource, selector };
872
+ });
873
+ }
874
+ function matchOperation(operation, argv) {
875
+ if (argv.length < operation.command.length)
876
+ return null;
877
+ const prefix = argv.slice(0, operation.command.length);
878
+ if (prefix.join("\0") !== operation.command.join("\0"))
879
+ return null;
880
+ const remainder = argv.slice(operation.command.length);
881
+ const { options, positionals } = parseOptionArgs(remainder, operation.required_options);
882
+ const expectedPositionals = operation.positionals ?? [];
883
+ if (positionals.length !== expectedPositionals.length)
884
+ return null;
885
+ for (const option of operation.required_options ?? []) {
886
+ if (!options[option])
887
+ return null;
888
+ }
889
+ const bindings = { ...options };
890
+ expectedPositionals.forEach((name, index) => {
891
+ bindings[name] = positionals[index];
892
+ });
893
+ return bindings;
894
+ }
895
+ function expandCombinedFlags(argv) {
896
+ return argv.flatMap((token) => {
897
+ if (token.startsWith("-") && !token.startsWith("--") && token.length > 2 && /^-[a-z]+$/i.test(token)) {
898
+ return Array.from(token.slice(1), (c) => `-${c}`);
899
+ }
900
+ return [token];
901
+ });
902
+ }
903
+ function tryMatch(operations, argv) {
904
+ return operations.flatMap((operation) => {
905
+ try {
906
+ const bindings = matchOperation(operation, argv);
907
+ return bindings ? [{ operation, bindings }] : [];
908
+ } catch {
909
+ return [];
910
+ }
911
+ });
912
+ }
913
+ async function resolveCommand(loaded, fullArgv) {
914
+ const [executable, ...commandArgv] = fullArgv;
915
+ if (!executable) {
916
+ throw new Error("Missing wrapped command");
917
+ }
918
+ if (executable !== loaded.adapter.cli.executable) {
919
+ throw new Error(`Adapter ${loaded.adapter.cli.id} expects executable ${loaded.adapter.cli.executable}, got ${executable}`);
920
+ }
921
+ let matches = tryMatch(loaded.adapter.operations, commandArgv);
922
+ if (matches.length === 0) {
923
+ const expanded = expandCombinedFlags(commandArgv);
924
+ if (expanded.length !== commandArgv.length) {
925
+ matches = tryMatch(loaded.adapter.operations, expanded);
926
+ }
927
+ }
928
+ if (matches.length === 0) {
929
+ throw new Error(`No adapter operation matched: ${fullArgv.join(" ")}`);
930
+ }
931
+ if (matches.length > 1) {
932
+ matches.sort((a, b) => b.operation.command.length - a.operation.command.length);
933
+ matches = [matches[0]];
934
+ }
935
+ const { operation, bindings } = matches[0];
936
+ const resource_chain = parseResourceChain(operation.resource_chain, bindings);
937
+ const detail = {
938
+ type: "openape_cli",
939
+ cli_id: loaded.adapter.cli.id,
940
+ operation_id: operation.id,
941
+ resource_chain,
942
+ action: operation.action,
943
+ permission: "",
944
+ display: renderTemplate(operation.display, bindings),
945
+ risk: operation.risk,
946
+ ...operation.exact_command ? { constraints: { exact_command: true } } : {}
947
+ };
948
+ detail.permission = canonicalizeCliPermission2(detail);
949
+ return {
950
+ adapter: loaded.adapter,
951
+ source: loaded.source,
952
+ digest: loaded.digest,
953
+ executable,
954
+ commandArgv,
955
+ bindings,
956
+ detail,
957
+ executionContext: {
958
+ argv: fullArgv,
959
+ argv_hash: await computeArgvHash(fullArgv),
960
+ adapter_id: loaded.adapter.cli.id,
961
+ adapter_version: loaded.adapter.cli.version ?? loaded.adapter.schema,
962
+ adapter_digest: loaded.digest,
963
+ resolved_executable: executable,
964
+ context_bindings: bindings
965
+ },
966
+ permission: detail.permission
967
+ };
968
+ }
969
+
970
+ // src/shapes/commands/explain.ts
971
+ import { defineCommand } from "citty";
972
+ var explainCommand = defineCommand({
973
+ meta: {
974
+ name: "explain",
975
+ description: "Show what permission a wrapped command would need"
976
+ },
977
+ args: {
978
+ adapter: {
979
+ type: "string",
980
+ description: "Explicit path to adapter TOML file"
981
+ },
982
+ _: {
983
+ type: "positional",
984
+ description: "Wrapped command (after --)",
985
+ required: false
986
+ }
987
+ },
988
+ async run({ rawArgs }) {
989
+ const command = extractWrappedCommand(rawArgs ?? []);
990
+ if (command.length === 0)
991
+ throw new Error("Missing wrapped command. Usage: shapes explain [--adapter <file>] -- <cli> ...");
992
+ const adapterOpt = extractOption(rawArgs ?? [], "adapter");
993
+ const loaded = loadAdapter(command[0], adapterOpt);
994
+ const resolved = await resolveCommand(loaded, command);
995
+ process.stdout.write(`${JSON.stringify({
996
+ adapter: resolved.adapter.cli.id,
997
+ source: resolved.source,
998
+ operation: resolved.detail.operation_id,
999
+ display: resolved.detail.display,
1000
+ permission: resolved.permission,
1001
+ resource_chain: resolved.detail.resource_chain,
1002
+ exact_command: resolved.detail.constraints?.exact_command ?? false,
1003
+ adapter_digest: resolved.digest
1004
+ }, null, 2)}
1005
+ `);
1006
+ }
1007
+ });
1008
+ function extractWrappedCommand(args) {
1009
+ const delimiter = args.indexOf("--");
1010
+ return delimiter >= 0 ? args.slice(delimiter + 1) : [];
1011
+ }
1012
+ function extractOption(args, name) {
1013
+ const delimiter = args.indexOf("--");
1014
+ const optionArgs = delimiter >= 0 ? args.slice(0, delimiter) : args;
1015
+ const index = optionArgs.indexOf(`--${name}`);
1016
+ if (index >= 0 && index + 1 < optionArgs.length)
1017
+ return optionArgs[index + 1];
1018
+ return void 0;
1019
+ }
1020
+
1021
+ // src/shapes/grants.ts
1022
+ import { computeCmdHash } from "@openape/core";
1023
+ import { cliAuthorizationDetailCovers, verifyAuthzJWT } from "@openape/grants";
1024
+ import { execFileSync } from "child_process";
1025
+ import { hostname } from "os";
1026
+ import consola3 from "consola";
1027
+
1028
+ // src/shapes/config.ts
1029
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1030
+ import { homedir as homedir6 } from "os";
1031
+ import { join as join6 } from "path";
1032
+ var AUTH_FILE2 = join6(homedir6(), ".config", "apes", "auth.json");
1033
+ function loadAuth2() {
1034
+ if (!existsSync6(AUTH_FILE2))
1035
+ return null;
1036
+ try {
1037
+ return JSON.parse(readFileSync5(AUTH_FILE2, "utf-8"));
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+ function getIdpUrl2(explicit) {
1043
+ if (explicit)
1044
+ return explicit;
1045
+ if (process.env.APES_IDP)
1046
+ return process.env.APES_IDP;
1047
+ if (process.env.SHAPES_IDP)
1048
+ return process.env.SHAPES_IDP;
1049
+ return loadAuth2()?.idp ?? null;
1050
+ }
1051
+ function getAuthToken2() {
1052
+ const auth = loadAuth2();
1053
+ if (!auth)
1054
+ return null;
1055
+ if (auth.expires_at && Date.now() / 1e3 > auth.expires_at - 30)
1056
+ return null;
1057
+ return auth.access_token;
1058
+ }
1059
+ function getRequesterIdentity2() {
1060
+ return loadAuth2()?.email ?? null;
1061
+ }
1062
+
1063
+ // src/shapes/http.ts
1064
+ async function discoverEndpoints2(idpUrl) {
1065
+ const response = await fetch(`${idpUrl}/.well-known/openid-configuration`);
1066
+ if (!response.ok)
1067
+ return {};
1068
+ return response.json();
1069
+ }
1070
+ async function getGrantsEndpoint2(idpUrl) {
1071
+ const discovery = await discoverEndpoints2(idpUrl);
1072
+ return String(discovery.openape_grants_endpoint ?? `${idpUrl}/api/grants`);
1073
+ }
1074
+ async function apiFetch2(path, options = {}) {
1075
+ const token = options.token ?? getAuthToken2();
1076
+ if (!token)
1077
+ throw new Error("Not authenticated. Run `apes login` first.");
1078
+ const idp = options.idp ?? getIdpUrl2();
1079
+ if (!path.startsWith("http") && !idp)
1080
+ throw new Error("No IdP URL configured. Use --idp or log in with apes.");
1081
+ const url = path.startsWith("http") ? path : `${idp}${path}`;
1082
+ const response = await fetch(url, {
1083
+ method: options.method ?? "GET",
1084
+ headers: {
1085
+ Authorization: `Bearer ${token}`,
1086
+ "Content-Type": "application/json"
1087
+ },
1088
+ body: options.body ? JSON.stringify(options.body) : void 0
1089
+ });
1090
+ if (!response.ok) {
1091
+ const text = await response.text();
1092
+ throw new Error(text || `${response.status} ${response.statusText}`);
1093
+ }
1094
+ return response.json();
1095
+ }
1096
+
1097
+ // src/shapes/grants.ts
1098
+ function decodePayload(token) {
1099
+ const [, payload] = token.split(".");
1100
+ if (!payload)
1101
+ throw new Error("Invalid JWT");
1102
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
1103
+ }
1104
+ async function createShapesGrant(resolved, params) {
1105
+ const grantsEndpoint = await getGrantsEndpoint2(params.idp);
1106
+ const requester = getRequesterIdentity2();
1107
+ if (!requester) {
1108
+ throw new Error("No requester identity available. Run `apes login` first.");
1109
+ }
1110
+ return apiFetch2(grantsEndpoint, {
1111
+ method: "POST",
1112
+ idp: params.idp,
1113
+ body: {
1114
+ requester,
1115
+ target_host: hostname(),
1116
+ audience: resolved.adapter.cli.audience ?? "shapes",
1117
+ grant_type: params.approval,
1118
+ command: resolved.executionContext.argv,
1119
+ reason: params.reason ?? resolved.detail.display,
1120
+ permissions: [resolved.permission],
1121
+ authorization_details: [resolved.detail],
1122
+ execution_context: resolved.executionContext
1123
+ }
1124
+ });
1125
+ }
1126
+ async function waitForGrantStatus(idp, grantId) {
1127
+ const grantsEndpoint = await getGrantsEndpoint2(idp);
1128
+ const deadline = Date.now() + 3e5;
1129
+ while (Date.now() < deadline) {
1130
+ const grant = await apiFetch2(`${grantsEndpoint}/${grantId}`, { idp });
1131
+ if (grant.status === "approved" || grant.status === "denied" || grant.status === "revoked")
1132
+ return grant.status;
1133
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
1134
+ }
1135
+ throw new Error("Timed out waiting for grant approval");
1136
+ }
1137
+ async function fetchGrantToken(idp, grantId) {
1138
+ const grantsEndpoint = await getGrantsEndpoint2(idp);
1139
+ const response = await apiFetch2(`${grantsEndpoint}/${grantId}/token`, {
1140
+ method: "POST",
1141
+ idp
1142
+ });
1143
+ return response.authz_jwt;
1144
+ }
1145
+ function grantedCliDetails(claims) {
1146
+ const details = claims.authorization_details;
1147
+ if (!Array.isArray(details))
1148
+ return [];
1149
+ return details.filter(
1150
+ (detail) => typeof detail === "object" && detail !== null && detail.type === "openape_cli"
1151
+ );
1152
+ }
1153
+ function hasStructuredCliGrant(claims) {
1154
+ return grantedCliDetails(claims).length > 0;
1155
+ }
1156
+ async function verifyAndExecute(token, resolved) {
1157
+ const payload = decodePayload(token);
1158
+ const issuer = String(payload.iss ?? "");
1159
+ if (!issuer)
1160
+ throw new Error("Grant token is missing issuer");
1161
+ const discovery = await discoverEndpoints2(issuer);
1162
+ const jwksUri = String(discovery.jwks_uri ?? `${issuer}/.well-known/jwks.json`);
1163
+ const result = await verifyAuthzJWT(token, {
1164
+ expectedIss: issuer,
1165
+ expectedAud: resolved.adapter.cli.audience ?? "shapes",
1166
+ jwksUri
1167
+ });
1168
+ if (!result.valid || !result.claims) {
1169
+ throw new Error(result.error ?? "Grant verification failed");
1170
+ }
1171
+ const claims = result.claims;
1172
+ const details = grantedCliDetails(claims);
1173
+ if (claims.execution_context?.adapter_digest && claims.execution_context.adapter_digest !== resolved.digest) {
1174
+ throw new Error("Adapter digest mismatch");
1175
+ }
1176
+ if (!hasStructuredCliGrant(claims)) {
1177
+ const argv = resolved.executionContext.argv;
1178
+ if (!argv?.length) {
1179
+ throw new Error("Resolved command is missing argv");
1180
+ }
1181
+ const expectedCmdHash = await computeCmdHash(argv.join(" "));
1182
+ if (claims.command?.join("\0") !== argv.join("\0")) {
1183
+ throw new Error("Granted command does not match current argv");
1184
+ }
1185
+ if (claims.cmd_hash && claims.cmd_hash !== expectedCmdHash) {
1186
+ throw new Error("Granted command does not match current argv");
1187
+ }
1188
+ if (!claims.command?.length && !claims.cmd_hash) {
1189
+ throw new Error("Grant is not a structured CLI grant and is missing command binding");
1190
+ }
1191
+ } else {
1192
+ if (!details.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail))) {
1193
+ throw new Error(`Grant does not cover required permission: ${resolved.permission}`);
1194
+ }
1195
+ const exactRequired = details.some(
1196
+ (detail) => cliAuthorizationDetailCovers(detail, resolved.detail) && detail.constraints?.exact_command
1197
+ );
1198
+ const isOnce = claims.grant_type === "once" || claims.approval === "once";
1199
+ const enforceArgvHash = exactRequired || isOnce && !!claims.execution_context?.argv_hash;
1200
+ if (enforceArgvHash && claims.execution_context?.argv_hash !== resolved.executionContext.argv_hash) {
1201
+ throw new Error("Granted command does not match current argv");
1202
+ }
1203
+ }
1204
+ const grantsEndpoint = await getGrantsEndpoint2(issuer);
1205
+ const consume = await fetch(`${grantsEndpoint}/${claims.grant_id}/consume`, {
1206
+ method: "POST",
1207
+ headers: {
1208
+ Authorization: `Bearer ${token}`
1209
+ }
1210
+ });
1211
+ if (!consume.ok) {
1212
+ throw new Error(`Consume failed: ${consume.status} ${consume.statusText}`);
1213
+ }
1214
+ const consumeResult = await consume.json();
1215
+ if (consumeResult.error) {
1216
+ throw new Error(`Grant rejected at consume step: ${consumeResult.error}`);
1217
+ }
1218
+ consola3.info(`Executing ${(resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv]).join(" ")}`);
1219
+ execFileSync(resolved.executable, resolved.commandArgv, { stdio: "inherit" });
1220
+ }
1221
+ async function findExistingGrant(resolved, idp) {
1222
+ const grantsEndpoint = await getGrantsEndpoint2(idp);
1223
+ const response = await apiFetch2(
1224
+ `${grantsEndpoint}?status=approved`,
1225
+ { idp }
1226
+ );
1227
+ const now = Math.floor(Date.now() / 1e3);
1228
+ const expectedAudience = resolved.adapter.cli.audience ?? "shapes";
1229
+ for (const grant of response.data) {
1230
+ const req = grant.request;
1231
+ if (req.grant_type === "once")
1232
+ continue;
1233
+ if (req.grant_type === "timed" && grant.expires_at && grant.expires_at <= now)
1234
+ continue;
1235
+ if (req.audience !== expectedAudience)
1236
+ continue;
1237
+ if (req.execution_context?.adapter_digest && req.execution_context.adapter_digest !== resolved.digest)
1238
+ continue;
1239
+ const cliDetails = (req.authorization_details ?? []).filter(
1240
+ (d) => d.type === "openape_cli"
1241
+ );
1242
+ if (cliDetails.length > 0) {
1243
+ if (cliDetails.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail)))
1244
+ return grant.id;
1245
+ } else if (req.permissions?.includes(resolved.permission)) {
1246
+ return grant.id;
1247
+ }
1248
+ }
1249
+ return null;
1250
+ }
1251
+
1252
+ // src/shapes/request-builders.ts
1253
+ import { computeCmdHash as computeCmdHash2 } from "@openape/core";
1254
+ async function buildExactCommandGrantRequest(command, options) {
1255
+ return {
1256
+ request: {
1257
+ requester: options.requester,
1258
+ target_host: options.target_host,
1259
+ audience: options.audience,
1260
+ grant_type: options.grant_type,
1261
+ command,
1262
+ cmd_hash: await computeCmdHash2(command.join(" ")),
1263
+ ...options.reason ? { reason: options.reason } : {},
1264
+ ...options.run_as ? { run_as: options.run_as } : {}
1265
+ }
1266
+ };
1267
+ }
1268
+ async function buildStructuredCliGrantRequest(resolved, options) {
1269
+ const details = "detail" in resolved ? [resolved.detail] : resolved.details;
1270
+ const permissions = "permission" in resolved ? [resolved.permission] : resolved.permissions;
1271
+ const command = "executionContext" in resolved && resolved.executionContext.argv?.length ? resolved.executionContext.argv : void 0;
1272
+ return {
1273
+ request: {
1274
+ requester: options.requester,
1275
+ target_host: options.target_host,
1276
+ audience: resolved.adapter.cli.audience ?? "shapes",
1277
+ grant_type: options.grant_type,
1278
+ permissions,
1279
+ authorization_details: details,
1280
+ execution_context: resolved.executionContext,
1281
+ ...command ? { command } : {},
1282
+ ...options.reason ? { reason: options.reason } : { reason: "summary" in resolved ? resolved.summary : details[0]?.display },
1283
+ ...options.run_as ? { run_as: options.run_as } : {}
1284
+ }
1285
+ };
1286
+ }
1287
+
1288
+ export {
1289
+ loadAuth,
1290
+ saveAuth,
1291
+ clearAuth,
1292
+ loadConfig,
1293
+ saveConfig,
1294
+ getIdpUrl,
1295
+ getAuthToken,
1296
+ getRequesterIdentity,
1297
+ ApiError,
1298
+ discoverEndpoints,
1299
+ getGrantsEndpoint,
1300
+ getAgentChallengeEndpoint,
1301
+ getAgentAuthenticateEndpoint,
1302
+ getDelegationsEndpoint,
1303
+ apiFetch,
1304
+ resolveAdapterPath,
1305
+ loadAdapter,
1306
+ tryLoadAdapter,
1307
+ appendAuditLog,
1308
+ installAdapter,
1309
+ getInstalledDigest,
1310
+ isInstalled,
1311
+ removeAdapter,
1312
+ findConflictingAdapters,
1313
+ fetchRegistry,
1314
+ searchAdapters,
1315
+ findAdapter,
1316
+ parseShellCommand,
1317
+ extractShellCommandString,
1318
+ loadOrInstallAdapter,
1319
+ resolveCapabilityRequest,
1320
+ resolveCommand,
1321
+ extractWrappedCommand,
1322
+ extractOption,
1323
+ createShapesGrant,
1324
+ waitForGrantStatus,
1325
+ fetchGrantToken,
1326
+ verifyAndExecute,
1327
+ findExistingGrant,
1328
+ buildExactCommandGrantRequest,
1329
+ buildStructuredCliGrantRequest
1330
+ };
1331
+ //# sourceMappingURL=chunk-G3Q2TMAI.js.map