Skip to content

fix(core, plugin-auth): a failed organization probe reads as readFailed, and an auth mail treats a failed currency read as transient - #28088

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-abzzym
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-abzzym

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#3 · os-elon-musk · session_016WV4czs2fGS6UiDUUbdGrf

Fixes #27733
Fixes #27710

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

Core: the organization probe is a read too (#27733). For a caller that names no organization where no wall stands, the direct sys_setting fallback first asks sys_organization (through resolveGuestOrganization) whose rows it may read. localizationFallbackReach turned a throw there into unowned, so no sys_setting read ran and the answer had no readFailed. It now returns { reach, probeFailed }, where probeFailed is isAuthzStoreUnavailableError(err), and resolveLocalizationContext sets readFailed: true on it. Per triage 6509207804, such an answer writes no failure memo; it is not stored in the success cache either (it holds no value to keep), and the dead entry it supersedes is dropped, so the next request probes again. Any other throw (an OS_TENANCY_POSTURE that cannot be read, a configuration error, not transient) still answers unowned with no readFailed. The settings-read memo and the settings-service legs are unchanged. The readFailed doc comment now names both triggers.

plugin-auth: the auth mails' tenant currency (#27710). withMailTenantCurrency (mail-tenant-currency.ts) wraps the email service the plugin hands the AuthManager. Its resolver used to return .currency for every answer, so a failed read added no currency and the render refused a no-argument {{ amount | currency }} hole TEMPLATE_INVALID with the "set localization.currency" remedy. Now:

  • The resolver moved from an inline closure in auth-plugin.ts to localizationMailCurrencyResolver (same file as the wrapper, not exported from the package entry). It rejects when readFailed is set, as messaging's getTenantCurrency does since fix(core, service-messaging): a failed settings read is retried, not dead-lettered as TEMPLATE_INVALID #27730.
  • On a rejected read the wrapper sends without a currency. When that send throws TEMPLATE_INVALID, it renders the template once more without sending (renderTemplate, with the ISO no-currency code XXX). If that still refuses TEMPLATE_INVALID, a default cannot cure the template (an @path value that is not a code) and that refusal stands. If it renders, or the service has no renderTemplate, the mail fails with the mail '<template>' was not rendered: the organization's default currency could not be read (<cause>); it can be sent once that read succeeds, which carries no TEMPLATE_* code. The auth mail has no retry ladder of its own: the caller (a resend endpoint, a background send) sees a transient error it can retry, instead of a permanent one.
  • Every other failure, a successful read with no default, and a template with no such hole are unchanged.

service-automation: not in this PR. #27710's notify-run leg moved to #28056 (seat 3). plugin.ts alone cannot express it: the resolver's rejection and its undefined are swallowed alike in run-resume.ts (run setup and withTenantCurrency); the resolver contract FlowTenantCurrencyResolver that says "throws, tolerated" is documented in engine.ts, a held hot file (#27951); and the missing-default refusal is a FlowTextTemplateError branded a guard refusal in builtin/template.ts, with no AutomationContext member (a packages/spec contract) to carry "the read failed" to it.

Why

Triage 6509207804 (#27733) and 6506866652 (#27257 retriage, which graded readFailed and filed #27710): a failed read learned nothing and is transient; only a read that succeeded and found no default is permanent. #27730 covered the sys_setting leg and messaging; this covers the probe leg one step earlier and the plugin-auth caller.

Tests

  • packages/core/src/security/resolve-localization-read-failed.test.ts (5 new): a store fault on the probe answers the built-ins with readFailed: true and reads no sys_setting; no memo of either kind (an engine with the write-epoch and middleware seams, so a success entry would be kept if written): the next request probes again, and once the store answers it reads CNY with no readFailed; a probe that succeeds and finds no organization has no readFailed; an unreadable posture has none; a caller naming its organization is not probed.
  • packages/services/service-messaging/src/template-tenant-currency.test.ts (1 new, the email channel through MessagingServicePlugin's own wiring): a delivery with no organization whose probe fails renders without currency, trial-renders with XXX, sends nothing, and is graded retryable with no TEMPLATE_ text; once the probe answers, the next delivery formats in CNY.
  • packages/plugins/plugin-auth/src/mail-tenant-currency.test.ts (10 new): with a failed read, a missing-default refusal becomes the transient error (exact text, no TEMPLATE_, one XXX trial, nothing sent); a service with no renderTemplate gets it too; an @path refusal, alone or behind the missing default, stands; TEMPLATE_NOT_FOUND stands with no trial; a template needing no default is sent; a successful read with no default keeps the render's refusal with no trial. Through localizationMailCurrencyResolver over the real resolveLocalizationContext: a failed sys_setting read and a failed sys_organization probe (identity mail) both reach the mail as the transient error; a read with no default reaches it as the refusal; a read that finds CNY formats in it.

Acceptance notes

  • Re-measured on main @ bc496bf7: localizationFallbackReach still mapped the probe's throw to UNOWNED_ORGANIZATION_REACH with no readFailed; auth-plugin.ts and service-automation/src/plugin.ts still returned .currency without reading readFailed. Both premises held.
  • Clause-②: no, confirmed from the diff: no exported type changes (readFailed?: true already exists); localizationMailCurrencyResolver lives in plugin-auth/src/mail-tenant-currency.ts, which no package entry re-exports. The contract review is therefore not expected, as the dispatch said.
  • Changeset: @objectstack/core and @objectstack/plugin-auth, patch. service-messaging moved no code (one new test), so it has none; service-automation has no change here.
  • Skills-docs: none. git grep -n "readFailed\|no tenant default\|could not be read" -- skills docs content names no text about this resolver or these mails. content/docs/automation/email-templates.mdx and content/docs/kernel/runtime-services/email-service.mdx describe only the "no default currency reaches the render" case, which is unchanged.
  • Branch name: the card thread's harness-pinned branch claude/project-thread-abzzym names no issue; claims 6520779833 / 6520770423 (amended) are the identity record.
  • Open PRs: none of the 20 open PRs touches these files. Setup: changing a member's role silently auto-grants a permission set; the result never says a set was granted (#27521 row 14) #27862 (seat 3) edits other hunks of plugin-auth/src.

Verification

Head eb1c3932 (one commit on main @ bc496bf7).

  • Ablations (scripts/ablation-replace.mjs; each leg red, then restored with blob == HEAD):
    • A1, the probe fault sets no kind (probeFailed: false): core 2 of 10 red; with core rebuilt, messaging 1 red (the probe pin) and plugin-auth 1 red (the end-to-end probe pin).
    • A2, the no-memo branch skipped (the answer falls into the success cache): core 1 red (the no-memo pin).
    • A3, every derivation throw counts (probeFailed: true): core 1 red (the unreadable-posture pin).
    • A4, the auth resolver never rejects: plugin-auth 2 red (both end-to-end read-fault pins); rerun after the last fixture change, still 2 red.
    • A5, the wrapper never re-grades (throw err): plugin-auth 5 red.
    • A6, the trial render's standing refusal ignored: plugin-auth 2 red (both @path pins).
  • Tests: @objectstack/core 128 files / 2878 passed; @objectstack/plugin-auth 204 / 3896 passed (10 skipped); @objectstack/service-messaging 62 / 756 passed.
  • Types: typecheck exit 0 for core, plugin-auth, service-messaging (the first messaging run caught an ES2022 Array.prototype.at in the new pin; replaced by an index).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 families, after a whole-tree turbo run build. 68 exited 0 on the first run; check:objectql-double-limit flagged the new plugin-auth find double as unjudged, and after rewriting it on the generic where-matcher it exits 0. That gate and the eight others that read test files (where-matcher, engine-double-contract, cross-package-test-inputs, test-source-alias, nul-bytes, issue-citations, undeclared-dep-imports, query-options-erasure) were rerun at eb1c3932, all exit 0; the derived list at eb1c3932 is the same 69. --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint: pnpm lint exit 0; ESLint on the two test files changed afterwards, 0 errors.
  • Dogfood battery: not run; the title has no ! and there is no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request. (Two sibling cards, one dispatch.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf

…ed, and an auth mail retries a failed currency read

The localization resolver's sys_organization probe, which decides whose
sys_setting rows a caller naming no organization may read, now answers
readFailed: true on an AuthzStoreUnavailableError, with no memo of either
kind. The auth mails' tenant currency reader rejects on readFailed, and a
render refusal that only the missing default caused becomes a transient
error that names the read instead of TEMPLATE_INVALID.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/plugin-auth, touching 12 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/email-templates.mdx (via sendTemplate (literal, a string literal on a changed line))
  • content/docs/kernel/index.mdx (via sendTemplate (literal, a string literal on a changed line))
  • content/docs/kernel/runtime-services/email-service.mdx (via sendTemplate (literal, a string literal on a changed line))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-2.mdx (via resolveLocalizationContext (symbol, a top-level function), sendTemplate (literal, a string literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 49 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ec7e15abcb66afd638932b68e0ce4bc1b17bb313 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9274bc7f848902c588b1d8bcf7f3c7632d7c9145 — the merge of head eb1c3932176dc9aab7cce58b1558289d706e0640 into base ec7e15abcb66afd638932b68e0ce4bc1b17bb313, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9274bc7f848902c588b1d8bcf7f3c7632d7c9145 && git checkout 9274bc7f848902c588b1d8bcf7f3c7632d7c9145
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ec7e15abcb66afd638932b68e0ce4bc1b17bb313 eb1c3932176dc9aab7cce58b1558289d706e0640 && git checkout -B drift-repro ec7e15abcb66afd638932b68e0ce4bc1b17bb313 && git merge --no-ff eb1c3932176dc9aab7cce58b1558289d706e0640

node scripts/docs-audit/affected-docs.mjs --json ec7e15abcb66afd638932b68e0ce4bc1b17bb313

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ec7e15abcb66afd638932b68e0ce4bc1b17bb313 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #28088 · head eb1c3932176dc9aab7cce58b1558289d706e0640 · 2026-10-30T12:38Z

维护者速读

席位核对

ACCEPT

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant