Skip to content

feat: rework manager api for cross-major calls - #9

Open
kp2pml30 wants to merge 1 commit into
v0.6-devfrom
feat/rework-manager-api
Open

feat: rework manager api for cross-major calls#9
kp2pml30 wants to merge 1 commit into
v0.6-devfrom
feat/rework-manager-api

Conversation

@kp2pml30

@kp2pml30 kp2pml30 commented Jul 28, 2026

Copy link
Copy Markdown
Member

Changeset

Depends-On: genlayerlabs/genlayer-node#1647
Depends-On: genlayerlabs/genlayer-studio#1716
Depends-On: #9

Summary by CodeRabbit

  • New Features
    • Added a WebSocket manager socket protocol for starting executions, streaming live events, attaching, cancelling, acknowledging retained results, and retrieving artifacts.
    • Added nested/cross-version contract call routing with permission boundaries and deterministic “small hash” reporting.
    • Added execution retention controls and a configurable maximum manager WebSocket message size.
    • Enabled Lua OS standard-library support.
  • Documentation
    • Expanded and reorganized API/spec and contributing/testing/setup/release documentation, including new manager-socket and cross-major call ADRs.
  • Bug Fixes
    • Improved execution lifecycle behavior (ack/retention, deadlines, cancellation) plus more robust artifact/status handling, with added coverage for socket ordering and forwarder termination.

@kp2pml30 kp2pml30 self-assigned this Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds shared nested-execution and hashing interfaces, refactors manager lifecycle and supervision, introduces a WebSocket execution protocol, updates cross-major routing specifications, and reorganizes contributor, testing, build, release, and fuzzing documentation.

Changes

Manager execution and protocol modernization

Layer / File(s) Summary
Shared interface and nested-run contracts
crates/modules-interfaces/...
Adds generated host and manager protocol definitions, nested-run envelopes and replies, decoding support, permission handling, execution defaults, and route-invariant hashing.
Execution lifecycle and nested supervision
implementation/src/manager/run.rs, implementation/src/manager/versioning.rs, implementation/src/manager/*_test.rs
Adds event snapshots, finish causes, retention and acknowledgement handling, nested execution, deadline and cancellation coordination, version resolution, and lifecycle tests.
Manager HTTP and WebSocket transport
implementation/src/manager/{mod.rs,handlers.rs,socket.rs}, implementation/Cargo.toml
Migrates routing to Axum, adds /ws, binary framing, subscriptions, artifact retrieval, status waiting, JSON responses, and transport tests.
Cross-major routing and protocol specification
docs/adr/*, docs/website/src/{impl-spec,spec}/..., executors/*
Documents cross-major delegation, nested host-loop methods, manager socket semantics, API schema changes, protocol constants, and executor revisions.
Documentation and repository tooling
docs/*, .genvm-tool.py, CLAUDE.md, .claude/skills/*, support/scripts/*, install/config/*
Reorganizes contributor documentation, expands testing and fuzzing guidance, updates suite collection and configuration, strengthens source checks, relocates Git caches, and enables Lua OS support.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: manager API rework to support cross-major calls.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rework-manager-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

GenVM PR actions

Tick a box to run it (the box unticks itself when handled). Actions only run while the PR has the ci-safe label.

  • Force run full tests
  • Rerun full tests
  • Provision executor PRs
  • Merge into dev

Full GenVM CI runs only when rtm or run-full-tests is set — "Force run full tests" is a sticky toggle for run-full-tests. Adding rtm marks the PR ready-to-merge and also runs full tests. Merge requires: rtm, green full tests, green E2E, and the branch 0 commits behind.

@github-actions

Copy link
Copy Markdown

Linked executor PR(s)

executor: genlayerlabs/genvm-executor#7
executor: genlayerlabs/genvm-executor#8

@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e core

@kp2pml30

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kp2pml30

kp2pml30 commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Note

Superseded where it disagrees with the triage comment, which carries the current
status of every finding below, the trusted-party scope disclaimer, and the retractions.

Review pass 1 — hard findings

Automated review of origin/v0.6...HEAD (umbrella + both executor lines). Five findings that
need a design decision rather than a mechanical fix. Three mechanical ones (topmost-runner
ChainState ignoring LatestFinal, v0.2 Default state-mode downgrade, a duplicated
calldata encode) are being fixed separately.

Determinism-focused probing found no leader/validator/sync hash divergence, but note the
coverage gap in the last section.


1. Cross-major calls drop the caller's custom-runner grants

crates/modules-interfaces/src/nested.rs:146, executors/v0.2.x/executor/src/lib.rs:400,
executors/v0.3.x/executor/src/lib.rs:396

NestedRunEnvelope carries no custom-runner grants, so the nested executor process starts
with an empty set. This conflicts with the normative startup rule that a CallContract child
receives the caller's entire loaded custom set
(docs/website/src/spec/03-vm/01-startup.rst:231). Declaring the field process-local in the
ADR does not by itself resolve the spec conflict.

Scenario: contract A registers custom:X, then calls contract B on another executor
major. B uses custom:X — available on an in-process call, missing on a cross-major one.

Options: either extend the envelope with a canonical, ordered representation of the
granted custom archives and rebuild the loaded set before the entry runner loads (preserving
child RAM charging), or decide that transferring grants is out of scope, reject such calls
canonically, and update the normative semantics to match.

2. Legacy calldata rewriting can replace the requested method

implementation/src/manager/run.rs:991

The manager unconditionally removes "method" and reinserts its value under "",
overwriting any "" already present, and does so before executor-version resolution. The
ABI specifies a closed key set (docs/website/src/spec/04-contract-interface/02-abi.rst:41),
so a current-format request carrying a stray "method" should be rejected as malformed.

Scenario: calldata {"": "safe", "method": "danger", "args": []} should fail validation.
Instead the manager drops "method", overwrites "safe" with "danger", and executes
danger.

Suggested direction: reject key collisions outright, and apply the legacy translation only
after a legacy executor/ABI has been resolved, so current-format requests reach current
validation unchanged.

3. attach can permanently miss the terminal event

implementation/src/manager/run.rs:558 and :634

attach snapshots lifecycle state before subscribing to the broadcast channel. A terminal
transition landing between the two is absent from the snapshot and already gone from the new
receiver.

Scenario: an attach observes Started; the executor publishes Finished before
events.subscribe() runs. A reconnecting client — or await_terminal during shutdown —
then waits forever.

Note: simply swapping the two operations trades this for duplicate terminal delivery. A
lifecycle watch channel, or sequence numbers plus a subscription established atomically with
the snapshot and filtered against it, would close it properly.

4. host_genvm_id idempotency is not atomic

implementation/src/manager/run.rs:1746, :1794, :769

Token lookup and insertion happen in separate critical sections, so two concurrent requests
can both see the token as absent and start separate executions — contrary to the documented
idempotency guarantee. Separately, GC removes expired tokens without checking that the mapping
still points at the expired id, so it can delete a freshly inserted mapping.

Suggested direction: reserve/check the token and install the execution in one atomic step;
in GC, remove a token only if it still maps to the expiring genvm_id. Worth a concurrent-run
test and a GC/start-race test.

5. ack can discard a still-running execution

implementation/src/manager/run.rs:576

ack removes the context unconditionally, but the protocol defines ACK as releasing state for
a finished run.

Scenario: a client ACKs immediately after the run response. The process keeps running,
while attach, cancel and artifact retrieval all return unknown_id, the socket subscription is
aborted, and reusing the idempotency token can start a duplicate execution.

Suggested direction: reject ACK unless a terminal event or result exists, then remove the
context and its token together. Worth an ACK-before-terminal test.


Coverage gap worth knowing about

The cross-major nested-run path is unreachable from the jsonnet integration cases:
tests/runner/genvm_tool_plugins/integration.py:685 always builds MockHost with
resolve_callcontract_executor_hook=None, and mock_host.py:139-151 then returns None, so
CallContractRoute is always InProcess for every jsonnet-driven case. Only the Python system
tests (tests/system/cross-major/test.py, MultiConnectionMockHost) can exercise it, so all
regression coverage for this PR's headline feature has to live there.

Also note executors/v0.3.x/.genvm-tool.py still ships ignore-hash: True, which disables
every hash comparison on the v0.3 line — including the leader-vs-validator one. Determinism
regressions on that line cannot fail CI until it is flipped.

@kp2pml30

kp2pml30 commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Note

Superseded where it disagrees with the triage comment, which carries the current
status of every finding below, the trusted-party scope disclaimer, and the retractions.

Review pass 2 — the route is consensus-visible

Second pass (a code review plus two independent determinism-probing agents, one driving the
Python system tests and one driving jsonnet integration cases). The headline finding below was
reached independently by the reviewer and by the system-test agent, from opposite directions.


1. A same-major call hashes differently depending on the route — this is the important one

executors/v0.3.x/executor/src/wasi/genlayer_sdk/run.rs:421,
executors/v0.2.x/executor/src/wasi/genlayer_sdk.rs:983,
crates/modules-interfaces/src/nested.rs:174

The in-process path folds RunResult::small_hash() — a 4-key map over kind, result,
subvm_hashes, wasm_store_hashes. The nested path folds the node-reported
execution_hash instead, an 8-key map that additionally commits to backtrace,
storage_changes, data_fees_consumed and data_fees_remaining (the last computed from the
nested request's empty bucket_totals, implementation/src/manager/run.rs:1874).

Measured on the same caller, same callee, same line, same code, identical result 303:

route parent execution hash
in-process (resolveNone) 4dc935aced8599329666fd9a34d9a47c116f89a8557867ffa38ea6b7359b5187
nested (resolve{'major': 3}) 79ac48a5ba4787823f86a156f789b892bc45becc556b06da4123978cac81e75a

The route is chosen by the host, not the contract. Purity of
resolve_callcontract_executor is pushed onto the host and nothing in the manager or either
executor enforces it. So a staged rollout in which one node routes a same-major call
in-process while another nests it forks consensus silently, with no contract change involved.

This also sits awkwardly against the claim that the result fingerprint is identical across
lines — for a same-major call the two routes demonstrably disagree.

Suggested direction: expose a canonical small-hash computation in the shared interfaces
layer, compute it from the callee's reported fields, and return it in NestedRunReply; both
call sites fold that, leaving the full execution hash for node-level result comparison. The
alternative is to narrow the spec claim explicitly and accept that the route is observable.

Base comparison is moot: the nested route does not exist on origin/v0.6, and small_hash is
untouched by this PR, so 4dc935… is what base produces.

2. A contract can turn an fd:// host configuration into an internal error

implementation/src/manager/run.rs:1824

fd:// is a supported executor host transport, but start_nested rejects it unconditionally,
and lines 1404-1407 convert that into an InternalError reply.

Scenario: a valid top-level execution is started with host 0 supplied as fd://N. A
contract calls an address whose resolver selects another major. The resolver succeeds, the
manager hits the bail!, and the caller gets an internal error rather than a canonical
VMError — i.e. contract input reaches an internal error, which the design explicitly rules
out.

Suggested direction: either relay host 0 through the manager, or reject the incompatible
run configuration up front (before contract execution) so this degrades to the ordinary
canonical major_mismatch outcome instead of being discovered by a contract.

3. Full writer queues silently violate the reply and terminal-event guarantees

implementation/src/manager/socket.rs:57 and :677

Both output queues use try_send. When the control queue fills, a run's subscription task
stops silently while the WebSocket stays connected — the client has no way to learn it must
reconnect and attach, so it can wait forever despite the terminal-event guarantee. On the
reply path, queue exhaustion propagates out of the handler and drops the connection without
replying to valid request ids.

Scenario: a client subscribed to more than 256 runs backpressures briefly while they
finish together; later terminal events are discarded. Pipelined artifact requests can likewise
fill the eight-frame artifact queue and kill the connection.

Suggested direction: await bounded capacity and apply backpressure to producers. If
overload must close the connection, close it explicitly so the client can recover.

4. Test-harness client: run identity ignores boot_id

tests/runner/origin/base_host.py:582, :758

The protocol defines run identity as (boot_id, genvm_id), but ManagerClient keys _runs
and orphan events by genvm_id alone, and deliberately keeps the old boot_id on reconnect
whenever any run remains.

Scenario: manager A finishes run 1 unacknowledged and restarts as B, whose ids restart at

  1. The old attach gets boot_id_mismatch but the stale state stays in _runs; a new run on B
    returns id 1, run() hands back the old RunState, wait_terminal() can return the old
    result immediately, and ack(1) acknowledges an unrelated run.

Suggested direction: key _runs/_orphan_events by (boot_id, genvm_id), always record
the latest hello boot id, and retire states from an obsolete generation after surfacing the
mismatch.

5. One disconnect with several live runs breaks recovery for all but one waiter

tests/runner/origin/base_host.py:826, :835

_mark_disconnected queues the connection-loss exception into every live run, but
_reconnected_once permits only one _reconnect_live_runs call per ManagerClient lifetime.

Scenario: two executions are live when the socket drops. The first waiter reconnects and
re-attaches both. The second consumes its already-queued disconnect marker, sees
_reconnected_once, and raises ManagerConnectionLost even though its run recovered fine. A
second genuine disconnect later is also unrecoverable.

Suggested direction: tag disconnects with a connection generation; after taking
_reconnect_lock, succeed if a newer generation is already connected, else reconnect that
generation. Allow later reconnects with bounded backoff instead of a lifetime boolean.

6. Pre-existing severity-4: unknown runner id surfaces as INTERNAL_ERROR

executors/v0.3.x/executor/src/rt/supervisor/actions.rs:177

A guest-supplied runner id shaped name:<canonical gvm32> whose pair is not installed reaches
anyhow::bail!("runner {}:{} not found") — a plain anyhow error, so the call site's
Error::trap(anyhow_to_wasmtime(e)) surfaces INTERNAL_ERROR where invalid_contract malformed_runner belongs. Reachable from gl.vm.spawn_sandbox(fn, runner=…)
(wasi/genlayer_sdk/run.rs:638) and gl.vm.map_file(runner, …).

The hash must be canonical gvm32 (52 chars, trailing padding bits zero); a non-canonical one
is rejected earlier by Bytes32Hash::from_gvm32 and correctly yields malformed_runner,
which is why this is easy to miss. Same class one layer down, currently shadowed:
Error::internal(format!("runner {id} not found")) at actions.rs:467.

Not a regression of this PR — the path is byte-identical at the base gitlink. Flagged
because it is a live severity-4, and because fixing it changes an execution result, so it
wants its own change rather than a quiet edit here.


Fixed in this pass

  • Topmost runner id now derives its ChainState from the imported storage mode, so a
    LatestFinal cross-major entry resolves finalized code instead of accepted code (both
    lines).
  • v0.2 CallContract with Default state now inherits the parent's mode instead of
    hardcoding LatestNonFinal.
  • Removed a duplicated, unused calldata::encode_obj on the v0.2 call path.
  • _assert_route_is_hash_invariant recorded a mismatch into a notes side-channel and passed
    anyway — it now asserts, pinning the current (defective) behaviour so it cannot silently
    pass in either direction and must be updated when finding 1 is fixed.

Test gaps that let these through

  • tests/runner/origin/base_host.py:185 — mock hosts accept mode in storage_read and
    ignore it (one storage dir). No test can observe accepted-vs-finalized divergence, for the
    runner blob or for contract storage. This is exactly why the hardcoded ChainState::Accepted
    was invisible. A two-snapshot host would close it.
  • tests/system/cross-major/test.py _assert_state_mode_crosses asserted only
    len(seen) == 1, never which mode — it would not have caught the Default-inheritance
    bug. Now asserts the value.
  • tests/runner/genvm_tool_plugins/integration.py:731 sets code = None iff not is_init, so
    is_init && code is None is unreachable from any integration case.
  • Fuel across the nested boundary is untested and unverified: MockHost.consume_gas is a
    no-op, so nothing enforces gas on either route, and DetFuelBudget is only ever read to
    build the next envelope (rt/mod.rs:73). Driving remaining_fuel_as_gen to 0 changes
    nothing.

Smaller items

  • ChainState::for_vm's catch-all _ => Accepted maps StorageType::Default to Accepted.
    When a v0.3 caller whose own mode is Default calls cross-major, the callee reads contract
    storage with mode Default but the runner blob with LatestNonFinal — and that is the most
    common cross-major shape. Probably identical bytes today, but the asymmetry is accidental;
    normalizing storage_read_mode once where it is computed would remove the question.
  • v0.2 substitutes LatestNonFinal where v0.3 propagates Default, so the two lines report a
    different state byte to resolve_callcontract_executor for identical contract logic.
    Harmless unless a host ever keys routing on it.
  • The v0.2 Default-inheritance fix changes behaviour for nested chains whose parent mode is
    LatestFinal/LatestNonFinal. No intra-round split (all nodes run one binary), but replay
    of historical v0.2 transactions can differ — worth a release note, since v0.2.x is shipped.
  • start_nested's two anyhow::ensure! guards (run.rs:1817, :1821) are dead code: both
    executors hardcode is_init: false / value: ZERO when deriving the child.
  • The permission bitmap → string translation (run.rs:1843-1868) cannot lose a bit, because
    the string is never consulted for a nested run — lib.rs:349 prefers the bitmap. The string
    is redundant rather than lossy.

Also verified clean

Leader/validator/sync agree on v0.3→v0.2, v0.2→v0.3 and a 6-hop alternating chain, guarded
against a vacuous pass by a check that a validator run handed an unconsumed leader result
fails. Child failure modes are all canonical. Plain-string user errors survive both
directions. State mode round-trips exactly for LATEST_FINAL and LATEST_NON_FINAL. The new
custom-runner grant resolution held up over 49 runs. Precompiled-module changes are not
contract-reachable.

@kp2pml30

kp2pml30 commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Note

Superseded where it disagrees with the triage comment, which carries the current
status of every finding below, the trusted-party scope disclaimer, and the retractions.

Review pass 3 — a contract-reachable internal error

Third pass. One contract-reachable INTERNAL_ERROR (fixed below), one methodological result that
weakens the determinism evidence from passes 1-2, and a set of design/hardening items.


1. FIXED — a zero fee budget aborts the transaction with INTERNAL_ERROR

executors/v0.3.x/executor/install/config/genvm.yaml, message_fee.delta_expr

A balance-funded internal message whose contract-supplied execution_budget_per_round is 0
died with INTERNAL_ERROR in all three modes:

rt::fees "failed to evaluate fee expression"
  error: internal error: undefined variable `false`
genvm "internal error": calculating message fee internal:
  failed to evaluate fee expression: undefined variable `false`

The fee expression language has no boolean literalsValue::Bool is only produced by
comparison operators and hasKey (executor/crates/common/src/expr/). So the else false
arm referenced an unbound identifier. That arm is dead for every non-zero budget and is
exactly what a zero budget evaluates, which is why nothing caught it.

The same delta_expr serves DeployContract, so gl.contract.deploy(..., use_balance=True)
with a zero per-round budget was the same defect.

Reachability: can_use_balance_for_message_fees comes from the root permission bitfield,
which a contract sets in its own __init__ via Root.get().set_permission(...) — after deploy
Root.lock_default() freezes the root slot, so the constructor is the only window.

Correction to an earlier claim in this comment: I first called this a regression of this
branch. It is not. The else false arm comes from be5cfe9 "chore: fix tests" (2026-07-16),
which is already an ancestor of origin/v0.3-dev and is also on pr/v0.3/fix/enforce-limits
and pr/v0.3/feat/gate-fuzz-examples. It shows up in this PR's umbrella diff only because the
executors/v0.3.x gitlink moves off acb37c7 (the initial split, which predates the whole
balance-funded path) onto a v0.3-dev-based commit. So: new in the umbrella diff,
pre-existing on the v0.3 line
— same class as the runner_ids finding, and it wants a fix on
v0.3-dev rather than being counted against this branch.

Fixed by spelling the false case as a comparison that never holds, preserving the intent
documented in the comment above it (execBudget > 0 && execBudget < floor). Probe now passes
12/12.

2. The jsonnet hash comparison does not compare execution hashes — read this before trusting a green probe

tests/runner/genvm_tool_plugins/integration.py:850

The hash artifact a jsonnet case compares across leader/validator/sync is a harness-built
tuple
[kind, data, fingerprint, storage_changes, events]. The executor's real
execution_hash additionally folds subvm_hashes, data_fees_consumed and
data_fees_remainingnone of which are in the compared tuple.

So a divergence confined to sub-VM folding or fee accounting passes silently even with
ignore-hash: False
. This is a coverage hole in the determinism testing itself, and it means
"all three modes agree" from a jsonnet case is a weaker claim than it reads as. The real value
is available as the execution_hash attribute of each step's result.pickle; the folding
probes in this pass used that as the oracle instead.

Worth either widening the compared tuple to the real execution hash, or documenting the gap
loudly where ignore-hash is configured.

3. Shared host-fns enum silently changed the v0.2 host wire protocol

executors/v0.2.x/executor/src/host/mod.rs:96-113 vs
crates/modules-interfaces/codegen/data/host-fns.json

Both lines previously had {ok:0, absent:1, forbidden:2, out_of_storage_gas:3} (verified via
git show acb37c7:executor/codegen/data/host-fns.json). The shared enum is now
{ok:0, evm_reverted:1, forbidden:2}. Against the rebuilt v0.2 line:

  • a host reply of 3 on storage_read/eth_call/get_balance/remaining_fuel_as_gen fails
    Errors::try_from → plain anyhow!INTERNAL_ERROR, where the canonical
    host out_of_storage_gas VMError was produced before;
  • a reply of 1 now maps to evm reverted instead of host absent — a consensus-visible
    result-string change.

v0.3's mapping was deliberately revised in the same series, and the argument that executors
ship from the same checkout as the manager means a same-release host speaks the new enum. So
this only bites hosts still speaking the old v0.2.16 protocol. Nothing in-tree exercises code
1-as-absent or code 3, so the suite cannot see it either way. Please confirm this is
intentional
— if old hosts exist, v0.2 wants a line-local enum or out_of_storage_gas back.

4. effect_free proves less than its name claims, and SPAWN_NONDET is translated for a module-less child

implementation/src/manager/run.rs:1296, :1861-1866, :1882

The check covers only storage_changes, emissions and data_fees_consumed.message_fee.
ReportedResult (crates/modules-interfaces/src/domain.rs:305) also carries nondet_results,
nondet_disagreement, llm_consumption and four more consumption buckets — all decoded and
dropped. A callee line that burned LLM budget or produced nondet output still reports
effect_free = true, and that consumption vanishes at the boundary.

Compounding it, SPAWN_NONDET is translated into the n permission char while :1882 forces
no_modules: true — granting a capability the manager structurally cannot service.

Not live today (derive_call_contract_permissions clears those bits), so this is
defence-in-depth — but the manager is the only party that can enforce it against a line it did
not derive. Suggest rejecting envelopes that assert SPAWN_NONDET (arguably also
WRITE_STORAGE/SEND_MESSAGES), and extending the predicate to every consumption field.

5. v0.2's in-process and nested routes grant different permissions for the same call

executors/v0.2.x/executor/src/wasi/genlayer_sdk.rs:843 (in-process child inherits
can_spawn_nondet) vs :920-940 (envelope deliberately never sets SPAWN_NONDET).

A third route-divergence mechanism, distinct from the hash-folding one — fixing that does not
fix this. A host that delegates a v0.2→v0.2 call gives the callee Forbidden on RunNondet
where the in-process route succeeds, so the same (address, state, major) triple has two
observable behaviours. All simultaneous honest nodes share the host policy, so it is not a
per-node split; it manifests under same-line delegation, and if routing policy differs between
the original run and a later sync, history diverges. It also contradicts the envelope table's
"permissions = CallContract-derived permissions", since v0.2's derivation includes
spawn_nondet. Either document the deviation or forbid same-major delegation.

6. Nested-spawn breadth is unaccounted

implementation/src/manager/run.rs:1808

remaining_recursion bounds depth only. for _ in range(1000): call(cross_major_addr).cheap_view()
spawns 1000 executor processes off one permit: nested runs hold no permit, nested_runs is a
completion counter, bucket_totals and initial_time_units_allocation are zeroed, and cheap
callees barely move the shared det fuel. Each hop costs fork/exec + runner load + wasm compile

  • a memory_limit-sized ceiling.

Bounded only by the strict deadline — nested runs do correctly inherit the parent's
strict_deadline (run.rs:2274-2291), so this is resource-thrash rather than runaway. Suggest
a per-root nested-spawn budget in start_nested, and/or a det-fuel charge per hop.

7. "nested" is inferred from memory_limit, and that same gate disables locked-slot enforcement

executors/v0.3.x/executor/src/exe/run.rs:110 (is_nested = memory_limit.is_some()),
executors/v0.3.x/executor/src/lib.rs:134-143; same shape in v0.2.

Permissions arrive in an independent envelope field, so "nested ⇒ read-only" holds only because
derive_call_contract_permissions happens to clear write_storage today. A future non-view
nested call, or a manager bug at run.rs:1898, silently yields a write-capable run with no slot
locks and no start-fee accounting. Suggest an explicit nested: bool in ExecutionData plus
entry-time rejection of nested envelopes asserting WRITE_STORAGE/SEND_MESSAGES/
USE_BALANCE_FOR_MESSAGE_FEES, and clamping remaining_recursion at entry to
min(supplied, VM_RECURSION).

8. resolve_abi_version accepts abi_major == 0 and routes to the newest line

implementation/src/manager/versioning.rs:154. major: 0 matches every 0.x version and
.max() picks the newest, so a host forwarding the advisory major (which is 0 for every
released line) silently routes cross-major calls to the newest line instead of erroring.

Deliberately not changed: 'major': 0 is load-bearing today — tests/runner/origin/base_host.py:955
sends it with a # FIXME, and cross-major, permits and manager-socket all rely on it as
the de-facto "default line". Rejecting 0 breaks them. Needs the FIXME resolved first, so
flagging rather than fixing.

9. Executor tarballs are downloaded and executed with no digest

install/lib/python/post-install/__main__.py:488 (download_executor), extraction :512.
Runners are content-addressed and verified; executor tarballs are fetched from manifest URLs
and unpacked as native binaries with nothing to check them against. Related: :481/:590
install v0.2.x runners with verify_hash=False, and the deferred check-install validation
only runs under --precompile (:592), so an install without it leaves unverified archives
the executor will load. Suggest a sha256 per executor artifact in support/manifest-base.yaml,
and running check-install unconditionally when anything was installed unverified.


Also fixed in this pass

  • Four v0.2 comments cited doc/website/src/spec/03-vm/05-permissions.rst — a page this PR
    deletes (content moved to 02-meta-properties.rst), with doc/ a typo for docs/. v0.3's
    equivalents were already updated; v0.2's now match.
  • ADR 015 contradicted itself on the envelope version field: the derivation section said it
    "is consumed at the boundary" while the rationale section said "it carries no version of its
    own", and the field was in fact dropped. Removed the stale mention.

Smaller items (not changed)

  • run.rs:1257-1265 — every manager-side nested failure surfaces as "contains effects", because
    nested_internal_error() sets effect_free: false and nested_run_ok checks that before
    kind. Outcome class is unchanged and the detail is not hashed, so this is debuggability
    only. Match on kind first.

  • run.rs:613 copies the whole artifact per 256 KiB chunk and re-serializes genvm_log to JSON
    per chunk; run.rs:385 re-serializes the whole log on every Finished event, including every
    attach snapshot. Unbounded under debug capture.

  • run.rs:1234 read_length_prefixed allocates a zeroed buffer from the length prefix before
    reading, with u32::MAX at both call sites. Needs a faulty child executor, so low likelihood,
    but try_reserve/incremental reads are cheap.

  • executors/*/executor/src/exe/precompile.rs:22-56 — SIGKILL between create and rename
    leaks *.tmp-<pid>-* forever; nothing sweeps the directory. The rename fix itself is correct.

  • run.rs:1467let _ = sync::DropGuard::new(…) drops immediately; it reads as a scope guard
    and is not one.

  • run.rs:2130 vs socket.rs:489 — the host_hello_data[1] rejection exists at the socket
    entry but not on the HTTP /genvm/run path, where it fires deep inside run_genvm_process.

  • implementation/src/scripting/pool.rs:69 — pool became unbounded on-demand; bounded today only
    via permits.

  • implementation/src/llm/providers.rs:1044 (and :372,:444,:555,:617,:713,:777,:890,:966) —
    malformed provider responses fabricate false/empty values instead of propagating failure
    after charging.

  • executors/v0.3.x/executor/src/host/mod.rs:77-85 — pre-existing, but run_nested is a new
    caller: Box::new_uninit_slice + assume_init before read_exact on a host-supplied u32
    length is technically UB and unbounded.

  • Corrected — an earlier version of this bullet claimed the branch deleted several golden
    .hash files. It does not: git diff --diff-filter=D origin/v0.6..HEAD -- '*.hash' is empty,
    and the v0.3 line tracks zero .hash files in the first place (v0.2 tracks 195), because
    ignore-hash: True in executors/v0.3.x/.genvm-tool.py suppresses both the comparison and
    the golden write at integration.py:929. The paths I listed came from misreading a
    whole-tree diff in which executors/v0.2.x and libs/unhardcoded-engine are added
    submodules.

    The real point stands and is now sharper: if ignore-hash: False is ever committed for v0.3,
    there is no golden set to compare against, and integration.py:950-954 writes a missing
    golden from the first run — so the first comparison per environment is vacuous for the whole
    line. Goldens have to be generated and committed together with the flip. Note also that
    disagree_in_sandbox is a deliberate-disagreement case (nasty-determinism tag, supplied
    leader_nondet) and cannot pass under a blanket flip.

Verified clean this pass

The three earlier Rust fixes were re-checked and hold — and ChainState::for_vm(false, …) turns
out to matter more than cosmetically: the canonical id is the archive-cache key, so LatestFinal
and LatestNonFinal runs previously shared a cache cell and a finalized run could be served
accepted-state code.

Also attacked and held: the precompile gate and non-det existence check (no per-node cache
divergence); the recursion-budget rework; hello-bytes write / notify_finished removal;
start_nested's envelope→ExecutionData mapping field-by-field, including deadline derivation;
root-slot poisoning via raw storage_write (closed by Root.lock_default()); backtrace content
reaching a hash; sub-VM fan-out and fold ordering over 14 mixed steps; fee-parameter magnitude
boundaries. Nothing on the nested path reads wall clock, HashMap iteration order or scheduling
state.

Note for future probe authors: jsonnet numbers are IEEE doubles, so a literal like
79228162514264337593543950335 silently becomes 2^96. Magnitude-boundary values must be built
inside the contract.

Coverage gap in this pass

The genvm-tool rewrite (support/tools/genvm-tool/, watchdog, codegen), the new
tests/runner/genvm_tool_plugins/ninja.py, git-third-party, log_asserts.py/logger.py,
support/scripts/*.py, and the Python host/test surface (base_host.py, public_abi.py,
host_fns.py, manager_api.py, mock_host.py, and the three system-test files) were not
reviewed
in this pass. Treat them as unreviewed rather than clean.

@kp2pml30

kp2pml30 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Note

Superseded where it disagrees with the triage comment, which carries the current
status of every finding below, the trusted-party scope disclaimer, and the retractions.

Follow-up pass — fixes applied, and two earlier claims retracted

Work done after review pass 3, driven by the todo list rather than by new fuzzing.


Retractions

Two findings in pass 3 were attributed to this branch and should not have been. Both are corrected
in place in that comment; repeating them here so the record is in one place.

  1. The zero-budget fee abort is not a regression of this branch. else false comes from
    be5cfe9 "chore: fix tests" (2026-07-16), already an ancestor of origin/v0.3-dev and also
    present on pr/v0.3/fix/enforce-limits and pr/v0.3/feat/gate-fuzz-examples. It appears in the
    umbrella diff only because the executors/v0.3.x gitlink moves off acb37c7 (the initial split)
    onto a v0.3-dev-based commit.

  2. The branch does not delete any golden .hash files.
    git diff --diff-filter=D origin/v0.6..HEAD -- '*.hash' is empty, and the v0.3 line tracked
    zero .hash files to begin with. The paths I listed came from misreading a whole-tree diff
    in which executors/v0.2.x and libs/unhardcoded-engine are added submodules.

Common cause: treating "new in the umbrella diff" as "authored by this branch" when a submodule is
added or its gitlink jumps. Worth keeping in mind for anyone else reviewing this PR the same way.

Fixed

  • invalid_contract malformed_runner for an uninstalled runner id. A guest picks the
    name:hash pair passed to spawn_sandbox/map_file; a well formed pair absent from all.json
    hit anyhow::bail! and aborted the transaction internally, while the neighbouring format and
    gvm32 failures — and an absent custom: runner — already produced a canonical error. New case
    runner/absent_runner_id covers all three shapes at runtime.
  • Boolean literals in the fee expression language. The language produced booleans from
    comparisons and hasKey but had no way to write one. true/false are now reserved words;
    the budget guard uses a real false instead of a comparison that never holds. 5 new expr tests.
  • New case message/use_balance_zero_budget pins the zero-budget path: the message is metered
    (fee 10280) rather than rejected, since the chain's floor applies only to a non-zero budget.
    The existing use_balance_budget_too_low only ever exercised the other arm, which is why the
    unbound identifier survived.

The harness now compares the real execution hash

integration.py built its own tuple [kind, data, fingerprint, storage_changes, events] and
compared that across leader/validator/sync. It now compares ReportedResult.execution_hash — the
value consensus actually uses.

Two things this uncovered:

  • The fingerprint slot was dead. base_host.py read a wire key fingerprint that no
    component emits (the field is backtrace), so the slot was None in 195 of 195 tracked
    goldens, including trap cases. Removed, along with ResultFingerprint and the now-unused
    calldata pretty-printer.
  • The old tuple omitted subvm_hashes, wasm_store_hashes, backtrace and both fee buckets,
    so a divergence confined to sub-VM folding or fee accounting passed silently. It also included
    events, which execution_hash does not fold — those stay covered by the messages
    semantics component, which only runs in the main mode. Worth deciding separately whether
    emissions belong in execution_hash.

v0.2's 195 goldens were regenerated against the new artifact and verified over repeated full runs:
1477/1477 with zero hash mismatches, and the goldens came back byte-identical, so the executor's
own hash is stable both across leader/validator/sync within a run and across runs. v0.3 still tracks none, because
ignore-hash: True suppresses both the comparison and the golden write for the whole line — so
committing that flip needs a golden set generated with it.

Audited and clean

Swept every anyhow!/anyhow::bail! in the v0.3 executor for further contract-reachable
INTERNAL_ERROR sources. None found beyond the one fixed above. The occurrences are either
inner causes already wrapped by Error::wrap into canonical VMErrors (actions.rs:22,529,1012,
supervisor/mod.rs:556, storage.rs:89,490), genuinely internal conditions (task join failures,
host protocol violations, propagating a nested executor's internal error), or dead code.

Incidental: wasi/mod.rs:52 add_to_linker_sync_dlsym has no callers and genlayer_dl
appears nowhere else, so its five anyhow sites are unreachable. Candidate for deletion.

Still open from pass 3

The headline finding is untouched: the call route is consensus-visible — in-process folds
RunResult::small_hash (4 keys), nested folds the node's execution_hash (8 keys), so the same
same-major call hashes differently depending on a host routing choice. That needs a design
decision, not a patch.

Also unaddressed: nested-spawn breadth, effect_free covering 3 of ~9 consumption fields,
nested being inferred from memory_limit, abi_major == 0 routing (blocked on the
base_host.py:955 FIXME), and unverified executor tarballs.

The v0.2 host-fns enum question from pass 3 is withdrawn — v0.2 is legacy and slated for
removal, so an old-host compatibility break there does not matter.

Tracking: GVM-331 covers the StorageType::Default inconsistency and the mock host's mode-blind
storage_read, which is what currently makes accepted-vs-finalized divergence untestable.

@github-actions github-actions Bot added the not rebased branch is behind its base; rebase before it can be merged label Jul 29, 2026
@kp2pml30

Copy link
Copy Markdown
Member Author

Triage of review passes 1-3

Single status table for every finding raised in the review passes above, so none of them has to be
re-read to know where it stands. Passes 1-3 are superseded by this comment where they disagree.

Scope disclaimer — trusted parties

This project trusts the host, the release manifest and the executor tarballs it names, and the LLM
providers. Findings whose attacker is one of those are out of scope by design and are not
tracked. That covers, from pass 3: unverified executor tarball digests and verify_hash=False for
the v0.2 runner set, read_length_prefixed allocating from a host-supplied length
(run.rs:1241), the on-demand VM pool (scripting/pool.rs:69), providers fabricating defaults
from malformed responses (llm/providers.rs), and Box::new_uninit_slice before read_exact on a
host length (host/mod.rs:77).

The bar that does apply: a contract must not be able to produce an INTERNAL_ERROR or a
consensus split.

Intended, not defects

  • abi_major == 0 resolves to the newest line (versioning.rs:154) — intended. v0.2 is legacy;
    there is one live major in practice. The base_host.py FIXME that motivated the flag is gone, the
    behaviour stays.
  • ignore-hash: True on the v0.3 line (executors/v0.3.x/.genvm-tool.py) — by design.
  • v0.2 in-process vs nested spawn_nondet (pass 3, item 5) — accepted deviation, will be
    recorded in ADR 015 rather than fixed.
  • v0.2 Default-inheritance replay note (pass 2, smaller items) — moot: v0.2 is not used on the
    same chain for sync.

Retracted

  • "Fuel across the nested boundary is untested" (pass 2) — the claim is wrong. Nondet is off on
    the nested path and the call consumes nothing, so there is no fuel flow to test.
  • "A contract can turn an fd:// host configuration into an internal error" (pass 2, item 2) —
    the bail! at run.rs:1979 is real, but nothing lets a contract reach it: fd:// for host 0
    would have to be supplied by the operator as a manager-side descriptor, which no supported
    configuration does. Trusted-party misconfiguration, not contract input.
  • v0.2 host-fns enum change (pass 3, item 3) — withdrawn earlier; v0.2 is legacy.
  • The two retractions already recorded in the follow-up comment (zero-budget fee abort and deleted
    .hash goldens) stand as written there.

Fixed in this branch

Pass 1: attach missing the terminal event (3), non-atomic idempotency (4), ack of a running
execution (5). Pass 2: full writer queues (3), (boot_id, genvm_id) run identity (4), reconnect
after a multi-run disconnect (5), malformed_runner for an uninstalled runner id (6). Pass 3: the
zero fee budget abort (1), the harness now comparing the real execution_hash (2), effect_free
and unserviceable nested permissions (4), an explicit nested field plus a VM_RECURSION clamp
instead of inferring nested from memory_limit (7).

Open, being addressed on this branch

  • The call route is consensus-visible (pass 2, item 1). Fix: the callee reports
    RunResult::small_hash() alongside execution_hash, and both call sites fold that, so the two
    routes agree. execution_hash stays in the reply for node-level result comparison.
  • Cross-major calls drop custom-runner grants (pass 1, item 1). Resolution: grants are not
    transferred across a major boundary — a crossing call with a non-empty loaded custom set is
    rejected as canonically invalid. Spec (03-vm/01-startup.rst) and ADR 015 to be updated to match.

Tracked elsewhere

  • GVM-332
    — the legacy method calldata rewrite (pass 1, item 2). Dropped in v0.4; not patched here.
  • GVM-334
    — nested-spawn breadth is unaccounted (pass 3, item 6).
  • GVM-331
    StorageType::Default asymmetry and the mode-blind mock storage_read.

Small items still standing

run.rs:1596 (let _ = sync::DropGuard::new(…) drops immediately), the unreachable
wasi/mod.rs:52 add_to_linker_sync_dlsym, run.rs:1257 matching effect_free before kind in
nested failures, artifact chunking re-serializing genvm_log, the *.tmp-<pid>-* leak on SIGKILL
in precompile.rs, and the late host-index-1 rejection on the HTTP path.

@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from 2a1a057 to 5296764 Compare July 29, 2026 11:54
@github-actions github-actions Bot removed the not rebased branch is behind its base; rebase before it can be merged label Jul 29, 2026
@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from 5296764 to a9361a4 Compare July 29, 2026 12:04
@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from a9361a4 to c4450a3 Compare July 29, 2026 13:13
@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

2 similar comments
@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/website/src/impl-spec/appendix/manager-api.yaml (1)

263-277: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the 404 these routes now return.

create_router answers not_found() (404, JSON {"error": ...}) when the path id is not all-digits (implementation/src/manager/mod.rs lines 294-296, 308-310, 327-329), but both response blocks only list 200/500. Add a '404' entry so the route table matches the implementation. Also worth confirming whether the deprecated DELETE /genvm/{genvm_id} still accepts the ShutdownRequest query parameters that the router parses — none are documented.

Also applies to: 353-372

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/website/src/impl-spec/appendix/manager-api.yaml` around lines 263 - 277,
Update the response definitions for both affected GenVM routes, including the
block around the GenVM status response and the additional block around lines
353-372, to add a '404' response referencing the existing ErrorResponse schema.
Keep the documented 200 and 500 responses unchanged, and ensure the deprecated
DELETE route’s documented parameters match the ShutdownRequest query parameters
parsed by create_router.
implementation/src/scripting/mod.rs (1)

160-166: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Narrow StdLib::OS before exposing it to module scripts.

StdLib::OS also exposes os.execute, os.exit, os.getenv, os.remove, os.rename, os.tmpname, and os.setlocale; for LLM/Web modules, os.exit can terminate the manager and os.getenv exposes provider/env state. If this is only for os.time/os.date/os.clock, nil out the others after load_std_libs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/scripting/mod.rs` around lines 160 - 166, Restrict the OS
library exposed by the module scripting setup: after `load_std_libs`, remove
`os.execute`, `os.exit`, `os.getenv`, `os.remove`, `os.rename`, `os.tmpname`,
and `os.setlocale`, while preserving `os.time`, `os.date`, and `os.clock`.
Update the initialization flow around `lua_libs` and `load_std_libs` so scripts
retain only the required OS functions.
🧹 Nitpick comments (6)
implementation/src/manager/handlers.rs (1)

244-269: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider capping max_len for artifact reads.

ArtifactRequest.max_len is unbounded on the handler side; a very large value could trigger a large allocation/response if get_artifact doesn't itself clamp it (not shown in this file). This overlaps with the "artifact/log allocation costs" item already noted as a remaining open item for this PR, so flagging here mainly as a pointer to add a cap (e.g. similar to STATUS_WAIT_CAP) if not already enforced in run.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/handlers.rs` around lines 244 - 269, Cap
ArtifactRequest.max_len in handle_genvm_artifact before passing it to
get_artifact, reusing the established artifact-read cap if one exists in the run
context (otherwise add a suitable constant analogous to STATUS_WAIT_CAP). Ensure
requests above the cap are clamped or rejected consistently with existing
handler behavior, preventing oversized artifact allocations and responses.
implementation/src/manager/mod.rs (2)

364-369: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider scoping DefaultBodyLimit::disable() to the routes that need it.

Applied as a router-wide layer, every POST (/module/*, /log/level, /permits, /llm/check) also becomes unbounded, though only /genvm/run and /contract/detect-version carry large payloads. A per-route DefaultBodyLimit::max(...)/disable() keeps the small admin endpoints bounded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/mod.rs` around lines 364 - 369, Scope
DefaultBodyLimit::disable() to the /genvm/run and /contract/detect-version
routes instead of applying it router-wide, and preserve the default body limit
for /module/*, /log/level, /permits, and /llm/check. Update the router
construction around the existing fallback and with_state chain using per-route
layering.

185-265: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: collapse the repeated JSON-body closures.

/module/start, /module/stop, /module/restart, /log/level, /permits (POST) and /llm/check repeat the same parse-then-dispatch block. A small helper taking the handler fn would remove six copies.

♻️ Sketch
async fn json_route<F, Fut>(ctx: sync::DArc<AppContext>, body: Bytes, handler: F) -> Response
where
    F: FnOnce(sync::DArc<AppContext>, serde_json::Value) -> Fut,
    Fut: std::future::Future<Output = anyhow::Result<Response>>,
{
    match parse_json_body(body) {
        Ok(body) => unwrap_all_anyhow(handler(ctx, body).await),
        Err(error) => bad_request(error),
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/mod.rs` around lines 185 - 265, Optionally reduce
duplication by introducing a shared JSON route helper near the route setup that
parses Bytes, returns the existing parse-error response, and dispatches the
handler through unwrap_all_anyhow. Update the repeated closures for
handle_module_start, handle_module_stop, handle_module_restart,
handle_set_log_level, the POST permits handler, and the llm/check handler to use
it while preserving their current routes and behavior.
implementation/src/manager/socket.rs (2)

401-421: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid copying the payload out of the received frame.

body[HEADER_LEN..].to_vec() copies up to max_message_bytes (64 MiB by default) per frame. Message::Binary already yields Bytes, so body.slice(HEADER_LEN..) would hand the payload on without the allocation (decode_payload takes &[u8]).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/socket.rs` around lines 401 - 421, Update the
frame-return path in the socket message handling flow to use a zero-copy slice
of the received Bytes payload instead of allocating with to_vec(). Preserve the
existing HEADER_LEN offset and the returned payload type expected by
decode_payload, while leaving method and request validation unchanged.

569-581: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Error classification relies on comparing anyhow display strings.

e.to_string() == "boot_id_mismatch" and e.to_string() == "unknown_id" silently reclassify to unknown_id/malformed_frame the moment run.rs adds context or rewords a message. Since the protocol codes are part of the documented contract (manager-socket.rst), a typed error enum (or downcast_ref) from run_ctx.attach/get_artifact would make this contract compiler-checked.

Also applies to: 675-678

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/socket.rs` around lines 569 - 581, Replace
string-based anyhow error classification in the run_ctx.attach handling and the
corresponding get_artifact path with typed error matching, using a shared error
enum or downcast_ref. Map the typed boot_id_mismatch and unknown_id variants
directly to Errors::BootIdMismatch and Errors::UnknownId, while preserving the
existing fallback for other errors. Ensure added context does not change
protocol classification.
implementation/src/manager/run.rs (1)

922-924: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale, truncated doc comment above unsafe_overrides.

The preceding line still reads /// Debug-only override: force the executor directory (a version dir under the — a sentence cut off when reroute_to moved into UnsafeOverrides, which now carries its own docs. Drop it or replace it with a description of the struct field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 922 - 924, Remove or replace
the stale truncated doc comment immediately above the unsafe_overrides field in
the relevant struct. Document unsafe_overrides itself accurately, without
referencing the executor-directory behavior now owned by UnsafeOverrides
documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/014`. manager host socket protocol.md:
- Around line 46-49: Update the fenced block containing the wire-format example
near the method_id/request_id layout to include an appropriate language
identifier, such as text, while leaving the example content unchanged.

In `@docs/contributing/howto/building/docs.md`:
- Around line 17-18: Make the shell commands in
docs/contributing/howto/building/docs.md lines 17-18 and
docs/contributing/howto/testing/integration.md line 83 shell-safe by replacing
the angle-bracket placeholders <out> and <version> with concrete values or
defined, quoted variables such as "$OUT" and "$VERSION".

In `@docs/contributing/howto/committing/git-third-party.md`:
- Around line 30-33: Update the committing instructions around the
nested-repository workflow to explicitly tell readers to return to the executor
repository root after committing inside the nested repository, before running
`git third-party save executor/third-party/wasmtime`. Preserve the existing path
and command.

In `@docs/contributing/howto/testing/python.md`:
- Around line 12-14: Update the Nix flake path in the testing command after
changing into the genlayer-py-std directory so it resolves to the
repository-root support/nix/py-test location; use the correct four-level
relative path or run the build from the repository root.

In `@docs/website/src/impl-spec/appendix/host-loop.rst`:
- Around line 131-149: Clarify the run_nested handler’s response framing around
manager_start_nested_sync: document whether failures use the standard errors/ok
prefix before the payload, matching resolve_callcontract_executor, eth_call, and
get_balance, or explicitly state that errors are encoded inside the reply
envelope. Ensure the pseudocode specifies the exact framing host implementers
must follow.

In `@docs/website/src/impl-spec/appendix/manager-api.rst`:
- Around line 9-12: Update the deprecation note in the manager API appendix to
include GET /genvm/{id}/artifact alongside the other execution endpoints,
preserving the stated one-release-train removal window and socket protocol
guidance. Also mark the corresponding artifact route as deprecated in the
manager API specification, using the existing deprecation metadata pattern.

In `@docs/website/src/impl-spec/appendix/manager-socket.rst`:
- Around line 128-135: Update the manager-socket documentation around the run
response to state that only some validation is synchronous and may cause the
request to return an internal error; do not claim the ID is always returned
immediately. Clarify that permit acquisition and process spawning continue
asynchronously, with subsequent failures reported through event notifications,
and retain the automatic event subscription behavior.

In
`@docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst`:
- Around line 81-88: Update the delegation description around the “same derived
sub-VM” claim to qualify it for accepted delegations only. State that calls with
loaded custom runners are rejected, and that delegated sub-VM permissions
differ; reference the existing rules governing custom-runner rejection and
delegated permissions while preserving the equivalent observable results and
execution hashes for delegations that are accepted.

In `@executors/v0.2.x`:
- Line 1: Update the executors/v0.2.x gitlink to reference a commit reachable
from the configured remote, or fetch and publish the intended commit
2c2653992b798dd97a5362f49bb4581bd993fc18 before retaining the pointer; ensure
the final submodule reference is resolvable before merging.

In `@executors/v0.3.x`:
- Line 1: Update the gitlink for the executors/v0.3.x submodule from the
unavailable commit 96e758118e45f8edeafbe1331ef9619fe918f463 to a reachable
commit in the configured promisor remote, preserving the submodule dependency
while ensuring CI can check it out.

In `@implementation/src/manager/handlers.rs`:
- Around line 92-111: Bound the queued-execution wait in the run handler’s
`run_ctx.attach` flow with the existing `STATUS_WAIT_CAP`, applying the timeout
to each `events.changed()` await or the overall wait as consistent with
`handle_genvm_status`. Preserve normal event handling and return an appropriate
timeout error when no start event arrives within the cap.
- Around line 387-391: Build a provider-derived substitution map named
check_vars using the same config_vars context as the runtime LLM backend, then
pass check_vars to genvm_common::templater::patch_json when patching backend.
Preserve the existing DOLLAR_UNFOLDER_RE behavior and ensure config_data
placeholders are resolved before constructing the check backend.

In `@implementation/src/manager/mod.rs`:
- Around line 127-151: Introduce a bad_request response helper in the manager
module and route client-input parsing failures from parse_json_body,
parse_query, and deployment_timestamp through it instead of internal_error.
Return the appropriate 4xx response without logging these expected validation
failures at error level, while keeping internal_error for genuine handler or
server failures.

In `@implementation/src/manager/run.rs`:
- Around line 1526-1531: Update the run_nested reply-writing error path around
write_length_prefixed so that, after logging the failure, it immediately returns
from the enclosing loop/function instead of continuing to serve subsequent
requests. Preserve the existing successful-write flow and error logging.

In `@implementation/src/manager/socket.rs`:
- Around line 265-273: Update the serde_json::Value::Number handling to preserve
floating-point values instead of falling through to push_null(); encode the f64
representation using the encoder’s supported non-null representation, such as a
string, while retaining the existing i64 and u64 paths.
- Around line 50-78: Replace non-blocking control sends in async request
handling with awaited sends: add Writer::error_async using send_control_async,
and update the async reply/error call sites in
implementation/src/manager/socket.rs (lines 539, 545, 583, 603, 629, and
existing error callers) while retaining try_send only where awaiting is
impossible. No direct change is needed in
docs/website/src/impl-spec/appendix/manager-socket.rst lines 50-52 because the
documented guarantee remains correct after this code change.
- Around line 582-589: Update handle_attach so it sends the Attach response
through self.writer.send_control before subscribing with
self.subscribe(req.genvm_id, Ok(rx))?. Preserve the existing snapshot payload
and error propagation, ensuring terminal events cannot be emitted before the
attach response.

In `@install/config/genvm-manager.yaml`:
- Around line 11-12: Update the manager definition in
docs/schemas/default-config.json to declare execution_retention and
max_message_bytes alongside manifest_path and permits, using validators that
accept the configured duration and byte-limit values. Keep the schema aligned
with the defaults referenced by install/config/genvm-manager.yaml so schema
validation accepts both keys.

In `@support/scripts/check-source-text.py`:
- Around line 37-42: Update _violations() to scan the full source text rather
than only text.splitlines(), allowing the existing environment-mutation patterns
to match whitespace and newlines across multiline qualified calls, use imports,
and libc imports. Map each full-text match back to its corresponding source
line(s) so violations retain accurate reporting while continuing to detect
env::set_var, env::remove_var, libc mutation calls, and imported aliases.

In `@support/scripts/get-all-git.py`:
- Around line 155-162: Add a cross-process lock around the complete shared-cache
lifecycle in ensure_cache(), covering the existence check, repository
initialization, remote configuration, and fetch operations. Use a lock path
derived from the shared cache location returned by cache_path_for_url() or
git_common_dir(root), and ensure all callers serialize access before reading or
modifying the cache.

---

Outside diff comments:
In `@docs/website/src/impl-spec/appendix/manager-api.yaml`:
- Around line 263-277: Update the response definitions for both affected GenVM
routes, including the block around the GenVM status response and the additional
block around lines 353-372, to add a '404' response referencing the existing
ErrorResponse schema. Keep the documented 200 and 500 responses unchanged, and
ensure the deprecated DELETE route’s documented parameters match the
ShutdownRequest query parameters parsed by create_router.

In `@implementation/src/scripting/mod.rs`:
- Around line 160-166: Restrict the OS library exposed by the module scripting
setup: after `load_std_libs`, remove `os.execute`, `os.exit`, `os.getenv`,
`os.remove`, `os.rename`, `os.tmpname`, and `os.setlocale`, while preserving
`os.time`, `os.date`, and `os.clock`. Update the initialization flow around
`lua_libs` and `load_std_libs` so scripts retain only the required OS functions.

---

Nitpick comments:
In `@implementation/src/manager/handlers.rs`:
- Around line 244-269: Cap ArtifactRequest.max_len in handle_genvm_artifact
before passing it to get_artifact, reusing the established artifact-read cap if
one exists in the run context (otherwise add a suitable constant analogous to
STATUS_WAIT_CAP). Ensure requests above the cap are clamped or rejected
consistently with existing handler behavior, preventing oversized artifact
allocations and responses.

In `@implementation/src/manager/mod.rs`:
- Around line 364-369: Scope DefaultBodyLimit::disable() to the /genvm/run and
/contract/detect-version routes instead of applying it router-wide, and preserve
the default body limit for /module/*, /log/level, /permits, and /llm/check.
Update the router construction around the existing fallback and with_state chain
using per-route layering.
- Around line 185-265: Optionally reduce duplication by introducing a shared
JSON route helper near the route setup that parses Bytes, returns the existing
parse-error response, and dispatches the handler through unwrap_all_anyhow.
Update the repeated closures for handle_module_start, handle_module_stop,
handle_module_restart, handle_set_log_level, the POST permits handler, and the
llm/check handler to use it while preserving their current routes and behavior.

In `@implementation/src/manager/run.rs`:
- Around line 922-924: Remove or replace the stale truncated doc comment
immediately above the unsafe_overrides field in the relevant struct. Document
unsafe_overrides itself accurately, without referencing the executor-directory
behavior now owned by UnsafeOverrides documentation.

In `@implementation/src/manager/socket.rs`:
- Around line 401-421: Update the frame-return path in the socket message
handling flow to use a zero-copy slice of the received Bytes payload instead of
allocating with to_vec(). Preserve the existing HEADER_LEN offset and the
returned payload type expected by decode_payload, while leaving method and
request validation unchanged.
- Around line 569-581: Replace string-based anyhow error classification in the
run_ctx.attach handling and the corresponding get_artifact path with typed error
matching, using a shared error enum or downcast_ref. Map the typed
boot_id_mismatch and unknown_id variants directly to Errors::BootIdMismatch and
Errors::UnknownId, while preserving the existing fallback for other errors.
Ensure added context does not change protocol classification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c060afa2-a31d-471e-807d-257611a45f81

📥 Commits

Reviewing files that changed from the base of the PR and between 64de460 and c4450a3.

⛔ Files ignored due to path filters (23)
  • crates/modules-interfaces/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • implementation/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • support/tools/genvm-tool/genvm_tool/__main__.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_configure.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_docs.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_howto.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/model.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/rst.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog_main.py is excluded by !support/tools/genvm-tool/**
  • tests/runner/genvm_tool_plugins/genvm.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/integration.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/ninja.py is excluded by !tests/**
  • tests/runner/gvm_extra/mock_host.py is excluded by !tests/**
  • tests/runner/origin/base_host.py is excluded by !tests/**
  • tests/runner/origin/host_fns.py is excluded by !tests/**
  • tests/runner/origin/logger.py is excluded by !tests/**
  • tests/runner/origin/manager_api.py is excluded by !tests/**
  • tests/system/cross-major-observability/test.py is excluded by !tests/**
  • tests/system/cross-major/test.py is excluded by !tests/**
  • tests/system/manager-socket/test.py is excluded by !tests/**
  • tests/system/permits/test.py is excluded by !tests/**
  • tests/templates/util.jsonnet is excluded by !tests/**
📒 Files selected for processing (75)
  • .claude/settings.fuzzing.json
  • .claude/skills/agentic-fuzzing/SKILL.md
  • .genvm-tool.py
  • CLAUDE.md
  • crates/modules-interfaces/Cargo.toml
  • crates/modules-interfaces/codegen/data/host-fns.json
  • crates/modules-interfaces/codegen/data/manager-api.json
  • crates/modules-interfaces/src/domain.rs
  • crates/modules-interfaces/src/host_fns.rs
  • crates/modules-interfaces/src/lib.rs
  • crates/modules-interfaces/src/manager_api.rs
  • crates/modules-interfaces/src/nested.rs
  • docs/README.md
  • docs/adr/014. manager host socket protocol.md
  • docs/adr/015. cross-major contract calls.md
  • docs/contributing/README.md
  • docs/contributing/explanation/README.md
  • docs/contributing/explanation/docs-layout.md
  • docs/contributing/explanation/executor-lines.md
  • docs/contributing/explanation/merge-model.md
  • docs/contributing/explanation/vendored-trees.md
  • docs/contributing/howto/README.md
  • docs/contributing/howto/building/build.md
  • docs/contributing/howto/building/docs.md
  • docs/contributing/howto/building/runners.md
  • docs/contributing/howto/committing/git-third-party.md
  • docs/contributing/howto/committing/runners.md
  • docs/contributing/howto/committing/submodules.md
  • docs/contributing/howto/docs/style.md
  • docs/contributing/howto/extending/add-host-function.md
  • docs/contributing/howto/extending/add-llm-provider.md
  • docs/contributing/howto/extending/add-wasi-function.md
  • docs/contributing/howto/extending/modify-runner.md
  • docs/contributing/howto/extending/modify-wasmtime.md
  • docs/contributing/howto/extending/write-a-script.md
  • docs/contributing/howto/genvm-tool.md
  • docs/contributing/howto/pr.md
  • docs/contributing/howto/releasing/release-build.md
  • docs/contributing/howto/releasing/versioning.md
  • docs/contributing/howto/setup.md
  • docs/contributing/howto/testing/README.md
  • docs/contributing/howto/testing/fuzzing.md
  • docs/contributing/howto/testing/integration.md
  • docs/contributing/howto/testing/python.md
  • docs/contributing/howto/testing/rust.md
  • docs/contributing/howto/write-a-script.md
  • docs/contributing/tutorial/README.md
  • docs/contributing/tutorial/first-contribution.md
  • docs/website/src/impl-spec/appendix/host-loop.rst
  • docs/website/src/impl-spec/appendix/index.rst
  • docs/website/src/impl-spec/appendix/manager-api.rst
  • docs/website/src/impl-spec/appendix/manager-api.yaml
  • docs/website/src/impl-spec/appendix/manager-socket-consts.rst
  • docs/website/src/impl-spec/appendix/manager-socket.rst
  • docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst
  • docs/website/src/spec/03-vm/01-startup.rst
  • docs/website/src/spec/03-vm/02-meta-properties.rst
  • docs/website/src/spec/03-vm/05-result.rst
  • docs/website/src/spec/04-contract-interface/04-upgradability.rst
  • docs/website/src/spec/appendix/constants.rst
  • executors/v0.2.x
  • executors/v0.3.x
  • implementation/Cargo.toml
  • implementation/src/manager/handlers.rs
  • implementation/src/manager/handlers_test.rs
  • implementation/src/manager/mod.rs
  • implementation/src/manager/run.rs
  • implementation/src/manager/run_test.rs
  • implementation/src/manager/socket.rs
  • implementation/src/manager/socket_test.rs
  • implementation/src/manager/versioning.rs
  • implementation/src/scripting/mod.rs
  • install/config/genvm-manager.yaml
  • support/scripts/check-source-text.py
  • support/scripts/get-all-git.py
💤 Files with no reviewable changes (2)
  • docs/contributing/howto/write-a-script.md
  • .claude/settings.fuzzing.json

Comment on lines +46 to +49
```
u16 method_id (BE) | u64 request_id (BE) | calldata(payload)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify a language for the fenced block.

Use a language identifier such as text for the wire-format example so Markdown tooling can validate the fence consistently.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 46-46: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/014`. manager host socket protocol.md around lines 46 - 49, Update
the fenced block containing the wire-format example near the
method_id/request_id layout to include an appropriate language identifier, such
as text, while leaving the example content unchanged.

Source: Linters/SAST tools

Comment on lines 17 to 18
nix develop '.?submodules=1#gen-docs' --command \
sphinx-build -q -b dirhtml docs/website/src <out>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make executable documentation placeholders shell-safe. Angle-bracket placeholders in bash commands are parsed as redirection operators rather than substituted values.

  • docs/contributing/howto/building/docs.md#L17-L18: replace <out> with a concrete directory or a defined, quoted variable such as "$OUT".
  • docs/contributing/howto/testing/integration.md#L83-L83: replace <version> with a concrete version or a defined, quoted "$VERSION" variable.
📍 Affects 2 files
  • docs/contributing/howto/building/docs.md#L17-L18 (this comment)
  • docs/contributing/howto/testing/integration.md#L83-L83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/howto/building/docs.md` around lines 17 - 18, Make the
shell commands in docs/contributing/howto/building/docs.md lines 17-18 and
docs/contributing/howto/testing/integration.md line 83 shell-safe by replacing
the angle-bracket placeholders <out> and <version> with concrete values or
defined, quoted variables such as "$OUT" and "$VERSION".

Comment on lines +30 to +33
1. Edit, and **commit inside the nested repo** — patches come from the history
above the base commit
2. `git third-party save executor/third-party/wasmtime`, path relative to the
current directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify the working directory before git third-party save.

Step 1 tells the reader to commit inside the nested repository, but step 2 passes executor/third-party/wasmtime, which will not resolve if they remain there. Explicitly require returning to the executor repository root before running save.

Proposed clarification
-1. Edit, and **commit inside the nested repo** — patches come from the history
-   above the base commit
-2. `git third-party save executor/third-party/wasmtime`, path relative to the
-   current directory
+1. Edit, and **commit inside the nested repo** — patches come from the history
+   above the base commit, then return to the executor repository root
+2. From the executor repository root, run
+   `git third-party save executor/third-party/wasmtime`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. Edit, and **commit inside the nested repo** — patches come from the history
above the base commit
2. `git third-party save executor/third-party/wasmtime`, path relative to the
current directory
1. Edit, and **commit inside the nested repo** — patches come from the history
above the base commit, then return to the executor repository root
2. From the executor repository root, run
`git third-party save executor/third-party/wasmtime`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/howto/committing/git-third-party.md` around lines 30 - 33,
Update the committing instructions around the nested-repository workflow to
explicitly tell readers to return to the executor repository root after
committing inside the nested repository, before running `git third-party save
executor/third-party/wasmtime`. Preserve the existing path and command.

Comment on lines +12 to 14
cd executors/v0.3.x/runners/genlayer-py-std
env_dir="$(nix build --no-link --print-out-paths path:../../support/nix/py-test)"
PYTHONPATH="$PWD/src:$PWD/src-emb" "$env_dir/bin/pytest" tests/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the relative Nix flake path.

After cd executors/v0.3.x/runners/genlayer-py-std, ../../support/nix/py-test resolves to executors/v0.3.x/support/nix/py-test, not the repository-root support/nix/py-test. Use ../../../../support/nix/py-test or invoke the command from the repository root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/howto/testing/python.md` around lines 12 - 14, Update the
Nix flake path in the testing command after changing into the genlayer-py-std
directory so it resolves to the repository-root support/nix/py-test location;
use the correct four-level relative path or run the build from the repository
root.

Comment thread docs/website/src/impl-spec/appendix/host-loop.rst
Comment thread implementation/src/manager/socket.rs
Comment thread implementation/src/manager/socket.rs
Comment on lines +11 to +12
execution_retention: 5m # finished executions are retained until ack or this TTL
max_message_bytes: 67108864 # 64 MiB manager websocket message cap

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'default-config.json' -x jq '.properties.manager // .["$defs"] // .' {} | head -100
rg -n 'execution_retention|max_message_bytes' --glob '*.json'

Repository: genlayerlabs/genvm-manager

Length of output: 3076


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -t f 'genvm-manager.yaml|default-config.json' .

echo
echo "== install/config/genvm-manager.yaml first 20 lines =="
file=$(fd -t f 'genvm-manager.yaml' . | head -n1)
if [ -n "${file:-}" ]; then
  cat -n "$file" | sed -n '1,20p'
fi

echo
echo "== schema references and manager/additionalProperties occurrences =="
schema=$(fd -t f 'default-config.json' . | head -n1)
if [ -n "${schema:-}" ]; then
  rg -n '"(\$id|additionalProperties|properties)"|manager|execution_retention|max_message_bytes|additionalProperties' "$schema" | sed -n '1,220p'
fi

echo
echo "== JSON path extraction if jq exists =="
if command -v jq >/dev/null 2>&1 && [ -n "${schema:-}" ]; then
  jq 'def paths: path(..); def get(\$p): getpath(\$p);
    {"\$id": .["$id"],
     "$schema": .["$schema"],
     "manager_path": paths[] as $p | select($p|map(tostring)|join(".")=="properties.manager") as $p | $p,
     "manager_path_props": paths[] as $p | select($p|map(tostring)|join(".")|startswith("properties.manager.properties.")) as $p | $p,
     "manager_additionalProperties": .properties.manager | getpath(paths[] | select(. as $p | ($p|map(tostring)|join("."))=="additionalProperties") // []) // null
    }' "$schema" | sed -n '1,160p'
fi

Repository: genlayerlabs/genvm-manager

Length of output: 2197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

schema="docs/schemas/default-config.json"
echo "== manager schema lines 160-225 from $schema =="
sed -n '160,225p' "$schema" | nl -ba -v160

echo
echo "== manager properties exact names =="
python3 - <<'PY'
import json
from pathlib import Path

p = Path("docs/schemas/default-config.json")
data = json.loads(p.read_text())
manager = data.get("manager", {})
prop = manager.get("properties", {})
print(sorted(prop.keys()))
print("count:", len(prop))

keys = {"execution_retention", "max_message_bytes"}
for k in sorted(keys):
    print(f"{k}:", prop.get(k))
PY

echo
echo "== remote schema existence and manager snippet =="
for url in \
  "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/main/doc/schemas/default-config.json" \
  "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/main/docs/schemas/default-config.json" \
  "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/@GENVM_TAG@/doc/schemas/default-config.json" \
  "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/@GENVM_TAG@/docs/schemas/default-config.json"
do
  echo "-- $url --"
  if curl -sSfI "$url" | grep -q '^HTTP/'; then
    curl -sSf "$url" | sed -n '150,225p' | nl -ba -v150
  else
    echo "unavailable"
  fi
done

Repository: genlayerlabs/genvm-manager

Length of output: 277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

schema="docs/schemas/default-config.json"
echo "== manager schema lines 160-222 from $schema =="
sed -n '160,222p' "$schema"

echo
echo "== manager property names via Python =="
python3 - <<'PY'
import json
from pathlib import Path

p = Path("docs/schemas/default-config.json")
data = json.loads(p.read_text())
manager = data.get("manager", {})
prop = manager.get("properties", {})
print("manager_properties_count=", len(prop))
for k in sorted([m for m in prop if m in {"execution_retention", "max_message_bytes"} or len(m) > 20]):
    print(f"{k}={prop[m] is not None}")

print("manager_additional_properties=", manager.get("additionalProperties"))
print("has_manager=", bool(manager))
PY

echo
echo "== local doc alternate file =="
ls -la doc/schemas/default-config.json 2>/dev/null || true
ls -la docs/schemas/default-config.json 2>/dev/null || true

echo
echo "== remote manifest/schema existence and manager snippets =="
python3 - <<'PY'
import json
from urllib.request import urlopen, Request

urls = [
    "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/main/doc/schemas/default-config.json",
    "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/main/docs/schemas/default-config.json",
    "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/@GENVM_TAG@/doc/schemas/default-config.json",
    "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/@GENVM_TAG@/docs/schemas/default-config.json",
]
for base in urls:
    try:
        req = Request(base, headers={"User-Agent": "CodeRabbit"})
        with urlopen(req, timeout=15) as r:
            data = json.load(r)
        print(f"\n-- {base} -- status=", r.status, "size=", len(r.read(0).split()) if False else 0)
        manager = data.get("manager", {})
        props = manager.get("properties", {})
        print("manager_exists=", bool(manager), "properties_count=", len(props), "additionalProperties=", manager.get("additionalProperties"))
        for key in sorted({"threads", "execution_retention", "max_message_bytes"} & set(props.keys())):
            print(f"profile_{key}=", bool(props.get(key)))
        lines = base.rsplit("/", 1)[0].rsplit("/", 1)[-1].split("/", 1)[0].rsplit("/", 1)[-1]
        print("path=", lines)
    except Exception as e:
        print(f"-- {base} -- error={type(e).__name__}: {e}")
PY

Repository: genlayerlabs/genvm-manager

Length of output: 3354


Add the new manager keys to the config schema

install/config/genvm-manager.yaml references docs/schemas/default-config.json#/manager, and that manager schema only defines manifest_path/permits. Add execution_retention and max_message_bytes there, plus any schema validators required by these defaults, so schema-aware editors and CI do not reject the config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install/config/genvm-manager.yaml` around lines 11 - 12, Update the manager
definition in docs/schemas/default-config.json to declare execution_retention
and max_message_bytes alongside manifest_path and permits, using validators that
accept the configured duration and byte-limit values. Keep the schema aligned
with the defaults referenced by install/config/genvm-manager.yaml so schema
validation accepts both keys.

Comment thread support/scripts/check-source-text.py
Comment on lines 155 to +162
def cache_path_for_url(root: Path, url: str) -> Path:
parsed = urlparse(url)
url_path = parsed.path if parsed.scheme else url.rsplit(':', 1)[-1]
name = Path(url_path).name or 'repo'
name = re.sub(r'\.git$', '', name)
name = re.sub(r'[^A-Za-z0-9._-]+', '-', name).strip('-') or 'repo'
digest = hashlib.sha256(url.encode()).hexdigest()[:16]
return root / '.git' / 'genvm-submodule-cache' / f'{name}-{digest}.git'
return git_common_dir(root) / 'genvm-submodule-cache' / f'{name}-{digest}.git'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Serialize access to the shared submodule cache.

Using git_common_dir(root) makes one cache repository shared by linked worktrees, but ensure_cache() still performs an unlocked check/init/configure/fetch sequence. Concurrent setup can race on repository initialization, remote configuration, or fetch locks, leaving one invocation failed or consuming an incomplete cache. Add a cross-process lock around the cache lifecycle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@support/scripts/get-all-git.py` around lines 155 - 162, Add a cross-process
lock around the complete shared-cache lifecycle in ensure_cache(), covering the
existence check, repository initialization, remote configuration, and fetch
operations. Use a lock path derived from the shared cache location returned by
cache_path_for_url() or git_common_dir(root), and ensure all callers serialize
access before reading or modifying the cache.

@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from c4450a3 to 5eaa59b Compare July 29, 2026 16:11
@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from 5eaa59b to 7ecb349 Compare July 30, 2026 03:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
implementation/src/scripting/mod.rs (1)

160-166: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid exposing dangerous os functions unless scripts need them.

StdLib::IO is already enabled, but adding StdLib::OS exposes os.execute, os.getenv, and os.exit. If only harmless time/date functions are needed, load OS and then remove execute, getenv, and exit from the os table. This is especially relevant because the manager resolves ${ENV[...]} provider keys before spawning scripts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/scripting/mod.rs` around lines 160 - 166, Update the Lua
library setup around the lua_libs configuration to avoid exposing dangerous OS
capabilities: retain StdLib::OS for required time/date functionality, then
remove os.execute, os.getenv, and os.exit from the Lua os table before scripts
run. Preserve the existing libraries and ensure the manager’s
environment-provider resolution remains unaffected.
♻️ Duplicate comments (2)
support/scripts/check-source-text.py (1)

37-42: 🎯 Functional Correctness | 🟠 Major

Scan the full source text for environment mutations.

These patterns are still evaluated one text.splitlines() entry at a time, so qualified calls split across lines and multiline use std::env::{ ... } imports bypass the guard. Scan the full text and map match offsets back to source lines while preserving the existing opt-out behavior.

Proposed direction
-	for lineno, line in enumerate(text.splitlines(), 1):
-		if any(pat.search(line) for pat in _PATTERNS):
-			yield lineno, line.strip()
+	for pat in _PATTERNS:
+		for match in pat.finditer(text):
+			lineno = text.count('\n', 0, match.start()) + 1
+			yield lineno, match.group(0).strip()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@support/scripts/check-source-text.py` around lines 37 - 42, Update the
source-scanning logic around the environment-mutation regexes to search the
complete source text rather than individual text.splitlines() entries, so
qualified calls and multiline use std::env imports are detected. Map each match
offset back to its source line for diagnostics, and preserve the existing
opt-out behavior.
support/scripts/get-all-git.py (1)

149-162: 🩺 Stability & Availability | 🟠 Major

Serialize access to the shared submodule cache.

git_common_dir(root) makes linked worktrees share the same cache, but ensure_cache() still performs the check/init/configure/fetch lifecycle without a cross-process lock. Concurrent setup can corrupt initialization or leave callers observing an incomplete cache.

Wrap the complete ensure_cache() lifecycle in a lock derived from the cache path, including initialization, remote configuration, and fetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@support/scripts/get-all-git.py` around lines 149 - 162, Update ensure_cache()
to serialize its entire cache setup lifecycle with a cross-process lock derived
from the cache path returned by cache_path_for_url(). Acquire the lock before
checking or initializing the cache, and keep remote configuration and fetch
operations inside the locked scope so concurrent callers cannot observe or
modify an incomplete cache.
🧹 Nitpick comments (3)
docs/contributing/tutorial/README.md (1)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the tutorial’s “throwaway change” wording.

“Does not matter” is ambiguous for contributors; explicitly describe this as a throwaway or low-risk change.

Suggested wording
-Learning-oriented walkthroughs: follow them start to finish, on a change that
-does not matter, before you need the how-to guides
+Learning-oriented walkthroughs: follow them start to finish on a throwaway
+change before you need the how-to guides
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/tutorial/README.md` around lines 3 - 4, Update the tutorial
description near “Learning-oriented walkthroughs” to explicitly characterize the
practice change as throwaway or low-risk, replacing the ambiguous “does not
matter” wording while preserving the instruction to complete the walkthrough
before using how-to guides.

Source: Linters/SAST tools

implementation/src/manager/run.rs (2)

2396-2400: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Storing the child in process_handle only to take it back is hard to follow.

The handle is parked at Lines 2398-2400 and unconditionally reclaimed at Lines 2441-2444 with nothing awaiting in between except a mutex lock and publish_started. If the intent is to let Ctx::request_finish kill the process during that window, a comment saying so would help; otherwise the round-trip (and the "process handle missing" error path) can be dropped.

Also applies to: 2440-2449

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 2396 - 2400, The top-level
child process is unnecessarily stored in and reclaimed from process_handle
without an explanatory purpose. Either add a concise comment around the
process_handle assignment and retrieval documenting that Ctx::request_finish may
terminate the child during this window, or remove the round-trip and its
associated “process handle missing” error path if that behavior is not required.

605-636: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

get_artifact rebuilds the whole artifact for every chunk request.

Each call copies the full stdout/stderr (or re-serializes the entire genvm_log) into a fresh Vec just to hand back at most 256 KiB. Paging a large log through the socket becomes quadratic. stdout/stderr could slice as_bytes() directly, and the JSON-lines rendering could be memoized on the done struct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 605 - 636, The get_artifact
method currently materializes the entire artifact on every chunk request,
causing inefficient paging. Refactor its field handling to slice stdout and
stderr directly from their byte representations, and reuse a memoized genvm_log
JSON-lines rendering stored on the completed result/done struct instead of
rebuilding it per call; preserve total_len, offset clamping, chunk-cap, and
unknown-field behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/contributing/howto/building/runners.md`:
- Around line 7-10: Update the documented genvm-post-install command to align
with the default venv creation mode: remove any create-venv false setting, or
explicitly add create-venv true. Keep the existing runner download and
missing-executor options unchanged.

In `@docs/contributing/tutorial/first-contribution.md`:
- Around line 24-26: Update the pre-commit guidance in the contribution tutorial
to use the complete manager hook command, replacing or clearly qualifying nix
fmt with nix develop '.?submodules=1#full' --command pre-commit run --all-files.
Keep the separate command for running all CI hooks if still applicable, and
ensure the instructions distinguish formatting from passing the full pre-commit
suite.

In `@implementation/src/manager/handlers.rs`:
- Around line 244-268: Clamp the client-supplied max_len in
handle_genvm_artifact to the shared ARTIFACT_CHUNK_CAP before calling
get_artifact, matching the websocket path’s behavior. Make ARTIFACT_CHUNK_CAP
visible to this handler (for example, pub(super)) and reuse that single constant
across both transports.

In `@implementation/src/manager/mod.rs`:
- Around line 364-369: Replace the unbounded DefaultBodyLimit::disable() in the
manager router construction with a limit derived from
app_ctx.config.max_message_bytes, capturing that value before with_state and
applying it through the body-limit layer so oversized HTTP bodies remain bounded
and return the expected 413 error shape.

In `@implementation/src/manager/run.rs`:
- Around line 2430-2433: Update the cleanup logic in run_genvm_process so
log_sink_guard is only forgotten for top-level executions, or explicitly removes
the nested run’s own GENVM_BY_ID_LOGGER entry before returning. Ensure nested
runs do not leave entries keyed by their nested genvm_id while preserving the
existing publish_started behavior for top-level runs.

---

Outside diff comments:
In `@implementation/src/scripting/mod.rs`:
- Around line 160-166: Update the Lua library setup around the lua_libs
configuration to avoid exposing dangerous OS capabilities: retain StdLib::OS for
required time/date functionality, then remove os.execute, os.getenv, and os.exit
from the Lua os table before scripts run. Preserve the existing libraries and
ensure the manager’s environment-provider resolution remains unaffected.

---

Duplicate comments:
In `@support/scripts/check-source-text.py`:
- Around line 37-42: Update the source-scanning logic around the
environment-mutation regexes to search the complete source text rather than
individual text.splitlines() entries, so qualified calls and multiline use
std::env imports are detected. Map each match offset back to its source line for
diagnostics, and preserve the existing opt-out behavior.

In `@support/scripts/get-all-git.py`:
- Around line 149-162: Update ensure_cache() to serialize its entire cache setup
lifecycle with a cross-process lock derived from the cache path returned by
cache_path_for_url(). Acquire the lock before checking or initializing the
cache, and keep remote configuration and fetch operations inside the locked
scope so concurrent callers cannot observe or modify an incomplete cache.

---

Nitpick comments:
In `@docs/contributing/tutorial/README.md`:
- Around line 3-4: Update the tutorial description near “Learning-oriented
walkthroughs” to explicitly characterize the practice change as throwaway or
low-risk, replacing the ambiguous “does not matter” wording while preserving the
instruction to complete the walkthrough before using how-to guides.

In `@implementation/src/manager/run.rs`:
- Around line 2396-2400: The top-level child process is unnecessarily stored in
and reclaimed from process_handle without an explanatory purpose. Either add a
concise comment around the process_handle assignment and retrieval documenting
that Ctx::request_finish may terminate the child during this window, or remove
the round-trip and its associated “process handle missing” error path if that
behavior is not required.
- Around line 605-636: The get_artifact method currently materializes the entire
artifact on every chunk request, causing inefficient paging. Refactor its field
handling to slice stdout and stderr directly from their byte representations,
and reuse a memoized genvm_log JSON-lines rendering stored on the completed
result/done struct instead of rebuilding it per call; preserve total_len, offset
clamping, chunk-cap, and unknown-field behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f9a45f5-9eff-45f2-a386-f83319b2e70f

📥 Commits

Reviewing files that changed from the base of the PR and between c4450a3 and 7ecb349.

⛔ Files ignored due to path filters (23)
  • crates/modules-interfaces/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • implementation/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • support/tools/genvm-tool/genvm_tool/__main__.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_configure.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_docs.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_howto.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/model.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/rst.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog_main.py is excluded by !support/tools/genvm-tool/**
  • tests/runner/genvm_tool_plugins/genvm.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/integration.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/ninja.py is excluded by !tests/**
  • tests/runner/gvm_extra/mock_host.py is excluded by !tests/**
  • tests/runner/origin/base_host.py is excluded by !tests/**
  • tests/runner/origin/host_fns.py is excluded by !tests/**
  • tests/runner/origin/logger.py is excluded by !tests/**
  • tests/runner/origin/manager_api.py is excluded by !tests/**
  • tests/system/cross-major-observability/test.py is excluded by !tests/**
  • tests/system/cross-major/test.py is excluded by !tests/**
  • tests/system/manager-socket/test.py is excluded by !tests/**
  • tests/system/permits/test.py is excluded by !tests/**
  • tests/templates/util.jsonnet is excluded by !tests/**
📒 Files selected for processing (75)
  • .claude/settings.fuzzing.json
  • .claude/skills/agentic-fuzzing/SKILL.md
  • .genvm-tool.py
  • CLAUDE.md
  • crates/modules-interfaces/Cargo.toml
  • crates/modules-interfaces/codegen/data/host-fns.json
  • crates/modules-interfaces/codegen/data/manager-api.json
  • crates/modules-interfaces/src/domain.rs
  • crates/modules-interfaces/src/host_fns.rs
  • crates/modules-interfaces/src/lib.rs
  • crates/modules-interfaces/src/manager_api.rs
  • crates/modules-interfaces/src/nested.rs
  • docs/README.md
  • docs/adr/014. manager host socket protocol.md
  • docs/adr/015. cross-major contract calls.md
  • docs/contributing/README.md
  • docs/contributing/explanation/README.md
  • docs/contributing/explanation/docs-layout.md
  • docs/contributing/explanation/executor-lines.md
  • docs/contributing/explanation/merge-model.md
  • docs/contributing/explanation/vendored-trees.md
  • docs/contributing/howto/README.md
  • docs/contributing/howto/building/build.md
  • docs/contributing/howto/building/docs.md
  • docs/contributing/howto/building/runners.md
  • docs/contributing/howto/committing/git-third-party.md
  • docs/contributing/howto/committing/runners.md
  • docs/contributing/howto/committing/submodules.md
  • docs/contributing/howto/docs/style.md
  • docs/contributing/howto/extending/add-host-function.md
  • docs/contributing/howto/extending/add-llm-provider.md
  • docs/contributing/howto/extending/add-wasi-function.md
  • docs/contributing/howto/extending/modify-runner.md
  • docs/contributing/howto/extending/modify-wasmtime.md
  • docs/contributing/howto/extending/write-a-script.md
  • docs/contributing/howto/genvm-tool.md
  • docs/contributing/howto/pr.md
  • docs/contributing/howto/releasing/release-build.md
  • docs/contributing/howto/releasing/versioning.md
  • docs/contributing/howto/setup.md
  • docs/contributing/howto/testing/README.md
  • docs/contributing/howto/testing/fuzzing.md
  • docs/contributing/howto/testing/integration.md
  • docs/contributing/howto/testing/python.md
  • docs/contributing/howto/testing/rust.md
  • docs/contributing/howto/write-a-script.md
  • docs/contributing/tutorial/README.md
  • docs/contributing/tutorial/first-contribution.md
  • docs/website/src/impl-spec/appendix/host-loop.rst
  • docs/website/src/impl-spec/appendix/index.rst
  • docs/website/src/impl-spec/appendix/manager-api.rst
  • docs/website/src/impl-spec/appendix/manager-api.yaml
  • docs/website/src/impl-spec/appendix/manager-socket-consts.rst
  • docs/website/src/impl-spec/appendix/manager-socket.rst
  • docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst
  • docs/website/src/spec/03-vm/01-startup.rst
  • docs/website/src/spec/03-vm/02-meta-properties.rst
  • docs/website/src/spec/03-vm/05-result.rst
  • docs/website/src/spec/04-contract-interface/04-upgradability.rst
  • docs/website/src/spec/appendix/constants.rst
  • executors/v0.2.x
  • executors/v0.3.x
  • implementation/Cargo.toml
  • implementation/src/manager/handlers.rs
  • implementation/src/manager/handlers_test.rs
  • implementation/src/manager/mod.rs
  • implementation/src/manager/run.rs
  • implementation/src/manager/run_test.rs
  • implementation/src/manager/socket.rs
  • implementation/src/manager/socket_test.rs
  • implementation/src/manager/versioning.rs
  • implementation/src/scripting/mod.rs
  • install/config/genvm-manager.yaml
  • support/scripts/check-source-text.py
  • support/scripts/get-all-git.py
💤 Files with no reviewable changes (2)
  • docs/contributing/howto/write-a-script.md
  • .claude/settings.fuzzing.json
🚧 Files skipped from review as they are similar to previous changes (23)
  • docs/website/src/impl-spec/appendix/index.rst
  • executors/v0.2.x
  • docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst
  • docs/website/src/spec/appendix/constants.rst
  • docs/website/src/impl-spec/appendix/manager-api.rst
  • docs/contributing/explanation/README.md
  • docs/contributing/explanation/docs-layout.md
  • docs/contributing/explanation/merge-model.md
  • docs/contributing/howto/testing/fuzzing.md
  • docs/website/src/impl-spec/appendix/manager-socket-consts.rst
  • docs/contributing/howto/extending/modify-wasmtime.md
  • docs/contributing/howto/genvm-tool.md
  • docs/contributing/howto/releasing/versioning.md
  • docs/contributing/howto/testing/python.md
  • docs/contributing/howto/building/docs.md
  • docs/contributing/howto/extending/write-a-script.md
  • docs/website/src/spec/03-vm/01-startup.rst
  • docs/contributing/howto/building/build.md
  • docs/contributing/README.md
  • docs/contributing/howto/README.md
  • docs/contributing/howto/testing/rust.md
  • docs/contributing/howto/extending/add-wasi-function.md
  • docs/contributing/howto/releasing/release-build.md

Comment thread docs/contributing/howto/building/runners.md
Comment on lines +24 to +26
5. **Pass pre-commit** — the hooks run where you commit, so never
`--no-verify`. Ahead of time: `nix fmt` for the manager, or
`./support/ci/run.sh pipeline commit-hooks` for everything, as CI does

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the full manager pre-commit command here.

nix fmt does not clearly run the complete manager hook suite, while submodules.md specifies nix develop '.?submodules=1#full' --command pre-commit run --all-files. Replace or qualify nix fmt so contributors do not mistake formatting for passing all pre-commit checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/tutorial/first-contribution.md` around lines 24 - 26,
Update the pre-commit guidance in the contribution tutorial to use the complete
manager hook command, replacing or clearly qualifying nix fmt with nix develop
'.?submodules=1#full' --command pre-commit run --all-files. Keep the separate
command for running all CI hooks if still applicable, and ensure the
instructions distinguish formatting from passing the full pre-commit suite.

Comment on lines +244 to +268
#[derive(Debug, serde::Deserialize)]
pub struct ArtifactRequest {
pub field: String,
#[serde(default)]
pub offset: u64,
pub max_len: u32,
}

pub async fn handle_genvm_artifact(
ctx: sync::DArc<AppContext>,
genvm_id: run::GenVMId,
req: ArtifactRequest,
) -> Result<Json<serde_json::Value>> {
use base64::Engine;

let artifact = ctx
.run_ctx
.get_artifact(genvm_id, &req.field, req.offset, req.max_len)?;
Ok(Json(serde_json::json!({
"genvm_id": genvm_id,
"field": req.field,
"total_len": artifact.total_len,
"data_base64": base64::engine::general_purpose::STANDARD.encode(&artifact.data),
})))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

HTTP artifact reads are unclamped, unlike the websocket path.

socket.rs clamps max_len to ARTIFACT_CHUNK_CAP (256 KiB) before calling get_artifact (Line 657), but this handler forwards the client-supplied max_len verbatim. A request with max_len=4294967295 makes the manager materialize the chunk plus a ~1.33× base64 copy, so a few concurrent calls can exhaust memory. Share one cap across both transports.

🛡️ Proposed fix
     let artifact = ctx
         .run_ctx
-        .get_artifact(genvm_id, &req.field, req.offset, req.max_len)?;
+        .get_artifact(
+            genvm_id,
+            &req.field,
+            req.offset,
+            req.max_len.min(super::socket::ARTIFACT_CHUNK_CAP),
+        )?;

(requires making ARTIFACT_CHUNK_CAP visible outside socket, e.g. pub(super))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/handlers.rs` around lines 244 - 268, Clamp the
client-supplied max_len in handle_genvm_artifact to the shared
ARTIFACT_CHUNK_CAP before calling get_artifact, matching the websocket path’s
behavior. Make ARTIFACT_CHUNK_CAP visible to this handler (for example,
pub(super)) and reuse that single constant across both transports.

Comment on lines +364 to +369
.fallback(not_found)
.method_not_allowed_fallback(not_found)
// Contract code and calldata routinely exceed the default cap, and an
// exceeded cap answers with a plain-text 413 rather than our error shape.
.layer(axum::extract::DefaultBodyLimit::disable())
.with_state(app_ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Disabling the body limit entirely makes every POST an unbounded memory sink.

Bytes extractors buffer the whole body, so with DefaultBodyLimit::disable() a single client can drive the manager to OOM. The websocket path is bounded by config.max_message_bytes; the HTTP path should use the same knob rather than no limit, which also keeps the desired JSON error shape for oversized bodies (413 only fires above the cap).

🛡️ Proposed fix
-        // Contract code and calldata routinely exceed the default cap, and an
-        // exceeded cap answers with a plain-text 413 rather than our error shape.
-        .layer(axum::extract::DefaultBodyLimit::disable())
+        // Contract code and calldata routinely exceed axum's 2 MiB default, so
+        // raise the cap to the configured message size instead of removing it.
+        .layer(axum::extract::DefaultBodyLimit::max(max_body_bytes))

with let max_body_bytes = app_ctx.config.max_message_bytes; captured before with_state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/mod.rs` around lines 364 - 369, Replace the
unbounded DefaultBodyLimit::disable() in the manager router construction with a
limit derived from app_ctx.config.max_message_bytes, capturing that value before
with_state and applying it through the body-limit layer so oversized HTTP bodies
remain bounded and return the expected 413 error shape.

Comment on lines 2430 to 2433
log_sink_guard.forget();

Ok((genvm_id, rx))
}

#[cfg(test)]
mod tests {
use super::keep_tail;

#[test]
fn under_or_at_limit_is_unchanged() {
let mut s = String::from("hello");
keep_tail(&mut s, 10);
assert_eq!(s, "hello");
keep_tail(&mut s, 5);
assert_eq!(s, "hello");
if is_top_level {
exec_ctx.publish_started();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Nested runs leak their GENVM_BY_ID_LOGGER entry.

log_sink_guard.forget() is unconditional, but the map entry inserted at Line 2236 is keyed by the nested genvm_id, while finish_execution only removes exec.id (the parent). So every nested run permanently adds an entry to the global map. Forgetting the guard should be limited to the top-level case, or the nested path should remove its own key when run_genvm_process returns.

🔒️ Proposed fix
-    log_sink_guard.forget();
     if is_top_level {
+        log_sink_guard.forget();
         exec_ctx.publish_started();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log_sink_guard.forget();
Ok((genvm_id, rx))
}
#[cfg(test)]
mod tests {
use super::keep_tail;
#[test]
fn under_or_at_limit_is_unchanged() {
let mut s = String::from("hello");
keep_tail(&mut s, 10);
assert_eq!(s, "hello");
keep_tail(&mut s, 5);
assert_eq!(s, "hello");
if is_top_level {
exec_ctx.publish_started();
}
if is_top_level {
log_sink_guard.forget();
exec_ctx.publish_started();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 2430 - 2433, Update the
cleanup logic in run_genvm_process so log_sink_guard is only forgotten for
top-level executions, or explicitly removes the nested run’s own
GENVM_BY_ID_LOGGER entry before returning. Ensure nested runs do not leave
entries keyed by their nested genvm_id while preserving the existing
publish_started behavior for top-level runs.

* feat(manager): run cross-major contract calls as nested runs ✨
* feat(manager): make cross-contract call resolution opt-in ✨
* feat(manager): refuse nested envelopes asserting unserviceable permissions 🔒️
* fix(manager): refuse a nested reply carrying any reported effect 🔒️🐛
* fix(manager): stop the call route from being consensus-visible 🐛🔒️
* refactor(manager): serve executions over an event-driven websocket ♻️
* fix(manager): harden the run socket protocol 🔒️🐛
* fix(manager): drop runtime env mutation and open the lua stdlib 🔒️
* chore(codegen): generate host-fns and socket ids from one definition ♻️
* refactor(harness): compare the executor execution hash directly ♻️
* test(harness): route a call across a major boundary from jsonnet ✅
@kp2pml30
kp2pml30 force-pushed the feat/rework-manager-api branch from 7ecb349 to 2e3032d Compare July 30, 2026 06:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
implementation/src/scripting/mod.rs (1)

160-166: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add rationale before exposing StdLib::OS to module Lua scripts.

No shipped Lua script currently uses os, while this exposes command execution, filesystem removal/rename, temp-file creation, and environment inspection to operator-authored module VMs. Only enable the specific utilities needed, such as injecting os.time/os.date/os.clock directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/scripting/mod.rs` around lines 160 - 166, Update the
`lua_libs` configuration in the module scripting setup to remove `StdLib::OS`
and avoid exposing the full OS library to module Lua scripts. Preserve only the
required standard libraries, and provide narrowly scoped time utilities such as
`os.time`, `os.date`, or `os.clock` through the existing Lua setup if needed;
add the requested rationale at the configuration point.
🧹 Nitpick comments (7)
implementation/src/manager/run.rs (3)

2106-2110: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Post-start failures are reported as FailedToStart.

supervise_genvm_inner can fail after publish_started (e.g. child.wait() error, missing process handle), and every such error funnels into fail_to_start, so a client that already received Started then receives FailedToStart for the same run. Consider emitting a Finished-style terminal event (with the recorded finish cause) when started_event is already set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 2106 - 2110, Update the error
handling around supervise_genvm_inner so failures after publish_started do not
call fail_to_start; when started_event is already set, emit the existing
Finished-style terminal event using the recorded finish cause, while preserving
fail_to_start for pre-start failures.

354-376: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Full genvm-log re-serialization per size/chunk request.

genvm_log_json_lines rebuilds the whole log on every artifact_sizes_for and every get_artifact call, so a paginated client re-serializes the entire log for each 256 KiB chunk. Caching the serialized bytes (e.g. a OnceCell<Vec<u8>> on the done result) would make chunking O(size) once instead of per request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 354 - 376, Cache the
serialized genvm-log bytes on the completed result and reuse them across
artifact_sizes_for and get_artifact calls. Update genvm_log_json_lines and the
SingleGenVMContextDone construction/access paths so serialization occurs once,
while preserving the existing newline format and error handling.

540-554: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Error kinds are carried as magic strings across a module boundary.

attach signals boot_id_mismatch/unknown_id via anyhow::bail!, and socket.rs (Line 571) recovers them with e.to_string() == "boot_id_mismatch"; get_artifact has the same pattern with "unknown_id". Any context added to these errors silently reclassifies them into the wrong protocol error code. A small enum (or anyhow::Error::downcast_ref) would make the mapping compile-checked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 540 - 554, Replace the
string-based errors in the manager methods attach and get_artifact with a shared
typed error kind, such as an enum, and preserve the boot_id_mismatch and
unknown_id variants. Update socket.rs error mapping to downcast or otherwise
match those typed variants instead of comparing e.to_string(), while retaining
the existing protocol error codes.
implementation/src/manager/socket_test.rs (1)

95-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Coverage gap: the try_send path is untested.

These tests exercise forward_events, which uses send_control_async and correctly parks. The reply/error path from dispatch uses Writer::send_control (try_send) and tears the connection down when the queue is full — the case flagged in socket.rs. A test asserting that a full control queue does not kill the connection on a malformed-frame reply would pin that behavior down.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/socket_test.rs` around lines 95 - 137, Add a test
covering the dispatch reply/error path that uses Writer::send_control, filling
the capacity-1 control queue before submitting a malformed frame. Assert the
malformed-frame reply is eventually delivered once the queued frame is consumed
and that the connection remains usable afterward, covering the full-queue
try_send behavior in dispatch rather than only forward_events.
implementation/src/manager/run_test.rs (1)

340-378: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider trimming ROUNDS or documenting the cost.

10 000 rounds each allocate a full SingleGenVMContext (watch channel, semaphores, notifies) that stay live in known_executions for the whole test, plus a rendezvous handshake per round. A smaller sweep gives essentially the same race coverage at a fraction of the CI time/memory.

Also, the comment on Line 351-354 claims no panics inside the scope, but start_tx.send(()).unwrap() on Line 378 is one (it is unreachable only because round counts match the publisher loop).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run_test.rs` around lines 340 - 378, Reduce the
ROUNDS count in this race test or document why 10,000 iterations are necessary,
while preserving meaningful race coverage and lowering CI resource usage. In the
scoped loop around start_tx.send, remove the unwrap-based panic path by handling
a failed send consistently with the publisher’s early-exit behavior, and update
the nearby no-panics comment to match the implementation.
implementation/Cargo.toml (1)

33-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin axum to a concrete 0.8.x version.

implementation/Cargo.lock has axum 0.8.9, but implementation/Cargo.toml uses the rolling cxum = { version = "0.8", features = ["ws"] } requirement, so future lockfile regenerations can change the dependency graph unexpectedly. Pin it to 0.8.9 here as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/Cargo.toml` at line 33, Update the axum dependency declaration
in Cargo.toml to require the concrete 0.8.9 version while preserving the
existing ws feature.
docs/website/src/spec/03-vm/01-startup.rst (1)

236-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cross-reference error_inval as a code symbol.

This result-path text uses a bare literal for ``error_inval``` while nearby spec passages link VM error conditions with :ref: targets. Use the matching error-code reference target so the spec remains cross-linkable if the error name is changed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/website/src/spec/03-vm/01-startup.rst` around lines 236 - 240, Update
the result-path text in the startup specification to replace the bare
``error_inval`` literal with the matching cross-reference target used for VM
error codes, while preserving the existing wording and behavior description.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/contributing/howto/testing/integration.md`:
- Around line 59-69: Update the documentation paragraph describing --ignore-hash
to clarify that it skips comparison with stored .hash sidecars and prevents
their creation, while runtime leader-versus-validator/sync cross-mode comparison
remains enabled.

In `@docs/website/src/impl-spec/appendix/host-loop.rst`:
- Around line 40-44: Update the host-loop documentation around
resolve_callcontract_executor to state that a null reply only keeps locally
served majors in-process; callers unable to serve the advisory major must mint a
major selector and delegate the CallContract instead. Preserve the existing
hook_cross_contract_calls and host-0 routing description.

In `@docs/website/src/spec/03-vm/01-startup.rst`:
- Around line 19-21: Update the startup specification around the root-slot and
locked-slot read description to state the combined condition: the run must be
allowed to write storage and the sender must not be in upgraders. Add a
cross-reference to :ref:`gvm-def-locked-slot-nesting` so this rule remains
consistent with the upgradability specification.

In `@docs/website/src/spec/04-contract-interface/04-upgradability.rst`:
- Around line 54-59: Disambiguate the public-ABI version reference in the
upgradability documentation by linking or qualifying CURRENT_MAJOR with its
constants-appendix entry, distinct from manager_api::CURRENT_MAJOR. Update the
paragraph around the contract major-version comparison while preserving the
existing mismatch behavior and references.

In `@implementation/src/manager/handlers.rs`:
- Around line 233-236: Remove the unconditional ctx.run_ctx.ack(genvm_id) call
from handle_genvm_status so reading an available status does not consume the
execution. If legacy behavior must remain supported, only invoke ack when an
explicit legacy-query flag is present, while preserving status retrieval for
normal GET requests.

In `@implementation/src/manager/run.rs`:
- Around line 1246-1260: Replace the unbounded u32::MAX limits passed by
consume_result and run_nested with a concrete manager-side cap, preferably
derived from config.max_message_bytes, and pass that cap to
read_length_prefixed. Ensure read_length_prefixed rejects frames above the cap
before allocation, preserving normal reads for valid frames and preventing
oversized or truncated child-controlled headers from causing large manager
allocations.

---

Outside diff comments:
In `@implementation/src/scripting/mod.rs`:
- Around line 160-166: Update the `lua_libs` configuration in the module
scripting setup to remove `StdLib::OS` and avoid exposing the full OS library to
module Lua scripts. Preserve only the required standard libraries, and provide
narrowly scoped time utilities such as `os.time`, `os.date`, or `os.clock`
through the existing Lua setup if needed; add the requested rationale at the
configuration point.

---

Nitpick comments:
In `@docs/website/src/spec/03-vm/01-startup.rst`:
- Around line 236-240: Update the result-path text in the startup specification
to replace the bare ``error_inval`` literal with the matching cross-reference
target used for VM error codes, while preserving the existing wording and
behavior description.

In `@implementation/Cargo.toml`:
- Line 33: Update the axum dependency declaration in Cargo.toml to require the
concrete 0.8.9 version while preserving the existing ws feature.

In `@implementation/src/manager/run_test.rs`:
- Around line 340-378: Reduce the ROUNDS count in this race test or document why
10,000 iterations are necessary, while preserving meaningful race coverage and
lowering CI resource usage. In the scoped loop around start_tx.send, remove the
unwrap-based panic path by handling a failed send consistently with the
publisher’s early-exit behavior, and update the nearby no-panics comment to
match the implementation.

In `@implementation/src/manager/run.rs`:
- Around line 2106-2110: Update the error handling around supervise_genvm_inner
so failures after publish_started do not call fail_to_start; when started_event
is already set, emit the existing Finished-style terminal event using the
recorded finish cause, while preserving fail_to_start for pre-start failures.
- Around line 354-376: Cache the serialized genvm-log bytes on the completed
result and reuse them across artifact_sizes_for and get_artifact calls. Update
genvm_log_json_lines and the SingleGenVMContextDone construction/access paths so
serialization occurs once, while preserving the existing newline format and
error handling.
- Around line 540-554: Replace the string-based errors in the manager methods
attach and get_artifact with a shared typed error kind, such as an enum, and
preserve the boot_id_mismatch and unknown_id variants. Update socket.rs error
mapping to downcast or otherwise match those typed variants instead of comparing
e.to_string(), while retaining the existing protocol error codes.

In `@implementation/src/manager/socket_test.rs`:
- Around line 95-137: Add a test covering the dispatch reply/error path that
uses Writer::send_control, filling the capacity-1 control queue before
submitting a malformed frame. Assert the malformed-frame reply is eventually
delivered once the queued frame is consumed and that the connection remains
usable afterward, covering the full-queue try_send behavior in dispatch rather
than only forward_events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8faad9b9-bdd8-453c-bc07-3fb12f6caa1c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ecb349 and 2e3032d.

⛔ Files ignored due to path filters (23)
  • crates/modules-interfaces/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • implementation/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • support/tools/genvm-tool/genvm_tool/__main__.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_configure.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_docs.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/cmd_howto.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/model.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/codegen/rst.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog.py is excluded by !support/tools/genvm-tool/**
  • support/tools/genvm-tool/genvm_tool/tests/util/watchdog_main.py is excluded by !support/tools/genvm-tool/**
  • tests/runner/genvm_tool_plugins/genvm.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/integration.py is excluded by !tests/**
  • tests/runner/genvm_tool_plugins/ninja.py is excluded by !tests/**
  • tests/runner/gvm_extra/mock_host.py is excluded by !tests/**
  • tests/runner/origin/base_host.py is excluded by !tests/**
  • tests/runner/origin/host_fns.py is excluded by !tests/**
  • tests/runner/origin/logger.py is excluded by !tests/**
  • tests/runner/origin/manager_api.py is excluded by !tests/**
  • tests/system/cross-major-observability/test.py is excluded by !tests/**
  • tests/system/cross-major/test.py is excluded by !tests/**
  • tests/system/manager-socket/test.py is excluded by !tests/**
  • tests/system/permits/test.py is excluded by !tests/**
  • tests/templates/util.jsonnet is excluded by !tests/**
📒 Files selected for processing (76)
  • .claude/settings.fuzzing.json
  • .claude/skills/agentic-fuzzing/SKILL.md
  • .genvm-tool.py
  • CLAUDE.md
  • crates/modules-interfaces/Cargo.toml
  • crates/modules-interfaces/codegen/data/host-fns.json
  • crates/modules-interfaces/codegen/data/manager-api.json
  • crates/modules-interfaces/src/domain.rs
  • crates/modules-interfaces/src/host_fns.rs
  • crates/modules-interfaces/src/lib.rs
  • crates/modules-interfaces/src/manager_api.rs
  • crates/modules-interfaces/src/nested.rs
  • docs/README.md
  • docs/adr/014. manager host socket protocol.md
  • docs/adr/015. cross-major contract calls.md
  • docs/contributing/README.md
  • docs/contributing/explanation/README.md
  • docs/contributing/explanation/docs-layout.md
  • docs/contributing/explanation/executor-lines.md
  • docs/contributing/explanation/merge-model.md
  • docs/contributing/explanation/vendored-trees.md
  • docs/contributing/howto/README.md
  • docs/contributing/howto/building/build.md
  • docs/contributing/howto/building/docs.md
  • docs/contributing/howto/building/runners.md
  • docs/contributing/howto/committing/git-third-party.md
  • docs/contributing/howto/committing/runners.md
  • docs/contributing/howto/committing/submodules.md
  • docs/contributing/howto/docs/style.md
  • docs/contributing/howto/extending/add-host-function.md
  • docs/contributing/howto/extending/add-llm-provider.md
  • docs/contributing/howto/extending/add-wasi-function.md
  • docs/contributing/howto/extending/modify-runner.md
  • docs/contributing/howto/extending/modify-wasmtime.md
  • docs/contributing/howto/extending/write-a-script.md
  • docs/contributing/howto/genvm-tool.md
  • docs/contributing/howto/pr.md
  • docs/contributing/howto/releasing/release-build.md
  • docs/contributing/howto/releasing/versioning.md
  • docs/contributing/howto/setup.md
  • docs/contributing/howto/testing/README.md
  • docs/contributing/howto/testing/fuzzing.md
  • docs/contributing/howto/testing/integration.md
  • docs/contributing/howto/testing/python.md
  • docs/contributing/howto/testing/rust.md
  • docs/contributing/howto/write-a-script.md
  • docs/contributing/tutorial/README.md
  • docs/contributing/tutorial/first-contribution.md
  • docs/website/src/impl-spec/appendix/host-loop.rst
  • docs/website/src/impl-spec/appendix/index.rst
  • docs/website/src/impl-spec/appendix/manager-api.rst
  • docs/website/src/impl-spec/appendix/manager-api.yaml
  • docs/website/src/impl-spec/appendix/manager-socket-consts.rst
  • docs/website/src/impl-spec/appendix/manager-socket.rst
  • docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst
  • docs/website/src/spec/03-vm/01-startup.rst
  • docs/website/src/spec/03-vm/02-meta-properties.rst
  • docs/website/src/spec/03-vm/05-result.rst
  • docs/website/src/spec/04-contract-interface/04-upgradability.rst
  • docs/website/src/spec/appendix/constants.rst
  • executors/v0.2.x
  • executors/v0.3.x
  • implementation/Cargo.toml
  • implementation/src/manager/handlers.rs
  • implementation/src/manager/handlers_test.rs
  • implementation/src/manager/mod.rs
  • implementation/src/manager/run.rs
  • implementation/src/manager/run_test.rs
  • implementation/src/manager/socket.rs
  • implementation/src/manager/socket_test.rs
  • implementation/src/manager/versioning.rs
  • implementation/src/manager/versioning_test.rs
  • implementation/src/scripting/mod.rs
  • install/config/genvm-manager.yaml
  • support/scripts/check-source-text.py
  • support/scripts/get-all-git.py
💤 Files with no reviewable changes (2)
  • .claude/settings.fuzzing.json
  • docs/contributing/howto/write-a-script.md
🚧 Files skipped from review as they are similar to previous changes (24)
  • docs/website/src/impl-spec/appendix/index.rst
  • docs/website/src/spec/03-vm/05-result.rst
  • docs/website/src/spec/appendix/constants.rst
  • docs/contributing/explanation/vendored-trees.md
  • docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst
  • docs/website/src/impl-spec/appendix/manager-api.rst
  • docs/contributing/README.md
  • docs/contributing/howto/extending/modify-wasmtime.md
  • docs/contributing/howto/building/build.md
  • docs/contributing/howto/building/runners.md
  • docs/contributing/howto/extending/add-wasi-function.md
  • docs/contributing/howto/committing/runners.md
  • docs/contributing/howto/README.md
  • docs/contributing/howto/building/docs.md
  • docs/contributing/howto/testing/rust.md
  • docs/contributing/howto/genvm-tool.md
  • docs/contributing/howto/testing/fuzzing.md
  • docs/contributing/howto/releasing/release-build.md
  • docs/contributing/howto/docs/style.md
  • docs/contributing/howto/releasing/versioning.md
  • docs/contributing/explanation/executor-lines.md
  • docs/website/src/impl-spec/appendix/manager-socket.rst
  • docs/website/src/impl-spec/appendix/manager-api.yaml
  • install/config/genvm-manager.yaml

Comment on lines +59 to +69
`--ignore-hash` skips both comparison and sidecar creation — needed in runner
dev mode, where every execution hash moves
([modify-runner.md](../extending/modify-runner.md)), not for a new case. A line
can stop tracking sidecars altogether by returning `{'save-hashes': False}` from
`integration()` in its `.genvm-tool.py`, as v0.3.x currently does while its
hashes still move

Neither switch turns off the leader-vs-validator/sync comparison: with sidecars
disabled a non-main mode is compared against the main mode's hash from the same
run instead. Only that comparison makes a determinism regression visible, and a
non-main step has no semantics goldens of its own, so without it the step would

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify which hash comparison --ignore-hash disables.

“Skips both comparison and sidecar creation” conflicts with the following statement that leader/validator/sync comparison remains enabled. Specify that it skips the stored .hash sidecar comparison and creation, while runtime cross-mode comparison still runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/howto/testing/integration.md` around lines 59 - 69, Update
the documentation paragraph describing --ignore-hash to clarify that it skips
comparison with stored .hash sidecars and prevents their creation, while runtime
leader-versus-validator/sync cross-mode comparison remains enabled.

Comment on lines +40 to +44
``resolve_callcontract_executor`` moves to host 0 only when the run request sets
``hook_cross_contract_calls`` (see :doc:`manager-socket`). Otherwise the manager
answers it with a null reply, which keeps every ``CallContract`` in-process. A
host that does not route calls across major boundaries therefore need not
implement that method at all.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the null-resolution behavior.

Line 42 incorrectly says a null reply keeps every CallContract in-process. A caller that cannot serve the advisory major must mint a major selector and delegate; only locally served majors stay in-process.

Proposed fix
-answers it with a null reply, which keeps every ``CallContract`` in-process. A
-host that does not route calls across major boundaries therefore need not
-implement that method at all.
+answers it with a null reply. The caller keeps the call in-process only when it
+serves the advisory major; otherwise it mints a major selector and delegates,
+where the selected line produces the canonical mismatch if no compatible line
+is installed. A host that does not route calls across major boundaries therefore
+need not implement that method at all.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
``resolve_callcontract_executor`` moves to host 0 only when the run request sets
``hook_cross_contract_calls`` (see :doc:`manager-socket`). Otherwise the manager
answers it with a null reply, which keeps every ``CallContract`` in-process. A
host that does not route calls across major boundaries therefore need not
implement that method at all.
``resolve_callcontract_executor`` moves to host 0 only when the run request sets
``hook_cross_contract_calls`` (see :doc:`manager-socket`). Otherwise the manager
answers it with a null reply. The caller keeps the call in-process only when it
serves the advisory major; otherwise it mints a major selector and delegates,
where the selected line produces the canonical mismatch if no compatible line
is installed. A host that does not route calls across major boundaries therefore
need not implement that method at all.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/website/src/impl-spec/appendix/host-loop.rst` around lines 40 - 44,
Update the host-loop documentation around resolve_callcontract_executor to state
that a null reply only keeps locally served majors in-process; callers unable to
serve the advisory major must mint a major selector and delegate the
CallContract instead. Preserve the existing hook_cross_contract_calls and host-0
routing description.

Comment on lines +19 to +21
#. Reads the contract's root-slot data, and its locked slots when the run may
write storage. Locked slots only constrain writes, so a run without that
permission does not read them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the locked-slot read condition with the upgradability page.

Here the read is gated on "the run may write storage"; docs/website/src/spec/04-contract-interface/04-upgradability.rst (Lines 16-18) gates it on the sender not being in upgraders. Both are presumably conjuncts, but as written a reader gets two different rules. Consider stating the combined condition here and linking to :ref:gvm-def-locked-slot-nesting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/website/src/spec/03-vm/01-startup.rst` around lines 19 - 21, Update the
startup specification around the root-slot and locked-slot read description to
state the combined condition: the run must be allowed to write storage and the
sender must not be in upgraders. Add a cross-reference to
:ref:`gvm-def-locked-slot-nesting` so this rule remains consistent with the
upgradability specification.

Comment on lines +54 to +59
:ref:`gvm-def-consts-value-root-offsets-major` and stores the public-ABI major
version that the contract was built against. Top-level and runner loads compare
this byte to ``CURRENT_MAJOR`` and fail with
:ref:`gvm-def-str-trie-value-vm-error-invalid-contract-major-mismatch` when it
differs. :ref:`gvm-def-gl-call-call-contract` may instead delegate the callee to
another executor selected by the host.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Disambiguate CURRENT_MAJOR.

The name is also used for the manager socket protocol major (manager_api::CURRENT_MAJOR, implementation/src/manager/socket.rs Lines 306-315). Since this one is the public-ABI major, link it to its constants-appendix entry (or qualify it in prose) so the two are not read as the same value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/website/src/spec/04-contract-interface/04-upgradability.rst` around
lines 54 - 59, Disambiguate the public-ABI version reference in the
upgradability documentation by linking or qualifying CURRENT_MAJOR with its
constants-appendix entry, distinct from manager_api::CURRENT_MAJOR. Update the
paragraph around the contract major-version comparison while preserving the
existing mismatch behavior and references.

Comment on lines +233 to +236
let status = ctx.run_ctx.status(genvm_id);
if status.is_some() {
ctx.run_ctx.ack(genvm_id);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Do callers/docs expect GET status to consume the execution?
rg -n -C3 'genvm_id.*artifact|/genvm/' docs/website/src/impl-spec/appendix/manager-api.yaml
rg -n -C3 'handle_genvm_status|\back\(' implementation/src --type=rust

Repository: genlayerlabs/genvm-manager

Length of output: 5260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## run.rs ack/status implementations"
sed -n '520,600p' implementation/src/manager/run.rs | cat -n

echo
echo "## handlers.rs status handler"
sed -n '208,280p' implementation/src/manager/handlers.rs | cat -n

echo
echo "## api spec around /genvm/{genvm_id}"
sed -n '220,235p' docs/website/src/impl-spec/appendix/manager-api.yaml | cat -n

echo
echo "## websocket ack routing"
sed -n '600,635p' implementation/src/manager/socket.rs | cat -n

echo
echo "## StatusRequest definition"
rg -n -C5 'struct StatusRequest|pub struct StatusRequest|StatusRequest' implementation/src/manager -g '*.rs'

Repository: genlayerlabs/genvm-manager

Length of output: 13686


Do not auto-ack when returning status via /genvm/{id}.

handle_genvm_status currently calls ctx.run_ctx.ack(genvm_id) whenever status is present; ack() removes the execution and releases the token, so the first successful GET also makes retries/monitoring reads return null. Gate auto-ack behind an explicit legacy-query flag if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/handlers.rs` around lines 233 - 236, Remove the
unconditional ctx.run_ctx.ack(genvm_id) call from handle_genvm_status so reading
an available status does not consume the execution. If legacy behavior must
remain supported, only invoke ack when an explicit legacy-query flag is present,
while preserving status retrieval for normal GET requests.

Comment on lines +1246 to +1260
async fn read_length_prefixed<R: tokio::io::AsyncRead + Unpin>(
reader: &mut R,
max_len: usize,
) -> anyhow::Result<Vec<u8>> {
use tokio::io::AsyncReadExt;

match proc.try_wait() {
Ok(Some(status)) => {
log_debug!(id = exec.id, status = status; "genvm exited");
let len = reader.read_u32_le().await? as usize;
anyhow::ensure!(
len <= max_len,
"manager host frame is too large: {len} > {max_len}"
);
let mut data = vec![0; len];
reader.read_exact(&mut data).await?;
Ok(data)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

u32::MAX frame cap lets the child force a 4 GiB allocation in the manager.

read_length_prefixed allocates vec![0; len] up-front, and both consume_result and run_nested pass u32::MAX as max_len. The comment argues the bound is the child's memory limit, but the allocation happens in the manager process, which has no such limit — and the child is the process running untrusted contract code. A single bogus length header (or a truncated frame after a large header) is enough to spike manager RSS; with several concurrent runs it is an OOM.

Consider a concrete cap (e.g. tied to config.max_message_bytes) and/or incremental reads with try_reserve instead of a single pre-sized allocation.

🛡️ Sketch
-                    let data = match read_length_prefixed(&mut reader, u32::MAX as usize).await {
+                    let data = match read_length_prefixed(&mut reader, MAX_HOST_FRAME_LEN).await {

Also applies to: 1467-1467, 1491-1491

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@implementation/src/manager/run.rs` around lines 1246 - 1260, Replace the
unbounded u32::MAX limits passed by consume_result and run_nested with a
concrete manager-side cap, preferably derived from config.max_message_bytes, and
pass that cap to read_length_prefixed. Ensure read_length_prefixed rejects
frames above the cap before allocation, preserving normal reads for valid frames
and preventing oversized or truncated child-controlled headers from causing
large manager allocations.

@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

1 similar comment
@kp2pml30

Copy link
Copy Markdown
Member Author

/run-e2e

@coderabbitai coderabbitai Bot mentioned this pull request Jul 31, 2026
@github-actions github-actions Bot added the not rebased branch is behind its base; rebase before it can be merged label Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-safe not rebased branch is behind its base; rebase before it can be merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant