@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.
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getGenericAuditLogPath
4
+ } from "./chunk-OBF7IMQ2.js";
5
+
6
+ // src/shapes/commands/explain.ts
7
+ import { defineCommand } from "citty";
8
+ import { loadAdapter, resolveCommand } from "@openape/shapes";
9
+ var explainCommand = defineCommand({
10
+ meta: {
11
+ name: "explain",
12
+ description: "Show what permission a wrapped command would need"
13
+ },
14
+ args: {
15
+ adapter: {
16
+ type: "string",
17
+ description: "Explicit path to adapter TOML file"
18
+ },
19
+ _: {
20
+ type: "positional",
21
+ description: "Wrapped command (after --)",
22
+ required: false
23
+ }
24
+ },
25
+ async run({ rawArgs }) {
26
+ const command = extractWrappedCommand(rawArgs ?? []);
27
+ if (command.length === 0)
28
+ throw new Error("Missing wrapped command. Usage: shapes explain [--adapter <file>] -- <cli> ...");
29
+ const adapterOpt = extractOption(rawArgs ?? [], "adapter");
30
+ const loaded = loadAdapter(command[0], adapterOpt);
31
+ const resolved = await resolveCommand(loaded, command);
32
+ process.stdout.write(`${JSON.stringify({
33
+ adapter: resolved.adapter.cli.id,
34
+ source: resolved.source,
35
+ operation: resolved.detail.operation_id,
36
+ display: resolved.detail.display,
37
+ permission: resolved.permission,
38
+ resource_chain: resolved.detail.resource_chain,
39
+ exact_command: resolved.detail.constraints?.exact_command ?? false,
40
+ adapter_digest: resolved.digest
41
+ }, null, 2)}
42
+ `);
43
+ }
44
+ });
45
+ function extractWrappedCommand(args) {
46
+ const delimiter = args.indexOf("--");
47
+ return delimiter >= 0 ? args.slice(delimiter + 1) : [];
48
+ }
49
+ function extractOption(args, name) {
50
+ const delimiter = args.indexOf("--");
51
+ const optionArgs = delimiter >= 0 ? args.slice(0, delimiter) : args;
52
+ const index = optionArgs.indexOf(`--${name}`);
53
+ if (index >= 0 && index + 1 < optionArgs.length)
54
+ return optionArgs[index + 1];
55
+ return void 0;
56
+ }
57
+
58
+ // src/shapes/grants.ts
59
+ import { computeCmdHash } from "@openape/core";
60
+ import { cliAuthorizationDetailCovers, verifyAuthzJWT } from "@openape/grants";
61
+ import { execFileSync } from "child_process";
62
+ import { hostname } from "os";
63
+ import consola from "consola";
64
+
65
+ // src/audit/generic-log.ts
66
+ import { mkdir, appendFile } from "fs/promises";
67
+ import { homedir } from "os";
68
+ import { dirname, join } from "path";
69
+ function defaultGenericLogPath() {
70
+ return join(homedir(), ".config", "apes", "generic-calls.log");
71
+ }
72
+ async function appendGenericCallLog(entry, logPath) {
73
+ const path = logPath ?? defaultGenericLogPath();
74
+ await mkdir(dirname(path), { recursive: true });
75
+ await appendFile(path, `${JSON.stringify(entry)}
76
+ `, "utf-8");
77
+ }
78
+
79
+ // src/shapes/grants.ts
80
+ import {
81
+ apiFetch,
82
+ discoverEndpoints,
83
+ getGrantsEndpoint,
84
+ getRequesterIdentity,
85
+ isGenericResolved,
86
+ loadOrInstallAdapter,
87
+ resolveCommand as resolveCommand2
88
+ } from "@openape/shapes";
89
+ function decodePayload(token) {
90
+ const [, payload] = token.split(".");
91
+ if (!payload)
92
+ throw new Error("Invalid JWT");
93
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
94
+ }
95
+ async function createShapesGrant(resolved, params) {
96
+ const grantsEndpoint = await getGrantsEndpoint(params.idp);
97
+ const requester = getRequesterIdentity();
98
+ if (!requester) {
99
+ throw new Error("No requester identity available. Run `apes login` first.");
100
+ }
101
+ return apiFetch(grantsEndpoint, {
102
+ method: "POST",
103
+ idp: params.idp,
104
+ body: {
105
+ requester,
106
+ target_host: hostname(),
107
+ audience: resolved.adapter.cli.audience ?? "shapes",
108
+ grant_type: params.approval,
109
+ command: resolved.executionContext.argv,
110
+ reason: params.reason ?? resolved.detail.display,
111
+ permissions: [resolved.permission],
112
+ authorization_details: [resolved.detail],
113
+ execution_context: resolved.executionContext
114
+ }
115
+ });
116
+ }
117
+ async function waitForGrantStatus(idp, grantId) {
118
+ const grantsEndpoint = await getGrantsEndpoint(idp);
119
+ const deadline = Date.now() + 3e5;
120
+ while (Date.now() < deadline) {
121
+ const grant = await apiFetch(`${grantsEndpoint}/${grantId}`, { idp });
122
+ if (grant.status === "approved" || grant.status === "denied" || grant.status === "revoked")
123
+ return grant.status;
124
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
125
+ }
126
+ throw new Error("Timed out waiting for grant approval");
127
+ }
128
+ async function fetchGrantToken(idp, grantId) {
129
+ const grantsEndpoint = await getGrantsEndpoint(idp);
130
+ const response = await apiFetch(`${grantsEndpoint}/${grantId}/token`, {
131
+ method: "POST",
132
+ idp
133
+ });
134
+ return response.authz_jwt;
135
+ }
136
+ function grantedCliDetails(claims) {
137
+ const details = claims.authorization_details;
138
+ if (!Array.isArray(details))
139
+ return [];
140
+ return details.filter(
141
+ (detail) => typeof detail === "object" && detail !== null && detail.type === "openape_cli"
142
+ );
143
+ }
144
+ function hasStructuredCliGrant(claims) {
145
+ return grantedCliDetails(claims).length > 0;
146
+ }
147
+ async function verifyAndConsume(token, resolved) {
148
+ const payload = decodePayload(token);
149
+ const issuer = String(payload.iss ?? "");
150
+ if (!issuer)
151
+ throw new Error("Grant token is missing issuer");
152
+ const discovery = await discoverEndpoints(issuer);
153
+ const jwksUri = String(discovery.jwks_uri ?? `${issuer}/.well-known/jwks.json`);
154
+ const result = await verifyAuthzJWT(token, {
155
+ expectedIss: issuer,
156
+ expectedAud: resolved.adapter.cli.audience ?? "shapes",
157
+ jwksUri
158
+ });
159
+ if (!result.valid || !result.claims) {
160
+ throw new Error(result.error ?? "Grant verification failed");
161
+ }
162
+ const claims = result.claims;
163
+ const details = grantedCliDetails(claims);
164
+ if (claims.execution_context?.adapter_digest && claims.execution_context.adapter_digest !== resolved.digest) {
165
+ throw new Error("Adapter digest mismatch");
166
+ }
167
+ if (!hasStructuredCliGrant(claims)) {
168
+ const argv = resolved.executionContext.argv;
169
+ if (!argv?.length) {
170
+ throw new Error("Resolved command is missing argv");
171
+ }
172
+ const expectedCmdHash = await computeCmdHash(argv.join(" "));
173
+ if (claims.command?.join("\0") !== argv.join("\0")) {
174
+ throw new Error("Granted command does not match current argv");
175
+ }
176
+ if (claims.cmd_hash && claims.cmd_hash !== expectedCmdHash) {
177
+ throw new Error("Granted command does not match current argv");
178
+ }
179
+ if (!claims.command?.length && !claims.cmd_hash) {
180
+ throw new Error("Grant is not a structured CLI grant and is missing command binding");
181
+ }
182
+ } else {
183
+ if (!details.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail))) {
184
+ throw new Error(`Grant does not cover required permission: ${resolved.permission}`);
185
+ }
186
+ const exactRequired = details.some(
187
+ (detail) => cliAuthorizationDetailCovers(detail, resolved.detail) && detail.constraints?.exact_command
188
+ );
189
+ const isOnce = claims.grant_type === "once" || claims.approval === "once";
190
+ const enforceArgvHash = exactRequired || isOnce && !!claims.execution_context?.argv_hash;
191
+ if (enforceArgvHash && claims.execution_context?.argv_hash !== resolved.executionContext.argv_hash) {
192
+ throw new Error("Granted command does not match current argv");
193
+ }
194
+ }
195
+ const grantsEndpoint = await getGrantsEndpoint(issuer);
196
+ const consume = await fetch(`${grantsEndpoint}/${claims.grant_id}/consume`, {
197
+ method: "POST",
198
+ headers: {
199
+ Authorization: `Bearer ${token}`
200
+ }
201
+ });
202
+ if (!consume.ok) {
203
+ throw new Error(`Consume failed: ${consume.status} ${consume.statusText}`);
204
+ }
205
+ const consumeResult = await consume.json();
206
+ if (consumeResult.error) {
207
+ throw new Error(`Grant rejected at consume step: ${consumeResult.error}`);
208
+ }
209
+ }
210
+ function executeResolvedViaExec(resolved) {
211
+ consola.info(`Executing ${(resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv]).join(" ")}`);
212
+ execFileSync(resolved.executable, resolved.commandArgv, { stdio: "inherit" });
213
+ }
214
+ async function verifyAndExecute(token, resolved, grantId) {
215
+ await verifyAndConsume(token, resolved);
216
+ const isGeneric = isGenericResolved(resolved);
217
+ const start = Date.now();
218
+ let exitCode = 0;
219
+ try {
220
+ executeResolvedViaExec(resolved);
221
+ } catch (err) {
222
+ exitCode = err?.status ?? 1;
223
+ throw err;
224
+ } finally {
225
+ if (isGeneric && grantId) {
226
+ try {
227
+ await appendGenericCallLog(
228
+ {
229
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
230
+ cli: resolved.detail.cli_id,
231
+ argv: resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv],
232
+ argv_hash: resolved.executionContext.argv_hash ?? "",
233
+ grant_id: grantId,
234
+ exit_code: exitCode,
235
+ duration_ms: Date.now() - start
236
+ },
237
+ getGenericAuditLogPath()
238
+ );
239
+ } catch (logErr) {
240
+ consola.debug("Failed to append generic-call audit entry:", logErr);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ async function resolveFromGrant(grant) {
246
+ const argv = grant.request?.command;
247
+ if (!argv || argv.length === 0)
248
+ throw new Error("Grant request is missing command argv");
249
+ const executable = argv[0];
250
+ const adapter = await loadOrInstallAdapter(executable);
251
+ if (!adapter)
252
+ throw new Error(`No shapes adapter found for ${executable}`);
253
+ const resolved = await resolveCommand2(adapter, argv);
254
+ const grantDigest = grant.request.execution_context?.adapter_digest;
255
+ if (grantDigest && grantDigest !== resolved.digest) {
256
+ throw new Error(
257
+ `Adapter digest mismatch: grant was created against adapter ${grantDigest}, but local adapter is ${resolved.digest}. Reinstall or revert the adapter.`
258
+ );
259
+ }
260
+ return resolved;
261
+ }
262
+ async function findExistingGrant(resolved, idp) {
263
+ const grantsEndpoint = await getGrantsEndpoint(idp);
264
+ const response = await apiFetch(
265
+ `${grantsEndpoint}?status=approved`,
266
+ { idp }
267
+ );
268
+ const now = Math.floor(Date.now() / 1e3);
269
+ const expectedAudience = resolved.adapter.cli.audience ?? "shapes";
270
+ for (const grant of response.data) {
271
+ const req = grant.request;
272
+ if (req.grant_type === "once")
273
+ continue;
274
+ if (req.grant_type === "timed" && grant.expires_at && grant.expires_at <= now)
275
+ continue;
276
+ if (req.audience !== expectedAudience)
277
+ continue;
278
+ if (req.execution_context?.adapter_digest && req.execution_context.adapter_digest !== resolved.digest)
279
+ continue;
280
+ const cliDetails = (req.authorization_details ?? []).filter(
281
+ (d) => d.type === "openape_cli"
282
+ );
283
+ if (cliDetails.length > 0) {
284
+ if (cliDetails.some((detail) => cliAuthorizationDetailCovers(detail, resolved.detail)))
285
+ return grant.id;
286
+ } else if (req.permissions?.includes(resolved.permission)) {
287
+ return grant.id;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+
293
+ export {
294
+ extractWrappedCommand,
295
+ extractOption,
296
+ createShapesGrant,
297
+ waitForGrantStatus,
298
+ fetchGrantToken,
299
+ verifyAndConsume,
300
+ verifyAndExecute,
301
+ resolveFromGrant,
302
+ findExistingGrant
303
+ };
304
+ //# sourceMappingURL=chunk-BA2V3BBO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shapes/commands/explain.ts","../src/shapes/grants.ts","../src/audit/generic-log.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { loadAdapter, resolveCommand } from '@openape/shapes'\n\nexport const explainCommand = defineCommand({\n meta: {\n name: 'explain',\n description: 'Show what permission a wrapped command would need',\n },\n args: {\n adapter: {\n type: 'string',\n description: 'Explicit path to adapter TOML file',\n },\n _: {\n type: 'positional',\n description: 'Wrapped command (after --)',\n required: false,\n },\n },\n async run({ rawArgs }) {\n const command = extractWrappedCommand(rawArgs ?? [])\n if (command.length === 0)\n throw new Error('Missing wrapped command. Usage: shapes explain [--adapter <file>] -- <cli> ...')\n\n const adapterOpt = extractOption(rawArgs ?? [], 'adapter')\n const loaded = loadAdapter(command[0]!, adapterOpt)\n const resolved = await resolveCommand(loaded, command)\n\n process.stdout.write(`${JSON.stringify({\n adapter: resolved.adapter.cli.id,\n source: resolved.source,\n operation: resolved.detail.operation_id,\n display: resolved.detail.display,\n permission: resolved.permission,\n resource_chain: resolved.detail.resource_chain,\n exact_command: resolved.detail.constraints?.exact_command ?? false,\n adapter_digest: resolved.digest,\n }, null, 2)}\\n`)\n },\n})\n\nexport function extractWrappedCommand(args: string[]): string[] {\n const delimiter = args.indexOf('--')\n return delimiter >= 0 ? args.slice(delimiter + 1) : []\n}\n\nexport function extractOption(args: string[], name: string): string | undefined {\n const delimiter = args.indexOf('--')\n const optionArgs = delimiter >= 0 ? args.slice(0, delimiter) : args\n const index = optionArgs.indexOf(`--${name}`)\n if (index >= 0 && index + 1 < optionArgs.length)\n return optionArgs[index + 1]\n return undefined\n}\n","import type { OpenApeCliAuthorizationDetail, OpenApeGrant } from '@openape/core'\nimport { computeCmdHash } from '@openape/core'\nimport { cliAuthorizationDetailCovers, verifyAuthzJWT } from '@openape/grants'\nimport { execFileSync } from 'node:child_process'\nimport { hostname } from 'node:os'\nimport consola from 'consola'\nimport { getGenericAuditLogPath } from '../config.js'\nimport { appendGenericCallLog } from '../audit/generic-log.js'\nimport {\n apiFetch,\n discoverEndpoints,\n getGrantsEndpoint,\n getRequesterIdentity,\n isGenericResolved,\n loadOrInstallAdapter,\n resolveCommand,\n} from '@openape/shapes'\nimport type { ResolvedCommand } from '@openape/shapes'\n\nfunction decodePayload(token: string): Record<string, unknown> {\n const [, payload] = token.split('.')\n if (!payload)\n throw new Error('Invalid JWT')\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) as Record<string, unknown>\n}\n\ninterface SimilarGrantsInfo {\n similar_grants: Array<{ grant: { id: string }, similar_detail_indices: number[] }>\n widened_details: Array<{ permission: string }>\n merged_details: Array<{ permission: string }>\n}\n\nexport async function createShapesGrant(\n resolved: ResolvedCommand,\n params: {\n idp: string\n approval: 'once' | 'timed' | 'always'\n reason?: string\n },\n): Promise<{ id: string, status: string, similar_grants?: SimilarGrantsInfo }> {\n const grantsEndpoint = await getGrantsEndpoint(params.idp)\n const requester = getRequesterIdentity()\n if (!requester) {\n throw new Error('No requester identity available. Run `apes login` first.')\n }\n return apiFetch<{ id: string, status: string, similar_grants?: SimilarGrantsInfo }>(grantsEndpoint, {\n method: 'POST',\n idp: params.idp,\n body: {\n requester,\n target_host: hostname(),\n audience: resolved.adapter.cli.audience ?? 'shapes',\n grant_type: params.approval,\n command: resolved.executionContext.argv,\n reason: params.reason ?? resolved.detail.display,\n permissions: [resolved.permission],\n authorization_details: [resolved.detail],\n execution_context: resolved.executionContext,\n },\n })\n}\n\nexport async function waitForGrantStatus(idp: string, grantId: string): Promise<'approved' | 'denied' | 'revoked'> {\n const grantsEndpoint = await getGrantsEndpoint(idp)\n const deadline = Date.now() + 300_000\n\n while (Date.now() < deadline) {\n const grant = await apiFetch<{ status: 'pending' | 'approved' | 'denied' | 'revoked' }>(`${grantsEndpoint}/${grantId}`, { idp })\n if (grant.status === 'approved' || grant.status === 'denied' || grant.status === 'revoked')\n return grant.status\n await new Promise(resolve => setTimeout(resolve, 3000))\n }\n\n throw new Error('Timed out waiting for grant approval')\n}\n\nexport async function fetchGrantToken(idp: string, grantId: string): Promise<string> {\n const grantsEndpoint = await getGrantsEndpoint(idp)\n const response = await apiFetch<{ authz_jwt: string }>(`${grantsEndpoint}/${grantId}/token`, {\n method: 'POST',\n idp,\n })\n return response.authz_jwt\n}\n\nfunction grantedCliDetails(claims: Record<string, unknown>): OpenApeCliAuthorizationDetail[] {\n const details = claims.authorization_details\n if (!Array.isArray(details))\n return []\n\n return details.filter((detail): detail is OpenApeCliAuthorizationDetail =>\n typeof detail === 'object'\n && detail !== null\n && (detail as Record<string, unknown>).type === 'openape_cli',\n )\n}\n\nfunction hasStructuredCliGrant(claims: Record<string, unknown>): boolean {\n return grantedCliDetails(claims).length > 0\n}\n\n/**\n * Verifies a grant token against the resolved command and marks the grant\n * as consumed on the IdP. Does NOT execute anything — callers that want\n * the one-shot behavior should use `verifyAndExecute`, callers that want\n * to run the command themselves (e.g. the interactive REPL piping through\n * a persistent bash pty) should call this and then do their own execution.\n *\n * Split out so the interactive shell can re-use the verify + consume path\n * without being forced into the `execFileSync`-based one-shot execution.\n */\nexport async function verifyAndConsume(token: string, resolved: ResolvedCommand): Promise<void> {\n const payload = decodePayload(token)\n const issuer = String(payload.iss ?? '')\n if (!issuer)\n throw new Error('Grant token is missing issuer')\n\n const discovery = await discoverEndpoints(issuer)\n const jwksUri = String(discovery.jwks_uri ?? `${issuer}/.well-known/jwks.json`)\n const result = await verifyAuthzJWT(token, {\n expectedIss: issuer,\n expectedAud: resolved.adapter.cli.audience ?? 'shapes',\n jwksUri,\n })\n\n if (!result.valid || !result.claims) {\n throw new Error(result.error ?? 'Grant verification failed')\n }\n\n const claims = result.claims\n const details = grantedCliDetails(claims as unknown as Record<string, unknown>)\n\n if (claims.execution_context?.adapter_digest && claims.execution_context.adapter_digest !== resolved.digest) {\n throw new Error('Adapter digest mismatch')\n }\n\n if (!hasStructuredCliGrant(claims as unknown as Record<string, unknown>)) {\n const argv = resolved.executionContext.argv\n if (!argv?.length) {\n throw new Error('Resolved command is missing argv')\n }\n const expectedCmdHash = await computeCmdHash(argv.join(' '))\n if (claims.command?.join('\\0') !== argv.join('\\0')) {\n throw new Error('Granted command does not match current argv')\n }\n if (claims.cmd_hash && claims.cmd_hash !== expectedCmdHash) {\n throw new Error('Granted command does not match current argv')\n }\n if (!claims.command?.length && !claims.cmd_hash) {\n throw new Error('Grant is not a structured CLI grant and is missing command binding')\n }\n }\n else {\n if (!details.some(detail => cliAuthorizationDetailCovers(detail, resolved.detail))) {\n throw new Error(`Grant does not cover required permission: ${resolved.permission}`)\n }\n\n const exactRequired = details.some(detail =>\n cliAuthorizationDetailCovers(detail, resolved.detail) && detail.constraints?.exact_command,\n )\n\n const isOnce = claims.grant_type === 'once' || claims.approval === 'once'\n const enforceArgvHash = exactRequired || (isOnce && !!claims.execution_context?.argv_hash)\n\n if (enforceArgvHash && claims.execution_context?.argv_hash !== resolved.executionContext.argv_hash) {\n throw new Error('Granted command does not match current argv')\n }\n }\n\n const grantsEndpoint = await getGrantsEndpoint(issuer)\n const consume = await fetch(`${grantsEndpoint}/${claims.grant_id}/consume`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n\n if (!consume.ok) {\n throw new Error(`Consume failed: ${consume.status} ${consume.statusText}`)\n }\n\n const consumeResult = await consume.json() as { error?: string }\n if (consumeResult.error) {\n throw new Error(`Grant rejected at consume step: ${consumeResult.error}`)\n }\n}\n\n/**\n * Execute a verified + consumed resolved command directly via execFileSync,\n * inheriting stdio so the caller's terminal is handed to the child. Used by\n * the one-shot `apes run --shell` path.\n */\nexport function executeResolvedViaExec(resolved: ResolvedCommand): void {\n consola.info(`Executing ${(resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv]).join(' ')}`)\n execFileSync(resolved.executable, resolved.commandArgv, { stdio: 'inherit' })\n}\n\n/**\n * One-shot verify + consume + execute. Preserves the legacy behavior of\n * the `apes run --shell` path so existing callers keep working unchanged.\n *\n * When `resolved` carries the generic-fallback operation id\n * (`_generic.exec`), a JSONL audit entry is appended to the generic-calls\n * log after the child process exits. `grantId` is needed to write the\n * entry — callers that know the grant id should pass it; if omitted, the\n * log entry is skipped (the audit hook is best-effort, not a hard gate).\n *\n * This is the central exec path for sync (`--wait`), async-default\n * (`apes grants run <id> --wait`), and REPL one-shot, so a hook here\n * covers all three flows without duplicating logic in each caller.\n */\nexport async function verifyAndExecute(\n token: string,\n resolved: ResolvedCommand,\n grantId?: string,\n): Promise<void> {\n await verifyAndConsume(token, resolved)\n\n const isGeneric = isGenericResolved(resolved)\n const start = Date.now()\n let exitCode = 0\n try {\n executeResolvedViaExec(resolved)\n }\n catch (err) {\n exitCode = (err as { status?: number })?.status ?? 1\n throw err\n }\n finally {\n if (isGeneric && grantId) {\n // Best-effort: swallow log errors so a broken audit file never\n // blocks a successful command.\n try {\n await appendGenericCallLog(\n {\n ts: new Date().toISOString(),\n cli: resolved.detail.cli_id,\n argv: resolved.executionContext.argv ?? [resolved.executable, ...resolved.commandArgv],\n argv_hash: resolved.executionContext.argv_hash ?? '',\n grant_id: grantId,\n exit_code: exitCode,\n duration_ms: Date.now() - start,\n },\n getGenericAuditLogPath(),\n )\n }\n catch (logErr) {\n consola.debug('Failed to append generic-call audit entry:', logErr)\n }\n }\n }\n}\n\n/**\n * Re-resolve a ResolvedCommand from a previously-created grant's recorded\n * request (argv + adapter digest). Used by `apes grants run <id>` to replay\n * an approved grant locally after the async approval step.\n *\n * Throws when the grant is missing argv, when the adapter cannot be loaded,\n * or when the locally installed adapter's digest no longer matches what the\n * grant was issued against.\n */\nexport async function resolveFromGrant(\n grant: {\n request: {\n command?: string[]\n execution_context?: { adapter_digest?: string, argv_hash?: string }\n authorization_details?: Array<{ type?: string, permission?: string }>\n }\n },\n): Promise<ResolvedCommand> {\n const argv = grant.request?.command\n if (!argv || argv.length === 0)\n throw new Error('Grant request is missing command argv')\n\n const executable = argv[0]\n const adapter = await loadOrInstallAdapter(executable)\n if (!adapter)\n throw new Error(`No shapes adapter found for ${executable}`)\n\n const resolved = await resolveCommand(adapter, argv)\n\n const grantDigest = grant.request.execution_context?.adapter_digest\n if (grantDigest && grantDigest !== resolved.digest) {\n throw new Error(\n `Adapter digest mismatch: grant was created against adapter ${grantDigest}, but local adapter is ${resolved.digest}. Reinstall or revert the adapter.`,\n )\n }\n\n return resolved\n}\n\nexport async function findExistingGrant(\n resolved: ResolvedCommand,\n idp: string,\n): Promise<string | null> {\n const grantsEndpoint = await getGrantsEndpoint(idp)\n const response = await apiFetch<{ data: OpenApeGrant[] }>(\n `${grantsEndpoint}?status=approved`,\n { idp },\n )\n\n const now = Math.floor(Date.now() / 1000)\n const expectedAudience = resolved.adapter.cli.audience ?? 'shapes'\n\n for (const grant of response.data) {\n const req = grant.request\n if (req.grant_type === 'once')\n continue\n if (req.grant_type === 'timed' && grant.expires_at && grant.expires_at <= now)\n continue\n if (req.audience !== expectedAudience)\n continue\n if (req.execution_context?.adapter_digest && req.execution_context.adapter_digest !== resolved.digest)\n continue\n\n const cliDetails = (req.authorization_details ?? []).filter(\n (d): d is OpenApeCliAuthorizationDetail => d.type === 'openape_cli',\n )\n\n if (cliDetails.length > 0) {\n if (cliDetails.some(detail => cliAuthorizationDetailCovers(detail, resolved.detail)))\n return grant.id\n }\n else if (req.permissions?.includes(resolved.permission)) {\n return grant.id\n }\n }\n\n return null\n}\n","import { mkdir, appendFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\n/**\n * A single successful generic-fallback execution entry.\n * Denied, timeout, and cancelled grants are NOT logged here — they are\n * captured by the IdP's server-side audit trail.\n */\nexport interface GenericCallLogEntry {\n /** ISO 8601 timestamp of execution completion */\n ts: string\n /** CLI id as requested by the user (e.g. \"kubectl\") */\n cli: string\n /** Full argv as executed, including the executable */\n argv: string[]\n /** SHA-256 of the argv — matches the `argv:hash` selector in resource_chain */\n argv_hash: string\n /** Grant that authorized this execution */\n grant_id: string\n /** Process exit code */\n exit_code: number\n /** Wall-clock duration from grant-approval to process exit, milliseconds */\n duration_ms: number\n}\n\n/**\n * Default audit log location. Lives under `~/.config/apes/` for consistency\n * with the rest of apes' client state (`config.toml`, `auth.json`, …).\n */\nexport function defaultGenericLogPath(): string {\n return join(homedir(), '.config', 'apes', 'generic-calls.log')\n}\n\n/**\n * Append a single generic-call entry to the audit log in JSONL format.\n * Creates the containing directory if needed.\n *\n * @param entry The call record to append\n * @param logPath Optional override (usually from `config.generic.audit_log`)\n */\nexport async function appendGenericCallLog(\n entry: GenericCallLogEntry,\n logPath?: string,\n): Promise<void> {\n const path = logPath ?? defaultGenericLogPath()\n await mkdir(dirname(path), { recursive: true })\n await appendFile(path, `${JSON.stringify(entry)}\\n`, 'utf-8')\n}\n"],"mappings":"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","names":["resolveCommand"]}
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shapes/index.ts
4
+ import {
5
+ appendAuditLog,
6
+ buildExactCommandGrantRequest,
7
+ buildStructuredCliGrantRequest,
8
+ buildGenericAdapter,
9
+ buildGenericResolved,
10
+ discoverEndpoints,
11
+ extractShellCommandString,
12
+ fetchRegistry,
13
+ findAdapter,
14
+ findConflictingAdapters,
15
+ GENERIC_OPERATION_ID,
16
+ getInstalledDigest,
17
+ installAdapter,
18
+ isGenericResolved,
19
+ isInstalled,
20
+ loadAdapter,
21
+ loadOrInstallAdapter,
22
+ parseShellCommand,
23
+ removeAdapter,
24
+ resolveAdapterPath,
25
+ resolveCapabilityRequest,
26
+ resolveCommand,
27
+ resolveGenericOrReject,
28
+ searchAdapters,
29
+ tryLoadAdapter
30
+ } from "@openape/shapes";
31
+
32
+ export {
33
+ appendAuditLog,
34
+ buildStructuredCliGrantRequest,
35
+ buildGenericResolved,
36
+ extractShellCommandString,
37
+ fetchRegistry,
38
+ findAdapter,
39
+ findConflictingAdapters,
40
+ GENERIC_OPERATION_ID,
41
+ getInstalledDigest,
42
+ installAdapter,
43
+ isInstalled,
44
+ loadAdapter,
45
+ loadOrInstallAdapter,
46
+ parseShellCommand,
47
+ removeAdapter,
48
+ resolveCapabilityRequest,
49
+ resolveCommand,
50
+ resolveGenericOrReject,
51
+ searchAdapters
52
+ };
53
+ //# sourceMappingURL=chunk-JXS3KLJ5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shapes/index.ts"],"sourcesContent":["export {\n appendAuditLog,\n buildExactCommandGrantRequest,\n buildStructuredCliGrantRequest,\n buildGenericAdapter,\n buildGenericResolved,\n discoverEndpoints,\n extractShellCommandString,\n fetchRegistry,\n findAdapter,\n findConflictingAdapters,\n GENERIC_OPERATION_ID,\n getInstalledDigest,\n installAdapter,\n isGenericResolved,\n isInstalled,\n loadAdapter,\n loadOrInstallAdapter,\n parseShellCommand,\n removeAdapter,\n resolveAdapterPath,\n resolveCapabilityRequest,\n resolveCommand,\n resolveGenericOrReject,\n searchAdapters,\n tryLoadAdapter,\n} from '@openape/shapes'\nexport type {\n AdapterMeta,\n AuditEntry,\n BuiltGrantRequest,\n GrantRequestOptions,\n LoadedAdapter,\n ParsedShellCommand,\n RegistryEntry,\n RegistryIndex,\n ResolvedCapability,\n ResolvedCommand,\n ShapesAdapter,\n ShapesOperation,\n} from '@openape/shapes'\nexport { extractOption, extractWrappedCommand } from './commands/explain.js'\nexport { createShapesGrant, executeResolvedViaExec, fetchGrantToken, findExistingGrant, resolveFromGrant, verifyAndConsume, verifyAndExecute, waitForGrantStatus } from './grants.js'\n"],"mappings":";;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var CliError = class extends Error {
5
+ constructor(message, exitCode = 1) {
6
+ super(message);
7
+ this.exitCode = exitCode;
8
+ this.name = "CliError";
9
+ }
10
+ exitCode;
11
+ };
12
+ var CliExit = class extends Error {
13
+ constructor(exitCode = 0) {
14
+ super("");
15
+ this.exitCode = exitCode;
16
+ this.name = "CliExit";
17
+ }
18
+ exitCode;
19
+ };
20
+
21
+ // src/duration.ts
22
+ function parseDuration(value) {
23
+ const match = value.match(/^(\d+)\s*([smhd])$/);
24
+ if (!match) {
25
+ throw new Error(`Invalid duration format: "${value}". Use e.g. 30m, 1h, 7d`);
26
+ }
27
+ const amount = Number.parseInt(match[1], 10);
28
+ switch (match[2]) {
29
+ case "s":
30
+ return amount;
31
+ case "m":
32
+ return amount * 60;
33
+ case "h":
34
+ return amount * 3600;
35
+ case "d":
36
+ return amount * 86400;
37
+ default:
38
+ throw new Error(`Unknown duration unit: ${match[2]}`);
39
+ }
40
+ }
41
+
42
+ export {
43
+ CliError,
44
+ CliExit,
45
+ parseDuration
46
+ };
47
+ //# sourceMappingURL=chunk-QMMRZPD2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/duration.ts"],"sourcesContent":["export class CliError extends Error {\n constructor(message: string, public exitCode: number = 1) {\n super(message)\n this.name = 'CliError'\n }\n}\n\nexport class CliExit extends Error {\n constructor(public exitCode: number = 0) {\n super('')\n this.name = 'CliExit'\n }\n}\n","/**\n * Parse a human-readable duration string into seconds.\n * Supported formats: 30s, 5m, 1h, 7d\n */\nexport function parseDuration(value: string): number {\n const match = value.match(/^(\\d+)\\s*([smhd])$/)\n if (!match) {\n throw new Error(`Invalid duration format: \"${value}\". Use e.g. 30m, 1h, 7d`)\n }\n const amount = Number.parseInt(match[1]!, 10)\n switch (match[2]) {\n case 's': return amount\n case 'm': return amount * 60\n case 'h': return amount * 3600\n case 'd': return amount * 86400\n default: throw new Error(`Unknown duration unit: ${match[2]}`)\n }\n}\n"],"mappings":";;;AAAO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YAAY,SAAwB,WAAmB,GAAG;AACxD,UAAM,OAAO;AADqB;AAElC,SAAK,OAAO;AAAA,EACd;AAAA,EAHoC;AAItC;AAEO,IAAM,UAAN,cAAsB,MAAM;AAAA,EACjC,YAAmB,WAAmB,GAAG;AACvC,UAAM,EAAE;AADS;AAEjB,SAAK,OAAO;AAAA,EACd;AAAA,EAHmB;AAIrB;;;ACRO,SAAS,cAAc,OAAuB;AACnD,QAAM,QAAQ,MAAM,MAAM,oBAAoB;AAC9C,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,6BAA6B,KAAK,yBAAyB;AAAA,EAC7E;AACA,QAAM,SAAS,OAAO,SAAS,MAAM,CAAC,GAAI,EAAE;AAC5C,UAAQ,MAAM,CAAC,GAAG;AAAA,IAChB,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO,SAAS;AAAA,IAC1B,KAAK;AAAK,aAAO,SAAS;AAAA,IAC1B,KAAK;AAAK,aAAO,SAAS;AAAA,IAC1B;AAAS,YAAM,IAAI,MAAM,0BAA0B,MAAM,CAAC,CAAC,EAAE;AAAA,EAC/D;AACF;","names":[]}
@@ -77,4 +77,4 @@ export {
77
77
  isApesSelfDispatch,
78
78
  checkSudoRejection
79
79
  };
80
- //# sourceMappingURL=chunk-3COOEDPF.js.map
80
+ //# sourceMappingURL=chunk-RVAXRDC2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notifications.ts","../src/shell/apes-self-dispatch.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport consola from 'consola'\nimport { quote } from 'shell-quote'\nimport { loadConfig } from './config'\n\nexport interface PendingGrantInfo {\n grantId: string\n approveUrl: string\n command: string\n audience: string\n host: string\n}\n\n/**\n * Resolve the notification command for pending grants. Checks (in order):\n * 1. `APES_NOTIFY_PENDING_COMMAND` env var (highest priority — lets\n * parent programs like openclaw override per invocation)\n * 2. `[notifications] pending_command` in ~/.config/apes/config.toml\n *\n * Returns undefined if no notification command is configured.\n */\nfunction resolvePendingCommand(): string | undefined {\n if (process.env.APES_NOTIFY_PENDING_COMMAND)\n return process.env.APES_NOTIFY_PENDING_COMMAND\n\n const config = loadConfig()\n return config.notifications?.pending_command\n}\n\n/**\n * Escape a value for safe embedding inside a single-quoted shell string.\n * We use `shell-quote` to produce a safe literal, then strip the outer\n * quoting because the template substitution embeds the value inside the\n * user's command template which is itself passed to `sh -c`.\n */\nfunction shellEscape(value: string): string {\n // quote() wraps in single quotes and escapes internal single quotes\n // e.g. \"it's\" → \"'it'\\\\''s'\"\n // We return the raw escaped form so it's safe inside sh -c.\n return quote([value])\n}\n\n/**\n * Substitute template variables in the notification command.\n * All values are shell-escaped to prevent injection.\n */\nfunction renderTemplate(template: string, info: PendingGrantInfo): string {\n return template\n .replace(/\\{grant_id\\}/g, shellEscape(info.grantId))\n .replace(/\\{approve_url\\}/g, shellEscape(info.approveUrl))\n .replace(/\\{command\\}/g, shellEscape(info.command))\n .replace(/\\{audience\\}/g, shellEscape(info.audience))\n .replace(/\\{host\\}/g, shellEscape(info.host))\n}\n\n/**\n * Send a notification that a grant is awaiting human approval.\n *\n * This is **fire-and-forget**: the notification subprocess runs detached\n * and unref'd so it cannot block the grant flow. A 10-second timeout\n * kills it if it hangs (e.g. network issue reaching Telegram API).\n *\n * Only fires when a notification command is configured. Silently returns\n * if not — the grant flow must never depend on notifications.\n *\n * Only call this when the grant **actually requires waiting** (new grant\n * with pending status). Do NOT call when:\n * - An existing timed/always grant was reused (no human action needed)\n * - The grant was instantly approved (no waiting phase)\n */\nexport function notifyGrantPending(info: PendingGrantInfo): void {\n const template = resolvePendingCommand()\n if (!template)\n return\n\n const rendered = renderTemplate(template, info)\n\n try {\n const child = spawn('sh', ['-c', rendered], {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env },\n })\n\n // Don't let the notification process keep the parent alive\n child.unref()\n\n // Kill after 10 seconds if it hasn't exited\n const timeout = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n }\n catch {}\n }, 10_000)\n timeout.unref()\n\n child.on('exit', () => clearTimeout(timeout))\n }\n catch (err) {\n // Never let notification failure break the grant flow\n consola.debug('Notification command failed:', err)\n }\n}\n","import { basename } from 'node:path'\nimport type { ParsedShellCommand } from '../shapes/index.js'\n\n/**\n * Subset of `apes` subcommands that remain grant-gated even when invoked\n * as self-dispatches from inside an ape-shell context. These are the\n * three categories where the shell-grant layer adds real security value\n * that isn't duplicated by server-side auth gates or local-file-only\n * semantics:\n *\n * - `run` — spawns arbitrary executables, the core of the grant system\n * - `fetch` — forwards the bearer token to a user-specified URL\n * - `mcp` — binds a network port and serves a persistent API\n *\n * Every other `apes <subcmd>` either reads state, mutates the user's own\n * local config, or talks to the IdP through endpoints that are already\n * scoped by the auth token — gating them in the shell is redundant\n * friction, and under 0.9.0's async-default grant flow it actively\n * breaks `apes grants run <id>` via recursion (the polling call itself\n * creates a new grant, cascading indefinitely).\n *\n * This is the single source of truth shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode` (which\n * receives the bash-c-wrapped command after `rewriteApeShellArgs`\n * rewrites `ape-shell -c \"<cmd>\"` into `apes run --shell -- bash -c <cmd>`)\n *\n * Keep this list in sync with the blocklist snapshot test in\n * `shell-grant-dispatch.test.ts` — the tripwire that forces a review\n * decision whenever a new top-level apes subcommand is added.\n */\nexport const APES_GATED_SUBCOMMANDS = new Set(['run', 'fetch', 'mcp'])\n\n/**\n * Returns true if the parsed shell command is an `apes <subcmd>`\n * invocation that should bypass the grant flow entirely. Non-apes\n * binaries, compound lines (pipes, &&, etc.), and subcommands in\n * `APES_GATED_SUBCOMMANDS` all return false so they stay on the normal\n * grant path.\n *\n * The caller (either `requestGrantForShellLine` for the REPL path or\n * `runShellMode` for the one-shot path) is responsible for parsing the\n * input string and passing the resulting ParsedShellCommand here.\n */\nexport function isApesSelfDispatch(parsed: ParsedShellCommand | null | undefined): boolean {\n if (!parsed || parsed.isCompound)\n return false\n const invokedName = basename(parsed.executable)\n if (invokedName !== 'apes' && invokedName !== 'apes.js')\n return false\n const subCommand = parsed.argv[0]\n if (!subCommand)\n return false\n if (!APES_GATED_SUBCOMMANDS.has(subCommand))\n return true\n // `apes run --as <user>` has its own internal escapes-audience grant\n // flow (runAdapterMode delegates to runAudienceMode('escapes', ...)).\n // Double-gating it through the ape-shell session-grant layer would\n // fall through to a generic session grant that never reaches escapes.\n // Let it self-dispatch so the inner apes process handles elevation.\n if (subCommand === 'run' && parsed.argv.includes('--as'))\n return true\n return false\n}\n\n/**\n * Result of checking a parsed shell command for a leading `sudo` token.\n * The `reason` doubles as a ready-to-print error message so the REPL\n * and one-shot paths emit byte-identical text.\n */\nexport interface SudoRejection {\n reason: string\n}\n\n/**\n * Returns a rejection hint if the parsed line is a simple, non-compound\n * command whose leading executable is `sudo`. `sudo` is not available\n * inside ape-shell (the wrapper user is not in /etc/sudoers by design),\n * so agents and humans should use the explicit\n * `apes run --as root -- <cmd>` flow which routes through the escapes\n * setuid binary.\n *\n * Compound lines (pipes, &&, etc.) return null so the downstream\n * session-grant path can still negotiate a grant and bash surfaces the\n * real sudo error. We only short-circuit the leading-sudo case which is\n * the agent footgun.\n *\n * Shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode`\n */\nexport function checkSudoRejection(parsed: ParsedShellCommand | null | undefined): SudoRejection | null {\n if (!parsed || parsed.isCompound) return null\n if (basename(parsed.executable) !== 'sudo') return null\n const rest = parsed.argv.join(' ').trim()\n const hint = rest.length > 0\n ? `apes run --as root -- ${rest}`\n : 'apes run --as root -- <cmd>'\n return {\n reason: `sudo is not available in ape-shell. Use \\`${hint}\\` for privileged commands.`,\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa;AACtB,OAAO,aAAa;AACpB,SAAS,aAAa;AAmBtB,SAAS,wBAA4C;AACnD,MAAI,QAAQ,IAAI;AACd,WAAO,QAAQ,IAAI;AAErB,QAAM,SAAS,WAAW;AAC1B,SAAO,OAAO,eAAe;AAC/B;AAQA,SAAS,YAAY,OAAuB;AAI1C,SAAO,MAAM,CAAC,KAAK,CAAC;AACtB;AAMA,SAAS,eAAe,UAAkB,MAAgC;AACxE,SAAO,SACJ,QAAQ,iBAAiB,YAAY,KAAK,OAAO,CAAC,EAClD,QAAQ,oBAAoB,YAAY,KAAK,UAAU,CAAC,EACxD,QAAQ,gBAAgB,YAAY,KAAK,OAAO,CAAC,EACjD,QAAQ,iBAAiB,YAAY,KAAK,QAAQ,CAAC,EACnD,QAAQ,aAAa,YAAY,KAAK,IAAI,CAAC;AAChD;AAiBO,SAAS,mBAAmB,MAA8B;AAC/D,QAAM,WAAW,sBAAsB;AACvC,MAAI,CAAC;AACH;AAEF,QAAM,WAAW,eAAe,UAAU,IAAI;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,QAAQ,GAAG;AAAA,MAC1C,UAAU;AAAA,MACV,OAAO;AAAA,MACP,KAAK,EAAE,GAAG,QAAQ,IAAI;AAAA,IACxB,CAAC;AAGD,UAAM,MAAM;AAGZ,UAAM,UAAU,WAAW,MAAM;AAC/B,UAAI;AACF,cAAM,KAAK,SAAS;AAAA,MACtB,QACM;AAAA,MAAC;AAAA,IACT,GAAG,GAAM;AACT,YAAQ,MAAM;AAEd,UAAM,GAAG,QAAQ,MAAM,aAAa,OAAO,CAAC;AAAA,EAC9C,SACO,KAAK;AAEV,YAAQ,MAAM,gCAAgC,GAAG;AAAA,EACnD;AACF;;;ACtGA,SAAS,gBAAgB;AA+BlB,IAAM,yBAAyB,oBAAI,IAAI,CAAC,OAAO,SAAS,KAAK,CAAC;AAa9D,SAAS,mBAAmB,QAAwD;AACzF,MAAI,CAAC,UAAU,OAAO;AACpB,WAAO;AACT,QAAM,cAAc,SAAS,OAAO,UAAU;AAC9C,MAAI,gBAAgB,UAAU,gBAAgB;AAC5C,WAAO;AACT,QAAM,aAAa,OAAO,KAAK,CAAC;AAChC,MAAI,CAAC;AACH,WAAO;AACT,MAAI,CAAC,uBAAuB,IAAI,UAAU;AACxC,WAAO;AAMT,MAAI,eAAe,SAAS,OAAO,KAAK,SAAS,MAAM;AACrD,WAAO;AACT,SAAO;AACT;AA4BO,SAAS,mBAAmB,QAAqE;AACtG,MAAI,CAAC,UAAU,OAAO,WAAY,QAAO;AACzC,MAAI,SAAS,OAAO,UAAU,MAAM,OAAQ,QAAO;AACnD,QAAM,OAAO,OAAO,KAAK,KAAK,GAAG,EAAE,KAAK;AACxC,QAAM,OAAO,KAAK,SAAS,IACvB,yBAAyB,IAAI,KAC7B;AACJ,SAAO;AAAA,IACL,QAAQ,6CAA6C,IAAI;AAAA,EAC3D;AACF;","names":[]}