agent-capability-negotiation
Zero-dependency Python plugin for canonical capability manifests and 3-step pre-delegation negotiation handshake.
Before a parent AI agent delegates a task, it has no standardized way to know what a subagent can actually do. This plugin solves pre-delegation capability blindness with a signed manifest schema and a CAPABILITY_QUERY → CAPABILITY_ADVERTISE → CAPABILITY_BIND handshake.
Quick Start
pip install agent-capability-negotiation
Create a signed capability manifest:
from agent_capability_negotiation import create_manifest
manifest = create_manifest(
agent_id="subagent-abc123",
tools=["read_file", "write_file", "bash"],
scope={"read": ["/project/**"], "write": ["/project/src/**"], "forbid": []},
model="claude-sonnet-4-20250514",
context_window_tokens=200000,
skills=["python", "git"],
ttl_seconds=3600,
secret_key="shared-secret",
)
print(manifest.to_dict())
Negotiate a capability binding (parent side):
# 1. Query a subagent
python -m agent_capability_negotiation negotiate query \
--parent-id parent-xyz --subagent-id subagent-abc123
# 2. (Subagent responds with advertise + signed manifest)
# 3. Bind agreed scope
python -m agent_capability_negotiation negotiate bind \
--negotiation neg.json --agreed-scope-read /project/** \
--agreed-scope-write /project/src/**
# 4. Check compatibility before binding
python -m agent_capability_negotiation negotiate diff-report \
--manifest manifest.json --requires-tools read_file,write_file,bash \
--requires-scope-read /project/**
⚡ Performance & Benchmarks
This is a zero-dependency, stdlib-only library. It performs pure in-memory JSON/HMAC operations with no I/O, network, or external process overhead. No comparative benchmark against alternatives applies — there are no comparable alternatives on PyPI.
| Operation | Latency |
|---|---|
create_manifest (sign) |
< 0.1 ms |
verify_manifest (HMAC check) |
< 0.1 ms |
tokenize_manifest (base64) |
< 0.1 ms |
diff_report (5 tools, 3 scopes) |
< 0.5 ms |
Local replication: python3 benchmarks/run_benchmark.py
Why agent-capability-negotiation?
The problem: Before delegating to a subagent (via delegate_task, kanban_create, Codex subagent, etc.), a parent agent cannot discover the subagent's actual capabilities. This leads to:
- Blind delegation — task fails at runtime because the subagent lacks a required tool
- Capability drift — subagent tool bindings differ from what the parent assumed
- No pre-flight signal — existing solutions bind scope only after the subagent is already running
Existing tools that don't solve this:
subagent-delegation-contract(cycle_45): post-delegation scope binding, not pre-delegation discovery- Cursor/Copilot workspace permissions: static allowlists, not negotiated per-delegation
multi-agent-protocol(npm): message routing schema, not capability negotiation
Trade-off decisions:
- Zero runtime dependencies (stdlib only) — no install friction, no supply-chain risk
- HMAC-SHA256 signatures using stdlib
hmac+hashlib— nocryptographypackage needed - TTL on manifests — prevents stale capability info from causing runtime failures
- Structured diff report — human + machine-readable incompatibility signal before binding
Key Features
- Canonical capability manifest — signed JSON declaring tools, scoped permissions, model version, context window, skill tags
- 3-step negotiation handshake — CAPABILITY_QUERY → CAPABILITY_ADVERTISE → CAPABILITY_BIND (or NEGOTIATE_REJECT)
- HMAC-SHA256 signed manifests — stdlib only, prevents spoofing
- Capability diff report — compare task requirements against manifest, surface
[OK]/[WARN]/[ERROR]per requirement - Manifest TTL — re-advertise after long-running tasks or config changes
- Base64 token round-trip — serialize manifests for transport over text-only channels
- Zero runtime dependencies — pure Python stdlib
API Reference
Manifest API (agent_capability_negotiation.manifest)
from agent_capability_negotiation import (
create_manifest,
verify_manifest,
manifest_from_dict,
manifest_to_dict,
diff_manifests,
tokenize_manifest,
untokenize_manifest,
is_valid_tool_token,
is_valid_path_glob,
scope_covers,
CapabilityManifest,
ManifestError,
SignatureError,
ExpiredError,
)
# Create and sign a manifest
manifest = create_manifest(
agent_id="subagent-abc123",
tools=["read_file", "write_file", "bash"],
scope={"read": ["/project/**"], "write": ["/project/src/**"], "forbid": ["/project/secrets/**"]},
model="claude-sonnet-4-20250514",
context_window_tokens=200000,
skills=["python", "git"],
ttl_seconds=3600,
secret_key="shared-secret",
)
# Verify a manifest
ok, reason = verify_manifest(manifest, secret_key="shared-secret")
# -> (True, "signature ok, TTL ok, schema version compatible")
# Diff two manifests
diff = diff_manifests(manifest_a, manifest_b)
# -> DiffReport with per-tool and per-scope findings
# Token round-trip (base64-encoded signed blob)
token = tokenize_manifest(manifest, secret_key="shared-secret")
restored = untokenize_manifest(token, secret_key="shared-secret")
assert restored.agent_id == manifest.agent_id
# Validation helpers
is_valid_tool_token("read_file") # True
is_valid_tool_token("bash:git") # True (scoped tool)
is_valid_path_glob("/project/**/*.py") # True
scope_covers(["/project/**"], "/project/src/app.py") # True
Negotiation API (agent_capability_negotiation.negotiation)
from agent_capability_negotiation import (
issue_query,
advertise,
bind,
reject,
status_of,
diff_report,
build_diff_report,
Negotiation,
NegotiationState,
NegotiationMessage,
Requirement,
DiffFinding,
DiffReport,
)
# Step 1: parent issues a CAPABILITY_QUERY
neg = issue_query(parent_id="parent-xyz", subagent_id="subagent-abc123")
# neg.state == NegotiationState.QUERY_SENT
# Step 2: subagent advertises its manifest
advertise(neg, manifest)
# neg.state == NegotiationState.ADVERTISED
# Step 3: parent binds agreed scope
bind(neg,
agreed_scope_read=["/project/**"],
agreed_scope_write=["/project/src/**"],
agreed_forbid=["/project/secrets/**"],
binding_ttl_seconds=3600,
secret_key="shared-secret")
# neg.state == NegotiationState.BOUND
# Reject at any step
reject(neg, reason="scope too restrictive", party="subagent")
# neg.state == NegotiationState.REJECTED
# Query current status
status = status_of(neg)
# {"state": "BOUND", "binding_id": "bind-xyz", "expires_at": "..."}
# Diff task requirements against a manifest
report = build_diff_report(manifest, [
{"kind": "tool", "name": "read_file"},
{"kind": "tool", "name": "bash:git"},
{"kind": "scope", "mode": "read", "patterns": ["/project/**"]},
])
# report.summary() ->
# [OK] read_file — supported
# [WARN] bash:git — NOT advertised (available: bash without git scope)
# [OK] scope read — /project/** satisfied
# RECOMMENDATION: use git CLI wrapper instead of bash:git
CLI Reference
Module entry point
python -m agent_capability_negotiation [--version] [--help]
manifest subcommands
# Create a signed capability manifest
python -m agent_capability_negotiation manifest create \
--agent-id subagent-abc123 \
--tools read_file,write_file,bash \
--scope-read /project/** \
--scope-write /project/src/**,/project/tests/** \
--forbid /project/secrets/** \
--model claude-sonnet-4-20250514 \
--context-window-tokens 200000 \
--skills python,git \
--ttl-seconds 3600 \
--secret-key shared-secret
# Verify a manifest
python -m agent_capability_negotiation manifest verify \
--manifest manifest.json --secret-key shared-secret
# Diff two manifests
python -m agent_capability_negotiation manifest diff \
--manifest-a a.json --manifest-b b.json
# Serialize to base64 token
python -m agent_capability_negotiation manifest tokenize \
--manifest manifest.json --secret-key shared-secret
# Deserialize from base64 token
python -m agent_capability_negotiation manifest untokenize \
--token-file token.txt --secret-key shared-secret
negotiate subcommands
# Emit CAPABILITY_QUERY (parent -> subagent)
python -m agent_capability_negotiation negotiate query \
--parent-id parent-xyz --subagent-id subagent-abc123
# Attach manifest (subagent response to CAPABILITY_QUERY)
python -m agent_capability_negotiation negotiate advertise \
--negotiation query.json --manifest manifest.json --secret-key shared-secret
# Confirm CAPABILITY_BIND (parent)
python -m agent_capability_negotiation negotiate bind \
--negotiation advertised.json \
--agreed-scope-read /project/** \
--agreed-scope-write /project/src/** \
--agreed-forbid /project/secrets/** \
--binding-ttl-seconds 3600 \
--secret-key shared-secret
# Reject negotiation
python -m agent_capability_negotiation negotiate reject \
--negotiation neg.json --reason "scope too restrictive" --party parent
# Show negotiation status
python -m agent_capability_negotiation negotiate status --negotiation neg.json
# Diff task requirements against manifest
python -m agent_capability_negotiation negotiate diff-report \
--manifest manifest.json \
--requires-tools read_file,write_file,bash:git \
--requires-scope-read /project/** \
--requires-scope-write /project/src/** \
--human
Plugin Scripts
For Hermes/Claude Code plugin integration:
plugins/agent-capability-negotiation/
├── plugin.json # Plugin manifest
├── hooks.json # Pre-delegation handshake hook
├── rules/negotiation-protocol.md # Schema + negotiation states
├── skills/agent-capability-negotiation/SKILL.md # Behavioral runbook
└── scripts/
├── capability_manifest.py # Standalone manifest CLI
└── negotiate.py # Standalone negotiation CLI
# Smoke tests for plugin scripts
bash plugins/agent-capability-negotiation/scripts/capability_manifest.py --help
bash plugins/agent-capability-negotiation/scripts/capability_manifest.py --version
bash plugins/agent-capability-negotiation/scripts/negotiate.py --help
bash plugins/agent-capability-negotiation/scripts/negotiate.py --version
Limitations
- HMAC key distribution is out of scope — both parties must share a secret key via a channel this plugin does not manage
- Manifest transport is not handled — the base64 token must be transmitted by an external channel (stdout, file, message bus)
- Non-repudiation is not provided — HMAC only gives integrity, not authorship proof; use asymmetric signing if non-repudiation is required
- Context window units are self-reported by the subagent and not independently verified
- Path glob scope uses simple fnmatch-style patterns; complex filesystem ACLs require OS-level enforcement beyond this plugin
- Clock skew can cause legitimate manifests to appear expired if parties' system clocks differ significantly
Non-Goals
- Post-delegation scope drift detection (see
subagent-delegation-contract, cycle_45) - Message routing or multi-agent coordination protocols
- Asymmetric-key signature schemes
- Automatic key exchange or negotiation protocol establishment
- Integration with specific agent frameworks beyond the plugin interface
License
MIT License — see LICENSE.
Metadata
Release files for agent-capability-negotiation 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agent_capability_negotiation-0.1.0.tar.gz | 37.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_capability_negotiation-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.8 kB
Release files / agent_capability_negotiation-0.1.0.tar.gz
| Download URL | agent_capability_negotiation-0.1.0.tar.gz |
|---|---|
| Size | 37.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c34e7863159ad0293cbc14239c4ce14bdad0b22e524a95454fd23bf5f8d44e31
|
|
BLAKE2b-256 checksum How to use checksums |
6f8a50d80a23414da8c3e1e3c7327df41be06b0e0ed9fc4e3c1c377ec7bfb8da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Release files / agent_capability_negotiation-0.1.0-py3-none-any.whl
| Download URL | agent_capability_negotiation-0.1.0-py3-none-any.whl |
|---|---|
| Size | 22.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f20e32e79266dd6f14f1cf55ad839e2cbf35a093cad2fe16dc1e6709f85348a
|
|
BLAKE2b-256 checksum How to use checksums |
87a35881d3e44713b1c5868aa1170f68d2247fd2a9c2b8dc2e22d652d0943528
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|