You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
cratestack 0.7.10 (just bumped in #TBD) ships PR #500, which adds auth().isSystem() as a first-class policy-term builtin — a cratestack_core::SystemContext is now the only structurally-unforgeable way to mint a CoolContext for which is_system() is true, and auth().isSystem() can name that principal directly in a model's @@allow, for both read (list/detail) and write (create/update/delete) policies.
This is worth a deliberate look here, not a drive-by schema edit, because of how much history this exact gap has in vsms.
Why this matters here specifically
AGENTS.md's "Invariants that fail the build rather than production" section documents a single mistake shape found live seven separate times (App, AppClient, SenderIdRegistration, OperatorPrefixRule#94; Provider, Job#100; Message list/detail #96; DeliveryReceipt#121) and explicitly predicted an eighth: a model whose @@allow omits hasRole('system') doesn't error — CrateStack's list-route policy denial is row-level filtering to an empty array, so an internal system-context read just quietly returns nothing and the caller behaves as though the table were empty.
#155 closed the loop on detection with crates/sms-api/tests/system_context_golden_list_live_postgres.rs: a live-Postgres, seed-then-read golden test that (a) derives the full model list from schema/schema.cstack itself so it can't silently drift, (b) keeps a hand-maintained, justified classification (SYSTEM_READABLE_MODELS / NOT_REQUIRED_TO_BE_SYSTEM_READABLE) that fails loudly the moment a new model isn't classified either way, and (c) actually seeds a row and reads it back under a real system-role context against a real database for every model that's supposed to be system-readable. As of #155, all 13 models an internal system context reads today correctly admit hasRole('system'); the other 6 have no internal reader to break.
auth().isSystem() is a candidate to replace hasRole('system') as the mechanism itself, not just another way to spell the same string:
hasRole('system') today works because sms-auth's AuthProvider mints a synthetic role: "system" claim for internal callers, and every @@allow clause has to remember to check for it — a convention, not something the type system or CrateStack enforces. auth().isSystem() instead asks whether the CoolContext itself was constructed via SystemContext — per PR #500, "the sole, structurally-unforgeable way to mint a system CoolContext," with an explicit test proving no HTTP request can produce one (http_request_cannot_produce_a_system_context).
That closes a strictly worse failure mode than the one No invariant guards the hasRole(system) policy gap — found live seven times, an eighth is expected #155 guards: today, if role: "system" ever leaked into a real issued token (AGENTS.md's M1 section already flags this as the one state that must never happen — "system is synthetic and no issued token may carry it"), hasRole('system') policies would trust it. auth().isSystem() can't be spoofed by a claim at all, only by actually holding a SystemContext value, which only sms-api/sms-worker/sms-auth's own internal code can construct.
What this issue is NOT asking for
Not a schema edit. Not a decision to migrate today. This is a research/decision ticket:
Does adopting auth().isSystem() fully replace hasRole('system') in schema/schema.cstack, or do both need to coexist (e.g. is there a real distinction in this schema between "the request carries a system role claim" and "this is genuinely internal server code" that's worth keeping separate)?
If it's a full replacement: is it a mechanical, one-PR migration (swap every hasRole('system') clause for auth().isSystem(), verify byte-identical DDL per the standing regeneration discipline in AGENTS.md — a policy-only @@allow change never touches DDL), or does it interact with anything non-mechanical (e.g. sms_auth::system_context()'s own construction, Procedures::sys())?
Interaction with No invariant guards the hasRole(system) policy gap — found live seven times, an eighth is expected #155's golden test, specifically:system_context_golden_list_live_postgres.rs currently greps schema/schema.cstack's text for the literal string hasRole('system') in its per-model assertions (see e.g. lines asserting "hasRole('system') is absent from Provider's create policy"). If any model's policy is rewritten to auth().isSystem(), those specific string assertions would need updating to match — the live seed-then-read behavior they're proving stays valid (a SystemContext-backed read should still succeed), but the assertion text checking why it should succeed would go stale and either false-fail or (worse) silently stop asserting the right thing if left unchanged. Whoever picks this up should treat No invariant guards the hasRole(system) policy gap — found live seven times, an eighth is expected #155's test file as something this change touches, not something it can leave alone.
Whether auth().isSystem() is available for procedure-level policies at all — PR #500's own Scope section says explicitly it is not ("Procedure-level policies... #486's scope and the spike both concern model policies only"), so sms_api::require_permission's Layer 2 checks (#24, sms:send etc.) are unaffected either way.
Summary
cratestack 0.7.10 (just bumped in #TBD) ships PR #500, which adds
auth().isSystem()as a first-class policy-term builtin — acratestack_core::SystemContextis now the only structurally-unforgeable way to mint aCoolContextfor whichis_system()istrue, andauth().isSystem()can name that principal directly in a model's@@allow, for both read (list/detail) and write (create/update/delete) policies.This is worth a deliberate look here, not a drive-by schema edit, because of how much history this exact gap has in vsms.
Why this matters here specifically
AGENTS.md's "Invariants that fail the build rather than production" section documents a single mistake shape found live seven separate times (App,AppClient,SenderIdRegistration,OperatorPrefixRule#94;Provider,Job#100;Messagelist/detail #96;DeliveryReceipt#121) and explicitly predicted an eighth: a model whose@@allowomitshasRole('system')doesn't error — CrateStack's list-route policy denial is row-level filtering to an empty array, so an internal system-context read just quietly returns nothing and the caller behaves as though the table were empty.#155 closed the loop on detection with
crates/sms-api/tests/system_context_golden_list_live_postgres.rs: a live-Postgres, seed-then-read golden test that (a) derives the full model list fromschema/schema.cstackitself so it can't silently drift, (b) keeps a hand-maintained, justified classification (SYSTEM_READABLE_MODELS/NOT_REQUIRED_TO_BE_SYSTEM_READABLE) that fails loudly the moment a new model isn't classified either way, and (c) actually seeds a row and reads it back under a realsystem-role context against a real database for every model that's supposed to be system-readable. As of #155, all 13 models an internal system context reads today correctly admithasRole('system'); the other 6 have no internal reader to break.auth().isSystem()is a candidate to replacehasRole('system')as the mechanism itself, not just another way to spell the same string:hasRole('system')today works becausesms-auth'sAuthProvidermints a syntheticrole: "system"claim for internal callers, and every@@allowclause has to remember to check for it — a convention, not something the type system or CrateStack enforces.auth().isSystem()instead asks whether theCoolContextitself was constructed viaSystemContext— per PR #500, "the sole, structurally-unforgeable way to mint a systemCoolContext," with an explicit test proving no HTTP request can produce one (http_request_cannot_produce_a_system_context).role: "system"ever leaked into a real issued token (AGENTS.md's M1 section already flags this as the one state that must never happen — "systemis synthetic and no issued token may carry it"),hasRole('system')policies would trust it.auth().isSystem()can't be spoofed by a claim at all, only by actually holding aSystemContextvalue, which onlysms-api/sms-worker/sms-auth's own internal code can construct.What this issue is NOT asking for
Not a schema edit. Not a decision to migrate today. This is a research/decision ticket:
auth().isSystem()fully replacehasRole('system')inschema/schema.cstack, or do both need to coexist (e.g. is there a real distinction in this schema between "the request carries asystemrole claim" and "this is genuinely internal server code" that's worth keeping separate)?hasRole('system')clause forauth().isSystem(), verify byte-identical DDL per the standing regeneration discipline in AGENTS.md — a policy-only@@allowchange never touches DDL), or does it interact with anything non-mechanical (e.g.sms_auth::system_context()'s own construction,Procedures::sys())?system_context_golden_list_live_postgres.rscurrently grepsschema/schema.cstack's text for the literal stringhasRole('system')in its per-model assertions (see e.g. lines asserting"hasRole('system') is absent from Provider's create policy"). If any model's policy is rewritten toauth().isSystem(), those specific string assertions would need updating to match — the live seed-then-read behavior they're proving stays valid (aSystemContext-backed read should still succeed), but the assertion text checking why it should succeed would go stale and either false-fail or (worse) silently stop asserting the right thing if left unchanged. Whoever picks this up should treat No invariant guards the hasRole(system) policy gap — found live seven times, an eighth is expected #155's test file as something this change touches, not something it can leave alone.auth().isSystem()is available for procedure-level policies at all — PR #500's own Scope section says explicitly it is not ("Procedure-level policies... #486's scope and the spike both concern model policies only"), sosms_api::require_permission's Layer 2 checks (#24,sms:sendetc.) are unaffected either way.References
AGENTS.md's "Invariants that fail the build rather than production" section (the seven-instances history),crates/sms-api/tests/system_context_golden_list_live_postgres.rs(No invariant guards the hasRole(system) policy gap — found live seven times, an eighth is expected #155)