Renames the fast-path option to the cleaner Local across the stack (it's
still unreleased) and adds a service-level knob:
- client.Local() (was client.LocalDispatch) enables the in-process
fast-path; Options.Local is the field. The internal/network package it
dispatches through is the "local network".
- service.Local() (aliased micro.Local()) turns it on for a whole
service's client in one place — every co-located unary call (agent tool
calls, flow dispatch, gateway -> service) takes the fast-path, no
per-call wiring. Same o.Client.Init(...) pattern the Broker option uses.
Off by default; a no-op for distributed deployments since the fast-path
falls back to the network for anything not co-located.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
When caller and callee run in the same process, a unary Call pays the
full network tax — pool.Get, dial, codec-over-socket, and the transport
pump — even though the handler table is right there. This adds an opt-in
fast-path that dispatches directly.
- internal/network: a neutral registry (transport.Message in/out) so
client and server wire up without importing each other. A running server
registers a dispatcher under its name on Start, deregisters on Stop.
- server: localDispatch serves a request in-process through the same
router (identical wrappers/codecs/error mapping) over an in-memory
socket — no dial, no pipe, no gob.
- client: LocalDispatch() opt-in. In call(), a unary request whose body and
response are raw frames (codec/bytes.Frame — the agent/MCP/flow shape)
dispatches locally; everything else falls back to the network path
unchanged.
Correctness test proves the fast-path returns byte-identical replies to
the network path; benchmark shows ~545µs -> ~28µs (~20x) and ~3.6x fewer
allocations. Off by default. Covers #4817 (path b); the zero-copy typed
path remains a follow-up.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
Adds Reconcile(desired, observed) — the pure decision an operator's
reconcile loop runs: given a desired Agent/Service/Flow resource and the
observed cluster state, it returns the one action to converge (create /
update / noop) plus Ready/Error status conditions.
No controller-runtime, no client-go: the decision is a pure function of
desired + observed, so it's fully unit-testable without a cluster. A
future operator binary supplies Observed from the live cluster and applies
the Action; only that adapter needs the Kubernetes client — keeping the
heavy dependency out of the core module.
Covers #4842 (Option B). Tests: create-when-absent, noop-when-matched-and-
ready, update-on-drift, progressing-when-under-replicated, error-on-invalid
-spec.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
Adds a waiting run state so a flow step can suspend for external input
and resume durably — stage A of the durable-agentic-workflow design in
#4816.
- flow.Await(key, prompt) / flow.AwaitStep(...): a StepFunc that suspends
the run. runFrom recognizes the signal, checkpoints the run with status
"waiting" (recording what it awaits), and returns cleanly — a suspend is
not a failure, and it is not retried or graded.
- Flow.ResumeWith(ctx, runID, input): completes the awaited step with the
injected input (which becomes that step's output state) and continues
from the next step.
- Flow.Waiting(ctx): lists suspended runs with their Await metadata.
- ResumePending/Pending skip waiting runs — they need input, not a
restart. Existing crash-resume (Resume) is unchanged.
Additive: no signature or default-behavior changes. Await ergonomics
(sentinel-return) are the default proposed in #4816; open to AwaitStep-kind
instead if preferred.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
The AP2 primitives (checkout/payment mandates, Ed25519 sign/verify, the
x402 rail reference, attach-to-message) already existed, but the gateway
only *carried* mandates on the resulting task — it never verified them, so
ap2Verifications was never populated and a downstream paid path had no
trust signal.
Wire opt-in verification: set Options.AP2PublicKey (gateway) or
a2a.WithAP2PublicKey (embedded handler) and each mandate carried on a task
is verified (signature + task/context binding) with the result recorded in
task.AP2Verifications; the x402 settlement rail rides along for the paid
path. Off by default — mandates stay carried-but-unverified — so no payment
trust decision enters the default flow.
Adds a gateway integration test driving a real message/send that carries a
signed x402 payment mandate (verified, rail carried; tampered → surfaced as
unverified) plus a default-path test proving carry-only is unchanged.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
The A2A gateway's push-notification flow (tasks/pushNotificationConfig/set
→ deliverPush) POSTed task state to a caller-supplied URL via the default
HTTP client, so an untrusted A2A caller could aim the gateway at internal
addresses (loopback, link-local cloud metadata, RFC1918) it would
otherwise never reach — a server-side request forgery vector (#4129).
Add a default SSRF-safe policy: only http/https callbacks whose host does
not resolve to a loopback, private, link-local, multicast, or unspecified
address. It's enforced when the config is set (caller gets a clear
rejection, nothing stored) and again at delivery, and the delivery client
re-checks the resolved IP at dial time so a name that passes validation
can't be rebound to an internal address before connect.
Operators that need a trusted in-cluster receiver set Options.AllowPushURL
(gateway) or a2a.WithPushURLPolicy (embedded handlers) to own the policy;
that path skips the built-in private-IP dial guard by design.
Tests cover blocked/allowed URLs, the dial-time guard, set-time rejection,
default-deny delivery, and the operator override.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
The CRD manifests were kept in two places — real YAML under config/crd/
(for kubectl apply) and byte-identical const strings in manifests.go
(for the Go CRDManifests map) — which will silently drift.
Make config/crd/*.yaml the single source of truth and go:embed it;
CRDManifests now reads the embedded bytes. Drops ~120 lines of
duplicated YAML, no behavior change (still stdlib-only, tests unchanged).
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
Comment out the automatic triggers on every loop workflow so the
autonomous engine stops firing on its own while we land the current
round of fixes 1:1:
- planner / builder / coherence / security / release: drop the cron
schedules (no more hourly/daily/weekly runs, no nightly auto-release).
- triage: drop the workflow_run trigger so CI failures no longer
auto-dispatch agent tasks.
Each keeps workflow_dispatch, so any loop can still be run on demand,
and re-enabling is just uncommenting the trigger. No prompts, tokens, or
logic changed — only when the workflows fire.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
* a2a: conform to external A2A clients — well-known path + spec SSE events
The A2A gateway interoperated go-micro-to-go-micro but a real external
client (ADK, LangGraph, a2a-SDK) would not:
- Discovery: served the Agent Card at /.well-known/agent.json, but A2A
0.3.0 discovers it at /.well-known/agent-card.json. Serve both, with
agent-card.json canonical and agent.json a legacy alias — per-agent,
per-skill, and at the single-agent top level.
- message/stream emitted repeated full Task snapshots. External SSE
clients parse by `kind` and stop on `final:true`; a Task snapshot has
neither, so they never terminate. Emit spec-shaped TaskArtifactUpdate
(append) chunks and close with a TaskStatusUpdate final:true. The
non-streaming and resubscribe paths also close with a terminal marker.
- A streaming error set both `result` and `error` in one JSON-RPC
response (strict clients reject it). Emit a failed status-update
instead — never result and error together.
Tests assert the canonical card path, the status-update/artifact-update
event shapes ending in final:true, and that no response carries both
result and error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
* harness: update a2a-streaming to the spec-shaped stream events
The A2A gateway now emits artifact-update deltas and a terminal
status-update (final:true) instead of repeated full Task snapshots, so
the conformance harness must reassemble the answer from the append
artifact-update chunks and assert the final:true marker. This makes the
harness a stronger spec check rather than a snapshot-shape check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
* agent: update a2a stream test to spec-shaped events
TestA2AStreamUsesAgentChatPathWithTools decoded the last SSE event as a
completed Task snapshot with artifacts. The gateway now closes the stream
with a status-update (final:true) and carries the answer as append
artifact-update deltas, so reassemble the answer from those deltas and
assert the terminal completed status-update instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
---------
Co-authored-by: Claude <noreply@anthropic.com>
Two spend-safety fixes from the gap audit (#4814):
- Client.Do refused a 402 only on the budget check, but parsed
maxAmountRequired with a swallowed error, so a non-decimal, overflowing
or negative amount became 0 and passed the cap trivially while Payer.Pay
still signed against the string. Now reject any amount that is not a
positive integer before signing.
- Require settled only when the facilitator implemented Settler; a
verify-only facilitator served the resource while no funds moved. Add
Config.RequireSettlement to fail closed in that case.
Tests cover invalid/negative/overflow amounts and the verify-only
fail-closed path.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
Closes#4813. The stdio transport is the path an external MCP host (Claude
Desktop) uses, and it emitted broken output:
- tool results were `fmt.Sprintf("%v", decodedJSON)` → Go map-syntax
(`map[id:1 name:bob]`), not JSON. Now returned as JSON text.
- tool-execution failures were returned as JSON-RPC protocol errors; per the
MCP spec they must be a result with `isError:true` so the agent can read the
failure. Now they are (span/audit still record the error).
Both fixes are shared between stdio and websocket via a new `mcpToolResult`/
`mcpToolError` (dedupes the two transports). Added the missing stdio round-trip
tests (the package had zero) proving JSON output and the isError contract, using
an injected fake client; updated the websocket auth tests that asserted the old
protocol-error-on-tool-failure behavior.
Also fixes a pre-existing golangci-lint failure on master (unnecessary
`string(...)` conversion in grpcreflect.go from #4821) so the mcp package lints
clean — another one the required-checks gap let through.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
The last-100-commit assessment found the loop producing busy-work — but the root
cause was the roadmap: every forward item was hardening/conformance/docs-polish,
no net-new capability. The loop was faithfully executing a maintenance backlog.
Restock the roadmap with real capability bets and demote maintenance to explicit
background:
- Now: agents that pay (wire the existing x402 buyer into the agent runtime);
AP2 mandate foundation over A2A+x402 (#3552).
- Next: gRPC-reflection MCP; Kubernetes operator + CRDs.
- Later: the runtime-fitness loop (live Mu + operator/canary); HTTP/3; A2A
reconnection; memory/RAG.
- Ongoing: hardening/conformance/DX, capped so it never crowds out capability.
Repoint .github/loop/PRIORITIES.md to rank the capability bets at the top, with
the flagship decomposed into buildable issues (#4786 buyer wiring, #4787 spend
observability, #4788 example), so the loop pulls real work instead of grooming
itself.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>
An assessment of the last 100 commits found ~45% were pure "refresh planner
priorities" bookkeeping and much of the rest was thrashing on one weak provider
(AtlasCloud text-tool-call repair) and guarding docs the loop already wrote —
motion, not progress. Two prompt-policy fixes:
PLANNER (planner.md):
- Default to NOT committing. Post the assessment and close the issue; open a
PRIORITIES.md PR ONLY when the change is MATERIAL (top item changes, an item
is added/removed, or a top item's issue closed). No PRs for reorders below
the top, reword, or "keep it current" — that churn was the loop's #1 waste.
- Add a diminishing-returns guard: don't queue the Nth doc-guard or the Nth
robustness workaround for an already-tolerated class; mark exhausted areas
needs-human and rank real-headroom capability instead.
TRIAGE (triage.md):
- Cap the AtlasCloud/plan-delegate tail-chase: another instance of a class the
agent already tolerates is NOT filed as a routine patch — comment "recurred —
capped" and, if worth more, needs-human. Real regressions (lint/tests/
govulncheck on master) and genuinely new defects still get filed.
Prompt-only; reversible. Steers the loop toward outcomes over busy-work.
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
Co-authored-by: Claude <noreply@anthropic.com>