Skip to content

Define the launcher contract for gateway supervision - #394

Draft
SashaMIT wants to merge 21 commits into
developfrom
cursor/launcher-contract-1297
Draft

SashaMIT wants to merge 21 commits into
developfrom
cursor/launcher-contract-1297

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A supervisor starts Home with elastos gateway --addr localhost:8090. The Home origin is http://localhost:8090/home/. The passkey relying party id is localhost. Registration and sign-in at that origin succeed in local_home_passkey_registers_and_signs_in_at_localhost (elastos/crates/elastos-server/src/auth.rs). An IP-literal origin, including http://127.0.0.1:8090, is refused.

Passkeys that a browser created keep working after this change. Every release takes the relying party id from the page host (derive_rp, api/handlers/identity.rs), and browsers refuse an IP address as a relying party id, so a stored passkey names the host where it was made. passkey_from_an_earlier_release_signs_in_and_a_foreign_origin_is_refused (api/auth_gateway.rs) signs a stored localhost owner passkey in at ports 8090, 61180 and 18090, and a stored public-Home passkey at its https host. The same test refuses those passkeys at a foreign host, evil.localhost, 127.0.0.1, [::1] and a host that only contains the relying party id, and the auth state stays unchanged.

Readiness proves that this gateway holds the lock from the held lock, not from the lock file's JSON (36720070). GET /healthz returns HTTP 200 and the two bytes OK only while the process that answers holds {data}/host-process.lock as gateway or gateway-public (api/gateway_site.rs healthz -> host_lock::holds_gateway_host_lock). The proof is in-process state: each host lock this process takes is remembered with its role (host_lock.rs remember_held_host_lock, removed in FileLock::drop/release before the unlock). The JSON record (pid, role, addr) is not read: any process of the same user can rewrite it. The earlier head trusted that record in two places: /healthz (gateway_site.rs gateway_health_lock_matches, which compared JSON pid and role) and publish_gateway_lock_if_unowned (host_lock.rs, which skipped taking the lock when the JSON pid was its own, so a forged or stale record let a gateway run with no host lock).

The lock proves possession, not identity (13cdcad0, docs and tests only; no behaviour change). Holding the flock proves that a process has the host lock. It does not prove that the holder is a gateway or ElastOS: any process of the same user can open host-process.lock and hold it. docs/LAUNCHER_CONTRACT.md ("Readiness and Home URL", lines 58-75; "Data directory and lock", lines 123-125) now states this for /healthz, for the Home terminal's inherited descriptor (ELASTOS_GATEWAY_HOST_LOCK_FD: the terminal's check proves that the descriptor holds the lock and that its parent has the given pid, not that the parent is the gateway; a same-user process that holds the lock can give the same variables and its descriptor to its own child), and for the another ElastOS host already owns error (given for any holder, also one that is not ElastOS; it names the holder from a record that can be false). The comments at host_lock.rs (registry, inherited_gateway_host_lock_is_held, active_host_process), gateway_site.rs healthz and runtime_control.rs parent_gateway_lock_proven say the same. The error text is unchanged because the contract pins that string for supervisors.

One-step proof against the holder's own lock (85f3142e). held_host_role used to look up the registry, release its mutex, and then probe with a new open of the file. A release plus a retake by another holder could come between the two, and the probe then saw the other holder's lock. Now each registry entry keeps a second descriptor (try_clone, a dup) of the open file that holds the flock. held_host_role keeps the registry mutex from the lookup to the end of the probe, and a guard removes its entry under that mutex before it unlocks, so no release or retake can come between them. The probe (description_holds_lock) requires: the path names the same device and inode as the entry's descriptor; a new open cannot take the lock; and the entry's own descriptor can take it again. A repeated flock(LOCK_EX) on the description that already holds it is a no-op on Linux and macOS, and any other holder makes it fail, so this names the open file description that holds the lock (not the program behind it).

The subordinate runtime lane uses the same proof (eed83a72). runtime_control.rs used to open the lane when the JSON record named this pid as a gateway, or (for a Home terminal) named the parent pid as a gateway. Now subordinate_gateway_host opens it only when holds_gateway_host_lock is true in this process, or when this process is a Home terminal whose gateway still holds the lock: the gateway passes ELASTOS_GATEWAY_HOST_PID and ELASTOS_GATEWAY_HOST_LOCK_FD, a descriptor of its locked open file, and keeps that descriptor open across exec only for that child (api/gateway_home_terminal.rs, gateway_host_lock_for_child). The terminal must be the direct child of that pid, and host_lock::inherited_gateway_host_lock_is_held runs the same three-step probe on the inherited descriptor (and marks it close-on-exec). A gateway inside serve passes no descriptor, so its terminal is refused, as before. The record only names the holder in the refusal message. As stated above, this proves possession of the lock by the parent's descriptor, not that the parent is the gateway.

A held lock with an unreadable record is still held (4bb19dcf). active_host_process returned "no host" when the lock was held but its record was empty, being written or not JSON, so the update controller, the principal-root upgrade wait and the subordinate lane treated a running host as stopped. It now reports such a holder with role unknown.

Hostile tamper tests:

  • test_healthz_refuses_a_forged_or_foreign_lock_owner (api/gateway_tests/site_publication.rs): with no lock held, a record forged as this pid with role gateway or gateway-public, another pid, another role, a string pid, non-JSON and a missing file all return 503; a lock held by another open file (standing in for another process) with the record forged to this pid returns 503; this process holding the lock as serve with the record rewritten to gateway returns 503; a removed-and-recreated lock file with a forged record returns 503; a released gateway lock that left its own record returns 503. While this process holds the lock as gateway, rewriting the record does not change the 200.
  • test_the_host_lock_proves_possession_not_that_the_holder_is_the_gateway (api/gateway_tests/site_publication.rs, new): a real separate process, /bin/sleep, opens the lock file itself and takes the flock before exec (host_lock::test_support::HeldByAnotherProcess); the record names its pid as gateway. active_host_process reports pid = that process, role gateway; acquire_host_process_lock(.., "gateway", ..) and publish_gateway_lock_if_unowned both fail with another ElastOS host already owns ... (pid <sleep pid>, ...); held_host_role is none, gateway_host_lock_for_child is none, /healthz is 503. After the process ends, its leftover record is 503 and the gateway takes the lock and answers 200. The test also checks the contract sentences.
  • home_terminal_lane_proves_lock_possession_not_that_the_parent_is_the_gateway (runtime_control.rs, new): this test process takes the flock with a plain open (not as a host, so held_host_role is none) and starts a real child (the test binary, home_terminal_lane_probe_child) with ELASTOS_GATEWAY_OWNED_HOME_TERMINAL=1, ELASTOS_GATEWAY_HOST_PID=<its pid> and ELASTOS_GATEWAY_HOST_LOCK_FD kept open across exec. The child's parent_gateway_holds_host_lock answers true while the lock is held, false for a plain descriptor of the same file, and false for the same descriptor after the release. The test also checks the contract sentences.
  • gateway_takes_the_lock_when_only_a_forged_record_names_this_process (host_lock.rs): a record naming this pid does not stop the gateway from taking the real lock.
  • release_and_retake_prove_only_the_current_holder (host_lock.rs): gateway lock held -> gateway; released -> none; retaken by this process as serve -> serve only; another holder retakes -> none; the gateway retakes -> gateway.
  • an_entry_for_the_same_file_is_not_proof_when_another_holder_has_the_lock (host_lock.rs): the guard's open file is unlocked behind its back and another holder takes the lock; the registry entry for that file is not proof.
  • a_release_and_retake_cannot_come_between_the_lookup_and_the_probe (host_lock.rs): a test hook between the lookup and the probe starts a thread that releases this process's lock and lets another holder take it, and waits 300 ms; the answer must not be Some for a lock another holder took first. With the fix the release waits for the probe and the answer is gateway; afterwards it is none.
  • inherited_gateway_descriptor_proves_only_a_lock_its_gateway_still_holds (host_lock.rs): the inherited descriptor proves the lock while the gateway holds it; a plain open of the same file with a record naming the gateway, an unrelated file, fds 0, 2 and -1 are refused; after release, after another holder retakes, and after the gateway retakes (old descriptor), refused; only a new descriptor proves the new lock; a replaced lock file is refused; the check takes no lock when the lock is free.
  • subordinate_lane_in_this_process_needs_the_held_gateway_lock (runtime_control.rs): a record forged as this pid with role gateway/gateway-public while another holder has the lock is refused ("already owns this identity"); the held gateway lock opens the lane whatever the record says; after release the leftover record is refused; a holder with a non-JSON record is refused; this process holding serve is refused.
  • subordinate_lane_for_a_home_terminal_needs_its_gateway_lock_descriptor (runtime_control.rs): a record naming the parent as a gateway, with a plain descriptor while another holder has the lock, is refused; the gateway's descriptor opens the lane; a missing or wrong marker, a parent that is not the gateway pid, a missing or non-numeric descriptor are refused; after release and after another holder retakes, refused.
  • active_host_process_reports_a_holder_whose_record_cannot_be_read (host_lock.rs): an empty, non-JSON or wrong-typed record under a held lock is reported as an unknown holder; once released it is free.
  • Negative proof, round 3: with healthz switched back to the old JSON check and publish_gateway_lock_if_unowned switched back to read_lock_metadata(..).is_some_and(|meta| meta.pid == std::process::id()), the first two tests fail (left: 200 / right: 503, the forged record does not stand in for the lock).
  • Negative proof, round 4: (1) held_host_role restored to lookup, mutex released, then a fresh-open probe only: a_release_and_retake_... fails (a lock another holder took before the probe was reported as held: Some("gateway")) and an_entry_for_the_same_file_... fails (left: Some("gateway") right: None). (2) subordinate_gateway_host restored to active_host_process + owner.pid == std::process::id() with role gateway, and parent_gateway_lock_proven restored to "parent pid equals the record's pid with a gateway role": both subordinate_lane_* tests fail (unwrap_err() on an Ok value: true; assertion failed: !proven(&plain, ...)). (3) active_host_process restored to Ok(read_lock_metadata(&lock_path).map(Into::into)): active_host_process_reports_a_holder_... fails (held lock with record "" reported free) and the lane test fails (unwrap_err() on an Ok value: false). With the fixes all pass.
  • Negative proof, round 5 (the two new tests): (1) with the round-4 docs/LAUNCHER_CONTRACT.md restored, both fail on the contract checks (assertion failed: contract.contains("The lock proves possession, not identity."), assertion failed: contract.contains("ELASTOS_GATEWAY_HOST_LOCK_FD")). (2) With healthz changed to also accept "a held lock whose record says gateway" (treating possession as identity), the /bin/sleep test fails (a lock held by another process left: 200 right: 503). With parent_gateway_lock_proven changed to also require the record to name the parent as gateway, the child test fails (a descriptor that holds the lock, from a parent that is not a gateway), which shows the test pins exactly what the child's check accepts. Both mutations were reverted.

A refused passkey origin returns HTTP 403 with plain text. For a loopback page such as http://127.0.0.1:8090 it says: "Passkeys work on this computer only at the localhost address. Open http://localhost:8090/home/ and try again."

The Home origin decision is recorded below under Decision: localhost is the one Home origin.

Readiness gap, stated in docs/LAUNCHER_CONTRACT.md: the reply comes from whatever process answers on the port and describes that process's own data directory. An ElastOS gateway for another data directory that already listens on the port also returns OK, and so can any other program on the port. Until a reply names the lock holder, a supervisor confirms that its own child is still running and that the lock records that child's pid. The contract says that record is a hint, not proof: a process that is not that child can hold the lock and write a record that names the child.

elastos serve holds the host lock as serve, and its supervisor can start a gateway in the same process (elastos room open). That gateway keeps the serve lock and starts, and its /healthz returns 503 (gateway_inside_serve_starts_and_answers_health_unavailable, api/gateway_server.rs; gateway_inside_a_held_serve_lock_keeps_it, host_lock.rs). It recognises the serve lock from the held lock, not from the JSON pid.

On this branch the binary watch still exits 75 when the installed binary is replaced, when its path changes, and when the watch cannot read it. A supervisor uses the exit codes. It does not parse English sentences from stdout.

Intel Mac's public release id is x86_64-darwin. This contract does not publish a download for that id.

Head d30027ed. What this branch carries on top of develop: the launcher contract and localhost work (402d8739..9c9766b3: localhost binds on every loopback address, the 308 move of IP-literal Home pages to localhost, the passkey origin refusal with a plain message), a merge of the zip 9 fix (519a2df9, merging 756dad1c from fix/zip-9-rustsec / #417) so cargo-audit passes, a cherry-pick of Anders' develop commit 50515055 as f1fcb219 (the model-provider held-lock journal test uses the bounded retry), the lock fixes 36720070, 85f3142e, eed83a72, 4bb19dcf, and the possession wording and tests 13cdcad0. d30027ed: merge of origin/develop at bfefe92f (develop's fix for its own #430 Home link test; no conflicts; zip stays 9.0.4). At d30027ed: cargo test -p elastos-server --lib -- home_external_link possession home_terminal_lane healthz host_lock:: 27 passed, 1 ignored. develop is merged in at bfefe92f (d30027ed). Stack: #395 and #407 are drafts stacked on this branch. #407 is the only branch that changes the exit-75 meanings. Draft against develop. Do not merge.

Tested here (Linux worker, no IPv6), head 13cdcad0: cargo test -p elastos-server --lib -- possession subordinate_lane home_terminal_lane healthz host_lock:: gateway_inside_serve 28 passed, 1 ignored (the child half of the subprocess test, which the parent test runs), built from this worktree; cargo clippy -p elastos-server --all-targets -- -D warnings clean; cargo fmt --all -- --check, git diff --check and node scripts/public-copy-entropy-check.mjs pass. At 4bb19dcf: -- runtime_control update_controller update:: install_transaction gateway_server home_system home_terminal site_publication content_sandbox frontdoor 473 passed, 2 failed for reasons outside this change: readiness_uses_the_reported_ipv6_home_listener_and_checks_its_served_bytes (binds [::1]:0; the worker has no IPv6) and runtime_control::gateway_children::tests::cancelled_start_reaps_group_and_other_gateway_generation_is_preserved (gateway_children.rs:1059 !pid_is_alive(pid)), which fails the same way with the elastos-server/src tree of 36720070 on this host; node scripts/check-product-data.mjs and its test (99 pass) pass. macOS was not run here; the proof uses flock per open file description, a no-op repeated LOCK_EX by the holder, and device/inode, which macOS has, and the new tests use fork/exec, open and flock, which macOS has. Without device and inode numbers (not a supported Home platform), the proof fails closed.

Decision: localhost is the one Home origin

The team chose localhost everywhere. Browsers refuse WebAuthn at an IP address (Chromium: SecurityError: This is an invalid domain. at http://127.0.0.1), so passkeys work only at http://localhost:<port>. The launcher (Elacity/elastos-launcher #54) changes to open and probe http://localhost:8090/...; this branch changes the Runtime side.

Listeners (api/gateway_server.rs, bind_gateway_listeners). For --addr localhost:<port> the gateway binds each address the computer resolves for localhost, plus 127.0.0.1 and ::1 (browsers use both for localhost whatever the hosts file says), deduplicated, on one port, and serves the same router on each with one shared shutdown handle. If another program already uses any of them, or any other bind error occurs, the gateway does not start and exits 1 with, for example, Home cannot start because another program already uses 127.0.0.1:8090. Stop that program, then start Home again. Only a family the computer lacks (EADDRNOTAVAIL, EAFNOSUPPORT, as for ::1 without IPv6) is left out, and at least one address must bind. A localhost that resolves off this computer is refused. An IP-literal --addr keeps today's single TcpListener::bind(addr). host-process.lock still records --addr as given (localhost:8090).

Redirect (home_page_on_localhost). Only when the gateway listens on localhost: a GET/HEAD for /, /home, /home/, /apps/home, /apps/home/ whose Host is a loopback IP (127.0.0.1, [::1]) gets 308 to the same path and query at http://localhost:<bound port>. /healthz, /api/..., Home assets and app routes are not redirected, and /healthz answers 200 on each listener with Host: localhost:<port> (what launcher #54 probes) and with the IP host. A non-loopback Host is not redirected. An IP-literal gateway does not redirect, because it does not own the other localhost family and the redirect could hand the Home origin to another program.

Hidden elastos gateway default stays 127.0.0.1:8090. The contract and every script/CI caller pass --addr explicitly, but elastos gateway --public uses the default and turns it into a public bind with addr.replace("127.0.0.1", "0.0.0.0") (gateway_cmd.rs). Changing the default would make --public listen on loopback only. The contract says a launcher passes --addr localhost:8090.

Docs. docs/LAUNCHER_CONTRACT.md drops the single-family gap and states the listeners, the redirect, and that localhost is the one Home origin. It also states what proves that this gateway holds the lock (the held flock, not the JSON record) and that the lock proves possession, not identity. docs/HOME_LOCAL_SETUP.md (the old "only a script default, not product authority" line), docs/INSTALL.md, docs/MAC.md and docs/WINDOWS.md (which named localhost:61180/apps/home/ for the Windows launcher) cite the contract. 61180 is labelled the developer source-home lane.

Tests (api/gateway_server.rs, trusted_gateway_tests): localhost_binds_ipv4_and_ipv6_loopback_on_one_port, localhost_refuses_to_start_when_another_program_holds_ipv4_loopback, localhost_refuses_to_start_when_another_program_holds_ipv6_loopback, localhost_that_points_off_this_computer_is_refused, only_a_missing_address_family_may_be_left_out, localhost_addresses_add_both_browser_loopbacks_once, localhost_port_matches_only_the_name_localhost, ip_literal_addr_binds_one_listener_as_before, ip_literal_gateway_serves_home_pages_in_place, loopback_ip_home_pages_move_to_localhost_and_other_routes_stay. With the old single bind and no redirect, the IPv4 hostile test fails (left: "Address already in use (os error 98)") and the redirect test fails (left: (200, None) right: (308, Some("http://localhost:44553/"))). The worker host has no IPv6 (EAFNOSUPPORT), so the ::1 parts print SKIPPED IPv6 PART there and run only on CI hosts with ::1.

Remaining gaps. Windows forwarding of localhost into WSL2 is not tested here (the contract states this). The ::1 listener, the ::1 hostile-process refusal and the ::1 health and redirect checks did not run on the worker host (no IPv6); they run on CI hosts with ::1 and are not yet observed passing there. A real-browser passkey ceremony at http://localhost:8090 behind the redirect is still issue 4 below. Removing or replacing host-process.lock while a host runs makes that host's /healthz and subordinate lane refuse, but it still lets a second host take a lock on the new file; that is older than this branch and not fixed here. Any same-user process can hold the lock and so stop Home from starting (the gateway exits 1 with another ElastOS host already owns); that is the documented boundary, not a fix here. The Home terminal's inherited-descriptor probe has one narrow race: if the gateway releases its lock between the second and third probe step, the third step takes the lock again through the shared open file and the terminal keeps the home locked until it exits; this fails closed (no second host starts) and is documented at inherited_gateway_host_lock_is_held. The Home terminal lane was tested with in-process descriptors and with a real child process given the descriptor by a non-gateway parent, not with a real gateway-spawned PTY child. update_controller.rs prove_ready (from develop) still compares the record's pid, generation and role as one of several readiness checks (the private attach secret and served bytes are the others); it is not changed here: #437.

CI

  • test-elastos on 13cdcad0 failed in develop's own Home link test (403 vs 200), because this PR's CI merges develop and develop 8ea4aac/0dbba668 carried that broken test; not this PR's. d30027ed merges develop at bfefe92, which fixes the test. (Run 39090524496, job 117567346067, on 13cdcad0. home_external_link_names_the_launched_app_and_the_exact_address was on develop, not on this branch at 13cdcad0; it reached that run only through the CI merge with develop.)

source-home-linux (ubuntu-24.04) failed on 1e63148f (run 38899842659, job 116995610376). Raw log: the scripts/ci-installed-journeys.sh home step started at 14:01:42 and printed nothing until ci-installed-journeys.py, line 1261, in run: raise RuntimeError("finite installed UI journey failed") at 14:04:05. The Node journey printed no assertion, so the Python loop stopped it: line 1261 raises when browser_owner.poll() != 0 or finite.error, and finite.error is set only by the outside resource observer (observe, lines ~504-508 at that head, which stores the fixed string finite_resource_observation_failed and drops the exception text). The uploaded receipt for the 4 GiB journey shows finite_memory.status: failed, one action (engine_running at 0.016 s, engine pid 58344), memory_peak_bytes 693587968 of 4294967296, all memory_events zero (no OOM), cgroup_empty: true and results.finite_memory_boundary: failed; finite-model-busy.png exists and no home-journey.json was written. So the observer raised after the Busy refusal and before the engine_stopped stage was recorded. Which of its checks raised is not in any log or artifact (the runtime log journey-runtime.private.log is not uploaded). One check fits this point and is timing-dependent: on engine_stopped, the observer raises finite journey engine group exit is unconfirmed on the first sample where the engine is gone but a member of its process group is still alive (ci-installed-journeys.py lines ~477-479 on develop), without waiting; the guard and llama-server share a process group that Stop kills with one SIGKILL (capsules/model-provider/src/local_llama.rs:904). That is a hypothesis, not proven. The 1 GiB journey and the engine-absent journey in the same job passed with the same gateway, /healthz probe and origin. #413 records the observer's phase and message in the receipt, so the next failure names its check.

Principles check

  • P1. The gateway and its data directory stay on this machine.
  • P2. The passkey origin is the hostname localhost, so Home and the credential share one origin. A stored passkey keeps its own host.
  • P9. /healthz, the gateway's own lock check and the subordinate runtime lane prove that this gateway (or, for a Home terminal, the parent's descriptor) holds the lock, from the flock held by its own open file description on the file the path names now, in one step under the registry mutex, not from a record any same-user process can write. The lock proves possession, not identity: any same-user process can hold it, and the contract says so. It does not prove which data directory the supervisor meant; the contract states that gap.
  • P10. One start command, one Home origin (localhost, with a loopback-IP Home page redirected to it), and one readiness body.
  • P11 (listeners). Another program on any localhost address stops the start with a message naming that address; only a missing address family is left out.
  • P11. A foreign or IP-literal origin is refused. A gateway inside serve reports 503 instead of claiming Home readiness. A forged lock record, a foreign holder (also a separate non-ElastOS process), a replaced lock file, a released lock and a retaken lock report 503 and close the subordinate lane; a held lock with an unreadable record counts as held (fail closed).
  • P12. The contract, the handler, and the tests name the two bytes OK, the localhost origin, the lock proof and its possession-not-identity limit, and the readiness gap.
  • P14. The public Intel id in this contract is x86_64-darwin.
  • P15. This pull request does not admit a platform and does not publish a signed artifact.

Intel stays C until a signed artifact and a journey exist. No grade here is a merge approval.

Team issues to open

This agent cannot open GitHub issues. Please open these:

  1. Signed Intel artifact and journey. Title: Signed x86_64-darwin artifact, darwin-amd64 payloads, and Home journey. Body: Keep the signer closed until a producer, a darwin-amd64 component set, and an installed Home journey exist. Intel stays hidden until that set lands together.
  2. Machine-readable update status. Title: Machine-readable update status for exit 0. Body: Exit 0 currently covers more than one successful update outcome. Add a status a supervisor can read when it must distinguish "up to date" from "a newer release exists".
  3. Adversarial review. Title: Adversarial review for launcher process control. Body: An independent review from a non-Cursor model family, then irzhywau, on the exact SHA, for the exit-policy draft and any install or signer change.
  4. Browser passkey ceremony. Title: Browser passkey ceremony at http://localhost:8090. Body: The in-process WebAuthn test registers and signs in at that origin and refuses http://127.0.0.1:8090. A browser virtual-authenticator ceremony is still open if that proof is required.
  5. Readiness that names the lock holder. Title: /healthz reply that a supervisor can bind to its own Home. Body: /healthz answers for the process on the port. Decide whether the reply carries a value the supervisor can compare with the child it started (for example its pid), and change the contract body if so. The lock file's JSON is not proof of ownership, and the lock itself proves possession, not identity.
  6. Windows localhost into WSL2. Title: Prove Windows localhost reaches the WSL2 Home on 127.0.0.1 and ::1. Body: The team chose localhost as the one Home origin and the Runtime now listens on every localhost address. Prove on Windows that the browser's http://localhost:8090/home/ reaches the WSL2 gateway, and that a Windows process on port 8090 cannot take that origin.
  7. Finite journey diagnostics. Now carried by ci: installed journeys name the observer failure, and the SmolLM2 proof uses a vendored license #413, which records the observer's phase and message in the receipt.
  8. Update readiness and the lock record. Opened as Update readiness reads the host lock JSON record as evidence #437.

Record the install, start, /healthz, Home URL, data directory, lock, stop,
and update interface a desktop launcher can rely on, and lock the /healthz
body in the existing gateway test.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
@cursor

cursor Bot commented Oct 16, 2026

Copy link
Copy Markdown

Independent alignment audit of runtime PRs #394–#398 and launcher PRs #4–#14. No code was changed. Full report follows. The same text is in the auditor workspace as AUDIT.md.

Law read from origin/develop at 55dcb79. The open PRs branched from 75dcb4f. Grades are from the raw diffs, then the launcher tip 3f8097fe and the develop functions those diffs describe. Overall grade is the lowest principle. Principles 4, 7, 8, 13, 17, and 19 are A on every PR: these diffs do not touch capsule APIs, WebSpaces, agent grants, design tokens, or consequence classes.

Grade card

Runtime: #394 C, #395 C, #396 C, #397 D, #398 D.
Launcher: #4 C, #5 D, #6 D, #7 C, #8 C, #9 F, #10 F, #11 D, #12 D, #13 F, #14 D.
Composed launcher tip: F. Composed Intel tip (395→397→398): D. #394 is not an ancestor of #398, so the contract and the installer can disagree about Intel.

Principle 394 395 396 397 398
1 Local first A A A A A
2 Stable identity B A A C C
3 Ambient authority A A A A A
5 Small core A A A A A
6 Boundaries B B A A A
9 HTTP edge B A B A A
10 One path B A A D D
11 Fail closed B B B D C
12 Docs/tests C C C B D
14 Names A A A A A
15 Signed content C B A D C
16 UI authority A A A A A
18 Isolation B B B A A
Principle L4 L5 L6 L7 L8 L9 L10 L11 L12 L13 L14
1 Local first A A A A A B B A A D A
2 Identity C C D A C F F A C F C
3 Ambient B D A A C C C B A B A
5 Small core A C A A A B B A A B A
6 Boundaries B B B B A B B A A C B
9 HTTP edge B A B A A D D B A C B
10 One path B C D A B F F A B F C
11 Fail closed B B D B C F F D D F D
12 Docs/tests C C C B B D D C D C D
14 Names A A A C A A A A A B A
15 Signed C C D A C F F A C F C
16 UI A A A B A A A C A B A
18 Isolation B C A A A B B D A C B

What CI does not check

CI compiles and runs doubles. It does not check that the bytes a person installs are the bytes a signed head names.

Chain: pin file → HTTPS release-head.json → HTTPS install.sh → bash → signed release.json → URL /artifacts/elastos- → SHA-256 in that release → journal → sources.json head CID → later elastos update over Carrier.

F1 — S1 — The pin does not bind the bytes. P2, P10, P11, P15. Launcher #9, #10, #13. Tip 3f8097fe.

planRuntimeInstall returns fallback when the unsigned head is missing or disagrees (runtimeInstall.ts:60-91). A fetch error becomes that missing head (164-168). A matching plan that throws, other than the placeholder error, downloads again (179-199).

Both downloads are the same URL. The pinned flag only changes the filename. runtimeInstall.ts:233-247:

async function runInstallerStep(deps, onProgress, pinned) {
  const script = await deps.fetchText(deps.pin.fallbackInstallerUrl);
  assertSignedInstaller(script);
  const scriptPath = path.join(dir, pinned ? 'install-pinned.sh' : 'install-fallback.sh');
  deps.writeFile(scriptPath, script);
  await deps.runInstaller(scriptPath, ['--install-only']);
}

assertSignedInstaller (lines 14-31) searches for MAINTAINER_DID and HEAD_CID. The error says the installer is not signed. It does not run Ed25519. A script without those tokens is accepted.

parseReleaseHead (41-66) compares signer_did, channel, version, and latest_release_cid as strings. It does not verify the envelope.

scripts/runtime-smoke.sh:83-114 does the same compare and the same curl. On a match it checks that version output contains the pin. On a mismatch it still runs the script and skips that check (124-130).

This is fallback downgrade, pin bypass (--head-cid is never passed), MITM of the bootstrap (TLS is the only binding on the script; a replaced script never reaches the runtime verifier), and TOCTOU (head fetch, script fetch, write, bash, with no hash after the write).

#6 records a decision to fall back if the pinned install fails, and also says to verify the signature. PRINCIPLES.md §10 and §11 still require one path and a stop. The code does not verify the signature, and the two plans are the same bytes.

Fix: one plan. Verify the head. On mismatch or install error, stop and name the field. Delete the fallback action.

F2 — S1 — Version containment accepts the wrong tree. runtimeInstall.ts:85-89, 149-161, 202-206.

installedVersion.includes(pin.version) plus a present components.json returns already-installed before any publisher contact. The string 0.8.0-alpha.20 contains 0.8.0-alpha.2. resume-setup runs elastos setup on that binary. After fallback, the version check does not run. The unit test sets latest_release_cid equal to headCid (test/runtime-install.test.js:40-48), so it cannot see the field mix-up in F3.

Fix: full-string equality, and sources.json head_cid equals the verified pin.

F3 — S2 — Head CID and release CID are different objects.

publish-release.sh sets latest_release_cid to RELEASE_CID, then HEAD_CID to the ipfs add of release-head.json. The launcher compares pin headCid to latest_release_cid. A real head CID fails closed into F1's fallback. A release CID can pass and still be the wrong value for update --rollback-to, which release_cmd.rs treats as a head override.

develop scripts/install.sh:50 stamps HEAD_CID and line 1392 writes it to sources.json. Those are the only uses. The downloaded head is signature-checked. Its bytes are not required to hash to the stamp.

#12 stores the Head CID: line in ~/.elastos-launcher/previous-head.json with no signature (runtimeUpdate.ts:50-56, 114-128) and passes it to --rollback-to (88-96). The runtime signature check is what refuses a tampered CID. The test double returns QmPrevious and never runs it.

Fix: one pin field, the CID of the verified head bytes. Pass --head-cid. Require the installed Head CID to match. Store that value for rollback.

F4 — S2 — Stop and exit 75. P11, P18.

develop main.rs:1307 and the SIGKILL at main.rs:1320: gateway is its own process group; SIGTERM signals the group, waits 15s, then SIGKILL. gateway_site.rs:432 returns OK. #394's test locks those bytes. host_lock.rs:239 exits 75 also when the exe path cannot be read, ends in " (deleted)", moved, or cannot be re-hashed. #394's contract text says only that the binary was replaced.

Launcher runtimeGateway.ts:214-217 spawns detached. Stop SIGKILLs the pid only (180-191, 238-241). The smoke kills the group (runtime-smoke.sh:186-194). The first failed health poll counts as stopped (182-186) while the lock can still be held. isHealthOk trims the body (45-47); any local OK on port 8090 is treated as Home (151-161). Exit 75 restarts are capped at 3, and every 75 is labeled an update.

Fix: signal the process group. Stop only after the pid is dead and health has failed twice. Restart on 75 only for a replaced binary.

F5 — S2 — Intel is three names and three gates. #395, #397, #398. P10, P11, P12, P15.

Names: darwin-x64, x86_64-darwin, darwin-amd64. release-signer.py:1084 still admits only x86_64-linux, aarch64-linux, aarch64-darwin.

#397 makes package-release-binaries.sh print darwin-amd64 for Darwin-x86_64. It used to exit 1. CI only runs --print-platform. The script will now package that host.

#398 detect_platform selects x86_64-darwin. A missing key still dies with "No release available for platform". README, GETTING_STARTED, and INSTALL say the signed release supplies the Intel download. MAC.md in the same PR says the signer still refuses it. #394 still says Intel is outside the install set.

The #398 CI check slices install.sh before "# ── Main" and runs detect_platform. It does not fetch a signed release. At audit time, checks on #395, #397, and #398 were pending. No finished native /healthz log was available. #394's earlier rollup was green and does not build Intel.

Fix: keep both scripts refusing Intel until the signer, components.json, and a signed artifact admit the platform together. Make the README match MAC.md. Keep the native smoke labeled as a build smoke.

F6 — S2 — Launcher #5 passwordless sudoers. P3.

pc2Manager.ts in that diff writes /etc/sudoers.d/elastos with NOPASSWD for wg-quick, NOPASSWD:SETENV for awg-quick, killall amneziawg-go, and rm -rf /var/run/amneziawg/. visudo -c deletes a bad file. SETENV crosses the caller environment into a root command. notarize.js in the same diff waits and throws unless Apple accepts. #14 says this stack is 1.2.7 and merging #5 conflicts in package.json.

Fix: land #5 alone. Drop SETENV unless awg-quick requires one named variable. Rebase the runtime stack after it. Keep this file off the runtime install path.

F7 — S3 — Proof CI will not refuse. P12.

runtime-smoke.yml:13 is continue-on-error. #14's windows-wsl job exits 0 when wsl is missing. #395's source-gate test drives a Python stub; the shell smoke can accept OK from another listener, and command substitution hides a trailing newline the Rust test rejects. #12 rollback is a double. #14 marks a Linux click path done without the log in the diff. #396 was not executed on Windows. Nothing refuses XDG_DATA_HOME on /mnt/c. The KVM paragraph matches develop prepare_browser_linux_host and browser-vm-linux-network.py (systemd unit and udev rule). --install-only returns before that helper.

Fix: required Linux and macOS smokes. A Windows skip fails the job or is absent, and does not exit 0. Attach the Intel log and a two-head rollback log. Add a test where head CID and latest_release_cid differ and the install stops.

F8 — S3 — What #394 freezes, and what it does not.

Matches develop for addr 127.0.0.1:8090, no browser open, health body OK, /home/, data dirs, the lock error text, the two update stdout lines, and Carrier update in release_cmd.rs.

sources.rs:15 and :41 fall back to /tmp when home is missing. The new test does not take the lock or exit 75. install.sh json_get evals a Python expression that interpolates PLATFORM.

Fix: make the /tmp fallback a hard error. Say in the contract which rows are specified and not yet tested, or add those tests.

F9 — S3 — Preflight fetches the publisher. preflight.ts:129-137, added in #13.

A cold open GETs release-head.json and treats failure as offline. That is a publisher contact, not a telemetry SDK. electron-log stays local. PC2 still checks api.github.com on the PC2 target.

Fix: decide offline only during an install the user started.

F10 — S4

IPC remains pc2:* (index.ts). contextIsolation is true and nodeIntegration is false (index.ts:43-46). There is no will-navigate deny. #4 quotes a gateway ready line develop does not print; #394 has the real line. Uninstall confirms, then rmSync recursive on the data dir (runtimeTarget.ts:141).

Blind spots

CID equality in fixtures. continue-on-error. Stub versus native /healthz. One canary head cannot prove rollback. Electron kill versus the smoke's group kill. No Ed25519 test on the pin. /mnt/c and /tmp data dirs. #394 and #398 are separate stacks.

Missing evidence

Linux click-path log. Finished gateway-smoke-macos-intel log. Windows or WSL2 execution. A second signed head on a real rollback. This comment is the diff review. It is not a merge approval.

Fix order

  1. Delete the fallback. Mismatch stops.
  2. Verify Ed25519, bind the script hash or CID, pass --head-cid, clear ELASTOS_MAINTAINER_DID, ELASTOS_PUBLISHER_GATEWAY, and ELASTOS_HEAD_CID in the child.
  3. Version and head CID equality, including already-installed and resume-setup.
  4. Process-group stop. Two failed health polls. Exit 75 only for a replaced binary.
  5. Required smokes. Windows skip is not exit 0.
  6. Hold Intel at the signer until one signed artifact exists. One sentence in Define the launcher contract for gateway supervision #394, Name the Intel Mac release package darwin-amd64 #397, and Stop Intel Mac before release lookup and pin the verified head CID #398.
  7. install.sh stores the CID of the verified head and refuses a stamp mismatch.
  8. Closed: void review surface #5 alone, without SETENV unless a named variable requires it.

After 1–4 the F grades can move to C, and to B only with a required smoke log and a two-head rollback log. They do not reach A on stubs. #397 and #398 stay at the refusal until the signer admits the platform. #394 principle 15 can move to B after fix 7, and stays short of A until the lock, exit 75, and data-dir rows have tests.

Stopped here. No product branch was modified.

cursoragent and others added 2 commits October 16, 2026 05:31
Exit 75 stays the launcher restart for a replaced binary, a moved path,
or a file that changed and then could not be verified. A process that
cannot read its own executable path exits 1, and the contract says the
launcher leaves that process stopped.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
The release id is x86_64-darwin and the package key is darwin-amd64.
The launcher calls that host darwin-x64. The signer still refuses
x86_64-darwin, and this tree has no Intel download.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
@cursor

cursor Bot commented Oct 16, 2026

Copy link
Copy Markdown

Re-audit — launcher tip #27 and runtime #394–#398

Date: 2026-10-16. Same method as the earlier card on this pull request. No code was changed. No audited branch was updated.

Law is unchanged: PRINCIPLES.md on origin/develop (55dcb790), then AGENTS.md, then the capsule documents. Overall grade is the lowest principle. A holds. B holds with a bounded gap. C is partial, or the claim is wider than the evidence. D weakens the principle. F breaks it. Principles 4, 7, 8, 13, 17, and 19 stay A on every diff here: these diffs do not touch capsule APIs, WebSpaces, agent grants, design tokens, or consequence classes.

Heads read for this card:

The earlier grades for launcher #4–#14 stand as grades of those diffs: #4 C, #5 D, #6 D, #7 C, #8 C, #9 F, #10 F, #11 D, #12 D, #13 F, #14 D. The tip below is what a person would run.

Grade card

Runtime, current tips

Principle #394 #395 #396 #397 #398
1 Local first A A A A A
2 Stable identity B A A C C
3 No ambient authority A A A A A
5 Small trusted core A A A A A
6 User / operator / developer B B A A A
9 HTTP is edge transport B A B A A
10 One canonical path B A A C C
11 Fail closed, then explain B B B C B
12 Docs, code, tests agree C C C B C
14 Public names A A A A A
15 Signed content C B A C B
16 UI is not authority A A A A A
18 Isolation B B B A A
Overall C C C C C

#397 and #398 move from D to C. The signer still refuses x86_64-darwin. The installer now selects that id and stops when the signed release omits it, and the docs say that. #398 stores the UnixFS CID of the verified head bytes.

Composed runtime tip (#394 → #395 → #397 → #398, head 355bddd5): C. #396 stays C and is still outside that stack.

Launcher #15–#27

PR Overall What sets it
#15 C Requires the Linux and macOS smoke jobs. The script those jobs run still described a fallback at that layer.
#16 B Keeps launcher 1.4.0 on this stack. It leaves the #5 sudoers line in the tree.
#17 D CI imports a WSL rootfs from a mutable Ubuntu URL, with a size floor and no hash.
#18 C A pin mismatch stops the install. Signature and installer hash arrive in the next PR.
#19 D The app verifies the head and the installer hash. The smoke flatten drops installerSha256, so the required job dies before that check.
#20 B Exact version match, and the pin compares the UnixFS head CID.
#21 B The supervisor signals the process group and restarts exit 75 only for a replaced binary.
#22 B The workflow test requires the smoke and fails when wsl.exe is missing.
#23 C Fix it clears a stale lock by path string and pid existence.
#24 C The support bundle scrubs named secret lines and writes mode 0600.
#25 C Reset deletes the data directory after confirm, guarded by basename checks.
#26 D The product downloads the same unpinned rootfs, and Fix it returns ready when that action returns.
#27 B Forward slashes reach wslpath. The Windows log shows /mnt/d/a/elastos-launcher/elastos-launcher.

Composed launcher tip 22854830

Principle Grade
1 Local first B
2 Stable identity C
3 No ambient authority D
5 Small trusted core B
6 Boundaries C
9 HTTP edge C
10 One path C
11 Fail closed C
12 Docs, code, tests D
14 Public names B
15 Signed content D
16 UI is not authority C
18 Isolation C
Overall D

The earlier composed tip was F because a pin mismatch still installed. That install path is closed in the app. The tip is D because the required smoke is red, the Windows rootfs is unsigned, ELASTOS_RUNTIME_PIN replaces the trust root, and the #5 NOPASSWD:SETENV line is still in src/main/pc2Manager.ts.

Earlier findings, checked in raw code

F1, pin mismatch still installed — fixed on the app path, broken on the proof path. planRuntimeInstall has no fallback action (runtimeInstall.ts). A refused plan throws PinMismatchError. runInstallerStep requires installerSha256, hashes the download, writes ~/.elastos-launcher/install.sh, hashes the bytes it reads back, then runs bash with --install-only --head-cid --maintainer-did. verifyReleaseHead checks the pinned DID and the Ed25519 signature before that download (releaseHead.ts). The smoke exits 1 when the pin fields disagree (runtime-smoke.sh:129-131).

The required job does not reach that hash. The flatten at runtime-smoke.sh:62-69 copies channel, version, head CID, DID, gateway, and installer URL, and drops installerSha256. Line 136 then reads that key from the flat file. Run 38858365277, job smoke (linux-x64) 116834803834, on merge 71a1e25 of tip 22854830:

publisher channel=canary version=0.8.0-alpha.20 release_cid=QmTAEJ8DAprsv5PbWcudofQwXrzL7ZjSgmhRmU8nqmwirf head_cid=QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt
Pinned release matches the publisher. Verifying the signature and installer hash.
KeyError: 'installerSha256'

The signature script never ran. The header at runtime-smoke.sh:6-7 and .github/workflows/runtime-smoke.yml:3-7 still say the script falls back to the hosted installer. The code stops. The comment describes the old path.

F2, version substring — fixed on the compare, with a local shortcut. installedVersionMatches is exact equality with ElastOS Runtime v${pin} (runtimeInstall.ts:27-30). already-installed and resume-setup return when that version and sources.json head_cid match the pin, before any publisher fetch (runtimeInstall.ts:171-183). A local binary and a local sources file can satisfy that without a new signature check.

F3, head CID versus latest_release_cid — fixed on the install path, open on rollback. The pin headCid is QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt. The live log above shows that value as the smoke helper’s UnixFS CID of the publisher bytes, and shows latest_release_cid as the old QmTAEJ8… value. The launcher compare uses head.headCid (runtimeInstall.ts:76). #398 bind_verified_head_cid runs after verify_signature, refuses a stamp that names other bytes, and stores the CID it computed. Three copies of that CID helper exist (launcher releaseHead.ts, smoke Python, install.sh). This run shows the smoke copy and the pin agree on the live file. It does not show ipfs add. runtimeUpdate.ts:50-56 and :114-128 still store whatever token follows Head CID: in ~/.elastos-launcher/previous-head.json and pass it to --rollback-to.

F4, exit 75 and the wrong process — mostly fixed, and the contract is wider than the launcher. #394 exits 1 when the executable path cannot be read, and exits 75 for replaced, moved, unverifiable, and re-hash failure. The launcher restarts only when the code is 75 and the text matches was replaced on disk or changed on disk, and it excludes could not be read, could not be verified, and could not be re-verified (runtimeGateway.ts:30-34). A move (moved from) does not match, so it does not restart. The #398 contract table still says exit 75 covers a move and a failed verify, and that the launcher starts the gateway again. Stop waits until the pid has exited and /healthz has failed twice (runtimeGateway.ts:181-196), then signals the process group (:37-47). A foreign lock plus any trimmed OK on 127.0.0.1:8090 still opens Home (:206-216). signalProcessGroup swallows a failed kill.

F5, Intel names — the refusal is now the documented behavior. #398 selects x86_64-darwin on Intel and the docs say the signer refuses that id, so lookup stops with no Intel download. #397 prints package key darwin-amd64 and the test says that name is not a signed download. scripts/release-signer.py is unchanged. Intel is not an installable signed release.

F6, sudoers SETENV — still present. src/main/pc2Manager.ts:1034 still writes NOPASSWD:SETENV for awg-quick, plus killall amneziawg-go and rm -rf /var/run/amneziawg/. None of #15–#27 edit that line.

F7, smoke evidence — the old green log is a different commit. continue-on-error is gone. The checklist green run is 0102b48 / run 38850728295, whose smoke still used a substring version check and did not read installerSha256. Tip 22854830 is red on Linux and on Windows for the KeyError above. macOS arm64 was still queued when this card was written.

F8, installer stamp — fixed on the #398 tip only. bind_verified_head_cid is the fix. The /tmp data-dir fallback and json_get evaluation are outside these diffs.

F9, publisher fetch on open — still present. preflight.ts:206-214 GETs {publisherGateway}/release-head.json inside collectPreflight. Fix it’s online action does the same fetch, which matches a button the person clicked.

F10, IPC name and uninstall — still present, with a new Reset path. IPC includes pc2:resetRuntime. Reset confirms, then fs.rmSync(..., { recursive: true, force: true }) on the binary, the data directory, and install-stage.json (runtimeUninstall.ts:67-75).

New blind spots

N1 — S2 — unsigned Windows rootfs. windowsHost.ts:22-23 and scripts/windows-elastos-distro.ps1:12 download https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz. The product writer stores the whole body under ~/.elastos-launcher/ with no hash and no size cap (windowsHost.ts:206-214). The CI script requires 50MB, then wsl --import. Run 116834803984 downloaded 356,739,129 bytes, imported distro ElastOS as WSL 2, and printed kvm=yes. It then hit the same KeyError. The CI script runs wsl.exe --unregister ElastOS before import (windows-elastos-distro.ps1:33-35). On that runner the distro was absent (WSL_E_DISTRO_NOT_FOUND, unregister_exit=-1) and the script continued. The product import does not unregister. launchController.ts:271-273 treats a Windows Fix it as ready as soon as fixWindowsOnThisComputer() returns. The ready copy says the button cannot finish the install inside that environment.

N2 — S2 — stale lock delete. assertStaleLockPath checks the basename host-process.lock, the parent basename elastos, and the substrings /.pc2/ and /pc2.net/ (runtimeRecovery.ts:64-74). It does not lstat, realpath, or reject a symlink. pidAlive is process.kill(pid, 0) (:129-136), so any live pid blocks the delete, including a recycled pid. collectFailureFacts snapshots the lock text. fixCollectedFailure later fs.rmSyncs that path (:165-168). A new gateway that replaced the lock file between the snapshot and the delete loses the new file. Unlink does not drop a flock held on the previous inode. Fix it does not signal the pid. The port copy says ElastOS will not close other programs.

N3 — S3 — support bundle scrub is a line filter. scrubSupportText drops lines matching attach_secret, the coords filenames, private key, and BEGIN … PRIVATE KEY, and rewrites bearer and password= (preflight.ts:129-136). A secret that misses those patterns stays in the file. saveSupportBundle refuses another filename and refuses /.pc2/, /pc2.net/, and /elastos/ (:151-157), then writes mode 0600 on the Desktop when that directory exists, otherwise in the home directory (:141-143).

N4 — S2 — the pin file is replaceable from the environment. runtimePinCandidates tries ELASTOS_RUNTIME_PIN before the packaged runtime-pin.json (runtimePin.ts:106-112). Verification trusts that file’s DID, gateway, installer hash, and head CID. installerChildEnv deletes only ELASTOS_MAINTAINER_DID, ELASTOS_PUBLISHER_GATEWAY, and ELASTOS_HEAD_CID (releaseHead.ts:313-318).

N5 — S3 — Fix it copy and the call disagree. The interrupted screen says setup continues without downloading again (runtimeRecovery.ts:208-210). resumeSetup is installRuntimeOnThisComputer() (:164), which can fetch the publisher and the installer. The hash check still applies on that path.

N6 — S3 — Reset and Fix it path checks are string checks. Basename elastos, basename elastos for the binary, basename install-stage.json, and a .pc2 / pc2.net substring (runtimeUninstall.ts:6-16, :46-51). The Windows .pc2 test uses path.sep. A forward-slash path misses it. The recursive delete follows the path Node is given. The code does not resolve symlinks first. Stop failure text did not start it refuses the delete.

Evidence still missing

  • A green required smoke on tip 22854830. The current Linux and Windows jobs are red with KeyError: 'installerSha256'. The signature verifier and the installer hash did not run in that log.
  • The macOS arm64 job on the same run was queued at this reading.
  • A finished gateway-smoke-macos-intel success on Smoke the gateway health probe on Intel Mac #395, Name the Intel Mac release package darwin-amd64 #397, or Stop Intel Mac before release lookup and pin the verified head CID #398. Those checks had no success conclusion in this snapshot. Source-gate on those PRs is green.
  • /healthz inside the ElastOS WSL distro. The Windows job imported the distro and died in the smoke script before the gateway.
  • A byte compare of pin installerSha256 18768ef071a9f921fe0c1d902926c138e86f925dbaf5dfb9185fecb8789d6714 with https://elastos.elacitylabs.com/install.sh.
  • ipfs add of the live release-head.json compared with the local UnixFS helper.
  • Two signed heads on a real elastos update --rollback-to.
  • This comment is the review of the diffs and of those two failed logs. It is not a merge approval.

Fix order

  1. Put installerSha256 on the flattened smoke pin, delete the fallback sentences in the script header and the workflow, and attach a green log of tip 22854830 that includes the signature line and install.sh SHA-256 matches the pin.
  2. Refuse ELASTOS_RUNTIME_PIN, or require it to be the packaged pin. Keep the three installer environment deletions.
  3. Hash the WSL rootfs, and return ready only after a fresh distro check. Keep wsl --unregister ElastOS on the CI runner.
  4. Re-read the lock immediately before unlink, and unlink only when that read still shows a dead pid and the path is not a symlink. Use the same realpath rule before Reset’s recursive delete.
  5. Change the Stop Intel Mac before release lookup and pin the verified head CID #398 exit-75 sentence so a restart happens for a replaced binary and a changed hash. A move, an unreadable path, and a failed re-hash stay stopped, matching runtimeGateway.ts.
  6. Leave the publisher fetch on the Fix it online action. Remove it from the cold collectPreflight pass.
  7. Store a head CID only after elastos source show and a signature check the runtime already performed. An arbitrary Head CID: token stays out of --rollback-to.
  8. Drop SETENV from pc2Manager.ts:1034 in its own change.

After 1–4 the composed launcher tip can move from D to C, and to B only with the green smoke log and a two-head rollback log. #397 and #398 stay C until the signer admits x86_64-darwin and a signed Intel artifact exists. No grade here is a merge approval.

@cursor

cursor Bot commented Oct 16, 2026

Copy link
Copy Markdown

Third audit — launcher tip 2e8cd84 (PR #36) and runtime #394–#398

Date: 2026-10-16. Same method as the two earlier cards. No code was changed. No audited branch was updated.

Law is unchanged: PRINCIPLES.md on origin/develop (55dcb790), then AGENTS.md, then the capsule documents. Overall grade is the lowest principle. A holds. B holds with a bounded gap. C is partial, or the claim is wider than the evidence. D weakens the principle. F breaks it. Principles 4, 7, 8, 13, 17, and 19 stay A: these diffs do not touch capsule APIs, WebSpaces, agent grants, design tokens, or consequence classes.

Heads read for this card:

Evidence for the launcher tip is run 38863773128 on that exact head. The checklist inside the tip still cites the earlier green run 38862931438 (2191058).

Grade card

Runtime, unchanged tips

PR Overall Change since the re-audit
#394 C Same commit. Exit 75 in the contract still includes a move and a failed re-hash, and says the launcher starts again.
#395 C gateway-smoke-macos-intel is now success. The job probes the binary that job built. The signer still refuses x86_64-darwin.
#396 C Same commit. Still outside the #394 stack.
#397 C Same commit. Package key darwin-amd64 is still not a signed download. Intel smoke on this tip is success.
#398 C Same commit. bind_verified_head_cid still runs after the signature check. Intel lookup still stops.

Composed runtime tip (#394 → #395 → #397 → #398, head 355bddd5): C. #396 stays C.

Launcher #28–#36

Earlier overalls stand: #4 C, #5 D, #6 D, #7 C, #8 C, #9 F, #10 F, #11 D, #12 D, #13 F, #14 D, #15 C, #16 B, #17 D, #18 C, #19 D, #20 B, #21 B, #22 B, #23 C, #24 C, #25 C, #26 D, #27 B. Those are grades of those diffs. The tip grades below replace them for what a person would run.

PR Overall What sets it
#28 B The smoke flatten keeps installerSha256. The header says install stops.
#29 B The WSL rootfs is release 20240423, with a SHA-256, an exact size, and a 400MB cap.
#30 C Production refuses an env pin that changes the trust root. NODE_TEST_CONTEXT still selects dev mode.
#31 B Rollback stores a CID only after verifyReleaseHead on the bytes at /ipfs/<cid>.
#32 B already-installed and resume-setup run only after the signed head matches the pin.
#33 B Fix it re-reads the lock and refuses a symlink. Reset walks ancestors with lstat.
#34 C Windows Fix it is ready only when /healthz is OK. That probe is any OK on port 8090.
#35 B Home opens for a foreign lock only when that pid’s command is an elastos gateway on 127.0.0.1:8090.
#36 C The smoke sets ELASTOS_HEAD_CID and drops --head-cid. The Install button still passes --head-cid.

Composed launcher tip 2e8cd84

Principle Grade
1 Local first B
2 Stable identity B
3 No ambient authority D
5 Small trusted core B
6 Boundaries C
9 HTTP edge B
10 One path D
11 Fail closed B
12 Docs, code, tests D
14 Public names B
15 Signed content B
16 UI is not authority C
18 Isolation B
Overall D

The re-audit tip was D because the required smoke was red, the rootfs was unsigned, and an env pin could replace the trust root. Those three are fixed on the smoke path. The tip stays D because pc2Manager.ts:1034 still writes NOPASSWD:SETENV, and the Install button still passes a flag the pinned installer rejects.

Re-audit findings, checked in raw code

F1 / N2 smoke flatten — fixed, and the log shows it. scripts/runtime-smoke.sh copies installerSha256 into the flat pin. Run 38863773128 job smoke (linux-x64) 116851807624:

publisher channel=canary version=0.8.0-alpha.20 release_cid=QmTAEJ8DAprsv5PbWcudofQwXrzL7ZjSgmhRmU8nqmwirf head_cid=QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt
Release head signature matches the pinned maintainer
install.sh SHA-256 matches the pin
Installed version: ElastOS Runtime v0.8.0-alpha.20
Installed head CID matches the pin
/healthz is OK
/home/ HTTP 200
Gateway stopped cleanly

The installer log on that job also has [OK] Signature verified for the head and for the release, then [OK] Checksum and version match the signed release. Job smoke (darwin-arm64) 116851807398 has the same sentences plus Platform: macOS 14.8.9 on Apple silicon (aarch64-darwin). Job smoke (windows-wsl) 116851807680 has Rootfs SHA-256 matches the pin, kvm=yes, the same signature and hash lines, /healthz is OK, /home/ HTTP 200, and smoke_exit=0.

F2 local skip — fixed. planRuntimeInstall returns already-installed or resume-setup only after a verified publisher head is in hand (runtimeInstall.ts). A version string and a local sources.json no longer skip the signature check. The test refuses a head whose signature does not match. The local binary’s own SHA-256 is still not compared with the signed release.

F3 rollback token — fixed in the launcher, unrun as a runtime command. verifiedHeadForRollback accepts a Qm… token, fetches {publisher}/ipfs/{cid}, runs verifyReleaseHead, and stores the CID only when the computed CID equals the token (runtimeUpdate.ts). QmPrevious is refused before elastos update. I fetched the public objects, without running elastos update: https://elastos.elacitylabs.com/ipfs/QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt is the same 624 bytes as release-head.json, and the UnixFS helper’s CID matches the pin. The previous head in that payload is also served at /ipfs/<cid> and matches its own CID (canary 0.8.0-alpha.19). There is still no log of elastos update --rollback-to against a real binary.

F4 foreign OK and exit 75 — the open is fixed; the contract sentence is not. #35 opens Home for a held lock only when foreignListenerIsElastos is true: integer pid, addr exactly 127.0.0.1:8090, pid alive, command matches elastos or elastos.exe and the word gateway (runtimeGateway.ts). A python3 -m http.server command leaves Home closed. signalProcessGroup returns on ESRCH and throws on any other kill error. #394/#398 still say exit 75 covers a move and a failed verify, and that the launcher starts again. The launcher still restarts only for was replaced on disk or changed on disk.

F5 Intel — unchanged runtime, with the missing smoke log now green. gateway-smoke-macos-intel is success on #395, #397, and #398. That job starts the binary it built. scripts/release-signer.py still refuses x86_64-darwin. The installer still selects that id and stops when the signed release omits it.

F6 SETENV — still present. src/main/pc2Manager.ts:1034 still writes NOPASSWD:SETENV for awg-quick, plus killall amneziawg-go and rm -rf /var/run/amneziawg/. None of #28–#36 edit that line.

F7 smoke evidence — the tip run is green. The required jobs on 2e8cd84 succeeded. The file docs/RUNTIME_MIGRATION.md still points the checklist at run 38862931438.

F8 head stamp — still the #398 behavior, and the smoke exercised it. The published installer reads HEAD_CID="${ELASTOS_HEAD_CID:-__HEAD_CID__}" (scripts/install.sh:52 on 355bddd5) and bind_verified_head_cid refuses a stamp that names other bytes. The green log reached Installed head CID matches the pin.

F9 cold publisher fetch — still present. preflight.ts:206-214 still GETs release-head.json inside collectPreflight.

F10 Reset — symlink and forward-slash PC2 checks are in the tip. assertNoSymlinkAncestors lstats each path component and refuses a symlink (runtimeUninstall.ts). mentionsPc2 normalizes backslashes, so C:/Users/tester/.pc2/elastos is refused. A failed lstat is skipped and the walk continues. The recursive delete still follows.

N1 rootfs — fixed on the path the Windows job ran. URL is …/24.04/20240423/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz, SHA-256 8251e27ffff381a4af5f41dcb94d867de3e0d9774a9241908ab34555d99315ea, size 356739129, cap 400MB (windowsHost.ts). downloadToFile refuses any other URL, stops past the cap, and calls assertPinnedRootfs before the write. The Windows log says Rootfs SHA-256 matches the pin. ensureElastosRootfs returns a cached file without hashing when the caller omits read and accept. The product path passes both.

N4 env pin — fixed for production, open for one env flag. pinLoadMode returns dev when NODE_TEST_CONTEXT is set, before it looks at app.isPackaged (runtimePin.ts:121-129). Dev mode still takes ELASTOS_RUNTIME_PIN first and does not call overrideKeepsTrustRoot. A packaged app whose environment contains that variable gets the old trust-root bypass. Production mode throws ELASTOS_RUNTIME_PIN cannot replace the packaged trust root when the DID, gateway, installer hash, or channel CIDs differ.

N5 Fix it copy — still ahead of the call. The interrupted screen still says setup continues without downloading again (runtimeRecovery.ts). resumeSetup is installRuntimeOnThisComputer(), which now fetches and verifies release-head.json and skips the installer when the signed head matches. The installer bytes are not downloaded again. The head document is.

New blind spots in #28–#36

N7 — S2 — the Install button and the pinned installer disagree. runtimeInstall.ts:279-286 runs bash install.sh --install-only --head-cid <pin> --maintainer-did <pin>, and installerChildEnv deletes ELASTOS_HEAD_CID. On runtime 355bddd5, scripts/install.sh:1188 is *) die "Unknown option: $1". There is no --head-cid option. The head stamp is the environment variable at line 52. #36 changed the smoke to:

env -u ELASTOS_MAINTAINER_DID -u ELASTOS_PUBLISHER_GATEWAY \
  ELASTOS_HEAD_CID="${head_cid}" \
  bash "${installer}" --install-only --maintainer-did "${maintainer}"

That is the invocation the green run executed. test/runtime-install.test.js still expects --head-cid and expects ELASTOS_HEAD_CID to be absent. test/smoke-required.test.js expects the smoke to contain no --head-cid. Both tests can pass. The smoke does not exercise the Install button. A click dies with Unknown option: --head-cid after the hash check, so the wrong bytes are not installed. The one-click path does not finish.

N8 — S3 — Windows Fix it treats any OK as ready. launchController.ts:271-279 calls windowsFixResult, which returns ready when isHealthOk is true. It does not call foreignListenerIsElastos. Enabling WSL or importing the distro does not start the gateway, so this path stays still-blocked until something answers OK. The product copy still says the button cannot finish the install inside the distro.

N9 — S3 — the foreign check reads the command line, not the socket. A live pid whose ps or /proc/pid/cmdline text matches elastos gateway is enough. The listener on port 8090 is not tied to that pid. On Windows, ps failure returns a null command and Home stays closed.

What is left to reach B

The composed launcher tip reaches B when every principle below D is at least B. That is four changes:

  1. Pass the head the way install.sh accepts it, in runtimeInstall.ts, not only in the smoke. Set ELASTOS_HEAD_CID to the verified pin after clearing a parent value. Stop passing --head-cid. Point the unit test at that same invocation. One Install-button log, or a test that runs the pinned script’s option parser, has to show the install get past argument parsing.
  2. Remove NOPASSWD:SETENV from pc2Manager.ts:1034 in its own change.
  3. A packaged app stays in production pin mode when NODE_TEST_CONTEXT is set. Dev override stays on an unpackaged app.
  4. Finish the Windows journey inside the distro, or keep Fix it on still-blocked until that journey exists, and use foreignListenerIsElastos for that /healthz check. The interrupted-screen sentence should say the signed head is checked again.

The composed runtime tip reaches B when the #398 exit-75 sentence matches the launcher: restart for a replaced binary or a changed hash; a move, an unreadable path, and a failed re-hash stay stopped. Intel stays C until the signer admits x86_64-darwin and a signed artifact exists. A green local /healthz on a built binary does not change that.

What is left to reach A

B is the floor. A also needs this evidence and these residuals closed:

  • An Electron Install click on the same commit as the smoke, with the signature line and Installed head CID matches the pin in that click’s log.
  • elastos update --rollback-to of the served 0.8.0-alpha.19 head, then a return to 0.8.0-alpha.20, on a real binary. The two head objects are on the publisher. The command log is not.
  • The cold collectPreflight fetch of release-head.json removed, or moved to an action the person started. The support-bundle scrub is still a line filter (preflight.ts:129-136).
  • The foreign-gateway check bound to the process that accepted the /healthz connection.
  • The lock unlink and the Reset delete repeated against the same inode after the lstat, with a failed lstat refusing the delete.
  • ipfs add of the live head compared with the local UnixFS helper, or a written acceptance that this helper is the publisher’s definition. This card shows the helper, the pin, and the served bytes agree. It does not show Kubo.
  • The tag build notarized. The checklist item is still open.
  • Intel A only with a signed x86_64-darwin artifact the signer admits. Until then the honest grade is the refusal.

No grade here is a merge approval.

A moved path, an unreadable path, and a failed re-hash exit 1. The
launcher leaves that process stopped. The contract uses the same rule.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
cursoragent and others added 2 commits October 16, 2026 11:36
Restore the binary watch to the develop exit-75 behavior. This pull
request no longer changes which supersession paths restart. /healthz
returns the two bytes OK only when this process owns host-process.lock.
Passkey registration and sign-in use http://localhost:8090 and refuse
an IP-literal origin. The launcher contract states those facts and
stops using English stdout as a supervisor interface.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
The configured-host /healthz check writes the gateway lock before it
expects HTTP 200. Local guest fixtures use rp id localhost, matching
the origin the session grant accepts.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
elastos serve holds host-process.lock as serve, and its supervisor can
start a gateway in the same process for elastos room open. The readiness
change made that gateway take the lock a second time, so it stopped at
start with "another ElastOS host already owns" naming its own pid. The
gateway now keeps a record that already names this process. Its /healthz
returns 503 because the role is serve, not a Home gateway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…efused

Every release takes the relying party id from the page host, and
browsers refuse an IP address as that id, so a stored passkey names the
host where it was made. A test signs a stored localhost owner passkey in
at three ports and a public passkey at its https host, and refuses both
at foreign, IP-literal and look-alike hosts without a state change. The
refusal is HTTP 403 with plain text that names http://localhost:<port>/home/
for a loopback page, instead of "owner enrollment unavailable".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ract

/healthz describes the process that answers on the port, so another
gateway for a different data directory also returns OK. The contract now
says what a supervisor checks before it trusts OK. It also states that
--addr with a host name binds one address, so localhost:8090 listens on
one address family. The healthz test adds another role, a missing lock
and a string pid, and pins the gap text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
cargo-audit fails on every PR: zip 2.4.2 in elastos/Cargo.lock and in
capsules/object-provider/Cargo.lock (through its elastos-server path
dependency) has three advisories against ZipArchive::extract and the
stream extract API. The runtime calls neither; it reads each entry
itself. zip 9.0.4 is the patched release.

- elastos-server uses zip 9.0.4, still without default features. 9.x has
  no separate flate2 feature; deflate-flate2 now enables the dependency.
- Both lockfiles change only for zip: nix 0.31.3 and typed-path come in,
  derive_arbitrary goes. `cargo update -p zip` also moved unrelated range
  dependencies (windows-sys, getrandom); those lines stay as they were,
  and `cargo metadata --locked` accepts both files.
- ZipFile::name now returns a Result. The ZIP readers report an error as
  "invalid ZIP archive entry".
- Whole-archive extract keeps its rule for entry names. zip 9
  enclosed_name turns "/a.txt" into "a.txt" and folds "x/../a.txt" into
  "a.txt", so an entry that extract refuses today would be written. The
  runtime now checks the name with the rule zip 2.x applied: no NUL byte,
  no root or drive prefix, no climb above the archive root.

test_library_provider_extract_zip_refuses_absolute_dotdot_and_symlink_entries
covers absolute, "..", inner "..", backslash and symlink entries. It
passes on zip 2.4.2 and with this change, and fails on the Absolute.zip
case when extract uses zip 9 enclosed_name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
zip 9 for RUSTSEC-2026-0367/0368/0369 so cargo-audit passes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
claude and others added 5 commits October 17, 2026 09:17
…lhost

Passkeys work only at localhost, and a browser reaches localhost on
127.0.0.1 and on ::1. --addr localhost:<port> now binds each address the
computer resolves for localhost plus 127.0.0.1 and ::1, on one port, and
serves the same router on each. Another program on any of them, or any
other bind error, stops the start with a message that names the address.
Only an address family the computer lacks (EADDRNOTAVAIL, EAFNOSUPPORT)
is left out, and at least one address must bind. A localhost that points
off this computer is refused. An IP-literal --addr binds exactly as
before, and the host lock still records --addr as given.

When the gateway listens on localhost, a GET or HEAD for /, /home,
/home/, /apps/home or /apps/home/ with a loopback IP Host gets 308 to the
same path at http://localhost:<port>. Health, API, asset and app routes
answer on every address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The contract now states the localhost listeners, the redirect from a
loopback IP Home page, that localhost is the one Home origin, and that
the lock records --addr as given. It drops the single-family gap and
names the untested Windows-to-WSL2 localhost forwarding. HOME_LOCAL_SETUP,
INSTALL, MAC and WINDOWS cite it; 61180 is labelled the developer lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…d retry (#381)

Port of develop commit 5051505 (Anders Alm). The test flaked in CI with
"WouldBlock" at journal.rs:4223 on PR heads that predate the fix.

(cherry picked from commit 5051505)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ecord

Any process that runs as the same user can rewrite the JSON record in
host-process.lock. /healthz returned OK when that record named this pid
with role gateway, and a gateway skipped taking the lock when the record
named its pid. A forged or stale record could therefore make a process
that holds no lock answer OK, or start a gateway with no host lock.

This process now remembers each host lock it holds (role, device and
inode of the locked file). /healthz and publish_gateway_lock_if_unowned
answer from that state: the path must name the same file now, and a new
open of it cannot take the lock. The record is not read. A removed or
replaced lock file, a lock another process holds, and a released lock
return 503. The launcher contract states what proves ownership.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

36720070: /healthz and publish_gateway_lock_if_unowned now prove ownership from the flock this process holds on the file the lock path names (device/inode plus a failed re-lock), not from the lock file's JSON, which any same-user process can rewrite. Proof: test_healthz_refuses_a_forged_or_foreign_lock_owner and gateway_takes_the_lock_when_only_a_forged_record_names_this_process fail with the old JSON checks restored (left: 200, right: 503 for a forged gateway record) and pass with the fix.
Red run 38899842659 is explained in the body's CI section (outside observer stopped the 4 GiB journey before engine_stopped; its message was dropped, #413 records it).


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…-1297

Brings in the zip 9 merge, the journal test cherry-pick and the lock
ownership fix 3672007 from the base branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…ke-1297

Brings in #394's proof of gateway lock ownership from the held flock (3672007).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 3 review: fixes needed (B-). Host-lock and forged-owner tests pass.
The subordinate-lane check still trusts lock-file metadata; use the same ownership proof as /healthz.
Make the registry lookup and lock probe one step, and test release and retake.

held_host_role looked up the registry, released its mutex, and then
probed the lock with a new open of the file. A release and a retake by
another holder could come between the two, and the probe then saw the
other holder's lock and answered with this process's role. A registry
entry also stood for any lock on the same file, so a guard whose open
file was unlocked behind its back still counted when another holder
took the lock.

Each registry entry now keeps a second descriptor of the open file that
holds the flock. The proof keeps the registry mutex from the lookup to
the end of the probe, and a guard removes its entry under that mutex
before it unlocks. The probe checks that the path names the same file,
that a new open cannot take the lock, and that the entry's own open file
can take it again (which is a no-op only for the current holder).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
A managed runtime ran as the subordinate of a gateway when the JSON
record in host-process.lock named this pid as a gateway, or, for a Home
terminal, named its parent pid as a gateway. Any process of the same
user can write that record, so a forged or stale record opened the lane
while another host held the lock.

The lane now uses the same proof as /healthz. In the gateway process,
holds_gateway_host_lock checks the flock it holds. A Home terminal gets
a descriptor of the gateway's locked open file and the gateway pid when
the gateway starts it; it must be that gateway's direct child, and that
descriptor must hold the flock now (the path names the same file, a new
open cannot take the lock, and the inherited open file can). After the
gateway releases the lock, or another holder retakes it, the lane is
refused. The record only names the holder in the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
active_host_process reported no active host when the lock was held but
its JSON record was empty, being written, or not JSON. The update
controller, principal-root upgrade and the managed runtime lane then
treated a running host as stopped. The flock now decides whether a host
is active; such a holder is reported with role "unknown".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

4bb19dcf: the subordinate lane now uses the /healthz flock proof (in-process, or the gateway's inherited lock descriptor for a Home terminal). The registry lookup and the probe now run as one step under the registry mutex, against the holder's own open file. A held lock whose record cannot be read counts as held.
Proof: subordinate_lane_* (runtime_control.rs), release_and_retake_prove_only_the_current_holder, a_release_and_retake_cannot_come_between_the_lookup_and_the_probe, an_entry_for_the_same_file_is_not_proof..., active_host_process_reports_a_holder_whose_record_cannot_be_read. Each one fails when its fix is reverted (body lists the reverted lines). prove_ready record check moved to #437.


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…ke-1297

Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…ame-1297

Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #395.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #395 and #397.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…-root

Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #394 4bb19dc: one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held. host_lock.rs merged without conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@andersalm andersalm mentioned this pull request Oct 17, 2026
9 tasks
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 4 at 4bb19dc: B. One-step proof, lane proof and unreadable-record fixes all proven; the round-3 test gap is closed; CI green.
Contract: state that the lock proves possession, not that the holder is the gateway (a same-user process can hold it); add that test.

… is the gateway

Holding the flock on host-process.lock proves that a process has the
lock. It does not prove that the holder is a gateway or ElastOS: any
process of the same user can open the file and hold it. The launcher
contract now says so for /healthz, for the Home terminal's inherited
lock descriptor (ELASTOS_GATEWAY_HOST_LOCK_FD), and for the "another
ElastOS host already owns" error, which names the holder from a record
that can be false. Code comments in host_lock.rs, gateway_site.rs and
runtime_control.rs say the same.

Two tests pin the statement with real processes. In one, /bin/sleep
opens the lock file and takes the flock: the gateway does not start by
either path, this process's /healthz answers 503 and it gives no lock
descriptor to a child. In the other, this test process takes the flock
with a plain open (not as a host) and gives the descriptor and the
gateway variables to a real child process; the child's Home terminal
check accepts it while the lock is held, and refuses a descriptor that
does not hold the lock and the same descriptor after the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

SashaMIT commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

13cdcad0: docs/LAUNCHER_CONTRACT.md now says the host lock proves possession, not identity (also for the inherited ELASTOS_GATEWAY_HOST_LOCK_FD); docs and comments only, no behaviour change.
Proved with real processes: test_the_host_lock_proves_possession_not_that_the_holder_is_the_gateway (a /bin/sleep holder blocks the gateway, /healthz 503) and home_terminal_lane_proves_lock_possession_not_that_the_parent_is_the_gateway (a non-gateway parent's descriptor is accepted by the child); both fail with possession treated as identity.
CI: test-elastos fails in develop's own Home link test (403 vs 200, home_system.rs:11964) because pull_request CI merges develop 8ea4aac; fixed by #411's 619c096, already ported to #413; not this PR's.


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
…ke-1297

Brings in #394's statement that the host lock proves possession, not that
the holder is the gateway, with its two process tests (13cdcad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
…ame-1297

Brings in, through #395, #394's statement that the host lock proves
possession, not that the holder is the gateway (13cdcad), and #395's
hermetic default-command smoke test (630efc6) and "holds" wording
(ac97a4a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
Brings in, through #397 and #395, #394's statement that the host lock
proves possession, not that the holder is the gateway (13cdcad), and
#395's hermetic default-command smoke test (630efc6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
Brings in #398's portable swapped-Runtime test (da63c23: the Linux case
mocks a GNU-like mktemp instead of using the host's), and, through #397
and #395, #394's statement that the host lock proves possession, not
that the holder is the gateway (13cdcad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
…-root

Brings in #398's portable swapped-Runtime test (da63c23: the Linux case
mocks a GNU-like mktemp instead of using the host's), and, through #397
and #395, #394's statement that the host lock proves possession, not
that the holder is the gateway (13cdcad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
Brings in #394 13cdcad: the launcher contract states that the host lock
proves possession, not that the holder is the gateway, also for the
inherited ELASTOS_GATEWAY_HOST_LOCK_FD, with two process tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Brings develop to bfefe92, which fixes develop's own #430 Home link
test (it now uses Library, which every platform launches). This PR's
pull_request CI merges develop, so its test-elastos runs on 13cdcad /
16a132a tested develop 8ea4aac/0dbba668 and failed on that test
(403 vs 200, home_system.rs:11964). No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

Merged develop at bfefe92f (d30027ed, no conflicts). It carries develop's fix for its own Home link test, which failed test-elastos on 13cdcad0 because this PR's CI merges develop. zip stays 9.0.4.
Narrow suite at d30027ed (home_external_link, possession, home_terminal_lane, healthz, host_lock): 27 passed.


Generated by Claude Code

@SashaMIT

Copy link
Copy Markdown
Contributor Author

Re-review at d30027e: PASS (A); independent review PASS. Only a develop merge; lock tests and the gateway-identity tamper still hold; all CI jobs green.

SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
…ke-1297

Brings in #394's merge of develop (d30027e).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 19, 2026
Brings in #394's head d30027e, which merges develop at bfefe92
(develop's fix for its own #430 Home link test). No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants