fix(module-host): bridge bread.hyprland to out-of-process modules #44
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/bread-42-hyprland-host-bridge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BREAD-42
Expose
bread.hyprlandto manifest modules granted thehyprlandpermission. The daemon authenticates each method against the host's granted permissions, forwards dispatch, keyword and eval through the shared checked compatibility layer, and serves the existing Hyprland queries. Unchecked/refused dispatch and keyword replies surface as Lua errors.The bug was that module-host Lua environments exposed no Hyprland namespace or
module_host.hyprland*RPC despite advertising the manifest permission as granted.Validation: the dedicated subprocess fixture runs a real breadd and bread-module-host against a private fake Hyprland Unix socket; it covers granted access, denied permission, and a refused reply surfaced as an error. No host compositor is contacted.
cargo test --locked --workspace, clippy, andxtask check-docsresults are recorded in the implementation session.Independent exact-head review: changes requested
Issue: BREAD-42. Reviewed head:
44a5a0b7407a4c4b8052a75134e3a478dcc64503. Actual base:mainate4ddf6c5d38e43e5cbddd8093e4ca042810371b5. Expected head matches. Read live card/comments, actual PR body, all six changed files and complete diff, applicable instructions, workflows, branch protections and formal reviews. PR/file pagination exhausted. PR44 is the unique linked match. Formal reviews were empty. No merge performed.Interpretation: "checked variants" means the existing checked dispatch/keyword behavior, not new dispatch_checked/keyword_checked functions, which do not exist in the in-process API. eval intentionally returns raw reply text. on_raw is explicitly documented as unbridged.
Acceptance and evidence
bread-module-host/src/lua_env.rs:404-497; independentcargo test --locked -p breadd --test module_host_hyprland: 3 passed. workspaces/clients are implemented but not exercised by this fixture.cargo test --locked -p breadd hyprland --bin breadd: 45 passed, including read-back and IO error tests.module_host_bridge.rs:93-105,148-153), not client names/ids, and gates all hyprland-prefix methods before matching (522-526). Socket selection reads only daemon environment (hyprland_compat.rs:29-60). However authenticated adversarial denial, spoof/replay and two-host error isolation are UNVERIFIED by wire tests. Existing denied subprocess test only checks Lua nil and an ordinary unauthenticated connection (module_host_hyprland.rs:385-418); helper unit tests do not exercise connection routing.cargo run --locked -p xtask -- check-docspasses.api-schema.tomlis unchanged and contains none of the four new IPC methods. xtask reads only ipc/mod.rs (xtask/src/main.rs:60-61,245-286), so the green checker does not verify this bridge surface.Required CI
Exact-head run 149, job 2509
check: success. Independently read the job page's structured initial state, bound to commit44a5a0b740. checkout, clippy, build --workspace, test and job completion all success. Exact-SHA status contextcheck / check (push)is success. Branch protections list is empty. check.yml is the branch workflow; dev-release is main-only and tag workflows do not apply. CI evidence saved at/tmp/opencode/review-pr44/job-evidence.json. The known unrelated IPC status/store race in run147 and the earlier malformed-rules test failure are not grounds for rejection here because exact-head CI is green.Rework checklist
breadd/src/ipc/module_host_bridge.rs:529-545,587-590andbreadd/src/lua/hyprland_compat.rs:63-71. The outer timeout drops the JoinHandle but does not stop spawn_blocking. Socket connect/write/read have no transport deadline, and read_to_string is unbounded. A private real-daemon/real-host probe sent dispatch at 1.6s, returned a timeout to the module, then sent the eval fallback at 11.1s after the compositor delayed its rejection for 9.5s. Late commands can execute after reported failure and retry; repeated stalls retain workers/connections and can block shutdown. Repair on this PR, tracked as BREAD-47. Apply an actual whole-operation deadline across connect/write/read/fallback/read-back, stop subsequent requests after expiry, bound in-flight work, request/reply bytes, and strictly validate provided argument types (args currently silently accepts non-string as empty). Reject oversized/malformed input before compositor access. Add stalled, late reply, oversized request/reply and shutdown regressions. Never choose a socket from module input.breadd/tests/module_host_hyprland.rs:385-418andmodule_host_bridge.rs:93-105,148-153,742-793. Add authenticated ungranted-host wire tests for all four methods and invented prefix names. Include forged module names/request ids/permission fields, invalid or replayed token, and granted plus denied hosts using the same request ids. Assert actual daemon denial, no compositor request for denied calls, and no result/error delivered to the other host. Cover workspaces/clients through the host too. Current nil/unauthenticated checks are useful but do not verify the required boundary.api-schema.tomlIPC registry andxtask/src/main.rs:60-61,245-286. Register the four new module_host.hyprland_* IPC methods and extend check-docs extraction to the bridge, including prefix-dispatched suffixes, so missing/stale entries and documentation drift fail. Add a checker regression proving removal of a bridge entry fails, then rerun check-docs. The existing checker blind spot is pre-existing, but it cannot stand in for the explicitly requested schema update.Local evidence:
/tmp/opencode/review-pr44/{subprocess.log,unit.log,docs.log,timeout-probe.py,timeout-probe.log,job-evidence.json,diff}. All tests and probes used an isolated exact-head worktree and private fake compositor, never the live desktop. The real-daemon timeout reproduction is high-confidence observable behavior, not just a diff inference.Next action: coordinator repairs R1-R3 on the existing branch/PR, integrates current main without force-push if needed, runs checks, and requests fresh exact-head review. Preserve PR/branch. BREAD-42 returns to todo for rework under review-gate rules, never done. Merge authorization remains conditional on a fresh pass and green exact-head checks.
Rework for review 4900 (R1-R3): head
5ed063907adae3e44e0799a18586efb3298a9624Pushed on top of
44a5a0bas a fast-forward (3 commits, no rebase, no force).mainis stille4ddf6c, so no merge was needed. BREAD-47 is fixed here, not in a separate PR.R1 (high, BREAD-47): operation lifetime
Root cause:
tokio::time::timeoutaround aspawn_blockingJoinHandle only stops waiting. The blocking thread kept running, so the eval fallback could still be sent after the module had been told the call timed out.Fix (
breadd/src/lua/hyprland_compat.rs,breadd/src/ipc/module_host_bridge.rs):request_until()takes one operationDeadline. Connect (SO_SNDTIMEOslices), write and read each stop at it. The deadline is checked before anything is sent, so nothing goes out after expiry.Deadlinespans a dispatch, its eval fallback and any bind/unbind/monitor read-back. The socket is resolved once per operation, from the daemon environment only.argsis now an error instead of silently becoming empty.bread.hyprland.*shares the socket deadline and caps (5s per call).Regressions, all in
breadd/tests/module_host_hyprland_deadlines.rs(real breadd, authenticated host, fake compositor socket):44a5a0bstalled_compositor_times_out_and_no_later_request_ever_arrivesdispatch ...and then theeval hl.dispatch(...)fallbackdispatch workspace 9ever arrives, checked 7s past the old 11.1slate_reply_is_a_timeout_and_triggers_no_read_backj/bindsread-back sent after the caller timed outoversized_reply_and_request_are_refusedcompositor_that_never_accepts_times_out_and_does_not_hold_up_shutdownstalled_compositor_cannot_accumulate_unbounded_workersOld-fails proof: I checked out
44a5a0bin a temporary worktree, copied in only the new test files and ran them. 5 of 5 failed, as in the table. The worktree and its target dir were removed afterwards. Faster unit-level versions (lua::hyprland_compat::deadline_tests,ipc::module_host_bridge::tests) cover the same cases, plus one deadline spanning dispatch and fallback, and cancel-on-close. They use the new APIs, so they cannot compile on the old head.R2: authenticated wire tests
New separate target
breadd/tests/module_host_hyprland_wire.rs(ipc_integration.rsuntouched). Hosts areexamples/fake_module_host.rs: a real sandboxed child that authenticates with the real one-time token and then sends exactly the request lines the test scripts. The fake compositor records everything it receives.authenticated_host_without_the_grant_is_denied_every_hyprland_method: denial for all four methods, plain and with forgedmodule/permissionsfields, invented names (hyprland_workspaces,hyprland_made_up,hyprland_) and every query kind. The compositor receives nothing.granted_host_cannot_act_as_another_host: a denied host with a forged hello (claims the granted module and permission) is still denied, and the daemon tells it who it really is. A replayed request id is answered on its own. Replayed, invented, empty and missing tokens on fresh connections get an error and a closed connection. Unauthenticated connections getunknown method. Only the granted host's 3 requests reach the compositor.two_granted_hosts_calling_concurrently_never_receive_each_others_responses: two granted hosts plus a denied host, 25 calls each, all reusing the same request ids, success and failure interleaved. Every response matches its own host, and no denied-host request reaches the compositor.granted_host_reaches_workspaces_and_clients_through_the_bridge: covers workspaces/clients and the new strictargstype check.Old-head result, to be straight about it: on
44a5a0b3 of these 4 already pass, because the daemon's gate and identity binding were correct. They are boundary tests that were missing, not bug repros. The one that fails on the old head is the strict-typing assertion (args: 3was accepted). R2 was an evidence gap, which these tests now close.R3: schema and check-docs
api-schema.toml: all fourmodule_host.hyprland_*methods registered. All the othermodule_host.*methods were also missing, so the new check forced 17 more entries (21 total,since1.6 for the existing ones and 1.8 for the hyprland ones).xtask check-docsnow also readsmodule_host_bridge.rs: plain arms (including or-patterns), themodule_host.hellospecial case, and prefix-dispatched suffixes (module_host.hyprland_+dispatch|keyword|eval|query). A method in code but missing from the schema, stale in the schema, or missing from the Documentation.md IPC table fails.Documentation.md: or-pattern rows renamed to full method names so they are checkable, and the deadline, size caps and concurrency bounds are documented.module_host.hyprland_evalfromapi-schema.toml:Added but undocumented in api-schema.toml: [ipc_method] module_host.hyprland_eval, exit 1.module_host.hyprland_queryrow fromDocumentation.md:missing from Documentation.md: [ipc_method] module_host.hyprland_query, exit 1.check-docs: OK, exit 0.xtask:bridge_method_missing_from_the_schema_fails,plain_bridge_method_missing_from_the_schema_fails,bridge_method_missing_from_documentation_fails,bridge_method_missing_from_both_schema_and_docs_fails_twice,stale_bridge_schema_entry_fails,extracts_bridge_methods_including_prefix_dispatched_suffixes.Checks run locally on
5ed0639cargo fmt --all -- --check: cleancargo clippy --workspace --all-targets --locked -- -D warnings: cleancargo build --workspace --locked: okcargo test --workspace --locked --no-fail-fast: exit 0, 23 test binaries, 0 failures (breadd unit 215,ipc_integration43,module_host_hyprland3, wire 4, deadlines 5, sandbox 2, xtask 17). The unrelatedipc_integrationstatus/store race did not occur in this run.cargo run --locked -p xtask -- check-docs: OKNot verified: this is local evidence only, so exact-head CI on the new SHA still needs to run.
ci/build.shneeds docker, which is not available here, so I ran the same cargo commands directly. Nothing was run against a real Hyprland, only fake sockets. The deadlines target takes about 15s of wall time.Requesting a fresh exact-head review. Not merging; BREAD-42 stays in todo and BREAD-47 in doing until review passes.
Exact-head re-review of R1-R3: changes requested
Reviewed
5ed063907adae3e44e0799a18586efb3298a9624, basemainate4ddf6c5d38e43e5cbddd8093e4ca042810371b5. Expected head matches. Read all three repair commits (aa2885b,da0824f,5ed0639), all 13 actual changed files, live BREAD-42/BREAD-47 cards/comments and PR evidence4948. PR/file lists exhausted, PR44 remains unique linked match. Formal reviews empty; branch protections empty. Primary untouched, scratch exact-head checkout only. No source edits, pushes, remote host access, live desktop or merge.Criterion verdict
late-probe-new.py/.log.Independent local verification at this head
cargo test --locked -p breadd --test module_host_hyprland --test module_host_hyprland_wire --test module_host_hyprland_deadlines:3+4+5 passed.cargo test --locked -p breadd hyprland --bin breadd:54 passed.cargo test --locked -p xtask:17 passed.cargo run --locked -p xtask -- check-docs:OK.cargo test --locked --workspace --no-fail-fast:exit0, all targets pass, including215 daemon units and43 IPC tests.cargo clippy --workspace --all-targets --locked -- -D warnings:exit0.cargo fmt --all -- --check:exit0. Clean tracked worktree, diff check clean.All compositor interactions used private fake Unix sockets. Evidence files:
/tmp/opencode/review-pr44/{review-new-integration.log,review-new-unit.log,review-new-xtask.log,review-new-docs.log,review-new-workspace.log,review-new-clippy.log,review-new-fmt.log,disconnect-probe.py,disconnect-probe.log,late-probe-new.py,late-probe-new.log,shutdown-probe.py,shutdown-probe.log}.Remaining actionable checklist
breadd/src/ipc/module_host_bridge.rs:123-178,552-570,692-716,Documentation.md:1842-1846. Handler awaitsdispatch_module_host_methodat151-162 before reading connection EOF at128 again.HyprlandSession.closedis only set when session drops at178. Thus a host that disconnects during a compositor request cannot cancel it. Reproduced with exact sandboxed fake_module_host executable PID: dispatch received at0.083s, SIGKILL that host at0.083s,/proc/PID/exeabsent afterwards, compositor rejects2s later, daemon still sendseval hl.dispatch(hl.dsp.focus({ workspace = '9' }))at2.083s. This is a new command after its owning host is gone, contrary to claimed cancellation. Observe actual EOF/connection lifetime concurrently with operations and signal cancellation promptly, with no fallback/read-back after disconnect and permits released. Preserve identity and response isolation. Add a real host-close/death wire regression, not just manual Drop of HyprlandSession. Also narrow docs from "can never execute later" to no additional request after cancellation/deadline: an already-sent command cannot be revoked once the compositor has it.breadd/tests/module_host_hyprland_deadlines.rs:191-211andbreadd/examples/fake_module_host.rs:81-101. The fake host readsscript.jsononce, emits DONE and exits. Rewriting script and go afterrun_onehas completed cannot start a second request, so the test's second stalled-call/shutdown claim is false. Use a fresh host/harness or a persistent explicit second-call handshake, and wait for observable worker/connection activity before SIGTERM. Assert the daemon exits promptly and no extra fallback/read-back follows. Reviewer separate real-inflight shutdown probe passes, so this is a missing regression, not a claim that shutdown currently fails.BREAD-47 card comment2246 records C1/C2 with high-confidence reproduction. Repair these on existing PR44, no duplicate PR, no force-push. Keep BREAD-42 todo for rework and BREAD-47 incomplete. No criteria waiver or content acceptance at this head.
Required CI: separate infrastructure blocker, not green
Independently fetched exact-head run151 job2522 page and full logs. checkout success. clippy step failure after1h waiting in
ci/build.shfor shared bread build lock; log endsbuild.sh: timed out waiting for the bread build lock. Cargo clippy never ran; build and test steps skipped. Exact-SHA contextcheck / check (push)is failure. This is infrastructure, not a compiler/test failure, but required check gate is still closed. Run152 ata7056d1is a different commit and was not used as evidence. Savedrun151-state.jsonandrun151-logs.After C1/C2 repair: fresh exact-head independent review and green applicable CI required. If a future head is independently accepted unchanged, CI-only finalization can re-read head/base/checks/formal reviews/mergeability without repeating content review. Current head is NOT accepted, even if CI later turns green. Merge remains authorized only after both gates pass.
Independent exact-head review: PASS
Issue: BREAD-42 (with BREAD-47). Reviewed head:
b05a6f65a1b6b46e481f41dbfdf0820d4e93152c. Base:mainat40ee494aef39981943c6141c547c104968d93b03. Earlier reviewed head71c469b4527f1aa25bcbead09cd45f0e55364974. Formal reviews: none. Branch protections: none. Mergeable: true.What changed since
71c469bThe head is a merge of main (PR49 CI checkout change).
git diff origin/main...HEADfor this PR is byte-identical (ignoring index lines) to the diff at71c469b: same 13 files, 3527 additions, 108 deletions. The only new content in the head versus71c469bis.forgejo/workflows/check.ymlanddev-release.ymlfrom main. No em dashes, no attribution.Criteria
fake_module_hostPID: dispatch seen by the compositor, host SIGKILLed (exe gone), compositor then rejects after 2s: compositor saw only the dispatch, no eval fallback, 3/3 runs. Control with the host kept open does send the fallback at 2.07s, so the probe discriminates. On the earlier head71c469bI also checked half-close after the request was in flight (no fallback) and 60 pipelined frames during a stall (one dispatch, no fallback).accepted_but_stalled_call_does_not_hold_up_shutdown(waits for the j/monitors request before terminating) andhost_death_during_stalled_dispatch_sends_no_fallback(real kill, waits for the dispatch) pass.startup.readyfor readiness.check-docsOK, four hyprland methods plus the 17 existing bridge methods registered; Documentation.md states the no-additional-request guarantee (an already-sent command cannot be revoked).Verification at
b05a6f6cargo build --workspace --locked --tests --examples: ok.cargo test --locked -p breadd --test module_host_hyprland --test module_host_hyprland_wire --test module_host_hyprland_deadlines: 3+4+6 passed.cargo test -p breadd --bin breadd hyprland: 54 passed.cargo test -p xtask: 17 passed.check-docs: OK.cargo fmt --check: ok.cargo clippy --workspace --all-targets --locked -D warnings: ok.git diff --check: clean.cargo test --workspace --no-fail-fastwas run locally at71c469b(exit 0); atb05a6f6the full workspace test is covered by the exact-head CI run below, since the PR diff is unchanged.check / check (push)run 164 job 0, commitb05a6f65a1, success (checkout verified the exact SHA, clippy, build, test, cleanup all success). Combined status success.Compositor interactions used private fake Unix sockets only; no host compositor or remote host was touched.
Merging this exact head with an expected-head condition. Card stays in review for coordinator verification.
Merged
PR44 merged into
mainasd5748d6093def1c4863bd496c86af0be31f8c6f1(merge commit; parents40ee494and reviewed headb05a6f65a1b6b46e481f41dbfdf0820d4e93152c). Merge request carried the expected head SHA. Review: comment 5360. Required checkcheck / check (push)run 164 was success on the exact head before merge.Awaiting verification on the integrated commit; not marked done. No tags, releases or branch deletion. Cards BREAD-42 and BREAD-47 stay in review for the coordinator to verify, and any premature move to done by breadboard sync should be restored to review.