Your AI agent hasn't broken any rule. It's just not doing its job anymore — and nothing is watching for that.
Every authorization/policy/budget tool on the market today answers "was this one action allowed?" None of them answer "is this agent, across hundreds of unsupervised cycles, still doing the job it was hired for?" Plumbline is a small, dependency-light primitive that answers exactly that second question — and freezes the agent, up its own org chart, the moment the answer becomes no.
A plumb line is the fixed, gravity-true reference a builder checks a wall against — "off-plumb" is the actual term of art for structural drift. This tool does the same thing for an AI agent: every agent gets a signed mission anchor stating what it was actually hired to do, and every heartbeat cycle gets scored against that fixed point. Sustained drift escalates up the agent's org chart and freezes the agent; it never trusts the agent's own account of whether it's still on task.
| You are... | Plumbline gives you... |
|---|---|
| Building/operating long-horizon autonomous agents (coding agents, ops bots, research agents that run unsupervised for hundreds of heartbeat cycles) | An early-warning circuit breaker before a drifted agent burns a full work cycle on the wrong thing |
| Building an agent-orchestration platform (Paperclip-style org-chart/heartbeat systems, or your own internal one) | A drop-in drift-detection primitive — library or CLI — that plugs into your existing dispatch loop in ~5 lines, see Integrating drift detection |
| An engineering manager who is the "org chart" above an agent | You're the one who actually gets notified on WARN/CRITICAL and reviews the tamper-evident ledger before re-anchoring — this is built to be read by a human, not just logged |
| Security/compliance, auditing agent deployments | A hash-chained, digest-only ledger of every drift decision — provable and tamper-evident without ever handing an auditor raw agent output |
2026's AI agent governance market (Microsoft's Agent Governance Toolkit, Marchward, HELM AI Kernel, Preloop, OpenLegion, and others) has converged on the same stack: policy-as-code, RBAC, budget caps, and tamper-evident audit logs for individual tool calls. That stack answers "was this one action authorized?" It does not answer a different, increasingly-cited failure mode in 2026 agent research: mission drift — an agent whose individual actions are each authorized and cheap, but whose trajectory across hundreds of autonomous heartbeat cycles has quietly wandered off the goal it was hired for. Production failures cluster around step 100, not step 1, and nothing in the governance stack above is watching for that, because authorization checks are inherently per-action, not per-trajectory.
Platforms built around heartbeat-driven, org-chart-shaped agent teams (e.g. Paperclip) are exactly where this shows up: an agent wakes on a schedule, checks in, and acts — for hundreds of cycles, with no human re-reading its original job description each time.
Plumbline is not an authorization or audit-log tool — it composes with those (see below), it doesn't replace them.
- Not an authorization/policy engine. It doesn't decide what an agent is allowed to do — pair it with a policy layer (Cedar, OPA, or a tool like the ones named above) for that.
- Not an anomaly detector over raw telemetry. It only ever compares an agent's heartbeat content to that agent's own signed goal — there's no cross-agent baseline or behavioral model to poison.
- Not identity-authenticated at the CLI layer yet.
reanchor --approverchecks org-chart membership, not a cryptographic signature from that approver — see the note incli.py. A production integration should require the approver's own signing key.
MissionAnchor— an ECDSA (P-256) signed, immutable record of an agent's declared goal and its chain of command (org_chart_path, immediate manager first). Signed once, at hire time, by the issuing manager's key. Neither the agent nor anything downstream of it can rewrite its own mandate and have it still verify.LexicalDriftScorer— a dependency-free, deterministic term-frequency cosine-distance scorer between the anchored goal and each heartbeat's content. No ML runtime dependency, so it's fast and reproducible in CI. It implements aDriftScorerprotocol, so a stronger embedding-based backend can be swapped in for production without touching the anchor, ledger, or escalation logic.EscalationEngine— EWMA-smooths drift across heartbeats (so one off-topic message doesn't trip anything) and classifies each cycle asOK/WARN/CRITICALagainst the anchor's own thresholds.WARNnotifies the agent's immediate manager only;CRITICALnotifies the entire chain of command and freezes the agent — further heartbeats are rejected until a manager in that chain callsreanchor().DriftLedger— a hash-chained, tamper-evident log of every decision (LeaseGuard/HWAAM-style sealed chain). Raw heartbeat text is never stored, only a SHA-256 digest, so the ledger is safe to hand to an auditor. Editing, deleting, or reordering a past entry is detectable by walking the chain.
$ python examples/demo.py
Simulates a 3-level org chart (CEO -> CTO -> Engineer) where the engineer is anchored to "ship the login page UI" and slowly drifts onto an unrelated blockchain-migration tangent over 9 heartbeat cycles. Real output from this repo, not hand-written:
Mission anchor for 'engineer-7': "Build and ship the login page UI with email and password authentication."
Org chart above this agent: ['cto', 'ceo']
warn_threshold=0.7 critical_threshold=0.9
[cycle 1] "Wired up the login form and password field masking."
decision=OK cumulative_ewma=0.213
[cycle 2] "Fixed a bug in the login page's password reset email flow."
decision=OK cumulative_ewma=0.308
[cycle 3] "Added client-side validation to the login page email field."
decision=OK cumulative_ewma=0.403
[cycle 4] "Read up on blockchain identity standards for an hour, seems interesting."
decision=OK cumulative_ewma=0.582
[cycle 5] "Started sketching a design for a blockchain-based identity migration."
-> notified cto: WARN (ewma=0.708)
decision=WARN cumulative_ewma=0.708
[cycle 6] "Spent the day researching a full migration to a blockchain identity ledger."
-> notified cto: WARN (ewma=0.795)
decision=WARN cumulative_ewma=0.795
[cycle 7] "Drafted a proposal to move the entire auth system onto a blockchain."
-> notified cto: WARN (ewma=0.857)
decision=WARN cumulative_ewma=0.857
[cycle 8] "Continued the blockchain identity ledger migration proposal."
-> notified cto: WARN (ewma=0.900)
decision=WARN cumulative_ewma=0.900
[cycle 9] "Started implementing the blockchain identity ledger migration."
-> notified cto: CRITICAL (ewma=0.930)
-> notified ceo: CRITICAL (ewma=0.930)
decision=CRITICAL cumulative_ewma=0.930
agent FROZEN pending re-anchor by ['cto', 'ceo']
Ledger has 9 tamper-evident entries.
Integrity check: VALID
CTO reviews the ledger, reassigns the agent's mission, and re-anchors it:
frozen=False ledger entries now=10
pip install -e .
plumbline anchor-create --agent-id engineer-7 \
--goal "Build and ship the login page UI with email and password authentication." \
--org-chart cto,ceo --issued-by cto --out anchor.json
plumbline anchor-verify --anchor anchor.json
plumbline heartbeat-submit --anchor anchor.json --ledger ledger.json \
--text "Wired up the login form and password field masking."
plumbline status --ledger ledger.json
plumbline reanchor --anchor anchor.json --ledger ledger.json --approver cto
plumbline ledger-verify --ledger ledger.json
Plumbline isn't a platform — it's a check a platform calls once per heartbeat, alongside whatever it already does. It fits naturally into any heartbeat-driven, org-chart-shaped orchestrator (e.g. Paperclip): the platform keeps owning scheduling, budgets, and task dispatch; Plumbline only ever answers "has this agent's trajectory drifted from its stated goal."
Call EscalationEngine.submit_heartbeat() right after the platform records
an agent's heartbeat/work-log entry, wire on_notify into the platform's
own notification channel, and gate the next task dispatch on
engine.is_frozen():
from plumbline.escalation import EscalationEngine
from plumbline.ledger import DriftLedger
def notify_manager(recipient, event, anchor):
platform.notify(recipient, f"{anchor.agent_id} drift={event.decision.value}")
ledger = DriftLedger(mission_id=anchor.mission_id)
engine = EscalationEngine(anchor=anchor, ledger=ledger, on_notify=notify_manager)
# on every heartbeat the platform already records:
event = engine.submit_heartbeat(heartbeat_text)
if engine.is_frozen():
platform.pause_agent(anchor.agent_id) # don't dispatch the next ticketsubmit_heartbeat itself raises RuntimeError if called while frozen, so a
platform that just always calls it before dispatch gets the block enforced
for free, without needing its own separate frozen-state check.
For orchestrators that aren't Python, or that shell out to worker
processes, drive the same state machine through the CLI. heartbeat-submit
exits nonzero if the agent was already frozen, and prints FROZEN to
stdout the moment a heartbeat causes the freeze — check both:
OUTPUT=$(plumbline heartbeat-submit --anchor anchor.json --ledger ledger.json \
--text "$(cat heartbeat.txt)")
STATUS=$?
if [ "$STATUS" -ne 0 ] || echo "$OUTPUT" | grep -q FROZEN; then
platform-pause-agent "$AGENT_ID"
fiEither way, Plumbline never needs to know anything about the platform's own task/ticket model — it only ever sees the anchor, the ledger, and the raw heartbeat text.
pip install -e ".[dev]"
pytest
Python 3.10+. Single runtime dependency: cryptography.
The bundled LexicalDriftScorer's default warn_threshold=0.70 /
critical_threshold=0.90 are calibrated against its own output
distribution, not picked arbitrarily: paraphrased-but-on-topic heartbeats
typically land around 0.5-0.65 distance against it (bag-of-words has no
synonym understanding), while genuinely off-topic work saturates near 1.0.
If you swap in an embedding-based DriftScorer, recalibrate the thresholds
against its distribution — cosine distances from different scorers are
not comparable.
- Authorization propagation via signed delegation chains for human+agent
hierarchies is a solved problem in this author's own
HWAAM —
Plumbline deliberately does not re-solve it; a
MissionAnchorstates a goal, not a permission grant. - LeaseGuard bounds cumulative damage magnitude under disconnection. Plumbline bounds cumulative semantic drift from a stated goal — orthogonal axes; an agent can be well within its damage budget while still off-mission.
- GSH Framework watches MCP traffic for anomalous/malicious behavior. Plumbline never classifies behavior as good or bad in isolation, only as aligned or drifted relative to that specific agent's own declared goal.
Forks and PRs welcome — this is a young, deliberately small primitive with real open extension points, not busywork:
- Swap in an embedding-based
DriftScorer. The bundled scorer is a dependency-free bag-of-words baseline on purpose (see Threshold calibration); it implements a documentedDriftScorerprotocol precisely so a semantic/embedding backend can drop in without touching the anchor, ledger, or escalation code. - Cryptographically-signed
reanchorapproval.reanchor --approvercurrently checks org-chart membership, not a signature — see the note incli.py. Requiring the approver's own signing key (the same patternanchor-createalready uses for the issuer) is the single biggest hardening gap in the repo. - More platform integrations. The integration guide
covers Python-library and CLI/process integration; adapters for specific
orchestrators (Paperclip, LangGraph, custom cron runners) are welcome as
examples under
examples/.
Before opening a PR: pip install -e ".[dev]", then ruff check plumbline tests, mypy plumbline --ignore-missing-imports, and pytest -q all need
to stay clean — CI runs the same three across Python 3.10-3.13.
If you use Plumbline in your research, please cite the software:
@software{gentyala2026plumbline,
author = {Gentyala, Sunil},
title = {Plumbline},
year = {2026},
version = {0.1.0},
url = {https://github.com/sunilgentyala/Plumbline}
}Machine-readable metadata is in CITATION.cff; GitHub shows it under "Cite this repository".
Apache-2.0