Merge remote-tracking branch 'origin/master' into codex/trim-ai-prose
# Conflicts: # docs/AGENTS.md # docs/config-catalog.md # packages/bash/bash-sandbox/src/index.ts # packages/bash/bash/src/session-mode.ts # packages/bash/tool-bash/README.md # packages/code-runtime/code-runtime-worker/README.md # packages/compact/compact/src/index.ts # packages/core/agent-core/README.md # packages/hooks/hooks-claude/src/config.ts # packages/hooks/hooks-claude/src/index.ts # packages/hooks/hooks-codex/src/config.ts # packages/hooks/hooks-codex/src/index.ts # packages/llm/llm/README.md # packages/session-persistence/session-persistence-jsonl/README.md # packages/session-persistence/session-persistence/README.md # packages/skill/skill-local/README.md # packages/support/acp-snapshot/README.md # packages/support/invariants/src/index.ts # packages/ui/acp/README.md # packages/ui/jsonrpc-agent/README.md # packages/ui/jsonrpc/README.md # packages/ui/permission/README.md # packages/ui/user-approval/README.md # packages/ui/user-interaction/README.md # packages/web/web-search-deepseek/README.md
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# @deepseek-ai/dsh-tool-bash
|
||||
|
||||
The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). Pure schema + text shaping; every process concern lives behind the seam, so sandboxed or remote executor implementations swap in without changing what the model sees.
|
||||
The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). This package owns schema and text shaping while process concerns stay behind the seam. Executor facts can change rendered results, and a sandboxing executor activates the escalation fields, without moving those presentation rules into the backend.
|
||||
|
||||
Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`); the plugin stays pending until `ctx.bash` exists (`inject: ['tools', 'bash', 'systemPrompt']`).
|
||||
|
||||
The plugin also contributes the `tool:bash` prompt section (order 105) — the cross-call habit the per-tool descriptions cannot carry: check the `[exit code: N]` marker on every result and investigate failures before moving on. Sandbox mode is intentionally learned from denial results, not announced in the prompt; see [Per-session mode](#per-session-mode-switching).
|
||||
The plugin also contributes the `tool:bash` prompt section (order 105) — the cross-call habit the per-tool descriptions cannot carry: check the `[exit code: N]` marker on every result and investigate failures before moving on. A sandboxing executor changes the `bash` schema and result markers but adds no mode statement or switch notice; see [Per-session mode](#per-session-mode-switching).
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -38,7 +38,7 @@ The executor stores the spawning session id as the task's owner. `bash_output` a
|
||||
|
||||
## UI presentation
|
||||
|
||||
UI presentation is tool-owned through `presentCall` and `presentResult`. Foreground `bash` uses a terminal card whose title is the command, optional description is separate, and cwd follows `workdir` or the session; its result carries raw output and exit or signal data. Background runs, spawn failures, `bash_output`, and `bash_kill` use generic cards. Presenters are pure and replay-safe, and malformed older arguments fall back to generic rendering. See [`dsh-tools`](../../core/tools/) and [`dsh-acp`](../../ui/acp/) for card semantics.
|
||||
UI presentation is tool-owned through `presentCall` and `presentResult`. Foreground `bash` uses a terminal card whose title is the exact command and whose optional description is separate; cwd follows an explicit `workdir`—resolved by the bridge against the session when relative—or the session cwd. Its result carries raw output plus exit or signal data, and clients without terminal support receive a bridge-derived fenced console fallback. Background runs, spawn failures, `bash_output`, and `bash_kill` use generic cards. Presenters are pure and replay-safe; malformed older arguments fall back to generic rendering. See [`dsh-tools`](../../core/tools/) and [`dsh-acp`](../../ui/acp/) for card semantics.
|
||||
|
||||
## Background completion notices
|
||||
|
||||
@@ -56,4 +56,49 @@ Escalating bash calls resolve `ctx.approval` before execution. `allowed-once` ap
|
||||
|
||||
## Per-session mode switching
|
||||
|
||||
For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. The prompt does not announce the standing mode; denial results report the effective mode when the boundary matters. See the [sandbox switching contract](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md).
|
||||
For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. Neither the prompt nor a switch notice announces the standing mode; denial results report the effective mode when the boundary matters. See the [`dsh-bash` fold](../bash/README.md) and [sandbox switching contract](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
### System prompt
|
||||
|
||||
**What the model sees**: Every request in this plugin's registration scope contains the bash guidance below. A sandboxing executor adds no mode statement or switch notice. Scoped tool restrictions can hide the schemas without removing this independently registered section.
|
||||
|
||||
**Token effect**: Small fixed input cost per request while the plugin is active, unchanged by sandbox mode or mode switches.
|
||||
|
||||
#### Bash guidance
|
||||
|
||||
```markdown
|
||||
Check the [exit code: N] marker on every bash result; investigate failures before moving on.
|
||||
```
|
||||
|
||||
### Tool schemas
|
||||
|
||||
**What the model sees**: The model sees the generated [`bash`, `bash_output`, and `bash_kill` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash). `sandbox_permissions` and `justification` augment `bash` only when the mounted executor advertises sandboxing. Agent-scoped tool restrictions can remove the definitions for that agent.
|
||||
|
||||
**Token effect**: Fixed schema cost on every request where the tools are visible; sandbox support adds the escalation fields and its conditional description paragraph.
|
||||
|
||||
### Foreground result
|
||||
|
||||
**What the model sees**: The renderer emits the data-dependent stdout tail, then optional `[stderr]` and the stderr tail. With no output it emits exactly `(no output)`. Conditional lines are exactly `[output truncated; full output: <path-or-(unavailable)>]`, `[sandbox: file access denied under <mode> mode]`, `[timed out after <timeoutMs>ms]`, `[killed by signal: <signal>]`, and `[exit code: <exitCode>]`; the sandbox escalation and runner-failure lines are quoted in [`dsh-bash-sandbox`](../bash-sandbox/README.md).
|
||||
|
||||
**Token effect**: Zero result tokens before a call. Output is bounded per stream, while each emitted line remains in history until compaction.
|
||||
|
||||
### Background task context and results
|
||||
|
||||
**What the model sees**: Start returns exactly `started background task <taskId>`. Completion injects exactly `background bash task <taskId> finished <status>. Read its output with bash_output.` Reads return only the data-dependent delta or `(no new output)`, optionally `[some output was dropped from memory; full output: <paths-or-(unavailable)>]`, then exactly one of `[status: running]`, `[status: killed]`, `[status: killed by <signal>]`, or `[status: completed, exit code: <exitCode>]`. Kill returns `killed background task <taskId>` or `task <taskId> had already finished`.
|
||||
|
||||
**Token effect**: Start and status text is small; deltas are data-dependent. The completion notice and every tool result are retained until compaction, but polling does not repeat already-delivered output.
|
||||
|
||||
### Tool errors
|
||||
|
||||
**What the model sees**: Validation and policy failures are normalized as `Error: <message>`. This package's stable messages are `invalid command: expected a non-empty string`, `invalid description: expected a non-empty string`, `invalid timeoutMs: expected a positive number, got <value>`, `invalid escalation: sandbox_permissions requires a justification`, `invalid escalation: justification is only valid together with sandbox_permissions`, `invalid justification: expected a non-empty sentence`, `invalid task_id: expected a string, got <value>`, `task <taskId> belongs to another session`, `sandbox_permissions is not available in this composition (no sandboxing executor to escalate)`, `sandbox escalation to "<mode>" is not strictly wider than this call's current "<mode>" mode`, the approval-availability/rejection/cancellation variants, and `command aborted`.
|
||||
|
||||
**Token effect**: Only the failing call adds these retained tokens; a rejected escalation does not add command output because the command does not run.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -27,14 +27,8 @@ export const name = 'tool-bash'
|
||||
export const inject = ['tools', 'bash', 'systemPrompt']
|
||||
|
||||
/**
|
||||
* Validate the constraints the SchemaSpec can't express. `defineTool`
|
||||
* validates parsed args against the SchemaSpec before `execute` runs (the
|
||||
* arg-validation RFC), so type/required/enum checks are already done and `args`
|
||||
* is the validated `InferArgs` shape here. What remains are value constraints
|
||||
* the DSL has no vocabulary for: non-empty strings, a positive finite timeout,
|
||||
* and the escalation pairing (`sandbox_permissions` and `justification` travel
|
||||
* together — an approval prompt without a reason, or a reason driving nothing,
|
||||
* is a malformed ask).
|
||||
* Validate value constraints absent from SchemaSpec: non-empty strings, a
|
||||
* positive finite timeout, and paired escalation mode and justification.
|
||||
*/
|
||||
function validateBashArgs(args: BashToolArgs): void {
|
||||
if (args.command.trim().length === 0) {
|
||||
@@ -58,9 +52,7 @@ function validateBashArgs(args: BashToolArgs): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject an empty `task_id`. Type and presence are guaranteed by the
|
||||
* SchemaSpec validation (the arg-validation RFC); only the non-empty constraint, which the
|
||||
* DSL can't express, is left to check here.
|
||||
* Reject an empty `task_id`; SchemaSpec already validates type and presence.
|
||||
*/
|
||||
function validateTaskId(value: string): BashTaskId {
|
||||
if (value.length === 0) {
|
||||
@@ -70,10 +62,8 @@ function validateTaskId(value: string): BashTaskId {
|
||||
}
|
||||
|
||||
/**
|
||||
* The bash tool's validated argument shape — the base parameters plus the two
|
||||
* escalation fields, which are ADVERTISED only when the mounted executor
|
||||
* reports a confining default mode (absent from the schema otherwise, so the
|
||||
* SchemaSpec validator rejects them before `execute` ever sees one).
|
||||
* Validated bash arguments. Escalation fields are advertised only when the
|
||||
* mounted executor reports a confining mode.
|
||||
*/
|
||||
interface BashToolArgs {
|
||||
command: string
|
||||
@@ -86,10 +76,8 @@ interface BashToolArgs {
|
||||
}
|
||||
|
||||
/**
|
||||
* The strictly-wider table: what a call whose effective mode is the key may
|
||||
* escalate TO. Checked at EXECUTION, never baked into the schema — the
|
||||
* schema's enum is {@link ESCALATION_TARGETS}, because schemas are
|
||||
* registry-global while the effective mode is per-call truth.
|
||||
* Strictly wider modes for each effective mode. Execution checks this table
|
||||
* because the schema is global while the effective mode is per call.
|
||||
*/
|
||||
const WIDER_MODES: Record<string, readonly SandboxMode[]> = {
|
||||
'read-only': ['workspace-write', 'danger-full-access'],
|
||||
@@ -97,12 +85,9 @@ const WIDER_MODES: Record<string, readonly SandboxMode[]> = {
|
||||
}
|
||||
|
||||
/**
|
||||
* The closed escalation-target vocabulary — every mode a call could ever
|
||||
* escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
|
||||
* whenever the mounted executor confines: cutting the enum down to the modes
|
||||
* wider than the executor's DEFAULT would strand a session whose effective
|
||||
* mode sits below it (a `danger-full-access` default would advertise nothing
|
||||
* while a narrower-switched session stays confined with no lever).
|
||||
* All possible escalation targets. Advertise the global set because a session
|
||||
* override may be narrower than the executor default; execution rejects a
|
||||
* target that is not wider for that call.
|
||||
*/
|
||||
const ESCALATION_TARGETS: readonly SandboxMode[] = ['workspace-write', 'danger-full-access']
|
||||
|
||||
@@ -121,15 +106,15 @@ function bashDescription(escalationModes: readonly SandboxMode[]): string {
|
||||
+ 'poll it with `bash_output` and stop it with `bash_kill`.'
|
||||
if (escalationModes.length === 0) return base
|
||||
return base + ' Attempting a command the sandbox may deny is safe and expected: run it and read the '
|
||||
+ 'marker rather than assuming the denial. When a command IS denied and a wider mode would let it '
|
||||
+ 'succeed, escalate immediately in the SAME turn — the ONE sanctioned exception to a denial: retry '
|
||||
+ 'marker rather than assuming the denial. When a command is denied and a wider mode would let it '
|
||||
+ 'succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry '
|
||||
+ 'the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) '
|
||||
+ 'plus a one-sentence `justification`. Do not detour through chat to ask permission first — the '
|
||||
+ 'approval prompt raised by that retry IS how the user consents. If the session states approval '
|
||||
+ 'approval prompt raised by that retry is how the user consents. If the session states approval '
|
||||
+ 'prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. '
|
||||
+ 'Never escalate speculatively: ground the request in a real denial — normally the one THIS command '
|
||||
+ 'Never escalate speculatively: ground the request in a real denial — normally the one this command '
|
||||
+ 'just hit; escalating up front is fine only when this session already denied the same access. '
|
||||
+ 'A rejected escalation is final for THAT command — stop and explain, never work around '
|
||||
+ 'A rejected escalation is final for that command — stop and explain, never work around '
|
||||
+ 'it — but it does not forbid attempting or escalating other commands later.'
|
||||
}
|
||||
|
||||
@@ -166,15 +151,12 @@ export function renderResult(
|
||||
if (body.length === 0) body = '(no output)'
|
||||
|
||||
const markers: string[] = []
|
||||
// The sandbox marker precedes the exit-status markers so `[exit code: N]`
|
||||
// stays the LAST line (exitStatus() anchors its parse there). Denial is a
|
||||
// reported fact like timeout: the model decides how to react.
|
||||
// Keep `[exit code: N]` last so parseExitStatus() can recover it. A denial,
|
||||
// like a timeout, remains a reported fact for the model to handle.
|
||||
if (result.sandbox?.denied) {
|
||||
markers.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`)
|
||||
// The same-turn nudge lives at the decision point: only when this
|
||||
// composition advertises the fields (a lever is never hinted that the
|
||||
// schema does not offer), and inside the sandbox marker family so the
|
||||
// exit-code marker stays the last line.
|
||||
// Add the retry hint only when the schema advertises escalation, before
|
||||
// the final exit marker.
|
||||
if (escalationModes.length > 0) {
|
||||
markers.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]')
|
||||
}
|
||||
@@ -214,8 +196,7 @@ function presentBashCall(args: BashCallArgs): GenericCallView | TerminalCallView
|
||||
content: [{ type: 'text', text: args.description }],
|
||||
}
|
||||
}
|
||||
// A foreground run IS a terminal: the command titles the card, the description
|
||||
// renders above it, and the cwd (when the model gave a workdir) heads it.
|
||||
// A foreground run is a terminal; an explicit workdir supplies its cwd.
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command,
|
||||
@@ -238,7 +219,7 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView |
|
||||
if (isBackground || result.isError) {
|
||||
return { card: 'generic', content: [{ type: 'text', text: `\`\`\`console\n${raw.replace(/\n+$/, '')}\n\`\`\`` }] }
|
||||
}
|
||||
// A finished foreground run: RAW output + parsed exit for the terminal card.
|
||||
// A finished foreground run supplies raw output and parsed exit status.
|
||||
// The bridge derives the no-capability fenced fallback from `output`.
|
||||
return { card: 'terminal', output: raw, ...parseExitStatus(raw) }
|
||||
}
|
||||
@@ -283,9 +264,7 @@ function statusLine(task: BashTask): string {
|
||||
}
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
// The bash tools' cross-call HABIT, which the per-tool descriptions cannot
|
||||
// carry (they describe one call each): the exit-code marker is only useful
|
||||
// if the model actually checks it every time.
|
||||
// Cross-call guidance belongs in the prompt rather than one tool description.
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:bash',
|
||||
order: 105,
|
||||
@@ -293,26 +272,15 @@ export function apply(ctx: Context): void {
|
||||
})
|
||||
|
||||
/**
|
||||
* The caller's owner TOKEN — the owning agent's `session.header.id`, or
|
||||
* `undefined` for a non-agent caller. Read `session.header.id` (NOT
|
||||
* `session.id`): every other subsystem keys off the header id (the ACP bridge,
|
||||
* both persistence backends), and the sibling `resolveWorkdir` already reads
|
||||
* `session.header.cwd`, so using `session.id` here would be the asymmetry smell
|
||||
* the conventions flag. The two are equal in production, but the header is the
|
||||
* canonical identity.
|
||||
* Return the canonical session-header id used by ACP and persistence as the
|
||||
* task owner, or undefined for a non-agent caller.
|
||||
*/
|
||||
const callerToken = (exec: { agent?: Agent }): OwnerToken | undefined =>
|
||||
exec.agent ? OwnerToken(exec.agent.session.header.id) : undefined
|
||||
|
||||
/**
|
||||
* Authorize a `bash_output`/`bash_kill` call against the task's stored owner
|
||||
* token. Rejects when the task HAS an owner and it differs from the caller's
|
||||
* token — using `!== undefined` semantics, NOT truthiness, so an empty-string
|
||||
* token is still a real owner (never treated as unowned). An unowned task
|
||||
* (`ownerOf` returns `undefined`) is allowed; a truly unknown id is also
|
||||
* `undefined` here and then fails loudly at the subsequent
|
||||
* `readOutput`/`kill` ("unknown bash task"). The conservative no-agent caller
|
||||
* (`callerToken` undefined) cannot match an owned task and is rejected.
|
||||
* Reject access when a task has a different session owner. Unowned tasks are
|
||||
* allowed; unknown ids still fail in the subsequent read or kill.
|
||||
*/
|
||||
const assertTaskAccess = (taskId: BashTaskId, exec: { agent?: Agent }): void => {
|
||||
const owner = ctx.bash.ownerOf(taskId)
|
||||
@@ -348,32 +316,20 @@ export function apply(ctx: Context): void {
|
||||
const escalationModes: readonly SandboxMode[] = defaultMode === undefined ? [] : ESCALATION_TARGETS
|
||||
|
||||
/**
|
||||
* The session's standing mode override for an ordinary (non-escalating)
|
||||
* call: the `bash/sandbox-mode` fold of the calling agent's log, stamped
|
||||
* onto the request so EXECUTION follows the same effective mode the prompt
|
||||
* section states. Weakest precedence — an escalation grant (freshly
|
||||
* approved for exactly this call) outranks it, and without either the
|
||||
* executor's `resolve()` applies its configured default. Undefined for a
|
||||
* non-sandboxing executor (nothing honors it) and for agent-less callers
|
||||
* (no session to fold).
|
||||
* Return the calling session's folded standing mode. Approval outranks this
|
||||
* value and the executor default applies when it is absent; non-sandboxing
|
||||
* and agent-less calls have no override.
|
||||
*/
|
||||
const sessionOverride = (exec: ToolExecution): SandboxMode | undefined =>
|
||||
defaultMode === undefined || exec.agent === undefined ? undefined : effectiveSandboxMode(exec.agent.session.events)
|
||||
|
||||
/**
|
||||
* Resolve a sandbox-escalation request through `ctx.approval` BEFORE
|
||||
* anything executes. Returns the granted mode to stamp onto the bash
|
||||
* request; throws the distinct fail-closed text for every other path (no
|
||||
* service composed, an agent-less execution, a rejection, a cancellation,
|
||||
* an unanswerable ask) — the registry turns the throw into this call's
|
||||
* isError result, and nothing has run. The seam is consumed
|
||||
* opportunistically (`ctx.get`, the dsh-tools ask-routing pattern), so a
|
||||
* deployment without it degrades per call, never at registration.
|
||||
* Request one-shot escalation before execution. Missing approval context,
|
||||
* rejection, cancellation, and unavailable answers throw without running the
|
||||
* command; the optional seam is resolved per call through `ctx.get`.
|
||||
*/
|
||||
const approveEscalation = async (mode: string, justification: string, exec: ToolExecution): Promise<SandboxMode> => {
|
||||
// Schema validation only checks ADVERTISED keys, so an unadvertised `sandbox_permissions`
|
||||
// (no sandboxing executor) still reaches execute — reject it here so a human is never
|
||||
// prompted to "escalate" a sandbox that is not there.
|
||||
// Reject an unadvertised escalation before prompting for a nonexistent sandbox.
|
||||
if (escalationModes.length === 0) {
|
||||
throw new Error('sandbox_permissions is not available in this composition (no sandboxing executor to escalate)')
|
||||
}
|
||||
@@ -399,8 +355,7 @@ export function apply(ctx: Context): void {
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
})
|
||||
switch (outcome) {
|
||||
// The SchemaSpec enum already pinned `mode` to the closed target
|
||||
// vocabulary; the per-call check above proved it is strictly wider.
|
||||
// Schema validation pins the vocabulary; the per-call check proves widening.
|
||||
case 'allowed-once': return mode as SandboxMode
|
||||
case 'rejected': throw new Error(`the user rejected escalating this command to "${mode}"`)
|
||||
case 'cancelled': throw new Error(`approval for escalating to "${mode}" was cancelled`)
|
||||
@@ -457,8 +412,7 @@ export function apply(ctx: Context): void {
|
||||
...sandboxMode !== undefined ? { sandboxMode } : {},
|
||||
}
|
||||
if (args.run_in_background === true) {
|
||||
// Stamp the owner token (the agent's session id) onto the spec so the executor stores
|
||||
// it on the task — the isolation fence for bash_output/ bash_kill.
|
||||
// Store the session owner on the task for bash_output/bash_kill isolation.
|
||||
const task = ctx.bash.start(ctx.bash.resolve({ ...request, owner: callerToken(exec) }))
|
||||
return [{ type: 'text', text: `started background task ${task.id}` }]
|
||||
}
|
||||
@@ -492,9 +446,8 @@ export function apply(ctx: Context): void {
|
||||
}
|
||||
text += `\n${statusLine(read.task)}`
|
||||
if (read.task.sandbox?.runnerFailed) {
|
||||
// The sandbox RUNNER itself failed — the command never ran. The
|
||||
// foreground path surfaces this as the structured SANDBOX_UNAVAILABLE
|
||||
// error; a settled task's read carries the marker instead.
|
||||
// Background settlement carries the runner-failure fact that a
|
||||
// foreground call exposes as SANDBOX_UNAVAILABLE.
|
||||
text += `\n[sandbox: the sandbox runner itself failed under ${read.task.sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`
|
||||
} else if (read.task.sandbox?.denied) {
|
||||
// Mirrors the foreground result marker (and its same-turn escalation hint).
|
||||
|
||||
Reference in New Issue
Block a user