Task — engineering-spec@1

Install portability - MCP loopback+token, shasum fallback, atomic vendor

doneTASK-IMP-137
module improvement · class product · priority p1 · created 2026-07-23 · shipped null
depends on none · blocks none

TASK-IMP-137: Install portability - MCP loopback+token, shasum fallback, atomic vendor

Summary

Five verified portability/exposure defects in the install channel: the MCP server's --http mode binds every interface with zero auth while serving repo-rewriting, shell-running tools; bootstrap.sh hardcodes GNU sha256sum, breaking checksum verification on stock macOS; the payload's engines field admits Node 18 while shipping a subpackage that demands 24; the README documents a GitHub Action channel at a path install never creates; and the vendor step's rm -rf + cp -R leaves a reader-visible window where .cyberos/ is missing or partial. This task closes all five: loopback-by-default + optional bearer token, shasum fallback, one engine floor, a real ci/ vendor + docs truth, and stage-then-swap vendoring.

Problem

All verified first-hand 2026-07-23:

  1. LAN exposure by default (audit C4). cyberos-mcp.mjs:219 calls .listen(port) with no host, binding 0.0.0.0. POST /mcp accepts unauthenticated JSON-RPC and the tool set includes task_install (rewrites the repo) and task_gates (runs run-gates.sh, which evals configured commands). Anyone on the local network can drive both. The code comment defers auth to a production reverse proxy, but the default posture is the exposure.
  2. Checksum verification fails on macOS. bootstrap.sh:23 pipes to sha256sum -c; stock macOS ships shasum, not GNU coreutils. The security step of the curl|bash channel errors out on the platform the project is developed on - inviting users to bypass it.
  3. Engines contradiction. The generated payload admits node >=18 (build.sh:358); the shipped mcp/package.json demands >=24 <25; every .nvmrc pins 24.18.0. A Node-18 user passes npm's engine check and then runs components nobody has ever tested on 18.
  4. Phantom CI channel. README:158 tells operators to point a workflow at the composite action "after install.sh has committed .cyberos/ to the repo" - but install.sh never copies ci/, so the documented path does not exist in any installed repo.
  5. Partial-vendor window. Between rm -rf "$CY/cuo" and the completed cp -R, an agent reading .cyberos/ (which agents do constantly - it is their entry point) sees a half-machine. The TASK-IMP-103 lock serializes installers only.

Proposed Solution

MCP: --http binds 127.0.0.1 by default; a new --host <addr> flag opts into wider binding and its help text names the exposure; when the env var CYBEROS_MCP_TOKEN is set non-empty, every POST /mcp must carry Authorization: Bearer <token> (401 otherwise), while GET /healthz stays open for probes; binding non-loopback WITHOUT a token prints a loud warning. bootstrap.sh: detect sha256sum else fall back to shasum -a 256; fail with a clear message only when neither exists. Engines: the generated payload package.json pins "node": ">=24 <25", matching the .nvmrc floor and the mcp subpackage. CI channel: install.sh vendors ci/ into .cyberos/ci/ (ownership-marked like the other vendored trees), and README:158's instructions are corrected to reference the installed path with a working uses: example. Atomic vendor: each vendored subtree is staged as "$CY/<name>.tmp.<nonce>" then swapped into place (rm -rf old + mv staged) so the reader-visible gap per subtree shrinks from the full copy duration to a rename; the existing install lock continues to serialize installers. A new suite covers all five behaviors.

Alternatives Considered

Success Metrics

Scope

In scope: the five fixes above, their README/mcp-README corrections, the new test suite, CHANGELOG.

Out of scope / Non-Goals

Dependencies

None blocking. TASK-IMP-076 (done) shipped the MCP server and its --http mode; TASK-IMP-103 (done) shipped the installer lock whose reader gap this task closes the other half of. TASK-IMP-140's G16 gate builds on the atomic-vendor guarantee - forward reference only.

AI Authorship Disclosure

1. Description (normative)

2. Acceptance criteria

3. Edge cases

Audit

§1 — Verdict summary

Seven §1 clauses, seven ACs, seven edge cases including a security-class row (this task is itself a security fix). The audit pressure fell on verify-don't-skip semantics in the checksum fallback, the atomicity claim's honest bounds (what a kill mid-swap can still do), and making the two breaking changes (engines, binding) carry explicit migration paths.

§2 — Findings (all resolved)

ISS-001 — the checksum fallback could have been implemented as a skip

"Fall back when sha256sum is absent" is satisfiable by skipping verification — the worst possible reading for the one security step in a curl|bash channel. Resolved: clause 1.3 states "the fallback MUST verify, not skip"; AC 3 asserts the fallback FAILS on a corrupted archive (the negative half that distinguishes verifying from skipping).

ISS-002 — the atomicity claim was absolute in the first draft

"Atomic vendor" overclaims: rm -rf old && mv staged is two operations, not one atomic rename, and a kill between them still leaves a gap. Resolved: clause 1.6 bounds the claim honestly (window bounded by rename/move operations, not copy duration; stray staging cleanup at next install), and the edge case names the pathological kill-inside-the-pair remnant with its recovery story. AC 6 tests both the reader loop and the kill simulation at the stage-complete point.

ISS-003 — empty-token semantics were undefined

CYBEROS_MCP_TOKEN="" could be read as "auth on, all requests fail" (bricking loopback use via a stray export) or "auth off". Resolved: edge case pins empty-as-unset with the rationale (an empty bearer is unusable as a credential) and requires the mcp README to document it.

ISS-004 — token leakage via logs was unaddressed

A warning line that echoes the configured token (or a 401 body that reflects the attempted one) would put the secret in transcripts. Resolved: the security-class edge case requires that no token value is logged — the warning names the condition, never the secret.

ISS-005 — binding assertion needed to be observable, not configuration-echo

Asserting "we passed 127.0.0.1 to listen()" tests the argument, not the exposure. Resolved: AC 1 asserts via the OS-reported bound address AND a refused non-loopback connect where the harness supports a secondary address — behavior, not configuration.

ISS-006 — engines raise lacked a consumer migration statement

Raising the floor from >=18 to >=24 <25 is breaking for any consumer on older Node; the first draft noted "breaking" only generically. Resolved: edge case states the intended outcome (clear npm engines error) and requires the CHANGELOG to name the floor and the .nvmrc value; AC 7 asserts the breaking language.

ISS-007 — the vendored ci/ tree's uninstall story was unstated

Adding a new vendored tree without saying who removes it invites an uninstall leftover (the TASK-IMP-121/126 class). Resolved: Non-Goals states the tree is ownership-marked consistently so existing uninstall logic handles it, with TASK-IMP-126 named as the completeness owner.

§3 — TRACE-006 semantic sufficiency (per clause)

ClauseVerb demandCited test assertsVerdict
1.1 MUST bind loopback default; --host only opt-out; warn tokenless-widebound address + refused remote connect + wide-bind warningAC 1: asserts all threesufficient after revision (ISS-005)
1.2 MUST 401 without exact Bearer; healthz opentokenless 401, wrong 401, correct 200, healthz 200AC 2: asserts all foursufficient
1.3 MUST verify via fallback; abort when neither toolfallback success AND corrupted-archive failure + neither-tool abortAC 3: asserts all three including the verifying-negativesufficient after revision (ISS-001)
1.4 MUST declare >=24 <25exact engines string in scratch payloadAC 4: asserts exact matchsufficient
1.5 MUST vendor ci/ + correct READMEinstalled action.yml + uses: example + no stale claimAC 5: asserts all threesufficient
1.6 MUST stage-then-swap; clean strayszero reader absences over 20 reinstalls + kill leaves old tree + stray cleanupAC 6: asserts all threesufficient after revision (ISS-002)
1.7 MUST record five changes, two breakingfive mentions + breaking languageAC 7: asserts bothsufficient

§4 — Resolution

Seven findings — two security-honesty, five material — all resolved in the audited revision. Score = 10/10.

Status transition draft -> ready_to_implement is authorised by this verdict per STATUS-REFERENCE.md §1.1. The two human-acceptance gates in /ship-tasks are unchanged — this audit clears the spec-correctness gate only.


End of TASK-IMP-137 audit.