feat(working-directory): add WorkingDirectoryError and code directory failures

The working-directory service throws WorkingDirectoryError:
WORKING_DIRECTORY_INVALID for an empty cd, WORKING_DIRECTORY_NOT_FOUND
for a missing target, and WORKING_DIRECTORY_UNAVAILABLE when the
current and original directories are both gone or the service was
disposed. The working_directory tool reports a missing Agent as
ToolContextUnavailableError and an empty cd as ToolArgsError.

sandbox-policy reports a session cwd it cannot use as a workspace root
as SandboxInvalidRootError (SANDBOX_INVALID_ROOT), whose constructor
now names the role of the rejected path.

Refs #7869
This commit is contained in:
sjw1231 committed 2026-11-06 16:39:36 +08:00
1 parent 9626ccfae8
commit 45db11970c
16 files changed
+138 -33

No files matched your search

@@ -21,8 +21,8 @@
en: 6704447e52ec295b
zh: 9ed812d9e29d5533
/deepseek-ai-dsh-sandbox-policy/use-this-package/minimal-configuration:
en: 0616478ef5bb0566
zh: e3a5802aefcf8424
en: 0b62eae224181946
zh: 2e77e7f49bd074d4
/deepseek-ai-dsh-sandbox-policy/use-this-package/registering-a-session-s-writable-directory:
en: 841ea47ef1ed7ee6
zh: cd573e67a67742e2
+1 -1
View File
@@ -45,7 +45,7 @@ Load the package with a default mode; the fail-safe default is `read-only`, and
| Field | Default | Meaning |
|---|---|---|
| `mode` | `read-only` | The deployment default mode a session starts from, validated at load |
| `workspaceRoot` | `process.cwd()` | Absolute fallback root for agentless calls or sessions without a cwd; relative values fail at load. Normal agent calls use the session's immutable cwd |
| `workspaceRoot` | `process.cwd()` | Absolute fallback root for agentless calls or sessions without a cwd; relative values fail at load. Normal agent calls use the session's immutable cwd; a cwd containing a NUL byte fails the call with `SANDBOX_INVALID_ROOT` |
| `additionalWritableRoots` | `[]` | Physical absolute directories also writable under `workspace-write`; Windows file tools honor them, Windows subprocesses ignore them, and `read-only` grants no access |
For a linked Git worktree, the caller can grant its shared Git directory with `additionalWritableRoots`. The caller selects that directory explicitly; the policy does not inspect Git files or grant a parent checkout. These grants are deployment configuration shared by the composition's sessions, rather than saved Session permissions. Granting the entire Git directory also permits changes to hooks and configuration that a later host Git invocation can execute outside the sandbox; use that grant only for trusted tasks. Use physical absolute directory paths. The enforcing host rejects missing paths, non-directories, and symlinked directories on each call, including a directory replaced by a symlink between calls. Remote grants are validated on the remote host.
+1 -1
View File
@@ -45,7 +45,7 @@ kind: "package-reference"
| 字段 | 默认值 | 含义 |
|---|---|---|
| `mode` | `read-only` | 会话起始的部署默认模式,加载时验证 |
| `workspaceRoot` | `process.cwd()` | 无 agent 调用或没有 cwd 的会话所用的绝对回退根目录;相对值在加载时拒绝。普通 agent 调用使用会话的不可变 cwd |
| `workspaceRoot` | `process.cwd()` | 无 agent 调用或没有 cwd 的会话所用的绝对回退根目录;相对值在加载时拒绝。普通 agent 调用使用会话的不可变 cwd;cwd 含 NUL 字节时调用以 `SANDBOX_INVALID_ROOT` 失败 |
| `additionalWritableRoots` | `[]` | 在 `workspace-write` 下额外允许写入的物理绝对目录;Windows 文件工具执行这些授权,Windows 子进程忽略它们,`read-only` 不授予访问权限 |
对于关联的 Git worktree,调用方可通过 `additionalWritableRoots` 授予共享 Git 目录的写权限。调用方必须显式选择该目录;策略不会读取 Git 文件或授权父检出目录。这些授权是组合中所有会话共享的部署配置,而非保存的 Session 权限。授权整个 Git 目录也允许修改钩子和配置,后续主机 Git 调用可能在沙箱之外执行它们;仅对可信任务使用此授权。使用物理绝对目录路径。执行限制的主机在每次调用时拒绝不存在的路径、非目录和符号链接目录,包括两次调用之间被替换为符号链接的目录。远端授权在远端主机上验证。
+23 -6
View File
@@ -27,6 +27,7 @@ import { Context, Service } from '@deepseek-ai/cordis'
import { z as zod } from 'zod'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent'
import { SandboxInvalidRootError } from '@deepseek-ai/dsh-sandbox'
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { Session } from '@deepseek-ai/dsh-session'
import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection'
@@ -34,10 +35,9 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
export { SANDBOX_MODES, setSandboxMode } from './session-mode.ts'
/** Preserve execution-world spelling; enforcing providers resolve filesystem identity on their host. */
function resolveWorkspaceRoot(path: string): string {
if (!isAbsolute(path) || path.includes('\0')) throw new Error('sandbox-policy: workspace root must be an absolute execution-world path without NUL bytes')
return path
/** Whether a path is usable as an execution-world grant: absolute and free of NUL bytes. */
function isExecutionWorldPath(path: string): boolean {
return isAbsolute(path) && !path.includes('\0')
}
/**
@@ -160,7 +160,12 @@ export class SandboxPolicyService extends Service {
// runtime fact. `workspaceRoot` has NO schema default, so its fallback to
// the process cwd is real branching, resolved absolute either way.
this.defaultMode = config.mode as SandboxMode
this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())
// Execution-world spelling is preserved; enforcing providers resolve filesystem identity on their host.
const workspaceRoot = config.workspaceRoot ?? process.cwd()
if (!isExecutionWorldPath(workspaceRoot)) {
throw new Error('sandbox-policy: workspace root must be an absolute execution-world path without NUL bytes')
}
this.workspaceRoot = workspaceRoot
this.additionalWritableRoots = resolveAdditionalWritableRoots(config.additionalWritableRoots)
}
@@ -197,6 +202,8 @@ export class SandboxPolicyService extends Service {
* snapshotted together; existing operations and processes keep their policy.
* @param request - optional session and approved mode override.
* @returns the fully resolved per-call mode and absolute workspace root.
* @throws `SandboxInvalidRootError` (`SANDBOX_INVALID_ROOT`) when the session's recorded cwd is
* relative or contains a NUL byte.
*/
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy {
const { session } = request
@@ -206,12 +213,22 @@ export class SandboxPolicyService extends Service {
: resolveAdditionalWritableRoots([...(this.additionalWritableRoots ?? []), ...registrations.values()])
return {
mode: request.mode ?? (session === undefined ? undefined : this.overrideOf(session)) ?? this.defaultMode,
workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot),
workspaceRoot: this.sessionWorkspaceRoot(session),
...additionalWritableRoots === undefined ? {} : { additionalWritableRoots },
...session === undefined ? {} : { sessionId: session.id },
}
}
/** The session's recorded cwd, or the configured root, as the workspace-write boundary. */
private sessionWorkspaceRoot(session: Session | undefined): string {
const cwd = session?.header.cwd
if (cwd === undefined) return this.workspaceRoot
if (!isExecutionWorldPath(cwd)) {
throw new SandboxInvalidRootError(cwd, 'the Session working directory must be an absolute execution-world path without NUL bytes', 'sandbox workspace root')
}
return cwd
}
/**
* Permit writes under one directory in this Session's future workspace-write
* operations. The calling context owns the effect; disposal removes only this
@@ -67,6 +67,20 @@ describe('SandboxPolicyService', () => {
}
})
it('rejects a session cwd containing a NUL byte with SANDBOX_INVALID_ROOT', async () => {
const cwd = '/invalid\0session'
const ctx = await mounted({ mode: 'workspace-write', workspaceRoot: '/ws' })
try {
expect(() => ctx.sandboxPolicy.resolve({ session: session('sess-invalid-cwd', cwd) })).toThrow(expect.objectContaining({
name: 'SandboxInvalidRootError',
code: 'SANDBOX_INVALID_ROOT',
message: `Invalid sandbox workspace root ${JSON.stringify(cwd)}: the Session working directory must be an absolute execution-world path without NUL bytes`,
}))
} finally {
await ctx.fiber.dispose()
}
})
it('resolves the deployment policy for an agentless call', async () => {
const ctx = await mounted({ mode: 'workspace-write', workspaceRoot: '/fallback' })
expect(ctx.sandboxPolicy.resolve()).toEqual({
+8 -3
View File
@@ -19,10 +19,15 @@ import { relative } from 'node:path'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import type { SandboxExecutionPolicy } from './index.ts'
/** An explicit directory grant cannot be enforced without changing the caller's selected path. */
/** A workspace root or explicit directory grant cannot be enforced without changing the caller's selected path. */
export class SandboxInvalidRootError extends HarnessError {
constructor(path: string, detail: string) {
super(`Invalid additional writable directory ${JSON.stringify(path)}: ${detail}`, 'SANDBOX_INVALID_ROOT')
/**
* @param path - the rejected path, as the caller spelled it.
* @param detail - why the path cannot be enforced.
* @param subject - the role of the path in the policy, as the message names it.
*/
constructor(path: string, detail: string, subject = 'additional writable directory') {
super(`Invalid ${subject} ${JSON.stringify(path)}: ${detail}`, 'SANDBOX_INVALID_ROOT')
this.name = 'SandboxInvalidRootError'
}
}
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 87d7d2dbd7777429
/deepseek-ai-dsh-tool-working-directory/use-this-package:
en: bb750522dbdb3fef
zh: 85586a5c2137afa9
en: 83ab57140ebea28e
zh: 213c36a664a21f65
/deepseek-ai-dsh-tool-working-directory/understand-the-implementation:
en: f361fe510916fce8
zh: a2127f768ef9ac95
@@ -25,7 +25,7 @@ Read the Session’s current directory or enter an existing directory with the s
<a id="use-this-package"></a>
## Use this package
Mount alongside `tools` and `workingDirectory`. Call `working_directory({})` to read or `working_directory({ cd: "src" })` to change directory. The tool has no configuration fields.
Mount alongside `tools` and `workingDirectory`. Call `working_directory({})` to read or `working_directory({ cd: "src" })` to change directory. The tool has no configuration fields. A call without a calling Agent fails with `ToolContextUnavailableError` (`TOOL_CONTEXT_UNAVAILABLE`), an empty `cd` with `ToolArgsError` (`INVALID_ARGS`), and a missing directory with the service's `WorkingDirectoryError` codes.
```yaml
- name: '@deepseek-ai/dsh-tool-working-directory'
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用此包
与 `tools` 和 `workingDirectory` 一起挂载。调用 `working_directory({})` 读取目录,或调用 `working_directory({ cd: "src" })` 改变目录。此工具没有配置字段。
与 `tools` 和 `workingDirectory` 一起挂载。调用 `working_directory({})` 读取目录,或调用 `working_directory({ cd: "src" })` 改变目录。此工具没有配置字段。没有调用方 Agent 的调用以 `ToolContextUnavailableError`(`TOOL_CONTEXT_UNAVAILABLE`)失败,`cd` 为空以 `ToolArgsError`(`INVALID_ARGS`)失败,目录不存在时使用服务的 `WorkingDirectoryError` code。
```yaml
- name: '@deepseek-ai/dsh-tool-working-directory'
@@ -4,7 +4,7 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { defineTool, ToolArgsError, ToolContextUnavailableError } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-working-directory'
/** Cordis plugin name. */
@@ -32,7 +32,8 @@ export function apply(ctx: Context): void {
render: (_args, value) => [{ type: 'text', text: value.cwd }],
},
async execute(args, exec) {
if (exec.agent === undefined) throw new Error('working_directory requires an agent Session')
if (exec.agent === undefined) throw new ToolContextUnavailableError('working_directory requires an agent Session')
if (args.cd === '') throw new ToolArgsError(['cd must not be empty; omit it to read the current directory'])
const cwd = args.cd === undefined
? await ctx.workingDirectory.ensure(exec.agent, exec.signal)
: await ctx.workingDirectory.set(exec.agent, args.cd, exec.signal)
@@ -18,8 +18,8 @@
en: e65405026debde7c
zh: e12a648eb10ed16b
/deepseek-ai-dsh-working-directory/understand-the-implementation:
en: f0458147d29631b1
zh: 6a288670dd084ffb
en: 82d7372a58bb75c5
zh: e18ee833daa99581
/deepseek-ai-dsh-working-directory/further-exploration:
en: eabff683146786c8
zh: 7997dec17802ef64
+1 -1
View File
@@ -41,7 +41,7 @@ Directory validation and publication delegate through `working-directory/access`
<details>
<summary>Implementation internals — click to expand</summary>
The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. Prompt assembly validates the current directory before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API.
The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. Prompt assembly validates the current directory before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API. Failures are `WorkingDirectoryError` instances: `WORKING_DIRECTORY_INVALID` for an empty or malformed path, `WORKING_DIRECTORY_NOT_FOUND` when a requested change names no existing directory, and `WORKING_DIRECTORY_UNAVAILABLE` when neither the current directory nor the original project exists or the service has been disposed.
No runtime invariant companion is published because the service reads its only directory state from the Session projection and checks filesystem validity at use.
@@ -41,7 +41,7 @@ kind: "package-reference"
<details>
<summary>实现细节 — 点击展开</summary>
Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。提示词组装在发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。
Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。提示词组装在发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。失败均为 `WorkingDirectoryError`:路径为空或格式错误时为 `WORKING_DIRECTORY_INVALID`,请求切换到不存在的目录时为 `WORKING_DIRECTORY_NOT_FOUND`,当前目录与原始项目都不存在或服务已释放时为 `WORKING_DIRECTORY_UNAVAILABLE`。
不发布 `./invariant` 伴生模块,因为服务从 Session 投影读取唯一的目录状态,并在使用时检查文件系统有效性。
@@ -0,0 +1,34 @@
/** Machine-routable Session working-directory failures. @module */
import { HarnessError } from '@deepseek-ai/dsh-llm'
/**
* Every {@link WorkingDirectoryError} code:
* - `WORKING_DIRECTORY_INVALID` — a requested directory is empty or otherwise malformed.
* - `WORKING_DIRECTORY_NOT_FOUND` — the requested directory does not exist or is not a directory; the caller picks another path.
* - `WORKING_DIRECTORY_UNAVAILABLE` — neither the current directory nor the original project exists, or the owning service was disposed.
*/
export const WORKING_DIRECTORY_ERROR_CODES = [
'WORKING_DIRECTORY_INVALID',
'WORKING_DIRECTORY_NOT_FOUND',
'WORKING_DIRECTORY_UNAVAILABLE',
] as const
/** One code from {@link WORKING_DIRECTORY_ERROR_CODES}. */
export type WorkingDirectoryErrorCode = typeof WORKING_DIRECTORY_ERROR_CODES[number]
/** Working-directory failure carrying a stable {@link WorkingDirectoryErrorCode}. */
export class WorkingDirectoryError extends HarnessError {
override readonly code: WorkingDirectoryErrorCode
/**
* @param message - names the directory and the safe next step.
* @param code - the failure class consumers route on.
* @param options - standard `cause` chaining.
*/
constructor(message: string, code: WorkingDirectoryErrorCode, options?: ErrorOptions) {
super(message, code, options)
this.name = 'WorkingDirectoryError'
this.code = code
}
}
@@ -13,6 +13,9 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { Session } from '@deepseek-ai/dsh-session'
import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { WorkingDirectoryError } from './error.ts'
export { WORKING_DIRECTORY_ERROR_CODES, WorkingDirectoryError, type WorkingDirectoryErrorCode } from './error.ts'
declare module '@deepseek-ai/cordis' {
interface Events {
@@ -125,7 +128,7 @@ export class WorkingDirectoryService extends Service {
return assembly
}, { prepend: true })
ctx.effect(() => async () => {
this.lifecycle.abort(new Error('working-directory service disposed'))
this.lifecycle.abort(new WorkingDirectoryError('working-directory service disposed', 'WORKING_DIRECTORY_UNAVAILABLE'))
await Promise.allSettled(this.operations)
})
}
@@ -145,7 +148,8 @@ export class WorkingDirectoryService extends Service {
* @param signal - cancellation for filesystem inspection.
* @returns the existing directory; recovery is committed before fulfillment.
* A notice failure is warned without reverting the committed state.
* @throws when the original project is also unavailable, or when a `working-directory/access` listener rejects.
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_UNAVAILABLE` when the original project is also
* unavailable or the service is disposed, or the rejection of a `working-directory/access` listener.
*/
ensure(agent: Agent, signal?: AbortSignal): Promise<string> {
return this.serialize(agent, signal, control => this.ctx.waterfall('working-directory/access', { agent, path: null, signal: control }, async () => {
@@ -161,7 +165,10 @@ export class WorkingDirectoryService extends Service {
return current
}
const original = agent.session.header.cwd ?? this.defaultDirectory
const restored = await this.directory(original, original, control)
const restored = await this.directory(original, original, control, () => new WorkingDirectoryError(
`working-directory: directory does not exist: ${JSON.stringify(original)}; the current working directory ${JSON.stringify(current)} is also unavailable`,
'WORKING_DIRECTORY_UNAVAILABLE',
))
this.commit(agent, restored, true, control)
return restored
}))
@@ -174,11 +181,13 @@ export class WorkingDirectoryService extends Service {
* @param signal - cancellation before the durable change.
* @returns the canonical absolute directory, committed before fulfillment.
* A notice failure is warned; the next request still receives the committed directory.
* @throws when the requested path is not an existing directory.
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_INVALID` for an empty path and
* `WORKING_DIRECTORY_NOT_FOUND` when the path is not an existing directory, or the rejection of a
* `working-directory/access` listener.
*/
set(agent: Agent, path: string, signal?: AbortSignal): Promise<string> {
return this.serialize(agent, signal, control => this.ctx.waterfall('working-directory/access', { agent, path, signal: control }, async () => {
if (path.length === 0) throw new Error('working-directory: cd must not be empty')
if (path.length === 0) throw new WorkingDirectoryError('working-directory: cd must not be empty', 'WORKING_DIRECTORY_INVALID')
const directory = await this.directory(path, this.get(agent.session), control)
this.commit(agent, directory, false, control)
return directory
@@ -189,13 +198,19 @@ export class WorkingDirectoryService extends Service {
return `Current working directory: ${JSON.stringify(this.get(session))}.`
}
private async directory(path: string, cwd: string, signal: AbortSignal): Promise<string> {
private async directory(
path: string,
cwd: string,
signal: AbortSignal,
missing = (): WorkingDirectoryError => new WorkingDirectoryError(
`working-directory: directory does not exist: ${JSON.stringify(path)}`,
'WORKING_DIRECTORY_NOT_FOUND',
),
): Promise<string> {
const target = await this.ctx.fs.resolve(path, { cwd, signal })
const info = await this.ctx.fs.stat(target, signal)
signal.throwIfAborted()
if (info?.type !== 'directory') {
throw new Error(`working-directory: directory does not exist: ${JSON.stringify(path)}`)
}
if (info?.type !== 'directory') throw missing()
return this.ctx.fs.processPath(target)
}
@@ -115,6 +115,11 @@ describe('Session working directories', () => {
const agent = await f.harness.create(SessionId('removed-origin'), {}, { cwd: origin })
await rm(origin, { recursive: true })
await expect(f.ctx.workingDirectory.ensure(agent)).rejects.toThrow('directory does not exist')
await expect(f.ctx.workingDirectory.ensure(agent)).rejects.toMatchObject({
name: 'WorkingDirectoryError',
code: 'WORKING_DIRECTORY_UNAVAILABLE',
message: expect.stringContaining('is also unavailable') as string,
})
await expect(f.ctx.workingDirectory.set(agent, f.root)).resolves.toBe(f.root)
})
@@ -122,7 +127,9 @@ describe('Session working directories', () => {
await using f = await fixture()
await writeFile(join(f.root, 'file'), '')
await expect(f.ctx.workingDirectory.set(f.agent, 'file')).rejects.toThrow('directory does not exist')
await expect(f.ctx.workingDirectory.set(f.agent, 'file')).rejects.toMatchObject({ name: 'WorkingDirectoryError', code: 'WORKING_DIRECTORY_NOT_FOUND' })
await expect(f.ctx.workingDirectory.set(f.agent, '')).rejects.toThrow('cd must not be empty')
await expect(f.ctx.workingDirectory.set(f.agent, '')).rejects.toMatchObject({ name: 'WorkingDirectoryError', code: 'WORKING_DIRECTORY_INVALID' })
const controller = new AbortController()
controller.abort(new Error('cancelled directory change'))
await expect(f.ctx.workingDirectory.set(f.agent, f.root, controller.signal)).rejects.toThrow('cancelled directory change')
@@ -277,6 +284,7 @@ describe('Session working directories', () => {
expect(f.agent.session.snapshotEvents().filter(event => event.type === 'working-directory/change')).toEqual([])
expect(f.harness.claim(f.agent, 'next-step', 1)).toEqual([])
await expect(directories.ensure(f.agent)).rejects.toThrow('working-directory service disposed')
await expect(directories.ensure(f.agent)).rejects.toMatchObject({ name: 'WorkingDirectoryError', code: 'WORKING_DIRECTORY_UNAVAILABLE' })
expect(probe).toHaveBeenCalledOnce()
} finally {
release.resolve(undefined)
@@ -352,7 +360,18 @@ describe('Session working directories', () => {
})
expect(await execute({})).toMatchObject({ value: { cwd: f.root }, isError: false })
expect(await execute({ cd: 'child' })).toMatchObject({ value: { cwd: join(f.root, 'child') }, isError: false })
expect(await execute({}, false)).toMatchObject({ isError: true })
expect(await execute({}, false)).toMatchObject({
isError: true,
error: { info: { name: 'ToolContextUnavailableError', code: 'TOOL_CONTEXT_UNAVAILABLE' } },
})
expect(await execute({ cd: '' })).toMatchObject({
isError: true,
error: { info: { name: 'ToolArgsError', code: 'INVALID_ARGS' } },
})
expect(await execute({ cd: 'missing' })).toMatchObject({
isError: true,
error: { info: { name: 'WorkingDirectoryError', code: 'WORKING_DIRECTORY_NOT_FOUND' } },
})
await fiber.dispose()
expect(f.ctx.tools.schemas().some(tool => tool.name === 'working_directory')).toBe(false)
})