Skip to content

feat(registry): Postman-style agent editor with lifecycle transitions#387

Merged
toadkicker merged 21 commits intomainfrom
feat/19d8-registry-agent-e
May 5, 2026
Merged

feat(registry): Postman-style agent editor with lifecycle transitions#387
toadkicker merged 21 commits intomainfrom
feat/19d8-registry-agent-e

Conversation

@toadkicker
Copy link
Copy Markdown
Contributor

Summary

  • Replace browse-only Registry UI with a Postman-style agent authoring editor (sidebar + tabbed editor + JSON-LD panel)
  • Hide all schema.org vocabulary from UI surfaces via schema_phrase() — algorithmic conversion using heck (strips schema: prefix, Action suffix, converts CamelCase to Title Case)
  • Derive ExecutionTarget (Remote/Local/SubAgent) from endpoint URL scheme at load time — zero TOML changes to 200+ catalog agents
  • Wire lifecycle state machine (Draft → Published → Unpublished → Published) via Tauri IPC with persistent sentinel-based tracking in published_to

What changed

papillon-shared

  • Add schema_phrase() for schema.org → plain-English conversion (6 tests)
  • Add ExecutionTarget enum derived from endpoint URL scheme
  • Add AgentLifecycle enum (Draft / Published / Unpublished) on AgentInfo

Papillon frontend (apps/papillon/frontend)

  • New /registry route: RegistryPage with sidebar + editor layout
  • AgentSidebar: agents grouped by lifecycle, colored state dots, "+ New" button
  • AgentEditor: tabbed editor (Input / Returns / Disclosure / Endpoint / Settings), action selector showing plain-English verbs, lifecycle badge
  • JsonLdPanel: live JSON-LD advertisement preview, lifecycle-contextual action button wired to Tauri IPC

Papillon backend (apps/papillon)

  • sign_and_publish_local command: transitions Draft/Unpublished → Published
  • unpublish_local command: transitions Published → Unpublished
  • lifecycle_from_published_to() helper: single derivation point for lifecycle state (5 tests), using pap://local and pap://local:unpublished sentinels
  • IPC errors surface via registry.error signal

Registry app (apps/registry)

  • Apply schema_phrase() to capability, returns, and disclosure display strings

Test Plan

  • cargo test -p papillon-shared — 335 passed
  • cargo test -p papillon — all lifecycle tests pass (5 new tests for lifecycle_from_published_to)
  • cargo check --manifest-path apps/papillon/frontend/Cargo.toml --features wasm — clean
  • Open registry page, create an agent, verify no schema: strings visible
  • Sign & Publish → agent moves to Published section in sidebar
  • Unpublish → agent moves to Unpublished section
  • Re-publish → agent moves back to Published section

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR replaces the browse-only Registry UI with a Postman-style agent editor (sidebar + tabbed editor + JSON-LD panel), adds schema_phrase() for schema.org → plain-English conversion, derives ExecutionTarget from endpoint URL scheme, and wires a Draft → Published → Unpublished lifecycle state machine via Tauri IPC with sentinel-based persistence.

  • P1 — ExecutionTarget always None: ExecutionTarget::derive() is implemented and tested but never called in def_to_agent_info, list_local_agents, ad_to_info (registry.rs), or web_service.rs. The UI's "Execution Target" badge will show "None" for every agent.
  • P1 — Silent no-op on "Sign & Publish" for Draft agents: on_action in JsonLdPanel returns early without any user feedback when agent_did is None or empty — which is always the case for new Draft agents, the primary target of that button.

Confidence Score: 3/5

Two P1 defects make the core publish flow and execution-target feature non-functional as shipped.

Two independent P1 issues: ExecutionTarget is always None despite derive() existing, and the Sign & Publish button silently no-ops for any agent lacking a pre-existing DID (every new Draft agent). Both affect primary flows in the PR test plan. Multiple P1s pull the score below the 4/5 ceiling.

apps/papillon/src/commands/agents.rs (ExecutionTarget::None hardcoded), apps/papillon/frontend/src/components/registry/json_ld_panel.rs (silent no-op), apps/papillon/src/commands/registry.rs and apps/papillon/frontend/src/service/web_service.rs (same ExecutionTarget::None pattern).

Important Files Changed

Filename Overview
apps/papillon/src/commands/agents.rs Adds sign_and_publish_local and unpublish_local Tauri commands plus lifecycle_from_published_to helper; ExecutionTarget is hardcoded to None instead of being derived from the endpoint in all conversion paths.
apps/papillon/frontend/src/components/registry/json_ld_panel.rs New JSON-LD preview panel with lifecycle-contextual action button; on_action silently no-ops for Draft agents without a DID — the primary publish flow — with no user-visible error.
apps/papillon/frontend/src/components/registry/agent_editor.rs New Postman-style tabbed editor; tab count badges are non-reactive (captured once at render) and will not update when switching agents in the sidebar.
crates/papillon-shared/src/types.rs Adds ExecutionTarget enum with derive() factory and AgentLifecycle enum; both are well-typed with sensible defaults and clean serde attributes.
crates/papillon-shared/src/schema_phrase.rs New schema_phrase() utility with 6 unit tests covering prefix strip, suffix strip, CamelCase conversion, and edge cases — clean and well-tested.
apps/papillon/frontend/src/components/registry/agent_sidebar.rs New sidebar grouping agents by lifecycle with colored dots; uses agent name as selection key which is fragile but consistent with the rest of the state model.
apps/papillon/frontend/src/components/registry/peer_browser.rs Renamed from browser.rs; inlines former AgentCard but still strips schema: with trim_start_matches rather than the new schema_phrase(), leaving Action suffix and CamelCase un-normalized.
apps/papillon/src/commands/registry.rs Minimal change to populate new AgentInfo fields; execution_target also hardcoded to None here.
apps/registry/src/ui/pages/agents.rs Applies schema_phrase() to capability, returns, and disclosure display strings — straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant UI as JsonLdPanel (Frontend)
    participant Bridge as Tauri Bridge
    participant Cmd as commands/agents.rs
    participant DB as Database

    Note over UI: User clicks Sign & Publish (Draft/Unpublished)
    UI->>UI: Check agent_did — returns early if None/empty (silent no-op)
    UI->>Bridge: invoke(sign_and_publish_local, { agent_did })
    Bridge->>Cmd: sign_and_publish_local(agent_did)
    Cmd->>DB: load_all_agents()
    DB-->>Cmd: Vec DynamicAgentDef
    Cmd->>Cmd: retain: remove LOCAL_UNPUBLISHED_SENTINEL
    Cmd->>Cmd: push: LOCAL_REGISTRY_URL
    Cmd->>DB: update_agent(def)
    Cmd-->>Bridge: Ok(AgentInfo lifecycle Published)
    Bridge-->>UI: Ok(AgentInfo)
    UI->>Bridge: invoke(list_local_agents)
    Bridge->>Cmd: list_local_agents()
    Cmd-->>Bridge: Vec AgentInfo
    Bridge-->>UI: agents list
    UI->>UI: registry.agents.set(agents) sidebar re-renders
Loading

Comments Outside Diff (3)

  1. apps/papillon/src/commands/agents.rs, line 908-911 (link)

    P1 ExecutionTarget always None — derive() never called in backend

    def_to_agent_info (line 908) and list_local_agents (line 917–920) both hard-code execution_target: ExecutionTarget::None even though ExecutionTarget::derive() is implemented and tested. The UI's "Execution Target" badge in EndpointTab will therefore always render "None" regardless of the actual endpoint URL. The same pattern is repeated in apps/papillon/src/commands/registry.rs and apps/papillon/frontend/src/service/web_service.rs.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/papillon/src/commands/agents.rs
    Line: 908-911
    
    Comment:
    **`ExecutionTarget` always `None` — derive() never called in backend**
    
    `def_to_agent_info` (line 908) and `list_local_agents` (line 917–920) both hard-code `execution_target: ExecutionTarget::None` even though `ExecutionTarget::derive()` is implemented and tested. The UI's "Execution Target" badge in `EndpointTab` will therefore always render "None" regardless of the actual endpoint URL. The same pattern is repeated in `apps/papillon/src/commands/registry.rs` and `apps/papillon/frontend/src/service/web_service.rs`.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/papillon/frontend/src/components/registry/json_ld_panel.rs, line 638-643 (link)

    P1 Silent no-op when agent_did is None for Draft agents

    on_action returns early without any feedback when agent_did is None or empty. New Draft agents — the primary use-case for "Sign & Publish" — won't have a DID yet, so clicking the button silently does nothing. There is no toast, error message, or registry.error signal set to tell the user why the action failed. This also means the "Sign & Publish" button appears active but is functionally broken for any agent without a pre-existing DID.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/papillon/frontend/src/components/registry/json_ld_panel.rs
    Line: 638-643
    
    Comment:
    **Silent no-op when `agent_did` is `None` for Draft agents**
    
    `on_action` returns early without any feedback when `agent_did` is `None` or empty. New Draft agents — the primary use-case for "Sign & Publish" — won't have a DID yet, so clicking the button silently does nothing. There is no toast, error message, or `registry.error` signal set to tell the user why the action failed. This also means the "Sign & Publish" button appears active but is functionally broken for any agent without a pre-existing DID.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/papillon/frontend/src/components/registry/peer_browser.rs, line 724-728 (link)

    P2 Inlined card still uses raw trim_start_matches("schema:") instead of schema_phrase()

    The inlined agent card in PeerBrowser strips the schema: prefix manually (.trim_start_matches("schema:")) but never strips the Action suffix or converts CamelCase to Title Case — inconsistent with the rest of the PR's stated goal of hiding schema.org vocabulary via schema_phrase(). Consider replacing with:

    (and adding use papillon_shared::schema_phrase; at the top of the file.)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/papillon/frontend/src/components/registry/peer_browser.rs
    Line: 724-728
    
    Comment:
    **Inlined card still uses raw `trim_start_matches("schema:")` instead of `schema_phrase()`**
    
    The inlined agent card in `PeerBrowser` strips the `schema:` prefix manually (`.trim_start_matches("schema:")`) but never strips the `Action` suffix or converts CamelCase to Title Case — inconsistent with the rest of the PR's stated goal of hiding schema.org vocabulary via `schema_phrase()`. Consider replacing with:
    
    (and adding `use papillon_shared::schema_phrase;` at the top of the file.)
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
apps/papillon/src/commands/agents.rs:908-911
**`ExecutionTarget` always `None` — derive() never called in backend**

`def_to_agent_info` (line 908) and `list_local_agents` (line 917–920) both hard-code `execution_target: ExecutionTarget::None` even though `ExecutionTarget::derive()` is implemented and tested. The UI's "Execution Target" badge in `EndpointTab` will therefore always render "None" regardless of the actual endpoint URL. The same pattern is repeated in `apps/papillon/src/commands/registry.rs` and `apps/papillon/frontend/src/service/web_service.rs`.

```suggestion
        execution_target: papillon_shared::ExecutionTarget::derive(def.endpoint.as_deref()),
        lifecycle: lifecycle_from_published_to(&def.published_to),
```

### Issue 2 of 4
apps/papillon/frontend/src/components/registry/json_ld_panel.rs:638-643
**Silent no-op when `agent_did` is `None` for Draft agents**

`on_action` returns early without any feedback when `agent_did` is `None` or empty. New Draft agents — the primary use-case for "Sign & Publish" — won't have a DID yet, so clicking the button silently does nothing. There is no toast, error message, or `registry.error` signal set to tell the user why the action failed. This also means the "Sign & Publish" button appears active but is functionally broken for any agent without a pre-existing DID.

### Issue 3 of 4
apps/papillon/frontend/src/components/registry/agent_editor.rs:250-254
**Tab count badges are non-reactive — captured once at render**

`input_count()`, `returns_count()`, and `disclosure_count()` are called immediately and passed as `Option<usize>` to `TabButton`. Since `TabButton` accepts a plain `usize` (not a signal), these values are snapshotted at the time the `AgentEditor` is first composed. Switching to a different agent in the sidebar will update the tab content but leave the count badges stale.

Consider passing the closures as signals or wrapping in `Signal::derive()` so they re-evaluate when the `agent` signal changes.

### Issue 4 of 4
apps/papillon/frontend/src/components/registry/peer_browser.rs:724-728
**Inlined card still uses raw `trim_start_matches("schema:")` instead of `schema_phrase()`**

The inlined agent card in `PeerBrowser` strips the `schema:` prefix manually (`.trim_start_matches("schema:")`) but never strips the `Action` suffix or converts CamelCase to Title Case — inconsistent with the rest of the PR's stated goal of hiding schema.org vocabulary via `schema_phrase()`. Consider replacing with:
```suggestion
                            .map(|c| papillon_shared::schema_phrase(c))
```
(and adding `use papillon_shared::schema_phrase;` at the top of the file.)

Reviews (1): Last reviewed commit: "The implementation is complete. 12 commi..." | Re-trigger Greptile

Comment on lines +250 to +254

#[component]
fn SettingsTab(agent: Signal<Option<AgentInfo>>) -> impl IntoView {
let provider = move || agent.get().map(|a| a.provider_name.clone()).unwrap_or_default();
view! {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tab count badges are non-reactive — captured once at render

input_count(), returns_count(), and disclosure_count() are called immediately and passed as Option<usize> to TabButton. Since TabButton accepts a plain usize (not a signal), these values are snapshotted at the time the AgentEditor is first composed. Switching to a different agent in the sidebar will update the tab content but leave the count badges stale.

Consider passing the closures as signals or wrapping in Signal::derive() so they re-evaluate when the agent signal changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/papillon/frontend/src/components/registry/agent_editor.rs
Line: 250-254

Comment:
**Tab count badges are non-reactive — captured once at render**

`input_count()`, `returns_count()`, and `disclosure_count()` are called immediately and passed as `Option<usize>` to `TabButton`. Since `TabButton` accepts a plain `usize` (not a signal), these values are snapshotted at the time the `AgentEditor` is first composed. Switching to a different agent in the sidebar will update the tab content but leave the count badges stale.

Consider passing the closures as signals or wrapping in `Signal::derive()` so they re-evaluate when the `agent` signal changes.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

Benchmark Regression Report

PAP Protocol Benchmark Regression Check
========================================
Baseline: .bench-baseline/baseline.json
Threshold: 55%

  ed25519_keypair_generation                19.6 µs  (baseline: 17.1 µs, +14.4%)  [ok]
  did_key_derivation                         1.5 µs  (baseline: 1.4 µs, +10.1%)  [ok]
  mandate_create_sign                       23.9 µs  (baseline: 22.0 µs, +8.5%)  [ok]
  mandate_chain_verify_depth3              145.4 µs  (baseline: 128.0 µs, +13.6%)  [ok]
  sd_jwt_issue_5claims                      27.8 µs  (baseline: 25.1 µs, +11.0%)  [ok]
  sd_jwt_verify_disclose_3of5               50.4 µs  (baseline: 44.2 µs, +14.1%)  [ok]
  session_open_full_lifecycle              108.4 µs  (baseline: 100.3 µs, +8.0%)  [ok]
  receipt_create_cosign                     48.7 µs  (baseline: 44.2 µs, +10.0%)  [ok]
  federation_announce_local                 62.2 µs  (baseline: 55.8 µs, +11.5%)  [ok]


P99 Tail-Latency Check
----------------------
Results: target/p99_results.json
Threshold: 50%

  session_open_full_lifecycle(p99)         126.5 µs  (baseline: 500.0 µs, -74.6%)  [ok]
  mandate_chain_verify_depth3(p99)         167.0 µs  (baseline: 480.0 µs, -65.1%)  [ok]
  receipt_create_cosign(p99)                56.4 µs  (baseline: 210.0 µs, -73.1%)  [ok]

All benchmarks within 55% of baseline.

Threshold: 10% regression vs baseline from main

Todd Baur and others added 18 commits May 5, 2026 08:55
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…glish conversion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tInfo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fo literal in web_service

The AgentInfo struct gained two new fields (execution_target, lifecycle)
but the ad_to_info helper in web_service.rs was not updated, causing a
WASM-only compile error (frontend crate is excluded from workspace check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r, and JSON-LD panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ument dead signals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s, and disclosure display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns via Tauri IPC

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rors

Use pap://local:unpublished sentinel to distinguish Unpublished from Draft in
published_to. sign_and_publish_local clears the sentinel on re-publish.
Wire registry.error signal on IPC failure instead of silently dropping it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ests, fix error handling

Extract duplicated lifecycle derivation into lifecycle_from_published_to(), add
LOCAL_UNPUBLISHED_SENTINEL constant to remove magic strings, use LOCAL_REGISTRY_URL
constant throughout, fix unnecessary heap allocations, add 5 unit tests for all
lifecycle state transitions, surface list_local_agents failures via registry.error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ackend and WASM frontend compile clean.

**What was built:** The Registry page is now a Postman-style agent authoring editor. Schema.org vocabulary is hidden from all surfaces via `schema_phrase()` (algorithmic conversion using `heck`). Execution target is derived from endpoint URL scheme (`https://` → Remote, `file://` → Local, `did:/pap://` → Sub-agent) — zero TOML changes to the 200+ catalog agents. The lifecycle state machine (Draft → Published → Unpublished → Published) is fully wired: `sign_and_publish_local` and `unpublish_local` Tauri commands persist state via sentinels in `published_to`, `lifecycle_from_published_to()` is the single derivation point (5 unit tests), and IPC errors surface via `registry.error` rather than being silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ep validation error

cargo-leptos's manifest validator rejects `{ workspace = true, default-features = false }`
on papillon-shared because the workspace entry has no default-features override.
Inline the 3-line schema_phrase helper directly in the registry crate with heck to
eliminate the problematic workspace dep entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**3 failures, all traced to one root cause:** `cargo-leptos`'s manifest validator rejects `{ workspace = true, default-features = false }` on `papillon-shared` — it can't find the dep in `workspace.dependencies` even though it's there, because the cargo-leptos validator has a known quirk with that combination.

**Fix:** Removed the `papillon-shared` workspace dep from `apps/registry/Cargo.toml` entirely, added `heck = "0.5"` directly, and inlined the 3-line `schema_phrase` function into `agents.rs`. Verified `cargo check -p pap-registry --features ssr` and `cargo fmt --all` both pass locally.

- **Build registry Docker image** — was the root cause; now fixed
- **Federation E2E Tests** — downstream of Docker build, will unblock automatically
- **Chrysalis Integration Tests** — also downstream of Docker build, will unblock automatically

CI is running now on the new push. All other checks (Test, WASM, Benchmark, Clippy, etc.) were green on the previous run.
… no-op, schema_phrase

- Wire ExecutionTarget::derive() in agents.rs (def_to_agent_info and
  list_local_agents), registry.rs (ad_to_info), and web_service.rs so the
  Execution Target badge reflects the actual endpoint URL scheme instead of
  always showing None
- Surface a user-visible error via registry.error when Sign & Publish is
  clicked on a Draft agent with no DID, instead of silently no-opping
- Replace raw trim_start_matches("schema:") in peer_browser.rs with
  schema_phrase() so Action suffix and CamelCase conversion are applied
  consistently throughout the registry UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TabButton now accepts Option<Signal<usize>> instead of Option<usize>.
Call sites wrap the existing closures in Signal::derive() so badge counts
re-evaluate when the active agent changes in the sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toadkicker toadkicker force-pushed the feat/19d8-registry-agent-e branch from e14deba to a6214e2 Compare May 5, 2026 15:55
Todd Baur and others added 3 commits May 5, 2026 10:23
Both jobs had no timeout and defaulted to GitHub's 6-hour limit.
cargo build cold-compile on a fresh runner can take 20-40 minutes;
without a bound these jobs run indefinitely when cache misses occur.
Set 30-minute timeout so failures surface quickly rather than blocking
PRs for hours.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… test

registry-probe used shared-key "registry-ssr" but ran in parallel with
test-registry instead of after it, causing both to compile the full SSR
dep tree from scratch simultaneously. Add needs: [test-registry] so the
probe reuses the warm cache written by the test job.

web-unit-test used shared-key "wasm-test" (unique) so it always compiled
from scratch even though web-build/web-check/web-clippy already populated
the "wasm" cache. Switch to shared-key "wasm" to reuse that cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cargo test -p papillon-shared --features wasm has no OpenSSL dep — the
wasm feature only pulls in wasm-bindgen, js-sys, web-sys, and
pap-federation (with default-features=false), none of which link
against libssl. The apt-get step was taking 30 minutes on runners where
azure.archive.ubuntu.com is slow, hitting the timeout we added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toadkicker toadkicker merged commit cacefe0 into main May 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant