# IOTA Web Guardian — Coding Plan

> **Version**: 0.1.0 — MVP (Hackathon)
> **Network**: IOTA Rebased (Move VM)
> **Last updated**: 2026-03-09

---

## Workspace Layout

```
andromeda/
├── crates/
│   └── iota-guardian-secrets/   # lib crate — Secrets Manager (Task 1)
├── services/
│   ├── cvs/                     # Certificate Verifier Service (Task 2)
│   ├── x402/                    # x402 Payment Facilitator (Task 3)
│   ├── super-certifier-admin/   # Super Certifier Admin backend (Task 4)
│   ├── certifier-service/       # Certifier Service backend (Task 5)
│   ├── registration-app/        # Registration App backend (Task 6)
│   └── guardian/                # Guardian Rust Proxy (Task 7)
├── apps/
│   ├── super-certifier-admin/   # SvelteKit UI — Super Certifier Admin (Task 4)
│   ├── certifier-service/       # SvelteKit UI — Certifier Service (Task 5)
│   └── registration-app/        # SvelteKit UI — Registration App (Task 6)
├── cli/                         # AI Agent CLI binary (Task 8)
├── plugins/
│   └── iota-guardian-wp/        # WordPress Plugin PHP (Task 10)
├── docker-compose.yml           # Task 9
└── Cargo.toml                   # workspace manifest
```

---

## Build Order (dependency graph)

```
Task 1: iota-guardian-secrets
    ↓
Task 2: CVS ──────────────────────────────┐
Task 3: x402 ─────────────────────────────┤
Task 4: Super Certifier Admin (back+front) ┤ all independent
Task 5: Certifier Service (back+front) ────┤ after Task 1
Task 6: Registration App (back+front) ─────┤
Task 8: AI Agent CLI ──────────────────────┘
    ↓ (all services ready)
Task 7: Guardian Rust Proxy
    ↓
Task 9: Docker Compose
    ↓
Task 10: WordPress Plugin
```

---

## Task 1 — `iota-guardian-secrets` (lib crate)

**Goal**: Provide the cryptographic key management foundation used by all other services. No service should be coded until this crate exists and is tested.

**Crate path**: `crates/iota-guardian-secrets/`

**Deliverables**:
- `FileSecretsBackend` — implements `secret-storage::KeysStorage` for Ed25519 keys stored in a local JSON file (`~/.iota-guardian/keys.json` or configured path). Private key bytes are accessible to the CLI (MVP trade-off, documented in spec §5.8) but not exposed in the public API of this crate except via the `Signer` trait.
- `GuardianJwkStorage<B>` — implements `identity_iota::storage::JwkStorage` by delegating to any `B: KeysStorage`. `insert()` returns `KeyStorageError::Unsupported`.
- `GuardianKeyIdStorage` — implements `identity_iota::storage::KeyIdStorage` using a local JSON file for the `MethodDigest → KeyId` map.
- `GuardianStorage<B>` type alias: `identity_iota::storage::Storage<GuardianJwkStorage<B>, GuardianKeyIdStorage>`.

**Acceptance criteria**:
- `cargo test -p iota-guardian-secrets` passes
- `FileSecretsBackend::generate_key()` produces an Ed25519 keypair; calling `get_signer(key_id).sign(data)` returns a valid Ed25519 signature verifiable with the returned public key
- `GuardianJwkStorage::insert()` returns `Err(KeyStorageError)` (no panic)
- `GuardianStorage<FileSecretsBackend>` can be used with `identity_iota`'s `JwkDocumentExt::generate_method()` without errors (integration test using a local `IotaDocument` stub — no Tangle needed)
- `cargo clippy -p iota-guardian-secrets -- -D warnings` passes
- `cargo fmt --check -p iota-guardian-secrets` passes

---

## Task 2 — Certificate Verifier Service (CVS)

**Goal**: Read-only verification service. No Secrets Manager dependency — uses public keys from resolved DID Documents.

**Crate path**: `services/cvs/`

**Deliverables**:
- `POST /v1/verify` — accepts `{ verifiable_presentation: "<vp-jwt>", challenge: "uuid:..." }`, returns `VerifyResponse` (see spec §8.2)
- `GET /v1/health`
- In-memory `TrustedCertifiersCache` — loaded at startup from Super Certifier DID Document on Tangle, refreshed every `trusted_certifiers_refresh_seconds` (default 600). Startup fails with an error if the DID Document is unreachable or missing the `TrustedCertifiersRegistry` service entry.
- Config via `cvs.toml` (see spec §5.2)
- Two-phase verification logic (Phase A: VP signature; Phase B: VC signature + trust check)

**Acceptance criteria**:
- `cargo test -p cvs` passes
- Given: valid VP JWT (Ed25519-signed with known test key) + matching challenge nonce + valid VC from a certifier in the trusted list → returns `{ "valid": true, "subject_did": "...", "issuer_did": "...", ... }`
- Given: VP with wrong challenge nonce → `{ "valid": false, "reason": "challenge_mismatch" }`
- Given: VC with `exp` in the past → `{ "valid": false, "reason": "expired" }`
- Given: VC issued by a DID not in the trusted list → `{ "valid": false, "reason": "untrusted_issuer" }`
- Given: VC `sub` claim doesn't match VP `iss` claim → `{ "valid": false, "reason": "did_mismatch" }`
- Service startup fails (non-zero exit, error message logged) when Super Certifier DID Document is unavailable
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 3 — x402 Payment Facilitator

**Goal**: Verify IOTA L1 payments on the Tangle. Does not issue JWTs or generate payment challenges.

**Crate path**: `services/x402/`

**Deliverables**:
- `POST /v1/verify` — accepts x402 v2 `VerifyRequest` `{ paymentPayload, paymentRequirements }`, returns x402 v2 `SettlementResponse` (see spec §8.4)
- `GET /v1/supported` — returns `{ kinds: [{ x402Version: 2, scheme: "exact", network: "iota:rebased" }], extensions: [] }`
- `GET /v1/health`
- IOTA L1 tx lookup via `iota-sdk`: find transaction by `tx_id`, verify amount ≥ required, verify recipient address matches `payTo`, verify nonce in tx metadata matches `extra.nonce`
- Config via `x402.toml` (see spec §5.3)

**Acceptance criteria**:
- `cargo test -p x402` passes (unit tests mock `iota-sdk` calls with `wiremock`)
- Given: valid tx ID where amount, recipient, and nonce all match requirements → `{ "success": true, "transaction": "0x...", ... }`
- Given: tx not found within `payment_confirmation_timeout_seconds` → `{ "success": false, "errorReason": "payment_not_found" }`
- Given: tx found but nonce in metadata doesn't match → `{ "success": false, "errorReason": "invalid_nonce" }`
- Given: tx found but amount < required → `{ "success": false, "errorReason": "insufficient_funds" }`
- `GET /v1/supported` returns the expected JSON
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 4 — Super Certifier Admin (backend + UI)

**Goal**: Root-of-trust management. Creates Certifier DIDs, runs challenge-response, publishes Super Certifier DID Document.

**Backend crate path**: `services/super-certifier-admin/`
**Frontend app path**: `apps/super-certifier-admin/` (SvelteKit)
**Dependencies**: `iota-guardian-secrets` (Task 1)

**Backend deliverables**:
- `POST /v1/admin/certifiers` — accepts `{ public_key, name, jurisdiction }`, builds Certifier DID Document from public key, publishes to Tangle, returns `{ did, status: "pending_challenge" }`
- `GET /v1/admin/certifiers/{did}/challenge` — issues a challenge nonce; stores it against the DID record
- `POST /v1/admin/certifiers/{did}/challenge` — verifies Ed25519 signature over the nonce; on success sets status to `challenge_passed`
- `DELETE /v1/admin/certifiers/{did}` — marks certifier revoked
- `POST /v1/admin/publish` — re-signs and re-publishes the Super Certifier DID Document with current trust-eligible certifiers
- `GET /v1/admin/certifiers` — list all certifiers with status
- `GET /v1/admin/did-document` — current in-DB state of the Super Certifier DID Document
- `GET /v1/admin/health`
- PostgreSQL DB: certifiers table (did, public_key, name, jurisdiction, status, challenge_nonce, created_at, updated_at) + publish_audit_log table
- Config via `super-certifier-admin.toml`

**Frontend deliverables** (SvelteKit):
- Certifiers table page (list all certifiers, status badge, revoke button)
- Add Certifier form (public key, name, jurisdiction)
- Challenge-response step screen
- Publish button with confirmation modal
- All Tailwind CSS, Svelte 5 runes, server-side load functions

**Acceptance criteria**:
- `cargo test -p super-certifier-admin` passes
- `pnpm check` and `pnpm lint` pass in `apps/super-certifier-admin/`
- E2E API test: `POST /v1/admin/certifiers` with valid Ed25519 public key → response contains a valid `did:iota:rms:...` → `GET /v1/admin/certifiers/{did}/challenge` returns a nonce → sign nonce with corresponding private key → `POST /v1/admin/certifiers/{did}/challenge` returns `{ "status": "challenge_passed" }` → `POST /v1/admin/publish` → verify Super Certifier DID Document on Tangle (testnet) contains the new certifier DID in the `TrustedCertifiersRegistry` service entry
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 5 — Certifier Service (backend + UI)

**Goal**: Review agent registration requests, generate agent DIDs, issue VCs.

**Backend crate path**: `services/certifier-service/`
**Frontend app path**: `apps/certifier-service/` (SvelteKit)
**Dependencies**: `iota-guardian-secrets` (Task 1); shared DB with Registration App or API call to Registration App

**Backend deliverables**:
- `GET /v1/certifier/requests` — list pending requests (from shared DB or Registration App API)
- `GET /v1/certifier/requests/{uuid}` — request detail (company info, public key, KYA docs)
- `POST /v1/certifier/requests/{uuid}/approve` — generates agent DID Document from submitted public key, publishes to Tangle, signs VC with Certifier's own key via `GuardianStorage`, stores DID + VC in DB, sets request status to `APPROVED`
- `POST /v1/certifier/requests/{uuid}/reject` — accepts `{ reason }`, sets status to `REJECTED`
- `GET /v1/certifier/health`
- Config via `certifier-service.toml` (includes `signing_key_ref` for Certifier's own key)

**Frontend deliverables** (SvelteKit):
- Certification request dashboard (table: company, date, status, actions)
- Request detail page (KYA info, public key display, approve/reject buttons)
- All Tailwind CSS, Svelte 5 runes, server-side load functions

**Acceptance criteria**:
- `cargo test -p certifier-service` passes
- `pnpm check` and `pnpm lint` pass in `apps/certifier-service/`
- E2E API test: given a registration request with status `PENDING` and a valid Ed25519 public key → `POST .../approve` → agent DID exists on Tangle (testnet) → returned VC JWT has valid Ed25519 signature from the Certifier's key → `POST cvs/v1/verify` with a VP wrapping this VC returns `{ "valid": true }`
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 6 — AI Agent Registration App (backend + UI)

**Goal**: Company-facing registration, challenge-response, status polling, VC download.

**Backend crate path**: `services/registration-app/`
**Frontend app path**: `apps/registration-app/` (SvelteKit)
**Dependencies**: shared DB with Certifier Service (or API)

**Backend deliverables**:
- `POST /v1/registrations` — accepts company details + `agent_public_key`, stores in DB, returns `{ request_uuid, status: "PENDING", challenge: "uuid:..." }`
- `GET /v1/registrations/{uuid}/challenge` — issues a fresh challenge nonce
- `POST /v1/registrations/{uuid}/challenge` — verifies Ed25519 signature over nonce; on success status moves to `UNDER_REVIEW`
- `GET /v1/registrations/{uuid}` — poll status; when `APPROVED` includes `agent_did` and `vc_download_url`
- `GET /v1/credentials/{uuid}` — returns issued VC JWT (only if status is `APPROVED`)
- `GET /v1/certifiers` — returns certifiers from static TOML config (no Tangle call)
- `POST /v1/certifiers/{did}/submit` — associate request with a specific certifier
- PostgreSQL DB with corrected schema (see spec §6.3, includes `agent_public_key` and `challenge_nonce`)
- Config via `registration-app.toml` (includes `[[certifiers]]` list)

**Frontend deliverables** (SvelteKit, matching UI mockups in `architecture/ui/`):
- Screen 0 — AI Agent Registration form (company details, public key, file upload)
- Screen 2 — Status Dashboard (UUID polling, status timeline)
- Screen 3 — VC Dashboard (VC details, download button, integration guide)
- All Tailwind CSS, Svelte 5 runes, server-side load functions

**Acceptance criteria**:
- `cargo test -p registration-app` passes
- `pnpm check` and `pnpm lint` pass in `apps/registration-app/`
- E2E API test: `POST /v1/registrations` with valid public key → challenge returned → sign challenge → `POST .../challenge` → `{ "status": "verified" }` → poll `GET /v1/registrations/{uuid}` shows `UNDER_REVIEW` → (simulate Certifier approval by directly updating DB) → poll shows `APPROVED` with `vc_download_url` → `GET /v1/credentials/{uuid}` returns VC JWT
- `GET /v1/certifiers` returns the configured certifier list
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 7 — Guardian Rust Proxy

**Goal**: The only service a content creator deploys. Enforces the full request pipeline.

**Crate path**: `services/guardian/`
**Dependencies**: `iota-guardian-secrets` (Task 1); calls CVS (Task 2) and x402 (Task 3) over HTTP

**Deliverables**:
- Full request pipeline per spec §5.1:
  1. JWT fast path: verify `X-Guardian-Token` locally (Ed25519) → forward on success
  2. Opt-in gate: no `X-Agent-VP` → pass through to origin unchanged
  3. Identity gate: `X-Agent-VP` present → call `POST cvs/v1/verify` → on failure return `403`; on success → proceed to payment gate
  4. Payment gate: no valid `X-Guardian-Token` → build x402 v2 `PAYMENT-REQUIRED` header → return `402`
  5. Payment verification: `X-Agent-VP` + `PAYMENT-SIGNATURE` present → call `POST x402/v1/verify` → on success: issue `X-Guardian-Token` JWT (Ed25519, 5-min TTL), forward to origin, return with `X-Guardian-Token` + `PAYMENT-RESPONSE` headers
- Ed25519 JWT signing/verification via `iota-guardian-secrets` `FileSecretsBackend`
- Reverse proxy: forward requests to configured `origin`, return responses to client
- Config via `guardian.toml` (see spec §5.1)
- `GET /admin/health`, `GET /admin/stats`

**Acceptance criteria**:
- `cargo test -p guardian` passes (unit tests mock CVS and x402 with `wiremock`)
- JWT fast path: request with valid `X-Guardian-Token` (correct signature, not expired) → forwarded to origin; no CVS or x402 call made
- JWT fast path: expired token → `401` with fresh challenge nonce
- No `X-Agent-VP` header → request forwarded to origin unchanged
- `X-Agent-VP` + valid VP (mocked CVS returning `{valid: true}`) → `402` with `PAYMENT-REQUIRED` header; nonce in header matches the nonce from the `401` challenge
- `X-Agent-VP` + valid VP + `PAYMENT-SIGNATURE` + valid payment (mocked x402 returning `{success: true}`) → `200` response with `X-Guardian-Token` header; token parses correctly with expected claims (`sub`, `iss`, `tx`, `path`, `exp`)
- Full live E2E flow (testnet): `iota-guardian-agent fetch` against a Guardian protecting a local origin → `200` with content
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 8 — AI Agent CLI

**Goal**: Reference implementation and hackathon demo tool. The only place the agent's private key lives.

**Crate path**: `cli/`
**Dependencies**: `iota-guardian-secrets` (Task 1)

**Deliverables**:
- `iota-guardian-agent setup` — generates Ed25519 keypair via `FileSecretsBackend`, writes `~/.iota-guardian-agent/config.toml`, prints public key (hex)
- `iota-guardian-agent register --registration-url <url>` — interactive or flag-driven company detail submission, challenge-response loop against Registration App, polls until `APPROVED`, downloads VC to configured path
- `iota-guardian-agent fetch <url>` — full bootstrap flow:
  - Step 1: send request with `X-Guardian-Token` if cached and valid
  - Step 2: on `401`, sign VP JWT (local, `jsonwebtoken` + `FileSecretsBackend`), retry with `X-Agent-VP`
  - Step 3: on `402`, send IOTA L1 transfer (nonce in tx metadata), retry with `X-Agent-VP` + `PAYMENT-SIGNATURE`
  - Cache received `X-Guardian-Token`; print response body to stdout
- `iota-guardian-agent status` — prints DID, public key, VC expiry, cached token TTL
- Config file: `~/.iota-guardian-agent/config.toml` (see spec §5.8)

**Acceptance criteria**:
- `cargo test -p iota-guardian-agent` passes
- `setup`: keypair file created; public key printed
- `fetch <url>` bootstrap flow: request → `401` → VP signed → `402` → IOTA payment sent → `200` with body printed; `X-Guardian-Token` cached to disk
- `fetch <url>` second call within 5 minutes: request sent with `X-Guardian-Token` only; no new `401`/`402` round-trips
- `status`: correct DID, non-expired VC, token TTL displayed
- `cargo clippy` and `cargo fmt --check` pass

---

## Task 9 — Docker Compose

**Goal**: One-command startup of all services for local development and hackathon demo.

**File**: `docker-compose.yml` in repo root

**Deliverables**:
- Services: Guardian (8080), CVS (8081), x402 (8082), Super Certifier Admin (8083), Certifier Service (8084), Registration App (8085 backend, 5173 frontend), PostgreSQL (5432)
- Auto-generated `dev-keys.json` on first run (if absent)
- Environment variable documentation in a top-level `COMPOSE.md` or inline comments
- Health checks for all services

**Acceptance criteria**:
- `docker compose up` starts all services with zero manual steps
- All health check endpoints (`GET /*/health`) return `200` within 30 seconds of `docker compose up`
- `iota-guardian-agent fetch http://localhost:8080/content/test` completes end-to-end successfully (requires testnet IOTA node)
- `docker compose down` cleanly stops all services

---

## Task 10 — WordPress Plugin (secondary MVP)

**Goal**: Zero-server-access integration for WordPress operators.

**Directory**: `plugins/iota-guardian-wp/`

**Deliverables**:
- Plugin header conformant to WordPress Plugin Directory requirements
- Settings page (WP Admin → Settings → IOTA Web Guardian): wallet address, price, URL patterns, CVS endpoint, x402 endpoint
- HMAC-SHA256 key generated on first activation, stored in `wp_options`
- `template_redirect` hook: opt-in model — requests without `X-Agent-VP` pass through; requests with `X-Agent-VP` go through the full pipeline
- `rest_pre_dispatch` hook for REST API routes
- JWT verify (HMAC-SHA256) using `firebase/php-jwt`
- CVS and x402 calls via WordPress `wp_remote_post()`

**Acceptance criteria**:
- Plugin activates on WordPress 6.x without PHP errors or warnings
- A request to a protected path without `X-Agent-VP` returns the normal WordPress page (200)
- A request with `X-Agent-VP` but invalid VP (mocked CVS response) returns 403
- A request with a valid `X-Guardian-Token` HMAC-SHA256 JWT (signed with the stored secret, not expired) is passed through

---

## Task 9 — Multi-Agent CLI Support

**Goal**: Allow multiple independent agent identities to coexist on the same machine. Each identity has its own Ed25519 keypair, DID, and VC. Fix two backend race conditions exposed when agents submit concurrent requests.

**Files changed**:

| File | Change |
|---|---|
| `cli/src/agent_config.rs` | `profile_dir(profile)`, `load(profile)`, `save(profile, cfg)`, `migrate_legacy_profile()`, `profiles_root()` |
| `cli/src/main.rs` | `--profile <name>` global arg; `list-profiles` subcommand; migration call at startup |
| `cli/src/commands/setup.rs` | Accept and pass `profile` parameter |
| `cli/src/commands/register.rs` | Accept and pass `profile` parameter |
| `cli/src/commands/fetch.rs` | Accept and pass `profile` parameter (including `handle_payment`) |
| `services/registration-app/src/db.rs` | `verify_challenge(pool, id, nonce)` — atomic consume via `AND challenge_nonce = $2` |
| `services/registration-app/src/routes/registrations.rs` | Pass nonce to DB call; return 410 if already consumed |
| `services/registration-app/src/main.rs` | `max_connections(5)` → `max_connections(20)` |
| `services/certifier-service/src/db.rs` | `begin_approval()` and `revert_approval()` — atomic status transitions |
| `services/certifier-service/src/routes/requests.rs` | Use `begin_approval` guard; `revert_approval` on IOTA publish failure |
| `services/certifier-service/src/main.rs` | `max_connections(5)` → `max_connections(20)` |
| `spec/spec.md` | Added §13 Multi-Agent Support |
| `spec/coding-plan.md` | Added Task 9 (this section) |

**Acceptance criteria**:
- `cargo check --workspace` compiles clean
- `iota-guardian-agent --profile acme-bot setup` creates `~/.iota-guardian-agent/profiles/acme-bot/`
- `iota-guardian-agent list-profiles` lists all profiles with DID and VC status
- Legacy flat layout is auto-migrated to `profiles/default/` on first run
- Concurrent `POST /v1/registrations/:id/challenge` for the same ID → only one succeeds; second gets 410
- Concurrent `POST /v1/requests/:id/approve` for the same ID → only one proceeds with DID publish; second gets 409

---

## Commit Strategy

- Each task gets one or more commits
- Each commit covers one logical unit of work (e.g. "Add FileSecretsBackend", "Add GuardianJwkStorage adapter")
- Run `cargo fmt --all` + `cargo clippy --all --all-features --all-targets -- -D warnings` before each Rust commit
- Run `pnpm check` + `pnpm lint` before each SvelteKit commit
- Never push to remote without explicit user instruction
