@siax/write-guard (0.1.0)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/write-guard@0.1.0"@siax/write-guard": "0.1.0"About this package
@siax/write-guard
P000 (Emergency Estate Write Safety, Master Execution Contract):
authorizeRepositoryWrite(), a preventive, fail-closed repository-write
authorization gate, plus a privileged-override path that requires an AUD0
audit event. Zero EXTERNAL dependencies — @siax/schemas and (P012)
@siax/event-client are workspace-internal links only. Node ESM (.mjs),
Node ≥ 20 — same convention as @siax/schemas and @siax/doctor.
Why this package exists
A P015 sub-agent assumed sax3l/siax was the SIAX universal CLI repo (the
name matched what it was looking for), read the codebase, made real code
changes, and merged a PR — into what turned out to be a foreign Protosell
CRM (mirror:true, original_url pointing at github.com/sax3l/siax.git).
See
../../standard/execution-reports/SIAX_REPOSITORY_IDENTITY_EXCEPTIONS.md
for the full incident record. The failure was never "a decision function
would have said deny" — it's that nothing asked before the mutation.
This package is the thing that must be asked, every time, and the only
mutate()-invoking helper it exposes (guardedRepositoryWrite) is
structurally incapable of invoking mutate before the decision comes back
WRITE_ALLOWED.
Layout
packages/write-guard/
src/
authorize-repository-write.mjs # the core decision function (P000-B)
guarded-write.mjs # the call-order-proof wrapper (P000-B/D)
product-firewall.mjs # SIAX_PRODUCT_FIREWALL.yaml parser + lookups (P000-C)
repo-identity-from-v3.mjs # conservative siax.repo.v3 -> siax.repository-identity bridge
shadow-mirror-facts.mjs # P218: Gitea mirror:true facts, fail-closed loader + lookup
aud0-client.mjs # AUD0 event-ingestion client (P000-E) -- P012: delegates to @siax/event-client, same public API
privileged-override.mjs # the ONE sanctioned bypass path, audit-gated (P000-E)
index.mjs
test/
authorize-repository-write.test.mjs
p015-incident-regression.test.mjs # THE most important test file — see its header
shadow-mirror-write-denial.test.mjs # P218: the NO case, exercised against the real shadow mirrors
product-firewall.test.mjs
aud0-client.test.mjs
privileged-override.test.mjs
Core API
import {
authorizeRepositoryWrite, DECISION, guardedRepositoryWrite,
loadEstateRegistry, resolveEstateRegistration, DEFAULT_ESTATE_YAML_PATH,
} from '@siax/write-guard';
// P003: resolve the repo's row in the CANONICAL estate registry FIRST.
// This is a required input — omitting it denies the write, it does not
// re-open the older, weaker gate.
const estateRegistration = resolveEstateRegistration(
loadEstateRegistry(DEFAULT_ESTATE_YAML_PATH),
repoIdentity.repoId,
);
const decision = authorizeRepositoryWrite(repoIdentity, 'cl0ud.manage_tenant', {
type: 'push',
actorOwner: 'SIAX Technology AB',
estateRegistration,
});
// decision.decision is one of:
// WRITE_ALLOWED | WRITE_DENIED_PRODUCT | WRITE_DENIED_FOREIGN |
// WRITE_DENIED_AMBIGUOUS | WRITE_DENIED_WRONG_OWNER |
// WRITE_DENIED_NONCANONICAL | WRITE_DENIED_UNREGISTERED
// The call-order-proof wrapper — the only way this package performs a guarded mutation:
const outcome = guardedRepositoryWrite(repoIdentity, 'cl0ud.manage_tenant', operation, () => {
// actual mutation — git push, filesystem write, API call — ONLY reached if outcome.decision === WRITE_ALLOWED
});
repoIdentity must validate against @siax/schemas'
siax.repository-identity
schema — authorizeRepositoryWrite validates it itself as its first step and
fails closed (WRITE_DENIED_AMBIGUOUS) on any schema violation.
The estate-registration gate (P003)
P000 shipped a gate whose only estate input was
SIAX_PRODUCT_FIREWALL.yaml. "Is this repository registered in the estate?"
was simply not one of its questions — and repositoryIdentityFromV3 builds
the identity out of the TARGET repo's own siax.repo.v3.json, so a
brand-new repository could declare itself into existence with a
flattering tier and a capability claim, and nothing could contradict it.
vibe and siax-cli are the live evidence: created 2026-09-03, invisible
to every estate document for a day.
STEP 1.5 closes that. operation.estateRegistration is a record the caller
resolves from standard/estate/SIAX_ESTATE.yaml (via @siax/estate, whose
reader is re-exported here), and the step denies when it is:
| Situation | Decision |
|---|---|
| absent, or not even shaped like a registration | WRITE_DENIED_UNREGISTERED — "I could not check" is never "it is registered" |
registered: false |
WRITE_DENIED_UNREGISTERED |
| resolved for a DIFFERENT repo than the identity | WRITE_DENIED_AMBIGUOUS |
registry says writable: false |
WRITE_DENIED_PRODUCT (firewall confirmed) / WRITE_DENIED_AMBIGUOUS (firewall ambiguous) / WRITE_DENIED_UNREGISTERED |
registry's canonicalRemote disagrees with the identity's |
WRITE_DENIED_NONCANONICAL |
A later step (estate-capability-ownership) also lets the registry veto a
self-declared capability claim — but only when the registry actually
attributes capabilities to that repo. When it attributes none (most rows
today), the ALLOWED reason says so out loud rather than implying the check
was conclusive.
The eight-artifact creation contract this gate is the enforcement half of
lives in standard/repo-creation/REPO_CREATION_CONTRACT.md
(generated from @siax/estate's REQUIRED_ARTIFACTS), and the executable
proof that a rogue repo cannot write until registered is
test/p003-rogue-repo-gate.test.mjs —
a real git init repo, a real registry file, and a mutate that really
edits a file, so "denied" is proven by the file being unchanged.
Privileged override (P000-E)
import { requestPrivilegedOverride, createAud0Client } from '@siax/write-guard';
await requestPrivilegedOverride({
repoIdentity, capability, operation,
actor: 'simon',
reason: 'emergency hotfix approved verbally, backfilling audit trail',
aud0Client: createAud0Client(),
mutate: () => { /* the actual mutation */ },
});
Requires a non-empty actor and a substantive (>= 10 char) reason, and
fails closed if the AUD0 event cannot be sent — mutate is never
invoked without a proven audit record first. See
src/privileged-override.mjs's module doc for why this deliberately differs
from ACT0's own "degrade-and-continue" policy for ordinary audit events.
Honest status: whether https://api.aud0.siax.io is actually deployed
and reachable was not verified from this repo/pass — see
src/aud0-client.mjs's "HONEST STATUS" note. Tests use dependency-injected
fake fetch, same pattern as act0/src/aud0-client/__tests__/client.test.ts.
Consuming this from another repo
@siax/schemas/@siax/doctor are already set up to be consumed via
git.siax.io's own npm registry (.npmrc's @siax:registry=) — but actually
publishing there is blocked on a write:package-scoped token (see
prompts/003). @siax/write-guard has the exact same blocker; it is not
yet installable as an npm dependency from another repo (e.g.
siax-automation, cl0ud, top0).
Until that is unblocked, the supported cross-repo integration point is the
CLI, not the package: siax-doctor write-check (see
../doctor/README.md) wraps this package's
authorizeRepositoryWrite as a subprocess any estate automation tool can
shell out to before performing a mutation — no package-registry dependency
required, the same way siax-doctor's other ten subcommands are already
consumed as a binary. This pass did not modify siax-automation itself
(it has zero mutation code paths today — see BEFORE_STATE.md §7 — so there
is nothing there yet to retrofit), nor did it locate/wire the actual
write-capable estate automation (release tooling, multi-repo migration
scripts) the P000-E prompt asks about — that discovery work is flagged as a
genuine, unclosed gap, not silently assumed done.
P218 — the shadow-mirror gate (STEP 0.5)
authorizeRepositoryWrite() always had a shadow-mirror rule (STEP 1: deny when
writePolicy.mirrorDirection === 'external-is-canonical'). P218 asked what that
check looked like when the answer was NO, and found it could not fire:
grep -rn "external-is-canonical" . -> tests, schema/, generated docs ONLY
ZERO data files
Both identity bridges in this package hardcode the permissive value
(repo-identity-from-v3.mjs:35, product-firewall.mjs:199), so nothing in the
estate ever asserted the denying one. Measured consequence: a write against
siax-coming-soon — a live GitHub pull mirror serving four production domains —
returned WRITE_ALLOWED. That pre-fix decision is pinned as a regression in
test/shadow-mirror-write-denial.test.mjs.
STEP 0.5 now reads the authoritative Gitea fact instead of trusting the caller's assertion:
- source:
../../standard/estate/SIAX_SHADOW_MIRROR_FACTS.json, regenerated byscripts/refresh-shadow-mirror-facts.mjs(289 mirrors of 389 repos at 2026-09-05); - a listed repo is denied
WRITE_DENIED_FOREIGNat stepshadow-mirror-gitea-fact, regardless of classification,writable, owner or capability; - it lives inside the existing entry point, so
guardedRepositoryWrite,assertRepositoryWriteAllowedandsiax-doctor write-checkinherit it with no opt-in; - fail-closed: missing, malformed, empty or >30-day-old snapshot denies
(
shadow-mirror-facts-unavailable). An empty mirror list is treated as a failed read, never as "there are no mirrors".
Refresh the snapshot with:
GITEA_TOKEN=<read-scope token> node packages/write-guard/scripts/refresh-shadow-mirror-facts.mjs
GITEA_TOKEN=<read-scope token> node packages/write-guard/scripts/refresh-shadow-mirror-facts.mjs --check # drift check
opts.skipShadowMirrorCheck exists only so unit tests can exercise later
steps in isolation. It must never be set on a real write.
Human-readable register, including the cl0ud.app ownership finding:
../../standard/estate/SIAX_SHADOW_MIRROR_REGISTER.md.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @siax/estate | workspace:* |
| @siax/event-client | workspace:* |
| @siax/schemas | workspace:* |