@haven_ai/cli 0.0.0-dev.202609071016.16aa950 → 0.0.0-dev.202609071335.b2167b0
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 +28 -6
- package/dist/cli.cjs +96 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +96 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +96 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +96 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ checks a command against. `haven --help` prints the same list.
|
|
|
94
94
|
# auth
|
|
95
95
|
haven login # browser device-code approval (the default)
|
|
96
96
|
haven login --email you@example.com # password path instead (prompt or HAVEN_PASSWORD)
|
|
97
|
+
haven login --no-wait --json # print the link object and exit — resume with --poll
|
|
98
|
+
haven login --poll <device_code> # one poll round: 0 approved, 3 pending, 4 denied
|
|
97
99
|
haven whoami # user, session expiry, API URL
|
|
98
100
|
haven guide # the agent onboarding runbook
|
|
99
101
|
haven logout
|
|
@@ -105,6 +107,7 @@ haven wallets funding [--safe <id|address>] [--wait] # the paste-ready funding
|
|
|
105
107
|
haven agents list
|
|
106
108
|
haven agents show <id>
|
|
107
109
|
haven budget show <agentId>
|
|
110
|
+
haven budget show <agentId> --hashes
|
|
108
111
|
haven activity list [--safe <id|address>] [--agent <id>] [--direction in|out] [--limit <n>] [--offset <n>]
|
|
109
112
|
haven activity export [same filters] > activity.csv
|
|
110
113
|
haven activity export --format sie [--from <ISO>] [--to <ISO>] [--company <name>] > books.si
|
|
@@ -149,7 +152,9 @@ never calls `activate` — that is the whole design.
|
|
|
149
152
|
open budget); `--expires` takes unix seconds (default: 90 days).
|
|
150
153
|
- `budget revoke <agentId> <delegationHash>` prepares the sponsored
|
|
151
154
|
revocation (no gas, one signature). The hash comes from
|
|
152
|
-
`haven
|
|
155
|
+
`haven budget show <agentId> --hashes` or the dashboard. (`agents show` does
|
|
156
|
+
NOT print hashes — it renders the allowances projection, which has no hash
|
|
157
|
+
field. #2612.)
|
|
153
158
|
- `--wait` on either command polls until the human's signature lands (grant:
|
|
154
159
|
the hash turns `active`; revoke: the row turns `revoked`), 5 s interval, 15
|
|
155
160
|
minute ceiling. The build is idempotent for the same parameters while it is
|
|
@@ -235,16 +240,33 @@ driving this CLI must never hold its user's password.
|
|
|
235
240
|
```bash
|
|
236
241
|
haven login --json
|
|
237
242
|
# {"ok":true,"verification_url":"https://app.haven…/device?code=ABCD-2345",
|
|
238
|
-
# "user_code":"ABCD-2345","expires_at":"…"}
|
|
243
|
+
# "user_code":"ABCD-2345","device_code":"…","expires_at":"…"}
|
|
239
244
|
```
|
|
240
245
|
|
|
241
246
|
Under `--json` that object is printed **before** polling begins, so an agent
|
|
242
247
|
can hand its user the link immediately rather than after the flow completes.
|
|
243
|
-
|
|
244
|
-
|
|
248
|
+
Without `--no-wait` the CLI then polls until approved, widening the interval
|
|
249
|
+
when the server says `slow_down`.
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
**Non-blocking (for agents).** Under `--json` the wait is capped at **30
|
|
252
|
+
seconds**: on timeout the CLI emits
|
|
253
|
+
`{ "status": "pending", "device_code": "…", "retry_after": 5 }` and exits
|
|
254
|
+
**3** — the flow is still alive, poll again. `--no-wait` skips even that wait
|
|
255
|
+
and returns the link object at once. Either way, finish the flow with one
|
|
256
|
+
poll round per invocation, so nothing holds your turn open:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
haven login --api <api-url> --json --no-wait
|
|
260
|
+
# { "ok": true, "verification_url": "…", "user_code": "ABCD-2345",
|
|
261
|
+
# "device_code": "…", "expires_at": "…" } <- hand your user the link
|
|
262
|
+
haven login --poll <device_code> # repeat until it stops saying pending
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Exit codes carry the outcome an agent acts on: **0** once approved (the same
|
|
266
|
+
success object as the blocking path; the session is saved), **3** while still
|
|
267
|
+
pending — the object carries `retry_after`, widened when the server says
|
|
268
|
+
`slow_down` — and on an expired code, which means start over with a fresh
|
|
269
|
+
`login`. **4** when the human denied it (stop asking).
|
|
248
270
|
|
|
249
271
|
`haven login --email <address>` keeps the password path for a human who wants
|
|
250
272
|
it. It is not removed — it is simply no longer what an agent gets by asking to
|
package/dist/cli.cjs
CHANGED
|
@@ -27,7 +27,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
27
27
|
"--status",
|
|
28
28
|
"--amount",
|
|
29
29
|
"--recipient",
|
|
30
|
-
"--expires"
|
|
30
|
+
"--expires",
|
|
31
|
+
"--poll"
|
|
31
32
|
]);
|
|
32
33
|
function parseArgs(argv) {
|
|
33
34
|
const positionals = [];
|
|
@@ -38,7 +39,8 @@ function parseArgs(argv) {
|
|
|
38
39
|
yes: false,
|
|
39
40
|
noWait: false,
|
|
40
41
|
run: false,
|
|
41
|
-
wait: false
|
|
42
|
+
wait: false,
|
|
43
|
+
hashes: false
|
|
42
44
|
};
|
|
43
45
|
for (let i = 0; i < argv.length; i += 1) {
|
|
44
46
|
const arg = argv[i];
|
|
@@ -49,6 +51,7 @@ function parseArgs(argv) {
|
|
|
49
51
|
else if (arg === "--no-wait") flags.noWait = true;
|
|
50
52
|
else if (arg === "--run") flags.run = true;
|
|
51
53
|
else if (arg === "--wait") flags.wait = true;
|
|
54
|
+
else if (arg === "--hashes") flags.hashes = true;
|
|
52
55
|
else if (VALUE_FLAGS.has(arg)) {
|
|
53
56
|
const value = argv[++i];
|
|
54
57
|
if (value === void 0 || value.startsWith("--")) {
|
|
@@ -88,6 +91,8 @@ function parseArgs(argv) {
|
|
|
88
91
|
} else if (arg === "--format") {
|
|
89
92
|
if (value !== "csv" && value !== "sie") throw new Error('--format must be "csv" or "sie"');
|
|
90
93
|
flags.format = value;
|
|
94
|
+
} else if (arg === "--poll") {
|
|
95
|
+
flags.poll = value;
|
|
91
96
|
} else if (arg === "--from") flags.from = value;
|
|
92
97
|
else if (arg === "--to") flags.to = value;
|
|
93
98
|
else if (arg === "--company") flags.company = value;
|
|
@@ -110,6 +115,11 @@ function helpText() {
|
|
|
110
115
|
" login Sign in. Opens a browser device-code approval by default \u2014",
|
|
111
116
|
" it prints a code and a link, and never asks for a password",
|
|
112
117
|
" login --email <e> Password path instead (prompt, or HAVEN_PASSWORD)",
|
|
118
|
+
" login --no-wait Print the link and exit instead of polling. Under --json,",
|
|
119
|
+
" pass it: you get the object back at once (it carries",
|
|
120
|
+
" device_code) and finish with `login --poll <device_code>`",
|
|
121
|
+
" login --poll <code> One poll round of a started device flow: exit 0 approved,",
|
|
122
|
+
" 3 still pending (the object carries retry_after), 4 denied",
|
|
113
123
|
" logout Clear the saved session",
|
|
114
124
|
" whoami Show the signed-in user, session expiry and API URL",
|
|
115
125
|
"",
|
|
@@ -129,6 +139,8 @@ function helpText() {
|
|
|
129
139
|
" agents list List your agents",
|
|
130
140
|
" agents show <id> Show one agent + its budget",
|
|
131
141
|
" budget show <agentId> Show an agent's configured budget",
|
|
142
|
+
" budget show <agentId> --hashes Its delegation hashes \u2014 the second",
|
|
143
|
+
" argument `budget revoke` takes, printed nowhere else",
|
|
132
144
|
" activity list [--safe <id|address>] [--agent <id>] [--direction in|out] [--limit <n>] [--offset <n>]",
|
|
133
145
|
" activity export [filters] Emit CSV to stdout (--format csv, default)",
|
|
134
146
|
" activity export --format sie [--from <ISO>] [--to <ISO>] [--company <name>]",
|
|
@@ -370,7 +382,12 @@ function toFailure(err) {
|
|
|
370
382
|
code: "not_authenticated",
|
|
371
383
|
exit: EXIT.notAuthenticated,
|
|
372
384
|
message: err.message,
|
|
373
|
-
|
|
385
|
+
// #2618: the device flow is named FIRST because it is the path an
|
|
386
|
+
// agent can actually run — an agent never holds its user's password,
|
|
387
|
+
// which is the runbook's first rule; the previous hint led with the
|
|
388
|
+
// credentials as if they were the normal route. HAVEN_EMAIL and
|
|
389
|
+
// HAVEN_PASSWORD remain for a HUMAN scripting non-interactively.
|
|
390
|
+
hint: "Run `haven login` (device flow \u2014 it never asks for a password; under --json pass --no-wait, then resume with `login --poll <device_code>`). HAVEN_EMAIL and HAVEN_PASSWORD are the human, non-interactive path."
|
|
374
391
|
};
|
|
375
392
|
}
|
|
376
393
|
if (err.status >= 400 && err.status < 500) {
|
|
@@ -412,7 +429,7 @@ ${failure.hint}` : failure.message);
|
|
|
412
429
|
}
|
|
413
430
|
|
|
414
431
|
// src/agent-guidance-text.ts
|
|
415
|
-
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, `npx @haven_ai/cli 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. 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. `haven wallets funding` prints the address, the amount **and which chain** in one place; without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain. Read the chain off whichever you used and put it in your message \u2014 never assume one: a testnet deployment and production both call themselves Haven.\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 (below). 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 above. 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 agents show <agentId>`.\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 two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. 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 can pay.\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 two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 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, as above. 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## 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";
|
|
432
|
+
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, `npx @haven_ai/cli 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. 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. `haven wallets funding` prints the address, the amount **and which chain** in one place; without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain. Read the chain off whichever you used and put it in your message \u2014 never assume one: a testnet deployment and production both call themselves Haven.\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 (below). 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 above. 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 agents show <agentId>`.\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 two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. 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 can pay.\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 two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 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, as above. 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## 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";
|
|
416
433
|
|
|
417
434
|
// src/token.ts
|
|
418
435
|
function sessionExpiry(token) {
|
|
@@ -587,7 +604,7 @@ async function runConnector(connectorCommand, spawner, onStderr) {
|
|
|
587
604
|
|
|
588
605
|
// src/commands.ts
|
|
589
606
|
var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
|
|
590
|
-
var CLI_VERSION = "0.0.0-dev.
|
|
607
|
+
var CLI_VERSION = "0.0.0-dev.202609071335.b2167b0";
|
|
591
608
|
async function run(argv, deps = {}) {
|
|
592
609
|
const out = deps.out ?? ((l) => process.stdout.write(`${l}
|
|
593
610
|
`));
|
|
@@ -713,6 +730,7 @@ async function deviceLogin(args, d, baseUrl) {
|
|
|
713
730
|
ok: true,
|
|
714
731
|
verification_url: start.verification_url,
|
|
715
732
|
user_code: start.user_code,
|
|
733
|
+
device_code: start.device_code,
|
|
716
734
|
expires_at: new Date(deadline).toISOString()
|
|
717
735
|
},
|
|
718
736
|
() => `Open ${start.verification_url}
|
|
@@ -720,11 +738,20 @@ and approve the code ${start.user_code}.
|
|
|
720
738
|
It expires in ${Math.round(start.expires_in / 60)} minutes.`
|
|
721
739
|
);
|
|
722
740
|
if (args.flags.noWait) return EXIT.ok;
|
|
741
|
+
const jsonWaitMs = args.flags.json ? 3e4 : start.expires_in * 1e3;
|
|
742
|
+
const waitUntil = Date.now() + jsonWaitMs;
|
|
723
743
|
let interval = start.interval * 1e3;
|
|
724
744
|
for (; ; ) {
|
|
725
745
|
if (Date.now() >= deadline) {
|
|
726
746
|
throw new HavenCliError("The code expired before it was approved.", EXIT.notAuthenticated);
|
|
727
747
|
}
|
|
748
|
+
if (Date.now() >= waitUntil) {
|
|
749
|
+
d.o.data(
|
|
750
|
+
{ status: "pending", device_code: start.device_code, retry_after: Math.round(interval / 1e3) },
|
|
751
|
+
() => "Still waiting for approval \u2014 run the same command again to keep waiting."
|
|
752
|
+
);
|
|
753
|
+
return EXIT.notAuthenticated;
|
|
754
|
+
}
|
|
728
755
|
await d.sleep(interval);
|
|
729
756
|
let res = null;
|
|
730
757
|
try {
|
|
@@ -760,7 +787,55 @@ function deviceErrorCode(err) {
|
|
|
760
787
|
const body = err?.body;
|
|
761
788
|
return typeof body?.error === "string" ? body.error : null;
|
|
762
789
|
}
|
|
790
|
+
var POLL_RETRY_AFTER_SECONDS = 5;
|
|
791
|
+
async function devicePollOnce(args, d, baseUrl, deviceCode) {
|
|
792
|
+
const api = d.makeApi(baseUrl);
|
|
793
|
+
let res = null;
|
|
794
|
+
try {
|
|
795
|
+
res = await api.post("/auth/device/token", {
|
|
796
|
+
device_code: deviceCode
|
|
797
|
+
});
|
|
798
|
+
} catch (err) {
|
|
799
|
+
const code = deviceErrorCode(err);
|
|
800
|
+
if (code === "authorization_pending") {
|
|
801
|
+
d.o.data(
|
|
802
|
+
{ status: "pending", device_code: deviceCode, retry_after: POLL_RETRY_AFTER_SECONDS },
|
|
803
|
+
() => "Not approved yet \u2014 poll again shortly."
|
|
804
|
+
);
|
|
805
|
+
return EXIT.notAuthenticated;
|
|
806
|
+
}
|
|
807
|
+
if (code === "slow_down") {
|
|
808
|
+
d.o.data(
|
|
809
|
+
{
|
|
810
|
+
status: "pending",
|
|
811
|
+
device_code: deviceCode,
|
|
812
|
+
retry_after: POLL_RETRY_AFTER_SECONDS + 5
|
|
813
|
+
},
|
|
814
|
+
() => "Polling too fast \u2014 wait a moment longer, then poll again."
|
|
815
|
+
);
|
|
816
|
+
return EXIT.notAuthenticated;
|
|
817
|
+
}
|
|
818
|
+
if (code === "access_denied") {
|
|
819
|
+
throw new HavenCliError("The request was denied.", EXIT.refused);
|
|
820
|
+
}
|
|
821
|
+
if (code === "expired_token") {
|
|
822
|
+
throw new HavenCliError("The code expired before it was approved.", EXIT.notAuthenticated);
|
|
823
|
+
}
|
|
824
|
+
throw err;
|
|
825
|
+
}
|
|
826
|
+
await d.sessionStore.save({ token: res.token, apiBaseUrl: baseUrl, user: res.user });
|
|
827
|
+
emit(
|
|
828
|
+
d,
|
|
829
|
+
args.flags.json,
|
|
830
|
+
{ ok: true, email: res.user.email, expires_at: sessionExpiry(res.token), user: res.user, apiBaseUrl: baseUrl },
|
|
831
|
+
() => `Signed in as ${res.user.email}.`
|
|
832
|
+
);
|
|
833
|
+
return EXIT.ok;
|
|
834
|
+
}
|
|
763
835
|
async function cmdLogin(args, d) {
|
|
836
|
+
if (args.flags.poll) {
|
|
837
|
+
return devicePollOnce(args, d, baseUrlFor(args, d, null), args.flags.poll);
|
|
838
|
+
}
|
|
764
839
|
const email = args.flags.email ?? d.env.HAVEN_EMAIL;
|
|
765
840
|
if (!email) {
|
|
766
841
|
return deviceLogin(args, d, baseUrlFor(args, d, null));
|
|
@@ -928,10 +1003,24 @@ async function cmdAgentsShow(args, d) {
|
|
|
928
1003
|
);
|
|
929
1004
|
return EXIT.ok;
|
|
930
1005
|
}
|
|
1006
|
+
var HASH_DISCOVERY_HINT = "haven budget show <agentId> --hashes";
|
|
931
1007
|
async function cmdBudgetShow(args, d) {
|
|
932
1008
|
const id = args.positionals[0];
|
|
933
|
-
if (!id) throw new UsageError("Usage: haven budget show <agentId>");
|
|
1009
|
+
if (!id) throw new UsageError("Usage: haven budget show <agentId> [--hashes]");
|
|
934
1010
|
const { api } = await authed(args, d);
|
|
1011
|
+
if (args.flags.hashes) {
|
|
1012
|
+
const { delegations } = await api.get(`/agents/${id}/delegations`);
|
|
1013
|
+
emit(
|
|
1014
|
+
d,
|
|
1015
|
+
args.flags.json,
|
|
1016
|
+
delegations,
|
|
1017
|
+
() => delegations.length === 0 ? `No delegations on agent ${id}.` : table(
|
|
1018
|
+
["DELEGATION HASH", "STATUS", "VERSION"],
|
|
1019
|
+
delegations.map((r) => [r.delegation_hash, r.status, String(r.version)])
|
|
1020
|
+
)
|
|
1021
|
+
);
|
|
1022
|
+
return EXIT.ok;
|
|
1023
|
+
}
|
|
935
1024
|
const agent = await api.get(`/agents/${id}`);
|
|
936
1025
|
const allowances = agent.allowances ?? [];
|
|
937
1026
|
emit(
|
|
@@ -1031,7 +1120,7 @@ async function cmdBudgetRevoke(args, d) {
|
|
|
1031
1120
|
const [id, hash] = args.positionals;
|
|
1032
1121
|
if (!id || !hash) throw new UsageError("Usage: haven budget revoke <agentId> <delegationHash> [--wait]");
|
|
1033
1122
|
if (!/^0x[0-9a-fA-F]{64}$/.test(hash)) {
|
|
1034
|
-
throw new UsageError(
|
|
1123
|
+
throw new UsageError(`The delegation hash must be 0x followed by 64 hex characters \u2014 \`${HASH_DISCOVERY_HINT}\` lists them, or the dashboard.`);
|
|
1035
1124
|
}
|
|
1036
1125
|
const { api } = await authed(args, d);
|
|
1037
1126
|
const { delegations } = await api.get(`/agents/${id}/delegations`);
|