Accountable Surface Usage
September 5, 2026 ยท View on GitHub
What It Is
Accountable Surface is a local action workbench for AI agents. It lets a host runtime observe a target, propose an action, check that action against an operator-loaded grant, execute only through a bounded effector, verify the result, and record the journal.
Clone With Sibling Repositories
The core package composes with coherence-membrane and proof-surface.
git clone https://github.com/HarperZ9/accountable-surface.git
git clone https://github.com/HarperZ9/coherence-membrane.git
git clone https://github.com/HarperZ9/proof-surface.git
cd accountable-surface
Install For Development
$env:PYTHONPATH = "src;..\coherence-membrane\src;..\proof-surface\src"
python -m pip install -e ".[test]"
Run The Local Checks
python -m pytest
node --test web/*.test.mjs
Run The Basic Demo
python examples/demo.py
python examples/actuate_demo.py
Run As An MCP Server
python -m pip install -e ".[server]"
$env:PYTHONPATH = "src;..\coherence-membrane\src;..\proof-surface\src"
python -m accountable_surface.server
MCP client example:
{
"mcpServers": {
"accountable-surface": {
"command": "python",
"args": ["-m", "accountable_surface.server"],
"env": {
"PYTHONPATH": "C:/path/to/accountable-surface/src;C:/path/to/coherence-membrane/src;C:/path/to/proof-surface/src",
"ACCOUNTABLE_SURFACE_GRANTS": "C:/path/to/operator-grants.json",
"ACCOUNTABLE_SURFACE_JOURNAL": "C:/path/to/session-journal.jsonl"
}
}
}
}
Exposing An Effector Over MCP
perceive, propose, session_journal, and interocept are always available.
actuate writes, so it reaches only what the operator has exposed. Point
ACCOUNTABLE_SURFACE_EFFECTORS at a JSON file:
{"effectors": [
{"action_kind": "fs.write", "type": "filesystem", "root": "/srv/agent-sandbox"},
{"action_kind": "api.post", "type": "api", "service": "github"}
]}
A caller then asks for one action kind at a time:
{"action_kind": "fs.write", "target": "/srv/agent-sandbox/notes.md", "content": "hello"}
content is the text for a file write. For an api entry it is
{"intent": "post_comment", "body": {...}}, so the caller names an operation the
service declares and never a host, a header, or a route.
The reply carries the gate decision, the verify verdict, the composed certificate,
and the journal entry for that one call. The rest of the journal stays with the
operator, and session_journal is where the operator reads it.
Two decisions guard the call and both have to agree. This file says what a caller
can reach at all; the grant says what may be done with it. Leave the variable unset
and actuate refuses everything, however wide the grants are. The file refuses
command, browser, web, and uia by name, each with the reason. Run doctor to see
the exposed set, the reach of each entry, and the entries it turned down, so nobody
has to guess at an empty registry.
Using The Browser Backend (JS-Capable SPAs)
WebEffector drives server-rendered pages natively (stdlib, zero-dep) but runs no
JavaScript. BrowserEffector adds a JS-capable edge: click by accessible label,
follow cross-origin navigation, and run JS on a live single-page app -- all through
the same gate + verify + rollback + journal contract.
The browser backend is injectable. Tests and offline demos use the deterministic,
zero-dependency FakeBrowserDriver; production injects the optional
PlaywrightDriver.
Tests (deterministic, offline -- the default):
from accountable_surface import AccountableSurface, BrowserEffector, FakeBrowserDriver, BrowserAction
driver = FakeBrowserDriver(start="https://app.test/")
eff = BrowserEffector(driver, allowed_origins=["https://app.test"])
AccountableSurface().actuate(
eff, target="https://app.test/",
content=BrowserAction("navigate", url="https://app.test/dashboard"),
authorization=grant, # operator-loaded; no grant -> default-deny
)
Production (real headless Chromium -- optional, lazily imported):
python -m pip install "accountable-surface[browser]"
python -m playwright install chromium
from accountable_surface.playwright_driver import PlaywrightDriver
driver = PlaywrightDriver(headless=True, start="https://app.example.com/")
eff = BrowserEffector(driver, allowed_origins=["https://app.example.com"])
# ...same surface.actuate() contract, now with real JS execution.
Playwright is never a hard dependency: it is imported only when PlaywrightDriver
is instantiated, so the default install and the whole test suite stay zero-dep.
Run the offline SPA transcript with python examples/spa_actuate_demo.py.
Writing Through A Third-Party API
ApiEffector covers the case where the work belongs on someone else's service and
that service has an official write API. The agent names an intent, never a route:
from accountable_surface import (
AccountableSurface, ApiCall, ApiEffector, GITHUB_ISSUE_COMMENTS,
)
from accountable_surface.api_transport import UrllibApiDriver
eff = ApiEffector(UrllibApiDriver(), GITHUB_ISSUE_COMMENTS)
AccountableSurface().actuate(
eff,
target="/repos/octo/demo/issues/7/comments",
content=ApiCall("post_comment", {"body": "found a repro, steps below"}),
authorization=grant, # scope.allowed_actions must carry "api.post"
)
The service object declares every write that is possible at all. An intent it does
not list is refused, and a target the intent's path shape does not match is refused,
so post_comment cannot reach a collaborator or settings route on the same host.
The credential is a variable name in the service definition. Its value is read from the environment at the moment of the call and sent in a header, so it never appears in a Plan, an Observation, the journal, or an error message. Set it before the run:
$env:ACCOUNTABLE_SURFACE_GITHUB_TOKEN = "<a token with the narrowest scope that works>"
Verification re-reads the collection and looks for a member carrying the body that was authorized. The response to the write is never consulted, so a service that answers 201 and stores nothing comes back REFUTED.
Swap UrllibApiDriver for FakeApiDriver to exercise the whole path offline with
no network and no credential, the way the test suite does.
Acting On A Windows Application
UiaEffector reaches a desktop application through its control tree rather than
through the screen. The effector is built for one window and reads only that window:
from accountable_surface import AccountableSurface
from accountable_surface.uia_effector import UiaCommand, UiaEffector
from accountable_surface.uia_transport import PowerShellUiaDriver
eff = UiaEffector(PowerShellUiaDriver(path_to_uia_script), "Notepad")
AccountableSurface().actuate(
eff,
target="uia://Notepad/Message",
content=UiaCommand("set_value", text="the text to type into that field"),
authorization=grant, # scope.allowed_actions must carry "uia.set_value"
)
Two intents, so a grant can carry the reversible one on its own. set_value reads
the control's prior value first and puts it back when verification fails. invoke
presses the control and cannot be undone, so it stays needs-human unless the
operator passes allow_irreversible, and it needs a declared post-condition:
UiaCommand("invoke", expect={"kind": "appears", "element": "Saved"})
The other two post-conditions are disappears and value_is. A plan carrying none
of them is refused at preview time, because a press nobody can check is a press
nobody can authorize. Verification re-reads the window; the instrument reports ok
for anything it dispatched, so its own account of its work is never consulted.
A control tree the walk had to clip comes back UNVERIFIED, and a disappears check
against a clipped tree fails rather than reading the missing control as gone. Two
controls sharing one accessible name resolve to whichever the walk reached first,
which is an open null recorded in tests/test_false_success.py.
What this repo ships is the driver contract, the two rungs above it, and
FakeUiaDriver. The PowerShell script itself is supplied by the operator: it answers
JSON on stdout for tree, value, invoke, and setvalue, and exits 0 whatever
happened, so a refusal arrives as data. The transport refuses the blind keystroke
verbs input and type by name before it spawns anything, because they name no
control and nothing about them can be verified by re-reading a tree.
Honest null: the subprocess path has no test coverage. Exercising it needs Windows, a
live window, and a running application. Swap PowerShellUiaDriver for FakeUiaDriver
to run the whole path on any operating system with no window open, the way the test
suite does, and treat a first real call as unproven.
Choosing A Rung
structure_ladder climbs from the control tree to the screen and records why it
fell:
from accountable_surface.escalator import Question, structure_ladder
from accountable_surface.uia import UiaStructureOrgan
ladder = structure_ladder(UiaStructureOrgan(driver), "Notepad", capture)
ascent = ladder.resolve(Question("present", "Save"))
print(ascent.answer) # True, False, or None when nothing settled it
print(ascent.trace())
A whole control tree settles the question either way: the label resolves, or it does not and the tree was complete. A clipped tree falls instead, because a control that exists can be sitting past the cut.
rung 0 structure (low cost): fell -- the walk clipped the tree at the 400-control limit, so absence is not established
rung 3 pixels (high cost): fell -- pixels carry no control names, so 'Save' cannot be resolved from them; the sight is witnessed at phash <16 hex chars>
When the ladder runs out, ascent.status is NEEDS_HUMAN, ascent.rederivable is
"none", and ascent.witness carries the sight the deepest rung produced. That sight
has a content digest and a perceptual hash and answers nothing about a control named
Save. Reading it is a person's job.
The escalator only reads. Rungs 1 and 2 act, and acting stays with the effectors and the grants that bound them, so nothing here presses a control to find out what it does.
Boundary
- No grant means default deny.
- The model cannot provide its own authorization.
- Over MCP, an effector the operator has not exposed cannot be reached under any grant.
uiais not exposable over MCP at all. It acts on a window belonging to whoever is at the machine, and reaching that from off the machine is a separate decision.- Over MCP, an irreversible action stays
needs-human. No argument a remote caller passes reachesallow_irreversible. - Journals are append-only local records.
- Operator grant files and session journals are runtime inputs, not source files.
- Irreversible actions require explicit grant handling and verification.