`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.
Keep cron delivery help and automation docs explicit that --channel selects a channel plugin, not a per-conversation channel identifier.
Fixes#124646
Punchcard-Session: clear-timber-orchard-n1
* refactor(agents): reduce tool failure warnings to two rules
* test(agents): remove obsolete tool recovery receipt proof
* refactor(agents): drop unused meta param from buildToolMutationState
* test(agents): remove stale tool warning assertions
* test: export runtime source snapshot from closed runtime-snapshot mocks
Heal the main breakage introduced by 9441e3fe6e / #126531, which added a runtime source-snapshot read to provider model route resolution. Closed Vitest factories now return null for that source snapshot, preserving their pre-projection behavior.
* test(gateway): make compaction read-error faults order-immune
Generation-2 CI failure in run 32342180898, job 96343444772 showed that the mock factory initialized while shared gateway-server importers remained bound to the real transcript reader.
The dedicated isolated project fixes normal shards. Complete its ownership by adding it to the root project matrix and excluding the test from the non-isolated OPENCLAW_GATEWAY_PROJECT_SHARDS=0 fallback.
* fix(auth): keep a retired auth JSON from stranding a migrated store
Runtime failed closed with AUTH_PROFILE_MIGRATION_REQUIRED whenever a retired
credential file was present, even when the canonical SQLite store already held
the agent's profiles. One leftover auth.json therefore made a fully migrated
install unusable, and the gateway lifecycle preflight refused start/restart on
top of it, so every channel and provider stayed offline until Doctor ran.
A legacy file is now only fatal when the canonical store cannot serve
credentials. Doctor's importer never overwrites a usable stored credential, so
a file sitting beside a populated store is unarchived bytes, not pending
migration: runtime logs a one-time warning and keeps serving. An empty store
with a credential file still fails closed and never falls through to
environment auth. Startup degrades that owner to configured-unavailable
instead of refusing to boot, which lets the lifecycle preflight go away.
* refactor(secrets): retire the auth-profiles.json vocabulary
Auth profiles moved to SQLite, but operator-facing surfaces still named the
retired JSON file. The duplicate-agentDir error told operators to copy
auth-profiles.json to share credentials, which does nothing and lands the
second agent in a migration-required state; `openclaw migrate plan codex`
reported a target file that is never created; and the secrets picker labelled
candidates with a filename that no longer exists.
Renames the SecretTargetConfigFile discriminator to "auth-profile-store" and
corrects the operator-facing text, the migrate plan target, and the docs that
described the file as a live target. Genuine legacy-filename uses in doctor,
the security fixer, and migration fixtures are unchanged.
Also deletes resolveSecretPlanTargetByPath and ResolvedSecretPlanTarget from
the plugin SDK. They have no callers in core, plugins, or tests, and the
symbols are absent from the latest stable tag, so they carry no compatibility
obligation and are removed rather than deprecated. Their inline parameter type
was the only thing putting the retired filename on the public SDK surface.
* improve(wizard): warn about device-code phishing
The device-code prompt only warned against sharing the code, and only when an
expiry was known. Device-code phishing works the other way around: the attacker
starts the login and gets the victim to enter the attacker's code. Codes
delivered over a chat channel are the risky case and carry no expiry hint, so
the warning is now unconditional and covers received codes, matching the Codex
CLI prompt.
Also documents the Codex auth handoff: a subscription profile is installed as
in-memory external auth rather than persisted, and token refresh is inverted
so the refresh token stays in OpenClaw's store.
* fix(test): make transcript read-failure injection order-independent
server.sessions.compaction-read-errors.test.ts injected its failures with
mockRejectedValueOnce, which fails the NEXT call to loadTranscriptEvents
globally. Under --isolate=false a shard shares one worker, so any sibling
transcript read could consume the one-shot rejection before the compaction RPC
issued its own; compaction then ran against the real reader and returned ok,
failing three assertions. This shard was already red on main; a prior repair
fixed the mock's initialization order but left the call-order dependency.
Key the injection on the seeded sessionId instead, so unrelated readers cannot
consume it and the re-read case counts only its own session's reads.
Also updates two expectations invalidated by this branch: the duplicate-agentDir
remediation text, and the plugin SDK export ratchet, shrunk by the two retired
secret-plan exports.
The memory CLI resolved --agent by returning the caller's string verbatim,
so an id that is not configured produced a confident empty result:
`memory status` rendered a panel for it, `memory index` fabricated a
workspace-<id> path, and `memory search` reported No matches. A typo read
as an empty memory rather than a nonexistent agent, while hooks, status
--usage, capability, migrate, and session targets already rejected unknown
ids.
Consolidate that duplicated check into resolveConfiguredAgentId beside the
agent roster owner, reuse it at the matching core sites, and route memory
to it through the existing memory-core host-runtime facade so no new
plugin SDK surface is added.
The canonical hint uses formatCliCommand rather than a literal: under a
profile or container the bare command is wrong, so consolidating on a
literal would have regressed the hooks and migrate hints and left the
status, capability, and session-target hints unrunnable.
* fix(cli): route bare startup on configured inference, not a default agent
Bare `openclaw` resolved a single "default" agent to decide whether inference
was configured. On an explicit multi-agent roster (agents.ownership="explicit"
with 2+ entries) that resolution has no answer, so startup threw
AgentSelectionRequiredError instead of launching the TUI or onboarding.
The branch only needs to know whether inference is configured at all, so it now
asks exactly that across the roster and hands off to the TUI, which already
owns agent selection and resolves by workspace path first. Routing ambiguous
rosters to onboarding instead would have dropped a working multi-agent install
back into the setup wizard.
* fix(cli): honor the configured system agent for model read commands
`openclaw models status`, `models list`, `models auth list`, and
`models auth-order get` resolved a deprecated "default" agent when --agent was
omitted, so they threw AgentSelectionRequiredError on an explicit multi-agent
roster. schema.help.core.ts already documents agents.defaults.systemAgent.agentId
as the owner for these unscoped reads, and the Gateway twins
(models.ts, models-auth-status.ts) already honored it; only the local CLI paths
did not.
resolveModelsTargetAgent now takes a closed read/mutation mode. Reads resolve
explicit --agent, then the configured system agent, then a sole agent.
Credential mutations still require an explicit owner rather than guessing which
agent to write into, and say so with a flag that exists on those commands.
The deprecated resolver is gone from this owner. Help text for the two commands
whose default changed now names the system agent.
* fix(outbound): resolve ambient delivery owner through the system-agent chain
Agent-less outbound channel bootstrap resolved its owner via the deprecated
throwing resolver, so multi-agent explicit-ownership configs with only
agents.defaults.systemAgent.agentId set threw AgentSelectionRequiredError from
inside gateway delivery recovery, aborting the whole drain ("Delivery recovery
failed") for every queued entry.
Bootstrap now follows the same ambient owner chain heartbeats and cron use
(legacy default, then systemAgent, then sole agent), consolidated into one
canonical tryResolveAmbientOwnerAgentId in agent-scope-config; the duplicated
chain in cron/agent-id.ts is deleted and heartbeat resolution reuses the
helper. Truly ownerless fleets no longer throw either: bootstrap proceeds with
global-scope plugin discovery only, never selecting an agent workspace, so
per-entry delivery outcomes stay recorded instead of one entry starving the
recovery loop.
Regression tests fail pre-fix: agent-less bootstrap routes through the
configured system-agent owner, and ownerless fleets bootstrap without
throwing.
* refactor(agents): route ambient owner resolution through one canonical chain
The idiom `tryResolveLegacyCompatibilityAgentId(cfg) ?? resolveDefaultAgentId(cfg)`
was copied across nine ambient surfaces. It reads like a fallback but is not: when
the first call returns undefined, resolveDefaultAgentId reduces to resolveSoleAgentId
and throws, so the second leg only ever throws — and neither leg consults
agents.defaults.systemAgent.agentId. Every one of those surfaces raised
AgentSelectionRequiredError in explicit fleets that own ambient work through a
system agent.
Promote the chain to one owner in agent-scope-config: tryResolveAmbientOwnerAgentId
(undefined when ownerless) and resolveAmbientOwnerAgentId (throws with the caller's
existing selection context). The throwing variant is a drop-in for every former
composite: identical message when a fleet has no owner, but the system-agent leg
now resolves.
Converted: resolveDefaultAgentDir (the shared producer behind auth, model catalog,
doctor, wizard, and Plugin SDK helpers), config identity-avatar validation, prepared
model catalog and registry, session reset, Talk target, main-session routing and its
routing-contract fingerprint, session store targets, hooks CLI, plugin LLM
completion, and first-run onboarding.
resolveDefaultAgentId keeps backing explicit-selection surfaces where 'pass --agent'
is the right answer; its deprecation note now sends ambient callers to the new
helper.
* fix(sessions,hooks): keep agent-narrowing surfaces on explicit selection
CI caught the over-reach: session-store listing and hooks status reporting are
deliberately explicit-selection surfaces, not ambient system work. Both narrow
output to one agent, so adopting agents.defaults.systemAgent.agentId would hide
the other agents' sessions/hooks instead of failing with the escape hatches the
operator needs. sessions.default-agent-store.test.ts pins that contract by
configuring a system agent and still expecting the --agent/--all-agents error.
Restore the prior behavior at both sites, spelled honestly as
tryResolveLegacyCompatibilityAgentId ?? resolveSoleAgentId — provably identical
to the old composite, since the composite's second leg could only ever throw —
with a comment recording why these two resist the ambient chain.
* test: declare the ambient owner binding in agent-scope mock factories
Explicit vi.mock factories enumerate exports, so production calling
resolveAmbientOwnerAgentId broke prepared-model-catalog, prepared-model-registry,
and channel-resolution with 'No "resolveAmbientOwnerAgentId" export is defined on
the ... mock'. Declare it in the four factories; channel-resolution now drives the
non-throwing variant the bootstrap actually calls.
Also restore session-store targets and hooks status to their exact previous code.
Both stay out of the ambient chain by design, so the smallest correct diff at an
out-of-scope site is none: they now differ from main only by the comment recording
why they resist it.
* fix(models): resolve the model-config workspace through the ambient owner
ClawSweeper P1: prepareModelsConfigContext resolved agentDir through
resolveDefaultAgentDir (ambient chain) but its workspace through
resolveDefaultAgentId (deprecated throwing chain), so explicit fleets owned by a
system agent still failed model preparation on the workspace lookup — a one-sided
fix inside a single function.
Both halves now resolve the same ambient owner.
* docs(agents): describe the widened ambient system-agent ownership
config-agents.md claimed 'Other agent-scoped methods do not use this setting as
a general default', which this branch makes false: systemAgent.agentId is now
the ambient owner for agent dir/workspace resolution, outbound bootstrap and
delivery recovery, main-session routing, Talk, and onboarding.
Record the widened set, the two surfaces that deliberately still demand explicit
selection (sessions, hooks), and delivery recovery's per-entry degradation.
* fix(cli): resolve infer command owners via --agent and systemAgent fallback
Multi-agent rosters (agents.ownership=explicit) dead-ended every infer
runner command: resolveDefaultAgentId threw AgentSelectionRequiredError
telling callers to pass --agent, a flag none of the commands registered,
and agents.defaults.systemAgent.agentId was ignored.
Route model run, image generate/edit/describe/describe-many, video
generate, embedding create, and model auth logout through the existing
resolveCapabilityProviderAgentId chain (explicit --agent, then
agents.defaults.systemAgent.agentId, then the sole agent) with
per-command error surfaces, and register --agent on each command.
* docs(config): scope systemAgent infer fallback help to agent-owned commands
* perf(gateway): remove repeated logging and delivery scans
Exact session-delivery retries no longer scan the full queue. Logging and diagnostics reuse lifecycle-owned settings and listener interest so uninterested projections are skipped, while outbound WebSocket summaries are built only after recipient admission.
* fix(infra): break diagnostic listener import cycle
Keep event-type validation at the diagnostic dispatcher while the process-wide listener presence counter remains a leaf module.
* test(cli): use logging override owner
Exercise late one-shot JSON diagnostics through the canonical logger override setter so lifecycle-cached console settings are invalidated as they are in production.
* test(auth): use logging override owner
Configure the locked-update warning test through the canonical logger override setter so lifecycle-cached console settings are invalidated before assertion.
* test(gateway): normalize redacted media fixture
Compare durable inbound media facts against the public redaction contract so random identifiers that resemble sensitive text do not make the Gateway suite flaky.
runConfigUnset scoped its missing-path error to --dry-run, so the real
invocation printed "No change" and exited 0 while --dry-run on the same
input exited 1 with a specific message. Dry-run exists to predict the
real run, and a typo'd unset in a script reported success.
Restore the failure for both modes, keeping the strict-config assertion
introduced alongside the regression and the JSON5 byte preservation it
was added to guarantee.
`openclaw approvals allowlist add|remove` printed "Writing local approvals."
from the shared target-resolution helper, before the mutation decision was
made. Both idempotent paths ("Already allowlisted.", "Pattern not found.")
returned without saving, so the CLI announced a write that never happened.
`approvals set` had the same problem: it announced the write and then rejected
unparseable input.
Move the announcement from `loadWritableSnapshotTarget` into the local branch
of `saveSnapshotTargeted`, the function that owns the write. Every caller of
the shared seam is fixed at once and exit codes are unchanged: idempotent add
and remove still leave the requested end state satisfied and exit 0.
* fix(daemon): preserve unknown service load state
* test(daemon): update service load-state fixtures
* fix(onboard): preserve loaded service diagnostic
* fix(daemon): preserve unknown lifecycle state
* fix(status): preserve loaded JSON fields
* fix(daemon): fail restart on unknown service state
* fix(daemon): fence unknown service state
* fix(agents): finalize guided creation safely
Run channel post-write hooks only after config publication, defer portable auth copying until agent creation succeeds without overwriting newer credentials, and provision existing workspaces before publishing updates.
Keep JSON-only guided creation interactive while routing wizard output to stderr so stdout remains one machine-readable summary.
* fix(terminal): preserve note call signature
* fix(agents): pass committed config to setup hooks
* ci: split heavy codex changed-test shards
Cap non-isolated Codex extension processes at 20 files so 4-vCPU changed-target jobs do not starve real-time watches or hit the no-output watchdog.
* test(ci): align codex shard cap fixture
* docs(cli): clarify agents add JSON mode
* fix(config): secure CLI reads and mutations
Use exact plugin metadata for config get redaction, reject impossible SecretRef provider bindings in canonical validation, preserve include CAS provenance, and skip destructive authored no-op rewrites.
* fix(config): preserve inactive SecretRef startup
Keep normal Gateway validation scoped to active secret owners while strict config validation and write paths reject impossible provider/source bindings, including authored no-ops.
* fix(gateway): preserve config patch no-op
Keep unchanged Gateway config.patch requests on the existing no-op path; strict validation remains owned by actual config writes and the config CLI mutation surfaces.
Use an off-main-thread watchdog for the process-owning Gateway runner so a CPU-bound shutdown cannot outlive the managed restart deadline. Bound agent-harness disposal so listener teardown can continue.
Release note: Managed Gateway restarts now recover when shutdown work stalls the event loop while preserving graceful cleanup first.
Closes#125840
* feat(gateway): reach a Gateway behind an identity-aware proxy from the CLI
Operator CLI surfaces (tui, attach, call, probe, onboarding, and configure) can present configured gateway.remote.edgeAuth headers to an identity-aware proxy. Headers are origin-bound, WSS-only, never follow redirects, cannot set transport-owned headers, and are redacted. Identity-proxy upgrade rejections are classified and remediated instead of being reported as an unreachable gateway.
* test(gateway): cover config-aware probe calls
Keep status probe expectations aligned with the resolved configuration forwarded for origin-bound edge-auth resolution.
* fix(gateway): preserve edge auth across wizard saves and enforce wss before secret resolution
Preserve gateway.remote.edgeAuth only when the configured Gateway scope is unchanged, and reject non-WSS targets before resolving any edge-auth SecretInput.
With no servers configured, mcp probe emitted only its header line and exited
0, while mcp list and mcp doctor both name the condition and the command that
fixes it. Short-circuit the human path with the same guidance and skip building
a runtime for zero servers; JSON output keeps its existing empty envelope.