From 41a7c70cb1f1a69b3a7e5b805abd08d19700f569 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:08:35 +0800 Subject: [PATCH] docs: audit package limitations on current stack --- ...026-07-10-readme-known-limitations-gate.md | 2 +- packages/bash/bash-local/README.md | 2 + packages/bash/bash-sandbox/README.md | 7 +++ packages/bash/tool-bash/README.md | 4 +- packages/core/agent-core/README.md | 2 +- packages/core/agent-loop/README.md | 4 +- packages/core/agent/README.md | 3 +- packages/core/scope/README.md | 7 +++ packages/core/system-prompt/README.md | 5 +- packages/core/tools/README.md | 2 +- packages/fs/fs/README.md | 1 - packages/fs/tool-fs/README.md | 2 +- packages/sandbox/sandbox-local/README.md | 8 +++ packages/sandbox/sandbox/README.md | 7 +++ .../session-persistence/README.md | 2 +- packages/skill/skill-local/README.md | 9 ++- packages/skill/skill/README.md | 7 +++ packages/skill/tool-skill/README.md | 7 +++ packages/subagent/subagent-acp/README.md | 2 +- packages/subagent/subagent-fork/README.md | 1 - .../subagent/subagent-inprocess/README.md | 3 +- packages/subagent/subagent-spawn/README.md | 2 +- .../subagent/subagent-subprocess/README.md | 7 +++ packages/subagent/tool-subagent/README.md | 2 +- packages/support/subagent-mock/README.md | 8 ++- packages/ui/acp-agent/README.md | 7 ++- packages/ui/acp/README.md | 5 +- packages/ui/app-boot/README.md | 6 ++ packages/ui/stdio-agent/README.md | 5 +- packages/ui/user-approval/README.md | 7 +++ packages/util/timeout/README.md | 6 ++ packages/web/tool-web/README.md | 2 +- packages/workflow/tool-workflow/README.md | 10 ++- .../workflow/workflow-workerthread/README.md | 10 ++- packages/workflow/workflow/README.md | 10 ++- scripts/verify-readme-limitations.ts | 62 ++++++++++++------- 36 files changed, 180 insertions(+), 56 deletions(-) diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md index cc25c0efe7..d4814ce283 100644 --- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md +++ b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md @@ -8,7 +8,7 @@ The [documentation standard](../../../AGENTS.md) assigns limitations to the pack ## Decision -Every `packages///README.md` carries a canonical `## Known Limitations and Deferred Work` section: a condensed bullet list of consumer-visible gaps (unimplemented features, platform caveats, deliberate MVP cuts) and consciously postponed work (TODO markers, RFC deferrals still open). A `doc-sync` gate, `verify-readme-limitations` ([scripts/verify-readme-limitations.ts](../../../../scripts/verify-readme-limitations.ts)), enforces the shape per README: exactly one limitations-like heading, byte-equal to the canonical h2, with at least one top-level bullet. Near-miss headings at any level ("Limitations", "Deferred", "What is NOT here", …) fail the gate, so variant sections cannot creep back beside — or instead of — the canonical one. +Every package manifest under `packages///package.json` has a sibling README carrying a canonical `## Known Limitations and Deferred Work` section: a condensed bullet list of consumer-visible gaps (unimplemented features, platform caveats, deliberate MVP cuts) and consciously postponed work (TODO markers, RFC deferrals still open). A `doc-sync` gate, `verify-readme-limitations` ([scripts/verify-readme-limitations.ts](../../../../scripts/verify-readme-limitations.ts)), derives the package set from those manifests, rejects a missing README, and enforces the shape per README: exactly one limitations-like heading, byte-equal to the canonical h2, with at least one top-level bullet. Near-miss headings at any level ("Limitations", "Deferred", "What is NOT here", "Non-goals", …) fail the gate, so variant sections cannot creep back beside — or instead of — the canonical one. A package with genuinely nothing to declare is whitelisted (`NO_LIMITATIONS` in the script) and must NOT carry the section. The inverted check keeps the whitelist honest in both directions: an empty or boilerplate section cannot satisfy the gate, and giving a whitelisted package real limitations forces the whitelist edit in the same change. Whitelist entries are validated against the scanned package set, so a package rename or removal fails loud instead of silently un-gating a README. diff --git a/packages/bash/bash-local/README.md b/packages/bash/bash-local/README.md index 0a2c2ec5ec..ea4cc7dc74 100644 --- a/packages/bash/bash-local/README.md +++ b/packages/bash/bash-local/README.md @@ -28,9 +28,11 @@ Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; ## Known Limitations and Deferred Work - **Unconfined by itself** — this executor always runs commands with the harness process's authority; deployments needing confinement compose [`dsh-bash-sandbox`](../bash-sandbox/README.md), while per-call allow/deny/ask policy belongs on `tools/pre-execute`. +- **No persistent shell or PTY** — every call starts a fresh non-login `bash -c`; cwd-only persistence and interactive terminal sessions remain deferred until a real workflow requires them. - **POSIX-only** — the `bash` binary, detached process groups, group kills, and SIGTERM→SIGKILL escalation are hardcoded; Windows is unsupported. - **The credential scrub is a name heuristic** — `*KEY*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSWORD*`) pass through, and a whitelist for over-scrubbed vars is noted future work. - **Spill files are never deleted** — full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them. - **Finished background tasks are never evicted** — they stay in the task map, retaining their in-memory output tails, until executor disposal. +- **`OutputCollector.snapshot()` / `totalBytes` are test-shaped residuals** — the live poll path uses `readFrom()` and a marked cleanup can inline the final snapshot and remove the unused public getter. The raw process handling lives in `src/run.ts`; `src/index.ts` is the service wiring. diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index 2a5ae63b8e..9fdd223be5 100644 --- a/packages/bash/bash-sandbox/README.md +++ b/packages/bash/bash-sandbox/README.md @@ -31,3 +31,10 @@ Deny-only at the seam: a denial is a reported fact, and this executor never nego ``` The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, and `tests/seatbelt.e2e.ts` (the real provider + real runner driven through `ctx.bash`, world-verified, each self-skipping where its runner is absent); see [`examples/sandbox-acp-agent`](../../../examples/sandbox-acp-agent/) for the runnable demo. + +## Known Limitations and Deferred Work + +- **Confinement covers file effects only** — network access and process visibility are unchanged, so the modes are not a general-purpose security sandbox. +- **Denials are inferred from failed-command stderr** — backend signatures make the inference portable, but a matching application error can be classified as a denial and a denial omitted from the retained tail can be missed. +- **A background runner failure has no immediate error channel** — it is recorded on the settled task and surfaces when the caller polls with `bash_output`. +- **`danger-full-access` deliberately bypasses `ctx.sandbox`** — it is an explicit unconfined mode, not a wider sandbox profile. diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index 6251b659fb..f3168e00f0 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -38,7 +38,7 @@ The owning agent's session token (`session.header.id`) is stamped onto the task ## UI presentation -These tools own how their calls render in a UI (an editor's tool-call card) via the `dsh-tools` `presentCall`/`presentResult` seam, each returning a `card`-tagged render intent — a UI never special-cases tool names. A FOREGROUND `bash` run declares a **terminal card**: `presentCall` returns `{ card: 'terminal', title, description?, cwd? }` — the **title** is the exact `command` ("ls -la src"), the model-written `description` rides along (rendered ABOVE the card), and `cwd` comes from the model `workdir` when given (absolute as-is, relative for the UI bridge to resolve against the session cwd; else left for the bridge to fill from the session cwd) — and `presentResult` returns `{ card: 'terminal', title?, output?, exitCode?, signal? }` carrying the raw output plus the parsed `exitCode`/`signal`, so a capable client (Zed) renders a terminal card with an exit-status pill. The result carries the raw `output`; the bridge DERIVES the ` ```console ` fenced fallback for a no-terminal-capability UI (the tool no longer encodes the fences itself), so the model-facing result text stays unfenced. A `run_in_background` call is NOT a terminal (it returns a task id immediately and never streams a terminal — poll with `bash_output`) and instead returns a **generic card** (`{ card: 'generic', title, kind: 'execute', rawInput: command, content: [description] }`); an `isError` result (spawn failure / abort) likewise returns a `generic` result view with no exit pill (there is no real process exit). `bash_output`/`bash_kill` return a `generic` card with a task-scoped title ("Read output from background task bash-3" / "Kill background task bash-3") and the task id as rawInput. These methods are pure/display-only (they also run on `session/load` replay), and a malformed/older logged arg shape falls back to a generic presentation rather than throwing. See `packages/core/tools` ("Tool-owned UI presentation") and `packages/ui/acp` ("Terminal card" / "Tool-call presentation"). +These tools own how their calls render in a UI (an editor's tool-call card) via the `dsh-tools` `presentCall`/`presentResult` seam, each returning a `card`-tagged render intent — a UI never special-cases tool names. A FOREGROUND `bash` run declares a **terminal card**: `presentCall` returns `{ card: 'terminal', title, description?, cwd? }` — the **title** is the exact `command` ("ls -la src"), the model-written `description` rides along (rendered ABOVE the card), and `cwd` comes from the model `workdir` when given (absolute as-is, relative for the UI bridge to resolve against the session cwd; else left for the bridge to fill from the session cwd) — and `presentResult` returns `{ card: 'terminal', title?, output?, exitCode?, signal? }` carrying the raw output plus the parsed `exitCode`/`signal`, so a capable client (Zed) renders a terminal card with an exit-status pill. The result carries the raw `output`; the bridge DERIVES the ` ```console ` fenced fallback for a no-terminal-capability UI while the tool keeps model-facing result text unfenced. A `run_in_background` call is NOT a terminal (it returns a task id immediately and never streams a terminal — poll with `bash_output`) and instead returns a **generic card** (`{ card: 'generic', title, kind: 'execute', rawInput: command, content: [description] }`); an `isError` result (spawn failure / abort) likewise returns a `generic` result view with no exit pill (there is no real process exit). `bash_output`/`bash_kill` return a `generic` card with a task-scoped title ("Read output from background task bash-3" / "Kill background task bash-3") and the task id as rawInput. These methods are pure/display-only (they also run on `session/load` replay), and a malformed/older logged arg shape falls back to a generic presentation rather than throwing. See `packages/core/tools` ("Tool-owned UI presentation") and `packages/ui/acp` ("Terminal card" / "Tool-call presentation"). ## Background completion notices @@ -62,3 +62,5 @@ Under a sandboxing executor this plugin makes the session's standing mode overri - **Replay exit pills parse from result text** — output whose final line happens to be exactly `[exit code: N]` / `[killed by signal: …]` shows a wrong pill on session replay; a display-only known residual. - **The bash tools opt out of `timeout-policy` budgets** — `bash` keeps the executor-owned `BASH_TIMEOUT` path and `bash_output`/`bash_kill` declare no budget, per [the tool-call timeout-policy RFC](../../../docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). +- **Completion notices do not wake an idle agent** — they become durable context for the next request; a caller needing progress now must poll `bash_output` or send another message. +- **Tasks started outside an agent have no ownership fence** — their predictable ids are readable and killable by any caller; only agent-started tasks carry a session owner token. diff --git a/packages/core/agent-core/README.md b/packages/core/agent-core/README.md index 28e75a9d80..af660bf2b3 100644 --- a/packages/core/agent-core/README.md +++ b/packages/core/agent-core/README.md @@ -2,7 +2,7 @@ The **default executor-less, UI-less agent spine** as ONE Cordis bundle plugin. It loads the fixed set of services every harness agent needs, including the local skill provider, and forwards the loop's `agents` list as its own config — so an app package composes a working agent by adding only a front door and the swappable backends. -This is the package to read to see **the whole plugin tree at once** — the teaching role the inlined `echo-agent` `cordis.yml` used to play before the spine moved behind this bundle. +This is the package to read to see **the whole shared plugin tree at once**: the teaching overview of the spine behind every app package. ## The tree it loads diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 1004083829..31bf9bf008 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -36,7 +36,7 @@ interface Config { } ``` -Agents listed in config are auto-created at startup. `cwd` applies only to fresh config-created sessions; `resumeSessionId` keeps the persisted session header. There is no per-agent persona: the deployment persona is `dsh-system-prompt`'s own `persona` config, shared by every agent in the context. The plugin registers the built-in `model`/`cwd` prompt variables on `ctx.systemPrompt`, resolved per step from the `assemble({ agent })` context — runtime facts of the agents THIS loop drives, unlike the `harness:identity`/`deployment:persona` sections, which live on `dsh-system-prompt` so they survive a swapped loop plugin. +Agents listed in config are auto-created at startup. `cwd` applies only to fresh config-created sessions; `resumeSessionId` keeps the persisted session header. Config agents have no per-agent persona field: they use `dsh-system-prompt`'s deployment default, while programmatic factory callers can register an agent-scoped `deployment:persona` shadow in `setup`. The plugin registers the built-in `model`/`cwd` prompt variables on `ctx.systemPrompt`, resolved per step from the `assemble({ agent })` context — runtime facts of the agents THIS loop drives, unlike the `harness:identity`/default `deployment:persona` sections, which live on `dsh-system-prompt` so they survive a swapped loop plugin. ### Exported concrete class @@ -107,6 +107,6 @@ Everything that goes beyond "call the model, run the tools, repeat" belongs to p - **Tool calls within a step execute sequentially** — parallel execution waits on concurrency-safety metadata in the tool contract (see `dsh-tools`). - **No resume-or-create policy on the config path** — config-driven `create()` starts a fresh `${id}-session-` every run (`TODO(demo)`), and a config `resumeSessionId` whose resume fails logs a warning and creates no agent. -- **Config agents carry no session `cwd`** — `Config` has no cwd field, so a persona referencing `{{cwd}}` fails prompt assembly for config-created agents. +- **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona/tool composition is available only through the programmatic `ctx.agents.create()` / `resume()` factory options. - **No built-in turn budget** — the default continuation is `continue` whenever a step had tool calls or steering; bounding a runaway turn requires an `agent/turn-continuation` force-stop plugin. - **`runLoop`/`Inbox`/`InboxMessage` stay exported with no outside consumer** — [removal is proposed](../../../docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md). diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index c31eb3680c..c75ce874fb 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -55,9 +55,8 @@ The handle every plugin programs against: ## Known Limitations and Deferred Work - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. -- **`agent/session-start` cannot gate startup** — a synchronous, veto-less emit, so an async listener's injection is best-effort before turn 1; startup gating is a deferred loop-level change. +- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. - **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). -- **`AgentRegistry` enforces unique agent ids, not session ids** — two live agents sharing a session id mis-route bash owner-token notices; [id unification stays proposed](../../../docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.md). - **`HookContext` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. - **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`). - **`agent/pre-step`'s `fullSystemPrompt`/`sessionPrefix` parameters are a flagged smell** — compaction is their only consumer; a lazy prompt provider or a compaction-specific pressure seam is the marked revisit. diff --git a/packages/core/scope/README.md b/packages/core/scope/README.md index e57f475816..f76254a406 100644 --- a/packages/core/scope/README.md +++ b/packages/core/scope/README.md @@ -19,3 +19,10 @@ Scoped-context registration primitive. `createScope(ctx, key)` mints a Cordis co Ownership and visibility derive from ONE fact — which context a registration went through. An explicit `{ scope }` registration parameter could express "visible to X, disposed with Y", which is almost always a bug; the scoped context makes it unrepresentable. Rationale and alternatives: [the agent-scope RFC](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md). Handing out a scoped context hands out the minting plugin's service-resolution capability (resolution walks the minting fiber's dependency chain, not the holder's) — mint scopes from a plugin whose `inject` surface is what scope holders should reach. + +## Known Limitations and Deferred Work + +- **Only scope-aware surfaces isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context. +- **A context carries one nearest scope key** — nested scopes shadow their parent's tag rather than forming hierarchical or multi-membership policy sets. +- **Dispatch carriers preserve behavior, not identity** — listener `this` can call the subject's methods, but `this !== subject` and a method read returns a newly bound function. +- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected service surface, so a broader minter cannot later be narrowed by the holder. diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index f55df539d3..a3d4c0bc24 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -6,7 +6,7 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool- | Key | Default | Meaning | |---|---|---| -| `persona` | `''` | The deployment persona: the ONE deployment-authored prompt fragment, rendered as the order-0 `deployment:persona` section and shared by every agent in the context (subagents included). A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | +| `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | | `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `''` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md). | ## Service: `SystemPrompt` (ctx key: `systemPrompt`) @@ -45,9 +45,8 @@ Design rationale: [the prompt-variables RFC](../../../docs/rfc/implemented/archi ## Known Limitations and Deferred Work -- **No deployment-authored prompt text outside config** — the persona is this plugin's `persona` config, and every other section comes from the plugin that owns the fact. (The `harness:identity` line is deliberately a code literal: a harness fact, not a deployment choice; the `system-prompt/assemble` waterfall is the escape valve for a deployment that must drop it.) +- **Deployment-authored prompt text is config/composition only** — this plugin owns the global persona default, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API. - **No prompt compaction here** — it belongs on the `agent/pre-step` seam in `dsh-agent` (implemented by `dsh-compact-basic`). - **No escape syntax for literal `{{…}}` braces** — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one. -- **`persona` is context-global** — one deployment fragment shared by every agent, subagents included; per-agent personas need a `system-prompt/assemble` listener. - **`toolOrder` misconfiguration surfaces at prompt assembly (the first turn), not at boot** — only shape violations throw at config load. - **Sections sharing an `order` value tie-break by registration order** — a plugin-load artifact; determinism relies on the distinct-order band convention, unlike the canonicalized tool order. diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index b46a109679..63a094ce97 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -146,6 +146,6 @@ The wire collapse is the registry's own contribution (`systemPrompt.tools()` is - **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). - **`defineTool`'s schema DSL is a deliberate subset** — string/number/boolean/object/array with string-only `enum`; `validateArgs` tolerates extra keys and never applies `default` (`XXX(unused-default)` flags removing that field); raw-registered JSON-Schema tools validate their own input. - **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper. -- **Code Mode is TypeScript-only and registry-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`, and per-tool visibility tiers (one tool native, another code-only) are knowingly deferred. +- **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only. - **Code Mode bindings return text only** — non-text content blocks in a sub-call result collapse to `[ content]` placeholders. - **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md). diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index 9f5ce3d950..044ed53364 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -47,6 +47,5 @@ This package declares three events (see the generated [events catalog](../../../ - **Text-only by contract** — backends reject binary/non-UTF-8 content with `FS_NOT_TEXT`; binary-safe operations are a deliberate deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md). - **Seven primitives only** — no delete, rename/move, copy, or watch; `listDir` is single-level, with recursion, globbing, pagination, and search out of scope per [the directory-listing RFC](../../../docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md). -- **`listDir` has no in-repo consumer yet** — skill discovery and a model-facing `ls` tool are the named follow-ups; sessions list directories via `bash` meanwhile. - **No IO deadline** — the seam arms no timeout; cancellation is a best-effort optional `AbortSignal` per primitive (the deliberate [fs-family stance](../README.md)). - **Resolve-then-operate costs a remote backend two round-trips per tool call** — folding or caching resolution is left to such a backend. diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 88a0e9663a..f3988d6ae7 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -50,6 +50,6 @@ The read rendering (line windowing + output formatting) lives in `src/read-rende ## Known Limitations and Deferred Work -- **No directory-listing, glob, grep, or search tools ship** — a deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md) (and `ctx.fs.listDir` has no tool consumer yet); models fall back to `bash`. +- **No directory-listing, glob, grep, or search tools ship** — a deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md); `ctx.fs.listDir` serves provider code such as skill discovery but still has no model-facing consumer, so models fall back to `bash`. - **`read` handles UTF-8 text files only** — binary-safe reads and PDF/image/multimodal content are deferred; a directory target is `FS_NOT_REGULAR_FILE`. - **No timeout surface** — `read`/`write`/`edit` take no timeout argument and declare no `timeout-policy` budget; cancellation rides `exec.signal` only (the deliberate [fs-family stance](../README.md)). diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index abf7511438..0d6f70651c 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -16,3 +16,11 @@ Every rung has its keyless world-proof (`tests/bwrap.e2e.ts`, `tests/landlock.e2 ``` Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/); see [`examples/sandbox-acp-agent`](../../../examples/sandbox-acp-agent/) for the runnable composition. + +## Known Limitations and Deferred Work + +- **Windows has no runner** — `win32` fails closed with `SANDBOX_UNAVAILABLE`; an AppContainer-family backend is deferred. +- **Landlock may be partial** — older supported kernel ABIs confine only the access classes they expose, reported as `enforcement: 'partial'` rather than overstated as full. +- **Seatbelt depends on deprecated `sandbox-exec`** — macOS still ships it, but this provider cannot replace or probe that private policy engine if Apple removes it. +- **Runner selection is cached for the provider lifetime** — installing, removing, or repairing a runner requires reloading the plugin before selection changes. +- **`runnerCommand` is an operator assertion** — a configured custom runner skips functional probes and is assumed to implement the bwrap-shaped profile honestly. diff --git a/packages/sandbox/sandbox/README.md b/packages/sandbox/sandbox/README.md index ef381625b2..c552b7846e 100644 --- a/packages/sandbox/sandbox/README.md +++ b/packages/sandbox/sandbox/README.md @@ -9,3 +9,10 @@ Policy rides the call, not the provider: two consumers may confine under differe **Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). Implementations: [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/) (wraps `['bash', '-c', command]`). + +## Known Limitations and Deferred Work + +- **File effects are the whole policy vocabulary** — the seam expresses no network, process, syscall, device, or credential restrictions. +- **Same-world confinement only** — containers, microVMs, and remote execution require replacing capability implementations rather than adding a provider here. +- **Denial reporting is a stderr dialect** — the seam returns backend signatures instead of a typed runtime denial channel, so consumers that need classification must infer it from the child process's output. +- **One provider per context** — composing different sandbox mechanisms simultaneously requires a provider-level ladder or separate Cordis contexts; callers choose policy per call, not backend identity. diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index 9a96515082..92a432d44b 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -24,7 +24,7 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l The two first-party backends were byte-identical (or same-algorithm) for ALL of their write-path orchestration — the in-memory bookkeeping (per-id state, write-behind buffers, per-id serialization chains, per-session init promises), the `session/event` → buffer → `session/flush` drain, lazy materialization, crash-tail repair on load, the four `session/created` adoption cases (new / HMR-adopt / collision / ownerless-claim), and dispose-time quiescence. Only the STORAGE primitives differed (write bytes vs. INSERT rows). -`PersistenceCoordinator` owns that orchestration once. A first-party backend composes one (`new PersistenceCoordinator(ctx, this)`), implements the small `PersistenceBackend` hook interface, and delegates its four public service methods to the coordinator. This keeps the duplicated, correctness-heavy orchestration in a single place (it used to receive the same fixes twice). +`PersistenceCoordinator` owns that orchestration once. A first-party backend composes one (`new PersistenceCoordinator(ctx, this)`), implements the small `PersistenceBackend` hook interface, and delegates its four public service methods to the coordinator. The correctness-heavy orchestration therefore has one implementation and one place for fixes. The `PersistenceBackend` hooks (the only seam between the coordinator and storage): diff --git a/packages/skill/skill-local/README.md b/packages/skill/skill-local/README.md index c885416ff5..77814bdbae 100644 --- a/packages/skill/skill-local/README.md +++ b/packages/skill/skill-local/README.md @@ -28,10 +28,17 @@ Default roots are resolved in this provider's rank order: | 400 | `user-dsh` | `/skills` | | 500 | `user-agents` | `/skills` | -The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. The user DSH root skips its `.system` child so system-owned directories are not accidentally treated as normal user skills. DeepSeek Harness no longer ships built-in system skills from this provider; additional built-ins can be supplied later by another provider. +The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. The user DSH root skips its `.system` child so system-owned directories are not accidentally treated as normal user skills. This provider ships no built-in system skills; another provider can supply embedded or remote built-ins. When `ctx.fs` is available, discovery lists roots through `ctx.fs.listDir`, reads skill files through `ctx.fs.readText`, and probes `.git` through the filesystem service. Full skill loads forward the lookup abort signal to filesystem metadata and content reads. Without a filesystem service, the provider falls back to abortable Node filesystem I/O so minimal local contexts can still load skills. Missing, unreadable, or malformed skill files warn and skip instead of failing the whole request. ## Skill Format Skills can be single-level directory bundles (`/SKILL.md`) or flat Markdown files (`.md`). Nested `**/SKILL.md` discovery is intentionally not part of v1. Frontmatter is parsed as YAML with the `yaml` package; it requires `name` and `description`, while `whenToUse`, `disableModelInvocation`, and `metadata` are optional. Names must be kebab-case. + +## Known Limitations and Deferred Work + +- **Discovery is one level deep** — only `//SKILL.md` and `/.md` are recognized; nested skill trees and package manifests are ignored. +- **Project scope is the nearest `.git` ancestor** — workspaces without that marker fall back to the supplied cwd, with no alternate project-root marker or monorepo subproject selection. +- **Unreadable or malformed entries disappear with a warning** — the model catalog receives no per-skill diagnostic and cannot distinguish an absent skill from a skipped one. +- **No filesystem watching** — edits rely on the registry cache being evicted or invalidated by provider reload before a previously collected cwd is rediscovered. diff --git a/packages/skill/skill/README.md b/packages/skill/skill/README.md index 33d16e1b69..eeddc9dd11 100644 --- a/packages/skill/skill/README.md +++ b/packages/skill/skill/README.md @@ -32,3 +32,10 @@ The registry validates candidate names, descriptions, ranks, and provider owners ## Consumer boundary The registry does not render model guidance or register model-facing tools. [`@deepseek-ai/dsh-tool-skill`](../tool-skill) consumes `ctx.skills` to provide the session-prefix catalog and `skill` tool, so providers remain independent of the model surface. + +## Known Limitations and Deferred Work + +- **Completed catalogs have no TTL or watcher invalidation** — a provider's underlying files or remote data can change without a registration revision, so a cached cwd stays stale until eviction or provider/runtime reload. +- **Providers are queried sequentially** — one slow cooperative provider delays every provider registered after it; cancellation stops the caller's wait but cannot terminate work an uncooperative provider keeps running. +- **A provider-list failure removes that whole source for the request** — the registry logs and skips it, with no model-visible diagnostic or partial-catalog recovery contract. +- **Duplicate resolution is first-wins** — later lower-priority candidates are logged and hidden; there is no API to inspect all shadowed definitions. diff --git a/packages/skill/tool-skill/README.md b/packages/skill/tool-skill/README.md index 7e72f89000..ce65c9ce3a 100644 --- a/packages/skill/tool-skill/README.md +++ b/packages/skill/tool-skill/README.md @@ -19,3 +19,10 @@ The plugin contributes one user-role `` catalog through `agent/ Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers can resolve the right winning skill. A successful call returns one text tool result with ``, containing `` followed by ``. Resource guidance resolves paths or URLs explicitly referenced by the loaded instructions against `resourceBase`; referenced scripts, references, and assets load only when needed, and the tool does not enumerate a skill directory. Local filesystem skills provide a base directory, while remote or embedded providers can provide a URL or opaque provider-managed guidance. A name that cannot be resolved reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation: true` retain distinct `isError` results. The tool does not call `agent.inject()` in v1. Its result is already recorded as the tool result and becomes available to the next model step without duplicating the content as synthetic context. + +## Known Limitations and Deferred Work + +- **The catalog omits `whenToUse`, source, and provider metadata** — routing is based only on name and a capped description; `whenToUse` remains provider metadata and is not rendered by the loaded wrapper either. +- **Loaded instruction bodies have no size cap** — a provider can return a skill large enough to consume substantial next-step context; only catalog descriptions are truncated. +- **Resources are guidance, not attachments** — the tool reports a base directory/URL/opaque hint but neither enumerates nor fetches referenced files for the model. +- **Loading is one-shot text** — there is no partial, streaming, or cached-content handle when a remote provider is slow or a skill body is large. diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index db2b59d412..ca331796c8 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -73,7 +73,7 @@ Named `name` / `inject` / `Config` / `apply`, with **no default export**: the co ## Known Limitations and Deferred Work - **A fresh process per run** — persistent-process pooling is a future optimization ([the seam RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)). -- **No start-time capability enforcement** — an out-of-process child cannot honor the parent's `outputSchema`/`depthLimit`/`toolFilter`, so the provider advertises none and `request.parent` is ignored. +- **No start-time capability enforcement** — an out-of-process child cannot honor the parent's `outputSchema`/`depthLimit`/`toolFilter`/`persona`, so the provider advertises none and `request.parent` is ignored. - **Only `agent_message_chunk` text is collected** — the child's tool-call activity, thought chunks, and plan updates are not surfaced to the parent. - **Permission prompts are auto-answered** (`permission: allow | reject`) — no human is surfaced a child's `session/request_permission` in this cut. - **No snapshot-tier replay coverage** (`TODO(acp-subagent-replay)`) — an ACP child is its own process with its own replay shape, deferred. diff --git a/packages/subagent/subagent-fork/README.md b/packages/subagent/subagent-fork/README.md index a6e2785795..dcad34e80e 100644 --- a/packages/subagent/subagent-fork/README.md +++ b/packages/subagent/subagent-fork/README.md @@ -24,6 +24,5 @@ See [`dsh-subagent-spawn`](../subagent-spawn/README.md) for the run lifecycle, m ## Known Limitations and Deferred Work -- **Tool-scoping (`toolFilter`) is not supported** — declared `false`, so the service rejects a request needing it before `start` runs. - **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. - **The seed is a one-time snapshot** — the child sees the parent's completed turns as of the fork and nothing the parent logs afterwards; there is no live context sharing. diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index 6084fe569c..5f952a6c8b 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -40,6 +40,5 @@ Thrown by `startInProcessRun` when a spawn would exceed the request's `maxDepth` ## Known Limitations and Deferred Work -- **The structured-output runtime is context-global** — the tool registry and prompt assembly are context-wide while schemas differ per concurrent child, hence the final-assembly enforcement dance; per-agent/per-session scoping would dissolve it (the module-doc `FIXME`). - **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. -- **`toolFilter` is unimplemented in this driver** — both in-process backends declare it `false`; scoping a child's tool set is deferred. +- **Structured capture accepts the `defineTool` schema subset only** — unsupported JSON Schema constructs fail before the child is created; a provider needing a broader schema vocabulary requires a different runtime. diff --git a/packages/subagent/subagent-spawn/README.md b/packages/subagent/subagent-spawn/README.md index 0a3a17f087..4a86a003fd 100644 --- a/packages/subagent/subagent-spawn/README.md +++ b/packages/subagent/subagent-spawn/README.md @@ -20,5 +20,5 @@ The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../ ## Known Limitations and Deferred Work -- **Tool-scoping (`toolFilter`) is not supported** — the capability is declared `false`, so the service rejects a request needing it before `start` runs; scoping a child's tool set is deferred. - **Runs expose no `sendMessage`/`resume`** — the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. +- **Fresh means no parent transcript** — the child inherits cwd, lineage, model, and explicitly configured persona/tool restrictions, but none of the parent's conversation; use the fork provider when completed-turn context is required. diff --git a/packages/subagent/subagent-subprocess/README.md b/packages/subagent/subagent-subprocess/README.md index ccc68bf31b..f8ddce64c4 100644 --- a/packages/subagent/subagent-subprocess/README.md +++ b/packages/subagent/subagent-subprocess/README.md @@ -38,3 +38,10 @@ A per-run isolated config directory for an external CLI child (the target of `CL ## Testing `tests/subagent-subprocess.spec.ts`: the env scrub and config-dir helpers run against the real process env and real filesystem (the rm-failure path injects its rejection at the fs boundary — a real recursive-rm failure is not portably provokable, and root ignores permission bits); the exit waits and the dispose ladder run against a scriptable fake child, driving each escalation tier deterministically. The [ACP backend suite](../subagent-acp/README.md) exercises the same ladder against real subprocesses (EOF-cooperative, EOF-ignoring, and SIGTERM-trapping children) end to end. + +## Known Limitations and Deferred Work + +- **The credential scrub is name-based** — only variables matching `KEY` / `SECRET` / `TOKEN` are removed; differently named secrets such as `PASSWORD` pass through unless the backend supplies a stricter environment. +- **Signals target the direct child only** — teardown relies on a cooperative CLI to reap its descendants before exit; a re-parented or independently detached grandchild can outlive the ladder. +- **Fresh config-dir cleanup is best-effort** — an `rm` failure leaves private state under the OS temp root rather than failing disposal. +- **Pinned config directories are wholly operator-owned** — the helper neither creates, validates, locks, nor removes them, so concurrent runs may share and race on that state. diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 18ebea6ada..e7ff177c90 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -29,4 +29,4 @@ Background / poll collection is deferred (see the [RFC](../../../docs/rfc/implem - **Delegation blocks the parent turn** — synchronous collect only; background start + poll collection is deferred to the long-running-runtime redesign. - **Duplicate `toolName` across waiting loads is detected late** (`TODO(subagent-dup-toolname)`) — two loads waiting on providers collide only when a provider arrives, and the throw rolls back the provider's fiber rather than the misconfigured tool's; config-time detection needs a cross-fiber registry of intended names. -- **No per-child persona or prompt shaping** — `agentOptions` carries only `model`; the deployment persona is context-wide, and per-child system-prompt variation has no seam yet. +- **Child policy is fixed per tool registration** — `model`, persona, tool filter, and depth cap come from this plugin load's config, not model-call arguments; exposing another policy requires another distinctly named tool. diff --git a/packages/support/subagent-mock/README.md b/packages/support/subagent-mock/README.md index af169ed4c2..89b69c9ccb 100644 --- a/packages/support/subagent-mock/README.md +++ b/packages/support/subagent-mock/README.md @@ -13,8 +13,14 @@ Load it as a plugin (functional shape: `name`/`inject`/`Config`/`apply`, no defa | `name` | `mock` | Registry name to register the provider under. | | `reply` | `mock subagent reply` | The scripted child's final answer text. | | `stopReason` | `completed` | The stop reason `result` settles with. | -| `capabilities` | all `true` | Which start-time capabilities (`outputSchema`/`depthLimit`/`toolFilter`) the provider advertises. | +| `capabilities` | all `true` | Which start-time capabilities (`outputSchema`/`depthLimit`/`toolFilter`/`persona`) the provider advertises. | | `inheritsParentContext` | `false` | The context contract to declare; `true` exercises the fork-shaped tool wording in consumer tests. | | `structured` | `{ reply }` | Structured value surfaced when a request carries an `outputSchema` and the capability is on. | A `cancel()` issued before `result` settles flips the stop reason to `aborted`, so the cancellation path is observable. + +## Known Limitations and Deferred Work + +- **Scripted provider only** — it does not run a model, create a child agent, or exercise real prompt/tool-loop behavior. +- **One immediate synthetic outcome per run** — it models no multi-turn, streaming, readiness delay, or subprocess transport behavior. +- **`dispose()` is a no-op** — lifecycle tests using it prove consumer control flow, not resource teardown or quiescence of a real backend. diff --git a/packages/ui/acp-agent/README.md b/packages/ui/acp-agent/README.md index ed83bfaf4d..2e38e5a8cc 100644 --- a/packages/ui/acp-agent/README.md +++ b/packages/ui/acp-agent/README.md @@ -15,6 +15,7 @@ stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log (the bridge advertises `loadSession`) | | `@deepseek-ai/dsh-acp` | the bridge that owns stdout for JSON-RPC and provides ACP-backed user answers when a leaf explicitly exposes a user-question tool | | ~~`@deepseek-ai/dsh-tool-ask-user`~~ | **omitted by default** — ACP elicitation support is still client-dependent, so leaves must opt in deliberately | +| ~~`@deepseek-ai/dsh-user-approval`~~ | **omitted by default** — permission policy is deployment-specific; sandbox/approval leaves opt in and the ACP bridge then supplies the answerer | | ~~console logger~~ | **omitted** — it writes to stdout and would corrupt the protocol frames ([the stdout-purity footgun](../acp/README.md)) | | ~~`hmr`~~ | **omitted** — the editor owns the subprocess | @@ -27,6 +28,8 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron | `model` | (required) | the per-session agent template the bridge creates agents from | | `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` | | `toolOrder` | — | explicit model-facing tool order (a name list with one `''` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` | +| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-core` | +| `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-core` | | `persistenceRoot` | `./.sessions` | the JSONL backend's root directory | The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay) and a bash executor (`bash-local`). @@ -45,4 +48,6 @@ All diagnostics go to **stderr** — stdout is the protocol. ## Known Limitations and Deferred Work -- **The front-door cluster is fixed in code** — only `model`/`persona`/`toolOrder`/`persistenceRoot` are config; swapping the baked JSONL persistence for another backend, or adding the deliberately-omitted `tool-ask-user`, means a leaf-level sibling entry or a differently-composed app package. +- **JSONL persistence is baked in** — config chooses its root but cannot select a different backend; that requires a sibling entry or differently composed app package. +- **User-question and approval mechanisms are omitted by default** — the bridge can answer both when their services/tools are composed, but this front door does not enable those deployment policies itself. +- **A leaf can still corrupt stdout** — the app mounts no console logger, but it cannot prevent a sibling leaf entry from writing non-protocol bytes to the ACP channel. diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 465f0a42e6..c657d6c11f 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -46,7 +46,7 @@ Background-task isolation rides on `dsh-tool-bash`: bash task ids are global and ## Per-session cwd -Each session runs in its own workspace, recorded as the session's `SessionHeader.cwd`. On `session/new` the (absolute) request `cwd` becomes that header cwd; on `session/load` the resumed session keeps its PERSISTED header cwd and the request `cwd` must be absolute and equal to it, so the editor and bash executor agree on the workspace before an agent is constructed. A load whose persisted session has no absolute cwd is REJECTED up front via a metadata-only `list()` check, BEFORE resume constructs an agent (else bash would silently fall back to the server's launch dir, and a post-resume reject would leak the registered agent). `dsh-tool-bash` then defaults the bash workdir to the calling agent's `session.header.cwd` (an explicit model `workdir` still wins; a relative one resolves against the session cwd; with no session cwd the executor falls back to its own config / `process.cwd()`). So the server no longer has to be launched in the workspace — an editor can open any project folder, and N sessions over one connection can each target a different directory. (`additionalDirectories` is still rejected: widening the tool/filesystem scope beyond the single cwd is a separate sandbox concern.) +Each session runs in its own workspace, recorded as the session's `SessionHeader.cwd`. On `session/new` the (absolute) request `cwd` becomes that header cwd; on `session/load` the resumed session keeps its PERSISTED header cwd and the request `cwd` must be absolute and equal to it, so the editor and bash executor agree on the workspace before an agent is constructed. A load whose persisted session has no absolute cwd is REJECTED up front via a metadata-only `list()` check, BEFORE resume constructs an agent (else bash would silently fall back to the server's launch dir, and a post-resume reject would leak the registered agent). `dsh-tool-bash` then defaults the bash workdir to the calling agent's `session.header.cwd` (an explicit model `workdir` still wins; a relative one resolves against the session cwd; with no session cwd the executor falls back to its own config / `process.cwd()`). The server may be launched outside every workspace: an editor can open any project folder, and N sessions over one connection can each target a different directory. (`additionalDirectories` is still rejected: widening the tool/filesystem scope beyond the single cwd is a separate sandbox concern.) ## Tool-call presentation @@ -67,7 +67,7 @@ A tool whose call IS a shell command (`bash`) can render as a real **terminal ca - `tool_call`: `content:[…, {type:'terminal', terminalId}]` + `_meta.terminal_info.{terminal_id, cwd}` — the terminal id is the harness `callId`; the cwd is the card's explicit absolute `cwd`, else a relative `cwd` resolved against the session cwd, else the session's workspace cwd (the bridge fills the default, since the pure tool presenter can't see it). The card's `description` renders as a content block BEFORE the terminal block, so the description sits above the card. - `tool_call_update`: `_meta.terminal_output.{terminal_id, data}` (the terminal card's `output`) plus `_meta.terminal_exit.{terminal_id, exit_code | signal}` when the card reported a structured `exitCode`/`signal`. In terminal mode the update's `content` is OMITTED — an ACP `tool_call_update.content` REPLACES the call's content, so sending the fenced text block would clobber the terminal content block from the call. -When the client does NOT advertise the capability, none of the `_meta`/terminal content is emitted: the `tool_call` shows the `description` content block and the `tool_call_update` carries a ` ```console ` text block the bridge DERIVES by fencing the terminal result's `output` (the tool no longer double-encodes the fences) — so a non-Zed client is never worse off. The `_meta` object is ACP's spec-blessed extensibility point; the specific `terminal_info`/`terminal_output`/`terminal_exit` keys are a Zed convention, not the ACP `terminal/create` sub-protocol (which would make the editor execute the command, bypassing `dsh-bash`'s sandbox/env-scrub/ownership/cwd). Live incremental streaming and command classification are follow-ups. See [the terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [the render-intent-union RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). +When the client does NOT advertise the capability, none of the `_meta`/terminal content is emitted: the `tool_call` shows the `description` content block and the `tool_call_update` carries a ` ```console ` text block the bridge DERIVES by fencing the terminal result's unfenced `output` — so a non-Zed client is never worse off. The `_meta` object is ACP's spec-blessed extensibility point; the specific `terminal_info`/`terminal_output`/`terminal_exit` keys are a Zed convention, not the ACP `terminal/create` sub-protocol (which would make the editor execute the command, bypassing `dsh-bash`'s sandbox/env-scrub/ownership/cwd). Live incremental streaming and command classification are follow-ups. See [the terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [the render-intent-union RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). ## Settle-exactly-once @@ -87,6 +87,7 @@ Teardown reaches quiescence: for EVERY live session settle any pending prompt as - **Prompt content is `text` + `resource_link` only** — image, audio, and embedded-resource blocks are rejected, as is a non-empty `mcpServers` list at `session/new`. - **One configured `model` for every created session** — per-session model selection has no config or protocol surface here yet. - **Terminal cards render completed output** — live incremental streaming and command classification are named follow-ups of [the terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). +- **Permission answers are one-shot only** — the bridge offers `allow_once` / `reject_once`; durable `allow_always` grants and their storage/revocation policy remain deferred to the approval seam. ## stdout is the protocol diff --git a/packages/ui/app-boot/README.md b/packages/ui/app-boot/README.md index 92fbc333be..c31ff751e4 100644 --- a/packages/ui/app-boot/README.md +++ b/packages/ui/app-boot/README.md @@ -13,3 +13,9 @@ Shared boot glue for the app bins ([`dsh-stdio-agent`](../stdio-agent/README.md) Two failure classes the guards handle: `loader.await()` swallows init rejections (`Promise.allSettled`) — Node still exits non-zero on the resulting unhandled rejection, and `installFailLoud` replaces the noisy dump with one labelled line and a guaranteed `exit(1)`; a failed plugin IMPORT is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that `assertEntriesLoaded` turns into a `boot()` rejection. Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`) resolve through the cordis Loader's internal module loader, active only under `node --expose-internals`; the bins' subprocess smokes exercise that path, while this package's unit suite drives `boot()` in-process against configs with relative specifiers. + +## Known Limitations and Deferred Work + +- **Bare package specifiers depend on Loader internals** — production bins need `node --expose-internals`; an in-process caller without it must use resolvable relative/file specifiers or tsx path mapping. +- **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection. +- **Environment loading is cwd-scoped and optional** — the helper loads one `.env` file and warns on failure; it does not search parents, merge profiles, or validate required variables. diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 496aea08fa..62f24ecd14 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -26,8 +26,10 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte | Key | Default | Routed to | |---|---|---| | `model` | (required) | the pre-created `main` agent's model | -| `persona` | — | the deployment persona template (may reference `{{model}}`), routed to `dsh-system-prompt` | +| `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` | | `toolOrder` | — | explicit model-facing tool order (a name list with one `''` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` | +| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-core` | +| `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-core` | | `persistenceRoot` | `./.sessions` | the JSONL backend's root directory | | `welcome` | `ready.` | the stdin-chat banner | | `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) | @@ -68,3 +70,4 @@ Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — - **One pre-created `main` agent drives the readline UI** — there is no multi-session or concurrent-agent surface in this app; a run is one conversation. - **The front-door cluster is fixed in code** — the JSONL persistence backend and the ask-user tooling are baked; a different composition is a leaf-level sibling entry or another app package. +- **The question tool is not an approval answerer** — this app mounts `user-interaction` and `ask_user_question`, but not `ctx.approval`; a `tools/pre-execute` `ask` therefore fails closed unless the leaf composes an approval service and terminal answerer. diff --git a/packages/ui/user-approval/README.md b/packages/ui/user-approval/README.md index 821e98638f..2027d9c1f9 100644 --- a/packages/ui/user-approval/README.md +++ b/packages/ui/user-approval/README.md @@ -11,3 +11,10 @@ The seam also owns the per-session POLICY tier ([the sandbox RFC § Per-session One seam serves both ask paths of [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md): the `tools/pre-execute` `ask` decision (routed by [`@deepseek-ai/dsh-tools`](../../core/tools/) when this service is mounted; degrading to deny when it is not), and the sandbox post-denial escalated retry (the bash tool's `sandbox_permissions` gate in [`@deepseek-ai/dsh-tool-bash`](../../bash/tool-bash/) — [the sandbox RFC § Escalation](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md)). The full design: [the approval-seam RFC](../../../docs/rfc/implemented/feature/2026-07-06-approval-seam.md). Answerers today: the ACP bridge ([`@deepseek-ai/dsh-acp`](../../ui/acp/)) forwards to the editor's `session/request_permission` prompt for agents it owns. The audit events are log-only session records — the model only ever sees the tool result the asker derives from the outcome. + +## Known Limitations and Deferred Work + +- **Requests are valid only inside an open turn** — an idle or between-turn caller throws before auditing; a durable out-of-turn approval workflow is deferred. +- **Only one-shot grants exist** — the outcome vocabulary has `allowed-once` but no `allow-always`, remembered rule, revocation, or grant store; session policy is only `ask` / `never`. +- **The request carries no tool arguments** — a UI must correlate `callId` with an already rendered tool call, and a call-less request cannot be presented by the shipped ACP answerer. +- **No built-in answerer** — headless or incompletely composed deployments resolve `unavailable` and fail closed; the service itself never prompts a human. diff --git a/packages/util/timeout/README.md b/packages/util/timeout/README.md index db2b06ba53..2f1fb41ef8 100644 --- a/packages/util/timeout/README.md +++ b/packages/util/timeout/README.md @@ -40,3 +40,9 @@ Pass your own `code` to `timeoutOf` so classification composes under nesting: wh ## What does NOT get a timeout Local file `read`/`write`/`edit` take no `timeoutMs`: a syscall is best-effort-abortable at most, a timeout could not force `fsync`/`rename` to stop, and adding one would be an implicit default that violates explicit-over-implicit. See [`fs/`](../../fs/README.md). + +## Known Limitations and Deferred Work + +- **Notification only** — a deadline cannot stop work that ignores its signal; every capability still needs its own socket/process/task termination path. +- **`timeoutMs <= 0` is internal vocabulary** — it disables the local timer only after an owning backend has resolved policy, never as a public model/plugin knob. +- **The first abort reason wins classification** — when an upstream cancellation beats the local timer, this layer cannot later report that its own timeout would also have elapsed. diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index 9b0ea33d86..81c19e5e8b 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -38,4 +38,4 @@ The tool never calls a provider's `status()` and never enumerates providers — - **`htmlToMarkdown` is a minimal regex converter, not an HTML parser** — it strips script/style/noscript, keeps headings/bullets/links, and decodes about a dozen named entities; tables, images, and nested formatting are lost. - **The model-facing surface is minimal by design, with promotions deferred** — `max_results` stays a config bound (not a model argument), and `web_fetch` takes only `url` (no `format`/`prompt`/LLM-summarization mode); both are named later steps in [the seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md). -- **No permission gating** — both tools execute without any permission prompt or policy; integration awaits the deferred permission system, with the owner (a `tools/execute` plugin, provider config, or both) undecided. +- **No web-specific permission policy** — both tools execute without requesting `ctx.approval`; a deployment that needs confirmation must add a `tools/pre-execute` policy, and the package does not define persistent URL/domain grants. diff --git a/packages/workflow/tool-workflow/README.md b/packages/workflow/tool-workflow/README.md index 44160d8fc1..100d5418b8 100644 --- a/packages/workflow/tool-workflow/README.md +++ b/packages/workflow/tool-workflow/README.md @@ -4,7 +4,7 @@ The model-facing **`workflow` tool**: run a JavaScript orchestration script that ## What the model sees -Two parameters: `script` (required — the full `export const meta = {...}` + body text; the tool DESCRIPTION carries the complete authoring contract: hooks, semantics, the supported schema subset) and `args` (optional JSON object exposed to the script as the `args` global; a bare list is wrapped as a field, a deliberate deviation from Claude Code's any-JSON `args` so the wire schema stays honest). The plugin also contributes a `tool:` system-prompt section carrying the usage policy — use the tool only on an explicit user ask for a workflow / large orchestration; prefer plain subagent calls for one or two delegations — per the convention that tool guidance ships with the tool plugin, never in the deployment persona. +Three parameters: `script` (required plain-JavaScript body with no `export const meta` statement), `meta` (required JSON identity with `name`/`description` and optional `whenToUse`/`phases`), and `args` (optional JSON object exposed to the script as the `args` global; wrap a bare list as a field so the wire schema stays honest). The tool description carries the complete authoring contract: hooks, semantics, and the supported schema subset. The plugin also contributes a `tool:` system-prompt section carrying the usage policy — use the tool only on an explicit user ask for a workflow / large orchestration; prefer plain subagent calls for one or two delegations — per the convention that tool guidance ships with the tool plugin, never in the deployment persona. ## Lifecycle @@ -12,7 +12,7 @@ Collection is SYNCHRONOUS this cut (like [`dsh-tool-subagent`](../../subagent/to ## Render intent -Decided up front (per the [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md)): a `generic` card titled `workflow: `, the name sniffed TEXTUALLY from `args.script` (presentation must be a pure function of args, so it cannot ask the engine to parse); the script text rides as `rawInput`. The result keeps the generic card. +Decided up front (per the [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md)): a `generic` card titled `workflow: `, read directly from the call's `meta` parameter (presentation is a pure function of args); the script text rides as `rawInput`. The result keeps the generic card. ## Config @@ -20,3 +20,9 @@ Decided up front (per the [render-intent RFC](../../../docs/rfc/implemented/arch |---|---|---| | `toolName` | `workflow` | The model-facing tool name to register. | | `maxResultChars` | `50000` | Rendered-result ceiling; longer JSON is truncated with a notice. | + +## Known Limitations and Deferred Work + +- **The parent turn blocks until the whole workflow settles** — there is no background start/poll surface, and cancellation discards partial output as an error. +- **`args` must be an object and the result is bounded text** — callers wrap top-level arrays/scalars in a field, and JSON beyond `maxResultChars` is truncated rather than stored behind a retrieval handle. +- **Workflow policy is fixed per tool registration** — provider selection, caps, and tool name are deployment config, not model-call arguments. diff --git a/packages/workflow/workflow-workerthread/README.md b/packages/workflow/workflow-workerthread/README.md index 86d386350a..076db8423d 100644 --- a/packages/workflow/workflow-workerthread/README.md +++ b/packages/workflow/workflow-workerthread/README.md @@ -39,7 +39,7 @@ Per-run limits: a concurrency semaphore (`maxConcurrentAgents`), a total-`agent( A worker that dies unexpectedly (an OOM, a script reaching `process.exit` through the documented vm escape) settles the run `stopReason: 'error'` with the exit diagnostics — or `'cancelled'` when a cancel was in flight — and the host-side child registry is what winds every surviving child down. `dispose()` = cancel + immediate host-driven disposal of every registered child (a wedged worker can relay no dispose RPC, so child teardown overlaps the grace instead of starting after it; the worker's own dispose RPCs join the same per-child disposal) + bounded wait (result, then child-registry quiescence, capped by the grace) + unconditional `worker.terminate()`: the thread never outlives its run. Once a run settles, stray children a script fired without awaiting are cancelled too, and `dispose()` waits for their disposal (bounded by the grace) before returning. `agent-start`/`agent-end` pairing is host-guaranteed the same way: forwarded starts live in a ledger, worker-reported ends pair them on the graceful paths, and the termination paths (grace force-settle, worker death) synthesize the missing ends (outcome `cancelled`) before the run settles — a start still in flight across the force-settle can surface after `workflow/end`, immediately paired the same way. -**Engine-specific limitations**: worker startup is paid per run; on a termination path `agentsStarted` reports the HOST-observed count (accepted `child-start`s — calls still queued worker-side for a concurrency slot are unknowable then); and a returned promise or thenable resolves per JavaScript semantics BEFORE materialization — that is what makes an un-awaited `return agent('x')` work — with the value-boundary guard applying to the resolution. +A returned promise or thenable resolves per JavaScript semantics BEFORE materialization — that is what makes an un-awaited `return agent('x')` work — with the value-boundary guard applying to the resolution. ## Config @@ -51,3 +51,11 @@ A worker that dies unexpectedly (an OOM, a script reaching `process.exit` throug | `maxItemsPerCall` | `4096` | Items accepted by one `parallel()`/`pipeline()` call. | | `syncTimeoutMs` | `5000` | vm timeout for the script's initial synchronous slice (in the worker). | | `disposeGraceMs` | `5000` | How long a cancelled run may stay unsettled before force-settle + terminate; also bounds `dispose()`. | + +## Known Limitations and Deferred Work + +- **The worker/vm is not a security boundary** — model-written code can escape `node:vm` and reach the worker's process authority; a hostile-code deployment needs a separate-process or container engine. +- **One worker thread is paid per run** — there is no pool, warm runtime, or cross-run script cache. +- **No ambient timers, filesystem, or network are injected, but escaped code can still reach Node** — the missing globals are portability API, not containment. +- **Termination can only report host-observed starts** — `agentsStarted` excludes worker-side calls still queued behind concurrency when a forced termination makes them unknowable. +- **Cross-realm errors fail `instanceof Error` inside scripts** — workflow authors must branch on stable fields such as `name` and `code`. diff --git a/packages/workflow/workflow/README.md b/packages/workflow/workflow/README.md index 5332243a85..4c90239792 100644 --- a/packages/workflow/workflow/README.md +++ b/packages/workflow/workflow/README.md @@ -24,6 +24,12 @@ All observe-only emits carrying DATA SNAPSHOTS (`WorkflowRunInfo` = id + meta) - `workflow/phase`(info, title) / `workflow/log`(info, message) — script narration. - `workflow/agent-start`(info, agent) / `workflow/agent-end`(info, agent + outcome) — ready-child lifecycle correlated by `seq`; the [generated event contract](../../../docs/cordis-catalog/events.md#workflowagent-start--emit) defines publication and pairing. -## Non-goals (this cut) +## Known Limitations and Deferred Work -Background collection, journaling/resume, saved workflows, nested `workflow()`, token budgets — see the [RFC's deferred section](../../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md). +- **Foreground collection only** — the caller owns one live run and awaits it; background start/poll, spill handles, and detached collection are deferred. +- **No journaling or resume** — scripts, child progress, and intermediate values are not checkpointed, so a process restart cannot continue a run. +- **No saved or nested workflows** — the seam starts caller-supplied scripts only, and a workflow script receives no `workflow()` hook for recursive orchestration. +- **No token-budget vocabulary** — engines cap concurrency/items/agents, but neither the request nor result accounts for model tokens across children. +- **Runs are holder-owned, not service-tracked** — unloading the engine does not discover independent live handles; every consumer must dispose the run it started. + +See the [dynamic-workflows RFC](../../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md) for the deferred workflow surface. diff --git a/scripts/verify-readme-limitations.ts b/scripts/verify-readme-limitations.ts index 5a215a53fd..2d10dfe566 100644 --- a/scripts/verify-readme-limitations.ts +++ b/scripts/verify-readme-limitations.ts @@ -14,7 +14,9 @@ * package set, so a rename or removal fails loud instead of silently * un-gating a README. * - * Checks, per packages///README.md (fenced code excluded): + * The package set comes from `packages///package.json`, so a manifest with no + * sibling README fails instead of escaping a README-only glob. Checks, per + * package README (fenced code excluded): * 1. Non-whitelisted: exactly one limitations-like heading, byte-equal to the * canonical h2, with at least one top-level `- ` bullet before the next * heading. @@ -30,7 +32,7 @@ * Run: `tsx scripts/verify-readme-limitations.ts`. */ -import { globSync, readFileSync } from 'node:fs' +import { existsSync, globSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' const root = resolve(import.meta.dirname, '..') @@ -43,21 +45,18 @@ const CANONICAL = '## Known Limitations and Deferred Work' * package directory relative to the repo root). Their READMEs must NOT carry * the section; adding one moves the package off this list in the same change. */ -const NO_LIMITATIONS: readonly string[] = [ - 'packages/support/subagent-mock', - 'packages/ui/app-boot', - 'packages/util/brand', - 'packages/util/timeout', -] +const NO_LIMITATIONS: Readonly> = { + 'packages/util/brand': 'Type-only nominal-branding primitive with no runtime behavior or deferred work.', +} /** A heading that reads as a limitations section — canonical or drifted. */ function isLimitationsLike(headingText: string): boolean { return ( - /known limitation/i.test(headingText) + /\blimitations?\b/i.test(headingText) || /deferred work/i.test(headingText) || /what is not here/i.test(headingText) - || /^limitations?\b/i.test(headingText) || /^deferred\b/i.test(headingText) + || /^non-goals?\b/i.test(headingText) ) } @@ -66,37 +65,52 @@ interface Line { raw: string } +const ATX_HEADING = /^ {0,3}#{1,6}[ \t]+/ + /** Split a README into prose lines (fenced code dropped), keeping 1-based line numbers. */ function proseLines(text: string): Line[] { - let inFence = false + let fence: { marker: '`' | '~'; length: number } | undefined const kept: Line[] = [] text.split('\n').forEach((raw, i) => { - if (raw.startsWith('```')) { - inFence = !inFence + const token = /^ {0,3}(`{3,}|~{3,})/.exec(raw)?.[1] + if (token !== undefined) { + const marker = token[0] as '`' | '~' + if (fence === undefined) { + fence = { marker, length: token.length } + } else if (marker === fence.marker && token.length >= fence.length) { + fence = undefined + } return } - if (!inFence) kept.push({ index: i + 1, raw }) + if (fence === undefined) kept.push({ index: i + 1, raw }) }) return kept } -const readmes = globSync('packages/*/*/README.md', { cwd: root }).sort() -const scannedPackages = new Set(readmes.map(path => path.slice(0, -'/README.md'.length))) +const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).sort() +const scannedPackages = new Set(packageJsons.map(path => path.slice(0, -'/package.json'.length))) const failures: string[] = [] -for (const entry of NO_LIMITATIONS) { +for (const [entry, reason] of Object.entries(NO_LIMITATIONS)) { if (!scannedPackages.has(entry)) { failures.push(`whitelist entry ${entry} does not name a scanned package — renamed or removed? update NO_LIMITATIONS in scripts/verify-readme-limitations.ts in the same change`) } + if (reason.trim().length === 0) { + failures.push(`whitelist entry ${entry} has no justification — state why a limitations section would be empty boilerplate`) + } } -for (const readme of readmes) { - const pkg = readme.slice(0, -'/README.md'.length) +for (const pkg of scannedPackages) { + const readme = `${pkg}/README.md` + if (!existsSync(resolve(root, readme))) { + failures.push(`${readme}: package manifest has no sibling README with the \`${CANONICAL}\` section`) + continue + } const lines = proseLines(readFileSync(resolve(root, readme), 'utf8')) - const headings = lines.filter(line => /^#{1,6} /.test(line.raw)) - const limitations = headings.filter(line => isLimitationsLike(line.raw.replace(/^#{1,6}\s+/, ''))) + const headings = lines.filter(line => ATX_HEADING.test(line.raw)) + const limitations = headings.filter(line => isLimitationsLike(line.raw.replace(ATX_HEADING, ''))) - if (NO_LIMITATIONS.includes(pkg)) { + if (Object.hasOwn(NO_LIMITATIONS, pkg)) { for (const heading of limitations) { failures.push(`${readme}:${heading.index}: whitelisted as having no known limitations, but carries ${JSON.stringify(heading.raw)} — drop the section or remove the package from NO_LIMITATIONS`) } @@ -118,7 +132,7 @@ for (const readme of readmes) { } const headingAt = lines.indexOf(heading) const body = lines.slice(headingAt + 1) - const end = body.findIndex(line => /^#{1,6} /.test(line.raw)) + const end = body.findIndex(line => ATX_HEADING.test(line.raw)) const section = end === -1 ? body : body.slice(0, end) if (!section.some(line => /^- /.test(line.raw))) { failures.push(`${readme}:${heading.index}: the \`${CANONICAL}\` section has no top-level \`- \` bullet — state the limitations, or whitelist the package if there are genuinely none`) @@ -131,4 +145,4 @@ if (failures.length > 0) { process.exit(1) } -console.log(`verify-readme-limitations: ${readmes.length} package READMEs checked (${NO_LIMITATIONS.length} whitelisted), all conform.`) +console.log(`verify-readme-limitations: ${scannedPackages.size} package READMEs checked (${Object.keys(NO_LIMITATIONS).length} whitelisted), all conform.`)