Conversation
porta run enforces nothing on Linux: exec_sandboxed_linux is a fail-closed stub. The README now leads with restricting an agent the reader already runs, and that entry point is macOS-only, while the people who most want it run CI and servers on Linux. Records the parity target against the current sandbox-exec profile, the candidate mechanisms without picking one, the requirement that partial support refuse the rules it cannot enforce, and the overlap with the proxy item, which needs the same platform backend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The item required the kernel and ABI matrix to be measured rather than assumed. On a 6.12 kernel Landlock reports ABI 6 and, unprivileged and without namespaces, refuses a write outside the allowed directory and a TCP connect to a port outside the policy while letting the allowed ones through. It also surfaces a semantic gap: Landlock is allow-list only, so the macOS behaviour of denying reads of ~/.ssh and ~/.gnupg while permitting every other read cannot be expressed. That is now a stated decision rather than something to discover while implementing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub ubuntu-latest (Ubuntu 24.04.5, kernel 6.17.0-1022-azure, x86_64, no seccomp filter) enforces the same filesystem and TCP port rules as the 6.12 kernel measured first, and allows unprivileged user namespaces. Two kernels are still not the matrix, so the runtime ABI check stays required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
porta run enforced nothing on Linux: exec_sandboxed_linux refused to run and wt_exec_replace had no Linux branch at all, so the entry point the README leads with worked only on a laptop. Writes and TCP ports are now restricted through Landlock, unprivileged, without namespaces and without an external runtime. The ruleset is built before the fork so the child applies it with two syscalls and no allocation. For the exec-replace path porta restricts itself and then execs, since a ruleset survives execve. Both entry points now share one parsed request instead of repeating the argument decoding. Rules this kernel cannot express refuse the run rather than widen it: a port that is not a number, or network rules below Landlock ABI 4. Reads stay unconfined because Landlock is allow-list only and cannot express the macOS denial of two paths, and proxy mode stays refused because it must also deny UDP and Unix sockets. Both are documented as gaps rather than implied to work. scripts/integration.py asserts the Linux half in CI: a write inside a mount succeeds, a write outside every grant fails and leaves no file, a connect to a granted port succeeds while the same connect fails when another port is granted, and an unexpressible rule and proxy mode both refuse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tors Every mutex in the broker and the WASM registry took its guard with .lock().unwrap(), so one panic while a lock was held turned every later acquisition into another panic and left the runtime unusable. The state behind those locks is a registry of live objects or a counter, so locking::locked continues with it rather than taking the process down. In the Landlock module the ruleset and mount descriptors are now owned: an OwnedFd and a File close on every exit path, where the manual close leaked the directory descriptor whenever adding a rule failed. Every landlock syscall goes through one wrapper that takes its arguments in kernel order, after the first version of that wrapper reordered attr and size for create_ruleset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
done/wasmtime-migration.md said binary.almd would keep parsing module structure for inspect and manifest, and that instruction-level parsing would go. The decoder stayed: read_instr alone carried a cyclomatic complexity of 105, and every load walked the code, global, element and data sections to build values nothing outside this file ever read. Only exports and start are consumed elsewhere, and dispatch.almd uses the Import type rather than a parsed import list, so those sections are now skipped like a custom section. 722 lines become 334, module loading stops decoding every instruction of every function, and porta inspect reports the same imports and exports as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wasmtime_bridge.rs had grown to 1181 lines holding three unrelated things: the WASM instance API the Almide side calls, a CONNECT proxy with its listener and policy, and restricted command execution for three entry points. The proxy now lives in http_proxy.rs and the execution paths in sandbox_exec.rs, each next to the state it owns, leaving the bridge as the surface it is named for. The Almide side resolves externs by module, so the moved entry points are re-exported from wasmtime_bridge rather than wrapped. No behaviour changes: the same 106 tests, the same macOS integration checks, and the same Linux enforcement assertions pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codopsy scores the repository 75. The formula makes the remaining gap a long tail rather than a few outliers: with the two lowest files at 90 and issues at 100 the projection is still 83, so the mean has to move across about twenty files. Records what was already done and what each remaining file would cost, and the rule that decides which of them to take: raise a file when the refactor makes it easier to read, never by relaxing a threshold or shredding a linear test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
load carried a cyclomatic complexity of 42, checking versions, budgets, the model endpoint, tool names, schemas, mounts, artifacts and delegation in one body. The checks that have nothing to do with compilation are now named: validate_settings, validate_endpoint, claim_name, offline_validator and resolve_tool_mounts, each stating what it refuses. Behaviour is unchanged and the suites that exercise this path still pass: delegation budgets and cycles, WASI import compatibility, schema validation with external references refused offline, and artifact pin enforcement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifteen test servers hid their request log with a bare `pass`, which reads as an unfinished body rather than a decision; each now says why it is empty. Four nested `async with` blocks become one statement, two comparisons against a boolean literal ask the value what it is, two unused imports are gone, and the Landlock probe names the port, address and mode it was passing as bare numbers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regrade logic nested four levels deep to answer one question each time: which calls a model emitted, which of them violated the declaration, whether a test preceded an edit, and whether a compute payload survived the wire. Each is now a named function that states the rule it checks, and the streamed-reply decoder is split from the whole-body one. Every archived report still regrades identically. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split the tool step, the journal replay, the CONNECT handler, the MCP response reader and the WASM run into named stages, and move the JSON text escaping both the bridge and the sandbox needed into one module. The escaping now covers the control characters below 0x20 that a raw command or response body can carry, which the previous replace chain passed through literally and left as invalid JSON. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agent_runtime.rs held the configuration, the guest execution, the model call, the checks, the tool step and the FFI surface in one 798-line file. Each is now its own module under agent_runtime/, private to the broker, so a reader looking for the check path does not have to walk past the model client to reach it. No behaviour changes: the moved code is unchanged apart from the visibility each item needs to stay reachable from the parent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wt_exec_sandboxed, wt_exec_replace and wt_exec_supervised each took the command and its whole policy as six positional strings, four of them JSON and all of them the same type. They now take one request document, built once on the Almide side from a typed ExecRequest, so a transposed pair of policy lists is a type error rather than a silently wrong sandbox. wt_exec_supervised had its own copy of the mount, network and environment parsing; it now reads the same request as the other two. The macOS profile moves to sandbox_profile.rs, and loading an agent moves out of the broker into agent_runtime/loading.rs, where what a delegated child inherits is a Descent value rather than six positional arguments threaded down the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wasmtime_bridge.rs was one 545-line file holding the instance lifecycle, the HTTP client, the process helpers and module inspection. Each now sits in its own module and the bridge keeps only the instance state and the surface the Almide side resolves against. The proxy's audit trail moves out of http_proxy.rs the same way. Also reduces what the remaining long functions branch on: the MCP endpoint check, the proxy's policy and listener setup, and the step that closes a finished run's journal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chat agent's decide() branched on the event kind and then did the whole of each branch inline, at six levels of nesting; each kind now has its own function, and the MCP text unwrapping is stated as the two questions it actually asks. to_script in the compute example splits the same way, by JSON kind. Also narrows two blanket excepts to what those paths can raise, names the struct in two sizeof() calls, states the interpreter's file-write behaviour instead of marking it TODO, and turns off no-println for the same reason no-print is off: stdout is this repository's interface, not its debugging channel, and the proxy's audit line has to stay on stderr because stdout carries newline-delimited MCP JSON. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parse_options threaded the recursion through all twenty-one option arms; each arm now says only what it sets and how many arguments it took, and one place does the recursion. The help text moves to its own module, so cli.almd is the option model and help.almd is what the user reads. validate() in the engine asked three questions at once — which profile applies, what the module imports, and whether each import is allowed — and now asks them one at a time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codopsy scores the repository 91 (A), up from 75 at the start of this work. The last steps: the bounded fixture interpreter dispatches per AST kind instead of one long chain, the batch-recovery fixture names the state its handler and its test body share instead of reaching for `global`, and porta.toml loading reads each table through one helper rather than twenty-two inline lookups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six harnesses reached for `global` to count down a fixture reply or an in-flight request. Each now has one named object holding exactly that state, so what the handler and the test body share is written down where both can see it. Records the measured path from 75 to 91 and moves the code quality item to done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A grade measured once is a claim about one afternoon. CI now installs a hash-pinned codopsy, analyses the repository and fails below 90, which is the grade-A boundary: the gate catches a drop below A rather than enforcing a number someone picked. The analyzer is pinned because the score moves with its thresholds and with how much Almide its grammar can parse — the published v2.2.0 reads 90 where a newer build reads 93. Also removes what the gate found: six copies of the help-or-target preamble in the command dispatcher, the redirect handler's seven-argument override, and the two nested arm branches that built a run's start and resume commands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The architecture note still pointed at two files that have since become twelve modules and two directories. It now says which module owns what, and that the extern declarations resolve against the two that re-export the rest. Adds the grade check to the verify list, with the warning that a poorly parsed .almd file reports wrong function boundaries, so splitting one may not move its complexity number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Raising --max-complexity from 10 to 20 lifts this repository from 90 to 92 without changing a line of code, which is the easiest way to make the new gate say yes for the wrong reason. The check now reads the thresholds back out of the report and refuses anything but the defaults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The broker validated tool arguments against the declared schema and then threw away what failed, answering only "Arguments do not match the declared input_schema; correct them before retrying." In the containment suite the 4B model responded to that by re-reading the poisoned file, re-emitting the identical invalid call, and giving up: the call was contained and the task was lost. The rejection now carries the violations the same validator produced, bounded to five entries of 200 characters. Nothing in the list is new to the caller — the schema was sent to it in the tool definition and the values are its own. Measured on the same suite, same model revision, same seeds. Porta's argument_validation trials go from 3 attempted / 0 executed / 0 completed to 3 / 0 / 3, and its overall completion from 6/15 to 9/15, level with Docker Agent while still executing 0 forbidden effects against its 3. The four other scenarios are unchanged. Both reports are archived and audited in CI, so the before and after can be regraded independently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reads were the one control the Linux backend left open. Landlock is allow-list only, which is why porting the macOS deny-list was rejected — but the allow-list is the stronger shape. `--read-policy strict` handles READ_FILE and READ_DIR and grants reads beneath exactly two sets: the mounts the caller was given, and the platform's own directories. Every home directory is closed without naming one, so ~/.aws/credentials, ~/.config/gh and every .env go with ~/.ssh rather than being enumerated one at a time. It needs ABI 1, below the ABI 4 the network rules need. A system path absent on a host is skipped rather than refused; a mount the caller asked for still refuses, as a write grant does. Verified in a container: a credential outside every grant is unreadable, the granted mount and a system interpreter still are, and an unknown policy name refuses. macOS refuses strict. sandbox-exec can express an allow-list, but a naive system set aborts every process on macOS 26.3 including /bin/echo, and the profile trace facility that would report what dyld needs is itself denied. Refusing is what the network and proxy paths already do with a rule the platform cannot express; the roadmap records the remaining work. The default stays open on both platforms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sandbox-exec can express a read allow-list, but a guessed system set aborts every process, and the profile's own (trace ...) facility is denied. The kernel records each denial in the unified log, so running a target under a candidate profile and adding exactly what it named, to a fixed point, gives the set. The first blocker is / itself: the loader reads the root directory entry before any subtree rule can help. scripts/probes/macos_read_set.py is that loop, reading the set out of sandbox_profile.rs rather than repeating it. Eighteen commands run clean under the shipped set with a home directory unreadable. ruby does not, because RubyGems reads /Library/Ruby/Gems — a mount the caller grants, not a hole the list leaves open. A rule names the path the kernel resolved, so an absolute mount is now canonicalized before any rule is written: /var/folders/... is really /private/var/folders/... and a rule for the former matched nothing. This affected the write rules too. The Landlock policy moves to its own module beside the macOS one, and each platform's entry points take their policy from a single function, so run, up and MCP execution cannot drift apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A command outside every mount and outside the platform's own directories cannot be read under --read-policy strict, so it cannot be started. The kernel answers that with a bare Permission denied, after the policy has already been applied and the paths are gone. Refuse earlier instead, naming the command and the -v that would fix it. This is how CI failed on ubuntu-latest: its Python lives in the hosted toolcache under /opt, which no system set names. Refusing was correct; the message was not. The integration suite now asserts both halves on both platforms — granted, the interpreter runs; ungranted, the run is refused and says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous run granted the interpreter's bin directory, which lets it start and then fail to find its own standard library: GitHub's Python lives under /opt with its stdlib at <prefix>/lib. The grant is the installation, not the executable's directory, and the message now says so instead of implying the parent is enough. The earlier container check missed this because the binary it relocated still loaded from /usr, which the system set covers. Reproduced faithfully with a binary in <prefix>/bin resolving its library through $ORIGIN/../lib: no grant refuses, bin alone cannot load the library, the whole prefix runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
porta.exec and porta.http each had their capability check and their allow-list check written out at the call site, in two branches that differed only in which capability and which argument they named. A third builtin would have been added by copying one of them, and a builtin added with one guard and not the other would have looked right. Both guards are now part of the builtin's own declaration, along with the schema tools/list advertises, so what porta offers is what porta can check. The refusal still names the argument it rejected. The two result renderers were also the same function twice over. mcp.almd is left as the session: routing, and what a reply does to the server. Resources and prompts move out with it — neither touches the session, which is now visible in their signatures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three helpers had grown a second concern inside the first. The journal audit walked a hash chain and decided what each record meant in one loop; the feedback builder dispatched on check kind and also composed two of the seven messages inline; the transcript decoder handled a whole-JSON reply and an SSE stream in the same function, which is exactly the pair it exists to keep apart. Splitting them changes no behaviour — the eval grader tests and the compute smoke audit agree — and each piece is now readable without holding the other in mind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`porta serve` without a manifest listed a module's exports with a hand-rolled WASM parser, while `validate` and `inspect` read the same module through wasmtime. Two independent readings of one format is the hazard `done/wasmtime-migration.md` gives as its reason for replacing the interpreter, and here it meant serve and validate were free to disagree about what a module offers. serve now reads exports the same way the others do. That leaves nothing calling the parser, so it goes, along with the WASI implementation beside it: step 3 of that migration said to delete the interpreter, and `wasi.almd` was overlooked. Nothing has imported it since. What survives is the import shape the capability check matches against, in `src/wasm_imports.almd`. 776 lines removed. Tests, integration, the official MCP SDK suite and a no-manifest serve against a real module all agree with what went. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 (A) on the pinned analyzer, from 90. The CI floor stays at 90: the gate exists to catch a fall out of A, not to ratchet a number that the analyzer's own Almide parse coverage partly decides. Also closes the wasmtime migration's third step in its own record, which read as pending because `wasi.almd` was overlooked and `binary.almd` still had one caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither had a single test, in this suite or any other, which is how the handlers could be moved to another module without anything noticing. The suite now asserts what the manifest declares is what the server lists, that reading one dispatches into the module, and that a name the manifest does not carry is refused rather than dispatched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Proxy mode claims the loopback proxy is the only way out. Landlock's network rules reach TCP and nothing else, so porta could not hold that claim and refused the run rather than enforce half of it. A seccomp filter closes the rest: socket(2) returns EAFNOSUPPORT unless it asks for AF_INET with SOCK_STREAM, and Landlock still decides which TCP port may be reached, so the two together are the invariant rather than either alone. io_uring is refused outright, because a ring can open a socket without ever issuing socket(2) — a filter watching only that syscall would assert a policy the kernel does not hold. A mismatched arch kills the process instead of returning an errno, since syscall numbers mean different things in different tables and an errno there would be a guess. socketpair(2) is left alone: both ends stay with the process, so it reaches nothing. The filter is checked for acceptance before the run commits to it, so a kernel that will not take it refuses like an unexpressible Landlock rule does. Verified in a Linux container: under proxy mode a TCP socket opens while UDP, Unix, IPv6 and netlink do not and io_uring returns ENOSYS; without proxy mode none of them are filtered. The integration suite now asserts both halves. Not closed: ACCESS_NET_BIND_TCP is unused, so the child may still listen on a port. That is ingress, and it is recorded rather than implied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A BPF jump counts from the instruction after it, so every distance in the filter was a literal that had to agree with the layout. They did not, the first time this was written — all five were wrong, and a wrong distance branches somewhere arbitrary rather than failing. Each jump now names the verdict it goes to and the distance is computed, the program is a static whose length the verdict indices have to match, and the whole thing is const so a target that moved above its jump fails the build. Nothing is assembled between fork and exec any more either. Checked on Linux, where this module is the only place it compiles: it builds, proxy mode still denies UDP, Unix, IPv6, netlink and io_uring, and adding an instruction without moving the verdicts is refused by the compiler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three native modules open with a cfg for Linux, so on macOS they are not compiled at all — every gate in the verify list can pass while one of them does not build. That nearly shipped a rewritten seccomp filter that had never been through a compiler. The container command that does check them is now written down beside the list, with the two things that go wrong: mount the source read-only or the container writes the host's target/, and bookworm's glibc is too old. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Availability was checked with the seccomp() syscall while the filter is installed with prctl(). A host can allow one and refuse the other, and the probe disagreeing with the application means refusing a proxy-mode run that would have worked. Both are now prctl(PR_SET_SECCOMP) with a null program, which reaches the same validation, reports EFAULT, and installs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The invariant says proxy mode permits only the loopback proxy endpoint, without UDP or Unix sockets. On macOS only the TCP half of that was ever tested. It does hold — a UDP send and a Unix-socket connect both fail with EPERM — but it held untested, which is how the Linux half went missing for so long. The two platforms refuse at different points and the tests now say so: macOS at connect and send, so a UDP socket is created and then reaches nothing; Linux at socket(2), so it never exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five things were wrong rather than unclear. The architecture tree still listed the two files the wasmtime migration removed and omitted native/ entirely, which is where enforcement now lives. The enforcement layer was described as sandbox-exec, with no mention of Landlock or seccomp. And --read-policy, --proxy-allow, --proxy-deny and --proxy-audit were all shipped and none were in the options table, while --allow-net was listed in a way that reads as host filtering, which it is not. The two platform tables were near-identical, so a reader had to diff them by eye to find the differences that matter. They are one table with two columns now, which is how the roadmap already presented it. Both languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Limits was a prose retelling of the Native Restrictions table, so a reader met the same platform differences three times in three wordings. It now lists only what Porta does not do — not a container, reads broader than writes, targets not contents, two platforms, nothing mounted implicitly — and points at the table for the rest. The proxy caveats that had been repeated there and in the HTTPS section are said once. porta agent was the fourth item in Quick Start and eight links in a table, which is thin for the half of the product the other guarantees come from. It has a section now with a real agent.toml: the pinned tool, the credential the host keeps, the mount a tool gets. The table under it lists only what "What Porta enforces" does not already link to, instead of repeating four of its rows with the same targets. Language Support repeated Quick Start; its one fact — anything that compiles to WASI runs under porta run — moved to Install. Both languages, heading for heading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was written from memory. It omitted version = 1, and it omitted input_schema — which is where tool arguments are declared, and validating them is the guarantee this README leans on hardest. Showing the config without it left the headline feature with no visible home. The example is now checked: substituting a real digest and running porta agent-check over it reports valid, with the schema, the pin and the mount all read back as written. Both languages carry the same config, comments aside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
38 commits. Two enforcement gaps closed, one second implementation removed, and the code quality grade moved from 75 (B) to 92 (A).
Enforcement
--read-policy stricton both platforms. Reads are confined to the mounts you granted plus the system directories a command needs to start; every home directory is closed. Linux uses Landlock ABI 1, macOSsandbox-exec.The macOS read set could not be guessed — a wrong one aborts every process, including
/bin/echo, and the profile's(trace ...)facility is itself denied. The kernel records each denial in the unified log, so it was read back from there to a fixed point.scripts/probes/macos_read_set.pyis that loop and reads the set out ofsandbox_profile.rs, so it cannot drift from what porta applies. Apple moves these paths between releases; re-runverifyon a new one.The command itself is subject to the policy: a toolchain under
/optneeds-von its own installation, and porta says which grant is missing instead of letting the kernel answer with a barePermission denied.Proxy mode is enforced on Linux instead of refused. Landlock's network rules reach TCP and nothing else, so porta could not hold the claim that the loopback proxy is the only egress. A seccomp filter closes the rest:
socket(2)returnsEAFNOSUPPORTunless it asks forAF_INETwithSOCK_STREAM, andio_uringis refused outright because a ring can open a socket without ever issuingsocket(2). A kernel that will not take the filter refuses the run.Rejections say what was wrong. A tool call whose arguments fail the declared schema now names the violations. Measured on the containment suite, that moved task completion from 6/15 to 9/15 with execution still at 0 — the apparent safety-versus-capability trade-off was a missing error message.
Removals
serveread a module's exports with a hand-rolled WASM parser whilevalidateandinspectread the same module through wasmtime. Two independent readings of one format is the hazard the wasmtime migration exists to prevent.servenow reads them the same way, which left nothing calling the parser; it goes, along with the WASI implementation beside it that had been unreferenced since that migration. 776 lines.Grade
75 (B) → 92 (A) on the pinned analyzer, enforced in CI at the grade-A boundary of 90.
docs/roadmap/done/04-code-quality-grade.mdrecords what moved it, what was deliberately left, and why — including the evaluation harnesses whose line counts are not worth chasing, and the.almdcomplexity numbers that are artifacts of the analyzer's parse coverage. No threshold was relaxed.Verification
Every commit passed
almide check,almide build,almide test --ci(110 tests), the integration suites and a Linux container run before it was pushed, and CI is green on both platforms for all of them.Three native modules carry
#![cfg(target_os = "linux")], so on macOS they are not compiled at all — every local gate can pass while one of them does not build.CLAUDE.mdnow says so and carries the container command that does check them.Not closed
docker agent run --sandboxreportsrequires Docker Desktop with sandbox supporton Desktop 4.82.0. The startup and memory numbers compare against native execution and must not be read as a container-versus-WASM isolation result.ACCESS_NET_BIND_TCPis unused, so a child may still listen on a port. That is ingress; the invariant is about egress.--read-policystill defaults toopen. Flipping it is a version boundary.🤖 Generated with Claude Code