dsh-session-rewind 0.0.0-stage → 1.2.7

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.
@@ -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
+ }