Marketplace client¶
xcore_agent.agent.marketplace_client — client for the real
xcore-team/marketplace API (X-API-Key auth, HMAC-SHA256-signed plain ZIP).
xcore_agent.agent.marketplace_client
¶
Client for the real xcore-team/marketplace API — not the invented
.xdeploy/DEK/Ed25519 contract in hub_client.py.
Verified against the actual backend (xcore-team/marketplace, app/marketplace
and app/xdevkeys). The two contracts are structurally different enough that
this is a separate client + separate pipeline (marketplace_pipeline.py)
rather than a drop-in HubClient implementation:
- Auth is a static
X-API-Key: xdk_...header (fromPOST /xdevkeys/api-keys), not a login exchange producing a bearer token. - There is no "project": one deployment fetches one plugin or one service (xservices calls these "extensions"), identified by its marketplace slug.
GET /{plugins|services}/{slug}/install?version=latest|<version>returns the plugin's source tree as a plain ZIP (GitHub's zipball, not an encrypted.xdeploycontainer) plus response headers: X-Signature: hmac_sha256:X-Plugin / X-Service: name@version X-Repo: owner/repo@tag - The signature is HMAC-SHA256, not Ed25519 — see
crypto.verify_hmac_sha256_hexfor what that means for the trust model. - Deployment status is reported via
POST /deployments/report(app/xdeploymentson the backend) — a log entry per attempt, scoped to the API key holder (the operator), not the plugin's publisher, since deploying a public plugin doesn't require owning it.report_deploymentis best-effort: a reporting failure never fails a deployment that otherwise succeeded or failed on its own terms. - The Hub is an XCore instance: every plugin is mounted at a fixed
/app/<plugin-name>prefix (rootintegration.yaml:plugin_prefix: "/app"— a framework-level convention, not a dev-only artifact, so this applies in production too, absent any reverse proxy rewriting paths). Plugin artifacts live under themarketplaceplugin, service artifacts underxservices, and deployment reporting under a third, separatexdeploymentsplugin —base_urlis therefore the Hub's bare root (e.g.https://marketplace.xcorehub.dev, no plugin segment), and each request below picks its own/app/<plugin-name>mount internally so callers never need to know this backend's internal plugin layout.
Kind = Literal['plugin', 'service']
module-attribute
¶
DeploymentStatus = Literal['success', 'failed', 'rolled_back']
module-attribute
¶
FetchedArtifact
dataclass
¶
Source code in xcore_agent/agent/marketplace_client.py
MarketplaceClient
¶
HTTP client for the real xcore-team/marketplace API.
base_url is the Hub's bare root (no /app/... segment — see module
docstring); transport exists so tests can inject httpx.MockTransport
instead of hitting the network, production callers leave it as None.
Source code in xcore_agent/agent/marketplace_client.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | |
__init__(base_url: str, *, api_key: str, timeout: float = 60.0, transport: httpx.AsyncBaseTransport | None = None) -> None
¶
Source code in xcore_agent/agent/marketplace_client.py
__aenter__() -> MarketplaceClient
async
¶
__aexit__(exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None
async
¶
aclose() -> None
async
¶
get_latest_version(*, slug: str, kind: Kind = 'plugin') -> str
async
¶
Poll target for agent.watcher.Watcher/watch_sources — reads
the GET /{kind}/{slug} detail route, which reports
latest_version. Public for a visibility="public" target, but
sends X-API-Key anyway (real prod bug found running watch-sources
against a project with several private sources: this route only
recognized a JWT session, never an xdevkey, so a private plugin
404'd for every xcore-agent caller regardless of whether its key
actually had access — see xcore-team/marketplace's _api_key_
viewer_id/xcore-team/xservices's route fix for the server side of
this same commit).
Source code in xcore_agent/agent/marketplace_client.py
fetch_artifact(*, slug: str, version: str = 'latest', kind: Kind = 'plugin') -> FetchedArtifact
async
¶
Source code in xcore_agent/agent/marketplace_client.py
report_deployment(*, kind: Kind, slug: str, version: str, status: DeploymentStatus, started_at: str, completed_at: str, host_id: str = 'default', repo: str = '', error_message: str | None = None) -> None
async
¶
Report the outcome of one deployment attempt. started_at/completed_at
are ISO-8601 strings (MarketplaceDeploymentReport already stores them that
way). Raises ArtifactError on failure — callers that want "best-effort"
(recommended; see class docstring) should catch it themselves rather than
rely on this method swallowing errors, so tests and callers stay in control
of that choice.