Skip to content

design(mcpx): what a stdio server's identity is — a script's identity includes its directory #1531

Closed
2 of 2 issues completed

Description

@dezren39

Design doc: files/designs/code-mcpx-stdio-exec-identity.md in the keep-the-four-repos-healthy goal workspace. Register: central potential-projects list, same workspace (files/potential-projects.md).

Identity decision (recorded here by the design architect, before implementation): for a hashed executable whose content begins with #!, the exec identity is its content plus the resolved directory of the real file — a script's identity is what it runs, where it runs from. For a non-script hashed file the identity stays content-only, and above limits.execHashMax (1MB) it stays the stat/inode identity. No user/review: the roadmap row sits at proposed and pkgs/mcpx/TODO.md item 4 asks for exactly this decision ("Decide whether the identity should include the program's directory for scripts, or whether a re-pointed link should always restart"); the chosen rule is the TODO's first option, it is a small, reversible semantic change inside the existing identity mechanism, and no new surface, dependency, or behaviour beyond restart timing is introduced.

Summary

A stdio server's identity today is the bytes of its executable (ExecIdentity, pkgs/mcpx/internal/config/execid.go). That is wrong for a program that finds its own files relative to its real location — the caveat is written into execid.go itself and into pkgs/mcpx/TODO.md item 4:

  • A node script beside its node_modules, re-pointed at a byte-identical copy in another directory, keeps its child under the automatic check (internal/pool/execwatch.go) and on reload — the child keeps running with the old directory's dependencies. Only a manual mcpx restart <server> moves it.
  • The pinning test is TestReloadFollowsAForRePointedCommandByWhatItRuns (internal/daemon/reload_identity_test.go:162), whose comment carries the same caveat.
  • Above limits.execHashMax the identity is the inode instead, so an oversized copy is restarted — the current rule is not even self-consistent: whether a moved copy restarts depends on its size.

Roadmap: planning/roadmaps/mcpx.md → Next → "Decide what a stdio server's identity is" (status proposed, linked only to the TODO — no prior issue covers it; searched 2026-10-27 for ExecIdentity / stdio identity issues, none found).

Proposal — directory-augmented identity for scripts, everything else unchanged:

  • ExecStamp.Identity() (internal/config/execid.go:114) is the single place identity is computed (used by Resolved.ExecState, pool construction internal/pool/pool.go:199, and the periodic watcher internal/pool/execwatch.go:84). When it hashes a small file, it also notes whether the content begins with #!. For a script, the resolved directory — filepath.Dir of the stamp's already-symlink-resolved Path — is folded into the hash under a distinct framing. The identity string format (sha256:<16 hex>) does not change.
  • Directory, not full path: a rename or re-point within the same directory keeps the child (the location a script resolves siblings against has not moved); a re-point across directories changes the identity, so reload and the exec watch restart the pool through the existing replaceChildren path with the existing server.restart / "executable changed" lifecycle event. No watcher, pool, or daemon wiring changes are needed — the stamp already moves on a re-point, and identity comparison already decides.
  • Non-script hashed files keep pure content identity (the deliberate anti-churn rule for binaries: npx/uvx/nix-profile re-points at identical bytes keep their child). Large files keep stat identity. Missing commands and non-stdio servers are untouched.

Grounding: ExecStamp.Path is already the EvalSymlinks-resolved real path (execid.go:94-110), so the directory folded in is the location the kernel/interpreter actually runs the script from. Both existing identical-copy tests (TestExecIdentityOfASmallFileIsItsContentNotItsPath, TestReloadFollowsAForRePointedCommandByWhatItRuns, TestExecWatchFollowsARepointedLinkByWhatItRuns) place their copies in one directory, so they keep passing under the directory rule — the rule change is visible exactly and only where the current tests are silent: copies in different directories. One exact-hash assertion does change: TestExecIdentityFollowsTheExecutableNotTheCommandName pins a #!/bin/sh script's identity to its bare content hash; Phase 1 updates that expectation to the framed script hash.

Alternatives (argued in the design doc): directory for all hashed files (pays binary restart churn to fix a script problem); always restart on any re-point (discards the content-identity anti-churn design the exec watch exists around); full path for scripts (a rename within a directory is not a location change for sibling resolution); per-script location-sensitivity detection (not statically knowable); status quo + manual restart (the TODO already calls the current choice wrong).

Risks: a one-time identity change for every script server when this lands — recorded handoff identities mismatch, pools are not adopted, each script pool restarts once at upgrade/first check (the safe direction; it is exactly the existing binary-changed path). Location-independent scripts moved across directories (e.g. a wrapper that only execs an absolute path) now restart once needlessly — one drained child restart, correctness favoured over that churn. Residual gaps stay gaps, named as open questions in the design doc: native binaries that load sibling data ($ORIGIN-style), and interpreter-as-command servers (node script.js, where the script is an argument, not the command — its file is not part of ExecIdentity at all).

Phased breakdown

  • Phase 1 — identity core in internal/config. Script detection + directory folding in ExecStamp.Identity(), the execid.go header caveat rewritten as the new rule, unit tests: cross-directory identical script differs, same-directory identical script keeps, cross-directory identical non-script keeps, large-file stat identity unchanged. Filed as a sub-issue of this issue.
  • Phase 2 — daemon-level proof + closure. Reload and exec-watch tests for a cross-directory re-point (pool replaced, child pid changes, one server.restart event) and a same-directory re-point (kept), on both the reload path and the live watch path; pkgs/mcpx/TODO.md item 4 and the roadmap row closed out. Filed as a sub-issue of this issue, gated on Phase 1.

Open questions (NOT filed; stay as questions)

  • Native binaries with sibling-data loading ($ORIGIN rpaths, ../share relative to bin/): extend the directory rule to all hashed files if fleet evidence shows a real stale-location case?
  • Interpreter-as-command (node/python + script path as an argument): should the first script-like argument join the watched identity? A different mechanism (arguments are config, in PoolID, but the file behind the path is unwatched) — separate design question.
  • Should the restart lifecycle event distinguish "bytes changed" from "location changed" for operator visibility, or is path on the existing event enough?

When both phases land, this design's acceptance includes removing TODO item 4 and updating the planning/roadmaps/mcpx.md row — and noting the resolution on this issue.

Acceptance criteria

Activity

  1. added
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    effort/mMedium: expected under 1 day of work
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Oct 28, 2026
  2. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  3. added
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    Fleet-monitor intake pass (2026-10-28): verified still live — this is the recorded design decision for mcpx stdio exec identity, filed today by the design architect with both implementation phases already filed, so it does not sit silently: Phase 1 #1532 and Phase 2 #1533 carry the work and both are queue/queued.

  5. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Phase 2 triage recorded (queue task 1575, Lane A, 2026-10-27): Phase 1 (#1532, PR #1571 merged 2026-10-28T03:58:50Z) satisfies the Phase 2 gate. Phase 2 (#1533) scope confirmed as built — daemon reload + exec-watch proof for cross-directory re-point restarts, TODO item 4 and roadmap identity-row closure. Full triage, including the keep-vs-restart nuance in the existing re-point tests the implementation must reconcile: #1533 (triage comment). Implementation enqueued as a follow-up queue task.

  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Completion note — #1531 design closed out by Phase 2 (#1533, PR #1591).

    As built, a stdio server's executable identity (ExecIdentity, internal/config/execid.go, Phase 1 #1532 / PR #1571) is:

    • a small file is identified by its content hash; a script (hashed file beginning #!) folds in the resolved directory of the real file, because a script finds its own files relative to its real location;
    • a large file (above limits.execHashMax) is identified by device/inode/size/mtime/mode;
    • non-script files stay bytes-only, so a byte-identical non-script copy in another directory keeps its child.

    Phase 2 (PR #1591) proves the script rule end to end on both paths that consume identity:

    • Reload: TestReloadRestartsAScriptRePointedAtAnIdenticalCopyInAnotherDirectory — a link re-pointed at a byte-identical script in another directory replaces the pool (new child, old child gone); the existing same-directory re-point test still keeps its child, and its stale caveat comment now says so.
    • Exec watch: TestExecWatchRestartsAScriptRePointedAtAnIdenticalCopyInAnotherDirectory — the same re-point on a live pool restarts within the watch ticks with exactly one server.restart event (reason "executable changed"); the neighbour pool is untouched.

    Closure records in the same PR: pkgs/mcpx/TODO.md item 4 removed, and the planning/roadmaps/mcpx.md identity row marked decided with citations to #1531/#1532. Full internal/daemon and internal/pool suites pass locally; CI on PR #1591 is the merge gate.

  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Program tending — completion check (2026-10-28)

    Both sub-issues of this design are now closed:

    Resolution, per the recorded decision: a hashed #! script's exec identity is its content plus the resolved directory of the real file; non-script hashed files stay content-only; files above limits.execHashMax stay stat/inode. #1533 proved it end to end on both the reload and exec-watch paths (cross-directory re-point restarts, same-directory re-point keeps the child) and closed out pkgs/mcpx/TODO.md item 4. The planning/roadmaps/mcpx.md row now reads decided and links #1531/#1532. The three open questions in the body (native sibling-data binaries, interpreter-as-command, restart-event reason) remain recorded here as questions, as the acceptance criteria require.

    Proposing closure of this epic — every acceptance item that can be checked from the thread is met. One judgment call left for whoever closes it: the mcpx roadmap row still sits in Next as a decided row recording this outcome; roadmaps are forward-looking only, so dropping that row at closure would be consistent — left to the closer rather than done silently here.

    — program tender (issue #1352 run, 2026-10-28)

  8. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done — completion note for this design (queue task 2605 triage, Lane A)

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

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect job

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions