@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.
- package/README.i18n.yaml +2 -2
- package/README.md +21 -19
- package/README.zh.md +21 -19
- package/lib/client.js +34 -82
- package/lib/index.js +138 -62
- package/lib/typert.host.js +160 -525
- package/lib/typert.remote-client.d.ts +14 -17
- package/lib/typert.remote-client.js +125 -40
- package/lib/types/changes.d.ts +2 -2
- package/lib/types/changes.js +2 -2
- package/lib/types/client/change-feed.d.ts +6 -18
- package/lib/types/client/change-feed.js +6 -38
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/index.js +4 -8
- package/lib/types/client/provider.d.ts +9 -20
- package/lib/types/client/provider.js +13 -33
- package/lib/types/client/types.d.ts +7 -26
- package/lib/types/index.d.ts +65 -50
- package/lib/types/index.js +108 -61
- package/lib/types/types.d.ts +4 -4
- package/package.json +12 -14
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
3
|
import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
4
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
@@ -5,8 +6,8 @@ import { Deque } from "@deepseek-ai/dsh-deque";
|
|
|
5
6
|
/**
|
|
6
7
|
* Producer of the `changes` stream: every `fs/observed` emission whose target
|
|
7
8
|
* lies inside a generation's workspace root becomes one frame of that
|
|
8
|
-
* generation.
|
|
9
|
-
*
|
|
9
|
+
* generation. Instrumented filesystem operations emit these observations; the
|
|
10
|
+
* operating system is not watched.
|
|
10
11
|
* Each generation acknowledges its observation queue and resolved workspace
|
|
11
12
|
* root with `ready` before emitting any queued or live changes.
|
|
12
13
|
*/
|
|
@@ -107,22 +108,16 @@ var ChangeFollower = class {
|
|
|
107
108
|
//#endregion
|
|
108
109
|
//#region lib/types/index.js
|
|
109
110
|
/**
|
|
110
|
-
* Workspace file service:
|
|
111
|
-
* listings, and the
|
|
112
|
-
*
|
|
111
|
+
* Workspace file service: read-only file previews, workspace directory
|
|
112
|
+
* listings, and the filesystem-observation change feed, exposed as
|
|
113
|
+
* `workspaceFiles`.
|
|
113
114
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* path strings: `resolve` realpaths, so a prefix test cannot see a symlink
|
|
121
|
-
* that leaves the root. `lstat` rejects a link before that follow happens.
|
|
122
|
-
* 3. Every cap is validated Config, changeable per deployment. A page is cut by
|
|
123
|
-
* lines and refused, not shortened, when its bytes exceed the byte cap; a
|
|
124
|
-
* listing is cut by entries and says so.
|
|
125
|
-
* 4. Failures are one `RemoteError` per reason, declared in `./types`.
|
|
115
|
+
* File reads follow the composed filesystem's read access, including paths
|
|
116
|
+
* outside the workspace. The selected Session header supplies the base for
|
|
117
|
+
* relative paths, with the sandbox policy root as its no-cwd fallback, not a
|
|
118
|
+
* read-containment restriction. Directory listings and change observations
|
|
119
|
+
* remain workspace-scoped. File-kind checks and configured read caps apply to
|
|
120
|
+
* every preview; this service exposes no mutations.
|
|
126
121
|
*
|
|
127
122
|
* A page is cut from `streamText`, which decodes and rejects non-UTF-8 as it
|
|
128
123
|
* goes, so the file is read only up to the first character past the page and
|
|
@@ -247,12 +242,14 @@ function directoryEntry(child) {
|
|
|
247
242
|
...child.size === void 0 ? {} : { size: child.size }
|
|
248
243
|
};
|
|
249
244
|
}
|
|
250
|
-
/** Host Remote
|
|
245
|
+
/** Host Remote file reads and workspace directory observations over the composed filesystem. */
|
|
251
246
|
let WorkspaceFiles = (() => {
|
|
252
247
|
let _classSuper = TypertRemoteService;
|
|
253
248
|
let _instanceExtraInitializers = [];
|
|
254
249
|
let _read_decorators;
|
|
255
250
|
let _readBytes_decorators;
|
|
251
|
+
let _readAll_decorators;
|
|
252
|
+
let _readRelated_decorators;
|
|
256
253
|
let _stat_decorators;
|
|
257
254
|
let _list_decorators;
|
|
258
255
|
let _changes_decorators;
|
|
@@ -261,6 +258,8 @@ let WorkspaceFiles = (() => {
|
|
|
261
258
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
262
259
|
_read_decorators = [Remote];
|
|
263
260
|
_readBytes_decorators = [Remote];
|
|
261
|
+
_readAll_decorators = [Remote];
|
|
262
|
+
_readRelated_decorators = [Remote];
|
|
264
263
|
_stat_decorators = [Remote];
|
|
265
264
|
_list_decorators = [Remote];
|
|
266
265
|
_changes_decorators = [Remote({ mode: "stream" })];
|
|
@@ -286,6 +285,28 @@ let WorkspaceFiles = (() => {
|
|
|
286
285
|
},
|
|
287
286
|
metadata: _metadata
|
|
288
287
|
}, null, _instanceExtraInitializers);
|
|
288
|
+
__esDecorate(this, null, _readAll_decorators, {
|
|
289
|
+
kind: "method",
|
|
290
|
+
name: "readAll",
|
|
291
|
+
static: false,
|
|
292
|
+
private: false,
|
|
293
|
+
access: {
|
|
294
|
+
has: (obj) => "readAll" in obj,
|
|
295
|
+
get: (obj) => obj.readAll
|
|
296
|
+
},
|
|
297
|
+
metadata: _metadata
|
|
298
|
+
}, null, _instanceExtraInitializers);
|
|
299
|
+
__esDecorate(this, null, _readRelated_decorators, {
|
|
300
|
+
kind: "method",
|
|
301
|
+
name: "readRelated",
|
|
302
|
+
static: false,
|
|
303
|
+
private: false,
|
|
304
|
+
access: {
|
|
305
|
+
has: (obj) => "readRelated" in obj,
|
|
306
|
+
get: (obj) => obj.readRelated
|
|
307
|
+
},
|
|
308
|
+
metadata: _metadata
|
|
309
|
+
}, null, _instanceExtraInitializers);
|
|
289
310
|
__esDecorate(this, null, _stat_decorators, {
|
|
290
311
|
kind: "method",
|
|
291
312
|
name: "stat",
|
|
@@ -330,10 +351,12 @@ let WorkspaceFiles = (() => {
|
|
|
330
351
|
static inject = [
|
|
331
352
|
"fs",
|
|
332
353
|
"sandboxPolicy",
|
|
354
|
+
"sessions",
|
|
333
355
|
"typert"
|
|
334
356
|
];
|
|
335
357
|
static Config = z.object({
|
|
336
358
|
maxBytes: z.number().step(1).min(1).default(2 * 1024 * 1024),
|
|
359
|
+
maxFileBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER - 1).default(32 * 1024 * 1024),
|
|
337
360
|
maxLines: z.number().step(1).min(1).default(5e3),
|
|
338
361
|
maxEntries: z.number().step(1).min(1).default(2e3)
|
|
339
362
|
});
|
|
@@ -346,18 +369,36 @@ let WorkspaceFiles = (() => {
|
|
|
346
369
|
super(ctx, "workspaceFiles");
|
|
347
370
|
this.config = config;
|
|
348
371
|
this.feed = new WorkspaceChangeFeed(ctx);
|
|
372
|
+
ctx.inject(["sessions", "typert"], (scope) => {
|
|
373
|
+
scope.typert.lookups.register("workspaceFileScope", {
|
|
374
|
+
parameter: "workspaceFileScope",
|
|
375
|
+
wire: "workspaceFileScopeId",
|
|
376
|
+
hostTypeSymbol: "@deepseek-ai/dsh-api-workspace-files#WorkspaceFileScope",
|
|
377
|
+
wireTypeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
|
|
378
|
+
resolve: async (sessionId) => {
|
|
379
|
+
const live = scope.sessions.get(sessionId)?.header;
|
|
380
|
+
const stored = live === void 0 ? await scope.get("sessionPersistence")?.stat(sessionId) : void 0;
|
|
381
|
+
const header = live ?? stored?.header;
|
|
382
|
+
if (header === void 0) return void 0;
|
|
383
|
+
return {
|
|
384
|
+
sessionId,
|
|
385
|
+
workspaceRoot: header.cwd ?? scope.sandboxPolicy.workspaceRoot
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
349
390
|
}
|
|
350
391
|
/**
|
|
351
|
-
* Read one page of lines from a UTF-8
|
|
352
|
-
* @param
|
|
353
|
-
* @param path -
|
|
392
|
+
* Read one page of lines from a UTF-8 file readable by the filesystem backend.
|
|
393
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
394
|
+
* @param path - absolute path or path relative to the workspace root; files outside it are allowed.
|
|
354
395
|
* @param range - the line window; omitted fields take the page defaults.
|
|
355
396
|
* @param signal - caller cancellation.
|
|
356
397
|
* @returns the page, the file's version at the stat before it, and whether it reaches the last line.
|
|
357
398
|
*/
|
|
358
|
-
async read(
|
|
399
|
+
async read(workspaceFileScope, path, range, signal) {
|
|
359
400
|
const { offset, limit } = this.resolvePage(range);
|
|
360
|
-
const { target, info } = await this.locateFile(
|
|
401
|
+
const { target, info } = await this.locateFile(workspaceFileScope, path, signal);
|
|
361
402
|
const page = await this.cutPage(target, offset, limit, signal, path);
|
|
362
403
|
if (page.text.includes(NUL)) throw new RemoteError("workspace-file/not-text", `"${path}" contains NUL bytes`, { path });
|
|
363
404
|
return {
|
|
@@ -369,17 +410,17 @@ let WorkspaceFiles = (() => {
|
|
|
369
410
|
};
|
|
370
411
|
}
|
|
371
412
|
/**
|
|
372
|
-
* Read one byte window of a regular file
|
|
413
|
+
* Read one byte window of a regular file readable by the filesystem backend: raw
|
|
373
414
|
* bytes, no text decoding and no binary rejection.
|
|
374
|
-
* @param
|
|
375
|
-
* @param path -
|
|
415
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
416
|
+
* @param path - absolute path or path relative to the workspace root; files outside it are allowed.
|
|
376
417
|
* @param range - the byte window; omitted fields take the window defaults.
|
|
377
418
|
* @param signal - caller cancellation.
|
|
378
419
|
* @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
|
|
379
420
|
*/
|
|
380
|
-
async readBytes(
|
|
421
|
+
async readBytes(workspaceFileScope, path, range, signal) {
|
|
381
422
|
const { offset, length } = this.resolveWindow(range, path);
|
|
382
|
-
const { target, info } = await this.locateFile(
|
|
423
|
+
const { target, info } = await this.locateFile(workspaceFileScope, path, signal);
|
|
383
424
|
const data = await this.ctx.fs.readByteRange(target, {
|
|
384
425
|
offset,
|
|
385
426
|
length
|
|
@@ -393,25 +434,70 @@ let WorkspaceFiles = (() => {
|
|
|
393
434
|
};
|
|
394
435
|
}
|
|
395
436
|
/**
|
|
437
|
+
* Read a complete regular file as bytes, subject to the configured full-file cap.
|
|
438
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
439
|
+
* @param path - absolute or workspace-relative file path.
|
|
440
|
+
* @param signal - caller cancellation.
|
|
441
|
+
* @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
|
|
442
|
+
*/
|
|
443
|
+
async readAll(workspaceFileScope, path, signal) {
|
|
444
|
+
const { target, info } = await this.locateFile(workspaceFileScope, path, signal);
|
|
445
|
+
const limit = this.config.maxFileBytes;
|
|
446
|
+
if (info.size !== void 0 && info.size > limit) throw new RemoteError("workspace-file/too-large", `"${path}" exceeds the ${limit} byte full-file cap`, {
|
|
447
|
+
path,
|
|
448
|
+
limit
|
|
449
|
+
});
|
|
450
|
+
const data = await this.ctx.fs.readByteRange(target, {
|
|
451
|
+
offset: 0,
|
|
452
|
+
length: limit + 1
|
|
453
|
+
}, signal);
|
|
454
|
+
if (data.length > limit) throw new RemoteError("workspace-file/too-large", `"${path}" exceeds the ${limit} byte full-file cap`, {
|
|
455
|
+
path,
|
|
456
|
+
limit
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
...this.statOf(target, info),
|
|
460
|
+
offset: 0,
|
|
461
|
+
data: Buffer.from(data).toString("base64"),
|
|
462
|
+
eof: true
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Read a complete file relative to another file's directory, including outside the workspace.
|
|
467
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
468
|
+
* @param path - base file, absolute or workspace-relative.
|
|
469
|
+
* @param relativePath - relative filesystem path, not a URL or absolute path.
|
|
470
|
+
* @param signal - caller cancellation.
|
|
471
|
+
* @returns the complete related file using the ordinary file-size and access checks.
|
|
472
|
+
*/
|
|
473
|
+
async readRelated(workspaceFileScope, path, relativePath, signal) {
|
|
474
|
+
const relative = relativePath.replace(/\\/g, "/");
|
|
475
|
+
if (relative.length === 0 || relative.startsWith("/") || /^[a-z][a-z\d+.-]*:/iu.test(relative) || relative.includes(NUL)) throw new RemoteError("gateway/bad-request", "relativePath must be a relative filesystem path", {});
|
|
476
|
+
const { target } = await this.locateFile(workspaceFileScope, path, signal);
|
|
477
|
+
const absolute = this.ctx.fs.processPath(target);
|
|
478
|
+
const paths = absolute.startsWith("/") ? posix : win32;
|
|
479
|
+
return this.readAll(workspaceFileScope, paths.resolve(paths.dirname(absolute), relative), signal);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
396
482
|
* Report one regular file's identity, version, and size without its content.
|
|
397
|
-
* @param
|
|
398
|
-
* @param path -
|
|
483
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
484
|
+
* @param path - absolute path or path relative to the workspace root; files outside it are allowed.
|
|
399
485
|
* @param signal - caller cancellation.
|
|
400
486
|
* @returns the file's absolute path, current version, and byte size.
|
|
401
487
|
*/
|
|
402
|
-
async stat(
|
|
403
|
-
const { target, info } = await this.locateFile(
|
|
488
|
+
async stat(workspaceFileScope, path, signal) {
|
|
489
|
+
const { target, info } = await this.locateFile(workspaceFileScope, path, signal);
|
|
404
490
|
return this.statOf(target, info);
|
|
405
491
|
}
|
|
406
492
|
/**
|
|
407
|
-
* List the direct children of one directory inside the
|
|
408
|
-
* @param
|
|
493
|
+
* List the direct children of one directory inside the Session's workspace.
|
|
494
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
409
495
|
* @param path - workspace path, absolute or relative to the workspace root.
|
|
410
496
|
* @param signal - caller cancellation.
|
|
411
497
|
* @returns the directory's children in the backend's stable name order, bounded by the entry cap.
|
|
412
498
|
*/
|
|
413
|
-
async list(
|
|
414
|
-
const { root, workspaceRoot, entry } = await this.inspect(
|
|
499
|
+
async list(workspaceFileScope, path, signal) {
|
|
500
|
+
const { root, workspaceRoot, entry } = await this.inspect(workspaceFileScope, path, signal);
|
|
415
501
|
if (entry.type !== "directory") throw new RemoteError("workspace-file/not-directory", `"${path}" is a ${entry.type}`, {
|
|
416
502
|
path,
|
|
417
503
|
kind: entry.type
|
|
@@ -425,16 +511,16 @@ let WorkspaceFiles = (() => {
|
|
|
425
511
|
};
|
|
426
512
|
}
|
|
427
513
|
/**
|
|
428
|
-
* Stream every `fs/observed` observation of a file inside the
|
|
429
|
-
* workspace. Only
|
|
430
|
-
* watched.
|
|
431
|
-
* @param
|
|
514
|
+
* Stream every `fs/observed` observation of a file inside the Session's
|
|
515
|
+
* workspace. Only instrumented filesystem operations report here; the OS is
|
|
516
|
+
* not watched.
|
|
517
|
+
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
432
518
|
* @param signal - generation cancellation.
|
|
433
519
|
* @returns `ready` once the Host observation queue is active and the workspace
|
|
434
520
|
* root is resolved, then queued and live observations in emission order.
|
|
435
521
|
*/
|
|
436
|
-
changes(
|
|
437
|
-
return this.feed.follow(
|
|
522
|
+
changes(workspaceFileScope, signal) {
|
|
523
|
+
return this.feed.follow(workspaceFileScope.workspaceRoot, signal);
|
|
438
524
|
}
|
|
439
525
|
/** Apply the page defaults and caps here, so the request never carries them implicitly. */
|
|
440
526
|
resolvePage(range) {
|
|
@@ -461,25 +547,12 @@ let WorkspaceFiles = (() => {
|
|
|
461
547
|
};
|
|
462
548
|
}
|
|
463
549
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* `fs-local` whose cwd differs, and resolving explicitly makes the answer
|
|
467
|
-
* the same whichever instance answers.
|
|
468
|
-
*/
|
|
469
|
-
workspaceRootOf(agent) {
|
|
470
|
-
return this.ctx.sandboxPolicy.resolve({ session: agent.session }).workspaceRoot;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Gates 1 and 2 up to the point where the path's own type is known. The
|
|
474
|
-
* path is inspected before containment is decided, so a caller learns whether
|
|
475
|
-
* an outside path exists and what kind it is before `outside-workspace`
|
|
476
|
-
* refuses it; the caller is the Session's own owner, who can read the Host
|
|
477
|
-
* through the Agent anyway, and the accepted cost buys one `lstat` gate for
|
|
478
|
-
* every method instead of two resolution orders.
|
|
550
|
+
* Inspect the requested path itself before resolution follows its final
|
|
551
|
+
* component. Directory containment is checked separately by `list`.
|
|
479
552
|
*/
|
|
480
|
-
async inspect(
|
|
553
|
+
async inspect(workspaceFileScope, path, signal) {
|
|
481
554
|
if (path.length === 0) throw new RemoteError("gateway/bad-request", "path is required", {});
|
|
482
|
-
const workspaceRoot =
|
|
555
|
+
const { workspaceRoot } = workspaceFileScope;
|
|
483
556
|
const root = await this.ctx.fs.resolve(workspaceRoot, { signal });
|
|
484
557
|
const entry = await this.ctx.fs.lstat(path, { cwd: workspaceRoot }, signal);
|
|
485
558
|
if (entry === void 0) throw new RemoteError("workspace-file/not-found", `no entry at "${path}"`, { path });
|
|
@@ -503,13 +576,16 @@ let WorkspaceFiles = (() => {
|
|
|
503
576
|
* and size. The stat re-checks what `lstat` saw: the file may have gone or
|
|
504
577
|
* changed kind in between.
|
|
505
578
|
*/
|
|
506
|
-
async locateFile(
|
|
507
|
-
const {
|
|
579
|
+
async locateFile(workspaceFileScope, path, signal) {
|
|
580
|
+
const { workspaceRoot, entry } = await this.inspect(workspaceFileScope, path, signal);
|
|
508
581
|
if (entry.type !== "file") throw new RemoteError("workspace-file/not-regular-file", `"${path}" is a ${entry.type}`, {
|
|
509
582
|
path,
|
|
510
583
|
kind: entry.type
|
|
511
584
|
});
|
|
512
|
-
const target = await this.
|
|
585
|
+
const target = await this.ctx.fs.resolve(path, {
|
|
586
|
+
cwd: workspaceRoot,
|
|
587
|
+
signal
|
|
588
|
+
});
|
|
513
589
|
const info = await this.ctx.fs.stat(target, signal);
|
|
514
590
|
if (info === void 0) throw new RemoteError("workspace-file/not-found", `no entry at "${path}"`, { path });
|
|
515
591
|
if (info.type !== "file") throw new RemoteError("workspace-file/not-regular-file", `"${path}" is a ${info.type}`, {
|