Writing Widgets
This is the end-to-end author path for Gridmason: how you go from nothing to a published, verified widget. A widget is a custom element that a Gridmason host (the dashboard, or any app that mounts Gridmason widgets) loads and hands a capability-scoped data handle. You describe that element in a manifest, develop it against a local harness, run the exact checks a registry will run, and publish it with a keyless signature.
Everything below is a single tool — the gridmason binary — plus two contracts your
code targets: the widget ABI (how the host talks to your element) and the
manifest schema (how you describe your element to the platform). You never write
a build step you don't want, you never hand-write a signature, and you never guess at
what review will say — the CLI runs review's checks locally first.
Anatomy of a widget
A widget is three things that travel together: a custom element (the code), a manifest (the description), and a settings schema (the user-facing options). Understanding the contract between them is the whole job.
The custom element contract
Your widget is a standard customElements-registered element. The host mounts one
instance of your element per placed item on a page. It configures the element
before inserting it into the DOM, so your connectedCallback always observes a
fully-configured element.
The host sets four attributes and one property:
| Set as | Name | Carries | Read as |
|---|---|---|---|
| attribute | context |
The page-context value the page is scoped to (e.g. "the customer this page is about") | JSON |
| attribute | settings |
This instance's saved user settings, validated against your settings schema ({} when none) |
JSON |
| attribute | instance-id |
An opaque, stable id for this mount | string |
| attribute | edit-mode |
Whether the dashboard is in edit mode — a boolean attribute: presence is the signal, no value is significant | boolean |
| property | sdk |
The capability-scoped host SDK handle — all privileged I/O flows through it | object (opaque) |
Two rules make this ergonomic:
context,settings, andedit-modeupdate in place. When they change, the host updates the attribute on the same element — it does not tear your widget down and re-mount it. List them in your element'sobservedAttributesand react inattributeChangedCallback; a context change or an edit-mode toggle never loses your widget's state.- The
sdkhandle is delivered beforeconnectedCallback. The property name is pinned ABI (this.sdk), so you may readthis.sdksynchronously on the first line ofconnectedCallback. If your widget needs to interoperate with a host that mints a distinct handle per instance after mount, readthis.sdkat first use rather than latching it once — the host may re-assign it in place (no re-mount, no notification). When a host never sets a handle,this.sdkreadsundefined, so a widget that needs one must tolerate its absence.
What your widget receives through .sdk — the SDK handle (@gridmason/sdk) is
the only sanctioned path from your widget to data. It is capability-scoped: every
call is checked against min(user permissions, your declared capabilities) before any
transport happens, so a call your manifest didn't declare is denied with a typed
PermissionDenied before it leaves the widget. Through it you get records (read/
query domain records), net (scoped network fetch), events (a typed, namespaced
cross-widget event bus), context, settings, nav, telemetry, and identity.
You never call global fetch — you call sdk.
What your widget emits (events out). Your element dispatches bubbling, composed
DOM CustomEvents that the host shell catches. Two are part of the mount lifecycle:
gm:ready— dispatch this once your widget is interactive. If your widget finishes initializing synchronously, you don't need it — you're considered ready whenconnectedCallbackreturns. If your widget loads asynchronously, signal pending duringconnectedCallback(dispatch a bubblinggm:loadingevent, or set the boolean attributegm-loading); the host shows a skeleton until you dispatchgm:ready.gm:error(detail?: { message?, error? }) — dispatch at any time to fall back to the host's error card.
You may also dispatch your own author-defined outbound events (the vanilla scaffold
wires a sample gm:action button). Note these DOM CustomEvents are the mount-level
channel and are not the SDK event bus — the SDK event bus (sdk.events) is
capability-gated and needs an events:<ns> capability; the DOM events do not.
Cleanup is your responsibility. The host guarantees your disconnectedCallback
fires before your instance is removed or re-mounted. You must release anything you
allocated outside the SDK there: timers (setInterval/setTimeout), observers
(IntersectionObserver/ResizeObserver/MutationObserver), and listeners you added
to targets you don't own (window, document, media queries). SDK event-bus
subscriptions are auto-released on unmount, so those are not your burden — but a timer
or observer you leak keeps running after your widget leaves the page.
The manifest
manifest.json describes your artifact to the platform. It is validated against the
authoritative @gridmason/protocol manifest JSON Schema — the same schema gridmason lint and the registry both enforce. additionalProperties is false, so an unknown
field is an error.
Required fields (the artifact cannot load without them):
| Field | Meaning |
|---|---|
formatVersion |
Wire-format version of the manifest, as major.minor (e.g. "1.0"). |
tag |
Your widget's custom-element tag. Must be lowercase, contain a hyphen, use only [a-z0-9-] starting with a letter, and be prefixed with <publisher>-. |
kind |
One of widget, plugin, page-type, layout. |
name |
Human-readable name. |
publisher |
Your publisher namespace prefix (unique within one registry). |
version |
SemVer of this artifact (e.g. "0.1.0"). |
entry |
Path to the ES-module that registers the custom element; content-hashed at publish. |
Optional fields that describe placement, capabilities, and dependencies:
| Field | Meaning |
|---|---|
requiresContext |
Context slots you need the page to supply, keyed by slot name. Each slot may name a recordType (e.g. { "record": { "recordType": "customer" } }). |
supportsPages |
Page-type globs your widget may be placed on (e.g. ["dashboards.*"]). |
size |
Grid footprint: a required default [columns, rows], plus optional min / max. |
capabilities |
The privileged APIs you declare (see below). |
props |
Path to the JSON Schema for your widget's user-facing settings. |
requires |
Dependency-DAG edges: [{ tag, range }]. The registry rejects cycles at publish. |
sharedScope |
Import-map ranges you expect the host to satisfy (e.g. react, vue). Omit it for a fully self-contained module graph. |
thumbnail |
Path to a thumbnail asset. |
pageType |
Present only when kind is page-type. |
Capabilities are the load-bearing part. Each is { api, scope? }, where api is
one of records.read, records.write, net, or events, and scope is a
colon-delimited path (or omitted for an unscoped capability). The string form is
<api>[:<scope>]. Examples:
records.read:recordType:customer— readcustomerrecords. (The records scope grammar isrecords.read:recordType:<type>, not a barerecords.read:<type>.)net:api.example.com— make scoped network calls to that host.events:sales— emit/subscribe on thesalesevent namespace.
The SDK enforces min(user permissions, declared capabilities) on every call, so
declare only what your widget actually uses — a smaller capability set clears
review faster, and (as covered under Versioning)
adding a capability in a later version re-triggers a review lane. A capability you
declare but never use is an over-declaration to trim before you publish.
Page-type targeting. requiresContext says what the page must give you;
supportsPages says which page types you may be placed on. A widget that reads a
customer record declares requiresContext: { record: { recordType: "customer" } }
and a matching records.read:recordType:customer capability — the manifest and the
capability are paired, and a scaffold pairs them for you.
The settings schema
If your widget has user-facing options, ship a draft-07 JSON Schema at the path
your manifest's props points to (the scaffold generates props.schema.json, empty
to start). A widget that ships only a schema and no custom settings element is a
schema-only widget: the host renders your schema as a form in its own design
system and round-trips the values through the handle. You write the schema; the host
writes the field UI. See Settings forms below.
Scaffold to running
Install the CLI
npm i -g @gridmason/cli # provides the `gridmason` binary
# or, without installing:
npx @gridmason/cli --help
Requires Node.js >= 22. One binary spans the whole author loop — scaffold → develop → lint → publish — and runs the identical automated checks a registry review runs, locally, so "green locally" predicts "passes review."
Scaffold a project
gridmason widget init "Sales Chart" --publisher acme
cd sales-chart
init prompts for anything you don't pass as a flag (and, in a non-interactive
context like CI, errors instead of prompting for a missing required answer):
| Answer | Flag | Default | Notes |
|---|---|---|---|
| Name | [name] argument (required) |
— | Human name; slugified for the tag and directory. |
| Publisher prefix | --publisher <prefix> (required) |
— | The tag becomes <publisher>-<slug>; lowercase [a-z0-9-], starting with a letter. |
| Kind | --kind <kind> |
widget |
widget, plugin, page-type, or layout. |
| Framework | --framework <name> |
vanilla |
vanilla, react, or vue. Sets sharedScope defaults. |
init refuses to write into a non-empty directory — it never clobbers an existing
project. The scaffold is lint-clean out of the box: the publisher-prefix rule is
enforced at creation, so a scaffold can never emit a manifest that would fail review.
Templates. Three starters are available via --framework, and each emits a plain
ES-module entry that speaks the same widget ABI:
- vanilla — a hand-written ES module, no build step. This is the reference the
other two are measured against. It imports
@gridmason/sdk/vanillafor its helpers and declares nosharedScope. - react — a baseline that uses plain
createElement(no JSX), so it also needs no build step.sharedScopeisreact ^18,react-dom ^18. Adopt JSX and any ESM-emitting bundler when you want — the CLI is not a bundler. - vue — a baseline render function (no SFC), no build step.
sharedScopeisvue ^3. Add.vuefiles and a bundler when you want.
For React and Vue, the entry imports its framework by bare specifier (react,
react-dom/client, vue) — exactly the sharedScope entries — so the host supplies
the framework through its import map instead of your widget bundling its own copy.
@gridmason/sdk is not in sharedScope; it is the platform SDK the host provides
ambiently.
Project structure tour
gridmason widget init "Sales Chart" --publisher acme produces sales-chart/:
| File | Purpose |
|---|---|
manifest.json |
The manifest stub: publisher-prefixed tag, entry, props, thumbnail, a size default, a sample requiresContext slot, and a matching records.read capability. sharedScope is set for React/Vue. |
src/entry.js |
The ES-module entry that registers the custom element (from the chosen template). |
props.schema.json |
A draft-07 JSON Schema for your user-facing settings (empty to start). |
thumbnail.svg |
A neutral thumbnail placeholder, so the manifest's thumbnail path is valid from the first lint. |
src/<slug>.stories.js |
A framework-agnostic Storybook story stub that renders the element by tag. |
.github/workflows/ci.yml |
CI that runs gridmason lint on every push/PR — fails the PR before a registry review sees it. |
fixtures/ |
Seeded sample data for gridmason dev (default.json plus a contexts/ preset), derived from the manifest. |
package.json |
dev/lint scripts and the @gridmason/sdk + @gridmason/cli deps. |
README.md, .gitignore |
Authoring readme and standard ignores. |
The scaffolded entry registers your custom element idempotently
(if (!customElements.get(tag))), reads the four ABI attributes, emits gm:ready
on mount, and — before a real host wires a handle — falls back to a no-op SDK
(createNoopSDK, seeded from the attributes) so it renders on first run with no
host at all. You trim that fallback for production.
The framework templates consume the real SDK helpers over the handle:
- React (
@gridmason/sdk/react):useRecord(sdk, ref)reads the primary context record,useSettings(sdk)gives reactive settings. - Vue (
@gridmason/sdk/vue):useRecordanduseSettingscomposables returning reactive refs, called insetup(). - vanilla (
@gridmason/sdk/vanilla):watchRecordsubscribes to the primary record's read state andbindSettingsbinds settings imperatively; each returns anUnsubscribeyou call on teardown.
Every helper mirrors an sdk method 1:1 and adds no privileged logic, so your widget
stays auditable by reading its SDK calls.
Develop against the local harness
gridmason dev # fixture harness at http://127.0.0.1:3000
gridmason dev is the develop step. It starts a localhost server that does four jobs
and no more — it is a conduit, never a data backend:
- Serves your
entrymodule so the dashboard'sdevmode can hot-load it. - Runs a standalone fixture harness at
/that mounts your widget on the SDK fixture implementation — so the loop works with no dashboard and no backend. - Live-reloads on every edit to
src/,manifest.json, orfixtures/, and re-validates the manifest on the fly. - Mounts an SDK inspector at
/@dev/inspector(also linked from the harness dev bar).
Every datum your widget sees comes from a fixture file or a --proxy target,
never from the dev server itself. Useful invocations:
gridmason dev --context example-2 # mount the fixtures/contexts/example-2.json preset
gridmason dev --proxy http://localhost:5173 # forward SDK calls to a real running host
gridmason dev --port 4000 --json
There are two ways to run against a real host:
- The standalone fixture harness (
/) is best for the plain-ESM (vanilla) entry, which imports only its own module graph. A React or Vue entry imports its framework from the host's shared scope, so exercise it through the dashboard'sdevsideload (which supplies that scope), not the standalone harness. --proxy <host-url>trades fixtures for integration realism: your SDK calls are forwarded to a real running host, with the capability check still enforced — a capability your manifest doesn't declare stays denied through the proxy, and a denied call never reaches the target. (No shipped host implements the proxy receive side yet; this is the shape a host must match to be a--proxytarget.)
The SDK inspector answers one review-anticipating question: do the capabilities
your manifest declared match the SDK calls your widget actually makes? It has two
tables — declared capabilities (each marked used or not yet used this session) and
observed gated SDK calls (each with the capability it required, whether that capability
is declared or undeclared, and how it resolved). An undeclared row is a
red-flagged violation — the manifest never declared that capability, so a host and
review would deny it. A not yet used declared capability is an over-declaration to
trim. Call outcomes you'll see: fixture-hit (a fixture answered), default-empty
(allowed but no fixture matched — an uncovered path, add a fixture), allowed (a gated
events call), denied (the undeclared-capability violation). Only gated
records/net/events calls appear; ungated settings/nav/telemetry calls carry
no capability and are out of scope.
Fixtures for realistic dev data
gridmason widget init seeds a fixtures/ directory so the first gridmason dev
renders with data before you write a line of widget code. The layout:
fixtures/
default.json # the FixtureFile gridmason dev mounts by default
contexts/
<record>-2.json # a named page-context preset (gridmason dev --context <name>)
default.json is a complete fixture file: records (reads + queries), net
(scoped-request stubs), events (scripted emissions), and context (the default
page-context the widget mounts against). A contexts/<name>.json file is a standalone
named page-context preset; gridmason dev --context <name> loads it and overrides the
default context while records/net/events still come from default.json. A named
preset that doesn't exist is a hard error — a typo shouldn't silently fall back.
The seed is derived mechanically from your manifest — change the manifest's context
or capabilities and re-run init, and the seeded files change to match:
| Manifest declaration | Seeds |
|---|---|
a requiresContext slot with a recordType |
a records.read template (serves any id of that type), a one-row records.query list, and a record-ref for that slot in every context preset |
a requiresContext slot without a recordType |
a string placeholder (sample-<slot>) in every context preset |
a net:<host> capability |
one empty net stub keyed by host + path /, responding 200 with {} — fill it in |
an events:<ns> capability |
one scripted emission on that namespace (topic <ns>/sample, fired at delay: 0) |
Everything under fixtures/ is plain JSON and hot-reloads like source. Capability
enforcement is real even here: createFixtureSDK enforces your declared capabilities
exactly as a host would, so a call for a capability you didn't declare is denied and no
fixture satisfies it — fixture-green predicts review-green. If you add a
requiresContext type without its records.read:recordType:<type> capability, the dev
inspector flags that call denied, the same verdict review would give.
Quality gates
gridmason lint
gridmason lint [path] [--json] [--registry <url>]
gridmason lint runs the same automated checks the registry runs, from one shared
module the registry imports verbatim — one implementation, no divergence — so
local-green predicts review-pass. It reads <path>/manifest.json (default: the current
directory), runs every check, prints human diagnostics to stderr (one ✓/!/✗ line
per finding, with a fix hint under each failure), and exits 0 if and only if no
check failed (a warning does not fail the run). --json prints a single machine-
readable report to stdout for CI gating.
The checks fall into these categories:
- Manifest checks (fail on violation, resolved synchronously at publish): your
manifest conforms to the protocol JSON Schema; your
tagis well-formed and publisher-prefixed; each capability's scope grammar is well-formed. (These are three distinct check ids because they come from three protocol primitives, and a reviewer wants to know which failed.) - Dependency checks (fail): your
requiresgraph is acyclic. Offline,lintsees only your one manifest, so it can prove only a self-dependency; a--registryrun adds a check against the registry's live transitive graph to catch a cross-manifest cycle. - SDK-adherence checks (the frontend-remote human-review tier): your source does no
raw network I/O outside the SDK (no global
fetch,XMLHttpRequest,WebSocket,EventSource,sendBeacon— all egress goes through the handle); it doesn't reach ambient credential/storage surfaces the sandbox withholds (document.cookie, Web Storage — a fail;indexedDB/window.name— a warn); and it contains no obfuscation that hides network/DOM access (eval/new Functionfail; decode chains and computed global access warn).sdk.raw-networkis the check most likely to fail a naive widget — surfacing it locally is the point. - DOM checks (advisory warnings): a frontend remote keeps DOM access inside its own
subtree — document-wide queries,
document.body/head, top-level navigation, or cross-frame reach are flagged for the reviewer but do not fail your local gate.
Two honesty caveats worth internalizing. First, the sdk.* and dom.* checks are
heuristics run against a masked view of your source (comments and string contents
are blanked first, so a keyword in a comment is never mistaken for a call). They catch
the honest mistake and the lazy evasion, not a determined one — aliasing a primitive
into a variable, runtime name assembly, and code hidden in template-literal
interpolations can slip past. A clean run is evidence, not a guarantee; the registry's
review is the authority. Second, lint reads only your src/ tree plus the manifest
entry locally — the registry closes that gap by analyzing the built artifact.
Registry-aware checks (--registry <url>) add two more that consult the target
registry: a capability-diff against the last published version (warns on an increase —
see Versioning) and a server-side acyclic check
against the live dependency graph. Both are fail-safe, not fail-closed: an
unreachable or malformed registry yields a warning (the answer is unknown), never a
false pass or a phantom failure.
The --json report tags every check result with the registry review tier its
findings feed, so CI learns which review SLA the artifact will hit before it publishes.
Settings forms
If your widget is schema-only, verify its settings render correctly. The settings-form
helper (useSettingsForm in the React adapter) compiles your JSON Schema into an
ordered field list, seeds each field from the current settings (or the schema
default), and persists every edit through the handle — you write one hook call, the
host writes one field adapter against its design system. The SDK ships no field UI.
The v0 compiler handles a top-level object schema of scalar and single-choice properties:
| Schema property | Rendered control |
|---|---|
boolean |
checkbox |
number / integer |
number input |
any type with an enum |
select |
string with format: "textarea" |
textarea |
string (otherwise) |
text |
title becomes the label (falling back to the property name), description becomes
help text, default is the value shown when the store has none, and the schema
required array sets each field's required flag. A property whose type v0 doesn't
handle — a nested object, an array, null, or a type-union — is skipped, and a
schema that is not a type: "object" with properties compiles to an empty form.
Design your settings schema within these shapes for v0.
Conformance
"Conformance" in Gridmason refers to the host-conformance kit — the machine-checkable
definition of "a valid Gridmason host." As a widget author you don't run it; it is
what a host builder runs to prove their HostSDK implementation honors the six contract
rules (capability intersection before transport, net-host scoping, per-instance
identity, typed namespaced events, per-instance isolation, unmount revocation). Your
author-side equivalent of conformance is the pairing above: gridmason lint (review's
checks) plus a green gridmason dev SDK inspector (declared capabilities match observed
calls).
Publishing
Publishing is the trust path. You establish an OIDC identity, then publish
lint-gates, signs keyless, uploads, and polls the review outcome.
Establish an identity (gridmason login)
Gridmason signs keyless, Sigstore-style — there is no long-lived signing key on
your machine. login establishes the OIDC identity that is the real trust anchor; at
publish time a short-lived certificate is bound to that identity.
gridmason login # interactive browser sign-in
gridmason whoami # print the established identity, or report logged-out
In an interactive terminal, login with no token source opens your browser via the
standard OAuth native-app flow — an authorization code with PKCE and a 127.0.0.1
loopback redirect. login records only public OIDC claims (issuer, subject,
asserted claims, expiry) to an owner-only session.json under the gridmason config
directory. No private key and no token are ever written — signing mints an
ephemeral in-memory keypair at publish time and discards it, and publish re-acquires
a fresh short-lived token when it signs. The session is a record of who you are, not
a credential.
Which OIDC issuer to trust is a per-registry decision — the issuer you authenticate
against becomes the trust anchor the registry allowlists. login defaults to the
Sigstore public-good issuer; override it with --issuer (and the OAuth client with
--client-id) for a different trust domain:
gridmason login --issuer https://oauth2.sigstage.dev/auth # e.g. Sigstore staging
The registry enforces its own issuer allowlist and rejects a token from an
un-allowlisted issuer with 403 issuer_not_allowed, so pick an issuer the target
registry trusts. In CI, skip the browser: --ambient (auto-detected on GitHub Actions
with id-token: write) or --token <jwt> / GRIDMASON_OIDC_TOKEN. A non-interactive
context never opens a browser — it fails fast with an actionable
interactive-unsupported message pointing you to --ambient or --token.
Publish (gridmason publish)
gridmason publish [path] --registry <url> [--signer <sigstore|ephemeral>] [--sigstore <instance>] [--token <jwt>] [--ambient] [--json]
--registry is required — there is no baked-in default registry yet, so point it
at a running Gridmason Registry (you can self-host one from
github.com/gridmason/registry). path is the project directory (default: current
directory). The flow fails closed at every gate:
- Assemble the immutable, content-hashed artifact — each part addressed by the
SHA-256 of its exact bytes (
sha2-256:<hex>), computed with@gridmason/protocolso the digests match the registry's own content addressing. The uploaded set is a fixed convention:manifest.json, theentrymodule, every*.schema.json(schema), every.js/.mjs/.cjsundersrc/other than the entry (chunk), andREADME.mdplusdocs/*.md(doc).node_modules,dist/build,.git,.github,fixtures/,package.json, and*.stories.*/*.test.*/*.spec.*are never uploaded. - Lint-gate with the shared checks module — the identical code the registry's
automated review runs. If any check fails (a manifest-lint failure, a cyclic
requires, or an undeclared capability reach),publishprints the failing findings and refuses to upload — never upload known-bad. - Sign keyless. A short-lived signature is bound to your
loginOIDC identity; no long-lived key is written. - Upload to the registry's Publish API with your OIDC token as the bearer. The
registry content-addresses the parts, structurally validates the signature, enforces
(tag, version)immutability, and runs its automated review. - Poll the review status. On approval the registry countersigns, logs, and
CDN-publishes. On rejection
publishprints the reviewer's findings mapped to the samelintcheck ids you already know (sdk.raw-network,manifest.schema, …), and exits non-zero.
publish exits 0 when published or accepted-and-under-review, 1 on any refusal.
Envelope signing, in plain language. Your signature is the publisher half of a
two-party envelope. publish computes a canonical release document — the map of
{ served path → content hash } of everything you upload — and signs its hash
(releaseHash). That commits your signature to the exact served bytes: the registry
reproduces the same document byte-for-byte and refuses to countersign a signature whose
releaseHash doesn't bind the uploaded content. So the reviewed hash is the runnable
artifact.
Sigstore vs. ephemeral signer — when each is appropriate:
--signer sigstore(the default) is the real identity path. It uses the Sigstore public-good instance: an ephemeral keypair is minted in memory and a Fulcio short-lived certificate is obtained, bound to your OIDC identity — nothing touches disk. Transparency-log anchoring happens at the registry's countersign. This needs network and an allowlisted-issuer token. Use--sigstore stagingto select the staging CA. This is what you use to publish for real.--signer ephemeralis an offline keyless signer that reaches no network: it mints a per-invocation in-memory ECDSA P-256 keypair, signs, and self-issues a leaf certificate in the same profile the protocol verifies, then discards the key. It is a dev / e2e affordance, not a Fulcio identity — its cert chains to nothing a production host pins, so a conforming host that pins real Fulcio roots refuses it. Use it to drive the realpublishbinary deterministically in local/CI testing without Sigstore network; do not use it to publish something a production host must trust.
The review pipeline, from your side
After upload, publish polls the registry's publisher-facing review surface. What you
experience:
- Status polling.
publishpolls until the review reaches a terminal state and reports it. It also falls back gracefully to the upload response's state if the status surface is absent. - Findings speak the
lintvocabulary. Because the registry runs the same checks module you ran locally, a rejection's findings reference the same check ids — so you fix a review failure with the identical id you already know. A reviewer's hand-made judgement is shown as amanual-review finding. - Review tiers set the SLA. Your artifact hits a review lane based on what it
contains: manifest and dependency checks are the automated stage every publish
runs synchronously; SDK-adherence and DOM checks feed the frontend-remote human
review (a ~5-day lane); a capability increase feeds a re-review lane (~3 days).
The
--jsonlint report tells you which lane you're headed for before you publish.
Appeal. If a submission is rejected and you disagree:
gridmason appeal <artifact-id> --registry <url>
<artifact-id> is the id publish printed. appeal routes the submission to a
second reviewer — never the original, because the reviewer-is-not-the-author rule
is the registry's.
What countersigning means for you. On approval the registry applies the second
half of the signature envelope — its countersignature — and issues a
transparency-log inclusion proof. From then on, anyone can independently verify that the
reviewed hash is the runnable artifact: both signatures plus the content hash plus the
log inclusion form one chain that a host (or you, with gridmason verify) checks against
pinned trust roots before loading a release. You don't produce the countersignature
or the log proof — the registry does — but your publisher signature is what they bind to.
Revocation. Trust is revocable after the fact. Because every release is content-hashed, anchored to a signed release document, and recorded in a transparency log, a registry can publish a signed revocation feed that a host checks at load — so a widget later found to be malicious can be pulled from under hosts that already resolved it, without depending on the widget to behave. As an author, the practical consequence is that your published identity and the exact bytes you signed are permanently attributable; publish only what you're willing to stand behind.
Offline distribution (optional)
For air-gapped consumers, gridmason bundle export repackages an already-signed
release into a single self-verifying .gmb file that carries the signed chain and every
servable byte, and gridmason verify runs the full dual-signature + content-hash +
log-inclusion chain against pinned trust roots (online or offline against a .gmb).
bundle export signs nothing — it repackages what publish and the registry already
produced — so a fully green offline verify requires genuine registry signature
material.
Versioning and capability changes
Bump your manifest version (SemVer) for each release; (tag, version) is immutable at
the registry, so you can never overwrite a published version — you publish a new one.
The one thing to plan for is capability history. The registry compares each new version's declared capabilities against the last published version of the same tag:
- Adding a capability (one your last version didn't declare) is an increase,
which re-enters review at the capability re-review lane (a ~3-day SLA). This is by
design — a widget quietly gaining
net:orrecords.writeaccess between versions is exactly what review should re-examine. - Removing a capability is not a re-review trigger — a pure decrease passes.
- A first publish (the tag has never been published) has no baseline to diff, so it passes the capability-diff.
You can see this coming before you publish. gridmason lint --registry <url> runs the
registry-aware capability.diff check: it fetches your tag's last-published
capabilities and warns on each added api[:scope] ("will re-trigger review"). It's
advisory — it does not fail your local gate — but it lets you decide deliberately: if the
increase is intended, expect the slower SLA; if it's an accident (an over-declaration),
trim it. Because the check consults the registry it's fail-safe: an unreachable registry
warns rather than falsely passing. The registry runs the authoritative diff at publish;
lint --registry just makes the likely verdict visible early.
The practical discipline, then, is: declare the minimum capabilities your widget
actually uses (the dev inspector's not yet used markers and lint's findings help
you find over-declarations), and treat any capability addition in a later version as a
deliberate choice that costs a review cycle.