Skip to main content

nodus-approvals

Human-in-the-loop approval workflows for Nodus AI systems.

Policy-driven action gating with auto/deny/require/pairing modes, a full request lifecycle, and cryptographic peer-pairing codes. No required external dependencies — pure stdlib.

Status: v0.2.0 — published on PyPI.


Install

pip install nodus-approvals

What it provides

Component Purpose
ApprovalPolicy / ApprovalRule fnmatch-pattern rules; first match wins
ApprovalPolicy.require_for_effects Gate on what a tool declares, not its name
ApprovalRequest / ApprovalResult Pending action + decision record
InMemoryApprovalStore Thread-safe request storage
ApprovalGate check / approve / deny / poll lifecycle
PairingStore / generate_code 6-digit code exchange for peer pairing

Quick start

from nodus_approvals import ApprovalGate, ApprovalPolicy, ApprovalRule, ApprovalMode

policy = ApprovalPolicy(rules=[
    ApprovalRule(action_pattern="admin.*", mode=ApprovalMode.REQUIRE),
    ApprovalRule(action_pattern="read.*",  mode=ApprovalMode.AUTO),
    ApprovalRule(action_pattern="*",       mode=ApprovalMode.DENY),
])

gate = ApprovalGate(policy=policy)

result = gate.check("read.memory", requester_id="u1")
# result.approved == True  (matched AUTO rule)

result = gate.check("admin.delete", requester_id="u1")
# result.approved == False, result.pending == True  (REQUIRE — needs human)

gate.approve(result.request_id, approver_id="admin-1")

Approval modes

Mode Behaviour
AUTO Immediately approved; no human required
DENY Immediately denied
REQUIRE Creates a pending ApprovalRequest; blocks until approved or denied
PAIRING Requires a valid pairing code exchange before approval

ApprovalPolicy

Rules are evaluated in order; the first matching rule wins. Patterns use fnmatch — * matches any sequence, ? matches one character.

policy = ApprovalPolicy(rules=[
    ApprovalRule("payments.*",  ApprovalMode.REQUIRE, approver_ids=["finance"]),
    ApprovalRule("reports.*",   ApprovalMode.AUTO),
    ApprovalRule("*",           ApprovalMode.DENY),
])

policy.resolve(action) returns the matching ApprovalRule. When no pattern matches it falls back to REQUIRE — an unrecognised action asks a human rather than proceeding.

Gating on effects rather than names

require_for_effects builds a policy from what each tool declares, so there is no allowlist of tool names to keep in step:

from nodus_approvals import ApprovalPolicy, declared_effects, tools_with_effects

manifests = [
    {"name": "fs.read_file",  "effects": ["fs.read"]},
    {"name": "fs.write_file", "effects": ["fs.read", "fs.write"]},
    {"name": "math.add",      "effects": []},
]

policy = ApprovalPolicy.require_for_effects(manifests, ["fs.write"])
policy.resolve("fs.write_file").mode   # "require"
policy.resolve("fs.read_file").mode    # "auto"

The effect vocabulary is yours — the coarse nodus_lang_schema.VALID_EFFECTS (pure, reads_state, writes_state, network, filesystem, spawns_task) or a finer split such as fs.read / fs.write. Nothing here validates against a fixed set.

Two things to know:

  • The policy is a snapshot. It is built from the manifests as they are at the call. A tool registered afterwards is not covered until you rebuild. This removes the allowlist, not the need to notice new tools.

  • It never returns a policy that gates nothing. An effect no manifest declares raises ValueError, because a typo ("fs.wrte") is otherwise indistinguishable from "nothing needs approval" and the two have opposite consequences. To pass a standard vocabulary against a registry that may not use all of it, intersect first and decide in the open:

    gated = set(MY_VOCABULARY) & declared_effects(manifests)
    policy = ApprovalPolicy.require_for_effects(manifests, gated) if gated \
        else ApprovalPolicy.allow_all()
    

tools_with_effects(manifests, effects) returns just the names, for logging what a gate will stop or asserting in a test that a newly registered tool is covered.


ApprovalGate

gate = ApprovalGate(policy=policy, store=my_store)  # store defaults to InMemory

result = gate.check("some.action", requester_id="u1", context={"key": "val"})
# result.approved  — True/False
# result.pending   — True if REQUIRE mode and awaiting human decision
# result.request_id — present when pending

gate.approve(request_id, approver_id="approver-1", reason="looks good")
gate.deny(request_id,    approver_id="approver-1", reason="not authorised")
gate.poll(request_id)    # ApprovalResult | None — check current decision

Pairing codes

from nodus_approvals import PairingStore, generate_code

store = PairingStore(code_length=6, ttl_seconds=300)
code = store.issue(peer_id="peer-abc")        # e.g. "847291"
entry = store.validate(code)                   # PairingEntry | None
store.approve(code, approver_id="admin-1")    # mark as approved

generate_code(length=6) produces a cryptographically random numeric code. Expired entries are automatically excluded from validate.


InMemoryApprovalStore

from nodus_approvals import InMemoryApprovalStore, ApprovalRequest

store = InMemoryApprovalStore()
req = ApprovalRequest(action="admin.delete", requester_id="u1")
store.save(req)
store.get(req.id)
store.list_pending()
store.list_by_requester("u1")

Design

  • No required dependencies. Pure stdlib (fnmatch, secrets, threading, dataclasses, datetime, uuid).
  • Thread-safe. InMemoryApprovalStore and PairingStore use threading.Lock.
  • Pluggable storage. Any class satisfying the ApprovalStore protocol works.

Development

pip install -e ".[dev]"
pytest tests/ -q

License

MIT — see LICENSE.

Metadata

Release files for nodus-approvals 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nodus-approvals 0.2.0
File Size Uploaded
nodus_approvals-0.2.0.tar.gz 16.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nodus-approvals 0.2.0
File Interpreter ABI Platform
nodus_approvals-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 29.6 kB

Release files / nodus_approvals-0.2.0.tar.gz

Download URL nodus_approvals-0.2.0.tar.gz
Size 16.1 kB
Tags Source
SHA-256 checksum
How to use checksums
228428ad67a87836840680ff6a6e1785a768e240cccb2aca89e431d5b53eff10
BLAKE2b-256 checksum
How to use checksums
e04502351144f810642f447d6dc113ee35e78e19aa26a61c1224e0de567c2877
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / nodus_approvals-0.2.0-py3-none-any.whl

Download URL nodus_approvals-0.2.0-py3-none-any.whl
Size 13.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f8389d92950d160c0ef867291e3d1176512ed9759ce3b019cb1a352b4ce86338
BLAKE2b-256 checksum
How to use checksums
2772002dd97c3f06a3e809108b006c08353af1b9b6201e63bc5c9390966fdc55
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page