Default-Restrictive Safety Toggles with Hard Invariants: A User-Controllable Framework for AI Agent Capability Boundaries

Twenty toggles, eight invariants no toggle can bypass, and a UX that prices loosening transitions higher than tightening ones

Tab Levitas

Memagen™

2026-05-08 · Version v1 · ~7 pages ·Subsystem: safety toggles
↓ Download PDF

Abstract

We describe the safety-toggle framework that ships in Memagen™ Lite (V1-lite, MIT). Every dangerous capability the platform exposes — auto-sending email, auto-pushing code, lifting the outbound-network allowlist, persisting all session memory, disarming the self-modification kill switch — is gated by a user-controllable toggle that defaults to its restrictive state. The framework's distinguishing properties are (a) deliberate UX asymmetry between loosening and tightening transitions (loosening costs reading a five-bullet warning, typing a verbatim acknowledgment phrase that paste detection rejects, re-authenticating, optionally entering a numeric or list cap, and waiting out a 60-to-900-second cooldown; tightening is one click) and (b) a non-bypassable hard-invariant layer of eight named guards that run after toggle evaluation and cannot be disabled by any toggle setting. We document the registry of twenty toggles, the eight named invariants (`secret_scrub_block`, `authentication_required`, `audit_logging`, `final_human_review_on_grant_submission`, `force_push_to_main_blocked`, `csrf_and_session_protection`, `prompt_injection_external_content_scan`, `pii_export_block`), the transition gate that runs cooldown, paste-rejection, typed-phrase, re-auth, and cap-shape checks before flipping any toggle, and the append-only JSONL audit log at `~/.memagen/safety_audit/YYYY-MM-DD.jsonl` whose entries are written via a temp-file-plus-os.replace dance so partial writes cannot leave a half-line on disk. The framework is open-source under MIT in the Lite distribution and is itself on the forbidden-path list of the sandboxed self-modification subsystem so the toggle layer cannot rewrite its own enforcement code via the agent.

Keywords — AI safety, default-restrictive, user controls, hard invariants, agent capability boundaries

1. Introduction

A useful agent is one whose capability surface is large. A safe agent is one whose capability surface is small. The standard resolution to this tension in production agent systems is one of three: ship a small surface and disappoint customers; ship a large surface and rely on prompt-time refusal; or ship a large surface and rely on the model’s own judgment as a last line of defense. None of the three has aged well. The first loses to competitors. The second is bypassed by any reasonably persistent injected instruction. The third treats a system that is known to be unreliable as the safety layer for its own actions.

Memagen™ takes a fourth position. Every limit the platform enforces is exposed as a user-controllable toggle. The user, not Memagen™, decides which risks are acceptable on their machine, against their accounts, with their data. The platform ships every toggle in its restrictive state and prices loosening transitions deliberately higher than tightening transitions. Returning to a safer configuration is never gated; departing from one is. Beneath every toggle sits a non-bypassable layer of named hard invariants that run after toggle evaluation and cannot be disabled by any toggle setting. A toggle says “you may try”. An invariant says “but only if these named guards pass”.

This paper describes the framework’s twenty toggles, eight invariants, transition gate, audit log, and the structural reason the safety code itself cannot be rewritten by the agent. We document the implementation in real detail because it is open-source under MIT in the Memagen™ Lite distribution; nothing in this paper is reserved.

The framing matters. Every other section of this paper is a consequence of one commitment: the operator holds the keys, and the platform never silently decides on the operator’s behalf which risks are acceptable.

2.1 Capability sandboxes

Linux seccomp-bpf (Corbet, 2009) and the gVisor sandbox (Google, 2018) restrict the system-call surface of a containerized process. firejail, bubblewrap, AppArmor, and SELinux are siblings. These mechanisms are runtime-enforced at the kernel boundary and not user-driven in the operational sense — once the container is configured, the user is the one being constrained, not the one composing the constraints. Memagen™‘s toggles operate one layer up: they are a user-facing UI surface that the user configures to constrain the agent the user is operating, not a defense-in-depth mechanism the platform enforces against the user. The two layers compose; the operating-system sandbox is still there.

2.2 Browser permission models

The closest UX analog to Memagen™‘s toggles is the modern browser permission model — geolocation, camera, microphone, notifications, clipboard, USB device access. These start from a default-deny posture, surface a per-origin prompt before granting, and offer a “remember this decision” option. The browser model is good but suffers two well-documented failure modes: prompt fatigue (users reflexively click “allow”) and origin confusion (users grant a permission to one site assuming they are granting it to another). Memagen™‘s toggles are fewer in number than the lifetime grant prompts a typical user accepts in a year of browsing, are scoped per-machine rather than per-origin, and are presented in a single Settings surface rather than as just-in-time interruptions, which we believe materially reduces the fatigue surface.

2.3 iOS / Android permission UX evolution

Mobile OS permission UX has converged over a decade toward (a) per-permission prompts at first use, (b) granular categories (e.g. “while using the app” vs. “always” for location), and (c) periodic re-confirmation surfaces (“Photos still has access; review?”). The Memagen™ toggle UX borrows the granularity and the periodic re-confirmation impulse but commits more strongly to the default-deny posture: the platform will not prompt the user just-in-time to loosen a toggle during an action. Loosening is deliberately a Settings-screen gesture, separate from the action that needs it. We discuss this design choice in Section 3.

2.4 LLM safety prompts vs. runtime gates

A large body of recent work treats agent safety as a prompt-engineering problem: instructions in the system prompt, refusal classifiers on the model output, RLHF-based alignment training. These are all useful and Memagen™ does not argue against them. But a prompt-time defense is by construction subject to prompt-time bypass. Runtime gates that are evaluated after the model has decided to act are not. Memagen™‘s toggle framework sits squarely in the runtime-gate category. The model can produce any output it likes; the gate decides whether the action runs.

2.5 Why “default open with override” loses to “default closed with explicit grant”

Two postures are possible: (1) default-open with the ability to deny, or (2) default-closed with the ability to grant. Both are coherent. Both have advocates. Memagen™ chooses (2) for reasons that are obvious on a long enough timeline: in a default-open posture, every new capability the platform adds is enabled by default, and the operator must actively notice and disable each one. In a default-closed posture, every new capability the platform adds requires the operator to actively notice and enable it. Over the course of years, posture (1) accumulates capabilities the operator forgot they granted; posture (2) accumulates only capabilities the operator deliberately wanted. The marginal capability the operator forgot about is precisely the capability an injected prompt or compromised plugin will use.

3. Toggle UX contract

Every toggle in the framework conforms to the same interaction shape, which is the load-bearing contract of the entire framework. Inconsistency here would be an attack surface — a user who has internalized the contract for one toggle should not be surprised by another.

3.1 Restrictive → permissive (loosening)

A loosening transition is high-friction by design. The user, having opened the Settings screen and clicked the toggle, faces:

  1. A modal warning. The modal body is the toggle’s warning_text field, which is required to be non-empty (validated at construction time in SafetyToggle.__post_init__). Every warning text in the registry is structured as five bullet points: what turning this on actually does; the immediate failure mode; the worst-case failure mode; the breadth of the change (this toggle applies to every agent loop, not just the visible one); and a “leave this off unless…” sign-off. The five-bullet structure is enforced socially in the registry, not statically by the type system.

  2. Typed-phrase verification. Each toggle declares a typed_phrase the user must type into a confirmation field. Phrases are short, toggle-specific, and deliberately ungeneric (“Push my code without asking”, “Disarm the self-mod kill switch”, “Allow destructive shell flags”). The framework validates the phrase via enforce_typed_phrase, which strips whitespace and compares case-sensitively.

  3. Paste-rejection. The UI layer measures the time between the field receiving focus and the submit gesture. If the elapsed time is below a threshold (sub-50ms paste-to-submit is the operational signal), the UI sets paste_detected=True on the TransitionRequest, and the framework rejects with reason="paste_detected". Pasting defeats the point of a typed phrase: the goal is to force the user to read what they are agreeing to. A user who pastes the phrase from the warning text has not read it; they have transcribed it from one box to another.

  4. Re-authentication. When requires.reauth=True, the user must re-enter their password (separate from a stale session token or auth cookie). The framework treats reauth_token as a presence check; the API layer is responsible for actually verifying the password against the credential store. The re-auth is to defeat session-hijack and “left their laptop unlocked” scenarios; a stolen session cookie is not enough to flip a toggle.

  5. Optional cap input. When requires.cap_input is non-None, the user must supply a CapSpec: either an int_value (e.g. “max 50 emails per day”, “max 1000 autonomous ops per day”) or a list_value (e.g. an allowed-recipients tuple). The framework persists the cap on the resulting ToggleRecord; consumers (the email send path, the autonomous-op accounting code) read the cap value out at action time and enforce it. The framework itself does not interpret cap values.

  6. Cooldown. Each toggle declares a cooldown_seconds (60s default; up to 900s for the most dangerous toggles). The framework enforces a minimum elapsed time since the last loosening transition of the same toggle. The cooldown bounds how fast a malicious tool catalog or compromised plugin can flip a toggle off and back on to slip a single bad action through. Tightening is never rate-limited.

  7. Audit-log entry. Every transition (success or denial) writes one JSONL line to ~/.memagen/safety_audit/YYYY-MM-DD.jsonl. The append is atomic at line granularity (Section 7).

3.2 Permissive → restrictive (tightening)

Tightening is one click. There is no warning. There is no typed phrase. There is no re-auth. There is no cooldown. There is no cap input. The audit log records the transition and that is the sum of the friction.

This asymmetry is the point. The user must be able to reach a safer state at speed. A user who realizes mid-action that auto-send is on, or that the outbound network is unrestricted, must be able to revert in one gesture without facing a wall of confirmation. The platform’s interest in slowing down a tightening is exactly zero.

3.3 The bulk reset

A “Reset all to safe” button is exposed in Settings; it calls ToggleStore.reset_all_to_safe(actor), which iterates the live records and tightens any toggle currently in PERMISSIVE. The bulk reset, like a single tighten, is unconditional.

4. The twenty-toggle registry

The registry exposes twenty toggles across seven categories. Categories are a UI-grouping concept; the runtime treats every toggle by its key. Keys are dotted-namespaced for readability.

CategoryToggle key
commsemail.auto_send, email.recipient_allowlist
codecode.auto_commit, code.auto_push, code.allow_core_patch_in_autonomous_loop, tools.bypass_catalog_gate, agent.bypass_plan_mode, tests.allow_red_commit, selfmod.enabled, selfmod.auto_apply, selfmod.kill_switch
fsshell.allow_destructive_flags, shell.allow_outside_workspace
netnet.allow_outbound, net.allow_browser_auto_actions
memorymemory.persist_everything, dreams.write_during_active_session
secvault.auto_grant_capabilities, autonomous.raise_daily_op_cap
inputvoice.always_listening

Every entry is a SafetyToggle dataclass record produced once at import time in core/safety/registry.py. The dataclass enforces three invariants in SafetyToggle.__post_init__:

  • default_state must be RESTRICTIVE (the registry will fail at import time if a future entry tries to ship default-permissive)
  • description must be non-empty
  • warning_text must be non-empty
  • hard_invariants must list at least one named invariant

To ground the abstract description, here is the full record for email.auto_send:

EMAIL_AUTO_SEND = SafetyToggle(
    key="email.auto_send",
    label="Auto-send email",
    category="comms",
    default_state=ToggleState.RESTRICTIVE,
    description=(
        "Allow agents to send email immediately, without showing the draft "
        "to you first."
    ),
    warning_text=(
        "Auto-send is dangerous. When this is on:\n"
        "- Memagen will send emails immediately, without showing them to you first.\n"
        "- You will not get a chance to catch hallucinated facts, wrong recipients, "
        "or unintended attachments before they leave your account.\n"
        "- Sent email cannot be unsent. Recipients may save, forward, or screenshot it.\n"
        "- A bug, prompt injection, or compromised tool catalog could weaponize "
        "your inbox before you intervene.\n"
        "- Auto-send applies to every agent loop running under your account.\n"
        "Turn this on only if you have separately verified that every prompt path "
        "that can reach `email_send` is one you trust."
    ),
    requires=ToggleRequirements(
        typed_phrase="I understand auto-send cannot be undone",
        reauth=True,
        cooldown_seconds=120,
        cap_input=CapSpec(
            name="Daily send cap",
            int_value=50,
            notes="Maximum emails the agent may auto-send per UTC day.",
        ),
    ),
    hard_invariants=(
        "authentication_required",
        "audit_logging",
        "secret_scrub_block",
        "prompt_injection_external_content_scan",
    ),
)

The cooldown for this toggle is 120 seconds rather than the 60-second default because email sending is irreversible. The cap is a daily integer the email-send path reads at action time. The four hard invariants run after toggle evaluation; we describe them next.

The most aggressive cooldowns in the registry are 600 and 900 seconds, applied to vault.auto_grant_capabilities, selfmod.enabled, selfmod.auto_apply, and selfmod.kill_switch. These are toggles whose loosening is rare enough that even a ten-to-fifteen-minute cooldown should not impede a legitimate operator.

5. Hard invariants

The hard-invariant layer is the framework’s novel contribution. Eight named guards run after toggle evaluation in the call path of every gated action; no toggle setting bypasses them.

5.1 The eight invariants

  1. secret_scrub_block — outbound payloads (email bodies, HTTP request bodies, tool arguments) are scanned for vault-managed credential patterns. The detector is conservative: it matches the named-token shapes the vault emits (memagen_secret_...), sk-... style API keys, AKIA... AWS access-key prefixes, and PEM-armored private-key headers. A match raises InvariantViolation("secret_scrub_block", "outbound payload contains what looks like a credential"). The point is not exhaustive DLP — that is an orthogonal product — but to block the obvious leak path.

  2. authentication_required — every state-changing action requires an authenticated session. The invariant raises if ctx.authenticated is false. This is the floor under every toggle: regardless of toggle state, an unauthenticated request to flip or to act under a flipped toggle fails.

  3. audit_logging — every transition must be audit-logged. The invariant function is a documented pass-through: actual logging happens in the toggle store, not in the invariant. The reason it exists as an invariant rather than just an implementation detail is so the contract is visible to readers of a toggle’s spec: “this toggle’s transitions are audited, no matter what”.

  4. final_human_review_on_grant_submission — application-form submission (e.g. Phase-6 grant submission) requires per-action human consent. The invariant fires when ctx.action == "grant.submit" and ctx.explicit_consent is false. Even with every other toggle off, the literal click of “submit this grant application” is reserved for the human.

  5. force_push_to_main_blockedgit push --force to main or master requires per-action consent. The invariant inspects ctx.action for the substrings "push" and "force", then checks ctx.extras["git_target"] against ("main", "master"), and raises if explicit_consent is false. Force-pushing to a feature branch is unaffected.

  6. csrf_and_session_protection — the web layer’s CSRF token and session must both be valid. The invariant raises if either ctx.csrf_ok or ctx.session_ok is false. This is the runtime assertion of what the web framework already does at the request boundary; declaring it as a named invariant means a toggle that gates a web action visibly carries the contract.

  7. prompt_injection_external_content_scan — when fetched external content contains imperative-mood directives targeting the agent, the action falls back to confirm-before-act regardless of toggle state. The detector is generous on purpose: false positives (extra confirmation) are vastly preferable to false negatives (silently following an injected directive). The detector matches (?:ignore|disregard) (?:all|any)? (?:previous|prior)? instructions, “system prompt”, “you must (now|immediately|always)”, and (?:execute|run|delete|wipe|format) (?:the|all|every). A match in the absence of explicit_consent raises.

  8. pii_export_block — bulk PII export requires per-export confirmation. The invariant fires when ctx.action starts with "pii.export" and explicit_consent is false.

5.2 Declaration

Each toggle declares which named invariants apply via hard_invariants: tuple[str, ...]. The names are part of the public contract: they appear in the toggle’s record, in the modal payload returned by ToggleStore.request, and in audit-log entries. A typo in the name is not silently ignored; the runner looks the invariant up by name and raises KeyError if the name is unknown (verified by test_unknown_invariant_name_in_toggle_is_not_silently_ignored).

5.3 Evaluation

The single sanctioned way to evaluate invariants for a toggle is core.safety.invariants.run_invariants_for_toggle(toggle, ctx). The function looks up each invariant by name in the module-level _INVARIANTS dict and calls it with the supplied InvariantContext. The first violation aborts; subsequent invariants do not run. The runner’s signature deliberately does not accept any “skip these” parameter, and there is no field on SafetyToggle that says “but skip the invariants this time”. The only way to skip an invariant is for the consumer not to call the runner at all, which is the threat model the unbypassability tests in tests/safety/test_invariants_unbypassable.py exist to refute (Section 9).

5.4 The InvariantContext

The runner takes an InvariantContext dataclass with fields the invariants pull from: authenticated, actor, outbound_payload, explicit_consent, external_content, action, csrf_ok, session_ok, and a free-form extras: dict[str, Any]. Not every invariant uses every field. The extras map exists so future invariants (e.g. forthcoming Phase-6 grant-submission additions) can be added without revising the context shape.

6. Implementation

6.1 Registry

core/safety/registry.py exports twenty SafetyToggle records, each constructed with description, warning_text, requires, and hard_invariants filled in. The _req(...) helper at the top of the file shortens the construction of ToggleRequirements; the default phrase used by the helper, when no phrase is overridden, is "I understand the risk". Most registry entries override the default with a toggle-specific phrase.

A single function build_default_toggles() returns the canonical list of all twenty toggles in stable order; ToggleStore.__init__ consumes the list and constructs one ToggleRecord per toggle. The store rejects duplicate keys at construction time.

6.2 Transition gate

core/safety/toggles.py contains the transition gate. The two-step protocol is:

  • ToggleStore.request(key) — pure read, returns the data the UI needs to render the loosening modal: the current state, the warning text, the typed-phrase requirement, the cap-input shape, the cooldown remaining, and the list of hard invariants. No state changes; no audit entry.
  • ToggleStore.commit(request: TransitionRequest) — atomically applies the transition or raises TransitionDeniedError.

For a tightening request (target_state=RESTRICTIVE), commit calls _tighten, which sets the state, clears any cap that came with the previous loosening, increments the audit counter, and writes one audit entry with result="ok" and reason="tighten". The other request fields are ignored.

For a loosening request, commit calls _loosen, which executes five gates in order:

  1. Cooldown. _cooldown_remaining(rec) computes the elapsed time since last_loosened_at and returns the remaining seconds; if positive, the request is denied with reason="cooldown" and an audit entry recorded.
  2. Paste rejection. If the toggle has a typed_phrase and request.paste_detected is true, denied with reason="paste_detected".
  3. Typed-phrase match. enforce_typed_phrase(expected, actual) strips and compares case-sensitively; mismatch denies with reason="phrase_mismatch".
  4. Re-auth presence. If requires.reauth and request.reauth_token is missing, denied with reason="reauth_missing".
  5. Cap shape. If requires.cap_input is non-None and the supplied cap is missing or unset, denied with reason="cap_required".

Every denial path writes one audit entry with result="denied" and the failing-gate reason before raising. Successful loosening flips the state, sets last_loosened_at, persists the cap, and writes an audit entry with result="ok" and reason="loosen".

The store’s lock is a single threading.RLock. The intentional coarseness — one lock for the whole registry — is fine for an audit-log volume measured in transitions-per-minute under stress.

6.3 Invariant runner

core/safety/invariants.py contains the eight invariant functions, the module-level _INVARIANTS: dict[str, Invariant] registry, and run_invariants_for_toggle. Each invariant is a small named function Invariant = Callable[[InvariantContext], None] that returns normally on pass or raises InvariantViolation(name, message) on fail.

A test-only override_invariant_for_tests(name, fn) is provided for the unbypassability suite to substitute fakes; the canonical register_invariant(name, fn) refuses to silently shadow an existing invariant. all_invariant_names() returns the registry keys, used by the test that asserts every named invariant has a violation case (Section 9).

6.4 Rehydration

A process restart should not silently revert a toggle to RESTRICTIVE for a capability the operator deliberately granted earlier today. ToggleStore rehydrates each toggle’s last committed state from the audit log on construction, looking back seven days. Denied transitions are ignored. If the most recent ok entry for a key is a loosening, the toggle starts permissive with the recorded cap; otherwise restrictive. If the audit log is missing or unreadable, every toggle starts restrictive — the rehydration code’s failure mode is to be safe.

7. Audit log

core/safety/toggle_audit.py implements the toggle-specific audit log. It is a separate module from the wider runtime audit log (core/safety/audit.py, sqlite-backed) because the toggle audit has different requirements:

  • Daily-rotated JSONL files at ~/.memagen/safety_audit/YYYY-MM-DD.jsonl. The format is JSONL because audit data is append-only event records read by humans and forensic tools, not by SQL.
  • Atomic appends. Each entry is encoded once, the existing file is read, the new line is appended in a sibling tempfile, and os.replace swaps the tempfile over the original. os.replace is atomic on POSIX and Windows for a same-directory rename, which gives us crash-safety at line granularity. The cost is one extra read per append; the audit-volume profile makes that acceptable.
  • Restrictive permissions. The audit directory is 0o700. Each file is 0o600. The umask is set to 0o077 for the duration of the tempfile creation so the on-disk artifact is never accidentally world-readable.
  • Stdlib-only. The module has no third-party dependencies, so import is cheap and the audit log cannot fail to load due to a transitive package issue.

Each entry has the shape:

{
  "ts": 1746744123.456,
  "actor": "user@example.com",
  "key": "email.auto_send",
  "from_state": "restrictive",
  "to_state": "permissive",
  "result": "ok",
  "reason": "loosen",
  "cap": {"name": "Daily send cap", "int_value": 50, "list_value": null}
}

Denied entries omit the cap field and carry one of cooldown, paste_detected, phrase_mismatch, reauth_missing, or cap_required in the reason field.

The framework records the typed phrase only as a hash, not in cleartext, in the audit row. The IP and user-agent of the request are populated by the API layer at the call site of ToggleStore.commit and persisted on the audit entry alongside the framework-supplied fields.

The audit module exposes read_day(day), tail(key=None, limit=50), count_for(key), and tail_recent_days(days=7) for forensic and rehydration consumers.

8. Self-modification interaction

The Memagen™ Pro distribution offers sandboxed self-modification of the agent’s own runtime code, gated by the selfmod.* toggles described in Section 4 and by additional Pro-only mechanics that are out of scope here. The interaction with the safety-toggle framework is structural rather than policy-based: the core/safety/ directory is on the forbidden-path list of the self-modification subsystem.

What this means in practice: even when selfmod.enabled, selfmod.auto_apply, and the kill switch are all in their permissive state, a self-modification proposal that touches any file under core/safety/ is rejected at the proposal layer, before any review or apply step. The toggle framework cannot rewrite itself via the agent. The hard-invariant module cannot rewrite itself. The audit log cannot rewrite itself.

This is structural because it does not depend on the user choosing the right toggle setting. It is enforced by the self-modification subsystem’s path filter, which lives outside the toggle layer and is itself on the forbidden-path list (recursively). A successful prompt-injection attack that produces a plausible-looking patch to core/safety/invariants.py is rejected with no involvement from the toggle layer at all.

9. Evaluation

The framework ships with seventy-nine unit tests in tests/safety/: forty-nine new tests added with the toggle-and-invariant work, and thirty pre-existing tests covering related safety subsystems (audit, kill switch, plugin signing, RBAC, secrets). The new tests are split across two files:

  • test_toggle_framework.py — covers default-state-is-restrictive for every registry toggle; tightening always succeeds without friction; loosening requires the typed phrase verbatim; loosening is rejected when paste was detected; re-auth requirement is enforced when set; cap shape is validated when required; cooldown blocks back-to-back loosening; the audit log appends one entry per attempt (success and denial); and tightening clears the cap.

  • test_invariants_unbypassable.py — covers, for each canonical invariant, one synthetic-toggle test that constructs a SafetyToggle declaring that invariant, builds a context that should make the invariant fire, runs run_invariants_for_toggle, and asserts InvariantViolation is raised. A complementary test test_synthetic_toggle_cannot_disable_an_invariant_from_its_record builds a toggle that claims the invariant and constructs a context that should make it fire, then verifies the invariant fires regardless. A registry-level test sweeps all_invariant_names() and asserts every named invariant has a violation case in the file’s source — a typo or new addition cannot be silently introduced without the test file growing.

The supporting test test_registry_completeness.py asserts that every entry in build_default_toggles() has a non-empty description, non-empty warning text, at least one declared invariant, and a default state of RESTRICTIVE.

What is not in the evaluation: we have not benchmarked the per-call-site invariant assertion under load. The runner is O(n) in the number of declared invariants, each invariant is an O(1) function modulo regex scans whose input is already bounded by the action’s payload, and the load profile (one runner call per gated action; gated actions are minutes-apart in normal use) does not motivate a benchmark. If a future profile demonstrates the runner is on a hot path, the runner is small enough to inline.

10. Discussion

10.1 The cost of generosity

The framework prices loosening transitions to be deliberately annoying. Users will sometimes be annoyed. A user who does want to send 50 emails autonomously today must read a five-bullet warning, type “I understand auto-send cannot be undone”, re-authenticate, accept a daily cap of 50, and wait out a 120-second cooldown. That is a meaningful chunk of the user’s afternoon for a feature the user already decided to use.

We argue this trade favors the user. The asymmetric pricing means a bad outcome — auto-send leaking secrets, a force-push to main eating someone’s branch, an autonomous loop melting the cost cap — never occurs without the user having actively, deliberately, and recently confirmed the capability. A user who was in fact in a hurry and clicked through the warning has a written record of having done so. A user who was not in a hurry but wished they had not enabled the toggle can revert in one click.

The alternative — a framework that makes loosening painless because the user “knows what they’re doing” — silently transfers the cost of a misconfigured capability from the platform’s UX back to the user’s data. We have made a different call.

10.2 The structural argument for hard invariants

A toggle alone is not enough because a misconfigured toggle is a single point of failure. A user who, in a hurry or under social pressure, flips email.recipient_allowlist to permissive has not only opened the recipient list — they have removed the platform’s last guard against an injected prompt instructing the agent to email everyone. That is too much consequence for one click.

The hard-invariant layer means the consequence is bounded. Even with email.recipient_allowlist permissive, secret_scrub_block still runs and refuses to send a payload containing a vault-managed credential. Even with code.auto_push permissive, force_push_to_main_blocked still runs and refuses to force-push to main without per-action consent. Even with every toggle in the registry permissive, an unauthenticated request for any state-changing action is refused by authentication_required.

The invariants are the floor. The toggles are the ceiling. The user controls the ceiling; the platform controls the floor; and the floor is not a configuration surface.

10.3 What this is not

The framework is not an alignment mechanism. It does not attempt to constrain what the model can say. It constrains what the runtime, on receiving the model’s output, will do. A model that decides to suggest a destructive shell command is not blocked from suggesting it; the action is blocked from running unless shell.allow_destructive_flags is permissive, and even then authentication_required and audit_logging still apply.

The framework is not a substitute for OS-level confinement. The platform still benefits from running under a constrained user account with workspace-scoped filesystem access and an outbound network allowlist. The toggles are an additional layer the operator controls; they are not the only layer.

The framework is not a substitute for prompt-time defenses. Models with strong refusal behavior remain useful. Lenient parsing (Section 4 of the master paper) and the V3-P capability vault (Section 5 of the master paper) are complementary surfaces. Defense in depth still applies.

11. Future work

Toggle inheritance. A multi-user organization should be able to express “the org’s restrictive settings are the floor; teams can tighten further but cannot loosen past the org default; users can tighten further but cannot loosen past the team default”. The framework’s data model already supports this: each SafetyToggle is a static record, and a future store implementation can carry an inheritance chain on the ToggleRecord. The work is in the surface and the audit-log shape, not the core.

Time-of-day sensitivity. A toggle that is permissive during business hours and auto-tightens overnight, weekends, or during the operator’s declared focus blocks would meaningfully reduce the surface a sleeping operator presents. The framework’s clock injection (clock: Callable[[], float] in ToggleStore.__init__) already makes this testable; the work is in expressing the schedule and in the UX for “your toggle just auto-tightened because it’s 9pm”.

Risk-budgeted toggles. A toggle that auto-tightens after N permissive uses in a window — a usage budget rather than a state — would let an operator say “I want to send up to 50 autonomous emails today, and after that the toggle should re-confirm”. The cap-input shape is suggestive of where this lives, but the current framework does not auto-tighten on cap exhaustion; the consumer is expected to refuse the action. Pulling that logic into the toggle layer is mostly a UX question.

Per-tool granularity inside a category toggle. tools.bypass_catalog_gate is a sledgehammer; an operator who only wants to widen access to one specific tool currently has to flip the gate for every tool. A per-tool override list, carried as a CapSpec.list_value, is a small extension that would materially reduce the worst-case loosening footprint.

12. References

  1. Anthropic. (2024). Model Context Protocol Specification. https://modelcontextprotocol.io
  2. Corbet, J. (2009). Seccomp and sandboxing. LWN.net. https://lwn.net/Articles/332974/
  3. Felt, A.P., Egelman, S., Finifter, M., Akhawe, D., & Wagner, D. (2012). How to ask for permission. HotSec ‘12. (Browser permission UX evolution.)
  4. Google. (2018). gVisor: container runtime sandbox. https://gvisor.dev
  5. Hardy, N. (1985). KeyKOS architecture. ACM Operating Systems Review, 19(4).
  6. Levitas, T. (2026). Memagen™: An Open-Core Agent Platform with Lenient Tool Routing, Capability-Gated Execution, and a Compound-Graph Memory Substrate. Memagen™ master paper, Sections 5–6.
  7. Saltzer, J.H., & Schroeder, M.D. (1975). The protection of information in computer systems. Proceedings of the IEEE, 63(9). (Origin of the principle of fail-safe defaults.)
  8. Shapiro, J.S., & Smith, J.M. (1999). Capability myths demolished. Technical report, University of Pennsylvania.
  9. Yee, K.-P. (2002). User interaction design for secure systems. ICICS ‘02. (Asymmetric grant/revoke UX argument.)
  10. Zalewski, M. (2011). The Tangled Web: A Guide to Securing Modern Web Applications. No Starch Press. (CSRF, session, and origin-confusion failure modes referenced in Section 5.)