edge.effectors
edge.effectors
Zone B — the effector surface (Track G; hands-and-the-effector-layer.md).
Hands live HERE, never in the sealed core: anything that can ever touch the network belongs in edge (CONVENTIONS). Sensing-only (β = 0) until the blast-radius classes above it are earned (§4's graduated rollout); each effector is a reviewed native hand — re-implemented in this repo, never live-installed third-party skill code (§1: mine the ecosystem, don't adopt the runtime).
SensingEffector
dataclass
Serves pending sense requests from the handoff, one constrained fetch each.
upstreams (name -> https base URL) is the reviewed allowlist — the ONLY place a URL
exists in the whole sensing path. Empty (the default) means every request is refused:
fail-closed until the owner deliberately writes an entry.
handoff
instance-attribute
upstreams = field(default_factory=dict)
class-attribute
instance-attribute
transport = field(default_factory=UrllibTransport)
class-attribute
instance-attribute
enabled = False
class-attribute
instance-attribute
timeout_s = 20.0
class-attribute
instance-attribute
max_bytes = 512 * 1024
class-attribute
instance-attribute
run_once()
Serve every pending request (fetch or honest refusal), consuming each request file
after its observation is written. Returns the served request ids. Refuses entirely
(fail-closed) unless enabled.
ReversibleWriteEffector
dataclass
Stages approved reversible writes into drafts_dir, one JSON envelope each. stage returns
the artifact ref the EffectLedger records; rollback removes it. Local, no network.
drafts_dir
instance-attribute
enabled = False
class-attribute
instance-attribute
stage(actuator, params, *, ref=None)
Stage one approved reversible write as a draft envelope. Returns the artifact ref (for
the ledger + rollback). Refuses (fail-closed) unless enabled, and refuses an actuator with
no reviewed materialization. The requested filename (for stage_file) is stored as DATA in
the envelope — never used as the on-disk path — so no param can direct the write.
rollback(ref)
Remove a staged draft (the reversible undo). Returns True if an artifact was removed, False if it was already gone — idempotent, so a double-rollback is not an error.
read(ref)
Read back a staged draft envelope (for the owner's review surface). None if absent.
build_sensing_effector(config=None)
Wire the effector from config (lazy import, mirroring build_bridge — edge stays
import-light and never pulls config at module load). REFUSES unless [effectors] enabled.
build_reversible_write_effector(config=None)
Wire the effector from config (lazy import, mirroring the sensing builder). REFUSES unless
[effectors] enabled — the whole Track-G surface is OFF by default.
sensing
The read-only sensing effector (Zone B) — Track G item G3, blast radius β = 0.
The one generic sensing hand: it serves sense_fetch requests from the filesystem handoff
with a constrained HTTPS GET and writes the result back as an observation. Critically:
- It never imports core (nor ops, nor scheduler). Like the monitor and the bridge, the wire shapes are mirrored, not imported — the sealed core and this networked process agree on a JSON layout, never on a Python import (Invariant 2: network and private data never share a component; this process has no vault handle to leak).
- It resolves NAMES, not addresses. A request carries
upstream, a short name; the URL comes from this effector's own reviewed allowlist (upstreams, from config — empty by default, so every fetch is refused until the owner writes one in). The core-side request type cannot even represent a URL, so a steered reasoner cannot aim this hand anywhere the owner didn't pre-approve (the confused-deputy answer, both halves). - The fetch profile is powerless: https only, no redirects (a redirect off the allowlisted host is an exfil vector, so 3xx is refused outright), a hard response-size cap (refused, not truncated — truncation would silently alter data), a timeout, no auth headers, no cookies. It returns DATA; it cannot perform an action (§2: sensing hands are just new sensors).
- Refusals are honest. A request it will not serve (unknown upstream, non-https
allowlist entry, oversized/failed response) still produces an observation file — with
errorset and an empty body — so the core sees the refusal instead of a silent gap.
Wire layout (mirrored by core/sensing.py):
<handoff>/requests/<id>.json {"id", "actuator", "upstream", "terms", "ts"}
<handoff>/observations/<id>.json {"request_id", "upstream", "ts", "body", "error"}
OFF by default: enabled=False on the dataclass and [effectors] enabled=false in config —
build_sensing_effector refuses, and even a directly-constructed instance refuses to run.
REQUESTS = 'requests'
module-attribute
OBSERVATIONS = 'observations'
module-attribute
EffectorsDisabled
Bases: RuntimeError
The sensing effector is off (enabled=False / [effectors] enabled=false).
TransportError
Bases: RuntimeError
The constrained fetch refused or failed (non-https, redirect, oversize, network error).
Transport
Bases: Protocol
The minimal fetch surface the effector needs — satisfied by UrllibTransport or a test
fake. Deliberately tiny: GET only, byte-capped, no sessions, no auth — the whole authority
of a sensing hand is 'read this one allowlisted thing'.
get(url, *, timeout_s, max_bytes)
UrllibTransport
dataclass
The real constrained fetch (stdlib-only, like the cloud fetcher). https-only, redirects refused, size-capped read — refuses rather than truncates on oversize.
get(url, *, timeout_s, max_bytes)
SensingEffector
dataclass
Serves pending sense requests from the handoff, one constrained fetch each.
upstreams (name -> https base URL) is the reviewed allowlist — the ONLY place a URL
exists in the whole sensing path. Empty (the default) means every request is refused:
fail-closed until the owner deliberately writes an entry.
handoff
instance-attribute
upstreams = field(default_factory=dict)
class-attribute
instance-attribute
transport = field(default_factory=UrllibTransport)
class-attribute
instance-attribute
enabled = False
class-attribute
instance-attribute
timeout_s = 20.0
class-attribute
instance-attribute
max_bytes = 512 * 1024
class-attribute
instance-attribute
run_once()
Serve every pending request (fetch or honest refusal), consuming each request file
after its observation is written. Returns the served request ids. Refuses entirely
(fail-closed) unless enabled.
build_url(base, terms)
v0 query convention: append the scrubbed terms as one q= parameter. The base URL is
the owner's reviewed allowlist entry; terms are the only request-derived content in it.
build_sensing_effector(config=None)
Wire the effector from config (lazy import, mirroring build_bridge — edge stays
import-light and never pulls config at module load). REFUSES unless [effectors] enabled.
writes
The reversible-write effector (Zone B) — Track G item G5, blast radius small (β small).
Materializes an APPROVED class-2 effect into a staged draft artifact the owner can review and
delete — a draft reply, a tentative calendar hold, a staged file. Never a sent artifact: staging is
the whole action, and it is reversible by construction (rollback unlinks the draft). Sending a
staged draft is a different, irreversible, full-gated act (G6, ops/effect_exec.py) — this
effector has no send path at all.
Same Zone-B discipline as the sensing effector:
- It never imports core (nor ops, nor scheduler). It is driven by an approved
(actuator, params)pair the orchestrator passes in; the wire shape is agreed, not imported (Invariant 2 — this process has no vault handle to leak, and stages only local files). - Traversal is unrepresentable. The on-disk name is chosen by THIS effector (
<ref>.draft), never by a param: astage_filewhosenameis "../../etc/authorized_keys" still lands as a single file insidedrafts_dir(the requested name rides inside the envelope, as data). The staged path is re-checked to resolve withindrafts_dirbefore any write (fail-closed). - OFF by default:
enabled=Falseon the dataclass and[effectors] enabled=falsein config —build_reversible_write_effectorrefuses, and a directly-constructed instance refuses to stage. And even enabled, it is unreachable in the wired system until ε is raised past sensing.
A staged draft is a JSON envelope — {actuator, params, staged_at} — not the final artifact: the
drafts area is a review surface, so what lands is the proposed effect, awaiting a separate act to
realize it. That keeps staging reversible (delete the envelope) and safe (no arbitrary path write).
EffectorsDisabled
Bases: RuntimeError
The reversible-write effector is off (enabled=False / [effectors] enabled=false).
NotStageableError
Bases: ValueError
An actuator this effector has no reviewed materialization for was offered to it.
ReversibleWriteEffector
dataclass
Stages approved reversible writes into drafts_dir, one JSON envelope each. stage returns
the artifact ref the EffectLedger records; rollback removes it. Local, no network.
drafts_dir
instance-attribute
enabled = False
class-attribute
instance-attribute
stage(actuator, params, *, ref=None)
Stage one approved reversible write as a draft envelope. Returns the artifact ref (for
the ledger + rollback). Refuses (fail-closed) unless enabled, and refuses an actuator with
no reviewed materialization. The requested filename (for stage_file) is stored as DATA in
the envelope — never used as the on-disk path — so no param can direct the write.
rollback(ref)
Remove a staged draft (the reversible undo). Returns True if an artifact was removed, False if it was already gone — idempotent, so a double-rollback is not an error.
read(ref)
Read back a staged draft envelope (for the owner's review surface). None if absent.
build_reversible_write_effector(config=None)
Wire the effector from config (lazy import, mirroring the sensing builder). REFUSES unless
[effectors] enabled — the whole Track-G surface is OFF by default.