Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion deploy/rockbot-seed/agent-trust.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions deploy/rockbot-seed/well-known-agents.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions docs/framework-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading