Skip to content

Integrate canonical v0.1.0 - #2

Merged
rocklambros merged 44 commits into
GenAI-Security-Project:mainfrom
afogel:integrate-canonical-v0.1.0
Jun 5, 2026
Merged

Integrate canonical v0.1.0#2
rocklambros merged 44 commits into
GenAI-Security-Project:mainfrom
afogel:integrate-canonical-v0.1.0

Conversation

@afogel

@afogel afogel commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Integrates the canonical v0.1.0 spec into this docs site. The canonical materials introduce a tiered conformance model, eight new lifecycle hooks, a normative provenance + SessionContext + Intent system, a crypto-agile signature registry, and OTel/OCSF/CycloneDX/SPDX/SWID mapping tables — all of which were either missing or sketched-only in the prior content.

What's new

  • Modular JSON Schemas under specification/v0.1.0/: 9 top-level objects (envelopes, handshake, provenance, context-entry, ask/defer/modifications), 19 per-hook payloads, AgBOM canonical schema, Inspect format-mapping, OTel + OCSF mappings. The legacy specification/ACS/acs_schema.json is now a Draft 2020-12 aggregator manifest cross-referencing all modular schemas.
  • Conformance profiles (docs/spec/conformance.md): ACS-Core mandatory baseline + ACS-Trace, ACS-Inspect, ACS-Inspect-Dynamic, ACS-Provenance, ACS-Crypto, ACS-Audit, with handshake-based declaration.
  • Instrument-pillar rewrite (specification.md, hooks.md): JSON-RPC 2.0 envelope with HTTP+stdio transports, handshake/hello capability negotiation, full disposition vocabulary including DEFER, decision-result fields supporting paradigm composition (FIDES / CaMeL / AARM / IBAC), provenance with optional trust enum and default channel-to-trust mapping, SessionContext with normative chain hashing (RFC 8785 JCS + SHA-256), Intent immutability and intent-extension via ASK, crypto-agile signature registry (HMAC-SHA256 baseline; ECDSA, RSA-PSS, ML-DSA-44/65/87, SLH-DSA-128s/f, hybrids), replay protection, OPA reference policy-engine binding, and the new lifecycle hooks (sessionStart/End, turnStart/End, pre/postCompact, subagentStart/Stop, system/ping).
  • Trace pillar (docs/spec/trace/): OpenTelemetry semconv mapping with decision-as-span-event rule, OCSF event-class mapping (Authentication 3002, Application Activity 6002, Process Activity 1007, Datastore Activity 6005, Detection Finding 2004 for non-allow decisions, Inventory Info 5001 for AgBOM), severity_id mapping, ACS-Trace conformance bar.
  • Inspect pillar (docs/spec/inspect/): canonical AgBOM with seven component types, agbom/snapshot and agbom/changed wire methods, deterministic CycloneDX 1.6 / SPDX 3.0 / SWID derivations.
  • Topics: core_concepts.md updated to introduce the v0.1.0 vocabulary; ACS_in_action_example.md replaced with the canonical Appendix C worked example (toolCallRequest with argument-level provenance, IBAC + FIDES decision envelope).
  • References: new docs/references/ with hook_surfaces.md (lifecycle-hook landscape across coding agents) and standards_compatibility.md (CaMeL/FIDES/AARM/IBAC + Conseca, ControlValve, MELON, AgentSentry, LlamaFirewall, PROV-AGENT/Flowcept comparison). IBAC paper added under docs/assets/.

Commits (in order)

  1. Import canonical v0.1.0 modular JSON Schemas
  2. Rewrite Instrument-pillar spec from canonical v0.1.0
  3. Add Trace-pillar mappings (OTel + OCSF)
  4. Add Inspect-pillar AgBOM spec
  5. Add Conformance Profiles page
  6. Update topics: core_concepts and ACS_in_action_example
  7. Add reference materials
  8. Refresh acs.md landing page for v0.1.0
  9. Update mkdocs nav for v0.1.0 sections
  10. Fix mkdocs cross-links and stale anchors after spec rewrite

Test plan

  • uv run mkdocs build --strict succeeds with no warnings (only an informational notice that the existing A2A hook example pages aren't in the nav, matching pre-PR behavior — they're linked from extend_a2a.md).
  • uv run mkdocs serve and click through the new Specification > Conformance Profiles section, the rewritten Instrument > Specification + Hooks pages, the refreshed Trace + Inspect pages, and the new References section.
  • Spot-check a few schema GitHub links open the correct files.

afogel added 14 commits May 6, 2026 09:16
Replace the single legacy specification/ACS/acs_schema.json with the modular
v0.1.0 schema package from canonical ACS. The legacy file becomes an
aggregator manifest that cross-references the new modular files.

Adds:
  specification/v0.1.0/{request,response}-envelope.json
  specification/v0.1.0/{handshake,provenance,provenance-summary}.json
  specification/v0.1.0/{context-entry,ask-details,defer-details,modifications}.json
  specification/v0.1.0/hooks/*.json (19 per-hook schemas: session-start/end,
    agent-trigger, turn-start/end, user-message, agent-response,
    knowledge-retrieval, memory-context-retrieval, memory-store,
    tool-call-request/result, pre/post-compact, subagent-start/stop,
    system-ping, agbom-snapshot/changed)
  specification/v0.1.0/agbom/{component,document}.json
  specification/v0.1.0/inspect/format-mapping.json
  specification/v0.1.0/trace/{otel,ocsf}-mapping.json
specification.md and hooks.md now mirror the canonical ACS v0.1.0 spec
proposal: design principles, two-layer Guardian architecture, JSON-RPC 2.0
envelope with stdio + HTTP transports, capability-negotiation handshake
(handshake/hello), 16 native steps/* hooks, full disposition vocabulary
including DEFER, decision-result fields supporting paradigm composition
(FIDES / CaMeL / AARM / IBAC), provenance with optional trust enum and
default channel-to-trust mapping, SessionContext with normative chain
hashing (RFC 8785 JCS + SHA-256), Intent immutability and intent-extension
via ASK, crypto-agile signature registry (HMAC, ECDSA, RSA-PSS, ML-DSA,
SLH-DSA, hybrids), replay protection, OPA policy-engine reference binding,
the new lifecycle hooks (sessionStart/End, turnStart/End, pre/postCompact,
subagentStart/Stop), system/ping liveness, multi-tenancy reservation, and
the v0.2 / future deferred list.
Refresh docs/spec/trace/{events.md,extend_opentelemetry.md,extend_ocsf.md}
to align with the canonical v0.1.0 mapping:

- OpenTelemetry: span-name + required-attribute table for every step,
  decision-as-span-event rule, provenance attributes, span-link rule for
  cited_provenance_ids.
- OCSF: class assignments per step (Authentication 3002, Application
  Activity 6002, Process Activity 1007, Datastore Activity 6005, Detection
  Finding 2004 for deny/modify/ask/defer, Inventory Info 5001 for
  agbom/*) and disposition -> severity_id mapping.
- ACS-Trace conformance bar.
- Failure isolation: trace sink failure MUST NOT change the disposition.
Refresh docs/spec/inspect/{README.md,extend_cyclonedx.md,extend_spdx.md,
extend_swid.md} to align with the canonical v0.1.0 Inspect pillar:

- agbom/snapshot and agbom/changed wire methods, with audit-chain
  integration.
- Canonical component graph with seven types (model, mcp_server, a2a_peer,
  tool, knowledge_source, memory_store, agent_capability).
- registration_provenance on every component (REQUIRED under
  ACS-Provenance).
- CycloneDX 1.6, SPDX 3.0, SWID derivations from the canonical document.
- ACS-Inspect and ACS-Inspect-Dynamic conformance bars.
New docs/spec/conformance.md defining the v0.1.0 profile system: ACS-Core
(mandatory baseline) plus five optional profiles deployments declare in
the handshake's profiles_supported / profiles_accepted fields.

- ACS-Core: handshake, envelope, hooks, dispositions (incl. DEFER),
  SessionContext, Intent, replay protection, ping, wrapped MCP.
- ACS-Trace: OTel + OCSF event emission, decision-as-span-event.
- ACS-Inspect / ACS-Inspect-Dynamic: agbom/snapshot, agbom/changed.
- ACS-Provenance: field-level Provenance with optional trust enum.
- ACS-Crypto: ML-DSA-65 primary, SLH-DSA-128s backup, hybrids.
- ACS-Audit: request_hash on every ContextEntry.

Profiles compose; the page includes a quick-reference matrix.
core_concepts.md now introduces the v0.1.0 vocabulary:
- Three pillars (Instrument / Trace / Inspect) as co-equal.
- Two parties (Observed Agent / Guardian Agent with deterministic +
  optional agent layers).
- Session, Turn, Step, Hook Response, Provenance, SessionContext, Intent
  with the immutability rule.
- Default channel-to-trust mapping and the optional wire-level trust enum.

ACS_in_action_example.md now mirrors the canonical Appendix C worked
example: handshake, agbom/snapshot, sessionStart, userMessage,
toolCallRequest with argument-level provenance, IBAC + FIDES decision
envelope with reason_codes / policy_references / policy_data /
cited_provenance_ids, plus a parallel deny-shaped example.
- docs/references/hook_surfaces.md: survey of lifecycle-hook systems
  across coding agents (Claude Code, Cursor, Codex, Copilot, Replit,
  Tabnine, Windsurf) with a semantic-event matrix and migration notes.
  Source for the v0.1.0 hook taxonomy and the DEFER disposition design.
- docs/references/standards_compatibility.md: comparison of CaMeL, FIDES,
  AARM, IBAC, Conseca, MELON, ControlValve, AgentSentry, LlamaFirewall,
  PROV-AGENT/Flowcept; Google secure-agent guidance, Chrome User
  Alignment Critic, MCP security best practices, OWASP Agentic Top 10.
  Source for the deterministic-mediation core + pluggable detectors
  architecture.
- docs/assets/ibac-paper.pdf: primary source for the Intent model.
- docs/references/README.md: index linking the three sources.
Replace the work-in-progress intro with a v0.1.0 summary: what ACS is,
the three pillars (Instrument / Trace / Inspect), and the six profiles
(ACS-Core mandatory plus ACS-Trace, ACS-Inspect/-Dynamic,
ACS-Provenance, ACS-Crypto, ACS-Audit). Leaves the existing aspirational
SDK/MCP/A2A code samples below intact.
- Add Specification > Conformance Profiles entry pointing at the new
  spec/conformance.md page.
- Add a top-level References section exposing hook_surfaces.md and
  standards_compatibility.md.
- Schema-file references now point at GitHub blob URLs under
  https://github.com/Agent-Control-Standard/ACS/blob/dev/specification/v0.1.0/...
  Schemas live outside docs_dir, so relative links from the docs tree
  cannot reach them; absolute URLs work both in the rendered site and
  when reading docs raw.
- Update §13 anchor slug (#13-liveness-system-methods) to match
  Python-Markdown's TOC slugifier.
- A2A hook docs: replace stale spec-anchor links
  (#48-a2a-protocol-methods, #51-acssuccessresponse-object) with code
  spans for the method names and a pointer to the new dispositions
  section. The protocols/A2A/* namespace remains reserved for v0.2.
- extend_mcp.md: point at the new protocols/MCP/* anchor in hooks.md
  (the old #mcp-protocol-hooks anchor is gone).
- gitignore: add site/ (mkdocs build artifact).

`uv run mkdocs build --strict` now succeeds.
The hook-surfaces and standards-compatibility surveys plus the IBAC
paper are local research artifacts, not site content. Move them to
references/ at the repo root, gitignore that path, and drop the
References section from the mkdocs nav.

Files remain on disk locally; they no longer ship in the repo or build
into the site.
ACS_in_action_example.md now shows one denial-shaped envelope per
paradigm so readers can see how the same wire format expresses each:

- IBAC: Intent.parsed mismatch with intent_parsed and closest match
  exposed in policy_data.
- FIDES: P-T (planner-taint) failure with violating_argument_path,
  lineage_origins, and earliest_untrusted_provenance_id.
- CaMeL: per-argument dependency-graph violation showing the actual vs.
  expected lineage roots.
- AARM: cumulative-context denial with earliest_untrusted_step_id and
  the entry_count_by_origin aggregate from provenance_summary.
- IBAC + FIDES composition: a single decision citing both paradigms in
  policy_references, reason_codes, and a policy_data object keyed by
  paradigm name.

Also: repoint all schema GitHub URLs from Agent-Control-Standard/ACS
(404) to afogel/ACS_official (the actual configured remote).
ClientHello now carries an optional approver_types_supported array
(human / agent / service) mirroring the existing ServerHello field.

New §9.2 normatively requires Guardians to substitute DEFER or DENY for
ASK whenever the client/Guardian approver-type sets do not intersect.
This lets headless automation, IDE plugins, and runtimes with allow/deny
policy models (e.g., Microsoft Agent Governance Toolkit) participate in
ACS sessions as fully conformant ACS-Core deployments — without silently
allowing actions that would have been ASK'd elsewhere.

Updates:
- specification/v0.1.0/handshake.json: add approver_types_supported to
  ClientHello.
- docs/spec/instrument/specification.md: §4 mentions the new field; §6
  disposition table cross-references the fallback rule; new §9.2 spells
  out the normative substitution and the policy-driven DEFER vs. DENY
  choice.
…-band

The handshake-field approach was over-engineered. §9.2 already preserves
the security guarantee through Guardian-side substitution; the Guardian
can determine client ASK-handling capability by deployment-defined means
(agent identity, policy bundle, organizational configuration) without a
wire-format declaration.

Reverts the schema addition from a1b7958. The normative rule in §9.2
stands — Guardians MUST substitute DEFER or DENY when ASK isn't
reachable — but the trigger is now 'Guardian determines the client
cannot resolve ASK' rather than 'set intersection on the wire is
empty.' Smaller spec surface, same outcome.
@rocklambros

Copy link
Copy Markdown
Contributor

Heads up @afogel — doing some branch surgery that affects this PR. Want to flag it before it happens so nothing surprises you.

Context (and acknowledging the screwup): when I cloned/forked this repo from the original OWASP/AOS source, I left both dev and main as long-lived trunks instead of collapsing to a single trunk at fork time. That was my mistake. The result has been quietly drifting: divergent rebrand commits on each branch, separate histories, and (most visibly) confusing AI fetchers and external readers about which branch is canonical. The current state is the worst of both worlds — dev is the GitHub default, but the docs site (agentcontrolstandard.org) deploys from main. Cleaning it up now while main and dev are content-equivalent — that's the lowest-risk window we'll get.

The change:

  1. Rename devmain
  2. Delete the old main
  3. Update sync_version.yml to trigger on the new main

What this means for PR #2:

  • GitHub auto-retargets the base from devmain
  • Diff doesn't change (trees are byte-identical right now)
  • No rebase needed
  • Review continues uninterrupted on the new base

What you may want to do:

  • If you have unpushed local commits on integrate-canonical-v0.1.0, push them in the next few minutes — pushing to origin/dev after the rename will fail.
  • After the rename, refresh your local tracking: git fetch origin && git remote set-head origin -a, then rebase your branch onto the new main if you want a clean update.

Sorry for the disruption. Should be the last branch surgery for a while.

Catches the PR branch up with 5 commits on dev (sync_version trigger
switch, CONTRIBUTING/README/CLAUDE.md updates, deploy_mkdocs removal,
upstream docs typo fixes).

Conflicts resolved in favor of the PR branch:
- docs/acs.md: dev removed a TODO comment from text that the PR
  branch rewrote wholesale during the v0.1.0 documentation overhaul.
- docs/spec/instrument/specification.md: dev fixed a typo and broken
  anchor in a section that the PR branch replaced with the new §11
  error handling content. The PR branch's concise error code table
  supersedes the old verbose pre-rewrite tables.
@afogel

afogel commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

@rocklambros I fixed the merge conflicts, should be good to go?

Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md Outdated
Comment thread docs/spec/instrument/hooks.md
Comment thread docs/spec/instrument/hooks.md Outdated
afogel added 11 commits May 26, 2026 15:43
Expand the one-line A2A reservation in hooks.md to note that the
namespace is recognized in the handshake's wrapped_protocols field
for forward compatibility, while making explicit that v0.1 defines
no normative wrapping semantics. Addresses review feedback that the
prior wording was too terse to tell readers what 'reserved' means
operationally for v0.1 deployments.
Readers of the previous wording could not tell whether handshake or
sessionStart comes first when both are in play. Position sessionStart
explicitly after the handshake/hello exchange completes and before
any other steps/* hook, removing the ordering ambiguity flagged in
review.
Replace the terse 'User input received, before reaching the agent'
with wording that mirrors the 'reasoning context' terminology used
elsewhere in the spec, while preserving the existing provenance
sentence. Improves consistency with toolCallResult and clarifies
what 'reaching the agent' means in practice.
The previous wording implied the recipient is always a user, but
agentResponse also fires on A2A egress, subagent-return to a parent
agent, and outbound delivery to external systems. Generalize the
recipient list so the description holds across all deployment shapes
while keeping the existing provenance sentence intact.
Add a normative MUST clause to the toolCallRequest section requiring
frameworks to fire the hook for every action that leaves the agent's
reasoning context, regardless of whether the framework's tool
registry models the action as a tool. Enumerates the easy-to-miss
cases (filesystem reads/writes, network fetches, process execution,
shell commands) and explains why: primitives that bypass
toolCallRequest are invisible to policy, defeating the Guardian's
ability to enforce on outward actions.
'Fires before tool execution' left the boundary fuzzy: readers could
not tell whether the hook fires when the LLM emits a tool call token,
when the framework parses it, or somewhere in between. Replace with
the precise framing: after the framework has parsed a tool call from
the LLM's output, but before the tool is dispatched to its handler.
Matches the implementation contract Guardians need to reason about
intercept points.
v0.1 carries factual provenance (origin, source_id, derived_from) on
the wire and leaves trust classification to the Guardian against
local policy. Calling arguments 'tainted' in the hooks doc implies a
classification judgment the wire format does not make. Reword to
lineage-based framing that stays consistent with the rest of the
v0.1 provenance contract.
Mirror the userMessage update: describe the tool result as ingested
into the agent's reasoning context, not merely 'into the agent'. The
reasoning-context phrasing is the term of art used throughout the
spec for the boundary that hooks gate, so the two halves of the
tool-call lifecycle now describe their boundaries the same way.
Add a note at the top of the system/ping section stating that the
method lives in the system/* namespace, not steps/*, and that it is
not a hook in the enforcement sense. The audit and signature
exemptions were already documented further down, but readers landing
on this section first need the namespace distinction up front so they
do not assume the steps/* contract applies.
The conditional 'When the deployment populates trust on the wire'
clause in hooks.md restated specification.md §7.1's optional-trust
extension contract inline with the v0.1 baseline payload contract.
Readers landing on hooks.md without §7.1 context misread the MUST as
a wire-format requirement, which it is not (trust is an OPTIONAL
reserved enum, not a v0.1 baseline field).

The monotonicity rule still applies via §7.1's general clause for
agent_generated provenance, which already covers the summarization
case. hooks.md now describes only the v0.1 baseline (origin +
derived_from); extension semantics live in §7.1.
Every schema description that discussed the optional trust enum
pointed readers to specification.md §3.7, but the trust enum is
actually at §7.1 (and the channel-to-trust mapping at §7.2). Section
numbering must have shifted at some point and the schema refs were
left behind. Anyone clicking through would land nowhere.

Update all 10 affected files (provenance.json, provenance-summary.json,
response-envelope.json, inspect/format-mapping.json,
trace/otel-mapping.json, trace/ocsf-mapping.json, and the four
hook payloads that touch trust: post-compact, pre-compact, turn-end,
subagent-stop) to point at §7.1.
afogel added 2 commits June 1, 2026 14:18
The OCSF class table omitted steps/agentTrigger, which the normative
ocsf-mapping.json maps to 6002 Application Activity (and which the OTel
table already lists). Surfaced by a full events.md vs mapping audit
following the PR #2 review.
JSON-RPC 2.0 requires the response id to be null when the request id
cannot be determined (parse error / invalid request). The response
envelope forbade null, making correct error envelopes non-compliant.

Allow null on id, but constrain it to the error branch: a successful
(result) response still MUST echo the original non-null id.
@afogel

afogel commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @rocklambros for requesting a GH copilot, it gave a useful review.

I addressed the doc/schema consistency gaps; the provenance cluster needed a design decision rather than the suggested edit, so I want to explain that one in full. I'll address the 53 comments grouped into 9 distinct findings:

Provenance required on hook payloads (7 comments: userMessage, agentResponse, toolCallRequest, toolCallResult ,knowledgeRetrieval, memoryStore, memoryContextRetrieval)

GH copilot correctly pointed out that the base schemas required provenance while the prose says ACS-Core needs no field-level Provenance — that contradiction made ACS-Core non-implementable, since a pure-IBAC deployment must emit toolCallRequest. I addressed that, but not by making provenance globally optional.

Freely-omittable provenance reintroduces exactly the risk §7.1 warns against: provenance is the substrate FIDES/CaMeL/AARM read, and per-field omission lets a producing deployment silently drop it and launder taint. So the fix keeps optionality at the session level, not the field level:

  • Base schemas now mark provenance optional → ACS-Core / pure-IBAC validates.
  • provenance_producer: deterministic (the ACS-Provenance bar) requires it on every data-bearing field — all-or-nothing per session, not a per-emit choice.
  • provenance_producer: none emits none; a Guardian whose policy needs it refuses the session at handshake.

Since "required only when the producer is deterministic" is cross-document (payload shape depends on the handshake) and a single JSONSchema can't express it, we made it machine-checkable rather than prose-only: each data-bearing hook gets a strict *.acs-provenance.json variant that allOf-refs the base and restores provenance to required. ACS-Provenance deployments validate against the strict variant; ACS-Core against the base. §7 and the conformance profiles were updated to state this explicitly.

I sent a copy of the changes to @bar-capsule for added review, since the changes were non-trivial in nature.

response-envelope.jsonid: null for JSON-RPC errors

Correct, and fixed. A parse/invalid-request error can't echo a known id, so id now permits null — but constrained to the error branch: a successful result response still must echo the original non-null id.

trace/events.md out of sync with otel-mapping.json (3 comments)

All fixed: acs.tool.provider and acs.duration_ms marked optional; preCompact/postCompact split with the correctacs.compact.lineage_depth_after; agbom/snapshot (component_count) split from agbom/changed (change_reason). A full audit beyond the rows you flagged also turned up steps/agentTrigger missing from the OCSF class table — fixed too.

Doc fixes (accepted as-stated)

  • concepts/provenance.md origin enum corrected to the seven real values; dropped the bogus memory, restored a2a_inbound/external.
  • extend_cyclonedx.mdorigin: "configuration"system per component.json.
  • ACS_in_action_example.mdtool is now an object {name}, session_id is a UUID, and stale per-field trust removed (not a v0.1 wire field).
  • send_message_request.md — "Reponse"/"an response" fixed.

@rocklambros

Copy link
Copy Markdown
Contributor

Thanks @afogel i'll take a closer...human look this afternoon when I get back in front of my laptop and approve to merge if it looks good.

@rocklambros rocklambros left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @afogel — sat with this for a while and did an adversarial pass. Six things below I'd want to land before we tag v0.1.0. None of them are dealbreakers for the direction, more places where a conformant implementation could end up doing the thing the standard is meant to prevent, or two implementations could end up not interoperating. Happy to take any of these as follow-up PRs if you'd rather merge this and iterate. I've also opened separate issues for the things I think can wait.

Comment thread docs/spec/conformance.md Outdated
Comment thread docs/spec/instrument/specification.md Outdated
Comment thread specification/v0.1.0/modifications.json Outdated
Comment thread docs/spec/instrument/specification.md Outdated
Comment thread docs/spec/instrument/specification.md Outdated
Comment thread docs/spec/instrument/specification.md Outdated
afogel added 8 commits June 4, 2026 18:00
Adds section 17.1, a registry mapping each mandated refusal to a fixed
code in the -32000..-32099 ACS range, and cites those codes at the
refusal sites that previously returned none.

WHY: section 17 reserved the ACS error range and named three errors only
as examples, with no table binding names to codes. Meanwhile section 4
(version mismatch, provenance-required), section 10.2 (signature
failure), and section 10.3 (replay) mandate refusals without saying which
code to return. An Observed Agent that receives -32000 cannot tell
whether to retry, fall back to an older spec version, regenerate
request_id, or give up, so no SDK can write portable error recovery.

Each refusal now maps to one code, so an SDK branches on the code without
parsing prose:

  -32000 SESSION_REFUSED            residual policy refusal
  -32001 UNSUPPORTED_VERSION        no common acs_version (section 4)
  -32002 PROVENANCE_REQUIRED        provenance required, producer=none (4, 7)
  -32003 CAPABILITY_NOT_NEGOTIATED  method or profile not negotiated (4)
  -32004 SIGNATURE_INVALID          signature missing, malformed, or unverifiable (10)
  -32005 REPLAY_DETECTED            duplicate request_id or nonce (10.3)
  -32006 TIMESTAMP_OUT_OF_WINDOW    timestamp outside the skew window (10.3)
  -32007 CHAIN_MISMATCH             chain_hash disagrees with the Guardian head (8)

One code per reason, rather than a single SESSION_REFUSED catchall with
the reason in a data field, so an SDK can branch on the code alone.
SESSION_REFUSED stays as the residual for policy refusals that have no
more specific code. An error response MAY carry a data object with a
machine-readable reason, a human-readable message, and per-code fields
(supported_versions, method, skew_window_ms).

Sections 4, 10.2, and 10.3 now cite their codes. MODIFY_FIELDS_CONFLICT
is held until the modify-precedence decision lands, since a precedence
rule may remove the need for a conflict error.

Addresses the PR review comment on
docs/spec/instrument/specification.md:371.
Adds skew_window_ms to the handshake ServerHello and points section 10.3
at it.

WHY: section 10.3 requires Guardians to reject requests whose timestamp
is outside "the handshake-negotiated skew window," but no such field
existed in handshake.json. timeout_config covers response timeouts, not
request-timestamp skew. So conformant Guardians could not meet their own
MUST: each vendor would pick a different default, and an Observed Agent
with clock drift would be accepted by some Guardians and rejected by
others with no way to discover the threshold. Replay protection is part
of ACS-Core, so this was a baseline-level inconsistency.

skew_window_ms is optional, in milliseconds to match default_ms, with a
RECOMMENDED default of 300000 (5 minutes); deployments with
well-synchronized clocks SHOULD tighten it. A skew violation is rejected
with TIMESTAMP_OUT_OF_WINDOW (section 17.1), which echoes the window in
data.skew_window_ms. The field is chosen over "deployment-configured"
wording so the Observed Agent can read the window for clock-drift
diagnostics and feed it to Trace events.

Addresses the PR review comment on
docs/spec/instrument/specification.md:278.
…eview)

Replaces the unconstrained modifications object with two mutually
exclusive shapes and adds Specification 6.3.

WHY: modifications allowed modified_content, redactions, and
parameter_overrides in any combination (minProperties 1, no oneOf, no
order rule), and MODIFY is a mandatory disposition. With no rule, two
conformant frameworks could apply the same Guardian decision in
different orders and produce different effective tool calls, a
security-relevant divergence: a redaction and an override targeting the
same field resolve differently depending on which runs last. The
Guardian that issued the decision could not predict its own effect,
because the framework that applies the decision is usually a different
vendor than the one that made it.

The fix removes the conflict rather than resolving it by a fixed order.
A fixed apply-order would force every overlap to pick a winner, and
either choice fails: applying overrides last can re-expose a field a
redaction removed, and applying redactions last lets arbitrary
replacement text overwrite a value an override deliberately sanitized.
Instead:

  - modified_content is exclusive. It replaces the whole payload and
    cannot combine with the path-based edits, which have nothing to
    address inside an opaque replacement string.
  - redactions and parameter_overrides may combine only on disjoint
    targets. Disjoint edits commute, so the effective payload is
    order-independent and no apply-order rule is needed.
  - A Guardian MUST NOT emit a violating object. A framework that
    receives one fails closed to DENY and records an audit event,
    because it cannot determine the Guardian's intent.

This is the conservative shape. It is forward-compatible: overlapping
composition, or a Guardian-declared order, can be added in a later
version without a wire break, and it mints no new trust label on the
wire. No MODIFY_FIELDS_CONFLICT error code is added: an overlapping
decision is a Guardian-side validity error handled by fail-closed, not
a wire error the agent returns to the Guardian. The provenance of a
value introduced by parameter_overrides (its origin, and whether a
Guardian may raise the trust of data it rewrites) is deferred to a
later version alongside the audit-chain integrity work, since
tamper-evidence is what makes a Guardian trust-raise auditable.

The schema oneOf enforces exclusivity and the non-empty rule; the
disjoint-target rule is normative in Specification 6.3 because path
disjointness is not expressible in JSON Schema.

Addresses the PR review comment on
specification/v0.1.0/modifications.json:7.
Adds a normative "Signed input" rule to Specification 10 and repoints the
two references that pointed at an undefined "same canonical input."

WHY: Section 10 defined the signature envelope and the hybrid value
encoding, and the hybrid rule said verifiers must verify "over the same
canonical input," but the canonical input was never defined anywhere.
The Signature object carried no signed_fields or canonicalization
pointer either. With the input undefined, a producer that signed only an
inner payload (for example toolCallResult.outputs) produced a signature
an attacker could lift into a forged envelope with a different
session_id or request_id; verification still passed because the
signature never committed to the envelope. This is a cross-context
forgery, worst under the HMAC-SHA256 baseline.

The signed input is now fixed: the RFC 8785 (JCS) canonicalization of
the envelope with the signature field removed, UTF-8 encoded. It binds
the signature to method, metadata.session_id, request_id, and timestamp,
so a captured signature cannot be re-wrapped. Verifiers MUST recompute
this form and MUST reject a signature that does not cover it
(SIGNATURE_INVALID). This matches the JCS rule already used for the
audit chain in Section 8.

The input is fixed by the rule rather than declared per message. A
discoverable signed_fields field was considered and rejected: once the
input is normatively fixed, signed_fields would only let a producer
advertise partial coverage that omits session_id, reopening the gap the
rule closes, so it is the wrong direction.

Addresses the PR review comment on
docs/spec/instrument/specification.md:274.
…review)

Adds chain_hash and a signature to the response envelope and a normative
Specification 8.6, so the audit chain is tamper-evident to an outside
party rather than purely Guardian-internal.

WHY: SessionContext and its rolling chain_hash lived only on the
Guardian, and the response envelope had no chain_hash and no signature
field at all. A Guardian could drop a ContextEntry (for example a DENY),
recompute the chain forward, and present a clean history later, with no
on-wire signal of the rewrite. Calling that an audit chain implied a
tamper-evidence the wire never delivered.

Changes:
  - response-envelope.json: AcsResult gains chain_hash (lowercase-hex
    SHA-256 head) and signature (referencing the request envelope's
    Signature definition, computed on the canonical input from
    Specification 10).
  - Specification 8.6 (normative): for every step where the Guardian
    writes a ContextEntry (the content-bearing steps), the Guardian MUST
    include the resulting chain_hash in its response, covered by the
    response signature. Publishing the head as each decision is made
    lets an observer that records traffic detect a later rewrite.
  - CHAIN_MISMATCH is exposed two ways for two detectors: a deny
    reason_code 'chain_mismatch' when the Guardian can still return a
    decision, and the existing error code -32007 when it cannot.
    'chain_mismatch' is added to the reason_codes vocabulary.

Scope, stated honestly: tamper-evidence is bounded by the signature in
use. Under the HMAC baseline a published, signed head gives integrity
and lets a key-holder verify the chain, but does not bind the Guardian
itself, which holds the symmetric key and can re-sign a rewritten head.
Non-repudiation, proving to a third party that a specific Guardian
issued a specific head, requires the asymmetric ACS-Crypto profile. The
baseline catches accidental divergence and cross-Guardian disagreement;
defeating a compromised Guardian is a profile-level guarantee, not Core.

Reviewed the AARM reference architecture (arXiv 2602.09433) for
conflicts: AARM only recommends hash-chaining and lists non-repudiation
as an open problem, so a signed, published, hash-chained head exceeds
its accountability requirements and conflicts with none of them.

Addresses the PR review comment on
docs/spec/instrument/specification.md:177.
…(PR review)

Promotes two guarantees into the ACS-Core baseline and rewrites the
conformance and landing-page wording so the badge matches what Core
actually delivers.

WHY: the cover page promised ACS makes agents inspectable, traceable,
and instrumentable, but ACS-Core required none of Trace, AgBOM,
provenance, or signatures, and no rule forced the Observed Agent to wait
for or honor a decision. A vendor could ship "ACS-Core conformant" and
deliver none of the three, since every hook could fire and be ignored.

Core now requires two wire-observable guarantees, neither of which
touches the Guardian's policy logic:
  - Baseline integrity: every request and response is signed over the
    Section 10 canonical input. HMAC-SHA256 is the baseline; its
    per-session key is HKDF-derived from deployment-provided input
    keying material (a pre-shared secret or a transport channel binding)
    and the session_id. No in-band key-exchange is defined in v0.1; a
    deployment with no shared secret or secure transport uses an
    ACS-Crypto algorithm instead. Asymmetric and PQC stay ACS-Crypto.
  - Decision honoring (Section 6.4): the Observed Agent MUST wait for
    the decision up to the negotiated timeout and apply it. On timeout
    it applies the on_timeout posture, default proceed (fail-open) so
    ACS cannot take production down, and a fail-open proceed MUST be
    recorded as an audit event so the bypass is visible. A decision that
    arrives in time MUST be honored regardless of posture. on_timeout is
    added to the handshake ServerHello.

We did NOT add the proposed "Guardians MUST evaluate rather than
blanket-allow" rule. It dictates vendor implementation and is not
checkable from the wire; a permissive policy is a legitimate deployment
choice. Badge integrity comes from the two mechanical guarantees plus
honest wording, not an unprovable diligence mandate.

The conformance text and acs.md cover are rewritten to be precise.
"ACS-Core conformant" means the channel is authenticated and the agent
honors decisions. It does not claim the deployment is secure, that
policies are strict, or that the audit chain is tamper-evident against a
compromised Guardian: the HMAC baseline is symmetric, so non-repudiation
and tamper-evidence against the Guardian are the ACS-Crypto and
ACS-Audit profiles. Trace and Inspect stay profiles, so the cover now
gates the traceable and inspectable claims on them.

Addresses the PR review comment on docs/spec/conformance.md:26.
…roduction

Review question: if ACS is adopted as vendor middleware enforcing
control points via hooks, where does the spec fail closed when the
vendor's service goes down? Four gaps, each with its design decision:

1. Unify timeout, transport failure, and error-without-decision into
   one "decision failure" condition (Section 6.4). The old text was
   normative only "on timeout", but a vendor outage usually presents
   as a fast failure (connection refused, 503, JSON-RPC -32603), not
   a timeout. A conformant implementation could read those opposite
   ways: fail closed (no decision means no proceed) or fail open
   without the mandated audit event. One condition, one posture, one
   audit rule removes the divergence. Registry errors (17.1) keep
   their recovery actions within the timeout budget; unambiguous
   failures may resolve immediately so fail-open adds no latency.

2. Rename on_timeout -> on_decision_failure. The field now governs
   more than timeouts, and a field named for one failure mode invites
   implementers to scope it back down. Renaming is cheap pre-release;
   it will not be after v0.1.0 ships.

3. Keep fail-open (proceed) as the default, with mandatory audit.
   Inline enforcement ecosystems learned this the hard way: K8s
   admission webhooks default failurePolicy Fail and a webhook outage
   has locked entire clusters. A control standard that halts
   production on vendor outage will be ripped out, not tightened. The
   trade is stated in the spec text - an adversary who can disrupt
   the channel converts control into audit - so fail-closed is an
   informed opt-in, not a buried one.

4. Add a startup posture for handshake failure (Section 4.1).
   on_decision_failure is negotiated in the handshake, so it cannot
   govern a handshake that never completes - a bootstrap circularity
   the spec previously left silent. Default proceed matches the
   mid-session posture so one outage story covers both. A Guardian
   refusal is a decision, not a failure, and is excluded. Unguarded
   sessions must be recorded locally since no Guardian can receive
   the event. Mid-flight Guardian attachment is explicitly deferred
   to v0.2 rather than left undefined by omission.

Two clarifications ride along: the timeout_decision deny default is
declared deliberately opposite to on_decision_failure proceed (an
expired DEFER is a Guardian that flagged a concern and vanished, not
one that never answered, so a degraded Guardian fails closed where a
silent one fails open); and ping's signature exemption means liveness
cannot prove the signed hook path is healthy (a key-resolution outage
fails every hook while ping stays green), so deployments should
monitor decision failures directly. Per-method timeout budgets get a
SHOULD because every millisecond of timeout is worst-case latency on
the hot path and the protocol sets no ceiling.
@rocklambros
rocklambros self-requested a review June 5, 2026 06:57
@rocklambros
rocklambros merged commit f46d260 into GenAI-Security-Project:main Jun 5, 2026
bar-capsule added a commit to bar-capsule/ACS that referenced this pull request Jun 17, 2026
Two passes folded into one commit because they share helper code.

## Pass A — Security review (SECURITY.md threat model + tests + fixes)

12-threat model documented in adapters/SECURITY.md. 5 code fixes for
high-priority threats; 7 out-of-scope items documented honestly.

- T5 SSRF via ACS_GUARDIAN_URL: validate_guardian_url() allowlist
  (http/https only); called from every adapter's call_guardian and
  from do_handshake / ping in _common.
- T6 Guardian DoS via oversized request body: MAX_REQUEST_BODY_BYTES
  (1 MiB) cap, matching the handshake's max_payload_size_bytes
  advertisement. Refuses Content-Length > cap with HTTP 413.
- T7 leaky HMAC secret file: _check_secret_file_perms() refuses
  mode & 0o077 != 0, wrong owner, or symlink. Raises
  SecretFilePermissionsError instead of silently using a leaked key.
- T8 cache poisoning: save_session_state and do_handshake create
  cache dirs mode 0700 and files mode 0600 via os.open(O_CREAT, 0o600)
  so other local users can't read or poison adapter state.
- T9 regex DoS via huge command: scan_destructive_bash_safely()
  refuses inputs > 8 KiB, emits audit event, returns
  "input_too_large" sentinel — caller MUST treat as suspicious.

16 new tests in _common/tests/test_security.py, each named for the
specific attack it falsifies.

## Pass B — Harsh-reviewer audit + 3 production failure modes + fixes

Stepped back from the work, audited as a hostile reviewer. Identified
the 3 most-likely production failure modes; wrote tests that fail on
the current code; fixed the bugs.

- BUG GenAI-Security-Project#1 NAT _correlation_request_id collision: the old uuid5-from-
  (session, function, kwargs-hash) was deterministic. Two calls to
  the same tool with the same args (list_files(), get_status(), any
  idempotent tool — very common) produced identical request_ids and
  the Guardian's replay protection rejected the second with
  REPLAY_DETECTED. Fix: stash a fresh uuid4 on the context in
  pre_invoke; post_invoke reads it back. Per-call uniqueness + pre/
  post correlation both preserved.
- BUG GenAI-Security-Project#2 Guardian state lost on restart: GuardianState was RAM-only.
  A Guardian restart (deploy / OOM / autoscaling roll) wiped
  seen_request_ids, opening a replay window for every previously-sent
  envelope. §10.3 doesn't pause for the duration of a deploy. Fix:
  per-session state persisted to a JSON file under
  ACS_GUARDIAN_STATE_DIR (default ~/.cache/acs-guardian-state/),
  mode 0700/0600. State loads on first session-touch, persists on
  every mutation in check_replay and append_to_chain.
- BUG GenAI-Security-Project#3 lifecycle subscription race: _ensure_lifecycle_subscribed
  was a check-then-set with no lock. Two parallel pre_invoke calls
  (normal in NAT) both saw _lifecycle_subscribed=False and both
  subscribed; every WORKFLOW event then fired its ACS lifecycle hook
  twice. Fix: threading.Lock around the check-then-set, with re-check
  inside the lock.

4 new tests in nat/tests/test_failure_modes.py: 3 for the failure
modes above, 1 regression guard ensuring the BUG GenAI-Security-Project#1 fix preserves
pre/post correlation (post_invoke's request_id_ref must equal
pre_invoke's request_id, per tool-call-result.json:19-23).

## Test-strengthening: catching 2 mutations that previously slipped

Two mutation tests passed previously because of weaknesses in the
tests themselves:

- RollingChain::test_chain_hash_links_consecutive_requests only
  asserted hashes differed. Dropping previous_hash from the chain
  still produced different per-request hashes, so the mutation slipped.
  Strengthened test_chain_is_recomputable now EXTERNALLY recomputes
  the expected chain hash across 3 entries (now possible because the
  Guardian uses the client's timestamp) and asserts byte-equality.
  Also asserts the published hash does NOT match the "no
  previous_hash" computation.
- Cursor envelope-schema fixtures all used SESSION_UUID, so a
  skip-coercion mutation slipped. Added
  UuidCoercionForNonUuidCursorIds (2 tests) with conv-abc123 /
  chat_xyz / test-cc-session inputs, asserting the adapter coerces
  them to valid UUIDs and that the coercion is deterministic.

## Test counts after this commit (all green, zero hidden skips)

  _common:            16 security
  claude-code:        32 round-trip + envelope-schema
  cursor:             50 round-trip + envelope-schema + uuid-coercion
                      (1 intentional manual-Cursor skip)
  example-guardian:   20 spec-compliance
  nat:                24 (test_adapter 7 + test_live 5 +
                          test_envelope_schema 6 + test_lifecycle 2 +
                          test_failure_modes 4)

  Total: 142 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bar-capsule added a commit to bar-capsule/ACS that referenced this pull request Jun 17, 2026
Each item has a falsifying test in adapters/_common/tests/test_edge_cases.py
(17 tests total). Items not requiring code changes still have tests that
codify the safe behavior so a future regression would be caught.

## Items 1-12

GenAI-Security-Project#1  rfc8785 JCS consistency — test confirms fallback matches the
    rfc8785 package byte-for-byte on every ACS envelope shape we ship.
    No code change needed; a mixed-install signature mismatch would
    surface as test failure.

GenAI-Security-Project#2  Guardian regex DoS, server-side: _matches_destructive_bash now
    returns "too_large" for inputs > DESTRUCTIVE_SCAN_MAX_LEN (8 KiB).
    The Guardian denies with reason_codes=["input_too_large"] —
    fail-safe direction. Previously, _common had the cap but the
    Guardian iterated patterns directly, leaving the server unprotected.

GenAI-Security-Project#3  HA Guardian replay window: persist() now takes an exclusive flock
    on a .lock sidecar, re-reads on-disk state, merges (union of
    seen_request_ids / seen_nonces with earliest-timestamp wins), and
    atomically writes. check_replay re-reads the state on every call
    so Guardian A's writes are visible to Guardian B within one
    request. Cross-instance replay window closed under shared
    ACS_GUARDIAN_STATE_DIR.

GenAI-Security-Project#4  Unbounded seen_request_ids: switched to dict {rid: timestamp}.
    New evict_old_request_ids() drops entries older than 2 × skew
    window (replay impossible past skew anyway). check_replay calls
    eviction opportunistically every 100 inserts. Memory bound is now
    O(skew_window / inter-request-time), not unbounded. Backwards-
    compat for list-format state files preserved.

GenAI-Security-Project#5  Handshake cache TTL: do_handshake skips cache files older than
    ACS_HANDSHAKE_CACHE_TTL_SECONDS (default 3600s). Operator config
    changes propagate within the TTL.

GenAI-Security-Project#6  NAT id(context) collision: WeakKeyDictionary fallback for
    contexts that reject attribute assignment. Last-resort path
    (object isn't weak-referenceable either) returns a fresh uuid4
    per call and emits an audit event — pre→post correlation is
    lost in that path, but no silent collision.

GenAI-Security-Project#7  Unicode / NULL / surrogate round-trip: emoji, NULL bytes, multi-
    plane unicode all sign+verify cleanly. JCS handles them via
    UTF-8 encoding; no code change needed.

GenAI-Security-Project#8  ISO 8601 parse resilience: parse_iso8601 already accepts Z suffix,
    timezone offsets, millisecond + microsecond precision. Test
    codifies the accepted shapes + asserts garbage is rejected.

GenAI-Security-Project#9  ACS_GUARDIAN_HOST_ALLOWLIST: optional env-var allowlist that
    restricts validate_guardian_url to specific hostnames in addition
    to the http/https scheme check. Defense in depth against env-var
    attacks that smuggle a valid http:// URL to internal services.

GenAI-Security-Project#10 Cursor session-state file collision: _session_state_path now
    accepts an optional workspace parameter folded into the hash key.
    Cursor adapter passes the workspace_path / cwd so two Cursor
    windows with the same non-UUID conversation_id can't share state.

GenAI-Security-Project#11 Guardian envelope schema validation: if jsonschema + ACS_SPEC_DIR
    are available, every incoming envelope is validated against
    request-envelope.json before policy evaluation. Malformed envelopes
    rejected with -32600 Invalid Request. system/ping and
    handshake/hello exempt because their payload shapes differ.

GenAI-Security-Project#12 State-file hash length: bumped _session_state_path and
    _handshake_cache_path hashes from sha256[:16] (64-bit) to full
    sha256 (256-bit). Eliminates birthday collisions over deployment
    lifetime.

## Test counts after this commit (all green, 1 intentional manual skip)

  _common:            33 (16 security + 17 edge-cases)
  claude-code:        32
  cursor:             50
  example-guardian:   20
  nat:                24

  Total: 159 tests.

## Side-effects of the fixes

- Round-trip test fixtures updated to use real UUID session_ids
  (claude-code/test_adapter.py). Old "test-cc-session" fails the new
  Guardian-side envelope-schema check, which is correct — non-UUID
  session_ids never reached the Guardian from real Claude Code.
- Cursor adapter wires workspace through to load/save/record session
  state for GenAI-Security-Project#10 (new _workspace helper).
- example_guardian.py imports DESTRUCTIVE_SCAN_MAX_LEN from acs_common
  to keep the cap in one place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rocklambros added a commit that referenced this pull request Sep 10, 2026
Fixes 44 dead links in published specification prose, and adds the guard
that would have caught them.

## The defect

Every link of the form
`https://github.com/afogel/ACS_official/blob/dev/specification/v0.1.0/...`
returns **HTTP 404**. The fork is public, but it has no `dev` branch. 25
of the 44 are live on the published hooks page right now.

They all arrived in one commit, `f46d260 "Integrate canonical v0.1.0
(#2)"`, the donation import. The links were self-referential when the
spec lived in that repo, the content moved to `GenAI-Security-Project`,
and the links did not. The `dev` branch was later removed, turning stale
links into broken ones.

Affected: `hooks.md` (25), `specification.md` (8), `inspect/README.md`
(3), `trace/events.md` (2), and one each in `extend_cyclonedx.md`,
`extend_spdx.md`, `extend_swid.md`, `extend_ocsf.md`,
`extend_opentelemetry.md`.

## The fix

Each file link now points at that schema's own `$id`. The mapping was
verified programmatically against `specification/v0.1.0/**/*.json`
before anything was rewritten: every linked path exists, and its `$id`
equals the replacement URL exactly. Those URLs return 200 and are
already re-checked every six hours by `monitor-pages.yml`.

That location is also durable in a way the old one was not. A
`blob/<branch>` URL rots when a branch is renamed. A schema `$id` is the
identity the specification already commits to.

One of the 44 is a directory browse link with no `$id`. It points at the
canonical repository tree instead.

## Why this was invisible

Three things, each individually reasonable:

- No link checker exists anywhere in CI.
- `mkdocs build --strict` validates internal references only, never
external URLs.
- `tests/conftest.py` deliberately exempts `("a", "href")` as
non-fetching, which is correct, because that guard protects against
resources a browser fetches rather than links a human clicks.

So the repository had strong guards against third-party asset injection
and none against dead links in its own normative prose.

## The guard

`tests/test_doc_links.py`, committed before the fix so the failure is in
the history.

- **Foreign repositories.** No file under `docs/` may link to a
`github.com` repository outside a three-entry allowlist: this
repository, plus `prowler-cloud/py-ocsf-models` and `ocsf/examples`,
which are legitimate OCSF references already present. Positive
allowlist, so a new foreign repository fails until someone adds it
deliberately.
- **Schema links stay canonical.** Every docs link into the published
schema namespace must resolve to a schema that exists under
`specification/` and whose `$id` equals that exact URL. This catches
drift in both directions, including a schema that gets moved or renamed.

## Verification

- Guard finds 44 violations before the fix, 0 after
- `uv run pytest`: 204 passed, 1 skipped
- `uv run mkdocs build --strict`: clean
- Five rewritten URLs fetched live: all 200
- No `afogel` reference remains anywhere in the repository

---------

Signed-off-by: rocklambros <rock@rockcyber.com>
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.

6 participants