A freshly registered user's auto-provisioned default downstream key must
start with a zero spend allowance, not unlimited. Two parts:
- DEFAULT_NEW_USER_MAX_COST_USD changes from 1.0 to 0.0, so new users have no
free spend until they top up or an admin raises the cap.
- The constant's docs now spell out the failure mode it guards: a NULL
max_cost is treated as *unlimited* by the proxy, so the column must always
be bound on INSERT. The registration path (routes::auth::register) and the
admin create-user path (routes::admin::users) both already include the
max_cost column and bind this constant, so both now persist 0.0 rather than
leaving NULL.
Tests (cargo test --lib routes::auth::tests, 10 passed): assert the constant
is 0.0, and run the registration key INSERT against an in-memory DB to verify
the stored max_cost is non-NULL and exactly 0.0. The agent suites
(approval/sandbox/bash_sandbox/toctou) stay green.
The test AppConfig initializer in routes::auth listed pixelle_video twice
(a merge artifact from stacking the Gemini and comfy-cloud branches), which
made the lib test binary fail to compile with E0062 and blocked `cargo test
--lib` for the whole crate. Remove the duplicate; lib tests build again.
The SDK's built-in Bash tool ran commands as a bare subprocess with only
current_dir set — no OS boundary. On a server that let an agent shell read
/etc/shadow, cd /, write anywhere the process uid could, and reach the
network; the phase-1 path validation only guards the file tools, not bash.
(The SDK's SandboxSettings is dead config: Agent::new never reads it.)
Replace it with SandboxedBashTool — same name and input schema, registered
via custom_tools so it overrides the built-in by name — that confines the
command with bubblewrap, gated by permission mode:
- Default / AcceptEdits (review modes): run inside a bwrap namespace with the
system mounted read-only, the session sandbox dir bind-mounted rw at its
real path and made the cwd, /etc //root //home and all other host paths
absent, and --unshare-all (network included). Filesystem escape and network
egress are blocked by the kernel, not by convention.
- FullAccess (explicitly dangerous): unconfined, same as before, but every
command is audit-logged with a warning.
Fail-closed: in a review mode, if bwrap is unavailable the command is refused
rather than silently run unconfined — no fabricated boundary.
Verified end-to-end in tests/agent_bash_sandbox.rs by invoking the tool:
under Default mode `cat /etc/passwd` sees no host passwd, `ls /` exposes no
etc//root//home, a write to /etc/cron.d/x fails (and the host file is never
created), and the network is unreachable; in-sandbox work succeeds and lands
in the real session dir; FullAccess can see the host. bwrap 0.11.2 confirmed
functional on this host (root, CAP_SYS_ADMIN, user namespaces enabled).
Phase-1 suites stay green.
The previous approval registry used a oneshot channel, so two independent
signals decided an approval: the HTTP handler's result (from Sender::send's
bool) and the agent loop's allow/deny (from which timeout arm fired). At the
300s boundary these could disagree — a late /approve could see send() return
Ok (receiver not yet dropped) and report ok:true to the user, while the
timeout's final poll simultaneously fired and denied the tool. The frontend
was told "approved" while nothing ran. The mirror case (ok:false but allowed)
was impossible, so it never executed an unapproved tool, but the user-facing
signal could lie.
Replace the channel with a single source of truth: each entry is an
AtomicU8 state machine (Pending -> Approved/Denied/TimedOut) plus a Notify.
The decision is made by exactly one compare_exchange from Pending — whoever
wins (approver or timeout) settles it, everyone else observes a losing CAS
and no-ops. resolve() now returns whether *its* CAS won, so /approve tells
the frontend the truth; the loop decides purely from the settled atomic.
They read the same atomic and cannot disagree.
A PendingGuard removes the registry entry on drop (normal completion or
aborted/disconnected session), so there is no leak on any path. The callback
also emits an approval_resolved SSE event with the final decision so a late
click — already told ok:false — can dismiss its modal with the real outcome.
Verified with an integration test that races resolve() against a near-zero
timeout 5000x and asserts ok==true iff the executed decision is Approved
(never a mixed state). Run via tests/ because the in-crate lib test binary
is currently blocked by unrelated AppConfig test fixtures in other modules.
Adds the Agent Workbench HTTP surface wiring the open-agent-sdk loop into
BoosAPI:
- POST /api/agent/chat (SSE): runs an agentic turn and streams session /
text / thinking / tool_use / tool_result / approval_request / result /
error / done events. The agent's LLM calls are routed back through
BoosAPI's own OpenAI-compatible /v1 using the built-in downstream key
(constructed via the SDK's OpenAIProvider), so any aggregated model works
and usage flows through the normal accounting pipeline.
- POST /api/agent/approve: resolves a pending tool approval by request id.
- GET /api/agent/status: engine info + a session's accounting.
Safety is enforced in the SDK permission callback: every tool first passes
sandbox path validation (no escaping the per-session working dir), then the
risk/mode matrix — writes and command execution require an interactive
approval round-trip (default mode) that blocks the loop until the user
answers or the request times out (300s -> deny). Each session runs in its
own data/agent_sessions/{id}/ sandbox and turn usage is persisted.
Registered in the API router and added open-agent-sdk as a path dependency.
Implement the four Gemini native inbound endpoints so Gemini CLI / Google
GenAI SDKs can point their base URL at the gateway:
- GET /v1beta/models — directory listing in Gemini shape, built from
configured routes (like /v1/models); no upstream call, no billing.
- POST /v1beta/models/{model}:generateContent — non-streaming, routed
through ProxyConductor with failover + usage_logs + billing.
- POST /v1beta/models/{model}:streamGenerateContent — SSE passthrough via
open_stream_with_failover + sse_passthrough_response.
- POST /v1beta/models/{model}:countTokens — metadata call, routed for a
channel but not billed.
Auth accepts the gateway key from ?key= or x-goog-api-key (Gemini
convention) and reuses the downstream-key authorization + limits.
Billing closes the loop: gemini_extract_usage reads usageMetadata
(promptTokenCount / candidatesTokenCount) for non-streaming, and
gemini_stream_usage_from_line feeds the shared StreamUsageAccumulator so
streamed usageMetadata is billed too.
Unit tests cover model:action parsing, usage extraction, the stream usage
parser, and query forwarding (key/alt dropped).
Pre-existing breakage on this branch (introduced by the billing commits),
unrelated to the Gemini work but blocking both `cargo build` and
`cargo test`:
- admin/users.rs imported routes::auth::DEFAULT_NEW_USER_MAX_COST_USD,
which was never defined. Add it to auth.rs as $1.0 (the billing design's
"new users default to $1" starter allowance).
- AppConfig gained `payment` and `pixelle_video` fields but several test
helpers still used struct literals missing them. Initialize both with
Default::default() at each affected test site.
Add GET /api/cli-profiles/templates returning copy-ready config snippets
for pointing common AI clients at this gateway: Cursor, VS Code Continue,
Claude Code, and a generic OpenAI-compatible blob.
Ported from AQBot's list_gateway_templates (a desktop app that hardcodes
localhost:8080). Adapted for server-side use: the gateway origin is resolved
per request — ?base_url= override, then X-Forwarded-Proto + Host (correct
behind a TLS-terminating proxy), then config server.host:port fallback
(0.0.0.0/:: rewritten to 127.0.0.1, matching the comfyui-agent local-base
convention). OpenAI-compatible clients get the /v1 base; Claude Code gets
the bare origin since it appends /v1/messages itself.
Scope guard: this only returns config strings + copy hints. It deliberately
does not rewrite local CLI config files (AQBot's desktop cli_config behavior),
which has no meaning on a server.
New module proxy::cli_profiles::templates holds the snippet content; the admin
handler resolves the origin. Covered by 9 unit tests.