refactor: replace overloaded surface terminology
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
|
||||
README.md: d17dc0747833a0ecb85505260badc79ad739f27f
|
||||
README.md: 6b1e91425c07a073aaa8cbc00fd24318bf369d3c
|
||||
README.zh.md: e13ab2f04b84b59fbb3846c08139e679b2d43be1
|
||||
|
||||
@@ -23,7 +23,7 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
|
||||
- **`writeText`** — atomic: writes to a temp file opened exclusively (`wx`, `0o600`) inside a randomly-named private staging dir (`0o700`) next to the target, then fsyncs and publishes. An existing file's mode is preserved, while new files default to `0o600`; on Windows a new file inherits the destination directory's DACL, while replacement copies the target DACL onto the empty temp before writing and publishes through `ReplaceFileW` so the original access policy survives ([Windows DACL preservation Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md)). The `expected` guard is OPTIONAL: omitting it unconditionally creates-or-overwrites; `createIfAbsent` hard-links the staged file into place as an atomic no-replace publication, so a regular file created after the initial probe is preserved and rejected with `FS_NOT_OBSERVED`, while a non-regular path entry is preserved and rejected with `FS_NOT_REGULAR_FILE`; `replaceIfVersion` replaces only at the observed version (a missing target or mismatch is `FS_STALE_VERSION`). An overwrite returns the prior text as its contextual diff basis only when both the opened prior file and UTF-8 replacement are strictly below `config.diffBasisMaxBytes` (default 10 MiB). The descriptor read enforces that limit even if an external writer replaces or changes the file size after the initial probe. Otherwise the provider returns `before: null`, so presentation uses its whole-file fallback.
|
||||
- **`editText`** — atomic literal read-modify-write over the same primitive, serialized per target by a mutation lock. The `expected` guard is OPTIONAL: when supplied it verifies the version BEFORE literal matching (a stale edit reports `FS_STALE_VERSION`, never `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT` against newer content); omitting it edits the current content unconditionally. A missing target reports `FS_STALE_VERSION` either way. LF-normalizes for matching, restores the file's dominant CRLF/LF style, and rejects empty `oldString` / zero matches (`FS_EDIT_NOT_FOUND`) or ambiguous multi-matches without `replace_all` (`FS_AMBIGUOUS_EDIT`).
|
||||
|
||||
The package-root SDK surface is the default/named `LocalFileSystem` class plus `Config`. Raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring.
|
||||
The package-root SDK API is the default/named `LocalFileSystem` class plus `Config`. Raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-fs-policy",
|
||||
"description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service surface)",
|
||||
"description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)",
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
|
||||
@@ -30,7 +30,7 @@ async function setup() {
|
||||
}
|
||||
|
||||
describe('registration / disposal', () => {
|
||||
it('registers no service surface (it is a plugin, not ctx.fsPolicy)', async () => {
|
||||
it('registers no service API (it is a plugin, not ctx.fsPolicy)', async () => {
|
||||
const { ctx } = await setup()
|
||||
expect((ctx as Context & { fsPolicy?: unknown }).fsPolicy).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/fs/fs/README.md
|
||||
README.md: d53fe69456622e533e5ba5a96bd9dab10c188eaa
|
||||
README.md: 197114c02280f9ece81fa4b97af7e67aa39d77c4
|
||||
README.zh.md: 64b7d79687a0b6a0d81a5037ea6044d4a406f32b
|
||||
|
||||
@@ -48,7 +48,7 @@ This package declares three events (see the generated region of [filesystem.md](
|
||||
|
||||
## Vocabulary
|
||||
|
||||
`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsObservation` distinguishes `{ kind: 'present', version }` from `{ kind: 'absent' }`, so a policy can separate an unseen target from confirmed absence without performing I/O. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. `FsPathInfo` is the no-follow metadata shape that can report `symlink`, unlike target-level `FsInfo`. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy Agent Note](../../../.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_DIRECTORY`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_TOO_LARGE`, `FS_PERMISSION_DENIED`, `FS_IO_ERROR`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts.
|
||||
`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsObservation` distinguishes `{ kind: 'present', version }` from `{ kind: 'absent' }`, so a policy can separate an unseen target from confirmed absence without performing I/O. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. `FsPathInfo` is the no-follow metadata shape that can report `symlink`, unlike target-level `FsInfo`. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy Agent Note](../../../.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_DIRECTORY`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_TOO_LARGE`, `FS_PERMISSION_DENIED`, `FS_IO_ERROR`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry exposes `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export interface FsEditOutcome {
|
||||
|
||||
/**
|
||||
* Stable, machine-routable codes for filesystem failures. Carried on
|
||||
* {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
|
||||
* {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
|
||||
* results so retry/permission/UI layers can branch without parsing messages.
|
||||
*/
|
||||
export type FsErrorCode =
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** Shared surface-only post-policy selection for search result spill. @module dsh-tool-fs-search/surface */
|
||||
/** Shared top-level-call post-policy selection for search result spill. @module dsh-tool-fs-search/direct-call */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { JsonValue, PostToolDecision, ToolDefinition, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
/**
|
||||
* Return the accepted canonical value only when this tool still owns a direct
|
||||
* successful surface call and no downstream policy replaced either projection.
|
||||
* successful top-level call and no downstream policy replaced either projection.
|
||||
* @param ctx - the tool plugin context used to resolve the live scoped owner.
|
||||
* @param tool - the exact registered definition whose value may be projected.
|
||||
* @param exec - the completed execution identity.
|
||||
@@ -13,7 +13,7 @@ import type { JsonValue, PostToolDecision, ToolDefinition, ToolExecution, ToolEx
|
||||
* @param decision - the composed downstream post-policy decision.
|
||||
* @returns the canonical value to project, or `undefined` when spill must defer.
|
||||
*/
|
||||
export function acceptedSurfaceValue(
|
||||
export function acceptedDirectCallValue(
|
||||
ctx: Context,
|
||||
tool: ToolDefinition,
|
||||
exec: ToolExecution,
|
||||
@@ -17,7 +17,7 @@ import type { SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { runRipgrep, toWorkdirRelative, trySaveFormattedResult } from './search-core.ts'
|
||||
import { globSearchMeta, searchViewFromMeta } from './presentation.ts'
|
||||
import { acceptedSurfaceValue } from './surface.ts'
|
||||
import { acceptedDirectCallValue } from './direct-call.ts'
|
||||
|
||||
/**
|
||||
* Default cap on paths retained inline by one `glob` call (the `globMaxResults`
|
||||
@@ -360,7 +360,7 @@ export function applyGlobTool(ctx: Context, caps: GlobToolCaps): void {
|
||||
|
||||
ctx.on('tools/post-execute', async (exec, result, next) => {
|
||||
const decision = await next()
|
||||
const value = acceptedSurfaceValue(ctx, tool, exec, result, decision) as { root: string; paths: string[] } | undefined
|
||||
const value = acceptedDirectCallValue(ctx, tool, exec, result, decision) as { root: string; paths: string[] } | undefined
|
||||
if (value === undefined) return decision
|
||||
const paths = value.paths
|
||||
if (paths.length <= caps.maxResults) return decision
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { GrepMatch } from './search-core.ts'
|
||||
import { SearchError, previewLine, retainGrepMatches, runRipgrep, toWorkdirRelative, trySaveFormattedResult } from './search-core.ts'
|
||||
import { grepSearchMeta, searchViewFromMeta } from './presentation.ts'
|
||||
import { acceptedSurfaceValue } from './surface.ts'
|
||||
import { acceptedDirectCallValue } from './direct-call.ts'
|
||||
|
||||
/**
|
||||
* Default cap on flat matches retained inline by one `grep` call (the
|
||||
@@ -340,7 +340,7 @@ export function applyGrepTool(ctx: Context, caps: GrepToolCaps): void {
|
||||
|
||||
ctx.on('tools/post-execute', async (exec, result, next) => {
|
||||
const decision = await next()
|
||||
const value = acceptedSurfaceValue(ctx, tool, exec, result, decision) as { matches: GrepMatch[] } | undefined
|
||||
const value = acceptedDirectCallValue(ctx, tool, exec, result, decision) as { matches: GrepMatch[] } | undefined
|
||||
if (value === undefined) return decision
|
||||
const matches = value.matches
|
||||
if (matches.length <= caps.maxMatches) return decision
|
||||
|
||||
@@ -82,7 +82,7 @@ export type SearchErrorCode =
|
||||
|
||||
/**
|
||||
* Typed search failure. Extends {@link HarnessError} so it carries a stable
|
||||
* {@link SearchErrorCode} and chains `cause`; the tool registry surfaces
|
||||
* {@link SearchErrorCode} and chains `cause`; the tool registry exposes
|
||||
* `{ name, code }` on `isError` results so retry/permission/UI layers can
|
||||
* branch without parsing messages.
|
||||
*/
|
||||
|
||||
@@ -881,7 +881,7 @@ describe('glob results', () => {
|
||||
expect(spill?.saves).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps the full nested Code value without creating a surface spill', async () => {
|
||||
it('keeps the full nested Code value without creating a top-level spill', async () => {
|
||||
const { ctx, subprocess, spill } = await setup({ config: { globMaxResults: 2 }, spill: true })
|
||||
subprocess.handler = () => runResult('a.ts\nb.ts\nc.ts\nd.ts\n')
|
||||
const result = await call(ctx, 'glob', { pattern: '*.ts' }, {
|
||||
@@ -1028,7 +1028,7 @@ describe('grep results', () => {
|
||||
expect(spill?.saves).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps every nested Code match in the value without creating a surface spill', async () => {
|
||||
it('keeps every nested Code match in the value without creating a top-level spill', async () => {
|
||||
const { ctx, subprocess, spill } = await setup({ config: { grepMaxMatches: 1 }, spill: true })
|
||||
subprocess.handler = () => runResult(`${matchLine('a.ts', 1, 'one')}\n${matchLine('b.ts', 2, 'two')}\n`)
|
||||
const result = await call(ctx, 'grep', { pattern: 'o' }, {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { computeHunkDiffs, diffsFromMeta } from './diff.ts'
|
||||
import { remediateFsError } from './error.ts'
|
||||
import { sessionResolveOptions } from './session-cwd.ts'
|
||||
import type { FsSandboxSurface } from './sandbox.ts'
|
||||
import type { FsSandboxController } from './sandbox.ts'
|
||||
|
||||
/** Validated `edit` arguments after defaulting. */
|
||||
interface EditInput {
|
||||
@@ -71,9 +71,9 @@ export function formatEditOutput(displayPath: string, replaceAll: boolean): stri
|
||||
/**
|
||||
* Register the `edit` tool and its system-prompt guidance.
|
||||
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
||||
* @param sandbox - the shared sandbox-escalation surface (advertisement, mode stamping, denial mapping).
|
||||
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
||||
*/
|
||||
export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void {
|
||||
export function applyEditTool(ctx: Context, sandbox: FsSandboxController): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:edit',
|
||||
order: 102,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { applyWriteTool } from './write.ts'
|
||||
import { applyEditTool } from './edit.ts'
|
||||
import { applyReadImageTool } from './read-image.ts'
|
||||
import { READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from './read-render.ts'
|
||||
import { FsSandboxSurface } from './sandbox.ts'
|
||||
import { FsSandboxController } from './sandbox.ts'
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = 'tool-fs'
|
||||
@@ -70,10 +70,10 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.inject(['attachments'], (imageCtx) => {
|
||||
applyReadImageTool(imageCtx)
|
||||
})
|
||||
// One escalation surface shared by both mutating tools: advertisement gating,
|
||||
// One escalation API shared by both mutating tools: advertisement gating,
|
||||
// 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)
|
||||
const sandbox = new FsSandboxController(ctx)
|
||||
applyWriteTool(ctx, sandbox)
|
||||
applyEditTool(ctx, sandbox)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* The sandbox-escalation surface shared by the `write` and `edit` tools: the
|
||||
* The sandbox-escalation API shared by the `write` and `edit` tools: the
|
||||
* 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`
|
||||
@@ -30,11 +30,11 @@ export interface EscalationSchemaFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* The filesystem escalation surface: advertisement gating, per-call policy
|
||||
* The filesystem escalation API: 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 {
|
||||
export class FsSandboxController {
|
||||
/** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
|
||||
readonly escalationModes: readonly SandboxMode[]
|
||||
/** Shared per-session policy resolver, required by a confining backend. */
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { computeHunkDiffs, diffsFromMeta } from './diff.ts'
|
||||
import { remediateFsError } from './error.ts'
|
||||
import { sessionResolveOptions } from './session-cwd.ts'
|
||||
import type { FsSandboxSurface } from './sandbox.ts'
|
||||
import type { FsSandboxController } from './sandbox.ts'
|
||||
|
||||
/**
|
||||
* Validate value constraints the schema DSL can't express: only a non-blank
|
||||
@@ -57,9 +57,9 @@ interface WriteToolArgs {
|
||||
/**
|
||||
* Register the `write` tool and its system-prompt guidance.
|
||||
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
||||
* @param sandbox - the shared sandbox-escalation surface (advertisement, mode stamping, denial mapping).
|
||||
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
||||
*/
|
||||
export function applyWriteTool(ctx: Context, sandbox: FsSandboxSurface): void {
|
||||
export function applyWriteTool(ctx: Context, sandbox: FsSandboxController): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:write',
|
||||
order: 101,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Consumer-surface tests over a fake provider and the real policy collaborator: schemas,
|
||||
* Consumer API tests over a fake provider and the real policy collaborator: schemas,
|
||||
* validation, formatting, typed errors, intent dispatch, and observation-driven authorization.
|
||||
*/
|
||||
|
||||
@@ -759,7 +759,7 @@ describe('read caps are plugin config', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('sandbox escalation surface (write/edit)', () => {
|
||||
describe('sandbox escalation API (write/edit)', () => {
|
||||
/** 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: (SandboxExecutionPolicy | undefined)[] = []
|
||||
@@ -800,7 +800,7 @@ describe('sandbox escalation surface (write/edit)', () => {
|
||||
return { ctx, fs: ctx.fs as SandboxingFakeFs }
|
||||
}
|
||||
|
||||
/** A fake agent whose session records appends (the approval audit surface), mid-turn, carrying the given events for the fold. */
|
||||
/** A fake agent whose session records appends (the approval audit trail), mid-turn, carrying the given events for the fold. */
|
||||
function escalationAgent(events: Array<{ type: string; data?: Record<string, unknown> }> = []): object {
|
||||
return {
|
||||
id: 'agent-fs-esc',
|
||||
|
||||
Reference in New Issue
Block a user