Skip to content

fix(factory): make the relay workspace identity configurable - #311

Merged
khaliqgant merged 3 commits into
mainfrom
fix/relay-agent-name-config
Aug 21, 2026
Merged

fix(factory): make the relay workspace identity configurable#311
khaliqgant merged 3 commits into
mainfrom
fix/relay-agent-name-config

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 21, 2026

Copy link
Copy Markdown
Member

Problem

The relay fleet backend registers a workspace agent for the factory itself — not one it spawns. RelayFleetClient resolves that identity as options.agentName ?? DEFAULT_AGENT_NAME ('factory'), but createFleet constructed it with only { workspaceKey, env, log }. Nothing ever passed agentName, and no config key existed to supply one, so every relay deployment necessarily registered the same identity.

A host that cannot prove it still owns that name after a restart re-registers and collides with its own prior registration:

Agent "factory" already exists in this workspace

The fleet control plane then never initialises, the breaker opens, no agent is placed, and nothing is dispatched.

Change

Config plumbing only — three pieces:

  1. relay.agentName in the config schema (src/config/schema.ts). Optional, and deliberately not .default(...): RelayFleetClient keeps owning the 'factory' fallback, so the default lives in exactly one place and cannot drift. DEFAULT_AGENT_NAME is untouched. Declared on both config halves, mirroring preview: the workspace half carries a shared default, the node half overrides it per host. An identity that could only live on the shared half would be handed to every deployment in the workspace — the collision this exists to prevent.
  2. Threaded through createFleet (src/fleet/create-fleet.ts, src/cli/fleet.ts) as relayAgentName into the RelayFleetClient constructor. This is the one-line gap.
  3. Rejected at config load, never coerced. .trim().min(1) means an empty or whitespace-only value fails validation instead of silently becoming factory. A silently-defaulted identity is precisely how an unconfigurable identity stayed invisible. The relay object is .strict(), so agentname: is a load error rather than an ignored typo.

RelayFleetClient gains a read-only agentName accessor so a caller can confirm which identity a configuration actually resolved to. Registration logic is unchanged — it still reads the private #agentName exactly as before.

Not in scope: recovery-credential enrolment so an ephemeral host self-recovers its identity across restarts. That is a separate, larger change.

Compatibility

A config that omits relay.agentName produces undefined, which RelayFleetClient resolves to factory — byte-for-byte the identity every existing deployment registers today. No deployment changes identity on upgrade.

Tests — the required pair, each stated fail-before / pass-after

Verified by running the new tests against unmodified 2e52791 in a separate worktree, then against this branch.

MUST-FIRE — an explicit name reaches the client.

  • src/cli/fleet.test.tsforwards a configured relay agent name to fleet construction — the sharpest one: the CLI loads a config pinning relay.agentName: 'factory-cloud' and asserts fleet construction receives it.
    • Before: fails — expected undefined to be 'factory-cloud'. That is the outage in one assertion: the operator pinned a name and construction still got nothing.
    • After: passes.
  • src/fleet/create-fleet.test.tsregisters under the agent name the config supplies — parses a real config and asserts RelayFleetClient.agentName === 'factory-cloud'.
    • Before: fails — TypeError: Cannot read properties of undefined (reading 'agentName'), because config.relay does not exist.
    • After: passes.

MUST-NOT-FIRE — omitting it still yields factory.

  • src/fleet/create-fleet.test.tskeeps the built-in factory identity when the config omits an agent name, and src/cli/fleet.test.tsleaves the relay agent name unset when the config omits it.
    • Before: the create-fleet case fails with the same TypeError (the field does not exist yet); the CLI case passes trivially, since undefined is what the old code already forwarded.
    • After: both pass.
    • Its real job is forward-looking: it fails if anyone later gives the schema its own default, makes the key required, or changes the fallback. A surprise identity change would strand a live deployment exactly the way an unconfigurable one did.

Validationsrc/config/schema.test.tsrejects empty / whitespace-only / tab relay.agentName at config load instead of defaulting it, asserted by issue path and code (relay.agentName, too_small) rather than message text.

  • Before: fails with expected true to be false — base silently accepts agentName: '', which is the exact silent-default failure mode.
  • After: passes.

Split configs (added after review)

Three further tests in src/config/schema.test.ts cover the case where a cloud deployment and another deployment share one workspace: the node half pins the identity and is carried back on nodeConfig; the node half overrides a workspace-half default; the workspace half alone still applies.

Worth recording precisely, since the review's stated failure mode was not reachable: combineSplitConfigInput merges the two raw halves before FactoryConfigSchema.parse, so a node-half relay.agentName already reached the runtime config and already won. Two of the three tests therefore passed before the follow-up commit. The genuine gap was that NodeConfigSchema stripped the key, so the node-half view dropped it and a per-host identity was reflected back as workspace-shared config — that one assertion fails before (TypeError on loaded.nodeConfig.relay) and passes after.

Split-config validation (added after review)

combineSplitConfigInput merges the two halves' relay objects, which meant a workspace-half agentName was discarded unvalidated whenever the node half overrode it — a broken shared config would load clean on every host that set its own identity. Each half is now validated before the merge, mirroring validateClonePathSyntax directly above it, and the error names the offending half.

Verified against the prior head: the workspace-half case did not throw at all there; the node-half case already threw via the final parse and only gains a clearer message.

A further test pins what the workspace-shaped view reports after a node override. normalizeLoadedConfig projects both views from the merged config, so that view carries the effective identity — as it already does for preview, cloneRoot, and clonePaths. Nothing serializes workspaceConfig back to a shared file today, so this documents existing semantics rather than changing them.

Verification

  • npm run build — exit 0.
  • npm run featuremap:check — exit 0.
  • npm test (full suite, final head) — 1955 passed, 1 failed, a pre-existing flake. CI's package job hit the same one on the first run and passed on a re-run of the identical SHA.
  • Flakes seen and how each was ruled out (neither test edited):
    • fleet CLI runtime > keeps relay dispatch ownership…expected 3 to be +0. Ran src/cli/fleet.test.ts 3× on this branch (2 fail) and 3× on pristine 2e52791 (1 fail). Structurally it cannot be this change: that test injects deps.fleet, so buildFleet returns before it ever reads loaded.config.relay.
    • GitAgentWorktreeManager > discovers every run…Test timed out in 5000ms; passes when its file is run alone. Load-only, unrelated to this change.

All exit codes captured from $?, not from log text.

The relay fleet backend registers a workspace agent for the factory
itself, defaulting to `factory`. `createFleet` never passed an
`agentName`, and no config key existed to supply one, so every relay
deployment necessarily registered the same identity. A host that cannot
prove it still owns that name after a restart re-registers, collides
with its own prior registration (`Agent "factory" already exists in this
workspace`), and the fleet control plane never initialises -- no agent is
placed and nothing is dispatched.

Add `relay.agentName` to the config schema and thread it through
`createFleet` into `RelayFleetClient`. The key is optional and is
deliberately not defaulted in the schema: `RelayFleetClient` keeps
owning the `factory` fallback, so a config that omits it resolves to
exactly the identity it resolves to today. An empty or whitespace-only
value is rejected at config load rather than silently coerced to the
default -- a silently-defaulted identity is how this stayed invisible.

`RelayFleetClient` gains a read-only `agentName` accessor so a caller can
confirm which identity a configuration resolved to. Registration is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Session-Id: f425b9b8-04fe-42c6-8e24-808c8664dd8c
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b3c6a5b4-b074-42a3-b56b-7e10fd8665b1

📥 Commits

Reviewing files that changed from the base of the PR and between 2e52791 and 4330801.

📒 Files selected for processing (7)
  • src/cli/fleet.test.ts
  • src/cli/fleet.ts
  • src/config/schema.test.ts
  • src/config/schema.ts
  • src/fleet/create-fleet.test.ts
  • src/fleet/create-fleet.ts
  • src/fleet/relay-fleet-client.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The factory schema now supports an optional trimmed relay agent name. Fleet construction forwards this value through CLI and createFleet paths to RelayFleetClient. Tests cover validation, defaults, forwarding, and resolved identity.

Changes

Relay agent identity

Layer / File(s) Summary
Relay configuration contract
src/config/schema.ts, src/config/schema.test.ts
The schema accepts an optional non-empty relay.agentName, trims its value, rejects unknown keys, and defaults omitted relay settings to an empty object.
Relay identity fleet wiring
src/fleet/create-fleet.ts, src/fleet/relay-fleet-client.ts, src/fleet/create-fleet.test.ts
createFleet accepts relayAgentName and passes it to RelayFleetClient. The client exposes the resolved read-only agentName. Tests cover configured and default identities.
CLI relay forwarding
src/cli/fleet.ts, src/cli/fleet.test.ts
Injected and standard relay fleet construction forward config.relay.agentName. Dry-run tests cover configured and omitted values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 43308

This change makes the relay workspace identity configurable, rejects invalid values, and preserves the existing factory fallback when unset; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant FactoryConfigSchema
  participant buildFleet
  participant createFleet
  participant RelayFleetClient
  FactoryConfigSchema->>buildFleet: parse relay.agentName
  buildFleet->>createFleet: pass relayAgentName
  createFleet->>RelayFleetClient: construct with agentName
  RelayFleetClient-->>createFleet: expose resolved agentName
Loading

Suggested reviewers: miyaontherelay, kjgbot

Poem

A rabbit hops through config bright,
Trims relay names to fit just right.
Fleet paths carry the name along,
The client remembers where it belongs.
Tests cheer softly: identity strong!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: configurable relay workspace identity.
Description check ✅ Passed The description directly explains the relay identity problem, implementation, compatibility behavior, validation, tests, and verification results.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/relay-agent-name-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43308018a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/config/schema.ts
liveSubscription: liveSubscriptionSchema,
dispatch: dispatchSchema,
fleetHealth: fleetHealthSchema,
relay: relaySchema,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the relay identity in node-local config

In split-config/cloud deployments, WorkspaceConfig is the durable configuration shared by the workspace, while NodeConfig is the per-host half. Defining relay.agentName only on WorkspaceConfigObjectSchema therefore gives every Factory deployment in that workspace the same identity, and NodeConfigSchema strips the key if a deployment tries to set its own value there. With two deployments, the second still encounters the registration collision described above and its fleet control plane cannot initialize; this identity needs to be accepted as node/deployment-local configuration and merged from that half.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9373e35 — but the diagnosis needed one correction, so recording what I verified.

Half right. I probed loadFactoryConfig directly rather than reasoning from the schema. combineSplitConfigInput merges the two raw halves before FactoryConfigSchema.parse runs, so a nodeConfig half setting relay.agentName already reached factoryConfig.relay.agentName and already took precedence over the workspace half. The stated consequence — "the second deployment still encounters the registration collision" — was therefore not reachable: a per-deployment identity was settable from the node half as written.

The real gap, which is worth fixing. NodeConfigSchema does strip the key, so the node-half view returned by loadFactoryConfig carried no relay, while workspaceConfig did. A per-host identity was reflected back as workspace-shared configuration, and anything round-tripping the split halves would migrate one deployment's identity onto every other deployment in the workspace — the collision this setting exists to prevent, arriving by a different route.

So relay is now declared on both halves, mirroring how preview already is: workspace half = shared default, node half = per-host override. The two relay objects are merged rather than replaced, so a node half pinning only agentName cannot drop other workspace-half settings.

Covered by three new tests in src/config/schema.test.ts. Only one of them fails against the previous commit (TypeError on loaded.nodeConfig.relay) — the other two passed already, which is precisely the evidence that the runtime path was not broken. Full suite green apart from one pre-existing flake reproduced on unmodified 2e52791.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Re-trigger cubic

Review flagged that `relay.agentName` lived only on the workspace half.
The runtime config was already correct -- `combineSplitConfigInput`
merges the raw halves before parsing, so a node-half value did reach
`factoryConfig` and did take precedence -- but the node-half *view*
dropped the key, so a per-host identity was reflected back as
workspace-shared configuration. Anything round-tripping the split halves
would migrate one deployment's identity onto every other deployment in
the workspace: exactly the collision this setting exists to prevent.

Declare `relay` on `NodeConfigObjectSchema` and carry it in the node-half
projection, mirroring how `preview` is declared on both halves. The two
halves' `relay` objects are now merged rather than replaced, so a node
half pinning only `agentName` cannot drop other workspace-half settings.

Precedence is unchanged and now covered: node half wins, workspace half
supplies a shared default, and omitting both still resolves to `factory`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Session-Id: f425b9b8-04fe-42c6-8e24-808c8664dd8c

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/config/schema.ts Outdated
Comment thread src/config/schema.test.ts
Review caught a real hole in the split-config merge. `{...workspaceRelay,
...nodeRelay}` discards a workspace-half `agentName` whenever the node
half overrides it, so an invalid shared value never reached `relaySchema`
and a broken workspace config loaded clean on every host that happened to
set its own identity. That is the silent acceptance this key exists to
prevent, arriving one level up from the value itself.

Validate each half before the merge, mirroring `validateClonePathSyntax`,
which already validates both halves before node-local values take
precedence. The node half was in fact already rejected by the final parse;
it now fails with a message naming the offending half instead of a raw
issue list.

Also pins what the workspace-shaped view reports after a node override.
`normalizeLoadedConfig` projects both views from the *merged* config, so
that view carries the effective identity -- as it already does for
preview, cloneRoot, and clonePaths. Nothing serializes `workspaceConfig`
back to a shared file today, so this documents existing semantics rather
than changing them, and fails loudly if the projection moves under a
caller who starts to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Session-Id: f425b9b8-04fe-42c6-8e24-808c8664dd8c
@khaliqgant
khaliqgant merged commit e9453f2 into main Aug 21, 2026
8 checks passed
@khaliqgant
khaliqgant deleted the fix/relay-agent-name-config branch August 21, 2026 15:15
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