Skip to content

service-settings: the settings REST family (registerSettingsRoutes) answers an anonymous caller from its own authorization logic instead of the 401 floor; ADR-0056 D2 and ADR-0138 D2 require 401 — the fourth door blocking ADR-0138 criterion 2 #23142

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing and a measured reach, reach: an anonymous HTTP request, measured on a booted reference deployment by the anonymous-sweep conformance suite of PR #23137 (os-dev-report 6137316611 on #22146, at bf3ba9957). Route-level readings stay private; this card is at function level, as #22430, #22431 and #22432 were. Readers who act: triage, to route (packages/services/* is domain:services in the pm-dispatch domain table); then the owning seat. Blocks: ADR-0138 acceptance criterion 2 (#22146), and PR #23137, whose suite is red on this family alone.

Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, from the #22146 dev report (out-of-scope finding 1). ⛔ Not a claim; grade and lane are triage's.

What happens

packages/services/service-settings/src/settings-routes.ts#registerSettingsRoutes (:107 on origin/main c9b5b24b0), mounted by SettingsServicePlugin, opens no handler with an anonymous floor (shouldDenyAnonymous or the shared !userId && !isSystem → 401 decision). An unauthenticated request to the family is answered by the family's own authorization logic, with a status class other than 401. On the measured boot no settings value was served.

Governing text

Acceptance

Dedupe

MCP search_issues on objectstack-ai/objectstack, closed included: settings service REST routes anonymous caller 401 registerSettingsRoutes shouldDenyAnonymous → 5 hits: #22430 and #22432 (closed, the OpenAPI and /i18n doors), #10349, #7911 and #7981 (closed, other surfaces). None is the settings family. Tree read: git grep shouldDenyAnonymous origin/main -- packages/services/service-settings/src → 0 hits; control registerSettingsRoutes in the same directory → 1 hit.

Dedupe words: settings anonymous floor; registerSettingsRoutes shouldDenyAnonymous; ADR-0138 everything-else settings; anonymous settings index.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p1 · target:v18 · domain:services · area:access, into pm:queue. The settings family takes the shared anonymous floor

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Unblocks: #22146

  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 7 · 2026-10-13T04:18Z
    Session: session_01FzQy26KznSa5j2gnf3AzsE
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-23142-settings-anonymous-floor
    Worktree: objectstack-issue-23142
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/services/service-settings/src/settings-routes.ts (one shared anonymous floor for the family), its tests under packages/services/service-settings/, one .changeset/23142-*.md (stop on breach; explain in the report)
    Container & model: M (one shared floor, mounted once, with a per-route pin sweep), mode:subagent, model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
    Clause-②: no — it narrows the anonymous accept set to the ADR-0056 D2 floor; no published schema or export moves.
    Responsibility: service-settings' REST family (registerSettingsRoutes, the platform's own code) opens no anonymous floor | none: the family runs its own authorization logic before any shared floor | any anonymous HTTP caller of the settings REST family; the measured boot served no settings value (PR #23137's sweep)
    Thread-read: 6137894223
    Serial constraints cleared: no pm:dispatched claim declares service-settings, and no open PR touches it (read at this claim). The last landing on the file was bbe03f4068 (#20836), not today. PR #23137 (domain:spec, the anonymous sweep) is red on this family alone and turns green when this lands. Its pins are not touched here.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23142,
      "status": "done",
      "branch": "claude/issue-23142-settings-anonymous-floor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23171",
      "session": "session_01FzQy26KznSa5j2gnf3AzsE",
      "premise_still_valid": true,
      "summary": "registerSettingsRoutes (packages/services/service-settings/src/settings-routes.ts) now mounts all four routes of the settings REST family through one door, mountFamilyRoute. The door resolves the caller with contextFromRequest (unchanged), relays an authorization-store outage as its 503, then asks the shared ADR-0056 D2 decision shouldDenyAnonymous from @objectstack/core and answers a context with no userId with 401 UNAUTHENTICATED (ANONYMOUS_DENY_STATUS / _CODE / _MESSAGE through the family's sendError envelope), before any settings logic runs. Each route became a { serve, refuse } pair holding its old try body and catch arm byte for byte, so the floor and the outage relay are each mounted once for the family, not copied per handler; isSystem is not read and the path is not passed, so a moved basePath inherits no allow-list exemption. H1 measured true at 02bec6ca86 (0 shouldDenyAnonymous hits in the package, control registerSettingsRoutes 1); H2 followed: the shared decision before the family's own logic, envelope written the way service-datasource's admin family and rest's package family write it. A signed-in caller is served as before (signed-in control per route), nothing that was refused is now admitted, and no consumer was found that reads the family anonymously (console settings views and the timezone prompt are signed-in; every in-repo dogfood caller sends a bearer token). Producer = the registrar, as the card said.",
      "tests": "All at HEAD 3bb73c0cd1 unless noted. (1) pnpm --filter '@objectstack/service-settings^...' build under os-verify-lock: VERDICT command-exit 0; then the package build: exit 0, check-dts-emitted 2/2. (2) pnpm --filter @objectstack/service-settings test: 'Test Files 43 passed (43) / Tests 788 passed (788)', VERDICT command-exit 0; base 02bec6ca86 read 42 files / 755 tests. (3) pnpm --filter @objectstack/service-settings typecheck (tsc --noEmit): VERDICT command-exit 0; tsc --listFiles compiles 43 test files including the new settings-routes.anonymous-floor.pin.test.ts. (4) New pin file: census read off the registration (exactly 4 routes, equal to SETTINGS_ROUTE_LEDGER, no middleware); every captured route x 5 anonymous context shapes + a forged identity header, each asserting status 401 + error.code UNAUTHENTICATED + success false + shared message + no data, against a service that records and throws on any call (asserted untouched); a basePath the shared allow-list would exempt (non-vacuity asserted) still refused; signed-in control per route 200 with the caller's own context object reaching the service. (5) Fixture triage: 7 existing test files re-judged (signed-in fixtures gained a userId; pins of the retired anonymous 403/404/empty-list answers now assert 401; the capability refusals re-driven by a signed-in caller holding no capability). (6) Ablation, committed first, under the lock: scripts/ablation-replace.mjs removed the floor call (anchor 1 -> 0, blob c4f12f1dd2b9 -> 6e12080a10ea, on-disk floor-call count before/during/after 1/0/1, trap restore on EXIT INT TERM against an absolute path). Mutated suite: 'Test Files 5 failed | 38 passed (43) / Tests 38 failed | 750 passed (788)', 'AssertionError: expected 500 to be 401' / 'expected 403 to be 401' / 'expected 200 to be 401'. Of the new file's 30 cases, 25 went red (every anonymous case) and 5 stayed green (census + 4 signed-in controls); re-homed pins also red: tenancy posture 6, settings-routes 4, envelope 2, config-change audit 1. Restore proven by bytes: blob after restore == HEAD blob c4f12f1dd2b9, git diff HEAD empty. No build/dist leg: the pins import the subject relatively from src. (7) Gates: see the gates field. Narrowed CJS/ESM load of the changed package: require(dist/index.cjs) and import(dist/index.js) both load.",
      "gates": "Re-derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at tree 3bb73c0cd1: 65 commands, the same set as the PM's reading at gates.txt. All 65 were run, unlocked as check:* gates; the run ended 'ALL-DONE 3bb73c0cd1'. Results: 64 exit 0; 1 exit 3, pnpm check:dual-build-cjs-loads, which printed 'PREREQUISITE NOT MET ... some package has no dist/' (it reads every package's dist/ and only this package's dependency closure was built). That gate is NOT MEASURED, left to CI and narrowed per item (7) of tests. Reconciled with --ran plus a record of the recorded exit codes: 'Run reconciliation — 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN' ('1 DERIVED from a recorded exit 3'). Gates read directly: check:adr-0087-registration '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' (not-required (no-migration-prescription)); check:route-envelope exit 0; check:error-status-conformance exit 0; check:nul-bytes OK (11176 files); check:type-check-debt and check:type-check-coverage exit 0.",
      "line_budget": "changed lines 607 (+509 / -98) across 10 files vs the human-merge threshold 3000: under (dispatch-gates reading). No skills/** or governed path touched.",
      "files_changed": [
        ".changeset/23142-settings-anonymous-floor.md (+18)",
        "packages/services/service-settings/src/settings-routes.ts (+132 -60)",
        "packages/services/service-settings/src/settings-routes.anonymous-floor.pin.test.ts (+241, new)",
        "packages/services/service-settings/src/settings-routes.test.ts (+53 -10)",
        "packages/services/service-settings/src/settings-admission-tenancy-posture.test.ts (+26 -12)",
        "packages/services/service-settings/src/envelope.conformance.test.ts (+20 -7)",
        "packages/services/service-settings/src/settings-routes.authz-outage-relay.test.ts (+8 -2)",
        "packages/services/service-settings/src/config-change-audit.test.ts (+6 -4)",
        "packages/services/service-settings/src/localization-timezone-help.pin.test.ts (+3 -2)",
        "packages/services/service-settings/src/settings-crypto-fail-closed.test.ts (+2 -1)"
      ],
      "deviations": [
        "Clause-② line copied verbatim from claim 6138137601 into the PR body and the changeset. It carries no direction arm, although its own prose says the change narrows. The changeset declares the narrowing through its BREAKING banner and the bang summary, with minor under the launch-window convention and an ADR-0087 disposition, as the sibling doors did. See open_questions.",
        "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a robot-emoji PR footer. Per the role file, the commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body ends in the session-URL footer.",
        "Label writes: zero. The dispatch named no label, and skip-changeset does not apply because the package publishes. No label-write call was made.",
        "Scope: all edits stayed in the claimed file surface. The 7 existing service-settings test files besides the new pin are 'its tests' that leaned on an anonymous caller.",
        "Every heavy run went through os-verify-lock with slot issue-23142. The gate battery ran under nohup with foreground waits on tail --pid, and its runner has exited."
      ],
      "mcp_calls": "0",
      "api_writes": "2 relay strokes = 2 POST /repos/objectstack-ai/objectstack/dispatches (session token), executed by fleet-write.yml as objectstack-fleet[bot]: stroke 1 (run 38350659780, success) = POST /repos/objectstack-ai/objectstack/pulls (draft #23171) + POST /repos/objectstack-ai/objectstack/issues/23171/assignees (zhuangjianguo; body read back byte-identical); stroke 2 = POST /repos/objectstack-ai/objectstack/issues/23142/comments (this os-dev-report). Plus git push of the branch (not REST). Reads: gh api REST single-card and PR reads only.",
      "open_questions": [
        {
          "question": "Should the PR body's declaration line carry the narrowing arm, reading 'Clause-②: no (narrowing) — ...'? The claim's line has no arm, but the change is an accept-set narrowing of a published export's behaviour. A host that calls registerSettingsRoutes with its own contextFromRequest returning no userId is now refused 401. Siblings #22430, #22431 and #22432 declared 'no (narrowing)'.",
          "options": [
            "A: the seat edits PR #23171's Clause-② line to 'Clause-②: no (narrowing) — ...' (one PR-body PATCH, the seat's to write). check-changeset-no-major then reads a declared breaking change; the changeset is already minor, so nothing else moves.",
            "B: leave the line as claimed. Breaking-ness is still carried by the changeset's BREAKING banner and bang, which check:adr-0087-registration already reads green, but the PR body's declaration names no direction."
          ],
          "recommendation": "A. It keeps the declared direction equal to the shipped one and matches the three sibling doors; the arm is the carrier the gates read, and the prose banner is the weaker one."
        }
      ],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed. content/docs/protocol/kernel/config-resolution.mdx § HTTP Surface describes the family's capability gating but not the anonymous 401 floor in front of it. It is true but incomplete, and outside the claimed file surface.",
        "carrier: PR #23137 · noted, not filed. The dogfood authorization conformance matrix (packages/qa/dogfood/test/authz-conformance.matrix.ts) draws its population from the REST and runtime route ledgers only. The settings family's floor is therefore ratcheted by its package-local pins and by #23137's live sweep, not by that matrix.",
        "carrier: 承接者:无 · noted, not filed (inference, NOT MEASURED). The registrar does not consult the ADR-0069 auth-policy gate, and resolveAuthzContext does not apply it. Whether a session held by an auth policy reaches this family through host middleware was not measured."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #23171 at 3bb73c0cd · contract review PASS

    domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-13T05:43Z, under claim 6138137601 (thread-read 6137894223). The dev's report is 6138827602. Checked on GitHub and on origin/main, not from the report.

    Checklist

    Open question answered: option A. The PR body line reads Clause-②: no (narrowing) — …. The changeset's own line still has no arm. That is a carrier drift, not a blocker: per the record, neither gate reads that arm, and the BREAKING banner and the bang are what reach the CHANGELOG.

    Out-of-scope findings and carriers

    Landing: pr_ready + automerge_enable through the relay. This seat follows it to MERGED.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23171 merged through the merge queue at 2026-10-13T06:05Z as 20949e7a7c, and that commit is on origin/main (read 2026-10-13T06:07Z). domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE, under claim 6138137601, contract review PASS 6139053386 and ACCEPT 6139109957.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2securitytarget:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions