@ericsanchezok/synergy-util 3.0.23 → 3.1.0
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.codex +201 -0
- package/NOTICE +9 -0
- package/dist/activity.d.ts +9 -4
- package/dist/activity.js +20 -19
- package/dist/asset-reference.d.ts +15 -0
- package/dist/asset-reference.js +98 -0
- package/dist/atomic-file.d.ts +9 -0
- package/dist/atomic-file.js +86 -0
- package/dist/attachment-presentation.d.ts +26 -0
- package/dist/attachment-presentation.js +11 -0
- package/dist/capability.d.ts +1 -1
- package/dist/capability.js +5 -19
- package/dist/cli-command.d.ts +12 -0
- package/dist/cli-command.js +3 -0
- package/dist/encode.d.ts +3 -0
- package/dist/encode.js +7 -0
- package/dist/error.d.ts +36 -56
- package/dist/feature-config.d.ts +4 -0
- package/dist/feature-config.js +23 -0
- package/dist/features.d.ts +134 -0
- package/dist/features.js +195 -0
- package/dist/file-patch.d.ts +31 -0
- package/dist/file-patch.js +218 -0
- package/dist/filesystem-identity.d.ts +28 -0
- package/dist/filesystem-identity.js +71 -0
- package/dist/fn.d.ts +1 -1
- package/dist/fs-lock.d.ts +5 -1
- package/dist/fs-lock.js +14 -6
- package/dist/input-assistance.d.ts +51 -0
- package/dist/input-assistance.js +61 -0
- package/dist/installed-launcher.d.ts +18 -0
- package/dist/installed-launcher.js +39 -0
- package/dist/io-retry.d.ts +11 -0
- package/dist/io-retry.js +36 -0
- package/dist/json-value.d.ts +4 -0
- package/dist/json-value.js +12 -0
- package/dist/markdown-assets.d.ts +6 -0
- package/dist/markdown-assets.js +29 -0
- package/dist/markdown-link.d.ts +2 -0
- package/dist/markdown-link.js +8 -0
- package/dist/native-assets.d.ts +7 -0
- package/dist/native-assets.js +28 -0
- package/dist/network-error.d.ts +7 -0
- package/dist/network-error.js +111 -0
- package/dist/plugin-policy.d.ts +1 -1
- package/dist/process-group.d.ts +45 -0
- package/dist/process-group.js +156 -0
- package/dist/process-identity.js +5 -2
- package/dist/projection-patch.d.ts +9 -0
- package/dist/projection-patch.js +74 -0
- package/dist/reasoning-item.d.ts +1 -0
- package/dist/reasoning-item.js +16 -0
- package/dist/render-artifact.d.ts +352 -0
- package/dist/render-artifact.js +115 -0
- package/dist/render-ui.d.ts +202 -0
- package/dist/render-ui.js +208 -0
- package/dist/resource-reference.d.ts +73 -0
- package/dist/resource-reference.js +227 -0
- package/dist/retry.d.ts +4 -0
- package/dist/retry.js +56 -23
- package/dist/runtime-startup.d.ts +240 -2
- package/dist/runtime-startup.js +108 -9
- package/dist/terminal.d.ts +217 -0
- package/dist/terminal.js +111 -0
- package/dist/tool-display.d.ts +10 -0
- package/dist/tool-display.js +22 -1
- package/dist/tool-timeout.d.ts +21 -0
- package/dist/tool-timeout.js +1 -0
- package/dist/tool.d.ts +6 -7
- package/dist/workbench-view.d.ts +692 -0
- package/dist/workbench-view.js +90 -0
- package/dist/worker-plan.d.ts +39 -0
- package/dist/worker-plan.js +33 -0
- package/package.json +28 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare namespace FilePatch {
|
|
2
|
+
interface Hunk {
|
|
3
|
+
anchor?: string;
|
|
4
|
+
rows: {
|
|
5
|
+
kind: " " | "+" | "-";
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
eof: boolean;
|
|
9
|
+
}
|
|
10
|
+
type Section = {
|
|
11
|
+
type: "add";
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "delete";
|
|
16
|
+
path: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "update";
|
|
19
|
+
path: string;
|
|
20
|
+
move?: string;
|
|
21
|
+
hunks: Hunk[];
|
|
22
|
+
};
|
|
23
|
+
class Invalid extends Error {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
function parse(text: string): Section[];
|
|
27
|
+
function paths(text: string): string[];
|
|
28
|
+
function update(content: string, section: Extract<Section, {
|
|
29
|
+
type: "update";
|
|
30
|
+
}>): string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Provenance: https://github.com/openai/codex/tree/160b2b1ef62114eeb41e41e5e3be0000d944b713/codex-rs/apply-patch/src
|
|
2
|
+
// Local adaptation: Codex parsing and ordered match tiers; preserve BOM and terminal-newline state. Execution stays with the selected Workspace.
|
|
3
|
+
// Copyright 2025 OpenAI. Derived portions are Apache-2.0; see ../LICENSE.codex and ../NOTICE.
|
|
4
|
+
export var FilePatch;
|
|
5
|
+
(function (FilePatch) {
|
|
6
|
+
class Invalid extends Error {
|
|
7
|
+
name = "InvalidFilePatch";
|
|
8
|
+
}
|
|
9
|
+
FilePatch.Invalid = Invalid;
|
|
10
|
+
const whitespace = (text) => text.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
|
|
11
|
+
const trailingWhitespace = (text) => text.replace(/\p{White_Space}+$/gu, "");
|
|
12
|
+
function parse(text) {
|
|
13
|
+
if (text.includes("\0") || !text.isWellFormed())
|
|
14
|
+
throw new Invalid("Patch content must be valid Unicode text without NUL bytes.");
|
|
15
|
+
let lines = whitespace(text).split(/\r?\n/);
|
|
16
|
+
if (["<<EOF", "<<'EOF'", '<<"EOF"'].includes(lines[0]) && lines.at(-1)?.endsWith("EOF") && lines.length >= 4)
|
|
17
|
+
lines = lines.slice(1, -1);
|
|
18
|
+
if (whitespace(lines.shift() ?? "") !== "*** Begin Patch" || whitespace(lines.pop() ?? "") !== "*** End Patch")
|
|
19
|
+
throw new Invalid("Expected one complete *** Begin Patch / *** End Patch document, without surrounding text.");
|
|
20
|
+
const sections = [];
|
|
21
|
+
const filename = (line, prefix) => {
|
|
22
|
+
const value = line.slice(prefix.length);
|
|
23
|
+
if (!value || /[\0\r\n]/.test(value))
|
|
24
|
+
throw new Invalid("Patch paths must be nonempty and contain no NUL, CR or LF characters.");
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
let index = 0;
|
|
28
|
+
while (index < lines.length) {
|
|
29
|
+
const line = whitespace(lines[index++]);
|
|
30
|
+
if (line.startsWith("*** Add File: ")) {
|
|
31
|
+
const path = filename(line, "*** Add File: ");
|
|
32
|
+
const rows = [];
|
|
33
|
+
while (lines[index]?.startsWith("+"))
|
|
34
|
+
rows.push(lines[index++].slice(1));
|
|
35
|
+
sections.push({ type: "add", path, content: rows.length ? `${rows.join("\n")}\n` : "" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (line.startsWith("*** Delete File: ")) {
|
|
39
|
+
sections.push({ type: "delete", path: filename(line, "*** Delete File: ") });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!line.startsWith("*** Update File: "))
|
|
43
|
+
throw new Invalid(`Unexpected patch line: ${line}. Expected an Add, Update or Delete File section.`);
|
|
44
|
+
const section = {
|
|
45
|
+
type: "update",
|
|
46
|
+
path: filename(line, "*** Update File: "),
|
|
47
|
+
hunks: [],
|
|
48
|
+
};
|
|
49
|
+
while (index < lines.length) {
|
|
50
|
+
const row = lines[index];
|
|
51
|
+
const structural = trailingWhitespace(row);
|
|
52
|
+
if (/^\*\*\* (?:Add File: |Update File: |Delete File: )/.test(structural))
|
|
53
|
+
break;
|
|
54
|
+
if (!section.hunks.length && !section.move && structural.startsWith("*** Move to: ")) {
|
|
55
|
+
section.move = filename(structural, "*** Move to: ");
|
|
56
|
+
index++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let hunk = section.hunks.at(-1);
|
|
60
|
+
if (structural === "@@" || structural.startsWith("@@ ")) {
|
|
61
|
+
if (hunk && !hunk.rows.length)
|
|
62
|
+
throw new Invalid(`Empty hunk in ${section.path} at patch line ${index + 2}.`);
|
|
63
|
+
section.hunks.push({ anchor: structural === "@@" ? undefined : structural.slice(3), rows: [], eof: false });
|
|
64
|
+
index++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (hunk?.eof && structural === "") {
|
|
68
|
+
index++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (structural === "*** End of File" && hunk?.rows.length) {
|
|
72
|
+
hunk.eof = true;
|
|
73
|
+
index++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const kind = row === "" ? " " : row[0];
|
|
77
|
+
if (hunk?.eof || (kind !== " " && kind !== "+" && kind !== "-"))
|
|
78
|
+
throw new Invalid(`Invalid update row in ${section.path} at patch line ${index + 2}: ${row}. Use @@, context, +added or -removed lines.`);
|
|
79
|
+
if (!hunk) {
|
|
80
|
+
hunk = { rows: [], eof: false };
|
|
81
|
+
section.hunks.push(hunk);
|
|
82
|
+
}
|
|
83
|
+
hunk.rows.push({ kind, text: row.slice(1) });
|
|
84
|
+
index++;
|
|
85
|
+
}
|
|
86
|
+
if (section.hunks.some((hunk) => !hunk.rows.length))
|
|
87
|
+
throw new Invalid(`Empty update hunk in ${section.path}.`);
|
|
88
|
+
if (!section.hunks.length && !section.move)
|
|
89
|
+
throw new Invalid("An Update requires a hunk or Move to destination.");
|
|
90
|
+
sections.push(section);
|
|
91
|
+
}
|
|
92
|
+
if (!sections.length)
|
|
93
|
+
throw new Invalid("The patch contains no file sections.");
|
|
94
|
+
return sections;
|
|
95
|
+
}
|
|
96
|
+
FilePatch.parse = parse;
|
|
97
|
+
function paths(text) {
|
|
98
|
+
return parse(text).flatMap((section) => section.type === "update" && section.move ? [section.path, section.move] : [section.path]);
|
|
99
|
+
}
|
|
100
|
+
FilePatch.paths = paths;
|
|
101
|
+
function punctuation(text) {
|
|
102
|
+
return whitespace(text)
|
|
103
|
+
.replace(/[\u2010-\u2015\u2212]/g, "-")
|
|
104
|
+
.replace(/[\u2018-\u201b]/g, "'")
|
|
105
|
+
.replace(/[\u201c-\u201f]/g, '"')
|
|
106
|
+
.replace(/[\u00a0\u2002-\u200a\u202f\u205f\u3000]/g, " ");
|
|
107
|
+
}
|
|
108
|
+
function seek(lines, pattern, start, eof) {
|
|
109
|
+
if (!pattern.length)
|
|
110
|
+
return start;
|
|
111
|
+
if (pattern.length > lines.length)
|
|
112
|
+
return;
|
|
113
|
+
const first = eof ? Math.max(start, lines.length - pattern.length) : start;
|
|
114
|
+
for (const normalize of [(line) => line, trailingWhitespace, whitespace, punctuation]) {
|
|
115
|
+
const expected = pattern.map(normalize);
|
|
116
|
+
for (let index = first; index <= lines.length - pattern.length; index++)
|
|
117
|
+
if (expected.every((line, offset) => normalize(lines[index + offset]) === line))
|
|
118
|
+
return index;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function update(content, section) {
|
|
122
|
+
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
123
|
+
const body = content.slice(bom.length);
|
|
124
|
+
const original = [];
|
|
125
|
+
for (const match of body.matchAll(/([^\r\n]*)(\r\n|\n|\r|$)/g)) {
|
|
126
|
+
if (!match[0])
|
|
127
|
+
break;
|
|
128
|
+
original.push({ text: match[1], ending: match[2] });
|
|
129
|
+
}
|
|
130
|
+
const ending = original.find((line) => line.ending)?.ending ?? "\n";
|
|
131
|
+
const terminal = original.at(-1)?.ending !== "" && original.length > 0;
|
|
132
|
+
const lines = original.map((line) => line.text);
|
|
133
|
+
const replacements = [];
|
|
134
|
+
let cursor = 0;
|
|
135
|
+
for (const [index, hunk] of section.hunks.entries()) {
|
|
136
|
+
let start = cursor;
|
|
137
|
+
const missing = (pattern, anchor = false) => {
|
|
138
|
+
const earlier = seek(lines, pattern, 0, hunk.eof);
|
|
139
|
+
const reason = earlier !== undefined && earlier < start
|
|
140
|
+
? `out-of-order or overlapping context at original line ${earlier + 1}; hunks must follow source order`
|
|
141
|
+
: `no matching ${anchor ? "@@ anchor" : hunk.eof ? "end-of-file context" : "context"} at or after original line ${start + 1}`;
|
|
142
|
+
throw new Invalid(`Cannot apply hunk ${index + 1} in ${section.path}: ${reason}.\nExpected lines:\n${pattern.join("\n")}\nRead the affected range and correct this hunk.`);
|
|
143
|
+
};
|
|
144
|
+
if (hunk.anchor !== undefined) {
|
|
145
|
+
const anchor = seek(lines, [hunk.anchor], start, false);
|
|
146
|
+
if (anchor === undefined)
|
|
147
|
+
missing([hunk.anchor], true);
|
|
148
|
+
start = anchor + 1;
|
|
149
|
+
}
|
|
150
|
+
const old = hunk.rows.filter((row) => row.kind !== "+").map((row) => row.text);
|
|
151
|
+
const next = hunk.rows.filter((row) => row.kind !== "-").map((row) => row.text);
|
|
152
|
+
if (!old.length) {
|
|
153
|
+
replacements.push({ start: original.length, length: 0, lines: next });
|
|
154
|
+
cursor = start;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let pattern = old;
|
|
158
|
+
let added = next;
|
|
159
|
+
let position = seek(lines, pattern, start, hunk.eof);
|
|
160
|
+
if (position === undefined && pattern.at(-1) === "") {
|
|
161
|
+
pattern = pattern.slice(0, -1);
|
|
162
|
+
if (added.at(-1) === "")
|
|
163
|
+
added = added.slice(0, -1);
|
|
164
|
+
position = seek(lines, pattern, start, hunk.eof);
|
|
165
|
+
}
|
|
166
|
+
if (position === undefined)
|
|
167
|
+
missing(old);
|
|
168
|
+
let source = 0;
|
|
169
|
+
let target = 0;
|
|
170
|
+
let oldStart = 0;
|
|
171
|
+
let newStart = 0;
|
|
172
|
+
for (const row of hunk.rows) {
|
|
173
|
+
if (row.kind === " ") {
|
|
174
|
+
if (source >= pattern.length || target >= added.length)
|
|
175
|
+
break;
|
|
176
|
+
if (oldStart !== source || newStart !== target)
|
|
177
|
+
replacements.push({
|
|
178
|
+
start: position + oldStart,
|
|
179
|
+
length: source - oldStart,
|
|
180
|
+
lines: added.slice(newStart, target),
|
|
181
|
+
});
|
|
182
|
+
oldStart = source + 1;
|
|
183
|
+
newStart = target + 1;
|
|
184
|
+
}
|
|
185
|
+
if (row.kind !== "+")
|
|
186
|
+
source++;
|
|
187
|
+
if (row.kind !== "-")
|
|
188
|
+
target++;
|
|
189
|
+
}
|
|
190
|
+
if (oldStart !== pattern.length || newStart !== added.length)
|
|
191
|
+
replacements.push({
|
|
192
|
+
start: position + oldStart,
|
|
193
|
+
length: pattern.length - oldStart,
|
|
194
|
+
lines: added.slice(newStart),
|
|
195
|
+
});
|
|
196
|
+
cursor = position + pattern.length;
|
|
197
|
+
}
|
|
198
|
+
replacements.sort((a, b) => a.start - b.start);
|
|
199
|
+
const result = [];
|
|
200
|
+
cursor = 0;
|
|
201
|
+
for (const replacement of replacements) {
|
|
202
|
+
if (replacement.start < cursor)
|
|
203
|
+
throw new Invalid(`Overlapping replacements in ${section.path}.`);
|
|
204
|
+
for (const line of original.slice(cursor, replacement.start))
|
|
205
|
+
result.push(line);
|
|
206
|
+
for (const text of replacement.lines)
|
|
207
|
+
result.push({ text, ending });
|
|
208
|
+
cursor = replacement.start + replacement.length;
|
|
209
|
+
}
|
|
210
|
+
for (const line of original.slice(cursor))
|
|
211
|
+
result.push(line);
|
|
212
|
+
return (bom +
|
|
213
|
+
result
|
|
214
|
+
.map((line, index) => line.text + (index === result.length - 1 ? (terminal ? line.ending || ending : "") : line.ending || ending))
|
|
215
|
+
.join(""));
|
|
216
|
+
}
|
|
217
|
+
FilePatch.update = update;
|
|
218
|
+
})(FilePatch || (FilePatch = {}));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type ObjectStat = {
|
|
2
|
+
dev: bigint;
|
|
3
|
+
ino: bigint;
|
|
4
|
+
birthtimeNs: bigint;
|
|
5
|
+
};
|
|
6
|
+
type VolumeIdentity = (filename: string) => string | undefined | Promise<string | undefined>;
|
|
7
|
+
export declare function filesystemObjectID(stat: ObjectStat, volume?: string): string;
|
|
8
|
+
export declare function matchesLegacyFilesystemIdentity(expected: string, actual: string | undefined, allowRemount?: boolean): boolean;
|
|
9
|
+
export declare function identifyFilesystemObject(filename: string, volumeIdentity?: VolumeIdentity): Promise<{
|
|
10
|
+
directory: boolean;
|
|
11
|
+
physicalID: string;
|
|
12
|
+
legacyPhysicalID: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
directory: boolean;
|
|
15
|
+
physicalID: string;
|
|
16
|
+
legacyPhysicalID: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function identifyDirectory(directory: string, allowMissing?: boolean, volumeIdentity?: VolumeIdentity): Promise<{
|
|
19
|
+
path: string;
|
|
20
|
+
physicalID: string;
|
|
21
|
+
legacyPhysicalID: string | undefined;
|
|
22
|
+
} | {
|
|
23
|
+
legacyPhysicalID?: undefined;
|
|
24
|
+
path: string;
|
|
25
|
+
physicalID: undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function readOrCreateIdentityFile(filename: string): Promise<string>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { withFileLock } from "./fs-lock";
|
|
5
|
+
export function filesystemObjectID(stat, volume) {
|
|
6
|
+
return volume ? `volume-v1:${volume}:${stat.ino}:${stat.birthtimeNs}` : `${stat.dev}:${stat.ino}:${stat.birthtimeNs}`;
|
|
7
|
+
}
|
|
8
|
+
export function matchesLegacyFilesystemIdentity(expected, actual, allowRemount = false) {
|
|
9
|
+
if (actual === expected)
|
|
10
|
+
return true;
|
|
11
|
+
if (!allowRemount || !actual)
|
|
12
|
+
return false;
|
|
13
|
+
const previous = /^\d+:([1-9]\d*):([1-9]\d*)$/.exec(expected);
|
|
14
|
+
const current = /^\d+:([1-9]\d*):([1-9]\d*)$/.exec(actual);
|
|
15
|
+
return !!previous && !!current && previous[1] === current[1] && previous[2] === current[2];
|
|
16
|
+
}
|
|
17
|
+
export async function identifyFilesystemObject(filename, volumeIdentity) {
|
|
18
|
+
const stat = await fs.stat(filename, { bigint: true });
|
|
19
|
+
const directory = stat.isDirectory();
|
|
20
|
+
const overlay = directory && process.platform === "linux" && (await fs.statfs(filename, { bigint: true })).type === 0x794c7630n;
|
|
21
|
+
// OverlayFS copy-up replaces lower metadata, including birthtime, while preserving the directory.
|
|
22
|
+
// Provenance: https://docs.kernel.org/filesystems/overlayfs.html#directories
|
|
23
|
+
// Local adaptation: bind overlay directories to their mount device/inode rather than the backing layer's birthtime.
|
|
24
|
+
const legacyPhysicalID = overlay ? `overlay:${stat.dev}:${stat.ino}` : filesystemObjectID(stat);
|
|
25
|
+
const volume = !overlay && (await volumeIdentity?.(filename));
|
|
26
|
+
if (!volume)
|
|
27
|
+
return { directory, physicalID: legacyPhysicalID, legacyPhysicalID: undefined };
|
|
28
|
+
const current = await fs.stat(filename, { bigint: true });
|
|
29
|
+
if (filesystemObjectID(current) !== filesystemObjectID(stat))
|
|
30
|
+
throw new Error("Filesystem object changed during identity inspection");
|
|
31
|
+
return { directory, physicalID: filesystemObjectID(stat, volume), legacyPhysicalID };
|
|
32
|
+
}
|
|
33
|
+
export async function identifyDirectory(directory, allowMissing = false, volumeIdentity) {
|
|
34
|
+
const absolute = path.resolve(directory);
|
|
35
|
+
try {
|
|
36
|
+
const canonical = await fs.realpath(absolute);
|
|
37
|
+
const identity = await identifyFilesystemObject(canonical, volumeIdentity);
|
|
38
|
+
if (!identity.directory)
|
|
39
|
+
throw Object.assign(new Error("Workspace location is not a directory"), { code: "ENOTDIR" });
|
|
40
|
+
return { path: canonical, physicalID: identity.physicalID, legacyPhysicalID: identity.legacyPhysicalID };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (allowMissing && error.code === "ENOENT")
|
|
44
|
+
return { path: absolute, physicalID: undefined };
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function readOrCreateIdentityFile(filename) {
|
|
49
|
+
return withFileLock({ directory: path.join(path.dirname(filename), ".locks"), key: path.basename(filename) }, async () => {
|
|
50
|
+
const existing = await fs.readFile(filename, "utf8").catch((error) => {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
if (existing !== undefined) {
|
|
56
|
+
if (!/^[0-9a-f-]{36}$/.test(existing))
|
|
57
|
+
throw new Error("Invalid local filesystem namespace identity");
|
|
58
|
+
return existing;
|
|
59
|
+
}
|
|
60
|
+
const identity = randomUUID();
|
|
61
|
+
const handle = await fs.open(filename, "wx", 0o600);
|
|
62
|
+
try {
|
|
63
|
+
await handle.writeFile(identity);
|
|
64
|
+
await handle.sync();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await handle.close();
|
|
68
|
+
}
|
|
69
|
+
return identity;
|
|
70
|
+
});
|
|
71
|
+
}
|
package/dist/fn.d.ts
CHANGED
package/dist/fs-lock.d.ts
CHANGED
|
@@ -5,8 +5,12 @@ export interface FileLockOptions {
|
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
staleMetadataMs?: number;
|
|
7
7
|
timeoutMessage?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export declare class FileLockTimeoutError extends Error {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
constructor(key: string, message?: string);
|
|
8
13
|
}
|
|
9
|
-
export declare const HOLOS_ACCOUNTS_WRITE_LOCK_KEY = "holos-accounts:write";
|
|
10
14
|
export declare const LEGACY_API_KEY_WRITE_LOCK_KEY = "legacy-api-key:write";
|
|
11
15
|
export declare function authLockDirectory(synergyRoot: string): string;
|
|
12
16
|
export declare function fileLockPath(directory: string, key: string): string;
|
package/dist/fs-lock.js
CHANGED
|
@@ -2,11 +2,19 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { processStartIdentity } from "./process-identity";
|
|
5
|
+
import { retrySleep } from "./retry";
|
|
6
|
+
export class FileLockTimeoutError extends Error {
|
|
7
|
+
key;
|
|
8
|
+
constructor(key, message) {
|
|
9
|
+
super(message ?? `Timed out acquiring file lock for ${key}`);
|
|
10
|
+
this.key = key;
|
|
11
|
+
this.name = "FileLockTimeoutError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
5
14
|
const DEFAULT_RETRY_MS = 25;
|
|
6
15
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
16
|
const DEFAULT_STALE_METADATA_MS = 5_000;
|
|
8
17
|
const VERIFIED_LIVE_TTL_MS = 1_000;
|
|
9
|
-
export const HOLOS_ACCOUNTS_WRITE_LOCK_KEY = "holos-accounts:write";
|
|
10
18
|
export const LEGACY_API_KEY_WRITE_LOCK_KEY = "legacy-api-key:write";
|
|
11
19
|
export function authLockDirectory(synergyRoot) {
|
|
12
20
|
return path.join(synergyRoot, "data", "auth", ".locks");
|
|
@@ -18,6 +26,7 @@ export function fileLockPath(directory, key) {
|
|
|
18
26
|
export async function withFileLock(options, fn) {
|
|
19
27
|
const lock = await acquireFileLock(options);
|
|
20
28
|
try {
|
|
29
|
+
options.signal?.throwIfAborted();
|
|
21
30
|
return await fn();
|
|
22
31
|
}
|
|
23
32
|
finally {
|
|
@@ -25,6 +34,7 @@ export async function withFileLock(options, fn) {
|
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
async function acquireFileLock(options) {
|
|
37
|
+
options.signal?.throwIfAborted();
|
|
28
38
|
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
29
39
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
30
40
|
const staleMetadataMs = options.staleMetadataMs ?? DEFAULT_STALE_METADATA_MS;
|
|
@@ -36,8 +46,9 @@ async function acquireFileLock(options) {
|
|
|
36
46
|
const ownerToken = randomUUID();
|
|
37
47
|
const verifiedLiveUntil = new Map();
|
|
38
48
|
while (true) {
|
|
49
|
+
options.signal?.throwIfAborted();
|
|
39
50
|
if (Date.now() - startedAt >= timeoutMs) {
|
|
40
|
-
throw new
|
|
51
|
+
throw new FileLockTimeoutError(options.key, options.timeoutMessage);
|
|
41
52
|
}
|
|
42
53
|
let handle;
|
|
43
54
|
try {
|
|
@@ -60,7 +71,7 @@ async function acquireFileLock(options) {
|
|
|
60
71
|
if (await removeStaleLock(filename, snapshot.contents))
|
|
61
72
|
continue;
|
|
62
73
|
}
|
|
63
|
-
await
|
|
74
|
+
await retrySleep(Math.min(retryMs, Math.max(0, timeoutMs - (Date.now() - startedAt))), options.signal);
|
|
64
75
|
continue;
|
|
65
76
|
}
|
|
66
77
|
try {
|
|
@@ -188,6 +199,3 @@ function processExists(pid) {
|
|
|
188
199
|
return error.code !== "ESRCH";
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
|
-
async function sleep(milliseconds) {
|
|
192
|
-
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
193
|
-
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare namespace InputAssistance {
|
|
3
|
+
const Proposal: z.ZodObject<{
|
|
4
|
+
producer: z.ZodString;
|
|
5
|
+
proposalID: z.ZodString;
|
|
6
|
+
generatedFingerprint: z.ZodString;
|
|
7
|
+
acceptedFingerprint: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
const Info: z.ZodObject<{
|
|
10
|
+
version: z.ZodLiteral<1>;
|
|
11
|
+
proposals: z.ZodArray<z.ZodObject<{
|
|
12
|
+
producer: z.ZodString;
|
|
13
|
+
proposalID: z.ZodString;
|
|
14
|
+
generatedFingerprint: z.ZodString;
|
|
15
|
+
acceptedFingerprint: z.ZodString;
|
|
16
|
+
}, z.core.$strip>>;
|
|
17
|
+
submittedFingerprint: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
type Info = z.infer<typeof Info>;
|
|
20
|
+
const Metadata: z.ZodObject<{
|
|
21
|
+
assistance: z.ZodOptional<z.ZodObject<{
|
|
22
|
+
version: z.ZodLiteral<1>;
|
|
23
|
+
proposals: z.ZodArray<z.ZodObject<{
|
|
24
|
+
producer: z.ZodString;
|
|
25
|
+
proposalID: z.ZodString;
|
|
26
|
+
generatedFingerprint: z.ZodString;
|
|
27
|
+
acceptedFingerprint: z.ZodString;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
submittedFingerprint: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
function read(metadata: unknown): Info | undefined;
|
|
33
|
+
function fingerprint(text: string): string;
|
|
34
|
+
function merge(...values: Array<Info | undefined>): Info | undefined;
|
|
35
|
+
function accept(previous: Info | undefined, input: {
|
|
36
|
+
producer: string;
|
|
37
|
+
proposalID: string;
|
|
38
|
+
generated: string;
|
|
39
|
+
accepted: string;
|
|
40
|
+
}): {
|
|
41
|
+
version: 1;
|
|
42
|
+
proposals: {
|
|
43
|
+
producer: string;
|
|
44
|
+
proposalID: string;
|
|
45
|
+
generatedFingerprint: string;
|
|
46
|
+
acceptedFingerprint: string;
|
|
47
|
+
}[];
|
|
48
|
+
submittedFingerprint?: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
function submit(value: Info | undefined, text: string): Info | undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export var InputAssistance;
|
|
3
|
+
(function (InputAssistance) {
|
|
4
|
+
InputAssistance.Proposal = z.object({
|
|
5
|
+
producer: z.string().min(1).max(128),
|
|
6
|
+
proposalID: z.string().min(1).max(256),
|
|
7
|
+
generatedFingerprint: z.string(),
|
|
8
|
+
acceptedFingerprint: z.string(),
|
|
9
|
+
});
|
|
10
|
+
InputAssistance.Info = z
|
|
11
|
+
.object({
|
|
12
|
+
version: z.literal(1),
|
|
13
|
+
proposals: z.array(InputAssistance.Proposal).min(1).max(128),
|
|
14
|
+
submittedFingerprint: z.string().optional(),
|
|
15
|
+
})
|
|
16
|
+
.meta({ ref: "InputAssistance" });
|
|
17
|
+
InputAssistance.Metadata = z.looseObject({ assistance: InputAssistance.Info.optional() });
|
|
18
|
+
function read(metadata) {
|
|
19
|
+
if (!metadata || typeof metadata !== "object" || !("assistance" in metadata))
|
|
20
|
+
return;
|
|
21
|
+
const result = InputAssistance.Info.safeParse(metadata.assistance);
|
|
22
|
+
return result.success ? result.data : undefined;
|
|
23
|
+
}
|
|
24
|
+
InputAssistance.read = read;
|
|
25
|
+
// A portable diagnostic fingerprint, never an authentication or integrity check.
|
|
26
|
+
function fingerprint(text) {
|
|
27
|
+
let hash = 0xcbf29ce484222325n;
|
|
28
|
+
for (const byte of new TextEncoder().encode(text))
|
|
29
|
+
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
|
|
30
|
+
return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
|
|
31
|
+
}
|
|
32
|
+
InputAssistance.fingerprint = fingerprint;
|
|
33
|
+
function merge(...values) {
|
|
34
|
+
const proposals = new Map();
|
|
35
|
+
for (const value of values)
|
|
36
|
+
for (const proposal of value?.proposals ?? [])
|
|
37
|
+
proposals.set(JSON.stringify([proposal.producer, proposal.proposalID]), proposal);
|
|
38
|
+
if (!proposals.size)
|
|
39
|
+
return;
|
|
40
|
+
return InputAssistance.Info.parse({ version: 1, proposals: [...proposals.values()] });
|
|
41
|
+
}
|
|
42
|
+
InputAssistance.merge = merge;
|
|
43
|
+
function accept(previous, input) {
|
|
44
|
+
return merge(previous, {
|
|
45
|
+
version: 1,
|
|
46
|
+
proposals: [
|
|
47
|
+
{
|
|
48
|
+
producer: input.producer,
|
|
49
|
+
proposalID: input.proposalID,
|
|
50
|
+
generatedFingerprint: fingerprint(input.generated),
|
|
51
|
+
acceptedFingerprint: fingerprint(input.accepted),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
InputAssistance.accept = accept;
|
|
57
|
+
function submit(value, text) {
|
|
58
|
+
return value && { ...InputAssistance.Info.parse(value), submittedFingerprint: fingerprint(text) };
|
|
59
|
+
}
|
|
60
|
+
InputAssistance.submit = submit;
|
|
61
|
+
})(InputAssistance || (InputAssistance = {}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const InstallationPin: z.ZodObject<{
|
|
3
|
+
id: z.ZodUUID;
|
|
4
|
+
sha256: z.ZodString;
|
|
5
|
+
}, z.core.$strict>;
|
|
6
|
+
export declare const INSTALLED_WORKER_CONTROL_VERSION = 1;
|
|
7
|
+
export declare const InstalledWorkerReady: z.ZodObject<{
|
|
8
|
+
synergyLauncher: z.ZodLiteral<"worker-ready">;
|
|
9
|
+
version: z.ZodLiteral<1>;
|
|
10
|
+
runner: z.ZodLiteral<"__plugin-runtime-runner">;
|
|
11
|
+
pin: z.ZodObject<{
|
|
12
|
+
id: z.ZodUUID;
|
|
13
|
+
sha256: z.ZodString;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
export declare const CORE_RUNNERS: readonly ["storage-worker-runner", "owned-process-runner", "observability-worker-runner", "agent-turn-runner", "policy-worker-runner", "plugin-runtime-runner", "storage-maintenance-runner"];
|
|
17
|
+
export declare function installedWorkerEnvironment(env: Record<string, string | undefined>): Record<string, string>;
|
|
18
|
+
export declare function installedWorkerCommand(env: Record<string, string | undefined>, runner: string, args?: string[]): string[] | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const InstallationPin = z.object({ id: z.uuid(), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
3
|
+
export const INSTALLED_WORKER_CONTROL_VERSION = 1;
|
|
4
|
+
export const InstalledWorkerReady = z
|
|
5
|
+
.object({
|
|
6
|
+
synergyLauncher: z.literal("worker-ready"),
|
|
7
|
+
version: z.literal(INSTALLED_WORKER_CONTROL_VERSION),
|
|
8
|
+
runner: z.literal("__plugin-runtime-runner"),
|
|
9
|
+
pin: InstallationPin,
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
export const CORE_RUNNERS = [
|
|
13
|
+
"storage-worker-runner",
|
|
14
|
+
"owned-process-runner",
|
|
15
|
+
"observability-worker-runner",
|
|
16
|
+
"agent-turn-runner",
|
|
17
|
+
"policy-worker-runner",
|
|
18
|
+
"plugin-runtime-runner",
|
|
19
|
+
"storage-maintenance-runner",
|
|
20
|
+
];
|
|
21
|
+
export function installedWorkerEnvironment(env) {
|
|
22
|
+
return Object.fromEntries([
|
|
23
|
+
"SYNERGY_HOME",
|
|
24
|
+
"SYNERGY_RUNTIME_ROOT",
|
|
25
|
+
"SYNERGY_INSTALLATION_ROOT",
|
|
26
|
+
"SYNERGY_INSTALLATION_PIN",
|
|
27
|
+
"SYNERGY_LAUNCHER_COMMAND",
|
|
28
|
+
"SYNERGY_LAUNCHER_CONTROL_VERSION",
|
|
29
|
+
].flatMap((key) => (env[key] === undefined ? [] : [[key, env[key]]])));
|
|
30
|
+
}
|
|
31
|
+
export function installedWorkerCommand(env, runner, args = []) {
|
|
32
|
+
if (!env.SYNERGY_INSTALLATION_PIN)
|
|
33
|
+
return undefined;
|
|
34
|
+
InstallationPin.parse(JSON.parse(env.SYNERGY_INSTALLATION_PIN));
|
|
35
|
+
if (!env.SYNERGY_LAUNCHER_COMMAND)
|
|
36
|
+
throw new Error("An installed worker requires its verified launcher");
|
|
37
|
+
const command = z.array(z.string().min(1)).min(1).parse(JSON.parse(env.SYNERGY_LAUNCHER_COMMAND));
|
|
38
|
+
return [...command, runner, ...args];
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function isRetryableIOError(error: unknown): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Read a file as UTF-8, retrying transient lock errors (EPERM/EACCES/EBUSY).
|
|
4
|
+
* Non-retryable errors (ENOENT, malformed content) and retry exhaustion
|
|
5
|
+
* propagate so callers keep fail-closed semantics instead of silently
|
|
6
|
+
* treating an unreadable store as empty.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readFileWithRetry(file: string, options?: {
|
|
9
|
+
attempts?: number;
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
}): Promise<string>;
|
package/dist/io-retry.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Transient filesystem errors that occur when another process briefly holds a
|
|
4
|
+
* file handle (Windows sharing violations, antivirus scans, OneDrive sync).
|
|
5
|
+
* Retrying is safe; a permanent permission or data error must not be masked.
|
|
6
|
+
*/
|
|
7
|
+
const RETRYABLE_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
8
|
+
export function isRetryableIOError(error) {
|
|
9
|
+
if (typeof error !== "object" || error === null)
|
|
10
|
+
return false;
|
|
11
|
+
const code = error.code;
|
|
12
|
+
return typeof code === "string" && RETRYABLE_CODES.has(code);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read a file as UTF-8, retrying transient lock errors (EPERM/EACCES/EBUSY).
|
|
16
|
+
* Non-retryable errors (ENOENT, malformed content) and retry exhaustion
|
|
17
|
+
* propagate so callers keep fail-closed semantics instead of silently
|
|
18
|
+
* treating an unreadable store as empty.
|
|
19
|
+
*/
|
|
20
|
+
export async function readFileWithRetry(file, options) {
|
|
21
|
+
const attempts = Math.max(1, options?.attempts ?? 3);
|
|
22
|
+
const delayMs = Math.max(0, options?.delayMs ?? 100);
|
|
23
|
+
let lastError;
|
|
24
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
25
|
+
try {
|
|
26
|
+
return await fs.readFile(file, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
lastError = error;
|
|
30
|
+
if (!isRetryableIOError(error) || attempt === attempts)
|
|
31
|
+
throw error;
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw lastError;
|
|
36
|
+
}
|