design phase 3.5 marketplace emitter - sinrimin/harness-nexus GitHub Wiki
Status: design (2026-09-04). Grounded in the empirical spike (
research-phase-3.5-marketplace-emitter-spike.md) — every client-side behavior cited here was verified against a liveclaude plugin add/install/update/uninstallcycle.This re-plans the old "3.5 Claude Code adapter". Writing into
~/.claudeourselves is now the fallback (old CLI < 2.1.224, or airgapped hosts); the preferred distribution path for claude-code targets is native marketplace consumption. The adapter pipeline (3.3/3.4) is untouched and remains the path for Hermes and future non-marketplace targets.
A read-only emitter on the Harness Nexus server that turns a user's visible profiles into a Claude Code plugin marketplace:
claude plugin marketplace add https://<host>/api/marketplace/<token>/marketplace.json
claude plugin install <profile-name>@harness-nexus-<username>
Install, update (claude plugin update re-pulls the zip), and uninstall are
all owned by Claude Code — no install-state ledger on this path, no
client-side writes. Harness Nexus only emits a catalog and per-profile zip
archives.
-
Token in URL = the user's PAT (
hnpat_…). No new entity, no new storage, no new UI. Trade-off: the add-URL is exactly as sensitive as a CLI PAT (it is one) and rides in claude'ssettings.json; rotation = new PAT + re-add. A dedicated, revocable emit token is future work (listed in roadmap) — the swap is contained in one resolver function. -
One marketplace per user, name
harness-nexus-<username>(sanitized to claude's[a-z0-9-]expectations; unique per user — the spike showed claude rejects re-adding a marketplace name from a different URL, so per-user uniqueness prevents collisions on shared machines). -
One plugin per profile. Plugin name = sanitized profile name;
version =
profile.version(editors bump it;claude plugin updatepicks up the change). -
Catalog routes are PAT-in-path, headerless (claude sends no headers we
can rely on — spike §4). Unknown/revoked token →
404(no existence leak; matches our API 404 posture). These routes do not userequireAuth— there is noAuthorizationheader; they resolve the token themselves and setreq.user. -
MCP emission mirrors the Hermes adapter's split: proxy-mode entries
collapse into the single aggregated
/mcp?profile=<id>endpoint with a${HN_PAT}env placeholder (.mcp.jsonsupports env expansion); direct entries are emitted verbatim with${cred:NAME}placeholders left unresolved (CLI-side decryption needs a server API we don't expose — same limitation as 3.4, warned in the plugin description). -
Rules become skills. Claude Code plugins have no always-on "rules"
concept; a rule resource is emitted as
skills/<key>/SKILL.mdwith a description prefixRules:. Documented limitation, not silent. -
Zip assembly via
jszip(pure JS) in the server package. Archives are built on the fly — no cache, no persistence.
Two GET routes under packages/server/src/modules/marketplace.ts, both
bounded by a marketplaceTokens style preHandler that resolves the PAT:
| Route | Returns |
|---|---|
GET /api/marketplace/:token/marketplace.json |
Catalog: every profile visible to the token's user; each entry's source = { source: 'archive', url: <PUBLIC_BASE_URL>/api/marketplace/<token>/archives/<profileId>.zip }
|
GET /api/marketplace/:token/archives/:profileId.zip |
The profile assembled as a claude-code plugin zip |
Visibility = the same scope filter as the profile list API (global +
own personal; admin sees all). A profile the user can't see → 404.
Config addition (packages/server/src/config.ts): PUBLIC_BASE_URL
(default http://localhost:8080) — must be the public HTTPS origin in
production, because claude enforces https:// + non-loopback on archive
URLs (spike §2). Our Caddy :15921 front door satisfies this.
<profile-name>/
.claude-plugin/plugin.json # { name, version: profile.version, description }
.mcp.json # only when the profile has MCP entries
skills/<key>/SKILL.md # skill resources (inline)
skills/<key>/… # inline-bundle files (references/, scripts/)
skills/<key>/SKILL.md # rule resources (wrapped, see decision 6)
commands/<key>.md # command resources
agents/<key>.md # sub_agent resources
hooks/hooks.json # hook resources (claude-code-supported events only)
- The zip carries one top-level folder (spike §3 layout, "one folder deep").
- Hook emission filters by the existing
HOOK_SUPPORTmatrix (packages/shared/src/hooks.ts): events claude-code doesn't support are dropped with a note in the plugin description (same discipline as the Hermes adapter's skip-with-warning). - The MCP block in
.mcp.json:
{
"mcpServers": {
"harness-nexus": {
"type": "http",
"url": "<PUBLIC_BASE_URL>/mcp?profile=<profileId>",
"headers": { "Authorization": "Bearer ${HN_PAT}" }
},
"<direct-server-name>": { "…verbatim…" }
}
}The install hint (plugin description suffix) tells the user to export
HN_PAT with a profile-scoped PAT before launching claude.
-
packages/server/src/marketplace/emitter.ts— catalog builder + zip builder. Depends onUnitOfWork(profiles + resources + mcp-servers), not on route handlers (mirrors howmcp/registry.tsstays transport-decoupled). - No
corechanges. Nosharedschema changes (the marketplace.json shape we emit is a subset ofmarketplaceCatalogSchemaminus thearchivesource variant — extendmarketplaceSourceSchemawith thearchivekind so emit and parse share one schema). - Web UI: deferred to a follow-up (the add-command string is trivially shown later on the profile list; no screen exists for "marketplace" today).
- Unit: catalog + zip builders against the in-memory driver fixture.
-
scripts/smoke.mjs: PAT → fetchmarketplace.json(assert name/plugins shape + archive URLs) → fetch one archive (assert zip magic + expected member paths). - Manual E2E (documented for the future, gated on a non-loopback HTTPS
origin):
claude plugin marketplace add …against the deployed server.
- Dedicated/rotatable emit tokens (PAT reuse only; swap point is one function).
- Per-profile marketplaces, marketplace metadata editing, categories/tags beyond what profiles already carry.
- Local-write claude-code adapter (old-CLI fallback) — remains deferred, re-numbered behind the emitter.
- ZCode marketplace consumption (its catalog format is the same shape per our 7.2 schema, but ZCode stays out of install scope).
- Access-log redaction of the token (noted: the full URL appears in Fastify request logs; acceptable for now, flagged in the security section).