TIER docs

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.