Agent-First UI v0.8.0: A Credential Stays Where It Was Handed

by Agent-First Kit Contributors

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.autosave sends 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:, and data: only in attributes that hold URLs, so title="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 check and enable refuse 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, and close report session_list_failed, session_open_failed, session_close_failed, and session_not_found instead of frontend_error, so an agent can tell a missing session from a broken frontend.
  • afui ask --mode session is refused under AFUI_NO_REGISTRY instead of waiting on a session nobody can open, and a session delivery fails when its registry entry cannot be written.
  • --advertise-host keeps 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 --help exists; the frontend commands had no group of their own.

Breaking changes

  • Error::InvalidUpstreamUrl has no fields.
  • afui session error codes changed as listed above.
  • Proxied responses carry Referrer-Policy: no-referrer and, on a path, a second Content-Security-Policy holding 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_BINARY receives --app=file://…/launch.html rather than the session URL, and has to read the URL out of that page. test_support::stub_browser does 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