* fix(hooks): bound hook workspace manifest and HOOK.md reads
* fix(hooks): enforce maxBytes while reading hook metadata fd
ClawSweeper review noted that openRootFileSync validates stat size
before returning the fd, but the subsequent fs.readFileSync of the fd
was unbounded. A file that grows after validation could still OOM
workspace discovery. Replace the full fd read with a chunked bounded
reader that throws once maxBytes is exceeded, and add regression
coverage for the overflow case.
* test(hooks): cover bounded hook reads through public workspace API
* refactor(hooks): use canonical bounded fd reader for hook metadata
* fix(hooks): warn and skip oversized hook metadata during discovery
ClawSweeper review required the maintainer-selected oversized-metadata
diagnostic contract: warn and skip. readRootFileUtf8 now catches the
RangeError from the canonical readFileDescriptorBoundedSync helper and
emits one warning per oversized package.json/HOOK.md identifying the
path and the byte limit, while discovery continues for other hooks.
The redundant open-time maxBytes stat check is removed so the shared
bounded reader is the single owner of the byte cap; overflow always
surfaces as RangeError, keeping the warning accurate for both static
oversized files and post-open growth.
Add focused tests: warning content for both oversized surfaces,
continued discovery alongside an oversized hook, exact-limit acceptance,
and the plain-hook fallback when a package manifest is oversized.
* chore: retrigger CI after rebase
* refactor(hooks): tighten bounded metadata coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(infra): gateway pid probe no longer hangs when ps sampling stalls
readGatewayProcessArgsSync probes ps to verify gateway argv before
signaling or reporting listener PIDs. The darwin branch already passed
timeout: 1000, but spawnSync defaults killSignal to SIGTERM, which an
unresponsive or hung ps child can ignore and keep the gateway process
blocked past the timeout.
Force killSignal: SIGKILL (matching the doctor probe fix in #109243) so
a stalled ps probe is reaped at the deadline instead of hanging gateway
signal verification and listener PID discovery.
Extract GATEWAY_PS_PROBE_TIMEOUT_MS for the named timeout and assert the
full spawnSync options (encoding / killSignal / timeout) in the darwin
test.
* refactor: centralize bounded ps probes
Co-authored-by: 琚耀辉0668001366 <ju.yaohui@xydigit.com>
* refactor: leave doctor ps hardening to main
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
@openclaw/codex is force-updated with core, so string plan steps can only
reach the normalizer from a plugin pinned against an update. Record that
as a TODO instead of a standing version-skew contract.
* fix: guard terminateStaleProcessesSync against non-ESRCH kill errors
The two catch blocks silently swallowed all errors, not just ESRCH.
Non-ESRCH errors (e.g. EPERM) indicate the process is still alive but
cannot be signaled — continuing as if it were killed risks leaving a
stale process on the port.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: continue stale PID cleanup after signal errors
Co-authored-by: 丁宇婷0668001435 <ding.yuting@xydigit.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): guard secrets plan JSON.parse against malformed input
readPlanFile reads a user-specified file and parses it with JSON.parse
without a try-catch. A malformed or corrupted plan file produces a raw
SyntaxError that is not user-actionable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: tighten malformed secrets plan proof
Co-authored-by: 丁宇婷0668001435 <ding.yuting@xydigit.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(doctor): diagnose direct web fetch proxy routing
* test(doctor): cover proxy diagnostic through public API
* test(doctor): validate emitted proxy diagnostic type
Root cause of the flaky 120s timeout in "logs a swallowed finalize error
without blocking the background run" (CI run 29559328261, shard
agentic-gateway-methods): the preceding ACP plugin-subagent test registers a
subagent run whose completion keeps running in the background after the test
ends. That leaked chain (completeSubagentRunAttempt ->
emitSubagentEndedHookForRun -> ensureSubagentRegistryPluginRuntimeLoaded ->
ensureRuntimePluginsLoaded -> loadOpenClawPlugins) starts by calling
clearActivatedPluginRuntimeState(), which clears the process-wide detached
task lifecycle runtime registration. When it lands between the next test's
setDetachedTaskLifecycleRuntime(spy) install and that test's background
finalize, finalizeTaskRunByRunId re-reads the registration at call time,
falls back to the default executor, and finalizes the run successfully - so
the finalize spy is never called while the run itself completes with an ok
frame.
The prior event-driven rework (#109653, 883995f08d) could not fix this: it
only waits longer for a spy call that never comes. The older 2s
waitForAssertion flake ("expected 1 call, got 0") was the same mechanism.
Fix at the harness boundary: gateway agent handler tests now pin the
subagent-registry deps with a no-op ensureRuntimePluginsLoaded (matching
subagent-registry's own suites), applied at harness setup, in the shared
afterEach, and through the one wholesale deps override, so no leaked
ended-run hook can reload the plugin runtime and clear a later test's
runtime seam mid-test.
Proof:
- Deterministic repro (temporary env-gated interleaving patch in
subagent-registry.ts, not committed): hold the ACP plugin-child run's
ended-hook load until the next finalize-only runtime registration appears,
then reload synchronously. Pre-fix this reproduces the exact CI failure
(target times out at 120000ms; registration observed ABSENT right after
the reload). Post-fix, with the identical forced interleaving, all 238
tests pass and the registration stays present.
- Stress post-fix: 10x agent.test.ts and 5x src/gateway/server-methods with
OPENCLAW_VITEST_MAX_WORKERS=6 - 0 failures (3765 tests per shard run).
- The same wipe-then-re-register window exists in prod plugin activation
(loadOpenClawPlugins cache-miss path); flagged separately for an
owner-reviewed atomic-swap follow-up rather than bundled here.
* perf(sdk): load Claude CLI identity from a narrow plugin artifact
src/plugin-sdk/anthropic-cli.ts snapshots CLAUDE_CLI_BACKEND_ID at module
scope through the sync facade loader, which jiti-evaluates the full
anthropic api.js barrel on source checkouts: 130.86s of self time per
cold worker on CI (Testbox import profile), silently stalling every job
whose graph reaches cli-runner/prepare.ts. A narrow cli-api.js artifact
carries the two static facts; Testbox proof: the reliability+helpers
agents-core pair drops from 157.6s to 14.3s.
* perf(ci): stripe unit-fast and tooling node tests on the full plan
core-fast ran the import-bound unit-fast graph as one job (247s vitest,
181s module evaluation) and core-tooling as one serial job (241s); both
now stripe on the full plan like the compact plan, so the compact-only
expansion is deleted and the docker helper config rides with the
isolated shard on both plans. Group hints refreshed from main run
29551077288; stale per-file commands hints dropped (the consolidation
landed); cache-writer selection and the warm workflow track the stripe
names.
* fix(state): tolerate vanished sqlite sidecars in agent-db permission sweep
existsSync+chmodSync raced SQLite's own WAL/SHM cleanup: a checkpoint or
close between the two calls throws ENOENT (observed from the transcript
reconcile worker in server-startup-web-fetch-bind on CI). chmod directly
and swallow only ENOENT, which removes the TOCTOU window.
Stream bounded browser camera frames directly to Gemini Live and keep camera media off the Gateway. Add lifecycle, function-calling, fake-camera E2E, and live-smoke coverage.
Co-authored-by: shushushu <1064076525@qq.com>
* fix(ai): track Responses output items per index and require terminal stream events
* test(ai): add live Responses stream coverage
* test(infra): mirror packages live glob in live-config test
* fix: queued MEDIA attachments survive delivery retries
Local media carried in a legacy MEDIA: text directive was invisible to
queue staging (which reads only structured mediaUrl(s)), so the raw
directive was persisted with no queue-owned copy and a retry read the
already-deleted producer path — burning the retry budget and dropping the
attachment plus its text.
Project each source payload's effective media through the canonical
createOutboundPayloadPlan before staging and fold directive-derived local
sources into the queue copy's structured fields, so the existing spool
takes custody. The custody copy anchors both mediaUrl and mediaUrls to the
effective set so the staged copy overrides the preserved in-text directive
on replay; the rendered-batch plan is recomputed from the deduplicated
effective media to avoid count inflation. Raw pre-hook text, the copy-free
live send, the same media capability, and sensitive-media fail-closed are
all unchanged. No schema/config/TTL change.
Extends the ownership invariant from #108501 / #108502 to the legacy
MEDIA: text-directive carrier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(outbound): localize queue media custody
Trim the direct helper matrix in favor of end-to-end queue recovery proof.
Co-authored-by: Masato Hoshino <g515hoshino@gmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): canonicalize manifest plugin ids to lowercase
Plugin config policy lists (plugins.deny, plugins.allow, plugins.entries)
are lowercase-normalized through normalizePluginId, but a plugin's
self-declared manifest id was only trimmed, never lowercased. A plugin
publishing "id": "Malicious-Scraper" therefore never matched an operator
denylist entry of "malicious-scraper" and fell through to default
activation, loading its hooks, channels, secret integrations, and tools.
The same gap let a mixed-case spelling evade the core reserved-id check.
Canonicalize the id at the manifest parse boundary where
PluginManifestRecord.id is minted, so every downstream policy consumer
compares against the same canonical form.
* test(plugins): prove mixed-case deny at gateway startup
* test(plugins): assert denied gateway record is absent
* test(plugins): materialize allowed secret fixture
* fix(plugins): compare a derived policy key instead of rewriting manifest identity
Manifest ids stay exactly as declared. Deny, allow, and per-entry checks now
compare a lowercase policy key derived at each enforcement boundary, matching
the lowercase-normalized config lists.
The previous approach lowercased PluginManifestRecord.id at the parse boundary.
That id is also matched against the plugin runtime export id, and a mismatch is
a hard load failure, so an existing plugin declaring the same mixed-case id in
both its manifest and its runtime export would stop loading after upgrade.
The Gateway fixture now declares a mixed-case manifest id with a matching
mixed-case runtime export, covering that upgrade case, and drops the allowlist
that previously scoped discovery by a lowercase id and masked the denylist
behavior under test.
* fix(plugins): close mixed-case policy gaps
* test(plugins): use tracked policy fixture temp dir
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(acp): persist failed bound turns so transcripts match the channel
A bound ACP turn that failed delivered an error reply to the channel but
wrote nothing to the session transcript: persistAcpDispatchTranscript ran
only on the success path, and turnPromptText was block-scoped inside the
try so the catch could not reach it. The bound transcript then silently
diverged from what the user saw, and the next turn resumed from history
that never mentioned the failure. Persist failures were also swallowed at
verbose level.
Persist exactly once, after the turn's outcome settles:
- success writes after finalization, so a finalizer failure is no longer
recorded as a clean success;
- failures record the streamed output ahead of the error text, snapshotted
before delivery so the accumulator cannot fold the error in twice;
- a dispatch flag (not prompt-text truthiness) gates the failure write, so
attachment-only turns are covered and pre-dispatch setup failures do not
mint blank-prompt records.
Found while live-testing the ACP connectors: codex and gemini bind lanes
pass 5/5, claude times out with turns that complete but never appear in the
bound transcript. This fixes the silent-divergence class and makes that
failure observable; the claude-specific trigger is tracked separately.
* fix(acp): narrow acp resolution before reading meta in the shared persist helper