feat(prompts): ask create-time questions from the tray and the hub UI - #377
feat(prompts): ask create-time questions from the tray and the hub UI#377madarco wants to merge 13 commits into
Conversation
`rsync -a` implies `-D`, so codex's live `~/.codex/ipc/ipc.sock` made every agent-config stage try to recreate the socket. On macOS that bind() fails with EINVAL once the stage path passes sockaddr_un's 104-byte limit, so the rsync exited 23 and took the whole hetzner bake with it. Claude-Session: https://claude.ai/code/session_015SayJ3DT4xs8exBKeaG6vQ
The VNC path resolves its own target — the host's forwarded port is nothing inside the box — so it never picked up the sign-in fragment the host's browser gets, and openclaw's Control UI opened on its token prompt instead of the dashboard. `withServiceSignIn` applies the agent's declared url fields to a target, and every surface that opens the screen (CLI, dashboard, hub) now passes the provider that reads them. Claude-Session: https://claude.ai/code/session_015SayJ3DT4xs8exBKeaG6vQ
…rovider A service agent can now declare `modelAuth`: which other agents' host-held logins it may borrow, and the service task that ingests them. The host lands the lender's file at the lender's own credential path, 0600, over the carry step every provider already runs before the first supervisor task; the row's own task turns it into the agent's store. AgentBox never learns the format. OpenClaw borrows codex: `agentbox openclaw --model-auth codex`, the `openclaw.modelAuth` config key, or a TTY prompt defaulting to no. In the box `openclaw-model-auth` installs the official @openclaw/codex plugin and runs `openclaw migrate apply codex --item auth:openai` once, then leaves the profile to OpenClaw, which refreshes it independently. Measured on openclaw 2026.9.3: the auth store is SQLite and the retired JSON files are never read; a bare ~/.codex/auth.json shows a bootstrapped profile that a turn cannot use; OpenAI does not invalidate a prior refresh token on rotation, so two boxes seeded from one host file and the host itself all keep working. Also: docker's carry step now runs before the ctl daemon (it raced a task that reads a carried file on first boot), the credential fan-out reaches borrowing boxes and re-runs their ingest, a clone borrows what its source did, and the hub Box payload carries `borrowedCredentials`. Claude-Session: https://claude.ai/code/session_01WAFtRj2zMLmKKCFAyXLXvY
…ument it OpenClaw's own status view cannot say "already imported": a bare seeded ~/.codex/auth.json shows a bootstrapped openai:default profile, source store, status ok, that a turn cannot use — so the first live run skipped the import and the gateway came up with no usable auth. The task now hashes the seeded file and imports when the hash is new, which also stops a later boot from replacing the box's own refreshed chain (the import applies on every run). Docs: the OpenClaw page's "Model provider" section, the CLI reference, the openclaw.modelAuth config row, the borrowed-credentials paragraph in docs/agents.md, and docs/service-agent-model-auth-plan.md with what the PoC measured on openclaw 2026.9.3. Claude-Session: https://claude.ai/code/session_01WAFtRj2zMLmKKCFAyXLXvY
… push Per-gateway approval state, plus doctor's .migrated archives. The host's copy from an older openclaw rode the cloud static push into a 2026.9.3 E2B box and every turn failed with "Legacy exec approvals exist ... run doctor". Claude-Session: https://claude.ai/code/session_01WAFtRj2zMLmKKCFAyXLXvY
The `carry:` and `--model-auth` gates lived in apps/cli and called clack directly, so only the CLI could ask them. A box created from the tray or the hub web UI skipped both silently. Add `PromptRequest` in @agentbox/core — a generic envelope (kind/title/choices/ fallback) plus an optional typed `detail` a client MAY render richly (a carry file table, a credential card) and otherwise renders as `summary`. Ids are content-addressed over the question, so a preflight answer cannot be replayed onto a question that changed. Both gates move to @agentbox/sandbox-core and take a `PromptAsker` instead of a prompt library, so the hub can run the identical decision. The CLI keeps its behaviour through a clack asker: `-y` still does not auto-approve carry, and a non-TTY still refuses a `required` prompt with the gate's own hint. `CarryItem` moves to @agentbox/core (ctl re-exports it) because ctl -> relay -> sandbox-core would cycle; parsing stays with the caller via `loadCarrySpec`. The relay's `PromptAskEvent` gains the same `PromptKind` union and an optional `richDetail`, so host-action approvals can share one renderer later. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
A box created from the tray or the web UI now runs the same host-boundary gates a CLI create does, instead of silently skipping them. `POST /api/v1/projects/:id/create-preflight` returns the questions a create would ask; the client renders them and sends the answers back as `opts.promptAnswers`. Both halves call one `runCreateGates`, the preflight with a collecting asker and the create with a map-backed one, so the questions shown are by construction the questions asked. The gates run before the job is queued — the same front-loading the CLI's `-i` path does — so the queue worker is unchanged. A client that skips the preflight gets each prompt's fallback; a `required` one (carry, which moves host secrets) fails the create loudly rather than silently skipping. A stale answer id means the question changed since the preflight, and is refused rather than applied. A control box has no local checkout, so it can read neither the caller's `agentbox.yaml` nor their host logins: it reports both gates in `unavailable` rather than returning an empty list that looks like "nothing to ask". Also parses `opts.borrowCredentials`, which hub-backend has always read but validation dropped — the key was dead on the wire. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
Create now runs a preflight first: if the hub has questions, the form is replaced by them one at a time, and answering the last one submits the create with the collected answers. `PromptView` has two halves on purpose. The generic one (title, body, choice buttons) renders any prompt, including a topic this build has never heard of, so the hub can add a gate without shipping new UI. The typed one draws the variants worth drawing: a `carry:` file table (host -> box, size, flags, with a symlink-out-of-$HOME row highlighted) and a credential card. An unrecognised detail falls back to its `summary`. Gates the hub could not run are shown above the form rather than dropped, so a control box that cannot read this machine's files says so before the user commits to a box. Verified against a live hub through the API: preflight returns the real 15-entry carry table for this repo, a create without answers is refused, and one with the answer copies 13/15 entries into the box at 0600. The web page itself could not be exercised — every hub page 500s in the standalone build on a pre-existing untraced-execa error that also reproduces on the pre-branch staged bundle. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
Documents the preflight contract in the API reference (both halves of `PromptRequest`, why an unknown `topic` still renders, what `required` and a stale id mean), and adds the tray/web story to the `carry:` and OpenClaw model-provider pages — including the one case that genuinely cannot ask, a control box with no checkout of your project. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
The preflight learns its questions by running the real gates with a collecting asker, which answers each prompt with its own fallback — and carry's fallback is `cancel`. So the carry gate aborted the run and every later gate's question was missing from the list: an OpenClaw box created from the tray or the web UI came up with no model provider, silently, which is the exact bug class this work exists to remove. Collection is now an explicit dry run that decides nothing. Caught by asking a live hub for an `openclaw` preflight and getting one prompt back instead of two. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
The model-auth prompt used `AgentBorrowSpec.label` — a description, not a button label — as its choice text. At 44 characters the two buttons overflowed the tray card, running off the left edge and drawing the hint underneath them. It is now a plain yes/no; the card below already names which login it is. Buttons also wrap to a new row instead of overflowing: a choice label is free text from the hub, so no width can be assumed to fit. The hint moved to its own row, having been sized from whatever width was left over. Rest is copy. The prompts are read by whoever is deciding, not by whoever wrote the `carry:` block, so: no `carry:` block or daemon jargon in the questions, "from"/"to"/"notes" instead of src/dest/flags, "folder" instead of dir, "shortcut to somewhere else" instead of symlink-outside-home, and no octal file modes in the table. A hub that cannot reach your files now says that rather than mentioning local checkouts. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.
| ask: answerMapAsker(o.promptAnswers as PromptAnswer[] | undefined), | ||
| }); | ||
| } catch (err) { | ||
| return { ok: false, error: err instanceof Error ? err.message : String(err) }; |
There was a problem hiding this comment.
Clone fails when project has carry
High Severity
Hub create now always runs the host-boundary gates, and carry: is required. Clone still calls create with no promptAnswers, so any project with a non-empty carry: block is refused instead of spawning the new box.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.
| carry: gated.carry as QueueJobCreateOpts['carry'], | ||
| borrowCredentials: gated.borrowCredentials.length | ||
| ? gated.borrowCredentials | ||
| : o.borrowCredentials, |
There was a problem hiding this comment.
Empty gate falls back to client opts
Medium Severity
When the model-auth gate resolves to none, borrowCredentials still takes opts.borrowCredentials if that array is empty. An explicit none answer therefore loses to a client-supplied list, and a create for an agent with no modelAuth can still seed borrowed logins.
Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.
| } | ||
| setJobId(res.jobId); | ||
| router.refresh(); // surface the box as `creating` | ||
| await submitCreate([]); |
There was a problem hiding this comment.
Carry prompts appear after base bake
Medium Severity
Create still bakes first, then startCreate fetches preflight. Required carry: / model-auth questions therefore appear only after a bake that can take minutes, and bakeJobId stays set so the dialog still reads as a base-image build while asking them.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.
| `${pad(r.src, srcW)} -> ${pad(r.dest, destW)} ${pad(size, 9)} ${flags.join(', ')}`, | ||
| ); | ||
| } | ||
| return out.join('\n'); |
There was a problem hiding this comment.
CLI carry table hides mode and owner
Low Severity
The shared carry summary, which the CLI prints as the gate, no longer includes mode or user. Those fields sit on the row object but renderCarryTable only joins flags, so a TTY approval no longer shows permission or owner overrides.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.
The card put the whole question next to the icon, top-aligned, so a one-line
title sat high and read as misaligned. `PromptRequest` gains an optional short
`heading` ("Copy credentials", "Copy files") with the question as the line under
it, and the header block is measured and centred against the icon. A prompt
without a `heading` still renders — the question becomes the header — so an
older hub against a newer client degrades rather than breaks.
Drops the "one quick question" line; it now appears only as "N questions to go"
when there is actually a queue behind the current one.
Capitalises the Codex borrow label at its source in the agent registry, which
`--model-auth --help` renders too (hence the golden CLI-surface fixture).
Also drops octal modes and uids from the file table in both clients: not
something the person deciding whether to copy a file needs to read.
Verified end to end: a tray-created OpenClaw box came up authenticated with the
host's Codex login.
Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
Four findings, all real. **Clone was broken (high).** `create` now always runs the gates and `carry:` is `required`, but `clone` calls it with no answers — so every clone of a project declaring a carry block was refused. `opts.carryYes` is the API equivalent of `--carry-yes`, and clone sets it: the grant is inherited, not invented, since the source box was approved for this same project's block. Same rule `resyncCarryFiles` already applies when it re-copies within an existing grant. **An explicit "none" could lose.** `borrowCredentials` fell back to the client's list whenever the gate resolved to empty, so answering No lost to a supplied array. Both it and `carryYes` are now threaded INTO the gate as flag equivalents rather than merged with its answer afterwards, so precedence lives in one place. **Questions came after the bake.** The web modal baked first and asked in the create step, putting a required `carry:` question minutes into a build, under a card still titled "Building base image". The ask moved ahead of the bake decision. Threading the answers through `proceed` rather than reading them back from state is load-bearing: the no-bake path runs in the same tick as the `setAnswers` that produced them. **The CLI table lost mode and owner.** Simplifying the file table dropped them everywhere, but the terminal gate is the one surface where an entry landing 0644 or root-owned is worth seeing. Restored there; the GUI tables stay clean. Claude-Session: https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs


A box created from the tray or the hub web UI silently skipped every
host-boundary decision the CLI asks about. Two were live bugs:
carry:— the gate lived inapps/cliand was called only by the threeCLI create commands. The hub never parsed
carry:;POST /api/v1/boxesaccepted a pre-resolved
opts.carrythat only the CLI ever sent. A tray boxgot no carried files, no error, no log line, and
resyncCarryFilescould notrecover it (it returns early when
box.carry.entriesis empty).--model-auth— same shape.GET /api/v1/agentsoffersopenclaw, so atray-created OpenClaw box came up with no model provider. The queue plumbing
for
borrowCredentialsalready existed end to end, butparseCreateBoxOptsnever parsed the field, so the key
hub-backendreads was dead on the wire.Both gates were TTY-only by construction: carry threw on a non-TTY,
model-auth silently resolved to
none. Neither could reach a GUI.One mechanism, not two fixes
PromptRequestin@agentbox/coreis deliberately two halves. The generic one—
kind/title/body/choices/fallback— renders and answers anyprompt, including a
topica client has never seen, so the hub can add a gatewithout shipping new UI.
PromptDetailis the opt-in half a client may drawrichly (
file-table,credential); every variant carries a plainsummary, soan unrecognised one still renders. Ids are content-addressed over the question,
so an answer collected before
agentbox.yamlchanged no longer matches.Both gates moved to
@agentbox/sandbox-coreand take aPromptAskerinstead ofcalling a prompt library. Three implementations drive them: clack in the CLI, a
collecting asker that is the hub's preflight, and a map-backed one
replaying the client's answers. The preflight cannot drift from what the create
asks, because it runs the same gates.
POST /api/v1/projects/:id/create-preflightreturns the questions; answers rideback as
opts.promptAnswers. Gates run before the job is queued — the samefront-loading the CLI's
-ipath does — so the queue worker is unchanged.CLI behaviour is preserved by its asker:
-ystill does not auto-approve carry,and a non-TTY still refuses a
requiredprompt with the gate's own hint.Verified against a live hub
—for missing optional entries.approvecopied 13/15 entries into a hub-created box at 0600owned by
vscode;skip-this-runleft them out. The answer changes behaviour.falls back to its summary rather than failing the decode.
That testing caught a real bug: the collecting asker answers each prompt with
its fallback, and carry's fallback is
cancel— so the carry gate aborted therun and model-auth never appeared in the list. Fixed with an explicit dry-run
mode, plus a regression test.
Note on scope
This branch also carries three openclaw model-auth commits it is built on
(
model-auth-gate.tsexists only there), plus two unrelated fixes that werealready local. Happy to split if you'd rather review them separately.
The macOS tray half is in
madarco/agentbox-trayonnightly.Known gap
The hub web modal is written, typechecks and lints, but could not be exercised:
every hub page 500s in the standalone build on a pre-existing untraced-
execaerror that reproduces identically on the pre-branch staged bundle. Unrelated to
this change, but it means the web renderer is unverified.
https://claude.ai/code/session_016kDGJ95vXMRpE2KGhBLGrs
Note
High Risk
Changes how host secrets and borrowed OAuth logins are approved and copied at create time across CLI, hub API, and UI; incorrect gate replay or validation could skip carry, mis-seed credentials, or refuse valid creates.
Overview
Introduces a shared create-time prompt model (
PromptRequest/PromptAskerin@agentbox/core) and moves carry and model-auth decisions into@agentbox/sandbox-core, so the CLI, hub backend, and web UI run the same gates instead of TTY-only CLI logic.Hub & API: Adds
POST /projects/{id}/create-preflight(collecting asker) and threadsopts.promptAnswersplus parsedborrowCredentialsthrough box create validation andhub-backendbefore the job is queued. The create-box modal runs preflight first, renders prompts viaPromptView, and surfacesunavailablewhen the hub cannot read local project files or host logins.CLI: Replaces local carry prompting with the shared gate +
clackAsker; adds--model-authon service agents andresolveModelAuthbefore box creation; propagatesborrowCredentialsthrough queued jobs. OpenClaw gainsmodelAuthregistry settings (openclaw.modelAuth), anopenclaw-model-authingest task, and credential fan-out for boxes that borrow Codex logins.Other:
BoxRecord.borrowedCredentialsand clone/create plumbing; signed service URLs for VNC/web open paths; carry resolver import moved to sandbox-core; docs and OpenAPI updated for preflight and model auth.Reviewed by Cursor Bugbot for commit 2c01c2d. Configure here.