The generic provider-missing-auth error told operators to create a new agent:
No API key found for provider "openai". Auth store: <path> (agentDir: <path>).
Configure auth for this agent (openclaw agents add <id>) or copy only portable
static auth profiles from the main agentDir.
`openclaw agents add <id>` creates an agent. It does nothing about a missing
provider credential, so following it leaves you with a second agent that also
has no key.
Reproduced on a real build: a fresh install with only an Anthropic key, then
`openclaw memory index`. There is exactly one configured agent, `main`, and it is
correctly configured -- nothing about `agents add` applies. Doctor, diagnosing the
identical condition on the same install, gets it right, listing how to supply the
key and how to disable memory search. The command that actually failed gave worse
advice than the advisory check.
`models auth paste-api-key --provider <id>` is the suggestion rather than
`models auth login`, because `login` requires an owning provider plugin while this
generic fallback is also reached for pluginless, inline, and custom providers, and
for a plugin-owned provider that returns no specialized message. Pasting a key
resolves every "No API key found" case the fallback can produce. The `--agent`
hint is folded in for non-default agents, and `formatCliCommand` keeps the command
correct under a profile or container.
One message rather than two: distinguishing "no credential anywhere" from "this
agent lacks a profile another agent has" would need cross-agent store reads the
resolver does not do today, and the combined sentence is correct for both.
Production +1/-1.
* feat(control-ui): stream live draft previews in the typing indicator
Multi-identity sessions now show what a teammate is typing, not just that
they are typing: the composer's per-keystroke session.typing sends carry a
bounded tail of the draft (optional preview field, 400 code points max),
the gateway throttle re-emits on changed payloads at 250ms (boolean-only
stays at 1s, trailing edge keeps the latest draft), and the transcript
renders a per-actor bubble with the live text plus a blinking caret.
Actors without preview data keep the three-dot bubble.
Previews are ephemeral presence: never persisted, never part of the
session transcript or model context, excluded from aria-live regions, and
gated by the existing >=2-live-viewers, sharing-role, and incognito
checks. No new config surface.
* chore(protocol): regenerate Swift gateway models for typing preview
* fix(gateway): aggregate typing previews across same-actor connections
A boolean-only session.typing update from a second connection of the same
actor (another tab or device) erased their live draft preview, because
typing liveness aggregated per actor while the broadcast preview came only
from the latest request. Preview aggregation now lives with the connection
aggregation owner: updateTypingConnections tracks per-connection previews
and returns the newest non-empty preview among live connections, so the
broadcast keeps the active draft until its connection stops or expires.
Regression fails pre-fix (event lost its preview field).
* fix(cron): reject blank/invalid --webhook before delivery.mode flip
Presence-only typeof checks treated empty or non-http --webhook as a
delivery edit, forging mode=webhook with no URL and clearing the prior
chat destination on merge. Validate with normalizeHttpWebhookUrl first.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): cover webhook validation boundaries
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
* fix(cron): reject blank --model/--thinking on cron edit
Empty Commander values skipped the clear-* mutex after normalize, so
--model '' --clear-model still cleared the override. Align with fallbacks
and delivery clear presence checks.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): align empty model/thinking edit expectation with blank reject
The legacy cron-cli suite still expected blank --model/--thinking to be omitted; that contradicts the fail-closed blank validation and broke CI.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): mutex blank --model/--thinking with matching clear flags
Standalone blank overrides stay omitted. Flag presence now conflicts with
--clear-model/--clear-thinking instead of silently applying the clear.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): focus blank clear mutex coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
`openclaw skills install <slug>` and `skills verify <slug>` both answered a
ClawHub 404 with:
Skill "nonexistent-skill-xyz" not found. Run `openclaw skills list` to see available skills.
ClawHub is saying the slug is not in the registry. `skills list` lists the skills
already installed locally, so it cannot resolve a registry miss -- the operator
is sent to look at what they already have when they were trying to acquire
something new. `skills search` exists for exactly this and is one line away in
`skills --help`.
The sibling gets this right, which is what makes it a defect rather than a
preference: `plugins install <unknown>` answers with "Run `openclaw plugins
list` to see installed plugins, or `openclaw plugins search <name>` to look for
installable plugins."
The 404 branch now names ClawHub as the source of the miss and suggests
`skills search <slug>`. It also routes through `formatCliCommand` like the rest
of the file's sibling messages, so the suggestion stays correct under `--profile`
or `--container`; the hardcoded string did not.
The local-lookup message in `skills-cli.format.ts` is unchanged: there
`skills list` is the right answer, because that path is looking for a skill the
operator should already have.
Production +4/-1.
Doctor is the designated repair owner and nearly every CLI failure footer in this
product ends with `Try: openclaw doctor`. With a corrupt shared state database it
produced this, and nothing else:
┌ OpenClaw doctor
database disk image is malformed
No path. No indication of which database. No next step. `doctor --fix` printed
the identical two lines and repaired nothing, `OPENCLAW_DEBUG=1` added nothing,
and the standard `Reason:`/`Debug:`/`Try:` envelope never appeared. The operator
was in a closed loop: every command told them to run doctor, and doctor told
them a SQLite string with no object attached to it.
Corrupt the agent database instead and doctor already does the right thing --
names the file, names the reason, warns visibly, completes the full run, exits 0.
Same corruption class, two databases, opposite treatment.
The mechanism: `assertDoctorDatabaseSchemasCompatible` read
`preflightOpenClawDatabaseSchemas` and inspected only `incompatible`, silently
discarding `indeterminate`, which is exactly where an unreadable shared database
is recorded with its path and reason already populated. Doctor then proceeded and
died deeper with the context stripped, at
`src/infra/sqlite-readonly-location.ts:428` by way of state ownership inspection
and the config preflight.
Doctor now consumes that dropped signal and stops with a diagnosis that names the
file, the reason, what it deliberately did not do, and how to recover. It stops
rather than continuing like the agent-database path because shared state owns
write admission and holds the persisted plugin index the health context is built
from; disabling migrations still fails on that index, so continuing would mean a
bespoke degraded doctor. It does not recreate the database: that file holds auth
profiles among other things, so silent rebuild is data loss.
Also fixes an adjacent leak found in the same investigation: `io.load.ts` passed
a raw `Error` to the logger, so `doctor --session-sqlite inspect` printed a stack
trace with absolute `dist/*.js` frames without `OPENCLAW_DEBUG=1`, contradicting
the CLI's own debug-gating convention. It now logs formatted message text.
`doctor --lint` exiting 1 while bare `doctor --json` exits 0 was investigated and
left alone: commit 6e5bf3ec55 established that advisory JSON exit behavior
deliberately, and register.maintenance.test.ts covers it.
Production +11/-1.
Two defects in one command, both on the path a brand-new operator is on
immediately after `openclaw onboard`.
`channels status` never mentioned channels when none were configured. With the
gateway up it printed `Gateway reachable.` and a tip about `status --deep`;
without it, two blank lines where the channel list belongs. The operator asked
for the status of their channels and got gateway reachability. Its siblings
already handle this -- `channels list` prints `- no configured chat channels
(run \`openclaw channels list --all\` to see installable channels)` and
`openclaw status` prints `No channels configured` -- so `channels status` was
the lone holdout. Both renderers now emit that same line, moved to a shared
constant so the three surfaces cannot drift apart again.
The second is worse because it sends the operator somewhere wrong. The fallback
computed `gatewayAuthUnavailable = expectedError || isGatewaySecretRefUnavailableError(err)`,
and `isExpectedCliError` returns true for `isGatewayTransportError` -- a plain
ECONNREFUSED. So a gateway that simply was not running reported `Gateway auth
unavailable; showing config-only status.`, contradicting the `Gateway not
reachable at ws://... (ECONNREFUSED)` line printed three lines above it. Someone
who runs `channels status` before starting the gateway went hunting for a token
problem that did not exist. The flag now consults only the two genuinely
auth-related predicates; `expectedError` keeps its separate job of selecting the
canonical CLI failure output.
`isGatewayCredentialsCliError` becomes exported for that check. The JSON shape is
unchanged; only the truth of `gatewayAuthUnavailable` changes, and no test or
documented contract depended on transport errors setting it.
Production +27/-9.
`skills` (#126954) turned out to be one instance of a class. Two more surfaces
accepted an agent id that names nothing, and one of them wrote it to disk:
- `sandbox explain --agent nope-agent` exited 0 and printed a complete policy
report, including `Elevated: enabled: true` and a workspace root that does
not exist, for an agent `openclaw agents list` does not know.
- `approvals allowlist add "<pattern>" --agent nope-agent` exited 0, printed
`Writing local approvals.`, and persisted the entry under a key nothing will
ever read. The operator believes they approved an exec pattern; nothing was
approved. This is the severe one: a false record of an approval.
Sweeping `option("--agent"` across the CLI found the rest. Each hit was
classified as a selector (names the thing operated on, must validate) or a
filter (narrows a list, may legitimately return empty). Selectors now route the
explicit value through `resolveConfiguredAgentId`, the helper that already backs
`models`, `memory`, `sessions list`, `hooks`, and every capability surface:
`channels resolve`, `sessions export-trajectory`, `sessions archive/delete`,
`backup enable`, `backup git create`, `backup sqlite create`. Blank-only guards
close the empty-shell-variable hole in `hooks`, `sessions` list/cleanup/tail/
compact, `migrate`, and agent turns.
Filters are deliberately unchanged: `audit`, `usage-cost`, agent bindings,
`backup verify/restore`, and `sandbox recreate` all match existing records and
correctly report no matches. Cron is gateway-owned and already rejects an
unavailable agent server-side; it is untouched.
`sessions archive/delete` was not silent -- it failed with `Session not found.
Run openclaw sessions list --agent ghost --json to choose a valid key.` But that
suggested command itself exits 1 with `Unknown agent id "ghost"`, so the
remediation handed to the operator could not run. Validating locally, exactly as
`sessions list` already does, keeps the hint runnable without adding a roster
round-trip to the gateway.
Production +140/-38.
`openclaw skills check --agent nope-agent` exited 0 and printed a full report
headed "Agent: nope-agent" with 53 skills / 44 eligible, while the install's only
real agent reported 57 / 48. It did not fall back to the default -- it fabricated
an agent and produced confident, different numbers for it. `skills list` behaved
the same way.
Every sibling --agent surface already rejects an unknown id: `models auth list`,
`models list`, `models status`, `memory status`, and `sessions list` all exit 1
with "Unknown agent id". Skills was the only holdout, and the canonical helper
for it already exists -- `resolveConfiguredAgentId`, added for this exact class
when `memory --agent` had the same hole.
`resolveSkillsWorkspace` took the explicit --agent value verbatim while both the
workspace-inferred and default paths were validated. Route the explicit value
through `resolveConfiguredAgentId` so the message and behavior match the
siblings, including the profile-aware hint, and reject a blank --agent the way
memory does. Workspace inference and default resolution are unchanged.
Production +9 LOC.
With an external CLI credential discoverable, `models status` printed
"Auth store: <state>/agents/main/agent/openclaw-agent.sqlite" while
`models auth list` printed "<state>/state/openclaw.sqlite" -- two commands, one
install, different answers, and the agent database held no auth rows at all.
`resolveAuthStorePathForDisplay` chose between the agent-local file and the
shared owner with `hasLocalAuthProfileStoreSource`, which returns true for a
runtime snapshot. External-CLI discovery populates an agent-scoped runtime
snapshot, so `models status` -- which performs that discovery -- concluded the
agent owned a local store file. Those credentials live in the external tool's own
files, never in the agent database. Pointing HOME at an empty dir removes the
discovery and both commands already agreed, which isolates the trigger.
The displayed value is a file path, and only persisted state lives in a file, so
the decision now uses the persisted store probe. A genuinely local persisted
store still wins, including without an ownership record.
Clear completed restart-recovery ownership before admission and during Gateway startup while preserving live recovery fences.
Co-authored-by: EJ Campbell <ej.campbell@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(sessions): keep resolved skills out of durable state
Repair runtime-only skill persistence across SQLite, legacy stores, bounded Doctor cleanup, and lightweight health reads.
Refs #126663
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* test(health): assert lightweight session list projection
---------
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
Keep persisted provider, nested model, and producing runtime facts in CLI session inventories instead of reparsing the route or selecting next-turn harness policy.
`openclaw models auth list` printed `Auth state store:
<state>/agents/main/agent/openclaw-agent.sqlite` on every install created since
a8a9f284fb, and that file does not exist. Credentials now persist in the shared
state database, so an operator debugging auth was sent to the wrong file while
the listed profiles resolved correctly from somewhere else.
`resolveAuthStorePathForDisplay` and `resolveAuthStatePathForDisplay` named the
agent-local file whenever an agent dir was supplied. That matched storage before
shared-auth ownership moved and stopped matching afterwards. The same helpers
feed `models auth order`, `models list --status`, the auth overview, two
auto-reply directive surfaces, and the `path` field of doctor's auth
HealthFindings, so structured diagnostics pointed at the wrong file too.
Display now mirrors the loader's own selection: an agent with a local auth store
shows its own database, otherwise the shared owner. Both helpers move to the
`paths.ts` barrel so they can consult `hasLocalAuthProfileStoreSource` without a
cycle back through `path-resolve`. Nothing about storage or loading changes.
`model-auth-provider` no longer derives the agent dir from the store path -- that
would have reported the state directory once the shared owner is selected -- and
uses the caller's agent dir instead.
Production -4 LOC.
* fix(onboard): honor secret-input-mode ref for the generated gateway token
`openclaw onboard --secret-input-mode ref` was silently ignored for
`gateway.auth.token`: onboarding generated the token and wrote it into
`openclaw.json` as a plaintext string, so `openclaw doctor` warned about
`gateway.auth.token` on the install it had just created. The flag was
honored for provider credentials, so an operator who explicitly opted into
references still ended up with a plaintext secret and a remediation
(`openclaw secrets configure`) that cannot migrate a self-generated value,
because it validates a ref by resolving one that already exists.
Setup mints this token itself, so reference mode now provisions it:
- an ambient OPENCLAW_GATEWAY_TOKEN keeps an `env` ref to that variable, so a
later rotation stays authoritative instead of being pinned by a stale copy
- anything else (freshly generated, or an existing plaintext token being
migrated) goes into the shared SQLite secret store as a write-only `secret`
entry, with config holding only `{source:"store",...}`
An existing store entry wins over a freshly generated one, so reruns never
rotate a token already paired with clients. The store write precedes the
config write: a ref persisted without its value would leave the gateway
unauthenticatable, while an orphaned entry is reused by the next run.
The interactive wizard had the same dead end and is fixed the same way.
Default (plaintext) onboarding is unchanged.
User impact: `--secret-input-mode ref` now keeps the gateway token out of
openclaw.json, and a fresh install no longer self-reports a plaintext-secret
warning.
* test(onboard): split gateway onboarding suite under the max-lines gate
The added gateway auth-token tests pushed
onboard-non-interactive.gateway.test.ts to 1014 lines, over the max-lines
limit (check-lint-core-3). Repo policy is to split, never suppress.
Extract the shared vi.mock/harness preamble into
onboard-non-interactive.gateway.test-mocks.ts, following the existing
agent-command.test-mocks.ts pattern, and move the four gateway auth-token
storage tests into their own suite. The reachability mock becomes a holder
object so both suites can swap it across the module boundary, and hoisted
mocks are re-exported in a separate export clause because Vitest rejects
exporting a vi.hoisted binding at its declaration.
Test set is unchanged: the it-declaration multiset matches the pre-split
file exactly, with no duplication across the two suites.
* test(onboard): give the shared gateway onboarding mocks unique export names
check-export-name-collisions flagged `runtime` and `readConfigFileSnapshotMock`
as colliding with program.test-mocks.ts and plugins-cli-test-helpers.ts once the
gateway onboarding preamble became a shared module. Rename the exports to
gatewayOnboardRuntime / gatewayOnboardConfigSnapshotMock per the repo's
unique-export-name rule; suites alias them locally so the assertions read the
same as before.
* test(tooling): route the new gateway auth-token suite from its test helper
test-projects asserts which suites a change to
onboard-non-interactive.test-helpers.ts should run. The new
onboard-non-interactive.gateway-auth-token.test.ts imports that helper, so it
belongs in the expected routing plan.