Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

9
docs/refactor/access.md Normal file
View File

@@ -0,0 +1,9 @@
---
summary: "Redirect to /refactor/ingress-core"
read_when:
- Changing channel ingress, sender access, command authorization, or access-group handling
title: "Channel access cleanup"
sidebarTitle: "Channel access cleanup"
---
The channel access cleanup plan now lives in [Ingress core deletion plan](/refactor/ingress-core).

298
docs/refactor/acp.md Normal file
View File

@@ -0,0 +1,298 @@
---
summary: "Migration plan for making ACP session and ACPX process ownership explicit"
read_when:
- Refactoring ACP session lifecycle or ACPX process cleanup
- Debugging ACPX orphan processes, PID reuse, or multi-gateway cleanup safety
- Changing sessions_list visibility for spawned ACP or subagent sessions
- Designing ownership metadata for background tasks, ACP sessions, or process leases
title: "ACP lifecycle refactor"
sidebarTitle: "ACP lifecycle refactor"
---
ACP lifecycle currently works, but too much of it is inferred after the fact.
Process cleanup reconstructs ownership from PIDs, command strings, wrapper
paths, and the live process table. Session visibility reconstructs ownership
from session-key strings plus secondary `sessions.list({ spawnedBy })` lookups.
That makes narrow fixes possible, but it also makes edge cases easy to miss:
PID reuse, quoted commands, adapter grandchildren, multi-gateway state roots,
`cancel` versus `close`, and `tree` versus `all` visibility all become separate
places to rediscover the same ownership rules.
This refactor makes ownership first-class. The goal is not a new ACP product
surface; it is a safer internal contract for the existing ACP and ACPX behavior.
## Goals
- Cleanup never signals a process unless current live evidence matches an
OpenClaw-owned lease.
- `cancel`, `close`, and startup reaping have distinct lifecycle intents.
- `sessions_list`, `sessions_history`, `sessions_send`, and status checks use
the same requester-owned session model.
- Multi-gateway installs cannot reap each other's ACPX wrappers.
- Old ACPX session records keep working during migration.
- The runtime remains plugin-owned; core does not learn ACPX package details.
## Non-goals
- Replacing ACPX or changing the public `/acp` command surface.
- Moving vendor-specific ACP adapter behavior into core.
- Requiring users to manually clean state before upgrading.
- Making `cancel` close reusable ACP sessions.
## Target Model
### Gateway Instance Identity
Each Gateway process should have a stable runtime instance id:
```ts
type GatewayInstanceId = string;
```
It can be generated on Gateway startup and persisted in state for the life of
that install. It is not a security secret; it is an ownership discriminator used
to avoid confusing one Gateway's ACP processes with another Gateway's processes.
### ACP Session Ownership
Every spawned ACP session should have normalized ownership metadata:
```ts
type AcpSessionOwner = {
sessionKey: string;
spawnedBy?: string;
parentSessionKey?: string;
ownerSessionKey: string;
agentId: string;
backend: "acpx";
gatewayInstanceId: GatewayInstanceId;
createdAt: number;
};
```
The Gateway should return these fields on session rows where they are known.
Visibility filtering should be a pure check over row metadata:
```ts
canSeeSessionRow({
row,
requesterSessionKey,
visibility,
a2aPolicy,
});
```
That removes hidden secondary `sessions.list({ spawnedBy })` calls from
visibility checks. A spawned cross-agent ACP child is requester-owned because
the row says so, not because a second query happens to find it.
### ACPX Process Leases
Every generated wrapper launch should create a lease record:
```ts
type AcpxProcessLease = {
leaseId: string;
gatewayInstanceId: GatewayInstanceId;
sessionKey: string;
wrapperRoot: string;
wrapperPath: string;
rootPid: number;
processGroupId?: number;
commandHash: string;
startedAt: number;
state: "open" | "closing" | "closed" | "lost";
};
```
The wrapper process should receive the lease id and gateway instance id in its
environment:
```sh
OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...
```
When the platform allows it, verification should prefer live process metadata
that cannot be confused by command quoting:
- root PID still exists
- live wrapper path is under `wrapperRoot`
- process group matches the lease when available
- environment contains the expected lease id when readable
- command hash or executable path matches the lease
If the live process cannot be verified, cleanup fails closed.
## Lifecycle Controller
Introduce one ACPX lifecycle controller that owns process leases and cleanup
policy:
```ts
interface AcpxLifecycleController {
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
cancelTurn(handle: AcpRuntimeHandle): Promise<void>;
closeSession(input: {
handle: AcpRuntimeHandle;
discardPersistentState?: boolean;
reason?: string;
}): Promise<void>;
reapStartupOrphans(): Promise<void>;
verifyOwnedTree(lease: AcpxProcessLease): Promise<OwnedProcessTree | null>;
}
```
`cancelTurn` requests turn cancellation only. It must not reap reusable wrapper
or adapter processes.
`closeSession` is allowed to reap, but only after loading the session record,
loading the lease, and verifying the live process tree still belongs to that
lease.
`reapStartupOrphans` starts from open leases in state. It may use the process
table to find descendants, but it should not scan arbitrary ACP-looking
commands first and then decide they are probably ours.
## Wrapper Contract
Generated wrappers should stay small. They should:
- start the adapter in a process group where supported
- forward normal termination signals to the process group
- detect parent death
- on parent death, send SIGTERM, then keep the wrapper alive until the SIGKILL
fallback runs
- report root PID and process group id back to the lifecycle controller when
that is available
Wrappers should not decide session policy. They only enforce local process-tree
cleanup for their own adapter group.
## Session Visibility Contract
Visibility should use normalized row ownership:
```ts
type SessionVisibilityInput = {
requesterSessionKey: string;
row: {
key: string;
agentId: string;
ownerSessionKey?: string;
spawnedBy?: string;
parentSessionKey?: string;
};
visibility: "self" | "tree" | "agent" | "all";
a2aPolicy: AgentToAgentPolicy;
};
```
Rules:
- `self`: only the requester session.
- `tree`: requester session plus rows owned by or spawned from the requester.
- `all`: all same-agent rows, a2a-allowed cross-agent rows, and requester-owned
spawned cross-agent rows even when general a2a is disabled.
- `agent`: same agent only, unless an explicit owner relationship says the row
belongs to the requester.
This makes `tree` and `all` monotonic: `all` must not hide an owned child that
`tree` would show.
## Migration Plan
### Phase 1: Add Identity And Leases
- Add `gatewayInstanceId` to Gateway state.
- Add an ACPX lease store under the ACPX state directory.
- Write a lease before spawning a generated wrapper.
- Store `leaseId` on new ACPX session records.
- Keep existing PID and command fields for old records.
### Phase 2: Lease-First Cleanup
- Change close cleanup to load `leaseId` first.
- Verify live process ownership against the lease before signaling.
- Keep the current root PID and wrapper-root fallback only for legacy records.
- Mark leases `closed` after verified cleanup.
- Mark leases `lost` when the process is gone before cleanup.
### Phase 3: Lease-First Startup Reaping
- Startup reaping scans open leases.
- For each lease, verify the root process and collect descendants.
- Reap verified trees children-first.
- Expire old `closed` and `lost` leases with a bounded retention window.
- Keep command-marker scanning only as a temporary legacy fallback, guarded by
wrapper root and Gateway instance where possible.
### Phase 4: Session Ownership Rows
- Add ownership metadata to Gateway session rows.
- Teach ACPX, subagent, background-task, and session-store writers to populate
`ownerSessionKey` or `spawnedBy`.
- Convert session visibility checks to use row metadata.
- Remove visibility-time secondary `sessions.list({ spawnedBy })` lookups.
### Phase 5: Remove Legacy Heuristics
After one release window:
- stop relying on stored root command strings for non-legacy ACPX cleanup
- remove command-marker startup scans
- remove visibility fallback list lookups
- keep defensive fail-closed behavior for missing or unverifiable leases
## Tests
Add two table-driven suites.
Process lifecycle simulator:
- PID reused by unrelated process
- PID reused by another Gateway's wrapper root
- stored wrapper command is shell-quoted, live `ps` command is not
- adapter child exits, grandchild remains in the process group
- parent death SIGTERM fallback reaches SIGKILL
- process listing unavailable
- stale lease with missing process
- startup orphan with wrapper, adapter child, and grandchild
Session visibility matrix:
- `self`, `tree`, `agent`, `all`
- a2a enabled and disabled
- same-agent row
- cross-agent row
- requester-owned spawned cross-agent ACP row
- sandboxed requester clamped to `tree`
- list, history, send, and status actions
The important invariant: a requester-owned spawned child is visible wherever
the configured visibility includes the requester session tree, and `all` is not
less capable than `tree`.
## Compatibility Notes
Old session records may not have `leaseId`. They should use the legacy
fail-closed cleanup path:
- require a live root process
- require wrapper-root ownership when a generated wrapper is expected
- require command agreement for non-wrapper roots
- never signal based only on stale stored PID metadata
If a legacy record cannot be verified, leave it alone. Startup lease cleanup and
the next release window should eventually retire the fallback.
## Success Criteria
- Closing an old or stale ACPX session cannot kill another Gateway's process.
- Parent death does not leave stubborn adapter grandchildren running.
- `cancel` aborts the active turn without closing reusable sessions.
- `sessions_list` can show requester-owned cross-agent ACP children under both
`tree` and `all`.
- Startup cleanup is driven by leases, not broad command-string scans.
- The focused process and visibility matrix tests cover every edge case that
previously required one-off review fixes.

131
docs/refactor/canvas.md Normal file
View File

@@ -0,0 +1,131 @@
---
summary: "Plan and audit checklist for moving Canvas out of core and into a bundled experimental plugin."
read_when:
- Moving Canvas host, tools, commands, docs, or protocol ownership
- Auditing whether Canvas is still core-owned
- Preparing or reviewing the experimental Canvas plugin PR
title: "Canvas plugin refactor"
---
# Canvas plugin refactor
Canvas is low-use and experimental. Treat it as a bundled plugin, not a core feature. Core may keep generic gateway, node, HTTP, auth, config, and native-client plumbing, but Canvas-specific behavior should live under `extensions/canvas`.
## Goal
Move Canvas ownership to `extensions/canvas` while preserving the current paired-node behavior:
- the agent-facing `canvas` tool is registered by the Canvas plugin
- Canvas node commands are allowed only when the Canvas plugin registers them
- A2UI host/source files live under the Canvas plugin
- Canvas document materialization lives under the Canvas plugin
- CLI command implementation lives under the Canvas plugin, or delegates through a plugin-owned runtime barrel
- docs and plugin inventory describe Canvas as experimental and plugin-backed
## Non-goals
- Do not redesign the native app Canvas UI in this refactor.
- Do not remove Canvas protocol/client support from iOS, Android, or macOS unless a separate product decision says Canvas should be deleted.
- Do not build a broad plugin service framework only for Canvas unless at least one other bundled plugin needs the same seam.
## Current branch state
Done:
- Added bundled plugin package in `extensions/canvas`.
- Added `extensions/canvas/openclaw.plugin.json`.
- Moved the agent `canvas` tool from `src/agents/tools/canvas-tool.ts` to `extensions/canvas/src/tool.ts`.
- Removed core registration of `createCanvasTool` from `src/agents/openclaw-tools.ts`.
- Moved Canvas host implementation from `src/canvas-host` to `extensions/canvas/src/host`.
- Kept `extensions/canvas/runtime-api.ts` as the plugin-owned compatibility barrel for tests, packaging, and external public Canvas helpers.
- Moved Canvas document materialization from `src/gateway/canvas-documents.ts` to `extensions/canvas/src/documents.ts`.
- Moved Canvas CLI implementation and A2UI JSONL helpers into `extensions/canvas/src/cli.ts`.
- Moved Canvas host URL and scoped capability helpers into `extensions/canvas/src`.
- Moved Canvas node command defaults out of hardcoded core lists and into plugin `nodeInvokePolicies`.
- Added plugin-owned Canvas host config at `plugins.entries.canvas.config.host`.
- Moved Canvas and A2UI HTTP serving behind Canvas plugin HTTP route registration.
- Added generic plugin WebSocket upgrade dispatch for plugin-owned HTTP routes.
- Replaced Canvas-specific gateway host URL and node capability auth with generic hosted plugin surface and node capability helpers.
- Added plugin-owned hosted media resolvers so Canvas document URLs resolve through the Canvas plugin instead of core importing Canvas document internals.
- Added `api.registerNodeCliFeature(...)` so Canvas can declare `openclaw nodes canvas` as a plugin-owned node feature without manually spelling the parent command path.
- Removed production `src/**` imports of `extensions/canvas/runtime-api.js`.
- Moved the A2UI bundle source from `apps/shared/OpenClawKit/Tools/CanvasA2UI` to `extensions/canvas/src/host/a2ui-app`.
- Moved A2UI build/copy implementation under `extensions/canvas/scripts` and replaced root build wiring with generic bundled-plugin asset hooks.
- Removed the runtime legacy top-level `canvasHost` config alias.
- Kept the Canvas doctor migration so `openclaw doctor --fix` rewrites old `canvasHost` configs into `plugins.entries.canvas.config.host`.
- Removed old-agent Canvas protocol compatibility behind gateway protocol v4. Native clients and gateways now use only `pluginSurfaceUrls.canvas` plus `node.pluginSurface.refresh`; the deprecated `canvasHostUrl`, `canvasCapability`, and `node.canvas.capability.refresh` path is intentionally unsupported in this experimental refactor.
- Updated generated plugin inventory to include Canvas.
- Added plugin reference docs at `docs/plugins/reference/canvas.md`.
Known remaining core-owned Canvas surfaces:
- Native app Canvas handlers under `apps/` still intentionally consume the Canvas plugin surface
- native app Canvas protocol/client handlers under `apps/`
- published artifact output still uses `dist/canvas-host/a2ui` for backwards-compatible runtime lookup, but the copy step is now plugin-owned
## Target shape
`extensions/canvas` should own:
- plugin manifest and package metadata
- agent tool registration
- node invoke command policy
- Canvas host and A2UI runtime
- Canvas A2UI bundle source and asset build/copy scripts
- Canvas document creation and asset resolution
- Canvas CLI implementation
- Canvas docs page and plugin inventory entry
Core should own only generic seams:
- plugin discovery and registration
- generic agent tool registry
- generic node invoke policy registry
- generic gateway HTTP/auth and WebSocket upgrade dispatch
- generic hosted plugin surface URL resolution
- generic hosted media resolver registration
- generic node capability transport
- generic config plumbing
- generic bundled-plugin asset hook discovery
Native apps may keep Canvas command handlers as clients of the protocol. They are not the plugin runtime owner.
## Migration steps
1. Treat `plugins.entries.canvas.config.host` as the plugin-owned config surface.
2. Update docs so Canvas is described as an experimental bundled plugin.
3. Run focused Canvas tests, plugin inventory checks, plugin SDK API checks, and build/type gates affected by runtime boundaries.
## Audit checklist
Before calling the refactor complete:
- `rg "src/canvas-host|../canvas-host"` returns no live source imports.
- `rg "canvas-tool|createCanvasTool" src` finds no core-owned Canvas tool implementation.
- `rg "canvas.present|canvas.snapshot|canvas.a2ui" src/gateway` finds no hardcoded allowlist defaults outside generic plugin policy tests.
- `rg "extensions/canvas/runtime-api" src --glob '!**/*.test.ts'` is empty.
- `rg "canvas-documents" src` is empty.
- `rg "registerNodesCanvasCommands|nodes-canvas" src` is empty; the Canvas plugin registers `openclaw nodes canvas` through nested plugin CLI metadata.
- `rg "createCanvasHostHandler|handleA2uiHttpRequest" src/gateway` returns no gateway runtime ownership.
- `rg "apps/shared/OpenClawKit/Tools/CanvasA2UI|canvas-a2ui-copy|extensions/canvas/src/host/a2ui" scripts .github package.json` finds only compatibility wrappers or plugin-owned paths.
- `pnpm plugins:inventory:check` passes.
- `pnpm plugin-sdk:api:check` passes, or generated API baselines are intentionally updated and reviewed.
- Targeted Canvas tests pass.
- Changed-lanes tests pass for Canvas host/A2UI paths.
- PR body explicitly says Canvas is experimental and plugin-backed.
## Verification commands
Use targeted local checks while iterating:
```sh
pnpm test extensions/canvas/src/host/server.test.ts extensions/canvas/src/host/server.state-dir.test.ts extensions/canvas/src/host/file-resolver.test.ts
pnpm test src/gateway/server.plugin-node-capability-auth.test.ts src/gateway/server-import-boundary.test.ts
pnpm test extensions/canvas/src/config-migration.test.ts src/commands/doctor-legacy-config.migrations.test.ts
pnpm test test/scripts/changed-lanes.test.ts test/scripts/build-all.test.ts extensions/canvas/scripts/bundle-a2ui.test.ts test/scripts/bundled-plugin-assets.test.ts extensions/canvas/scripts/copy-a2ui.test.ts src/infra/run-node.test.ts
pnpm tsgo:extensions
pnpm plugins:inventory:check
pnpm plugin-sdk:api:check
```
Run `pnpm build` before push if runtime barrel, lazy import, packaging, or published plugin surfaces change.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,341 @@
---
summary: "Deletion-first plan for moving repeated channel ingress glue into core."
read_when:
- Auditing why the channel ingress refactor added too much code
- Moving route, command, event, activation, or access-group policy from bundled plugins into core
- Reviewing whether a channel ingress helper actually deletes bundled plugin code
title: "Ingress core deletion plan"
sidebarTitle: "Ingress core deletion"
---
# Ingress core deletion plan
The ingress refactor is not healthy while it adds thousands of net lines. Core
centralization only counts when bundled plugin production code gets smaller and
old third-party SDK compatibility is quarantined to SDK/core shims.
Desired runtime shape:
```text
bundled plugin event
-> extract platform facts locally
-> resolve shared ingress once when facts are available
-> branch on generic ingress projections/outcomes
-> perform platform side effects locally
old third-party helper
-> SDK compatibility shim
-> shared ingress-compatible projection where possible
-> old return shape preserved
```
Bundled plugins should not translate ingress back into local `AccessResult`,
`GroupAccessDecision`, `CommandAuthDecision`, `DmCommandAccess`, or
`{ allowed, reasonCode }` shapes unless that type is public plugin API.
## Budget
Measured against the PR merge-base with `origin/main`, including untracked
files.
```text
merge-base 1671e7532adb
current:
core production +3,922 / -546 = +3,376
docs +601 / -17 = +584
other +145 / -2 = +143
plugin production +4,148 / -5,388 = -1,240
tests +2,326 / -2,414 = -88
total +11,142 / -8,367 = +2,775
required:
plugin production <= -1,500
core production <= +1,500, or paid for by larger plugin deletion
tests <= +1,000
total <= +2,000
stretch:
plugin production <= -2,500
core production <= +1,200
total <= 0
```
Minimum remaining cleanup:
```text
plugin production needs 260 more net deleted lines
total needs 775 more net deleted lines
core production still +1,876 over standalone budget, unless paid down by plugin deletion
```
Comment-only deletion does not count as cleanup. The previous budget pass was
too generous because it included restored QQBot explanatory comments; this
document tracks executable/docs/test code movement only.
Re-measure after each cleanup wave:
```sh
base=$(git merge-base HEAD origin/main)
git diff --shortstat "$base"
git diff --numstat "$base" -- src/channels/message-access src/plugin-sdk extensions | sort -nr -k1 | head -n 80
pnpm lint:extensions:no-deprecated-channel-access
```
## Diagnosis
The first pass added the shared ingress kernel, then left too much plugin-local
authorization beside it:
```text
platform facts
-> shared ingress state and decision
-> plugin-local DTO or legacy projection
-> plugin-local if/else ladder
```
That duplicates the model. Core production grew by about 3,376 lines, while
bundled plugin production is 1,240 lines smaller. That is better than the first
pass, but it is not inside the minimum budget. The fix remains deletion-first:
- delete plugin DTOs that only rename ingress fields
- delete tests that only assert wrapper shape
- add core helpers only when the same patch deletes bundled plugin code
- keep old SDK compatibility in SDK/core shims only
- repack core after wrapper deletion exposes the stable shape
## Hotspots
Positive bundled production files that still need to shrink:
```text
extensions/telegram/src/ingress.ts +126
extensions/discord/src/monitor/dm-command-auth.ts +101
extensions/signal/src/monitor/access-policy.ts +92
extensions/feishu/src/policy.ts +85
extensions/slack/src/monitor/auth.ts +64
extensions/googlechat/src/monitor-access.ts +59
extensions/nextcloud-talk/src/inbound.ts +51
extensions/matrix/src/matrix/monitor/access-state.ts +49
extensions/irc/src/inbound.ts +44
extensions/imessage/src/monitor/inbound-processing.ts +36
extensions/qa-channel/src/inbound.ts +34
extensions/qqbot/src/bridge/sdk-adapter.ts +33
extensions/tlon/src/monitor/utils.ts +30
extensions/twitch/src/access-control.ts +22
extensions/qqbot/src/engine/commands/slash-command-handler.ts +20
extensions/telegram/src/bot-handlers.runtime.ts +19
```
The branch is not inside the minimum budget yet. The remaining review-relevant
work should delete repeated authorization flow, turn scaffolding, or wrapper
tests before adding another core abstraction.
## Current Code Read
The healthy core seam already exists in `src/channels/message-access/runtime.ts`:
it owns identity adapters, effective allowlists, pairing-store reads, route
descriptors, command/event presets, access groups, and the final resolved
`ResolvedChannelMessageIngress` projection.
The remaining growth is mostly plugin glue layered on top of that seam:
- `extensions/telegram/src/ingress.ts` wraps core decisions in Telegram-specific
command/event helpers, then call sites still pass precomputed normalized
allowlists and owner lists.
- `extensions/discord/src/monitor/dm-command-auth.ts`,
`extensions/feishu/src/policy.ts`, `extensions/googlechat/src/monitor-access.ts`,
and `extensions/matrix/src/matrix/monitor/access-state.ts` still keep
local policy DTOs or legacy decision names beside ingress.
- `extensions/signal/src/monitor/access-policy.ts` correctly keeps Signal
identity normalization and pairing replies local, but still has a wrapper
seam that should collapse into direct ingress consumption.
- `extensions/nextcloud-talk/src/inbound.ts`, `extensions/irc/src/inbound.ts`,
`extensions/qa-channel/src/inbound.ts`, `extensions/zalo/src/monitor.ts`, and
`extensions/zalouser/src/monitor.ts` still repeat route/envelope/turn
assembly that can move to shared turn helpers outside the ingress kernel.
Conclusion: moving more code into core is only useful if it deletes these
plugin wrapper layers in the same patch. Adding another abstraction while
leaving wrapper returns in place repeats the mistake.
## Boundary
Core owns generic policy:
- allowlist normalization and matching
- access-group expansion and diagnostics
- pairing-store DM allowlist reads
- route, sender, command, event, and activation gates
- admission mapping: dispatch, drop, skip, observe, pairing
- redacted state, decisions, diagnostics, and SDK compatibility projections
- reusable generic descriptors for identity, route, command, event, activation,
and outcomes
Plugins own transport facts and side effects:
- webhook/socket/request authenticity
- platform identity extraction and API lookups
- channel-specific policy defaults
- pairing challenge delivery, replies, acks, reactions, typing, media, history,
setup, doctor, status, logs, and user-facing copy
Core must stay channel-agnostic: no Discord, Slack, Telegram, Matrix, room,
guild, space, API client, or plugin-specific default in
`src/channels/message-access`.
## Acceptance Rule
Every new core helper must delete bundled plugin production code immediately.
```text
one bundled caller reject; keep plugin-local
two bundled callers accept only if plugin production LOC drops
three or more callers plugin deletion must be at least 2x new core LOC
compatibility-only helper SDK/core shim only; never bundled hot paths
```
Stop and redesign if:
- plugin production LOC increases
- tests grow faster than production shrinks
- a bundled hot path returns a DTO that only renames `ResolvedChannelMessageIngress`
- a core helper needs a channel id, platform object, API client, or
channel-specific default
## Work Packages
1. Freeze the budget.
Put LOC in the PR, keep deprecated-ingress lint green, and include before/after
LOC in cleanup commits.
2. Delete thin DTO seams.
Replace plugin-local wrapper returns with `ResolvedChannelMessageIngress`,
`senderAccess`, `commandAccess`, `routeAccess`, or `ingress` directly. Start
with QQBot, Telegram, Slack, Discord, Signal, Feishu, Matrix, iMessage, and
Tlon. Delete wrapper-shape tests; keep behavior tests.
3. Add outcome classification only with deletions.
A generic classifier may expose `dispatch`, `pairing-required`,
`skip-activation`, `drop-command`, `drop-route`, `drop-sender`, and
`drop-ingress`. It must derive from the decision graph, not reason strings,
and migrate at least three plugins in the same patch.
4. Add route descriptor builders only with deletions.
Generic route target and route sender helpers are acceptable only if they
immediately shrink route-heavy plugins: Google Chat, IRC, Microsoft Teams,
Nextcloud Talk, Mattermost, Slack, Zalo, and Zalo Personal.
5. Add command/event presets only with deletions.
Centralize text-command, native-command, callback, and origin-subject shapes.
Command consumers must default to unauthorized when no command gate ran;
events must not start pairing.
6. Add identity presets only where they remove boilerplate.
Stable-id, stable-id-plus-aliases, phone/e164, and multi-identifier helpers
are allowed when raw values enter only adapter input and redacted state keeps
opaque ids/counts.
7. Share authorized turn assembly.
Outside the ingress kernel, remove repeated route/envelope/context/reply
scaffolding from QA Channel, IRC, Nextcloud Talk, Zalo, and Zalo Personal.
Core may own route/session/envelope/dispatch sequencing; plugins keep
delivery and channel-specific context.
8. Quarantine compatibility.
Deprecated SDK helpers stay source-compatible, but bundled hot paths must not
import deprecated ingress or command-auth facades. Compatibility tests should
use fake third-party plugins, not bundled-plugin internals.
9. Repack core.
After wrapper deletion, collapse one-use modules, remove unused exports, move
compatibility projection out of hot paths, and keep focused tests for identity,
route, command/event, activation, access groups, and compatibility shims.
## Deletion Waves
Run these in order. Each wave must lower bundled production LOC.
1. Wrapper collapse, expected plugin delta: -400 to -600.
Replace plugin-local `resolveXAccess`, `resolveXCommandAccess`, and
`accessFromIngress` result types with direct reads from
`ResolvedChannelMessageIngress`. First targets: Discord DM command auth,
Feishu policy, Matrix access state, Telegram ingress, Signal access policy,
QQBot SDK adapter.
2. Shared outcome helpers, expected plugin delta: -200 to -350.
Add one generic classifier only if it deletes repeated
`shouldBlockControlCommand`, pairing, activation skip, route block, and sender
block ladders across at least three plugins.
3. Route descriptor builders, expected plugin delta: -200 to -350.
Move repeated route target and route sender descriptor assembly into core
helpers. First targets: Google Chat, IRC, Microsoft Teams, Nextcloud Talk,
Mattermost, Slack, Zalo, Zalo Personal.
4. Turn assembly sharing, expected plugin delta: -250 to -450.
Use common route/session/envelope/dispatch sequencing for simple inbound
plugins. First targets: QA Channel, IRC, Nextcloud Talk, Zalo, Zalo Personal.
5. Core repack, expected core delta: -300 to -700.
After plugins consume runtime projections directly, delete one-use modules,
merge tiny files back into `runtime.ts` or focused siblings, and keep SDK
compatibility files separate from bundled hot paths.
6. Test pruning, expected test delta: -300 to -600.
Delete tests that only assert removed wrapper shapes. Keep behavior tests for
command denial, group fallback, origin-subject matching, activation skip,
access groups, pairing, and redaction.
Expected minimum landing shape after these waves:
```text
plugin production <= -1,500
core production about +1,800 to +2,200 before final repack
tests <= +500
total <= +2,000
```
## Do Not Move
Do not move platform config defaults, setup UX, doctor/fix copy, API lookups,
Slack owner-presence checks, Matrix alias/verification handling, Telegram
callback parsing, command syntax parsing, native command registration, reaction
payload parsing, pairing replies, command replies, acks, typing, media, history,
or logs.
## Verification
Targeted local loop:
```sh
pnpm lint:extensions:no-deprecated-channel-access
pnpm test src/channels/message-access/message-access.test.ts src/plugin-sdk/channel-ingress-runtime.test.ts src/plugin-sdk/access-groups.test.ts
pnpm test extensions/<changed-plugin>/src/...
pnpm plugin-sdk:api:check
pnpm config:docs:check
pnpm check:docs
git diff --check
```
Use Testbox for broad changed gates/full-suite proof once the LOC trend is
inside budget.
Each work package records:
- before/after LOC by category
- deleted plugin wrappers
- new core helper LOC, if any
- targeted tests run
- remaining hotspot list
## Exit Criteria
- bundled production imports no deprecated channel-access or command-auth facades
- compatibility code is isolated to SDK/core seams
- bundled plugins consume ingress projections or generic outcomes directly
- plugin production LOC is at least 1,500 net negative against `origin/main`
- core production LOC is `<= +1,500`, or any excess is paid for while total
stays `<= +2,000`
- representative tests cover redaction, route, command/event, activation,
access-group, and channel-specific fallback behavior