Skip to content

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 error set 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: a stage_file whose name is "../../etc/authorized_keys" still lands as a single file inside drafts_dir (the requested name rides inside the envelope, as data). The staged path is re-checked to resolve within drafts_dir before any write (fail-closed).
  • OFF by default: enabled=False on the dataclass and [effectors] enabled=false in config — build_reversible_write_effector refuses, 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.