diff --git a/CLAUDE.md b/CLAUDE.md index d24def5..2b5d1c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Status -Foragent is at **milestone 4** (spec §9.1): three capabilities now — `fetch-page-title` (step 2, Playwright), `extract-structured-data` (step 3, Playwright + LLM), and `post-to-site` (step 4, Playwright + credential broker). The credential broker + first `ISitePoster` (Bluesky) are in. Storage-state persistence, 2FA input-required flow, k8s-secrets broker, and per-tenant credential namespaces are all deferred — tracked in `docs/framework-feedback.md` step 4. The authoritative design document is `docs/foragent-specification.md` — read it before making non-trivial changes. Framework-level observations from each milestone are captured in `docs/framework-feedback.md`. +Foragent is at **milestone 5** (spec §9.1): the A2A surface is wired end-to-end against RockBot as the first real user via the `docker-compose.yml` harness, pinned to `rockylhotka/rockbot-agent:0.8.5`. Three capabilities are exercised — `fetch-page-title` (step 2, Playwright), `extract-structured-data` (step 3, Playwright + LLM), and `post-to-site` (step 4, Playwright + credential broker). Validation was scoped to "poster dispatches" — real Bluesky posting requires populating `FORAGENT_BLUESKY_*` in `.env` and is not yet covered by the milestone. Storage-state persistence, 2FA input-required flow, k8s-secrets broker, and per-tenant credential namespaces are still deferred — tracked in `docs/framework-feedback.md` step 4. The authoritative design document is `docs/foragent-specification.md` — read it before making non-trivial changes. Framework-level observations from each milestone are captured in `docs/framework-feedback.md`. ## Build / test diff --git a/deploy/rockbot-seed/agent-trust.json b/deploy/rockbot-seed/agent-trust.json index b30002f..0466fa6 100644 --- a/deploy/rockbot-seed/agent-trust.json +++ b/deploy/rockbot-seed/agent-trust.json @@ -2,7 +2,7 @@ { "agentId": "Foragent", "level": 4, - "approvedSkills": ["fetch-page-title", "extract-structured-data"], + "approvedSkills": ["fetch-page-title", "extract-structured-data", "post-to-site"], "firstSeen": "2026-04-21T00:00:00+00:00", "lastInteraction": "2026-04-21T00:00:00+00:00", "interactionCount": 0 diff --git a/deploy/rockbot-seed/well-known-agents.json b/deploy/rockbot-seed/well-known-agents.json index 8922bea..6f6e3d4 100644 --- a/deploy/rockbot-seed/well-known-agents.json +++ b/deploy/rockbot-seed/well-known-agents.json @@ -17,6 +17,11 @@ "id": "extract-structured-data", "name": "Extract Structured Data", "description": "Navigate to a URL and extract data matching a natural-language description, returning JSON. Input the target URL and a description of what to extract." + }, + { + "id": "post-to-site", + "name": "Post to Site", + "description": "Authenticate against a configured site (by credential identifier) and publish a post. Input JSON {\"site\":\"bluesky\",\"credentialId\":\"...\",\"content\":\"...\"} or message metadata fields site / credentialId / content. Credential values never cross the A2A boundary." } ] } diff --git a/docker-compose.yml b/docker-compose.yml index 32e1e83..0611c56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ # - foragent — this project; exposes HTTP A2A on port 5210 # - rockbot-init — seeds /data/agent with RockBot profile + well-known-agents.json # pointing at foragent -# - rockbot — rockylhotka/rockbot-agent:latest, configured to know Foragent +# - rockbot — rockylhotka/rockbot-agent:0.8.5, configured to know Foragent # as an A2A peer it can delegate tasks to # - blazor — rockylhotka/rockbot-blazor:latest, web UI for chatting with # the rockbot agent. Open http://localhost:8080 to test. @@ -61,7 +61,7 @@ services: RabbitMq__VirtualHost: / Gateway__AgentName: Foragent Gateway__InternalAgentName: Foragent - Gateway__Description: "Browser agent — fetch-page-title in step 1" + Gateway__Description: "Browser agent — fetch-page-title, extract-structured-data, post-to-site" # RockBot will call Foragent with header X-Api-Key: rockbot-calls-foragent ApiKeys__rockbot-calls-foragent__AgentId: RockBot ApiKeys__rockbot-calls-foragent__DisplayName: RockBot @@ -81,7 +81,7 @@ services: Credentials__bluesky-rocky__Values__password: ${FORAGENT_BLUESKY_APP_PASSWORD:-} rockbot-init: - image: rockylhotka/rockbot-agent:latest + image: rockylhotka/rockbot-agent:0.8.5 user: root entrypoint: ["/bin/sh", "-c"] command: @@ -117,7 +117,7 @@ services: - ./deploy/rockbot-seed:/seed:ro rockbot: - image: rockylhotka/rockbot-agent:latest + image: rockylhotka/rockbot-agent:0.8.5 depends_on: rockbot-init: condition: service_completed_successfully diff --git a/docs/framework-feedback.md b/docs/framework-feedback.md index 41703de..078649d 100644 --- a/docs/framework-feedback.md +++ b/docs/framework-feedback.md @@ -164,6 +164,76 @@ design in a way that allows adding it without breaking changes: poster, promote the URL to config and add an allowlist check around `IBrowserSession.OpenPageAsync`. +## Step 5 — RockBot as Foragent's first real user + +Validation loop per spec §9.1 step 5. The goal is not new capabilities — it's +to watch the end-to-end A2A path (RockBot → Foragent → response) actually +run, with RockBot the agent standing in for a real caller. + +### What was exercised + +Ran `docker compose up --build` against pinned `rockylhotka/rockbot-agent:0.8.5` +(latest framework release shipping the metadata pass-through and gateway +packaging fixes from steps 1/3). Directly curled Foragent through the HTTP +A2A gateway for all three skills: + +- `fetch-page-title` against `https://example.com` → `"Example Domain"`. +- `extract-structured-data` with JSON body → LLM returned `{"heading":"Example Domain"}`. +- `post-to-site` with unknown site → safe dispatcher error listing known sites. +- `post-to-site` with `bluesky-rocky` id not present in the broker → clean + `"Credential 'bluesky-rocky' is not configured."` No credential id in the + response (only the requested id, which the caller already knows), no + exception surface, no leaked site internals. Warning log in Foragent + carries the id for operator debugging. + +Bluesky posting against the real site is intentionally deferred — this +milestone was scoped to "poster dispatches" (capability → broker lookup → +ISitePoster dispatch). The real-post path will be exercised once the user +populates `FORAGENT_BLUESKY_IDENTIFIER` / `FORAGENT_BLUESKY_APP_PASSWORD` +in `.env`. + +### Framework observations + +- **RockBot's peer registry shadows the peer's agent-card instead of + discovering from it.** Filed as + [rockbot#287](https://github.com/MarimerLLC/rockbot/issues/287). The + A2A v1 spec defines `/.well-known/agent-card.json` as the authoritative + skill list, but RockBot's `well-known-agents.json` carries a *full* + duplicate copy of each peer's skills (id, name, description). This is + a framework-side flaw, not a Foragent one — Foragent already collapses + its own internal duplication to a single `ForagentCapabilities.Skills` + source. Consequence: adding a capability to Foragent requires editing + the peer's seed file, not just Foragent's code. The registry should + carry only the peer coordinates (`url`, auth config) and let the A2A + client fetch + cache the card at first contact. +- **`agent-trust.json` reseed is an all-or-nothing hammer.** The init + script uses `[ ! -s /data/agent/agent-trust.json ]` to guard against + overwriting user-curated trust. Reasonable default, but it means + adding a new Foragent skill requires wiping the `rockbot-data` volume + — which also wipes memory, conversations, and feedback. Distinct from + #287: `approvedSkills` is legitimately RockBot-owned local policy and + shouldn't be auto-discovered, but the reconciliation flow (peer adds + a skill → operator decides whether to approve) should not require a + destructive volume wipe. +- **Caller identity flows cleanly on the bus side.** The + `ApiKeys__rockbot-calls-foragent__AgentId: RockBot` env-var maps the + HTTP API key to a caller id, and `AgentTaskContext.MessageContext.Agent` + carries `"RockBot"` through to the capability. When per-tenant credential + scoping lands (spec §7.5), this is where tenant resolution belongs — + no changes needed to the A2A wire format. +- **RockBot 0.8.5 started clean on first boot of the step-4 harness** — + previous milestones had a `mcp.json` deserialization warning on cold + start that self-resolves; still present but not blocking. Worth + mentioning so the next session doesn't chase it. + +### Not yet exercised in this step + +- **RockBot LLM-driven invocation of Foragent via the blazor UI.** The + direct-curl path validates the A2A server surface end-to-end, but the + "RockBot reasons its way to `invoke_agent` based on user chat" path was + not driven in this session. The harness is running (blazor on :8080) + for ad-hoc validation outside the milestone PR. + ## Step 3 — Second capability (extract-structured-data) - **A2A metadata pass-through.** Filed as [rockbot#281](https://github.com/MarimerLLC/rockbot/issues/281),