refactor: retire due compat-ledger surfaces (context-engine host params, deactivate alias, logging internals) (#121845)

* refactor(plugins): retire deactivate hook alias

* refactor(plugin-sdk): prune retired facade exports

* test(logging): isolate logger test controls

* refactor(logging): internalize file transport controls

* test(plugin-sdk): preserve retired facade coverage

* test(auto-reply): remove stale diagnostic imports

* refactor(logging): delete dead config-read guard

shouldSkipMutatingLoggingConfigRead had no production caller even on main;
it survived the dead-export scan only via logger's testApi re-export. The
test-isolation commit removed that mask, exposing the fossil. Delete the
guard, its test-only re-export, its mock entry, and its dedicated test file.

* refactor(plugin-sdk): retire due compatibility subpaths

* test(plugin-sdk): type group policy predicates

* refactor(plugin-sdk): split removed subpath records

* refactor(secrets): remove retired collector barrel

* test(plugin-sdk): tighten wildcard surface pin

* refactor(plugin-sdk): retire matrix facade metadata

* style(plugin-sdk): format facade metadata

* fix(ci): load channel setup contracts from source

Repair the main-owned regression from 99d662473c (Peter Steinberger): the new env-contract test could consume stale ignored dist metadata instead of the checked-in plugin declaration.

* test(plugin-sdk): refresh API baseline after rebase
This commit is contained in:
Peter Steinberger
2026-08-12 12:41:27 -07:00
committed by GitHub
parent 49092ebab3
commit dceb2c343c
116 changed files with 562 additions and 1557 deletions
@@ -1 +0,0 @@
{"contentHash":"0349da0a93dadbdcff03ae8064b5066c5f2e1c28ee298bfa9ab3f1f8563300eb","entrypoint":"agent-config-primitives","importSpecifier":"openclaw/plugin-sdk/agent-config-primitives"}
@@ -1 +1 @@
{"contentHash":"31bcb6e7b685c67391cc16a005f4ff55ffd97d382a10f4b1796ddf2fd303a308","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"0afe8b5e500dce4237ca4b5023ab225cfd31a9897e0d4bfdb2341886df035952","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"108af57a4b4bc57c70912fc40319876bb6ec994cce59d2b9c991980e18c4f80f","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"4580a38b9e871c7d448c90cd247673929caaa7a298877d96d8e3f7e7c970b27a","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"adc174fdd6ceeaa664ecf483d76aec22ecb111a7a8f6534cb78463b4347abb5e","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"}
{"contentHash":"96ad6087a29b2cb11f793f954afd31446854d173396b0d7fded033fddddf4dd3","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"dd255204bf11dccfe9d866e7a6725935bfead7378a52ddfa6b60b9a58c0cdf76","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"b84b53ca4792a410eb488ae4fb303e362c3c84d8eb72e37cf9c922183792a7cd","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"4bc2ad950266148cfbb2acc7996de31401d88dd5f80ea28754e76f3bd66ef0d2","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"cd582aff7d7278ddc71a3c99bd4f8b46e2083411f02096e8649e74b39ab001ea","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"f0be441226a760f75dc1634f93d7f28586a0b18f690e2dd0f87c5360f2cb7f03","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
{"contentHash":"017a51cefc0713190d68e7ba9a7901d45342451ca3b05c4830072340fa86515e","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
@@ -1 +0,0 @@
{"contentHash":"be803c0341214a8e731f98cb7bf98654410ab1e0254d90f9f1ad9f442f70669f","entrypoint":"channel-logging","importSpecifier":"openclaw/plugin-sdk/channel-logging"}
@@ -1 +1 @@
{"contentHash":"8bf7d1fd1e21861fd472365e0ab10ca811800e1b0cb9e6aac0e4d6a41613a9a2","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"abb22832153a23cd5187f92f2cdee6d81889d61697fae04c8647eb3370a1aa93","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"d430c3def27acb48a5356dafac17b89b0cbfcb1d3f959136b8c7ae82586d87df","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"a82dcfdcc661b2567ba2ba631d589887d5ca29cfe1d8600696fe0db859f4969f","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"aa9a880fa3abde607a3664e55cf6bd35966528d436c0438aa3c1b7607c8e46e7","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"034d00199482a5490cb8202aa2ff6c5acd438486e8ab616b5f76674af7fb78c4","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"6200788e555ccd22764a7aff25a94b4ce7176dd1443a5fe6f3bb81f23c561811","entrypoint":"channel-policy","importSpecifier":"openclaw/plugin-sdk/channel-policy"}
{"contentHash":"d6252e057b614160557b42923f45f42491b5872c835e5f1d372a031e338ecdd4","entrypoint":"channel-policy","importSpecifier":"openclaw/plugin-sdk/channel-policy"}
@@ -1 +0,0 @@
{"contentHash":"c705cf7c1ba06b0d473d6cd83e4701e8ec464b226446d5465ebffb0f656dc4d0","entrypoint":"channel-secret-runtime","importSpecifier":"openclaw/plugin-sdk/channel-secret-runtime"}
@@ -1 +0,0 @@
{"contentHash":"2e6f9a8e8a07de789f6673813b5acc17dd08a11420c9de1b3ac509d37786adc6","entrypoint":"channel-streaming","importSpecifier":"openclaw/plugin-sdk/channel-streaming"}
@@ -1 +1 @@
{"contentHash":"f2849884f368eadff174176a2808c1c74d041968049b460d6f8b408682c5862b","entrypoint":"config-mutation","importSpecifier":"openclaw/plugin-sdk/config-mutation"}
{"contentHash":"56a3c566d98d262c3ac07c830f2b33bd864e35930335d7b4e3e2ccc76607779e","entrypoint":"config-mutation","importSpecifier":"openclaw/plugin-sdk/config-mutation"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"2c21d121fd9d14d7f45368b5f02dff257b6750215d63d4be9ace28b5c0d3c0f8","entrypoint":"config-runtime","importSpecifier":"openclaw/plugin-sdk/config-runtime"}
{"contentHash":"180a151549ad301c08f9265c3f9fb7a17821c93dd5b0e4f6128f7512312dc909","entrypoint":"config-runtime","importSpecifier":"openclaw/plugin-sdk/config-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"4c667b7e88cd752c294bcc731bc157c1ccb2a7aa4afc58f4013409191f3a9b28","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"2efff507ffe87196b554e29043eadc0ba17a32864f3ccd1864d4200add9d8f87","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"027b2280171324b628865fb4cdf9f72c13c059385a2fbec5b650c69d66beecdc","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"31bfbc294becf37f8ee037a0f8f50c69e4d04e097ff9bea16089115ee93b80df","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -1 +1 @@
{"contentHash":"adf5aa7e44a7bc6f6e0607fa35eae1fcb197749bb5a129c1848f85d61dc319a2","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
{"contentHash":"bb371c8a354a150acf264357e3cbcb755efc6f42d9a2143e5a9254c3bc3c2aa8","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
@@ -1 +0,0 @@
{"contentHash":"c6ff5b325384c258ae96e677568d58652fe66ebc6745669a2137970154ef4b92","entrypoint":"group-access","importSpecifier":"openclaw/plugin-sdk/group-access"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"ca7135503da9f048e237fd1242f463f74feccb22da0bc63770e16d982487a7a9","entrypoint":"hook-runtime","importSpecifier":"openclaw/plugin-sdk/hook-runtime"}
{"contentHash":"c27a0ddd9f0a12bbb8377e8fcf8422ed2e066b8d248bc250c34e06518f1891a3","entrypoint":"hook-runtime","importSpecifier":"openclaw/plugin-sdk/hook-runtime"}
@@ -1 +1 @@
{"contentHash":"94ee499e1ea0653d3e7daed7c6aaa2db8d7c3026433eba62b19a5f6cda93028b","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"07dbe53008bb2487dd63f28db4d81424cfb67498e4508aae6d86667e78fe1855","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"0b044375b584739e90aa9591e4be679a90cae0fd5e29b3ede60b942d236a7f3a","entrypoint":"logging-core","importSpecifier":"openclaw/plugin-sdk/logging-core"}
{"contentHash":"2efeb44c37461f4b366018d4e5e11b0c2e107336202388d8f17688b71954fa39","entrypoint":"logging-core","importSpecifier":"openclaw/plugin-sdk/logging-core"}
-1
View File
@@ -1 +0,0 @@
{"contentHash":"192aa90e9d1d6e1bae65abd591cd0ab6307dc2bcc7626092e48ba96a171f30f8","entrypoint":"matrix","importSpecifier":"openclaw/plugin-sdk/matrix"}
@@ -1 +1 @@
{"contentHash":"69fe3479e42b74771fb86d228264e8e19a3a16e1a07df4f89499111c2df2bc13","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"5ccd913160d86827e36459bf6c6e88a169306dd24e913b85e288cbbf2ea12157","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
@@ -1 +1 @@
{"contentHash":"9c8d6c57eae34ab72ec465dbf610f75d5d1bf737d7f04eadab63c806ea8a51fa","entrypoint":"model-session-runtime","importSpecifier":"openclaw/plugin-sdk/model-session-runtime"}
{"contentHash":"cd909c52a27a558de6f2647c44544b81f80038bb02577fe9b60ea612a609948e","entrypoint":"model-session-runtime","importSpecifier":"openclaw/plugin-sdk/model-session-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"f058be9cbffbcb6e1dafe69cbc701c7cc06d1ffc90323256685e31b111f91193","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"d9ae989750b9f69072b5767cdf8995bde3f431da315e031cb80f35d2052a3917","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"e7ac61e29d41e43a4c780831a8aa0e5bed10e47630a36879f2555216eb1cce54","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"dce833138906cea4ce741068911c2e4818ebc37693edb2d2883e54eb7b25778e","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"c63113a4720b3d4795a3deb00ce3652cc9481522af311b9a269e4fe0d84703bb","entrypoint":"provider-auth","importSpecifier":"openclaw/plugin-sdk/provider-auth"}
{"contentHash":"ae9c54c47a1937328f56b23f03590d458248f58c597645ec5e0f65b19464e75a","entrypoint":"provider-auth","importSpecifier":"openclaw/plugin-sdk/provider-auth"}
@@ -1 +1 @@
{"contentHash":"12f6874f2b2c49f6fa22b91acdae820a725564d381a5b26dc4b2b4e65d0ef803","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"0010661448f741237afcb47b75bdc2178d962e4e5751dfc4d26fdb1c4c487997","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
@@ -1 +1 @@
{"contentHash":"538f9d1c94c854e2c23a20e2b178be281775abf1ede99d21a543c689ccb28a78","entrypoint":"security-runtime","importSpecifier":"openclaw/plugin-sdk/security-runtime"}
{"contentHash":"e793cfb3c3eb076bb73ed290ddc08c49d2c8ef35fcf46bdbcac66f24c6413bf2","entrypoint":"security-runtime","importSpecifier":"openclaw/plugin-sdk/security-runtime"}
@@ -1 +0,0 @@
{"contentHash":"9d63e2bd18378154e40c4055f34010a6db6c6e7913ebbbfcf2e8565ad7302d93","entrypoint":"text-runtime","importSpecifier":"openclaw/plugin-sdk/text-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"f76d5908b66bb49851cccc530dc63fc5f3352b327963f1091a0f34fe9ccc693b","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"a392d8bbb2ababa55a16ac115beff0ed0b790723c8e6c69c0a46d44c6984ed4e","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"e9dd8d432098b26f2e102fe41edee7c556558ea9b26a0077da72b0967d6649d2","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"a9f8b45ec3684fdb607c50fc47c41f62255c9ac0eae282b42441df58400fce31","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
-1
View File
@@ -1 +0,0 @@
{"contentHash":"eafdc7277066acf893bfef9b635689c1a69d6d5af22ad8cee2b47351d9b53ca6","entrypoint":"zod","importSpecifier":"openclaw/plugin-sdk/zod"}
+1 -1
View File
@@ -697,7 +697,7 @@ plugin fields. See [Channel plugins](/plugins/sdk-channel-plugins).
Runtime and config helpers live under matching focused `*-runtime` subpaths
(`approval-runtime`, `agent-runtime`, `lazy-runtime`, `directory-runtime`,
`text-runtime`, `runtime-store`, `system-event-runtime`, `heartbeat-runtime`,
`text-utility-runtime`, `runtime-store`, `system-event-runtime`, `heartbeat-runtime`,
`channel-activity-runtime`, etc.). Prefer `config-contracts`,
`plugin-config-runtime`, `runtime-config-snapshot`, and `config-mutation`
instead of the broad `config-runtime` compatibility barrel.
+2 -2
View File
@@ -78,8 +78,8 @@ separately tracked so supported upgrade paths can still repair old config.
The remaining dated compatibility areas are:
- the August and September SDK subpath windows listed in the migration guide
- `api.on("deactivate", ...)` and `api.on("subagent_spawning", ...)` hook aliases
- the September SDK subpath window listed in the migration guide
- the `api.on("subagent_spawning", ...)` hook alias
- memory-specific embedding registration and the beta.5 session-store bridge
- WhatsApp inbound callback aliases described below
- explicit channel target parsing and `openclaw/plugin-sdk/messaging-targets`
+4 -3
View File
@@ -212,7 +212,6 @@ For `sessions.create` calls with `parentSessionKey` and `emitCommandHooks: true`
| Hook | Purpose |
| -------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `gateway_start` / `gateway_stop` | Start or stop plugin-owned services with the Gateway |
| `deactivate` | Deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins |
| `cron_reconciled` | Reconcile against the complete Gateway cron state after startup or reload |
| `cron_changed` | Observe Gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) |
| **`before_install`** | Inspect staged skill or plugin install material from a loaded plugin runtime |
@@ -846,6 +845,10 @@ clean up long-running resources. The cron scheduler can still be loading when
`gateway_start` runs, so do not use it as the baseline signal for an external
cron projection.
The legacy `api.on("deactivate", ...)` alias was removed in August 2026. Use
`gateway_stop` for cleanup; see the
[migration note](/plugins/sdk-migration#deactivate-hook-alias).
Do not rely on the internal `gateway:startup` hook for plugin-owned runtime
services.
@@ -1049,8 +1052,6 @@ before the next major release:
new plugins should not return thread routing from it. Core prepares
`thread: true` subagent bindings through channel session-binding adapters
before `subagent_spawned` fires.
- **`deactivate`** remains as a deprecated cleanup compatibility alias until
after 2026-08-16. New plugins should use `gateway_stop`.
- **`onResolution` in `before_tool_call`** now uses the typed
`PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` /
`timeout` / `cancelled`) instead of a free-form `string`.
+25 -24
View File
@@ -576,6 +576,23 @@ Provider plugins should register text-inference providers through
`ApiRegistry` should register directly on that registry so provider ownership
and teardown stay scoped to the prepared runtime.
### Deactivate hook alias
The `api.on("deactivate", handler)` compatibility alias was removed. Register
the same shutdown cleanup with `gateway_stop`:
```typescript
// Before
api.on("deactivate", async (event, ctx) => {
await stopPluginService(ctx);
});
// After
api.on("gateway_stop", async (event, ctx) => {
await stopPluginService(ctx);
});
```
### Private testing barrel
`openclaw/plugin-sdk/testing` was repo-local and excluded from shipped package
@@ -663,29 +680,6 @@ timeline for current status.
</Accordion>
<Accordion title="deactivate hook -> gateway_stop">
**Old**: `api.on("deactivate", handler)`.
**New**: `api.on("gateway_stop", handler)`. Same shutdown cleanup
contract; only the hook name changes.
```typescript
// Before
api.on("deactivate", async (event, ctx) => {
await stopPluginService(ctx);
});
// After
api.on("gateway_stop", async (event, ctx) => {
await stopPluginService(ctx);
});
```
`deactivate` remains wired as a deprecated compatibility alias until it is
removed after 2026-08-16.
</Accordion>
<Accordion title="subagent_spawning hook -> core thread binding">
**Old**: `api.on("subagent_spawning", handler)` returning
`threadBindingReady` or `deliveryOrigin`.
@@ -1073,9 +1067,16 @@ The July 30 rows were removed after their early maintainer-authorized sweep:
unused subpaths were deleted, earlier compatibility aliases were deleted, and
bundled-only modules were demoted to private-local build mappings.
The August 15 compatibility subpaths `agent-config-primitives`,
`channel-logging`, `channel-secret-runtime`, `channel-streaming`,
`group-access`, `matrix`, `text-runtime`, and `zod` were retired early by
explicit SDK-owner approval in August 2026. Use the focused replacements in
the [Plugin SDK subpath catalog](/plugins/sdk-subpaths), and import `zod`
directly from the `zod` package. `inbound-reply-dispatch` remains available
until the next Plugin SDK major.
| Removal gate | Tier | SDK subpaths |
| ----------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2026-08-15` | Earlier compatibility deprecations | `agent-config-primitives`, `channel-logging`, `channel-secret-runtime`, `channel-streaming`, `group-access`, `matrix`, `text-runtime`, `zod` |
| `2026-09-01` | Earlier compatibility deprecations | `channel-lifecycle`, `channel-message`, `channel-reply-pipeline`, `config-runtime`, `infra-runtime` |
| `next-plugin-sdk-major` | Major-version compatibility gate | `inbound-reply-dispatch` |
| `2026-10-01` | Media legacy projection | `agent-media-payload`, plus the non-subpath `MsgContext Media*` fields, channel inbound media payload builders, `buildMediaPayload`, hook media aliases, and `{{Media*}}` templates |
+2 -9
View File
@@ -41,9 +41,8 @@ Only the later-window deprecated subpaths remain exported. July 2026 aliases and
unused subpaths were deleted, while bundled-only helpers were removed from the
public package and are labeled private-local below. The maintained list is
`scripts/lib/plugin-sdk-deprecated-public-subpaths.json`; CI rejects bundled
`plugin-sdk/text-runtime` are compatibility only, and `plugin-sdk/zod` is a
compatibility re-export: import `zod` directly from `zod`. The broad domain
barrels `plugin-sdk/agent-runtime`, `plugin-sdk/channel-lifecycle`,
imports of these compatibility-only subpaths. The broad domain barrels
`plugin-sdk/agent-runtime`, `plugin-sdk/channel-lifecycle`,
`plugin-sdk/conversation-runtime`, `plugin-sdk/hook-runtime`,
`plugin-sdk/media-runtime`, `plugin-sdk/plugin-runtime`, and
`plugin-sdk/security-runtime` are likewise deprecated in favor of focused
@@ -111,7 +110,6 @@ new code; see the per-row notes below.
| `plugin-sdk/channel-config-writes` | Private-local after July 2026; Channel config-write authorization helpers |
| `plugin-sdk/channel-plugin-common` | Shared channel plugin prelude exports |
| `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers |
| `plugin-sdk/group-access` | Deprecated group-access decision helpers; use `resolveChannelMessageIngress` from `plugin-sdk/channel-ingress-runtime` |
| `plugin-sdk/direct-dm-guard-policy` | Private-local after July 2026; Narrow direct-DM pre-crypto guard policy helpers |
| `plugin-sdk/discord` | Deprecated Discord compatibility facade for published `@openclaw/discord@2026.3.13` and tracked owner compatibility; new plugins should use generic channel SDK subpaths |
| `plugin-sdk/telegram-account` | Deprecated Telegram account-resolution compatibility facade for tracked owner compatibility; new plugins should use injected runtime helpers or generic channel SDK subpaths |
@@ -120,7 +118,6 @@ new code; see the per-row notes below.
| `plugin-sdk/channel-inbound` | Shared inbound helpers for event classification, context building, formatting, roots, debounce, mention matching, mention-policy, and inbound logging |
| `plugin-sdk/channel-inbound-debounce` | Narrow inbound debounce helpers |
| `plugin-sdk/channel-mention-gating` | Private-local after July 2026; Narrow mention-policy, mention marker, and mention text helpers without the broader inbound runtime surface |
| `plugin-sdk/channel-streaming` | Deprecated compatibility facade. Use `plugin-sdk/channel-outbound`. |
| `plugin-sdk/channel-streaming-config` | Dependency-light channel streaming config readers (`getChannelStreamingConfigObject`, `resolveChannelStreamingNativeTransport`) for doctor contract closures and other control-plane paths that must not load the reply pipeline |
| `plugin-sdk/channel-send-result` | Reply result types |
| `plugin-sdk/channel-actions` | Channel message-action helpers, plus deprecated native schema helpers kept for plugin compatibility |
@@ -197,7 +194,6 @@ usage endpoint failed or returned no usable usage data.
| `plugin-sdk/command-surface` | Private-local after July 2026; Command-body normalization and command-surface helpers |
| `plugin-sdk/allow-from` | Allow-from parsing, normalization, resolution, and matching helpers |
| `plugin-sdk/provider-auth-login-flow-runtime` | Private-local after July 2026; Lazy provider auth login flow helpers for private channel and Web UI device-code pairing |
| `plugin-sdk/channel-secret-runtime` | Deprecated broad secret-contract surface (`collectSimpleChannelFieldAssignments`, `getChannelSurface`, `pushAssignment`, secret target types); prefer the focused subpaths below |
| `plugin-sdk/channel-secret-basic-runtime` | Narrow secret-contract exports and target-registry builders for non-TTS channel/plugin secret surfaces |
| `plugin-sdk/channel-secret-tts-runtime` | Private-local after July 2026; Narrow nested channel TTS secret assignment helpers |
| `plugin-sdk/secret-ref-runtime` | Narrow SecretRef typing, resolution, setup-plan construction, and setup CLI scaffolding for plugin-owned secret providers |
@@ -223,7 +219,6 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/codex-mcp-projection` | Private-local after July 2026; Bundled Codex helper for projecting user MCP server config into Codex thread config; not for third-party plugins |
| `plugin-sdk/codex-session-transcript-runtime` | Private-local bundled Codex helper for serializing transcript-mirror writes; not for third-party plugins |
| `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers |
| `plugin-sdk/matrix` | Deprecated Matrix compatibility facade for older third-party channel packages; new plugins should import `plugin-sdk/run-command` directly |
| `plugin-sdk/runtime-store` | `createPluginRuntimeStore` |
| `plugin-sdk/plugin-command-runtime` | Registry-generation-bound native plugin command candidates, terminal catalog decisions, and exact selected dispatch execution |
| `plugin-sdk/plugin-runtime` | Deprecated broad barrel for plugin command/hook/http/interactive helpers; prefer focused plugin runtime subpaths |
@@ -282,7 +277,6 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/acp-runtime` | Private-local after July 2026; ACP runtime/session and reply-dispatch helpers |
| `plugin-sdk/acp-runtime-backend` | Private-local after July 2026; Lightweight ACP backend registration and reply-dispatch helpers for startup-loaded plugins |
| `plugin-sdk/acp-binding-resolve-runtime` | Private-local after July 2026; Read-only ACP binding resolution without lifecycle startup imports |
| `plugin-sdk/agent-config-primitives` | Deprecated agent runtime config-schema primitives; import schema primitives from a maintained plugin-owned surface |
| `plugin-sdk/boolean-param` | Loose boolean param reader |
| `plugin-sdk/dangerous-name-runtime` | Private-local after July 2026; Dangerous-name matching resolution helpers |
| `plugin-sdk/device-bootstrap` | Device bootstrap and pairing token helpers, including `BOOTSTRAP_HANDOFF_OPERATOR_SCOPES` |
@@ -352,7 +346,6 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/transcripts` | Private-local after July 2026; Shared transcript source provider types, registry helpers, meeting-provider bridge factory, session descriptors, and utterance metadata |
| `plugin-sdk/webhook-targets` | Private-local after July 2026; Webhook target registry and route-install helpers |
| `plugin-sdk/web-media` | Shared remote/local media loading helpers |
| `plugin-sdk/zod` | Deprecated compatibility re-export; import `zod` from `zod` directly |
| `plugin-sdk/plugin-test-api` | Repo-local minimal `createTestPluginApi` helper for direct plugin registration unit tests without importing repo test helper bridges |
| `plugin-sdk/agent-runtime-test-contracts` | Repo-local native agent-runtime adapter contract fixtures for auth, delivery, fallback, tool-hook, prompt-overlay, schema, and transcript projection tests |
| `plugin-sdk/channel-test-helpers` | Repo-local channel-oriented test helpers for generic actions/setup/status contracts, directory assertions, account startup lifecycle, send-config threading, runtime mocks, status issues, outbound delivery, and hook registration |
@@ -554,12 +554,6 @@
"openclaw/plugin-sdk/channel-secret-basic-runtime": [
"../dist/plugin-sdk/channel-secret-basic-runtime.d.ts"
],
"openclaw/plugin-sdk/channel-secret-runtime": [
"../dist/plugin-sdk/channel-secret-runtime.d.ts"
],
"openclaw/plugin-sdk/channel-streaming": [
"../dist/plugin-sdk/channel-streaming.d.ts"
],
"openclaw/plugin-sdk/error-runtime": [
"../dist/plugin-sdk/error-runtime.d.ts"
],
-6
View File
@@ -548,12 +548,6 @@
"openclaw/plugin-sdk/browser-maintenance": [
"../../dist/plugin-sdk/src/plugin-sdk/browser-maintenance.d.ts"
],
"openclaw/plugin-sdk/channel-secret-runtime": [
"../../dist/plugin-sdk/channel-secret-runtime.d.ts"
],
"openclaw/plugin-sdk/channel-streaming": [
"../../dist/plugin-sdk/channel-streaming.d.ts"
],
"openclaw/plugin-sdk/error-runtime": [
"../../dist/plugin-sdk/error-runtime.d.ts"
],
-32
View File
@@ -451,10 +451,6 @@
"types": "./dist/plugin-sdk/channel-setup.d.ts",
"default": "./dist/plugin-sdk/channel-setup.js"
},
"./plugin-sdk/channel-streaming": {
"types": "./dist/plugin-sdk/channel-streaming.d.ts",
"default": "./dist/plugin-sdk/channel-streaming.js"
},
"./plugin-sdk/channel-streaming-config": {
"types": "./dist/plugin-sdk/channel-streaming-config.d.ts",
"default": "./dist/plugin-sdk/channel-streaming-config.js"
@@ -704,10 +700,6 @@
"./plugin-sdk/thread-bindings-session-runtime": {
"default": "./dist/plugin-sdk/thread-bindings-session-runtime.js"
},
"./plugin-sdk/text-runtime": {
"types": "./dist/plugin-sdk/text-runtime.d.ts",
"default": "./dist/plugin-sdk/text-runtime.js"
},
"./plugin-sdk/text-chunking": {
"types": "./dist/plugin-sdk/text-chunking.d.ts",
"default": "./dist/plugin-sdk/text-chunking.js"
@@ -745,10 +737,6 @@
"types": "./dist/plugin-sdk/channel-secret-basic-runtime.d.ts",
"default": "./dist/plugin-sdk/channel-secret-basic-runtime.js"
},
"./plugin-sdk/channel-secret-runtime": {
"types": "./dist/plugin-sdk/channel-secret-runtime.d.ts",
"default": "./dist/plugin-sdk/channel-secret-runtime.js"
},
"./plugin-sdk/channel-secret-tts-runtime": {
"default": "./dist/plugin-sdk/channel-secret-tts-runtime.js"
},
@@ -881,10 +869,6 @@
"./plugin-sdk/account-resolution-runtime": {
"default": "./dist/plugin-sdk/account-resolution-runtime.js"
},
"./plugin-sdk/agent-config-primitives": {
"types": "./dist/plugin-sdk/agent-config-primitives.d.ts",
"default": "./dist/plugin-sdk/agent-config-primitives.js"
},
"./plugin-sdk/access-groups": {
"default": "./dist/plugin-sdk/access-groups.js"
},
@@ -947,10 +931,6 @@
"types": "./dist/plugin-sdk/discord.d.ts",
"default": "./dist/plugin-sdk/discord.js"
},
"./plugin-sdk/matrix": {
"types": "./dist/plugin-sdk/matrix.d.ts",
"default": "./dist/plugin-sdk/matrix.js"
},
"./plugin-sdk/device-bootstrap": {
"types": "./dist/plugin-sdk/device-bootstrap.d.ts",
"default": "./dist/plugin-sdk/device-bootstrap.js"
@@ -1020,10 +1000,6 @@
"types": "./dist/plugin-sdk/channel-inbound-debounce.d.ts",
"default": "./dist/plugin-sdk/channel-inbound-debounce.js"
},
"./plugin-sdk/channel-logging": {
"types": "./dist/plugin-sdk/channel-logging.d.ts",
"default": "./dist/plugin-sdk/channel-logging.js"
},
"./plugin-sdk/channel-mention-gating": {
"default": "./dist/plugin-sdk/channel-mention-gating.js"
},
@@ -1125,10 +1101,6 @@
"./plugin-sdk/group-activation": {
"default": "./dist/plugin-sdk/group-activation.js"
},
"./plugin-sdk/group-access": {
"types": "./dist/plugin-sdk/group-access.d.ts",
"default": "./dist/plugin-sdk/group-access.js"
},
"./plugin-sdk/global-singleton": {
"default": "./dist/plugin-sdk/global-singleton.js"
},
@@ -1472,10 +1444,6 @@
"types": "./dist/plugin-sdk/web-media.d.ts",
"default": "./dist/plugin-sdk/web-media.js"
},
"./plugin-sdk/zod": {
"types": "./dist/plugin-sdk/zod.d.ts",
"default": "./dist/plugin-sdk/zod.js"
},
"./plugin-sdk/agent-core": {
"default": "./dist/plugin-sdk/agent-core.js"
},
-16
View File
@@ -28,14 +28,6 @@
"types": "./dist/src/plugin-sdk/channel-activity-runtime.d.ts",
"default": "./src/channel-activity-runtime.ts"
},
"./channel-secret-runtime": {
"types": "./dist/src/plugin-sdk/channel-secret-runtime.d.ts",
"default": "./src/channel-secret-runtime.ts"
},
"./channel-streaming": {
"types": "./dist/src/plugin-sdk/channel-streaming.d.ts",
"default": "./src/channel-streaming.ts"
},
"./cli-runtime": {
"types": "./dist/src/plugin-sdk/cli-runtime.d.ts",
"default": "./src/cli-runtime.ts"
@@ -212,10 +204,6 @@
"types": "./dist/src/plugin-sdk/talk-config-runtime.d.ts",
"default": "./src/talk-config-runtime.ts"
},
"./text-runtime": {
"types": "./dist/src/plugin-sdk/text-runtime.d.ts",
"default": "./src/text-runtime.ts"
},
"./text-utility-runtime": {
"types": "./dist/src/plugin-sdk/text-utility-runtime.d.ts",
"default": "./src/text-utility-runtime.ts"
@@ -235,10 +223,6 @@
"./video-generation": {
"types": "./dist/src/plugin-sdk/video-generation.d.ts",
"default": "./src/video-generation.ts"
},
"./zod": {
"types": "./dist/src/plugin-sdk/zod.d.ts",
"default": "./src/zod.ts"
}
}
}
@@ -1,3 +0,0 @@
// Public package facade for channel secret runtime helpers.
export * from "../../../src/plugin-sdk/channel-secret-runtime.js";
@@ -1,3 +0,0 @@
// Public package facade for channel streaming helpers.
export * from "../../../src/plugin-sdk/channel-streaming.js";
-3
View File
@@ -1,3 +0,0 @@
// Public package facade for text runtime helpers.
export * from "../../../src/plugin-sdk/text-runtime.js";
-3
View File
@@ -1,3 +0,0 @@
// Public package facade for the shared zod re-export.
export * from "../../../src/plugin-sdk/zod.js";
+6 -1
View File
@@ -70,7 +70,7 @@ let missing = 0;
`import { buildChannelConfigSchema, DmPolicySchema } from "openclaw/plugin-sdk/channel-config-schema";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
import { z } from "openclaw/plugin-sdk/zod";
import { z } from "zod";
const runtimeStore = createPluginRuntimeStore<PluginRuntime>({
pluginId: "package-consumer",
@@ -110,6 +110,11 @@ export default defineChannelPluginEntry({
const openclawPackagePath = join(consumerRoot, "node_modules", "openclaw");
mkdirSync(dirname(openclawPackagePath), { recursive: true });
symlinkSync(repoRoot, openclawPackagePath, process.platform === "win32" ? "junction" : "dir");
symlinkSync(
join(repoRoot, "node_modules", "zod"),
join(consumerRoot, "node_modules", "zod"),
process.platform === "win32" ? "junction" : "dir",
);
const result = spawnSync(
process.execPath,
@@ -51,8 +51,4 @@ export const BANNED_INTERNAL_PLUGIN_SDK_FACADE_MODULES: BannedInternalPluginSdkF
modulePath: "src/plugin-sdk/inbound-envelope",
canonical: "openclaw/plugin-sdk/channel-inbound",
},
{
modulePath: "src/plugin-sdk/text-runtime",
canonical: "the focused typed public Plugin SDK subpath for the imported helper",
},
];
@@ -64,8 +64,6 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"openclaw/plugin-sdk/channel-secret-basic-runtime": [
"../dist/plugin-sdk/channel-secret-basic-runtime.d.ts",
],
"openclaw/plugin-sdk/channel-secret-runtime": ["../dist/plugin-sdk/channel-secret-runtime.d.ts"],
"openclaw/plugin-sdk/channel-streaming": ["../dist/plugin-sdk/channel-streaming.d.ts"],
"openclaw/plugin-sdk/error-runtime": ["../dist/plugin-sdk/error-runtime.d.ts"],
"openclaw/plugin-sdk/secret-ref-runtime": ["../dist/plugin-sdk/secret-ref-runtime.d.ts"],
"openclaw/plugin-sdk/ssrf-runtime": ["../dist/plugin-sdk/ssrf-runtime.d.ts"],
@@ -1,5 +1,4 @@
[
"channel-lifecycle",
"infra-runtime",
"text-runtime"
"infra-runtime"
]
@@ -1,21 +1,13 @@
[
"agent-media-payload",
"agent-config-primitives",
"channel-lifecycle",
"channel-logging",
"channel-message",
"channel-reply-pipeline",
"channel-secret-runtime",
"channel-streaming",
"command-auth",
"config-runtime",
"discord",
"group-access",
"inbound-reply-dispatch",
"infra-runtime",
"matrix",
"messaging-targets",
"telegram-account",
"text-runtime",
"zod"
"telegram-account"
]
+1 -5
View File
@@ -111,11 +111,7 @@ export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((en
);
/** Supported SDK facades backed by bundled plugins until generic contracts replace them. */
export const supportedBundledFacadeSdkEntrypoints = [
"discord",
"matrix",
"telegram-account",
] as const;
export const supportedBundledFacadeSdkEntrypoints = ["discord", "telegram-account"] as const;
/** Plugin-owned surfaces intentionally public and documented for third-party plugins. */
export const publicPluginOwnedSdkEntrypoints = ["memory-core-host-engine-foundation"] as const;
-8
View File
@@ -18,7 +18,6 @@
"setup",
"setup-runtime",
"channel-setup",
"channel-streaming",
"channel-streaming-config",
"setup-tools",
"archive",
@@ -90,7 +89,6 @@
"conversation-runtime",
"thread-bindings-runtime",
"thread-bindings-session-runtime",
"text-runtime",
"text-chunking",
"agent-scope-runtime",
"agent-runtime",
@@ -101,7 +99,6 @@
"plugin-command-runtime",
"plugin-runtime",
"channel-secret-basic-runtime",
"channel-secret-runtime",
"channel-secret-tts-runtime",
"secret-ref-runtime",
"secret-file-runtime",
@@ -158,7 +155,6 @@
"account-id",
"account-resolution",
"account-resolution-runtime",
"agent-config-primitives",
"access-groups",
"allow-from",
"allowlist-config-edit",
@@ -177,7 +173,6 @@
"collection-runtime",
"direct-dm-guard-policy",
"discord",
"matrix",
"device-bootstrap",
"diagnostic-runtime",
"error-runtime",
@@ -196,7 +191,6 @@
"channel-feedback",
"channel-inbound",
"channel-inbound-debounce",
"channel-logging",
"channel-mention-gating",
"channel-lifecycle",
"channel-ingress-runtime",
@@ -228,7 +222,6 @@
"ssrf-dispatcher",
"string-coerce-runtime",
"group-activation",
"group-access",
"global-singleton",
"directory-config-runtime",
"directory-runtime",
@@ -332,7 +325,6 @@
"webhook-targets",
"webhook-request-guards",
"web-media",
"zod",
"agent-core",
"agent-sessions",
"llm"
+1 -100
View File
@@ -15,14 +15,7 @@ import {
import { builtinModules } from "node:module";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import {
dirname,
isAbsolute,
join,
posix as pathPosix,
relative,
win32 as pathWin32,
} from "node:path";
import { isAbsolute, join, posix as pathPosix, relative, win32 as pathWin32 } from "node:path";
import { pathToFileURL } from "node:url";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "../src/plugin-sdk/facade-activation-contract.ts";
@@ -469,7 +462,6 @@ export function collectInstalledPackageErrors(params: {
errors.push(...collectInstalledBundledExtensionManifestErrors(params.packageRoot));
errors.push(...collectInstalledAlwaysAllowedRuntimeFacadeErrors(params.packageRoot));
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
errors.push(...collectInstalledPluginSdkDeclarationErrors(params.packageRoot));
errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));
@@ -613,97 +605,6 @@ export function collectInstalledContextEngineRuntimeErrors(packageRoot: string):
return errors;
}
function resolveInstalledDistRelativeImport(params: {
distRoot: string;
importerPath: string;
specifier: string;
}): string | null {
if (!params.specifier.startsWith(".")) {
return null;
}
const candidatePath = join(dirname(params.importerPath), params.specifier);
const candidatePaths = [
candidatePath,
`${candidatePath}.js`,
`${candidatePath}.mjs`,
`${candidatePath}.cjs`,
join(candidatePath, "index.js"),
join(candidatePath, "index.mjs"),
join(candidatePath, "index.cjs"),
];
for (const resolvedPath of candidatePaths) {
const relativePath = relative(params.distRoot, resolvedPath);
if (
relativePath.length === 0 ||
relativePath.startsWith("..") ||
isAbsolute(relativePath) ||
!existsSync(resolvedPath)
) {
continue;
}
return resolvedPath;
}
return null;
}
export function collectInstalledPluginSdkZodArtifactErrors(packageRoot: string): string[] {
const distRoot = join(packageRoot, "dist");
const entryRelativePath = "dist/plugin-sdk/zod.js";
const entryPath = join(packageRoot, entryRelativePath);
const pending = [entryPath];
const visited = new Set<string>();
while (pending.length > 0) {
const filePath = pending.pop();
if (!filePath || visited.has(filePath)) {
continue;
}
visited.add(filePath);
if (!existsSync(filePath)) {
return [`installed package is missing required plugin SDK artifact: ${entryRelativePath}`];
}
const relativePath = relative(packageRoot, filePath).replaceAll("\\", "/");
const fileStat = lstatSync(filePath);
if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
return [
`installed package plugin SDK artifact '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
];
}
const source = readFileSync(filePath, "utf8");
const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
if (!parsedSpecifiers.ok) {
return [
`installed package plugin SDK artifact '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
];
}
for (const specifier of parsedSpecifiers.specifiers) {
if (specifier === "zod" || specifier.startsWith("zod/")) {
return [
`installed package plugin SDK zod artifact must be self-contained but ${relativePath} imports ${specifier}.`,
];
}
const resolvedPath = resolveInstalledDistRelativeImport({
distRoot,
importerPath: filePath,
specifier,
});
if (resolvedPath) {
pending.push(resolvedPath);
}
}
}
return [];
}
function collectInstalledPluginSdkDeclarationErrors(packageRoot: string): string[] {
const pluginSdkDistRoot = join(packageRoot, "dist", "plugin-sdk");
const errors: string[] = [];
+5 -17
View File
@@ -122,7 +122,6 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
health: 0,
// +1: shipped channel setup state-migration declaration during its migration window.
"channel-entry-contract": 1,
"channel-streaming": 54,
"approval-gateway-runtime": 1,
"approval-handler-runtime": 1,
"approval-reply-runtime": 0,
@@ -141,22 +140,16 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"agent-media-payload": 3,
// +2: deprecated media projection type and builder.
"reply-payload": 2,
// +1: flushLogger projected through the deprecated text-runtime barrel.
"text-runtime": 192,
"agent-runtime": 2,
"channel-secret-runtime": 23,
// +4: session-write lease no-op compatibility stubs through the 2026.10 train.
// +4: legacy AgentHarness, attempt, embedded-run, and side-question contracts remain
// deprecated while external harnesses migrate to required-capability V2 contracts.
"agent-harness": 2,
"agent-harness-runtime": 12,
"agent-config-primitives": 2,
"command-auth": 78,
discord: 47,
matrix: 1,
// +4: deprecated media projection type, builder, and turn aliases.
"channel-inbound": 18,
"channel-logging": 4,
"channel-lifecycle": 23,
// +1: shared ingress error factory projected through the deprecated message barrel.
// +1: shared ingress retention defaults projected through the deprecated message barrel.
@@ -169,12 +162,10 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"session-store-runtime": 4,
// +2: shipped Slack and Discord setup helpers retained through their package migration window.
"setup-runtime": 2,
"group-access": 13,
"reply-history": 6,
"messaging-targets": 12,
"provider-auth": 19,
"telegram-account": 3,
zod: 282,
} satisfies Record<string, number>);
export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env) {
@@ -194,7 +185,7 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
// +1: dependency-light channel streaming config readers for doctor closures
// (realtime-voice-activation is private-local and not counted here).
// +1: registry-bound plugin command planning and exact selected execution.
152,
144,
env,
),
publicExports: readPluginSdkSurfaceBudgetEnv(
@@ -275,7 +266,7 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
// +2: narrow channel agent-run terminal reader and outcome contract.
// +5: narrow string, record, and error coercion helpers.
// +1: normalized Gateway public origin resolver for plugin-generated links.
4879,
4308,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
@@ -342,7 +333,7 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
// +1: narrow channel agent-run terminal reader.
// +5: narrow string, record, and error coercion helpers.
// +1: normalized Gateway public origin resolver for plugin-generated links.
2932,
2572,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
@@ -351,24 +342,21 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
// +2: shipped Slack and Discord setup compatibility helpers.
// +10: named media legacy projection deprecations across public compatibility barrels.
// +2: channel prompt-context type and metadata builder compatibility aliases.
// +1: flushLogger projected through the deprecated text-runtime barrel.
// +1: shared ingress error factory projected through channel-message.
// +1: shared ingress retention defaults projected through channel-message.
// +1: shipped channel setup state-migration declaration during its migration window.
// +4: session-write lease no-op compatibility stubs through the 2026.10 train.
// +7: restore still-existing deprecated inbound-dispatch compatibility re-exports.
// +6: source-compatible harness contracts retained during the V2 migration window.
1716,
1142,
env,
),
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_WILDCARD_REEXPORTS",
// -1: text-runtime now names its global-singleton exports explicitly.
// -1: infra-runtime now names its error exports explicitly.
// -1: infra-runtime excludes the internal system-event receipt API.
// -2: text-runtime names record and string coercion compatibility exports explicitly.
// -1: infra-runtime re-exports number coercion directly from its canonical owner.
75,
50,
env,
),
};
@@ -249,11 +249,6 @@ vi.mock("./dispatch-acp-media.runtime.js", async () => {
vi.mock("../../logging/diagnostic.js", () => ({
markDiagnosticSessionProgress: diagnosticMocks.markDiagnosticSessionProgress,
isStuckSessionRecoveryEnabled: (config?: { diagnostics?: { enabled?: boolean } }) =>
config?.diagnostics?.enabled !== false,
requestStuckDiagnosticSessionRecovery: vi.fn(),
resolveStuckSessionWarnMs: () => 120_000,
resolveStuckSessionAbortMs: () => 360_000,
}));
vi.mock("./dispatch-acp-transcript.runtime.js", () => ({
@@ -3,7 +3,6 @@ import { vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { TtsAutoMode } from "../../config/types.tts.js";
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import type { StuckSessionRecoveryOutcome } from "../../logging/diagnostic-session-recovery.js";
import type {
PluginHookBeforeDispatchResult,
PluginHookReplyDispatchResult,
@@ -69,13 +68,6 @@ const diagnosticMocks = vi.hoisted(() => ({
logMessageProcessed: vi.fn(),
logSessionStateChange: vi.fn(),
markDiagnosticSessionProgress: vi.fn(),
requestStuckDiagnosticSessionRecovery: vi.fn<() => Promise<StuckSessionRecoveryOutcome>>(
async () => ({
status: "skipped" as const,
action: "keep_lane" as const,
reason: "active_reply_work" as const,
}),
),
}));
const messageAuditMocks = vi.hoisted(() => ({
enabled: true,
@@ -500,12 +492,6 @@ vi.mock("../../logging/diagnostic.js", () => ({
logSessionStateChange: diagnosticMocks.logSessionStateChange,
logSessionTurnCreated: vi.fn(),
markDiagnosticSessionProgress: diagnosticMocks.markDiagnosticSessionProgress,
isStuckSessionRecoveryEnabled: (config?: { diagnostics?: { enabled?: boolean } }) =>
config?.diagnostics?.enabled !== false,
requestStuckDiagnosticSessionRecovery: diagnosticMocks.requestStuckDiagnosticSessionRecovery,
resolveStuckSessionWarnMs: () => 120_000,
resolveStuckSessionAbortMs: (stuckSessionWarnMs: number) =>
Math.max(300_000, stuckSessionWarnMs * 3),
}));
vi.mock("../../audit/message-audit-events.js", () => ({
emitTrustedMessageAuditEvent: messageAuditMocks.emitTrustedMessageAuditEvent,
@@ -4,7 +4,6 @@ import { RUN_STALE_TAKEOVER_MS } from "../../logging/diagnostic-run-activity.js"
import type { ReplyPayload } from "../types.js";
import {
createDispatcher,
diagnosticMocks,
mocks,
noAbortResult,
resetPluginTtsAndThreadMocks,
@@ -67,7 +66,6 @@ describe("dispatchReplyFromConfig stale visible admission recovery", () => {
mocks.routeReply.mockResolvedValue({ ok: true, delivered: true, messageId: "mock" });
mocks.tryFastAbortFromMessage.mockReset();
setNoAbort();
diagnosticMocks.requestStuckDiagnosticSessionRecovery.mockReset();
});
afterEach(() => {
@@ -107,7 +105,6 @@ describe("dispatchReplyFromConfig stale visible admission recovery", () => {
expect(settled).toBe(false);
expect(waitChanges).toEqual([true]);
expect(replyResolver).not.toHaveBeenCalled();
expect(diagnosticMocks.requestStuckDiagnosticSessionRecovery).not.toHaveBeenCalled();
activeOperation.complete();
const result = await resultPromise;
@@ -143,7 +140,6 @@ describe("dispatchReplyFromConfig stale visible admission recovery", () => {
await vi.advanceTimersByTimeAsync(REPLY_RUN_TERMINAL_SETTLE_TIMEOUT_MS);
const result = await resultPromise;
expect(diagnosticMocks.requestStuckDiagnosticSessionRecovery).not.toHaveBeenCalled();
expect(activeOperation.result).toEqual({ kind: "failed", code: "run_stalled" });
expect(result).toMatchObject({
queuedFinal: true,
@@ -4,7 +4,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ReplyPayload } from "../types.js";
import {
createDispatcher,
diagnosticMocks,
mocks,
noAbortResult,
resetPluginTtsAndThreadMocks,
@@ -56,7 +55,6 @@ describe("dispatchReplyFromConfig terminal visible admission recovery", () => {
mocks.routeReply.mockResolvedValue({ ok: true, delivered: true, messageId: "mock" });
mocks.tryFastAbortFromMessage.mockReset();
mocks.tryFastAbortFromMessage.mockResolvedValue(noAbortResult);
diagnosticMocks.requestStuckDiagnosticSessionRecovery.mockReset();
sessionStoreMocks.currentEntry = undefined;
sessionStoreMocks.entriesBySessionKey.clear();
});
@@ -88,7 +86,6 @@ describe("dispatchReplyFromConfig terminal visible admission recovery", () => {
const result = await dispatchReplyFromConfig(dispatchParams);
expect(diagnosticMocks.requestStuckDiagnosticSessionRecovery).not.toHaveBeenCalled();
expect(activeOperation.result).toMatchObject({
kind: "failed",
code: "run_failed",
@@ -501,12 +501,6 @@ export const describe0BeforeEach0 = () => {
diagnosticMocks.logMessageProcessed.mockClear();
diagnosticMocks.logSessionStateChange.mockClear();
diagnosticMocks.markDiagnosticSessionProgress.mockClear();
diagnosticMocks.requestStuckDiagnosticSessionRecovery.mockReset();
diagnosticMocks.requestStuckDiagnosticSessionRecovery.mockResolvedValue({
status: "skipped",
action: "keep_lane",
reason: "active_reply_work",
});
diagnosticMocks.logMessageDispatchStarted.mockClear();
diagnosticMocks.logMessageDispatchCompleted.mockClear();
hookMocks.runner.hasHooks.mockClear();
@@ -19,7 +19,8 @@ function createTestProgressDraftCompositor(
...overrides,
});
}
import { DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS } from "./streaming.js";
const DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS = 1_500;
describe("createChannelProgressDraftCompositor", () => {
it("tracks compact per-turn progress receipts", () => {
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createChannelProgressDraftCompositor } from "./progress-draft-compositor.js";
import { DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS } from "./streaming.js";
const DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS = 1_500;
function createProgress(update: () => Promise<boolean | void> | boolean | void) {
return createChannelProgressDraftCompositor({
@@ -5,8 +5,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
buildChannelProgressDraftLine,
createChannelProgressDraftGate,
DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS,
DEFAULT_PROGRESS_DRAFT_LABELS,
formatChannelProgressDraftLine,
formatChannelProgressDraftLineForEntry,
formatChannelProgressDraftText,
@@ -16,7 +14,6 @@ import {
mergeChannelProgressDraftLine,
resolveChannelPreviewStreamMode,
resolveChannelProgressDraftMaxLineChars,
resolveChannelProgressDraftLabel,
resolveChannelProgressDraftMaxLines,
resolveChannelProgressDraftRender,
resolveChannelStreamingBlockCoalesce,
@@ -30,7 +27,9 @@ import {
resolveChannelStreamingPreviewToolProgress,
resolveTranscriptBackedChannelFinalText,
selectLongerFinalText,
} from "./channel-streaming.js";
} from "./streaming.js";
const DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS = 1_500;
describe("channel-streaming", () => {
afterEach(() => {
@@ -216,22 +215,6 @@ describe("channel-streaming", () => {
).toBe(false);
});
it("uses auto progress labels when no explicit label is configured", () => {
expect(DEFAULT_PROGRESS_DRAFT_LABELS).toEqual(["Working"]);
expect(resolveChannelProgressDraftLabel({ random: () => 0 })).toBe(
DEFAULT_PROGRESS_DRAFT_LABELS[0],
);
expect(resolveChannelProgressDraftLabel({ random: () => 0.99 })).toBe(
DEFAULT_PROGRESS_DRAFT_LABELS.at(-1),
);
expect(
resolveChannelProgressDraftLabel({
entry: { streaming: { progress: { label: " AUTO " } } },
random: () => 0,
}),
).toBe(DEFAULT_PROGRESS_DRAFT_LABELS[0]);
});
it("separates progress labels from detail lines with a blank line", () => {
const entry = { streaming: { progress: { label: "Working" } } };
@@ -243,23 +226,22 @@ describe("channel-streaming", () => {
).toBe("Working\n\n🛠️ pgrep -fl Discord || true (agent)\n• Discord is installed.");
});
it("supports explicit progress labels and custom label sets", () => {
it("renders automatic and configured progress labels through the public formatter", () => {
expect(formatChannelProgressDraftText({ lines: [], random: () => 0 })).toBe("Working");
expect(
resolveChannelProgressDraftLabel({
entry: { streaming: { progress: { label: "Crunching" } } },
formatChannelProgressDraftText({
entry: { streaming: { progress: { label: " AUTO " } } },
lines: [],
random: () => 0,
}),
).toBe("Crunching");
).toBe("Working");
expect(
resolveChannelProgressDraftLabel({
formatChannelProgressDraftText({
entry: { streaming: { progress: { labels: ["Pearling"] } } },
lines: [],
random: () => 0.5,
}),
).toBe("Pearling");
expect(
resolveChannelProgressDraftLabel({
entry: { streaming: { progress: { label: false } } },
}),
).toBeUndefined();
});
it("formats bounded progress draft text", () => {
+7 -12
View File
@@ -35,10 +35,7 @@ export type {
ChannelDeliveryStreamingConfig,
ChannelPreviewStreamingConfig,
ChannelStreamingBlockConfig,
ChannelStreamingCommandTextMode,
ChannelStreamingConfig,
ChannelStreamingProgressConfig,
ChannelStreamingPreviewConfig,
StreamingMode,
TextChunkMode,
} from "../config/types.base.js";
@@ -78,12 +75,10 @@ function asCommandTextMode(value: unknown): ChannelStreamingCommandTextMode | un
return value === "raw" || value === "status" ? value : undefined;
}
export { DEFAULT_PROGRESS_DRAFT_LABELS } from "../shared/progress-labels.js";
// Short enough that a multi-tool turn is never silent, long enough that a
// quick answer posts no draft at all: the gate only creates the draft when the
// timer fires, and finalize cancels it.
export const DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS = 1_500;
const DEFAULT_PROGRESS_DRAFT_INITIAL_DELAY_MS = 1_500;
const DEFAULT_PROGRESS_DRAFT_MAX_LINE_CHARS = 120;
// Narration is a short paragraph, not a compact tool line; it gets its own
// budget so the utility-model text is not mid-word truncated at line width.
@@ -173,7 +168,7 @@ export type ChannelProgressLineOptions = {
commandText?: ChannelStreamingCommandTextMode;
};
export type ChannelProgressDraftRenderMode = "text" | "rich";
type ChannelProgressDraftRenderMode = "text" | "rich";
export type AgentPlanStepStatus = "pending" | "in_progress" | "completed";
@@ -182,7 +177,7 @@ export type AgentPlanStep = {
status: AgentPlanStepStatus;
};
export type AgentPlanStepInput = AgentPlanStep | string;
type AgentPlanStepInput = AgentPlanStep | string;
function isAgentPlanStepStatus(value: unknown): value is AgentPlanStepStatus {
return value === "pending" || value === "in_progress" || value === "completed";
@@ -276,7 +271,7 @@ export type ChannelProgressDraftLineInput =
summary?: string;
};
export type ChannelProgressDraftLineKind = ChannelProgressDraftLineInput["event"];
type ChannelProgressDraftLineKind = ChannelProgressDraftLineInput["event"];
export type ChannelProgressDraftLine = {
/** Stable line id used to update an existing progress line in place. */
@@ -379,7 +374,7 @@ function itemKindToToolName(kind: string | undefined): string | undefined {
}
/** Tools whose detail is raw command text; commandText policy applies to these. */
export function isCommandToolName(name: string | undefined): boolean {
function isCommandToolName(name: string | undefined): boolean {
return isCommandBearingToolCall(name);
}
@@ -484,7 +479,7 @@ export function formatChannelProgressDraftLine(
return buildChannelProgressDraftLine(input, options)?.text;
}
export function resolveChannelProgressDraftLineOptions(
function resolveChannelProgressDraftLineOptions(
/** Channel streaming config source for command-text defaults. */
entry: StreamingCompatEntry | null | undefined,
/** Caller-supplied line formatting overrides. */
@@ -927,7 +922,7 @@ function normalizeProgressLabels(labels: unknown): string[] {
return normalized;
}
export function resolveChannelProgressDraftLabel(params: {
function resolveChannelProgressDraftLabel(params: {
entry?: StreamingCompatEntry | null;
seed?: string;
random?: () => number;
+3
View File
@@ -1,4 +1,5 @@
// Channels add tests cover guided setup, plugin install paths, and channel account config writes.
import path from "node:path";
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getBundledChannelSetupPlugin } from "../channels/plugins/bundled.js";
@@ -372,6 +373,8 @@ function registerExternalChatSetupPlugin(pluginId = "@vendor/external-chat-plugi
}
async function registerBundledSetupPlugin(channelId: string): Promise<void> {
// Exercise the checked-in declarations, not a stale local dist tree left by an earlier build.
vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.resolve("extensions"));
const actual = await vi.importActual<typeof import("../channels/plugins/bundled.js")>(
"../channels/plugins/bundled.js",
);
@@ -5,7 +5,7 @@ import {
onInternalDiagnosticEvent,
resetDiagnosticEventsForTest,
} from "../../infra/diagnostic-events.js";
import { resetDiagnosticStateForTest } from "../../logging/diagnostic.js";
import { resetDiagnosticStateForTest } from "../../logging/diagnostic.test-support.js";
vi.mock("../../agents/auth-profiles/source-check.js", () => ({
hasAnyAuthProfileStoreSource: vi.fn(() => false),
+2 -3
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { theme } from "../packages/terminal-core/src/theme.js";
import { isVerbose, isYes, logVerbose, setVerbose, setYes } from "./globals.js";
import { logDebug, logError, logInfo, logSuccess, logWarn } from "./logger.js";
import { logDebug, logError, logInfo, logWarn } from "./logger.js";
import {
resetLogger,
setLoggerOverride,
@@ -29,10 +29,9 @@ describe("logger helpers", () => {
logInfo("info", runtime);
logWarn("warn", runtime);
logSuccess("ok", runtime);
logError("bad", runtime);
expect(log).toHaveBeenCalledTimes(3);
expect(log).toHaveBeenCalledTimes(2);
expect(error).toHaveBeenCalledTimes(1);
});
-12
View File
@@ -47,7 +47,6 @@ function logWithSubsystem(params: {
const info = theme.info;
const warn = theme.warn;
const success = theme.success;
const danger = theme.error;
export function logInfo(message: string, runtime: RuntimeEnv = defaultRuntime) {
@@ -72,17 +71,6 @@ export function logWarn(message: string, runtime: RuntimeEnv = defaultRuntime) {
});
}
export function logSuccess(message: string, runtime: RuntimeEnv = defaultRuntime) {
logWithSubsystem({
message,
runtime,
runtimeMethod: "log",
runtimeFormatter: success,
loggerMethod: "info",
subsystemMethod: "info",
});
}
export function logError(message: string, runtime: RuntimeEnv = defaultRuntime) {
logWithSubsystem({
message,
-7
View File
@@ -1,7 +1,6 @@
// Logging config helpers read and normalize logger configuration.
import fs from "node:fs";
import { isRecord as isObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { getCommandPathWithRootOptions } from "../cli/argv.js";
import { resolveConfigEnvVars } from "../config/env-substitution.js";
import { resolveConfigIncludes, resolveConfigIncludesForTopLevelKey } from "../config/includes.js";
import { resolveConfigPath, resolveIncludeRoots } from "../config/paths.js";
@@ -55,12 +54,6 @@ function resolvePartialDiagnosticLoggingConfig(logging: unknown): LoggingConfig
return Object.keys(partial).length > 0 ? (partial as LoggingConfig) : undefined;
}
/** Avoids config reads that can mutate or validate config while schema/config commands run. */
export function shouldSkipMutatingLoggingConfigRead(argv: string[] = process.argv): boolean {
const [primary, secondary] = getCommandPathWithRootOptions(argv, 2);
return primary === "config" && (secondary === "schema" || secondary === "validate");
}
/** Reads the logging block from config, caching by resolved config path. */
export function readLoggingConfig(): LoggingConfig | undefined {
try {
+1 -1
View File
@@ -14,7 +14,7 @@ import {
import { defaultRuntime } from "../runtime.js";
import { withEnv } from "../test-utils/env.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { testApi } from "./logger.js";
import { testApi } from "./logger.test-support.js";
import { loggingState } from "./state.js";
import {
captureConsoleSnapshot,
@@ -173,7 +173,7 @@ function applyRecoveryOutcomeToDiagnosticState(params: {
markActivity();
}
export function requestStuckSessionRecoveryOutcome(
function requestStuckSessionRecoveryOutcome(
params: RequestStuckSessionRecoveryParams,
): Promise<StuckSessionRecoveryOutcome | undefined> {
const inFlightKey = recoveryRequestKey(params.request);
@@ -26,11 +26,8 @@ import {
} from "./diagnostic-run-activity.js";
import { markDiagnosticModelStartedForTest } from "./diagnostic-run-activity.test-support.js";
import { recoverStuckDiagnosticSession } from "./diagnostic-stuck-session-recovery.runtime.js";
import {
logSessionStateChange,
resetDiagnosticStateForTest,
startDiagnosticHeartbeat,
} from "./diagnostic.js";
import { logSessionStateChange, startDiagnosticHeartbeat } from "./diagnostic.js";
import { resetDiagnosticStateForTest } from "./diagnostic.test-support.js";
async function expectPendingAfterEventLoopTurn(promise: Promise<unknown>): Promise<void> {
let settled = false;
+25
View File
@@ -0,0 +1,25 @@
import "./diagnostic.js";
type DiagnosticTestApi = {
resetDiagnosticStateForTest(): void;
resolveStuckSessionAbortMs(stuckSessionWarnMs: number): number;
resolveStuckSessionWarnMs(): number;
};
function getTestApi(): DiagnosticTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.diagnosticTestApi")
] as DiagnosticTestApi;
}
export function resetDiagnosticStateForTest(): void {
getTestApi().resetDiagnosticStateForTest();
}
export function resolveStuckSessionAbortMs(stuckSessionWarnMs: number): number {
return getTestApi().resolveStuckSessionAbortMs(stuckSessionWarnMs);
}
export function resolveStuckSessionWarnMs(): number {
return getTestApi().resolveStuckSessionWarnMs();
}
+4 -2
View File
@@ -51,11 +51,13 @@ import {
logMessageQueued,
logSessionStateChange,
markDiagnosticSessionProgress,
startDiagnosticHeartbeat as startDiagnosticHeartbeatImpl,
} from "./diagnostic.js";
import {
resetDiagnosticStateForTest,
resolveStuckSessionAbortMs,
resolveStuckSessionWarnMs,
startDiagnosticHeartbeat as startDiagnosticHeartbeatImpl,
} from "./diagnostic.js";
} from "./diagnostic.test-support.js";
function startDiagnosticHeartbeat(
config?: Parameters<typeof startDiagnosticHeartbeatImpl>[0],
+15 -80
View File
@@ -44,7 +44,6 @@ import {
} from "./diagnostic-session-context.js";
import {
requestStuckSessionRecovery,
requestStuckSessionRecoveryOutcome,
resetDiagnosticSessionRecoveryCoordinatorForTest,
type RecoverStuckSession,
} from "./diagnostic-session-recovery-coordinator.js";
@@ -71,7 +70,7 @@ import {
stopDiagnosticStabilityRecorder,
} from "./diagnostic-stability.js";
export { diagnosticLogger, logLaneDequeue, logLaneEnqueue } from "./diagnostic-runtime.js";
export { diagnosticLogger } from "./diagnostic-runtime.js";
const webhookStats = {
received: 0,
@@ -179,36 +178,6 @@ async function recoverStuckSession(
});
}
/**
* @deprecated Unused by core since the dispatch-side recovery loop was removed
* (#101910); reply admission owns stale-run reclaim now. Kept only because the
* plugin SDK re-exports this module; scheduled for removal in the next SDK major.
*/
export function isStuckSessionRecoveryEnabled(config?: OpenClawConfig): boolean {
return areDiagnosticsEnabledForProcess() && isDiagnosticsEnabled(config);
}
/**
* @deprecated Unused by core since the dispatch-side recovery loop was removed
* (#101910); reply admission owns stale-run reclaim now. Kept only because the
* plugin SDK re-exports this module; scheduled for removal in the next SDK major.
*/
export async function requestStuckDiagnosticSessionRecovery(
params: StuckSessionRecoveryRequest,
): Promise<StuckSessionRecoveryOutcome | undefined> {
return requestStuckSessionRecoveryOutcome({
recover: recoverStuckSession,
classification: {
eventType: "session.stalled",
reason: "visible_reply_wait_timeout",
classification: "stalled_agent_run",
activeWorkKind: "embedded_run",
recoveryEligible: false,
},
request: params,
});
}
function formatDiagnosticWorkLabel(
state: {
sessionId?: string;
@@ -499,11 +468,11 @@ function formatDiagnosticWorkLabels(work: DiagnosticWorkSnapshot): string {
return parts.join(" ");
}
export function resolveStuckSessionWarnMs(): number {
function resolveStuckSessionWarnMs(): number {
return DEFAULT_STUCK_SESSION_WARN_MS;
}
export function resolveStuckSessionAbortMs(stuckSessionWarnMs: number): number {
function resolveStuckSessionAbortMs(stuckSessionWarnMs: number): number {
return resolveStalledEmbeddedRunAbortMs(stuckSessionWarnMs);
}
@@ -916,15 +885,6 @@ export function logSessionStateChange(
markActivity();
}
export function updateDiagnosticSessionFile(params: SessionRef) {
if (!areDiagnosticsEnabledForProcess()) {
return;
}
const state = getDiagnosticSessionState(params);
state.sessionFile = params.sessionFile?.trim() || undefined;
markActivity();
}
export function markDiagnosticSessionProgress(params: SessionRef) {
if (!areDiagnosticsEnabledForProcess()) {
return;
@@ -996,7 +956,7 @@ function formatSessionActivityLogFields(activity: DiagnosticSessionActivitySnaps
return fields.join(" ");
}
export function logSessionAttention(
function logSessionAttention(
params: SessionRef & {
state: SessionStateValue;
ageMs: number;
@@ -1127,25 +1087,6 @@ export function logSessionAttention(
return classification;
}
export function logRunAttempt(params: SessionRef & { runId: string; attempt: number }) {
if (!areDiagnosticsEnabledForProcess()) {
return;
}
diag.debug(
`run attempt: sessionId=${params.sessionId ?? "unknown"} sessionKey=${
params.sessionKey ?? "unknown"
} runId=${params.runId} attempt=${params.attempt}`,
);
emitDiagnosticEvent({
type: "run.attempt",
sessionId: params.sessionId,
sessionKey: params.sessionKey,
runId: params.runId,
attempt: params.attempt,
});
markActivity();
}
export function logToolLoopAction(
params: SessionRef & {
toolName: string;
@@ -1191,18 +1132,6 @@ export function logToolLoopAction(
markActivity();
}
export function logActiveRuns() {
if (!areDiagnosticsEnabledForProcess()) {
return;
}
const now = Date.now();
const activeSessions = Array.from(diagnosticSessionStates.entries())
.filter(([, s]) => s.state === "processing")
.map(([id, s]) => `${id}(q=${s.queueDepth},age=${Math.round((now - s.lastActivity) / 1000)}s)`);
diag.debug(`active runs: count=${activeSessions.length} sessions=[${activeSessions.join(", ")}]`);
markActivity();
}
let heartbeatInterval: NodeJS.Timeout | null = null;
let lastDiagnosticHeartbeatTickAt: number | undefined;
@@ -1391,11 +1320,7 @@ export function stopDiagnosticHeartbeat() {
uninstallDiagnosticStabilityFatalHook();
}
export function getDiagnosticSessionStateCountForTest(): number {
return diagnosticSessionStates.size;
}
export function resetDiagnosticStateForTest(): void {
function resetDiagnosticStateForTest(): void {
stopDiagnosticHeartbeat();
resetDiagnosticSessionRecoveryCoordinatorForTest();
resetDiagnosticSessionStateForTest();
@@ -1410,4 +1335,14 @@ export function resetDiagnosticStateForTest(): void {
resetDiagnosticStabilityRecorderForTest();
resetDiagnosticStabilityBundleForTest();
}
const testing = {
resetDiagnosticStateForTest,
resolveStuckSessionAbortMs,
resolveStuckSessionWarnMs,
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.diagnosticTestApi")] = testing;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
-1
View File
@@ -7,7 +7,6 @@ const { readLoggingConfigMock } = vi.hoisted(() => ({
vi.mock("./config.js", () => ({
readLoggingConfig: readLoggingConfigMock,
shouldSkipMutatingLoggingConfigRead: () => false,
}));
let logging: typeof import("../logging.js");
+1 -1
View File
@@ -9,7 +9,7 @@ import {
setLoggerOverride,
} from "../logging.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { testApi } from "./logger.js";
import { testApi } from "./logger.test-support.js";
const DEFAULT_MAX_FILE_BYTES = 100 * 1024 * 1024;
const logPathTracker = createSuiteLogPathTracker("openclaw-log-cap-");
+2 -1
View File
@@ -3,7 +3,8 @@ import fs from "node:fs";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { appendRegularFile } from "../infra/regular-file.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { getLogger, resetLogger, setLoggerOverride, testApi } from "./logger.js";
import { getLogger, resetLogger, setLoggerOverride } from "./logger.js";
import { testApi } from "./logger.test-support.js";
const logPathTracker = createSuiteLogPathTracker("openclaw-file-transport-");
+15 -6
View File
@@ -283,7 +283,7 @@ if (process.env.VITEST !== "true") {
}
/** Enqueues one serialized record without waiting for filesystem I/O. */
export function enqueueFileLog(entry: FileLogQueueEntry): void {
function enqueueFileLog(entry: FileLogQueueEntry): void {
if (processExiting) {
writeEntriesSync([entry]);
return;
@@ -305,7 +305,7 @@ export function enqueueFileLog(entry: FileLogQueueEntry): void {
}
/** Waits until every record currently queued for the async transport has settled. */
export async function flushFileLogQueue(): Promise<void> {
async function flushFileLogQueue(): Promise<void> {
for (;;) {
if (scheduledFlush) {
clearImmediate(scheduledFlush);
@@ -323,7 +323,7 @@ export async function flushFileLogQueue(): Promise<void> {
}
/** Synchronously rescues pending records for process.exit() and crash-adjacent paths. */
export function drainFileLogQueueSync(): void {
function drainFileLogQueueSync(): void {
if (scheduledFlush) {
clearImmediate(scheduledFlush);
scheduledFlush = null;
@@ -339,15 +339,15 @@ export function drainFileLogQueueSync(): void {
writeEntriesSync(entries);
}
export function setFileLogQueueMaxRecordsForTests(value?: number): void {
function setFileLogQueueMaxRecordsForTests(value?: number): void {
maxQueuedRecords = Math.max(1, value ?? DEFAULT_MAX_QUEUED_RECORDS);
}
export function setFileLogAppenderForTests(value?: FileLogAppender): void {
function setFileLogAppenderForTests(value?: FileLogAppender): void {
appendFile = value ?? appendRegularFile;
}
export function resetFileLogTransportForTests(): void {
function resetFileLogTransportForTests(): void {
drainFileLogQueueSync();
removeProcessHooks();
processExiting = false;
@@ -355,3 +355,12 @@ export function resetFileLogTransportForTests(): void {
maxQueuedRecords = DEFAULT_MAX_QUEUED_RECORDS;
warnedRotationFiles.clear();
}
export const fileLogTransport = {
drainSync: drainFileLogQueueSync,
enqueue: enqueueFileLog,
flush: flushFileLogQueue,
resetForTests: resetFileLogTransportForTests,
setAppenderForTests: setFileLogAppenderForTests,
setMaxQueuedRecordsForTests: setFileLogQueueMaxRecordsForTests,
};
+13
View File
@@ -0,0 +1,13 @@
import os from "node:os";
type LoggerHostnameResolver = () => string;
export const defaultLoggerHostnameResolver: LoggerHostnameResolver = () => os.hostname();
export const loggerHostnameState: {
cached: string | null;
resolver: LoggerHostnameResolver;
} = {
cached: null,
resolver: defaultLoggerHostnameResolver,
};
@@ -10,7 +10,7 @@ import {
import { getChildLogger, getLogger, resetLogger, setLoggerOverride } from "../logging.js";
import { withEnv } from "../test-utils/env.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { testApi as loggerTest } from "./logger.js";
import { testApi as loggerTest } from "./logger.test-support.js";
import { createDiagnosticLogRecordCapture } from "./test-helpers/diagnostic-log-capture.js";
const secret = "sk-testsecret1234567890abcd";
+1 -1
View File
@@ -4,7 +4,7 @@ import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { getLogger, resetLogger, setLoggerOverride } from "../logging.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { testApi } from "./logger.js";
import { testApi } from "./logger.test-support.js";
const logPathTracker = createSuiteLogPathTracker("openclaw-log-ts-");
+1 -3
View File
@@ -44,9 +44,7 @@ describe("logger transport registry", () => {
expect(
(loggerModule as unknown as Record<string, unknown>).registerLogTransport,
).toBeUndefined();
expect(
(loggerModule.testApi as unknown as Record<string, unknown>).registerLogTransportForTest,
).toBeUndefined();
expect((loggerModule as unknown as Record<string, unknown>).testApi).toBeUndefined();
});
it("does not publish mutable log transport state on a well-known global symbol", async () => {
-36
View File
@@ -1,36 +0,0 @@
// Logger settings tests cover normalization of logger configuration values.
import { describe, expect, it } from "vitest";
import { testApi } from "./logger.js";
describe("shouldSkipMutatingLoggingConfigRead", () => {
it("matches config schema and validate invocations", () => {
expect(
testApi.shouldSkipMutatingLoggingConfigRead(["node", "openclaw", "config", "schema"]),
).toBe(true);
expect(
testApi.shouldSkipMutatingLoggingConfigRead(["node", "openclaw", "config", "validate"]),
).toBe(true);
});
it("handles root flags before config validate", () => {
expect(
testApi.shouldSkipMutatingLoggingConfigRead([
"node",
"openclaw",
"--profile",
"work",
"--no-color",
"config",
"validate",
"--json",
]),
).toBe(true);
});
it("does not match other commands", () => {
expect(
testApi.shouldSkipMutatingLoggingConfigRead(["node", "openclaw", "config", "get", "foo"]),
).toBe(false);
expect(testApi.shouldSkipMutatingLoggingConfigRead(["node", "openclaw", "status"])).toBe(false);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { expandHomePrefix } from "../infra/home-dir.js";
import { isLegacyRollingLogFilePath, resolveRollingLogFilePathForDate } from "./log-file-path.js";
import { fileLogTransport } from "./logger-file-transport.js";
import { defaultLoggerHostnameResolver, loggerHostnameState } from "./logger-hostname-state.js";
export const testApi = {
drainFileLogQueueSyncForTests: fileLogTransport.drainSync,
flushFileLogQueueForTests: fileLogTransport.flush,
resetFileLogTransportForTests: fileLogTransport.resetForTests,
resolveActiveLogFile(file: string): string {
const expandedFile = expandHomePrefix(file);
return isLegacyRollingLogFilePath(expandedFile)
? resolveRollingLogFilePathForDate(expandedFile, new Date())
: expandedFile;
},
setFileLogAppenderForTests: fileLogTransport.setAppenderForTests,
setFileLogQueueMaxRecordsForTests: fileLogTransport.setMaxQueuedRecordsForTests,
setHostnameResolverForTests(resolver?: () => string): void {
loggerHostnameState.resolver = resolver ?? defaultLoggerHostnameResolver;
loggerHostnameState.cached = null;
},
};
+11 -41
View File
@@ -1,6 +1,5 @@
// Logger implementation writes structured log output with redaction and transports.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
@@ -23,20 +22,14 @@ import {
DEFAULT_POSIX_TMP_ROOT,
resolvePreferredOpenClawTmpDir,
} from "../infra/tmp-openclaw-dir.js";
import { readLoggingConfig, shouldSkipMutatingLoggingConfigRead } from "./config.js";
import { readLoggingConfig } from "./config.js";
import { resolveEnvLogLevelOverride } from "./env-log-level.js";
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
import { isLegacyRollingLogFilePath, resolveRollingLogFilePathForDate } from "./log-file-path.js";
import { resolveDefaultRollingLogFile } from "./log-file-path.js";
import { canUseNodeFs, formatLocalDate, LOG_PREFIX, LOG_SUFFIX } from "./log-file-shared.js";
import {
drainFileLogQueueSync,
enqueueFileLog,
flushFileLogQueue,
resetFileLogTransportForTests,
setFileLogAppenderForTests,
setFileLogQueueMaxRecordsForTests,
} from "./logger-file-transport.js";
import { fileLogTransport } from "./logger-file-transport.js";
import { defaultLoggerHostnameResolver, loggerHostnameState } from "./logger-hostname-state.js";
import { setLoggerFileTargetResolver } from "./logger-settings-internal.js";
import { redactSecrets, redactSensitiveText } from "./redact.js";
import { loggingState } from "./state.js";
@@ -71,7 +64,6 @@ type ResolvedRuntimeSettings = ResolvedSettings & { rolling: boolean };
export type LoggerResolvedSettings = ResolvedSettings;
type TsLogRecord = Record<string, unknown>;
type LoggerConfigLoader = () => OpenClawConfig["logging"] | undefined;
type HostnameResolver = () => string;
type DiagnosticLogCode = {
line?: number;
@@ -95,9 +87,6 @@ const MAX_DIAGNOSTIC_LOG_NAME_CHARS = 120;
const MAX_FILE_LOG_MESSAGE_CHARS = 4 * 1024;
const MAX_FILE_LOG_CONTEXT_VALUE_CHARS = 512;
const DIAGNOSTIC_LOG_ATTRIBUTE_KEY_RE = /^[A-Za-z0-9_.:-]{1,64}$/u;
const defaultHostnameResolver: HostnameResolver = () => os.hostname();
let hostnameResolver: HostnameResolver = defaultHostnameResolver;
let cachedHostname: string | null = null;
type DiagnosticLogAttributes = Record<string, string | number | boolean>;
@@ -298,14 +287,14 @@ function buildFileLogMessage(numericArgs: readonly unknown[]): string | undefine
}
function resolveLogHostname(): string {
if (cachedHostname) {
return cachedHostname;
if (loggerHostnameState.cached) {
return loggerHostnameState.cached;
}
const hostname = hostnameResolver().trim();
const hostname = loggerHostnameState.resolver().trim();
if (!hostname) {
return "unknown";
}
cachedHostname = hostname;
loggerHostnameState.cached = hostname;
return hostname;
}
@@ -636,7 +625,7 @@ function buildLogger(settings: ResolvedRuntimeSettings): TsLogger<LogObj> {
...traceFields,
};
const line = redactSensitiveText(JSON.stringify(redactLogRecordForTransport(record)));
enqueueFileLog({
fileLogTransport.enqueue({
file: activeFile,
hostname: expectDefined(structuredFields.hostname, "structured log hostname"),
maxFileBytes: settings.maxFileBytes,
@@ -746,7 +735,7 @@ export function getResolvedLoggerSettings(): LoggerResolvedSettings {
/** Flushes queued file logs before a graceful owner exits the process. */
export async function flushLogger(): Promise<void> {
await flushFileLogQueue();
await fileLogTransport.flush();
}
// Test helpers
@@ -763,27 +752,8 @@ export function resetLogger() {
loggingState.cachedConsoleSettings = null;
loggingState.overrideSettings = null;
loadLoggerConfig = loadLoggerConfigDefault;
hostnameResolver = defaultHostnameResolver;
cachedHostname = null;
}
export const testApi = {
drainFileLogQueueSyncForTests: drainFileLogQueueSync,
flushFileLogQueueForTests: flushFileLogQueue,
resetFileLogTransportForTests,
resolveActiveLogFile,
setFileLogAppenderForTests,
setFileLogQueueMaxRecordsForTests,
setHostnameResolverForTests: (resolver?: HostnameResolver) => {
hostnameResolver = resolver ?? defaultHostnameResolver;
cachedHostname = null;
},
shouldSkipMutatingLoggingConfigRead,
};
export { testApi as __test__ };
function resolveActiveLogFile(file: string): string {
return resolveActiveLogFileWithMode(file, isLegacyRollingLogFilePath(file));
loggerHostnameState.resolver = defaultLoggerHostnameResolver;
loggerHostnameState.cached = null;
}
function resolveActiveLogFileWithMode(file: string, rolling: boolean): string {
+4 -4
View File
@@ -25,8 +25,8 @@ import {
} from "./redact-patterns.js";
import { redactRegisteredSecretValues } from "./secret-redaction-registry.js";
export type RedactSensitiveMode = "off" | "tools";
export type RedactPattern = string | RegExp;
type RedactSensitiveMode = "off" | "tools";
type RedactPattern = string | RegExp;
type LoggingConfig = OpenClawConfig["logging"];
const DEFAULT_REDACT_MODE: RedactSensitiveMode = "tools";
@@ -127,12 +127,12 @@ const DEFAULT_REDACT_PREFILTER_RE = new RegExp(
"iu",
);
export type RedactOptions = {
type RedactOptions = {
mode?: RedactSensitiveMode;
patterns?: RedactPattern[];
};
export type ResolvedRedactOptions = {
type ResolvedRedactOptions = {
mode: RedactSensitiveMode;
patterns: RegExp[];
redactFormBodies: boolean;
+2 -1
View File
@@ -4,7 +4,8 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { setConsoleSubsystemFilter, shouldLogSubsystemToConsole } from "./console.js";
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
import { resetLogger, setLoggerOverride, testApi } from "./logger.js";
import { resetLogger, setLoggerOverride } from "./logger.js";
import { testApi } from "./logger.test-support.js";
import { loggingState } from "./state.js";
import { createSubsystemLogger } from "./subsystem.js";
@@ -1,6 +0,0 @@
/**
* @deprecated Public SDK subpath has no bundled extension production imports.
* Import the needed schema primitives from a maintained plugin-owned surface.
*/
export { ReplyRuntimeConfigSchemaShape } from "../config/zod-schema.core.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
-7
View File
@@ -1,7 +0,0 @@
/** @deprecated Compatibility subpath. Use `channel-inbound` or `channel-outbound`. */
export {
type LogFn,
logAckFailure,
logInboundDrop,
logTypingFailure,
} from "../channels/logging.js";
+103
View File
@@ -10,9 +10,112 @@ import {
coerceNativeSetting,
createDangerousNameMatchingMutableAllowlistWarningCollector,
createRestrictSendersChannelSecurity,
evaluateGroupRouteAccessForPolicy,
evaluateSenderGroupAccessForPolicy,
normalizeAllowFromList,
resolveSenderScopedGroupPolicy,
} from "./channel-policy.js";
describe("retained group policy helpers", () => {
it.each([
{
name: "preserves disabled policy",
input: { groupPolicy: "disabled" as const, groupAllowFrom: ["a"] },
expected: "disabled",
},
{
name: "keeps allowlist policy when sender allowlist is present",
input: { groupPolicy: "allowlist" as const, groupAllowFrom: ["a"] },
expected: "allowlist",
},
{
name: "maps allowlist to open when sender allowlist is empty",
input: { groupPolicy: "allowlist" as const, groupAllowFrom: [] },
expected: "open",
},
])("$name", ({ input, expected }) => {
expect(resolveSenderScopedGroupPolicy(input)).toBe(expected);
});
it.each([
{
name: "blocks disabled sender policy",
input: {
groupPolicy: "disabled" as const,
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: (): boolean => true,
},
expected: {
allowed: false,
reason: "disabled",
groupPolicy: "disabled",
providerMissingFallbackApplied: false,
},
},
{
name: "blocks sender allowlist with an empty list",
input: {
groupPolicy: "allowlist" as const,
groupAllowFrom: [],
senderId: "123",
isSenderAllowed: (): boolean => true,
},
expected: {
allowed: false,
reason: "empty_allowlist",
groupPolicy: "allowlist",
providerMissingFallbackApplied: false,
},
},
])("$name", ({ input, expected }) => {
expect(evaluateSenderGroupAccessForPolicy(input)).toEqual(expected);
});
it.each([
{
name: "blocks disabled route policy",
input: {
groupPolicy: "disabled" as const,
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: true,
},
reason: "disabled",
},
{
name: "blocks an empty route allowlist",
input: {
groupPolicy: "allowlist" as const,
routeAllowlistConfigured: false,
routeMatched: false,
},
reason: "empty_allowlist",
},
{
name: "blocks an unmatched allowlisted route",
input: {
groupPolicy: "allowlist" as const,
routeAllowlistConfigured: true,
routeMatched: false,
},
reason: "route_not_allowlisted",
},
{
name: "blocks a disabled matched route",
input: {
groupPolicy: "open" as const,
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: false,
},
reason: "route_disabled",
},
])("$name", ({ input, reason }) => {
expect(evaluateGroupRouteAccessForPolicy(input)).toMatchObject({ allowed: false, reason });
});
});
describe("mutable allowlist table helpers", () => {
it("collects standard account, DM, and nested group lists in stable order", () => {
expect(
+91 -5
View File
@@ -68,13 +68,99 @@ export {
resolveEffectiveAllowFromLists,
resolveOpenDmAllowlistAccess,
} from "./channel-access-compat.js";
export {
evaluateGroupRouteAccessForPolicy,
evaluateSenderGroupAccessForPolicy,
resolveSenderScopedGroupPolicy,
} from "./group-access.js";
export { createAllowlistProviderRestrictSendersWarningCollector };
type GroupRouteAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
reason: "allowed" | "disabled" | "empty_allowlist" | "route_not_allowlisted" | "route_disabled";
};
type SenderGroupAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
reason: "allowed" | "disabled" | "empty_allowlist" | "sender_not_allowlisted";
};
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function resolveSenderScopedGroupPolicy(params: {
groupPolicy: GroupPolicy;
groupAllowFrom: string[];
}): GroupPolicy {
if (params.groupPolicy === "disabled") {
return "disabled";
}
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
}
/** @deprecated Use route descriptors with `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateGroupRouteAccessForPolicy(params: {
groupPolicy: GroupPolicy;
routeAllowlistConfigured: boolean;
routeMatched: boolean;
routeEnabled?: boolean;
}): GroupRouteAccessDecision {
if (params.groupPolicy === "disabled") {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
}
if (params.routeMatched && params.routeEnabled === false) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_disabled" };
}
if (params.groupPolicy === "allowlist") {
if (!params.routeAllowlistConfigured) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
}
if (!params.routeMatched) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_not_allowlisted" };
}
}
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
}
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateSenderGroupAccessForPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied?: boolean;
groupAllowFrom: string[];
senderId: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
}): SenderGroupAccessDecision {
const providerMissingFallbackApplied = Boolean(params.providerMissingFallbackApplied);
if (params.groupPolicy === "disabled") {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "disabled",
};
}
if (params.groupPolicy === "allowlist") {
if (params.groupAllowFrom.length === 0) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "empty_allowlist",
};
}
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "sender_not_allowlisted",
};
}
}
return {
allowed: true,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "allowed",
};
}
/** Normalizes allowFrom entries into trimmed unique string identifiers. */
export function normalizeAllowFromList(list: Array<string | number> | undefined | null): string[] {
if (!Array.isArray(list)) {
-34
View File
@@ -1,34 +0,0 @@
/**
* @deprecated Public SDK subpath has no bundled extension production imports.
* Prefer focused channel secret subpaths such as channel-secret-basic-runtime
* and channel-secret-tts-runtime.
*/
export {
collectConditionalChannelFieldAssignments,
collectNestedChannelFieldAssignments,
collectNestedChannelTtsAssignments,
collectSimpleChannelFieldAssignments,
getChannelRecord,
getChannelSurface,
hasConfiguredSecretInputValue,
isBaseFieldActiveForChannelSurface,
normalizeSecretStringValue,
resolveChannelAccountSurface,
} from "../secrets/channel-secret-collector-runtime.js";
export type {
ChannelAccountEntry,
ChannelAccountPredicate,
ChannelAccountSurface,
} from "../secrets/channel-secret-collector-runtime.js";
export {
collectSecretInputAssignment,
hasOwnProperty,
isEnabledFlag,
pushAssignment,
pushInactiveSurfaceWarning,
pushWarning,
} from "../secrets/runtime-shared.js";
export type { ResolverContext, SecretDefaults } from "../secrets/runtime-shared.js";
export { isRecord } from "../secrets/shared.js";
export type { SecretTargetRegistryEntry } from "../secrets/target-registry-types.js";
-4
View File
@@ -1,4 +0,0 @@
/** @deprecated Compatibility subpath. Use `openclaw/plugin-sdk/channel-outbound`. */
export * from "../channels/streaming.js";
/** @deprecated Shipped through this facade; remove with the subpath after 2026-08-15. */
export type { SlackChannelStreamingConfig } from "../config/types.slack.js";
-319
View File
@@ -1,319 +0,0 @@
/**
* Tests group access policy helpers and SDK-visible access decisions.
*/
import { describe, expect, it } from "vitest";
import {
evaluateGroupRouteAccessForPolicy,
evaluateMatchedGroupAccessForPolicy,
evaluateSenderGroupAccess,
evaluateSenderGroupAccessForPolicy,
resolveSenderScopedGroupPolicy,
} from "./group-access.js";
describe("resolveSenderScopedGroupPolicy", () => {
const cases: Array<{
name: string;
input: Parameters<typeof resolveSenderScopedGroupPolicy>[0];
expected: ReturnType<typeof resolveSenderScopedGroupPolicy>;
}> = [
{
name: "preserves disabled policy",
input: {
groupPolicy: "disabled",
groupAllowFrom: ["a"],
},
expected: "disabled",
},
{
name: "keeps allowlist policy when sender allowlist is present",
input: {
groupPolicy: "allowlist",
groupAllowFrom: ["a"],
},
expected: "allowlist",
},
{
name: "maps allowlist to open when sender allowlist is empty",
input: {
groupPolicy: "allowlist",
groupAllowFrom: [],
},
expected: "open",
},
];
it.each(cases)("$name", ({ input, expected }) => {
expect(resolveSenderScopedGroupPolicy(input)).toBe(expected);
});
});
describe("evaluateSenderGroupAccessForPolicy", () => {
const cases: Array<{
name: string;
input: Parameters<typeof evaluateSenderGroupAccessForPolicy>[0];
expected: ReturnType<typeof evaluateSenderGroupAccessForPolicy>;
}> = [
{
name: "blocks disabled policy",
input: {
groupPolicy: "disabled",
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: () => true,
},
expected: {
allowed: false,
reason: "disabled",
groupPolicy: "disabled",
providerMissingFallbackApplied: false,
},
},
{
name: "blocks allowlist with empty list",
input: {
groupPolicy: "allowlist",
groupAllowFrom: [],
senderId: "123",
isSenderAllowed: () => true,
},
expected: {
allowed: false,
reason: "empty_allowlist",
groupPolicy: "allowlist",
providerMissingFallbackApplied: false,
},
},
];
it.each(cases)("$name", ({ input, expected }) => {
expect(evaluateSenderGroupAccessForPolicy(input)).toEqual(expected);
});
});
describe("evaluateGroupRouteAccessForPolicy", () => {
const cases: Array<{
name: string;
input: Parameters<typeof evaluateGroupRouteAccessForPolicy>[0];
expected: ReturnType<typeof evaluateGroupRouteAccessForPolicy>;
}> = [
{
name: "blocks disabled policy",
input: {
groupPolicy: "disabled",
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: true,
},
expected: {
allowed: false,
groupPolicy: "disabled",
reason: "disabled",
},
},
{
name: "blocks allowlist without configured routes",
input: {
groupPolicy: "allowlist",
routeAllowlistConfigured: false,
routeMatched: false,
},
expected: {
allowed: false,
groupPolicy: "allowlist",
reason: "empty_allowlist",
},
},
{
name: "blocks unmatched allowlist route",
input: {
groupPolicy: "allowlist",
routeAllowlistConfigured: true,
routeMatched: false,
},
expected: {
allowed: false,
groupPolicy: "allowlist",
reason: "route_not_allowlisted",
},
},
{
name: "blocks disabled matched route even when group policy is open",
input: {
groupPolicy: "open",
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: false,
},
expected: {
allowed: false,
groupPolicy: "open",
reason: "route_disabled",
},
},
];
it.each(cases)("$name", ({ input, expected }) => {
expect(evaluateGroupRouteAccessForPolicy(input)).toEqual(expected);
});
});
describe("evaluateMatchedGroupAccessForPolicy", () => {
const cases: Array<{
name: string;
input: Parameters<typeof evaluateMatchedGroupAccessForPolicy>[0];
expected: ReturnType<typeof evaluateMatchedGroupAccessForPolicy>;
}> = [
{
name: "blocks disabled policy",
input: {
groupPolicy: "disabled",
allowlistConfigured: true,
allowlistMatched: true,
},
expected: {
allowed: false,
groupPolicy: "disabled",
reason: "disabled",
},
},
{
name: "blocks allowlist without configured entries",
input: {
groupPolicy: "allowlist",
allowlistConfigured: false,
allowlistMatched: false,
},
expected: {
allowed: false,
groupPolicy: "allowlist",
reason: "empty_allowlist",
},
},
{
name: "blocks allowlist when required match input is missing",
input: {
groupPolicy: "allowlist",
requireMatchInput: true,
hasMatchInput: false,
allowlistConfigured: true,
allowlistMatched: false,
},
expected: {
allowed: false,
groupPolicy: "allowlist",
reason: "missing_match_input",
},
},
{
name: "blocks unmatched allowlist sender",
input: {
groupPolicy: "allowlist",
allowlistConfigured: true,
allowlistMatched: false,
},
expected: {
allowed: false,
groupPolicy: "allowlist",
reason: "not_allowlisted",
},
},
{
name: "allows open policy",
input: {
groupPolicy: "open",
allowlistConfigured: false,
allowlistMatched: false,
},
expected: {
allowed: true,
groupPolicy: "open",
reason: "allowed",
},
},
];
it.each(cases)("$name", ({ input, expected }) => {
expect(evaluateMatchedGroupAccessForPolicy(input)).toEqual(expected);
});
});
describe("evaluateSenderGroupAccess", () => {
const cases: Array<{
name: string;
input: Parameters<typeof evaluateSenderGroupAccess>[0];
expected: ReturnType<typeof evaluateSenderGroupAccess>;
}> = [
{
name: "defaults missing provider config to allowlist",
input: {
providerConfigPresent: false,
configuredGroupPolicy: undefined,
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: () => true,
},
expected: {
allowed: true,
groupPolicy: "allowlist",
providerMissingFallbackApplied: true,
reason: "allowed",
},
},
{
name: "blocks disabled policy",
input: {
providerConfigPresent: true,
configuredGroupPolicy: "disabled",
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: () => true,
},
expected: {
allowed: false,
reason: "disabled",
groupPolicy: "disabled",
providerMissingFallbackApplied: false,
},
},
{
name: "blocks allowlist with empty list",
input: {
providerConfigPresent: true,
configuredGroupPolicy: "allowlist",
defaultGroupPolicy: "open",
groupAllowFrom: [],
senderId: "123",
isSenderAllowed: () => true,
},
expected: {
allowed: false,
reason: "empty_allowlist",
groupPolicy: "allowlist",
providerMissingFallbackApplied: false,
},
},
{
name: "blocks sender not allowlisted",
input: {
providerConfigPresent: true,
configuredGroupPolicy: "allowlist",
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "999",
isSenderAllowed: () => false,
},
expected: {
allowed: false,
reason: "sender_not_allowlisted",
groupPolicy: "allowlist",
providerMissingFallbackApplied: false,
},
},
];
it.each(cases)("$name", ({ input, expected }) => {
expect(evaluateSenderGroupAccess(input)).toEqual(expected);
});
});
-177
View File
@@ -1,177 +0,0 @@
/**
* @deprecated Public SDK subpath has no bundled extension production imports.
* Use resolveChannelMessageIngress from channel-ingress-runtime instead.
*/
import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js";
import type { GroupPolicy } from "../config/types.base.js";
export { resolveOpenProviderRuntimeGroupPolicy };
export type { GroupPolicy };
/** Reason code returned when evaluating a sender against group policy. */
export type SenderGroupAccessReason =
| "allowed"
| "disabled"
| "empty_allowlist"
| "sender_not_allowlisted";
/** Sender-level group access decision plus the effective group policy. */
export type SenderGroupAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
reason: SenderGroupAccessReason;
};
/** Reason code returned when evaluating a configured group route. */
export type GroupRouteAccessReason =
| "allowed"
| "disabled"
| "empty_allowlist"
| "route_not_allowlisted"
| "route_disabled";
/** Route-level group access decision plus the effective group policy. */
export type GroupRouteAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
reason: GroupRouteAccessReason;
};
/** Reason code returned when evaluating a precomputed allowlist match. */
export type MatchedGroupAccessReason =
| "allowed"
| "disabled"
| "missing_match_input"
| "empty_allowlist"
| "not_allowlisted";
/** Matched-input group access decision plus the effective group policy. */
export type MatchedGroupAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
reason: MatchedGroupAccessReason;
};
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function resolveSenderScopedGroupPolicy(params: {
groupPolicy: GroupPolicy;
groupAllowFrom: string[];
}): GroupPolicy {
if (params.groupPolicy === "disabled") {
return "disabled";
}
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
}
/** @deprecated Use route descriptors with `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateGroupRouteAccessForPolicy(params: {
groupPolicy: GroupPolicy;
routeAllowlistConfigured: boolean;
routeMatched: boolean;
routeEnabled?: boolean;
}): GroupRouteAccessDecision {
if (params.groupPolicy === "disabled") {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
}
if (params.routeMatched && params.routeEnabled === false) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_disabled" };
}
if (params.groupPolicy === "allowlist") {
if (!params.routeAllowlistConfigured) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
}
if (!params.routeMatched) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_not_allowlisted" };
}
}
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
}
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateMatchedGroupAccessForPolicy(params: {
groupPolicy: GroupPolicy;
allowlistConfigured: boolean;
allowlistMatched: boolean;
requireMatchInput?: boolean;
hasMatchInput?: boolean;
}): MatchedGroupAccessDecision {
if (params.groupPolicy === "disabled") {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
}
if (params.groupPolicy === "allowlist") {
if (params.requireMatchInput && !params.hasMatchInput) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "missing_match_input" };
}
if (!params.allowlistConfigured) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
}
if (!params.allowlistMatched) {
return { allowed: false, groupPolicy: params.groupPolicy, reason: "not_allowlisted" };
}
}
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
}
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateSenderGroupAccessForPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied?: boolean;
groupAllowFrom: string[];
senderId: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
}): SenderGroupAccessDecision {
const providerMissingFallbackApplied = Boolean(params.providerMissingFallbackApplied);
if (params.groupPolicy === "disabled") {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "disabled",
};
}
if (params.groupPolicy === "allowlist") {
if (params.groupAllowFrom.length === 0) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "empty_allowlist",
};
}
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "sender_not_allowlisted",
};
}
}
return {
allowed: true,
groupPolicy: params.groupPolicy,
providerMissingFallbackApplied,
reason: "allowed",
};
}
/** @deprecated Use `resolveOpenProviderRuntimeGroupPolicy` plus `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
export function evaluateSenderGroupAccess(params: {
providerConfigPresent: boolean;
configuredGroupPolicy?: GroupPolicy;
defaultGroupPolicy?: GroupPolicy;
groupAllowFrom: string[];
senderId: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
}): SenderGroupAccessDecision {
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.configuredGroupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
});
return evaluateSenderGroupAccessForPolicy({
groupPolicy,
providerMissingFallbackApplied,
groupAllowFrom: params.groupAllowFrom,
senderId: params.senderId,
isSenderAllowed: params.isSenderAllowed,
});
}
-6
View File
@@ -1,6 +0,0 @@
/**
* @deprecated Compatibility facade for older third-party channel packages that
* imported the previous Matrix-shaped helper bundle. New plugins should import
* `openclaw/plugin-sdk/run-command` directly.
*/
export { runPluginCommandWithTimeout } from "./run-command.js";
-99
View File
@@ -1,99 +0,0 @@
/**
* @deprecated Broad public SDK barrel. Prefer focused text/chunking/logging
* subpaths and avoid adding new imports here.
*/
export * from "../logger.js";
export * from "../logging/diagnostic.js";
export * from "../logging/logger.js";
export * from "../logging/redact.js";
export * from "../logging/redact-identifier.js";
export * from "../../packages/markdown-core/src/ir.js";
export * from "../../packages/markdown-core/src/render-aware-chunking.js";
export * from "../../packages/markdown-core/src/render.js";
export * from "../../packages/markdown-core/src/tables.js";
export { resolveGlobalMap, resolveGlobalSingleton } from "../shared/global-singleton.js";
// Public compatibility: this explicit list intentionally keeps isRecord on text-runtime.
export {
asNullableObjectRecord,
asNullableRecord,
asOptionalObjectRecord,
asOptionalRecord,
asRecord,
isRecord,
readStringField,
} from "../../packages/normalization-core/src/record-coerce.js";
export * from "../shared/scoped-expiring-id-cache.js";
export {
hasNonEmptyString,
localeLowercasePreservingWhitespace,
lowercasePreservingWhitespace,
normalizeFastMode,
normalizeLowercaseStringOrEmpty,
normalizeNullableString,
normalizeOptionalLowercaseString,
normalizeOptionalString,
normalizeOptionalStringifiedId,
normalizeOptionalThreadValue,
normalizeStringifiedEntries,
normalizeStringifiedOptionalString,
readStringValue,
resolvePrimaryStringValue,
type FastMode,
} from "../../packages/normalization-core/src/string-coerce.js";
// Public compatibility: keep this legacy barrel's existing normalization surface
// without promoting new normalization-core helpers into the Plugin SDK.
export {
normalizeArrayBackedTrimmedStringList,
normalizeAtHashSlug,
normalizeCsvOrLooseStringList,
normalizeHyphenSlug,
normalizeOptionalTrimmedStringList,
normalizeSingleOrTrimmedStringList,
normalizeSortedUniqueStringEntries,
normalizeSortedUniqueTrimmedStringList,
normalizeStringEntries,
normalizeStringEntriesLower,
normalizeTrimmedStringList,
normalizeUniqueSingleOrTrimmedStringList,
normalizeUniqueStringEntries,
normalizeUniqueStringEntriesLower,
normalizeUniqueTrimmedStringList,
sortUniqueStrings,
uniqueStrings,
uniqueValues,
} from "../../packages/normalization-core/src/string-normalization.js";
export * from "../shared/string-sample.js";
export * from "../shared/text/assistant-visible-text.js";
export * from "../shared/text/auto-linked-file-ref.js";
export * from "../shared/text/code-regions.js";
export * from "../shared/text/reasoning-tags.js";
export * from "../shared/text/strip-markdown.js";
export * from "../../packages/terminal-core/src/safe-text.js";
export * from "../infra/system-message.ts";
export * from "../utils/directive-tags.js";
export * from "../utils/chunk-items.js";
export * from "../utils/fetch-timeout.js";
export * from "../utils/reaction-level.js";
export * from "../utils/with-timeout.js";
export {
CONFIG_DIR,
clamp,
clampInt,
clampNumber,
displayPath,
displayString,
ensureDir,
escapeRegExp,
normalizeE164,
pathExists,
resolveConfigDir,
resolveHomeDir,
resolveUserPath,
tryParseJson as safeParseJson,
shortenHomeInString,
shortenHomePath,
sleep,
sliceUtf16Safe,
truncateUtf16Safe,
} from "../utils.js";

Some files were not shown because too many files have changed in this diff Show More