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

@@ -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