Skip to content

Make Python macros BOUND by default with explicit RAW authority - #15

Merged
worflor merged 26 commits into
mainfrom
macros/bound-raw-v2
Sep 19, 2026
Merged

worflor merged 26 commits into
mainfrom
macros/bound-raw-v2

Conversation

@worflor

@worflor worflor commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Stack

This is PR 2 of 2 and is intentionally stacked on #14 (macros/runtime-truth). Review #14 first; this PR assumes its transactional registration, generation pinning, ephemeral test path, canonical ids, worker retirement, and control-plane recovery.

What changed

Make Python macro authority source-owned and explicit without removing the project's full-power Python philosophy.

Source/compiler contract

  • no directive means BOUND

  • exactly # neuron: raw means RAW

  • malformed/duplicate Neuron directives are compiler-policy errors instead of guessed settings

  • the Workshop RAW toggle adds/removes that exact source line; there is no hidden sidecar preference

  • catalog cards derive BOUND/RAW from source

  • promote the visual compiler from a body-only Vec<MacroNode> to a whole MacroDocument, preserving module prefix/suffix, the actual entry wrapper, options/imports/helpers/comments around the entry, body nodes, and execution mode through block edits

Backward compatibility

  • existing pre-feature .py macros are atomically stamped RAW exactly once before the new default takes effect

  • the migration marker is written last, so interruption retries safely

  • bundled/new macros created after migration remain BOUND by absence of the pragma

  • BOUND and RAW persistent state use the same per-macro JSON namespace, so switching mode does not fork memory

Runtime authority

  • split MacroHost into two independently supervised warm CPython domains; RAW and BOUND never share an interpreter

  • each domain has its own crash breaker, warm latch, registry population, prompt lifetime, and recovery

  • prompt ids use disjoint namespaces; a dead RAW sidecar no longer withdraws live BOUND prompts (and vice versa)

  • registered dispatch chooses the domain from an in-memory source-derived mode map; no disk or policy parse moves onto the 1000 Hz path

  • RAW remains ordinary unrestricted CPython

  • BOUND receives curated imports/builtins plus the public neuron capability proxy and refuses ambient file/process/network/native-FFI/reflection surfaces through the supported language path

  • arbitrary neuron.run requires RAW

BOUND is intentionally described as policy containment, not a hostile-code Python sandbox. The feature is for safer/default and agent-authored macros, not for executing adversarial untrusted Python.

Host-owned effects

  • broker BOUND key/chord/text/ghost typing, mouse, clipboard, focus, device/audio/OBS effects, and persistent macro state through Rust

  • Rust independently re-checks arm/mock authority before effectful work; Python's helper check is no longer the final security decision

  • native adapters fail closed on invalid buttons/keys and clamp protocol-sized mouse coordinates before narrowing

  • state verbs are host-owned but not SAFE-gated because they mutate Neuron macro state, not user input/device/system state

Composition

  • same-domain synchronous invoke keeps existing native-Python subroutine semantics

  • cross-domain calls go through Rust

  • RAW → BOUND is allowed and executes the callee inside BOUND

  • BOUND → RAW is refused before dispatch, so invoke() cannot become an authority-escalation tunnel

  • async cross-domain calls still enter the target's serial worker

Verification

  • Linux (WSL): cargo test --workspace --exclude neuron-app --locked passed on the final stacked branch.

  • Windows: .\validate.ps1 and .\validate.ps1 -Mode full passed on the final branch. Full mode covered the feature matrix, release build, and CI-safe ignored tests; its reference emitter now receives its required scratch directory. .\validate.ps1 -Mode lint passed with advisory rustfmt findings; full mode reported advisory clippy findings.

  • Built and launched neuron-app --safe with a separate run directory. Workshop displayed the RAW toggle and the seeded macro card displayed its BOUND label.

  • Bundled-CPython BOUND/RAW authority, migration, whole-document round trip, and macro stress tests passed locally.

  • GitHub Actions cannot start the what changed runner because of the account billing or spending-limit annotation; platform test jobs are skipped.

  • Live input injection and hardware-write behavior were not exercised.

Threat-model note

RAW is still the deliberate full-power escape hatch. A RAW macro has the user's normal Python/process authority and can bypass Neuron's helpers. BOUND meaningfully reduces ambient authority and structurally isolates itself from RAW, but it is not presented as adversarial-code containment.

Before this can merge

If the pull request contains work by more than one author, every author must

record the same acceptance in the pull request, unless the submitter has

authority to accept for them.

Give macros one compiler-owned authority marker: absence means BOUND and the exact
header line '# neuron: raw' opts into unrestricted Python. Malformed or duplicate
Neuron directives are policy errors rather than guessed settings, and the Workshop
toggle edits that source line instead of storing a second preference.

Promote the visual constructor from a body-only Vec<MacroNode> to MacroDocument.
Parsing now carries the module prefix, the actual entry wrapper, typed body and
suffix together, so a block edit keeps imports, NEURON_OPTIONS, helpers, comments,
main-vs-macro naming and ordinary multi-line signatures instead of manufacturing
a new bare macro() module.

The catalog exposes BOUND/RAW from the same source-derived model. parse_macro stays
as a compatibility projection over the whole-document parser, so existing callers
do not need a second Python grammar implementation.
BOUND changes the default for new source, not the meaning of code users already
trusted. On the first observation of an install's macro directory, atomically stamp
every pre-feature Python file with '# neuron: raw' and write the migration marker
last. A partial IO failure therefore retries rather than silently reclassifying an
old unrestricted macro.

Run that migration before seeding bundled examples. Existing user macros keep RAW,
while examples created by the new build are new source and remain BOUND. Once the
marker exists, later files are never rewritten, which makes absence of the directive
the stable new-code default rather than a version-dependent guess.
Turn source policy into process topology. RAW remains the compiler/control lane and
unrestricted runtime; BOUND gets its own independently supervised CPython process.
Registration, ephemeral tests, blocking invokes and live fires all route from the
source-derived mode, while the 1000 Hz path performs only an in-memory mode lookup.

Each lane owns its own crash breaker and warm latch. A control-plane failure retires
only the process that served it, arm state is mirrored to both, and prompt ids use
disjoint namespaces so two independent sidecars can share the existing beacon UI
without pid collisions.

The child handshake now names its execution domain and Rust refuses a mismatched
process. No containment is claimed by this commit yet; it establishes the structural
boundary that the BOUND capability policy can actually rely on.
Give the BOUND interpreter a deliberately small Python surface: curated imports,
no file/process/eval primitives, no private/dunder reflection, and no executable
module initialization beyond literals, imports and plain function definitions.
RAW keeps ordinary CPython unchanged. This is policy containment for trusted and
agent-authored macros, not a claim that CPython is a hostile-code security sandbox.

The familiar neuron helpers keep their names in both worlds, but BOUND input,
clipboard and focus operations cross the framed host boundary and reuse Neuron's
native primitives. Arbitrary neuron.run is explicitly RAW-only.

Move the final SAFE/mock decision to Rust as well. The helper still refuses early,
but every effectful act frame is checked again at execution time before native
input, audio, device, clipboard, focus or external-integration work can land.
The sidecar supplies macro identity and per-fire mock state so that check cannot
be bypassed by simply reaching the protocol directly.
BOUND macros should not need ambient filesystem authority just to remember a
counter or preference. Route store/load/forget/stored through the existing act
protocol and keep the JSON files in the same per-macro state directory and shape
RAW already uses, so switching modes does not strand data.

Rust owns the per-macro state lock, JSON parsing and atomic replacement for
BOUND. Locks are keyed by macro id so unrelated namespaces never serialize, and
Windows sharing violations get the same bounded retry budget as the Python RAW
path.

State verbs are deliberately not SAFE-gated: they mutate Neuron-owned macro
state, not the user's input/device/clipboard/system state. The host still
validates the macro id and JSON boundary before touching disk.
A split runtime must not turn neuron.invoke into an authority tunnel. Same-domain
calls keep their existing direct Python semantics. When a target lives in the
other interpreter, invocation returns to Rust: RAW may call a BOUND macro, which
still executes inside BOUND, while BOUND -> RAW is refused before any target fire
is sent.

The bridge forwards the trigger-time context, options, mock state and target
generation. Async calls remain queued on the callee's serial actor; synchronous
cross-domain calls wait on the callee result without moving its code into the
caller's interpreter. Same-domain synchronous return values remain native Python;
cross-domain values are the sidecar protocol's string result.

This makes the authority rule structural: a callee can never acquire more ambient
authority merely because a stronger or weaker macro invoked it.
Exercise the real bundled interpreters as two domains in one integration binary.
BOUND must fail ambient file access and os import through its normal language
surface and report neuron.run as RAW-only; RAW must retain ordinary import/open
power. Same-domain invoke keeps native Python values, RAW -> BOUND crosses the
Rust bridge, and BOUND -> RAW returns no callable.

The same proof carries BOUND state through a deliberate switch to RAW and checks
the asynchronous RAW -> BOUND path by observing the callee's persisted marker,
not by firing the target from the test itself.

Also pin the actual authority boundary directly in Rust: a forged effectful act
frame is refused while the process is disarmed before clipboard/native execution
is reached. That test does not depend on Python's convenience gate.
The BOUND/RAW router exposed one transactional gap: after the source write it
updated mode/generation in the host before the prepared callable's final commit
ack. A concurrent press could therefore route to a revision that was durable but
not yet live.

Keep the old routing truth until COMMIT succeeds. Only then publish source, mode,
generation and options, and only then retire an old execution lane after a mode
switch.

If the final acknowledgement is lost, retire the candidate session and restore
the previous source atomically instead of guessing whether the new callable won.
The next use therefore reconstructs last-known-good disk + manifest truth.
Keep ordinary macro behavior on the new BOUND default, but make every test that
deliberately needs ambient os/sys/ctypes access or a process-kill crash opt into
RAW explicitly. This preserves the old test intent instead of letting BOUND import
policy turn runtime/crash tests into registration failures.

Rework the act stress around the new host authority boundary. Private _act probes
run RAW, forged effectful frames are expected to fail closed while disarmed, and
rid/timeout/concurrency correlation uses host-owned state replies that remain safe
to exercise without hardware. Separate RAW pid and BOUND module-state canaries prove
neither sidecar silently respawns during its half of the stress.

Also fix curated from-import validation: BOUND authorizes the imported module root,
not the imported symbol name. The authority e2e now covers that form and proves a
RAW process crash leaves the independent BOUND interpreter warm.
With BOUND and RAW in separate warm interpreters, the old RetireAll event became
incorrect: a RAW crash could withdraw an unrelated BOUND ask that was still alive
and waiting for the user.

Make process death emit RetireDomain(mode). Prompt ids already occupy disjoint
high-bit namespaces, so the GUI can remove/stop only prompts owned by that domain.
The CLI ignores retirement as before.

Update crash stress accordingly: two BOUND asks remain answerable across a RAW
process death, while the RAW lane emits its own retirement event and then proves
it can respawn and serve a fresh ask. RAW-only queue/runtime crash tests now wait
for RAW-domain retirement explicitly.
Make the Rust capability layer fail closed instead of merely approximating the
old Python helpers. macro_key/hotkey/click now re-check the process arm gate
inside the native adapter as defense in depth, unknown mouse button names are
rejected instead of silently becoming a left click, and hotkey preserves the
Python helper's arbitrary key-chord semantics rather than treating every chord
as a modifier-prefixed single key.

Clamp brokered mouse coordinates before narrowing i64 protocol values to i32 so
giant accidental values cannot wrap into unrelated cursor motion. A parser unit
test pins invalid mouse-button refusal without touching real input.
Bring the public and internal docs in line with the runtime that now exists.
New macros are BOUND by default, one source-owned '# neuron: raw' directive
selects unrestricted Python, and the two tiers never share an interpreter.

Be explicit about the threat model: BOUND is capability-oriented policy
containment for ordinary and agent-authored macros, not a promise that CPython
can safely execute hostile code. RAW remains full user-authority Python by
design and can bypass Neuron's helpers. Existing macros migrate to RAW once so
an upgrade does not silently revoke authority.

Also correct the runtime/network wording, invocation direction, host-side arm
enforcement, prompt-domain isolation, and the old one-sidecar comments so the
README, SECURITY, GDD, TDD, STATUS and source headers all describe one system.
The pyruntime respawn phase killed the RAW interpreter while its outstanding ask
had accidentally remained BOUND. That made a RAW RetireDomain event arrive while
the prompt under test correctly survived in the other process.

Make both the pre-crash and post-respawn asks explicitly RAW so the phase proves
what it says: one RAW crash retires RAW prompts and the rebuilt RAW lane serves a
fresh beacon.

Also update the resident-budget comments for multiple warm macro-host helpers.
The allowlist already permits python.exe descendants without assuming a count.
The whole-document parser already kept multi-line main/macro wrappers, but a
one-line suite such as `def main(ctx, n=3): return n` fell back to the canonical
`def macro(ctx):` wrapper on the first canvas edit. That made an advertised
source-preservation edge silently rename the entry and discard its signature.

Use the first body statement's AST location to split the physical line at the
suite boundary. AST columns are UTF-8 byte offsets, so slice encoded bytes before
decoding the preserved prefix. The visual body may expand onto normal indented
lines, but main-vs-macro, async/signature text, decorators and leading header
source survive.

Pin the case against the real sidecar with a RAW one-line `main` document.
BOUND already failed closed at runtime, but authoring still let restricted code
look syntactically valid until Save or execution. Promote the containment rules
into the pure AST Check path so authority is visible before user code runs.

The BOUND policy pass now walks the whole module, including function bodies:
forbidden imports, ambient file/eval/reflection builtins, private/dunder access
and neuron.run report line-numbered "requires RAW" errors. Module initialization
remains declarative, while future-annotations keeps non-executing annotations
available without unnecessary escalation.

Rust derives the mode from source and sends it to the zero-exec compiler check;
durable registration runs the same check before preparing a candidate. Workshop
copy now calls this a compiler check instead of syntax-only validation.

Keep the legacy process-spawn stress explicitly RAW, since it is testing RAW's
arm gate. Add real-sidecar assertions that BOUND run/os/open fail at Check with
source lines and that the exact '# neuron: raw' directive makes the same source
compiler-valid.
The inherited last-known-good e2e intentionally fails during module top-level
execution after the candidate reaches the runtime. BOUND now rejects executable
top-level statements in the pure compiler-policy pass, which would make that
fixture pass for the wrong reason.

Mark the baseline and broken replacement RAW so the test continues to exercise
the prepare/execute failure path, proving the old callable and durable file
survive a real candidate-load exception rather than only a policy rejection.
@worflor
worflor force-pushed the macros/bound-raw-v2 branch from 0e7c14b to 0e193d9 Compare September 19, 2026 20:35
Base automatically changed from macros/runtime-truth to main September 19, 2026 22:18
@worflor
worflor marked this pull request as ready for review September 19, 2026 22:19
@worflor
worflor merged commit ed55d04 into main Sep 19, 2026
2 of 3 checks passed
@worflor
worflor deleted the macros/bound-raw-v2 branch September 19, 2026 22:20
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