docs: align public copy with the brand voice - no em dashes, AP3X vocabulary, research-themed examples

This commit is contained in:
FilipBl4gojevic
2026-08-01 10:07:33 +02:00
parent c263794879
commit 64adeb635c
4 changed files with 139 additions and 140 deletions

View File

@@ -79,8 +79,8 @@ Self-hosting instructions are below.
## Local signer (non-custodial path)
`packages/signer` is a local MCP server that holds your key and signs transactions locally
instead of handing your mnemonic to a shared host. It has **no network access at all** —
stdio transport, no Provider, no egress — so a key given to it never reaches a shared server
instead of handing your mnemonic to a shared host. It has **no network access at all**:
stdio transport only, no Provider, no egress, so a key given to it never reaches a shared server
or your model provider. Four tools: `vector_signer_get_address`, `vector_signer_decode_transaction`,
`vector_signer_sign`, `vector_signer_get_spend_limits`.
@@ -124,7 +124,7 @@ limitations.
| Tool | Description |
|------|-------------|
| `vector_build_send_apex` | Build an unsigned AP3X payment (keyless — sign with the local signer) |
| `vector_build_send_apex` | Build an unsigned AP3X transfer (keyless - sign with the local signer) |
| `vector_build_send_tokens` | Build an unsigned native-token transfer (keyless) |
| `vector_build_transaction` | Build an unsigned multi-output transaction (keyless, never submits) |
| `vector_dry_run` | Simulate a transaction without submitting - estimate fees and validate |
@@ -267,7 +267,7 @@ If this instance will be reachable by anyone but you, set `MCP_AUTH_TOKENS` firs
| `VECTOR_MCP_SESSION_IDLE_MS` | `/mcp` session idle timeout before the reaper closes it, in ms (min `100`) | `600000` (10 min) |
| `VECTOR_MCP_SESSION_SWEEP_MS` | How often the `/mcp` idle reaper sweeps, in ms (min `100`) | `60000` (1 min) |
| `VECTOR_MCP_MAX_SESSIONS_PER_IDENTITY` | Max concurrent `/mcp` sessions per identity before the oldest is evicted (min `1`) | `32` |
| `MCP_AUTH_TOKENS` | Bearer tokens that may call this server. Comma-separated; each entry is `label:token` or a bare token. **When unset, the server is open to anyone who can reach it.** | _(unset — auth disabled)_ |
| `MCP_AUTH_TOKENS` | Bearer tokens that may call this server. Comma-separated; each entry is `label:token` or a bare token. **When unset, the server is open to anyone who can reach it.** | _(unset - auth disabled)_ |
The four numeric knobs above fail loudly at startup on a malformed value: anything that is not
a plain integer, or is below its listed minimum, raises a startup error naming the variable and
@@ -292,10 +292,10 @@ it, lives in your local signer instead - see `VECTOR_SIGNER_SPEND_LIMIT_PER_TX`
> (`VECTOR_MCP_MAX_SESSIONS_PER_IDENTITY`) - see the Configuration table above for defaults and
> minimums.
> **Malformed values fail loudly, not silently.** The server refuses to start if
> **Malformed values fail loudly.** The server refuses to start if
> `MCP_AUTH_TOKENS` contains an empty token, a token with embedded whitespace, a
> duplicate token, or a value where every comma-separated entry is blank (e.g. a
> stray `,,,`) — each raises a startup error that names the problem but never
> stray `,,,`). Each case raises a startup error that names the problem but never
> echoes a token value. Commas delimit entries and cannot be escaped, so generate
> tokens from a comma-free charset (hex / base64url / alphanumeric).
@@ -339,7 +339,7 @@ it, lives in your local signer instead - see `VECTOR_SIGNER_SPEND_LIMIT_PER_TX`
npm run test:unit
```
No wallet, no network — pure logic only (CBOR encode/decode assertions).
No wallet, no network - pure logic only (CBOR encode/decode assertions).
```bash
npm run test:smoke

View File

@@ -1,4 +1,4 @@
# Vector MCP Server — Tool Reference
# Vector MCP Server - Tool Reference
## Overview
@@ -6,7 +6,7 @@ The Vector MCP Server exposes blockchain tools that AI agents can use to interac
## Prerequisites
1. A Vector wallet address (source of funds) — the tools below are keyless and take no mnemonic; signing happens separately via your local signer
1. A Vector wallet address that holds your AP3X - the tools below are keyless and take no mnemonic; signing happens separately via your local signer
2. Access to Vector network endpoints (Ogmios, submit API, Koios)
3. Node.js >= 14.0.0
@@ -14,10 +14,10 @@ The Vector MCP Server exposes blockchain tools that AI agents can use to interac
### vector_get_balance
Get ADA and token balances for any Vector address.
Get AP3X and token balances for any Vector address.
**Parameters:**
- `address` (string, required) — Vector address to check (addr1...)
- `address` (string, required) - Vector address to check (addr1...)
**Example prompt:** "What's the balance of addr1qx..."
@@ -26,33 +26,33 @@ Get ADA and token balances for any Vector address.
List unspent transaction outputs for an address.
**Parameters:**
- `address` (string, required) — Address to query (addr1...)
- `address` (string, required) - Address to query (addr1...)
**Example prompt:** "Show me the UTxOs at addr1qx..."
### vector_build_send_apex
Build an unsigned transaction sending AP3X to a recipient (keyless — takes no key material). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction sending AP3X to a recipient (keyless, takes no key material). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of funds and change (addr1...)
- `recipientAddress` (string, required) — Recipient address (addr1...)
- `amount` (number, required) — Amount of AP3X to send (minimum 1)
- `metadata` (string, optional) — Transaction metadata in JSON format
- `changeAddress` (string, required) - Your wallet address; holds the AP3X and receives change (addr1...)
- `recipientAddress` (string, required) - Recipient address (addr1...)
- `amount` (number, required) - Amount of AP3X to send (minimum 1)
- `metadata` (string, optional) - Transaction metadata in JSON format
**Example prompt:** "Send 5 AP3X to addr1qy..."
### vector_build_send_tokens
Build an unsigned transaction sending Vector native tokens (keyless — takes no key material). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction sending Vector native tokens (keyless, takes no key material). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of funds and change (addr1...)
- `recipientAddress` (string, required) — Recipient address (addr1...)
- `policyId` (string, required) — Token policy ID
- `assetName` (string, required) — Asset name (can be empty)
- `amount` (string, required) — Amount of tokens to send
- `adaAmount` (number, optional) — AP3X to include with tokens
- `changeAddress` (string, required) - Your wallet address; holds the AP3X and receives change (addr1...)
- `recipientAddress` (string, required) - Recipient address (addr1...)
- `policyId` (string, required) - Token policy ID
- `assetName` (string, required) - Asset name (can be empty)
- `amount` (string, required) - Amount of tokens to send
- `adaAmount` (number, optional) - AP3X to include with tokens
**Example prompt:** "Send 100 of token with policy abc123... to addr1qy..."
@@ -61,21 +61,21 @@ Build an unsigned transaction sending Vector native tokens (keyless — takes no
Build an unsigned multi-output transaction with metadata (keyless, never submits). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of funds and change (addr1...)
- `outputs` (array, required) — Array of `{ address, lovelace, assets? }` objects
- `metadata` (string, optional) — JSON metadata (attached under label 674)
- `changeAddress` (string, required) - Your wallet address; holds the AP3X and receives change (addr1...)
- `outputs` (array, required) - Array of `{ address, lovelace, assets? }` objects
- `metadata` (string, optional) - JSON metadata (attached under label 674)
**Example prompt:** "Build a transaction sending 5 AP3X to addr1qx... and 3 AP3X to addr1qy..."
### vector_dry_run
Simulate a transaction without submitting — returns fee estimate and validation (keyless).
Simulate a transaction without submitting - returns fee estimate and validation (keyless).
**Parameters:**
- `txCbor` (string, optional) — Hex CBOR of a built transaction to evaluate
- `outputs` (array, optional) — If no txCbor, build a TX from these outputs and evaluate
- `changeAddress` (string, optional) — Your wallet address; required when building from outputs
- `metadata` (string, optional) — JSON metadata when building from outputs
- `txCbor` (string, optional) - Hex CBOR of a built transaction to evaluate
- `outputs` (array, optional) - If no txCbor, build a TX from these outputs and evaluate
- `changeAddress` (string, optional) - Your wallet address; required when building from outputs
- `metadata` (string, optional) - JSON metadata when building from outputs
**Example prompt:** "How much would it cost to send 10 AP3X to addr1qx...?"
@@ -84,89 +84,89 @@ Simulate a transaction without submitting — returns fee estimate and validatio
Get transaction history for an address via Koios indexed queries.
**Parameters:**
- `address` (string, required) — Address to query (addr1...)
- `limit` (number, optional) — Number of transactions (default: 20, max: 50)
- `offset` (number, optional) — Pagination offset (default: 0)
- `address` (string, required) - Address to query (addr1...)
- `limit` (number, optional) - Number of transactions (default: 20, max: 50)
- `offset` (number, optional) - Pagination offset (default: 0)
**Example prompt:** "Show me recent transactions for addr1qx..."
### vector_build_deploy_contract
Build an unsigned transaction deploying a Plutus/Aiken smart contract by locking funds at its script address (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction deploying a Plutus/Aiken smart contract by locking AP3X at its script address (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of funds and change (addr1...)
- `scriptCbor` (string, required) — Compiled script CBOR hex
- `scriptType` (string, required) — "PlutusV1", "PlutusV2", or "PlutusV3"
- `initialDatum` (string, optional) — Datum CBOR hex (default: void `d87980`)
- `lovelaceAmount` (number, optional) — AP3X to lock in lovelace (default: 2 AP3X)
- `changeAddress` (string, required) - Your wallet address; holds the AP3X and receives change (addr1...)
- `scriptCbor` (string, required) - Compiled script CBOR hex
- `scriptType` (string, required) - "PlutusV1", "PlutusV2", or "PlutusV3"
- `initialDatum` (string, optional) - Datum CBOR hex (default: void `d87980`)
- `lovelaceAmount` (number, optional) - AP3X to lock in lovelace (default: 2 AP3X)
**Example prompt:** "Deploy this smart contract with 5 AP3X locked"
### vector_build_interact_contract
Build an unsigned transaction interacting with a deployed smart contract — lock funds or spend from it (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction interacting with a deployed smart contract: lock AP3X or spend from it (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of funds, change, and the required signer for spends (addr1...)
- `scriptCbor` (string, required) — Compiled script CBOR hex
- `scriptType` (string, required) — "PlutusV1", "PlutusV2", or "PlutusV3"
- `action` (string, required) — "spend" or "lock"
- `redeemer` (string, optional) — Redeemer CBOR hex (for spend, default: void)
- `datum` (string, optional) — Datum CBOR hex (for lock, default: void)
- `lovelaceAmount` (number, optional) — Lovelace to lock (for lock)
- `utxoRef` (object, optional) — `{ txHash, outputIndex }` for specific UTxO (for spend)
- `assets` (object, optional) — Native assets for lock: `{ "unit": "quantity" }`
- `changeAddress` (string, required) - Your wallet address; holds the AP3X, receives change, and provides the required signer for spends (addr1...)
- `scriptCbor` (string, required) - Compiled script CBOR hex
- `scriptType` (string, required) - "PlutusV1", "PlutusV2", or "PlutusV3"
- `action` (string, required) - "spend" or "lock"
- `redeemer` (string, optional) - Redeemer CBOR hex (for spend, default: void)
- `datum` (string, optional) - Datum CBOR hex (for lock, default: void)
- `lovelaceAmount` (number, optional) - Lovelace to lock (for lock)
- `utxoRef` (object, optional) - `{ txHash, outputIndex }` for specific UTxO (for spend)
- `assets` (object, optional) - Native assets for lock: `{ "unit": "quantity" }`
**Example prompt:** "Collect funds from the escrow contract at addr1..."
**Example prompt:** "Collect the locked AP3X from the escrow contract at addr1..."
### vector_build_register_agent
Build an unsigned transaction that registers an agent in the Vector on-chain registry — mints a soulbound identity NFT and locks a 10 AP3X deposit (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction that registers an agent in the Vector on-chain registry: mints a soulbound identity NFT and locks a 10 AP3X deposit (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; source of the deposit and change (addr1...)
- `name` (string, required) — Agent name (max 64 chars)
- `description` (string, required) — Short description of the agent's purpose (max 256 chars)
- `capabilities` (array of strings, required) — Capability tags (e.g. `["investing", "research"]`)
- `framework` (string, required) — Framework used (e.g. "OpenClaw", "LangChain", "CrewAI", "custom")
- `endpoint` (string, required) — A2A communication endpoint URL (or empty string if not applicable)
- `changeAddress` (string, required) - Your wallet address; source of the deposit and change (addr1...)
- `name` (string, required) - Agent name (max 64 chars)
- `description` (string, required) - Short description of the agent's purpose (max 256 chars)
- `capabilities` (array of strings, required) - Capability tags (e.g. `["research", "data-extraction"]`)
- `framework` (string, required) - Framework used (e.g. "OpenClaw", "LangChain", "CrewAI", "custom")
- `endpoint` (string, required) - A2A communication endpoint URL (or empty string if not applicable)
**Example prompt:** "Register an agent named TradingBot with investing and research capabilities"
**Example prompt:** "Register an agent named ResearchAgent with research and data-extraction capabilities"
### vector_build_update_agent
Build an unsigned transaction that updates a registered agent's profile fields — only the specified fields change, the rest are preserved (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction that updates a registered agent's profile fields: only the specified fields change, the rest are preserved (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — The agent owner's wallet address (addr1...)
- `agent_id` (string, required) — Agent DID to update: `did:vector:agent:{policyId}:{nftAssetName}`
- `name` (string, optional) — New agent name
- `description` (string, optional) — New description
- `capabilities` (array of strings, optional) — New capability tags (replaces the existing list)
- `framework` (string, optional) — New framework identifier
- `endpoint` (string, optional) — New A2A endpoint URL (or empty string to clear)
- `changeAddress` (string, required) - The agent owner's wallet address (addr1...)
- `agent_id` (string, required) - Agent DID to update: `did:vector:agent:{policyId}:{nftAssetName}`
- `name` (string, optional) - New agent name
- `description` (string, optional) - New description
- `capabilities` (array of strings, optional) - New capability tags (replaces the existing list)
- `framework` (string, optional) - New framework identifier
- `endpoint` (string, optional) - New A2A endpoint URL (or empty string to clear)
**Example prompt:** "Update my agent's description to mention DeFi support"
**Example prompt:** "Update my agent's description to mention archive-extraction support"
### vector_build_transfer_agent
Build an unsigned transaction that transfers agent ownership to a new address (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — The CURRENT owner's wallet address (addr1...)
- `agent_id` (string, required) — Agent DID to transfer: `did:vector:agent:{policyId}:{nftAssetName}`
- `new_owner_address` (string, required) — Bech32 address of the new owner (must be a verification-key address, not a script address)
- `changeAddress` (string, required) - The CURRENT owner's wallet address (addr1...)
- `agent_id` (string, required) - Agent DID to transfer: `did:vector:agent:{policyId}:{nftAssetName}`
- `new_owner_address` (string, required) - Bech32 address of the new owner (must be a verification-key address, not a script address)
**Example prompt:** "Transfer my agent to addr1qy..."
### vector_build_deregister_agent
Build an unsigned transaction that deregisters an agent — burns the identity NFT and returns the 10 AP3X deposit to the owner wallet (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
Build an unsigned transaction that deregisters an agent: burns the identity NFT and returns the 10 AP3X deposit to the owner wallet (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — The agent owner's wallet address; receives the returned deposit (addr1...)
- `agent_id` (string, required) — Agent DID to deregister: `did:vector:agent:{policyId}:{nftAssetName}`
- `changeAddress` (string, required) - The agent owner's wallet address; receives the returned deposit (addr1...)
- `agent_id` (string, required) - Agent DID to deregister: `did:vector:agent:{policyId}:{nftAssetName}`
**Example prompt:** "Deregister my agent and reclaim the deposit"
@@ -175,10 +175,10 @@ Build an unsigned transaction that deregisters an agent — burns the identity N
Build an unsigned transaction that sends an on-chain message to a registered agent via TX metadata (label 674), delivering 2 AP3X to the agent's owner address (keyless). Sign the returned CBOR with your local signer, then broadcast with `vector_submit_transaction`.
**Parameters:**
- `changeAddress` (string, required) — Your wallet address; pays the 2 AP3X delivery and fee (addr1...)
- `agent_id` (string, required) — Recipient agent DID: `did:vector:agent:{policyId}:{nftAssetName}`
- `message_type` (string, required) — Type of message: "inquiry", "proposal", or "result"
- `payload` (string, required) — Message payload (max 512 chars)
- `changeAddress` (string, required) - Your wallet address; covers the 2 AP3X delivery and fee (addr1...)
- `agent_id` (string, required) - Recipient agent DID: `did:vector:agent:{policyId}:{nftAssetName}`
- `message_type` (string, required) - Type of message: "inquiry", "proposal", or "result"
- `payload` (string, required) - Message payload (max 512 chars)
**Example prompt:** "Send an inquiry message to did:vector:agent:... asking about pricing"
@@ -261,6 +261,6 @@ and [docs/architecture/non-custodial-split.md](architecture/non-custodial-split.
All tools return helpful error messages with troubleshooting tips. Common issues:
- **Insufficient balance** — Ensure the wallet at `changeAddress` has enough AP3X for the transaction + fees
- **Connection failed** — Verify Ogmios endpoint is reachable
- **Invalid address** — Vector addresses start with `addr1`
- **Insufficient balance** - Ensure the wallet at `changeAddress` has enough AP3X for the transaction + fees
- **Connection failed** - Verify Ogmios endpoint is reachable
- **Invalid address** - Vector addresses start with `addr1`

View File

@@ -20,7 +20,7 @@ The server's spend limits and rate limits are app-layer only, so they do not con
Success criteria:
1. No hosted tool accepts a mnemonic or private key. Grep-able: zero `mnemonic` params in the builder package.
2. The signing key never enters the model context, never crosses the network.
3. Spend policy is enforced where it is actually enforceable — locally, per user.
3. Spend policy is enforced where it is actually enforceable: locally, per user.
4. Every PR in the series is independently reviewable and leaves `main` coherent.
## 3. Decisions (locked)
@@ -30,14 +30,14 @@ Success criteria:
| Deployment reality | Hosted multi-tenant is flagship → seed-to-server is P0 |
| Architecture | Keyless hosted **builder** + local companion **signer** |
| Repo topology | Single repo, **npm workspaces** monorepo |
| Migration | **Hard cut per family** — no dual-mode, no deprecation window |
| Migration | **Hard cut per family** - no dual-mode, no deprecation window |
| Key storage | Pluggable `KeySource`; v1 = env var / `0600` file |
| Signer policy | Decode tx → **net outflow** → limits → audit → sign. Fully autonomous |
| Tool flow | 4 calls: `get_address` → `build_*` → `sign` → `submit` |
| Tool naming | Explicit `build_*` / `signer_*` / `submit_*` prefixes |
| Cross-cutting fixes | Entangled ones inline; independent ones as separate small PRs |
| Rollout | Testnet auto-deploys and may break; mainnet held until complete |
| Testing | **TDD** — tests written first |
| Testing | **TDD** - tests written first |
| Auth | Enforced when `MCP_AUTH_TOKENS` is configured |
## 4. Architecture
@@ -46,17 +46,17 @@ Success criteria:
mcp-server/ npm workspaces root
├── packages/
│ ├── shared/ @apexfusion/vector-mcp-shared
│ │ exports "./tx" pure CBOR, types, formatting — NO network
│ │ exports "./provider" OgmiosProvider, endpoints — network
│ │ exports "./tx" pure CBOR, types, formatting - NO network
│ │ exports "./provider" OgmiosProvider, endpoints - network
│ ├── builder/ @apexfusion/vector-mcp-builder HOSTED · keyless · network
│ └── signer/ @apexfusion/vector-mcp-signer LOCAL · holds key · no network
```
Two properties carry the security argument:
**The signer has no network interface, inbound or outbound.** It speaks **stdio** (the natural MCP local transport) — no port, no listener. Submission lives on the builder, so it has no egress either. The signer is a pure function: `CBOR in → signed CBOR out`, gated by policy.
**The signer has no network interface, inbound or outbound.** It speaks **stdio** (the natural MCP local transport): no port, no listener. Submission lives on the builder, so it has no egress either. The signer is a pure function: `CBOR in → signed CBOR out`, gated by policy.
**The boundary is enforced by the module graph, not convention.** `shared` uses subpath exports; the signer imports `@apexfusion/vector-mcp-shared/tx` and *cannot* reach `/provider`. A reviewer can verify the signer is network-free by reading its imports.
**The module graph enforces the boundary.** `shared` uses subpath exports; the signer imports `@apexfusion/vector-mcp-shared/tx` and *cannot* reach `/provider`. A reviewer can verify the signer is network-free by reading its imports.
### Why zero-network is possible
@@ -119,7 +119,7 @@ interface Policy {
`vector_signer_decode_transaction` is a read-only preview so an agent can show a human what it is about to sign.
**Accepted trade-off of the 4-call shape:** the signer records **intent to spend** at signing time, not confirmed submission. If a submit later fails, the audit log over-counts. Conservative, therefore acceptable — and it is the price of the signer having no network access. Documented, not hidden.
**Accepted trade-off of the 4-call shape:** the signer records **intent to spend** at signing time, not confirmed submission. If a submit later fails, the audit log over-counts. Conservative, therefore acceptable, and it is the price of the signer having no network access.
### Two-step self-improvement flow
@@ -134,18 +134,18 @@ This is why self-improvement is the last family: the LLM must drive a stateful,
## 7. Tool inventory
**Builder — 24 tools at full migration, zero keys.**
**Builder - 24 tools at full migration, zero keys.**
| Group | Tools |
|---|---|
| Read-only (8, unchanged) | `get_balance`, `get_utxos`, `get_transaction_history`, `dry_run`, `discover_agents`, `get_agent_profile`, `self_improvement_browse`, `self_improvement_analyze_metrics` |
| Build — tx (3) | `build_send_apex`, `build_send_tokens`, `build_transaction` |
| Build — contracts (2) | `build_deploy_contract`, `build_interact_contract` |
| Build — registry (5) | `build_register_agent`, `build_update_agent`, `build_transfer_agent`, `build_deregister_agent`, `build_message_agent` |
| Build — self-improvement (4) | `build_self_improvement_proposal_lock`, `build_self_improvement_proposal_spend`, `build_self_improvement_critique`, `build_self_improvement_endorse` |
| Build - tx (3) | `build_send_apex`, `build_send_tokens`, `build_transaction` |
| Build - contracts (2) | `build_deploy_contract`, `build_interact_contract` |
| Build - registry (5) | `build_register_agent`, `build_update_agent`, `build_transfer_agent`, `build_deregister_agent`, `build_message_agent` |
| Build - self-improvement (4) | `build_self_improvement_proposal_lock`, `build_self_improvement_proposal_spend`, `build_self_improvement_critique`, `build_self_improvement_endorse` |
| Chain ops (2, new) | `submit_transaction`, `await_transaction` |
**Signer — 4 tools, holds the key.**
**Signer - 4 tools, holds the key.**
`signer_get_address` · `signer_decode_transaction` · `signer_sign` · `signer_get_spend_limits`
@@ -168,7 +168,7 @@ TDD throughout: failing test before implementation, every PR.
| `signer` | **Adversarial** (below) | throwaway key | no |
| integration | build→sign→submit round trip | **funded testnet** | yes |
**Mandatory adversarial cases for the signer** — these are the security boundary:
**Mandatory adversarial cases for the signer** - these are the security boundary:
- Drain to a foreign address exceeding the limit → **refused**
- Change returning to own address → **excluded** from net outflow
@@ -185,13 +185,13 @@ TDD throughout: failing test before implementation, every PR.
|---|---|---|
| **1** | CI harness: build + unit tests. Ships this design doc into `docs/architecture/` so reviewers have the rationale | no |
| **2** | Fix the 8 type errors (import Zod via `zod/v4`) + **typecheck becomes a hard gate** | no |
| **3** | Workspace scaffolding — pure `git mv`, zero logic change, validated by PR 1–2 CI | no |
| **3** | Workspace scaffolding: pure `git mv`, zero logic change, validated by PR 1–2 CI | no |
| **4** | **Signer package** (purely additive): `KeySource`, decode, `PolicyEngine`, audit, 4 tools. Heavy TDD | no |
| **5** | Builder hardening: auth (enforced when configured), per-identity rate limits, `submit_transaction`, `await_transaction` | no |
| **6** | **Family 1 — wallet/tx keyless.** Mnemonics removed; `build_*` tools land ✅ **(landed — this PR)** | **yes** |
| **7** | **Family 2 — agent registry keyless.** Drops `@ts-nocheck` from `agent-network.ts` ✅ **(landed — this PR)** | yes |
| **8** | **Family 3 — self-improvement keyless.** Ref-UTxO env config carried over from the custodial code; the suspected CBOR encoding issue closed as no-bug (audit #10, golden-pinned against a live on-chain token) ✅ **(landed — this PR)** | yes |
| **9** | Mainnet cutover: README, security-page correction, deliberate `workflow_dispatch` ✅ **(landed — deployed 2026-07-31)** | — |
| **6** | **Family 1: wallet/tx keyless.** Mnemonics removed; `build_*` tools land ✅ **(landed - this PR)** | **yes** |
| **7** | **Family 2: agent registry keyless.** Drops `@ts-nocheck` from `agent-network.ts` ✅ **(landed - this PR)** | yes |
| **8** | **Family 3: self-improvement keyless.** Ref-UTxO env config carried over from the custodial code; the suspected CBOR encoding issue closed as no-bug (audit #10, golden-pinned against a live on-chain token) ✅ **(landed - this PR)** | yes |
| **9** | Mainnet cutover: README, security-page correction, deliberate `workflow_dispatch` ✅ **(landed - deployed 2026-07-31)** | - |
**PR 4 is the review that matters.** It is additive, self-contained, and *is* the security boundary. It breaks nothing while it is reviewed.
@@ -216,14 +216,14 @@ TDD throughout: failing test before implementation, every PR.
PR1 ─→ PR2 ─→ PR3 ─┬─→ PR4 (signer) ─┐
└─→ PR5 (builder hardening) ─┴─→ PR6 ─→ PR7 ─→ PR8 ─→ PR9
A…H independent of the spine and of each other — land any time
A…H independent of the spine and of each other - land any time
```
PR 4 and PR 5 both depend on the workspace split (PR 3) and can proceed in parallel; PR 6 needs both, because the first keyless family requires a signer to sign with and a `submit_transaction` to submit through.
## 11. Rollout
Testnet auto-deploys on push to `main` (`deploy.yml`) and **is the proving ground** — breaking it from PR 6 onward is expected and useful signal. Mainnet deploys only by manual `workflow_dispatch` and **stayed on the last custodial image until PR 9** ✅ **(cutover deployed 2026-07-31, both networks now run the same image)**. This uses the existing CI split as designed; no new infrastructure.
Testnet auto-deploys on push to `main` (`deploy.yml`) and **is the proving ground**: breaking it from PR 6 onward is expected and useful signal. Mainnet deploys only by manual `workflow_dispatch` and **stayed on the last custodial image until PR 9** ✅ **(cutover deployed 2026-07-31, both networks now run the same image)**. This uses the existing CI split as designed; no new infrastructure.
## 12. Findings from local verification
@@ -231,30 +231,30 @@ Verified locally on Windows 11, **Node v25.8.0 / npm 11.11.0**: `npm ci` clean i
> **Verification caveat:** local Node is **25.8.0**; the Dockerfile deploys on **node:22-alpine**, and `package.json` claims `>=14`. Local green does not prove green on the deploy runtime. PR 1's CI must pin the **Node 22** matrix so the gate matches production, and PR B corrects the declared floor.
**`npm run typecheck` fails on `main` — 8 errors, all `src/vector/vector.ts`.**
**`npm run typecheck` fails on `main`: 8 errors, all `src/vector/vector.ts`.**
```
512,3 · 702,3 · 782,3 · 920,3 · 990,3 · 1192,3 · 1250,3 TS2589 instantiation excessively deep
1250,10 TS2769 no overload — z.record(z.string()).optional()
1250,10 TS2769 no overload - z.record(z.string()).optional()
```
CI only runs `npm run build`, and `tsup`/esbuild strips types without checking them, so this has never been caught.
**This corrects an earlier misattribution.** The `@ts-nocheck` headers are attributed in-code to "Cardano WASM imports," but the real cause is `server.tool()` generic inference exceeding TypeScript's instantiation depth on rich Zod schemas — the same failure leaking out of the one file lacking the escape hatch.
**This corrects an earlier misattribution.** The `@ts-nocheck` headers are attributed in-code to "Cardano WASM imports," but the real cause is `server.tool()` generic inference exceeding TypeScript's instantiation depth on rich Zod schemas: the same failure leaking out of the one file lacking the escape hatch.
**Root cause, resolved (PR 2).** The initial hypothesis — a stale `@modelcontextprotocol/sdk@^1.4.1` — was wrong: `^1.4.1` is the declared *range*, and npm resolves it to 1.27.1. The actual cause is that the app imported Zod 3 (`from "zod"`) while the SDK expects Zod 4; its `ZodRawShapeCompat` dual-compat type degrades on Zod 3 schemas, and anything richer than a bare `z.string()` exceeds the instantiation limit. The TS2769 is a downstream symptom, not a second defect. Ruled out by experiment before landing the fix: upgrading the SDK to 1.29.0, a duplicate Zod in the tree, pinning Zod 3.24.4, `moduleResolution: bundler`/`nodenext`, the newer `registerTool` API, and hoisting schemas to a `const`. The fix is importing via the `zod/v4` subpath — already shipped inside `zod@3.25.76` — so no dependency change was needed.
**Root cause, resolved (PR 2).** The initial hypothesis, a stale `@modelcontextprotocol/sdk@^1.4.1`, was wrong: `^1.4.1` is the declared *range*, and npm resolves it to 1.27.1. The actual cause is that the app imported Zod 3 (`from "zod"`) while the SDK expects Zod 4; its `ZodRawShapeCompat` dual-compat type degrades on Zod 3 schemas, and anything richer than a bare `z.string()` exceeds the instantiation limit. The TS2769 is a downstream symptom, not a second defect. Ruled out by experiment before landing the fix: upgrading the SDK to 1.29.0, a duplicate Zod in the tree, pinning Zod 3.24.4, `moduleResolution: bundler`/`nodenext`, the newer `registerTool` API, and hoisting schemas to a `const`. The fix is importing via the `zod/v4` subpath, already shipped inside `zod@3.25.76`, so no dependency change was needed.
**Accepted trade-off.** `zod/v4` routes schemas through a different JSON Schema serializer than v3 (the SDK uses the vendored `zod-to-json-schema` for v3, Zod's native `toJSONSchema` for v4). Consequently **no tool advertises `additionalProperties: false` any more**, where the v3 path emitted it. All three tool modules were converted together so the 23-tool surface stays uniform rather than split. Runtime enforcement is unaffected — missing required arguments are still rejected, and extra keys were already stripped under v3. `test/smoke/tool-schemas.snapshot.json` pins the resulting surface so any further drift is deliberate.
**Accepted trade-off.** `zod/v4` routes schemas through a different JSON Schema serializer than v3 (the SDK uses the vendored `zod-to-json-schema` for v3, Zod's native `toJSONSchema` for v4). Consequently **no tool advertises `additionalProperties: false` any more**, where the v3 path emitted it. All three tool modules were converted together so the 23-tool surface stays uniform rather than split. Runtime enforcement is unaffected: missing required arguments are still rejected, and extra keys were already stripped under v3. `test/smoke/tool-schemas.snapshot.json` pins the resulting surface so any further drift is deliberate.
## 13. Residual risks
**Native-asset outflow is not limited in v1.** Spend limits apply to lovelace. A transaction moving ~2 AP3X plus a valuable NFT to a foreign address passes the lovelace check. Blanket-denying asset outflow is not viable because legitimate registry flows mint an NFT to the registry script address.
v1 mitigation: all asset movements are decoded, logged, and returned in the policy decision, and `assetOutflowPolicy` is configurable (`allow` default | `deny` | `allowlist`) for high-value wallets. **Stated as a known limitation, not glossed.** Post-v1: per-policy asset rules.
v1 mitigation: all asset movements are decoded, logged, and returned in the policy decision, and `assetOutflowPolicy` is configurable (`allow` default | `deny` | `allowlist`) for high-value wallets. Post-v1: per-policy asset rules.
**Key at rest is plaintext in v1** (env var or `0600` file). The win is that the key is *local and never transits the LLM or a shared host* — not that it is in an HSM. `KeySource` exists so keychain/HSM/hardware land later without touching the sign path.
**Key at rest is plaintext in v1** (env var or `0600` file). The win is that the key is *local and never transits the LLM or a shared host*, not that it is in an HSM. `KeySource` exists so keychain/HSM/hardware land later without touching the sign path.
**Prompt injection is mitigated, not eliminated.** The signer refuses transactions violating policy, but an injected LLM can still cause any transaction *within* policy. Recipient allowlisting is the next lever.
**Prompt injection is a residual risk.** The signer refuses transactions violating policy, but an injected LLM can still cause any transaction *within* policy. Recipient allowlisting is the next lever.
## 14. Open items

View File

@@ -8,29 +8,29 @@ provider.
## Why it has no network
- **stdio transport only** — no port, no HTTP listener, nothing to connect to
- **no Provider and no Lucid instance** — signing uses CML directly
- **stdio transport only** - no port, no HTTP listener, nothing to connect to
- **no Provider and no Lucid instance** - signing uses CML directly
- **submission lives on the builder**, so the signer needs no egress either
`test/boundary.test.ts` checks this mechanically — it walks every file under `src/` for a
`test/boundary.test.ts` checks this mechanically: it walks every file under `src/` for a
network-capable import or an ambient network global (`fetch`, `WebSocket`, ...), and checks
`package.json` for a network-capable dependency. That check is a text scan, not a sandbox, and
it is scoped accordingly: it reliably catches *accidental* regressions and *casual* misuse — an
it is scoped accordingly: it reliably catches *accidental* regressions and *casual* misuse, an
added dependency, a forgotten forbidden import, an unthinking `fetch(...)` call, including one
routed around the bare identifier via `globalThis['fetch']` or `globalThis.fetch`. It does not
catch *deliberate* obfuscation. Verified directly against the hardened check: an ambient global
reached through a plain alias (`const f = fetch; f(...)`) or a forbidden module specifier
assembled at runtime from concatenated strings (`import('node:' + 'dns')`) both still pass.
Closing gaps like those is a code-review responsibility, not something a fixed set of text
patterns can guarantee — what this check actually verifies is that today's shipped `src/` has no
patterns can guarantee: what this check actually verifies is that today's shipped `src/` has no
*accidental or careless* network capability, not a proof that none could ever be smuggled in.
`test/integration/roundtrip.test.ts` imports `@lucid-evolution/lucid` and the shared package's
`/provider` — that does **not** weaken the check above. That file lives under `test/`,
`/provider` - that does **not** weaken the check above. That file lives under `test/`,
which `boundary.test.ts` deliberately scans only as far as `src/`, and which the build never
bundles (`tsup`'s only entry point is `src/index.ts`). Those imports exist so the test can
build a real transaction the way the hosted *builder* does (no key) before handing it to the
signer's actual decode/policy/sign code path — the network calls belong to the test's
signer's actual decode/policy/sign code path: the network calls belong to the test's
stand-in builder, never to code that ships.
## Defense in depth
@@ -43,13 +43,13 @@ expose:
by one function whose parameter list never includes the key source. That's structural, not a
matter of discipline: reintroducing signing capability there means visibly changing that
function's signature, not adding a line inside a handler.
- **Key material resists accidental logging.** TypeScript `private` is compile-time only — at
- **Key material resists accidental logging.** TypeScript `private` is compile-time only: at
runtime the field is an ordinary enumerable property, fully visible to `JSON.stringify`,
`console.log`, and `util.inspect`. Mnemonics and derived private keys are therefore held in
native JavaScript `#private` fields instead, and both `KeySource` classes override
`toJSON()` and Node's inspect symbol so that even a stray `console.log(keySource)` prints a
safe description instead of the secret. This defends against *accidental* disclosure (a
debugging log line, a crash reporter serializing local state) — it is not a defense against
debugging log line, a crash reporter serializing local state); it is not a defense against
a compromised process, which can still read the key out of memory directly.
- **The audit log is crash-safe.** Every entry is written to a temp file and renamed into
place, so a process kill mid-write costs at most the one entry in flight, never the rest of
@@ -63,11 +63,11 @@ Pick one key source:
|---|---|
| `VECTOR_SIGNER_MNEMONIC_FILE` | Path to a file containing your mnemonic. Use `chmod 600`. Recommended. |
| `VECTOR_SIGNER_MNEMONIC` | The mnemonic inline. Convenient, but ends up in your shell history and process environment. |
| `VECTOR_SIGNER_PRIVATE_KEY` + `VECTOR_SIGNER_ADDRESS` | A derived bech32 signing key. **Safest in terms of blast radius** — it controls one account rather than your whole HD tree, so a leak is bounded. See the mismatch caveat under Known limitations. |
| `VECTOR_SIGNER_PRIVATE_KEY` + `VECTOR_SIGNER_ADDRESS` | A derived bech32 signing key. **Safest in terms of blast radius**: it controls one account rather than your whole HD tree, so a leak is bounded. See the mismatch caveat under Known limitations. |
If more than one is set, `config.ts` checks them in this order: `VECTOR_SIGNER_PRIVATE_KEY` +
`VECTOR_SIGNER_ADDRESS`, then `VECTOR_SIGNER_MNEMONIC`, then `VECTOR_SIGNER_MNEMONIC_FILE`.
Unset the ones you are not using — a leftover `VECTOR_SIGNER_MNEMONIC` in your shell profile
Unset the ones you are not using: a leftover `VECTOR_SIGNER_MNEMONIC` in your shell profile
silently wins over a `VECTOR_SIGNER_MNEMONIC_FILE` you just configured.
Optional:
@@ -81,12 +81,12 @@ Optional:
**`VECTOR_SIGNER_AUDIT_LOG_PATH`, precisely:** if unset, the log is written to
`vector-signer-audit-log.json` resolved against the *package's own install location* (three
directories up from `build/index.js` — the monorepo root, in this repo), **not** against
directories up from `build/index.js`, the monorepo root, in this repo), **not** against
whatever directory the process happened to be started from. That makes the location
predictable regardless of your MCP client's working directory, but it also means the file is
easy to forget about. Like the builder's `vector-audit-log.json`, this exact filename is listed
in this repo's `.gitignore`, so a default-configured signer run from a checkout will not get its
audit log accidentally staged. That entry only matches the default filename, though — if you
audit log accidentally staged. That entry only matches the default filename, though: if you
point `VECTOR_SIGNER_AUDIT_LOG_PATH` at some other path inside the repo, check before you
`git add`, or just set the variable to somewhere outside the repo entirely.
@@ -99,7 +99,7 @@ point `VECTOR_SIGNER_AUDIT_LOG_PATH` at some other path inside the repo, check b
| `vector_signer_sign` | Policy-check and sign. Returns signed CBOR. |
| `vector_signer_get_spend_limits` | Limits, today's committed total, recent decisions. |
No tool accepts key material as a parameter — that is asserted by the smoke test.
No tool accepts key material as a parameter; that is asserted by the smoke test.
## What "net outflow" means
@@ -116,32 +116,31 @@ over-counts, which refuses too much rather than too little.
lovelace. A transaction moving ~2 AP3X plus a valuable NFT passes the lovelace check.
Asset movements appear in `decode_transaction` output and the audit log so they are
visible, but they are not yet a policy input.
- **The audit log records intent, not confirmation.** The signer has no network and cannot
- **The audit log records intent to spend, at signing time.** The signer has no network and cannot
observe whether a submit succeeded, so a failed submit still counts against the daily
budget. Conservative by design.
- **A durable audit record is mandatory for signing — which means an unwritable audit log
stops all signing, not just accounting.** `AuditLog.append()` throws if the write fails, and
- **A durable audit record is mandatory for signing: an unwritable audit log halts every
signature.** `AuditLog.append()` throws if the write fails, and
`vector_signer_sign` withholds a signature it already produced rather than return one with
no durable record of it — the alternative would let a restarted process silently under-count
no durable record of it. The alternative would let a restarted process silently under-count
today's spend, which quietly raises the effective daily limit. The practical effect: if the
audit log's directory disappears, the disk fills, or the path loses write permission, every
`vector_signer_sign` call starts returning `REFUSED` — including transactions well inside
policy — until it is writable again. That refusal names the audit failure explicitly rather
than reading like an ordinary policy refusal, but it is still a full stop on signing, not a
warning.
`vector_signer_sign` call starts returning `REFUSED`, including transactions well inside
policy, until it is writable again. That refusal names the audit failure explicitly, unlike
an ordinary policy refusal. Signing stops outright until the log is writable again.
- **The key is plaintext at rest** in v1 (env var or file). The win is that it is *local*
and never transits your model provider or a shared host — not that it is in an HSM.
and never transits your model provider or a shared host, not that it is in an HSM.
`KeySource` exists so keychain and hardware backends can be added without touching the
signing path.
- **`VECTOR_SIGNER_PRIVATE_KEY` and `VECTOR_SIGNER_ADDRESS` are not cross-checked against each
other.** Each is validated as well-formed independently, but nothing confirms the address is
actually the one the private key controls — that check is possible entirely offline (this
actually the one the private key controls: that check is possible entirely offline (this
package already depends on the CML library that could do it) but is not wired up yet. A
mismatched pair still constructs and signs without error. The failure surfaces later: either
as a chain-level rejection at submission, or as the policy engine failing to recognise your
own change as change and over-counting it as outflow — the safe direction, but confusing if
you hit it without knowing why.
- **Prompt injection is bounded, not eliminated.** Policy refuses out-of-limit
own change as change and over-counting it as outflow (safe, but confusing if
you hit it without knowing why).
- **Prompt injection is a residual risk.** Policy refuses out-of-limit
transactions, but an injected agent can still cause any transaction *within* policy.
Recipient allowlisting is the next lever.
- **Pairing this signer with the builder in this codebase now removes the custody exposure it
@@ -218,5 +217,5 @@ npm run test:integration:signer
Requires a **funded Vector testnet** wallet. Builds a real unsigned transaction the way the
hosted builder does (no key), then decodes, policy-checks, and signs it with the real signer
code path against live chain data. **Never submits.** Not run in CI — see
code path against live chain data. **Never submits.** Not run in CI; see
`test/integration/roundtrip.test.ts`.