Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `ActionTrace.result_summary` (#93): successful invocations now record a
redaction-safe summary of the firewalled `Frame` (`fact_count`, `row_count`,
`warning_count`, `has_handle` — counts/flags only, never raw driver data), so
an invocation's outcome is auditable directly via `Kernel.explain`. A
repository safety check's pass/block decision is now recorded in the audit
trail (`result_summary["row_count"] == 0` ⇒ passed), not merely inferred from
whether a later publish occurred. Failed runs keep `result_summary == None`.
- Ecosystem integration cookbook: a new `docs/integrations/` section with two
reference flows and runnable, offline companions.
- **contextweaver — policy before action** (#92): documents that context
routing is advisory and policy enforcement still happens before execution.
New [`docs/integrations/contextweaver.md`](docs/integrations/contextweaver.md)
and [`examples/contextweaver_policy_flow.py`](examples/contextweaver_policy_flow.py)
demonstrate the `allow` / `ask`-confirm / `deny` outcomes (the `ask` outcome
is derived from the recoverable `insufficient_justification` denial code).
- **Repository safety check as a policy-controlled capability** (#93): a
`RepositoryCheckDriver` that shells out to a local checker (e.g. VibeGuard)
gates a high-impact `repo.publish_artifact` action behind a passing
`repo.code_safety_check`, with both steps recorded in the audit trace. New
[`docs/integrations/repository_safety_check.md`](docs/integrations/repository_safety_check.md)
and [`examples/repository_safety_check.py`](examples/repository_safety_check.py).
- Both examples run in `make ci`; `docs/integrations.md` and the README link
the new pages.

## [0.9.0] - 2026-05-29

### Added
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ example:
python examples/http_driver_demo.py
python examples/tutorial.py
python examples/readme_quickstart.py
python examples/contextweaver_policy_flow.py
python examples/repository_safety_check.py

ci: fmt-check lint type test example
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the
- [Architecture](docs/architecture.md)
- [Security model](docs/security.md)
- [Integrations (MCP, HTTPDriver)](docs/integrations.md)
- [contextweaver: policy before action](docs/integrations/contextweaver.md)
- [Repository safety checks as a capability](docs/integrations/repository_safety_check.md)
- [Designing capabilities](docs/capabilities.md)
- [Context Firewall](docs/context_firewall.md)

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Transforms `RawResult → Frame`. Never exposes raw output to the LLM.
Stores full results by opaque handle ID with TTL. `expand()` supports pagination, field selection, and basic equality filtering.

### TraceStore
Records every `ActionTrace`. `explain(action_id)` returns the full audit record.
Records every `ActionTrace`. `explain(action_id)` returns the full audit record. On a successful invocation the trace also carries a `result_summary` — a redaction-safe dict of counts/flags (`fact_count`, `row_count`, `warning_count`, `has_handle`) derived from the firewalled `Frame`, never from raw driver data — so an invocation's outcome is auditable directly (e.g. a repository safety check passed iff `result_summary["row_count"] == 0`). Failed runs have `result_summary == None`.

### Adapters (`agent_kernel.adapters`)
Vendor-specific tool-format adapters that translate between `Capability` objects
Expand Down
15 changes: 15 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,18 @@ instrument_kernel(kernel)
`instrument_kernel` is idempotent — calling twice on the same kernel is a
no-op. Use `agent_kernel.otel.reset_instrumentation(kernel)` in tests to
re-instrument with a different provider.

## Ecosystem integration patterns

These reference flows show how agent-kernel composes with neighboring Weaver
projects and external checkers. Each has a runnable, offline companion under
`examples/`.

- [contextweaver + agent-kernel: policy before action](integrations/contextweaver.md)
— context routing is advisory; selection is not permission. Demonstrates the
`allow` / `ask`-confirm / `deny` outcomes.
Companion: [`examples/contextweaver_policy_flow.py`](../examples/contextweaver_policy_flow.py).
- [Repository safety checks as a policy-controlled capability](integrations/repository_safety_check.md)
— gate a high-impact action behind a deterministic check that shells out to a
local command (e.g. VibeGuard), with the result recorded in the audit trace.
Companion: [`examples/repository_safety_check.py`](../examples/repository_safety_check.py).
100 changes: 100 additions & 0 deletions docs/integrations/contextweaver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# contextweaver + agent-kernel: policy before action

`contextweaver` and `agent-kernel` solve adjacent parts of the same runtime
problem and are designed to compose:

- **`contextweaver`** narrows *what the model sees* — it compiles context and
produces a shortlist of candidate tool cards for a goal.
- **`agent-kernel`** enforces *what the agent may do* — capabilities, policy,
HMAC tokens, the context firewall, and the audit trace.

The contract between them is simple and worth stating plainly:

> **Routing is advisory. Selection is not permission.** A capability appearing
> on a contextweaver shortlist does not authorize it. Every selected action
> still flows through the full agent-kernel pipeline
> (policy → token → invoke → firewall → trace) before anything executes.

This page describes the reference flow. The runnable companion is
[`examples/contextweaver_policy_flow.py`](../../examples/contextweaver_policy_flow.py),
which uses synthetic data only and runs offline (it does **not** depend on
`contextweaver`).

## The flow

```
goal
contextweaver → tool-card shortlist (advisory candidate set)
host / model selects an intended action
agent-kernel → grant_capability() → policy decision
│ │
│ ├─ allow → invoke() → firewall → Frame + ActionTrace
│ ├─ ask → recoverable denial; host confirms, retries
│ └─ deny → terminal denial; host must not retry
ActionTrace (audit)
```

1. **contextweaver** builds a small candidate/tool-card shortlist for the goal.
The shortlist is deliberately permissive — it may include capabilities the
current principal is not allowed to use.
2. The host (or model) selects an intended action from the shortlist.
3. **agent-kernel** evaluates that action against capability/policy rules.
4. Only approved actions proceed to execution; the rest are surfaced as
`ask`/confirm or `deny`.

## Three outcomes on a binary policy

agent-kernel's `PolicyDecision` is binary (`allowed: bool`) — there is no
dedicated "ask" verdict. The three host-level outcomes are derived from the
stable `reason_code` on a denial:

| Outcome | How it arises | Stable signal | Host behavior |
|---|---|---|---|
| **allow** | The principal satisfies policy. | `grant_capability()` returns a grant; `invoke()` returns a `Frame`. | Proceed; the action is audited. |
| **ask / confirm** | A WRITE action — or a DESTRUCTIVE action the principal is *otherwise authorized for* — is missing its justification. | `PolicyDenied` with `reason_code == DenialReason.INSUFFICIENT_JUSTIFICATION`. | *Recoverable.* Prompt the human to confirm and supply a justification, then re-grant. |
| **deny** | The principal lacks a required role (or another non-recoverable rule fails). | `PolicyDenied` with e.g. `reason_code == DenialReason.MISSING_ROLE`. | *Terminal.* Do not retry as-is; surface remediation from `Kernel.explain_denial()`. |

Treating a missing justification as `ask` is the natural mapping: the
`DefaultPolicyEngine` already requires a justification of at least 15 characters
for WRITE/DESTRUCTIVE capabilities, so "ask the human to confirm and explain"
is exactly what unblocks the call. A missing role, by contrast, cannot be
satisfied by confirmation and is terminal for that principal.

Note the ordering: role checks run **before** the justification check, so the
`ask` outcome is only reachable once the principal already satisfies the role
requirement. For the DESTRUCTIVE `tickets.delete` below, the support agent lacks
`admin`, so it surfaces as a terminal `missing_role` `deny` regardless of
justification — the example never reaches `ask` for that capability.

## Why the shortlist is not a grant

In the example, contextweaver shortlists `docs.search` (READ),
`tickets.update_status` (WRITE), **and** `tickets.delete` (DESTRUCTIVE) for a
support-agent goal. The agent principal holds `["reader", "writer"]` but not
`admin`, so `tickets.delete` is denied at grant time even though it was
"routed". This is the whole point: context selection improves relevance; it
never widens authority.

## Mapping to Weaver concepts

- A contextweaver tool card maps to a `Capability` (or its public
`CapabilityDescriptor`).
- "Selecting an action" maps to building a `CapabilityRequest` (carry the
machine-readable `intent` and `scope` so declarative policies can match
without parsing free text).
- Enforcement is `Kernel.grant_capability()` followed by `Kernel.invoke()`;
the resulting `ActionTrace` satisfies weaver-spec invariant **I-02**
(every execution is preceded by a policy decision and followed by a trace).

## Related

- `examples/contextweaver_policy_flow.py` — runnable, offline.
- [contextweaver](https://github.com/dgenio/contextweaver)
- [weaver-spec](https://github.com/dgenio/weaver-spec)
134 changes: 134 additions & 0 deletions docs/integrations/repository_safety_check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Repository safety checks as a policy-controlled capability

Agents that can write files, open PRs, or publish artifacts need a clear
pattern for running deterministic checks **before** a high-impact action.
agent-kernel already models policy enforcement, capabilities, firewall
redaction, and auditable tool calls — a repository-level check fits naturally
as a capability that is invoked under explicit policy and recorded in the audit
trace.

This page describes the pattern. The runnable companion is
[`examples/repository_safety_check.py`](../../examples/repository_safety_check.py),
which is deterministic, offline, and depends on no specific checker.

> agent-kernel does **not** implement scanning logic and does **not** depend on
> any particular checker. The check is an adapter that shells out to a local
> command. The example uses a tiny embedded scanner so it runs in CI; in
> production you point it at a real tool such as
> [VibeGuard](https://github.com/dgenio/vibeguard).

## The pattern

```
agent wants to publish
repo.code_safety_check (READ capability → RepositoryCheckDriver shells out)
├─ clean → grant + invoke repo.publish_artifact (WRITE)
└─ findings → host blocks the publish; check result is still audited
```

Two capabilities:

| Capability | Safety class | Backed by | Role |
|---|---|---|---|
| `repo.code_safety_check` | `READ` | `RepositoryCheckDriver` (shells out to a checker) | Runs a deterministic scan and returns findings. |
| `repo.publish_artifact` | `WRITE` | any execution driver | The high-impact action, gated behind a passing check. |

## The shell-out adapter

`RepositoryCheckDriver` implements the `Driver` protocol and runs the configured
command as `[*command, path]`:

```python
class RepositoryCheckDriver:
def __init__(self, command: list[str], *, driver_id: str = "repo_safety") -> None:
self._command = list(command)
self._driver_id = driver_id

@property
def driver_id(self) -> str:
return self._driver_id

async def execute(self, ctx: ExecutionContext) -> RawResult:
path = ctx.args.get("path")
if not path:
raise DriverError("repository check requires a 'path' argument ...")
proc = await asyncio.create_subprocess_exec(
*self._command, str(path),
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode not in (0, 1): # checker itself failed
raise DriverError(f"...: {stderr.decode(errors='replace').strip()}")
findings = json.loads(stdout or b"[]") # [] = clean
return RawResult(capability_id=ctx.capability_id, data=findings,
metadata={"exit_code": proc.returncode})
```

Conventions the adapter relies on (and most scanners follow):

- **Exit `0`** = clean, **exit `1`** = findings present. Any other exit code is
treated as the checker *failing* and surfaces as a `DriverError`.
- Findings are a JSON array on stdout. Non-JSON output raises `DriverError`
rather than being silently treated as "clean".

To use VibeGuard instead of the embedded scanner, construct the driver with its
command, e.g. `RepositoryCheckDriver(command=["vibeguard", "scan", "--json"])`.

## Reading the verdict from the Frame

The gate decision is made on the **firewalled Frame**, not on raw driver output.
Invoke the check in `table` mode and treat a non-empty preview as a block:

```python
frame = await kernel.invoke(token, principal=principal,
args={"operation": "code_safety_check", "path": path},
response_mode="table")
findings = list(frame.table_preview)
passed = not findings
```

When `passed` is `False`, the host simply does not grant
`repo.publish_artifact`. Because the check ran through `Kernel.invoke()`, it
produced an `ActionTrace` — so the decision to block is auditable via
`Kernel.explain(action_id)`, satisfying weaver-spec **I-02**.

## Audit trail

Both the check and (when it passes) the publish are recorded:

```python
check_trace = kernel.explain(check_action_id) # always available
publish_trace = kernel.explain(publish_action_id) # only when the check passed
```

Each `ActionTrace` records the capability, principal, driver, and timestamp for
the step. It also carries a redaction-safe `result_summary` derived from the
firewalled `Frame` (counts and flags only — never raw findings), so the check's
pass/block **decision** is recorded in the audit trail itself, not merely
inferred from whether a later publish happened:

```python
check_trace = kernel.explain(check_action_id)
blocked = (check_trace.result_summary or {}).get("row_count", 0) > 0
```

`result_summary` is built only from the post-`Firewall` `Frame`, so recording it
never widens the I-01 boundary or leaks scanned content into the audit log. A
reviewer can therefore confirm both that the publish was preceded by a check and
what that check decided.

## Non-goals

- agent-kernel does not implement scanning logic.
- VibeGuard (or any checker) is never a required dependency.
- The check does not bypass existing policy enforcement — it is *additional*
to the normal grant/invoke pipeline.

## Related

- `examples/repository_safety_check.py` — runnable, offline.
- [VibeGuard](https://github.com/dgenio/vibeguard)
- [weaver-spec](https://github.com/dgenio/weaver-spec)
1 change: 1 addition & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| Handle scope escape (expand exceeds grant) | Handles persist grant constraints; `HandleStore.expand` rechecks `max_rows`, `allowed_fields`, `scope`, and principal binding (#76) |
| Memory exfiltration via tool output | `SensitivityTag.MEMORY` capabilities gate sensitive reads and durable writes; `ActionTrace.args` redacts payload-like fields for `memory.*` capabilities (#75) |
| Raw memory payload reaching audit log | Kernel strips `payload`/`content`/`value`/`memory`/`text`/`body` from `ActionTrace.args` for `memory.*` capabilities |
| Scanned content / raw result reaching audit log | `ActionTrace.result_summary` is built only from the post-firewall `Frame` (counts and flags, never raw driver data), so the audit trail records an invocation's outcome without re-introducing the data the firewall removed |

## Token scopes

Expand Down
Loading
Loading