dsh-session-rewind 0.0.0-stage → 1.2.8
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/LICENSE +21 -0
- package/README.en.md +153 -0
- package/README.md +162 -2
- package/cordis.patch.yml +9 -0
- package/icon.svg +7 -0
- package/lib/client.js +1714 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +61 -4
- package/src/index.js +10 -0
- package/src/lib/archive.js +399 -0
- package/src/lib/inplace.js +205 -0
- package/src/lib/restore.js +117 -0
- package/src/lib/route.js +594 -0
- package/src/lib/surface.js +185 -0
- package/src/plugin.js +667 -0
- package/test/client-smoke.mjs +440 -0
- package/test/route-smoke.mjs +272 -0
- package/test/selftest.mjs +262 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restoring one captured file to its pre-Turn state.
|
|
3
|
+
*
|
|
4
|
+
* Three mechanisms are used on purpose:
|
|
5
|
+
*
|
|
6
|
+
* - content that was valid UTF-8 goes through the Harness `fs` service, so the
|
|
7
|
+
* restore obeys the same path resolution, locking and version guards as a
|
|
8
|
+
* model-issued write;
|
|
9
|
+
* - a plugin-originated write has no Agent context, and the filesystem service
|
|
10
|
+
* refuses to write without one ("file access denied under workspace-write
|
|
11
|
+
* mode"). The policy is therefore rebuilt from the Turn's own workspace: the
|
|
12
|
+
* same confinement the Turn ran under, widening only for a path the archive
|
|
13
|
+
* recorded outside that workspace;
|
|
14
|
+
* - a binary pre-image, or a file the Turn created and that must therefore
|
|
15
|
+
* disappear, needs an operation the `fs` service does not expose. Those go
|
|
16
|
+
* through Node directly, on a path that is never client input: it is the
|
|
17
|
+
* exact absolute path recorded while the Turn ran.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { rm, writeFile } from 'node:fs/promises';
|
|
21
|
+
|
|
22
|
+
const FULL_ACCESS = 'danger-full-access';
|
|
23
|
+
const WORKSPACE_WRITE = 'workspace-write';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild the write policy for one restore target.
|
|
27
|
+
*
|
|
28
|
+
* Inside the Turn's workspace the narrower `workspace-write` mode is enough;
|
|
29
|
+
* outside it, only the wider mode can reach the file, and using it is still
|
|
30
|
+
* symmetric with the write the Turn itself performed there.
|
|
31
|
+
*
|
|
32
|
+
* @param {{
|
|
33
|
+
* fsService: { resolve: Function, contains: Function },
|
|
34
|
+
* target: object,
|
|
35
|
+
* workspaceRoot: string | undefined,
|
|
36
|
+
* }} input
|
|
37
|
+
* @returns {Promise<{ mode: string, workspaceRoot: string }>}
|
|
38
|
+
*/
|
|
39
|
+
async function sandboxPolicyFor({ fsService, target, workspaceRoot }) {
|
|
40
|
+
const root = typeof workspaceRoot === 'string' && workspaceRoot !== ''
|
|
41
|
+
? workspaceRoot
|
|
42
|
+
: process.cwd();
|
|
43
|
+
let inside = false;
|
|
44
|
+
try {
|
|
45
|
+
const rootTarget = await fsService.resolve(root);
|
|
46
|
+
inside = fsService.contains(rootTarget, target) === true;
|
|
47
|
+
} catch {
|
|
48
|
+
inside = false;
|
|
49
|
+
}
|
|
50
|
+
return { mode: inside ? WORKSPACE_WRITE : FULL_ACCESS, workspaceRoot: root };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {{
|
|
55
|
+
* fsService: { resolve: Function, stat: Function, contains: Function, writeText: Function },
|
|
56
|
+
* archive: { readBlob: (name: string | null) => Promise<Buffer | null> },
|
|
57
|
+
* file: {
|
|
58
|
+
* path: string, displayPath: string, existed: boolean, text: boolean,
|
|
59
|
+
* blob: string | null, size: number, reason?: string,
|
|
60
|
+
* },
|
|
61
|
+
* workspaceRoot?: string,
|
|
62
|
+
* syncObservation?: (absolute: string, observation: { kind: string, version?: string }) => Promise<void>,
|
|
63
|
+
* }} input
|
|
64
|
+
* @returns {Promise<{ path: string, status: 'restored' | 'deleted' | 'skipped' | 'failed', message?: string }>}
|
|
65
|
+
*/
|
|
66
|
+
export async function restoreFile({ fsService, archive, file, workspaceRoot, syncObservation }) {
|
|
67
|
+
const displayPath = typeof file.displayPath === 'string' && file.displayPath !== ''
|
|
68
|
+
? file.displayPath
|
|
69
|
+
: file.path;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A raw-Node mutation is invisible to DSH's fs observation policy, so the
|
|
73
|
+
* policy would keep judging the next write against the pre-restore state and
|
|
74
|
+
* reject it as a stale version. Publishing the true observation re-syncs it.
|
|
75
|
+
*/
|
|
76
|
+
const sync = async (observation) => {
|
|
77
|
+
if (typeof syncObservation !== 'function') return;
|
|
78
|
+
try {
|
|
79
|
+
await syncObservation(file.path, observation);
|
|
80
|
+
} catch {
|
|
81
|
+
/* an unsynced observation only costs a spurious stale-version retry */
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (file.existed !== true) {
|
|
86
|
+
// The Turn created this file, so the pre-Turn state is "absent".
|
|
87
|
+
await rm(file.path, { force: true });
|
|
88
|
+
await sync({ kind: 'absent' });
|
|
89
|
+
return { path: displayPath, status: 'deleted' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const bytes = await archive.readBlob(file.blob);
|
|
93
|
+
if (bytes === null) {
|
|
94
|
+
return {
|
|
95
|
+
path: displayPath,
|
|
96
|
+
status: 'skipped',
|
|
97
|
+
message: 'the recorded pre-Turn content is no longer available',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const target = await fsService.resolve(file.path);
|
|
102
|
+
|
|
103
|
+
if (file.text === true) {
|
|
104
|
+
const policy = await sandboxPolicyFor({ fsService, target, workspaceRoot });
|
|
105
|
+
const current = await fsService.stat(target);
|
|
106
|
+
const expected = current === undefined || current === null
|
|
107
|
+
? { kind: 'createIfAbsent' }
|
|
108
|
+
: { kind: 'replaceIfVersion', version: current.version };
|
|
109
|
+
await fsService.writeText(target, bytes.toString('utf8'), expected, undefined, policy);
|
|
110
|
+
return { path: displayPath, status: 'restored' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await writeFile(file.path, bytes);
|
|
114
|
+
const after = await fsService.stat(target);
|
|
115
|
+
if (after !== undefined && after !== null) await sync({ kind: 'present', version: after.version });
|
|
116
|
+
return { path: displayPath, status: 'restored' };
|
|
117
|
+
}
|