@openape/apes 1.28.13 → 1.29.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/chunk-BA2V3BBO.js +304 -0
- package/dist/chunk-BA2V3BBO.js.map +1 -0
- package/dist/chunk-JXS3KLJ5.js +53 -0
- package/dist/chunk-JXS3KLJ5.js.map +1 -0
- package/dist/chunk-QMMRZPD2.js +47 -0
- package/dist/chunk-QMMRZPD2.js.map +1 -0
- package/dist/{chunk-3COOEDPF.js → chunk-RVAXRDC2.js} +1 -1
- package/dist/chunk-RVAXRDC2.js.map +1 -0
- package/dist/cli.js +26 -397
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -306
- package/dist/index.js +27 -27
- package/dist/index.js.map +1 -1
- package/dist/{orchestrator-REICEX3F.js → orchestrator-P7QFDBBK.js} +8 -6
- package/dist/{orchestrator-REICEX3F.js.map → orchestrator-P7QFDBBK.js.map} +1 -1
- package/dist/{server-JGX5FIDP.js → server-QN35XDYH.js} +7 -5
- package/dist/server-QN35XDYH.js.map +1 -0
- package/package.json +6 -4
- package/dist/chunk-3COOEDPF.js.map +0 -1
- package/dist/chunk-PEA2RDWK.js +0 -1068
- package/dist/chunk-PEA2RDWK.js.map +0 -1
- package/dist/chunk-ZEUSCNCH.js +0 -1067
- package/dist/chunk-ZEUSCNCH.js.map +0 -1
- package/dist/server-JGX5FIDP.js.map +0 -1
package/dist/chunk-PEA2RDWK.js
DELETED
|
@@ -1,1068 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getGenericAuditLogPath
|
|
4
|
-
} from "./chunk-OBF7IMQ2.js";
|
|
5
|
-
|
|
6
|
-
// src/shapes/adapters.ts
|
|
7
|
-
import { createHash } from "crypto";
|
|
8
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
-
import { basename, join } from "path";
|
|
11
|
-
|
|
12
|
-
// src/shapes/toml.ts
|
|
13
|
-
function parseKeyValue(line) {
|
|
14
|
-
const eqIndex = line.indexOf("=");
|
|
15
|
-
if (eqIndex === -1)
|
|
16
|
-
return null;
|
|
17
|
-
const key = line.slice(0, eqIndex).trim();
|
|
18
|
-
const value = line.slice(eqIndex + 1).trim();
|
|
19
|
-
if (!key || !value)
|
|
20
|
-
return null;
|
|
21
|
-
return { key, value };
|
|
22
|
-
}
|
|
23
|
-
function parseTomlValue(raw) {
|
|
24
|
-
const trimmed = raw.trim();
|
|
25
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
26
|
-
return trimmed.slice(1, -1);
|
|
27
|
-
if (trimmed === "true")
|
|
28
|
-
return true;
|
|
29
|
-
if (trimmed === "false")
|
|
30
|
-
return false;
|
|
31
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
32
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
33
|
-
if (!inner)
|
|
34
|
-
return [];
|
|
35
|
-
return inner.split(",").map((value) => value.trim().replace(/^"|"$/g, ""));
|
|
36
|
-
}
|
|
37
|
-
return trimmed;
|
|
38
|
-
}
|
|
39
|
-
function parseAdapterToml(content) {
|
|
40
|
-
const result = {};
|
|
41
|
-
const operations = [];
|
|
42
|
-
let currentSection = "root";
|
|
43
|
-
let currentEntry = {};
|
|
44
|
-
const flushOperation = () => {
|
|
45
|
-
if (currentSection === "operation" && Object.keys(currentEntry).length > 0) {
|
|
46
|
-
operations.push(currentEntry);
|
|
47
|
-
currentEntry = {};
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
for (const rawLine of content.split("\n")) {
|
|
51
|
-
const line = rawLine.trim();
|
|
52
|
-
if (!line || line.startsWith("#"))
|
|
53
|
-
continue;
|
|
54
|
-
if (line === "[cli]") {
|
|
55
|
-
flushOperation();
|
|
56
|
-
currentSection = "cli";
|
|
57
|
-
result.cli = {};
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (line === "[[operation]]") {
|
|
61
|
-
flushOperation();
|
|
62
|
-
currentSection = "operation";
|
|
63
|
-
currentEntry = {};
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const kv = parseKeyValue(line);
|
|
67
|
-
if (!kv)
|
|
68
|
-
continue;
|
|
69
|
-
const value = parseTomlValue(kv.value);
|
|
70
|
-
if (currentSection === "root") {
|
|
71
|
-
;
|
|
72
|
-
result[kv.key] = value;
|
|
73
|
-
} else if (currentSection === "cli") {
|
|
74
|
-
;
|
|
75
|
-
result.cli[kv.key] = value;
|
|
76
|
-
} else {
|
|
77
|
-
currentEntry[kv.key] = value;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
flushOperation();
|
|
81
|
-
result.operation = operations;
|
|
82
|
-
if (result.schema !== "openape-shapes/v1") {
|
|
83
|
-
throw new Error(`Unsupported adapter schema: ${result.schema ?? "missing"}`);
|
|
84
|
-
}
|
|
85
|
-
if (!result.cli?.id || !result.cli.executable) {
|
|
86
|
-
throw new Error("Adapter is missing cli.id or cli.executable");
|
|
87
|
-
}
|
|
88
|
-
if (!result.operation?.length) {
|
|
89
|
-
throw new Error("Adapter must define at least one [[operation]] entry");
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
schema: result.schema,
|
|
93
|
-
cli: {
|
|
94
|
-
id: String(result.cli.id),
|
|
95
|
-
executable: String(result.cli.executable),
|
|
96
|
-
...result.cli.audience ? { audience: String(result.cli.audience) } : {},
|
|
97
|
-
...result.cli.version ? { version: String(result.cli.version) } : {}
|
|
98
|
-
},
|
|
99
|
-
operations: result.operation.map((operation) => {
|
|
100
|
-
if (!Array.isArray(operation.command) || operation.command.some((token) => typeof token !== "string")) {
|
|
101
|
-
throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing command[]`);
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(operation.resource_chain) || operation.resource_chain.some((token) => typeof token !== "string")) {
|
|
104
|
-
throw new Error(`Operation ${String(operation.id ?? "<unknown>")} is missing resource_chain[]`);
|
|
105
|
-
}
|
|
106
|
-
if (typeof operation.id !== "string" || typeof operation.display !== "string" || typeof operation.action !== "string") {
|
|
107
|
-
throw new TypeError("Operation must define id, display, and action");
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
id: operation.id,
|
|
111
|
-
command: operation.command,
|
|
112
|
-
...Array.isArray(operation.positionals) ? { positionals: operation.positionals } : {},
|
|
113
|
-
...Array.isArray(operation.required_options) ? { required_options: operation.required_options } : {},
|
|
114
|
-
display: operation.display,
|
|
115
|
-
action: operation.action,
|
|
116
|
-
risk: operation.risk || "low",
|
|
117
|
-
resource_chain: operation.resource_chain,
|
|
118
|
-
...operation.exact_command !== void 0 ? { exact_command: Boolean(operation.exact_command) } : {}
|
|
119
|
-
};
|
|
120
|
-
})
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/shapes/generic.ts
|
|
125
|
-
import { canonicalizeCliPermission, computeArgvHash } from "@openape/grants";
|
|
126
|
-
var GENERIC_OPERATION_ID = "_generic.exec";
|
|
127
|
-
var SYNTHETIC_SCHEMA = "openape-shapes/v1";
|
|
128
|
-
function buildGenericAdapter(cliId) {
|
|
129
|
-
const adapter = {
|
|
130
|
-
schema: SYNTHETIC_SCHEMA,
|
|
131
|
-
cli: {
|
|
132
|
-
id: cliId,
|
|
133
|
-
executable: cliId,
|
|
134
|
-
audience: "shapes",
|
|
135
|
-
version: "synthetic"
|
|
136
|
-
},
|
|
137
|
-
operations: []
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
adapter,
|
|
141
|
-
source: "<synthetic>",
|
|
142
|
-
digest: "synthetic",
|
|
143
|
-
synthetic: true
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
async function buildGenericResolved(cliId, fullArgv) {
|
|
147
|
-
if (fullArgv.length === 0)
|
|
148
|
-
throw new Error("buildGenericResolved: fullArgv must include the executable");
|
|
149
|
-
const executable = fullArgv[0];
|
|
150
|
-
const commandArgv = fullArgv.slice(1);
|
|
151
|
-
const argvHash = await computeArgvHash(fullArgv);
|
|
152
|
-
const display = `Execute (unshaped): \`${cliId} ${commandArgv.join(" ")}\``;
|
|
153
|
-
const detail = {
|
|
154
|
-
type: "openape_cli",
|
|
155
|
-
cli_id: cliId,
|
|
156
|
-
operation_id: GENERIC_OPERATION_ID,
|
|
157
|
-
resource_chain: [
|
|
158
|
-
{ resource: "cli", selector: { name: cliId } },
|
|
159
|
-
{ resource: "argv", selector: { hash: argvHash } }
|
|
160
|
-
],
|
|
161
|
-
action: "exec",
|
|
162
|
-
permission: "",
|
|
163
|
-
display,
|
|
164
|
-
risk: "high",
|
|
165
|
-
constraints: { exact_command: true }
|
|
166
|
-
};
|
|
167
|
-
detail.permission = canonicalizeCliPermission(detail);
|
|
168
|
-
const adapter = buildGenericAdapter(cliId);
|
|
169
|
-
return {
|
|
170
|
-
adapter: adapter.adapter,
|
|
171
|
-
source: adapter.source,
|
|
172
|
-
digest: adapter.digest,
|
|
173
|
-
executable,
|
|
174
|
-
commandArgv,
|
|
175
|
-
bindings: {},
|
|
176
|
-
detail,
|
|
177
|
-
executionContext: {
|
|
178
|
-
argv: fullArgv,
|
|
179
|
-
argv_hash: argvHash,
|
|
180
|
-
adapter_id: cliId,
|
|
181
|
-
adapter_version: SYNTHETIC_SCHEMA,
|
|
182
|
-
adapter_digest: adapter.digest,
|
|
183
|
-
resolved_executable: executable,
|
|
184
|
-
context_bindings: {}
|
|
185
|
-
},
|
|
186
|
-
permission: detail.permission
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function isGenericResolved(resolved) {
|
|
190
|
-
return resolved.detail.operation_id === GENERIC_OPERATION_ID;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// src/shapes/adapters.ts
|
|
194
|
-
function digest(content) {
|
|
195
|
-
return `SHA-256:${createHash("sha256").update(content).digest("hex")}`;
|
|
196
|
-
}
|
|
197
|
-
function adapterDirs() {
|
|
198
|
-
return [
|
|
199
|
-
join(process.cwd(), ".openape", "shapes", "adapters"),
|
|
200
|
-
join(homedir(), ".openape", "shapes", "adapters"),
|
|
201
|
-
join("/etc", "openape", "shapes", "adapters")
|
|
202
|
-
];
|
|
203
|
-
}
|
|
204
|
-
function findByExecutable(executable) {
|
|
205
|
-
for (const dir of adapterDirs()) {
|
|
206
|
-
if (!existsSync(dir))
|
|
207
|
-
continue;
|
|
208
|
-
try {
|
|
209
|
-
const files = readdirSync(dir).filter((f) => f.endsWith(".toml"));
|
|
210
|
-
for (const file of files) {
|
|
211
|
-
const path = join(dir, file);
|
|
212
|
-
const content = readFileSync(path, "utf-8");
|
|
213
|
-
const match = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
|
|
214
|
-
if (match && match[1] === executable)
|
|
215
|
-
return path;
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return void 0;
|
|
221
|
-
}
|
|
222
|
-
function resolveAdapterPath(cliId, explicitPath) {
|
|
223
|
-
if (explicitPath) {
|
|
224
|
-
if (existsSync(explicitPath))
|
|
225
|
-
return explicitPath;
|
|
226
|
-
throw new Error(`Adapter file not found: ${explicitPath}`);
|
|
227
|
-
}
|
|
228
|
-
const candidates = adapterDirs().map((dir) => join(dir, `${cliId}.toml`));
|
|
229
|
-
const match = candidates.find((path) => existsSync(path));
|
|
230
|
-
if (match)
|
|
231
|
-
return match;
|
|
232
|
-
const byExec = findByExecutable(cliId);
|
|
233
|
-
if (byExec)
|
|
234
|
-
return byExec;
|
|
235
|
-
throw new Error(`No adapter found for ${cliId}`);
|
|
236
|
-
}
|
|
237
|
-
function loadAdapter(cliId, explicitPath) {
|
|
238
|
-
const source = resolveAdapterPath(cliId, explicitPath);
|
|
239
|
-
const content = readFileSync(source, "utf-8");
|
|
240
|
-
const adapter = parseAdapterToml(content);
|
|
241
|
-
const idMatch = adapter.cli.id === cliId;
|
|
242
|
-
const fileMatch = basename(source) === `${cliId}.toml`;
|
|
243
|
-
const execMatch = adapter.cli.executable === cliId;
|
|
244
|
-
if (!idMatch && !fileMatch && !execMatch)
|
|
245
|
-
throw new Error(`Adapter ${source} does not match requested CLI ${cliId}`);
|
|
246
|
-
return {
|
|
247
|
-
adapter,
|
|
248
|
-
source,
|
|
249
|
-
digest: digest(content)
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
async function resolveGenericOrReject(cliId, fullArgv, opts) {
|
|
253
|
-
if (!opts.genericEnabled)
|
|
254
|
-
throw new Error(`No adapter found for ${cliId}`);
|
|
255
|
-
return await buildGenericResolved(cliId, fullArgv);
|
|
256
|
-
}
|
|
257
|
-
function tryLoadAdapter(cliId, explicitPath) {
|
|
258
|
-
try {
|
|
259
|
-
return loadAdapter(cliId, explicitPath);
|
|
260
|
-
} catch {
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// src/shapes/audit.ts
|
|
266
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
267
|
-
import { homedir as homedir2 } from "os";
|
|
268
|
-
import { dirname, join as join2 } from "path";
|
|
269
|
-
function auditPath() {
|
|
270
|
-
return join2(homedir2(), ".config", "apes", "audit.jsonl");
|
|
271
|
-
}
|
|
272
|
-
function appendAuditLog(entry) {
|
|
273
|
-
const full = {
|
|
274
|
-
...entry,
|
|
275
|
-
action: entry.action,
|
|
276
|
-
timestamp: entry.timestamp ?? Date.now()
|
|
277
|
-
};
|
|
278
|
-
const path = auditPath();
|
|
279
|
-
const dir = dirname(path);
|
|
280
|
-
try {
|
|
281
|
-
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
282
|
-
appendFileSync(path, `${JSON.stringify(full)}
|
|
283
|
-
`);
|
|
284
|
-
} catch {
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/shapes/installer.ts
|
|
289
|
-
import { createHash as createHash2 } from "crypto";
|
|
290
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
291
|
-
import { homedir as homedir3 } from "os";
|
|
292
|
-
import { join as join3 } from "path";
|
|
293
|
-
function adapterDir(local) {
|
|
294
|
-
const base = local ? process.cwd() : homedir3();
|
|
295
|
-
return join3(base, ".openape", "shapes", "adapters");
|
|
296
|
-
}
|
|
297
|
-
function adapterPath(id, local) {
|
|
298
|
-
return join3(adapterDir(local), `${id}.toml`);
|
|
299
|
-
}
|
|
300
|
-
function sha256(content) {
|
|
301
|
-
return `SHA-256:${createHash2("sha256").update(content).digest("hex")}`;
|
|
302
|
-
}
|
|
303
|
-
async function installAdapter(entry, options = {}) {
|
|
304
|
-
const local = options.local ?? false;
|
|
305
|
-
const dest = adapterPath(entry.id, local);
|
|
306
|
-
const dir = adapterDir(local);
|
|
307
|
-
const response = await fetch(entry.download_url);
|
|
308
|
-
if (!response.ok)
|
|
309
|
-
throw new Error(`Failed to download adapter ${entry.id}: ${response.status} ${response.statusText}`);
|
|
310
|
-
const content = await response.text();
|
|
311
|
-
const digest2 = sha256(content);
|
|
312
|
-
if (digest2 !== entry.digest)
|
|
313
|
-
throw new Error(`Digest mismatch for ${entry.id}: expected ${entry.digest}, got ${digest2}`);
|
|
314
|
-
const updated = existsSync3(dest);
|
|
315
|
-
if (!existsSync3(dir))
|
|
316
|
-
mkdirSync2(dir, { recursive: true });
|
|
317
|
-
writeFileSync(dest, content);
|
|
318
|
-
return { id: entry.id, path: dest, digest: digest2, updated };
|
|
319
|
-
}
|
|
320
|
-
function getInstalledDigest(id, local) {
|
|
321
|
-
const path = adapterPath(id, local);
|
|
322
|
-
if (!existsSync3(path))
|
|
323
|
-
return null;
|
|
324
|
-
const content = readFileSync2(path, "utf-8");
|
|
325
|
-
return sha256(content);
|
|
326
|
-
}
|
|
327
|
-
function isInstalled(id, local) {
|
|
328
|
-
return existsSync3(adapterPath(id, local));
|
|
329
|
-
}
|
|
330
|
-
function removeAdapter(id, local) {
|
|
331
|
-
const path = adapterPath(id, local);
|
|
332
|
-
if (!existsSync3(path))
|
|
333
|
-
return false;
|
|
334
|
-
unlinkSync(path);
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
function findConflictingAdapters(executable, excludeId) {
|
|
338
|
-
const conflicts = [];
|
|
339
|
-
const dirs = [
|
|
340
|
-
join3(process.cwd(), ".openape", "shapes", "adapters"),
|
|
341
|
-
join3(homedir3(), ".openape", "shapes", "adapters")
|
|
342
|
-
];
|
|
343
|
-
for (const dir of dirs) {
|
|
344
|
-
if (!existsSync3(dir))
|
|
345
|
-
continue;
|
|
346
|
-
try {
|
|
347
|
-
for (const file of readdirSync2(dir).filter((f) => f.endsWith(".toml"))) {
|
|
348
|
-
const path = join3(dir, file);
|
|
349
|
-
const content = readFileSync2(path, "utf-8");
|
|
350
|
-
const execMatch = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
|
|
351
|
-
const idMatch = content.match(/^\s*id\s*=\s*"([^"]+)"/m);
|
|
352
|
-
if (execMatch?.[1] === executable && idMatch?.[1] !== excludeId) {
|
|
353
|
-
conflicts.push({ file, path, adapterId: idMatch?.[1] ?? file, executable });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} catch {
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return conflicts;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// src/shapes/registry.ts
|
|
363
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
364
|
-
import { homedir as homedir4 } from "os";
|
|
365
|
-
import { join as join4 } from "path";
|
|
366
|
-
var REGISTRY_URL = process.env.SHAPES_REGISTRY_URL ?? "https://raw.githubusercontent.com/openape-ai/shapes-registry/main/registry.json";
|
|
367
|
-
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
368
|
-
function cacheDir() {
|
|
369
|
-
return join4(homedir4(), ".openape", "shapes", "cache");
|
|
370
|
-
}
|
|
371
|
-
function cachePath() {
|
|
372
|
-
return join4(cacheDir(), "registry.json");
|
|
373
|
-
}
|
|
374
|
-
function readCache() {
|
|
375
|
-
const path = cachePath();
|
|
376
|
-
if (!existsSync4(path))
|
|
377
|
-
return null;
|
|
378
|
-
try {
|
|
379
|
-
const raw = readFileSync3(path, "utf-8");
|
|
380
|
-
const stat = JSON.parse(raw);
|
|
381
|
-
if (stat._cached_at && Date.now() - stat._cached_at > CACHE_TTL_MS)
|
|
382
|
-
return null;
|
|
383
|
-
return stat;
|
|
384
|
-
} catch {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function writeCache(index) {
|
|
389
|
-
const dir = cacheDir();
|
|
390
|
-
if (!existsSync4(dir))
|
|
391
|
-
mkdirSync3(dir, { recursive: true });
|
|
392
|
-
writeFileSync2(cachePath(), JSON.stringify({ ...index, _cached_at: Date.now() }, null, 2));
|
|
393
|
-
}
|
|
394
|
-
async function fetchRegistry(forceRefresh = false) {
|
|
395
|
-
if (!forceRefresh) {
|
|
396
|
-
const cached = readCache();
|
|
397
|
-
if (cached)
|
|
398
|
-
return cached;
|
|
399
|
-
}
|
|
400
|
-
const response = await fetch(REGISTRY_URL);
|
|
401
|
-
if (!response.ok)
|
|
402
|
-
throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`);
|
|
403
|
-
const index = await response.json();
|
|
404
|
-
writeCache(index);
|
|
405
|
-
return index;
|
|
406
|
-
}
|
|
407
|
-
function searchAdapters(index, query) {
|
|
408
|
-
const q = query.toLowerCase();
|
|
409
|
-
return index.adapters.filter(
|
|
410
|
-
(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)
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
function findAdapter(index, idOrExecutable) {
|
|
414
|
-
return index.adapters.find(
|
|
415
|
-
(a) => a.id === idOrExecutable || a.executable === idOrExecutable
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// src/shapes/shell-parser.ts
|
|
420
|
-
import { basename as basename2 } from "path";
|
|
421
|
-
import consola from "consola";
|
|
422
|
-
import { parse as shellParse } from "shell-quote";
|
|
423
|
-
var COMPOUND_OPERATORS = /* @__PURE__ */ new Set(["&&", "||", ";", "|", "&", ">", ">>", "<"]);
|
|
424
|
-
function parseShellCommand(raw) {
|
|
425
|
-
const trimmed = raw.trim();
|
|
426
|
-
if (trimmed.length === 0) return null;
|
|
427
|
-
let tokens;
|
|
428
|
-
try {
|
|
429
|
-
tokens = shellParse(trimmed);
|
|
430
|
-
} catch {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
const hasShellExpansion = /\$\(|`/.test(trimmed);
|
|
434
|
-
const hasOperatorToken = tokens.some((t) => typeof t === "object" && t !== null && "op" in t && COMPOUND_OPERATORS.has(t.op));
|
|
435
|
-
const isCompound = hasShellExpansion || hasOperatorToken;
|
|
436
|
-
const stringTokens = [];
|
|
437
|
-
for (const t of tokens) {
|
|
438
|
-
if (typeof t === "string") {
|
|
439
|
-
stringTokens.push(t);
|
|
440
|
-
} else {
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
if (stringTokens.length === 0) return null;
|
|
445
|
-
return {
|
|
446
|
-
executable: stringTokens[0],
|
|
447
|
-
argv: stringTokens.slice(1),
|
|
448
|
-
isCompound,
|
|
449
|
-
raw: trimmed
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
function extractShellCommandString(command) {
|
|
453
|
-
if (command.length < 3) return null;
|
|
454
|
-
if (command[0] !== "bash" && command[0] !== "sh") return null;
|
|
455
|
-
if (command[1] !== "-c") return null;
|
|
456
|
-
return command.slice(2).join(" ");
|
|
457
|
-
}
|
|
458
|
-
async function loadOrInstallAdapter(cliId) {
|
|
459
|
-
const lookupId = basename2(cliId);
|
|
460
|
-
const local = tryLoadAdapter(lookupId);
|
|
461
|
-
if (local) return local;
|
|
462
|
-
try {
|
|
463
|
-
const index = await fetchRegistry();
|
|
464
|
-
const entry = findAdapter(index, lookupId);
|
|
465
|
-
if (!entry) return null;
|
|
466
|
-
consola.info(`Installing shapes adapter for ${entry.id} from registry...`);
|
|
467
|
-
await installAdapter(entry, { local: false });
|
|
468
|
-
appendAuditLog({
|
|
469
|
-
action: "adapter-auto-install",
|
|
470
|
-
cli_id: entry.id,
|
|
471
|
-
digest: entry.digest,
|
|
472
|
-
source: "ape-shell"
|
|
473
|
-
});
|
|
474
|
-
return tryLoadAdapter(entry.id);
|
|
475
|
-
} catch (err) {
|
|
476
|
-
consola.debug(`ape-shell adapter auto-install failed for ${lookupId}:`, err);
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// src/shapes/capabilities.ts
|
|
482
|
-
import { canonicalizeCliPermission as canonicalizeCliPermission2 } from "@openape/grants";
|
|
483
|
-
function parseOperationChainEntry(entry) {
|
|
484
|
-
const [resource, selectorSpec = "*"] = entry.split(":", 2);
|
|
485
|
-
if (!resource) {
|
|
486
|
-
throw new Error(`Invalid resource chain entry: ${entry}`);
|
|
487
|
-
}
|
|
488
|
-
if (selectorSpec === "*") {
|
|
489
|
-
return { resource, selectorKeys: [] };
|
|
490
|
-
}
|
|
491
|
-
const selectorKeys = selectorSpec.split(",").map((segment) => {
|
|
492
|
-
const [key] = segment.split("=", 2);
|
|
493
|
-
if (!key)
|
|
494
|
-
throw new Error(`Invalid selector segment: ${segment}`);
|
|
495
|
-
return key;
|
|
496
|
-
});
|
|
497
|
-
return { resource, selectorKeys };
|
|
498
|
-
}
|
|
499
|
-
function operationChain(operation) {
|
|
500
|
-
return operation.resource_chain.map(parseOperationChainEntry);
|
|
501
|
-
}
|
|
502
|
-
function knownSelectorKeys(operations, resource) {
|
|
503
|
-
const keys = /* @__PURE__ */ new Set();
|
|
504
|
-
for (const operation of operations) {
|
|
505
|
-
for (const entry of operationChain(operation)) {
|
|
506
|
-
if (entry.resource !== resource)
|
|
507
|
-
continue;
|
|
508
|
-
for (const key of entry.selectorKeys) {
|
|
509
|
-
keys.add(key);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return Array.from(keys).sort();
|
|
514
|
-
}
|
|
515
|
-
function parseResourceSelector(raw) {
|
|
516
|
-
const [lhs, value] = raw.split("=", 2);
|
|
517
|
-
if (!lhs || !value) {
|
|
518
|
-
throw new Error(`Invalid selector: ${raw}`);
|
|
519
|
-
}
|
|
520
|
-
const [resource, key] = lhs.split(".", 2);
|
|
521
|
-
if (!resource || !key) {
|
|
522
|
-
throw new Error(`Selectors must be in resource.key=value form: ${raw}`);
|
|
523
|
-
}
|
|
524
|
-
return { resource, key, value };
|
|
525
|
-
}
|
|
526
|
-
function formatSelector(selector) {
|
|
527
|
-
if (!selector || Object.keys(selector).length === 0)
|
|
528
|
-
return "*";
|
|
529
|
-
return Object.entries(selector).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join(",");
|
|
530
|
-
}
|
|
531
|
-
function summarizeDetail(detail) {
|
|
532
|
-
const chain = detail.resource_chain.map((resource) => `${resource.resource}[${formatSelector(resource.selector)}]`).join(" -> ");
|
|
533
|
-
return `Allow ${detail.action} on ${detail.cli_id} ${chain}`;
|
|
534
|
-
}
|
|
535
|
-
function resolveCapabilityRequest(loaded, params) {
|
|
536
|
-
if (params.resources.length === 0) {
|
|
537
|
-
throw new Error("At least one --resource is required");
|
|
538
|
-
}
|
|
539
|
-
if (params.actions.length === 0) {
|
|
540
|
-
throw new Error("At least one --action is required");
|
|
541
|
-
}
|
|
542
|
-
const selectorMap = /* @__PURE__ */ new Map();
|
|
543
|
-
for (const rawSelector of params.selectors ?? []) {
|
|
544
|
-
const { resource, key, value } = parseResourceSelector(rawSelector);
|
|
545
|
-
const current = selectorMap.get(resource) ?? {};
|
|
546
|
-
current[key] = value;
|
|
547
|
-
selectorMap.set(resource, current);
|
|
548
|
-
}
|
|
549
|
-
const resource_chain = params.resources.map((resource) => {
|
|
550
|
-
const selector = selectorMap.get(resource);
|
|
551
|
-
const knownKeys = knownSelectorKeys(loaded.adapter.operations, resource);
|
|
552
|
-
if (selector) {
|
|
553
|
-
for (const key of Object.keys(selector)) {
|
|
554
|
-
if (!knownKeys.includes(key)) {
|
|
555
|
-
throw new Error(`Unknown selector ${resource}.${key} for adapter ${loaded.adapter.cli.id}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return selector && Object.keys(selector).length > 0 ? { resource, selector } : { resource };
|
|
560
|
-
});
|
|
561
|
-
const requestedSequence = params.resources.join("\0");
|
|
562
|
-
const matchingOperations = loaded.adapter.operations.filter((operation) => {
|
|
563
|
-
const sequence = operationChain(operation).map((entry) => entry.resource).join("\0");
|
|
564
|
-
return sequence === requestedSequence || sequence.startsWith(`${requestedSequence}\0`);
|
|
565
|
-
});
|
|
566
|
-
if (matchingOperations.length === 0) {
|
|
567
|
-
throw new Error(`No adapter operation supports resource chain: ${params.resources.join(" -> ")}`);
|
|
568
|
-
}
|
|
569
|
-
const details = params.actions.map((action) => {
|
|
570
|
-
const matchingActionOps = matchingOperations.filter((operation) => operation.action === action);
|
|
571
|
-
if (matchingActionOps.length === 0) {
|
|
572
|
-
throw new Error(`Action ${action} is not valid for resource chain: ${params.resources.join(" -> ")}`);
|
|
573
|
-
}
|
|
574
|
-
const exact_command = matchingActionOps.every((operation) => operation.exact_command === true);
|
|
575
|
-
const risks = ["low", "medium", "high", "critical"];
|
|
576
|
-
const risk = matchingActionOps.reduce((current, operation) => {
|
|
577
|
-
return risks.indexOf(operation.risk) > risks.indexOf(current) ? operation.risk : current;
|
|
578
|
-
}, "low");
|
|
579
|
-
const detail = {
|
|
580
|
-
type: "openape_cli",
|
|
581
|
-
cli_id: loaded.adapter.cli.id,
|
|
582
|
-
operation_id: `capability.${action}`,
|
|
583
|
-
resource_chain,
|
|
584
|
-
action,
|
|
585
|
-
permission: "",
|
|
586
|
-
display: "",
|
|
587
|
-
risk,
|
|
588
|
-
...exact_command ? { constraints: { exact_command: true } } : {}
|
|
589
|
-
};
|
|
590
|
-
detail.permission = canonicalizeCliPermission2(detail);
|
|
591
|
-
detail.display = summarizeDetail(detail);
|
|
592
|
-
return detail;
|
|
593
|
-
});
|
|
594
|
-
return {
|
|
595
|
-
adapter: loaded.adapter,
|
|
596
|
-
source: loaded.source,
|
|
597
|
-
digest: loaded.digest,
|
|
598
|
-
executable: loaded.adapter.cli.executable,
|
|
599
|
-
details,
|
|
600
|
-
executionContext: {
|
|
601
|
-
adapter_id: loaded.adapter.cli.id,
|
|
602
|
-
adapter_version: loaded.adapter.cli.version ?? loaded.adapter.schema,
|
|
603
|
-
adapter_digest: loaded.digest,
|
|
604
|
-
resolved_executable: loaded.adapter.cli.executable,
|
|
605
|
-
context_bindings: Object.fromEntries(
|
|
606
|
-
Array.from(selectorMap.entries()).flatMap(
|
|
607
|
-
([resource, selector]) => Object.entries(selector).map(([key, value]) => [`${resource}.${key}`, value])
|
|
608
|
-
)
|
|
609
|
-
)
|
|
610
|
-
},
|
|
611
|
-
permissions: details.map((detail) => detail.permission),
|
|
612
|
-
summary: details.map((detail) => detail.display).join("; ")
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// src/shapes/parser.ts
|
|
617
|
-
import { buildCliAuthDetail, computeArgvHash as computeArgvHash2, matchArgvToOperation } from "@openape/grants";
|
|
618
|
-
async function resolveCommand(loaded, fullArgv) {
|
|
619
|
-
const [executable, ...commandArgv] = fullArgv;
|
|
620
|
-
if (!executable) {
|
|
621
|
-
throw new Error("Missing wrapped command");
|
|
622
|
-
}
|
|
623
|
-
if (executable !== loaded.adapter.cli.executable) {
|
|
624
|
-
throw new Error(`Adapter ${loaded.adapter.cli.id} expects executable ${loaded.adapter.cli.executable}, got ${executable}`);
|
|
625
|
-
}
|
|
626
|
-
const match = matchArgvToOperation(loaded.adapter.operations, commandArgv);
|
|
627
|
-
if (!match) {
|
|
628
|
-
throw new Error(`No adapter operation matched: ${fullArgv.join(" ")}`);
|
|
629
|
-
}
|
|
630
|
-
const { operation, bindings } = match;
|
|
631
|
-
const detail = buildCliAuthDetail(loaded.adapter.cli.id, operation, bindings);
|
|
632
|
-
return {
|
|
633
|
-
adapter: loaded.adapter,
|
|
634
|
-
source: loaded.source,
|
|
635
|
-
digest: loaded.digest,
|
|
636
|
-
executable,
|
|
637
|
-
commandArgv,
|
|
638
|
-
bindings,
|
|
639
|
-
detail,
|
|
640
|
-
executionContext: {
|
|
641
|
-
argv: fullArgv,
|
|
642
|
-
argv_hash: await computeArgvHash2(fullArgv),
|
|
643
|
-
adapter_id: loaded.adapter.cli.id,
|
|
644
|
-
adapter_version: loaded.adapter.cli.version ?? loaded.adapter.schema,
|
|
645
|
-
adapter_digest: loaded.digest,
|
|
646
|
-
resolved_executable: executable,
|
|
647
|
-
context_bindings: bindings
|
|
648
|
-
},
|
|
649
|
-
permission: detail.permission
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// src/shapes/commands/explain.ts
|
|
654
|
-
import { defineCommand } from "citty";
|
|
655
|
-
var explainCommand = defineCommand({
|
|
656
|
-
meta: {
|
|
657
|
-
name: "explain",
|
|
658
|
-
description: "Show what permission a wrapped command would need"
|
|
659
|
-
},
|
|
660
|
-
args: {
|
|
661
|
-
adapter: {
|
|
662
|
-
type: "string",
|
|
663
|
-
description: "Explicit path to adapter TOML file"
|
|
664
|
-
},
|
|
665
|
-
_: {
|
|
666
|
-
type: "positional",
|
|
667
|
-
description: "Wrapped command (after --)",
|
|
668
|
-
required: false
|
|
669
|
-
}
|
|
670
|
-
},
|
|
671
|
-
async run({ rawArgs }) {
|
|
672
|
-
const command = extractWrappedCommand(rawArgs ?? []);
|
|
673
|
-
if (command.length === 0)
|
|
674
|
-
throw new Error("Missing wrapped command. Usage: shapes explain [--adapter <file>] -- <cli> ...");
|
|
675
|
-
const adapterOpt = extractOption(rawArgs ?? [], "adapter");
|
|
676
|
-
const loaded = loadAdapter(command[0], adapterOpt);
|
|
677
|
-
const resolved = await resolveCommand(loaded, command);
|
|
678
|
-
process.stdout.write(`${JSON.stringify({
|
|
679
|
-
adapter: resolved.adapter.cli.id,
|
|
680
|
-
source: resolved.source,
|
|
681
|
-
operation: resolved.detail.operation_id,
|
|
682
|
-
display: resolved.detail.display,
|
|
683
|
-
permission: resolved.permission,
|
|
684
|
-
resource_chain: resolved.detail.resource_chain,
|
|
685
|
-
exact_command: resolved.detail.constraints?.exact_command ?? false,
|
|
686
|
-
adapter_digest: resolved.digest
|
|
687
|
-
}, null, 2)}
|
|
688
|
-
`);
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
function extractWrappedCommand(args) {
|
|
692
|
-
const delimiter = args.indexOf("--");
|
|
693
|
-
return delimiter >= 0 ? args.slice(delimiter + 1) : [];
|
|
694
|
-
}
|
|
695
|
-
function extractOption(args, name) {
|
|
696
|
-
const delimiter = args.indexOf("--");
|
|
697
|
-
const optionArgs = delimiter >= 0 ? args.slice(0, delimiter) : args;
|
|
698
|
-
const index = optionArgs.indexOf(`--${name}`);
|
|
699
|
-
if (index >= 0 && index + 1 < optionArgs.length)
|
|
700
|
-
return optionArgs[index + 1];
|
|
701
|
-
return void 0;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/shapes/grants.ts
|
|
705
|
-
import { computeCmdHash } from "@openape/core";
|
|
706
|
-
import { cliAuthorizationDetailCovers, verifyAuthzJWT } from "@openape/grants";
|
|
707
|
-
import { execFileSync } from "child_process";
|
|
708
|
-
import { hostname } from "os";
|
|
709
|
-
import consola2 from "consola";
|
|
710
|
-
|
|
711
|
-
// src/shapes/config.ts
|
|
712
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
713
|
-
import { homedir as homedir5 } from "os";
|
|
714
|
-
import { join as join5 } from "path";
|
|
715
|
-
var AUTH_FILE = join5(homedir5(), ".config", "apes", "auth.json");
|
|
716
|
-
function loadAuth() {
|
|
717
|
-
if (!existsSync5(AUTH_FILE))
|
|
718
|
-
return null;
|
|
719
|
-
try {
|
|
720
|
-
return JSON.parse(readFileSync4(AUTH_FILE, "utf-8"));
|
|
721
|
-
} catch {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function getIdpUrl(explicit) {
|
|
726
|
-
if (explicit)
|
|
727
|
-
return explicit;
|
|
728
|
-
if (process.env.APES_IDP)
|
|
729
|
-
return process.env.APES_IDP;
|
|
730
|
-
if (process.env.SHAPES_IDP)
|
|
731
|
-
return process.env.SHAPES_IDP;
|
|
732
|
-
return loadAuth()?.idp ?? null;
|
|
733
|
-
}
|
|
734
|
-
function getAuthToken() {
|
|
735
|
-
const auth = loadAuth();
|
|
736
|
-
if (!auth)
|
|
737
|
-
return null;
|
|
738
|
-
if (auth.expires_at && Date.now() / 1e3 > auth.expires_at - 30)
|
|
739
|
-
return null;
|
|
740
|
-
return auth.access_token;
|
|
741
|
-
}
|
|
742
|
-
function getRequesterIdentity() {
|
|
743
|
-
return loadAuth()?.email ?? null;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// src/shapes/http.ts
|
|
747
|
-
async function discoverEndpoints(idpUrl) {
|
|
748
|
-
const response = await fetch(`${idpUrl}/.well-known/openid-configuration`);
|
|
749
|
-
if (!response.ok)
|
|
750
|
-
return {};
|
|
751
|
-
return response.json();
|
|
752
|
-
}
|
|
753
|
-
async function getGrantsEndpoint(idpUrl) {
|
|
754
|
-
const discovery = await discoverEndpoints(idpUrl);
|
|
755
|
-
return String(discovery.openape_grants_endpoint ?? `${idpUrl}/api/grants`);
|
|
756
|
-
}
|
|
757
|
-
async function apiFetch(path, options = {}) {
|
|
758
|
-
const token = options.token ?? getAuthToken();
|
|
759
|
-
if (!token)
|
|
760
|
-
throw new Error("Not authenticated. Run `apes login` first.");
|
|
761
|
-
const idp = options.idp ?? getIdpUrl();
|
|
762
|
-
if (!path.startsWith("http") && !idp)
|
|
763
|
-
throw new Error("No IdP URL configured. Use --idp or log in with apes.");
|
|
764
|
-
const url = path.startsWith("http") ? path : `${idp}${path}`;
|
|
765
|
-
const response = await fetch(url, {
|
|
766
|
-
method: options.method ?? "GET",
|
|
767
|
-
headers: {
|
|
768
|
-
Authorization: `Bearer ${token}`,
|
|
769
|
-
"Content-Type": "application/json"
|
|
770
|
-
},
|
|
771
|
-
body: options.body ? JSON.stringify(options.body) : void 0
|
|
772
|
-
});
|
|
773
|
-
if (!response.ok) {
|
|
774
|
-
const text = await response.text();
|
|
775
|
-
throw new Error(text || `${response.status} ${response.statusText}`);
|
|
776
|
-
}
|
|
777
|
-
return response.json();
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// src/audit/generic-log.ts
|
|
781
|
-
import { mkdir, appendFile } from "fs/promises";
|
|
782
|
-
import { homedir as homedir6 } from "os";
|
|
783
|
-
import { dirname as dirname2, join as join6 } from "path";
|
|
784
|
-
function defaultGenericLogPath() {
|
|
785
|
-
return join6(homedir6(), ".config", "apes", "generic-calls.log");
|
|
786
|
-
}
|
|
787
|
-
async function appendGenericCallLog(entry, logPath) {
|
|
788
|
-
const path = logPath ?? defaultGenericLogPath();
|
|
789
|
-
await mkdir(dirname2(path), { recursive: true });
|
|
790
|
-
await appendFile(path, `${JSON.stringify(entry)}
|
|
791
|
-
`, "utf-8");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// src/shapes/grants.ts
|
|
795
|
-
function decodePayload(token) {
|
|
796
|
-
const [, payload] = token.split(".");
|
|
797
|
-
if (!payload)
|
|
798
|
-
throw new Error("Invalid JWT");
|
|
799
|
-
return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
|
|
800
|
-
}
|
|
801
|
-
async function createShapesGrant(resolved, params) {
|
|
802
|
-
const grantsEndpoint = await getGrantsEndpoint(params.idp);
|
|
803
|
-
const requester = getRequesterIdentity();
|
|
804
|
-
if (!requester) {
|
|
805
|
-
throw new Error("No requester identity available. Run `apes login` first.");
|
|
806
|
-
}
|
|
807
|
-
return apiFetch(grantsEndpoint, {
|
|
808
|
-
method: "POST",
|
|
809
|
-
idp: params.idp,
|
|
810
|
-
body: {
|
|
811
|
-
requester,
|
|
812
|
-
target_host: hostname(),
|
|
813
|
-
audience: resolved.adapter.cli.audience ?? "shapes",
|
|
814
|
-
grant_type: params.approval,
|
|
815
|
-
command: resolved.executionContext.argv,
|
|
816
|
-
reason: params.reason ?? resolved.detail.display,
|
|
817
|
-
permissions: [resolved.permission],
|
|
818
|
-
authorization_details: [resolved.detail],
|
|
819
|
-
execution_context: resolved.executionContext
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
async function waitForGrantStatus(idp, grantId) {
|
|
824
|
-
const grantsEndpoint = await getGrantsEndpoint(idp);
|
|
825
|
-
const deadline = Date.now() + 3e5;
|
|
826
|
-
while (Date.now() < deadline) {
|
|
827
|
-
const grant = await apiFetch(`${grantsEndpoint}/${grantId}`, { idp });
|
|
828
|
-
if (grant.status === "approved" || grant.status === "denied" || grant.status === "revoked")
|
|
829
|
-
return grant.status;
|
|
830
|
-
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
831
|
-
}
|
|
832
|
-
throw new Error("Timed out waiting for grant approval");
|
|
833
|
-
}
|
|
834
|
-
async function fetchGrantToken(idp, grantId) {
|
|
835
|
-
const grantsEndpoint = await getGrantsEndpoint(idp);
|
|
836
|
-
const response = await apiFetch(`${grantsEndpoint}/${grantId}/token`, {
|
|
837
|
-
method: "POST",
|
|
838
|
-
idp
|
|
839
|
-
});
|
|
840
|
-
return response.authz_jwt;
|
|
841
|
-
}
|
|
842
|
-
function grantedCliDetails(claims) {
|
|
843
|
-
const details = claims.authorization_details;
|
|
844
|
-
if (!Array.isArray(details))
|
|
845
|
-
return [];
|
|
846
|
-
return details.filter(
|
|
847
|
-
(detail) => typeof detail === "object" && detail !== null && detail.type === "openape_cli"
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
function hasStructuredCliGrant(claims) {
|
|
851
|
-
return grantedCliDetails(claims).length > 0;
|
|
852
|
-
}
|
|
853
|
-
async function verifyAndConsume(token, resolved) {
|
|
854
|
-
const payload = decodePayload(token);
|
|
855
|
-
const issuer = String(payload.iss ?? "");
|
|
856
|
-
if (!issuer)
|
|
857
|
-
throw new Error("Grant token is missing issuer");
|
|
858
|
-
const discovery = await discoverEndpoints(issuer);
|
|
859
|
-
const jwksUri = String(discovery.jwks_uri ?? `${issuer}/.well-known/jwks.json`);
|
|
860
|
-
const result = await verifyAuthzJWT(token, {
|
|
861
|
-
expectedIss: issuer,
|
|
862
|
-
expectedAud: resolved.adapter.cli.audience ?? "shapes",
|
|
863
|
-
jwksUri
|
|
864
|
-
});
|
|
865
|
-
if (!result.valid || !result.claims) {
|
|
866
|
-
throw new Error(result.error ?? "Grant verification failed");
|
|
867
|
-
}
|
|
868
|
-
const claims = result.claims;
|
|
869
|
-
const details = grantedCliDetails(claims);
|
|
870
|
-
if (claims.execution_context?.adapter_digest && claims.execution_context.adapter_digest !== resolved.digest) {
|
|
871
|
-
throw new Error("Adapter digest mismatch");
|
|
872
|
-
}
|
|
873
|
-
if (!hasStructuredCliGrant(claims)) {
|
|
874
|
-
const argv = resolved.executionContext.argv;
|
|
875
|
-
if (!argv?.length) {
|
|
876
|
-
throw new Error("Resolved command is missing argv");
|
|
877
|
-
}
|
|
878
|
-
const expectedCmdHash = await computeCmdHash(argv.join(" "));
|
|
879
|
-
if (claims.command?.join("\0") !== argv.join("\0")) {
|
|
880
|
-
throw new Error("Granted command does not match current argv");
|
|
881
|
-
}
|
|
882
|
-
if (claims.cmd_hash && claims.cmd_hash !== expectedCmdHash) {
|
|
883
|
-
throw new Error("Granted command does not match current argv");
|
|
884
|
-
}
|
|
885
|
-
if (!claims.command?.length && !claims.cmd_hash) {
|
|
886
|
-
throw new Error("Grant is not a structured CLI grant and is missing command binding");
|
|
887
|
-
}
|
|
888
|
-
} else {
|
|
889
|
-
if (!details.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail))) {
|
|
890
|
-
throw new Error(`Grant does not cover required permission: ${resolved.permission}`);
|
|
891
|
-
}
|
|
892
|
-
const exactRequired = details.some(
|
|
893
|
-
(detail) => cliAuthorizationDetailCovers(detail, resolved.detail) && detail.constraints?.exact_command
|
|
894
|
-
);
|
|
895
|
-
const isOnce = claims.grant_type === "once" || claims.approval === "once";
|
|
896
|
-
const enforceArgvHash = exactRequired || isOnce && !!claims.execution_context?.argv_hash;
|
|
897
|
-
if (enforceArgvHash && claims.execution_context?.argv_hash !== resolved.executionContext.argv_hash) {
|
|
898
|
-
throw new Error("Granted command does not match current argv");
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const grantsEndpoint = await getGrantsEndpoint(issuer);
|
|
902
|
-
const consume = await fetch(`${grantsEndpoint}/${claims.grant_id}/consume`, {
|
|
903
|
-
method: "POST",
|
|
904
|
-
headers: {
|
|
905
|
-
Authorization: `Bearer ${token}`
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
if (!consume.ok) {
|
|
909
|
-
throw new Error(`Consume failed: ${consume.status} ${consume.statusText}`);
|
|
910
|
-
}
|
|
911
|
-
const consumeResult = await consume.json();
|
|
912
|
-
if (consumeResult.error) {
|
|
913
|
-
throw new Error(`Grant rejected at consume step: ${consumeResult.error}`);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
function executeResolvedViaExec(resolved) {
|
|
917
|
-
consola2.info(`Executing ${(resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv]).join(" ")}`);
|
|
918
|
-
execFileSync(resolved.executable, resolved.commandArgv, { stdio: "inherit" });
|
|
919
|
-
}
|
|
920
|
-
async function verifyAndExecute(token, resolved, grantId) {
|
|
921
|
-
await verifyAndConsume(token, resolved);
|
|
922
|
-
const isGeneric = isGenericResolved(resolved);
|
|
923
|
-
const start = Date.now();
|
|
924
|
-
let exitCode = 0;
|
|
925
|
-
try {
|
|
926
|
-
executeResolvedViaExec(resolved);
|
|
927
|
-
} catch (err) {
|
|
928
|
-
exitCode = err?.status ?? 1;
|
|
929
|
-
throw err;
|
|
930
|
-
} finally {
|
|
931
|
-
if (isGeneric && grantId) {
|
|
932
|
-
try {
|
|
933
|
-
await appendGenericCallLog(
|
|
934
|
-
{
|
|
935
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
936
|
-
cli: resolved.detail.cli_id,
|
|
937
|
-
argv: resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv],
|
|
938
|
-
argv_hash: resolved.executionContext.argv_hash ?? "",
|
|
939
|
-
grant_id: grantId,
|
|
940
|
-
exit_code: exitCode,
|
|
941
|
-
duration_ms: Date.now() - start
|
|
942
|
-
},
|
|
943
|
-
getGenericAuditLogPath()
|
|
944
|
-
);
|
|
945
|
-
} catch (logErr) {
|
|
946
|
-
consola2.debug("Failed to append generic-call audit entry:", logErr);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
async function resolveFromGrant(grant) {
|
|
952
|
-
const argv = grant.request?.command;
|
|
953
|
-
if (!argv || argv.length === 0)
|
|
954
|
-
throw new Error("Grant request is missing command argv");
|
|
955
|
-
const executable = argv[0];
|
|
956
|
-
const adapter = await loadOrInstallAdapter(executable);
|
|
957
|
-
if (!adapter)
|
|
958
|
-
throw new Error(`No shapes adapter found for ${executable}`);
|
|
959
|
-
const resolved = await resolveCommand(adapter, argv);
|
|
960
|
-
const grantDigest = grant.request.execution_context?.adapter_digest;
|
|
961
|
-
if (grantDigest && grantDigest !== resolved.digest) {
|
|
962
|
-
throw new Error(
|
|
963
|
-
`Adapter digest mismatch: grant was created against adapter ${grantDigest}, but local adapter is ${resolved.digest}. Reinstall or revert the adapter.`
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
return resolved;
|
|
967
|
-
}
|
|
968
|
-
async function findExistingGrant(resolved, idp) {
|
|
969
|
-
const grantsEndpoint = await getGrantsEndpoint(idp);
|
|
970
|
-
const response = await apiFetch(
|
|
971
|
-
`${grantsEndpoint}?status=approved`,
|
|
972
|
-
{ idp }
|
|
973
|
-
);
|
|
974
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
975
|
-
const expectedAudience = resolved.adapter.cli.audience ?? "shapes";
|
|
976
|
-
for (const grant of response.data) {
|
|
977
|
-
const req = grant.request;
|
|
978
|
-
if (req.grant_type === "once")
|
|
979
|
-
continue;
|
|
980
|
-
if (req.grant_type === "timed" && grant.expires_at && grant.expires_at <= now)
|
|
981
|
-
continue;
|
|
982
|
-
if (req.audience !== expectedAudience)
|
|
983
|
-
continue;
|
|
984
|
-
if (req.execution_context?.adapter_digest && req.execution_context.adapter_digest !== resolved.digest)
|
|
985
|
-
continue;
|
|
986
|
-
const cliDetails = (req.authorization_details ?? []).filter(
|
|
987
|
-
(d) => d.type === "openape_cli"
|
|
988
|
-
);
|
|
989
|
-
if (cliDetails.length > 0) {
|
|
990
|
-
if (cliDetails.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail)))
|
|
991
|
-
return grant.id;
|
|
992
|
-
} else if (req.permissions?.includes(resolved.permission)) {
|
|
993
|
-
return grant.id;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return null;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// src/shapes/request-builders.ts
|
|
1000
|
-
import { computeCmdHash as computeCmdHash2 } from "@openape/core";
|
|
1001
|
-
async function buildExactCommandGrantRequest(command, options) {
|
|
1002
|
-
return {
|
|
1003
|
-
request: {
|
|
1004
|
-
requester: options.requester,
|
|
1005
|
-
target_host: options.target_host,
|
|
1006
|
-
audience: options.audience,
|
|
1007
|
-
grant_type: options.grant_type,
|
|
1008
|
-
command,
|
|
1009
|
-
cmd_hash: await computeCmdHash2(command.join(" ")),
|
|
1010
|
-
...options.reason ? { reason: options.reason } : {},
|
|
1011
|
-
...options.run_as ? { run_as: options.run_as } : {}
|
|
1012
|
-
}
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
async function buildStructuredCliGrantRequest(resolved, options) {
|
|
1016
|
-
const details = "detail" in resolved ? [resolved.detail] : resolved.details;
|
|
1017
|
-
const permissions = "permission" in resolved ? [resolved.permission] : resolved.permissions;
|
|
1018
|
-
const command = "executionContext" in resolved && resolved.executionContext.argv?.length ? resolved.executionContext.argv : void 0;
|
|
1019
|
-
return {
|
|
1020
|
-
request: {
|
|
1021
|
-
requester: options.requester,
|
|
1022
|
-
target_host: options.target_host,
|
|
1023
|
-
audience: resolved.adapter.cli.audience ?? "shapes",
|
|
1024
|
-
grant_type: options.grant_type,
|
|
1025
|
-
permissions,
|
|
1026
|
-
authorization_details: details,
|
|
1027
|
-
execution_context: resolved.executionContext,
|
|
1028
|
-
...command ? { command } : {},
|
|
1029
|
-
...options.reason ? { reason: options.reason } : { reason: "summary" in resolved ? resolved.summary : details[0]?.display },
|
|
1030
|
-
...options.run_as ? { run_as: options.run_as } : {}
|
|
1031
|
-
}
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
export {
|
|
1036
|
-
GENERIC_OPERATION_ID,
|
|
1037
|
-
buildGenericResolved,
|
|
1038
|
-
resolveAdapterPath,
|
|
1039
|
-
loadAdapter,
|
|
1040
|
-
resolveGenericOrReject,
|
|
1041
|
-
tryLoadAdapter,
|
|
1042
|
-
appendAuditLog,
|
|
1043
|
-
installAdapter,
|
|
1044
|
-
getInstalledDigest,
|
|
1045
|
-
isInstalled,
|
|
1046
|
-
removeAdapter,
|
|
1047
|
-
findConflictingAdapters,
|
|
1048
|
-
fetchRegistry,
|
|
1049
|
-
searchAdapters,
|
|
1050
|
-
findAdapter,
|
|
1051
|
-
parseShellCommand,
|
|
1052
|
-
extractShellCommandString,
|
|
1053
|
-
loadOrInstallAdapter,
|
|
1054
|
-
resolveCapabilityRequest,
|
|
1055
|
-
resolveCommand,
|
|
1056
|
-
extractWrappedCommand,
|
|
1057
|
-
extractOption,
|
|
1058
|
-
createShapesGrant,
|
|
1059
|
-
waitForGrantStatus,
|
|
1060
|
-
fetchGrantToken,
|
|
1061
|
-
verifyAndConsume,
|
|
1062
|
-
verifyAndExecute,
|
|
1063
|
-
resolveFromGrant,
|
|
1064
|
-
findExistingGrant,
|
|
1065
|
-
buildExactCommandGrantRequest,
|
|
1066
|
-
buildStructuredCliGrantRequest
|
|
1067
|
-
};
|
|
1068
|
-
//# sourceMappingURL=chunk-PEA2RDWK.js.map
|