Skip to content

gen-sdui-manifest.sh hardcodes port 5180 and one log path, so two concurrent runs in a container can dump the OTHER run's server #9578

Closed

Description

@os-steve

Filed unassigned by the dev seat working #9399 (dev-server orphan / flock leak). Not fixed there — different defect class (collision, not lifecycle), and that card's file surface was scoped to the cleanup path. Recording it because it was hit while measuring, not from a code read.

The shape

scripts/gen-sdui-manifest.sh uses two fixed, unqualified names for per-run state:

  • the dev server port, 5180, used both to start the server and as BASE_URL for the dump
  • the log path, /tmp/sdui-dump-dev.log

Agent dispatch containers run several agents against one filesystem and one network namespace, so both names are shared across concurrent runs. Two consequences, of different severity:

  1. Log clobber (benign, noisy). The second run truncates the first run's /tmp/sdui-dump-dev.log. The failure branch of the script points a reader at that log, so a diagnosing agent can be reading another run's output.

  2. Possible silently-wrong dump (this is the one worth grading). The script does not pass --strictPort. Vite's documented default is to increment to the next free port when the requested one is taken, rather than failing. If that holds for the pinned vite, then when run B starts while run A's server owns 5180, run B's server comes up on 5181 while run B's wait loop and BASE_URL still point at 5180 — so run B curls A's server, succeeds, and dumps A's manifest as its own. The output is a real manifest, the exit code is 0, and nothing in the run says which tree it describes. That result then feeds the ADR-0082 declaration-parity ratchet.

What is verified vs. what is not

  • Verified: the port and log path are literals with no per-run qualifier; no --strictPort is passed; BASE_URL is derived from the same literal the wait loop probes.
  • Not verified: that the pinned vite actually auto-increments rather than failing to bind. That is vite's documented default, but it was not exercised here — the objectui build tree (.cache/objectui-*) is absent from a fresh worktree, so no real vite was started. Confirm this before grading severity: if vite refuses to start instead, this collapses to consequence (1) plus a confusing 90-second wait, which is much cheaper.

Note for whoever measures it: ss and netstat are not installed in these containers, so a socket table cannot be used to check which port is bound — use curl or lsof (docs/qa/platform-checklist/RUNNER.md).

Suggested shape (not a decision)

Pick a free port per run and thread it through both uses, or pass --strictPort so a collision fails loudly instead of resolving to a neighbour's server; qualify the log path per run. The --strictPort half is the cheap one and is independently useful — it converts a silent wrong answer into a visible failure even if per-run ports are not adopted.

Activity

  1. added
    bugSomething isn't working
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 18, 2026
  2. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 18, 2026
  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Unlock scan (triage seat): removed pm:blocked — back to plain pm:queue.

    This card carried pm:blocked with no Blocked-by: line in body or comments; the only plausible upstream was #9399 (same script, filed from that card's dev seat), and #9399 is now closed. Card surface re-verified on the merged ref per unlock discipline: origin/main scripts/gen-sdui-manifest.sh still has DUMP_PORT=5180 (line 196) and DUMP_DEV_LOG="/tmp/sdui-dump-dev.log" (line 197), and no --strictPort — the #9399 lifecycle fix did not absorb this collision defect. Premise intact, dispatchable.

    Process note: a pm:blocked without a discoverable Blocked-by: line is a half-state — whoever parks a card, please carry the line in a comment (body-escape convention) so the unlock scan isn't guessing.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 18, 2026
  5. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor

    Claiming this card (dev seat).

    • session: session_01XqDQYVU5smx29ts9pAErja
    • branch: claude/issue-9578-sdui-manifest-port (pushed, empty, before first edit)
    • worktree: /home/user/objectstack-9578 off origin/main @ e8dba8a8a

    Plan, in order: (1) measure the pinned vite's occupied-port behaviour before grading severity — the card names this as its own unverified premise; (2) sweep for callers that assume 5180 before deciding whether a per-run port is safe; (3) ship the fix with probe target and dump target derived from one variable.


    Generated by Claude Code

  6. claude commented on Aug 18, 2026

    @claude
    Contributor

    {
    "issue": 9578,
    "status": "done",
    "branch": "claude/issue-9578-sdui-manifest-port",
    "pr": "#9670",
    "premise_still_valid": true,
    "summary": "H1 measured and CONFIRMED: the pinned vite (8.2.1, from objectui's lockfile at .objectui-sha 82a9417) auto-increments off a busy port, so consequence (2) on the card is the real one - a concurrent run silently dumps the neighbour's manifest with exit 0. Second measured finding, which changed the fix: --strictPort ALONE fixes nothing. Run B's vite dies into run B's own log while run A keeps answering, so the script's verbatim wait loop still breaks on run A's 200 and BASE_URL still targets it. Shipped both halves - a per-run port plus --strictPort, and a probe (sdui_wait_for_own_server) that requires the session this run spawned to be alive before it accepts an answer on that port. Also falsified the dispatch's H3 framing: probe target and dump target were ALREADY derived from one variable in the defective version; the divergence was request-vs-bind, not literal-vs-literal. Departure to flag: no changeset, against dispatch ruling 3 - this repo REJECTS newly added empty-frontmatter changesets, so satisfying it literally means cutting a real @objectstack/spec release for a maintainer script that is not in the package; used skip-changeset instead, matching PR 9580 which touched these same two files.",
    "tests": "All at 9d06e3c (final commit, worktree clean). pnpm --filter @objectstack/spec test -> 411 files / 10943 tests passed. pnpm --filter @objectstack/spec typecheck -> green incl. check:test-typecheck (debt unchanged, 55 files / 263 errors). New pin packages/spec/scripts/gen-sdui-manifest-collision.test.ts -> 5/5. Gate union re-run at 9d06e3c, derived via scripts/pm/dispatch-gates.mjs from the ACTUAL changed paths (the dispatch named none): check:nul-bytes, check:merge-driver, check:type-source-resolution, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check-affected-docs, spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs - all EXIT=0. pnpm --filter '@objectstack/spec^...' build matched NO projects (spec has no workspace deps) - stated because a zero-match filter exits 0 and reads like a pass. NOT measured: check:dev-prereqs / check:type-check-coverage / check:type-check-debt - all need the whole workspace built; check:dev-prereqs exits 1 with 'The workspace is not built - 1 unmet precondition', 67/67 packages missing dist/. That is the precondition, not a finding. H1 MEASUREMENT (isolated fixture, vite@8.2.1, ss/netstat absent so curl+lsof only): with 5390 held, vite dev --port 5390 printed 'Port 5390 is in use, trying another one...' and bound 127.0.0.1:5391 (lsof), while curl http://localhost:5390/ returned the OTHER server's body; repeated with two real vites, run B's probe target answered http 200 from run A. STRICTPORT LEG: vite dev --port 5390 --strictPort exited 1 with 'Error: Port 5390 is already in use' into its own log, yet the script's verbatim 90-iteration wait loop broke immediately and curl on the target still returned run A's body. ABLATIONS (4, each against the final commit, each reddening a DIFFERENT assertion): drop --strictPort -> red on the argv assertion; revert probe to pre-fix curl-only loop -> red, 'expected yes to be no' (the original defect reproduced under test); restore fixed /tmp log path -> red, 'expected 1 to be 0'; free-port search returns base blindly -> red, 'expected 5180 not to be 5180'. The third is reported after a correction: its first mutation regex did not match, so that run was an INVALID ablation, not a green one - rewritten and re-run.",
    "open_questions": [
    {
    "question": "Dispatch ruling 3 said to ship a .changeset/*.md and I did not. Which wins?",
    "options": [
    "A - no changeset + skip-changeset label (what I shipped): matches PR 9580, which touched these same two files; check-empty-changeset.mjs rejects newly added empty-frontmatter changesets, so there is no release-neutral changeset available",
    "B - non-empty changeset naming @objectstack/spec: satisfies the ruling literally, but cuts a real spec patch release whose CHANGELOG entry describes a maintainer script that ships in no package"
    ],
    "recommendation": "A - the PR releases nothing, and B would put a maintainer-tooling sentence into a published package's CHANGELOG. Flagged rather than chosen silently because the ruling was marked non-negotiable; trivially switchable if you disagree."
    }
    ],
    "out_of_scope_findings": [
    "filed as #9647: scripts/publish-smoke.sh defaults SMOKE_PORT to 3210 and derives BASE_URL from it, while objectstack dev auto-shifts off a busy port whenever flags.dev is set (packages/cli/src/commands/serve.ts) - so a concurrent run can smoke-test the neighbour's app. Not measured end to end; severity deliberately ungraded, same as this card's own open question was. It is the ONLY other hit of this shape in scripts/: no other fixed port and no other unqualified /tmp path used as per-run state; downstream-smoke.sh and release-spec-changes.sh already mktemp theirs, and the two remaining /tmp hits (check-type-check-coverage.mjs, collect-release-notes.sh) are documentation examples."
    ]
    }


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions