dsh-live-compact 1.0.7 → 1.0.9

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/raw.js CHANGED
@@ -6,8 +6,15 @@
6
6
  * An existing file must match the first meta hash. A mismatch is not a backup.
7
7
  */
8
8
  import { createHash } from "node:crypto";
9
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
9
+ import { existsSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
+ import {
12
+ discardShadowFiles,
13
+ ensureShadowDir,
14
+ tightenShadow,
15
+ writeShadowFile,
16
+ writeShadowOwner,
17
+ } from "./shadow.js";
11
18
 
12
19
  export const PRUNE_MARKER = "[... tool result middle pruned ...]";
13
20
 
@@ -90,40 +97,106 @@ export function rawExcerptText(surface, record) {
90
97
  // 正文已经带修剪标记、又没有旧文件时,不把修剪后的文本当成原文。
91
98
  // Write once per toolCallId. An existing file is verified, never overwritten.
92
99
  // Pruned text with no earlier file is not stored as the original.
93
- export function captureToolResult({ dir, toolCallId, text, turn, name, pathLabel }) {
94
- if (!toolCallId) return { ok: false, reason: "missing-id", toolCallId: "" };
100
+ export function captureToolResult({
101
+ dir,
102
+ toolCallId,
103
+ text,
104
+ turn,
105
+ name,
106
+ pathLabel,
107
+ access = {},
108
+ }) {
109
+ if (!toolCallId) {
110
+ return { ok: false, reason: "missing-id", toolCallId: "" };
111
+ }
112
+
95
113
  const body = String(text ?? "");
96
114
  const file = join(dir, rawName(toolCallId));
97
- if (existsSync(file)) return readExisting(file, toolCallId);
98
- if (body.includes(PRUNE_MARKER)) return { ok: false, reason: "already-pruned", toolCallId };
115
+ const metaPath = `${file}.meta.json`;
116
+
117
+ if (!existsSync(file) && body.includes(PRUNE_MARKER)) {
118
+ return { ok: false, reason: "already-pruned", toolCallId };
119
+ }
120
+
121
+ const readProtectedExisting = () => {
122
+ tightenShadow(file, "file", access);
123
+ if (existsSync(metaPath)) {
124
+ tightenShadow(metaPath, "file", access);
125
+ }
126
+ return readExisting(file, toolCallId);
127
+ };
128
+
129
+ let createdRaw = false;
130
+
99
131
  try {
100
- mkdirSync(dir, { recursive: true, mode: 0o700 });
132
+ ensureShadowDir(dir, access);
133
+
134
+ if (existsSync(file)) return readProtectedExisting();
135
+
101
136
  const bytes = Buffer.from(body, "utf8");
102
137
  const hash = contentHash(bytes);
103
- const metaPath = `${file}.meta.json`;
104
- writeFileSync(file, bytes, { flag: "wx", mode: 0o600 });
105
- try {
106
- writeFileSync(metaPath, JSON.stringify({
138
+
139
+ writeShadowFile(file, bytes, access, { exclusive: true });
140
+ createdRaw = true;
141
+
142
+ writeShadowFile(
143
+ metaPath,
144
+ JSON.stringify({
107
145
  toolCallId,
108
146
  bytes: bytes.length,
109
147
  sha256: hash,
110
148
  turn: turn ?? null,
111
149
  name: name || "",
112
150
  label: pathLabel || "",
113
- }), { mode: 0o600 });
114
- } catch (error) {
115
- try {
116
- unlinkSync(file);
117
- } catch {}
151
+ }),
152
+ access,
153
+ );
154
+
155
+ return {
156
+ ok: true,
157
+ existed: false,
158
+ path: file,
159
+ bytes: bytes.length,
160
+ sha256: hash,
161
+ toolCallId,
162
+ label: pathLabel || "",
163
+ };
164
+ } catch (error) {
165
+ if (createdRaw) discardShadowFiles([file], error);
166
+
167
+ if (
168
+ error?.code === "ESHADOWACCESS" ||
169
+ error?.code === "ESHADOWCLEANUP"
170
+ ) {
118
171
  throw error;
119
172
  }
120
- return { ok: true, existed: false, path: file, bytes: bytes.length, sha256: hash, toolCallId, label: pathLabel || "" };
121
- } catch (error) {
122
- if (error?.code === "EEXIST" && existsSync(file)) return readExisting(file, toolCallId);
173
+
174
+ if (error?.code === "EEXIST" && existsSync(file)) {
175
+ return readProtectedExisting();
176
+ }
177
+
123
178
  return { ok: false, reason: "unwritable", toolCallId };
124
179
  }
125
180
  }
126
181
 
182
+ export function captureOwnedToolResult(options) {
183
+ const { dir, access = {} } = options;
184
+ const owner = join(dir, "owner");
185
+ const ownerExisted = existsSync(owner);
186
+ let ownerWritten = false;
187
+
188
+ try {
189
+ writeShadowOwner(dir, access);
190
+ ownerWritten = true;
191
+ return captureToolResult(options);
192
+ } catch (error) {
193
+ if (ownerWritten && !ownerExisted) {
194
+ discardShadowFiles([owner], error);
195
+ }
196
+ throw error;
197
+ }
198
+ }
199
+
127
200
  export function pointerFor(record, readFile = readFileSync) {
128
201
  const id = record?.toolCallId || "";
129
202
  if (!id) return "";
package/shadow.js CHANGED
@@ -7,9 +7,26 @@
7
7
  * Delete legacy root files, then dead sessions, then old files of the current session.
8
8
  * Never delete the current card's file or another live session. ENOENT counts as already freed.
9
9
  */
10
+ import { execFileSync } from "node:child_process";
10
11
  import { randomBytes } from "node:crypto";
11
- import { closeSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
- import { join, resolve } from "node:path";
12
+ import {
13
+ chmodSync,
14
+ closeSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ mkdtempSync,
18
+ openSync,
19
+ readFileSync,
20
+ readSync,
21
+ readdirSync,
22
+ renameSync,
23
+ rmSync,
24
+ statSync,
25
+ unlinkSync,
26
+ writeFileSync,
27
+ } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { basename, dirname, join, resolve } from "node:path";
13
30
 
14
31
  const PLUGIN_NAME = /^(?:(?:cut|official|shadow)-\d+-[0-9a-f]{8}|raw-[0-9a-f]{16}|\d+-[0-9a-f]{8}-[0-9a-f]{8}|\d+-[0-9a-f]{8,16})\.txt$/;
15
32
  const SESSION_MARK = /^# spine-session (\S+)/;
@@ -99,6 +116,179 @@ export function recordCutStats(dir, { saved = 0, rewrite = 0 } = {}) {
99
116
  }
100
117
  }
101
118
 
119
+ export function shadowAccessPlan(platform = process.platform, userSid = "") {
120
+ if (platform === "win32") {
121
+ if (!/^S-\d+(?:-\d+)+$/.test(userSid)) {
122
+ throw new Error("spine: current Windows user SID is required");
123
+ }
124
+
125
+ return {
126
+ kind: "acl",
127
+ usesChmodOnly: false,
128
+ inheritance: "remove",
129
+ grant: [
130
+ `*${userSid}:(OI)(CI)F`,
131
+ "*S-1-5-18:(OI)(CI)F",
132
+ "*S-1-5-32-544:(OI)(CI)F",
133
+ ],
134
+ sddl:
135
+ `D:P(A;OICI;FA;;;${userSid})` +
136
+ "(A;OICI;FA;;;SY)" +
137
+ "(A;OICI;FA;;;BA)",
138
+ };
139
+ }
140
+
141
+ return {
142
+ kind: "mode",
143
+ usesChmodOnly: true,
144
+ dirMode: 0o700,
145
+ fileMode: 0o600,
146
+ };
147
+ }
148
+
149
+ export function defaultShadowAccessRunner({ path, kind, platform, execFile = execFileSync }) {
150
+ if (platform !== "win32") {
151
+ const plan = shadowAccessPlan(platform);
152
+ chmodSync(path, kind === "dir" ? plan.dirMode : plan.fileMode);
153
+ return true;
154
+ }
155
+
156
+ const identity = execFile(
157
+ "whoami.exe",
158
+ ["/user", "/fo", "csv", "/nh"],
159
+ { encoding: "utf8", windowsHide: true },
160
+ );
161
+ const match = identity.match(/,"(S-\d+(?:-\d+)+)"\s*$/i);
162
+ if (!match) {
163
+ throw new Error("spine: cannot determine current Windows user SID");
164
+ }
165
+
166
+ const plan = shadowAccessPlan("win32", match[1]);
167
+ const target = resolve(path);
168
+ const temp = mkdtempSync(join(tmpdir(), "spine-acl-"));
169
+
170
+ try {
171
+ const aclFile = join(temp, "access.acl");
172
+ const aclBody = `\ufeff${basename(target)}\r\n${plan.sddl}\r\n`;
173
+
174
+ writeFileSync(aclFile, Buffer.from(aclBody, "utf16le"), {
175
+ flag: "wx",
176
+ mode: 0o600,
177
+ });
178
+
179
+ execFile(
180
+ "icacls.exe",
181
+ [dirname(target), "/restore", aclFile, "/q"],
182
+ { stdio: "pipe", windowsHide: true },
183
+ );
184
+
185
+ return true;
186
+ } finally {
187
+ rmSync(temp, { recursive: true, force: true });
188
+ }
189
+ }
190
+
191
+ export function tightenShadow(path, kind = "dir", access = {}) {
192
+ const platform = access.platform ?? process.platform;
193
+ const runner = access.runner ?? defaultShadowAccessRunner;
194
+
195
+ try {
196
+ if (runner({ path, kind, platform }) !== true) {
197
+ throw new Error("permission runner did not confirm success");
198
+ }
199
+ } catch (cause) {
200
+ const error = new Error(
201
+ "spine: shadow permission action failed; backup refused",
202
+ { cause },
203
+ );
204
+ error.code = "ESHADOWACCESS";
205
+ throw error;
206
+ }
207
+ }
208
+
209
+ export function ensureShadowDir(dir, access = {}) {
210
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
211
+ tightenShadow(dir, "dir", access);
212
+ return dir;
213
+ }
214
+
215
+ export function discardShadowFiles(files, cause) {
216
+ const errors = [];
217
+
218
+ for (const file of files) {
219
+ try {
220
+ unlinkSync(file);
221
+ } catch (error) {
222
+ if (error?.code !== "ENOENT") errors.push(error);
223
+ }
224
+ }
225
+
226
+ if (errors.length > 0) {
227
+ const error = new AggregateError(
228
+ [cause, ...errors],
229
+ "spine: shadow write failed and cleanup failed",
230
+ );
231
+ error.code = cause?.code || "ESHADOWCLEANUP";
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ export function writeShadowFile(
237
+ file,
238
+ body,
239
+ access = {},
240
+ { exclusive = false } = {},
241
+ ) {
242
+ const dir = dirname(file);
243
+ ensureShadowDir(dir, access);
244
+
245
+ const target = exclusive
246
+ ? file
247
+ : join(
248
+ dir,
249
+ `.${basename(file)}-${randomBytes(8).toString("hex")}.tmp`,
250
+ );
251
+
252
+ let fd = null;
253
+ let created = false;
254
+ let protectedPath = target;
255
+
256
+ try {
257
+ fd = openSync(target, "wx", 0o600);
258
+ created = true;
259
+
260
+ if (!exclusive) {
261
+ renameSync(target, file);
262
+ protectedPath = file;
263
+ }
264
+
265
+ tightenShadow(protectedPath, "file", access);
266
+ writeFileSync(fd, body);
267
+
268
+ const opened = fd;
269
+ fd = null;
270
+ closeSync(opened);
271
+ return file;
272
+ } catch (error) {
273
+ if (fd !== null) {
274
+ try {
275
+ closeSync(fd);
276
+ } catch {}
277
+ }
278
+
279
+ if (created) discardShadowFiles([protectedPath], error);
280
+ throw error;
281
+ }
282
+ }
283
+
284
+ export function writeShadowOwner(dir, access = {}) {
285
+ return writeShadowFile(
286
+ join(dir, "owner"),
287
+ String(process.pid),
288
+ access,
289
+ );
290
+ }
291
+
102
292
  export function shadowMaxBytes(env = process.env) {
103
293
  const bytes = Number(env.SPINE_SHADOW_MAX_BYTES);
104
294
  if (Number.isFinite(bytes) && bytes > 0) return Math.floor(bytes);