Skip to content

feat(client): the profile slot carries its own loopback port - #954

Closed
chughtapan wants to merge 2 commits into
impl/harness-status-schema-rehomefrom
impl/profile-slot-and-mcp-port
Closed

feat(client): the profile slot carries its own loopback port#954
chughtapan wants to merge 2 commits into
impl/harness-status-schema-rehomefrom
impl/profile-slot-and-mcp-port

Conversation

@chughtapan

Copy link
Copy Markdown
Owner

Plan step 8 — the head of the critical path. Stacked on #953. Atomic by construction, not by preference.

The blocker this removes

profileRecordSchema was exactly {agentId, apiKey, agentName}. Nothing could tell a daemon which port to bind, so moltzapd required a caller-supplied --port and only the test helper ever passed one — by binding port 0, reading the number, and closing. No production code could compute an MCP URL, which is the reason harnessClientForAccount has zero production callers and both adapters still construct MoltZapService directly.

The shape, landed once

{ agentName, mcpPort, agentId?, apiKey? }   // agentId + apiKey together or absent together

A slot exists before registration and carries no AgentId; Registry commit adds the identity pair. The invariant is a schema filter, and isRegisteredProfile narrows to RegisteredProfileRecord at the three places that need credentials.

loadServiceConfig now distinguishes no slot (ProfileNotFoundError) from slot with no committed identity (ProfileNotRegisteredError) — the branch the registration surface needs later.

moltzapd resolves its own port from the slot it was named with. --port is gone from the published binary, and the packed-tarball check now asserts its absence rather than its presence.

Why there is no green intermediate

STRICT_PARSE_OPTIONS uses onExcessProperty: "error", and mcpPort is required, so the old and new shapes are mutually undecodable. There is no ordering in which both work — a split is impossible by construction. Every writer moves here: register.ts (gains an operator-supplied --mcp-port), config.test-utils.ts, simulator workspace.ts, and the openclaw container fixture.

Pre-launch, so the ~/.moltzap/config.json invalidation is accepted with no shim and no migration.

Port selection stays out of the daemon

The precedent rejects port-zero allocation and bind fallback, so the daemon never selects a port. Where a test or simulator needs one, the harness acts as the operator: it reserves a free port and writes it into the slot before the child starts. reservePort is gone from the daemon spawn path; reserveTestMcpPort and the simulator's reserveSlotMcpPort sit on the writer side, which is a different thing.

The EADDRINUSE test was restructured accordingly — the blocker now binds the slot's port, since that is the only port the daemon will try.

Gates

  • git grep '"--port"' packages/client/ → nothing
  • a slot without mcpPort fails decode rather than defaulting — new test slotWithoutMcpPortFailsDecode, plus halfCommittedSlotFailsDecode (agentId without apiKey) and uncommittedSlotDecodes
  • pnpm typecheck + pnpm nx run-many -t typecheck:tests — 0 errors
  • pnpm lint — 0 warnings, 0 errors (knip, arch:check, arch:config:check)
  • pnpm nx run-many -t test — all 9 projects green
  • generated docs/cli/reference.mdx now advertises --mcp-port

Deliberately not here

createProfileSlot, commitProfileIdentity, and resolveHarnessEndpoint were drafted and removed: knip flagged them as unused exports, and they have no consumer until steps 9 and 12. Landing them now would be the same dead-code-ahead-of-its-consumer problem this stack already has in #945. They come with their callers.

ADR conformance (R-pass)

Governing outcome Record Owner Binds this diff? Verdict Evidence
"Each named local profile has one nonzero stable mcpPort… port-zero allocation and bind fallback are rejected" 20260728-endpoint-daemon-speaks-modern-mcp v2 (present on main) no — but followed CONFORMS The step-0 sweep classified this v2-governing: freeze row G1-DEC-601 requires a profile storing mcpPort and grep mcpPort packages/ returned zero before this PR. Implemented to the same shape and the same rejections regardless.
Slot exists before Registry commit; identity commit is irreversible docs/spec/harness/daemon.md v2 no CONFORMS Slot/identity split mirrors it; production adoption stays main-owned.
main No main-governing record covers production profile persistence. Confirmed against the 5 main-governing records identified in the #926 baseline.

No new divergence introduced. This PR does not by itself admit the production contract — the main-owned ADR bundle (plan step 5) is still outstanding, and this implements the shape the maintainer selected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P76aaa1STr3WPZ3nDascta

A profile record was exactly {agentId, apiKey, agentName}, so nothing
could tell a daemon which port to bind and moltzapd required a
caller-supplied --port. No production code could compute an MCP URL.

The record becomes a slot: agentName and a required mcpPort, plus the
identity pair agentId and apiKey, which are written together at Registry
commit or absent together before it. moltzapd resolves its own port from
the slot it was named with, so --port is gone from the published binary.
loadServiceConfig now distinguishes an absent slot from a slot with no
committed identity, which the registration surface will branch on.

Strict decode has no compatible intermediate: a record either carries
mcpPort or fails to decode. Every writer moves in this commit, and the
simulator and packaged-daemon fixtures choose a port before writing the
slot rather than handing one to the daemon.

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

Copy link
Copy Markdown
Owner Author

Superseded by #974, which collapses this stack into one PR against main. This branch's commits are all contained in it — nothing is lost, and this thread stays readable. Closing to leave one place to review.

@chughtapan chughtapan closed this Aug 6, 2026
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.

1 participant