plugin: author the /v1/admin/plugins surface cloud already serves

`generated/hanzo.json` — the measured reading of the binary's own router —
carried four operations the contract never named: `GET /v1/admin/plugins` and
`POST /v1/admin/plugins/{name}/{enable,disable,reload}`, registered by
`clients/plugin` as zip typed ops.

Undeclared is unreachable. Not from an SDK, and not from `hanzo`: the CLI's
`genspec` ITERATES this master and consults the live route table only to
REFUTE, so a path absent here can never enter the CLI spec no matter what the
server serves. The server having shipped them is exactly half the contract.

They belong in `plugin/`, not `admin/`. `admin/` is the aggregator subsystem
(`clients/admin`), whose own description says it reads identity from IAM and
money from commerce; a process supervisor is neither. `affiliates/`, `authors/`
and `referrals/` already establish the convention — a subsystem authors its own
`/v1/admin/<subsystem>` operator paths beside its public ones.

Prose, operationIds and parameters are verbatim from the emitted spec. The
response schemas are NAMED instead of inlined — ListOut → Host → PluginStatus →
PluginUsage, ActionOut → Result — each field carrying the doc comment of the Go
type it encodes, and `since` typed as a date-time string rather than the empty
object a `time.Time` renders as through reflection.

audit.py: all four move from `undeclared` to covered.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-28 09:37:57 -07:00
co-authored by Hanzo Dev
parent bf332c9137
commit 2237e4b445
4 changed files with 2586 additions and 85 deletions
+21
View File
@@ -2,6 +2,27 @@
## v1.0.0
### `plugin/` — author the `/v1/admin/plugins` operator surface cloud already serves
`generated/hanzo.json` carried four operations the contract never named:
`GET /v1/admin/plugins` and `POST /v1/admin/plugins/{name}/{enable,disable,reload}`,
served by `hanzoai/cloud/clients/plugin` as zip typed ops. Undeclared routes are
unreachable from every consumer of this repo — no SDK method, and no `hanzo`
command, because `hanzoai/cli`'s `genspec` iterates the AUTHORED master and uses
the live route table only to REFUTE. A path absent here can never enter the CLI
spec no matter what the server serves.
They live in `plugin/`, not `admin/`: `admin/` is the aggregator subsystem
(`clients/admin`), and the convention already established by `affiliates/`,
`authors/` and `referrals/` is that a subsystem authors its own
`/v1/admin/<subsystem>` operator paths beside its public ones.
Prose, operationIds and parameters are carried verbatim from the emitted spec.
The response schemas are named rather than inlined — `ListOut` → `Host` →
`PluginStatus` → `PluginUsage`, `ActionOut` → `Result` — with each field's
description lifted from the Go type it encodes, and `since` typed as a
`date-time` string rather than the empty object a `time.Time` renders as.
### Add `research/` — the /v1/research versioned R&D evidence surface (HIP-0512)
Adds the `research` service under the Intelligence domain: the R&D evidence plane
+1766 -82
View File
File diff suppressed because it is too large Load Diff
+406
View File
@@ -1528,6 +1528,146 @@ paths:
$ref: '#/components/schemas/admin_OverviewData'
'401':
$ref: '#/components/responses/admin_Unauthorized'
/v1/admin/plugins:
get:
tags:
- Admin
summary: List what each host is running
description: "list reports what each host is actually running: every loaded plugin with its\nversion, pid, uptime, reload\
\ and restart counts, and its measured CPU, RSS,\nthread and fd cost \u2014 read from the kernel, which is only answerable\
\ at all\nbecause a plugin is a process.\n\nReading this from deployment config would answer what was INTENDED. Only\
\ the\nprocess knows what is TRUE, and during a rolling upgrade the two disagree on\npurpose.\n"
operationId: plugin_adminPlugins
parameters:
- name: scope
in: query
required: false
description: 'Scope "host" answers for THIS host only. Default "fleet" fans out to every
live peer. A peer answers a host-scoped read, which is what stops the
fan-out recursing.
'
schema:
type: string
responses:
'200':
description: Per-host plugin inventory plus the fleet drift roll-up.
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_ListOut'
'401':
$ref: '#/components/responses/plugin_Unauthorized'
/v1/admin/plugins/{name}/disable:
post:
tags:
- Admin
summary: "Disable a plugin \u2014 its routes answer 503, not 404"
description: "disable stops the plugin. Its routes STAY REGISTERED and answer 503 \u2014 not 404.\n\nThat is zip's choice\
\ and this keeps it. Removing the routes would mutate the\nroute table, and re-adding them on enable would grow it\
\ without bound across\nrepeated cycles, which is the invariant that makes reloads flat in memory. It\nis also the\
\ better answer: 404 says \"no such API\" and a client may cache it\nand stop retrying, while 503 says \"this API\
\ exists and is down right now\",\nwhich is true and retryable. Which of the two 503s this is \u2014 deliberate stop\n\
or crash \u2014 is what the status's disabled flag reports.\n"
operationId: plugin_adminDisablePlugin
parameters:
- $ref: '#/components/parameters/plugin_PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_NameIn'
responses:
'200':
description: Per-host result of the disable.
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_ActionOut'
'401':
$ref: '#/components/responses/plugin_Unauthorized'
/v1/admin/plugins/{name}/enable:
post:
tags:
- Admin
summary: Enable a stopped or disabled plugin
description: 'enable brings a stopped or disabled plugin back on the artifact it already
has: the zero Plugin names no new artifact, so Reload reuses the loaded spec
and clears the disabled flag. Named for what an operator means by it.
'
operationId: plugin_adminEnablePlugin
parameters:
- $ref: '#/components/parameters/plugin_PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_NameIn'
responses:
'200':
description: Per-host result of the enable.
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_ActionOut'
'401':
$ref: '#/components/responses/plugin_Unauthorized'
/v1/admin/plugins/{name}/reload:
post:
tags:
- Admin
summary: Reload a plugin onto another build
description: 'reload swaps a plugin for another build without dropping a request. The
replacement is started and proven to be LISTENING before any traffic moves to
it, so a bad build leaves the old one serving and returns an error rather
than a hole; the old process then drains before it is killed.
With a version or url+sum it pins; naming a digest this host has run before is
the rollback, and costs no network because the digest IS the cache key. With
neither it restarts what is already loaded.
Fleet scope applies it to one host at a time and STOPS at the first failure,
so a build that cannot come up reaches exactly one host.
'
operationId: plugin_adminReloadPlugin
parameters:
- name: name
in: path
required: true
description: Name is the app, from the path. It must be one the manifest declares.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_ReloadIn'
responses:
'200':
description: Per-host result of the reload, with the digest now running.
content:
application/json:
schema:
$ref: '#/components/schemas/plugin_ActionOut'
'401':
$ref: '#/components/responses/plugin_Unauthorized'
/v1/admin/products:
get:
tags:
@@ -81076,6 +81216,48 @@ components:
type: string
description:
type: string
plugin_ActionOut:
type: object
description: 'The envelope every mutation answers with. Data is per host and in the order
applied, so a halted rollout reads as the prefix that succeeded followed by
the one that did not.
'
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/plugin_Result'
plugin_Drift:
type: object
description: 'One plugin''s agreement across the fleet. Versions holds every distinct
digest seen running; more than one means a rollout is incomplete or stuck,
which is the single question this whole view exists to answer.
'
properties:
name:
type: string
versions:
type: array
items:
type: string
running:
type: integer
down:
type: integer
disabled:
type: integer
drifted:
type: boolean
plugin_Error:
type: object
description: Standard error envelope.
@@ -81089,6 +81271,72 @@ components:
type: string
code:
type: string
plugin_Host:
type: object
description: 'One host''s own account of what it is running. Reported per host rather than
merged, because during a rollout the hosts disagree BY DESIGN and a merged
view hides exactly the state an operator is watching for.
'
properties:
host:
type: string
description: 'Host is the pod''s stable id, and Addr where it was reached. Self is true
for the host that answered the request.
'
addr:
type: string
self:
type: boolean
error:
type: string
description: 'Err is set when a peer could not be reached. Its plugins are then
unknown, which is NOT the same as none, so the list stays empty and the
drift below refuses to conclude anything from it.
'
plugins:
type: array
items:
$ref: '#/components/schemas/plugin_PluginStatus'
plugin_ListOut:
type: object
description: "The fleet board \u2014 one entry per host, plus the drift roll-up."
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/plugin_Host'
drift:
type: array
items:
$ref: '#/components/schemas/plugin_Drift'
data2:
type: integer
plugin_NameIn:
type: object
description: 'Addresses one plugin by name, for the operations that take nothing else.
'
properties:
name:
type: string
description: Name is the app, from the path.
scope:
type: string
description: 'Scope "host" applies here only; default "fleet" applies everywhere.
'
plugin_PluginInfo:
type: object
description: 'Introspection view of one mounted plugin. Derived from the manifest
@@ -81120,6 +81368,157 @@ components:
type: array
items:
$ref: '#/components/schemas/plugin_PluginInfo'
plugin_PluginStatus:
type: object
description: "One plugin's process view on one host \u2014 what `GET /v1/plugins` cannot\nanswer, because that reports\
\ the manifest and this reports the kernel.\n"
properties:
name:
type: string
prefix:
type: string
description: "Prefix is the FIRST subtree this plugin answers \u2014 the one a log line\nnames it by. Prefixes is\
\ every subtree, and a plugin may own several.\nReporting only the first would understate the blast radius of\
\ taking\nthis plugin down, which is the question a fleet view exists to answer.\n"
prefixes:
type: array
items:
type: string
source:
type: string
description: 'Source is where the binary came from: "embedded", "path", "url", or
"remote" for an instance this host did not start.
'
enum:
- embedded
- path
- url
- remote
version:
type: string
description: "Version is the artifact's SHA-256 when it was installed from a URL \u2014\nthe only version identifier\
\ that cannot drift from the bits actually\nrunning, since it IS the bits. Empty for the other sources.\n"
addr:
type: string
description: Addr is the socket or address serving it.
pid:
type: integer
description: PID is the child process, or 0 when this host did not start it.
running:
type: boolean
description: 'Running is false after Unload, or after a child exited and no Reload has
replaced it. Its routes stay registered and answer 503, so a false here
is the difference between "not deployed" and "deployed but down".
'
disabled:
type: boolean
description: "Disabled is true when Unload stopped it deliberately, as opposed to it\nhaving crashed. Both answer\
\ 503, so without this an operator cannot tell\na maintenance window from an outage \u2014 and would page for\
\ the former.\n"
since:
type: string
format: date-time
description: "Since is when the CURRENT instance started \u2014 it resets on Reload, so it\nreports the age of what\
\ is running, not of the mount.\n"
reloads:
type: integer
description: 'Reloads counts successful swaps since Load. A climbing number on one
host and not its peers is the signal that a rollout is uneven.
'
restarts:
type: integer
description: 'Restarts counts times the supervisor brought this plugin back after it
died on its own. Distinct from Reloads, which are deliberate: a nonzero
Restarts is a plugin crashing, and a climbing one is a crash loop.
'
usage:
$ref: '#/components/schemas/plugin_PluginUsage'
plugin_PluginUsage:
type: object
description: "A plugin's resource cost, measured rather than self-reported.\n\nThis exists because a plugin is a process.\
\ A linked-in subsystem cannot be\nmeasured this way at any price \u2014 one process has one RSS and one CPU clock,\n\
and nothing inside it can be attributed to a subsystem. Running a service as\nits own process makes \"which app is\
\ eating the memory\" a question with an\nexact answer instead of an argument.\n\nZero means not measured (no /proc,\
\ or nothing running), not measured-as-zero.\n"
properties:
cpuNs:
type: integer
format: int64
description: 'CPU is total user+system time this instance has consumed since it
started, in nanoseconds. It only ever climbs, so a rate is the
interesting derivative.
'
rssBytes:
type: integer
format: int64
description: "RSS is resident memory in bytes \u2014 the honest number for \"what does this\nplugin cost\", as opposed\
\ to virtual size.\n"
threads:
type: integer
description: 'Threads and FDs are the two limits a busy service hits first, and both
are leaks when they climb without bound.
'
fds:
type: integer
plugin_ReloadIn:
type: object
description: "Names an artifact to run. Exactly one of Version or URL+Sum, or neither to\nrestart the artifact already\
\ loaded \u2014 which is how a wedged plugin is\nbounced without changing what it runs.\n"
properties:
name:
type: string
description: 'Name is the app, from the path. It must be one the manifest declares.
'
version:
type: string
description: "Version is a release tag, resolved to a URL and digest through the\norigin's binaries.json index \u2014\
\ the same index CI publishes, so there is\nno second table mapping versions to digests.\n"
url:
type: string
description: 'URL is the artifact directly, for an origin with no index. Sum is its hex
SHA-256 and is REQUIRED with it: zip refuses an unverified download, and
so does this.
'
sum:
type: string
scope:
type: string
description: 'Scope "host" applies here only. Default "fleet" rolls it out one host at
a time, halting on the first host that fails to come up.
'
plugin_Result:
type: object
description: One host's outcome for one action.
properties:
host:
type: string
ok:
type: boolean
version:
type: string
msg:
type: string
pricing_BlockStoragePricing:
type: object
properties:
@@ -87384,6 +87783,13 @@ components:
description: URL-encoded JSON input for tRPC queries
schema:
type: string
plugin_PluginName:
name: name
in: path
required: true
description: Name is the app, from the path.
schema:
type: string
projects_DeploymentId:
name: id
in: path
+393 -3
View File
@@ -16,10 +16,17 @@ info:
(`http` default, or `zap`).
Because plugins mount at arbitrary manifest-defined prefixes, their routes are
NOT part of this fixed contract. The ONLY static HTTP surface this subsystem
NOT part of this fixed contract. The static HTTP surface this subsystem
registers is the introspection endpoint `GET /v1/plugins`, which lists what is
currently mounted. If `CLOUD_PLUGINS` is unset, zero plugins mount and the
list is empty.
currently mounted, plus the operator surface under `/v1/admin/plugins` that
reads each host's running processes and swaps, stops and starts them. If
`CLOUD_PLUGINS` is unset, zero plugins mount and the list is empty.
Introspection and operation answer different questions. `GET /v1/plugins`
reports the MANIFEST view — name, kind, prefix — for the host that answered.
`/v1/admin/plugins` reports the PROCESS view — pid, uptime, reload and restart
counts, measured CPU/RSS/thread/fd cost — per host across the fleet, plus the
drift roll-up that says whether a rollout finished.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
@@ -39,6 +46,8 @@ security:
tags:
- name: Plugins
description: Runtime plugin introspection.
- name: Admin
description: Fleet plugin operation — list, enable, disable, reload.
paths:
/v1/plugins:
get:
@@ -60,6 +69,140 @@ paths:
$ref: '#/components/schemas/PluginList'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins:
get:
tags:
- Admin
summary: List what each host is running
description: |
list reports what each host is actually running: every loaded plugin with its
version, pid, uptime, reload and restart counts, and its measured CPU, RSS,
thread and fd cost — read from the kernel, which is only answerable at all
because a plugin is a process.
Reading this from deployment config would answer what was INTENDED. Only the
process knows what is TRUE, and during a rolling upgrade the two disagree on
purpose.
operationId: adminPlugins
parameters:
- name: scope
in: query
required: false
description: |
Scope "host" answers for THIS host only. Default "fleet" fans out to every
live peer. A peer answers a host-scoped read, which is what stops the
fan-out recursing.
schema:
type: string
responses:
'200':
description: Per-host plugin inventory plus the fleet drift roll-up.
content:
application/json:
schema:
$ref: '#/components/schemas/ListOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/enable:
post:
tags:
- Admin
summary: Enable a stopped or disabled plugin
description: |
enable brings a stopped or disabled plugin back on the artifact it already
has: the zero Plugin names no new artifact, so Reload reuses the loaded spec
and clears the disabled flag. Named for what an operator means by it.
operationId: adminEnablePlugin
parameters:
- $ref: '#/components/parameters/PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NameIn'
responses:
'200':
description: Per-host result of the enable.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/disable:
post:
tags:
- Admin
summary: Disable a plugin — its routes answer 503, not 404
description: |
disable stops the plugin. Its routes STAY REGISTERED and answer 503 — not 404.
That is zip's choice and this keeps it. Removing the routes would mutate the
route table, and re-adding them on enable would grow it without bound across
repeated cycles, which is the invariant that makes reloads flat in memory. It
is also the better answer: 404 says "no such API" and a client may cache it
and stop retrying, while 503 says "this API exists and is down right now",
which is true and retryable. Which of the two 503s this is — deliberate stop
or crash — is what the status's disabled flag reports.
operationId: adminDisablePlugin
parameters:
- $ref: '#/components/parameters/PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NameIn'
responses:
'200':
description: Per-host result of the disable.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/reload:
post:
tags:
- Admin
summary: Reload a plugin onto another build
description: |
reload swaps a plugin for another build without dropping a request. The
replacement is started and proven to be LISTENING before any traffic moves to
it, so a bad build leaves the old one serving and returns an error rather
than a hole; the old process then drains before it is killed.
With a version or url+sum it pins; naming a digest this host has run before is
the rollback, and costs no network because the digest IS the cache key. With
neither it restarts what is already loaded.
Fleet scope applies it to one host at a time and STOPS at the first failure,
so a build that cannot come up reaches exactly one host.
operationId: adminReloadPlugin
parameters:
- name: name
in: path
required: true
description: Name is the app, from the path. It must be one the manifest declares.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReloadIn'
responses:
'200':
description: Per-host result of the reload, with the digest now running.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
BearerAuth:
@@ -69,6 +212,14 @@ components:
description: |
JWT access token issued by Hanzo IAM (https://hanzo.id). The same token
authenticates every Hanzo service.
parameters:
PluginName:
name: name
in: path
required: true
description: Name is the app, from the path.
schema:
type: string
responses:
Unauthorized:
description: Missing or invalid bearer token.
@@ -106,6 +257,245 @@ components:
type: string
description: Mount point at which the plugin's routes are served.
example: /v1/pricing
ListOut:
type: object
description: The fleet board — one entry per host, plus the drift roll-up.
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/Host'
drift:
type: array
items:
$ref: '#/components/schemas/Drift'
data2:
type: integer
Host:
type: object
description: |
One host's own account of what it is running. Reported per host rather than
merged, because during a rollout the hosts disagree BY DESIGN and a merged
view hides exactly the state an operator is watching for.
properties:
host:
type: string
description: |
Host is the pod's stable id, and Addr where it was reached. Self is true
for the host that answered the request.
addr:
type: string
self:
type: boolean
error:
type: string
description: |
Err is set when a peer could not be reached. Its plugins are then
unknown, which is NOT the same as none, so the list stays empty and the
drift below refuses to conclude anything from it.
plugins:
type: array
items:
$ref: '#/components/schemas/PluginStatus'
PluginStatus:
type: object
description: |
One plugin's process view on one host — what `GET /v1/plugins` cannot
answer, because that reports the manifest and this reports the kernel.
properties:
name:
type: string
prefix:
type: string
description: |
Prefix is the FIRST subtree this plugin answers — the one a log line
names it by. Prefixes is every subtree, and a plugin may own several.
Reporting only the first would understate the blast radius of taking
this plugin down, which is the question a fleet view exists to answer.
prefixes:
type: array
items:
type: string
source:
type: string
description: |
Source is where the binary came from: "embedded", "path", "url", or
"remote" for an instance this host did not start.
enum:
- embedded
- path
- url
- remote
version:
type: string
description: |
Version is the artifact's SHA-256 when it was installed from a URL —
the only version identifier that cannot drift from the bits actually
running, since it IS the bits. Empty for the other sources.
addr:
type: string
description: Addr is the socket or address serving it.
pid:
type: integer
description: PID is the child process, or 0 when this host did not start it.
running:
type: boolean
description: |
Running is false after Unload, or after a child exited and no Reload has
replaced it. Its routes stay registered and answer 503, so a false here
is the difference between "not deployed" and "deployed but down".
disabled:
type: boolean
description: |
Disabled is true when Unload stopped it deliberately, as opposed to it
having crashed. Both answer 503, so without this an operator cannot tell
a maintenance window from an outage — and would page for the former.
since:
type: string
format: date-time
description: |
Since is when the CURRENT instance started — it resets on Reload, so it
reports the age of what is running, not of the mount.
reloads:
type: integer
description: |
Reloads counts successful swaps since Load. A climbing number on one
host and not its peers is the signal that a rollout is uneven.
restarts:
type: integer
description: |
Restarts counts times the supervisor brought this plugin back after it
died on its own. Distinct from Reloads, which are deliberate: a nonzero
Restarts is a plugin crashing, and a climbing one is a crash loop.
usage:
$ref: '#/components/schemas/PluginUsage'
PluginUsage:
type: object
description: |
A plugin's resource cost, measured rather than self-reported.
This exists because a plugin is a process. A linked-in subsystem cannot be
measured this way at any price — one process has one RSS and one CPU clock,
and nothing inside it can be attributed to a subsystem. Running a service as
its own process makes "which app is eating the memory" a question with an
exact answer instead of an argument.
Zero means not measured (no /proc, or nothing running), not measured-as-zero.
properties:
cpuNs:
type: integer
format: int64
description: |
CPU is total user+system time this instance has consumed since it
started, in nanoseconds. It only ever climbs, so a rate is the
interesting derivative.
rssBytes:
type: integer
format: int64
description: |
RSS is resident memory in bytes — the honest number for "what does this
plugin cost", as opposed to virtual size.
threads:
type: integer
description: |
Threads and FDs are the two limits a busy service hits first, and both
are leaks when they climb without bound.
fds:
type: integer
Drift:
type: object
description: |
One plugin's agreement across the fleet. Versions holds every distinct
digest seen running; more than one means a rollout is incomplete or stuck,
which is the single question this whole view exists to answer.
properties:
name:
type: string
versions:
type: array
items:
type: string
running:
type: integer
down:
type: integer
disabled:
type: integer
drifted:
type: boolean
ActionOut:
type: object
description: |
The envelope every mutation answers with. Data is per host and in the order
applied, so a halted rollout reads as the prefix that succeeded followed by
the one that did not.
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/Result'
Result:
type: object
description: One host's outcome for one action.
properties:
host:
type: string
ok:
type: boolean
version:
type: string
msg:
type: string
NameIn:
type: object
description: |
Addresses one plugin by name, for the operations that take nothing else.
properties:
name:
type: string
description: Name is the app, from the path.
scope:
type: string
description: |
Scope "host" applies here only; default "fleet" applies everywhere.
ReloadIn:
type: object
description: |
Names an artifact to run. Exactly one of Version or URL+Sum, or neither to
restart the artifact already loaded — which is how a wedged plugin is
bounced without changing what it runs.
properties:
name:
type: string
description: |
Name is the app, from the path. It must be one the manifest declares.
version:
type: string
description: |
Version is a release tag, resolved to a URL and digest through the
origin's binaries.json index — the same index CI publishes, so there is
no second table mapping versions to digests.
url:
type: string
description: |
URL is the artifact directly, for an origin with no index. Sum is its hex
SHA-256 and is REQUIRED with it: zip refuses an unverified download, and
so does this.
sum:
type: string
scope:
type: string
description: |
Scope "host" applies here only. Default "fleet" rolls it out one host at
a time, halting on the first host that fails to come up.
Error:
type: object
description: Standard error envelope.