Centralize untrusted audio transcript framing across channel plugins and keep Discord voice transcripts out of typed command provenance.
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Preserve opaque source strings across tool-result replay while retaining masking for direct .env reads, registered or recognizable secrets, structured sensitive fields, and payment data.
`models status --agent` selected the agent's primary and fallbacks but
resolved aliases from global defaults only, so a bare per-agent alias was
displayed and probed as a different provider than the runtime selects.
Status, --check, auth diagnostics, provider-use reporting and --probe could
all inspect the wrong route and report false missing or auth state.
The canonical resolvers already merge per-agent model rows when given an
agentId, so this passes the agentId that was already resolved locally and
deletes the synthetic global-default config that carried the agent primary.
Displayed aliases now come from the effective agent-scoped index, which
applies per-agent replacement instead of reading agents.defaults.models.
Unscoped status keeps reporting global defaults.
Refs #127585
Preserve existing unrestricted host files when a write or edit fails partway through by updating regular files in place and restoring the original prefix and length.
Writable-but-unreadable regular files now fail before mutation.
Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Prevent ClawHub skill updates from silently replacing locally modified files. Verify recorded install digests at the rollback boundary, restore on mismatch, and require explicit --force for unverifiable legacy installs.
Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
`openclaw onboard --json` wrote a JSON object to stdout on success, and the
top-level CLI handler emitted {"ok":false,"error":...} for thrown errors, but
every option-validation rejection printed to stderr and exited 1 with an empty
stdout. Automation parsing stdout got a valid object for some bad flags and a
parse error for others, with no way to tell a rejected flag from a crash.
Measured before this change, all with --json and all with empty stdout:
--mode, --flow, --daemon-runtime, --gateway-bind, --gateway-auth,
--gateway-token-ref-env, a missing --gateway-password, and --auth-choice.
Routes every onboarding option rejection through one shared primitive that
emits { ok: false, phase: "options", message } when --json is set, then reports
and exits exactly as before. 'options' joins the existing phase vocabulary
('daemon-install', 'gateway-health'). Every human message, stderr stream, and
exit code is unchanged; this only adds the stdout object that was missing.
Onboarding answered an unknown --import-from with "Migration provider \"x\" is
not installed or bundled. Install it before starting the transactional import."
— no list of valid ids, no next command, and advice to go install something
that is usually just a typo.
Its siblings already do this correctly: resolveMigrationProvider appends
"Available providers: ..." and the migrate selection helper appends
"Available skills: ...". The missing --import-from case in onboarding itself
already points at `openclaw migrate list`. Only the unknown-id case dead-ended.
Collapses the two duplicated throws into one guard used by every path, which
also closes a gap: the no-back-navigation branch returned the picker result
without validating it at all.
Non-interactive onboarding accepted `--gateway-bind custom` with no
`gateway.customBindHost`, wrote `bind: "custom"` to the config, and reported
success. The Gateway refuses that config outright:
Gateway failed to start: gateway.bind=custom requires gateway.customBindHost.
`doctor` and `config validate` both call the written config healthy, so the
first sign of trouble is a Gateway that will not start.
Every other onboarding flag already rejects a mode whose companion value is
missing (--gateway-auth password, --auth-choice openai-api-key, --auth-choice
custom-api-key). This restores the same contract for bind, reusing the same
validateDottedDecimalIPv4Input the interactive wizard and `openclaw configure`
apply to this field. Pre-seeding gateway.customBindHost keeps working.
Allow healthy current-version state databases to open without taking SQLite writer locks while preserving schema repair, legacy cron migration, and external ownership fencing.
Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Repair managed OpenClaw host links under the startup migration lease before plugin state migrations, preventing retained Codex upgrades from repeating the same readiness failure.
Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Preserve parser-owned bounded diagnostics in managed Claude live turns and share the canonical output-limit formatter.
Co-authored-by: Marvinthebored <peter@lindsey.jp>
Use canonical session-key normalization during subagent reconciliation so case-distinct Matrix and Signal peers cannot be reported as completed. Preserve structural casing reconciliation and remove the O(N) fallback scan.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Alix-007 <li.long15@xydigit.com>
The permission diagnosis added in #127703 compared Node's reported errno
path against the configured directory as raw strings. Node reports the
canonical path, so a config directory reached through a symlink never
matched and the operator fell back to the raw EACCES the change existed to
replace. macOS /var -> /private/var makes this ordinary, not exotic.
Resolve the directory only when the raw comparison fails, so successful
config writes gain no syscall. The narrow path check stays: an unrelated
permission error from the caller's own mutation must keep propagating.
The Mantis lane and the Telegram user driver reached for /proc to enforce
media containment and to prove a pid was still alive. Off Linux those paths
do not exist, so the containment check compared a literal "/proc/self/fd/N"
string against the output root and refused every file, while both liveness
probes silently reported "gone": the lane stole a held harness lock and the
driver deleted the observer pid file without ever signalling the process.
Containment now uses each runtime's strongest portable construct. Python
descends the media path one component at a time from an open directory
descriptor (O_DIRECTORY|O_NOFOLLOW, dir_fd), which is race-free and also
covers the intermediate directories the old leaf-only O_NOFOLLOW did not.
Node has no openat(2), so publicRelativePath re-walks the resolved
components after the descriptor is open and refuses any that became a
symlink. Liveness moves to process.kill(pid, 0) and to ps state/args, which
keeps the pid-reuse and zombie handling the /proc reads provided.
The compound-command approval binding test asserted an operand count that
only held where the host interpreters live in a root-owned prefix; a
Homebrew python3 is writable, so it correctly binds as a third executable
operand. Assert the script operands by path instead.
* fix(daemon): skip gateway port-release assertion when stopping a node-host LaunchAgent
The generic LaunchAgent stop path (assertGatewayPortReleasedAfterStop)
resolves a gateway-relative port from the stopped service's own
program arguments/env and then asserts that port is no longer bound
after stop. `resolveNodeService()` reuses this exact same stop path
for the node-host service, but a node-host's "--port" argument is the
Gateway port it connects to *outward as a client* -- it never binds
that port itself.
On a host where the node-host and Gateway are co-located (a common,
documented topology), this made `openclaw node stop`/`restart` fail
with a false-positive "gateway port NNNN is still busy after
LaunchAgent stop" error, because the assertion was inadvertently
checking whether the co-located Gateway's own (legitimately still
open) port had been released -- something the node-host stop has no
business asserting.
Skip the port-release assertion entirely when OPENCLAW_SERVICE_KIND
indicates the node-host service, using the existing service-kind
env marker already used elsewhere in the daemon lifecycle code for
this exact gateway-vs-node distinction.
Fixes openclaw/openclaw#124296 (secondary bug: node stop/restart
false-positive port-collision guard when co-located with Gateway).
* fix(daemon): apply node-vs-gateway port guard exemption to LaunchAgent restart
The stop path (assertGatewayPortReleasedAfterStop in launchd-stop.ts) was
previously patched to skip the gateway-port-released check when
OPENCLAW_SERVICE_KIND is "node", but restartLaunchAgent in
launchd-lifecycle.ts had its own independent busy-port ownership guard
that was not touched, so `openclaw node restart` still failed for
co-located node-host + Gateway topologies.
Extract the OPENCLAW_SERVICE_KIND-based decision into a single shared
helper, shouldSkipGatewayPortOwnershipCheck (new module
launchd-node-gateway-guard.ts), and use it from both the stop and
restart guards so the two lifecycle paths can no longer diverge.
Add a regression test for co-located restart succeeding, mirroring the
existing stop regression test.
Refs openclaw/openclaw#124296
* test(daemon): add real-launchctl proof for co-located node/gateway port guard (#124296)
Adds an e2e integration test that drives real launchctl (no mocked
launchctl or port-inspection calls): a real "gateway" LaunchAgent that
genuinely binds a scratch TCP port, and a real "node-host" LaunchAgent
(OPENCLAW_SERVICE_KIND=node) targeting that same port. Verifies both
stopLaunchAgent and restartLaunchAgent succeed against the real busy
co-located port, and that the co-located Gateway LaunchAgent stays
running throughout.
Run against the pre-fix commit (2a7d735), this reproduces the exact
reported false-positive:
Error: gateway port <port> is busy but is not verifiably owned by
LaunchAgent ai.openclaw.launchd-int-node-<id>
Run against this branch, all 8 launchd integration tests pass.
* test(daemon): probe real port busy-ness before exercising node lifecycle
Addresses ClawSweeper P2 ask: the real-launchctl e2e proof only relied on
the LaunchAgent runtime status reporting 'running' for the gateway, which
doesn't guarantee the listener has actually bound the scratch TCP port yet
(status vs. socket bind can race). Add explicit probePortUsage() calls
(the same real, unmocked TCP probe used in production code) immediately
before both the stop and restart assertions, so the test proves the port
is genuinely busy at the moment the guard is exercised, not just inferred
from launchd state.
* refactor(daemon): resolve gateway ownership at launchd context boundary
Co-authored-by: Anis Khan <2815766+aniskhan001@users.noreply.github.com>
---------
Co-authored-by: Anis Khan <2815766+aniskhan001@users.noreply.github.com>
Every config write failed with a bare Node errno naming openclaw.json.lock
when the config directory was not writable. The lock file is an internal
artifact; the directory permissions are the actual problem, so operators
were sent to investigate the wrong thing.
withConfigMutationLock is the choke point for all config writes, so the
diagnosis lands there once and every writer benefits. Only EACCES, EPERM,
and EROFS whose reported path sits directly in the config directory are
relabelled; the caller's mutation runs inside the same scope, so an
unrelated permission error from its own work must keep propagating.
Non-interactive onboarding's gateway-health failure emitted the generic
classification hint and the caller's phase-specific hint together, so the
first and most prominent Fix told operators to restart a managed gateway
while the next line said no daemon had been installed.
logNonInteractiveOnboardingFailure now suppresses the classification hint
when the caller already supplied one; the caller knows whether
--install-daemon was requested and the classifier does not. Callers that
pass no hints are unchanged.
* fix(tui): own local shell process scopes
Keep local shell root results independent from authoritative process-scope
extinction, and make TUI shutdown cancel and join its ProcessSupervisor scope.
On Windows, create commands atomically inside a retained non-breakaway Job
Object and prove extinction from exact Job accounting.
Fixes#127488
* fix(process): preserve optional extinction waits