Skip to main content

ARC 2.0.0-beta.35 · after arc#4065

Content access reference

Fields, resolution order, admit rules, HTTP headers, and discovery boundaries for the Web Device serve-time access gate.

Lookup page for the serve-time gate. For the authoring sequence, start at Restrict who can read a page.

Evidence on this page is the merged implementation in arc#4065 (3fa375a41) plus a local run of this site on ARC 2.0.0-beta.35 at http://arcblock.localhost:4922/ on 2026-08-15.

Where to write the policy

LocationFieldApplies to
blocklet.yamlaccessDefault: publicSite-wide hatch: unmarked routes and records inherit public only when this exact sentinel is written. Absent means omit-deny
blocklet.yamlaccessPaths[]Locale-stripped page path. Longest match wins
blocklet.yaml collections.<type>accessContent details of that type. Not the collection list page
Route / pageaccessList and static pages. Not a substitute for a missing record on a detail
Content object front matteraccessThat record only. Tightens the scope; cannot relax it
Content object front matterpreviewDenied-reader shell. Does not make the body public

collections.<type>.readRole is not this table.

Resolution

Policy is a ceiling, in this order:

  1. Longest matching accessPaths[].match (locale prefix already stripped).
  2. collections.<type>.access on a content detail. Ignored on the list page so a type-level member default does not lock /articles/.
  3. Route access on list and static pages. Details do not inherit a public list route as if the record were public.
  4. accessDefault: public.

Then the record:

Record accessEffect
omittedInherit the scope. If the scope is public, the page is public. If there is no scope, undeclared → 404
publicNo extra role. Does not unlock a locked scope
noneHard deny. Admin and owner cannot inherit it
any other stringAND-tighten. The caller must satisfy every named role from scope and record

admin and owner in the caller’s role list admit any declared gated resource. They do not admit an undeclared page or none.

Admit is contains-match on caller.roles, not a numeric role level. A caller with both member and admin satisfies an admin page. A caller with only member does not.

Path match:

matchHits
/docs/internal/**/docs/internal/runbook, not the list /docs/internal
/articles/members-onlythat exact detail, and /articles/members-only/...
longer prefixwins over a shorter one

yaml
accessPaths:
  - match: /docs/internal/**
    access: admin
  - match: /docs/internal/public-note
    access: public

Decision kinds

decideWebAccess returns one of:

kindWhen
fullCaller is admitted
preview-shellDenied, and preview: true
promo-shellDenied, preview is not true, and the record is listable
not-foundUndeclared, none, or denied and not listable

On a content detail, preview-shell and promo-shell live-render through the site layout. Only not-found short-circuits as a bare 404. SiteServer then:

  • If preview !== false, cut a first-screen teaser (PAYWALL_TEASER_CHARS = 480, PAYWALL_TEASER_MAX_BLOCKS = 2, last top-level block withheld, leading H1/H2 that repeats the hero title dropped).
  • If that teaser HTML is non-empty, the visible gate copy uses the preview phrasing even when kind is promo-shell.
  • If preview: false, no teaser. Listable → gate only. Unlistable → 404.

CJK characters count as 2 visual units.

HTTP, cache, and SEO

OutcomeStatusCacheRobots
Full public page200no-cache on this captureindexable
Full gated page (admitted caller)200no-cache on this captureno forced noindex
Teaser / gate200private, no-storeX-Robots-Tag: noindex and <meta name="robots" content="noindex">
404404private, no-storen/a

Shared public edge cache is legal only when kind === "full" and effective access is public. Prerender may still write the full body into .web-cache/<path>/index.html. The serve gate must run before that file is replayed. Do not publish .web-cache as a static host.

Gated HTML may advertise only a declared SEO description or excerpt. A docs summary derived from the protected body does not go into meta, Open Graph, or JSON-LD. post-hero coverImage is cleared on a shell. Accept: text/markdown is refused on a gated detail.

Live capture, anonymous admin fixture:

text
HTTP/1.1 200 OK
cache-control: private, no-store
x-robots-tag: noindex

Live capture, same URL with x-caller-did + x-caller-role: admin (local seam only):

text
HTTP/1.1 200 OK
cache-control: no-cache

BODY_MARKER_ADMIN_ONLY present; no access-gate.

Discovery versus the HTML list

Anonymous sitemap, llms.txt, MCP get, and index query force listedOnPublicList: false.

RecordHTML public listAnonymous discovery
public or inherited public (collection / path / hatch)Full cardFull fields (arc#4086)
gated, preview: truePromo / preview cardShell fields only; body stripped
gated, no preview: truePromo card if listableAbsent: no URL, no body
undeclared or noneAbsentAbsent

Media and feeds

Item-local media and collection RSS use the same admit function. Preview and promo shells never grant bytes. A public list card is not a media admit.

Local caller seam

When the process has AFS_TEST_ALLOW_CALLER_HEADER=1:

HeaderRole
x-caller-didRequired. Role headers without it do not admit
x-caller-role or x-caller-rolesComma-separated role names

This is a test hook. It is not a public authentication API.