Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ vana app request --scopes github.repositories # prints an approval URL, wai
vana app read github.repositories --grant <id> # signed read
```

`request` returns the grant id once the person approves. `read` stops at
`request` returns the grant id once the person approves. Asking the same
person again extends their grant rather than replacing it: `request` keeps
what the live grant already covers and prints what it keeps, adds and
removes (`--remove-scopes` to give one up). `read` stops at
exit 4 with the exact price before spending anything; add `--pay` to settle
it from escrow and `--max-fee` to cap it. The rest of the group:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"@inquirer/prompts": "8.3.0",
"@inquirer/search": "^4.1.6",
"@modelcontextprotocol/sdk": "^1.27.1",
"@opendatalabs/vana-sdk": "4.0.0",
"@opendatalabs/vana-sdk": "4.3.1",
"@sigstore/bundle": "^5.0.0",
"ajv": "^8.20.0",
"chromium-bidi": "15.0.0",
Expand Down
16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions skills/builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ vana app request --scopes spotify.savedTracks,spotify.playlists --json
Waits up to ten minutes by default (`--timeout <seconds>`). On approval the
outcome carries `grantId` and the exact read command in `remedy`.

One grant per owner and app: an approval replaces the grant's scopes, it
does not add a second grant. So when this machine already holds an approval
for the app key, `request` reads that live grant and asks for the union, and
prints what it keeps, adds and removes before creating the request. Give up
a scope with `--remove-scopes a,b`; pick whose grant to extend with
`--owner <address>` when more than one person approved; send `--scopes`
verbatim with `--no-merge-grant`. `--json` carries `kept`, `added`,
`removed` and `grantUnion.status` (`merged`, `no_live_grant`,
`owner_unknown`, `owner_ambiguous`, `disabled`, `unavailable`). With no
earlier approval the person is unknown until they approve, and the approval
page keeps what they already granted.

When no human is watching the terminal, do not block:

```bash
Expand Down
26 changes: 20 additions & 6 deletions src/cli/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ export function registerAppCommands(
"--scopes <list>",
"Comma-separated scopes to request (with --question, defaults to --derived)",
)
.option(
"--remove-scopes <list>",
"Comma-separated scopes the app's live grant should drop",
)
.option(
"--owner <address>",
"Whose live grant to extend, when more than one person approved this app",
)
.option(
"--no-merge-grant",
"Send --scopes verbatim instead of keeping what the live grant covers",
)
.option("--question <text>", "Derivative question to carry on the request")
.option("--derived <scope>", "Scope the answer is written to")
.option(
Expand All @@ -64,12 +76,14 @@ export function registerAppCommands(
.option("--app-id <id>", "App id shown during approval")
.option("--app-name <name>", "App name shown during approval")
.option("--app-url <url>", "App homepage shown during approval")
.action(async (commandOptions: Record<string, string | undefined>) => {
process.exitCode = await runAppRequest({
...getOptions(),
...commandOptions,
});
});
.action(
async (commandOptions: Record<string, string | boolean | undefined>) => {
process.exitCode = await runAppRequest({
...getOptions(),
...commandOptions,
});
},
);

const requests = app
.command("requests")
Expand Down
21 changes: 21 additions & 0 deletions src/cli/app/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ function emitReadSuccess(
* than implying the read was free by protocol design. When the gateway
* starts quoting a price, this is where the escrow path plugs in.
*
* Since vana-sdk 4.2.0 the jobs client signs a quoted price on its own when
* the gateway answers 402. `maxPrice: "0"` keeps this leg from spending:
* a charged read stops before anything is signed, as `payment_required`.
*
* Waking a cold sandbox takes seconds, so the inline wait is used and a
* timeout is `not_ready` (exit 6) rather than a failure.
*/
Expand All @@ -499,6 +503,7 @@ async function runEnclaveRead(
grantId: grantId as `0x${string}`,
scope,
wait: MAX_INLINE_WAIT_SECONDS,
maxPrice: "0",
});

const text = new TextDecoder().decode(result.body);
Expand All @@ -519,6 +524,22 @@ async function runEnclaveRead(
server: "gateway job queue",
});
} catch (error) {
if (isPaymentRequired(error)) {
const details = error.details ?? {};
return emitAppOutcome(options, {
status: "failed",
code: "payment_required",
message:
"This enclave read is charged, and paying for enclave reads is not supported by this CLI yet. Nothing was signed or spent.",
network: network.name,
data: {
delivery: "enclave",
owner,
amount: details.amount ?? null,
asset: details.asset ?? null,
},
});
}
return emitAppOutcome(options, {
status: "failed",
code: enclaveErrorCode(error),
Expand Down
Loading
Loading