Skip to content

Sidecar Runtime, Rule-Based Decision Gate, and Intent Guardian (v0.1 + v0.2) - #5

Merged
pramodbn27 merged 14 commits into
mainfrom
feat/decision-gate-and-intent-guardian
Aug 15, 2026
Merged

Sidecar Runtime, Rule-Based Decision Gate, and Intent Guardian (v0.1 + v0.2)#5
pramodbn27 merged 14 commits into
mainfrom
feat/decision-gate-and-intent-guardian

Conversation

@pramodbn27

@pramodbn27 pramodbn27 commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the v0.1 and v0.2 milestones from ROADMAP.md: the Sidecar
runtime, a rule-based Decision Gate (Policy Advisor + Risk Evaluator), and
Intent Guardian (IntentEnvelope + constraint validation), attached via a
LangGraph adapter that supports both Observe mode (v0.1, logs only) and
Govern mode (v0.2, a BLOCK is actually enforced).

What's in each version

v0.1 — Sidecar Runtime + Rule-Based Decision Gate

  • Sidecar (on_sidecar_failure: fail_open | fail_closed, required, no
    default), Decision(status, risk, reason), DecisionContext
  • Policy Advisor (gate.policy) — deterministic YAML allow/deny rules
  • Risk Evaluator (gate.risk) — rule-based, tool-name + argument-pattern
  • LangGraph adapter (adapters.langgraph.attach) — Observe mode only

v0.2 — Intent Guardian

  • IntentEnvelope, ConstraintBinding, IntentGuardian
    (agentic_sidecar.intent) — constraint validation only (numeric/enum/
    allow-list), authority deferred until a real scenario motivates its
    binding shape
  • WARN added to Decision.status alongside ALLOW/BLOCK
  • Govern mode — SidecarBlockedError raised by the adapter (not by
    Sidecar.evaluate(), which only ever computes a Decision) when a
    BLOCK should stop the call
  • Sidecar.set_intent() to swap envelopes between tasks

Full detail in CHANGELOG.md's [0.1.0]/[0.2.0] entries.

Design notes worth flagging in review

  • Sidecar.attach(agent) from README's original Planned Python API is
    not how this actually ships: core/ must not import from adapters/
    (AGENTS.md Package Boundaries), so wrapping a framework's tool-call
    surface is each adapter's own function
    (agentic_sidecar.adapters.langgraph.attach(sidecar, tools)), not a
    generic method on Sidecar. Documented in README as the real v0.1/v0.2
    shape vs. the longer-term target.
  • DecisionContext is deliberately not frozen (unlike Decision):
    Sidecar.evaluate() injects the active intent snapshot and decision
    history by mutating the caller's own instance in place, so a caller
    building SidecarBlockedError after the call sees the fully-evaluated
    context, not the bare one it originally constructed.
  • Both examples (examples/langgraph_*.py) run fully offline against a
    small scripted chat model — no API key needed to try them.

Testing

make check (ruff check, ruff format --check, mypy --strict, pytest) is
clean. 125 tests, 99% coverage. python -m build + twine check pass.

pramodbn27 and others added 14 commits August 15, 2026 15:46
Decision(status, risk, reason) -- frozen, status in {ALLOW, WARN, BLOCK}.
DecisionContext -- tool_name/tool_args plus IntentSnapshot/HistoryEntry,
both injected by Sidecar.evaluate() rather than constructed by callers.
Deliberately not frozen (unlike Decision): Sidecar.evaluate() injects
intent/history by mutating the caller's own instance in place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ArgOp + compare() -- extracted so gate/risk.py's argument-pattern rules
and intent/alignment.py's constraint bindings can share one comparator
implementation instead of duplicating it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Raised by an adapter (not by Sidecar.evaluate() itself) in Govern mode
when the Decision Gate returns BLOCK for a proposed tool call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deterministic, YAML-driven allow/deny rules by tool-name glob pattern.
Zero LLM calls (ROADMAP.md Design Constraint 2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rule-based risk classification: tool-name glob plus an optional
argument-pattern check (tool, arg_name, op, arg_value), built on
core.operators.compare(). Zero LLM calls; promoting the classifier to a
model is explicitly deferred (Design Constraint 4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
goal, requester, constraints, authority, expiry (concept.md §6).
is_expired()/to_snapshot() require timezone-aware datetimes -- a naive
value would otherwise only fail deep inside evaluate(), swallowed by
on_sidecar_failure instead of surfacing at construction time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ConstraintBinding binds one envelope constraint to a specific tool
argument and comparison op; evaluate_alignment()/IntentGuardian check
bindings plus envelope expiry, producing WARN or BLOCK per binding
severity. Scope is deliberately narrow -- constraints only, matching
concept.md §9's framing of this as the first concrete
semantic-authorization check. authority has no binding mechanism yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires Policy Advisor, Risk Evaluator, and Intent Guardian into a single
Decision Gate. on_sidecar_failure (fail_open|fail_closed) is required
with no default. mode=observe (default, logs only) or mode=govern
(v0.2: a BLOCK is enforced by the attached adapter, not by evaluate()
itself). set_intent() swaps the active envelope between tasks, and
raises if intent is given without 'intent_guardian' in roles rather
than silently never consulting it. risk_block_threshold is validated
at construction rather than only surfacing as a KeyError inside
evaluate(). evaluate() injects the active intent snapshot and decision
history into the caller's own DecisionContext, in place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
attach(sidecar, tools) wraps a list of tool callables so every call is
evaluated by a Sidecar first. No import-time dependency on the
langgraph package itself. In Observe mode the wrapped call always
executes; in Govern mode a BLOCK raises SidecarBlockedError instead
(WARN/ALLOW still call through in both modes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-export Sidecar, Decision, DecisionContext, DecisionStatus, RiskLevel,
SidecarBlockedError at the top level. Add pyyaml (gate.policy/gate.risk
YAML loading) as a core dependency and a langgraph extra for building a
real graph around adapters.langgraph's wrapped tools. Bump version to
0.2.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Runnable, offline (no API key) example against a real
langgraph.prebuilt.create_react_agent agent: a Policy Advisor deny rule
and a Risk Evaluator argument-threshold rule both fire and are logged,
but neither stops the call -- Observe mode only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The refund-limit scenario from concept.md §9 end to end: an $850 refund
request raises SidecarBlockedError before the real tool runs; a $120
request goes through normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Document the real (per-adapter attach(), not Sidecar.attach()) v0.1/v0.2
Python API alongside the longer-term planned shape; mark v0.1 and v0.2
deliverables shipped in ROADMAP.md; update AGENTS.md's Status/Repo Map/
Design Constraints to match what's actually implemented. Also fixes
picked up in review: ROADMAP.md's v0.2 deliverable cited concept.md §22
for the refund-limit example (that's actually §9); a stray 'four' where
Design Constraints lists five; a semantica.py placeholder shown as if
it already existed; core/sidecar.py's Package Layout entry still
describing attach() as living there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pramodbn27 pramodbn27 self-assigned this Aug 15, 2026
@pramodbn27
pramodbn27 merged commit 5892b49 into main Aug 15, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7598d939bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tool_name = getattr(tool, "__name__", repr(tool))

@functools.wraps(tool)
def _sidecar_wrapped_tool(*args: Any, **kwargs: Any) -> Any:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve coroutine semantics when wrapping async tools

When tool is an async function, this always-synchronous wrapper makes inspect.iscoroutinefunction(wrapped) false. LangGraph/LangChain can consequently register it as a synchronous tool and receive an unawaited coroutine object instead of the tool result, so the underlying async operation may never execute. Select an async def wrapper for coroutine functions and await the original tool.

Useful? React with 👍 / 👎.

Comment on lines +104 to +105
default_risk = data.get("default", "LOW")
return cls(rules, default_risk=default_risk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject invalid rule-set defaults during loading

When YAML contains a misspelled default such as default: SEVERE, from_mapping() stores that unchecked value because its type is Any; the first unmatched action then raises while constructing RiskResult, and a Sidecar(on_sidecar_failure="fail_open") converts the configuration error into ALLOW. This recreates the fail-open misconfiguration problem that risk_block_threshold now validates, so validate default_risk at construction/loading time and do the same for PolicyAdvisor.default_effect.

Useful? React with 👍 / 👎.

Comment on lines +193 to +194
self._inject(context)
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard context injection with the failure policy

Because _inject() runs before the try, any validation error while creating the intent snapshot or history escapes evaluate() instead of resolving through fail_open or fail_closed. This is reachable through the public mutable models—for example, mutating an active envelope's goal to an invalid value or mutating a previously recorded context before the next evaluation—and contradicts the method's promise to always resolve and record a decision. Include injection in the guarded evaluation path.

AGENTS.md reference: AGENTS.md:L108-L110

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant