Agent-First UI v0.8.0: A Credential Stays Where It Was Handed
An AFUI session URL is its own credential, and this release follows it everywhere it went: process arguments, Referer headers, redirects, debug output, and streams that outlived a revocation. A question can now show images, tables, and diffs as evidence, and every ending of a session goes through one slot.
An AFUI session has no login. Its URL is the credential: whoever holds it can answer the question, drive the page, or end the session. That is what makes a session cheap to hand to a person, and it means that every copy of the URL something makes is a copy of the credential. This release went looking for the copies.
Where the URL went
Into the browser’s arguments. A local window was started as
chromium --app=<session URL>, and a process’s arguments are readable by every
user on the machine, through ps or /proc/<pid>/cmdline. The browser is now
started on a small page in the window’s private temporary directory, readable
only by you, which moves on to the session URL with location.replace. Nothing
credential-shaped is left in the process list.
A browser confined to a snap or Flatpak has a temporary directory of its own and cannot see that page, so it alone is still handed the URL directly — a window that never reaches its page would cost the person the session outright.
Into Referer headers. When every session gets an origin of its own, the
credential is part of the hostname: s-<credential>.<host>. A Provider page
that loaded a script from a CDN would tell the CDN that name. Every proxied
response now carries Referrer-Policy: no-referrer, and a page cannot override
it with a <meta name=referrer> or a referrerpolicy attribute: the markup
guard refuses both.
Into the address bar. A third-party panel announced as
/panel?handoff_secret=… that answered with an ordinary trailing-slash
redirect handed that query straight to the browser, its history, and the frame
URL. A redirect that stays on the session now loses the announced query; the
proxy adds it back on the next request anyway.
Into logs. UiMountCredential, UiSessionEntry, and UiDeliveryFacts
derived Debug, so {:?} on a session listing printed live URLs. They print
*** now. Error::InvalidUpstreamUrl no longer carries the rejected URL at
all, since that URL is usually a credentialed one.
Past its own revocation. Revoking or expiring a credential removed it from the routing table, which stops new requests. An event stream the Provider was already sending kept flowing. A response still streaming under a credential now ends when the credential does, the same rule the remote view’s proxy already applied to what it relays.
Isolation that does not depend on an attribute
On the shared page’s default shape, every session is a path on one origin, and
what keeps them apart is the frame’s sandbox attribute. An attribute stays
behind when someone opens the frame’s address by hand. The proxied response now
carries the same sandbox as a Content Security Policy of its own, so it holds
however the page is reached, and a Provider’s own sandbox can narrow it but
never widen it: a browser enforces every policy it is given. Service-Worker-Allowed
is dropped, and a request path with a . or .. segment is refused rather than
joined onto an upstream’s base path.
Frames may now open popups, which escape the sandbox. Without them,
afui.openExternal inside a framed session opened nothing and reported success.
One ending, wherever it comes from
A session ends exactly once, and every ending — an answer, a close, a lapse —
is offered to the same slot, so whichever arrives first is the outcome. A link’s
attention lapse was the one path that skipped it: an answer given in the same
moment could come back as ask_expired, and a completion handle kept afterwards
could still answer a session that had already reported Expired. It goes
through the slot now, as does a window that fails rather than closes.
Two replay paths also ran things they should not have. A page that reconnected
and resent a call whose finished record had been evicted got that call run a
second time; it is now answered ui_call_expired. And a call cancelled while the
connection was down is no longer replayed before its cancel; only the cancel is
sent.
Evidence on a question
afui ask can show what a judgment is about, not just describe it. Context
blocks now include images, tables, and diffs:
{"context":[
{"type":"image","path":"preview-a.png","title":"Image A"},
{"type":"table","columns":["ID","Status"],"rows":[["A","Ready"]]},
{"type":"diff","text":"@@ -1 +1 @@\n-before\n+after"}
],"questions":[{"id":"assessment","question":"What should change?","options":[]}]}
Images are local PNG, JPEG, or WebP, checked against their file signatures and
read into bounded runtime storage before the page is ready; the page receives a
reference, never a local path. A question with no options asks only for a
written answer. A complete request can come from --request file:request.json,
stdin, an environment variable, or a dot path inside a JSON, TOML, or YAML
document, and a request too large for one runtime frame is delivered as a
session-protected reference instead of lowering the documented limits.
Smaller fixes
afui.autosavesends one save at a time. A second save sent while the first was in flight named the hash from before it, and the server answered a conflict that never happened.- The markup guard reads
javascript:,vbscript:, anddata:only in attributes that hold URLs, sotitle="Metadata: Q3"no longer fails a whole render. It now refuses a refresh<meta>, a form that posts by itself (action,formaction,method), and<base>and frames. afui frontend checkandenablerefuse a frontend containing a symlink, as loading it already did, instead of reporting ready a frontend no render can read.- An announced upstream without a port gets the one its scheme implies. Before, the registry’s liveness probe could not reach it and pruned it the first time anyone listed sessions.
afui session list,open, andclosereportsession_list_failed,session_open_failed,session_close_failed, andsession_not_foundinstead offrontend_error, so an agent can tell a missing session from a broken frontend.afui ask --mode sessionis refused underAFUI_NO_REGISTRYinstead of waiting on a session nobody can open, and asessiondelivery fails when its registry entry cannot be written.--advertise-hostkeeps the bound port for an IPv6 address.- The ask page says Cancelled. after Cancel, and shows the server’s reasons in the page’s own language.
afui frontend --helpexists; the frontend commands had no group of their own.
Breaking changes
Error::InvalidUpstreamUrlhas no fields.afui sessionerror codes changed as listed above.- Proxied responses carry
Referrer-Policy: no-referrerand, on a path, a secondContent-Security-Policyholding the frame’s sandbox. A Provider UI that needs browser storage needs a session origin of its own, as before, and a frame address opened by hand on a path is now sandboxed too. - A stub browser behind
AFUI_BROWSER_BINARYreceives--app=file://…/launch.htmlrather than the session URL, and has to read the URL out of that page.test_support::stub_browserdoes this already.
Also in this release: AFUI moves to agent-first-data 0.36.0.
Getting it
$ brew install agentfirstkit/tap/afui
$ cargo install agent-first-ui