* test(codex): split the session-catalog suite along module seams
Replace the 4.8k-line grandfathered session-catalog.test.ts with seven
seam-anchored suites plus shared fixtures, and remove its max-lines
suppression and baseline entry. The broad openclaw/plugin-sdk/node-host
vi.mock becomes a narrow session-catalog-pty.runtime.ts boundary so the
non-isolated extension-codex worker no longer re-instantiates the
plugin-sdk graph once per split file (the crash PR #124178 measured).
Test bodies are verbatim; AST parity across the split checked out
96/96 identical.
AI-assisted (Codex worker under maintainer review).
* test(codex): carry caller origin in the cron-authority capability fixture
PR #118579 widened the in-process CronCreatorAuthorityCapability run
scope with a contractually required callerOrigin, and the new transcript
tool wiring reads it during turn startup. The hand-built fixture in
run-attempt.configured-mcp.test.ts predated the field, so
bindActiveOperatorTurnAuthority threw and runCodexAppServerAttempt
rejected while five tests awaited turn/start - 120s timeouts, then a
worker teardown crash. #118579's CI never ran the extension-codex lane
(cross-lane classification gap), so main's codex lane was latently red.
Bisected to 8668aeb9698; fixture now mints the local-operator origin the
helper's name promises.
AI-assisted (maintainer-diagnosed, Codex-era fixture repair).
* test(codex): keep catalog fixture internals private
* refactor(session-catalog): unify adoption coordination
* refactor(codex): split session catalog modules
* fix(anthropic): narrow adopted history for import
The coordinator migration hoisted history to an optional outer binding;
afterCreate needs the definite array from the create scope.
Also drop the retired session-catalog max-lines ratchet entry.
Keep routine Codex commands from pausing for approval in default and team YOLO sessions. The default exec policy omitted its full mode, so a generic tool hook could promote approvalPolicy from never to untrusted.
* fix(codex): keep prior finals when a coda follows without tool work
Live persist last-won a single assistant slot, so a trailing final_answer
replaced the real summary. Keep explicit finals after the last native tool;
phase-less coordination text stays last-wins.
* fix(codex): record the native-work barrier from item notifications
turn/completed.items is a last_agent_message Summary, so result-time
tool scans cannot see intervening work. Invalidate persistable finals
when native tools arrive, drop trailing silent tokens, and let a later
unphased message replace earlier finals.
* fix(codex): make the persistable-text barrier once-per-handoff
Delayed native completions must not move the barrier past an answer that
arrived while that tool was already active. Dynamic tool calls are the
same handoff. If no audible text exists after the barrier, recover the
last persistable answer instead of dropping it.
* fix(codex): keep a post-handoff silent final authoritative
A tool after an audible final starts a new answer segment. If that
segment is NO_REPLY, do not revive the pre-tool text. Recover a prior
answer only when the post-handoff segment is empty.
* fix(codex): classify silent finals with the delivery payload predicate
Token-only matching missed JSON and reasoning-wrapped NO_REPLY
payloads, so a trailing control envelope could join into the visible
answer. Use the shared payload-level predicate.
* fix(codex): honor unphased replacement and sleep handoffs
A later unphased item must invalidate older finals even when a later
final skips it, and sleep is a native answer barrier.
* feat(gateway): add remote-exec cloud placements
* feat(codex): run cloud turns through remote exec
* fix(sandbox): quote ssh_config path directives containing whitespace
Crabbox lease keys default to ~/Library/Application Support/... on macOS;
unquoted IdentityFile/UserKnownHostsFile/CertificateFile arguments tokenize
on the space and fail as 'extra arguments'. Found via live remote-exec
cloud-worker proof.
* test: consolidate gateway maintenance schedule coverage
* fix(ci): invalidate plugin sdk declarations on state changes
* feat(codex): pass explicit skill selections as structured turn input
Thread OpenClaw-resolved explicit $skill selections through the reply
pipeline to the Codex app-server harness and emit them as structured
UserInput::Skill turn items when the Codex catalog (skills/list) knows the
same SKILL.md path. Codex then injects the skill natively and blocks its
text scanner from double-selecting the same name, while unmatched names
stay scannable for Codex-native-only skills and the prompt instruction
block keeps owning skills Codex cannot see. Catalog misses and RPC errors
fail open to the instruction path; the turn never fails on skill lookup.
Part of #123367
* fix(plugins): defer llama-cpp ssrf-runtime load out of the setup closure
The doctor-contract closure guard forbids the SSRF barrel in the
doctor/legacy-setup closure (cold-loads DNS, proxy state, logging); the
static import landed with the managed llama-server switch (#123105) and
first fails when a PR selects the guard lane. Defer to a dynamic import at
download time per the guard's stated remedy.
Codex byte-scans every turn text input for $name skill mentions and
[@name](plugin://...) links (codex-rs/skills/src/mentions.rs), so historical
$skill tokens inside OpenClaw's projected <conversation_context> block and
inbound quoted-reply/room-backlog context counted as current explicit
invocations and injected skill bodies the user never requested.
Neutralize mention sigils with same-length fullwidth lookalikes (the
escapeCodexChatText technique) in projected history and inbound context;
only the raw current request stays selectable.
Fixes#122812
* fix(sessions): isolated gateways no longer inherit HOME external session catalogs
A gateway on isolated state (custom OPENCLAW_STATE_DIR/CONFIG_PATH/OPENCLAW_HOME,
relocated home, or any named profile) listed, read, continued, archived, and
reopened the operator's real Claude Code/Codex/OpenCode/Pi sessions from the
process HOME. External catalogs now require the default install identity for
process-HOME scans: every catalog verb receives the isolation policy and rejects
HOME-fallback local targets, unknown providers fail closed unless they declare
supportsProcessHomeIsolation, and one structured warning records the skip.
Paired-node hosts and explicitly rooted stores (CLAUDE_CONFIG_DIR, CODEX_HOME,
OPENCODE_DB, Pi session dirs) keep working; default-identity gateways are
unchanged.
* fix(sessions): inject catalog HOME-isolation fact at registry construction
* chore(sdk): regenerate plugin API baselines after rebase
* chore(sdk): regenerate plugin API baselines after rebase
Safely finalize settled Codex turns when an overload arrives after tool completion. Keep other provider failures fail-closed and preserve channel delivery.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(codex): skip mirrors for sessionless runs
* fix(codex): restore openai model provider on media understanding turns
PR #122163 made thread/start modelProvider caller-supplied and updated the
web-search caller but missed media understanding, breaking its tests on main
full runs (cross-lane gap). Pass the provider explicitly and export the
retire binding prod now touches from the shared-client test mock.
* test(codex): align prompt assertions with reworded guidance
PR #121522 reworded the Skill Workshop guidance and cd7b7f639d reworded
the message-tool final-reply text; both updated core tests but missed
these codex mirror assertions (cross-lane gap breaking main full runs).
* test(codex): scope agent-projection fixture session to its agent
PR #114388 made multi-agent session ownership explicit; the atlas-scoped
projection test still used the shared main-scoped session key and now
trips AgentSelectionRequiredError (fourth cross-lane escape on main).
Use the selected harness for isolated title generation, including native Codex auth.
Retry failed dashboard titles from the first user message without overriding manual names.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* refactor: consolidate coercion ownership
Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.
The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.
* fix: guard integer option ownership
Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.
* fix: keep integer helpers on numeric facade
Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.
* fix: point numeric coercion to number runtime
Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
Per-requester OAuth for HTTP MCP servers: in shared channels each trusted sender connects their own account. New config: mcp.servers.<name>.oauth.identity ("shared" default, byte-identical behavior) and gateway.publicOrigin (HTTPS except loopback) for the new GET /oauth/mcp/callback served on the Gateway HTTP server. Requester tokens are isolated per (channel, account, sender) in mcp_oauth_stores rows (no schema bump); callbacks correlate through a durable state-keyed one-time index with a 10-minute TTL; per-requester servers are fail-closed out of static/scheduled runtimes; unauthenticated senders get a sign-in link with a portable URL button; MCP config mutations clear requester credentials at the canonical owner. mcp status --json keeps the legacy authStatus fields additively. Proven by a two-user self-hosted-Executor E2E through the real gateway callback. Part of #122034.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(agents): rename spawn_task tool to suggest_task
Models across vendors read the name spawn_task as 'starts background
work' and refuse to call it when the user has not asked to start
anything (Claude Sonnet 5, verbatim: 'spawn_task (which actively starts
background work)... Since you didn't ask me to start work, I'm flagging
this as a follow-up instead'). The tool only records a suggestion card,
so the name defeated the feature.
Rename the model-facing tool to suggest_task everywhere (catalog,
descriptors, presets, display config, OpenClawKit tool-display, docs)
and migrate persisted operator state via doctor: config tool policies
and cron SQLite toolsAllow entries that reference spawn_task are
rewritten to suggest_task. No runtime alias remains; runtime exposes
only the canonical name.
Live clean-room matrix on a dev gateway (virgin session per cell):
suggested-task-card phrasing now yields exactly one card on
anthropic/claude-sonnet-5, claude-haiku-4-5, openai/gpt-5.6-sol,
gpt-5.6-luna, and google/gemini-3.1-pro; pre-rename the same bait
produced prose-only follow-ups.
* chore(doctor): keep legacy tool-name predicate module-local
isLegacyTaskSuggestionToolName has no external consumers; the exported
form tripped the knip unused-export gate (deadcode:exports).
* fix(doctor): keep tool-name migration out of plugin-owned config
The rename migration traversed the whole raw config, so any object
carrying a toolsAllow key was rewritten - including opaque
plugins.entries.*.config, which core must not mutate. Scope the apply
pass to the typed core roots (tools, agents, channels, gateway) and pin
plugin config preservation in the test. Plugin-owned tool lists migrate
via the owning plugin's doctor contract instead.
* fix(ci): heal main deadcode and stale Swift protocol mirror
Unrelated main breakage blocking this PR's merge gate, healed here per
landing policy:
- #121653 left dead exports in src/skills/workshop: make
removeSkillCollectionDirectory and SkillCollectionRestoreResult
module-local; register collection-review.ts under the documented knip
ignoreIssues test-only-export convention (production runs it via the
scheduled maintenance loop).
- #121673 added ApprovalResolveParams.reviewer without regenerating the
Swift mirror; regenerate GatewayModels.swift (additive only).
* fix(ci): satisfy generic approval-runtime request signature in resolver test
Third main-heal carried by this PR: #121673 (9935ca3b30) left the
scoped-request mock's concrete inferred type unassignable to the generic
GatewayNativeApprovalRuntime request signature, breaking check-test-types
on main. Cast the mock at the runtime literal; assertions keep the Mock.
* chore(ci): drop approval-test cast superseded by main's typed mock rewrite
* chore(ci): drop duplicate knip entry superseded by main's heal