Merge refreshed schema DSL into canonical tool outputs
# Conflicts: # packages/bash/tool-bash/tests/tools.spec.ts
This commit is contained in:
@@ -110,10 +110,10 @@ export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void {
|
||||
},
|
||||
async execute(args: EditToolArgs, exec) {
|
||||
const input = parseEditArgs(args)
|
||||
// Resolve the per-call sandbox mode (escalation grant > session override
|
||||
// > backend default) BEFORE anything executes.
|
||||
const sandboxMode = await sandbox.stampMode('edit', args, exec)
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec))
|
||||
// Resolve the per-call sandbox policy (approved mode > session override
|
||||
// > backend default, plus the session cwd root) BEFORE anything executes.
|
||||
const sandboxPolicy = await sandbox.resolvePolicy('edit', args, exec)
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec, input.filePath, sandboxPolicy?.workspaceRoot))
|
||||
// Single-slot decision: the policy plugin returns { version: vObserved } or
|
||||
// throws FS_NOT_OBSERVED; the bare default is undefined (unconditional edit).
|
||||
// No stat — the bare default never manufactures a version basis.
|
||||
@@ -125,11 +125,11 @@ export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void {
|
||||
{ oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll },
|
||||
intent,
|
||||
exec.signal,
|
||||
sandboxMode,
|
||||
sandboxPolicy,
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
// A sandbox denial becomes the shared [sandbox: …] marker; any other error passes through.
|
||||
throw sandbox.mapError(error, sandboxMode)
|
||||
throw sandbox.mapError(error, sandboxPolicy)
|
||||
}
|
||||
// Record the observed version (a no-op when no policy plugin listens).
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
|
||||
@@ -64,7 +64,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
streamMinSize: resolved.readStreamMinSize,
|
||||
})
|
||||
// One escalation surface shared by both mutating tools: advertisement gating,
|
||||
// per-call mode stamping, and denial-marker mapping, all keyed off whether
|
||||
// per-call policy resolution, and denial-marker mapping, all keyed off whether
|
||||
// the mounted ctx.fs confines (ctx.fs.sandboxMode).
|
||||
const sandbox = new FsSandboxSurface(ctx)
|
||||
applyWriteTool(ctx, sandbox)
|
||||
|
||||
@@ -123,7 +123,7 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const input = parseReadArgs(args, caps.limit)
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec))
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec, input.filePath))
|
||||
|
||||
// One stat: type check + size routing + the version recorded as observed.
|
||||
// A concurrent write can only make a later guarded mutation fail stale and require reread.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The sandbox-escalation surface shared by the `write` and `edit` tools: the
|
||||
* per-call mode stamp, the advertised escalation fields, and the denial-marker
|
||||
* per-call policy resolution, the advertised escalation fields, and the denial-marker
|
||||
* mapping — all delegating the vocabulary and the fail-closed approval
|
||||
* sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash`
|
||||
* uses), so bash and fs escalate identically. Built ONCE per plugin from
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
|
||||
import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import { FsError } from '@deepseek-ai/dsh-fs'
|
||||
|
||||
/** The two escalation arguments a mutating tool may carry (advertised only under a confining backend). */
|
||||
@@ -30,20 +30,23 @@ export interface EscalationSchemaFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* The filesystem escalation surface: advertisement gating, per-call mode
|
||||
* stamping (folding the session's `sandbox/mode` override), the one-approved
|
||||
* wider retry, and denial-marker mapping. A pure product of `ctx` at plugin
|
||||
* apply time.
|
||||
* The filesystem escalation surface: advertisement gating, per-call policy
|
||||
* resolution, the one-approved wider retry, and denial-marker mapping. A pure
|
||||
* product of `ctx` at plugin apply time.
|
||||
*/
|
||||
export class FsSandboxSurface {
|
||||
/** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
|
||||
readonly escalationModes: readonly SandboxMode[]
|
||||
/** The backend's default mode, or `undefined` when `ctx.fs` does not confine. */
|
||||
private readonly defaultMode: SandboxMode | undefined
|
||||
/** Shared per-session policy resolver, required by a confining backend. */
|
||||
private readonly policy: SandboxPolicyService | undefined
|
||||
|
||||
constructor(private readonly ctx: Context) {
|
||||
this.defaultMode = ctx.fs.sandboxMode
|
||||
this.escalationModes = this.defaultMode === undefined ? [] : ESCALATION_TARGETS
|
||||
const defaultMode = ctx.fs.sandboxMode
|
||||
this.escalationModes = defaultMode === undefined ? [] : ESCALATION_TARGETS
|
||||
this.policy = defaultMode === undefined ? undefined : ctx.get('sandboxPolicy')
|
||||
if (defaultMode !== undefined && this.policy === undefined) {
|
||||
throw new Error('tool-fs: the mounted filesystem confines but ctx.sandboxPolicy is missing')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,37 +73,29 @@ export class FsSandboxSurface {
|
||||
}
|
||||
|
||||
/**
|
||||
* The session's standing mode override for an ordinary (non-escalating)
|
||||
* call — the `sandbox/mode` fold of the calling agent's log. Undefined for a
|
||||
* non-confining backend and for agent-less callers.
|
||||
*/
|
||||
private sessionOverride(exec: ToolExecution): SandboxMode | undefined {
|
||||
if (this.defaultMode === undefined || exec.agent === undefined) return undefined
|
||||
return effectiveSandboxMode(exec.agent.session.events)
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode to STAMP onto this mutation: an approved escalation grant (a
|
||||
* The policy to stamp onto this mutation: an approved escalation grant (a
|
||||
* strictly wider retry resolved through `ctx.approval` before anything
|
||||
* executes), else the session's standing override, else `undefined` (the
|
||||
* backend applies its own default). Validates the escalation argument
|
||||
* executes), else the session's standing mode. The calling session's cwd is
|
||||
* always carried as the workspace root. Validates the escalation argument
|
||||
* pairing first.
|
||||
* @param toolName - the mutating tool's name, for the approval audit trail.
|
||||
* @param args - the call's escalation arguments.
|
||||
* @param exec - the tool-execution context (agent, callId, signal).
|
||||
* @returns the mode to pass to the mutation, or undefined for the backend default.
|
||||
* @returns the policy to pass to the mutation, or undefined for an
|
||||
* unsandboxed backend.
|
||||
*/
|
||||
async stampMode(toolName: string, args: FsEscalationArgs, exec: ToolExecution): Promise<SandboxMode | undefined> {
|
||||
async resolvePolicy(toolName: string, args: FsEscalationArgs, exec: ToolExecution): Promise<SandboxExecutionPolicy | undefined> {
|
||||
validateEscalationArgs(args.sandbox_permissions, args.justification)
|
||||
const standingPolicy = this.policy?.resolve({ ...exec.agent ? { session: exec.agent.session } : {} })
|
||||
if (args.sandbox_permissions === undefined || args.justification === undefined) {
|
||||
return this.sessionOverride(exec)
|
||||
return standingPolicy
|
||||
}
|
||||
if (this.escalationModes.length === 0) {
|
||||
throw new Error('sandbox_permissions is not available in this composition (no sandboxing filesystem to escalate)')
|
||||
}
|
||||
const effectiveMode = (this.sessionOverride(exec) ?? this.defaultMode) as SandboxMode
|
||||
return approveEscalation(
|
||||
{ requestedMode: args.sandbox_permissions, justification: args.justification, effectiveMode, subject: 'operation' },
|
||||
const policy = standingPolicy as SandboxExecutionPolicy
|
||||
const approvedMode = await approveEscalation(
|
||||
{ requestedMode: args.sandbox_permissions, justification: args.justification, effectiveMode: policy.mode, subject: 'operation' },
|
||||
{
|
||||
approver: this.ctx.get('approval'),
|
||||
agent: exec.agent,
|
||||
@@ -109,6 +104,7 @@ export class FsSandboxSurface {
|
||||
signal: exec.signal,
|
||||
},
|
||||
)
|
||||
return { ...policy, mode: approvedMode }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,14 +118,14 @@ export class FsSandboxSurface {
|
||||
* confining backend, which always advertises the escalation fields, so the
|
||||
* hint always applies here.
|
||||
* @param error - the error thrown by the mutation.
|
||||
* @param stampedMode - the mode stamped onto the call (names the mode in the marker).
|
||||
* @param policy - the policy stamped onto the call (names the mode in the marker).
|
||||
* @returns the error to throw — the marker `FsError` for a sandbox denial, else the original.
|
||||
*/
|
||||
mapError(error: unknown, stampedMode: SandboxMode | undefined): unknown {
|
||||
mapError(error: unknown, policy: SandboxExecutionPolicy | undefined): unknown {
|
||||
if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error
|
||||
// A FS_SANDBOX_DENIED only arises under a confining backend, so defaultMode
|
||||
// (hence the resolved mode) is defined here.
|
||||
const mode = (stampedMode ?? this.defaultMode) as SandboxMode
|
||||
// A FS_SANDBOX_DENIED only arises under a confining backend, whose tool
|
||||
// path always resolves a policy before mutation.
|
||||
const mode = (policy as SandboxExecutionPolicy).mode
|
||||
return new FsError(`${sandboxDenialMarker(mode)}\n${escalationHintMarker('operation')}`, 'FS_SANDBOX_DENIED', { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,36 @@
|
||||
*/
|
||||
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { canonicalPath } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
const PARENT_PATH_SEGMENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/
|
||||
|
||||
/**
|
||||
* The session workspace cwd for this call, or `undefined` when none applies.
|
||||
* @param exec - the tool-execution context; only its optional `agent` is read.
|
||||
* @param requestedPath - the path the provider will resolve; parent traversal
|
||||
* makes a symlinked cwd's filesystem identity observable.
|
||||
* @returns the calling agent's session cwd, or undefined for a non-agent caller (the backend then applies its own default).
|
||||
*/
|
||||
export function sessionCwd(exec: ToolExecution): string | undefined {
|
||||
return exec.agent?.session.header.cwd
|
||||
export function sessionCwd(exec: ToolExecution, requestedPath: string): string | undefined {
|
||||
const cwd = exec.agent?.session.header.cwd
|
||||
if (cwd === undefined || (!PARENT_PATH_SEGMENT.test(cwd) && !PARENT_PATH_SEGMENT.test(requestedPath))) return cwd
|
||||
return canonicalPath(cwd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolution options shared by all model-facing filesystem tools.
|
||||
* @param exec - the tool-execution context supplying session cwd and cancellation.
|
||||
* @param requestedPath - the path the provider will resolve.
|
||||
* @param policyWorkspaceRoot - resolved per-call root, when a mutation carries sandbox policy.
|
||||
* @returns provider resolution options for the current tool call.
|
||||
*/
|
||||
export function sessionResolveOptions(exec: ToolExecution): { cwd?: string; signal?: AbortSignal } {
|
||||
const cwd = sessionCwd(exec)
|
||||
export function sessionResolveOptions(
|
||||
exec: ToolExecution,
|
||||
requestedPath: string,
|
||||
policyWorkspaceRoot?: string,
|
||||
): { cwd?: string; signal?: AbortSignal } {
|
||||
const cwd = policyWorkspaceRoot ?? sessionCwd(exec, requestedPath)
|
||||
return {
|
||||
...cwd !== undefined ? { cwd } : {},
|
||||
signal: exec.signal,
|
||||
|
||||
@@ -100,21 +100,21 @@ export function applyWriteTool(ctx: Context, sandbox: FsSandboxSurface): void {
|
||||
},
|
||||
async execute(args: WriteToolArgs, exec) {
|
||||
const input = parseWriteArgs(args)
|
||||
// Resolve the per-call sandbox mode (escalation grant > session override
|
||||
// > backend default) BEFORE anything executes; an escalating call
|
||||
// resolves approval here and throws its distinct text on any non-grant.
|
||||
const sandboxMode = await sandbox.stampMode('write', args, exec)
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec))
|
||||
// Resolve the per-call sandbox policy (approved mode > session override
|
||||
// > backend default, plus the session cwd root) BEFORE anything executes;
|
||||
// an escalating call throws its distinct text on any non-grant.
|
||||
const sandboxPolicy = await sandbox.resolvePolicy('write', args, exec)
|
||||
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec, input.filePath, sandboxPolicy?.workspaceRoot))
|
||||
// Single-slot decision: the policy plugin produces createIfAbsent/
|
||||
// replaceIfVersion; the bare default is undefined (unconditional). No stat.
|
||||
const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)
|
||||
let outcome: FsWriteOutcome
|
||||
try {
|
||||
outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal, sandboxMode)
|
||||
outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal, sandboxPolicy)
|
||||
} catch (error: unknown) {
|
||||
// A sandbox denial becomes the shared [sandbox: …] marker (the model
|
||||
// recognizes it from bash); any other error passes through.
|
||||
throw sandbox.mapError(error, sandboxMode)
|
||||
throw sandbox.mapError(error, sandboxPolicy)
|
||||
}
|
||||
// Record the observed version (a no-op when no policy plugin listens).
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, sep } from 'node:path'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
@@ -24,8 +27,10 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import { STREAM_MIN_SIZE } from '../src/read.ts'
|
||||
import { formatReadOutput } from '../src/read-render.ts'
|
||||
import type { FileReadOutcome } from '../src/read-render.ts'
|
||||
import { sessionCwd } from '../src/session-cwd.ts'
|
||||
import ApprovalService from '@deepseek-ai/dsh-user-approval'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
@@ -107,6 +112,32 @@ function text(result: { content: { type: string; text?: string }[] }): string {
|
||||
return result.content.filter(b => b.type === 'text').map(b => b.text).join('')
|
||||
}
|
||||
|
||||
describe('session cwd resolution', () => {
|
||||
const execution = (cwd?: string) => cwd === undefined
|
||||
? {}
|
||||
: { agent: { session: { header: { cwd } } } }
|
||||
|
||||
it('retains ordinary spelling but resolves the cwd before parent traversal', () => {
|
||||
const cwd = process.cwd()
|
||||
const throughParent = `${cwd}${sep}..`
|
||||
expect(sessionCwd(execution() as never, 'file.txt')).toBeUndefined()
|
||||
expect(sessionCwd(execution(cwd) as never, 'file.txt')).toBe(cwd)
|
||||
expect(sessionCwd(execution(throughParent) as never, 'file.txt')).toBe(realpathSync.native(throughParent))
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-tool-fs-session-cwd-'))
|
||||
const physical = join(root, 'physical')
|
||||
const link = join(root, 'link')
|
||||
try {
|
||||
mkdirSync(physical)
|
||||
symlinkSync(physical, link, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
expect(sessionCwd(execution(link) as never, 'child.txt')).toBe(link)
|
||||
expect(sessionCwd(execution(link) as never, `..${sep}parent.txt`)).toBe(realpathSync.native(link))
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('registration', () => {
|
||||
it('registers read, write, and edit', async () => {
|
||||
const { ctx } = await setup()
|
||||
@@ -610,9 +641,9 @@ describe('read caps are plugin config', () => {
|
||||
})
|
||||
|
||||
describe('sandbox escalation surface (write/edit)', () => {
|
||||
/** A confining fake `ctx.fs`: reports a default mode, records the per-call mode stamped, and can arm a sandbox denial. */
|
||||
/** A confining fake `ctx.fs`: reports a default mode, records each per-call policy, and can arm a sandbox denial. */
|
||||
class SandboxingFakeFs extends FakeFs {
|
||||
stamped: (SandboxMode | undefined)[] = []
|
||||
stamped: (SandboxExecutionPolicy | undefined)[] = []
|
||||
override get sandboxMode(): SandboxMode {
|
||||
return 'workspace-write'
|
||||
}
|
||||
@@ -621,9 +652,9 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
content: string,
|
||||
expected?: FsWriteIntent,
|
||||
_signal?: AbortSignal,
|
||||
sandboxMode?: SandboxMode,
|
||||
sandboxPolicy?: SandboxExecutionPolicy,
|
||||
): Promise<FsWriteOutcome> {
|
||||
this.stamped.push(sandboxMode)
|
||||
this.stamped.push(sandboxPolicy)
|
||||
return super.writeText(target, content, expected)
|
||||
}
|
||||
override async editText(
|
||||
@@ -631,9 +662,9 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
edit: FsEditRequest,
|
||||
expected?: { version: FsVersion },
|
||||
_signal?: AbortSignal,
|
||||
sandboxMode?: SandboxMode,
|
||||
sandboxPolicy?: SandboxExecutionPolicy,
|
||||
): Promise<FsEditOutcome> {
|
||||
this.stamped.push(sandboxMode)
|
||||
this.stamped.push(sandboxPolicy)
|
||||
return super.editText(target, edit, expected)
|
||||
}
|
||||
}
|
||||
@@ -642,6 +673,7 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write' })
|
||||
await ctx.plugin(SandboxingFakeFs)
|
||||
await ctx.plugin(FsPolicy)
|
||||
if (opts.approval === true) await ctx.plugin(ApprovalService)
|
||||
@@ -654,7 +686,7 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
return {
|
||||
id: 'agent-fs-esc',
|
||||
session: {
|
||||
header: { version: 0, id: 'sess-fs-esc', createdAt: 0 },
|
||||
header: { version: 0, id: 'sess-fs-esc', createdAt: 0, cwd: '/session-project' },
|
||||
events: [{ type: 'turn/start' }, ...events],
|
||||
append: (type: string, data: Record<string, unknown>) => { events.push({ type, data }) },
|
||||
},
|
||||
@@ -667,6 +699,14 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
return schema as unknown as { parameters: { properties: Record<string, { enum?: string[] }> } }
|
||||
}
|
||||
|
||||
it('fails load when a confining filesystem has no shared sandbox-policy resolver', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(SandboxingFakeFs)
|
||||
await expect(ctx.plugin(ToolFs)).rejects.toThrow('tool-fs: the mounted filesystem confines but ctx.sandboxPolicy is missing')
|
||||
})
|
||||
|
||||
it('advertises no escalation fields under a non-confining backend', async () => {
|
||||
const { ctx } = await setup()
|
||||
expect(ctx.fs.sandboxMode).toBeUndefined()
|
||||
@@ -686,16 +726,16 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('a plain write stamps nothing (backend default) and no session override folds without one', async () => {
|
||||
it('a plain write stamps the default mode with the calling session root', async () => {
|
||||
const { ctx, fs } = await setupConfining()
|
||||
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent())
|
||||
expect(fs.stamped).toEqual([undefined])
|
||||
expect(fs.stamped).toEqual([{ mode: 'workspace-write', workspaceRoot: '/session-project' }])
|
||||
})
|
||||
|
||||
it('a standing session override folds onto the stamp', async () => {
|
||||
const { ctx, fs } = await setupConfining()
|
||||
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent([{ type: 'sandbox/mode', data: { mode: 'read-only' } }]))
|
||||
expect(fs.stamped).toEqual(['read-only'])
|
||||
expect(fs.stamped).toEqual([{ mode: 'read-only', workspaceRoot: '/session-project' }])
|
||||
})
|
||||
|
||||
it('a denied write maps to the shared marker plus the escalation hint (isError)', async () => {
|
||||
@@ -728,7 +768,7 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
agent: escalationAgent() as never,
|
||||
signal: new AbortController().signal,
|
||||
})
|
||||
expect(fs.stamped).toEqual(['danger-full-access'])
|
||||
expect(fs.stamped).toEqual([{ mode: 'danger-full-access', workspaceRoot: '/session-project' }])
|
||||
})
|
||||
|
||||
it('a rejected escalation fails closed with its own text and never mutates', async () => {
|
||||
|
||||
Reference in New Issue
Block a user