Skip to content

feat(a2a): A2A 0.3.0 Agent Card conformance (closes #85, FWS-1)#97

Merged
initializ-mk merged 2 commits into
mainfrom
feat/issue-85-a2a-agent-card-conformance
Jun 4, 2026
Merged

feat(a2a): A2A 0.3.0 Agent Card conformance (closes #85, FWS-1)#97
initializ-mk merged 2 commits into
mainfrom
feat/issue-85-a2a-agent-card-conformance

Conversation

@initializ-mk
Copy link
Copy Markdown
Contributor

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 build and forge run time, and a new agent_card_published audit event on startup + hot-reload.

What was wrong before

Concern Before
Path Served at /.well-known/agent.json; A2A 0.3.0 canonical path is /.well-known/agent-card.json.
Shape Missing required fields version, protocolVersion, defaultInputModes, defaultOutputModes, securitySchemes, security.
Skills compiler.ConfigToAgentSpec never populated spec.A2A.Skills from SKILL.md, so AgentCardFromSpec mapped an empty list. AgentCardFromConfig only surfaced builtin tools. SKILL.md skills never reached the card in either dev or post-build.
Audit No agent_card_published event despite the spec calling for one.

What landed

Types (additive — no breaking changes)

  • forge-core/a2a/types.goAgentCard + Skill extended with A2A 0.3.0 required fields. New SecurityScheme, OAuthFlows, OAuthFlow, AgentProvider types.

Card population

  • forge-core/runtime/agentcard.goAgentCardFromSpec + 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:

    auth.providers[].type A2A scheme
    static_token / http_verifier http + bearer
    oidc / azure_ad openIdConnect with discovery URL
    gcp_iap apiKey in X-Goog-Iap-Jwt-Assertion header
    aws_sigv4 http + bearer (custom bearerFormat: "forge-aws-v1")

    Additive — pre-existing schemes are preserved.

  • forge-core/runtime/agentcard_skills.go (new)AppendSkillsFromDescriptors bridges contract.SkillDescriptora2a.Skill with 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 — ConfigToAgentSpec ignored SKILL.md, so neither dev nor prod ever surfaced SKILL.md skills. Fixed in both places so the card is identical regardless of whether agent.json exists:

  • forge-cli/build/agentspec_skills.go (new) + agentspec_stage.go — build pipeline walks skills/*.md, skills/*/SKILL.md, and the main skill file (skills.path or default SKILL.md), parses frontmatter, writes deduplicated ID-sorted entries into spec.A2A.Skills.
  • forge-cli/runtime/runner_agentcard_skills.go (new) — runner mirrors the same walk at startup and on hot-reload via enrichAgentCardWithSkills. 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, the runtime walk is the sole source.

Path swap + legacy alias

  • forge-cli/server/a2a_server.goGET /.well-known/agent-card.json is now the canonical handler. Legacy /.well-known/agent.json routes to handleAgentCardLegacy which emits the same body plus Deprecation: true (RFC 8594) and Link: </.well-known/agent-card.json>; rel="successor-version". Removable one release after this ships.
  • forge-core/auth/middleware.goDefaultSkipPaths admits both paths (GET + OPTIONS).
  • Runner banner now shows the canonical path.

Audit event

  • forge-core/runtime/audit.go — new EventAgentCardPublished constant.
  • forge-cli/runtime/runner_agentcard_audit.go (new)emitAgentCardPublished writes the 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. Sizes + hash only — no card body bytes in the event, matching every other Forge audit event's discipline.

Regression tests — 29 total, all -race green

forge-core/runtime/agentcard_test.go (9 tests) — required-field population, fallback defaulting, JSON omitempty (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; 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) — 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.mdAgentSpecStage row updated to mention spec.A2A.Skills population.
  • README.md — Operations table links the new reference page.
  • CHANGELOG.mdAdded entry; legacy path noted under Deprecated.

Gates

  • 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 run — 0 issues across both modules
  • gofmt -l forge-core/ forge-cli/ — clean

Acceptance criteria

  • Forge agent serves /.well-known/agent-card.json per A2A 0.3.0 spec.
  • Card includes identity, service endpoint, A2A capabilities, authentication schemes (Bearer, ApiKey, openIdConnect), AgentSkill objects with id/name/description/tags.
  • SKILL.md frontmatter maps cleanly to AgentSkill objects with no information loss for spec-defined fields. Mapping pinned by test.
  • Card endpoint behavior identical in forge dev (local) and deployed modes — both walk SKILL.md; both emit the same JSON shape. Pinned by tests at both layers.
  • Audit emits agent_card_published event on agent startup with the card metadata for traceability (no payload bytes — sha256 + size only).

Architectural decisions

  • Agent Card extensions kept out of public types. Forge-internal fields (egress allowlist, denied tools, trust hints, guardrails) are NOT serialized into the card. The card carries only what A2A 0.3.0 defines.
  • Card fixed at startup (with hot-reload via file watcher). The runtime walks SKILL.md once at startup; the file watcher re-runs the walk and re-emits the audit event on forge.yaml / skills/ changes. No background refresh otherwise.
  • agent_card_published carries metadata + hash, not the card body. Same byte-payload discipline every other Forge audit event respects.

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.
@initializ-mk initializ-mk merged commit 54c3742 into main Jun 4, 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.

FWS-1 — A2A spec conformance audit (Agent Card at /.well-known/agent-card.json)

1 participant