@haven_ai/cli 0.10.0-alpha.0 → 0.12.0-alpha.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/README.md +44 -14
- package/dist/cli.cjs +43 -21
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +43 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +43 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +43 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,9 @@ The CLI talks to the hosted Haven backend by default. Point it elsewhere with
|
|
|
53
53
|
|
|
54
54
|
The path an agent walks, and where it stops. Four of the six steps in
|
|
55
55
|
[`/for-agents.md`](https://github.com/d-hinders/Haven-AI/blob/dev/packages/frontend/public/for-agents.md)
|
|
56
|
-
are your user's
|
|
56
|
+
are your user's. With a CLI session you can take step 3 — creating the agent
|
|
57
|
+
and setting its budget — off their hands, and `--run` also does step 4, which
|
|
58
|
+
is yours.
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
61
|
# 1. Get a scoped session. Prints a code and a link for your user to approve in
|
|
@@ -83,7 +85,8 @@ prints the address, the amount **and which chain** — read the chain from there
|
|
|
83
85
|
rather than assuming one. You can compose the message; you cannot send the money.
|
|
84
86
|
|
|
85
87
|
**Every command takes `--json`**, and every refusal is a JSON object with a
|
|
86
|
-
machine-readable `code
|
|
88
|
+
machine-readable `code` (an `agents connect --run` refusal carries the
|
|
89
|
+
connector's own code, under `outcome.error`). The contract and the six exit codes are in
|
|
87
90
|
[*For agents and scripts*](#for-agents-and-scripts) below — read that before
|
|
88
91
|
branching on anything.
|
|
89
92
|
|
|
@@ -103,13 +106,13 @@ haven login # browser device-code approval (the def
|
|
|
103
106
|
haven login --email you@example.com # password path instead (prompt or HAVEN_PASSWORD)
|
|
104
107
|
haven login --no-wait --json # print the link object and exit — resume with --poll
|
|
105
108
|
haven login --poll <device_code> # one poll round: 0 approved, 3 pending, 4 denied
|
|
106
|
-
haven whoami #
|
|
109
|
+
haven whoami # email and name; --json adds session expiry and API URL
|
|
107
110
|
haven guide # the agent onboarding runbook
|
|
108
111
|
haven logout
|
|
109
112
|
|
|
110
113
|
# read
|
|
111
114
|
haven wallets list
|
|
112
|
-
haven wallets balances --safe <id|address>
|
|
115
|
+
haven wallets balances [--safe <id|address>]
|
|
113
116
|
haven wallets funding [--safe <id|address>] [--wait] # the paste-ready funding instruction (#2534)
|
|
114
117
|
haven agents list
|
|
115
118
|
haven agents show <id>
|
|
@@ -145,6 +148,7 @@ haven agents connect --status <setupId> [--wait]
|
|
|
145
148
|
# budgets: construct-and-hand-off (#2539) — the CLI never signs
|
|
146
149
|
haven budget grant <agentId> --amount 25 --token USDC --period 1440
|
|
147
150
|
haven budget grant <agentId> --amount 25 --token USDC --period 1440 --recipient <address> --wait
|
|
151
|
+
haven budget grant <agentId> --amount 25 --token USDC --period 1440 --expires <unix-seconds>
|
|
148
152
|
haven budget revoke <agentId> <delegationHash> [--wait]
|
|
149
153
|
```
|
|
150
154
|
|
|
@@ -164,7 +168,14 @@ never calls `activate` — that is the whole design.
|
|
|
164
168
|
route). `--amount` is in
|
|
165
169
|
whole tokens, read from your wallet's balances exactly like
|
|
166
170
|
`agents connect`; `--recipient` pins the budget to one address (omit for an
|
|
167
|
-
open budget); `--expires` takes unix seconds
|
|
171
|
+
open budget); `--expires` takes unix seconds, at most 365 days from now
|
|
172
|
+
(`#4351`; default: 90 days). A grant
|
|
173
|
+
adds a budget to an EMPTY (token, recipient) slot only: when the agent
|
|
174
|
+
already has a live budget there, the build is refused (exit 4,
|
|
175
|
+
`error_code: BUDGET_SLOT_OCCUPIED` with `existing_delegation_hash` under
|
|
176
|
+
`--json`, #3933) and the hint says to edit that budget on the agent's page,
|
|
177
|
+
which replaces it and stops the old one on-chain. A grant cannot replace it
|
|
178
|
+
— that would leave the old delegation enabled.
|
|
168
179
|
- `budget revoke <agentId> <delegationHash>` prepares the sponsored
|
|
169
180
|
revocation (no gas, one signature). The hash comes from
|
|
170
181
|
`haven budget show <agentId> --hashes` or the dashboard. (`agents show` does
|
|
@@ -177,7 +188,8 @@ never calls `activate` — that is the whole design.
|
|
|
177
188
|
returns the same hash, so `--wait` converges instead of chasing a version
|
|
178
189
|
that never activates.
|
|
179
190
|
- Under `--json`, grant returns the backend's build object —
|
|
180
|
-
`{
|
|
191
|
+
`{ delegation_hash, version, delegate_account_address, signing_payload,
|
|
192
|
+
build_id, typed_data_hash, signing_url }`
|
|
181
193
|
(`build_id` and `typed_data_hash` are the delegation hash, named for API
|
|
182
194
|
clarity) — plus `agent_id` and `status`. The link first, the settled status
|
|
183
195
|
after: the same two-emission shape as device login.
|
|
@@ -194,7 +206,7 @@ rather than rounding it away.
|
|
|
194
206
|
The connector command is **printed, never composed** — it is the same string
|
|
195
207
|
the dashboard shows for the same setup, because both render what the backend
|
|
196
208
|
built. `--run` executes it for you as a child process with exactly `--json`
|
|
197
|
-
appended and nothing else changed, streams the connector's
|
|
209
|
+
appended and nothing else changed, streams the connector's stderr, and puts the
|
|
198
210
|
thing you have to act on first.
|
|
199
211
|
|
|
200
212
|
If the connector refuses — it cannot tell which runtime to wire, or the machine
|
|
@@ -217,7 +229,7 @@ Approving the budget stays with the human, in the browser, every time.
|
|
|
217
229
|
|
|
218
230
|
### `haven wallets funding`
|
|
219
231
|
|
|
220
|
-
Prints the funding instruction a human acts on: what to send (
|
|
232
|
+
Prints the funding instruction a human acts on: what to send (the token's
|
|
221
233
|
documented minimum-useful amount), to which address, on which chain, plus the
|
|
222
234
|
explorer link and a faucet link on testnets. It reads
|
|
223
235
|
`GET /user/accounts/:accountId/funding` — the same facts the dashboard's funding
|
|
@@ -225,7 +237,8 @@ card shows — and composes nothing locally, so the printed sentence and the
|
|
|
225
237
|
dashboard can never disagree about the amount.
|
|
226
238
|
|
|
227
239
|
`--wait` polls the same read until the account counts as funded, printing the
|
|
228
|
-
elapsed time on stderr
|
|
240
|
+
elapsed time while it waits (on stderr under `--json`), and exits 0 the moment
|
|
241
|
+
`funded` flips.
|
|
229
242
|
On timeout it exits 1 with the elapsed time in the message. It is read-only in
|
|
230
243
|
every mode: it never sends anything and never touches a faucet — the transfer
|
|
231
244
|
itself stays with the human.
|
|
@@ -293,7 +306,12 @@ budget, change signers, move funds, change your credentials, or rotate an
|
|
|
293
306
|
agent's keys — neither the delegate key (`/agents/:id/rekey/*`) nor the API key
|
|
294
307
|
(`/agents/:id/rotate-key`). `haven agents rotate-key` therefore needs an
|
|
295
308
|
ordinary session (`haven login --email`), not a device-code one: issuing a
|
|
296
|
-
fresh credential is a change of authority, and the human keeps those.
|
|
309
|
+
fresh credential is a change of authority, and the human keeps those. Two more
|
|
310
|
+
commands call routes the allow-list leaves out, so they need an ordinary
|
|
311
|
+
session too: `haven wallets rename` (`PUT /user/accounts/:id`) and
|
|
312
|
+
`haven activity export --format sie` (`GET /accounting/export`). Under a
|
|
313
|
+
device-code session the backend answers all three with a 401, so they exit
|
|
314
|
+
**3**, the same as a missing session. The allow-list lives in
|
|
297
315
|
`packages/backend/src/middleware/owner-cli.ts`; a route that is not on it
|
|
298
316
|
refuses, because #1640 already refuses every purpose-carrying token everywhere
|
|
299
317
|
and this is a single opt-in exception. A census test measures what the
|
|
@@ -306,12 +324,22 @@ haven agents list --json # success: the payload, unchanged
|
|
|
306
324
|
haven agents show missing --json # failure: one object, still parseable
|
|
307
325
|
```
|
|
308
326
|
|
|
309
|
-
A failure is
|
|
327
|
+
A failure is this object, with two exceptions below:
|
|
310
328
|
|
|
311
329
|
```json
|
|
312
330
|
{ "ok": false, "error": { "code": "not_authenticated", "message": "Not authenticated.", "hint": "Run `haven login` ..." } }
|
|
313
331
|
```
|
|
314
332
|
|
|
333
|
+
The first exception is a connector refusal under `haven agents connect --run`. It
|
|
334
|
+
exits **4** with the run object (`setup_id`, `approval_url`,
|
|
335
|
+
`connector_command`, `connector_exit_code`, `outcome`, `relay`), and the
|
|
336
|
+
connector's own error (`code`, `next_action`) is under `outcome.error`.
|
|
337
|
+
|
|
338
|
+
The second is a device login the user has not confirmed in the browser yet
|
|
339
|
+
(`haven login --json` past its wait, or `haven login --poll <code> --json`). It
|
|
340
|
+
exits **3** with `{ "status": "pending", "device_code": …, "retry_after": … }`
|
|
341
|
+
and no `ok` or `error` (see the login section above).
|
|
342
|
+
|
|
315
343
|
Success keeps whatever shape the command already returned — including the bare
|
|
316
344
|
arrays the list commands emit — so a script that parses a success today keeps
|
|
317
345
|
working. `login`, `logout` and the manage commands, which used to print only a
|
|
@@ -325,7 +353,7 @@ sentence, now emit an object as well.
|
|
|
325
353
|
| `1` | Failed | Something broke that none of the below describes (a 5xx, an unexpected error). Retrying may help. |
|
|
326
354
|
| `2` | Usage | The command line was wrong — unknown command, missing argument, bad flag, or a `--safe` that matches nothing. Fix the argv; retrying it unchanged will not help. |
|
|
327
355
|
| `3` | Not authenticated | No stored session, or the backend rejected the one we have. Run `haven login`. |
|
|
328
|
-
| `4` | Refused | The
|
|
356
|
+
| `4` | Refused | The answer is no: a 403, 410 or other 4xx from the backend (its own `error` text is echoed when it sends one), or a refusal the CLI makes itself — for example a live budget on `agents revoke`, credential-shaped `feedback submit` text, the connector's refusal under `agents connect --run`, or a device login the human denied. |
|
|
329
357
|
| `5` | Network | The backend could not be reached at all. Check connectivity and `--api`. |
|
|
330
358
|
|
|
331
359
|
**Why a 401 is `3` and not `4`.** The two overlap by definition — a 401 *is* the
|
|
@@ -353,7 +381,9 @@ test; the copy exists so this package keeps **zero runtime dependencies** and
|
|
|
353
381
|
|
|
354
382
|
- `--api <url>` or `HAVEN_API_URL` — backend URL (defaults to the hosted Haven
|
|
355
383
|
backend). The backend is pinned into the saved session at login.
|
|
356
|
-
- `HAVEN_EMAIL` / `HAVEN_PASSWORD` — non-interactive login (CI/scripts).
|
|
384
|
+
- `HAVEN_EMAIL` / `HAVEN_PASSWORD` — non-interactive login (CI/scripts). With
|
|
385
|
+
`HAVEN_EMAIL` set, a bare `haven login` takes the password path, not the
|
|
386
|
+
device flow.
|
|
357
387
|
- Session is stored owner-only at `~/.haven/session.json`. Treat it like a
|
|
358
388
|
secret; `haven logout` removes it.
|
|
359
389
|
|
|
@@ -362,5 +392,5 @@ test; the copy exists so this package keeps **zero runtime dependencies** and
|
|
|
362
392
|
The CLI authenticates as the user and talks to the same JWT API as the
|
|
363
393
|
dashboard. It can read everything and perform backend-only management; anything
|
|
364
394
|
that moves funds or changes on-chain authority is signed by your wallet/passkey
|
|
365
|
-
in the dashboard. Haven never holds your
|
|
395
|
+
in the dashboard. Haven never holds your account's owner key or any delegate key
|
|
366
396
|
through this tool.
|
package/dist/cli.cjs
CHANGED
|
@@ -90,6 +90,9 @@ function parseArgs(argv) {
|
|
|
90
90
|
} else if (arg === "--expires") {
|
|
91
91
|
const n = Number(value);
|
|
92
92
|
if (!Number.isInteger(n) || n <= 0) throw new Error("--expires must be a unix timestamp in seconds");
|
|
93
|
+
if (n > Math.floor(Date.now() / 1e3) + 365 * 86400) {
|
|
94
|
+
throw new Error("--expires must be at most 365 days from now");
|
|
95
|
+
}
|
|
93
96
|
flags.expires = n;
|
|
94
97
|
} else if (arg === "--format") {
|
|
95
98
|
if (value !== "csv" && value !== "sie") throw new Error('--format must be "csv" or "sie"');
|
|
@@ -174,9 +177,9 @@ function helpText() {
|
|
|
174
177
|
"Budgets (construct-and-hand-off \u2014 the CLI never signs, #2539):",
|
|
175
178
|
" budget grant <agentId> --amount <n> --token USDC --period <minutes>",
|
|
176
179
|
" [--recipient <address>] [--expires <unix-s>] [--wait]",
|
|
177
|
-
" Prints
|
|
180
|
+
" Prints an approval link; the human approves in the dashboard.",
|
|
178
181
|
" budget revoke <agentId> <delegationHash> [--wait]",
|
|
179
|
-
" Prints a revocation link; the human
|
|
182
|
+
" Prints a revocation link; the human approves in the dashboard.",
|
|
180
183
|
"",
|
|
181
184
|
"Options:",
|
|
182
185
|
" --json One JSON value on stdout, prose on stderr, on every",
|
|
@@ -471,7 +474,7 @@ ${failure.hint}` : failure.message);
|
|
|
471
474
|
}
|
|
472
475
|
|
|
473
476
|
// src/agent-guidance-text.ts
|
|
474
|
-
var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, `<packages.cli.one_liner> login --api <api-url>` afterwards gets you a scoped session for steps 3-4 (that flag or `HAVEN_API_URL`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. That command is the manifest's `packages.cli.one_liner` (`/.well-known/haven.json`) \u2014 run it as given: `packages.cli.channel` serves the full spec. Do not hold the process open while you wait: under `--json`, pass `--no-wait` to get the link object back at once, then poll it with `haven login --poll <device_code>` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.\n2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. `/.well-known/haven.json` names `chains.default` as the deployment's expected chain, but after login `haven wallets funding` prints the address, the amount and which chain in one place; confirm that chain before you message your user. Without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain; never assume one: a testnet deployment and production both call themselves Haven. Before you write that message, read the manifest: `environment` says whether this deployment is `production`, and each `chains.supported` entry says whether that chain is a `testnet`. Real money is at stake only on a non-testnet chain of a `production` deployment \u2014 tell your user which case theirs is.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: `haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>` prints the same connector command and approval link; add `--run` to do step 4 too.\n4. **YOU \u2014 run the connector command** in that prompt (see What you run). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Budget changes later (second token, raise, revoke)\n\nThe first budget rides the setup in The sequence. After the agent exists, a CLI session can construct the LATER changes \u2014 a second token, a bigger amount, a recipient pin, a stop \u2014 and hand your user a link; the human still signs, every time:\n\n```\nhaven budget grant <agentId> --amount <n> --token USDC --period <minutes> [--recipient <address>] [--wait]\nhaven budget revoke <agentId> <delegationHash> [--wait]\n```\n\nThe CLI never signs: it prints a dashboard link, your user opens it and signs with their passkey or wallet. `--wait` polls until the human's signature lands. The hash for `revoke` is in `haven budget show <agentId> --hashes`.\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. Before you send anything, let me get you the exact address **and network** \u2014 `haven wallets funding` prints both, and the funding card on `<host>/dashboard` shows the same. Please do not send to an address or a chain I have not confirmed with you; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only three changes to the command above are permitted, and no others: appending --json; re-running it once with --runtime <name> added, only if the connector refuses because it could not determine the agent runtime, naming the harness you are running in and using one of the values that refusal lists; and re-running it once with the one flag I chose added, only after I have answered a wiring_collision relay \u2014 --name <the suggested_name or a name I gave> to add alongside, or --replace to replace \u2014 keeping any change already made. If that re-run is refused too, relay the refusal to me and stop. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the `allowances[]` rows carry `funds_cover_remaining`, and `false` there is a heads-up to mention to your user, not a refusal.\n- `needs_approval` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the command-modification rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 \u2014 account, funding, budget \u2014 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## If something breaks\n\nIf a Haven result carries `client_update`, that client is out of date: run its `upgrade_command` exactly as given \u2014 a connector command may name your pair with `--name <slug>` \u2014 then any repair line it prints, then retry. `required: true` means payments are refused until you do. What changed: [/releases](/releases).\n\nSomething else wrong? `haven feedback submit \"<text>\"` sends it to Haven \u2014 it needs `haven login` first. Never put a credential in that text: no API key, no delegate key, no session token, no recovery phrase.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
|
|
477
|
+
var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. The budget also ENDS on a date your user chooses \u2014 30, 90 (default), 180 or 365 days out \u2014 and that day is enforced on-chain too. When the budget ends, your authority to pay ends with it; ask your user to renew it (a new approval on the agent's page, or a new `budget grant`). Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, `<packages.cli.one_liner> login --api <api-url>` afterwards gets you a scoped session for steps 3-4 (that flag or `HAVEN_API_URL`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. That command is the manifest's `packages.cli.one_liner` (`/.well-known/haven.json`) \u2014 run it as given: `packages.cli.channel` serves the full spec. Do not hold the process open while you wait: under `--json`, pass `--no-wait` to get the link object back at once, then poll it with `haven login --poll <device_code>` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.\n2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. `/.well-known/haven.json` names `chains.default` as the deployment's expected chain, but after login `haven wallets funding` prints the address, the amount and which chain in one place; confirm that chain before you message your user. Without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain; never assume one: a testnet deployment and production both call themselves Haven. Before you write that message, read the manifest: `environment` says whether this deployment is `production`, and each `chains.supported` entry says whether that chain is a `testnet`. Real money is at stake only on a non-testnet chain of a `production` deployment \u2014 tell your user which case theirs is.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: `haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>` prints the same connector command and approval link; add `--run` to do step 4 too.\n4. **YOU \u2014 run the connector command** in that prompt (see What you run). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Budget changes later (second token, raise, revoke)\n\nThe first budget rides the setup in The sequence. After the agent exists, a CLI session can construct the LATER changes \u2014 a second token, a bigger amount, a recipient pin, a stop \u2014 and hand your user a link; the human still signs, every time:\n\n```\nhaven budget grant <agentId> --amount <n> --token USDC --period <minutes> [--recipient <address>] [--wait]\nhaven budget revoke <agentId> <delegationHash> [--wait]\n```\n\nThe CLI never signs: it prints a dashboard link, your user opens it and signs with their passkey or wallet. `--wait` polls until the human's signature lands. The hash for `revoke` is in `haven budget show <agentId> --hashes`.\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. Before you send anything, let me get you the exact address **and network** \u2014 `haven wallets funding` prints both, and the funding card on `<host>/dashboard` shows the same. Please do not send to an address or a chain I have not confirmed with you; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only three changes to the command above are permitted, and no others: appending --json; re-running it once with --runtime <name> added, only if the connector refuses because it could not determine the agent runtime, naming the harness you are running in and using one of the values that refusal lists; and re-running it once with the one flag I chose added, only after I have answered a wiring_collision relay \u2014 --name <the suggested_name or a name I gave> to add alongside, or --replace to replace \u2014 keeping any change already made. If that re-run is refused too, relay the refusal to me and stop. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the `allowances[]` rows carry `funds_cover_remaining`, and `false` there is a heads-up to mention to your user, not a refusal.\n- `needs_approval` \u2014 no budget can pay. `allowances[]` empty with `budgetEndedAt` set: your budget ENDED on that date; waiting will not help \u2014 ask your user to set a new one in their Haven tab. `allowances[]` empty with `budgetEndedAt` null: nobody approved one yet; ask your user again \u2014 there is no queue. Rows, all spent: it refills at `budgetRefillsAt`; tell your user, then wait or ask them to raise it (`null`: it ends first; ask them to renew it).\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the command-modification rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 \u2014 account, funding, budget \u2014 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## If something breaks\n\nIf a Haven result carries `client_update`, that client is out of date: run its `upgrade_command` exactly as given \u2014 a connector command may name your pair with `--name <slug>` \u2014 then any repair line it prints, then retry. `required: true` means payments are refused until you do. What changed: [/releases](/releases).\n\nSomething else wrong? `haven feedback submit \"<text>\"` sends it to Haven \u2014 it needs `haven login` first. Never put a credential in that text: no API key, no delegate key, no session token, no recovery phrase.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
|
|
475
478
|
|
|
476
479
|
// src/token.ts
|
|
477
480
|
function sessionExpiry(token) {
|
|
@@ -3152,7 +3155,7 @@ async function checkTextForSecrets(text, deps) {
|
|
|
3152
3155
|
|
|
3153
3156
|
// src/commands.ts
|
|
3154
3157
|
var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
|
|
3155
|
-
var CLI_VERSION = "0.
|
|
3158
|
+
var CLI_VERSION = "0.12.0-alpha.0";
|
|
3156
3159
|
var CLI_CLIENT_IDENTITY = `@haven_ai/cli/${CLI_VERSION}`;
|
|
3157
3160
|
function accountAddressOf(s) {
|
|
3158
3161
|
return s.account_address ?? "";
|
|
@@ -3611,6 +3614,11 @@ function budgetSummary(allowances) {
|
|
|
3611
3614
|
if (!allowances || allowances.length === 0) return "\u2014";
|
|
3612
3615
|
return allowances.map((a) => `${a.allowance_amount} ${a.token_symbol}`).join(", ");
|
|
3613
3616
|
}
|
|
3617
|
+
var STILL_ENABLED_STATUSES = /* @__PURE__ */ new Set([
|
|
3618
|
+
"pending",
|
|
3619
|
+
"active",
|
|
3620
|
+
"replaced"
|
|
3621
|
+
]);
|
|
3614
3622
|
var BUDGET_WAIT_TIMEOUT_S = 15 * 60;
|
|
3615
3623
|
var BUDGET_WAIT_INTERVAL_MS = 5e3;
|
|
3616
3624
|
async function cmdBudgetGrant(args, d) {
|
|
@@ -3648,13 +3656,27 @@ async function cmdBudgetGrant(args, d) {
|
|
|
3648
3656
|
}
|
|
3649
3657
|
const amount = parseTokenAmount(args.flags.amount, token.decimals, token.symbol);
|
|
3650
3658
|
if (!amount.ok) throw new UsageError(amount.message);
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3659
|
+
let built;
|
|
3660
|
+
try {
|
|
3661
|
+
built = await api.post(`/agents/${id}/delegations/build`, {
|
|
3662
|
+
token_address: token.address,
|
|
3663
|
+
recipient_address: args.flags.recipient ?? null,
|
|
3664
|
+
budget_atomic: amount.atomic,
|
|
3665
|
+
period_seconds: args.flags.period * 60,
|
|
3666
|
+
...args.flags.expires !== void 0 ? { expires_at: args.flags.expires } : {}
|
|
3667
|
+
});
|
|
3668
|
+
} catch (err) {
|
|
3669
|
+
const body = err instanceof CliApiError ? err.body : void 0;
|
|
3670
|
+
if (body?.error_code === "BUDGET_SLOT_OCCUPIED") {
|
|
3671
|
+
throw new HavenCliError(
|
|
3672
|
+
err instanceof Error ? err.message : "This agent already has a live budget for this token and recipient.",
|
|
3673
|
+
EXIT.refused,
|
|
3674
|
+
"Edit that budget on the agent's page in the Haven dashboard instead: editing replaces it and stops the old one on-chain. Open task and sub-budgets carved from the old budget end when it is stopped.",
|
|
3675
|
+
{ error_code: "BUDGET_SLOT_OCCUPIED", existing_delegation_hash: body.existing_delegation_hash ?? null }
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
throw err;
|
|
3679
|
+
}
|
|
3658
3680
|
const emitGrant = (status) => emit(
|
|
3659
3681
|
d,
|
|
3660
3682
|
args.flags.json,
|
|
@@ -3662,10 +3684,10 @@ async function cmdBudgetGrant(args, d) {
|
|
|
3662
3684
|
() => [
|
|
3663
3685
|
`Budget of ${args.flags.amount} ${token.symbol} per ${args.flags.period} minutes built for agent ${id}.`,
|
|
3664
3686
|
args.flags.recipient ? `Recipient pin: ${args.flags.recipient}` : null,
|
|
3665
|
-
"Open this link and
|
|
3687
|
+
"Open this link and approve \u2014 the budget goes live the moment you do:",
|
|
3666
3688
|
built.signing_url,
|
|
3667
3689
|
`Delegation: ${built.delegation_hash} (version ${built.version})`,
|
|
3668
|
-
status === "active" ? "
|
|
3690
|
+
status === "active" ? "Approved and active." : "Waiting for your approval. Run the same command with --wait to poll until it is active."
|
|
3669
3691
|
].filter((line) => line !== null).join("\n")
|
|
3670
3692
|
);
|
|
3671
3693
|
emitGrant();
|
|
@@ -3675,19 +3697,19 @@ async function cmdBudgetGrant(args, d) {
|
|
|
3675
3697
|
await d.sleep(BUDGET_WAIT_INTERVAL_MS);
|
|
3676
3698
|
const { delegations } = await api.get(`/agents/${id}/delegations`);
|
|
3677
3699
|
const mine = delegations.find((row) => row.delegation_hash === built.delegation_hash);
|
|
3678
|
-
if (mine
|
|
3700
|
+
if (mine && (mine.is_live ?? mine.status === "active")) {
|
|
3679
3701
|
emitGrant("active");
|
|
3680
3702
|
return EXIT.ok;
|
|
3681
3703
|
}
|
|
3682
3704
|
if (mine?.status === "revoked" || mine?.status === "replaced") {
|
|
3683
3705
|
throw new HavenCliError(
|
|
3684
|
-
`Delegation ${built.delegation_hash} is now ${mine.status} without being
|
|
3706
|
+
`Delegation ${built.delegation_hash} is now ${mine.status} without being approved.`,
|
|
3685
3707
|
EXIT.refused
|
|
3686
3708
|
);
|
|
3687
3709
|
}
|
|
3688
3710
|
}
|
|
3689
3711
|
throw new HavenCliError(
|
|
3690
|
-
`Timed out waiting for delegation ${built.delegation_hash} to go active (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The
|
|
3712
|
+
`Timed out waiting for delegation ${built.delegation_hash} to go active (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The approval link stays valid until it expires \u2014 approve it, or rerun with --wait.`,
|
|
3691
3713
|
EXIT.failed
|
|
3692
3714
|
);
|
|
3693
3715
|
}
|
|
@@ -3719,11 +3741,11 @@ async function cmdBudgetRevoke(args, d) {
|
|
|
3719
3741
|
args.flags.json,
|
|
3720
3742
|
{ agent_id: id, delegation_hash: hash, status, ...prepared },
|
|
3721
3743
|
() => [
|
|
3722
|
-
"Revocation prepared \u2014 one
|
|
3723
|
-
"Open this link and
|
|
3744
|
+
"Revocation prepared \u2014 one approval, sponsored (no gas).",
|
|
3745
|
+
"Open this link and approve to stop this budget:",
|
|
3724
3746
|
revokeUrl,
|
|
3725
3747
|
`Delegation: ${hash}`,
|
|
3726
|
-
status === "revoked" ? "Revoked." : "The budget keeps working until you
|
|
3748
|
+
status === "revoked" ? "Revoked." : "The budget keeps working until you approve. Run the same command with --wait to poll until it is revoked."
|
|
3727
3749
|
].filter((line) => line !== null).join("\n")
|
|
3728
3750
|
);
|
|
3729
3751
|
emitRevoke("pending_revoke");
|
|
@@ -3739,7 +3761,7 @@ async function cmdBudgetRevoke(args, d) {
|
|
|
3739
3761
|
}
|
|
3740
3762
|
}
|
|
3741
3763
|
throw new HavenCliError(
|
|
3742
|
-
`Timed out waiting for delegation ${hash} to be revoked (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The link stays valid \u2014
|
|
3764
|
+
`Timed out waiting for delegation ${hash} to be revoked (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The link stays valid \u2014 approve it, or rerun with --wait.`,
|
|
3743
3765
|
EXIT.failed
|
|
3744
3766
|
);
|
|
3745
3767
|
}
|
|
@@ -3779,7 +3801,7 @@ async function cmdAgentRevoke(args, d) {
|
|
|
3779
3801
|
const agent = await api.get(`/agents/${id}`);
|
|
3780
3802
|
if ((agent.live_delegation_count ?? 0) > 0) {
|
|
3781
3803
|
const { delegations } = await api.get(`/agents/${id}/delegations`);
|
|
3782
|
-
live = delegations.filter((r) =>
|
|
3804
|
+
live = delegations.filter((r) => STILL_ENABLED_STATUSES.has(r.status));
|
|
3783
3805
|
}
|
|
3784
3806
|
} catch (err) {
|
|
3785
3807
|
d.o.note(`Couldn't check budgets for agent ${id}, so the revocation was not attempted.`);
|