design cc marketplace deploy - sinrimin/harness-nexus GitHub Wiki
Issue #6 Β· shipped 2026-09-22 Β· daemon 0.25.0-i6
Since Phase 3.5, claude-code profiles install through the CC-native plugin
marketplace emitter β the ONLY path was manual: create a marketplace-scoped
PAT on the Tokens page, then run claude plugin marketplace add β¦ +
claude plugin install <profile>@harness-nexus-<user> on the machine. The
machine page's Deploy tab filtered profiles to DEPLOYABLE_TARGETS = [hermes, codex, deepseek, pi], so a claude-code profile (e.g. cc-profile)
showed no deployable entry at all. This issue turns those two commands into a
deploy method: one click from the Deploy tab, install-or-update, riding the
unchanged C4 job pipeline (queue / replay / progress / result).
-
claude plugin marketplace add <url>is an idempotent no-op when the marketplace name exists ("already on disk β declared in user settings") β but it does not refresh a changed source URL. After a token rotation the stale URL wedgesmarketplace updateuntil the marketplace is removed and re-added. -
claude plugin marketplace update <name>refreshes the catalog (rc=0). -
claude plugin install <p>@<mp> -yis a no-op success when already installed; it does NOT pull a newer version.-yis required for non-TTY β the daemon always passes it. -
claude plugin update <p>@<mp> -ypulls the new version (verified 1.0.0 β 1.0.1); already-latest is rc=0. - CC's state is readable JSON, so no CLI-text parsing anywhere:
-
~/.claude/plugins/known_marketplaces.jsonβ{name: {source: {source:'url', url}, installLocation, lastUpdated}} -
~/.claude/plugins/installed_plugins.jsonβ{version: 2, plugins: {"p@mp": [{scope, installPath, version, β¦}]}}
-
-
CC decides updates by version number. The emitter writes
profile.versionintoplugin.jsonand the marketplace entry, so content-only changes with a frozen version are invisible (#18 made this moot: versions are auto-numbered β decimal, carry at 16 β and bump exactly when the profile's entries change; the web no longer offers a manual version field) ("already at the latest version"). A version bump IS the publish act.
POST /api/machines/:id/jobs {profileId} no longer 409s for
target === 'claude-code'. The job stays type: 'deploy' but its payload
carries a marketplace arm (shared marketplaceDeployArmSchema):
The daemon's runDeploy parses the payload with deployJobPayloadSchema and
branches on the arm to the new executor (cli/src/daemon/cc-marketplace.ts);
everything else keeps the 3.3 adapter pipeline untouched.
Gates (route, modules/jobs.ts):
- Soft capability gate: an ONLINE daemon without
marketplace-deployβ409 DAEMON_NO_MARKETPLACE_DEPLOY(offline may queue, the deploy/harness rule). Daemon0.25.0-i6advertises the capability. - Owner-marketplace visibility: the daemon reads the catalog as the MACHINE
OWNER (see tokens below), so the profile must be global or owned by the
machine owner β otherwise
409 PROFILE_NOT_IN_OWNER_MARKETPLACE(an admin deploying their own personal profile onto a user's machine would emit a catalog that never contains it).
Result (deployResultDataSchema, additive): method: 'marketplace' +
installedVersion (what CC itself recorded in installed_plugins.json);
directory is ~/.claude/plugins. No AgentInstance is registered β
JobService.onResult skips registerInstance when
method === 'marketplace'. Chat keys off the runtime-detected instance
(9 W1); a plugin install is not a runtime install, and a deploy row would
falsely suggest the platform owns that runtime's config.
The URL the daemon adds is
${baseUrl}/api/marketplace/<MACHINE PAT>/marketplace.json β the daemon uses
the token it already holds in ~/.hnx/config.json; no secret rides the job
payload or the job row. resolveTokenUser (modules/marketplace.ts) now
accepts both marketplace-scoped PATs (the manual flow) and machine-ctl
PATs (resolve to the machine's owner). Security posture:
- The REST auth hook still REJECTS machine tokens β blast radius stays the two emitter GET routes.
- A machine PAT already grants full
/ctlcontrol of the owner's machine; reading the owner's marketplace is a strictly weaker capability. - CC persists the URL inside
~/.claudeβ the token already lives on that same machine in~/.hnx/config.json(0600); equivalent exposure.
The archive URLs inside the catalog carry the same token (the emitter embeds
it), so plugin install/update fetches authenticate identically.
Reuses the harness-job command pattern: inherits the daemon env
(NODE_EXTRA_CA_CERTS and proxy vars must reach the claude CLI), 10-min
per-command timeout with SIGTERMβSIGKILL, throttled stdout/stderr tail into
job:progress. Sequence:
- Read
known_marketplaces.json; same name but different URL (token rotation,PUBLIC_BASE_URLchange) βclaude plugin marketplace removefirst (rig-verified live: the pre-#6 manual marketplace used the user's marketplace PAT; the first one-click deploy removed and re-added under the machine PAT). -
claude plugin marketplace add <url>(idempotent). -
claude plugin marketplace update <name>. - Read
installed_plugins.json; installed βclaude plugin update <p>@<mp> -y, elseclaude plugin install <p>@<mp> -y. - Re-read
installed_plugins.jsonβinstalledVersionin the result.
One-click update = the same deploy again. Bump the profile version first (see below) β that is the only thing that makes CC see a new build.
The blocker: version was hardcoded '1.0.0' at create and absent from
updateProfileSchema β a marketplace update was IMPOSSIBLE to publish. Now
PATCH /api/profiles/:id accepts version (min(1).max(64)), and the web
Profiles page gained an Edit dialog (name / description / version + the
entry checkboxes β MCP servers and per-kind resources, preselected from the
stored rows; target stays immutable). Saving rebuilds entries from the
checkbox state and carries pinnedVersion/installOptions over for
still-selected ids; entries whose target no longer appears in the caller's
lists drop out β saving self-heals dead references instead of 409-ing
forever. The create dialog shares the same picker component
(EntryPickers + useEntryLists + buildEntries in Profiles.tsx).
Row menu β Edit; the row itself shows the current v<version> badge.
- Deploy tab: the profile picker now includes claude-code profiles, labeled
claude-code Β· marketplacevs the bare target for adapter deploys. The job table's Detail cell rendersmarketplace Β· <plugin> v<version>for succeeded marketplace deploys (jobDetailText). - Strings:
machineDetail.viaMarketplace(enmarketplace/ zhεΈεΊ), updateddeployDesc/noDeployable;profiles.editTitle/editDesc/versionLabel/versionHint/updated(en/zh).
CC enforces https:// + non-loopback origins on archive sources
(CLI β₯ 2.1.224): PUBLIC_BASE_URL must be the public HTTPS origin, and the
machine must trust its CA (NODE_EXTRA_CA_CERTS β inherited by the executor
from the daemon env). The rig already runs this shape (Caddy on :15921).
claude must be on the daemon's PATH.
- One-click deploy of
cc-profile(2 entries: 1 MCP + 1 skill): job succeeded,installedVersion 1.0.0,claude plugin listshowscc-profile@harness-nexus-ashen β enabled; the stale-URL remove+add path fired for real (user-PAT URL β machine-PAT URL). - One-click update:
PATCH version 1.1.0β same deploy βplugin updateran, CC reports1.1.0. - The installed zip's
.mcp.jsonis the C2 client-mode shape (hnx mcp serve --profile <id> --server <https base>, no PAT, no inline secrets); skills land under the plugin'sskills/. - Leak scan clean (executor spawns are short-lived synchronous children).
- No AgentInstance rows created for marketplace deploys (verified).
-
server/test/jobs.test.tsβ new describe: dispatch carries the arm (baseUrl/name/plugin) + no instance registration; emitter accepts the machine PAT (catalog + archive URLs carry it) and 404s unknown tokens; foreign-personal profile βPROFILE_NOT_IN_OWNER_MARKETPLACE; PATCH version works + target stillTARGET_IMMUTABLE. The old claude-codeβTARGET_NOT_DEPLOYABLEgate test flipped toDAEMON_NO_MARKETPLACE_DEPLOY. -
cli/test/cc-marketplace.test.tsβ fakeclaudeshim on PATH recording argv + maintaining CC's state JSONs: fresh install, update routing, stale-URL remove-before-add, subcommand failure settles the job failed. -
scripts/smoke.mjsβ[#6]section: full E2E through a REAL daemon dist with the shim (accepted β succeededmethod/installedVersionβ machine PAT in the add URL β emitter serves the machine PAT β second job routes toplugin updateβ no agent instance). 434 passed / 0 failed.
- Platform-side uninstall (
claude plugin uninstall) β CC's own/pluginsurface manages it; a row action could wrap it later. - Inventory/AgentInstance linkage for marketplace deploys.
- opencode (no profile install adapter β the W12 stance is unchanged).
{ "profileId": "β¦", "marketplace": { "baseUrl": "https://106.54.210.46:15921", // PUBLIC_BASE_URL "marketplaceName": "harness-nexus-ashen", // harness-nexus-<owner username> "pluginName": "cc-profile" // the profile's name } }