Español: README.es.md
Python reference implementation of the return path MCP lacks: a standard channel for an AI agent to report the friction it hit while using a tool, addressed to that tool's maintainer.
Normative spec: spec/SPEC.md (RFC 2119) · canonical JSON Schemas · conformance test vectors
Design rationale (Spanish): docs/superpowers/specs/2026-05-30-afp-protocol-design.md
uv sync# 1. Build a complete field report from a partial JSON
uv run afp report --from partial.json --out report.json
# 2. Validate (JSON Schema + secret hard-block)
uv run afp validate report.json
# Validate an afp.json manifest
uv run afp validate-manifest afp.json
# 3. Deposit it respecting the routing policy
# Without an afp.json in --dir, only local/draft is allowed (never auto remote submission).
uv run afp submit report.json --dir . --sink draft
# Atomic alternative: build and deposit in a single call
uv run afp report --from partial.json --submit --dir . --sink draft
# Review local drafts before promoting them
uv run afp drafts list --dir .
uv run afp drafts show afp_<id> --dir .
# Explicit promotion after human review
uv run afp drafts promote afp_<id> --dir . --sink github_issues
# Discard a stale draft (friction verified fixed, or promoted issue closed);
# reason + timestamp are recorded in .afp/discarded.json
uv run afp drafts discard afp_<id> --dir . --reason "verified fixed: ..."Every time a submission is deposited as draft (submit, report --submit,
dogfood without a manifest), AFP emits on stderr a line with the stable
prefix AFP-REVIEW: stating how many drafts are pending and the command to
review them:
AFP-REVIEW: 2 drafts pendientes de revisión humana → afp drafts list --dir .
This is a documented contract: an agentic harness can detect the
AFP-REVIEW: prefix on stderr to show the human there are drafts to approve,
and a human can read it directly. The OK: ... -> draft:/path stays on stdout.
Minimal partial.json:
{
"subject_uri": "pkg:pypi/ruff",
"goal": "lint the project",
"expectation": "JSON output with the errors",
"observed": "plain-text output with no structure",
"friction_type": "wrong_output",
"fault_domain": "tool",
"severity": "degraded"
}integrations/ teaches agents to file AFP drafts when they hit
tool friction, and to verify/discard stale drafts (always --sink draft; a
human reviews and promotes).
Install AFP once:
uv tool install git+https://github.com/Vasallo94/afp-protocolThen install the harness integration you use:
afp integrations install codex --global
afp integrations install claude-code --global
afp integrations install cursor --project .
afp doctorIntegrations install from packaged resources; cloning this repository is not
required. Agents emit drafts only. Humans review with afp drafts list/show
and promote with afp drafts promote.
Tool maintainers can initialize the trust manifest that enables reviewed remote promotion:
afp init \
--dir . \
--subject mcp://github.com/acme/weather-mcp \
--sink github_issues \
--repo acme/weather-mcpTo report friction found while using this very implementation:
uv run afp dogfood \
--goal "validate a report generated by AFP" \
--expectation "the CLI should explain why it fails" \
--observed "the message did not make clear which field was invalid" \
--friction-type confusing_interface \
--fault-domain tool \
--severity degraded \
--sink draftBy default this creates a local draft under .afp/drafts/. The AFP repo
declares its own afp.json, so manually reviewed reports can later be
promoted to issues on Vasallo94/afp-protocol using the remote sink.
Without an afp.json declared by the tool, AFP never auto-submits to a
third-party repo: it only writes to local (spool) or draft (human review).
Remote submission (e.g. github_issues) is always maintainer opt-in, and
the report's subject_uri is additionally verified to fall under the one
declared by the manifest (anti-spoofing):
- PURL: same package base (
@versionand#fragmentdon't change the owner). - http(s)/mcp: same host/authority (
api.acme.com.evil.com≠api.acme.com) and the report's path equals the manifest's or is a per-segment sub-path (/v1owns/v1/charges, not/v1abc).
Sensitive data handling (§5): high-confidence secrets (tokens, keys, JWT,
Bearer) are a hard-block — if detected, the submission is aborted.
Direct PII (email) does not abort: it is redacted to
[REDACTED_EMAIL] and the report continues, because an email mentioned in
free text is usually useful information for the maintainer.
Remote sinks (github_issues/gitlab_issues) are idempotent by
report_id: a local ledger (.afp/submitted.json) maps report_id -> ref,
so resubmitting the same report (e.g. a retry after a network timeout whose
submission actually landed) returns the previous ref instead of opening a
duplicate issue. It is deterministic and does not depend on the provider's
search indexing lag.
Local sinks are not deduplicated: local is an append spool and draft
is already overwritten by report_id. Semantic deduplication across
distinct reports (grouping "14 agents blocked on the same thing") remains the
Harvester's job (§7 of the spec).
To route reports to an internal GitLab, the tool declares in its afp.json:
{
"afp_version": "0.2",
"subject_uri": "mcp://<gitlab-host>/<group>/<project>",
"sink": {
"type": "gitlab_issues",
"host": "<gitlab-host>",
"repo": "<group>/<project>",
"label": "afp-report"
},
"redaction": "required",
"accepts_remote": true
}Requires glab installed and authenticated against the host
(glab auth login --hostname <host>). Promotion to an issue is explicit and
reviewed: afp drafts promote <id> --dir <repo> --sink gitlab_issues.
uv run pytest -v