@visa/cli 5.0.0-rc.363 → 5.0.0-rc.365

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.
@@ -21,27 +21,23 @@ Markdown skill, CLI, and MCP protocol instead.
21
21
 
22
22
  ## One ceremony, two surfaces
23
23
 
24
- Both supported surfaces share the same local pending record and the same server-owned
25
- setup operation:
24
+ Both supported surfaces run the same protected enrollment and share the same local
25
+ pending record:
26
26
 
27
- 1. Visa MCP server: `setup_start`, then `setup_status` until it reports done
28
- (`setup_resume` / `setup_cancel` for the interrupted and abandoned cases).
29
- 2. Raw CLI: `visa setup start "<name>" --rails card,wallet --format json`, then
30
- `visa setup status --format json`.
27
+ 1. Visa MCP server: `agent_enroll`, then `agent_enroll` with `wait: true` once the
28
+ owner has the page open.
29
+ 2. Terminal: `visa connect` (add `--format json` for machine output).
31
30
 
32
- The start operation returns a review URL for the human. The browser receives only that
33
- URL. The runtime retains its private Ed25519 key and local claim material. Polling both
34
- resumes the same pending identity after an interruption and performs the runtime's own
35
- steps — a setup nothing polls is a setup that never finishes.
31
+ Each returns one link and a short code for the human. The browser receives only that
32
+ link. The agent's keys are generated on this device and never leave it, and the
33
+ approval page certifies only the keys this device named in the link. Calling it again
34
+ resumes the same pending enrollment after an interruption.
36
35
 
37
36
  Use `agentId` as the stable identity. `identityKeyJkt` identifies the currently bound
38
- Ed25519 public key and can change after key rotation. Only the rails named in the
39
- operation are configured; email, `.visa` names, and tap bindings remain separate
40
- follow-up configuration.
37
+ Ed25519 public key and can change after key rotation.
41
38
 
42
- The older `pair_agent_start` / `pair_agent_poll` and `enroll_agent` surfaces still exist
43
- for published clients, but they pair identity ALONE and cost the owner one further
44
- approval per rail. Do not use them to connect a new agent.
39
+ The older plugin pairing tools and MCP enrollment tools are gone: they drove CLI
40
+ commands that no longer exist. `agent_enroll` (MCP) and `visa connect` are the one door.
45
41
 
46
42
  ## Install and mount
47
43
 
@@ -98,22 +94,20 @@ For example, with `HOME=/opt/data/home` and no `VISA_CLI_HOME`, identity lives
98
94
  under `/opt/data/home/.visa-cli` and checkout contact under
99
95
  `/opt/data/home/.visa-mcp`. A profile at `/opt/data/.visa-mcp` belongs to a
100
96
  different state namespace. Do not scan, copy, or auto-adopt it: contact files
101
- contain owner PII. Save the profile explicitly through `checkout_profile` in
102
- the running MCP process, or run the CLI command with the exact same environment.
103
- `visa agent preflight --format json` reports the non-secret path and source it
104
- checked.
97
+ contain owner PII. The host passes checkout details to `purchase_create` itself;
98
+ no Visa tool writes the profile.
105
99
 
106
100
  ## Runtime setup map
107
101
 
108
- | Runtime | Setup surface |
109
- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
- | OpenClaw | Install `@visa/cli@rc` (`npm install -g @visa/cli@rc`) and run `visa agent skill --runtime openclaw` (or `visa-cli connect openclaw`). It plants the skill under `~/.openclaw/skills/` and mounts the Visa MCP server into `~/.openclaw/openclaw.json`. (The `@visa/visa-cli-openclaw` plugin package is prerelease). |
111
- | Hermes | Run `visa-cli connect hermes`, install the skill with `visa agent skill --runtime hermes` (it resolves the per-profile dir), and use `setup_start`. `hermes claw migrate` can import an existing OpenClaw setup. |
112
- | Codex | Run `visa agent skill --runtime codex`. It plants the skill under `~/.codex/skills/` and mounts the Visa MCP server into `~/.codex/config.toml` in the same step. |
113
- | Other runtime | Put `visa` on `PATH`, mount the same MCP server if supported, or use the raw `visa setup start` and `visa setup status` commands as the universal fallback. |
102
+ | Runtime | Setup surface |
103
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | OpenClaw | Install `@visa/cli@rc` (`npm install -g @visa/cli@rc`) and run `visa connect openclaw`. It plants the skill under `~/.openclaw/skills/` and mounts the Visa MCP server into `~/.openclaw/openclaw.json`. (The `@visa/visa-cli-openclaw` plugin package is prerelease). |
105
+ | Hermes | Run `visa connect hermes`. It resolves the per-profile skills directory and refuses to guess when several profiles exist — set `HERMES_PROFILE` to choose one. `hermes claw migrate` can import an existing OpenClaw setup. |
106
+ | Codex | Run `visa connect codex`. It plants the skill under `~/.codex/skills/` and mounts the Visa MCP server into `~/.codex/config.toml` in the same step. |
107
+ | Other runtime | Put `visa` on `PATH` and mount the same MCP server by hand; `get_status` shows what is connected. |
114
108
 
115
- The skill itself can be installed with `visa agent skill`. Pass `--runtime <name>` or
116
- `--dir <path>` when auto-detection is not appropriate, then reload the runtime.
109
+ The skill is planted by `visa connect <client>`, into that client's own skills
110
+ directory. Reload the runtime afterwards.
117
111
 
118
112
  ## Optional Hermes wrapper
119
113
 
@@ -6,7 +6,7 @@ allowed-tools: Bash(visa:*) Bash(visa-cli:*) Bash(node:*) Bash(npm:*) Bash(npx:*
6
6
  metadata:
7
7
  author: visa
8
8
  homepage: https://visacli.sh/agents
9
- version: '0.8.0'
9
+ version: '0.9.0'
10
10
  # OpenClaw-namespaced extension (agentskills.io keeps `metadata` free-form, so
11
11
  # non-standard runtime config lives here — `user-invocable` is not a standard
12
12
  # top-level field). OpenClaw auto-installs `install[]` when `requires.bins` are
@@ -59,21 +59,28 @@ Running the same command again resumes an interrupted enrolment; `--restart` rep
59
59
  unclaimed request with a fresh code. `--ceiling <usd>` and `--per-transaction <usd>`
60
60
  propose limits the owner confirms on the approval page.
61
61
 
62
+ ## The tools the Visa MCP server serves
63
+
64
+ The Visa MCP server serves exactly these tools: `get_status`, `agent_enroll`,
65
+ `discover`, `pay`, `history` and `feedback`, plus the card preview's
66
+ `purchase_create`, `purchase_retrieve` and `purchase_cancel` where the build lists
67
+ them. Call no other Visa tool name, whatever an older guide, a memory or another
68
+ agent says: every earlier setup, login, pairing, capability and wallet tool is gone.
69
+ Read the live tool list if in doubt.
70
+
62
71
  ## The older ceremonies were deleted — do not call them
63
72
 
64
- `setup_start`, `setup_status`, `setup_resume`, `setup_cancel`, `setup_agent`,
65
- `enroll_agent`, `agent_connect`, `agent_connect_poll`, `agent_connect_cancel`,
66
- `agent_handoff_claim` and `agent_pairing_cancel` are gone, along with the whole
67
- `visa setup` and `visa agent` command groups — including the retired
68
- `agent pair`, `agent create`, `agent verify`, `agent enroll-claim`,
69
- `agent claim`, `agent pairing-resume`, `agent connect`, `agent handoff-claim`
70
- and every `agent grant-*` verb.
73
+ The whole `visa setup` and `visa agent` command groups are gone too — including the
74
+ retired `agent pair`, `agent create`, `agent verify`, `agent enroll-claim`,
75
+ `agent claim`, `agent pairing-resume`, `agent connect`, `agent handoff-claim` and
76
+ every `agent grant-*` verb.
71
77
 
72
78
  **`visa connect` is the LIVE door and is not on that list.** It is the
73
79
  top-level command, not the retired `visa agent connect` subcommand: one is the
74
80
  one door, the other is a ceremony that no longer exists.
75
81
 
76
- Calling any of them returns one refusal:
82
+ A retired command, or an older build still answering a retired tool name, returns
83
+ one refusal:
77
84
 
78
85
  ```json
79
86
  {
@@ -100,7 +107,7 @@ a new device-held Ed25519 identity key, and the wallet limits the owner approved
100
107
 
101
108
  **Where the `agentId` comes from — read this before you quote one.** Do not invent it and
102
109
  do not read it out of the command's terminal output, which you often cannot see. Read it
103
- from `get_status` or `agent_capabilities` once the enrolment finishes. Never substitute a
110
+ from `get_status` once the enrolment finishes. Never substitute a
104
111
  correlation id, a confirmation code, or an origin for an `agentId`.
105
112
 
106
113
  **Re-running the command does not repair an agent that already exists** — it creates a
@@ -170,9 +177,9 @@ OpenClaw (`~/.openclaw/openclaw.json`):
170
177
 
171
178
  Hermes (`~/.hermes/config.yaml`). **Hermes passes ONLY this `env:` map to the MCP
172
179
  subprocess — it does NOT inherit the gateway environment.** Omitting a required variable
173
- (an RC access code, the right `HOME`, `PATH`) makes the server exit on every start while
174
- `agent_capabilities` — which reads on-disk grant state, not live tool registration — can
175
- still report rails as available. Always set the map explicitly:
180
+ (an RC access code, the right `HOME`, `PATH`) makes the server exit on every start, so no
181
+ Visa tool registers at all, while the terminal — which reads on-disk state with the
182
+ gateway's own environment — can still look healthy. Always set the map explicitly:
176
183
 
177
184
  ```yaml
178
185
  mcp_servers:
@@ -205,26 +212,28 @@ existing OpenClaw installation. See `RUNTIMES.md` for the complete runtime map.
205
212
  ## Sign in first — the wallet is owner-bound
206
213
 
207
214
  The USDC wallet is delegated out of the owner's own wallet, so this runtime needs a live
208
- owner session. Establish it **before** the enrolment command runs:
215
+ owner session. The one door takes care of the order: `agent_enroll` (and `visa connect`)
216
+ signs the owner in first whenever the session is missing or has lapsed, and only then
217
+ starts the enrolment.
209
218
 
210
- - [ ] Call `agent_login` (MCP, default action `"start"`) or run
211
- `visa connect --format json` (CLI). The result carries a sign-in `browserUrl`
212
- and a short 6-character `confirmCode`.
219
+ - [ ] Call `agent_enroll` (MCP) or run `visa connect --format json` (CLI). When a sign-in
220
+ is owed, the result carries a sign-in `browserUrl` and a short 6-character
221
+ `confirmCode`.
213
222
  - [ ] Relay **both** to the human in your reply — the bare URL on its own line, and the
214
223
  code. You are very often not in a terminal they can see; the chat message is the
215
224
  only place these values reach them.
216
225
  - [ ] The human opens the link, signs in (Google or email), and **types the confirmation
217
226
  code into the sign-in page** — into the browser, never back to you in chat.
218
- - [ ] Claim the session: `agent_login {"action":"claim"}` (the CLI command polls on its
219
- own). Once claimed, the session token is stored locally.
227
+ - [ ] Call `agent_enroll` again once they have (the CLI command polls on its own). It
228
+ picks up the approved sign-in on this device and moves on to the enrolment link.
220
229
 
221
- `agent_login` establishes the OWNER's session on this device. It creates no agent and
222
- grants no spending authority — those come from the enrolment command and the approval the
223
- owner gives in their browser.
230
+ Signing in establishes the OWNER's session on this device. It creates no agent and grants
231
+ no spending authority — those come from the enrolment and the approval the owner gives in
232
+ their browser.
224
233
 
225
234
  If something reports `{"code":"session_required"}` or "Not logged in", that is this
226
- ordering rule, not a fault: run `agent_login`, drive the sign-in above to a claimed
227
- session, then continue.
235
+ ordering rule, not a fault: call `agent_enroll` (or run `visa connect`), drive the sign-in
236
+ above, then continue.
228
237
 
229
238
  The account that signs in is the owner the enrolment binds to, and the same account must
230
239
  be signed in on the approval page. A different account there fails closed.
@@ -244,7 +253,7 @@ be signed in on the approval page. A different account there fails closed.
244
253
  - [ ] **Tell them what they are approving**: this device, and the spending limits. One
245
254
  approval covers all of it.
246
255
  - [ ] **Confirm from a tool, not from their word.** Poll `get_status` until
247
- `pairing.paired` is `true`, then read `agent_capabilities` for what is actually live.
256
+ `pairing.paired` is `true`; the same `get_status` says what is actually live.
248
257
 
249
258
  ### Relaying values, and what you must never accept
250
259
 
@@ -277,7 +286,7 @@ On success, report the agent and what is actually live:
277
286
 
278
287
  > Your Visa agent is set up. &lt;Report only the capabilities the live map confirms.&gt;
279
288
 
280
- Read the rails from `agent_capabilities`, never from what was requested.
289
+ Read the rails from `get_status`, never from what was requested.
281
290
 
282
291
  ## Interruption and resume
283
292
 
@@ -323,15 +332,12 @@ _second, separate_ agent. Do this instead:
323
332
  from `get_status`. Start another enrolment only if they
324
333
  explicitly want a second agent.
325
334
  2. **Report status honestly — "set up" is several separate things.** Never imply the agent
326
- can spend just because it exists. Read it live from tools rather than guessing from
327
- prose: `agent_capabilities` returns the DERIVED capability map, `get_status` reports
328
- pairing / account / version, `get_status` answers "can it spend, and
329
- what is missing", and `agent_login` establishes or confirms the account session.
335
+ can spend just because it exists. Read it live from `get_status` rather than guessing
336
+ from prose: it reports pairing, account and version, and answers "can it spend, and
337
+ what is missing".
330
338
  - **Identity** — bound to _this user's_ account, on _this device_.
331
339
  - **Spending** — the limits the owner approved in the browser. You **cannot** self-grant
332
- either rail, and never self-mint a wallet with `wallet_init` on mainnet — it throws
333
- `WalletCredentialRequiredError` until the owner's delegation lands.
334
- - **Mesh (`.visa` messaging)** — separate; `visa register <name>` joins it.
340
+ either rail or self-mint a wallet: the wallet arrives only by the owner's delegation.
335
341
  - **Trusted (TAP)** — follows spend/provisioning; don't promise it before then.
336
342
  3. **Scope everything to the user.** The identity is bound to the account they signed in
337
343
  with; the wallet and limits are theirs. Speak in terms of "your agent / your account /
@@ -340,8 +346,8 @@ _second, separate_ agent. Do this instead:
340
346
  ## An existing agent is missing a capability
341
347
 
342
348
  There is no rail-add ceremony any more: `visa agent grant-card` / `grant-wallet` /
343
- `grant-activate` / `grant-claim` and the `agent_connect` tools were deleted, and calling
344
- one returns `legacy_door_removed`.
349
+ `grant-activate` / `grant-claim` were deleted, and calling one returns
350
+ `legacy_door_removed`.
345
351
 
346
352
  What to do instead:
347
353
 
@@ -362,17 +368,13 @@ yourself.
362
368
 
363
369
  ## Spending, once a rail is live
364
370
 
365
- Wallet: set the policy with `wallet_policy_set` (per-transaction / daily / session USD
366
- caps plus optional network and merchant allow/deny lists that refuse an x402 payment
367
- BEFORE it is signed), then `wallet_pay`. The served wallet tools are `wallet_discover`
368
- (search the public x402 Bazaar), `wallet_probe` (read a challenge without paying),
369
- `wallet_pay` / `wallet_directory_pay` (pay, policy-enforced), `wallet_history` /
370
- `wallet_reconcile` (local ledger + resolve `reconciling` holds), `wallet_fund` (funding
371
- address + faucet), and `wallet_export` (export key material — dangerous). All spending is
372
- gated by the owner-approved local policy caps.
371
+ Wallet: `discover` finds what to buy (a query, or a listing id to read one without
372
+ paying), `pay` pays it with a mandatory `max` ceiling, and `history` reads this agent's
373
+ payments. The limits are the ones the owner approved; the owner changes them in the Visa
374
+ Console, never you. Funding is the owner's, in the Console.
373
375
 
374
376
  Card: Visa-operated browser checkout is retired; another enrollment will not repair it.
375
- `start_card_mandate` and `pay_merchant` refuse with `card_browser_checkout_removed`.
377
+ The retired browser-checkout tools refuse with `card_browser_checkout_removed`.
376
378
  On local builds that list `purchase_create`,
377
379
  `purchase_retrieve` and `purchase_cancel`, the protected card preview uses the
378
380
  host's checkout tools and the existing eligible card and agent identity. Follow
@@ -396,32 +398,14 @@ messaging authority. Do not attempt mesh registration or improvise another trans
396
398
 
397
399
  ## Optional agent mailbox (separate from setup)
398
400
 
399
- Connecting an agent does not provision an email address or inbox. If the agent needs a
400
- mailbox — e.g. to receive a merchant's account-signup or one-time-code email —
401
- connect one explicitly, from the connected runtime, with the MCP tool:
402
-
403
- ```
404
- agent_mail_connect { "agentId": "<agentId>" }
405
- ```
406
-
407
- There is no terminal command for this. It requires an
408
- already-connected stable-agent identity on this runtime — it reads the local agent
409
- record and proves the Ed25519 identity to the service. It issues the stable
410
- agent mailbox if one does not exist, then stores an inbox-scoped credential in an
411
- owner-only `0600` runtime file so this runtime can read that one inbox.
412
-
413
- Be honest about scope. A mailbox grants an email address and the ability to read
414
- that inbox — nothing more. It is **not** identity, a wallet, spend authority, a
415
- card, or a `.visa` name, and it never authorizes a payment. Do not claim setup
416
- set up mail. Once connected, the MCP `wallet_mail_status`, `wallet_mail_read`,
417
- and `wallet_mail_await_otp` tools read the inbox (e.g. to await a sender-checked
418
- one-time code); without the scoped credential those reads fail closed. Keep the
419
- org-wide AgentMail key off the runtime — provisioning happens only through
420
- `mail-connect` under operator control.
401
+ Connecting an agent does not provision an email address or inbox, and no Visa tool
402
+ connects or reads one. There is no terminal command for this either. Do not claim setup
403
+ set up mail, and do not improvise a mailbox. A mailbox would grant an email address and
404
+ nothing more: it never authorizes a payment.
421
405
 
422
406
  ## Optional checkout profile (separate from setup)
423
407
 
424
- Do not collect a checkout profile to repair `pay_merchant`: Visa browser checkout
408
+ Do not collect a checkout profile to revive Visa browser checkout: it
425
409
  is retired. On local builds that list `purchase_create`, `purchase_retrieve` and
426
410
  `purchase_cancel`, the protected card preview reuses this identity and verified
427
411
  card. The host supplies checkout and billing details, owns browser submission,
@@ -433,12 +417,11 @@ chat, bypass eligibility, or retry an uncertain merchant submission. Existing
433
417
  `~/.visa-mcp/contact.json` files use the format below; preserve them with mode `0600`.
434
418
  Never infer or invent identity or address data.
435
419
 
436
- Create and inspect this profile through the `checkout_profile` MCP tool whenever the
437
- payment flow runs through MCP. Do not shell a terminal command as a substitute unless the
438
- shell has the exact same `HOME` and `VISA_CLI_HOME` as the MCP subprocess. A profile
439
- found under another root is owner PII, not a migration candidate: never scan, copy, or
440
- auto-adopt it. If the roots drifted, keep the root holding the paired identity and have the
441
- owner save the profile again through `checkout_profile` in that runtime.
420
+ No Visa tool writes this profile; the host passes checkout and billing details to
421
+ `purchase_create` itself. Any shell that reads an existing profile must have the exact
422
+ same `HOME` and `VISA_CLI_HOME` as the MCP subprocess. A profile found under another root
423
+ is owner PII, not a migration candidate: never scan, copy, or auto-adopt it. If the roots
424
+ drifted, keep the root holding the paired identity.
442
425
 
443
426
  ```jsonc
444
427
  {
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
3
- import { join } from 'node:path'
2
+ import { mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
4
5
  import { tmpdir } from 'node:os'
5
6
  import { randomUUID } from 'node:crypto'
6
7
 
@@ -55,7 +56,7 @@ describe('setup.mjs portable provisioner', () => {
55
56
 
56
57
  it('accepts exact matching or newer RC versions', () => {
57
58
  expect(isSemverCompatible('4.1.0-rc.159', '4.1.0-rc.159')).toBe(true)
58
- expect(isSemverCompatible('4.1.0-rc.166', '4.1.0-rc.159')).toBe(true)
59
+ expect(isSemverCompatible('5.0.0-rc.360', '4.1.0-rc.159')).toBe(true)
59
60
  expect(isSemverCompatible('4.1.0-rc.200', '4.1.0-rc.159')).toBe(true)
60
61
  })
61
62
 
@@ -66,10 +67,18 @@ describe('setup.mjs portable provisioner', () => {
66
67
  })
67
68
  })
68
69
 
70
+ describe('default floor', () => {
71
+ it('refuses a 4.x CLI that still served the retired pairing tools', () => {
72
+ expect(isSemverCompatible('4.1.0-rc.166')).toBe(false)
73
+ expect(isSemverCompatible('5.0.0-rc.334')).toBe(false)
74
+ expect(isSemverCompatible('5.0.0-rc.335')).toBe(true)
75
+ })
76
+ })
77
+
69
78
  describe('findCliBinary', () => {
70
79
  it('returns visa when visa --version succeeds', () => {
71
80
  const mockExec = vi.fn((cmd) => {
72
- if (cmd === 'visa') return 'visa 4.1.0-rc.166'
81
+ if (cmd === 'visa') return 'visa 5.0.0-rc.360'
73
82
  throw new Error('command not found')
74
83
  })
75
84
  expect(findCliBinary(mockExec)).toBe('visa')
@@ -77,7 +86,7 @@ describe('setup.mjs portable provisioner', () => {
77
86
 
78
87
  it('returns visa-cli when only visa-cli succeeds', () => {
79
88
  const mockExec = vi.fn((cmd) => {
80
- if (cmd === 'visa-cli') return 'visa-cli 4.1.0-rc.166'
89
+ if (cmd === 'visa-cli') return 'visa-cli 5.0.0-rc.360'
81
90
  throw new Error('command not found')
82
91
  })
83
92
  expect(findCliBinary(mockExec)).toBe('visa-cli')
@@ -96,21 +105,21 @@ describe('setup.mjs portable provisioner', () => {
96
105
  const mockExec = vi.fn((cmd, args) => {
97
106
  if (args.includes('capabilities')) {
98
107
  return JSON.stringify({
99
- data: { build: { version: '4.1.0-rc.166' } },
108
+ data: { build: { version: '5.0.0-rc.360' } },
100
109
  })
101
110
  }
102
- return '4.1.0-rc.166'
111
+ return '5.0.0-rc.360'
103
112
  })
104
- expect(detectCliVersion('visa', mockExec)).toBe('4.1.0-rc.166')
113
+ expect(detectCliVersion('visa', mockExec)).toBe('5.0.0-rc.360')
105
114
  })
106
115
 
107
116
  it('falls back to --version when capabilities fails', () => {
108
117
  const mockExec = vi.fn((cmd, args) => {
109
118
  if (args.includes('capabilities')) throw new Error('unknown command')
110
- if (args.includes('--version')) return 'v4.1.0-rc.166\n'
119
+ if (args.includes('--version')) return 'v5.0.0-rc.360\n'
111
120
  throw new Error('unknown')
112
121
  })
113
- expect(detectCliVersion('visa', mockExec)).toBe('4.1.0-rc.166')
122
+ expect(detectCliVersion('visa', mockExec)).toBe('5.0.0-rc.360')
114
123
  })
115
124
  })
116
125
 
@@ -120,7 +129,7 @@ describe('setup.mjs portable provisioner', () => {
120
129
  writeFileSync(
121
130
  bundlePath,
122
131
  `
123
- const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll', 'setup_start'];
132
+ const tools = ['get_status', 'agent_enroll', 'discover', 'pay', 'history', 'feedback'];
124
133
  `
125
134
  )
126
135
  const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
@@ -128,7 +137,7 @@ describe('setup.mjs portable provisioner', () => {
128
137
  expect(probe.missingTools).toEqual([])
129
138
  })
130
139
 
131
- it('returns missing tools when bundle lacks handoff tools (e.g. 4.0.1 bundle)', () => {
140
+ it('returns missing tools when the bundle predates the six served tools (e.g. 4.0.1)', () => {
132
141
  const bundlePath = join(tempDir, 'index.js')
133
142
  writeFileSync(
134
143
  bundlePath,
@@ -138,11 +147,7 @@ describe('setup.mjs portable provisioner', () => {
138
147
  )
139
148
  const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
140
149
  expect(probe.ok).toBe(false)
141
- expect(probe.missingTools).toEqual([
142
- 'agent_handoff_claim',
143
- 'agent_connect',
144
- 'agent_connect_poll',
145
- ])
150
+ expect(probe.missingTools).toEqual(['get_status', 'agent_enroll'])
146
151
  })
147
152
 
148
153
  it('returns false when bundle file is missing', () => {
@@ -151,13 +156,27 @@ describe('setup.mjs portable provisioner', () => {
151
156
  expect(probe.missingTools).toEqual([...REQUIRED_MCP_TOOLS])
152
157
  })
153
158
 
154
- it('does not treat agent_connect_poll as a match for agent_connect', () => {
159
+ it('does not treat a longer tool name as a match for agent_enroll', () => {
155
160
  const bundlePath = join(tempDir, 'index.js')
156
- writeFileSync(bundlePath, `const tools = ['agent_handoff_claim', 'agent_connect_poll'];`)
161
+ writeFileSync(bundlePath, `const tools = ['get_status', 'agent_enroll_claim'];`)
157
162
  const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
158
163
  expect(probe.ok).toBe(false)
159
- expect(probe.missingTools).toContain('agent_connect')
160
- expect(probe.missingTools).not.toContain('agent_connect_poll')
164
+ expect(probe.missingTools).toEqual(['agent_enroll'])
165
+ })
166
+
167
+ it('requires only served tools: every required tool is one of the six', () => {
168
+ const served = readFileSync(
169
+ join(
170
+ dirname(fileURLToPath(import.meta.url)),
171
+ '../../../../../cli/src/mcp-server/six-tools.ts'
172
+ ),
173
+ 'utf8'
174
+ )
175
+ const block = served.match(/SIX_TOOL_NAMES = \[([^\]]*)\]/)
176
+ expect(block).not.toBeNull()
177
+ const six = [...block[1].matchAll(/'([a-z_]+)'/g)].map((m) => m[1])
178
+ expect(six).toHaveLength(6)
179
+ for (const tool of REQUIRED_MCP_TOOLS) expect(six).toContain(tool)
161
180
  })
162
181
  })
163
182
 
@@ -189,18 +208,18 @@ describe('setup.mjs portable provisioner', () => {
189
208
  expect(result.version).toBe('4.0.1')
190
209
  })
191
210
 
192
- it('reports ready when version is >= 4.1.0-rc.159 and required tools exist', () => {
211
+ it('reports ready when version is >= 5.0.0-rc.335 and required tools exist', () => {
193
212
  const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
194
213
  mkdirSync(bundleDir, { recursive: true })
195
214
  writeFileSync(
196
215
  join(bundleDir, 'index.js'),
197
- `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
216
+ `const tools = ['get_status', 'agent_enroll', 'discover', 'pay', 'history', 'feedback'];`
198
217
  )
199
218
 
200
219
  const mockExec = vi.fn((cmd, args) => {
201
- if (args.includes('--version')) return '4.1.0-rc.166'
220
+ if (args.includes('--version')) return '5.0.0-rc.360'
202
221
  if (args.includes('capabilities')) {
203
- return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
222
+ return JSON.stringify({ data: { build: { version: '5.0.0-rc.360' } } })
204
223
  }
205
224
  throw new Error('unknown')
206
225
  })
@@ -208,15 +227,15 @@ describe('setup.mjs portable provisioner', () => {
208
227
 
209
228
  const result = verifyCliReadiness(mockExec, mockNpmRoot)
210
229
  expect(result.ready).toBe(true)
211
- expect(result.version).toBe('4.1.0-rc.166')
230
+ expect(result.version).toBe('5.0.0-rc.360')
212
231
  expect(result.missingTools).toEqual([])
213
232
  })
214
233
 
215
234
  it('reports stale_or_incomplete when version is compatible but the MCP bundle is missing', () => {
216
235
  const mockExec = vi.fn((cmd, args) => {
217
- if (args.includes('--version')) return '4.1.0-rc.166'
236
+ if (args.includes('--version')) return '5.0.0-rc.360'
218
237
  if (args.includes('capabilities')) {
219
- return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
238
+ return JSON.stringify({ data: { build: { version: '5.0.0-rc.360' } } })
220
239
  }
221
240
  throw new Error('unknown')
222
241
  })
@@ -225,7 +244,7 @@ describe('setup.mjs portable provisioner', () => {
225
244
  const result = verifyCliReadiness(mockExec, mockNpmRoot, () => null)
226
245
  expect(result.ready).toBe(false)
227
246
  expect(result.reason).toBe('stale_or_incomplete')
228
- expect(result.version).toBe('4.1.0-rc.166')
247
+ expect(result.version).toBe('5.0.0-rc.360')
229
248
  expect(result.missingTools).toEqual([...REQUIRED_MCP_TOOLS])
230
249
  })
231
250
  })
@@ -241,15 +260,15 @@ describe('setup.mjs portable provisioner', () => {
241
260
  mkdirSync(bundleDir, { recursive: true })
242
261
  writeFileSync(
243
262
  join(bundleDir, 'index.js'),
244
- `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
263
+ `const tools = ['get_status', 'agent_enroll', 'discover', 'pay', 'history', 'feedback'];`
245
264
  )
246
265
  return ''
247
266
  }
248
267
  if (cmd === 'visa') {
249
268
  if (!installed) throw new Error('command not found')
250
- if (args.includes('--version')) return '4.1.0-rc.166'
269
+ if (args.includes('--version')) return '5.0.0-rc.360'
251
270
  if (args.includes('capabilities')) {
252
- return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
271
+ return JSON.stringify({ data: { build: { version: '5.0.0-rc.360' } } })
253
272
  }
254
273
  }
255
274
  throw new Error(`unknown command ${cmd}`)
@@ -269,7 +288,7 @@ describe('setup.mjs portable provisioner', () => {
269
288
  expect.any(Object)
270
289
  )
271
290
  expect(result.status).toBe('installed')
272
- expect(result.version).toBe('4.1.0-rc.166')
291
+ expect(result.version).toBe('5.0.0-rc.360')
273
292
  })
274
293
 
275
294
  it('scenario 2: stale CLI installed (4.0.1) -> upgrades to @visa/cli@rc and succeeds', async () => {
@@ -283,12 +302,12 @@ describe('setup.mjs portable provisioner', () => {
283
302
  upgraded = true
284
303
  writeFileSync(
285
304
  join(bundleDir, 'index.js'),
286
- `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
305
+ `const tools = ['get_status', 'agent_enroll', 'discover', 'pay', 'history', 'feedback'];`
287
306
  )
288
307
  return ''
289
308
  }
290
309
  if (cmd === 'visa') {
291
- const version = upgraded ? '4.1.0-rc.166' : '4.0.1'
310
+ const version = upgraded ? '5.0.0-rc.360' : '4.0.1'
292
311
  if (args.includes('--version')) return version
293
312
  if (args.includes('capabilities')) {
294
313
  return JSON.stringify({ data: { build: { version } } })
@@ -311,7 +330,7 @@ describe('setup.mjs portable provisioner', () => {
311
330
  expect.any(Object)
312
331
  )
313
332
  expect(result.status).toBe('upgraded')
314
- expect(result.version).toBe('4.1.0-rc.166')
333
+ expect(result.version).toBe('5.0.0-rc.360')
315
334
  })
316
335
 
317
336
  it('scenario 3: current CLI installed -> idempotent no-op', async () => {
@@ -319,14 +338,14 @@ describe('setup.mjs portable provisioner', () => {
319
338
  mkdirSync(bundleDir, { recursive: true })
320
339
  writeFileSync(
321
340
  join(bundleDir, 'index.js'),
322
- `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
341
+ `const tools = ['get_status', 'agent_enroll', 'discover', 'pay', 'history', 'feedback'];`
323
342
  )
324
343
 
325
344
  const mockExec = vi.fn((cmd, args) => {
326
345
  if (cmd === 'visa') {
327
- if (args.includes('--version')) return '4.1.0-rc.166'
346
+ if (args.includes('--version')) return '5.0.0-rc.360'
328
347
  if (args.includes('capabilities')) {
329
- return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
348
+ return JSON.stringify({ data: { build: { version: '5.0.0-rc.360' } } })
330
349
  }
331
350
  }
332
351
  throw new Error(`unexpected execution ${cmd}`)
@@ -342,7 +361,7 @@ describe('setup.mjs portable provisioner', () => {
342
361
 
343
362
  expect(mockExec).not.toHaveBeenCalledWith('npm', expect.anything(), expect.anything())
344
363
  expect(result.status).toBe('already_ready')
345
- expect(result.version).toBe('4.1.0-rc.166')
364
+ expect(result.version).toBe('5.0.0-rc.360')
346
365
  })
347
366
 
348
367
  it('scenario 4: install failure reports one explicit upgrade action and exits 1', async () => {
@@ -375,9 +394,9 @@ describe('setup.mjs portable provisioner', () => {
375
394
  const mockExec = vi.fn((cmd, args) => {
376
395
  if (cmd === 'npm') return ''
377
396
  if (cmd === 'visa') {
378
- if (args.includes('--version')) return '4.1.0-rc.166'
397
+ if (args.includes('--version')) return '5.0.0-rc.360'
379
398
  if (args.includes('capabilities')) {
380
- return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
399
+ return JSON.stringify({ data: { build: { version: '5.0.0-rc.360' } } })
381
400
  }
382
401
  }
383
402
  throw new Error(`unknown command ${cmd}`)
@@ -395,7 +414,7 @@ describe('setup.mjs portable provisioner', () => {
395
414
 
396
415
  expect(errorSpy).toHaveBeenCalledWith(
397
416
  expect.stringContaining(
398
- 'Readiness check failed: installed CLI v4.1.0-rc.166 is missing required tools'
417
+ 'Readiness check failed: installed CLI v5.0.0-rc.360 is missing required tools'
399
418
  )
400
419
  )
401
420
  expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('npm install -g @visa/cli@rc'))