Repository navigation
Guardana Control 0.16.0
Upgrading from 0.15 changes four things an operator must act on.
approvals.authority (memory, directory or signed) replaces
approvals.provider and has no default; provider: file becomes
authority: directory, and approvals.hold_journal_dir is required beside
every approvals directory. The approvals directory is format 2.0 and a
1.0 one is refused: let its holds expire, within approvals.ttl, then
empty it. /healthz answers schema 2 without run ids; a reader that needs
them queries health.socket. An identifier the contract's rule refuses, such
as a project_id with white space at either end, is refused when the
configuration loads. The TCP export still works with a doctor warning and
is removed in 0.17: set export.endpoint: unix:///path and
export.collector_uid instead.
New: under approvals.authority: signed a plane takes authority only from
operators a signed roster names. A held call runs only on an answer an
operator signs. The pause file (3), the run records (3.0), the stop list
(2.0) and its route and lifts (v1alpha2) are signed and held to floors
the plane raises, and the plane refuses to start where its own account could
write any of them
(ADR-0073,
ADR-0075,
ADR-0076,
ADR-0077).
To move a plane to signed, open its runs again in a new, empty directory
and start a new stop list. state backup and state restore copy a stopped
plane's floors, runs, pause file and stop list, and a restore only raises a
floor (ADR-0078). A pause can name
one run, agent or principal. Under signed the plane's own account still
raises the roster's floor, so it can withhold a newer roster within the held
one's lifetime. The hook profile's qualification was not run again for this
release.
Added
- Under
approvals.authority: signedroster operators sign the reaction
documents, bound to the plane's route file or stop list: anadminsigns a
reaction-route/v1alpha2route withroute sign --key, naming a
project_idand ascopeon each rule; an operator signs stop list2.0's
header withstops init --key, at any--serialwhile the plane's floor
holds none or, carried, at the next serial naming the old header, the old
list held to the plane's floor with--stops-floor; and an operator signs a
reaction-lift/v1alpha2lift withstops lift --key, naming the header it
lifts from.stops init,liftandlisttake--rosterand
--root-keyand judge what is on disk through the roster before they sign;
each signing says on standard error what it signed. The plane holds the
header and the part of the list it accepted to a stop list floor in the new
reaction.stops_floor_dir, whichstops floor initmakes for the
listener's tenant, and refuses a list cut back below it, also at a read
that found nothing new. At every read it asks the operator ports again,
within one poll interval, about the route, the header and each lift's
signer, so a revoked operator, a route admin without the grant, or ports
that do not answer in time make the stop state unknown;
/healthzgives the header serial and
health.socketalso who signed the header and the route (ADR-0077). guardana-control state backup <config> <dir>copies a stopped plane's
floors, runs, pause file and stop list into a new owner-only directory
under abackup.jsonmanifest0.1, never the spool, the hold journal,
the approvals store or a key.state restore <config> <dir>refuses a
backup of another plane or a tampered one, restores only the bytes it
verified, holds the spool's and the journal's locks, checks every class
before its first write, raises every floor to the higher of the two,
stamps every plane floor, one only live holds included, at the restore
time rounded up to the second so the old statement confirms nothing,
closes every run, a run named only by a close the backup carried
included, and joins taint under each root's lock, a state lost beside
its lock or record as the worst. Underdirectoryit keeps a
stop list that holds the backup's, carries the backup's into a new list
where none survived, and refuses two histories; undersignedit writes
no operator file, raises the stop list floor in its own order, and names
the files to put back (ADR-0078).- Under
approvals.authority: signedthe pause file is version3, signed
by a roster operator grantedoperateover the plane's tenant and project:
pause init --key <k> --tenant <t> --project <p>makes one bound to its
own path, andpause addandpause removewith--key,--rosterand
--root-keycheck the file they start from against the roster before they
sign it at the next serial. The plane checks it at every read and raises a
floor in the newpause.floor_dir, whichpause floor initmakes, so an
older signed file put back is refused after a restart too, and a copy from
another path is refused; planes that use one path on two hosts must differ
in tenant or project, or a copy between them is taken./healthzgives the serial and the floor
underpause.signed,health.socketalso the signer, anddoctorchecks
the file without raising the floor (ADR-0075). - Under
approvals.authority: signeda run counts only from a record3.0
an operator grantedoperatesigned for the run's tenant and project and
for the plane's ownruns.dir:runs openandruns closetake
--key <k> --roster <r> --root-key <p>,runs closealso the plane's
--tenantand--project; they refuse to sign over a record the roster
does not vouch for, of another tenant, project or directory, or unsigned,
and say on standard error what they signed. A plane refuses arecords/
or a record its own account owns. When the plane takes a closed record it carries the close into
state/, at a call or at its start, so the open record put back is
refused; a close whose signer may no longer operate there is carried too. A
closestate/does not take is held and refused as carried, written at
the next signed resolve, start or stop that can, and counted as
carry_failed; holding 64, the plane refuses every signed resolve, and a
start that cannot carry one refuses, naming the run. A held close protects
only its own plane. A record
copied from another path counts in neither; planes that use one path on two
hosts must differ in tenant or project. The plane writes a root's state at
its first admitted call and blocks the root's calls when that state is later
removed.superviseandreactread a signed layout only with
--roster,--root-key,--tenantand--project, and take a record
only as the plane would./healthzcounts the records judged and refused by cause under
runs.signed, anddoctorcounts the records by state and by signer
(ADR-0076). pause add --run <id>,--agent <id>and--principal <id>pause one
opened run, or every run under a root, one agent or one principal. A run
is named by its id fromruns open, and any other spelling is refused. Each is
matched against what the plane vouches for: the opened run the listener
resolved from its token, and the agent and principal its configuration
names, never a run id or a client name the client sent. A call under no
opened run is paused by every run entry./healthz'spause.coveragesays per entry whether it coversall,
someornoneof the plane's calls, andpause.coverage_causeswhy a
run entry covers none: its run is not inruns.dir, is another project's,
expired or closed.doctorcounts the three and names each agent or
principal entry that is not the listener's and each such run entry.guardana-gateway spool drain <dir> <out>moves a stopped plane's spool
quarantine into a new owner-only file of JSON Lines, synced before the
quarantine is emptied, so its bytes stop counting against
evidence.max_bytes. It is refused while a plane holds the spool.guardana-control runs prune --expired-before <time> --archive <dir> <runs-dir>removes whole run trees whose every run expired before the
time, after copying their records and state into a new owner-only archive.
A tree whose root is live is left alone. One whose root expired is kept,
named, and the command exits 1, when a run under it did not expire, a
plane holds its state lock or a file of it cannot be read.approvals.authority: signed: a held call runs only on an answer an
operator of the trust roster signed (approval-answer/v1alpha1), filed in
approvals.inbox_dir, whose every member matches the hold and whose
signer's grants let them approve the call's tenant and project. It reads
trust.*andoperators.*;doctorand/healthzreporttrust,
operators.authenticator,operators.authorizerandapprovals.authority,
andpipeline_answers_refused_totalcounts the answers it did not take, by
cause (ADR-0073).approvals approve|reject --key <file> --inbox <dir>signs an answer
with an operator's key from the plane's own record of the hold and files it
in the inbox, never replacing a file; a key file the group or others may
read is refused.console --inbox <dir>names the inbox in the command
each signed hold shows, and without--approver-idonly reads, refusing to
start as the plane's account.devruns its plane undersigned, with a
roster and an operator key of its own, and printsinbox,operator_key
andapprove_command.export.endpoint: unix:///pathwith the requiredexport.collector_uid
sends the evidence over a Unix socket. On every connection the plane reads
the peer's account (SO_PEERCREDon Linux,LOCAL_PEERCREDon macOS) and
writes nothing, the credential included, to another or to one it cannot
name; the records stay in the spool, the attempt counts as
exporter.retried_peer, and each cause is logged once.
guardana-gateway collect --listen unix:///pathcreates such a socket and
prints theexport.collector_uidto set;--listen host:portworks as
before. Other platforms refuse both keys at load.health.socketserves the whole health answer, run ids included, on a
Unix socket that answers only the plane's own account and
health.socket_uid; any other peer is closed unanswered. The socket is
0600, or0666withhealth.socket_uid, so that account can connect
and the peer's account alone decides. Anything at the
path but a stale socket of the plane's account is refused, and the plane
removes its socket when it stops.doctorwarns of every TCPexport.endpoint, naming the unix form.
Changed
- Breaking: under
approvals.authority: signedav1alpha1route, a
1.0stop list, a lift of the route's lift key andreaction.public_key
are refused;reaction.stops_floor_diris required withreaction.route;
the plane refuses to start when its own account may write the route file,
the stops directory or either's directory, and refuses at the read one that
account owns.reactneeds--approvalsand will not run as the account
that owns it. A1.0list is not carried into2.0: start a new list.
Nothing changes undermemoryordirectory. - Breaking:
/healthzanswers"schema": "2"and names no run:stops
carries the countsunheldandunread, and leaves outentries,
unheld_runsandunread_runs, which onlyhealth.socketanswers. A
reader of the old shape finds noschemamember. - TCP export stays in this release with a
doctorwarning and goes in 0.17:
over plaintext, whoever holds the port while the collector is down
receives the credential. Move toexport.endpoint: unix:///path. - Breaking: under
approvals.authority: signedruns.dirholds
records/, the operators', andstate/, the plane's; a runs directory of
unsigned records, and a2.0record inrecords/, are refused. The plane
refuses to start when its own account may writerecords/or a directory
on its path, or ownsrecords/, andruns openandruns closewithout--keyrefuse the
directory, naming the flags. Open the runs again into a new, empty
directory with--key. - Breaking: under
approvals.authority: signeda pause file of version
1or2blocks every call,pause.floor_diris required with
pause.file, and the plane refuses to start when its own account may write
the pause file or a directory on its path, and refuses a file that account
owns at every read.devkeeps its pause file inpause/.pause addandpause remove
without--keyrefuse a signed file, and the console writes no pause under
signedor into a signed file, naming the command that signs it. - Breaking:
approvals.authority(memory,directoryorsigned)
replacesapprovals.providerand has no default. A configuration without
it, or with the old key, is refused naming both;provider: filebecomes
authority: directory.approvals.hold_journal_diris required beside
every directory./healthzreportsapprovals.authorityin place of
approvals.provider. - Breaking: the approvals directory is format
2.0: a record carries no
resolution and takes the held or the answered name only. A1.0directory
or record is refused with a message naming both versions; empty the
directory, whose holds expire withinapprovals.ttl.approvals listand
the console no longer print a resolution. A record is2.1: it names the
authority holding it and the call's tenant and project. Undersignedthe
plane writes its files0640, for an operators' group to read. - Breaking: a claim (
approvals approve|reject --approver-id, or the
console) for a hold undersignedis refused naming the signing command,
where it was written and ignored; the console answers no signed hold. On a
devplane the page shows the hold and the command, and approves nothing. operators roster signrefuses, writing nothing, when--outchanged
between its first read and the replace under the lock.- The pause commands write pause file
2; a plane and the commands read1
as before. - Breaking:
project_id,tenant_id,environment, the
listener.principal.*andlistener.agent.*keys and an upstream's
tenant_idandenvironmentare refused when the file loads, naming the
key, if the contract's identifier rule refuses them, white space at either
end for one; such a configuration loaded and had every call refused. Two
upstreams with one name are refused at load, naming both, rather than when
the gateway starts.adapters/mcp.Newandadapters/enforce.Newrefuse the
same values, any string of the listener's identity, and aTRANSACTor
IDENTITY_OR_ACCESStool while the listener's principal has no tenant.
pkg/contract.CheckIDis the one rule for a required identifier
(ADR-0074). - The gateway applies the
read_only,restrict_resourcesand
deny_external_sinkobligations itself, for every adapter, before an
approval is asked for. The enforcement API and the hook bridge, which denied
every call under a rule with one of them, now run a call the check holds
for;shorten_timeoutstays aDENYthere. Onenforce_api,
restrict_resourcesneedsresource_fromon the tool, or the empty
resource id is refused. Over MCP a failing check is now the gateway's own
DENYwithOBLIGATION_NOT_UNDERSTOOD, recordedACTION_BLOCKEDwith no
ACTION_STARTED, and a call that would be held for an approval, under
APPROVEor by aREQUIRE_APPROVALrule, is denied instead.
An adapter that declares a type the gateway applies is refused at start
(ADR-0067). - The release workflow makes its draft release before the build, through
scripts/draft-release.sh, and proves the release list and the upload host
serve it before anything is uploaded; a tag that already has a release, or
a draft of the same name, is refused, a draft a failed run left included
(RELEASING.md).
Fixed
- A long-running plane no longer reaches
approvals.max_recordsand refuses
every hold: it forgets expired approval records and ledger marks older
thanapprovals.ttlevery quarter ofapprovals.ttlwhile it serves, not
only at start. A sweep that fails is logged and counted under
approvals.sweepin/healthz.
Security
- Under
approvals.authority: signedany valid rejection wins, a rejection
counts only from an operator who may approve, whatever time it names. An
answer the roster or the authorizer could not judge, a batch read in part,
or an inbox file for the approval that cannot be read leaves the call
INDETERMINATErather than reads as no rejection. A start under it refuses
a configuration file or trust root key file the plane's account could
change and any key set in the environment. A spend records the
authenticated approver and the answer's digest in the plane's ledger only. - Only the plane marks an approval spent or given up, in a ledger (
1.0)
underledger/inapprovals.hold_journal_dir, under the journal's lock
and owned by the plane's account. A file an approver writes into the
approvals directory can no longer make the plane read an approval as spent
or given up; a name that once meant either is refused as foreign. A hold
the plane settles at start leaves that directory, soapprovals listand
the console no longer offer it.
Image: ghcr.io/guardana/control-gateway@sha256:e3dd8048ef0d0dd4168192907d9702c6587d83df053762a8c67c03f2a285bbba