NEWS
Rama main · réplica de lectura del repositorio original.
Quilzo user-visible changes.
Newest first. Only what somebody running this would notice; the commit log
has the rest.
Unreleased
==========
** Governance
*** A publish of the draft, from the command line or over MCP, puts live
exactly the commit its checks were asked about; a save landing while
they ran went live unchecked. For a day after an administrator is
removed, the one left cannot approve anything alone (their own
requests, playbooks, policy changes, installs, the first token) unless
a second administrator holds the role again; a store that only ever had
one administrator is unaffected. Approving a group mapping that makes
administrators shows every live provisioning token, who made it and how
it was approved (People, or quilzo pending show), and a token made
while such a mapping waits takes two administrators. A chatbot asked
inside an agent's run is charged to the run's budget and its person.
** Who has access
*** Quilzo reviews the access only it grants. Every
access.review.every (ninety days, or the organisation's AC-2 period
where that is shorter), an administrator keeps or revokes each grant
made by hand, what each identity provider group means here, and each
long-lived token: quilzo access review start, items, decide and close,
or Review who has access on People. Access unused for ninety days is
listed first and proposed for revoke; keeping privileged access needs a
reason; nobody decides their own access or a group they are in; the
reviewer is a person, signed in with two factors on People; a revoke
takes effect at once and never leaves the store without an
administrator. Closing writes the evidence, and the posture finding
access.review-due, with a mail to the security contact, says when one
is overdue.
*** When the identity provider says a session is over, it is. quilzo
signals connect binds an OpenID Shared Signals stream (CAEP and RISC)
from the OIDC or SAML provider people sign in with, received at POST
/sso/signals: a revoked session or changed credential ends that
provider's sessions, a compromised credential also asks the person to
sign in again and tells the security contact, and a disabled account
is suspended as SCIM suspends one (and restored when enabled again).
The last administrator is never suspended by a signal; at most 20
people an hour are acted on, and the rest wait for the provider to
send them again. quilzo signals status and the Sign-ins
screen show the stream, and signals.stream-silent says when it has
stopped answering. The shield can contain it ("signals").
** Security
*** A model's draft never goes live unapproved. The admin's Publish
put an agent-written draft live when no approval count was set, and
the command line published past an approval policy it could not read;
every surface now holds a model's draft for a person, holds any draft
for the people approval.required_humans asks for, and refuses to
publish when the policy cannot be read.
*** Access is decided as it stands now: both content APIs re-read the
access rules, /mcp and token exchange see a revoked token at once, and
a token's last use there is written down. A throttled address slows
only its failures, so one guesser behind a proxy locks nobody else
out. A passkey whose counter went back is refused and recorded as a
copy. A permission granted alone reaches the commands its work needs.
A group mapped to auditor grants it until a day (scim map --until). A
reader lists their own grants and tokens; whoever may grant, everyone's.
*** SCIM and Shared Signals never leave a store without someone who can
grant access, even when the last administrator is deactivated and
renamed in one request; events are acted on in the order they were
issued, a replayed event touches only sessions open when it was sent,
and events past the hourly cap wait instead of being lost. The scanner
finds every kind of secret Quilzo makes.
*** One CSV guard for every CSV Quilzo writes, the subscriber's address
included, and an upload check that finds a formula wherever a
spreadsheet would run it. Importing thousands of same-titled items
takes time in proportion to them, not minutes.
** Found by using it as people do
*** The chatbot shows a sentence as verified only when it says what its
passage says: one whose figure, time of day, duration, price, "not"
or opposite word turns the passage around is taken out before the
visitor sees it. Research reports are checked the same way.
*** A webhook whose receiver is restarting gets the event: the second and
third attempts wait one and then four seconds, and a receiver that
answers 429 or 408 is tried again like one that answers 5xx. An
integration tool that says it failed makes quilzo integrations call
exit non-zero and is a failure on the record; a call refused before it
is sent is recorded as denied.
*** Asking any command for help (-h, -help, --help, --h, with or without
a value) prints that command's help and does nothing else; quilzo init
--help created a store, and brand init --help and token keygen --help
wrote files. auth revoke's help names both its arguments, auth list,
token list and token stale keep long names in their columns, and the
read-only API's refusals name the flags that turn writes on.
*** Single sign-on as organisations run it: SAML refuses an address the
provider says it has not verified (saml add --verified-attribute; the
Keycloak preset asks for email_verified), an identity provider on the
organisation's own network is reached where sso.reach declares its
range (a weaker setting: a reason and a second administrator), and
signing out offers signing out of the OIDC provider too. The profile
says what you hold and where, as your session holds it. Refusals say
why: plain http for what the connection is for, the command that
needed a token, a refused SAML response once and by its provider's
name, and where oidc status looked for the client secret.
*** The admin says how long ago one way ("3 hours ago", "1 day ago",
"1 minute"); the Today screen said "3 h ago" beside "3 hours ago", and
"1 minutes" appeared where a minute was meant. The help lists the
public site's API flags under quilzo site, where they work.
*** A duration setting takes days and weeks (90d, 13w), and Quilzo says a
duration in days where it said 2160h or 2400h0m0s. An access review
closes only once its evidence is written. A posture finding about a
token names the token, and the Security screen says whose it is.
*** Agents: a decide step takes an object or a list as its state, a
person's answer to an escalated question keeps what the decider
settled, agent commands take their flags either side of the file or
name, and a finished run says what the agent concluded.
** Installing
*** deploy/compose.yaml is a reference stack that works as written: the
admin over HTTPS from a Caddy of its own on 127.0.0.1, the published
site behind Caddy, and a log writer of its own that holds the only
writable copy of the audit log. Following deploy/README.md word for
word gives a store on a volume, a design before the site starts, an
enrolment link that names the published address, and a first token.
The image's entrypoint names its store (/srv/store), so no command can
lose it, and any command that would make a store on a container's own
layer says it would go with the container. An image built from a tag
says its version.
** Kept features made right
*** Self-defence tells its owner: a hold on the shield's playbooks no
longer silences its alerts (a decoy touched during a hold is still
told), and holding and letting the playbooks act again are both told.
Alerts that have nowhere to go are a posture finding (alerts.unrouted),
and a mail that could not be sent is a failure on the record. A flaw in
this build is told once a day, not every hour. Sign-in mails say what
happens next. A break-glass recovery is told to the security contact.
A missing or month-old catalogue of exploited flaws is said by self
check and is a posture finding (self.kev-missing).
*** Compliance evidence says what the code does: compliance summary counts
the quantum-broken algorithms it generates (it printed "none"), the
control and pledge statements say what is automatic and which server
checks itself, a FedRAMP indicator nothing checks is "stated", and
posture scan --oscal keeps accepted risks as not satisfied, with who
accepted them. A timestamp is over the full live commit, the subject an
anchor commits to, so each shows the other; anchors say whether they are
of a publication or a log head, and quilzo anchor export writes the .ots
files the ots client reads. Sending a digest out (anchor submit and
upgrade, timestamp stamp) takes grant and is on the record. The
classification marking gate refuses what it cannot read, instead of
letting it through.
*** Rooms keep their word: erasing a message rewrites the room so no
byte of it is left on disk (copies already in backups age out with
them), a room's retention is applied by the admin's sweep, a closed or
direct room is read only by its members, reactions are kept (quilzo
room react), and the room list shows what is unread.
*** The studio follows a revoked token without a restart, records what it
uploads as the token's holder, and records sound only when somebody
ticks "Record sound too", with a notice to ask the people it will hear.
*** Share links keep a digest of their secret, not the secret, and are
matched in constant time; links already given out keep working.
quilzo desktop consent fails when it could not ask.
*** A research plan reads as signed only when its signature checks.
Making pictures, video and voice (media generate, film, speak,
transcribe) needs use-models, as assist and evolve run do. A recording
over the 25 MB a transcription service takes is transcribed from its
sound alone when ffmpeg is installed. quilzo cell warm makes the
snapshot every cell session restores. The chatbot and research find
figures with one detector.
*** The shield can turn down bookings, the newsletter and the fediverse,
which it was asked about and could never switch off. Replica answers
are private to shared caches. An agent card that does not validate is
left out of a static copy instead of failing it. The Permanent web
screen gives the same content identifier as quilzo ipfs id, and no
longer bundles members-only or scheduled pages. The public /mcp
negotiates the MCP revision as the admin's does.
** Fixes from a security review of the whole program
*** The admin and the site start for the account the store belongs to.
`quilzo init --admin NAME` ends by telling its reader to start the
admin, and the systemd unit and the image start the servers without a
token; all three were refused with "publish needs a verified identity"
once the store had an administrator. The account that owns the store
(or root) may now start `serve` and `site` without one; anybody else
still needs publish, and is told whose store it is. Everything else
that account does on the store still needs a token.
*** The account menu no longer scrolls. A rule meant to enlarge small
links in the bar reached the menu's items, so the two items overlapped
and the menu showed a scroll bar beside two choices.
*** `go install github.com/quilzo/quilzo/cmd/quilzo@vX.Y.Z` makes a
binary that says it is vX.Y.Z; it said "dev".
*** Outbound requests report the status that came back (they reported 0),
a staff notice to a chat service that answers 202 or 204 is delivered,
federation delivery gives up on a receiver that drips its answer, and an
agent's web_fetch is held to the hosts it declared on every redirect,
not only the first and last address.
*** Member sign-in returns only to a page on this site; a "next" address
with a tab or newline in it went elsewhere.
*** The public site's passkey sign-in is limited per address and its
prompts are bounded; one inbox is sent a newsletter confirmation once an
hour however its address is spelled (dots and +tags); the files every
form keeps together have a ceiling (1 GiB), and a form past it says so;
search scores are computed over the pages a visitor may read, so they no
longer reveal whether a hidden or embargoed page contains a word.
*** A path-scoped session (an author on /blog) is held to its path in bulk
actions, the pages list, notes, preview links and Today's figures; a
peer is sent the whole store only on a credential for the whole store;
exporting the form fields marked sensitive asks about the session, as
purging them does.
*** /signout from a link the person did not follow, an image or a frame
asks "Sign out?" instead of signing them out. The menu's Sign out still
signs out at once.
*** A submission through the share sheet is audited, counted and alerts
staff, as the same form's own route always has.
*** Reading untrusted files is bounded: a PDF's streams together inflate
to at most 64 MiB, YAML refuses nesting deeper than 64 and reads long
values in one pass, a calendar's folded lines unfold in one pass, and a
CSV row wider than a sheet may be is refused before it is read.
*** Promoting into production is publishing. `quilzo env promote …
production` and the Publishing screen's promote put a draft live without
the publish checks (review approvals, provenance, accessibility,
marking) and fired no published event. They now run every check on the
commit being promoted, record the publish and notify as publishing does,
and `env promote` takes publish's options. Approvals are checked on the
commit being published, so staging cannot go live on a later draft's
approval.
*** Dual control holds where one administrator could act alone: an
enrolment link for an administrator, and the provisioning token while a
group mapping makes administrators, each wait for a second
administrator, and are then received once by whoever asked (People, or
`quilzo pending redeem`). A session confined to /@security, or capped
at reader, can no longer change the shield, the organisation's policy,
or another administrator's standing.
*** Agents hand work on as safely as they do it: a supervisor's delegate is
reviewed as the supervisor is, its writes are marked as a model's work,
it acts and is charged for the person who started the run, it gets only
what is left of the run's tokens and money, and a hand-off the reviewer
refuses waits for a person who is shown where it goes and what it is.
Pausing a run no longer gives back what its steps and delegates spent,
and a decider's and the reviewer's model calls are charged to the run.
*** A tool step counts as a write when its integration says it writes, not
only its manifest, so it is never batched with reads or approved by its
own requester. Another person's remembered procedure is their words in
the run that recalls it, and taints it. A delegate holds only the tools
its supervisor declares by name. quilzo_check runs only the checks that
gate publishing.
*** Apps connected over OAuth: removing an allowed host ends its
connections however the app spells the host, and a code redeemed after
its app was removed connects nothing. A refresh asking for less
narrows its access token, never the grant. SCIM hears a deactivation
however its attributes are spelled, refuses what it cannot apply, and a
stale change can no longer undo a concurrent deactivation. HTTP message
signatures are bound to this server's own address. Quilzo's A2A client
speaks A2A 1.0 where a card offers it, so one Quilzo agent can call
another. The Authorization header is read in any case of its scheme,
and DPoP, agent credentials and federation share one bounded replay
cache that a flood of proofs cannot fill.
*** Pages an agent reads with web_fetch, OpenAPI documents an agent is
declared from, and WordPress and Ghost exports are read in time in
proportion to their size: a crafted page, a self-referring request body
or a fan of references could take minutes, gigabytes or the process
itself.
** Quilzo defends itself; a security team's own tools are no longer in it
*** A security team's tools left Quilzo: collecting from other systems
(connect, collect, inbound, source, spool, telemetry and the Feeds and
Events screens), detections, Sigma, correlations and the trial ring
(detect, sigma, correlate, proving, hunt, triage), the finding queue and
risk (finding, analyst), cases and incidents with their regulatory
clocks, actions on other platforms, automations, vulnerabilities (vuln,
sca, sarif, appsec, feed, reachability), the estate and workforce risk,
vendors, access reviews, reminders, notices to customers, work tracking,
estate canaries, and the MCP tools over all of them. Their screens are
gone from the admin, and the Security operations menu is Protection:
Shield and Sign-ins. Quilzo keeps what it needs to defend itself, and
hands its audit log to any SIEM that reads OCSF, as it is written
(siem.stream.url).
*** A risky sign-in has to confirm it is its person, built in and on.
Impossible travel, a session copied to another device or country, Tor
or a hosting provider, an administrator in a country they have never
signed in from, or three unfamiliar things at once: the session is
held until a passkey or a fresh single sign-on confirms it, and the
security contact is mailed about the worst of them. This was a set of
automation rules nobody had until they added them; it is now the
setting signin.adaptive (NIST SP 800-53 IA-10), on by default, and off
is a weaker setting that takes two administrators. A person reporting
that a sign-in was not them ends every session they hold.
*** Posture drift is on the record. A check that starts failing is
written to the audit log once (posture.drifted), and again when it is
fixed (posture.restored); a high or critical one is mailed to the
security contact. It used to open a finding in the queue that left.
*** The shield's "open a case" asks for one in the audit log
(shield.open-case, with the playbook, signal, subject and a summary),
where the security team's tools pick it up; "notify" mails the security
contact and the agent's sponsor directly. Neither keeps the request
that raised the signal waiting on a mail server any more: mail is sent
in the background, and the command line waits up to thirty seconds for
it before it exits.
*** quilzo self kev FILE keeps the CVE ids of CISA's catalogue of
vulnerabilities known to be exploited, so a flaw in this build that is
being exploited is contained at once. It was read from the
vulnerability workbench's imports, which left.
*** Mail is configured in mail.json (notify/mail.json is still read).
Shadow AI on the Fleet screen is what agents' own programs reached
around the gateway; what people across the estate reach is in its
proxy and DNS logs, which belong to the organisation's own security
tooling.
** Changed
*** An agent's box narrows two more doors.
- A tunnel through the box's proxy that carries nothing in either
direction for five minutes is closed and recorded as such. It
lasted as long as the run, which may be hours. Silence is the
tunnel's, not one direction's, so a long download is not cut off.
- A netlink socket opens only for the routing tables, which the C
library reads. Netfilter, xfrm, audit and the generic families are
the kernel's usual way from an unprivileged socket to root, and
nothing in a box uses them. Packet, raw, local, Multipath TCP and
crypto (AF_ALG, the CVE-2026-31431 route) sockets were already
refused.
*** The admin takes you where you meant to go.
- The name in the bar goes to the screen you start on (Today, unless
you chose Pages), as signing in does. It went to the page list
whatever you started on.
- A screen below one in the menu (a page's editor, one agent, one
framework) has a way up to it above its heading: "← Pages". On a
phone, where the menu is closed, that was missing, and the screens
that had a way up drew it three different ways.
- A message (something refused, not saved, not found) offers the way
back to where you were, not "Back to pages".
- Signing in returns you to the screen you asked for, by token,
passkey or single sign-on (OIDC and SAML). Followed links and
sessions that ended mid-task used to land on the start screen
(WCAG 2.2.5).
*** Help opens in a new tab, and says so. Help with this screen, All
documentation and every other link that leaves the admin open in a
new tab, so somebody partway through a form keeps what they typed.
Each one is marked with an icon and "(opens in a new tab)" for a
screen reader (WCAG technique G201). The accessibility check that
runs before publishing now reports a link that opens a new tab
without saying so (new-tab-not-announced, advisory).
*** What waits on a person is one list wherever they look. quilzo
approvals now has what Approvals and Today had and it did not: the
change to the site waiting for approvals, and the features agents
drafted for a person to read. All three now have the newsletter issues
agents proposed, which were only on the Newsletter screen.
*** Scheduled publishing needs no timer. quilzo serve publishes each
entry within a second of its time, and at once for one that fell due
while it was down; schedule.in_server turns it off for a deployment
that would rather run quilzo schedule run from a timer of its own.
The two never publish one entry twice. The Publishing screen lists
what fired in the last thirty days, and why anything was refused.
*** A scheduled publish goes out on the authority of the person who
scheduled it, checked when it fires, and of whatever fires it. It
used to need only the timer's: somebody who scheduled a launch and then
lost the publisher role, or whose grant ran out, still had it published
by an administrator's cron token. It carries the publisher role and no
more, every gate runs then, and the record says it came by the schedule
and names who fired it.
*** A site that has chosen no style is drawn in Material 3 Expressive, as
the admin is. The Design screen offered Classic as the default and
Material 3 Expressive as one choice among seven, while the admin a
person was looking at was drawn in it. Expressive is now the default
and listed first; Classic is the calmer choice, the look sites had
before styles. Motion's and the colour scheme's options are Springy
and Playful, so no choice is called Expressive twice.
*** The admin on a phone and a laptop. Every screen was opened at 1280,
768 and 390 pixels and measured: a table on a phone is now a card for
each row, its long names wrapping, rather than columns scrolled off the
right of the screen; a row's actions are one button at its end, on
Pages and Data alike, whose icon is no longer squeezed to a sliver; the
media library is a card for each file, its rights, crop, address and
replacement under Details; Pages names each page by its title; and the
access table says Edit drafts and Install features rather than the
names the policy file uses.
*** Pages has a New page box: an address, made into a page name, opens
the editor on it. A page could only be started by typing its name
into the browser's address bar. Today shows the site at a glance (its
pages, records and files, when it was last published and by whom) and
where to start, so a quiet day no longer reads as an empty store.
*** A page is given a form in its editor: Form on this page lists the
forms there are, and None takes it off. Forms said to paste HTML into
a template; it now points at the editor, and keeps the code for a
design of one's own under a disclosure.
** Fixed
*** The connected-accounts callback no longer reflects what it is sent.
It turned a provider's error text into a message on the Connections
screen. The address is one anybody can link to before any state is
checked, so that text could be a sentence of the linker's. Now it
shows only an OAuth error code (access_denied and the like).
*** Pictures from OpenAI's current image models. Every request for a
picture carried response_format=b64_json, which those models refuse
as an unknown parameter, so quilzo media generate through OpenAI
failed. It is sent now only for a route added with --ask-base64 (or
"Ask it for the picture itself" on the Models screen), for an
endpoint that answers with a link unless asked; a link is still never
fetched. A provider's refusal of a picture (its content policy) goes
back to whoever asked, rather than resting the route and trying the
next.
** Added
*** The editor's preview shows what you type, before you save. A
moment after typing stops, the page beside the form shows what is
typed, rendered as readers will get it and saved nowhere; without
script, "Show in the preview" does the same. What is typed and not
saved is kept for the tab: after a reload the editor offers to put it
back, and leaving with it unsaved asks first. Showing unsaved words
takes what saving takes, so somebody who may only read sees the saved
draft.
*** Video from a prompt. quilzo media film "PROMPT" --author WHO
[--aspect 9:16] asks a gateway route added with --films, through one
of two new provider profiles: google-veo (Veo through the Gemini
API: predictLongRunning, then the operation asked after) or fal
(fal's queue: taken, asked after, the video fetched from fal's
storage without the key). Every address an answer names must be
https on the provider's host, a key never follows a redirect to
another host, and what comes back must be an MP4. It is stored
marked as generated, with the model, the prompt and the person
accountable, and needs captions before a page shows it, as every
video does. Masked, budgeted and in the ledger as pictures are.
*** Subscriptions. A catalogue row with "interval": week, month or year
is sold as a subscription: a recurring price at Stripe, a payment
link that starts a subscription the buyer manages there, and a Buy
button that says "£12.00 a month". The demo shop's product type has
the field.
*** Agents ask other vendors' agents, over A2A. A new integration kind,
a2a: quilzo integrations import agent-card.json declares one from its
card, switched off. Its skills are tools, pinned, allowed and
approved as an MCP server's are; a call is message/send naming the
skill, and a task it takes on is followed with tasks/get until it
finishes or ninety seconds pass. A card whose service address is off
the declared host is refused before anything is sent, and the
message id is the step's idempotency key.
*** The content API describes itself. /api/v1/openapi.json is an
OpenAPI 3.1 document of every route: pages, collections and records,
similar pages and search by meaning, and the replica routes, with
their parameters, their If-Match rule and their errors. The same for
every installation, so it needs no token; everything it describes
still does. A test fails on a route served and not described.
*** A remote MCP server's sign-in, discovered. quilzo connect provider
discover NAME https://SERVER/mcp reads the server's protected-
resource metadata (RFC 9728) and its authorization server's (RFC
8414 or OpenID discovery), requires PKCE with S256, registers this
installation there as a public client (RFC 7591) or takes
--client-id, and keeps a provider whose tokens carry the resource
indicator (RFC 8707) and go to that server alone. Each person then
connects in Your accounts, and an MCP integration naming the
connection reaches the server as them.
*** An agent's writes elsewhere are not done twice, and not lost. Each
write is named by its run and step and sent with an Idempotency-Key,
the same whenever that step is sent again, so a service that keeps
them does it once. A busy or silent far side is tried again, up to
three times, as Retry-After asks: a read always, a write only with
its key. One that still did not get through is kept, with the key
and what came back but never the arguments, on the Integrations
screen and in quilzo integrations undelivered, for a person to settle
and take off the list; nothing is sent again by itself.
*** Search engines told what changed, as it is published. With
site.indexnow on (off by default), each publish sends the addresses
of the pages that changed to IndexNow, which Bing, Yandex, Seznam,
Naver and others share: a page reusing a changed block among them,
never one left out of results or a block's own. The key that shows
the addresses are the site's is served at /KEY.txt. The first
publish after turning it on is only a starting point. It goes out
as its own network purpose, indexnow, refused when offline.
*** What people looked for and did not find. The cookieless counts now
include addresses people asked for that are not on the site (not the
ones somebody hunting for an admin asks for, which the shield keeps)
and searches that found nothing, masked as unanswered questions are.
The Analytics screen lists both, each missing address with the page
it most likely meant and, for whoever may publish, "Redirect it".
quilzo redirect list | add FROM TO [--temporary] | remove FROM keeps
the same redirects, and the running site reads them again within a
second of a change: no restart.
*** Pages tell search engines and agents where they stand. Every page
names where it lives with rel=canonical once the site has an address
(a layout that writes its own keeps it). A page can be left out of
search results ("noindex", or the editor's box), in its head, its
X-Robots-Tag and the sitemap, or name the address it copies
("canonical", or "Published first at") and leave the sitemap. Every
page has a Markdown copy of its words at /NAME.md, linked from the
page and from llms.txt, never indexed and naming the page as
canonical. "block" and "noindex" are system fields, so a typed page
can carry them; a typed page names a canonical address through a
"canonical" field its type declares.
*** A form can take a file: a CV, a photograph of the damage. A field of
kind file takes images and documents, or the kinds it names; a file
is taken by what its bytes are, with the media library's checks (an
image must decode, a PDF must not run anything), at most 10 MB, 20 MB
a submission. It is kept with the submission and deleted with it,
and staff download it from the Forms screen as an attachment named
afresh, under a policy that runs nothing; reading it is audited
(form.file.read). The starters, the embed snippet and the chatbot's
form offers send such a form as multipart.
*** A form can ask a question only on an earlier answer: an order number
when the topic is Returns, who gave it when "It was a gift" is
ticked. "when" on the field, or "Asked only when" in the form
builder. The server asks it only on that answer, and keeps nothing
for it otherwise; the page hides it until then with /form/NAME.css,
by :has() and no script, and a browser without :has() shows it with
the words that say when it is asked. The starter layouts give each
field a wrapper and each choice its value, which this needs.
*** Reusable blocks: sections written once and shown on many pages.
quilzo section block PAGE makes a page a block, quilzo section reuse
PAGE BLOCK puts its sections on another, and quilzo section blocks
says where each is used; the Sections screen does all three. A block
is saved, published and rolled back with the draft, and never served
on its own: no address, sitemap entry, search result or chatbot
passage. A page reusing one is that page with those sections to
every layout, search index and chatbot, and its ETag changes when the
block does. A block for members only is never shown on a public
page, a block in a block shows nothing, and a block a page still
reuses cannot be deleted or made a page again.
*** Visitors say whether a chatbot's answer helped. Two buttons under
an answer, Yes and No, counted with nothing about who pressed them;
the chatbot's screen and quilzo assistant gaps NAME say how many
helped. With chatbot.gaps on, a "no" keeps the question, masked,
beside the questions it could not answer, marked as one whose answer
did not help; asking it again proves nothing, so such a group waits
for a person to close it. The form carries a token the site signed
for that question, good once and for an hour, so the route cannot
put somebody's own words in front of the owner. The conversation
page no longer says nothing is stored when chatbot.gaps keeps some.
*** A draft can be shared with somebody who has no account. quilzo
share PAGE [--days 7], or "Make a link" in the page's editor, gives a
link to that page as the draft is now: later changes do not reach it,
it lives thirty days at the most, and quilzo share revoke ID (or
Revoke in the editor) ends it at once. The public site serves it at
/shared/ID with a note saying it is a draft, marked not to be
indexed, stored or referred onward; an ended link is the site's own
404. Sharing takes what publishing takes, since it shows the page to
somebody outside; revoking takes what editing it takes.
*** Backups of the whole store, encrypted and checked. quilzo backup key
makes a key (shown once, kept nowhere here); quilzo backup create
FILE takes the store and the layouts, encrypted with AES-256-GCM in
numbered parts, and reads it back whole to check every file against
its manifest before calling it a backup. quilzo backup verify checks
one without restoring it, and quilzo backup restore FILE --into DIR
restores one into an empty directory and checks its history. With
backup.dir set, the admin server makes one a day and keeps
backup.keep (seven). CP-9 is in the controls table.
*** Staff hear when an agent waits for them. An agent's run that stops
to ask a person to approve what it would do, or to answer it, is a
new event, approval.waiting, in notify.events by default. Staff are
told by mail, or in Slack, Teams or Discord (notify.staff), which
agent waits, never what it would do, with the admin's address;
deciding is still done signed in, under Approvals. No agent may
start from it, since an agent started by another's waiting is how a
queue feeds itself.
*** A calendar and issues, ready-made for agents acting for somebody.
quilzo integrations template lists them, and quilzo integrations
template google-calendar (or microsoft-calendar, github-issues)
declares one, switched off and unpinned, using the account each
person connects at the provider. Each reads (events, issues) and
adds (a meeting, an issue, a comment), and nothing more; --uses
narrows it further.
*** Agents act with a person's own accounts. An administrator declares
a provider (quilzo connect provider add NAME --template google,
microsoft or github, or its endpoints), people connect their accounts
on Your accounts or with quilzo connect start, and an integration that
names a connection goes with the account of the person the agent acts
for, and nobody else's.
- OAuth 2.1 with PKCE, and a state that finishes once, for the person
who started it.
- Tokens are sealed, refreshed when due, sent only to the hosts their
provider names, and revoked at the provider on disconnecting.
- An agent acting on its own name, and the gateway, have no person,
and are refused.
*** A model reads text aloud. quilzo media speak "text" (or @FILE)
--voice V --author WHO sends it to a route that speaks (quilzo gateway
route add NAME --speaks, or "It reads text aloud" on the Models
screen), and stores the MP3 marked as a synthetic voice, with the
model and the person accountable, and its words as its transcript.
Text holding personal data goes only to a route allowed it, since a
voice cannot leave it out the way a masked prompt does.
*** A model drafts a video's captions. quilzo media transcribe VIDEO
--lang en --author WHO sends the recording to a route that transcribes
(quilzo gateway route add NAME --transcribes, or "It transcribes
recordings" on the Models screen). The timed text comes back as
WebVTT and is stored as a caption file marked as a model's, unattached.
A person reads it, mends it, and attaches it with quilzo media
captions, so the gate that refuses a video without captions is
satisfied by a person and not by a model. A recording's voices cannot
be masked, so it goes only to a route allowed personal data, and it
is charged by the minute (--price-minute).
*** Chatbots find passages by what they mean, not only by their words.
Add a route that makes embeddings (quilzo gateway route add NAME
--embeds, or "It makes embeddings" on the Models screen) and name it
in chatbot.embeddings. Chatbots that answer with a model then find
the passage about returns when a visitor asks about a refund.
- The site's passages are embedded after each publish (or now, with
quilzo assistant embed), and kept by hash, never by their text.
- Each question is embedded as it is asked, masked.
- A passage that shares too few of a question's words is answered
from only when it is at least chatbot.embeddings_floor alike.
- Chatbots that only quote their pages, or are kept to one route,
send nothing.
- The test screen shows each passage's meaning rank and similarity.
*** A chatbot chooses the model that answers it. A chatbot can name a
route, or the model a route serves, that answers it first, with the
gateway's other routes behind it: --route on quilzo assistant add, or
"Model that answers first" on its page. With --route-only ("Only that
one"), its visitors' words go to that route and no other, and when it
cannot answer the chatbot quotes its pages instead.
*** Anthropic's models are spoken to natively. Anthropic is in the
providers' catalogue (quilzo gateway providers anthropic), and a route
to it speaks its Messages API rather than an OpenAI-compatible
imitation. A route can say:
- how hard the model thinks (--effort);
- that it runs only in the US (--geo us);
- that a request its model declines is answered by another of
Anthropic's models (--server-fallback).
These are on the Models screen too. A temperature or a top_p, which
Anthropic's models refuse, is refused where the route is made. A
request a model's safeguards decline goes back to whoever asked, says
why, and is not sent on to the next route.
*** A chatbot answers from the site's records too. An owner chooses which
collections it reads (products, events, opening times), on the
chatbot's page or with quilzo assistant add --collections, and it
answers from their published records as it does from pages: each
record a passage of its fields, cited by its name. Fields named for a
person (an email address, a telephone number, an address, a password)
are never read, and the chatbot reads the records again when they are
published anew.
*** A chatbot can ask one of the site's agents to do something for a
visitor. A new kind of action, agent: the chatbot offers it ("Look up
your booking"), the visitor sees what will be asked, corrects it if
it is wrong, and presses to ask. The public site, which is confined,
hands the request to the admin server, which runs the agent on its
sponsor's authority, no more than an author's: as a reader unless the
action says it writes, and then still only drafting and proposing.
What the visitor typed is given to the agent as somebody else's
words, and the run is tainted by it from its first step. Who the
visitor is comes only from a token the host site signs (HMAC-SHA256,
with a key sealed as a credential the chatbot names), checked again
by the admin server, never from what they type; an action can be
offered only to visitors the site vouches for. The visitor is shown
the agent's answer when its run read nothing private and waits for
nobody; otherwise they are told a person will look at it. quilzo
assistant action NAME ACTION --kind agent --target AGENT [--fields
a,b] [--writes] [--identity --identity-secret NAME], or the
chatbot's page in the admin.
*** An agent's own run does what the agent interface offers. A manifest
could declare any operation of the agent interface (records, media,
menus, newsletters, chatbots, research, the site crew); an outside
agent calling them through MCP got them, and the same agent run here
got "not implemented" for all but nine. A run now performs the reads
and the writes that make a draft or a proposal (quilzo agent
capabilities lists them) through that same interface, as the run's
caller, with the role each needs and every check its handler makes; a
write waits on the agent's autonomy except a proposal. An
administrator's reports, and publishing other than by proposing, are
not an agent's, and a manifest naming one is refused when it is
written. So a gardener agent can read list_chatbot_gaps and draft
the page that answers what a chatbot could not.
*** What a chatbot could not answer becomes the page that answers it.
Off until the owner turns on chatbot.gaps: then the questions a
chatbot could not answer are kept, with addresses, numbers and cards
masked and nobody named, a day to a file, deleted whole after
chatbot.gaps_days (30). The chatbot's page in the admin, and quilzo
assistant gaps NAME, group them by what they are about, most asked
first, each with a link to write the page that answers it, or to
close it as needing none. When the site is published, each open
group's questions are asked again (at most 25), and a group the
chatbot now answers is closed, naming the page it answered from; one
asked about again opens again. With chatbot.gaps_agents on as well,
an agent holding list_chatbot_gaps reads the open groups too, to
draft the pages for a person to review. The audit log says a group
was read, checked or closed, never what was asked.
*** An agent's browser and workspace work together. browser_save keeps
the page in the workspace as a PDF, a picture of all of it, its markup
or its words; browser_download presses a link that downloads and
keeps the file (at most 32 MB, under a name of its own, never a path
the server chose), weighed as the press it is; browser_upload puts a
workspace file into a page's file field, only on a host the agent may
send to and always after a person is told which file and where. The
first two need file_write, the third file_read, and all three keep to
the workspace's quota and to a run that only reads.
*** Agents read the web and search it, without a browser. web_fetch reads
an https address: a page's words and links, without its scripts and
forms, or text and JSON as they are. It reads the hosts the
declaration's "web": {"fetch": [...]} names, or any public host with
{"any": true}, never a private address, and a redirect is held to the
same. web_search asks the provider agents.search names (Brave,
Tavily, Exa, Serper, SerpApi, Kagi, Perplexity, Jina, or your own
SearXNG at agents.search_url), with its key sealed as
search-PROVIDER. Both taint the run, and once a run holds something
private an address or a query waits for a person, since either could
carry it. Claude Code's WebFetch and WebSearch now import as these.
*** An agent's browser does more of what Chrome does. New actions, each a
capability:
- browser_scroll: by screens, or an element into view;
- browser_key: Enter, Tab, Escape, arrows, Page Up and Down and the
rest. Enter or Space that would press a button or send a form is
weighed as pressing that button is;
- browser_navigate: back, forward or reload, never back to a page a
form led to, which would send the form again;
- browser_wait: until the page shows some text, or for some seconds;
- browser_hover: rest the pointer on something, as for a menu;
- browser_emulate: the page as a phone, tablet, laptop or desktop, in
light or dark, with reduced motion, or as printed;
- browser_console: what the page logged, its errors and the requests
that failed, kept only for an agent that may read it;
- browser_check: the page's speed (first byte, largest contentful
paint, layout shift, requests), what the accessibility checks find,
and what a search engine reads of it.
Anthropic's, OpenAI's and Gemini's computer-use models now scroll,
press keys, wait, hover and go back through these, where before they
were refused. A browser grant over MCP that offers every action
offers these too; name --ops to hold one to fewer.
quilzo browser find lists the Chrome, Chromium, Edge and Chrome for
Testing builds this machine has, with their versions, for
browser.path; quilzo browser status says the version in use.
*** Agents edit, move, delete and search files, and run code. An agent's
workspace had list, read, write and run a named program. It now has:
- file_edit: replace exact text (once, or every time), or put lines in
after a line;
- file_move: rename or move, where what was at the new name goes to
the bin rather than being lost;
- file_delete: into the workspace's bin, kept a week, from where
file_move brings it back; deleting from the bin is for good and
waits for a person;
- file_search: files by name, lines by text or regular expression;
- file_read of part of a file, by its lines, however large;
- code_run: Python, JavaScript, TypeScript, shell, Ruby, Perl, PHP or
Lua, with an interpreter the declaration names. The code reads the
workspace and changes nothing there unless it says "writes", and
then a person reads it whole and approves it first. It never
reaches the network.
OpenAI's, Copilot's and Bedrock's code interpreters now import as
code_run, and Claude Code's Grep, Glob and Edit as file_search and
file_edit.
*** Models from more than forty providers, by name. quilzo gateway
providers lists them, from runtimes on this machine to model makers,
open-model hosts, routers, clouds and regional providers, each with
what it says it does with what it is sent; quilzo gateway route add
NAME --provider ID takes the address and the way it wants its key from
the provider's profile. A provider's route reaches that provider's
host and nothing else, at public addresses (this network for a local
runtime), and its key can be sealed (--key-secret) rather than in an
environment variable. The Models screen offers the same list. An
operator's own providers.json adds profiles or replaces built-in ones.
*** Staff hear what visitors are waiting on. When a visitor asks a
chatbot for a person or sends a form, notify.staff is told: by mail,
or in a Slack, Teams or Discord channel through an incoming webhook
whose address is kept sealed (secret:NAME); an address in the setting
itself is refused. A notice says which chatbot or form and how many,
never who or what they wrote, and at most thirty go in an hour.
notify.events chooses what is told.
*** Agents start on their own. A trigger starts one at a time of day
(quilzo agent trigger NAME add --every 24h --at 07:30 --zone
Europe/London) or when something happens: pages go live, a draft is
proposed, records change, a form is sent, a booking is made, somebody
joins the newsletter, a visitor asks a chatbot for a person, or a
chatbot cannot answer. Each run is the agent's sponsor's, checked when
it starts, and capped at drafting and proposing. A trigger starts at
most its day's runs, never a second while its first is going, never
from what its own agent did, and a chain of agents starting each other
stops at three. The agent's page in the admin has the same, under
When it runs on its own; agents.triggers turns it off.
*** What happens on the site is kept as events, without people in them:
which page, collection, form or chatbot, never who or what they wrote.
The public site keeps the events visitors cause in a log of its own,
and one that claims a page went live is refused and recorded.
*** Spreadsheets and calendars in and out. quilzo records import reads a
CSV from Excel, Google Sheets, Numbers, Airtable, Notion or Shopify
into any collection, whatever it was saved as (commas, semicolons or
tabs; UTF-8, UTF-16 or Windows-1252), matching columns to fields by
name, label or --map, reading each value as its field's kind, and
asking rather than guessing when a date could be day or month first.
A row is the record already there when its id or key field (the page
link by default) says so, and is changed rather than added; a sheet
brought in twice is the same records. One bad row refuses the sheet
unless --skip-bad; --dry-run writes nothing. quilzo records export
writes a collection back out as CSV or JSON, which comes back in as the
same records, with no cell a spreadsheet would run as a formula.
quilzo events import brings a calendar's events in from a file or
the address Google, Outlook or Apple publish it at: each time a
repeating event happens, a year either side, on the site's clock, with
private events left out and the address never printed. The Data
screen downloads a collection as a spreadsheet and brings rows in, and
Make's From a spreadsheet reads the same files.
*** Bring a site from the file it exported. quilzo site import, and
Make › Bring your site, take a WordPress or Squarespace export (.xml),
a Ghost export (.json, with --site for where it was, for its
pictures), or a folder or zip of a site's files: the Markdown of Hugo,
Jekyll, Eleventy, Astro, Docusaurus or Gatsby, a Notion or Obsidian
export, or the pages Webflow exports. Pages keep their headings,
lists, numbered lists, quotations, links, bold, italic and pictures;
posts become the blog, each with its picture, author, tags, dates and
the pictures among its words; links between them go to where they are
now; the menu is the export's; every old address redirects. Drafts,
private posts and posts for members or paying subscribers stay behind
and are counted on the card, as are comments and shortcodes. A
package can bring records, named by the hash of a staged file and
written once, when it is first installed.
*** A site brought by its address keeps its links, bold, italic and
lists: prose sections take a body in the prose subset, and a table's
row reads as one line.
*** The prose subset takes a picture from the library, on a line of its
own with what it shows (), and backslash
escapes (\*, \[). A link with no words is not one, and a quotation
looks like one.
*** A post with no summary is listed, and described to search engines,
by the start of its words.
*** A blog: posts newest first, each on its own page, older ones a page
at a time. quilzo blog setup, or Ready to add on the Make screen,
installs it as one feature: a "post" type (title, page link,
published, written by, summary, the post, picture, tags, last
changed), its collection, a listing of them newest first, a Blog page
and a page for each post. A posts section on any page shows the
newest, with when each went out and who wrote it, and Older and Newer
posts lead back to the first. Each post's page has its words with
headings, lists, bold, italic and links, its tags, the posts either
side, and schema.org BlogPosting data (headline, dates, author,
picture, keywords); it is an article to whoever shares it.
site.blog names the blog's listing, and a site with no feed gets the
blog as its feed at /feed.xml. A package can carry a "blog" block,
so a site an agent proposes arrives with one.
*** Events: what is on, a calendar to subscribe to, and when and where
for search engines. quilzo events setup, or Ready to add on the Make
screen, installs events as one feature: an "event" type (title, date,
time, end_date, until, venue, location, online, summary, picture,
price, tickets, status), its collection, a listing of what is to come
and one of what has been, a What's on page and a page for each event.
An events section on any page lists what is still to come, with the
date as people say it, a link to the event's page and a calendar entry
to add; an event leaves the list when it is over, by the site's clock,
with nothing to publish. /events.ics is the site's events as a
calendar visitors subscribe to (advertised in the head and llms.txt),
and each event's page carries schema.org Event data: start and end
with the site's offset, place or online location, status (cancelled,
postponed, moved online) and tickets. Records gain a time-of-day
field, listings gain upcoming and past conditions (with a fallback
field, so a festival stays on until its last day), and a page listing
by the clock is never answered "not modified" with an event that is
over. site.events names the events listing and site.timezone the
site's clock. Over MCP, upcoming_events shows an agent what is on; a
package can carry an "events" block, so a site an agent proposes
arrives with its calendar.
*** A newsletter, with every address asking for it first. A newsletter
section on any page takes sign-ups; the address is mailed one link,
and nothing else is sent until the button on the page it opens is
pressed (a filter opening the link is not a yes). An address that
never does is forgotten after a week, and one address is mailed a
link at most once an hour. An issue is a published page: quilzo
newsletter test PAGE --to YOU, then quilzo newsletter send PAGE
--yes, or the Newsletter screen. It goes as text and HTML, one
message to each confirmed address, paced, written down as it goes and
carried on from where it stopped, with a link to leave and the
List-Unsubscribe and List-Unsubscribe-Post headers of RFC 8058, which
end the subscription with one POST and delete the address. The
public site writes sign-ups and never reads an address back; links
and ids are keyed hashes made with newsletter.key, which the site can
read and not write. Lists import from Mailchimp, Substack, Ghost,
MailPoet, Buttondown and others (quilzo newsletter import FILE
--agreed "where they agreed"), keeping anybody who left there as
having left, and export as CSV. Webhooks can be told "subscribed"
and "unsubscribed", with no address. Over MCP an agent sees the
counts (newsletter_status) and proposes an issue (propose_issue); a
person sends it. A store a service manager keeps read-only needs
quilzo site --prepare run once to make the newsletter's directory and
key; until then the site starts with sign-ups off and says so, as it
now does for bookings too.
*** Agents can ask the site (NLWeb). The public site answers a question in
plain words with the published pages that match, as schema.org
objects: GET or POST /ask with query, mode (list, summarize or
generate), query_id and decontextualized_query, streamed as
server-sent events unless streaming=false, and the same over MCP at
/mcp with one read-only tool, ask. Summarize and generate add the
answer the site's own question box gives, with its sources, limits
and disclosure. It answers only from what a visitor may read now,
never a members' page, writes nothing, and sits behind the site's
crawl licence like every page. /llms.txt says where it is. The
question box no longer reads the service a booking section books or
the board a comments section shows as something the page says.
*** Bookings: appointments on the site. A site says its hours each week
in its own time zone, the dates it is closed, how much notice it
needs and how far ahead it books, and the services it offers, each
with how long it takes and how long to leave after it (quilzo booking
setup FILE, or the Bookings screen). A booking section on any page
offers the free times as buttons, with no script; a visitor books one,
is shown it on the same page with a calendar file to add, and can
cancel it from there. The time is checked again under a lock as it
is booked, so two visitors cannot get it. One calendar, one booking
at a time. The public site keeps the calendar, which holds when and
for what and never who, and writes who booked once without being able
to read it back; names, email addresses and telephone numbers are read
on the Bookings screen and in quilzo booking list, and forgotten a set
number of days after the appointment. quilzo booking feed makes a
calendar address for the owner's phone, showing times and services and
never names. Webhooks can be told "booked" and "booking-cancelled",
with the service and nothing else. A feature package can carry a
"bookings" block, so a site an agent proposes arrives taking bookings;
its card says what it collects and two people approve it, an upgrade
keeps the owner's calendar address and any pause, and a setup made by
hand is not taken over. Over MCP, booking_times shows an agent the
free times visitors see, and never who booked; on the page, a visitor's
own browser agent is offered the booking form as a WebMCP tool, which
the visitor still submits.
*** An agent can be the site crew, and a person still decides. Over MCP,
site_build makes a whole site from a description, site_import brings
one in by its address, and propose_feature takes a package the agent
wrote. None of them installs anything: each waits as a proposal, under
Proposed for you on the Make screen, on Approvals and in quilzo feature
proposals, until a person reads its card and installs it under the same
approvals as anything they bring. A newer proposal of the same package
replaces the older one, at most twenty wait, and one nobody reads lapses
after two weeks. site_blanks and site_fill then fill in what the person
told the agent; those pages are marked as an agent's words, and the
pages of a package an agent wrote as a model's. Filling in blanks now
checks each page it changes, so a deny on a page holds against a fill as
it does against an edit.
*** Bring your site by its address. `quilzo site import
https://old.example --install`, or Bring your site on the Make screen,
reads a live site, whatever it was built with: robots.txt first, then
the site's map of itself or its links, that host only, through the
fetcher that refuses private addresses, at a polite pace and within a
page budget (--pages). Each page's content, not its header, footer or
navigation, becomes a hero and sections; the site's own navigation
becomes its menus; its name and theme colour become its look, refused
when unreadable; the pictures it describes are kept in the media
library with those descriptions, and one with none is left out rather
than described by somebody who did not write it. Scripts, frames and
forms are left behind and named. It arrives as a site package, read
and approved as one decision, its pages marked as carried over by
software. A site block can now carry redirects, only to the site's own
pages and never from one, installed into the site's own redirect map
(redirects.json in the store) and taken out with the package, so the
old addresses keep working. quilzo site serves that map with any
--redirects file laid over it, a redirect that would hide a page the
site already has is left out, and quilzo export carries the map.
*** Payments, through the provider's own checkout. `quilzo pay enable
stripe`, or the Payments screen, asks a second administrator to let
the site take money; once they agree, a Stripe payment link is made for
every product the published catalogue sells at a price, and the site
shows a Buy button beside a product only when a live link charges
exactly what the page says: the same product, name, amount and
currency. After every publish and rollback the links follow: a new
price gets a new link, then the old one is retired, and while the
provider cannot be reached a changed price shows no button rather than
the old price. The key is a sealed secret the admin reads (`quilzo
secret set stripe`, a restricted key is enough); the site reads only
the links. Quilzo never sees a card and keeps no order: `quilzo pay
orders` and the screen read the latest from Stripe, and the audit log
records that somebody looked, never who bought. `quilzo pay disable`
takes the buttons down at once and retires every link.
*** A whole site from a sentence. `quilzo site new "a bakery in Leeds"
--install`, or Build a whole site on the Make screen, writes a site
package: pages and sections for the kind of site (business, product,
restaurant, portfolio, community or event; `quilzo site kinds`), main
and footer menus, a contact form, a question box that answers from
the site's own pages and sends nothing anywhere, a privacy page
written from what the forms collect, why, on what basis and for how
long, and a look checked for contrast. A model rewrites the words for
the description when one is allowed, and what it answers is held to
the builder's start: links only to the site's own pages, sections this
program renders, forms that pass their checks, nothing that reaches
another server. Nothing is made up: a price, an address, opening
hours or anybody's words that the description does not give are left
as [[blanks]], which `quilzo site fill` or the Fill in screen writes in
once for every page they are on. A page with a blank in it does not
publish (the new blanks gate). Installed, the pages are marked for
how they were written: by a model, which discloses, or by the builder's
template, and filling them in makes them the person's too.
*** The only administrator installs a feature alone. A package two
people would approve, such as one with a contact form, installed with
no second administrator to ask, as other changes that need two are,
and the record says it was approved alone.
*** A package can be a whole site. Beside its pages, menus, forms and
chatbots, a package may declare the site itself: its name, a sentence
about it (the new setting site.description, which quilzo site
--description still overrides), the layouts it renders through and
its look: one of the theme's styles, a colour the palette is
generated from, motion and any other token, refused when the result
is not readable. Installed, it writes the look into the templates
directory (--templates, the same one quilzo site and quilzo serve
read), gives a site with no layouts the starter's, and sets the name.
One package owns the site. A site whose name or look somebody set by
hand is a conflict until --take-site; what it was is kept, and
removing the package puts it back. The layouts stay either way.
*** Agents come in from other platforms, and go back out.
`quilzo agent import FILE` reads OpenAI Assistants, Claude Code
subagents and Agent SDK agents, Google ADK agent configs, Microsoft
365 Copilot declarative agents, Open Agent Specification, Letta agent
files, Amazon Bedrock agents (get-agent or CloudFormation), CrewAI
agents.yaml and AutoGen AgentChat components, recognising the format
from the file. Each agent becomes a declaration, with a report of
what was kept, what was changed to work here, what was left out and
what is yours to decide; --save declares it, a team's helpers first,
and -o DIR writes it out (with any OpenAPI document its actions came
with) to change before `quilzo agent declare`. An imported agent
starts at propose, or draft if it writes. `quilzo agent export NAME
--to F` writes one for Claude Code, the Agent SDK, ADK, Microsoft 365
Copilot, Agent Spec or CrewAI, and says what that format cannot hold;
`quilzo agent formats` lists them. The admin has both: Agents, Import
one from another platform, and Take it to another platform on each
agent's page.
*** A declaration carries an agent's instructions and model. The
instructions go to the model with every decision, after the
operations and the rules, which still decide what can be done; the
model is tried first on the gateway route serving it (or the route of
that name), and the others after it. Both are on the agent's page.
*** The audit log streams to a SIEM as it is written. Set
siem.stream.url (https, or http to this machine) and quilzo serve
pushes every fifteen seconds: OCSF batches that follow each other
exactly, each with the chain envelope, every record's Merkle leaf and
a tree head signed with Ed25519 and ML-DSA, the request signed as an
HTTP message (RFC 9421) over its body's digest, with a key kept for
this. The receiver can prove it holds every record, unchanged; the
cursor moves only when it takes a batch, and a receiver that says it
holds less is sent the rest again. People stay pseudonymous unless
siem.stream.reveal is set, and a SIEM off this network needs
siem.stream.allow_remote; both are weaker settings, so they take two
administrators. `quilzo siem stream status | key | send`, and posture
reports a stream failing for an hour.
*** Privileged changes take two administrators (NIST SP 800-53 AC-5,
CM-5(4), AU-9(5)). Making somebody an administrator of anything,
issuing an administrator's token, a setting weaker than its default,
fewer approvals for a change (review.required_approvals or
approval.required_humans, including putting one back to a lower
default) and moving the audit log each wait for a second
administrator, on the command line or on the People and Settings
screens. They are listed on the People screen under Waiting for a
second administrator and by `quilzo pending`; another administrator
approves or declines (`quilzo pending approve|decline ID`, or the
buttons), which makes the change, and whoever asked may withdraw it.
An approved administrator's token is issued only to whoever asked,
within a day: `quilzo pending redeem ID`. A request waits a week. A
store with one administrator is not left stuck: their change is made
as they ask and recorded as approved alone, as shield changes now are
too. The shield's `--now` and `playbook set`, which changed playbooks
on the machine without a second administrator, are gone.
*** Sessions end at NIST's limits, and signing in again keeps the page.
The admin signs out a browser nobody has used for session.idle, now an
hour by default and held between five minutes and an hour, with no
way to switch it off; anybody who can grant access is signed out after
fifteen minutes at most. A minute before, the page warns as it did;
afterwards it offers to sign in again with the person's passkey where
it is, so a half-written page is not lost. Only the person whose
page it was carries on: somebody else's passkey is refused, and is
told to use the sign-in page. A configuration holding session.idle
0s, or more than an hour, loads with the default and says so. Site
members sign in again after thirty days, not ninety.
*** Single sign-on counts as two factors only when the identity provider
says so. An OIDC sign-in asks for one no older than session.max
(max_age), for amr and auth_time by name where the provider reads the
claims parameter, and for the acr values named with `quilzo oidc
configure --mfa-acr`; it counts as two factors when its ID token's amr
names mfa or its acr is one of those, and its auth_time is that recent.
A SAML sign-in counts when the assertion says so, in the context or
attribute its preset names; nothing is asked for, because a provider
that cannot meet a requested context refuses the sign-in. The Okta
preset reads an attribute statement named amr whose value is
session.amr. Anything else is one factor: the session waits at
"Confirm it's you" for the person's passkey, which a Google Workspace
SAML sign-in always does. A second factor is always required, so
`quilzo saml add --require-mfa` and the Second factor checkbox are
gone. For Microsoft Entra, add the optional claims amr and auth_time
to the ID token; `quilzo oidc configure` and `quilzo oidc status` say
what each provider needs, and `quilzo oidc check` whether it reads the
claims parameter. Posture counts a SAML provider as covering its
people only when it can say two factors.
*** Smart lockout. Ten failed passkey sign-ins to one account from places
it is not used from lock those places out of it for thirty minutes,
doubling each time up to a day; the person's own places (the networks
they have signed in from) keep working, so nobody can lock somebody out
by failing in their name. A hundred failures in a row from anywhere
turn passkey sign-in to the account off until the person signs in with
a single sign-on that says it used two factors, or another
administrator unlocks it on the Sign-ins
screen, which lists who is locked out. Nobody unlocks themselves there,
and somebody who administers the whole site is unlocked only by another
who does; at the store's machine, `quilzo auth unlock NAME` is how a
store whose only administrator is locked out gets them back, with the
short-lived token `quilzo auth recover` issues when no other is left. Lockouts are recorded,
raise the account-locked sign-in signal, and the person is told how many
failures came before their next sign-in. The counts are kept in
lockout.json, shared by every process and kept across restarts. Site
members get the same, with a recovery code as the other way back in, and
the same answer as any failure, so a lockout tells nobody an account
exists. Throttling can no longer be switched off and its numbers are
held between bounds (at most 10 free failures, a ceiling between 10 and
100 an hour, delays between 1s and 1h, an alert by 20 failures); a
configuration holding auth.throttle, auth.lockout.hard or a number
outside its bounds loads without it, and quilzo serve and quilzo config
say so. New members' recovery codes and invitations are 120 bits.
*** Enrolment links: how somebody gets their first passkey. `quilzo
auth enrol NAME`, or Give a passkey enrolment link beside them on the
People screen, makes a one-time link that adds a passkey for that
person and does nothing else. It works once, ends within a day
(--for sets less), and is replaced by a newer link for the same
person; only its hash is kept, guessing at links is throttled, none
works during a lockdown, and giving and using one are recorded. `quilzo
init --admin NAME` makes a new store with its first administrator and
prints their link, so a new store has two factors from its first day;
an upgraded one keeps its fourteen days. Links default to
http://localhost:8080, because a browser refuses an IP address as the
site a passkey belongs to; admin.base_url sets another. `quilzo
serve` says to open a loopback admin as localhost.
*** The demonstration comes back out. `quilzo demo remove` says what
taking the Marginalia demonstration out of a store would remove, and
with --yes removes it: its pages, records, types, listings, forms,
menus, vocabularies, claim rules, images, settings, research project,
template and example files. Each goes only while it is still what the
demonstration installed and nothing that stays uses it. A page or
record somebody edited or added, a form holding enquiries from the
public, a listing a page still shows, an image content or the site's
icon still uses, the template and site name while pages are left, and
a file somebody changed all stay, and are listed. The removal is
published as one commit through the same gates as `quilzo publish`; if
publishing is refused, everything the live site reads is put back and
running it again finishes the job. It is refused while the draft holds
unpublished work. The record it works from, demo.json, is checked
against the digest the audit log holds for it, and a changed one is
refused. An emptied store takes the demonstration again; the install
refuses a store, template or examples folder already holding anything
it would write over. Needs publish; a model cannot do it.
*** Ideas in research projects. `quilzo research ideas NAME --count 3`
asks a model for hypotheses the project could test, a sceptical
critique of each, and judges them in pairs, each pair both ways round
so a judge that prefers whatever it read first decides nothing.
Standings come from a Bradley-Terry fit of the matches, the same
whatever order they were played in, and an idea is ranked once it has
played three. Each idea is shown beside what computations made of it
(`research run --idea ID`) and the verdict a person records from the
claims they produced (`--verdict ID --outcome
supported|refuted|inconclusive --claim NAME`). Ideas proposed after
the data was first seen say so; an idea whose text addresses the judge
is held, never judged; a model never writes the plan. Judging uses the
gateway consumer research:ideas:judge, so it can be a different model
from the one proposing. On the Research screen and as research_ideas
over MCP.
*** Evolve: search over programs, against an evaluator they cannot reach.
`quilzo evolve run --program seed.py --evaluator evaluator.py
--input data.csv --eval-input cases.csv` searches for a better
program: a model proposes SEARCH/REPLACE diffs to the program it is
shown first (the seed may mark what evolves with # EVOLVE-BLOCK
markers), the changed program runs in the agent box with no network
and bounded time, memory, files and disk, and the evaluator scores it
in a box of its own, receiving only the files the candidate wrote in
output/ and writing output/metrics.json, one flat object of numbers.
The candidate cannot read the evaluator or its cases, or write its
metrics; a run that goes over a limit is not scored. Selection is
deterministic code, weights from --weight NAME=NUMBER (negative to
minimise) or the plain mean. Because the search adapts to its
evaluator's cases, --test-input and --test-eval-input give a test set,
scored once on the final population and never used to choose. Every
generation is journalled before the population changes, with the seed
and model on its first line, and every program that ran is kept by its
digest. Programs and their output reach the model fenced as data.
Needs use-models; costs the model gateway as consumer "evolve".
*** The catalogue for shopping agents, over UCP. With ucp.catalogue on,
the site serves the Universal Commerce Protocol (version 2026-08-25):
its profile at /.well-known/ucp, and catalog search, lookup and
product at /ucp/catalog/. The same listing and prices as
/catalogue.json, every product in it rather than the listing's first
page, read fresh on each request. Reads only: there are no carts,
checkout or orders, and an agent buys from the product's page. Each
address has ucp.requests.per_hour. The demonstration shop turns it on.
*** Agents that pay, within mandates. A REST operation that spends money
declares "pays": where its amount, currency and payee are, a
parameter, body.FIELD or =VALUE. `quilzo spend grant AGENT` gives an
agent a mandate: a currency, named payees, a limit per payment, a
budget and an end date, granted by a person who may approve its
actions. A payment within a mandate granted --without-asking goes
ahead and is debited; one within a mandate that asks, or beyond every
mandate, waits for a person, and a person may approve one on its own.
Amounts are read exactly in each currency's decimal places. Every
payment leaves a receipt (`quilzo spend receipts`), a failed one still
counts, and paying is never earned away from asking. An agent's call
may now send a JSON body to an operation that declares one.
*** Features from a repository. A pull request's CI posts a package to
/features/check and gets the card and the plan, with both as Markdown
for a comment; the CI that runs on merge posts it to /features/install
and the feature is installed, or waits for a second person in Approvals
with the job as the one who asked. The job holds no secret: it uses a
token exchanged for its identity token. The same card, plan and rules
as the Make screen apply, and installing from another machine takes a
token that is short or bound to a key.
*** Short-lived and bound credentials for programs. A CI job exchanges
the identity token its platform signs for each run for a short Quilzo
token, so no secret is kept in the repository: `quilzo trust add
docs-ci --github octo/docs --ref refs/heads/main --as ci:docs --role
author --on /docs` declares which issuer, audience and claims are
admitted and what the job gets, for at most an hour; GitLab and any
other issuer with a discovery document work the same way. POST
/token/exchange is RFC 8693 token exchange: a job's identity token, or a
Quilzo token for a narrower one that lasts fifteen minutes. A token can
be bound to a program's key (`quilzo token keygen`, `token issue
--key`, or a DPoP proof at the exchange): it is then accepted only with
a fresh proof made by that key, RFC 9449, at /mcp, /v1 and /a2a, and
refused everywhere else. From another machine, publishing, rolling
back, granting, managing tokens and agents and installing need a token
bound to a key or one that lasts an hour or less; reading and drafting
take any token (token.proof_for_risky). New tokens last a week by
default and ninety days at most (token.ttl.default, token.ttl.max).
*** Ask Today a question, and keep the answer. Somebody who may use the
store's models asks in their own words, and the answer is cards Quilzo
draws: a figure, a table, bars, what is waiting, agent runs. The model
is told the collections they may read, by their fields and choices, and
the agents they manage, and chooses the cards and what each counts in
JSON; it never sees a record. Each card is checked as a listing is and
filled in from what the person may read, and says in words what it
counted. "Keep on Today" pins the answer as a view, drawn again on
every visit with the access the person has then. Charged to their
budget; `quilzo config set today.ask false` turns asking off.
*** A brief on Today: a few sentences on what matters most, written by the
configured model below what is waiting. The model never writes a
figure: it refers to each by a placeholder and Quilzo puts in the
figure, as a link to its records; a sentence with a number of its own,
a link or markup is left out. It is given counts, kinds of decision and
agents' names, never anything somebody else wrote. Written on a
person's first visit of the day and again when the figures change, at
most every half hour, through the model gateway and charged to their
budget; it says which model wrote it and when. An auditor is not given
one. `quilzo config set today.brief false` turns briefs off.
*** Today, the first screen after signing in. What is waiting for you to
decide, from every part of the program (the same items as Approvals),
with questions first, then what cannot be taken back, then the oldest;
and a count for each part of the store you can open: pages changed in
the draft and not published, agent runs going, finished and gone wrong
in the last day, visitors waiting for a reply, findings to triage and
cases open. Each count links to the records it counts, and the same
grant check as the menu decides which are shown. Signing in with a token, an identity provider or a passkey
lands on it; "Start on Pages after signing in instead" keeps the old
start, in your browser.
*** Roles for the people who work in an app. A feature package can
declare roles over its own collections, pages and agents: read or write
a collection's records; read, write or publish a page; run an agent or
approve what it waits for. The lab sample example declares a
technician and a manager. The card lists them, and nobody holds one
until somebody who may grant access gives it to them: `quilzo auth grant
tess lab-samples/technician`, on the People screen, or by mapping an
identity provider group to it. A role is the bindings it stands for,
marked with the role, so `auth explain` names it, and it is removed
whole and never half of it by hand. An upgrade that changes a role
changes what its holders may do, and needs somebody who may grant
access when anybody holds it; a version that drops a role, or removing
the feature, takes it from everybody. `feature show` says who holds
each. A model drafting a feature does not write roles.
*** Access per collection of records. Each collection is checked as
itself, at /@data/NAME, on the command line, the Data screen, the agent
interface (list_collections, list_records, write_record) and the content
API, so `quilzo auth grant tech author --on /@data/samples` lets
somebody, or an agent, work on the samples and nothing else, and see
only the collections they may read. A role over the site covers every
collection as before. The content API asked about /NAME, the namespace
of pages, so a grant on a section of pages reached the collection of the
same name; it no longer does.
*** Permissions for working with agents. Five decisions that were each
given to the nearest role, and all but two meant "administrator of
everything", are permissions of their own: run-agents, approve,
manage-agents, use-models and install-features. With none granted, each
comes with the role it always did. Each can also be granted alone on
part of the store (/@agents for every agent, /@agents/NAME for one), on
the People screen or with `quilzo auth grant mo approve --on
/@agents/newsletter`; it ends, can be denied, and `auth explain` names
it. Approval follows ownership: an agent's sponsor, and whoever holds
approve on the agent or on the page its action would change, decide what
it waits for, see those runs on Approvals and open and decide them
without the rest of the agents studio. Naming a sponsor stays an
administrator's, and a package that declares agents or webhooks needs
managing those agents, or an administrator, besides install-features.
A token caps a permission granted alone by its path and limits, so a
reader's own token can use what they were granted; identity provider
groups named after a permission grant it.
*** Research projects whose every claim can be checked. `quilzo research`
keeps a question, a plan registered before the data is analysed (its
SHA-256 signed with the log's keys; later changes are amendments with a
reason, made by a person), and computations: a Python, R or Julia script
run in a box with no network on copies of the inputs named, recorded by
the digests of its code, inputs, outputs and environment, with the
numbers it wrote to values.json. `research rerun` repeats one and says
whether every number and file came out the same. A report states a
number only as {{claim}}, pinned to a computation, and cites only works
an open index returned (OpenAlex, Crossref, arXiv, by DOI, PubMed ID or
arXiv ID); `research check` flags every number written in the prose,
every unresolved citation, every cited sentence its source does not
support, and every claim that did not reproduce. `research report`
writes it with references and a provenance table. Agents work on a
project through research_* operations and cannot register or amend a
plan. The Research screen shows the report with each number linked to
its computation.
*** A model gateway for agents built anywhere. With models.remote on, the
admin answers OpenAI's chat completions and responses and Anthropic's
messages at /v1, through the model gateway: an agent made with ADK,
Agent Framework, LangChain, CrewAI, OpenAI's Agents SDK or either
provider's SDK changes its base address and uses a Quilzo token as its
key. Each call is charged to the token's person against their budget
(door:NAME, or agent:NAME, and person:NAME), masked like every other,
cleaned of invisible characters, and recorded as model.door, refusals
included; it takes a token that may draft, in a store with an access
policy. Naming agent/NAME as the model brings that agent's memory: what
it keeps about the person is recalled into the conversation, and the
model is offered quilzo_remember, which Quilzo performs before answering.
What an agent built elsewhere remembers is kept as unreviewed, and held
until a person confirms it when the conversation carried a tool's
results or a picture. The Models screen shows the addresses; the
shield can close it (models).
*** Agents learn from their mistakes. `quilzo agent lessons reflect RUN`
has a model write a candidate lesson ("when X, do Y") from a run that
went wrong; `lessons try NAME ID` runs the agent's evaluations with its
playbook and with the lesson added, under the model its runs use; a
person adopts it for that model only if it helped and planted
instructions steered no more cases. Runs then read their adopted
lessons as advice, and each run's record names them. A lesson naming a
host, an address or something shaped like a credential is refused, and
one written from a run that read somebody else's words is adopted only
by somebody who says they read it. Agents › Runs › What it is learning
shows the agent week by week and its lessons.
*** REST APIs as tools. `quilzo integrations import openapi.json --uses
A,B` declares an API's named operations as an integration of kind http,
switched off: each operation is a tool with the same allow-list, pins,
gateway, approvals and audit as an MCP server's. A call is built only
from what the operation declares, refused if it carries anything else,
sent over https to the declared host with the credential added (bearer,
or a named header), and its answer cut off at 32 KiB. A REST API can be
offered to other agents through /mcp/gateway/NAME. The same command
reads an MCP registry server.json into an MCP server declaration;
without --uses it lists what is on offer and declares nothing.
*** Facts with time. An agent remembers a fact with a key ("shipping_city"),
and a newer fact with the same key replaces the older one, which is
kept with when it stopped being true and what replaced it; `quilzo
memory history AGENT KEY` shows the chain. A fact learnt after reading
somebody else's words replaces nothing until a person confirms it.
Recall ranks by words and vectors fused by reciprocal rank, with
recency, over the text itself, so it works the same whichever model
an agent uses; it returns only what matches and stops at 6,000
characters.
*** Features as packages (/make, `quilzo feature`): one file declares the
types, collections, listings, forms, pages, menus, chatbots, agents and
webhooks a piece of work needs. `feature check` shows its permission
card, worked out from the package: what it stores and whether that is
about people, what it collects, shows and sends, and what its agents
may do. One that holds personal data, collects from the public, sends
outside or adds an agent that acts without asking waits for a second
administrator. A feature only changes what it made, never publishes,
and can be rolled back or removed as one. `feature export --sign`
signs it with the log's keys; `feature install --keys` checks them.
`feature capture` packages blocks built by hand.
*** Make a feature from a sentence or a spreadsheet. `quilzo feature draft
"…"` (and Make › Describe it) has a model draft a package; the reader
keeps content blocks only, never an agent, a webhook or a link to
another site, and a draft that fails the checks is shown its failures
once. `quilzo feature from FILE.csv` (and Make › From a spreadsheet)
infers a type from the values, with a collection, a listing and a page;
`feature install --rows` imports the rows with it, after approval when
it needs a second person. Neither installs anything by itself.
*** Approvals (/approvals, `quilzo approvals`) lists everything waiting for
the person looking: what agent runs ask, questions they need answered,
actions the reviewer stopped, tool calls held for a second person, acts
a case waits to have approved, a change waiting for review, memories
held until confirmed. Each says what deciding commits to. Several runs
waiting on the same action that only reads or acts can be let through
together; anything that writes, sends, pays, deletes or publishes, and
every question, is decided on its own.
*** A decider less sure than its declaration demands stops the run with its
question, typed as it was asked, the model's guess and confidence beside
it. The run goes on with the person's answer (`quilzo agent answer RUN
STEP NAME=VALUE`, or the run's page). The reviewer's refusal is a hold
on the exact action, and letting it through performs that action.
Steps a decider answered show how sure it was.
*** Every step records what it cost in tokens and money; `quilzo agent cost
--by agent|person|goal|day` and /agents/cost group the kept runs. A run
that paused for a person no longer starts its money budget again.
*** `earn_after` lets a kind of action stop asking after that many
approvals in a row while evaluations still earn the agent its autonomy;
a decline, the reviewer, the breaker or a regressed evaluation sends it
back. Sending, paying, deleting, publishing and signing in always ask.
`quilzo agent earned NAME [--reset]` and the agent's page show it.
*** `quilzo agent credential issue|list|revoke|verify|keys`: a signed W3C
Verifiable Credential another vendor's agent presents at a browser
grant, bound to its own key and proved on every request; the issuer's
keys are at /.well-known/agent-credentials.json.
*** `quilzo --json eval run` prints the whole report as its machine
document, attack-success numbers and all — the planted-instruction
rates at one and many attempts, per case — for a deployment to publish
what it measured.
*** `agent.reviewer` (off by default) names a second model every action is
offered to before it runs. It sees the goal a person set and the
action chosen, and nothing of what the agent saw — no page, no
document — so nothing a page planted can reach it. It can let an
action through or stop the run with its reason, for a person to read
and continue from; it can never widen or rewrite one. A reviewer that
cannot be reached fails closed: nothing runs. It is the last defence,
not the only one — the manifest, the breaker and ask-first are
deterministic and come first — and it costs one more model call per
action, which is why it waits for a setting.
*** Desktop cells, experimental: `internal/cell` drives a microVM
(Firecracker or Cloud Hypervisor, whichever binary the operator
installed) over its own Unix socket, restores a warm snapshot of a
desktop, and speaks to a helper inside through the snapshot's vsock:
the accessibility tree as an outline with references, presses, typing
into fields, the four named keys, and a picture. A press is weighed
by this program from the element's role and name, Enter always asks,
and typing into a cell is under the exfiltration breaker. `quilzo cell
status` says whether this machine can run one and what it would
restore (`cell.backend`, `cell.binary`, `cell.snapshot`, `cell.memory`,
`cell.vsock`). One cell runs at a time. The image, with its X server,
accessibility tree, apps and the helper, is the operator's to build,
and the microVM runs as this program's user without Firecracker's
jailer.
*** The governed browser is offered over MCP, to other vendors' agents:
`quilzo browser grant NAME --read HOST,… [--write HOST,…] [--daily N]
[--pictures]` declares what an outside client — Claude Code, Codex,
anything that speaks MCP — may do at `/mcp/browser/NAME` on the admin,
reached with any token whose access reaches that path: a reader's to
read the page, an author's to act on it. Each call is a step of a
kept run: the same host rule on every request the page makes, the same
breaker, and a press that commits to something held for a person
exactly as a gateway tool is — approved once, within the hour, by
somebody else. The browser is Quilzo's, in a box; the client never
holds its pipe, and nothing it sends is a selector or a script — it
names elements by the references of the outline it is given. A grant
offers no credentials and no signing in. `--off` withdraws one.
*** A run waiting on a person over its browser can be taken over: on the
run's page, "Take the page over and do it yourself" puts the person's
own hands on the page — a click on the picture presses there, and text
or Enter, Tab, Esc and Backspace go to where the page holds its focus.
Each thing done is recorded in the log as that person's (`agent.person`),
never as the agent's or the model's, and what a person typed is kept by
length and digest and not by its words. The page's rule still holds
for their hands: the browser reaches only what the agent declared.
The question they took it over for still wants its answer, so the run
is declined or let go ahead when they are done.
*** `browser.cache` (off by default) replays a browser flow that worked
without a model call. A remembered step passes every check a decided
one does — the manifest, the breaker, ask-first with the person — and
confirms itself by the page that follows it: the moment the page is not
the one the flow remembers, the model decides again and the memory is
dropped. Signing in is never remembered. What the cache holds is
sealed with the store's keyring and kept per person, so a run for one
person never types what a run for another typed, and each replay is
written to the log
(`agent.cache`) with the digest of the page it ran against, so a
receipt can say which steps ran without a model.
*** An agent's manifest may name a `dialect`: the vendor computer-use
language its model answers in — `anthropic-computer`,
`anthropic-browser`, `openai-computer` or `gemini-computer`. The model
is prompted in its own maker's shape and its answer is translated onto
Quilzo's actions, which changes nothing about what it may do: a word
the manifest does not hold is refused in the manifest's own terms, and
a vendor gesture with no honest equivalent (a scroll, a right click, a
drag) is refused rather than approximated. A click by coordinate
resolves to the outlined element at that point and is weighed as that
element's own press; typing goes to the field last pressed, re-checked
before anything is typed. A model answering in a dialect may return
several actions at once, as its maker's runtime taught it; they run in
order, one a step, and the first failure drops the rest.
*** `quilzo agent declare FILE` amends an agent that is already declared.
An amendment that widens what it may do is refused until `--widen`,
with the exact list: capabilities, hosts, programs, tools, sign-ins,
what stopped asking a person, budgets, what it reads and remembers,
autonomy and kind. The widening allowed is written to the log as it
was read.
*** `quilzo agent receipt RUN --bundle FILE` writes one HTML file of the
run's steps, the pictures a person was asked about, and the receipt
with its proofs. It needs the access the agents screen needs, since it
carries the run's pictures, and nothing in it runs.
*** `quilzo agent template save FILE` adds an archetype of the store's
own, a kind with a starting declaration, and `quilzo agent new NAME
--kind KIND` knows it. A template validates as written, never claims
a built-in kind or the supervisor's, and is not removed while agents
of its kind are declared.
*** An agent holding `decide` asks a declared decider a typed question
inside its run. An answer under the decider's confidence threshold
says a person decides and carries no value to act on.
*** `quilzo desktop consent` asks the desktop's own portal for the
person's keyboard and pointer and gives the session straight back.
No run may use a person's desktop.
** Fixed
*** Pictures and the writing assistant go through the gateway. quilzo
media generate and the assistant (quilzo assist and the Assistant
screen) called the configured model directly: outside every budget
and the usage ledger, and media generate without taking personal data
or credentials out of its prompt. A route added with --draws is an
images model used for pictures and nothing else, charged by the
picture (--price-picture); a picture is charged to media and to the
person, and the assistant to assist and to the person, each against
its budget and on the record.
*** A --token given on the command line is who every command acts as.
The check in front of each command used it, and a hundred and
thirty-five places inside commands then acted as, and recorded,
whoever the token file or QUILZO_TOKEN named instead: an approval that
needed a second person could compare the wrong one.
*** An entry added to the schedule while another was cancelled or fired
is kept. The browser, the command line and the timer each read the
schedule, changed it and wrote it back with nothing in between, so the
second writer lost the first's change. They now take one lock. And
quilzo schedule add and cancel record the person --token named, not
whoever QUILZO_TOKEN or the token file did.
*** A forwarded address is believed only from a proxy that is named or one
on this machine, and the shield exempts this machine, not every private
address. With site.trusted_proxy or admin.trusted_proxy on and no proxy
named, X-Forwarded-For was believed from any private network, container
bridge, link-local or carrier-grade NAT address, so anything there could
choose its own address for every limit and the audit log; now only
loopback is, and the site and the admin say once which other machine
forwarded an address that was not believed, with the command that names
it. The shield never blocked or slowed an address on a private network
or carrier-grade NAT, or a request a named proxy made itself; both are
now counted, slowed and blocked like anybody's, and only this machine
and the networks added with `quilzo shield trust add` are never blocked.
`quilzo shield lift` on this machine ends a block.
*** A page of nothing but unclosed brackets or links rendered in time
that grew with the square of its length: twenty thousand took a second
and a half, on every request. Each marker now looks a bounded way for
its partner.
*** Every record has its page, not only those among a listing's first
rows. A detail page found its record among the rows the listing shows
at once, so the 51st event, the 21st product of a catalogue listing
twenty, and every post but the newest answered "not found" at their
own addresses, linked from feeds and search results; a copy of the
site left their pages out too. The listing's conditions and allow-list
decide, as before, and its number of rows does not.
*** The sitemap lists the page of every record: each post, event and
product at its own address. It listed pages only, so it named none
of them and named instead the page that stands for them all, which is
no address of its own. A sitemap whose records depend on the clock
changes its validator with it.
*** quilzo events setup and the Make screen's ready-made features put
pages in the draft marked as written by Quilzo, so they publish. They
had no provenance, and publishing refused them until somebody said who
wrote a page they had not written.
*** A picture field on a record's form offers the library's pictures by
what they show: type "rye" and choose, instead of pasting the hash of
a file. The help under a picture's description names the picture by
its label ("Picture"), not by its field's key.
*** quilzo records add takes a list as JSON: tags='["rye","bread"]'.
It was stored as the words, and a type wanting a list refused it.
*** The Make screen says a ready-made feature is installed instead of
offering to add it again.
*** quilzo mcp answers in the revision the client asked for. On
standard input every client was answered in 2025-06-18, even one
asking for 2025-11-25; it negotiates as the HTTP transport does.
*** The Sign-ins screen's Help opens the manual's Sign-ins guide, not
the automations guide that left with them. Its first line no
longer says "the 0 networks you declared" on a new store. The agent
form no longer lists security operations nothing offers, and quilzo
help says what shadow AI is now: what agents' own programs reached
around the gateway.
*** The public site is confined. quilzo site re-executes itself under
Landlock before it opens anything: it reads the store and writes only
its own state, and the store's policy, tokens and configuration, and
its root, are out of its reach. It checks that at start and refuses
to run if they are not. What it writes moved out of the store's root
into directories of its own (shield/, fediverse/, usage/,
webhook-deliveries/, tmp/; webhook deliveries are kept apart from the
endpoints and their secrets), and the first command run after an
upgrade moves an existing store's across. quilzo site --prepare
makes and lists those paths for a service manager; --confine=false
runs the site unconfined and says so. The systemd unit in deploy/
named the wrong store and only the submissions directory, so a site
run by it could keep no shield state, audit record, analytics, board
or member; it lists every path the site writes now.
*** People are pseudonymous in every part of an audit record. The log
pseudonymised a record's principal and the details that name a person,
and left the same people in clear in its sentences and its resource:
"make lee admin on /", "lee declined this", "rae is reader on /",
"dana is the only administrator", a grant's resource "/lee". Those
went to the log and to every batch streamed from it. A record's
writer now names everybody it mentions as a person (a grant's
subject, whom a token is for, who decided an agent's step or a held
call, an agent's sponsor, who gave an enrolment link, the people a
posture finding lists), and a keyed log gives them the same handle
wherever the record names them. The Logs screen names the people the
store knows there too. The test suites check every record they write
for a person in clear.
*** Mapping an identity provider's group to admin takes two
administrators. A group mapped to admin, or to the analyst or
compliance job, makes everybody the identity provider puts in it an
administrator, and one administrator could map it from the
Provisioning screen or quilzo scim map, past the second administrator
that making one by hand waits for. It waits now, on People and in
quilzo pending, and is made when another administrator approves it.
A group mapped to anything less is still one administrator's to map.
*** A quick answer to a running agent's question is not lost. The run's
page and quilzo agent approve can answer from the moment the question
is offered, and the process holding the run removed whatever answer it
found when it started to look, so an answer given in that moment was
thrown away and the run waited until its time ran out. Each question
is stamped when it is asked, an answer names the question it is for,
and nothing is removed before it is read.
*** Smart lockout works for IPv6. The admin hands the lockout a client's
source as internal/clientip writes it, which for IPv6 is the client's
/64 as a prefix, and the lockout read that as no address at all: an
IPv6 guesser was never locked out of anything, an IPv6 person never
had a familiar place, and a guesser could fail a hundred times in a
row and turn an account's passkeys off for its owner. Every source is
placed now, and one that cannot be is treated as unfamiliar.
*** A passkey sign-in or removal lands on the key it is for. The admin
changed its list of passkeys by position, with nothing to stop two
requests at once: a sign-in wrote the counter that detects a cloned key
back at the position it found before checking the signature, and a
removal cut at a position found before another removal moved the
list, so either could land on somebody else's passkey, and a save could
write an older list over a newer one. Every change is now made by the
key's id, to the list as it stands, and saved before the next.
*** A budget for a model cannot be refilled by a full disk. The gateway
wrote each call to model-usage.jsonl and threw the write's error away,
so a ledger that could not be written kept counting in memory while
the file, which budgets are counted from after a restart, fell behind.
A call the file refuses is now kept and written first at the next
call; until then a caller with a budget is refused (one without has
nothing to forget), and the audit log says when it started
(gateway.ledger-failed) and when it ended (gateway.ledger-restored).
It is not reported to the shield as overspending.
*** The audit stream's receiver checks the address its sender signed.
Behind a proxy that ends TLS it rebuilt the address from the request,
plain HTTP to an internal host, and refused every batch. Its Origin
is the receiver's public address, which a receiver of Quilzo's stream
sets from admin.base_url.
*** Publishing over MCP is a tool of its own, quilzo_publish. It was an
operation of quilzo_write, whose description says "Never publishes",
so a host that asked a person whether an agent might change the draft
was asking whether it might put the draft in front of readers too.
quilzo_publish tells a host it changes what readers see
(destructiveHint), so a host can let an agent draft and still ask
before it publishes. quilzo_write refuses publish and says which tool
to use; a program that called it by name moves to quilzo_publish, and
an agent that finds its operations with quilzo_find is told.
*** --json answers in JSON everywhere it lists something. Nineteen
commands printed their prose instead: agent list and templates, apps
list, audit, auth list and roles, brand check and list, browser
status, diff, inbox list, integrations list, log, peer list, research
list, rights, secret list, token list and version. Each now writes
one document (never a token's hash or a secret's value, and a refusal
still exits non-zero), and a test runs every one of them as a caller
would.
*** Getting started's "Publish it" opens Review. It linked to /publish,
which only takes the publish form, so the one link on the first screen
a new installation shows that matters most answered "POST only".
*** Passkeys and Inventory fit a phone. The passkey list was the one
table in the admin outside a scrolling frame, so its Remove buttons sat
off the right edge and the whole screen scrolled sideways; a sentence
naming a file by its whole path did the same on Inventory. A test now
fails on a table outside the frame.
*** A picture opened on its own is shown as a picture. The admin's
style policy refused the styles the browser gives an image or a film
it displays by itself, with an error in the console for each, so the
picture sat in the corner of a white page. Media shown in place now
has a policy of its own: the browser's styles, and nothing that runs.
*** CodeQL reads this program's escaping. Its cross-site scripting rule
reported every page the public site renders, and its cookie rule every
cookie the admin sets, and the required check failed any change large
enough to touch them. A model pack in .github/codeql/extensions says
which functions escape and why, each claim recorded with what would
make it wrong in docs/codeql-dismissals.md, and three small changes let
the analyser follow the rest; twenty-one of twenty-two alerts are gone
without a dismissal.
*** A fresh install gets its first token again. `quilzo auth grant you
admin` then `quilzo token issue laptop --principal you`, the way the
manual says, waited for a second administrator nobody could be: the
first token in a store, for the only administrator the policy names,
is now that administrator's change, made alone, and recorded as the
bootstrap (dual.approved, bootstrap true). A second token still
needs one presented.
*** What the admin does is in the audit log. Its records took their
principal from the detail's "by", and publishing from the browser,
adding and resolving a note, editing a picture, exchanging a token and
enforcing the organisation's policy were written without one, so the
log refused each of them (AU-3: who) and kept nothing; so were the
refusals at the door, with nobody signed in: a wrong enrolment link, a
refused single sign-on, an account locked by somebody's guesses. Each
now names who acted, a refusal at the door is kept as an anonymous,
unverified caller, and a refusal or a failure is recorded as one
rather than as a success. A test walks the admin's source and fails
on a record that says nobody.
*** A grant says what it lets somebody do. The line after `quilzo auth
grant` was worked out from the policy as it was before the grant, so
it said "can now: nothing" for every grant.
*** A sandboxed program can no longer reach the network through
Multipath TCP. Landlock's TCP rules do not apply to MPTCP sockets
(seen on Linux 6.18), and Go has opened its listeners as MPTCP by
default since 1.24, so an extension whose network was denied could
still connect anywhere and listen, and an agent's program could
listen inside its box. The system call filter now says MPTCP does
not exist, which Go and the C libraries fall back to plain TCP on,
and refuses listen(2), which binds a socket that has no address
without asking Landlock. Extensions, which are not hardened, now
get this part of the filter whenever their network is denied.
*** Two factors on every request a person makes to the admin (NIST SP
800-63B-4 AAL2), checked on each request rather than at the sign-in
form. A session now records how it was signed in to, its assurance
level and when. A passkey, which asks for its PIN, fingerprint or
face, is two factors; a single sign-on is two when the identity
provider says it used two; a token is one.
Signing in by pasting a token gives a session that reaches only
"Confirm it's you" until the person's passkey confirms it. A token
sent in an Authorization header is refused by the admin's screens;
programs use the command line, the API or the agent interface. A
passkey is added from a two-factor session. Single sign-on required
now holds on every request, so a session that did not come through
the identity provider is refused however it was made. An
installation gets fourteen days from the first start of this version:
until then a token alone still works, every page says when that ends,
and the posture check mfa.no-second-factor names who has no passkey
or single sign-on that can say it used two factors.
*** The provisioning token ends. Whoever holds it can create people and,
through a mapped group, make them administrators, and it worked for
ever. `quilzo scim token` now makes one that lasts 180 days (--days,
at most 365); `quilzo scim status`, the Provisioning screen and the
posture check scim.token-expiring say when it ends, a month ahead.
An ended token is refused, which also stops the identity provider
taking access away, so make the next one before then. A token made
before this release keeps working for 180 days from when it was made,
and for at least 30 days from the first time it is used after the
upgrade.
*** Single sign-on through OpenID Connect finishes only in the browser
that started it. The callback address of a sign-in somebody else
began at the provider, opened in another browser, signed that browser
in as them (login CSRF). Starting a sign-in now sets a cookie the
callback requires, as SAML sign-in already did.
*** A member's recovery code opens the account to add a new passkey and
nothing else, for fifteen minutes. It used to give an ordinary
session: the members' pages, posting, new recovery codes, removing
passkeys and deleting the account. Adding the passkey ends that
session and signs the member in as a passkey sign-in would.
*** A REST integration's request body is checked against the schema its
declaration gives before it is sent. A field the schema does not
list is refused unless the schema allows other properties, so a model
steered by something it read cannot add one the API would accept.
*** The fediverse inbox checks the body against the digest the signature
covered, and only that one. A sender that signed only the older
Digest header could have its body replaced by anybody on the way who
added a Content-Digest of their own, because the unsigned header was
the one read.
*** "Confirm it's you" no longer sends a code by email. NIST SP
800-63B-4 forbids email for this (3.1.3.1: email shall not be used
for out-of-band authentication), and whoever can read a person's mail
could pass it. A session the automations hold back is confirmed with
the person's passkey, by signing in again through single sign-on, or
by another administrator from Sign-ins. Three failed passkey
confirmations from such a session raise verification-failing for the
automations, as three wrong codes did.
*** Agents do nothing while their actions cannot be recorded. With the
log writer down, the audit key unreadable, the log's disk nearly full
or the last record refused, an agent's run used to go on acting with
no record of what it did. Each action is now checked against the log
first and refused while it could not be written, and the run stops
with that reason; a step whose record was refused stops the run
before its next action. The same holds for changes made over the
machine interface, presses in a browser an outside agent drives, and
tools forwarded to another system through the MCP gateway; reading
goes on. What people do in the admin and on the command line still
goes ahead with the failure reported, as before.
*** Exchanging a copied token no longer gets round proof of possession.
From another machine, publishing, rolling back, granting, managing
tokens and agents and installing take a token bound to a key or one
that lasts an hour or less. A long-lived bearer token exchanged at
/token/exchange gave a fifteen-minute session that passed as short,
and an exchange proved with any key gave one that passed as bound,
so whoever copied the token could do all of it. A session now counts
as the token it was exchanged from: from an unbound token it is as
long-lived and as unbound as that token, whatever key proved the
exchange; from a bound token, whose own key must prove the exchange,
it is short and bound. An app's consented token, a CI job's own
token (quilzo trust) and key-bound tokens work as before.
*** The site's search finds pages published while it runs. The index
was built once, when `quilzo site` started, so a page published
afterwards could not be found until a restart. It is now built again
from the live commit the first time somebody searches after a
publish, as are the vectors behind /api/v1/similar and
/api/v1/search/vector.
*** A run waiting for a person is never pruned. Runs are kept up to
five hundred, oldest removed first, and that count took away runs
still waiting on a question, and runs a process was working on. Only
finished runs are removed now.
*** `quilzo --json a11y` answers in JSON: the pages checked, every
finding, and what was and was not checked on each page.
*** `quilzo publish` with nothing drafted names the add command as it is
typed: quilzo add NAME=FILE.json.
*** Writes that happen at the same time no longer undo each other's
security decisions. A sign-in in the admin could write back a token
that the command line had just revoked; a publish sending webhooks
could put back an endpoint removed while it was sending; provisioning
requests arriving together, or the admin and the command line, could
each lose the other's grant or revocation; a run could bring back a
person's memory that had just been forgotten. The policy, the agents'
declarations, the webhooks and each agent's memory are now changed
under the file's own lock on what is stored at that moment, and the
tokens are written merged with what is stored, so a revocation is
never lost.
*** Provisioning that cannot take somebody's access away says so. A
deactivation, a deletion, a rename or a group change whose access
could not be removed answered success, and the person kept it; it now
answers an error the identity provider sends again, and a deleted
user or group stays until it can be removed properly.
*** Model calls work with 2026's frontier models. Every call sent a
temperature of 0.2, which Claude's current models answer with an
error, so each call through such a route failed, rested the route and
moved to the next one without anybody being told why. Nothing beyond
the conversation and the caller's own limit is sent now unless the
route asks for it: `quilzo gateway route add` takes --temperature,
--top-p, --max-tokens-field max_completion_tokens (for OpenAI's
reasoning models), --timeout for a model that reasons at length, and
--auto-tool-choice for a model that refuses a forced tool choice. The
Models screen has the same fields under "What this model is sent", and
the list of routes and `quilzo gateway status` show them.
*** A request every route would refuse is no longer sent to each in
turn, and no longer rests a route. A provider's 400, 413 or 422 goes
back to the caller, and the model door answers it with that status
rather than 502; a timeout, a rate limit, an outage, a refused key or
an account out of credit still rests the route and moves on, and a
caller giving up rests nothing. Somebody allowed at the model door
could otherwise keep a route resting for everyone with requests it
refuses anyway. These calls count against their callers' budgets.
*** The reviewer is a second model. agent.reviewer names a route, and
the review used to walk the whole fallback chain from the first route,
usually the model the agent itself runs on. It is now asked through
the route named and no other, so a reviewer that cannot be reached
stops the run instead of being replaced by the agent's own model.
`quilzo config set agent.reviewer` and the Settings screen refuse a
route that is not declared, and the posture check
agent.reviewer-not-second reports a reviewer on the first route or on
one that has been removed. Reviews are charged as reviewer:AGENT and
to the person who started the run. The action the reviewer is shown
is fenced with a marker new for every review, so text the agent chose
cannot close the fence and go on as the reviewer's own prompt; and
settings that cannot be read stop a run instead of letting it run
without its reviewer.
*** A tool choice naming one tool is passed on as that tool. The model
door read OpenAI's {"type":"function","function":{"name":...}},
the Responses API's {"type":"function","name":...} and Anthropic's
{"type":"tool","name":...} as "any tool", so the model could call one
the caller had not asked for. A choice of a tool the request does not
offer is refused.
*** Drafts, features, evolution, research ideas and reflections are
charged to the person who asked for them, as person:NAME, so a budget
set on a person bounds those as well as agent runs. A budget may name
a caller by the email address single sign-on gives them
(door:dana@quilzo.io).
*** Quilzo reads its own audit log on the server's timer. The rules about
Quilzo itself, what agents did and the correlations across them ran
only when somebody ran `quilzo detect run`, a feed pushed events or a
scheduled collection brought some in, so an installation that collects
from nowhere ran none of them. Every quarter of an hour in which the
log has grown, the rules run, and what they raise goes to the
automations.
*** A new store starts with the shipped detection rules, and a server
whose store has no rules directory installs them when it starts. They
are files in the store as before, the organisation's to tune or switch
off; a store that has its own rules keeps exactly those.
*** Making somebody an administrator is detected on every path: the
People screen, `quilzo auth grant`, and provisioning from an identity
provider, which now records each role it gives and takes away
(scim.granted, scim.revoked).
*** `quilzo token exchange --json` is recorded; the session a script asked
for left no record.
*** A webhook a feature package adds, changes or removes is recorded on
its own, with where it sends, and feature.installed records how risky
its card said the install was and how many people it took.
*** A deny or a grant on part of the site holds on the machine interface.
Its page operations asked about the whole site, so somebody denied
/legal could list, read, search, compare, write and export it over MCP,
and somebody granted only /blog could use none of it. Each page is
now asked about its own path.
*** Publishing asks about every page it would change, on the command
line, in the admin and over MCP. A deny of publishing on one page was
not seen by the check on the whole site, so publishing the draft
published that page too.
*** An export is refused to somebody who may not read every page and
every collection in it, on the command line, in the admin and over
MCP, rather than handing them what they may not read.
*** What an agent waits for is decided by the session, not the person.
A session made from a read-only token approves nothing, and one capped
at reader or confined to part of the site approves only what that
credential could. The admin rebuilt every session as an administrator
with no limits before asking, so a narrow credential could let a held
payment or publish go ahead whenever its holder could elsewhere.
*** The person who started an agent run does not let its risky steps go
ahead while somebody else could: a step that pays, publishes, deletes,
sends or writes to another system waits for another person who may
approve it. Declining, answering a question and approving a change to
a draft are as before. When nobody else could decide, the person who
started it still may; the audit record says the approval was made
alone, and posture counts them (approval.alone).
*** A person approves anything a model wrote before it is published, and
no setting turns that off; `quilzo review require --human-for-ai` is
gone. The person whose agent run wrote a draft approves it only when
nobody else may publish, and is then recorded as approving alone.
*** `quilzo review approve` takes the publish permission, as the review
screen already did; a reader's approval counted towards publishing.
*** A passkey has to ask for the PIN, fingerprint or face that unlocks
it. A key that was only touched signed an administrator or a member
in, and an administrator's sign-in was still taken as a strong one: let
through a lockdown, with its address exempted from the shield. Adding
a passkey and signing in with one now ask the browser to verify the
person, and an answer without it is refused with what to set up; no
setting turns this off. A passkey already registered keeps working
when it signs in with its PIN, fingerprint or face. Each passkey
records whether it can be synced to other devices and whether it is,
the second brought up to date at every sign-in, and an administrator's
passkey sign-in is in the audit log with both.
*** Somebody suspended by provisioning is refused at the passkey sign-in,
as single sign-on refuses them, and the refusal is in the audit log.
Their passkey still made a session, which every screen then refused,
and exempted their address from the shield. A suspension is a live
deny of everything on the whole site; a deny of publishing, of one
permission or of a person's own work, or one that has ended, no longer
refuses a single sign-on or passkey sign-in as if it were one.
*** A passkey is added or removed only from a session that holds the
whole person: their own role, over the whole store, with no limits on
the token. Any signed-in session could add one, including one from a
token narrowed to part of the store, a lesser role or reading only,
and the passkey then signed in as the person's full self and outlived
the token. Refusals, enrolments and removals are in the audit log.
Passkeys now also work behind a proxy that terminates TLS when
admin.behind_tls_proxy is set, as the session cookie and single
sign-on already did.
*** Signing in with an identity provider or a passkey gives a session the
highest role the person holds anywhere. It took the role they held
over the whole site, so an analyst, administrator of /@security alone,
got a reader's session that refused them the security screens, as did
anybody granted only part of the store. The session's role is a cap;
the policy still decides where it reaches.
*** What an app did through the agent interface, and what an agent did
under its own identity, is recorded in that act's own audit record:
granting, assistants, deciders, automations, findings and every other
command an app can reach. The record named no model, which the log
refuses for an AI act, so only the interface's mcp.call record was
kept. It now names the app (mcp-client) or the agent as its model,
and a test reads the source for any record that does not.
*** `quilzo records import` now checks every record against its
collection's type, as adding one record does; one that does not fit
imports nothing.
v0.3.0 — 20 September 2026
==========================
Ninety changes. Most came out of two sweeps: driving the running program
through a browser and a terminal rather than reading it, and an audit for
things this program declared and never executed. The second found more, and the
reason is consistent -- in every case the code was correct about its own job
and wrong about what another part was doing with it.
The security fixes here also shipped as v0.2.2 on the v0.2.x line. That release
notes what did not backport and why: three fixes are for code that does not
exist in v0.2.x, and two checks were left out because their subject does not
either.
Eighty-two changes. Most of them came out of two sweeps: driving the running
program through a browser and a terminal rather than reading it, and an audit
for things this program declared and never executed. The second found more
than the first, and the reason is consistent — in every case the code was
correct about its own job and wrong about what another part was doing with it.
** Declared, and never executed
*** `quilzo vault rotate` printed that it had rotated and had not. It added a
key and made it active; the objects wrapped under the old one were never
rewrapped, so the key nobody could remove was the one everything still
needed. The rewrap existed, correct and complete, and had no caller.
*** Six settings reported success and did nothing. Settable, validated,
compliance-mapped, rendered in the admin, risk-acceptance recorded — and
read by nothing at all.
*** Seven of eight agent archetypes could not do what they declared. The
executor was constructed with no resolvers, so the content scope in a
manifest was never asked about anything.
*** A manifest restricted nothing and said otherwise on every screen. Three
more fields of the same struct followed: retrieval by type and locale, then
by path, each carefully intersected and then consulted by nobody.
*** Two things were removed rather than wired, because a manifest may declare
only what this build does: agent memory, which was declarable in three
tiers, refused without a retention period, capped at ninety days, rendered
in two interfaces and published on the agent card, and which nothing wrote
or read; and the `http` and `process` integration kinds, which validated,
displayed and switched, and which only `mcp` is ever called for. A
process integration's digest — the one supply-chain control that would
matter if one ran — was checked for shape and never against a file.
*** A lockout nobody could see. The throttle's snapshot was written for a
command that does not exist and, as written, could not: the counters live
in the memory of the process holding them, so the throttle's consequences
were unobservable on every surface. The state is on the admin's posture
screen now — how many, how hard, how long,
and no names, because the list of principals currently being attacked is
itself worth protecting.
*** The content API validated content and wrote nothing down. Every other
write path records a binding saying *this content, this type, at this
moment*; the API had its own copy of the gate and skipped that half, so
content arriving over HTTP was the one kind with no record of what it was
validated against.
*** A field recorded on every WebAuthn enrolment to answer a question no
screen asked.
*** `quilzo studio` was a fourth surface with its own idea of how many
sign-in attempts are free, built after the comment saying there is one
place for that.
** Found by using it
*** Eighty valid form submissions, one address, none refused. Three faults on
the anonymous write path, including a demo form that could not be
submitted at all, and a spam check that worked perfectly and counted
nothing.
*** A peer pull read a ref, decided from it, and wrote without holding it.
Sixteen concurrent writes against one base all passed the check and all
committed; fifteen edits vanished.
*** An action carrying only a delegate ended the run. Action.Done was
`Op == "" && Tool == ""`, complete on the day it was written, and a
supervisor's first hand-off named neither -- so the loop stopped, took
whatever the supervisor had said to the delegate as its answer, and
reported a clean run that did nothing. The same field was missing when
tool calls were added. A test now compares what the dispatcher routes on
against what Done knows about, in the package that adds the branch.
*** The host a tool call reaches was a field the model filled in. It is
resolved from the manifest now, and a request naming another host is
refused.
*** Two keys can be the same setting. `primary` and `primary.dark` are
different strings and the same setting for the dark scheme, so `theme
generate` and `theme import` both compared keys literally, never demanded
`--replace`, and left half the old design in charge of the scheme fewer
people look at.
** Agents
*** An agent can call a tool. Everything guarding one already existed — a
host allow-list with no wildcards, a vault-named credential, a client that
refuses a tool the far side has newly advertised — and the dispatcher had
no branch for it, so every authorised call fell through and answered that
it was not implemented. A tool call now taints the run for the same
reason reading the store does, and more so: a third-party result is
whatever a host returned on this request, reviewed by nobody here.
*** The watchdog could not see a single agent run. internal/agentwatch exists
to notice a model behaving badly -- an agent hammering at operations it
keeps being refused -- and its only input is the audit log, filtered to
events where the actor was a model. Every `agent run` was recorded under
the principal of whoever started it, with the manifest's name as a detail
field, so:
$ quilzo agents
no model has acted in the last 24h0m0s
with six runs in the log. cmd/quilzo/assist.go already said why that is
the wrong way round -- "the model is the actor, and it is recorded as one"
-- and the MCP surface and the assistant both did it that way. The runner
built specifically for agents did not.
The principal matters as much as the kind, because the watchdog buckets by
it: with the person there, every agent one person runs would have merged
into one report, and "the support bot is hammering at publish" and "the
archivist is fine" would have averaged into a number about neither.
A run without `--model` stays a person checking a manifest. Nothing chose
anything -- the plan is the manifest, every capability tried once in a
fixed order -- so it cannot misbehave in the way the watchdog looks for.
*** An outcome is not a bill. Both agent record sites asked Receipt.Billable
and wrote "denied" when it said no. Billable asks whether there is an
outcome to charge for; denied means the gate refused something. Three runs
that could not reach the model were recorded as denied, and the watchdog
then reported an agent repeatedly refused -- its signal for one that has
not accepted the answer, which describes a broken endpoint not at all.
*** The same refusal read differently each time. The agent gate's refusal
naming which hosts an agent may reach was built by ranging over a map, and
Go randomises map iteration on purpose -- so the same refusal, for the same
reason, produced a different sentence on every run, into a tamper-evident
log where it was then recorded under a different digest depending on
nothing.
Found by writing the check for the claim it breaks. The AgentDojo figure
is reported as a lower bound rather than an estimate, and that is only true
if the gate answers the same way every time; nothing was checking.
`scripts/agentdojo/passk.py` runs the whole corpus k times in fresh
processes and in shuffled order, comparing every individual decision and
its reason rather than the totals -- totals can agree while decisions swap.
pass^10 is 1.00, and the same property is a unit test so a change that
breaks it fails where it was broken.
Not the reliability literature's pass^k, which asks whether an agent
succeeds on all k attempts and needs a model. Said in both the harness and
its README, because a benchmark that quietly substitutes one number for
another is worse than one that says what it lacks.
*** A tainted run says what tainted it. The taint rule is the load-bearing
part of the agent design -- anything produced after reading content
somebody else may have written needs a person -- and the person was handed
one bit. The receipt said `tainted: true` and nothing else, so reviewing
it honestly meant re-reading the site, which nobody does, and the approval
became a click.
Every source is recorded as it is read: a page by name, a listing by its
ref, a tool by its name and the host it reached, and a delegate's sources
folded up into its supervisor's. It appears in the receipt, in the audit
record, in the refusal that says the run cannot publish, and in what
`agent run` prints.
Found while driving it: the first version counted read events rather than
distinct sources, so a run that read three things repeatedly reported "and
6 more" and would have sent a reviewer looking for six pages that do not
exist.
*** A supervisor can hand work to the agents it names. Manifest.Delegates was
validated, refused on anything that is not a supervisor, copied out of the
supervisor archetype, and published on the A2A agent card under the
governance gap the research calls delegation with accountability -- where
another system reads it to decide what this one will do before asking for
anything. Nothing read the field, so a supervisor's whole reason for
existing did not happen and the card said it did.
A delegate runs as the intersection of its own manifest and its
supervisor's, never as its own declaration, on what is left of the
supervisor's budget rather than on what it declared. What the child spent,
what refused it, and whether it read untrusted content all come back to the
parent -- the last of those because a tainted child answering into a clean
parent would defeat the taint rule with one indirection. The log names
both agents, which is the half that makes it accountability rather than
delegation.
*** Search returned nothing for a question. The ranker required every term of
a query to appear -- "Every term must appear. Anything less returns most
of the site" -- which is correct for two words that are both on the page
somebody wants, and scores precision@1 of 1.00 on the judgement set. It
also means one word the page does not happen to use empties the result, so
"how long does delivery take" matched nothing on a site with a delivery
page. An empty result reads to a visitor as "this site does not cover
that" and to a model as a broken tool.
It is BM25 now, which is what the conjunction was standing in for: drop the
requirement and the longest page wins everything by having more of every
word, and saturation and length normalisation are exactly the two parts
that answer that. The original six judgements still score 1.00; five
sentence-shaped queries went from nothing at all to 1.00.
Three other repairs were measured and rejected, and the reasons are
recorded in the tests: a cosine floor from the second index (a stopword
query scored higher than a real question), ranking by how many terms
matched (it puts the About page above the Delivery page, because About
holds "how", "does" and "take"), and requiring only the rare terms (rarity
is not relevance -- one incidental rare word empties the result again).
*** Two working indexes no agent could ask. Asked which page covered returns,
an agent's only move was to list every page and read them one at a time
until the budget ran out or it guessed right — while the full-text and
vector indexes were built at publish, handed to the public API, and
reachable from no agent at all. Neither index is new; asking is.
** The design system you already use
*** `quilzo theme import` reads a W3C design token file — Design Tokens Format
Module 2025.10 — and fills this site's tokens from it. `theme export`
goes the other way. This ships nobody's palette and nobody's name: a
design system's code licence usually permits a derived palette and its
trademark policy does not permit the name, and two of the large ones
separately forbid copying the look. Reading the file the system publishes,
on the machine of somebody entitled to use it, is a customer's own design
system rather than an imitation of one.
The pairing is written down and not guessed, because naming is where
design systems differ most and a matcher that swapped the brand colour for
the body text colour would produce a palette that is coherent, plausible
and inverted. `--list` prints every path in the file; `--skeleton` writes
the left-hand side out. Imported values go through the same contrast gate
a hand-typed one does.
** Pictures and video
*** A picture can be replaced. media.File.Supersedes was declared with a
comment saying exactly why it matters -- "a page still pointing at last
season's packaging shot is not a corrupted store, it is a store where
nobody recorded that a newer one exists" -- and nothing in this program
ever wrote it or read it. A field whose own documentation describes a
problem it does not solve.
`quilzo media replace OLD NEW`, and a control on the media screen. The
succession is recorded on the newer file, because the older one cannot be
rewritten; every draft reference is pointed at the replacement in one
commit, on pages and on records, at whatever depth an author nested it;
and the listing shows both directions, since Supersedes points backwards
and the person looking at the retired picture is the one who needs to
know. A chain is ordinary and a loop is refused.
One implementation for both surfaces. Two that each walked the content
would disagree -- one would read pages and not records, or match the bare
id and not the "/media/..." spelling -- and the site would be half
replaced by whichever door somebody used. That is not hypothetical: it is
what the image-rights gate did before internal/media held a single answer
about what a reference looks like.
*** A photograph is stored the way up it is meant to be seen, and the metadata
this pipeline promises to remove is removed on every path rather than
most.
*** A picture can be chosen instead of typed as a hash, cropped in an
interface that has no canvas, and told which part of it must survive a
crop. An edit derives a new object; the original stays, because nothing
here is overwritten.
*** A video does not publish without captions, and the accessibility report
says so either way. A range request for two bytes of a film reads two
bytes of it.
*** A picture asked of a model cannot arrive undeclared. `quilzo media
verify` checks every C2PA manifest, ours and theirs — the verifier this
program already had and could not reach.
*** `quilzo studio` records the screen into the media library. It is a fourth
process rather than a route in the admin, because a capture surface needs
JavaScript and the admin's policy is that it has none.
** Writing, and the interface
*** An editor can bold a word, make a list and link a phrase; edit from the
page rather than from a form describing it; say "this paragraph is wrong";
and do one thing to several pages at once.
*** `quilzo find` searches pages, screens, settings, types and files from
anywhere.
*** A field can name another page, and publishing refuses one that names
nothing. A new record starts as the shape its type declares. A listing
goes where the page puts it. A section keeps its name when the one above
it is removed.
*** What points at this page, which nothing could answer before. When did
anybody last look at this. Whose job a page is.
*** Four screens scrolled sideways on a phone and the links were sixteen
pixels tall; five more scrolled sideways and then downwards because of it.
The menu, the bar and the scrolling column were rebuilt over several
passes until the page scrolls and the menu is the only thing pinned.
** Speed, each figure measured on a running site
*** A page cost the same whether the site had eleven pages or two hundred.
A listing pages now: 6.83 ms -> 2.66 ms on a 211-page site.
*** The layout was re-parsed for every reader, and two thirds of the memory to
serve a page went on building the same escaper: 714 KB across 1163
allocations -> 234 KB across 737.
*** A page re-read the media library from disk for every row.
*** Seventy percent of every page was bytes nobody had to send. Responses are
compressed: 6,443 -> 1,955 bytes for the home page. A browser holding two
copies of a page sends two validators, and the list is now parsed the way
RFC 9110 says.
*** The sitemap and the feeds transferred in full on every poll, and every
listing-backed route served whatever the records were at boot.
`/catalogue.json` — the largest thing the site serves, on the one route
whose purpose is being polled by a machine — had no validator and no
cache directive at all.
*** Six servers had one timeout each and nothing bounding the rest.
** Honesty
*** The security report told you to run commands this program does not have.
*** Every way to publish runs the same checks, and a test now walks the source
and fails on a surface that does not.
v0.2.2 — 20 September 2026
==========================
A backport release. Everything here was fixed on the development branch after
v0.2.1 and is applied to the v0.2.x line rather than left to wait for the next
feature release.
** The publish path
*** Four ways to publish ran four different sets of checks. Not a subset
each: a different set each. The browser published a draft the command
line refuses, with an unsubstantiated claim on it; a message in a chat
room went straight to live having run the type gate and accessibility and
nothing else. All four run the same table now, and a test walks the
source and fails on a surface that does not.
*** Two gates in the browser exited like gates that had passed. Both returned
an empty slice on every error, and an empty slice reads to the caller as
"nothing is wrong": a page that could not be rendered published clean.
*** A proposal is put through the publish gates before anybody accepts it.
The checks were only asked at publication, which for a model's draft is
the last possible moment and the wrong one.
** Access
*** A binding scoped to part of the site meant nothing on either surface.
Every handler that read a page name asked whether the caller could act on
"/", and an admin denied author on /about still holds admin on "/" -- so
the deny never matched. The browser is where that rule is displayed, and
somebody reads it there and believes it.
*** A chat user published the operator's draft. The draft ref is one ref for
the whole store, and the chat publisher read it, added the message's page
and published the lot.
*** Four ways a credential was less than it said it was.
*** A lockout nobody could see. The throttle's consequences were
unobservable on every surface.
** Data
*** An erasure reported success and erased nothing. The JSON branch of
`quilzo form erase` answered {"matched": n, "removed": true} and returned
-- before the delete loop and before the audit record, both of which came
after it.
*** Eighty valid form submissions, one address, none refused. Three faults on
the anonymous write path, including a demo form that could not be
submitted at all and a spam check that worked perfectly and counted
nothing.
*** A peer pull read a ref, decided from it, and wrote without holding it.
Sixteen concurrent writes against one base all passed the check and all
committed; fifteen edits vanished.
*** The vault rotated nothing. `quilzo vault rotate` added a key, made it
active, printed success, and left every object wrapped under the old one
-- so the key nobody could remove was the key everything still needed.
** Agents
*** A manifest restricted nothing and said otherwise on every screen, and
seven of eight archetypes could not do what they declared.
*** The content API validated content and recorded no binding, so content
arriving over HTTP was the one kind with no record of what it was
validated against.
*** The same refusal read differently each time. The refusal naming which
hosts an agent may reach ranged over a map, so it produced a different
sentence -- and a different digest in a tamper-evident log -- on every run.
** The audit log
*** Two acts went unlogged because of the name of a field, a broken chain was
not mentioned, and a rule written to catch an empty log could never fire.
** Not in this release, and why
*** Tool calls and delegation. An agent's declared tool host was a field the
model filled in, and a tool call did not taint the run. Neither exists in
v0.2.x: Manifest.Tools and Manifest.Delegates were both added afterwards.
*** The "references" gate refuses a page whose field names a page that is not
being published. It is built on the `page` field kind, which v0.2.x does
not have, so there is nothing for it to check -- and a gate that cannot
fire is worse than an absent one, because the table says it ran.
*** search_pages and similar_pages are not in the archetypes or in the
executor's list here. They were wired to the agent surface afterwards,
and listing them would let a manifest validate while declaring a
capability this build cannot carry out -- the exact failure that list was
added to prevent.
v0.2.1 — 8 September 2026
=========================
*** The binary in the container image knows which release it is. The release
workflow passed `--build-arg VERSION=` and the Dockerfile declared no such
ARG, so it was discarded without a word: every published image carried a
binary answering "dev" to `--version` while the label beside it said the
real number.
It matters more here than it would elsewhere. This program's argument for
generating its bill of materials from the binary rather than from the
source tree is that the binary knows what it is -- so `quilzo compliance
sbom` run inside the image produced a document naming version "dev", which
is the failure that reasoning exists to prevent.
Found by running `--version` against the published v0.2.0 image rather than
by reading the workflow. The release now checks that the image agrees with
the tag, because a build argument the other side ignores is exactly the
kind of mismatch nothing was comparing.
v0.2.0 — 8 September 2026
=========================
A security release. Everything here was found by testing the program rather
than by reading it, and each item was reproduced before it was fixed.
** Fixed, and each was reachable
*** A value could break out of the attribute it was rendered into. The
engine decided which HTML context a value landed in by scanning back 256
bytes for a `<`; pad the output past that with one ordinary field and an
attribute looked like text, where quotes are not escaped. The value then
closed the attribute and opened an event handler. Both fields are
ordinary content, so no template author had to make a mistake, and the
shipped demo theme has that exact shape. `quilzo scan` did not catch it.
Context is tracked as the output is produced now, one pass, no window.
Two more ways out closed with it: a quoted `>` in an earlier attribute
made the engine think the tag had ended, so an `href` was never
scheme-checked and `javascript:` survived; and an unquoted attribute value
could start a second attribute, because a space ends one and escaping
quotes does not help. A value inside <script> or <style> is refused
rather than escaped, because entities are not decoded there and the
escaping is inert.
All 49 shipped templates render byte-for-byte as before.
*** Static copies carried no Content-Security-Policy at all. Every `ipfs
write`, every archived deposit, every site pinned to IPFS. The policy was
computed for each response and then dropped, because a file cannot send a
header. It is written into the document now. This is what the template
fix above was relying on: on the served site `script-src 'none'` stopped
the attribute breakout, and on the copy somebody archives nothing did.
*** A captured ActivityPub inbox request could be replayed forever. The
draft-cavage signature format carries `created=` as a parameter that only
enters the signing base when the signature says it covers it -- and it was
read either way. Append `,created=<now>` to a captured request and the
base is unchanged, the signature still verifies, and the age check reads
the attacker's number. A three-month-old Follow, Undo or Delete replays
as the actor whose request was captured. Only a timestamp the signature
covers is read now, and `expires` is enforced, which it never was.
*** --own-only was never enforced. It was offered on the command line and as
a checkbox, both confirmed the restriction had taken effect, and the
function that resolves such a binding had no caller: anybody granted
`author --own-only` could edit every page in the store. The flag is
withdrawn and refuses rather than being ignored. Nothing loses access,
because nothing was restricted. Use `--on /path`, which is enforced.
*** Twenty of twenty-three agent operations checked no authority. An author
could read any page in the unpublished draft, dump the whole site, and
pull the posture, inventory, integrity and agent-activity reports that the
admin restricts to an administrator -- on the stated grounds that a list
of where the defences are thin is a target list. Every operation declares
a role now and is checked centrally, and a server with no authorisation
hook refuses rather than serves.
*** A peer could write a file anywhere on disk. Objects arriving from a peer
were checked to hash to the id offered and parsed no further, so a tree
whose entry names were `../../../../tmp/pwned` was stored, read back
without re-validation, and joined onto the output directory by `quilzo
export`. The store validates a tree it is handed, and the exporter checks
the join regardless.
*** AI Act Article 50 was enforced on one surface of three. README said
publishing unmarked content is refused, not warned about. The command
line never checked, so `quilzo publish && deploy` shipped unmarked content
and exited zero; the agent interface skipped its check whenever the check
errored rather than when it passed. Both refuse now. A person can
override with `--force-unmarked --reason`, an agent cannot, and the
override is recorded with the gates it waived.
*** Publishing from the browser left no audit record. Not the publish, not
the override, not the reason -- while the screen labelled the reason box
"Recorded." The command line had recorded all three all along, so the log
held every publish made from a script and none made from the browser.
*** An author could rewrite content types through the browser and was refused
from a script. Binding or unbinding a type turns off the gate that checks
content, so an author who can do it has escaped the rules that constrain
authors. The browser needs publish for it now, as the command line always
has.
*** The session cookie was not marked Secure behind a TLS-terminating proxy,
which is the ordinary deployment. Its value is the API token. Set
`admin.behind_tls_proxy` when something in front of this speaks HTTPS.
** Usability
*** --token works on every command. It was read globally and removed nowhere,
so seven of eighty-one commands accepted it and the rest exited 1 with
"flag provided but not defined". Among the rest was `add`, the commonest
write -- and every refusal in the program ends by advising `--token`.
** Honesty
*** Retention is enforced when something enforces it. The form package said
submissions are removed "without anybody asking" and nothing removes
anything on its own. `quilzo form expire` does it; a cron entry or systemd
timer is the intended arrangement, and without one submissions past their
declared ceiling are kept. A declared retention period and an enforced one
are different things.
v0.1.0 — 7 September 2026
=========================
The first tagged release. Everything below is in it; there was no release
before this one, so nothing here is a change from something you were running.
Why 0.1.0 and not 1.0.0. A 1.0 in semantic versioning is a promise about
compatibility, and the honest position is that this project has one
maintainer and no users yet whose upgrade this would be. GOVERNANCE.md names
the single-maintainer risk at the top of the file rather than at the bottom,
and a 1.0 would be the one immodest sentence in a repository that has spent
some effort not writing those.
The asymmetry decided it. A 1.0 can follow whenever the compatibility
promise is one somebody is willing to keep. Releasing 1.0 first and walking
it back is not a thing that can be done.
What a release gets you that a clone does not: static binaries for six
platforms, a multi-arch container image on ghcr.io, a CycloneDX bill of
materials generated by the binary rather than from the source tree, a
cryptographic inventory including post-quantum status, SHA-256 sums, and build
provenance attested to the workflow run that produced it. The build refuses
to ship from a modified tree, and refuses to ship if the third-party
dependency count is anything but zero.
** Design and templates
*** A page chooses its layout. templates/ is a directory of layouts and a
page names one in its own body. A page naming a layout the site does not
have is refused at publish rather than rendered through the default,
because a page nobody designed with no message anywhere is worse than a
failure.
*** A page's shape is content. Nineteen kinds of section — hero, features,
metrics, bar and ring charts, split, gallery, carousel, video, steps,
timeline, quote, logos, pricing, questions, table, people, prose, notice,
call to action — ordered by the content rather than the markup, so
reordering a homepage is an edit that rolls back like any other.
*** Themes are a checked token set. Colours in both schemes, three type
stacks, the type scale, the measure, corner radius, spacing density,
breakpoints and a two-stop gradient. Colour contrast is computed over
the generated stylesheet and a theme that puts body text below 4.5:1 is
refused at publish. This closes a gap the program used to disclose: it
listed contrast under what it could not check, and it generates the
stylesheet now.
*** Typefaces are served from the site's own origin. Put a .woff2 in
templates/fonts/ and it is validated and served at /fonts/. There is
deliberately no way to name a font on another host.
*** quilzo template adopt converts a template written for another system —
Liquid, Twig, Jinja, Django, Handlebars, Mustache, Go templates, Hugo.
Script, event handlers, executable URL schemes and external stylesheets
are removed; anything that could not be translated is named with the
shape it should become, and the layout is not written while any remain.
** The assistant
*** A proposed template that can execute is refused. The assistant already
refused {% raw %}, on the grounds that turning escaping off is a human
decision. It now refuses a proposal carrying a <script> element, an
inline event handler or a javascript:, vbscript: or data: URL, for the
same reason and using the same patterns quilzo template adopt strips from
a foreign template.
This closes a gap between the two ways markup written elsewhere gets into
the template store. adopt treated it as untrusted and stripped it; a
model's proposal was checked for {% raw %} and nothing else, so a
prompt-injected model could put script into a layout that renders on
every page using it. Content-Security-Policy stopped it running, which
is defence in depth working rather than the control being present.
Adding script to a template by hand still works. That is the decision
this refuses to make on your behalf, not one it takes away.
** Structured data
*** WebPage, Article and BreadcrumbList for ordinary pages, built in Go
because JSON inside a script element needs JSON escaping and the template
renderer escapes for HTML. Product data on detail routes already
existed.
** Access
*** --own-only is withdrawn, and refuses rather than being ignored. It was
offered on the command line and as a checkbox in the admin, both said the
restriction had taken effect, and nothing enforced it: the function that
resolves an own-only binding had no caller, and every check dropped the
field. Anybody granted `author --own-only` could edit every page in the
store. Nothing loses access now, because nothing was restricted. Use
`--on /path`, which is enforced. A binding written before this is shown
as never enforced rather than silently as a restriction.