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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_capability_negotiation-0.1.0.tar.gz.
File metadata
- Download URL: agent_capability_negotiation-0.1.0.tar.gz
- Upload date:
- Size: 37.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c34e7863159ad0293cbc14239c4ce14bdad0b22e524a95454fd23bf5f8d44e31
|
|
| MD5 |
1c244bf08643c36e1987cb5667419791
|
|
| BLAKE2b-256 |
6f8a50d80a23414da8c3e1e3c7327df41be06b0e0ed9fc4e3c1c377ec7bfb8da
|
File details
Details for the file agent_capability_negotiation-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_capability_negotiation-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f20e32e79266dd6f14f1cf55ad839e2cbf35a093cad2fe16dc1e6709f85348a
|
|
| MD5 |
d7042c4695ff18976cfca5ae9c33481c
|
|
| BLAKE2b-256 |
87a35881d3e44713b1c5868aa1170f68d2247fd2a9c2b8dc2e22d652d0943528
|