Softanza
DémarrerEnglish
Softanza
Recherche · narration

The Rest of the Seams

I2's first pass wired six classes and stopped. The design doc's section 7 table kept the rest of the rows in plain sight, and the completion document said so as the system's largest honest gap: a detection sees only what a seam emits. The kinds were in the catalog; the calls were not written.

par Mansour Ayouni · dans le dépôt depuis le 2026-07-31 · série · la série sécurité · domaine Sécurité et gouvernance

Cet article est écrit en anglais, comme la bibliothèque.

A detection can only ever see what a seam emits

Every code block below is real, and every output block is its actual output (the run is base/test/system/security_seams2_narrated.ring, 41 assertions, ~0.3 seconds). Incident analysis I2, second pass — doc/design/SOFTANZA_INCIDENT_ANALYSIS.md.

The gap that was listed, not hidden

This pass writes them. Ten more places where the library already knew something and forgot it one call later.

An id-token, and the one slot that held it

Haro
oCli = new stzOidcClient("https://id.acme.com", "shop-app")
aTok = oCli.VerifyIdTokenAt("not-a-jwt", "", $NOW)

illustration

Sortie promise
  [OK] the client refuses it
  [OK] ...and the refusal is now REMEMBERED, not just returned
  [OK] ...naming the issuer that claimed to have signed it
  the token is not a well-formed JWT

@cLastWhy holds exactly one of these. Ten in a row — a botched key rotation, or a forged-token campaign — read identically from any single call to Why().

The distinction the provider could not make

The catalog has always had an oauth.code.replayed kind: an authorization code was redeemed twice. The provider could not honestly emit it. Its refusal read "unknown or already-used authorization code", because a spent code is deleted, so a stolen code replayed and a code nobody ever issued arrive as exactly the same miss.

So the provider now remembers the codes it has spent — bounded, and they are single-use and short-lived, so an old entry has nothing left to protect:

Haro
aAuth = oOp.AuthorizeAt([ :clientId = "shop-app", ... ], "dana", $NOW)
oOp.ExchangeCodeAt("shop-app", "s3cret", aAuth[:code], ...)   # succeeds
oOp.ExchangeCodeAt("shop-app", "s3cret", aAuth[:code], ...)   # the replay
oOp.ExchangeCodeAt("shop-app", "s3cret", "a-code-nobody-issued", ...)

illustration

Sortie promise
  [OK] the second is refused
  [OK] ...and recorded as a REPLAY, the error it is
  [OK] a code that was never issued is NOT called a replay
  [OK] ...it is a rejected client, the warning it is
  the answer to both is identical -- only the ledger tells them apart

The refusal returned to the caller stays byte-identical, so an attacker learns nothing from the difference. The distinction exists only in evidence — which is exactly where it belongs.

One place, not nine

There are nine Status(401, ...) sites inside stzAppServer.ring alone, and every application adds more. Wiring each one would have produced a seam that saw the library's own refusals and missed the ones an application wrote — the more interesting half.

The verdict is read once, where the response is final:

Haro
oSrv.Get_("/vault", func oReq, oResp {
    oResp.Status(403, "Forbidden").Json([ "error", "not yours" ]) })

illustration

Sortie promise
  [OK] a real 403 came back over a real socket
  [OK] ...and the refusal reached the ledger from a USER's route
  [OK] ...naming the method and path, and nothing from the headers
  GET /vault
  [OK] a 200 writes nothing -- a 404 is a typo and a 500 is a bug

Only 401 and 403 carry a security meaning. And nothing from the request body or its headers is written, so a credential in an Authorization header cannot reach the ledger through this door.

What a counter cannot say

Haro
oRl.SetLimit("scraper", 1, 1)
oRl.Allow("scraper")   # x3

illustration

Sortie promise
  [OK] the counter says how many were shed
  [OK] ...the ledger says when each one was
  [OK] a shed is INFO -- the limiter working, not a failure

A steady trickle and a sudden flood read the same from RejectedCount(). The times are what turns a burst of sheds into a detection — and a shed is deliberately info, because shedding is the limiter doing its job.

The refusals that were only ever a return value

Cross-world crossings, federated calls, the production door, and the bare secret's reveal gate all knew and all forgot. The federation's four refusal points — an SSRF-shaped path, a facet nobody offers, a missing bond, the capability lattice — each wrote @cWhy and returned ""; they now share one door, which also removed four copies of the timing bookkeeping.

  [OK] an unbonded caller is refused
  [OK] ...and so is an SSRF-shaped path -- the same door
  unsafe path rejected (must start with '/', no CRLF): http://evil.example/x

The production door records one event per unsound service, not one per refused deploy, so an incident's Subjects() names which services were still fake:

  [OK] one event per UNSOUND SERVICE, not one per refused deploy
  service:shop/payments -- sandbox-in-production: still bound to a SANDBOX
  in a production phase -- a fake must never ship

...and it is recorded at the refusal, never inside MayGoLive(). That one is a preflight anyone may ask speculatively, and a predicate that writes evidence fills the ledger with questions instead of events.

The bare secret's gate refuses by raising, which reaches whoever wrote the try/catch and nobody else:

Haro
try
    oSec.Reveal(oBot)      # an LLM actor: inference-only, sandboxed
catch
done

illustration

Sortie promise
  [OK] the raise still fires
  [OK] ...and the attempt survives it
  [OK] the secret's NAME is written -- never its value
  [OK] ...and the value appears nowhere in the exported chain

The guard's own empty catch is exactly the caller that used to erase the attempt.

A verb, not a side effect

ReachesEffectful() was the obvious place to record an escalation, and it is the wrong one. An investigation asks the reachability questions dozens of times while reconstructing an incident; a question that writes evidence chains the investigator's own curiosity.

So the recording is a deliberate act:

Haro
oGr.AddActor("billing-agent", "sandboxed")
oGr.Uses("billing-agent", "deploy-tool")
oGr.Grants("deploy-tool", "effectful")
aEsc = oGr.AuditEscalations()

illustration

Sortie promise
  [OK] the query alone writes NOTHING -- an investigator may ask freely
  [OK] the audit finds the sandboxed actor that reaches effectful
  [OK] ...and records it
  [OK] the PATH is named, not merely the risk
  a sandboxed actor reaches effectful via billing-agent -> deploy-tool -> effectful

"billing-agent can reach effectful" names a risk. "billing-agent → deploy-tool → effectful" names the edge to cut.

The seam that changed the vocabulary

Sessions end, and the row that proved they existed is deleted. So the event is written first:

  [OK] revoking it is remembered
  [OK] the TOKEN is never written -- it is a bearer credential
  [OK] ...the user is, because that is what an investigation asks

Writing the token into evidence would turn the evidence file into a way in. The user and the address are descriptors; the token is not.

But an expiring session is neither granted nor refused. No gate ran; nobody was told no. The outcome vocabulary had three verdicts and no way to say "this simply happened", so a fourth arrived — Observed — and finding a home for it exposed a live defect in two places:

Haro
def IsRefusal()
    return @cOutcome != "granted"      # both here and in Refusals()

illustration

Read that with a fourth outcome in the world and an expired session is a refusal. In a system whose entire argument is that a warning should mean something, routine housekeeping would have been counted as attacks in every pivot. Both are now positive lists:

  [OK] its outcome is OBSERVED -- no gate ran, nobody was told no
  [OK] ...so the ledger's own refusal pivot skips it
  [OK] an observed event reports itself as no refusal
  [OK] ...while a failure still is

The negative form is the bug. != granted was correct for exactly as long as there were three outcomes, and it failed silently the moment there were four.

The honest limit that does not close

  [OK] every event this guard wrote is chained
  15 events across ten newly-wired seams

Every row of the design doc's seam table now emits. What remains is not a phase — it is the permanent shape of the caveat: all of this witnesses what the library's gates decide. Application code that refuses on its own terms, without passing a Softanza gate and without answering 401 or 403, is still invisible. No amount of wiring inside the library reaches it; only an application choosing to record its own refusals does.


Ten seams, 41 assertions, one new outcome, and two defects that had been sitting in a negative comparison since I0.

Prouvé par

L'article nomme ces gardes de scénario de la bibliothèque :

Citer

Mansour Ayouni. « The Rest of the Seams ». Softanza narrations, 2026. https://mayouni.github.io/stzsite/fr/narrations/stz-security-seams-2-narration.html

Non exécuté : son code ne se charge pas tel qu'il est écrit. L'article est publié tel qu'il est écrit ; ses blocs sont des illustrations. Le code des articles est du code Haro. Là où un article a été écrit avant que la langue prenne son nom actuel, le nom a été changé et rien d'autre. Genre, domaine et série lus dans le nom du fichier, année tirée du premier commit : dérivés, jusqu'à ce que l'article porte son propre en-tête. Le fichier sur GitHub(s'ouvre dans un nouvel onglet)