design phase 3 install - sinrimin/harness-nexus GitHub Wiki
Status: 3.1 β shipped (McpServer.mode + stdio-in-direct + rename), 3.2 β shipped (Profile.target immutable install target), 3.3+ not started. This revision restructures the install pipeline around the ECC adapter-factory pattern and re-prioritizes targets (Hermes β Claude Code β Codex). PRD:
prd-phase-3-install.md. Research:research-phase-3-ecc-install-patterns.md(the adapter pattern evidence base) +research-phase-3-plugin-targets.md(per-target formats).
Three coupled changes:
-
MCP Management β
β
McpServer.mode(proxy|direct); stdio re-enabled, gated todirect. "MCP Connections" β "MCP Management". -
Target-bound profiles β
β
Profile.target(immutable) declares the single Agent tool a profile is shaped for; the creation form narrows offered resources/hook events by it. -
Install pipeline β³ β
hnx installresolves a profile, a target adapter (one per target, ECC-style factory) plans file operations, and an apply step materializes them into the target's native format. Priority order: Hermes β Claude Code β Codex.
What changed in this revision (vs. the prior draft):
- ZCode is out of install scope. No reference material is reproducible; ECC has zero ZCode support.
zcodestays in theAgentTargetenum (non-breaking) but has no adapter βhnx install --target zcodeβ409 TARGET_UNSUPPORTED.- Codex is in. A new first-class target (TOML config, JSON manifest with a unique
interfaceblock, stdio-only MCP).- Architecture follows ECC. The old "TargetWriter interface" + "CC+ZCode share a writer" model is replaced by an adapter factory + plan/apply + install-state ledger. Each target gets its own adapter. See
research-phase-3-ecc-install-patterns.md.- Priority is Hermes-first. Hermes ships before Claude Code (it was previously last/lowest-confidence) because it is the most-needed target, despite needing
hermes_cli/schema verification first.
The dependency boundary is unchanged: core β shared β server β sdk-ts
β cli (adapters + installer) β apps/web.
Implemented. McpServer (core/src/domain/user.ts:56) carries mode: 'proxy' | 'direct'. No proxied field β mode is the only pooling signal; the
registry filters list({ mode: 'proxy' }) directly (registry.ts:111).
| mode | sse |
streamable-http |
stdio |
|---|---|---|---|
proxy |
β | β | β 409 STDIO_REQUIRES_DIRECT
|
direct |
β | β | β |
The registry only dials mode === 'proxy' rows. A direct row is never opened
by Harness Nexus β it is emitted verbatim into a target's plugin at install
time. mode lives on McpServer (migration v2), enforced at the route layer
(STDIO_REQUIRES_DIRECT via requiresDirect()).
Implemented. Profile.target: AgentTarget (core/src/domain/profile.ts:23) is
required at create, immutable after (PATCH target β 409 TARGET_IMMUTABLE).
The zod schemas mirror this (shared/schemas/profile.ts); migration v5 added
the profiles.target column (DEFAULT 'generic').
AgentTarget is currently 'claude-code' | 'zcode' | 'hermes' | 'codex' | 'generic', defined in two places kept in sync manually (core/domain/ resource.ts + shared/schemas/profile.ts) β shared deliberately does not
depend on core. Priority order: hermes β claude-code β codex. zcode is
retained but unsupported (no adapter); generic is a placeholder.
Already built β do not recreate. Phase 4.5 landed the matrix in
packages/shared/src/hooks.ts.HOOK_EVENTS,HOOK_SUPPORT(keyed byAgentTarget), andDECLARATIVE_HOOK_TARGETSare the single source of truth for the hook-event side.hermesandcodexare bothnull(structurally different hook models β Python plugins / TOML config respectively), so a declarativehooks.jsonresource cannot target them.
A per-artifact compatibility matrix β (resourceKind, fromTarget, toTarget) β portable | convertible | unsupported(reason) β is net-new and lands with the
cross-target import phase (3.7). It will live in a new
packages/shared/src/target-compat.ts.
This part adopts the ECC pattern wholesale. Read
research-phase-3-ecc-install-patterns.md for the evidence base.
hnx install --profile <id> --server <url> --token <pat> [--target <t>] [--apply] [--out <dir>]
β
βΌ
ProfileResolver ββ (SDK fetch from server) βββΆ ResolvedProfile
β (Profile + fetched artifacts:
β skill bodies, hook configs,
β McpServer rows for MCP entries)
βΌ
TargetAdapter (registry lookup by target) βββΆ planOperations()
β returns InstallPlan β NO writes
βββ HermesAdapter (plugin.yaml YAML + config.yaml mcp_servers YAML)
βββ ClaudeCodeAdapter (.claude-plugin/plugin.json + .mcp.json JSON)
βββ CodexAdapter (.codex-plugin/plugin.json + config.toml TOML)
β
βΌ
[ --apply ] Installer.materialize(plan) βββΆ write files + install-state ledger
(default: dry-run, print plan) βββΆ harness-nexus-install-state.json
The key ECC lesson: plan and apply are separate. The default hnx install
run is a dry-run that prints what would happen; --apply materializes. The
plan is the unit the user reviews before any file is touched.
Each adapter is produced by a factory from a small config (mirrors ECC's
createInstallTargetAdapter). A minimal adapter is a ~10-line config; adapters
needing format transformation override planOperations.
// packages/cli/src/install/types.ts
export interface Operation {
kind: 'copy-file' | 'merge-json' | 'merge-toml' | 'merge-yaml';
sourcePath: string; // resolved profile artifact
destinationPath: string; // target-native path
mergePayload?: unknown; // for merge-* operations
}
export interface InstallPlan {
adapter: { id: string; target: AgentTarget; kind: 'home' | 'project' };
targetRoot: string; // where files land (e.g. ~/.hermes, ~/.codex)
installStatePath: string; // ledger location
operations: Operation[];
sensitive: boolean; // true if any op carries decrypted direct-mode creds
}
export interface TargetAdapter {
readonly target: AgentTarget;
readonly nativeRoot: string; // '.hermes' | '.codex' | '.claude-plugin'
resolveRoot(input: ResolveInput): string;
planOperations(resolved: ResolvedProfile, input: ResolveInput): InstallPlan;
validate(input: ResolveInput): ValidationIssue[];
}
export function createTargetAdapter(config: AdapterConfig): TargetAdapter {
/* β¦ */
}// packages/cli/src/install/adapters/hermes.ts β minimal shape
export const hermesAdapter = createTargetAdapter({
target: 'hermes',
kind: 'home',
rootSegments: ['.hermes'],
nativeRoot: '.hermes',
// planOperations overridden to emit YAML plugin.yaml + config.yaml
});A profile's resources are target-agnostic in storage. The adapter filters out
anything that does not belong to its target at plan time (ECC's
isForeignPlatformPath pattern). One resolved profile feeds N targets without
per-target duplication.
Confirmed decision: proxy is the default; direct is the escape hatch for
offline / no-server installs. For each kind === 'mcp' entry, branch on the
referenced McpServer.mode:
-
proxy entries collapse into one aggregated endpoint
/mcp?profile=<id>with${user_config.PAT}(the product thesis). No upstream secret is shipped. Per-format emission:- Hermes: YAML
mcp_servers:block inconfig.yaml - Claude Code: JSON
mcpServersin.mcp.json - Codex: TOML
[mcp_servers.*]inconfig.tomlβ stdio-only, refusesurl
- Hermes: YAML
-
direct entries are written verbatim (transport included: stdio command /
HTTP url).
${cred:NAME}placeholders in transport string fields are resolved to decrypted plaintext at emit time (inlined into url/command/args/env/headers) and the output is flaggedsensitive.
Every --apply writes harness-nexus-install-state.json in the target root,
recording the adapter, the request, the resolved plan, and every operation.
This is the surface that enables future doctor (drift detection), repair
(restore managed files), and uninstall (remove only managed files). The ledger
is written from day one (Phase 3.3) so the surface exists even before those
subcommands ship.
| Hermes (1st) | Claude Code (2nd) | Codex (3rd) | |
|---|---|---|---|
| Manifest |
plugin.yaml YAML
|
.claude-plugin/plugin.json JSON
|
.codex-plugin/plugin.json JSON (+interface block) |
| Config |
config.yaml YAML
|
.mcp.json JSON
|
config.toml TOML
|
| MCP block |
mcp_servers: YAML |
mcpServers JSON |
[mcp_servers.*] TOML, stdio-only
|
| Memory/rules |
AGENTS.md auto-inject |
wrap-as-skill (no auto-load) |
AGENTS.md auto-inject |
| Hooks |
null (Python model) |
~30 declarative events | config/prompts-based |
| Confidence | Medium (verify hermes_cli/) |
High | High (via ECC) |
The format divergence (YAML / JSON / TOML) is exactly why each target gets its own adapter rather than a shared "JSON-family" writer.
hnx install --profile <id> --server <url> --token <pat> [--target <t>] [--apply] [--out <dir>]-
--profileresolves via the SDK against--server(a profile is a reference bundle β its entries point at server-side resources, and proxy-mode MCP needs the/mcpendpoint, so the server is required). No local-manifest path. -
--targetoverrides the profile's owntargetonly forgenericprofiles; for a target-bound profile it must match (else409 TARGET_MISMATCH).zcodeβ409 TARGET_UNSUPPORTED(no adapter). - Without
--apply: dry-run, prints the plan. With--apply: writes files + ledger. -
genericinstall semantics are deferred (decided when 3.3 starts). - Output root defaults to the target's native home (
~/.hermes,~/.codex, β¦) forkind: 'home'adapters;--outoverrides.
Take: adapter factory + frozen adapter + overridable planOperations;
foreign-platform path filtering; plan/apply separation; install-state ledger;
format-aware config emitters; per-target independent adapters.
Leave (ECC-specific): ECC's three-layer declaration (modules/profiles/ components β we use Profile + Resource); ECC's content; ECC's Codex bash sidecar β we unify Codex into one adapter with a TOML emitter (ECC splits it across a Node adapter + bash script, which is the one rough edge to improve on).
Unchanged from prior design (lands in 3.7). Two-step endpoint:
POST /api/profiles/:id/import-from { sourceProfileId } β 200 CompatibilityReport
POST /api/profiles/:id/import-apply { sourceProfileId, selectedEntryIds[] } β 200 Profile
CompatibilityReport carries per-entry portable | convertible | unsupported
driven by the per-artifact compatibility matrix (3.7's net-new
target-compat.ts). zcode as a destination is universally unsupported
(no adapter).
All under /api, same { error, message } shape. Unchanged from 3.1/3.2 except
the install path is CLI-only (no server endpoint for install β the CLI resolves
locally or via SDK fetch):
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/mcp-servers |
requireAuthβ |
accepts mode; stdio requires direct
|
| PATCH | /api/mcp-servers/:id |
requireAuthβ‘ |
mode change allowed if transport permits |
| POST | /api/profiles |
requireAuthβ |
requires target
|
| PATCH | /api/profiles/:id |
requireAuthβ‘ |
omits target; carrying it β 409 TARGET_IMMUTABLE
|
| POST | /api/profiles/:id/import-from |
requireAuthβ‘ |
owner-or-admin on both profiles |
| POST | /api/profiles/:id/import-apply |
requireAuthβ‘ |
owner-or-admin on both profiles |
β admin required iff scope === 'global'. β‘ ownership check on both profiles.
New error codes: STDIO_REQUIRES_DIRECT (409), TARGET_IMMUTABLE (409),
TARGET_MISMATCH (409), TARGET_UNSUPPORTED (409), PROFILE_SOURCE_NOT_ACCESSIBLE (404).
Follows the Signal design system. Status: the MCP Management rename + mode badge (3.1) and the Profile target picker + target column badge (3.2) are shipped. Remaining UI work tracks the install/import phases:
- Install UX (3.3+): a "Install" action on a profile that triggers the CLI (or an in-browser plan preview) β design deferred until the CLI shape is real.
- Import flow (3.7): "Import from profileβ¦" β picker β compatibility report (green = portable, amber = convertible, red = unsupported) β confirm.
- Sub-agents are no longer "hidden for zcode" (zcode is unsupported wholesale);
the target picker offers
hermes,claude-code,codex,generic.
- MCP mode and profiles inherit the existing scope model. No new scope concepts.
-
Direct-mode installs are sensitive. If any emitted file carries a decrypted
credential (a direct MCP entry whose
${cred:NAME}resolved to plaintext), the CLI prints a warning and the output dir gets restrictive permissions (0700). Proxy-only installs carry only a${user_config.PAT}placeholder and are not sensitive. -
The PAT in a proxy install is
${user_config.PAT}, not a literal β the tool prompts at enable time (CC β keychain). The CLI never writes a PAT to disk unless explicitly told to. - Import never silently drops. Every unsupported entry appears in the report with a reason.
- ECC + Superpower import adapters (3.8+ β distinct from the install adapters).
- Hosted per-profile marketplace / registry.
-
doctor/repair/uninstallsubcommands (the ledger is written from 3.3 to enable them later). - Per-PAT profile binding and tool-level authorization (still from 2.2).
- The stdio bridge entry (Phase 6) β direct mode here is the tool spawning stdio, not Harness Nexus.
-
3.1 β
β
modeon McpServer + stdio-in-direct + rename + migration v2. -
3.2 β
β
Profile.target(immutable) + migration v5 + TARGET_IMMUTABLE. -
3.3 β Install pipeline skeleton: adapter factory + registry + plan/apply
- install-state ledger +
hnx installCLI (dry-run default). Adapter interface - a stub adapter, but no real target adapter yet.
- install-state ledger +
-
3.4 β Hermes adapter (priority 1; verify
hermes_cli/schema first). YAMLplugin.yaml+config.yamlmcp_servers:; proxy/direct MCP emission. -
3.5 β Marketplace emitter (priority 2; re-planned). Serve the user's
profiles as a Claude Code plugin marketplace over plain HTTP(S) β
claude plugin marketplace add <url>+ native install/update/uninstall. Seephase-3.5-marketplace-emitter.md. The local-write Claude Code adapter (JSON.claude-plugin/plugin.json-
.mcp.json; rules-as-skill wrapping) is demoted to a fallback for old CLIs (<2.1.224, noarchivesource) / airgapped hosts β deferred behind the emitter.
-
-
3.6 β Codex adapter (priority 3). JSON
.codex-plugin/plugin.json(+interface)- TOML
config.toml; stdio-only MCP;AGENTS.mdmarker-merge.
- TOML
- 3.7 β Cross-target import (report + apply) + per-artifact compat matrix + UI.
- 3.8 β Other well-known agents (openclaw / kimi / qwen / etc., optional) + ECC/Superpower import adapters.
zcode is explicitly unsupported across all install phases (no adapter). It can
be revived if a reference surface re-emerges.