Developer access and upload identity
The production v2 upstream administration handler (#1500) authorizes root through the normal bearer/session/CSRF boundary before reading the host token file or probing the pinned receiver. Read-only serving refuses its mutations. Audited settings repair preserves reserved snapshot bytes and destination identities; it can resume delivery after receiver reissue and local token replacement. Federation and upstream paths reject redirects and unsupported methods before the application mux. Read-only mutation routes remain absent. A canceled settings save or probe can leave its wait behind an active push without changing the credential file or delivery state.
Monthly named history (#1497)
Monthly verification binds the same root, branch, self or name-free projection
that the holder received. It does not grant access to additional people.
The CLI refuses retired tiersealedmanifest1 receipts with COULD NOT CHECK;
old stored seal rows do not supply a fallback proof.
Monthly close runs automatically on writable servers, after a fixed 24-hour UTC grace, starting with the earliest retained input or close within the supported report years (2020 onward). An input-free installation does not freeze an empty prior month. It requires no arming, pins or skips. Read-only serving reads existing closes and does not create them. Root reads all frozen people; node keys read their frozen dated branch and personal keys their holder. Team, Person and Models keep their existing screens and use frozen rows for closed months. A later membership move does not move an earlier month's figures. Close folds a read snapshot outside the write reservation, then rechecks the close marker, erasure epoch, aliases and correction policy before publication. Concurrent changes force a fresh fold. It checks the stored correction policy before construction and refuses unresolved policy or mismatched contribution evidence. Later quality, credit and policy changes do not rewrite frozen rows or their verification proof. Aliases join one person only after each identifier's dated placement is checked. Subjectless captured spend remains visible without supplying census evidence. Last-upload status uses receipt history independently of the selected window.
The month-scoped manifest uses the same holder boundary and snapshot. Name-free cross-branch comparisons remain available to root and node keys; personal keys receive HTTP 403 for both monthly comparison forms. These comparisons expose only k-safe group totals and carry no hidden person hashes or erasure revisions. Peer bands use frozen monthly inputs, exclude the subject and retain coaching's per-peer contribution guard. They require a monthly close; legacy seal snapshots, raw-input coaching digests and credentials do not supply or invalidate a peer population. Late raw uploads and later org edits do not change the frozen group selection.
Server reporting modes are retired. serve takes no aggregation setting;
--aggregation, nonempty TIER_AGGREGATION, and the YAML aggregation key
are rejected. Root keys read everyone, node keys read their dated branch, and
personal keys read their holder. Organization, Team, Person and Models views
remain available, including group-to-person expansion. Name-free monthly
comparisons retain the k floor (default 5, minimum 3), complementary
suppression. Archived raw-manifest verification retains its original pinned
arithmetic; that pin cannot select a server access policy.
Serve no longer requires legacy viewer credentials or an --admin-viewer
identifier at startup. Holder authorization and audit-before-card-read remain
mandatory on real data. The synthetic demo uses a private invented database.
Identity introduced schema version 13; opening the database now upgrades it to schema version 30, including personal-key claim codes (#1425), vendor mappings, consented graft staging, import apply, mapping-audit source redaction and retirement of the obsolete seal and canonical seal-history stores. Back it up before upgrading; older binaries refuse this schema, and rollback requires restoring the backup.
Credentials now carry a random, non-secret credential id. Uploads record which credential sent each event and when the server received it; credentials also record their last authenticated upload and last upload that added data. Nothing derived from the secret is stored in these upload records or shown.
The store supports upload, root, node, personal, metrics, federation,
and export credential kinds. Node keys bind a node and a manage or read
capability; federation keys bind a node and server id. Export keys have an expiry
and can be consumed once. Inventory records include the minter credential id
or a host-cli/break-glass sentinel. Root credentials can be minted on the host
with tierd tokens issue --root.
The credential-kind CLI change added no web routes; the organization web routes
now available are described below. Viewer rows are removed; surviving keys keep their ids and verifiers.
Production uses tier-federation/v2: origin-qualified monthly people/models,
conserved integer cells, strict pages and durable receipts. Federation tokens
remain in 0600 host files and authorize only the pinned admission/route.
HTTPS is required by default, through direct TLS or a configured trusted proxy’s HTTPS assertion. Root may allow plain HTTP in Settings; names, figures and the federation key then travel unencrypted.
No signing keys or signatures are used. Reader authorization remains independent
of the sender's federation credential.
The identity layer is batch 3 of #826. Batch 5 (#1372) adds the coaching card:
GET /api/v1/developer/{id}/coaching and the existing named score route return
it through the same subject authorization and audit boundary. Its builder takes
no viewer parameter. Each real card request commits a log entry before the builder reads
the card snapshot. The person, root and dated branch holders can open it A card with no verified spend shows its empty state.
Batch 4 (#1371) adds the bounded raw-data retention engine. The v0.7.0 release gate requires identity, retention and the card together; all three are now built. See the coaching card retro. TIER is for coaching AI practice, not pay, promotion, performance reviews, discipline or dismissal. Anyone with host/database access can read the database; these controls apply at the API.
Host bootstrap and holder keys
Start tierd serve --db "$HOME/.tier/tier.db" on loopback to initialize the database,
then issue root locally with tierd tokens issue --db "$HOME/.tier/tier.db" --root.
Sign in with that show-once key to create the organization and invite people.
People redeem their own claim codes and create device keys. The host may also
issue a self-bound upload key with tokens issue --upload --subject ID --label laptop.
tokens list shows public inventory; tokens revoke --credential-id ID revokes one key.
Host metrics issuance uses tokens issue --metrics; that key opens only /metrics.
Viewer tokens, viewer-tokens, declarations, bulk declaration-based issuance,
shared read tokens and the legacy admin token are removed. The declaration PUT
route is absent; hierarchy JSON/CSV declaration fields are rejected. There is one
credential table and no auth-off setting. Retired serve flags, YAML keys and
nonempty server token environment settings fail explicitly. Generic client
--api-token options carry an issued key; they do not configure server authority.
Read boundary and view log
Root, a node key whose dated branch contains the person, and that person's
personal key open GET /api/v1/scores/{subject} and
GET /api/v1/developer/{subject}/coaching. Read-only node keys can read these
cards too. Figures are clipped to the reader's dated branch. Viewer declarations
and legacy admin credentials are removed. The person can read their view log;
managers use the card and live branch screen.
With a personal sign-in, My card reads GET /api/v1/me/views to show only the
caller's own coaching_view_audit rows: exactly viewer, label, route and
ts (time). The page shows the viewer as well as the label. The subject comes from that identity; caller-supplied ids and all query
parameters are refused. Root, node and shared tokens cannot use this path. It
writes nothing and works on read-only servers without verified spend. A coaching card with no verified spend shows an empty state. An empty log says “Nobody has viewed your card yet.”
In real-data reporting, administrators are named readers: their named reads,
identity/finance reads and compliance exports are audited too, including exports
through aliases. Root compliance exports are audited in every real deployment. The legacy DELETE /api/v1/developer/{id} writes no audit
row; the root DELETE /api/v1/org/people/{dev} writes a name-free person_erased
audit row.
The audit row is written before the response is built, and commits before
any response is released. Admin discovery, audit and construction share one
transaction/snapshot so concurrent remaps cannot add an unlogged subject. A failed write refuses
the read with 503 and Retry-After. Named reads are limited to 60 per minute per
credential. --view-log-days sets the log's retention (default 90, range 1–365), capped at the named-history retention window on every real writable server; it is pruned at startup, on audit writes and daily. Retention scrubs expired
credentials and retained remap metadata when a
subject has no retained activity or membership.
On real servers, /scores?period=YYYY-MM contains only the full-tree monthly
name-free comparison, under the close's frozen k/complement decision.
Root, node (including Read) and personal keys receive the same decision. Shared, legacy-admin and viewer credentials cannot read it.
An omitted period selects the latest monthly close; no close returns 404.
Mounted monthly readers refuse in read-only serving when they require audit recording.
Only an admitted root key clears authentication failures; other and refused keys stay neutral.
Arbitrary windows and repository/team/work-type filters return 400. No read
creates a close or uses late raw rows to pad its census. The monthly comparison
has no hidden-contributor dominance calculation or declaration-based exception.
/scores/compare?period_a=YYYY-MM&period_b=YYYY-MM returns both monthly
projections in order. Omit both periods for the latest close and its preceding
UTC calendar month; both must exist. /report_manifest?period=YYYY-MM returns
the same name-free monthly proof, with the same latest-close default. The
explicit month=YYYY-MM manifest retains its named holder projection and
filters. All these reads use the bounded in-memory audit admission and fail
closed with 503 if that admission is unavailable. No hidden row digest or erasure
revision is disclosed. The dashboard renders closed monthly group figures and
withholding, with links to live Team, My card and Models.
Per-developer fidelity rows and raw bulk event/outcome/quality exports remain
denied on real servers, including root requests. /metrics remains operator-only because successive scrapes
can reveal individual changes. The synthetic demo keeps its descriptive panels. Its explicit period selectors use the same frozen monthly comparison reader as real servers. Archived raw verification reads are independent of the legacy sealer.
Any future "not on this card" field must be a share, never dollars.
All API routes, including admin reads, have an explicit credential policy; unclassified routes fail closed. Health/version probes remain public. Compliance exports still require the admin credential and are recorded in "Viewed by".
Each developer ships with their own credential
tierd ship --server https://tier.example --repo /path/to/repo \
--developer maya --bound-token @/secure/maya-upload-token
TIER_BOUND_TOKEN is the environment equivalent of --bound-token. An explicit
--api-token takes precedence over that environment fallback. With a bound
credential, --developer must match its subject; omit it to let the server stamp
the subject. The server
stamps the subject on every event, including events omitting developer, and
refuses the entire batch if an event names a different developer. Only personal/upload keys upload on real servers; historical shared/admin evidence stays uncounted. Clients cannot
supply identity provenance: a posted provenance value is ignored.
token_events.provenance is server-written:
| Provenance | Named eligibility |
|---|---|
bound_upload |
Frozen credential subject |
vendor_attributed |
Frozen root-authored mapping subject; counts only while its mapping evidence is valid |
server_watch |
Only the local watcher running under --watch-as |
shared_upload |
Team totals only, including unbound local collectors/proxies |
admin_cost |
/costs; team totals only |
legacy_unverified |
Existing/unclassified rows; team totals only |
Unmapped vendor identifiers and real service accounts stay team-only. A vendor event receives its named subject and mapping ID inside the insert transaction, from a root-authored mapping. Alias/team-map edits never change that binding or promote an earlier event, including on replay. Old alias-stamped vendor rows have no mapping ID and remain uncounted. Alias/team-map edits are audited. Shared uploads cannot change verified rows through an idempotency-key replay.
Root-authored vendor mappings (#1476)
Root alone may create, list, End or Void mappings through
/api/v1/org/vendor-mappings. Browser requests use the root session, exact
same-origin checks and CSRF protection for mutations; scripts may use root
bearer credentials. Node, personal, upload and metrics keys receive 403. Retired static/viewer secrets receive 401. The store independently re-resolves root authority in the transaction.
Create accepts a pairs list of vendor, vendor_user_id, and developer.
The developer must already be a canonical roster person who has not left.
Alias names are refused as mapping targets. Bulk creation accepts 1–200 pairs.
Copilot IDs are decimal GitHub numeric user IDs, never logins; leading zeroes are removed. Cursor IDs are
trimmed, lowercased emails. Empty IDs, the literal "null", and recognizable
Cursor service IDs without @ (sa_… or serviceAccountId…, ignoring case)
are refused; emails such as sa_jones@corp.com remain valid. Normalized IDs
are limited to 320 bytes for either vendor; control characters are refused.
Only one active mapping can exist for each vendor ID.
Both vendor pollers read all mapping partitions once per pass, including ended
and voided rows; day membership follows the effective interval and excludes voids. Copilot resolves numeric
IDs through the current billing seats list; renamed logins follow the same ID,
and reused logins require a mapping for their new ID. Alias-only polling continues
for spend and supplies no counted identity. Cursor uses the mapping normalizer on
usage emails; "null" and service/automation accounts supply no counted identity.
The vendor ID travels only as transient insert input: it is never a token_events
column, serialized event field, log value, error detail or metric label. Only the
opaque vendor_mapping_id binds event evidence; mapping-specific reconciliation
keys contain that opaque mapping ID, never the vendor ID. Copilot installations
need organization GitHub Copilot Business: read in addition to billing access;
accept the App's updated permissions
after upgrading. A missing seat permission disables mapping polling and appears
in the credential probe; alias spend capture continues. When a mapped ID is absent
from a successful seats read, its active or ended, non-voided partition preserves
its stored rows exactly; the organization remainder subtracts that spend and the
day can settle. A mapping with no stored rows and no seat contributes nothing.
A failed seats read keeps days with stored non-voided mapping partitions
incomplete and unsettled for retry.
The server sets effective_from to the next UTC midnight. Usage from before
that date never counts. Mapping creation never promotes an already inserted
event. Each pair creates one org audit row; bulk creation commits all mappings
and audits in one transaction, or none.
End (POST /{id}/end) closes the mapping at server time and keeps evidence
from before that time, including events first received after End whose timestamps
fall inside the mapping interval. Invalid vendor identities leave spend stored
unbound. Void (POST /{id}/void) withdraws all its past
counting too. Void is final, including for an ended mapping; a replacement needs
a new mapping with a new effective date. Neither operation changes stored sealed
report bytes. Both write an org audit row. Erasure hard-deletes the person's
mappings, including ended or voided rows assigned to other people carrying the same
vendor and vendor ID, and scrubs named audit detail matching both vendor and vendor ID to
NULL. Access exports include every mapping with the subject's vendor and vendor
ID, including rows assigned to other people, and its dates; events omit a NULL
vendor_mapping_id to preserve pre-mapping export shapes.
The vendor-mapping migration is schema step 24, followed by graft at step 25, import apply at step 26, mapping-audit source redaction at step 27, and erased-import identifiers at step 28. Upgrading a database from before v24 stamps version 28 and prints a one-time vendor-mapping notice that older binaries cannot reopen it.
The pollers pass transient mapping identity for eligible usage. Root-mapped
vendor events count from effective_from; alias-only events stay uncounted.
Replay inside the lookback moves voided spend to the legacy partition without
rewriting old rows. Erased, unknown partitions leave only their day incomplete;
later days still ingest.
For root, the People page offers Unmapped vendor users, with a count only
after fetching. Clicking Fetch vendor users requests
GET /api/v1/org/vendor-mappings/unmapped; merely opening the page does not
contact either vendor. Copilot reads billing seats keyed by
numeric GitHub user ID; Cursor reads /teams/members using the configured Admin
API key over HTTPS without redirects. Removed members, service accounts and the
literal "null" are excluded. Lists exist only in request memory, with no database,
cache, log, audit or metric retention. The GET is available in read-only serving
and performs no writes.
Suggestions require a normalized login or email equal to a current roster ID or
alias. Ambiguous exact matches receive no suggestion. Root reviews the vendor ID,
current login and person before accepting one or Accept all N. Confirmation
shows the unmapped GET's effective_from date when a list has been fetched;
CSV confirmation before a fetch makes no date estimate. Saved feedback and the
Status cell show the first counted date returned by the server when saving.
Saved users leave the unmapped list using the server's normalized identities.
The bulk route remains atomic: a conflict saves none of the batch, is shown on
its row, and leaves the other rows available for review. Each saved pair has its
own audit row. CSV paste uses columns vendor,vendor_user_id,developer, checks
syntax and roster choices without echoing the file, and uses the same bulk
route (200 pairs max). IDs are sent as raw trimmed text for server validation and
normalization.
An unavailable or unconfigured vendor returns Copilot seat list unavailable —
try again or Cursor seat list unavailable — try again, without its previous
list. Missing Copilot seat permission returns Copilot seat permission not
granted — accept the App's updated permissions. CSV import remains available
when either live fetch fails. Overlapping fetches or fetches less than 60 seconds
apart receive 429 with Retry-After; each vendor has its own 20-second deadline.
End uses a plain confirmation; Void requires typing the vendor ID and
warns that past usage stops counting. The person-erasure dialog includes vendor
mappings.
Retire the shared upload token
Shared ingest credentials, their policy and request tally are removed. The
--ingest-token and --shared-ingest flags, TIER_INGEST_TOKEN and
TIER_SHARED_INGEST environment variables, and http.ingest_token and
http.shared_ingest YAML settings are retired; stale configuration fails
explicitly. The shared-token diagnostic route and Settings control are absent.
Schema 30 drops the old daily tally and saved shared-ingest policy.
Issue a personal upload credential for each developer with tierd tokens issue --db tier.db --subject maya --label laptop --upload. Transfer
it through your normal secret channel and switch each laptop to tierd ship --developer maya --bound-token @/secure/maya-upload-token. Historical
shared_upload provenance remains uncounted for k and named coaching evidence;
issuing keys does not promote old history. Root keys cannot upload.
The /keys dashboard administers org keys through POST /api/v1/org/keys;
My devices manages a personal sign-in's own device keys.
Bind the server's own watcher
serve --watch-repo /path/to/repo --watch-as maya requires maya in the imported
team map. The projects directory (~/.claude/projects) must already exist.
Every watched projects directory/subdirectory and evidence file must be owned
by the serving user, with no group/other write permission. Existing insecure
trees refuse startup; dynamic evidence opens and new project subtrees repeat
the checks. Repository validation is startup-only, restricted to the repository
root and attribution metadata: .git config, HEAD, refs, packed-refs and
worktrees (including linked/common Git directories). Source files and dependency
trees such as node_modules are outside that check. Symlinks in checked paths
are refused. Platforms without the
required descriptor-relative Unix opens refuse this mode.
Checks use fstat on opened descriptors. Directory traversal uses openat with
O_NOFOLLOW; stat, fingerprint and parsing use the same opened file description.
Path replacement after checking cannot substitute the bytes being parsed.
Without --watch-as, local capture continues but its spend remains team-only.
Other local collectors do not inherit the watcher's identity declaration.
Admin web credentials and sign-in (#1417)
The admin scope is available on real servers. GET /api/v1/me
accepts a root, node or personal bearer key, or an in-memory session. Node keys
always bind to the key's node, independently of the holder's membership or
minter. Personal keys follow the holder's current node. Revocation or a holder's
recorded departure refuses the next request. Upload, legacy viewer, federation
and export credentials cannot enter this scope. These keys and sessions do not
open legacy routes. The org administration APIs are described below. The
dashboard shell is available at /signin (#1437, plan P20). Keys, My devices and
Organization pages are available; page bodies awaiting their administration APIs
remain empty and labelled with their page title.
Keys and My devices keep their signed-in navigation and banner when a list cannot load, with “Couldn't load the list. Try again.” on the page; a list 401 restarts sign-in. Key mutations show local copy only: 403/404 say “Your key cannot make this change.”; 429 says “Too many key requests. Try again in {n} seconds.” for a usable Retry-After delay, otherwise “Too many key requests. Try again later.” Duplicate-label conflicts retain the label guidance, while an already-revoked key says “This key was already revoked. Refresh to see the current keys.” and an inactive holder says “This person has left, so their key cannot be reissued.” Server error text is never displayed.
Static admin, shared read and viewer tokens are retired. Root, node and personal sign-ins resolve issued keys from the same credential table. Anonymous protected reads fail closed, including before the first root is issued.
The first root, node or personal key insert, or an admin tree edit (naming a
level; creating, renaming, moving or removing a node; adding a person; marking
a departure), permanently arms a database flag in the same transaction. Nothing
else arms it: not the upgrade migration, not legacy team-map writes (hierarchy
PUT/POST, tierd hierarchy import, the demo baseline), not erasure and not the
retention prune. Protected reads require credentials even on an unarmed loopback install. Once armed, tierd serve refuses a specific
non-loopback --addr with no --trusted-proxy, where sign-in could never
succeed (loopback and wildcard binds still accept loopback sign-in). The only
exemptions are the synthetic demo and offline verify-report / score --outcomes;
admin-web routes are absent for offline recomputation; the demo has read-only
synthetic org and coaching screens. Schema 19 prevents older servers ignoring this flag.
To sign in, send POST /api/v1/session with the key in the Bearer header,
Content-Type: application/json, an exact Origin, and {} or
{"return_to":"/org"}. Unknown fields and trailing JSON are refused.
return_to must start with exactly one slash; backslashes and control characters
are refused. The JSON response contains csrf_token and return_to.
Sign-in bootstraps CSRF using bearer authentication and the Origin check; later
cookie mutations require the server-held token in X-CSRF-Token and the session
cookie. Org administration writes sent by a script with a bearer key and no
cookie need neither (see below).
DELETE /api/v1/session requires both, JSON content type and the same Origin
check, returns 204, and expires the cookie. Bearer-only sign-out is refused.
Cookie-authenticated GET /api/v1/me returns the same CSRF token in its
X-CSRF-Token response header so sign-out works after a full page navigation.
Bearer reads and refusals do not return that header.
When Origin is absent, Sec-Fetch-Site: same-origin also passes; an explicit
mismatched Origin is always refused. Missing both gives 403, including at sign-in.
Sessions are process-local memory only, with 60-minute idle and 12-hour absolute
limits. Requests re-resolve the credential and holder from the database; a restart
signs everyone out. Sessions do not write the database and work on read-only
servers. Cookies use HttpOnly, SameSite=Strict, Path=/ and no Domain.
Secure and __Host- apply whenever the effective origin is HTTPS; on permitted
plain-HTTP loopback, cookies use HttpOnly + SameSite=Strict without Secure.
The name is __Host-tier-session for HTTPS and tier-session for permitted
plain-HTTP loopback, with the nonstandard serving port appended (for example
tier-session-8080). Set, read and clear use the effective origin's cookie
name; the other name is ignored on reads and cleared when setting or clearing
the session cookie. On /api/v1/me and /api/v1/session,
cookie and Authorization together return 400 and clear the cookie, so a stale
cookie cannot block the next sign-in; a cookie alone never signs in (401).
Legacy routes ignore the cookie entirely: it neither authorizes nor refuses a
request there. Any cookie-bearing admin-web request with an explicit Origin
that differs from the effective serving origin is refused 403 before session
lookup or rate accounting. A GET without Origin keeps the fetch-metadata rule:
a GET /api/v1/me carrying the cookie with Sec-Fetch-Site
other than same-origin or none is refused 403 and does not refresh the
session. Sessions are bound to their serving origin. Plain HTTP sign-in
requires both a loopback peer and a loopback host; otherwise HTTPS is required.
Only direct peers in --trusted-proxy (alias --trusted-proxy-cidr) may supply
X-Forwarded-Proto and X-Forwarded-Host. Configure the proxy to replace these
headers; untrusted forwarding and client-supplied XFF never prove HTTPS.
All admin-web responses carry Cache-Control: no-store, CSP and nosniff.
Requests are limited to 60 per minute per credential, shared between bearer
and session use. Unknown keys count against the per-IP failed-auth lockout;
recognized revoked or departed-holder credentials neither increment nor reset
it. A credential keeps at most 16 sessions (a new sign-in evicts its oldest),
within 4096 per process; capacity refusal is 503. A root, node or personal key
sent as a bearer to a legacy route is refused 403, unless the client IP is
locked out, when it gets exactly the legacy answer an unknown token gets.
Node credentials read closed monthly person reports and live named
figures and cards within their dated branch. Card reads use the
dated holder check.
The shell learns sign-in state only from /api/v1/me, using its page allowlist
and the approved role navigation. Root navigation includes Organization, People,
Keys, Federation, Coverage and Settings. Managing node keys see Organization,
People, Keys and Coverage; read-only node keys see Organization, Federation and
Coverage. Root and node keys also see Team and Models. Personal keys see My card,
My devices and their own Models. The acting banner uses the
returned holder, key kind and node path. Unknown roles or failed identity reads
leave navigation and identity hidden. Direct navigation to a page without its
allowlist grant keeps that URL and shows “This page is outside this sign-in.”
with page content hidden, and clears any return target. Keys and My devices also
require their role navigation grant. Import pages check the role before loading:
Company import requires root; Source export requires root or personal. A wrong
role clears the return target and returns to its first allowed navigation page.
This allowlist gates every shell page,
including auxiliary pages; no key kind bypasses it. A 401 identity response opens
sign-in. Before navigating, the shell stores the exact requested path and query,
without a fragment, in the tab’s sessionStorage under tier_admin_return_to,
never in a URL parameter or localStorage. Sign-in sends that path as return_to;
the server validates it again. After a successful identity reload, the shell
opens the server-returned path and clears the stored value. An existing session
opening /signin also resumes the stored path. Both client and server require a
relative path starting with a single /, with no backslashes or control
characters. The client bounds targets to 2048 characters; the server bounds them
to 2048 bytes. Hostile stored or returned paths fall back to the first allowed
page, as does sign-in without a requested page. A failed identity reload, and
every failed sign-in except a 400 conflicting-session refusal, preserve the
target for retry; the 400 clears it. Sign-out clears the target even if the
sign-out request fails. If tab storage is unavailable, sign-in still works using
the default page. There is no MFA step.
Other identity failures leave the form hidden and offer a session-check retry,
including after a successful sign-in POST whose identity reload fails. The
shell pages and /admin.js return 404 in synthetic demo and unsealed recompute
modes, matching the disabled admin web API.
Sign-in sends the form key only as a bearer header on POST /api/v1/session,
with JSON and the browser's same-origin Origin check. The field clears as soon
as the POST starts, including on refusal or network failure. The shell never
reads the HttpOnly cookie or assumes its name; keys and CSRF tokens are never
persisted by its JavaScript. Sign-in failures distinguish rejected keys, 429
lockout with Retry-After seconds, unavailable service, network failure and a
conflicting session request. Sign-out sends the cookie and in-memory CSRF token.
Success or 401/403 clears local shell state and returns to sign-in. Other
sign-out failures preserve the shell and CSRF token and show an explicit
failure so the reader can try signing out again. Organization state clears on
every sign-out attempt, including failures.
All old and new dashboard pages and scripts serve CSP, nosniff and no-store.
Org administration APIs (#1421, #1422, #1423)
Root keys read the whole tree and manage levels, nodes, people and keys. Managing node keys read people and public key metadata within their branch and edit tree nodes strictly below their binding. Read-only node keys read the tree, live Team figures and cards within their dated branch. The administrative people and key inventories refuse them with 403, and writes still require Manage. Sealed shared report audiences retain their existing k floor.
The tree, people and key GET routes accept bearer credentials or the admin
session, and refuse unknown, repeated or undecodable query parameters with 400.
Writes follow D2: a script sends the key as a bearer, with no session cookie,
and needs neither X-CSRF-Token nor Origin, because a browser never attaches
a bearer by itself. A cookie session's write requires its X-CSRF-Token and
P08's exact Origin check. Both kinds require JSON content type; cookie plus
bearer is 400. Legacy admin/read/viewer credentials cannot use these APIs. All
responses are no-store; the shared per-credential limit is 60 requests per
minute. Mutations are absent under --read-only, except claim redemption
which explicitly refuses with 403. Every successful org
administration write, including key issue, revoke and reissue and manager
invitation, permanently arms the D1 admin-web flag in its own transaction.
A write re-resolves the credential verifier, holder lifecycle, membership and
current tree after acquiring the database write lock. orgauth.Authorize makes
the authorization decision in that same transaction. Audit insertion and the D1
sticky activation flag commit with the edit. Revoking a credential or marking
its holder left between the initial request check and the write therefore
prevents the write. Lock contention returns retryable 503.
Creating a person requires developer and node_id. It creates their native
membership and one claim code, returned once in claim.code alongside
claim.expires_at; no list can recover that code. Existing people, alias identifiers or historical credential holders give 409; developer
identifiers are global, so that 409 tells the caller the name exists somewhere
in the organisation. Each credential may create at most 25 people per UTC
day across its whole branch (tierd serve --people-cap-per-day, 1–10000),
so creating child nodes does not multiply it; root's branch is the whole
organisation. The count is persisted across credentials and restarts, and the
append-only creation audits enforce it even after erasure. Placement or origin alone does not count a person: status starts as
not_counted until named evidence with own-credential bound_upload or
server_watch provenance arrives, or a vendor event arrives with valid
root-authored mapping evidence. This status is an inventory
indicator, not the window-specific k census. Shared and legacy uploads never
activate it.
Manager invitation requires an existing active holder within the caller's
branch and a strict-descendant node binding. Its fixed label is manager; a
second active invitation for the same person and node gives 409, while one
person may manage several nodes. Managing node keys may grant manage;
only root may grant read, as required by the authorization core. Invitation
does not create viewer declarations. Its node key reads the people and cards in
that dated branch.
POST /org/keys accepts holder, kind, label and optional capability.
Only upload and node kinds are accepted; personal keys cannot be issued or
reissued through this inventory. Direct personal-key revocation through
/org/keys/{cid}/revoke is root-only. For node keys, the binding is the holder's current stored membership, with
manage or read capability. Other kinds reject capability. Root credentials
remain host-CLI-issued. Personal keys use /me/devices to mint,
revoke or reissue only their own upload keys, whose holders cannot be supplied
in the device request. Device keys cannot sign in.
Key operations use opaque credential IDs. Reissue revokes just that credential
and mints its replacement under one write lock, with key_revoked, key_issued
and key_reissued audit rows; a failure rolls everything back. Revoking or
reissuing an already-revoked credential gives 409. The replacement retains the
label, holder, kind and binding, but starts unused with no last-data timestamp.
Other devices keep working. Duplicate active labels are scoped to holder and
kind, and for node keys also to the binding. Revocation does not require an
active holder: Mark-as-left revokes a leaver's keys at the click, and any key
of theirs still live stays revocable by root and by managers above their last
placement; a legacy key whose holder was never placed is revocable by root.
Only root revokes a personal key directly; a manager ends one only by marking
the person left. Reissue requires an active holder and otherwise gives 409.
A key listed in the caller's inventory that it may not change (such as a node
key at its own binding) gives 403; a key outside the inventory gives 404. On
/me/devices/{id}, another holder's key, a non-device key and an unknown id all
give the same 404.
People lists show created_at and left_on as UTC dates, never exact times.
Ancestor key inventories show dates and Fresh/Stale/Silent/Never chips; the
personal device list additionally shows exact last_data_at (or null). Fresh
means under 24 hours, Stale means 1–7 days, Silent means over 7 days, Never means
no data. used means persisted credential use or data receipt, not signing in:
sign-in remains free of database writes. Key lists contain no secrets, verifier
hashes, prefixes or token lengths.
Level-0 node renaming returns 409 until billing membership and actual-spend follow-through is implemented. Moving a subtree with people under another level-0 org returns 409 and leaves placement, dated membership and billing membership unchanged. Empty subtrees can move between orgs. Moving a node to its current parent changes nothing and appends no audit row. Nonempty node removal returns 409, including nodes with people, children, active node keys, unrevoked keys of a leaver placed there or a federated-server binding. Root's full key inventory retains revoked keys after their placement node is removed.
When an existing native person predates the legacy upload membership projection, the first authorized device issue fills only that missing projection and starts membership at the server clock. Existing membership is preserved; the device issue and audit enclose the projection update atomically.
For placements below the three legacy levels, the compatibility team is the person's node, division is its parent and org is the level-0 ancestor. Creation, rename and move keep that projection and the dated membership in the same transaction. Existing level-0–2 skipped-level mappings retain their behavior.
Personal-key claim codes (#1425, ruling E with 6a)
Only the person redeeming a code receives the working personal key. Both root
and node(manage) administrators hand out a claim code, which opens no card,
session or upload route. The credential table never stores a claim as a key.
The authorization core refuses direct personal-key minting by every principal.
The holder may replace their own existing personal key with
POST /api/v1/me/key/rotate; its response shows the replacement once. Save it
in a password manager. Old personal keys and their sessions then stop working;
device keys keep working.
Add-person returns {developer,claim:{code,expires_at}}. Give the person this
one-time code securely. It has 256 random bits, is stored only as a SHA-256
hash, is bound to their developer and node, and expires after 72 hours. The
person opens /claim on this server, enters the code and clicks Make my key.
No sign-in is needed; the page also stays available to signed-in visitors.
It sends {code} to POST /api/v1/claim, with JSON content type and the
server's Origin (or same-origin fetch metadata), without a bearer or a TIER
admin-session cookie (the page omits cookies; the API ignores unrelated cookies,
such as an access proxy's).
Redemption requires HTTPS off loopback, as sign-in does, and returns
{credential_id,secret} once. The page clears the code, shows the personal key
once with Copy and Hide buttons, and links to /signin. Save the key in a
password manager: it cannot be shown again. The page never puts the key in a URL,
browser storage or a log. Consumption, personal-key
creation, audit and notice commit in one transaction. Read-only servers refuse
redemption. Unknown, expired, cancelled and already-used codes count toward
the shared per-IP failed-auth lockout. An expired code gets its own 410
refusal. The page tells the person to ask their administrator for a new code
when a code is wrong or expired. For a used code, it presents the API's
“already claimed at — ask your admin” as the ruled r219 copy:
“already claimed at — ask your TIER owner (root) for a recovery code”;
root can see its refusal in /org/audit, recorded at most once per code per
hour. Redemption is audited with the claim-code actor sentinel, so root can
tell it from host CLI use.
POST /org/people/{dev}/claim cancels earlier unused codes and issues a new
one. A managing node may do this only while the subject has no active personal
key. Root issues a recovery claim at any time for an active member. Issuing it
keeps the old key working; redeeming it revokes every personal key of the
holder, including one created by rotation, and every device key any of them
minted, in the same transaction. Holder rotation cancels only outstanding
onboarding codes; a pending recovery code survives it, so a stolen key cannot
rotate itself out of recovery. A move away from the code's bound node makes it
unusable; issue a new code for the current membership.
Person move (POST /api/v1/org/people/{dev}/move) accepts {"node_id":"..."}
from root or a node(manage) principal that contains both the person's current
node and the target node. Authorization is checked inside the write transaction.
A hidden or unknown person returns 404; a visible person with a destination
outside the manager's branch returns 403. A left person returns 409, a same-node
move returns 400, and unknown, removed or federated destinations are refused.
At one server click instant, the person's and aliases' dated memberships close
and new memberships open at the destination. Same-org moves leave billing unchanged.
A cross-org move follows the hierarchy import (UpsertHierarchy) rule:
non-destination open seats close in the click's UTC month, and a destination seat
opens unless the person has an unsuperseded deliberate end there. When a destination
seat opens, the person holds a seat in both orgs for the click month, as the billing
ledger records months. Earlier work
stays attributed to the old node. Personal and device keys stay with the person;
Effective access is the union of each holder’s active keys. A person or node
move that changes current access sends each affected subject one notice listing
the holders granted and removed, in the same transaction. A non-root manager's move that would give that same manager new access
returns 409. One person_moved audit records the actor's
opaque credential_id and the old and new node IDs. No date parameter is accepted.
Mark-as-left (POST /org/people/{dev}/leave) cancels unused codes, revokes
every key the person holds,
and closes both membership ledgers (for every alias of the person) at the
server click time. A managing node must be allowed to revoke each of those
keys: a root key, or a node key at or above its binding, refuses the whole
leave with 403 and nothing is written. A leave that would revoke the last live
root key is refused with 409. The stated leave date is stored on the person as
stated_leave_date and in the audit; it never backdates membership or figures.
The response is {revoked:[credential_id],membership_closed_at}.
The people inventory exposes state, never codes: Invited · expires ,
Claimed at , Expired, or Left. A person with no claim history is
Uninvited; a cancelled code can show Cancelled. Lists derive Expired without
writing; claim_expired is recorded once, by the next write that touches the
code (redemption, reissue or leave). claim_issued, key_claimed, claim_expired and
claim_cancelled each create an org audit row and a subject notice, available
through /me/audit and /me/notices after sign-in. Personal and node principals read every audit action about themselves through
/me/audit, with named detail and other actors’ names redacted, and only their
own notices through /me/notices. Managing nodes'
subtree audit remains restricted to claim lifecycle events; other subtree audit
actions are root-only. D10 separates names from
permanent action, time, actor credential id, node ids and opaque claim id.
/me/devices includes manager/root-added devices with added_by (the manager's
opaque credential id) and added_by_label (the minting manager's developer id).
The person may revoke these devices with their own personal key.
This amends plan §2.2's personal “Minted by” row to the person redeeming a claim. D4's “bulk issue personal keys” becomes bulk issue claim codes; the planned bulk workflow must use this same claim path. Viewer issuance and declarations are removed; device upload issuance uses holder keys and the claim lifecycle above.
The claim page refuses to send a code over plain HTTP outside loopback. Open the server over HTTPS to redeem it.
The claim rollout stamps database schema version 20. Older binaries refuse this database: they lack claim/notices erasure and allow direct personal-key issuance. Keep backups suitable for rollback; reopening the upgraded database with an older server is refused. The first start on an older database prints a one-time upgrade notice.
Real-person census and last uploads (#1431)
The k floor counts fixed named people once per reporting window, regardless of
how many devices they own. Evidence must be bound_upload from the person's own
local credential, server_watch (--watch-as), or vendor_attributed
with valid root-authored insert-time mapping evidence. Roster-login matches
and legacy aliases alone never count. Census and coaching use the same evidence predicate,
including the own-credential check. Shared/legacy tokens,
manual costs, roster-only people, imported-unbound people and federated people
count zero. A later alias edit cannot reassign frozen evidence. Shared-token
teams may become withheld after upgrading; distribute claim codes and have each
person create their own device keys. A node that controls its subtree's keys can
still invent people with uploads. Legacy seals recorded census rule d4-c2-v2
and seal fold rule 4. Those producers are removed; current closes freeze the
monthly census and contribution evidence. Retained legacy tables are not read
by current reports, and old sealed receipts refuse verification with exit 2.
Credential-free bound_upload
evidence written by dev builds between 2026-10-24 02:42Z and 22:16Z counts zero;
no release writes that state.
Root and managing nodes read per-person last_upload_date and freshness on
GET /api/v1/org/people and per-key last_data_date and freshness on
GET /api/v1/org/keys. These are UTC dates. A personal key reads its own exact
last_upload_at on GET /api/v1/me and device last_data_at on
GET /api/v1/me/devices. Reads recheck org authorization in the data snapshot.
Shared upload and read tokens receive neither date nor freshness fields.
Fresh is less than 24 hours since the latest bound server receipt. Stale is 24 hours through exactly 7 days; Silent is more than 7 days. Never means there is no bound receipt, including legacy rows with NULL credential ID. Server-watch can count for k but does not claim an upload receipt. People, device and self views retain Silent and the last upload date or time after retention using persisted upload-credential receipt metadata older than the retention watermark.
GET /api/v1/org/nodes/{id}/reporting returns reporting_7d and people_total
for active people in the authorized subtree, with the 7-day boundary included.
Counts are omitted when qualifying non-bot evidence within that seven-day
window cannot meet k; eligibility and reporting use the same precise receipt-time
comparison, including fractional seconds. The evidence field is always
live_7d_bound_receipts_and_watch: these current receipt/watch diagnostics are
not reconstructed from closed monthly rows. Tree complementary
suppression gate the full forest without dominance before selecting a subtree, preventing
parent-minus-children disclosure of a sub-k sibling. All reporting-summary keys retain the shared floor of at least 3. The root-only withheld message
names the cause; other principals see “Withheld — ask your admin”. Outside-subtree
nodes return 404, including for read-only node keys. Startup and local
tierd doctor --db use the report census's roster, bot and remap eligibility
when naming the no-bound-evidence cause, without identifying people.
Erasure and persisted settings (#1440)
The organization chooses names_visibility in /settings: on (default), top_admin_only, or off. Root alone can save it through PUT /api/v1/org/settings, and the existing audit records old and new values. Names off refuses root as well as node keys for other people's cards; top admin only permits root alone. Personal keys retain their own card and figures. Monthly capture is independent of the choice and changing it never rewrites a closed month.
When names are hidden, reports, CSV and manifests use the same whole-company k decision as name-free comparisons. No filtered branch/model figures or row digests accompany it. Team, Person and Models show that decision for an explicitly requested whole closed month or the latest closed month, labelled as such; they do not recompute a live anonymous window. The reading server applies its choice to all origins. Opening a full card on another server checks that server's choice. orgauth.MayExpandNames is the shared policy seam for received-row readers. Root data-subject exports remain available and audited under every names choice. Finance read-back refuses when names are off, before writing its view audit; rosters, identity administration and explicit data transport retain their existing authority.
Viewer declarations and declaration notices are removed. Named reads use the person, and root and dated branch holders when the organization's names choice permits them, including read-only node keys. Card opens commit the view log before construction. Person and node moves notify subjects when effective holder access changes; all active keys of each holder are unioned. Notice failure or a non-root self-grant rolls back the whole move.
Only root may DELETE /api/v1/org/people/{dev} with {"confirm":"<dev>"}.
Authorization, erasure and the name-free person_erased audit share one transaction.
Managers may mark people as left. The developer DELETE route requires a root key.
Root may GET and PUT /api/v1/org/settings. PUT accepts
retention_days (integer, 60–365), names_visibility (on, top_admin_only, off), and federation_require_https (boolean, default true). GET returns settings, keyed by setting name,
with string value, source and pinned metadata. An explicit flag, environment
variable or config key pins the control, even when explicitly set to its default;
a pinned-key save returns 409 with its host change instruction. Otherwise saved
DB values win, then the 90-day default. The shared resolver applies to
serve. The environment pin is TIER_DEVELOPER_RETENTION_DAYS; host flags/config retain
their existing names. PUT validates the whole resulting set; k is at least 3. Saves are audited with opaque credential
IDs and old/new values and apply without restart. Read-only serve mounts no PUT.
Lowering retention first returns 200 with preview{cutoff,rows}, a
confirmation_token, and unchanged settings. The preview executes the same scheduled prune inside a rolled-back savepoint,
returning exact current deletions by table, rows_pruned, rollup_rows, and
audit_rows_scrubbed. It changes no stored data or watermark. Counts describe
the preview time; data arriving before the next scheduled prune can change them. Repeat the identical PUT with the
token within ten minutes using the same root credential. A different change,
changed settings, another credential, an expired token or a consumed token is
refused with 409. The new window applies at the next retention prune; the
confirmation response says the fold is pending. Widening never restores history.
The v23 settings migration recorded resolved settings on legacy seals. Those
producers, their preview and host status are removed. Current monthly closes
freeze the source and scoring provenance required by monthly verification;
old tiersealedmanifest1 receipts refuse with exit 2. Read-only serving runs no
close worker, and shutdown joins monthly close before the final read-audit flush.
Graft credentials and explicit identity mappings
Schema v25 adds consented graft staging and mapping erasure after the v23 settings and v24 vendor-mapping migrations.
Whoever holds a person's personal key can read or change that person's graft
consent through /api/v1/me/consent. Consent is only as strong as claim-code
custody: root or a managing node that obtains and redeems the person's onboarding
claim code can hold that personal key and consent with it. Root and node keys
cannot call this route directly. The body has no subject field. Every consent
change is audited and writes a consent_changed notice for the subject.
Withdrawal affects future snapshots; it cannot recall an earlier snapshot.
The /import-source dashboard page reads and changes only this signed-in
personal holder's consent. Giving requires a destination label; withdrawal
sends only {"move_named":false}. Root sees export issuance instead of consent
controls. /import is root-only; it offers active native destinations and
requires a saved explicit map/new decision and target for every clash before
apply is enabled. The page renders the API's stored apply summary, and never
persists the preview, mapping choices or secret in browser storage.
The dashboard fixes map to the matched existing person shown in the preview.
The new choice alone allows an edited target. Import pages stay outside the
main navigation; root reaches Company import from Organization's Import a team
link. Both import pages still require their /api/v1/me grant and role check.
The dashboard labels the retained-row total and class table as what the source
reports in preview and completion, hides empty reports, and allows import.
Consent responses, notices, and bundle_pulled records state: "Some rows stay on
the source because bundle v1 exports only accepted events and outcomes."
Bundle v1 pins jsonl, codex-rollout, opencode, and muse at realtime;
refused event and outcome rows remain on the source. Its digest-covered,
source-claimed left_behind report aggregates count and net cost by fixed class
for active, non-departed consenters only, with no per-person split. Preview and
the stored apply receipt carry it and a separate left_behind_rows counter.
An absent report is refused. Name-free totals exclude all active consenters;
the retained-row report accounts for their retained rows without changing the
source ledger. See the contract.
A root credential mints a shown-once export credential valid for one hour. That
credential opens only the HTTPS bundle GET endpoint, once, with bearer authorization.
HEAD and other methods return 405 before rate limiting or consumption. Sending a
recognized export credential over plain HTTP consumes it before returning 403;
a later HTTPS pull is refused. Revoking the issuing root also prevents pulls.
It cannot sign in, open cards, or read organization data. Named snapshot
construction, consumption, and source bundle_pulled audits share one transaction;
a failed audit releases no snapshot and does not consume the credential.
Name-free totals are advisory residuals of validated frozen monthly data, computed
in an earlier read snapshot. Consent, the erasure epoch and monthly close candidates
and revisions are rechecked before release; a change returns a retryable
503 without consuming the credential. Unknown guesses
count toward IP lockout; recognized refusals are limiter-neutral. The bundle
endpoint rejects cookies and trusts HTTPS forwarding only from configured proxies.
IP lockout returns 429 with Retry-After, including for a valid export key.
Only root can pull a company-side preview or save mappings. The destination must be an active native node, checked before fetching and again before staging. The company server fetches from whatever HTTPS host root names in the source URL and sends the export key there. Choosing the host is a root-only trust decision; internal, loopback and link-local addresses are allowed. The export credential is used only for the outbound request; it is never stored in the batch, mapping, audit, or session. Authentication and destination scope are resolved again in the staging write transaction. An equal source/local developer ID is a clash, even if both strings appear to name the same person. An explicit mapping is required before apply can proceed. Preview and mapping changes are audited; preview does not bind or create an imported person.
Data-subject export, erasure, and retention resolve held staged people and their
batch-person metadata through the explicit mapping before matching local IDs.
Unmapped people match the source ID. A colliding source name cannot export the
held mapping or invalidate another local person's mapped stage. Once staging is
absent, source-named subjects can export source metadata with the mapping's target
developer omitted, and erase that metadata. Erasing a source-only name in an
id_mapped audit row nulls only source_developer, preserving the target's row.
Only root can POST /api/v1/org/imports/{batch}/apply. Imports are explicitly
exempt from --people-cap-per-day because this route is root-only and audited.
The request has no fields;
Origin, session CSRF, rate limits, and no-store match the other admin mutations.
Apply resolves authorization and rechecks readiness in the same write transaction
that creates people, imports usage, consumes staging, and audits the result.
Refused or failed applies leave all of that state unchanged. Applied receipts
support retries after staging has been cleared. A matching bundle cannot be
redirected to another destination or used to recreate erased named history.
A new imported person is created at the chosen native destination with origin
imported and status imported_unbound. Apply issues no keys, claims, or viewer
declarations. The ordinary root-managed onboarding and device-key flow is needed
for an own-credential local upload to count them toward k. Source events never
count as local evidence, and watcher evidence alone cannot bind an imported
person. Imported spend remains Unknown until the subject has qualifying local
evidence, then joins that subject in live identity totals and future seal
populations. Imported rows never supply census evidence; existing sealed
populations and apply receipts remain frozen.
Event deduplication resolves developer_alias to the canonical destination
person and uses that person together with the transport identity or content
fingerprint. Identical events belonging to different people keep separate rows
and spend. Within one canonical person, equal content fingerprints collapse
even source rows with distinct message IDs and keys. When no same-person
duplicate exists, a transport key or UID owned by another person refuses the
transaction. Preview uses source identifiers before mapping; the dashboard
labels this count "before mapping". Changing a mapping can change the advisory
count. If apply refuses a transport identity already owned by a different
person, save an explicit mapping to the previous destination person through
PUT /api/v1/org/imports/{batch}/mappings, then apply again. Erased people remain
excluded, and name-free Other totals carry no added person names.
A mapping to an existing person preserves that person's local placement, keys. Viewer declarations are removed. Non-consenting source people have no person row; their safe source totals are retained as name-free Other under the destination. Departed consenters also contribute to these protected closed-month totals; their names and named histories remain excluded.
Allowed federated servers
A federated server reports closed monthly people, models and conserved totals to this server on a schedule. Root
and managing node keys may allow a server below a native group in their own
branch. It occupies one place within the six-level limit. Accepted reports
carry origin-qualified monthly people, group paths, integer measures,
conservation controls and frozen census to the receiving server.
Root can read v2 monthly group names and named figures through /api/v1/org/report,
with corresponding sums through /api/v1/org/totals;
node credentials can read accepted server detail only within their authorized
branch. Committed v2 monthly people are readable under the dated admission mount;
raw events and full coaching evidence remain on the owning server.
Placement, token revocation and token reissue are audited. Managers can read
these lifecycle audit rows in their branch through /api/v1/org/audit.
Allow and reissue responses show a random federation token once. Inventories
return only its opaque credential id, issue/revocation times and status. Reissue
revokes the old token and creates its replacement in one transaction. Revocation
stops future pushes and preserves accepted periods and the server's place. A
sibling-branch manager cannot revoke that server (404).
Federation credentials have no holder and cannot open admin pages or read
reports; their only read is their own /api/v1/federation/status.
Federation v2 is active in production. Its page receipts, owning-route tombstones and permanent restore blocks use origin-qualified identities throughout relay chains; equal logins on different origins remain separate people.
POST /api/v1/federation/push requires the organization’s HTTPS policy and one active federation bearer. Each strict v2 page belongs to an explicitly approved origin and pinned
route under the receiver-owned mount. Staging is invisible to readers. Exact
receipts survive later commits; a changed retry is refused. Status exposes only
that admission, and abort consumes incomplete staging without undoing a commit.
Re-parenting a bound mount or its ancestor is refused. Owning tombstones travel
on the authenticated pinned route. Root central erasure is local authority and
never fabricates a descendant tombstone.
Failed guesses count toward the existing IP lockout. Recognized refusals remain neutral. The refusal log keeps time, source address class, reason and a server id only when a federation token identified it. No presented credential, verifier, prefix, length or unauthenticated claimed server identity enters the log. Refusals buffer in bounded memory and flush every 60 seconds and after HTTP shutdown. A crash loses unflushed entries (normally up to 60 seconds, longer during a flush failure); failed flushes retain the bounded batch for retry. Detail is capped at 10,000 rows and 90 days; overflow becomes hourly reason counts, also retained for 90 days. The worker prunes both durable stores even when no new refusals are buffered. Root can read source classes. Top-level node readers can read refusals without source classes and see identified servers only in their branch. Unidentified attempts and hourly reason counts have no branch attribution. Read-only serving mounts no push or server-management mutation and never buffers or flushes refusal writes.
People, Settings and first-run pages (#1449)
/people uses the reader's subtree inventory. Native groups in that subtree are
available for person placement and moves, excluding the person's current node
from move choices; manager invitations require a strict descendant of the current
sign-in's binding. Move is offered before authorization
is rechecked by the API, including the 409 refusal for a non-root self-grant.
Root alone sees recovery and typed erasure controls. Existing
claim-reuse audit rows flag the affected person with a recovery action. Claim
codes and invited-manager node keys use the same ephemeral result and cleanup
rules as Keys and My devices; neither enters browser storage or navigation.
Open card links use /card for the holder and /person?developer={dev} for a
branch reader. Leave dates default to the UTC day, which also bounds the input and submit
validation.
/settings is root-only. Pinned settings show their source and host change
instruction with disabled controls. Unpinned saves take effect immediately;
lowering retention first displays the exact server preview and then submits the
unchanged preview request plus the confirmation token. No browser save sends
an Authorization bearer. The cookie session's server-held CSRF token accompanies
every mutation. Root can open /first-run for a link to live Team figures. Card notices are read only for the signed-in
subject, with declaration snapshots and both directions of effective-access changes.
Sealed subtree reports (#1441)
Root, node and personal credentials can read GET /api/v1/org/report and
GET /api/v1/org/report.csv for exactly one period=YYYY-MM. These routes read
frozen month_person rows. When the organization's names choice permits it, root sees local people across branches, node keys
including Read see their dated branch, and personal keys see only themselves.
The JSON is a monthly manifest with attached people and model results. People
sum to branch spend and merged points, including nameless remainders after
erasure or retention. Ratios use pooled inputs and too little evidence yields
“no score”. Node filters narrow the dated branch; a moved child never widens
that key's historical access. A monthly manifest pins both branch and selected
node when they differ. Shared, upload, federation, metrics and legacy-admin
credentials cannot open these routes. Cookie plus bearer is refused.
For comparison, use
GET /api/v1/org/report/compare?period=YYYY-MM&compare=YYYY-MM, repeating
compare for up to twelve additional months. This route reads month_person
and returns name-free monthly manifests with the same k-safe full-tree decision
for root and node keys. Personal and shared credentials cannot open it. Node and
window filters are refused. Monthly manifests, reports and CSV share an audited
projection. Missing closes return 404; they are never rebuilt from raw activity.
Every monthly report projection requires an available audit writer and enters the bounded in-memory audit buffer before
figures are materialized. Two failed minute flushes or insufficient buffer
capacity close reads with 503 until recovery. Request reads write no database rows.
Report, comparison and CSV routes are absent under --read-only.
Native costs and points follow frozen dated placement. Name-free comparisons count each qualified person once and retain complementary suppression checks. They do not publish hidden row hashes or census counts. Named branch reports do not apply the comparison k floor.
Named monthly reports/manifests and people CSV apply the same dated scope to frozen paid cells, including paid-only people and nameless remainders. Personal keys receive only their own paid cells under identities and alias links frozen at that close; later aliases cannot retarget a closed report or card. Billing absence, recorded zero and credits remain distinct; model filters receive no monthly bill allocation. Comparison JSON omits hidden IDs, fold membership and census counts. Frozen horizon, partial-input and pricing provenance disclosures accompany each month.
Root alone can read /api/v1/org/report/status for monthly close metadata,
newest first. It exposes month, close time, rule, source-registration status and
the recorded import/restart loss flag, with no person payloads or digests.
Revocation is checked in the same read snapshot. Audit admission is required
before responding; an unavailable audit returns 503. Scores and time-to-merge
also read monthly rows. The root-only /api/v1/report_manifest?audience=diagnostic
audience selects the latest close by default or accepts month=YYYY-MM, refuses
node/model filters, and returns frozen diagnostic evidence without named people.
Audit admission precedes projection; an unavailable writer, read-only serving or
exhausted admission returns 503 with Retry-After. tier_identity_unjoined uses
the latest local close at startup and after closing, with NaN for unavailable
evidence; comparison reads do not update it.
Root/node credentials can read /api/v1/org/totals and .csv for exactly one
closed month. They sum authorized local and committed received person/model
rows and nameless remainders. Both the key's branch and a selected descendant
must occur, in order, on the same frozen effective path. Remote paths use an
immutable admission prefix; current moves cannot rewrite it. Personal keys
remain local: a matching remote login grants no access. When names are hidden, totals return the same whole-company monthly group
manifest or group CSV as the report, with the comparison k floor. Permitted named branch totals
have no comparison k floor; cross-branch name-free comparisons use one complete
k/complement decision and distinct origin/person identities.
The selected month never rolls backward. Missing origins are explicit, an
incomplete figure is labelled reported subtotal, and offline origins retain
available history with the stamp reported through YYYY-MM-DD. Remote Person
shows monthly numbers, ratios and model associations. Its full-card link uses
only the approved HTTPS hostname and month/login; opening it requires the
reader's own child sign-in. Monthly manifests distinguish received rows from
local evidence refolding and do not claim to inspect child raw captures.
Production v2 and its sender workers are active.
Totals use the subtree in-memory audit tally, fail closed before report bytes and are absent under read-only serving. Shared tokens cannot open the monthly report or totals routes. Node totals name their selected subtree.
Root/node credentials can read /api/v1/org/coverage for live status or a
month=YYYY-MM report, with format=csv for export. Authorization applies to
the selected subtree on real servers; personal, device, federation
and legacy shared/break-glass tokens are refused. A node cannot discover an
outside group's status, name or server schedule. Coverage shows no individual
freshness or contributor counts. Sub-k and complementary-withheld group rows show status only,
without reasons or dates. Shown groups may show setup/stopped reasons and
aggregate UTC report, period-end and next-expected dates. Monthly details also
require a shown frozen month_person comparison decision, using at least its
frozen k; no-receipt months use that gate for setup/stopped reasons. Legacy seals
cannot substitute for a missing close. Receipt gating uses the shared k/
complement rule without tree dominance. Repeated uploads and multiple model
rows cannot add people to its floor. local_evidence labels the local status
and dates as live bound-upload receipt diagnostics in JSON/CSV and the Coverage
page. Late receipts and retention can change these diagnostics, while monthly
figures stay frozen.
The same setup/no-reports/stopped definition applies to live and monthly views.
No group publishes contributor counts, including in closed months. Server rows describe their own
schedule, accepted history and setup/stopped reason. The N-of-M summary counts
native groups separately from servers. Server Current uses the end boundary of
the latest month whose end plus the 24-hour monthly-close grace has passed, ahead of the
Silent push-age rule. Unended month= requests return 400. Coverage reuses the
D9 in-memory tally and fail-closed behavior, is absent under --read-only, and
never writes to the database per request.
The Organization page (/org, P21) uses the same cookie session, role navigation
and in-memory CSRF token as Keys and My devices. Root and node readers see only
their returned subtree. Managing node keys edit descendants and create children
within that branch; root also names up to six levels. Federated places remain
read only. Moves need authority over both parents. Tree mutations send JSON and
session CSRF; 403/404 and unknown 409 refusals (including self-grant refusals) show
“Your key cannot make this change.” Known duplicate-name, cycle and depth
conflicts use cause-specific messages; 400 asks readers to check the group name,
level and parent. Report-read 403 uses “Your key cannot read this report.” Both
reads and edits use the rate-limit countdown and disable editing during
Retry-After, without displaying raw response error text.
Child upstream administration (P28 + P29)
Only root can read or save /api/v1/org/upstream or invoke its /test action.
Node managers, personal keys, legacy tokens and federation keys cannot configure
an upstream or test its credential. Browser writes use the existing session,
CSRF and Origin checks. A save re-resolves root while holding the write lock and
records upstream_configured; refused calls do not touch the credential file.
The host configures the central origin, issued server ID and absolute token
file through the federation flags, environment or YAML block described in
API compatibility.
The central address is display-only. Root may paste a replacement bearer into
the 0600 host file when its directory permits atomic replacement. The new file
is staged first and replaces the active token only after settings and audit
commit. Database failure preserves the old token; rename failure is reported
after commit. Temporary files are removed on failure. GET and PUT
responses expose only token_stored, never a secret or its file path. Test
connection reads only central status and leaves push sequence and history intact.
Read-only serving offers GET only and starts no background pusher.
Federation and coverage pages (P31 + P33)
Federation and Coverage use the existing root/node session and subtree APIs.
Only root sees the Child server settings link and receives upstream in its
page allowlist. Managing node keys can allow and rotate tokens within their
branch; read-only node keys receive no mutation controls. Placement choices come
from the authorized native tree. Refused attempts are read only by root or a
top-level node reader; other readers see an empty Refused attempts table.
Company totals are shown and fetched only for root and top-level nodes. Managing
node navigation follows the mock and omits Federation; the page remains
authorized by the server allowlist.
The Federation audit section is restricted to root and managing nodes.
Shown-once federation tokens are cleared on Hide, same-tab navigation, pagehide, and session clearing, and are never stored in browser persistence or URLs. Child settings never returns a stored token or offers an editable central address. Its paste clears after submission and writes through the existing root-only, CSRF-protected upstream API. Coverage and totals download links use the same cookie session and API authorization as their displayed rows. Downloads keep the session shell visible. A host with no upstream configuration shows its setup state with Save and Test connection disabled; connection-test results distinguish upstream failures from session failures.
v0.7.0 organizations: who can do what
Use these v0.7.0 scope rules alongside the earlier CLI recipes. A node is a group in the organization tree; its subtree is that group and all groups below it. Legacy credentials have no route authority.
| Credential | Where to find its authority and limits |
|---|---|
| Root | Host-only root issuance; people, tree and key administration; erasure and settings are root-only on /org routes. |
| Managing node | People creation, device keys and delegation to strict descendants; branch inventories and their limits. |
| Read-only node | Tree access without key inventories; monthly named branch reports. |
| Personal | Claim-code redemption and holder rotation; own device management; graft consent. |
| Device / upload | Holder-bound event uploads; issuance by an administrator or the holder's personal key. |
| Federation | Branch-scoped issuance, pushes and own-server status only. |
| Shared / legacy | Retired credentials have no authority. Root administers hierarchy, manual cost, outcomes and erasure; personal/upload keys alone ingest events. Viewer declarations and hierarchy declaration columns are removed. |
For individual cards, read the dated holder and view-log rule. Read authority over a dated branch includes the people and cards in that branch. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.
The related trust limits are subtree padding and one server per developer, federated totals reported by them, not re-checked here, and the child upstream SSRF residual.
Erased people and later imports (#1491, ruling A⁗)
TIER keeps this record so a completed erasure stays effective. Your organisation decides the legal basis with its counsel.
On an erasure request, if any identifier has an import trace, TIER stores one unlinked keyed hash for each canonical ID, alias and mapped source name. A batch row mapped to someone else is not a trace. If erasure deletes a mapping row whose target is someone else, it also records that source name; a wrong exclusion is reported and reversible. Applied-batch plaintext source-name rows for the erased mapped person are deleted. Retention ageing makes no entry and never deletes entries. The record holds no figures, timestamp or link between identifiers and uses its own HMAC domain. Hashes never appear in API output, logs, audit rows, receipts, previews or data-subject exports. Those compliance artifacts state the record's existence in fixed wording, without checking membership or counting entries.
The record is not anonymous: anyone holding the database could test a guessed identifier. It matches on the identifier alone, so the same identifier from any source is excluded, including a different person. Preview and apply leave a match behind, rechecking inside the apply transaction, with this report line naming nobody: “One or more people were left behind so a completed erasure stays effective.” The bundle is not refused; lookup failure refuses the import. A wrongly excluded person has to come forward because the report names nobody; root can release them. A different (renamed) identifier evades the record. A person erased here with no import trace is not recorded, except this clash case, so a later first import can recreate them. A staged copy can remain for 24 hours.
Entries are kept until root clears them or the database is deleted, with no timer.
A clear removes only the named identifier; a returning person with several
identifiers needs one clear each. Root states either “new consent” or “a different
person with that identifier” (the API value is different person with that identifier).
The response is identical whether or not an entry existed. Each clear is audited
without the identifier, hash or entry count. An audited clear-all states
“our counsel refused this record”. There is no dashboard control. Nothing in the
release waits for counsel, and no Article is cited as the record's legal basis.
The secret and entries share the database file. Entries do not survive a restore
from an older backup that predates them: restoring it loses newer entries and
undoes those erasures. Re-run erasures after a restore.
See the root API commands for one-identifier clear and counsel-refusal clear-all.
The root erase dialog and success message disclose the fixed
erased_import_record notice: TIER retains a record so a completed erasure stays
effective, and the organisation decides its legal basis with counsel. Import
previews and completion receipts show the nameless erasure_notice as text.
A mapping to an erased target leaves the source person behind at apply, including
a replacement for an earlier mapping. Each source pull on both servers records
its own import attempt; a failure before the pull starts leaves the export key
usable. The import recording warning is dated and appears only on overlapping
reports and totals. Monthly
comparisons disclose loss recorded at the monthly close through
live_capture_may_be_lost; they do not re-read today's loss records.
Upload credentials are refused at issuance and use for departed subjects,
including their canonical aliases. The tierd tokens command defaults to
~/.tier/tier.db, like other commands. Issue upload keys individually with
--upload --subject ID --label LABEL; the secret is shown once.
Source export is available in the
root and personal navigation. Hiding a shown-once key or code asks for confirmation.
Anyone holding a claim code can redeem it and sign in as that person.
Live Team, Person and Models screens (#1495)
Open Organization and choose a group name to see its people on Team. It starts with the last 30 days; choose From and Before dates for another half-open UTC period. Name is the initial sort. Click a column heading to sort spend, merged points, dollars per point, upload time or practices. A small sample says "no score" and spend with no merges is marked. The group total is the sum of these people when the organization permits names for this key. Read-only node keys have the same named read scope as managing node keys; management actions still require Manage.
Choose a person to open the rendered coaching card and Models for their work. My card also renders the card and keeps the holder's view history under every names choice. The notice reads: "Your organization chooses who can expand groups to names. You can always see your own figures." Cards and the live route clip events and outcomes at dated person and group moves; no new ancestor reads earlier off-branch live figures.
GET /api/v1/org/live?since=YYYY-MM-DD&until=YYYY-MM-DD&node=ID accepts a root,
node or personal bearer or session. Omit node for the whole authorized branch;
a personal key returns its own figures. Upload, federation and legacy keys do
not open it. It reads one snapshot and admits a bounded in-memory report-audit
count per covered UTC month and requested scope before building figures. It works
before any month has closed; unavailable audit admission or read-only serving
returns 503 with Retry-After and no figures. Card reads commit their view log first and require a writable real
server. The private synthetic demo can render cards without writing a view log.
Model rows show captured spend, associated merged points and dollars per point.
Each outcome's points are split by its models' recorded attributable cost;
missing model evidence stays in "No model recorded". Points can include cost
from the attribution lookback while spend stays in the chosen period. Task mix
and other practices can explain differences. Available Model Report figures
come bundled from Edition 2 and
Edition 1, using the shared orgsync task.
Accepted runs and dollars per accepted run stay distinct from repository points;
a benchmark with zero accepted runs shows "no score". Edition 1 retains the
published rounded per-accepted figure; a total is shown only when bundled.
The application makes no runtime request to the published report.
Production sends frozen monthly v2 rows through the pinned authenticated route. Central readers retain their own dated scope, name-expansion policy, k and audit-before-read checks. Local-only report and manifest bytes are unchanged. The aggregate bridge, v1 wire/storage/pending paths and legacy Open switch are removed. Eligible upgrades preserve admissions and credentials; accepted v1 history refuses migration without changing the database.
Monthly time-to-merge uses the same holder-key boundary and read-audit admission as monthly scores in every mode. Root, managing and read-only node keys, and personal keys receive one name-free full-tree projection; a key cannot narrow the sample by node, model or arbitrary window. Shared, legacy-admin and viewer credentials are refused. The front page's “Open branch” link preserves the month and opens the named Organization report; its existing dated holder scope still limits the people and CSV. A group link never grants named sibling access. The front-page month control also selects its frozen time-to-merge report. A failed manifest clears the displayed figures and starts no further report request; there is no legacy sealed-mode fallback.
Reading the live screens
The URL printed by tierd demo opens a front page with links to Organization,
Team, Person and Models. Names open the rendered Person screen. The private
synthetic demo labels its invented people and figures, returns empty card
notices and view history, and explains that sealed-month reports and edits are
unavailable there. Real servers retain their normal access checks.
Totals name the selected group. Spend means list-price usage, not the bill. Merged points weight completed work by size and quality; dollars per point is list-price spend divided by those points. An up arrow means higher is usually helpful; a down arrow means lower is usually helpful. Context / output means input tokens per output token; task needs affect the useful ratio. Practice suggestions use the person's recorded gaps even without a changing fortnightly trend. A model with few accepted report runs carries a reminder to check whether it can complete similar work despite a low local cost per point.
We grade the work so we can coach the people. A figure reflects the work someone took on as well as how they use AI. Team-screen reads do not appear in the person's viewed-by log; individual card opens are logged on real servers.
Live figures: identity, placement and limits (#1496)
Live Team, Person and Models reads join aliases to one canonical person after checking each raw identifier's dated placement. Spend uses the stored named subject when present and otherwise the producer's developer identifier, so shared uploads and upgraded history remain visible without gaining census eligibility. Personal keys open only their own card and figures. Last upload uses receipt history independently of the figures window, with the existing credential receipt fallback after retention; its visibility follows the holder's branch at receipt time.
Root totals include activity before placement. The live response's groups
array contains visible node totals (node_id, parent_id, name, spend_usd,
merged_points), plus unplaced:true and direct:true rows where needed.
Pass a direct:true row's node_id as ?node=direct:<placement> to read only that placement's direct people, under the same dated branch authorization.
Unplaced belongs only under the root. With independent company roots and no
placement evidence, it stays in the forest remainder visible only to the global
root credential; company node keys cannot infer its owner. Children plus direct and unplaced figures
reconcile with each parent; the overlapping ancestor rows must not be added
together. Organization uses one live response and loads sealed reports
independently, so large trees do not spend one read per group.
Live figures and holder cards default to 30 days and refuse windows longer than 90 days with HTTP 400: choose a shorter period or use sealed reports for older months. This window cap is the simpler bound on live read cost. Recorded zero-cost models receive an equal model share of associated points; remaining points are divided among paid models by attributable cost. With only zero-cost models the split is equal. "No model recorded" has a null cost per point rather than implying free work. Card opens on read-only servers explain that the view log needs a writable server.
Retired evaluation_mode YAML and saved settings are ignored. They cannot
relax live access or k; historical report replay retains its recorded labels.
Monthly name-free cache levers use the frozen monthly rows and full-tree comparison decision. Prefold sets, seal-body assembly and its raw lever reader are removed. Archived raw-report verification retains its recorded audience checks and per-measure census; it cannot grant listener access.
Federation reader settings include explicit host pins inside the same read
snapshot as saved values. A pinned names_visibility applies to root and node
readers under the same policy as a saved choice. Hidden-name Company totals
and Organization overview render the shared closed-month group decision.
Monthly billing revisions use a separate frozen-basis ledger-ID chain. Readers and
CSV distinguish awaiting billing, recorded date, revised date and the previous
paid amount; still expected sources prevent recorded from meaning final. A close
without a basis says billing not recorded at close and refuses late invoices.
Name-free group rows exclude paid outside shown groups at every ancestor and
carry the same amount-free note when the company line is nonzero. Named readers
keep each person on their frozen path. Personal keys receive the same name-free
/scores and /scores/compare decisions; /org/report/compare remains denied.
Federation v2 carries the frozen paid partition, recording/allocation status, realtime numerator, billing reconciliation and its independent revision chain per origin and month. It retains quality, horizon, retention, capture-loss, price/rubric/build evidence, dated labels and selectors. Relay forwards each origin once; its own invoice never reallocates onto child contributions. Readers sum integer numerators and denominators before deriving ratios; unknown billing remains unknown. Local and received rows use one census-only publication decision, with no fold-equality check. Paid stays outside that decision, with nodeless paid on the name-free company line. Erasure, retention and replay preserve amounts without restoring names. Multi-origin JSON and CSV retain each origin's disclosures. The organization owns its data; unreleased databases have no compatibility adapter. Paid is schema step 31 and federation is step 32.
Name-free live reads require one exact closed UTC month and refuse explicit partial or multi-month bounds with HTTP 400. Whole-month dashboard selections use the committed report including received origins. Name-free origin disclosures retain frozen warnings while omitting origin identity and build inventory; partial-input status follows only selected origins.