Octoscript treats generated scripts, tool descriptions, and tool inputs as untrusted. The runtime has two separate security boundaries:
- The language boundary exposes no ambient filesystem, process, network, or
platform APIs. Scripts can reach a tool only through
tool.callor an explicitly host-controlledtool.start(...).await()promise. - The execution boundary must contain any adapter with OS effects. The current Linux Bubblewrap backend launches a dedicated worker with a narrowly scoped filesystem policy; other desktop, mobile, and embedded targets still need their own platform-specific containment backend.
fixed_file_catalog::FixedFileCatalog is the supplied narrow local-file
adapter. During trusted setup, a host registers a bounded set of already-opened
regular files under canonical opaque identifiers. A script can request only an
identifier through an explicitly registered text tool; it cannot provide a
path, enumerate the catalog, traverse directories, or write a file. The
catalog retains a descriptor rather than a path, so a later path replacement
does not redirect that entry. Reads remain bounded by both the catalog and tool
policy, require UTF-8, and convert all adapter errors to generic script-facing
messages. File identity is pinned, not file content: a host must either select
immutable files or treat mutable content as untrusted data. This adapter is not
a general filesystem API, symlink policy, or OS containment boundary. A grant
to one catalog tool covers every entry in that catalog; hosts needing a narrower
file scope must use separate tools/catalogs or an input-aware authorization
hook. A successful read necessarily reveals that its requested identifier was
granted, so opaque identifiers should be unguessable when that matters.
The adapter's byte bound is not a wall-clock I/O bound; a slow or remote
host-selected file can still block the local handler. Hosts must select files
with acceptable latency or use a contained worker with a deadline for broader
or potentially blocking effects.
When its explicit feature is enabled, HttpEndpointCatalog supplies a similarly
narrow outbound JSON adapter. Trusted setup fixes each full endpoint URL,
method, opaque identifier, and optional endpoint-bound credential reference.
The executable request contract accepts only a catalog identifier and, for POST,
a bounded JSON object or array body; it rejects URL, method, header, query,
secret, and redirect selectors before the adapter runs. The contract publishes
opaque endpoint identifiers to the host-side catalog but not URLs or credential
references. HTTPS is required by default; the explicitly named insecure HTTP
constructor is for trusted local or development services only and cannot carry
a credential binding. The adapter disables environment proxies and redirect
following, exposes no cookie API, bounds script request input, response headers,
bodies, and total request time, and requires a 2xx JSON object or array response.
An explicit trusted HttpEndpointSecretResolver can inject a bounded sensitive
header into one configured HTTPS endpoint only after input checks. Octoscript cannot
name, read, enumerate, serialize, or receive a secret. Script-facing failures
are generic and do not disclose endpoint membership, URLs, credential references,
secret values, status codes, headers, response bodies, or transport details.
This is endpoint-bound injection, not a general secret broker; host metadata and
endpoint URLs must not contain credentials, and POST body semantics need a
separate reviewed schema or adapter when broad input is not safe.
The optional platform-keyring-secret-resolver feature maps only setup-selected
opaque endpoint-secret IDs to exact service/account locations in explicit native
macOS, iOS, or Windows credential stores. It reads existing values only at
invocation time, never exposes those mappings through accessors or Debug, and
never creates, updates, rotates, or deletes credentials. Unsupported Linux and
embedded targets fail closed instead of using keyring-rs's in-process mock
store. This is still not a general credential broker, rollback anchor, or OS
network boundary; resolver latency and native-store behavior remain host
responsibility.
This endpoint catalog mediates one registered API surface only. It does not pin DNS, enforce a firewall or per-origin egress rule, contain a blocking request after it starts, reduce the embedding process's network authority, or restrict another trusted adapter. A fixed URL is still trusted host policy. Effects requiring real network isolation need a target-specific containment or network backend.
The canonical Octoscript profile is an effect-free preflight in front of the vendored Makepad parser. A profile rejection never reaches that parser or a host binding; a profile acceptance is then independently parsed by the VM before evaluation. The runtime carries executable canonical-fixture regression coverage, but the two parsers are not formally proven equivalent. Parser/VM differential fuzzing is required before a stable language release.
Standalone Runtime masks inherited Makepad platform/debug entry points before
canonical and compatibility evaluation. The source surface cannot reach the
vendored math, GC, pod, shader, regex, HTML, or direct standard-output APIs;
std.log, std.print, std.println, std.regex, and String.parse_html()
are specifically unavailable. mod.std.assert, the frozen no-authority
mod.std.math, mod.std.json, mod.std.text, mod.std.array, and
mod.std.object modules, ordinary bounded language operations, and explicitly
installed host modules remain available.
This avoids unreviewed native output and native allocations outside Octoscript's
tracked heap from becoming generated-source behavior. It does not alter a host
that embeds the raw Makepad VM, and a trusted host can still install a reviewed
capability under any otherwise-masked module name through the normal policy
boundary.
The only built-in numeric module added by Octoscript itself is frozen,
effect-free mod.std.math. It provides bounded-arity scalar f64 operations
and constants, not the vendored shader-oriented mod.math module. It cannot
perform I/O, access host state, read time or entropy, or load a Rust crate.
Its floating-point results are ordinary script data rather than a portable
bit-for-bit cross-platform numeric guarantee; non-finite results remain
ineligible for the JSON capability boundary.
The frozen mod.std.json module exposes only json.parse(document) and
json.stringify(value). Both reuse the runtime's strict byte-, nesting-, and
cycle-bounded JSON boundary; they do not inspect adapters, host state, files,
processes, networks, clocks, entropy, or Rust crates. A JSON helper therefore
does not grant authority beyond pure bounded data conversion.
The frozen mod.std.text module exposes a small literal string-shaping
surface. Its casing and replacement functions stream output through the VM's
configured string bound, while predicates only inspect supplied strings. It
does not expose regex matching, adapters, host state, files, processes,
networks, clocks, entropy, or Rust crates; it grants no authority beyond local
bounded data conversion.
The frozen mod.std.array module exposes only array.len(value),
array.slice(value, start, end), array.concat(left, right), and
array.reverse(value). Its transforms have no callbacks or host hooks, produce
shallow arrays, and reject source arrays over 4,096 items before native
traversal; concat also rejects a combined result over that bound. len is
constant-time and does not traverse the array. The module cannot inspect
adapters or host state, and cannot access files, processes, networks, clocks,
entropy, or Rust crates.
The frozen mod.std.object module exposes only object.len(value),
object.keys(value), object.values(value), and object.merge(left, right).
It accepts plain record or JSON-object data, reads own fields only, never
follows prototypes, and invokes no callbacks or host hooks. keys, values,
and merge reject source records over 4,096 own text-keyed fields; merge
also rejects a combined source-field count over that bound. len is
constant-time and does not traverse fields. Returned arrays and records are
shallow VM data, not host handles. The module cannot inspect adapters or host
state, and cannot access files, processes, networks, clocks, entropy, or Rust
crates.
Runtime replaces inherited direct value.to_json() and
document.parse_json() dispatches with bounded JSON methods. The default
direct-conversion ceiling is 64 KiB and 64 container levels, and a host may
lower it through execution limits. Parsing accepts only strict JSON strings or
UTF-8 byte arrays; it is bounded before the value is copied into the VM.
Cycles, unsupported values, non-finite numbers, and duplicate object keys are
rejected on serialization; malformed or non-UTF-8 input is rejected on parsing.
Either direction rejects depth exhaustion and input or output exhaustion as
ordinary native errors rather than unbounded parser or serializer work. This
protects Octoscript Runtime evaluation, including the explicit
compatibility-evaluation entry point; it does not alter a host that directly
embeds the raw Makepad VM.
Runtime also caps every newly constructed script string at 256 KiB by
default through ExecutionLimits::max_string_bytes; a host can lower that
limit for its target. An attempted overflow terminates the current evaluation
as an uncatchable hard resource failure, including during compatibility
evaluation and bounded JSON reconstruction.
ExecutionLimits::max_heap_bytes additionally caps tracked retained capacity
in the Octoscript-owned VM heap. Its default is 8 MiB and it accounts for script
strings, arrays, object storage, slot tables, and intern tables. Sparse array
and object writes, plus conservative object-map rehashes, are rejected before
they request their backing allocation; other normal script allocations raise
the same uncatchable hard resource failure when retained storage crosses the
cap. This heap-only accounting is not a process allocator quota: VM parser/code
storage, other VM control vectors, allocator metadata, compiled regex internals,
and opaque trusted Rust adapter allocations remain outside it.
ExecutionLimits::max_stack_values caps live VM operand values at 32,768 by
default, and ExecutionLimits::max_call_frames caps active VM call frames,
including the root frame, at 1,024 by default. Both terminate the current
evaluation as uncatchable hard resource failures. They do not account for
native Rust stacks, parser or code storage, other VM control vectors, allocator
metadata, or opaque trusted adapter allocations.
Targets that need process-wide memory or effect containment must layer an
operating-system boundary around the worker.
Canonical try/catch handles ordinary script and native-binding errors and
unwinds Octoscript function calls, but it is not a sandbox or transaction. It
cannot catch string-allocation, heap-allocation, operand-stack, call-frame,
instruction-limit, or hard-deadline termination, inspect an error object, widen a
capability lease, refund a call,
erase an audit outcome, or bypass a workflow data contract. A caught error is
discarded before the fallback runs and does not appear in the evaluation
diagnostics. An uncaught native error is host-facing and may contain
adapter-provided text, so adapters must return disclosure-safe messages and
keep private detail in trusted logs.
octoscript-lsp is a host-only helper for a trusted local editor client. It never
reads a document URI, evaluates source, creates a capability host, resolves an
imported module, or loads an adapter. Its top-level fn/let outline and
same-document lexical definition/reference index are derived only from valid
client-provided canonical source and grant no tool authority. Binding-kind hover
and neutral symbol highlights use that same index; they do not expose runtime
values or claim read/write analysis. Lexical completion uses only
expression-identifier sites and binding visibility metadata from the supplied
snapshot. It may retain a site from incomplete source only when the site ends
before or at the first syntax diagnostic. It never queries runtime values,
module exports, a live tool catalog, or adapter metadata, and completion
candidates do not grant or predict authority. A local editor integration may
supply bounded static projections in initialization options: a tool projection
uses only names, formats, and descriptions to complete a direct visible
mod.tool literal, while a separate module-interface projection uses canonical
mod.* paths and descriptions for direct import-path or imported-module member
spelling. The LSP never queries or authenticates either projection, loads or
validates a referenced module, presents no partial projection after malformed or
over-limit input, or lets module metadata replace the fixed mod.tool methods.
It also carries a compiled-in projection of the frozen standalone mod.std
namespace: use mod. can suggest std, and use mod.std. can suggest only
the documented array, assert, json, math, object, and text core
modules. The server
suppresses advisory children below that namespace, matching the runtime's
frozen std object; an integration must use a distinct host-owned mod.*
namespace for capabilities.
For an exact visible use mod.std.assert binding, and direct std.assert(...)
after use mod.std, assertion hover and signature help are likewise compiled
in and carry no catalog lookup, host access, or authority. The same holds for
fixed completion, hover, and signature help for direct use mod.std.json
bindings, use mod.std.text bindings, use mod.std.array bindings, and
use mod.std.object bindings.
Suggested names remain subject to runtime module binding, catalog, and lease
checks. Guarded rename is advertised only to a client that supports versioned
document edits. It never renames an
import path, never operates on a truncated index, validates the replacement with
the canonical lexer and parser, and returns edits only when the complete remapped
lexical report is unchanged apart from the selected name and byte offsets. This
is indexed lexical preservation, not proof about unindexed forward references,
fields, reflection, or other name-coupled runtime semantics. Every returned edit
is bound to the source version used for analysis.
The lexical index is source-local, conservative, bounded to 4,096 retained
definitions and resolved references, and lazily cached only for the current
document version. Completion sites have a separate 4,096-entry bound and cache;
either truncation is exposed to the client as isIncomplete. Symbol truncation
also suppresses all completion candidates because an omitted inner definition
could shadow a retained outer binding. A definition or hover is returned only
when its retained occurrence has an exact binding. The index is not a type
checker, module resolver, capability analysis, or authorization decision. The
server retains at most 128 document states and no source text larger than the
canonical 256 KiB limit, but the underlying LSP framing layer decodes an inbound
message before that retention limit applies. The optional tool projection is
separately bounded to 128 entries and 512 KiB of retained names/descriptions;
the optional module-interface projection is bounded to 256 entries and 512 KiB
of retained paths/descriptions. Their inbound LSP messages are likewise decoded
first. Do not expose its stdio
transport to a hostile peer or describe it as an IPC resource sandbox; place a
separate bounded transport or operating-system boundary in front of such a peer.
octoscript-protocol defines the portable, attenuated handoff from a policy host
to a contained worker. It validates manifests, request uniqueness, formats,
byte limits, and call budgets. Its SessionAuthenticator can also bind each
worker frame to a host-provisioned BLAKE3 session key, directional role, and
strict sequence number, rejecting tampering, reflection, and replay before a
message is used. Its private-pipe bootstrap can carry that already-generated
key and session ID once to a newly launched worker, but it does not generate or
protect the key, establish an exchange, attest a worker, encrypt transport, or
enforce an operating-system policy itself. The host must provide a trusted
bootstrap channel and containment backend before an effectful adapter is
considered contained.
octoscript-worker is the worker-side implementation of that protocol boundary.
It accepts only an explicitly registered Rust adapter for a granted capability,
requires host admission to bind a fresh authenticated session to its tenant
journal scope, and persists durable intent before an adapter effect through a
monotonic compare-and-swap journal revision and a current host-issued fencing
lease. The scope is host-selected and the admission boundary must validate the
session/scope tenant binding together; a store rejects leases from superseded
workers. It restores its in-memory journal and poisons the session when
persistence fails; a post-effect failure also returns an indeterminate error
and requires a fresh authenticated reload before bounded reconciliation or
adapter-specific recovery. A duplicate pending operation remains pending and
an exact durable key is bound to its canonical tool input, so changed input
fails closed. It does not isolate adapters: embedding it in the interpreter
process, a mobile app, or an unrestricted service leaves it with that process's
ambient authority. Its journal-store trait is a contract, not a file/database
implementation or anti-rollback mechanism.
AuthenticatedWorkerJournalStore connects the runtime to
AuthenticatedStore plus FencedRollbackProtectedStore. It authenticates the
journal bytes and binds each write to a record key, current revision, scope,
and fencing token, but it inherits the backend's durability and anti-rollback
guarantees. VolatileMemoryStore covers integration tests only and must never
be treated as a production worker journal backend.
The bridge derives its record key from a host-owned namespace and journal
scope. A new admission must atomically reserve a nonzero token through the
backend or an equivalently durable lease service; never calculate a token from
current_fence + 1 after a separate read. Fence state and data must share the
same record key, and the backend must revalidate exact token equality in its
atomic compare-and-swap. Provision an AuthenticatedStore key only to the
trusted storage coordinator or a narrowly scoped storage client. Do not place
a general storage key in an untrusted contained adapter process merely because
that process hosts a worker session. The bridge intentionally exposes no
general-purpose authenticated-store handle or caller-selected record key. The
host admission authority must reserve a fence for this exact bridge record;
the runtime cannot infer a record binding from a raw u64 token.
The optional AnchoredSqliteStore persists payload candidates locally but
accepts them only when a host RollbackAnchor has committed their revision,
content hash, and fence. SQLite is not that anchor and does not become one by
being opened with durable settings. A real anchor must be linearizable and
survive both its own failover and local-database rollback. All writers must
share that anchor and one SQLite file. Host recovery after an anchor outage
must stop new admissions, reserve a fresh opaque recovery fence, then discard
only unanchored candidates through the backend API. Never recover by deleting
the SQLite file or by rebuilding anchor state from it.
RollbackAnchorService<A> is only a bounded, canonical protocol dispatcher
around a host-owned RollbackAnchor. AuthorizedRollbackAnchorService<A, Z>
can require Z to authorize an already authenticated caller's exact operation
and record before the backend runs; its fixed policy has no wildcard or
implicit write grants. Neither type authenticates a caller, listens on a
socket, serializes concurrent callers, selects tenant scope, or improves A's
durability. A network service must enforce those boundaries outside the handler
and return generic failures without backend details. A volatile or rollbackable
backend remains unsuitable after being wrapped by either dispatcher.
The optional octoscript-storage keyring feature retrieves a host-provisioned
32-byte storage key from native credential stores on macOS, iOS, and Windows.
It reads an existing binary credential only, rejects unsupported targets rather
than using an in-process mock, and never creates, rotates, or deletes platform
credentials. On supported targets it invokes the explicit native credential
implementation rather than keyring-rs's process-configurable default builder.
Credential storage protects key material; it does not provide the linearizable,
rollback-resistant compare-and-swap required by RollbackAnchor. Do not use it
as evidence that local payload storage is rollback protected.
Worker adapters must explicitly declare read-only/idempotent safety before the
non-durable invoke path is enabled, and a bounded reconciliation contract
before durable dispatch or compensation is enabled. A declaration is a trusted
Rust-code review obligation, not proof of external exactly-once behavior. A
durable adapter must recover status by the host operation key and pass that key
to a provider idempotency mechanism when one exists. The runtime's ordering
prevents a duplicate adapter invocation for an existing journal key, but it
cannot make an external provider idempotent or queryable.
ProtocolWorkerClient connects that validation layer to a host-owned
WorkerTransport; its registration rejects a local policy that is broader than
the worker grant. This still does not make an in-process transport isolated.
The optional InProcessAuthenticatedWorkerTransport authenticates every
ordinary worker invocation in-process, but it is only suitable for a static,
trusted mobile or embedded adapter catalog. It confers no OS, memory, process,
or resource containment; the adapter retains all authority of the embedding
application. Do not use it to run untrusted local-tool workloads.
mobile::MobileRuntimeBuilder is a narrower direct-adapter profile for mobile
and embedded hosts. It accepts only app-provided local adapters during setup;
consuming build() yields a runtime with no registration, external claim,
external completion, or worker-transport API. JSON adapters must carry an
executable input/output JsonToolContract, so structured script data remains
validated at the Rust boundary. This seals the catalog exposed through that
profile, not the embedding application's Rust authority: a host can still
choose a lower-level runtime, and every registered adapter retains the app's
ambient authority. It must not expose an arbitrary executable, filesystem,
network-origin, plugin, or crate selector. collect_garbage() is host-scheduled
and may cost time proportional to the live VM heap; it is not a per-pump
resource limit or containment mechanism. An HttpEndpointCatalog registered
through the feature-gated builder is consumed before sealing, so generated
source cannot add an endpoint, change its URL, method, headers, query, or
redirect behavior, or select an endpoint credential. It remains in-process API
mediation, not mobile or embedded operating-system containment. A FixedFileCatalog registered
through the builder is consumed before sealing and has the same opaque-ID,
descriptor-pinning, and mutable-content limitations described above.
octoscript_workflow::mobile::MobileWorkflowBuilder applies the same static local
adapter rule to host-owned workflow execution. Its sealed result can create
plans from trusted steps or data-only drafts, approve only named per-step
policies, checkpoint, and execute. It intentionally does not expose the
underlying CapabilityRuntime, manual lease issuance, full-catalog approval,
external-operation APIs, or mutable registration. This prevents the facade
from widening its own catalog after setup; it does not constrain a host that
deliberately chooses lower-level APIs or reduce the ambient authority of a
registered Rust adapter.
The optional JSON-line worker channel carries one bounded authenticated frame at a time over host-provided I/O. It limits a line to 1 MiB before decoding and poisons the channel after any write, flush, read, decode, size, or framing failure; the authenticated call transport likewise poisons itself after an invalid or unexpected worker response. A host must discard that session rather than retrying on the stream. This is protocol robustness, not containment: the host still owns trusted key provisioning, cancellation semantics, child lifecycle, and the platform sandbox that restricts the worker's OS authority. The optional Bubblewrap watchdog can enforce host-selected wall-clock force stops for one synchronous transport invocation and for a whole worker session measured from spawn, but it is not an authenticated cancellation acknowledgement or effect-recovery decision.
Protocol v5's optional multiplexed JSON-line path is separate from that
synchronous transport. It admits one active ordinary invocation and one exact
authenticated cancellation request, with independently owned directional
authentication state. The worker path accepts only adapters explicitly
registered as CancellableWorkerAdapter; it keeps frame processing outside the
adapter thread and sets a cancellation token only after request authentication
and reauthorization. A positive acknowledgement is valid only when the adapter
has stopped its effect and guarantees no result follows. too_late requires a
validated ordinary result first, while unsupported keeps the call active.
These are trusted Rust adapter contracts, not properties asserted by Octoscript
source or inferred from a process exit.
SupervisedMultiplexedWorkerSession additionally requires its transport and
lifecycle supervisor to name the same session, arms the deadline before
dispatch, and resolves the watchdog race before exposing a terminal event. A
deadline, forced stop, EOF, authentication failure, or transport error poisons
the session and remains indeterminate. Only an authenticated positive
acknowledgement whose supervision completed normally may drive the runtime's
two-phase cancellation confirmation. The workflow integration applies events
through WorkflowEngine, not runtime_mut(), so retained-step state cannot be
bypassed accidentally.
octoscript-sandbox::bubblewrap is the first such platform sandbox integration.
It accepts only a fixed host-selected worker executable and fixed arguments,
constructs a fresh Bubblewrap mount namespace, clears the worker environment,
creates a new session, binds the worker to its parent lifecycle, and mounts
only read-only runtime paths plus active manifest-selected host-backed or
bounded ephemeral file_root entries. It uses --unshare-all and never emits
--share-net, so it does not retain the host network namespace. It also emits
--cap-drop ALL unconditionally, including when Bubblewrap is launched by
root, so the worker cannot retain Linux capabilities needed to undo mount or
namespace policy. It rejects executable and secret selectors, and rejects
every network_origin selector unless trusted host setup supplies the optional
exact Linux network broker described below. It also rejects overlapping or root
mounts and requires the worker program to live in a read-only runtime mount,
avoiding a writable grant as an executable source.
Hosts that explicitly select
require_no_further_user_namespaces also get Bubblewrap's mandatory
--unshare-user --disable-userns sequence, which prevents the worker from
creating further user namespaces. That mode has no compatibility fallback and
will fail on unsupported, setuid, or user-namespace-restricted hosts; it does
not mean Bubblewrap never created an internal nested namespace.
With the Linux-only octoscript-capabilities/linux-network-broker feature, the
host can derive one exact NetworkOriginAccess set from the compiled manifest,
bind a reviewed fixed-endpoint or exact-origin HTTP catalog to it, and install
the returned LinuxNetworkBrokerMount into that same policy. The broker creates
a CSPRNG-named 0700 directory containing exactly one 0600 Unix socket. The
policy requires descriptor-pinned mount sources, checks that exact directory
shape and that the socket and directory share an owner, requires the catalog
authority to match the manifest's distinct opaque IDs exactly, and mounts that
directory read-only into the worker. The worker still receives no host network
namespace; within this broker path, the host broker outside the sandbox is the
only component that resolves a configured secret or opens the reviewed HTTP
connection.
The private socket is not a general local authorization protocol. Its
filesystem ownership and parent directory remain trusted host setup, and any
process that can legitimately access the socket can issue requests within its
aggregate session catalog. Descriptor pinning retains the selected directory
identity through launch but cannot freeze mutable descendants, so trusted host
setup must retain exclusive control of that directory. Separately selected
runtime and file-root mounts are independent trusted policy decisions and can
expose their own Unix sockets; hosts requiring broker-only IPC must exclude
them. A reviewed LinuxNetworkBrokerClient rechecks the active worker grant
before connecting, and the host catalog rechecks its exact identifier set, but
those are not OS per-tool separation. A worker process has the union of its
manifest's network origins. Hosts needing finer isolation must run separate
attenuated sessions with separate broker directories. The broker does not
expose raw TCP/UDP/DNS, arbitrary Unix sockets, catalog discovery, credentials,
or a general proxy; it only carries one bounded catalog request per connection.
It also does not make a POST durable or idempotent.
BubblewrapCommand::spawn_with_bootstrap additionally checks that the private
bootstrap session matches the compiled manifest before launch, then writes the
versioned preamble to the dedicated child stdin pipe. A failed write kills and
reaps the child. This avoids exposing the key in argv or environment variables,
but it is transfer only: it does not provide key exchange, encryption,
attestation, or key storage.
For Linux deployments with a host-owned delegated cgroup-v2 parent,
CgroupV2Policy can be used with BubblewrapCommand::spawn_in_cgroup or
spawn_with_bootstrap_in_cgroup. The policy creates a fresh child, applies
selected cpu.max, memory.max, memory.swap.max, pids.max, and per-device
io.max controls, and starts a fixed host-side runner. The runner moves itself
into that child before it executes Bubblewrap. Octoscript observes the direct child
in cgroup.procs before it returns a managed worker handle, so lifecycle
teardown cannot race a runner that has not yet joined the cgroup. The cgroup
path and I/O device identifiers are never Octoscript values, worker protocol
fields, or Bubblewrap arguments.
The host must enable and delegate the required controllers under a dedicated
parent before launch. Octoscript verifies the parent is mounted from cgroup v2 and
deliberately does not modify
cgroup.subtree_control, because changing a shared parent can affect unrelated
workloads. The policy fails before launch when a selected controller or
cgroup.kill is unavailable. The runner is trusted host code, must remain
immutable to untrusted actors, and is not mounted into the worker runtime.
For a managed cgroup-backed worker, explicit termination, watchdog expiry, and
bootstrap failure call cgroup.kill before reaping the direct Bubblewrap
process. This covers the worker cgroup subtree, including descendant forks,
where Child::kill alone would not. A cgroup cleanup or kill failure is a
containment failure, not a successful cancellation result. memory.max is a
memory-cgroup boundary rather than an RSS-only metric; Octoscript additionally sets
memory.oom.group=1 when it selects that control. memory.swap.max=0 prevents
anonymous memory in the worker cgroup from being swapped out. io.max bounds
selected BPS and IOPS classes for one trusted block-device major:minor
identity, but it is not a filesystem quota. pids.max counts tasks, including
threads, and cpu.max is CPU
bandwidth rather than a wall-clock deadline. The Linux cgroup v2 documentation
defines the kernel semantics.
Hosts may additionally select the typed
WorkerSeccompProfile::DenyKnownEscapeSurface. Octoscript generates a fixed cBPF
program and transfers it over an anonymous launch-only descriptor to
Bubblewrap, which consumes and closes that descriptor before it attaches the
filter immediately before worker execution. The profile verifies the syscall
ABI, kills an x86-64 x32 ABI attempt, rejects mount and namespace construction,
known kernel-control interfaces, tracing/cross-process-memory calls, keyrings,
personality, and TIOCSTI. Bubblewrap requires no_new_privs before it
installs this filter, so a worker can add only stricter seccomp constraints. It
is intentionally default-allow for dynamic-worker
compatibility: it permits execve, does not constrain arbitrary future or
unlisted syscalls, and is neither a capability mechanism nor a complete
syscall sandbox. The profile can return ENOSYS for clone3 to force a legacy
clone fallback with namespace flags checked, which may be incompatible with
a particular worker. See docs/linux-bubblewrap.md
for the exact supported architectures, denied operations, and limitations.
For a fixed worker whose target ABI and runtime have been independently
reviewed, a host can instead provide a bounded WorkerSeccompAllowlist through
set_seccomp_allowlist. This selects WorkerSeccompProfile::StrictAllowlist:
Octoscript keeps the ABI/x32 and fixed escape-surface guards, returns ALLOW only
for listed syscall numbers, and kills every other syscall. An empty, duplicate,
oversized, or missing list is rejected rather than falling back to default-allow
filtering. The list is trusted host configuration, never Octoscript source, worker
input, LLM output, or caller-provided cBPF. Policy compilation rejects a list
without the required execve; the host must additionally cover any fixed
resource-limit runner and the exact worker runtime. With
LandlockExecutableRunner, Octoscript does not give the strict program to
Bubblewrap: it gives the fixed runner a bounded compiler-generated encoding,
which the runner installs only after its fully enforced Landlock ruleset and
descriptor cleanup, immediately before the fixed inner exec. This internal
handoff is not Octoscript source, worker input, manifest data, or caller-provided
cBPF in the Bubblewrap policy API, and launch has no direct-worker or
unfiltered fallback. It is still a
syscall boundary only: because execution must normally remain available, it
does not mediate an executable path, a network origin, device access, secrets,
or capability grants.
LandlockExecutableRunner is an optional Linux-only defense-in-depth boundary
for exact filesystem-backed executable targets. The host configures a distinct
read-only runtime runner path; Octoscript adds the fixed worker, an optional limit
runner, and bounded explicit additional paths, then the bundled
octoscript-landlock-runner installs LANDLOCK_ACCESS_FS_EXECUTE rules with a
hard Landlock compatibility requirement before it starts the inner command. It
rejects unsupported platforms at compilation and unsupported or incomplete
kernel enforcement at startup rather than falling back to direct worker
execution. A dynamically linked inner worker or resource-limit runner also
needs its resolved regular ELF loader listed as an explicit target. Its rules
are inherited by worker descendants. In descriptor-pinned mode Octoscript also overlays the Landlock runner
and every explicit allowed target from retained descriptors, preventing path
replacement after compilation from changing those selected files.
Do not describe this as a complete executable or code-loading sandbox. It does not control dynamic-loader reads, shared libraries, plugins, bytecode engines, JITs, networking, device access, secrets, capability grants, special filesystems, or already-open descriptors. An allowed interpreter can run code it reads. Treat the Linux Landlock documentation as the source of kernel semantics and layer immutable runtime ownership, mount/descriptor isolation, cgroups, and a suitable syscall policy around it.
An optional host-configured octoscript-limit-runner can execute the fixed worker
only after applying selected Linux rlimits and disabling core dumps. The runner,
limits, worker target, and worker arguments are all compiled from trusted Rust
policy; Octoscript source and tool data cannot control any of them. It must be a
distinct executable in a read-only runtime mount, and a setup or exec failure
does not fall back to direct worker execution. The host still must reject a
failed authenticated worker startup because Bubblewrap spawn alone does not
prove that the runner applied its limits.
Before it executes the worker, the bundled runner marks every descriptor from 3 onward close-on-exec. The launcher's standard streams are explicitly configured as private input/output pipes and null stderr, so this prevents a nonstandard host descriptor inherited through Bubblewrap from becoming worker authority. It does not make the standard streams secret: the worker protocol and host logging policy must still treat their contents as sensitive.
The optional rlimits remain narrow per-process controls, not a replacement for
the cgroup profile: CPU is cumulative time, address space is virtual memory,
open files are file descriptors, and file size is per created file.
RLIMIT_NPROC is per-real-UID thread accounting, can include unrelated
processes, and is not enforced for real UID 0 or a process with
CAP_SYS_ADMIN or CAP_SYS_RESOURCE. Hard limits prevent an unprivileged
worker from raising them, but a process with CAP_SYS_RESOURCE in the initial
user namespace can do so. Do not describe rlimits as a worker-tree process
limit, RSS ceiling, aggregate disk quota, wall-clock deadline, seccomp policy,
cancellation mechanism, or complete sandbox. Use a cgroup policy and dedicated
non-root sandbox identity where the available cgroup properties are required.
RLIMIT_CPU does not bound a sleeping or blocked worker. A host using the
optional BubblewrapWorkerWatchdog through BoundedWorkerTransport can arm a
nonzero trusted wall-clock deadline for one synchronous transport invocation.
BubblewrapWorkerSessionDeadline can separately force-stop a whole worker
session from its spawn time, including idle time. The watchdog owns the child
in a separate host thread, force-stops and reaps it on either expiry, and
treats a response race as indeterminate. It is not authenticated in-band
cancellation and does not establish whether an adapter effect occurred. A host
that does not use the watchdog must independently schedule lifecycle
termination on a monotonic timer, discard the session, and reconcile any
durable effect. The runner does not implement that timer or a worker
cancellation acknowledgement.
The optional multiplexed ordinary-call transport can send a protocol v5
cooperative request while this same watchdog remains armed. This does not
change watchdog semantics: only the worker adapter's exact authenticated
acknowledged disposition is cancellation proof. A deadline or process-tree
kill without that disposition is still indeterminate. Durable operation,
compensation, and reconciliation frames do not acquire in-band cancellation
through this path.
An explicit private /tmp and each active EphemeralFileRoot can have a
Bubblewrap --size allocation ceiling. Each ceiling bounds aggregate
data-block allocation in that one tmpfs mount, not its independent inode or
directory-entry count. Ephemeral roots are empty on worker startup, disappear
with the mount namespace, and may consume memory or swap; they must not hold a
durable journal or effect record. Multiple roots have independent ceilings,
not a shared session budget. None of these limits is a process-memory, CPU,
process-count, or persistent-filesystem quota.
The tmpfs mounts are nosuid,nodev, but Octoscript does not claim they are
noexec: a compromised worker can write executable content into an ephemeral
root and invoke it when the runtime and syscall policy allow. Denying an
executable capability selector prevents generated source from selecting a
host command; it does not mediate native execve calls inside the worker.
Active host-backed read-write roots are rejected by default. Host code can
explicitly allow one only when it already enforces an independent persistent
storage quota; Octoscript cannot validate that quota.
require_bounded_file_root_writes adds rejection of an enabled unbounded
private /tmp, non-recursive read-only remounts of the empty namespace root,
/proc, and /dev after all selected submounts are created, and mandatory
further-user-namespace lockdown. It overrides the explicit unbounded-write
acknowledgement. Without that lockdown, a worker could reacquire
namespace-scoped mount authority after capabilities were dropped. Device and
proc interfaces retain their kernel-defined semantics, and the mode does not
constrain downstream adapter effects. After transport
pipes move out of the startup handle,
BubblewrapWorkerLifecycle::terminate force-terminates and reaps the worker. It
is process control only: the host must drop the session and reconcile a durable
effect rather than infer that process termination cancelled or rolled it back.
Bubblewrap is a low-level sandbox constructor, not a complete security policy.
This backend has no portable aggregate quota for persistent host-backed storage,
no device quota, per-origin network proxy, D-Bus mediation, complete executable
or code-loading policy, direct secret-selector handling, or universal
cancellation for arbitrary or durable adapters. Linux generic project quotas
are an opt-in exception only for a descriptor-pinned directory on a supporting
filesystem and Linux 5.14-or-later kernel. A selected quota root requires the
mandatory further-user-namespace lockdown, which prevents a worker that owns
the root from changing the project ID or inheritance state through Linux
filesystem-attribute ioctls in the initial user namespace. Octoscript checks the
provisioned project ID, inheritance bit, nonzero hard block and inode limits,
current usage, configured per-root ceilings, and aggregate distinct
(filesystem, project ID) hard limits before launch. The filesystem, not
Octoscript, enforces those limits after launch. The host must prevent a privileged
quota administrator from raising, disabling, or retagging the project while
the worker is active.
Project quotas do not constrain process memory, device access, network,
execution, data outside that project, adapter effects, or non-Linux targets.
Its separate endpoint-bound secret broker is not a general credential or
worker-secret delivery mechanism. Protocol v5 can
layer an exact ordinary-call request over its private pipes only for reviewed
cancellable adapters. Its optional strict allowlist is a target-specific
syscall boundary, not a replacement for those missing controls. The optional
octoscript-workflow/bubblewrap-recovery coordinator adds a narrow post-exit path:
it requires a session-bound reaping proof, reloads a fenced authenticated host
ledger, uses a differently keyed least-privilege contained session for one
bounded reconciliation, reaps that session, and compare-and-swap persists the
observation. It does not report cancellation, redispatch an effect, choose
compensation, implement the worker journal, or resume a workflow. The optional
watchdog supplies only trusted wall-clock process stops
described above, its optional runner provides only the narrow rlimits described
above, and its cgroup profile supplies only the CPU, memory, swap, task, and
selected per-device I/O controls described above. The per-device I/O control is
neither a filesystem quota nor a guarantee that buffered writeback will be
attributed to the worker on every filesystem.
DenyKnownEscapeSurface provides only the fixed default-allow hardening
described above. StrictAllowlist kills unlisted syscalls but does not replace
an executable-path or capability policy, and normally has to retain the
worker's initial execution syscall. A private /tmp is opt-in and
unbounded unless the host selects its explicit Bubblewrap size limit; active
ephemeral file roots are always bounded individually. Those ceilings do not
replace a cgroup memory policy or persistent-filesystem quota. The filesystem
boundary is per worker session, not per individual invocation: an attenuated
manifest should be narrowed before launch when per-call filesystem isolation
is required.
Policy source paths must be host-owned and immutable to untrusted actors from
compilation through worker exit, including their executable and symlink
targets; the current path-based launcher cannot eliminate that race. A fixed
worker program also does not prevent a compromised worker from executing or
reading other files deliberately exposed through a runtime mount; runtime
mounts must remain minimal and immutable. On a failure it does not fall back to
an unrestricted worker. See
docs/linux-bubblewrap.md before enabling it for
untrusted local effects.
Each registered tool declares a stable identifier and limits for calls, input bytes, and output bytes. Calls are recorded in an ordered audit log. Unknown, over-budget, over-depth, or malformed calls fail before a tool handler is invoked.
The in-process audit view retains only its configured recent entries (1,024 by
default, 8,192 maximum) and exposes an eviction counter. It is deliberately
bounded so a long-lived untrusted-script host cannot grow memory through
observability alone. audit_since(cursor) exports only a contiguous retained
range, ordered by a distinct per-record event_sequence; it rejects a cursor
overtaken by eviction or clear_audit rather than silently returning a partial
history. The older sequence field correlates one invocation and can repeat
across retries, cancellation, or streaming, so it is not an export cursor.
Hosts that need complete retention must surface an export gap and use a
separate authenticated durable sink. The optional
octoscript_capabilities::durable_audits::CapabilityAuditStore supplies one
bounded sink for runtime-exported batches: it validates the data-only audit
shape, requires contiguous source sequences, deduplicates exact retained
overlap, rejects retention gaps and conflicts, and writes through the supplied
rollback-protected store's compare-and-swap boundary. Its 1,024-event and
192 KiB limits are independent of the in-memory view. VolatileMemoryStore
is development-only; an ordinary key-value store that cannot prevent rollback
does not meet this contract. A retained view, its sequences, its loss counter,
and a successful export are not an authorization decision, durable record,
effect proof, or permission to resume a workflow. See capability audit
export.
octoscript_workflow::durable_events::WorkflowEventStore provides one bounded
authenticated workflow-telemetry journal for host-owned operator/audit replay.
It accepts only contiguous engine-exported sequences, rejects source gaps and
contradictory retained overlaps, records retention eviction explicitly, and
uses the supplied rollback-protected store's compare-and-swap boundary. The
journal includes no source, tool payload, approval, grant, worker key, or VM
promise. It is not a workflow checkpoint, operation ledger, effect proof,
cancellation acknowledgement, or permission to resume a workflow. A host must
still use fresh approval, idempotency, and authenticated reconciliation for an
external effect.
Registered tool names are restricted to 128-byte lowercase ASCII capability identifiers. A denied call can still carry an arbitrary dynamic Octoscript string, so the audit view preserves only a fixed-length, session-scoped BLAKE3 label for invalid or oversized unrecognized names. It does not retain that raw script value. The label is a correlation aid, not a credential or a secrecy guarantee against a host that already knows the candidate value.
A denied or failed tool call can transfer control to a script catch branch,
but reservation, budget, and audit decisions remain final. A fallback tool call
is separately authorized and charged. Handler failure is not evidence that an
effect was rolled back; ambiguous or durable effects still require host-owned
idempotency, reconciliation, and compensation policy.
JSON capabilities are an explicit policy type. They accept only JSON object or
array envelopes: envelope validation happens before the Rust handler is called,
and before a result is returned to Octoscript. JsonToolContract adds an
executable, bounded schema subset at the same boundary. Input contract failure
does not invoke a handler or consume a call; output contract failure does not
reach Octoscript. This is a data contract, not a way to deserialize arbitrary Rust
types or grant a script access to a crate. The typed Serde bridge requires a
JsonToolContract and validates that contract before input deserialization and
after output serialization; a Rust struct is never the authoritative policy.
Host-pump deferred tool promises are bounded per runtime and run only when the
trusted host calls CapabilityRuntime::pump; one default pump tick processes
at most one tool. Hosts may choose a bounded batch with pump_up_to. They
are cooperative scheduling, not a threading or isolation mechanism. A paused
script with no runnable capability work must be resumed by a host that
understands the relevant suspension source. A settled promise record remains
until it is unreachable and the trusted host calls
CapabilityRuntime::collect_garbage() at a suitable idle point. Collection is
not implicit in pump() because a full VM sweep can take time proportional to
the live heap.
External-only tools add a host-managed completion path. They have no in-process handler and are denied to synchronous calls. A trusted host claims each operation, then explicitly completes it or uses a two-phase cancellation-request/adapter-acknowledgement path; the runtime reuses the normal output validation and audit boundary. A request leaves the promise pending and blocks retries and stream forwarding. Confirmation is rejected until a request exists, but the trusted host remains responsible for deciding whether an adapter acknowledgement is credible. This does not terminate a worker or enforce an operating-system policy. A force-stop is indeterminate and must not be reported as cooperative cancellation without separate proof.
The multiplexed worker bridge keeps ExternalToolId, external input, and the
runtime cancellation identity on the host. The wire request repeats only its
own control ID and the exact already-authorized session, invocation request,
and tool. ExternalToolWorkerBinding rejects drift in host ID, tool, call
index, attempt, or idempotency key before sending. It accepts only one request
per target. A result-wins race is not exposed until result and too_late
have both authenticated in required order. An acknowledged race suppresses the
ordinary result. The host must still treat the contained worker and reviewed
adapter implementation as part of the trusted cancellation contract; keyed
framing authenticates the session and ordering, not semantic honesty.
External retries are also host-only. A script receives no retry API and cannot
spend another capability call by requesting another attempt. For each claimed
operation, the host may use its stable idempotency_key when forwarding an
attempt to a worker. The key includes a runtime-session nonce sourced from
operating-system entropy when available, with a process-local time/PID fallback
otherwise, so normal new host processes do not reuse the old counter-only value.
It is a correlation and deduplication value, not a capability token, authorization
credential, or durable operation identity; the opaque ExternalToolId must
remain owned by the host. Hosts that need replay across restarts must persist
their own workflow identity and authenticate every worker request with the
keyed protocol frame or an equivalent transport mechanism. An adapter must not
retry a non-idempotent effect unless its worker performs deduplication using
that key or an equivalent durable identity.
Authenticated reconciliation can query a live claimed operation without
serializing its ExternalToolId. CapabilityRuntime creates an authenticated
request carrying only the session, tool, request ID, and operation key, then
opens a matching worker response before it applies running, succeeded,
failed, or cancelled. The result must match both that request and the
currently claimed operation; a successful payload also passes through the
existing output limit and JSON-contract boundary. This does not make a
promise, operation handle, or VM state restartable. A durable host workflow
must persist and authenticate its own operation identity, then decide whether
to reconcile, retry, compensate, or fail before it constructs a fresh runtime.
The optional JSON-line OneShotAuthenticatedOperationWorkerTransport provides
one separately authenticated durable dispatch, reconciliation, or compensation
exchange after a host has opened a fresh contained-worker session. It validates
the active manifest, request identity, and result before returning, then is
consumed; a failure poisons it. This bounds the transport-level recovery
attempt, but it does not restart a worker, provide durable storage, prove an
effect's outcome, acknowledge cancellation, approve output, or resume a
workflow. The host must restore and authenticate both its ledger and the
worker journal, choose recovery policy, persist the verified observation, and
issue fresh approval before it can run any later workflow work.
The optional octoscript-workflow/bubblewrap-recovery integration owns the narrow
Linux composition of those steps for reconciliation only. It accepts a proof
that the old Bubblewrap session was reaped, generates a new session key without
fallback, requires one exact-tool manifest, reserves a durable writer fence,
starts and later reaps a watchdog-bounded fresh worker, and persists the bound
observation through authenticated fenced compare-and-swap. It returns the
authenticated result only to trusted host code and redacts it from Debug; the
ledger stores only lifecycle state. A terminal result is still not approval to
resume, and any transport, deadline, cleanup, fence, or compare-and-swap race
discards the observation.
An external tool may opt into bounded host-visible output chunks. The runtime accepts chunks only for a claimed operation, applies source-byte, aggregate, and post-redaction limits, and returns only the redacted text to the host. Chunks are not installed as a Octoscript API or buffered as script-visible state. A redactor is trusted host Rust code, not generated script code; it must remain small and non-blocking, and it cannot substitute for a contained worker or output validation by the receiving UI, log, or LLM adapter. Stream limits span all retries of the same operation, so a retry cannot reset an output budget. The redactor is frozen once the tool reserves its first call, preventing a mid-operation configuration change from altering the release policy.
Hosts can set a deferred deadline on each tool policy. Expiration is enforced before a queued host-pump handler begins and through CapabilityRuntime::expire_timed_out_tools for external work. It cannot stop a Rust handler that is already blocking; effectful adapters still need their own I/O deadline and containment policy. A result delivered after the deferred deadline is rejected as timed out.
One v0.1 runtime is single-flight: a host must resume or discard a suspended evaluation before submitting new source to that runtime instance. Hosts that need independent concurrent workflows should use separate runtime instances.
Workflow plans are approved by the Rust host. An approval is bound to one plan
and consumed by execution, so a script cannot manufacture approval for another
workflow or resume a rejected plan by mutating its own state. Plans are also
bound to their creating workflow engine; another engine cannot approve,
checkpoint, or execute a foreign plan object. Each plan or resume approval
also carries process-local CapabilityLease authority tied to the originating
runtime and its exact serialized tool-catalog fingerprint. A lease lists
allowed tool names and non-widening call budgets; every tool.call,
tool.start, JSON variant, and dynamically computed name is checked when the
host reserves it. The lease remains active across await and continuation,
and the host rejects tool registration while it is active. A catalog change
after approval causes the lease to fail closed before execution or resume.
For least-privilege LLM workflows, the host can instead approve an ordered
lease queue with approve_with_step_capability_leases. It supplies exactly one
lease per trusted step, and the engine activates only the current lease. An
early step therefore cannot use authority assigned to a later step, including
while it is waiting on an external operation; the current lease is retained
until that step resolves. The checkpoint-resume variant accepts exactly the
remaining suffix, so completed-prefix authority is not renewed after restart.
This queue enforces host-reviewed ordering only: it does not infer correct
grants from generated source, tool-call hints, or step names. The optional
synchronous ToolCallAuthorizer can further deny an already leased invocation,
but cannot add authority. This is script-level authority control, not adapter
containment: a permitted Rust adapter still needs an appropriate
contained-worker boundary before it can safely process untrusted local-tool
work.
WorkflowStepCapabilityPolicy is a host-only convenience for the common case
without a custom authorizer. Its ordered step IDs and grant lists are checked
against the trusted plan before approve_with_step_capability_policies or its
resume counterpart issue any current-runtime lease. A policy intentionally has
no serialized form and cannot invoke a tool; it is configuration, not
authority. Hosts must build it from their own policy decision, never directly
from generated source, a checkpoint, or review hints. A host that needs a
ToolCallAuthorizer issues a manual lease instead.
WorkflowPlan::review is an effect-free presentation aid for that host
approval flow. It returns per-step canonical syntax reports and direct tool
call hints, but creates no runtime, lease, or approval. An empty hint list is
not authority and does not prove a step is pure: invalid source, aliases,
control flow, and computed names deliberately remain outside its scope.
Workflow plans are capped at 1,024 steps and 1 MiB of aggregate source before
the engine retains them, limiting generated-plan review and lease-queue growth.
WorkflowDraft is a separate bounded untrusted input format for an LLM's
proposed step list. Its JSON envelope accepts only a format version plus step
IDs and source, rejects unknown fields, caps wire data at 2 MiB, and bounds
the decoded step collection before it can become a plan. Parsing and reviewing
it do not create a capability runtime, grant, lease, approval, checkpoint, or
operation handle. plan_draft records planning only; the host must still
select grants from trusted policy and separately approve execution. Review
hints remain non-authoritative, including for dynamic names.
WorkflowEngine likewise retains only a configured recent in-memory event
view (1,024 by default, 8,192 maximum) and exposes its eviction count. Those events are
operational telemetry only; failure events retain diagnostic counts but never
diagnostic text. They must not be used to replay a workflow. Checkpoints and
operation ledgers remain the separate bounded data-only recovery records, each
requiring fresh host approval and authenticated storage.
Workflow checkpoints are bounded data-only records of a completed step prefix. They bind to the ordered trusted plan through a BLAKE3 fingerprint, but include no approval, grant, VM state, output, promise, or external operation handle. Loading one cannot run a workflow: the host must recreate the plan and current capability policy, authenticate its durable storage, and explicitly issue a fresh checkpoint-bound approval. A checkpoint is not proof that its prefix ran or that an interrupted step is safe to replay; hosts must use idempotency, reconciliation, or compensation for effects around the restart boundary.
WorkflowOperationLedger records durable external-operation intent separately
from a checkpoint. Each record is bound to the trusted plan and retains only a
tool, step, non-authorizing operation key, input digest, and worker-observed
state. Reconciliation request construction requires the exact current input
bytes and fails closed when their digest differs. The preferred derived key
also binds the plan, step, tool, input, and host-supplied durable nonce, so a
host should not reuse it for a different logical effect. A ledger revision is
only a host-facing compare-and-swap or watermark hook: it does not authenticate
storage, prevent rollback by itself, validate a worker output, restore a VM
promise, or authorize a workflow restart. Those decisions remain with an
authenticated storage backend and fresh host policy.
The persisted input fingerprint is an unkeyed correlation digest, not encrypted
secret storage; hosts must pass opaque secret selectors rather than credential
values into the ledger identity.
An operation ledger may hold one compensation intent only after its original
operation is durably succeeded. That intent binds a separate cmp- key,
canonical-input digest, tenant scope, and active capability-grant fingerprint;
it never stores raw compensation input, output, an approval, or a grant.
Compensation approvals are process-local, one-use, session-bound host values.
They must be issued only after the intent is durably persisted and must be
reissued after a restart for the exact same record. A changed tenant, key,
input, or grant fails closed. The ledger cannot prove an inverse effect is
semantically correct or automatically restart a workflow.
CompensationGrantVerifier is invoked by the workflow host before approval
and again before frame sealing, so a production host must connect it to current
tenant policy, revocation, and any grant-lease state rather than treating a
stored fingerprint as a still-valid capability.
octoscript-storage authenticates host-owned record bytes with a provisioned
BLAKE3 key and binds them to an opaque record namespace, name, revision, and
key ID. It supports verification-key rotation, but it does not encrypt payloads
or generate, transfer, or protect storage keys. Its RollbackProtectedStore
trait is deliberately strict: an implementation must atomically return a
record with its durable revision floor, and atomically advance that floor with
a successful compare-and-swap. The included VolatileMemoryStore is only a
process-local test/development implementation, not a durable backend. A file,
database, or mobile key-value adapter must not claim rollback protection unless
it has a separate platform trust anchor and the required atomic semantics.
Generated Octoscript source receives neither a store nor a key.
Worker protocol v5 provides authenticated ordinary-call cancellation,
operation-dispatch, and explicit compensation frames. Cancellation is an
ephemeral adapter contract; durable effects still rely on the journal. A
contained worker's WorkerOperationJournal records
the tool, key, canonical-input digest, state, and at most one compensation
record before its adapter runs an effect. A compensation is admitted only for
a succeeded original operation under the same tool and tenant scope, with an
exact active-grant fingerprint and a separately bounded nonzero compensation
grant. An exact duplicate returns the stored compensation state; a changed
tool, key, input, grant, scope, or contradictory terminal result is rejected.
The host should reconcile an ambiguous response rather than blindly
re-dispatching or creating another inverse effect. This remains a worker
idempotency primitive, not semantic rollback, key exchange, process
containment, or authorization granted to Octoscript source. The journal retains
terminal result data for idempotent replies, so its storage may need encryption
in addition to authentication. The canonical-input digest is an unkeyed
correlation value, so operation payloads must contain opaque secret selectors
rather than credential values.
This baseline does not yet provide an arbitrary filesystem or network tool adapter, dynamic origin policy, a general secret broker, signed packages, full JSON Schema, mobile policy backends, or general-purpose process containment. The supplied fixed-file and feature-gated fixed-endpoint catalogs are intentionally narrower than general filesystem or network adapters. The Linux Bubblewrap launcher is a deliberately narrow worker policy, not a substitute for those missing boundaries. Those features must not be inferred from the presence of the VM.
Tool descriptions and schemas are available only through the host-side
catalog. They are not script-visible authority. Schemas registered solely as
ToolMetadata remain prompt metadata; only JsonToolContract is executable.
The catalog publishes this distinction as contract_enforced, so a host or
LLM prompt builder does not need to infer enforcement from the presence of a
schema field.
octoscript_core::tool_call_hints and the octoscript tool-calls CLI command are
effect-free source-review aids, not static authorization. They recognize only
direct tool method syntax and deliberately do not resolve aliases,
shadowing, runtime string values, reachability, or imports. A host must never
derive a capability grant from that output alone; every actual call remains
subject to lease and reservation-time checks.
CapabilityCatalogLimits bounds both the number of registered descriptors and
the complete serialized host catalog before a new handler is retained. This
limits catalog-driven prompt and allocation growth, but a trusted host must
still select a suitable bound and review the metadata it registers. A catalog
limit is not an authorization rule, input validator, or containment boundary.