SIAX Technology (sax3l)

@siax/write-guard (0.1.0)

Published 2026-09-16 09:35:08 +00:00 by admin

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 sentmutate 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 by scripts/refresh-shadow-mirror-facts.mjs (289 mirrors of 389 repos at 2026-09-05);
  • a listed repo is denied WRITE_DENIED_FOREIGN at step shadow-mirror-gitea-fact, regardless of classification, writable, owner or capability;
  • it lives inside the existing entry point, so guardedRepositoryWrite, assertRepositoryWriteAllowed and siax-doctor write-check inherit 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:*
Details
npm
2026-09-16 09:35:08 +00:00
0
UNLICENSED
latest
42 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-09-16