SIAX Technology (sax3l)

@siax/agent-policy (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/agent-policy@0.1.0
"@siax/agent-policy": "0.1.0"

About this package

Band30 P19 (sax3l/siax-masterplan#38): a fail-closed, default-deny evaluator for siax.agent-capability-policy documents — validates a policy, and decides allow/deny for one claimed agent action (governance capability, tool-allowlist category, network egress, or secret access). Zero EXTERNAL dependencies — @siax/schemas and @siax/write-guard are workspace-internal links only. Node ESM (.mjs), Node >= 20 — same convention as @siax/schemas, @siax/doctor and @siax/write-guard.

@siax/agent-policy

Band30 lane P19 (agent sandbox / context security / rollout safety — sax3l/siax-masterplan#38): a fail-closed, default-deny policy contract for what an agent identity/session is permitted to do, plus a real evaluator for it. Zero EXTERNAL dependencies — @siax/schemas and @siax/write-guard are workspace-internal links only. Node ESM (.mjs), Node ≥ 20 — same convention as @siax/schemas, @siax/doctor and @siax/write-guard.

Why this package exists

This estate has spent a night dealing with an unidentified process that auto-approves (via a siax-bot account) and then merges-or-closes pull requests across many repos, and — a newer variant — pushes directly to main, bypassing review entirely. Root cause, already confirmed and tracked at masterplan#25: every agent session in the estate authenticates with the same Gitea site-admin credential, which can trivially Sudo-impersonate any other account (including siax-bot) via Gitea's own built-in mechanism — and that same credential is already piped into CI as an Actions secret. The incident itself is tracked, still open and unattributed, at masterplan#30.

Every pass through this estate tonight has operated under the same manually-enforced rule: never self-approve, never self-merge, never self-deploy. That rule lived entirely in prose and operator discipline. This package is that rule, encoded as something a program can actually check — packages/write-guard's sibling for this specific incident, the same way authorize-repository-write.mjs was built directly from the P015 incident rather than as a generic "authz framework" that happens to fit.

What this is

  • A schema — schema/siax.agent-capability-policy.schema.json (draft 2020-12, validated by packages/schemas/src/siax.agent-capability-policy.mjs, documented at packages/schemas/docs/generated/siax.agent-capability-policy.md). It structurally forces every policy document to:

    1. Declare a tool allowlist (read, comment, push_feature_branch, open_pr, ...) — absence is a denial.
    2. Declare all seven governance capabilities (approve_pr, merge_pr, mutate_branch_protection, mutate_secrets, deploy_production, dns_mutation, credential_rotation) explicitly, and structurally forbids granted: true without a well-formed grant record shaped like an attribution (actor, reason, grantedAt). The schema can only check the shape — it cannot express "the grant's actor must differ from subject.identityRef" as a cross-field rule, so a self-issued grant (same identity as its own subject) still validates against the schema; the evaluator is what actually rejects it (DENIED_GRANT_SELF_ISSUED, see below).
    3. Declare a network egress allowlist, defaultDeny: true fixed by const.
    4. Declare secret scopes at a none|read|write level, never a value.
    5. Declare budgets — maxActions/maxCommands/maxFiles/maxDurationMinutes/ maxTokens/maxModelCostUsd, field names matching sax3l/siax-automation's src/devos/loops/loop-run.mjs checkCaps().
    6. Reference (never hardcode) the estate's canonical Product Firewall list — standard/estate/SIAX_PRODUCT_FIREWALL.yaml, the same file packages/write-guard/src/product-firewall.mjs already parses.
    7. Name a subject the policy is issued to, structurally forbidden from being the most obvious shared-admin literals (defense in depth — see below for the real check).
  • An evaluator (src/evaluate-action.mjs) — evaluateAgentAction(policy, action, opts), a pure(-ish — it reads the committed Product Firewall YAML via @siax/write-guard unless a caller injects a fixture) decision function, fail-closed and short-circuiting on the first denial, same discipline as authorize-repository-write.mjs. Plus evaluateEgressRequest and evaluateSecretAccess for the network/secret dimensions. See the module's own header for the full step order and rationale.

  • A CLI (src/cli.mjs, bin: siax-agent-policy):

    siax-agent-policy validate     --policy policy.json [--json]
    siax-agent-policy check-action --policy policy.json --repo siax-standard \
                                    --capability merge_pr --actor-id simon \
                                    --session-id sess-1 --run-id run-1 [--json]
    siax-agent-policy check-egress --policy policy.json --host git.siax.io --port 443 [--json]
    siax-agent-policy check-secret --policy policy.json --scope gitea/read-token --level read [--json]
    

    Exit codes: 0 valid/allowed, 1 invalid/denied, 2 the command itself could not run (bad flags, unreadable file) — same three-way split as @siax/credential-scan's CLI, so a broken invocation never looks like a clean denial.

What this is NOT (read this before citing this package as "solved")

This is a prepared, consumable contract — not a deployed enforcement point. Nothing in siax-standard can:

  • Change Gitea's actual permission/impersonation model, or stop the shared site-admin credential from being able to Sudo-impersonate any account. That is masterplan#25's open root-cause item, and requires Simon's action (an admin-enforcement setting, or moving agent identities off the shared account) — it is out of this repository's reach entirely.
  • Intercept a real git push, a real Gitea API call, or a real CI job. evaluateAgentAction only returns an answer; something has to call it, and mean it, before every privileged action. As of packages/doctor's agent-policy-check subcommand (below), that "something" now exists as a reachable, tested siax-doctor-family preflight — but nothing in this estate's actual CI/ACT0/Gitea integration calls IT yet either. The gap moved one layer down, it did not close: a caller still has to know to shell out to siax-doctor agent-policy-check before a privileged action, same as it would have had to know to call the CLI directly.
  • Attribute the ONGOING incident (masterplan#30) to an actor. This package makes the shared-credential pattern detectable going forward (an action carrying actorId: "admin" is a policy violation under this contract, reportable the moment something checks) — it does not retroactively identify who has been running the rogue process.

To make this a live control, at least one of the following would need to happen:

  1. An ACT0 workflow step calls siax-agent-policy check-action (or the library function directly) before every merge/deploy/branch-protection/ secret/DNS/credential step, and is wired to actually abort on a denial — partially done in this pass: packages/doctor's new agent-policy-check subcommand (siax-doctor agent-policy-check --policy <path> --repo <r> --capability <c> --actor-id <id> --session-id <id> --run-id <id>) is that CLI preflight, wired into the same siax-doctor dispatcher and --fail-on=block exit-code convention as write-check, tested end-to-end against both fresh fixtures and this package's own real committed fixtures (see packages/doctor/test/commands/agent-policy-check.test.mjs). What is STILL not done, and still out of scope for this repository: an actual ACT0 workflow step, or siax-automation's estate doctor, invoking that command before a real merge/deploy/branch-protection/secret/DNS/ credential step and aborting on its exit code. siax-standard can provide the gate; it cannot place itself in front of Gitea/ACT0's real execution path from inside this repo.
  2. siax-automation's estate doctor (or a future credential-issuing system) is extended to issue per-session credentials that are actually SCOPED to a siax.agent-capability-policy document, rather than every session sharing the one site-admin token — the structural fix subject
    • KNOWN_SHARED_IDENTITIES here are written IN ANTICIPATION of, not yet wired to.
  3. Gitea branch protection / required-reviewer settings are configured so that an approve_pr/merge_pr denial from this contract has something real to block against — today, per masterplan#25, protection is documented to be bypassable by the shared admin identity regardless.

Items 2 and 3 were not built or wired in this pass — building them would mean inventing a live credential-issuing system or a new Gitea integration, explicitly out of scope for this bounded PR.

Consuming this from another repo

Same blocker as @siax/write-guard and @siax/schemas: publishing to git.siax.io's npm registry is blocked on a write:package-scoped token (prompts/003). Until that's unblocked, consume this via the CLI (siax-agent-policy, above), the same way @siax/write-guard is consumed as siax-doctor write-check rather than as an npm dependency.

Testing

node --test packages/agent-policy/test/*.test.mjs
# or, from the repo root:
pnpm test:agent-policy

test/evaluate-action.test.mjs proves, with real assertions (not just schema-shape checks):

  • Default-deny actually denies — a policy with no explicit grant for merge_pr (or any of the other six governance capabilities, individually) returns DENIED_NOT_GRANTED.
  • An explicit grant is correctly honored — a well-formed, unexpired grant with a matching actorId returns ALLOWED; an expired one or one whose own grantor is a shared identity does not.
  • The Product-Firewall-reference case denies regardless of any other grant (fail-closed even under a misconfigured policy) — proven twice: once against an injected fixture repo with a perfectly well-formed merge_pr grant for EVERY governance capability, and once against the REAL, committed standard/estate/SIAX_PRODUCT_FIREWALL.yaml, so the claim holds against actual estate data, not only a synthetic example.
  • Shared-identity attribution denial (admin/root/... as either the claimed actor or the grantor), identity-mismatch denial, budget vetoes, network-egress allow/deny, and secret-scope level checks.

test/cli.test.mjs exercises the real main() entry point end to end, including all three exit-code classes.

Dependencies

Dependencies

ID Version
@siax/schemas workspace:*
@siax/write-guard workspace:*
Details
npm
2026-09-16 09:35:08 +00:00
2
UNLICENSED
latest
21 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-09-16