Skip to content

Guardana Control 0.11.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 18:34
· 111 commits to main since this release
Immutable release. Only release title and notes can be modified.

A tool outside MCP can now ask for a decision before it acts and report how
the call ended, through the experimental enforcement API (ADR-0051). The
same policy, approvals and trail apply as to an MCP call. A reference client and
conformance cases that a client in another language can run come with it.
REPEATED_DENIAL is version 2, which needs a signed route upgrade (see
Changed). Fixes from a code audit bound several waits and refuse records
another account could rewrite. From this release a version is a plain
X.Y.Z without the -alpha suffix
(ADR-0025); the
project is still alpha, and the 0.x series still makes no compatibility
promise between minor versions.

Added

  • The enforcement API contract, guardana.control.enforce.v1alpha1: the
    admit, complete and abort messages a tool outside MCP exchanges with
    the plane as JSON over HTTP on a Unix socket (ADR-0051). Unstable and
    outside the v1 promise; buf breaking does not compare it.
  • guardana-gateway run serves it under listener.kind: enforce_api, with
    listener.socket, listener.execution_lifetime (1s to 1h, no default),
    listener.max_body_bytes (1 KiB to 256 KiB, default 128 KiB),
    listener.max_open_per_run (default 16, below approvals.max_open) and a
    tools table that classifies each tool by name. Such a plane needs
    runs.dir and refuses upstreams, overrides and the keys only an MCP
    listener reads; an MCP listener refuses the new keys. A sweep abandons
    expired handles. A stop abandons every open execution before the spool
    drains. /healthz reports the adapter's counts under enforce, and
    doctor checks an enforcement API plane.
    A guide shows a tool of your
    own decided through it.
  • adapters/enforce, its adapter, builds a tool's envelope from the run
    token and the operator's tool table. A handle is single use and expires.
    The adapter refuses a duplicate request key, closes an expired execution as
    abandoned and limits a run's open executions. It blocks a rewrite that
    moves the decided resource, and answers RECORDED only once the closing
    record is written. It declares cooperative enforcement and reads a
    resource and a run token the way the MCP adapter does
    (internal/adapterkit).
  • internal/enforceclient, the reference client, runs a tool only with the
    bytes an EXECUTE answer authorized. It aborts on an obligation it does
    not know, completes UNKNOWN when the tool cannot tell how the call ended,
    and never sends a request twice. testdata/enforce/ holds the client and the receiver
    conformance cases, each with a negative control.
  • The coverage inventory kind enforce_tool: a tool an enforcement API plane
    classifies is "decided, not enforced" in every mode, since a cooperating
    client, not the plane, keeps it from its effect.
  • The pipeline can close an execution nobody completed or aborted as failed,
    with result unknown and the new reason code EXECUTION_ABANDONED. It never
    closes one as a success and does not halt the plane for it. A byte mismatch
    a client reports through a cooperating adapter blocks only that run's
    material calls; one reported through an inline adapter halts the plane.
    Metrics count them:
    pipeline_runs_halted, pipeline_abandoned_total,
    pipeline_sink_failures_abandoned_total, pipeline_halted_mismatch and
    pipeline_halted_sink.
  • Control and supervision
    explains the 1.0 goal and what the alpha does today: observation across
    connected channels, procedure checks and reactions within scoped authority.
    It keeps the AuthZEN, enforcement and operator API roles apart.
    Backlog task B28 plans continuous supervision.
  • Planned requirements (B23–B27, proposed ADR-0050): previewing MCP setup,
    qualifying coding-agent hooks, reviewing tool-definition changes,
    simulating candidate policies and reviewing findings with an incident
    export. None is built.

Changed

  • On an enforce_api plane /metrics writes the enforcement API adapter's
    counts (enforce_*) and the flag listener_enforce_api, and leaves out the
    MCP adapter's adapter_* metrics. doctor checks the socket's directory as
    run does, and judges pause entries against the tool table.
  • approvals.max_held_per_run (default 16, only with runs.dir, below
    approvals.max_held) bounds one run's held requests; an expired hold no
    longer counts against its run. A configuration with runs.dir and an
    approvals.max_held of 16 or less no longer loads until
    approvals.max_held_per_run is set below it. A tool's provider and resource_type are
    bounded at 1024 bytes. Under policy.fail_open_read an enforce_tool line
    says a read runs undecided while the policy is unavailable.
  • The configuration loader refuses a tool name over 1024 bytes, a
    resource_from the adapters refuse, and a socket path over 103 bytes.
  • examples/evidence-report reports a timeout or unknown outcome as
    uncertain, not failed, exits 1 on it, and raises effect_uncertain.
  • REPEATED_DENIAL is version 2. When some denials it would cite are in doubt
    (a broken chain or an event id read with two contents), it cites only the
    clean ones and stays CONFIRMED if at least max_denials are clean;
    otherwise nothing changes. Its finding ids change with the version.
    route sign, react, the stops writers and a plane refuse a route naming
    rule_version "1". To upgrade, set it to "2", raise the route's serial
    and sign it, run stops init --route <new> --public-key <file> --carry <old dir> --carry-route <old route> <new dir>, point reaction.route,
    reaction.stops and react at the new route and list, and restart the
    plane. The carry keeps every unlifted stop, at version 1. A lifted run whose
    denials still meet the rule is stopped again by its version 2 finding, which
    has another id, and is lifted again. Only --carry-route reads the old
    version.
  • The adapter capability Block is now Enforcement: none, cooperative or
    inline. ENFORCE, APPROVE and LOCKDOWN need at least a cooperating
    adapter, and the MCP adapter is inline; doctor and the modes table name
    cooperative and inline enforcement apart.
  • Two approvals from the security maintainers now cover every file ADR-0008
    says the gate is built from (all of scripts/ and .github/, the
    Makefile, the linter and buf configurations, go.mod and go.sum) and
    the gateway's wiring of its decision point, adapter, approvals and stop. A
    test holds the declaration to the record's list.
  • README and the home page show exact-action approval, changed tool definitions
    and a denied refund retried through another tool, with their coverage limits
    and links to the policies and runnable example.
  • README, the home page and the social card lead with observing agents and
    controlling how they act. The 1.0 goal is marked planned beside what the
    alpha does, and the declared repository description says the same.

Fixed

  • scripts/github-settings-check.sh and the bootstrap, run by macOS's bash
    3.2 on a loaded machine, could build a declared ruleset as nothing and
    report it unknown: bash 3.2 fails a builtin's write to a pipe that a child's
    exit interrupts ("printf: write error: Interrupted system call"). The
    declarations are now printed through cat, which nothing interrupts.
  • /healthz could show the spool drained while the exporter's
    acknowledged count did not yet include what it had released, since it
    read the exporter first and the exporter counted a release after making it.
    On a loaded machine guardana-gateway scenario run then failed a foreign
    trail file with the wrong reason. The spool is now read first, and a release
    is counted under the lock the exporter's counters are read under.
  • guardana-control notify could count a delivered alert as failed with
    "killing the program's process group: operation not permitted". On a loaded
    macOS host the kernel can refuse the program's group with EPERM for a moment
    after its leader was reaped. It is now asked again for up to a second; a
    group still refused after that is reported as before.
  • Opening a findings log, an observation log or a trail file cut a torn last
    line by the length read before the file was locked. When a second writer
    opened the same file, cut the tail and committed a write between the first
    one reading the length and taking the lock, the first then truncated that
    committed write. Open now reads the length and judges the file again once
    it holds the lock.
  • The records, markers, state and lock files under an approvals, a runs and
    a hold journal directory were read when a group or the world could write
    them, and all but the hold journal's also when another account owned them.
    An account with write access to such a file could rewrite it in place,
    granting an approval, extending a run or washing a root clean. Each is now
    refused as a file that is not the directory's.
  • A hold journal entry was read whatever schema version its approval carried.
    An approval of another major, or with none, now leaves the entry unreadable.
  • Closing an approvals handle while a call ran on it was a data race on whether
    the handle was open.
  • A pause, a stop or a halt that came while an opened run's root was being
    raised was missed, and the call was recorded as started anyway. The plane
    now checks them again after a raise that wrote, and a halt raised after
    admission also blocks a material call before a resume spends its approval
    and before it starts.
  • One opened run can hold at most 16 requests for approval, so it cannot fill
    approvals.max_held for every other run; a hold past that is blocked
    EVIDENCE_UNAVAILABLE, like one past approvals.max_held.
  • /healthz names only the plane's own halts: a mismatch that halted one run
    no longer sets halt.executed_args_mismatch, and halt.evidence_unwritable
    is set beside it when both stand.
  • A client's abort whose record the sink refused counts as a lost closing
    record, so run exits 1, as a lost completion did.
  • make quality runs every check even when make is told one is up to date
    (-o, --old-file, or either in MAKEFLAGS or GNUMAKEFLAGS), and refuses
    a MAKE or GO from the environment, any command-line variable but
    GO=go, -e, a FUZZTIME or RANGE from the environment, a makefile read
    beside the Makefile, and an --eval or -E string for any target; its
    second make starts with none of the first's flags, and CI clears make's
    flag variables before running it.
  • The test report refuses a passing package in which a test started and never
    reported a result, as when the test ends the process with exit status 0. A
    test that writes its own pass before it exits still hides the tests after
    it; review catches that.
  • The documentation checks read the commit's own file list in a gate-commit
    export instead of a walk that skips dot-directories.
  • The release gate refuses a commit that is not main's head, and refuses
    unless main's newest CodeQL analysis is of the commit being released.
  • guardana-gateway waited at start, with no listener bound, on an upstream
    that took the connection and never answered its initialization. The
    connect is now bounded by upstream.list_timeout, and the configuration
    reference says so.
  • runs open, runs close, runs list and supervise --runs waited without
    a bound for another operator command's lock while opening the directory;
    their ten-second bound now covers the open too.
  • supervise read a request whose events name two runs as one run's request.
    Such a request is now in doubt and confirms nothing.
  • A floor store that stopped answering kept one call per policy poll waiting
    in the background; calls to it now take turns, so at most one waits.
  • A raise of a policy floor could wait out its deadline while reads of the
    same floor directory in the same process ran back to back, since each read
    took the lock again before the raise next tried. Reads that start while a
    raise of the process waits now go after it; across processes the lock
    still favours no one.

Image: ghcr.io/guardana/control-gateway@sha256:e556ba3f4ca62614ce6e55678b437dd16a0b1b2ee6fe7160ca611f8e31654793