@deepseek-ai/dsh-api-workspace-files 0.1.5-alpha.1 → 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,29 @@
1
1
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
2
2
  import { parseFileAddress } from '@deepseek-ai/dsh-util-workspace-path';
3
3
  /**
4
- * Build the `file` provider over one Remote face, one change feed, and the Client's Session list.
4
+ * Build the `file` provider over one Remote face and one change feed.
5
5
  * @param remote - the Remote face carrying `workspaceFiles.stat`.
6
6
  * @param changes - the per-session change fan-out.
7
- * @param sessions - the current Session, read for absolute addresses on every open and reload.
8
7
  * @returns the provider to register into `ctx.resources`.
9
8
  */
10
- export function createFileResourceProvider(remote, changes, sessions) {
9
+ export function createFileResourceProvider(remote, changes) {
11
10
  return {
12
11
  protocol: 'file',
13
12
  async *open(address, { signal }) {
14
- const resolved = resolve(address, sessions);
13
+ const resolved = resolve(address);
15
14
  if (!resolved.ok) {
16
15
  yield resolved;
17
16
  return;
18
17
  }
19
18
  const { sessionId, path } = resolved.value;
20
19
  // Queue changes delivered to this Client while stat is pending.
21
- const notices = changes.follow(sessionId, address, signal);
20
+ const notices = changes.follow(sessionId, signal);
22
21
  const stat = () => remote.workspaceFiles.stat(sessionId, path, signal);
23
22
  // Read through a call: a plain `signal.aborted` is narrowed to `false` by
24
23
  // the first check and would read as always-false after the later awaits.
25
24
  const aborted = () => signal.aborted;
26
25
  // Undefined while the last stat failed: the follow is on the address, not
27
- // on the file, so a write or a reload can still bring the file live.
26
+ // on the file, so a write can still bring the file live.
28
27
  let current;
29
28
  try {
30
29
  if (!await notices.ready || aborted())
@@ -34,7 +33,7 @@ export function createFileResourceProvider(remote, changes, sessions) {
34
33
  return;
35
34
  if (first.ok) {
36
35
  notices.bind(first.value.absolutePath);
37
- current = metadataOf(first.value, false);
36
+ current = first.value;
38
37
  yield { ok: true, value: current };
39
38
  }
40
39
  else {
@@ -51,11 +50,11 @@ export function createFileResourceProvider(remote, changes, sessions) {
51
50
  // consumer learns nothing new.
52
51
  if (notice.version === current.version)
53
52
  continue;
54
- current = { ...current, version: notice.version, changed: true };
53
+ current = { ...current, version: notice.version };
55
54
  yield { ok: true, value: current };
56
55
  continue;
57
56
  }
58
- // A Host notice may mean stale content; only a reload clears the flag.
57
+ // A Host notice may mean stale content.
59
58
  const again = await stat();
60
59
  if (aborted())
61
60
  return;
@@ -65,7 +64,7 @@ export function createFileResourceProvider(remote, changes, sessions) {
65
64
  continue;
66
65
  }
67
66
  notices.bind(again.value.absolutePath);
68
- current = metadataOf(again.value, notice.kind !== 'restat');
67
+ current = again.value;
69
68
  yield { ok: true, value: current };
70
69
  }
71
70
  }
@@ -73,20 +72,14 @@ export function createFileResourceProvider(remote, changes, sessions) {
73
72
  notices.dispose();
74
73
  }
75
74
  },
76
- reload(address) {
77
- const resolved = resolve(address, sessions);
78
- if (resolved.ok)
79
- changes.requestRestat(resolved.value.sessionId, address);
80
- },
81
75
  };
82
76
  }
83
77
  /**
84
78
  * Resolve one address to the Host call it stands for, or to the failure frame it earns.
85
79
  * @param address - the full address, scheme included.
86
- * @param sessions - the Client's Session list.
87
80
  * @returns the Host file, or the `unsupported-address` / `unknown-workspace` failure.
88
81
  */
89
- function resolve(address, sessions) {
82
+ function resolve(address) {
90
83
  const parsed = parseFileAddress(address);
91
84
  if (parsed === undefined)
92
85
  return { ok: false, error: unsupportedAddress(address) };
@@ -95,10 +88,7 @@ function resolve(address, sessions) {
95
88
  const sessionId = parsed.sessionId;
96
89
  return { ok: true, value: { sessionId, path: parsed.path } };
97
90
  }
98
- const sessionId = sessions.current();
99
- if (sessionId === undefined)
100
- return { ok: false, error: unknownWorkspace(address) };
101
- return { ok: true, value: { sessionId, path: parsed.path } };
91
+ return { ok: false, error: unknownWorkspace(address) };
102
92
  }
103
93
  /**
104
94
  * The failure frame's error for an address this provider does not serve.
@@ -109,21 +99,11 @@ function unsupportedAddress(address) {
109
99
  return new RemoteError('workspace-file/unsupported-address', `${address} is not a dsh-resource://file/session/<sessionId>/<path> or dsh-resource://file/absolute/<path> address`, { address });
110
100
  }
111
101
  /**
112
- * The failure frame's error for an absolute address with no current Session.
102
+ * The failure frame's error for an absolute address with no Session.
113
103
  * @param address - the offending address.
114
104
  * @returns the typed error.
115
105
  */
116
106
  function unknownWorkspace(address) {
117
- return new RemoteError('workspace-file/unknown-workspace', `${address} requires a current Session`, { address });
118
- }
119
- /**
120
- * The resource value one `stat` result amounts to.
121
- * @param stat - what the Host reported.
122
- * @param changed - whether the consumer's content may be stale: `true` after a
123
- * Host notice prompted the stat, `false` for the opening stat and a reload's.
124
- * @returns the metadata frame value.
125
- */
126
- function metadataOf(stat, changed) {
127
- return { absolutePath: stat.absolutePath, version: stat.version, changed, ...(stat.bytes === undefined ? {} : { bytes: stat.bytes }) };
107
+ return new RemoteError('workspace-file/unknown-workspace', `${address} requires a dsh-resource://file/session/<sessionId>/<path> address`, { address });
128
108
  }
129
109
  //# sourceMappingURL=provider.js.map
@@ -1,11 +1,11 @@
1
+ import type { WorkspaceFileStat } from '../types.ts';
1
2
  declare module '@deepseek-ai/dsh-client-ui-slots' {
2
3
  interface ResourceProtocolMap {
3
4
  /**
4
5
  * One workspace file's metadata, addressed as
5
- * `dsh-resource://file/session/<sessionId>/<workspace-relative path>` or
6
- * `dsh-resource://file/absolute/<absolute path>`.
6
+ * `dsh-resource://file/session/<sessionId>/<path>` (absolute or workspace-relative).
7
7
  */
8
- file: WorkspaceFileResource;
8
+ file: WorkspaceFileStat;
9
9
  }
10
10
  }
11
11
  /** What a `file` tab is asked to reveal on open or navigation; JSON-shaped. */
@@ -17,7 +17,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
17
17
  interface RemoteErrorDetailsMap {
18
18
  /**
19
19
  * The address is not a `dsh-resource://file/` address in a scope the
20
- * provider serves: `session/<sessionId>/<workspace-relative path>` or
20
+ * provider recognizes: `session/<sessionId>/<path>` or
21
21
  * `absolute/<absolute path>`. Raised by the Client provider; the Host never
22
22
  * emits it.
23
23
  */
@@ -25,7 +25,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
25
25
  readonly address: string;
26
26
  };
27
27
  /**
28
- * An `absolute` address has no current Session to authorize its Host call.
28
+ * An `absolute` address has no Session to authorize its Host call.
29
29
  * Raised by the Client provider; the Host never emits it. Session addresses
30
30
  * are resolved by the Host without a Client Session summary.
31
31
  */
@@ -34,23 +34,6 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
34
34
  };
35
35
  }
36
36
  }
37
- /**
38
- * One workspace file as the resource model carries it: metadata only.
39
- *
40
- * The stream reports that the file moved on; it never carries content. A
41
- * consumer reads the text itself, by page, and uses `version` and `changed` to
42
- * know when its pages are stale.
43
- */
44
- export interface WorkspaceFileResource {
45
- /** Absolute path in the Host filesystem, as returned by the last successful stat. */
46
- readonly absolutePath: string;
47
- /** The Host's latest report of the file's version: from `stat` first, then from each reported write. */
48
- readonly version: string;
49
- /** Byte size as of the last `stat`, when the backend reports it. */
50
- readonly bytes?: number;
51
- /** The Host reported a write after the last `stat`; a reload (`stat` again) clears it. */
52
- readonly changed: boolean;
53
- }
54
37
  /** One Host-reported write inside the session's workspace. */
55
38
  export type WorkspaceFileEdit = {
56
39
  readonly kind: 'changed';
@@ -58,8 +41,6 @@ export type WorkspaceFileEdit = {
58
41
  } | {
59
42
  readonly kind: 'absent';
60
43
  };
61
- /** What one follower of a path receives: a Host write, or a local request to `stat` again. */
62
- export type WorkspaceFileNotice = WorkspaceFileEdit | {
63
- readonly kind: 'restat';
64
- };
44
+ /** What one follower of a path receives: a Host write. */
45
+ export type WorkspaceFileNotice = WorkspaceFileEdit;
65
46
  //# sourceMappingURL=types.d.ts.map
@@ -1,20 +1,14 @@
1
1
  /**
2
- * Workspace file service: paged text reads, byte-window reads, stats, directory
3
- * listings, and the agent-write change feed inside one session's workspace
4
- * root, exposed as the `workspaceFiles` Remote namespace.
2
+ * Workspace file service: read-only file previews, workspace directory
3
+ * listings, and the filesystem-observation change feed, exposed as
4
+ * `workspaceFiles`.
5
5
  *
6
- * Reads through `ctx.fs` are deliberately unconfined — the sandboxing backend
7
- * fences writes and edits only, and says so. Every constraint this service
8
- * needs is therefore its own, and there are four:
9
- *
10
- * 1. The path is authorized by containment in the session's workspace root.
11
- * 2. Containment is decided by {@link FileSystem.contains}, never by comparing
12
- * path strings: `resolve` realpaths, so a prefix test cannot see a symlink
13
- * that leaves the root. `lstat` rejects a link before that follow happens.
14
- * 3. Every cap is validated Config, changeable per deployment. A page is cut by
15
- * lines and refused, not shortened, when its bytes exceed the byte cap; a
16
- * listing is cut by entries and says so.
17
- * 4. Failures are one `RemoteError` per reason, declared in `./types`.
6
+ * File reads follow the composed filesystem's read access, including paths
7
+ * outside the workspace. The selected Session header supplies the base for
8
+ * relative paths, with the sandbox policy root as its no-cwd fallback, not a
9
+ * read-containment restriction. Directory listings and change observations
10
+ * remain workspace-scoped. File-kind checks and configured read caps apply to
11
+ * every preview; this service exposes no mutations.
18
12
  *
19
13
  * A page is cut from `streamText`, which decodes and rejects non-UTF-8 as it
20
14
  * goes, so the file is read only up to the first character past the page and
@@ -26,8 +20,8 @@
26
20
  */
27
21
  import type { Context } from '@deepseek-ai/cordis';
28
22
  import z from '@deepseek-ai/schemastery';
29
- import type { Agent } from '@deepseek-ai/dsh-agent';
30
- import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
23
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
24
+ import { TypertRemoteService, type TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
31
25
  import type { WorkspaceByteRange, WorkspaceDirectoryListing, WorkspaceFileBytes, WorkspaceFileRange, WorkspaceFileStat, WorkspaceFileText, WorkspaceFileWatchFrame } from './types.ts';
32
26
  export type * from './types.ts';
33
27
  declare module '@deepseek-ai/cordis' {
@@ -36,6 +30,19 @@ declare module '@deepseek-ai/cordis' {
36
30
  workspaceFiles: WorkspaceFiles;
37
31
  }
38
32
  }
33
+ /** Header-derived file resolution context for one Session identity. */
34
+ export interface WorkspaceFileScope {
35
+ /** Session identity received on the wire. */
36
+ readonly sessionId: SessionId;
37
+ /** Session workspace root, or the deployment fallback when its header has no cwd. */
38
+ readonly workspaceRoot: string;
39
+ }
40
+ declare module '@deepseek-ai/dsh-typert-protocol' {
41
+ interface TypertLookupMap {
42
+ /** Resolve a Session id to its workspace root without loading its event body or activating an Agent. */
43
+ workspaceFileScope: TypertLookup<WorkspaceFileScope, SessionId>;
44
+ }
45
+ }
39
46
  /** Deployment caps on one page or one listing. */
40
47
  export interface Config {
41
48
  /**
@@ -46,12 +53,14 @@ export interface Config {
46
53
  * way. The file itself has no size cap: a caller pages through it.
47
54
  */
48
55
  readonly maxBytes: number;
56
+ /** Inclusive byte cap on a complete-file read; larger files are refused, never truncated. */
57
+ readonly maxFileBytes: number;
49
58
  /** Default and largest page size in lines; a request asking for more is refused. */
50
59
  readonly maxLines: number;
51
60
  /** Cap on returned directory entries; the rest is dropped and reported cut. */
52
61
  readonly maxEntries: number;
53
62
  }
54
- /** Host Remote service over the composed filesystem, confined to one workspace. */
63
+ /** Host Remote file reads and workspace directory observations over the composed filesystem. */
55
64
  export declare class WorkspaceFiles extends TypertRemoteService {
56
65
  private readonly config;
57
66
  static inject: string[];
@@ -63,68 +72,74 @@ export declare class WorkspaceFiles extends TypertRemoteService {
63
72
  */
64
73
  constructor(ctx: Context, config: Config);
65
74
  /**
66
- * Read one page of lines from a UTF-8 text file inside the Agent's workspace.
67
- * @param agent - target Agent resolved from the Session identity on the wire.
68
- * @param path - workspace path, absolute or relative to the workspace root.
75
+ * Read one page of lines from a UTF-8 file readable by the filesystem backend.
76
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
77
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
69
78
  * @param range - the line window; omitted fields take the page defaults.
70
79
  * @param signal - caller cancellation.
71
80
  * @returns the page, the file's version at the stat before it, and whether it reaches the last line.
72
81
  */
73
- read(agent: Agent, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>;
82
+ read(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>;
74
83
  /**
75
- * Read one byte window of a regular file inside the Agent's workspace: raw
84
+ * Read one byte window of a regular file readable by the filesystem backend: raw
76
85
  * bytes, no text decoding and no binary rejection.
77
- * @param agent - target Agent resolved from the Session identity on the wire.
78
- * @param path - workspace path, absolute or relative to the workspace root.
86
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
87
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
79
88
  * @param range - the byte window; omitted fields take the window defaults.
80
89
  * @param signal - caller cancellation.
81
90
  * @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
82
91
  */
83
- readBytes(agent: Agent, path: string, range: WorkspaceByteRange, signal: AbortSignal): Promise<WorkspaceFileBytes>;
92
+ readBytes(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceByteRange, signal: AbortSignal): Promise<WorkspaceFileBytes>;
93
+ /**
94
+ * Read a complete regular file as bytes, subject to the configured full-file cap.
95
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
96
+ * @param path - absolute or workspace-relative file path.
97
+ * @param signal - caller cancellation.
98
+ * @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
99
+ */
100
+ readAll(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>;
101
+ /**
102
+ * Read a complete file relative to another file's directory, including outside the workspace.
103
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
104
+ * @param path - base file, absolute or workspace-relative.
105
+ * @param relativePath - relative filesystem path, not a URL or absolute path.
106
+ * @param signal - caller cancellation.
107
+ * @returns the complete related file using the ordinary file-size and access checks.
108
+ */
109
+ readRelated(workspaceFileScope: WorkspaceFileScope, path: string, relativePath: string, signal: AbortSignal): Promise<WorkspaceFileBytes>;
84
110
  /**
85
111
  * Report one regular file's identity, version, and size without its content.
86
- * @param agent - target Agent resolved from the Session identity on the wire.
87
- * @param path - workspace path, absolute or relative to the workspace root.
112
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
113
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
88
114
  * @param signal - caller cancellation.
89
115
  * @returns the file's absolute path, current version, and byte size.
90
116
  */
91
- stat(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceFileStat>;
117
+ stat(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceFileStat>;
92
118
  /**
93
- * List the direct children of one directory inside the Agent's workspace.
94
- * @param agent - target Agent resolved from the Session identity on the wire.
119
+ * List the direct children of one directory inside the Session's workspace.
120
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
95
121
  * @param path - workspace path, absolute or relative to the workspace root.
96
122
  * @param signal - caller cancellation.
97
123
  * @returns the directory's children in the backend's stable name order, bounded by the entry cap.
98
124
  */
99
- list(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceDirectoryListing>;
125
+ list(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceDirectoryListing>;
100
126
  /**
101
- * Stream every `fs/observed` observation of a file inside the Agent's
102
- * workspace. Only Agent filesystem operations report here; the OS is not
103
- * watched.
104
- * @param agent - target Agent resolved from the Session identity on the wire.
127
+ * Stream every `fs/observed` observation of a file inside the Session's
128
+ * workspace. Only instrumented filesystem operations report here; the OS is
129
+ * not watched.
130
+ * @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
105
131
  * @param signal - generation cancellation.
106
132
  * @returns `ready` once the Host observation queue is active and the workspace
107
133
  * root is resolved, then queued and live observations in emission order.
108
134
  */
109
- changes(agent: Agent, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
135
+ changes(workspaceFileScope: WorkspaceFileScope, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
110
136
  /** Apply the page defaults and caps here, so the request never carries them implicitly. */
111
137
  private resolvePage;
112
138
  /** Apply the byte-window defaults and cap; a window above the cap is refused, not shortened. */
113
139
  private resolveWindow;
114
140
  /**
115
- * The workspace root comes from the policy, not from the backend's own cwd
116
- * default: the `minimal` preset shadows the host provider with a bare
117
- * `fs-local` whose cwd differs, and resolving explicitly makes the answer
118
- * the same whichever instance answers.
119
- */
120
- private workspaceRootOf;
121
- /**
122
- * Gates 1 and 2 up to the point where the path's own type is known. The
123
- * path is inspected before containment is decided, so a caller learns whether
124
- * an outside path exists and what kind it is before `outside-workspace`
125
- * refuses it; the caller is the Session's own owner, who can read the Host
126
- * through the Agent anyway, and the accepted cost buys one `lstat` gate for
127
- * every method instead of two resolution orders.
141
+ * Inspect the requested path itself before resolution follows its final
142
+ * component. Directory containment is checked separately by `list`.
128
143
  */
129
144
  private inspect;
130
145
  /** Resolve an inspected path and refuse it unless the workspace contains it. */