crewship page
Manage Pages — workspace-scoped dashboards built from panels. A page holds no datasource and no credentials; a panel renders the last payload its declared producer pushed. See the Pages guide for what a page is (and is not), the freshness contract, and the permission model these commands enforce./api/v1/pages/...); the CLI supplies the
workspace from your current context the same way it does for
crewship saved-view and crewship routine.
Every subcommand honours the global --format table|json|yaml|ndjson|quiet.
Machine output is the server’s own document passed through unmodified rather
than re-encoded, so a field this build has never heard of is not lost on the
way out. --format quiet prints one key per line — the slug, the link id, the
webhook id, the action id, the version seq — so a listing can be piped into the
command that consumes it. A listing with nothing in it prints nothing under
quiet, rather than the sentence the human format would use: an empty pipe is
empty, and a line saying so would be read as a key. page export is the
exception in the other direction: it prints YAML unless you ask for
--format json.
Subcommands
crewship page list
GET /api/v1/pages. Returns the pages you may see — a page you
have no read grant for and no crew membership on any of its panels for is
not in the list.
list takes no flags of its own. Columns are SLUG NAME PANELS STATE OWNER LAST DATA, where PANELS counts every panel on the page including the
ones sealed to you. --format quiet prints one slug per line, which is the
form the next command in a pipe wants.
crewship page get <slug>
GET /api/v1/pages/{slug}. Prints the page definition and every
panel you may see. A panel whose owning crew you are not in, and for which you
hold no grant, arrives as a sealed placeholder — its id, its width, its tab
and its owning crew’s name, and nothing else — so the page has the same shape
for every reader. In the human output that is one line:
--format json the panel carries "sealed": true. Key on that field being
present rather than on other fields being absent: a serialisation bug and a
permission decision are opposite failures, and only one of them is safe.
crewship page create --file <file>
--file is a Layer-1 page definition — human-authored YAML, apiVersion: crewship/v1, kind: Page:
sla: 30s is YAML sugar; on the wire it travels as sla_seconds (an integer).
Every panel needs sla; a panel without one does not validate — there is no
default that means “never mind” (§4). owner names the crew whose membership
gates who can see the panel; it is the permission anchor, not decoration.
producer names the routine, script, agent or webhook allowed to push data
into it — see page set below.
Three optional keys shape the page rather than its permissions:
span: is 1–12 and defaults to 12, the full width of the grid. A page holds at
most 24 panels and 256 KiB of spec.
Remember that page update --file replaces
the spec: omit any of these on a re-apply and they are deleted.
--owner crew/<slug> — whose page it is
script- or webhook-produced panel — and
ownership of a crew-owned page counts every member of that crew. Hand the page
to crew/ops and anyone in ops can correct a panel by hand, without a grant and
without stopping the producer that normally writes it. (A workspace admin can do
that on any page, owned or not.)
Note the two different owners. A panel’s owner: decides who may see it.
The page’s owner — and a workspace admin — may hand-write a script- or
webhook-produced panel and edit the spec. A panel produced by a routine or
an agent is writable only by that producer, or by a subject holding an
explicit produce grant, no matter who owns the page. The two owners are set in
different places on purpose: the panel’s is part of the document, the page’s is
not.
--owner takes crew/<slug> and nothing else — not ops, not user/<id>.
Your own id is already the default, and the crew has to exist in this workspace
or the create is refused naming it. Ownership is a create-time decision:
page update re-applies a spec and never moves it, because a re-apply that
silently transferred a page would be a permission change nobody asked for. There
is no transfer command at all — ownership moves only when the owning user is
erased from the workspace, and then only to a crew.
page get and page list both show the owner, so what you set is visible
without reading the database.
A panel may also declare the sensor half — a wake: threshold that opens an
issue on a crew when the pushed payload crosses it, and on_failure: for when
it stops arriving at all:
writes: has to name a panel on this page — a gate pointing at a panel
that is not there is refused when the page is saved. It also does not grant
anything: the woken agent still needs produce authority on that panel, which on
the container door means the panel declares producer: agent/<its slug> or a
human granted it produce. A gate whose target declares producer: routine/…
is answered by running that routine, not by the woken agent writing into it.
The CLI sends both verbatim and the server parses them: when is checked
against the panel’s own schema, and a predicate that panel could never satisfy
is refused here rather than accepted and silently never matched. See
Pages → wake gates for the grammar and
page get plus crewship automation list for
seeing what a gate became.
A panel produced by a routine may also declare refresh: — on:wake or
on:panels-changed, and nothing else. It is the event that runs that panel’s
producer, so the analysis is already on the page when a human arrives instead of
starting when they get there:
refresh: whose producer is not a routine/ (it cannot run a
script, call a webhook producer, or dispatch an agent), an on:wake on a page
that declares no gate, and an on:wake on a panel that declares its own gate —
which is a loop. Like a gate, it compiles to a row crewship automation list
shows. See Pages → refresh.
Backed by POST /api/v1/pages.
crewship page update <slug>
PATCH /api/v1/pages/{slug}.
This replaces the spec, it does not merge into it: a panel field left out
of the file is deleted, including
tab:, icon:, public:, actions:,
wake:, on_failure: and refresh:. Panels are reconciled by id, so a panel
that survives the edit keeps its payload ring.
If the document declares a metadata.slug different from the slug you passed,
the CLI refuses before sending: a page’s slug is its address. --owner is
create-only and update never sends it, so a re-apply never moves ownership.
Every save — through this command, the in-app editor, or an agent with a
write grant — becomes a new row in the page’s version history. See
page rollback to undo one.
crewship page delete <slug>
Backed by
DELETE /api/v1/pages/{slug}.
crewship page set
The single write path for panel data. Everything that produces a number,
status, or table on a page — a cron job, a script on a host you control, a
routine step — ends here.
Backed by
PUT /api/v1/pages/{slug}/panels/{id}/data. page set authenticates
with your session or CLI token the way any other crewship command does, and a
producer script always calls the same command wherever it runs.
A producer running inside a crew container does not use this command, and
could not: there is no The
crewship binary in the sandbox image. It pushes to
its sidecar — PUT http://localhost:9119/pages/{page}/{panel}, body = the
payload, ?state=failed when it ran and could not measure — which attaches
the identity from the agent’s own token and forwards to
PUT /api/v1/internal/pages/{page}/data. The credential stays in the sidecar
and never enters the agent process:fd-3 form is deliberate: a bearer token passed as -H is visible in
the container’s process table.Note the panel: rozbor, not sluzby. rozbor declares producer: agent/riley, and the curl above runs inside riley’s container. sluzby
declares producer: script/watch-services.sh, and this door would refuse it
— which is the rule stated immediately below, so an example that pushed there
would be teaching the opposite of the paragraph it sits in.Declare such a panel producer: agent/<slug>, not producer: script/….
A script producer means “a human’s CLI token pushes this”, and the
unattended path admits only the declared routine/ or agent/. A container
writing a script-declared panel is refused with 403 unless a human issued
it a produce grant. See
Pushing from inside a container.producer, run_id,
produced_at) and the freshness clock. A payload the caller cannot prove they
own — the caller is not the panel’s declared producer — is refused with
403, and the refusal itself writes a journal entry and notifies the page
owner (§7.1b rule 3): treat a 403 here as a signal to investigate, not noise
to retry past.
A working producer script
The panel above ismetric.v1
({value, unit?, delta?, target?, sparkline?[]}). A cron job or a step inside
a routine can push it with nothing more than jq and the CLI:
status.v1 panel ({items[{name, state: ok|warning|critical, label}]})
pushes a list rather than a number:
table.v1 panel takes keyed row objects matching its declared
columns, not positional arrays — this is the canonical shape the schema
validates against:
Limits that turn into responses, not silent drops
The 2 s floor is the one a
--loop 1 producer meets first, and it is not a
token bucket: it is enforced inside the write itself, so it holds across
replicas and across all four doors. The CLI prints the scope and the wait —
this panel is being pushed faster than its rate limit allows — retry in 2s —
and exits 6, so a loop can sleep exactly that long rather than guess.
crewship page actions
List the actions a panel declares.
GET /api/v1/pages/{slug}/panels/{id}/actions. The list comes from
the page’s stored spec — the same list the server resolves a click against
— so what you see is exactly what can be dispatched. A panel you may not see
answers 404, the same answer an unknown panel gives, so an action list is
never an existence oracle for somebody else’s crew.
Only call actions reach the server. A link navigates to an internal entity
(issue, run, page, agent) and the renderer builds the address — there is
no URL field anywhere in the schema and there will not be one. A toggle is
client-side panel state. A custom action resolves to a handler compiled into
the web client.
--format quiet prints one action id per line — column 0 is the argument
page action takes, so the two compose:
quiet and exits
0. Do not read the empty output as a failure — a panel with no actions and a
panel you may not see are told apart by the exit code, not by the text.
crewship page action
Dispatch one of a panel’s declared actions.
Backed by
POST /api/v1/pages/{slug}/panels/{id}/actions/{actionId}.
You cannot name a routine here, and that is the design
There is no--routine flag, and the request body carries only the
collected inputs. The server resolves the action id against the page’s stored
spec and dispatches the routine named there. A compromised client, an injected
narrative panel, or an agent cannot choose what runs, because the wire format
has no field for it. The allow-list is not a check the server remembers to
perform — it is the only path that exists.
Inputs are validated server-side against the action’s own declaration. An input
the action did not declare is refused, not passed through; a required input
that is missing is refused before anything runs; a fixed param the page author
set is not yours to override.
It returns when the run is queued, not when it finishes
The answer is202 with a pending id. Nothing waits for the run — a page button
on a ten-minute routine must not hold a connection. Watch the run itself on the
page, in the activity feed, or with crewship routine runs.
Retrying is safe, replaying with different inputs is not
Every dispatch carries anIdempotency-Key, generated locally, one per
invocation. Pass --idempotency-key to pin it so a retry from a shell loop or a
CI step resolves to the original dispatch instead of starting a second run.
Reusing a key with different inputs is refused with 409 rather than
silently deduped onto the first run — a replayed key that quietly returned
somebody else’s result would tell you a click succeeded that never happened.
Who may dispatch
Two halves, and both must hold:- You can see the panel — membership of its owning crew, or a workspace
admin role. A caller who would get a sealed placeholder gets
404, not403: the action does not exist for them, and a403would confirm it exists for someone else. - You hold what the routine requires — MANAGER+, and the routine must be
active. A page button is never a cheaper way to run a routine than the routine’s own surface.
page.action.dispatched, carrying who clicked,
which action, and which routine the server resolved it to.
Declaring an action
Actions are declared in the page document, by a human editing the page. An agent can write a panel’s data; it can never author the button underneath it.- an action
kindoutsidecall | link | toggle | custom, or none at all; - a
callthat names no routine, or names one that does not exist here; - a
linkcarrying anything URL-shaped instead of an entity id; - a
toggletargeting a panel that is not on the page; - two actions on one page sharing an id;
- an input named after a fixed param, or an input collecting a secret;
- actions on a
narrative.v1panel — a panel that renders agent-written prose and can also trigger an action is refused in this release.
crewship page grant <slug>
Grants layer on top of a panel’s crew-based visibility — they widen who may
reach the page, never who may reach a crew’s data (§7.1 rule 3, §7.1b).
Only a human can run this command successfully. An agent holding
write on
the page can rebuild its layout freely but cannot issue a grant — not even
to another agent in its own crew — by any route, including this one (§7.1b
rule 1).page.grant_added), so who granted what to
whom is always answerable later.
Backed by the page’s grants endpoint (GET/PUT/DELETE …/grants).
crewship page revoke <slug>
page.grant_removed.
revoke takes the same three subject flags as grant — --user, --crew,
--agent — plus an optional --level. Omit the level and every level that
subject holds is withdrawn at once.
A grant can also stop working without being revoked. It is live only while
the human who issued it still has the reach they granted from: leave the
workspace, lose ADMIN, or be removed from the page’s owning crew, and every
grant you issued narrows with you. The row stays —
page grants shows it as
inert, with the reason — so you can see what would come back if that person’s
access were restored. Revoking is how you remove a grant; this is how one
stops being borrowed authority nobody is accountable for any more.crewship page grants <slug>
produce) which panels it covers.
crewship page export <slug>
crewship routine export
uses for the marketplace — a “page template” is not a separate object, it is
an exported page spec.
Export carries the page spec only, not panel data. Data is state that
belongs to the install it came from; moving it between workspaces would
mean shipping numbers with no producer behind them.
crewship page import [bundle.yaml]
Import either binds every declared reference and creates the page, or refuses
and names which reference it could not resolve — it does not create a page
full of dead panels pointing at a producer that does not exist locally.
--bind is repeatable, once per reference, and is deliberately not
comma-separated: both halves are slugs, and a slug may plausibly contain a
comma where a flag may simply be repeated. Binding one reference twice is
refused rather than resolved last-wins.
crewship page rollback <slug>
Rolls back the page structure — which panels exist, their layout, their
owners and producers. It never resurrects old panel data: a panel a rollback
brings back renders dimmed, in a “waiting for first data” state, even if rows
for it survive in the payload ring. The ring is cleared for the same reason for
a panel whose schema, producer or owning crew the rollback changed — the footer
would otherwise credit a producer that did not produce the number, or show a
payload to a crew the restored spec never admitted. Showing an old payload as current after a
rollback is exactly the dishonesty the freshness
contract exists to prevent. Up to the
last 50 versions are kept.
crewship page versions <slug>
page rollback. Up to
the last 50 versions are kept. Panel data is not versioned here; a version is
a record of what the page was shaped like, not of what it showed.
crewship page publish <slug>
Publish a page to someone outside the workspace, behind an expiring link.
The URL is printed once. The token is stored as a SHA-256 digest, so
nothing can show it again — not this command, not
page links, not the
database. Lose it and you mint a new one.
Publishing exposes only the panels explicitly marked public, never the whole
page. See Public pages.
crewship page links <slug>
page publish above for why.
crewship page unpublish <slug>
Withdraws one link without touching the others. Several links per page is the
intended shape — revoking the one you sent a supplier does not break the one on
the wall display.
crewship page webhook
Let a producer that cannot run this binary write one panel: a cron on
someone else’s box, a Zapier step, a PLC gateway, a GitHub Action. Anything
that can run the CLI should use page set
instead — one write path, provenance attached server-side, and no credential
to leak.
create prints the URL once and nothing can show it again: the token is
stored as a SHA-256 digest, the same at-rest shape pipeline
webhooks use. Copy it into the sender’s secret store, or mint a
second one — several tokens per panel is the intended shape, so revoking the
PLC’s does not break the GitHub Action’s.
The sender POSTs the panel’s payload as the body, with no envelope around it:
?state=failed — with whatever payload it has. fresh and stale are the
server’s arithmetic and are not a sender’s to claim, and neither is
produced_at: provenance is attached server-side, so a body carrying a
timestamp or a producer name is refused rather than believed.
What the token can and cannot do
A webhook is aproduce grant in a different coat, and it obeys every rule
that grant does.
A token whose panel is deleted from the page spec dies with it, the same way
the panel’s payload ring does.
Flags
webhook list never shows a token value — the column holds a digest, so there
is nothing to show. A revoked token stays in the listing on purpose: “was it
used after we pulled it” is the question an incident asks, and a deleted row
cannot answer it.
Exit codes
Standard CLI exit codes. What a producer script needs to branch on:
Refused locally, before anything is sent (always exit 2): a page document
that does not validate, a
<page>/<panel> address with no slash, an empty or
non-JSON --data, an --owner that is not crew/<slug>, a missing
--file/--panel/--id, a --to that is not positive, an --input that is
not k=v, a malformed or repeated --bind, a bundle whose format is not
crewship-page-bundle/v1, and a --file whose metadata.slug disagrees with
the slug you passed to update.
Everything else is the server’s answer — including an unknown slug and an
unknown --level, both of which are checked there and not here.
Related
- Pages guide — what a page is, the freshness contract, and the full permission model.
crewship routine— the usual producer behind a panel.crewship automation— what a panel’swakegate compiles to.- Internal IPC API — the sidecar trust boundary
page setuses inside a container.