feat(a2a): A2A 0.3.0 Agent Card conformance (closes #85, FWS-1)#97
Merged
Conversation
Forge's Agent Card now conforms to the A2A 0.3.0 spec end-to-end:
canonical path, required-field shape, security schemes derived from
the configured auth chain, and SKILL.md skills bridged into the
published card at both forge build and forge run time.
What was wrong before:
Path: served at /.well-known/agent.json; A2A 0.3.0 spec is
/.well-known/agent-card.json.
Shape: missing required fields version, protocolVersion,
defaultInputModes, defaultOutputModes, securitySchemes,
security.
Skills: SkillBuilderCodegen aside, the build pipeline's
ConfigToAgentSpec never populated spec.A2A.Skills from
SKILL.md — so the runtime's AgentCardFromSpec path mapped
an empty list. AgentCardFromConfig only surfaced builtin
tools. Net effect: SKILL.md skills never reached the card,
dev or post-build.
Audit: no agent_card_published event despite the spec calling for
one at startup.
What landed:
forge-core/a2a/types.go AgentCard + Skill extended additively
with the A2A 0.3.0 required fields.
SecurityScheme, OAuthFlows, OAuthFlow,
AgentProvider types added. Existing
consumers unaffected.
forge-core/runtime/agentcard.go
AgentCardFromSpec + AgentCardFromConfig
populate Version (from spec/cfg, default
"0.0.0"), pinned ProtocolVersion =
"0.3.0", default IO modes
["text/plain", "application/json"],
fallback Skill.Tags so A2A 0.3.0's
non-empty-tags requirement is always
met.
forge-core/runtime/agentcard_security.go (new)
PopulateSecuritySchemes derives the
card's securitySchemes + security from
the auth chain — static_token /
http_verifier -> http+bearer, oidc /
azure_ad -> openIdConnect with discovery
URL, gcp_iap -> apiKey in header,
aws_sigv4 -> custom bearerFormat
"forge-aws-v1". Additive: pre-existing
schemes on the card are preserved.
forge-core/runtime/agentcard_skills.go (new)
AppendSkillsFromDescriptors bridges
contract.SkillDescriptor -> a2a.Skill
with category-first tag ordering,
case-insensitive dedup, deterministic
ID-sorted output so card bytes are
stable across restarts (so the audit
event's sha256 is meaningful).
forge-core/runtime/audit.go New EventAgentCardPublished constant.
forge-core/auth/middleware.go
DefaultSkipPaths admits both the
canonical /.well-known/agent-card.json
and the legacy /.well-known/agent.json
(GET + OPTIONS).
forge-cli/build/agentspec_skills.go + agentspec_stage.go
Build pipeline now writes spec.A2A.Skills
from SKILL.md frontmatter — walking
skills/*.md, skills/*/SKILL.md, and the
main skill file (skills.path or default
SKILL.md). Output sorted by ID for
byte-stable agent.json across rebuilds.
Skips files without name: frontmatter
(no A2A-stable identity); dedupes
first-wins.
forge-cli/runtime/runner.go + runner_agentcard_skills.go (new)
Runner mirrors the same walk at startup
and on hot-reload via enrichAgentCardWith
Skills. Additive: when the build artifact
already populated spec.A2A.Skills, the
runtime walk is a no-op via dedup. When
there is no build artifact (forge dev,
forge run from source), the runtime walk
is the sole source. Card skill list is
identical in dev and deployed modes.
forge-cli/runtime/runner_agentcard_audit.go (new)
emitAgentCardPublished writes the audit
event after server start (and on each
hot-reload) with name, version, protocol_
version, url, skill_count, capabilities,
security_schemes (names only),
card_size_bytes, card_sha256. Best-
effort: serialization failure logs a
warning, never blocks startup. NO card
body bytes in the event (size + hash
only) — same byte-payload discipline
every other Forge audit event respects.
forge-cli/server/a2a_server.go
Registers GET /.well-known/agent-card.json
as the canonical handler. Legacy
/.well-known/agent.json routes to a new
handleAgentCardLegacy that emits the
same body plus a Deprecation: true
response header per RFC 8594 and a Link
header pointing at the successor path.
Removable one release after this ships.
forge-cli/runtime/runner.go banner
Now shows the canonical path.
Regression tests (29 total, all -race green):
forge-core/runtime/agentcard_test.go (9 tests)
Required-field population, fallback defaulting, JSON omitempty
behavior, deterministic skill ordering, dedup against existing
skills, SKILL.md frontmatter -> AgentSkill mapping fidelity.
forge-core/runtime/agentcard_security_test.go (8 tests)
Every supported provider type -> scheme mapping; no-op semantics
when chain is empty or cfg is nil; Security array OR-ordering;
additive-no-clobber behavior when caller pre-configured a scheme.
forge-cli/server/a2a_server_agentcard_test.go (3 tests)
HTTP end-to-end: canonical path returns card with NO Deprecation
header; legacy path returns identical body WITH Deprecation +
Link headers; byte-for-byte body parity across both paths.
forge-cli/build/agentspec_skills_test.go (6 tests)
Flat + subdir SKILL.md discovery; nameless files skipped;
cross-path dedup; deterministic ID-sorted output; no-op when no
SKILL.md exists; custom skills.path honored.
forge-cli/runtime/runner_agentcard_skills_test.go (4 tests)
Runtime enrichment adds parsed SKILL.md; preserves existing
skills (no clobber); no-op when no SKILL.md present; skips
nameless files.
Documentation:
docs/reference/a2a-agent-card.md (new)
Full reference: card shape, SKILL.md -> AgentSkill mapping table,
where the skill list comes from (build pipeline AND runtime),
security-scheme mapping table, audit event shape, dev vs deployed
parity guarantees.
docs/security/audit-logging.md
Adds agent_card_published to the event types table with field
inventory + cross-link.
docs/core-concepts/how-forge-works.md
AgentSpecStage row updated to mention spec.A2A.Skills population.
README.md
Operations table links the new reference page.
CHANGELOG.md
Added entry under "Added"; legacy path noted under "Deprecated".
go test -race -count=1 across forge-core/runtime, forge-core/auth,
forge-cli/build, forge-cli/server, forge-cli/runtime — all green.
golangci-lint and gofmt clean across forge-core + forge-cli.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #85 (FWS-1).
Brings Forge's Agent Card to A2A 0.3.0 spec conformance: canonical path, required-field shape, security schemes derived from the configured auth chain, SKILL.md skills bridged into the published card at both
forge buildandforge runtime, and a newagent_card_publishedaudit event on startup + hot-reload.What was wrong before
/.well-known/agent.json; A2A 0.3.0 canonical path is/.well-known/agent-card.json.version,protocolVersion,defaultInputModes,defaultOutputModes,securitySchemes,security.compiler.ConfigToAgentSpecnever populatedspec.A2A.Skillsfrom SKILL.md, soAgentCardFromSpecmapped an empty list.AgentCardFromConfigonly surfaced builtin tools. SKILL.md skills never reached the card in either dev or post-build.agent_card_publishedevent despite the spec calling for one.What landed
Types (additive — no breaking changes)
forge-core/a2a/types.go—AgentCard+Skillextended with A2A 0.3.0 required fields. NewSecurityScheme,OAuthFlows,OAuthFlow,AgentProvidertypes.Card population
forge-core/runtime/agentcard.go—AgentCardFromSpec+AgentCardFromConfigpopulateVersion(from spec/cfg, default0.0.0), pinnedProtocolVersion = "0.3.0", default IO modes["text/plain", "application/json"], fallbackSkill.Tagsso A2A 0.3.0's non-empty-tags requirement is always met.forge-core/runtime/agentcard_security.go(new) —PopulateSecuritySchemesderives the card'ssecuritySchemes+securityfrom the auth chain:auth.providers[].typestatic_token/http_verifierhttp+beareroidc/azure_adopenIdConnectwith discovery URLgcp_iapapiKeyinX-Goog-Iap-Jwt-Assertionheaderaws_sigv4http+bearer(custombearerFormat: "forge-aws-v1")Additive — pre-existing schemes are preserved.
forge-core/runtime/agentcard_skills.go(new) —AppendSkillsFromDescriptorsbridgescontract.SkillDescriptor→a2a.Skillwith category-first tag ordering, case-insensitive dedup, deterministic ID-sorted output (so card bytes are stable and the audit event's sha256 is meaningful).SKILL.md walk at both build and runtime
You caught the gap during review —
ConfigToAgentSpecignored SKILL.md, so neither dev nor prod ever surfaced SKILL.md skills. Fixed in both places so the card is identical regardless of whetheragent.jsonexists:forge-cli/build/agentspec_skills.go(new) +agentspec_stage.go— build pipeline walksskills/*.md,skills/*/SKILL.md, and the main skill file (skills.pathor defaultSKILL.md), parses frontmatter, writes deduplicated ID-sorted entries intospec.A2A.Skills.forge-cli/runtime/runner_agentcard_skills.go(new) — runner mirrors the same walk at startup and on hot-reload viaenrichAgentCardWithSkills. Additive: when the build artifact already populatedspec.A2A.Skills, the runtime walk is a no-op via dedup; when there is no build artifact, the runtime walk is the sole source.Path swap + legacy alias
forge-cli/server/a2a_server.go—GET /.well-known/agent-card.jsonis now the canonical handler. Legacy/.well-known/agent.jsonroutes tohandleAgentCardLegacywhich emits the same body plusDeprecation: true(RFC 8594) andLink: </.well-known/agent-card.json>; rel="successor-version". Removable one release after this ships.forge-core/auth/middleware.go—DefaultSkipPathsadmits both paths (GET + OPTIONS).Audit event
forge-core/runtime/audit.go— newEventAgentCardPublishedconstant.forge-cli/runtime/runner_agentcard_audit.go(new) —emitAgentCardPublishedwrites the event after server start (and on each hot-reload) withname,version,protocol_version,url,skill_count,capabilities,security_schemes(names only),card_size_bytes,card_sha256. Sizes + hash only — no card body bytes in the event, matching every other Forge audit event's discipline.Regression tests — 29 total, all
-racegreenforge-core/runtime/agentcard_test.go(9 tests) — required-field population, fallback defaulting, JSONomitempty(capabilities/securitySchemes/provider omitted when nil), deterministic skill ordering, dedup against existing skills, SKILL.md frontmatter → AgentSkill mapping fidelity.forge-core/runtime/agentcard_security_test.go(8 tests) — every supported provider type → scheme mapping; no-op semantics; Security-array OR-ordering; additive (no-clobber) when caller pre-configured a scheme.forge-cli/server/a2a_server_agentcard_test.go(3 tests) — HTTP end-to-end: canonical path returns card with no Deprecation header; legacy path returns identical body with Deprecation + Link headers; byte-identical body across both paths.forge-cli/build/agentspec_skills_test.go(6 tests) — flat + subdir SKILL.md discovery; nameless files skipped; cross-path dedup; deterministic ID-sorted output; no-op when no SKILL.md; customskills.pathhonored.forge-cli/runtime/runner_agentcard_skills_test.go(4 tests) — runtime enrichment adds parsed SKILL.md; preserves existing skills (no clobber); no-op when no SKILL.md present; skips nameless files.Documentation
docs/reference/a2a-agent-card.md(new) — card shape, SKILL.md → AgentSkill mapping table, where the skill list comes from (build pipeline AND runtime), security-scheme mapping table, audit event shape, dev vs deployed parity guarantees.docs/security/audit-logging.md— addsagent_card_publishedto the event types table with field inventory + cross-link.docs/core-concepts/how-forge-works.md—AgentSpecStagerow updated to mentionspec.A2A.Skillspopulation.README.md— Operations table links the new reference page.CHANGELOG.md—Addedentry; legacy path noted underDeprecated.Gates
go test -race -count=1acrossforge-core/runtime,forge-core/auth,forge-cli/build,forge-cli/server,forge-cli/runtime— all greengolangci-lint run— 0 issues across both modulesgofmt -l forge-core/ forge-cli/— cleanAcceptance criteria
/.well-known/agent-card.jsonper A2A 0.3.0 spec.forge dev(local) and deployed modes — both walk SKILL.md; both emit the same JSON shape. Pinned by tests at both layers.agent_card_publishedevent on agent startup with the card metadata for traceability (no payload bytes — sha256 + size only).Architectural decisions
forge.yaml/ skills/ changes. No background refresh otherwise.agent_card_publishedcarries metadata + hash, not the card body. Same byte-payload discipline every other Forge audit event respects.