diff --git a/.changeset/v17-docs-sweep-run-4.md b/.changeset/v17-docs-sweep-run-4.md
new file mode 100644
index 0000000000..d2ae98d79b
--- /dev/null
+++ b/.changeset/v17-docs-sweep-run-4.md
@@ -0,0 +1,8 @@
+---
+---
+
+Docs-only: v17 docs sweep run 4 (rc.1 catch-up; closes the missing-run-2
+coverage gap over `a641d10..0f9faa2`). Fixes the retired `api.requireAuth`
+opt-out still being taught in permissions/forms docs (#3963) and the
+half-updated kernel services checklist (16 → 15 services after the `workflow`
+slot retirement, #4451). Releases nothing.
diff --git a/content/docs/kernel/services-checklist.mdx b/content/docs/kernel/services-checklist.mdx
index 763f88e664..9a8e906815 100644
--- a/content/docs/kernel/services-checklist.mdx
+++ b/content/docs/kernel/services-checklist.mdx
@@ -14,7 +14,7 @@ the code wins. See [Plugins & Packages](/docs/plugins/packages) for the full
package catalog.
-The ObjectStack protocol defines **16 kernel services** registered via the `CoreServiceName` enum (the never-implemented `graphql` entry was removed in v17). Each service maps to a set of protocol methods governed by its per-domain contract (`DataProtocol`, `MetadataProtocol`, ...) — the transitional `ObjectStackProtocol` composition alias was dissolved in v17 (ADR-0076 D9); capability availability comes from the runtime discovery `services` registry.
+The ObjectStack protocol defines **15 kernel services** registered via the `CoreServiceName` enum (v17 removed the never-implemented `graphql` entry and retired the never-filled `workflow` slot, #4451). Each service maps to a set of protocol methods governed by its per-domain contract (`DataProtocol`, `MetadataProtocol`, ...) — the transitional `ObjectStackProtocol` composition alias was dissolved in v17 (ADR-0076 D9); capability availability comes from the runtime discovery `services` registry.
**Key architecture principle**: the kernel guarantees only **data** and **metadata**, and even those are filled by packages (`@objectstack/objectql`, `@objectstack/metadata`) rather than baked in — the kernel's own contribution is an in-memory fallback for the `core` slots that have one (`metadata`, `cache`, `queue`, `job`, `i18n` — **not** `auth`). Everything else — including **auth** and **automation** — is delivered by plugins. `@objectstack/objectql` is an example kernel implementation to get the basic API running; production kernels will be rebuilt as separate plugins.
@@ -50,7 +50,7 @@ The ObjectStack protocol defines **16 kernel services** registered via the `Core
├─────────────────────────────────────────────────────────┤
│ Plugin Layer │
│ All other services: analytics, auth, automation, │
-│ workflow, ui, realtime, notification, ai, i18n, │
+│ ui, realtime, notification, ai, i18n, │
│ search, file-storage, cache, queue, job │
│ │
│ Discovery API reports availability per service │
@@ -70,17 +70,16 @@ The ObjectStack protocol defines **16 kernel services** registered via the `Core
| 3 | **analytics** | `optional` | 2 | ❌ Plugin Required | `@objectstack/service-analytics` |
| 4 | **auth** | `core` | — | ✅ Implemented | `@objectstack/plugin-auth` |
| 5 | **ui** | `optional` | 5 | ❌ Nothing fills this slot | `@objectstack/metadata-protocol` — `/ui/view` is served by its `protocol` service, not by a `ui` service |
-| 6 | **workflow** | `optional` | 3 | ❌ Nothing ships | — |
-| 7 | **automation** | `optional` | 1 | ❌ Plugin Required | `@objectstack/service-automation` |
-| 8 | **realtime** | `optional` | 6 | ❌ Plugin Required (in-process only — no HTTP/WS route is mounted) | `@objectstack/service-realtime` |
-| 9 | **notification** | `optional` | 7 | ❌ Plugin Required | `@objectstack/service-messaging` |
-| 10 | **ai** | `optional` | — | ❌ Nothing ships in this repo | `@objectstack/service-ai` (Cloud/EE — not installable, so the table entry is `null`) |
-| 11 | **i18n** | `core` | 3 | ✅ Built-in (in-memory fallback) | `@objectstack/service-i18n` |
-| 12 | **file-storage** | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
-| 13 | **search** | `optional` | — | ❌ Nothing ships | — |
-| 14 | **cache** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-cache` |
-| 15 | **queue** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-queue` |
-| 16 | **job** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-job` |
+| 6 | **automation** | `optional` | 1 | ❌ Plugin Required | `@objectstack/service-automation` |
+| 7 | **realtime** | `optional` | 6 | ❌ Plugin Required (in-process only — no HTTP/WS route is mounted) | `@objectstack/service-realtime` |
+| 8 | **notification** | `optional` | 7 | ❌ Plugin Required | `@objectstack/service-messaging` |
+| 9 | **ai** | `optional` | — | ❌ Nothing ships in this repo | `@objectstack/service-ai` (Cloud/EE — not installable, so the table entry is `null`) |
+| 10 | **i18n** | `core` | 3 | ✅ Built-in (in-memory fallback) | `@objectstack/service-i18n` |
+| 11 | **file-storage** | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
+| 12 | **search** | `optional` | — | ❌ Nothing ships | — |
+| 13 | **cache** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-cache` |
+| 14 | **queue** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-queue` |
+| 15 | **job** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-job` |
The Provider column mirrors `CORE_SERVICE_PROVIDER` in
@@ -319,7 +318,7 @@ registered under their own names (`security.permissions`, `security.rls`,
---
-## 5–7. Business Services
+## 5–6. Business Services
### 5. ui Service — 5 declared methods, none routed ❌
`listViews`, `getView`, `createView`, `updateView`, `deleteView`
@@ -337,7 +336,7 @@ the domain answers **501** with that remedy spelled out, not a generic "install
plugin".
-### 6. workflow Service — retired in v17
+### Retired in v17: the `workflow` slot
The slot, its `IWorkflowService` contract and the three `WorkflowProtocol`
methods (`getWorkflowConfig`, `getWorkflowState`, `workflowTransition`) were
removed in [#4451](https://github.com/objectstack-ai/objectstack/issues/4451):
@@ -350,7 +349,7 @@ flow nodes on the approvals runtime (ADR-0019 — decisions via
`@objectstack/plugin-approvals`), and record-triggered automation is lifecycle
hooks + `record_change` flows.
-### 7. automation Service — 1 method ✅ `@objectstack/service-automation`
+### 6. automation Service — 1 method ✅ `@objectstack/service-automation`
`triggerAutomation`
Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled triggers.
The `/automation` dispatcher domain gates on `isServiceServeable`, so a slot filled by
@@ -358,9 +357,9 @@ a self-declared stub answers as an empty one.
---
-## 8–11. Communication Services
+## 7–10. Communication Services
-### 8. realtime — 6 methods · `@objectstack/service-realtime`
+### 7. realtime — 6 methods · `@objectstack/service-realtime`
`realtimeConnect`, `realtimeDisconnect`, `realtimeSubscribe`, `realtimeUnsubscribe`, `setPresence`, `getPresence`
@@ -374,7 +373,7 @@ in-process only and no HTTP/WS surface is mounted. Re-advertising waits on a rea
transport.
-### 9. notification — 7 methods · `@objectstack/service-messaging`
+### 8. notification — 7 methods · `@objectstack/service-messaging`
`registerDevice`, `unregisterDevice`, `getNotificationPreferences`, `updateNotificationPreferences`, `listNotifications`, `markNotificationsRead`, `markAllNotificationsRead`
The slot name is `notification` (singular) and the package that fills it shares no word
@@ -386,7 +385,7 @@ on `INotificationService` — a send-only provider (SMTP, Twilio, a webhook) fil
slot legitimately without an inbox, and each route probes its own method and answers
501 when absent.
-### 10. ai — contract removed ❌
+### 9. ai — contract removed ❌
~~`aiNlq`, `aiSuggest`, `aiInsights`~~
@@ -402,7 +401,7 @@ The `ai` slot still exists in `CoreServiceName`, but nothing in this repo fills
it (`CORE_SERVICE_PROVIDER.ai` is `null`).
-### 11. i18n — 3 methods
+### 10. i18n — 3 methods
`getLocales`, `getTranslations`, `getFieldLabels`
**Service Name**: `i18n` · **Criticality**: `core`
@@ -467,7 +466,7 @@ AppPlugin will:
---
-## 12–16. Infrastructure Services
+## 11–15. Infrastructure Services
`cache`, `queue`, and `job` are `core` services: like `i18n`, the kernel auto-injects an in-memory fallback when no plugin registers them (see `CORE_FALLBACK_FACTORIES` in `packages/core/src/fallbacks/`). The `optional` services (`file-storage`, `search`) stay disabled until a plugin provides them.
diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx
index 917b2ee9e0..abb452377f 100644
--- a/content/docs/permissions/authorization.mdx
+++ b/content/docs/permissions/authorization.mdx
@@ -48,7 +48,7 @@ site — the file you read when behavior surprises you.
| # | Gate | What it decides | Enforcement site | Failure direction |
|---|---|---|---|---|
-| 1 | **Anonymous deny** | No identity → HTTP 401. **Uniform across every HTTP surface that reaches object data** (#2567): REST `/data` and the metadata endpoints (`/meta`) — the raw-hono standard `/data` routes left the matrix when that duplicate surface was deleted in v17 (#4073), and the dispatcher GraphQL endpoint left when the GraphQL surface was removed (`/graphql` now 404s) — one shared decision, so a caller denied on `/data` can't read the same rows through a sibling door. **Default-on** (ADR-0056 D2): serving the whole data plane publicly requires an explicit `api.requireAuth: false` opt-out, which logs a boot warning. Narrower public surfaces do **not** need it — each derives its own authorization from a declaration rather than from the deployment posture: control plane (`/auth`, `/health`, `/discovery`) is allow-listed; public form submission carries a `publicFormGrant` (ADR-0056 Option A); share-links validate their token then read as SYSTEM; and an anonymous **GET** of the book/doc read surface is admitted so `book.audience: 'public'` works under the secure default, with the ADR-0046 §6.7 audience gate — `'public'` only, fail-closed — doing the authorizing (#3963). | `packages/core/src/security/anonymous-deny.ts` `shouldDenyAnonymous` — called by `rest-server.ts` `enforceAuth` and the dispatcher `handleMetadata`/`handleAI` (default in `packages/spec/src/api/rest-server.zod.ts`); a source-enumerating ratchet in `authz-conformance.test.ts` fails CI if a new surface ships ungated | fail-closed |
+| 1 | **Anonymous deny** | No identity → HTTP 401. **Uniform across every HTTP surface that reaches object data** (#2567): REST `/data` and the metadata endpoints (`/meta`) — the raw-hono standard `/data` routes left the matrix when that duplicate surface was deleted in v17 (#4073), and the dispatcher GraphQL endpoint left when the GraphQL surface was removed (`/graphql` now 404s) — one shared decision, so a caller denied on `/data` can't read the same rows through a sibling door. **Unconditional** (#3963, closing out ADR-0056 D2): the `api.requireAuth: false` opt-out was retired in v17 — the key is tombstoned, so authoring it is a parse error, and anonymous callers are denied on every data surface with no deployment-level escape hatch. Narrower public surfaces do **not** need it — each derives its own authorization from a declaration rather than from the deployment posture: control plane (`/auth`, `/health`, `/discovery`) is allow-listed; public form submission carries a `publicFormGrant` (ADR-0056 Option A); share-links validate their token then read as SYSTEM; and an anonymous **GET** of the book/doc read surface is admitted so `book.audience: 'public'` works under the secure default, with the ADR-0046 §6.7 audience gate — `'public'` only, fail-closed — doing the authorizing (#3963). | `packages/core/src/security/anonymous-deny.ts` `shouldDenyAnonymous` — called by `rest-server.ts` `enforceAuth` and the dispatcher `handleMetadata`/`handleAI` (default in `packages/spec/src/api/rest-server.zod.ts`); a source-enumerating ratchet in `authz-conformance.test.ts` fails CI if a new surface ships ungated | fail-closed |
| 2 | **Public-form grant** | An anonymous form submission carries a declaration-derived `publicFormGrant` authorizing ONLY create + read-back on the form's declared target object — never anything else (ADR-0056 Option A). No guest-portal configuration needed (anonymous principals hold the `guest` position). | `packages/plugins/plugin-security/src/security-plugin.ts` (ObjectQL middleware) | scope-limited allow |
| 3 | **Object CRUD** | `allowRead/Create/Edit/Delete` (+ the destructive lifecycle class `allowTransfer/Restore/Purge`, gated ahead of the M2 operations — #1883) resolved across the caller's permission sets. | `packages/plugins/plugin-security/src/permission-evaluator.ts` `checkObjectPermission` | fail-closed 403 |
| 4 | **OWD / sharing** | Org-wide default (`private` / `public_read` / `public_read_write` / `controlled_by_parent`; **unset or unknown ⇒ `private`, fail-closed** — ADR-0090 D1) plus the external dial (`externalSharingModel`, ADR-0090 D11), manual record shares, criteria sharing rules (owner-type rules were removed from the authoring surface in v17 rather than left declared-but-skipped — [Sharing Rules](/docs/permissions/sharing-rules#recipient-types)), business-unit hierarchy widening (ADR-0057 D5: scope-depth hierarchy lives on `sys_business_unit`, not positions). | `packages/plugins/plugin-sharing/src/sharing-service.ts` + `sharing-rule-service.ts` | fail-closed to owner-only |
diff --git a/content/docs/permissions/explain.mdx b/content/docs/permissions/explain.mdx
index b240fcdd7b..22ce906a27 100644
--- a/content/docs/permissions/explain.mdx
+++ b/content/docs/permissions/explain.mdx
@@ -86,8 +86,9 @@ through.
## Who may ask
-The endpoint is **authenticated-only** — even on `requireAuth: false`
-deployments (an access report is sensitive even about oneself). Beyond that,
+The endpoint is **authenticated-only** (an access report is sensitive even
+about oneself; since v17 anonymous access to object data is always denied
+anyway — the `requireAuth: false` opt-out was retired, #3963). Beyond that,
authorization lives in the *service*, so REST and in-process callers share one
rule:
diff --git a/content/docs/ui/forms.mdx b/content/docs/ui/forms.mdx
index ce26f72ac4..a5da15de35 100644
--- a/content/docs/ui/forms.mdx
+++ b/content/docs/ui/forms.mdx
@@ -44,7 +44,8 @@ Only the spec'd whitelist of form fields is accepted; everything else (status, o
> from the form's own declaration — `{ object: }` —
> and the SecurityPlugin authorizes **only** create + the immediate read-back on
> exactly that object, never anything else and never the anonymous fall-open. So
-> public forms work under secure-by-default (`requireAuth: true`) with **no**
+> public forms work under the unconditional anonymous-deny posture (v17
+> retired the `requireAuth: false` opt-out, #3963) with **no**
> `guest_portal` permission set. The `guest_portal` permission set + `anonymous` flag
> are still attached for **back-compat** (object hooks that detect a guest via a
> falsy `ctx.user?.id`), but they are no longer the authorization mechanism.
@@ -231,7 +232,7 @@ The companion `GET /api/v1/forms/:slug` route returns `500 FORM_RESOLVE_FAILED`
### Auth model
-- Neither route calls `enforceAuth`, so they work even when the project is configured with `requireAuth: true`.
+- Neither route calls `enforceAuth`, so they work under the always-on anonymous-deny default (there is no `requireAuth` knob to configure since v17).
- The execution context handed to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin honors `publicFormGrant` first — a create + read-back grant scoped to exactly the declared object — so authorization holds even without a `guest_portal` profile. `permissions: ['guest_portal']` is retained for back-compat.
- No CSRF or auth header is needed; embed the form on any domain.
diff --git a/docs/v17-docs-sweep.md b/docs/v17-docs-sweep.md
index af81f6636d..63495816bc 100644
--- a/docs/v17-docs-sweep.md
+++ b/docs/v17-docs-sweep.md
@@ -132,4 +132,62 @@ merely *references* changed code, use the `docs-accuracy-audit` workflow the
found seventeen. Grep the pattern repo-wide (including `skills/`), do not work a
fingerprint list file-by-file.
+### 2026-08-02 — run 4 (rc.1 catch-up; closes the run-2 gap)
+
+- **Watermark:** framework `ff17642` (origin/main, post-#4590) · spec
+ `17.0.0-rc.1` · `PROTOCOL_VERSION = '17.0.0'`.
+- **Coverage note:** the log holds run 1 (`a641d10`) and run 3 (version-number
+ pass only) — **no run 2 exists**, so the 362 changesets added
+ `a641d10..0f9faa2` had never been swept with removal fingerprints. This run's
+ delta is therefore `a641d10..HEAD` (435 new changesets, ~48 breaking), not
+ just the run-3 watermark.
+- **Fingerprints added:** `api.requireAuth` (retired outright, #3963 — a doc
+ presenting the opt-out as available is drift); `workflow` kernel service slot
+ (#4451); standalone `validation` metadata kind (#4509, ADR-0088); `job`
+ runtime create/org override (#4509); flow `script` `config.actionType` /
+ `.template` / `.recipients` / `.variables` / inline `script` (#4343);
+ `query.cursor` / `query.distinct` / `joins` / `windowFunctions` /
+ nested-select (#4286, #4196); `EnhancedApiError.fieldErrors` (ADR-0114 D4);
+ `BatchOptions.validateOnly` (#4052); `registerStandardEndpoints` (#4073);
+ `readReplicas` (#4481); connector template / trigger-registry Connector
+ clusters (#4500, #4503); dual-source names (`WebhookConfig` from `./api`,
+ `CacheStrategyEnum`, `MetadataFormat` `'yml'/'ts'/'js'` aliases,
+ contracts `ShareRecipientType` → `RecordShareRecipientType`; #4537–#4539,
+ #4572).
+- **Fixed (drift → corrected):**
+ - `permissions/authorization.mdx` — gate 1 still said serving the data plane
+ publicly "requires an explicit `api.requireAuth: false` opt-out"; the
+ opt-out is retired and tombstoned (#3963).
+ - `permissions/explain.mdx` — "even on `requireAuth: false` deployments", a
+ posture that no longer exists.
+ - `ui/forms.mdx` — two mentions of configuring `requireAuth`; rewritten
+ against the unconditional anonymous-deny posture.
+ - `kernel/services-checklist.mdx` — partially updated at change time: §6 and
+ the provider callout documented the `workflow` retirement, but the page
+ still claimed **16** services, listed `workflow` in the architecture
+ diagram, and carried its live row in the summary table. Now 15 services,
+ row removed, tables and section numbering re-aligned (enum verified:
+ 15 members).
+- **Judged, not drift (skip re-checking):** `actionType` in
+ `deployment/validating-metadata.mdx` / `skills/objectstack-ui` (dashboard
+ action buttons — same key name, different surface); flows.mdx / automation
+ skill (document the #4343 retirement); `bulkActionDefs` in ui docs (teach
+ the new typed shape, #4457); `having` (documents "enforced since #4286");
+ `fieldErrors` in api docs (documents the tombstone); `windowFunctions` /
+ `query.distinct` hits (all "removed in spec 17" notices, run 3 fixed their
+ version numbers); `maxRetries` in flows.mdx (explicit count — the required
+ posture, #4247).
+- **Zero-hit fingerprints (nothing to fix):** `validateOnly`,
+ `registerStandardEndpoints`, `readReplicas`, `CacheStrategyEnum`,
+ `WebhookConfig`, `MetadataFormat` aliases, `ShareRecipientType`, standalone
+ `.validation.ts`, job `allowRuntimeCreate`, connector template cluster,
+ `storage.notNull` misclaims, plugin lifecycle-hook family.
+- **Method note:** docs-at-change-time discipline improved markedly in this
+ window — most removals landed with their retirement callouts already written
+ (flows.mdx #4343, query-syntax #4286, error-catalog ADR-0114). The drift
+ that survives is the *second-order* kind: a page partially updated (checklist
+ count vs narrative) or a cross-reference in a page the change didn't touch
+ (requireAuth in explain/forms). Grep the fingerprint repo-wide even when the
+ primary page looks done.
+