@lousy-agents/agent-shell 5.15.7 → 5.16.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/dist/index.js +468 -28
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as __rspack_createRequire } from "node:module";
|
|
3
3
|
const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
|
|
4
4
|
var __webpack_modules__ = ({
|
|
5
|
-
|
|
5
|
+
330(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
6
6
|
|
|
7
7
|
;// CONCATENATED MODULE: external "node:child_process"
|
|
8
8
|
const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
|
|
@@ -128,16 +128,24 @@ function createBoundedReadStream(opened, maxBytes) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
|
|
131
|
+
|
|
131
132
|
function isZero(value) {
|
|
132
133
|
return value === 0 || value === 0n;
|
|
133
134
|
}
|
|
135
|
+
function sameStatValue(left, right) {
|
|
136
|
+
return typeof left === typeof right ? left === right : BigInt(left) === BigInt(right);
|
|
137
|
+
}
|
|
138
|
+
function sha256Hex(data, encoding) {
|
|
139
|
+
const buffer = typeof data === "string" ? Buffer.from(data, encoding ?? "utf8") : data;
|
|
140
|
+
return (0,external_node_crypto_namespaceObject.createHash)("sha256").update(buffer).digest("hex");
|
|
141
|
+
}
|
|
134
142
|
function file_identity_sameFileIdentity(left, right, platform = process.platform) {
|
|
135
|
-
if (left.ino
|
|
143
|
+
if (!sameStatValue(left.ino, right.ino)) {
|
|
136
144
|
return false;
|
|
137
145
|
}
|
|
138
146
|
// On Windows, path-based stat calls can report dev=0 while fd-based stat
|
|
139
147
|
// reports a real volume serial; treat either-side dev=0 as "unknown device".
|
|
140
|
-
if (left.dev
|
|
148
|
+
if (sameStatValue(left.dev, right.dev)) {
|
|
141
149
|
return true;
|
|
142
150
|
}
|
|
143
151
|
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
|
@@ -1529,6 +1537,377 @@ async function runPinnedPathHelper(params) {
|
|
|
1529
1537
|
}
|
|
1530
1538
|
}
|
|
1531
1539
|
|
|
1540
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
1546
|
+
function getGlobalManagers() {
|
|
1547
|
+
const globalWithState = globalThis;
|
|
1548
|
+
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
1549
|
+
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
1550
|
+
}
|
|
1551
|
+
return globalWithState[GLOBAL_STATE_KEY];
|
|
1552
|
+
}
|
|
1553
|
+
function resolveManagerState(key) {
|
|
1554
|
+
const managers = getGlobalManagers();
|
|
1555
|
+
let state = managers.get(key);
|
|
1556
|
+
if (!state) {
|
|
1557
|
+
state = { cleanupRegistered: false, held: new Map() };
|
|
1558
|
+
managers.set(key, state);
|
|
1559
|
+
}
|
|
1560
|
+
return state;
|
|
1561
|
+
}
|
|
1562
|
+
async function readLockSnapshot(lockPath) {
|
|
1563
|
+
try {
|
|
1564
|
+
const stat = await promises_namespaceObject.lstat(lockPath);
|
|
1565
|
+
const raw = await promises_namespaceObject.readFile(lockPath, "utf8");
|
|
1566
|
+
try {
|
|
1567
|
+
const parsed = JSON.parse(raw);
|
|
1568
|
+
const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1569
|
+
? parsed
|
|
1570
|
+
: null;
|
|
1571
|
+
return { raw, payload, stat };
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
return { raw, payload: null, stat };
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
catch (err) {
|
|
1578
|
+
if (err.code === "ENOENT") {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
throw err;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function snapshotMatches(current, observed) {
|
|
1585
|
+
if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
if (observed.raw !== undefined) {
|
|
1589
|
+
return current.raw === observed.raw;
|
|
1590
|
+
}
|
|
1591
|
+
return observed.stat !== undefined && current.stat !== undefined;
|
|
1592
|
+
}
|
|
1593
|
+
async function removeLockIfUnchanged(lockPath, observed) {
|
|
1594
|
+
const current = await readLockSnapshot(lockPath);
|
|
1595
|
+
if (!current || !observed) {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
if (!snapshotMatches(current, observed)) {
|
|
1599
|
+
// The lock changed after we decided it was stale. Leave the fresh holder's
|
|
1600
|
+
// file alone; deleting by path here would break mutual exclusion.
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
async function lockSnapshotStillPresent(lockPath, observed) {
|
|
1607
|
+
const current = await readLockSnapshot(lockPath);
|
|
1608
|
+
return !!current && !!observed && snapshotMatches(current, observed);
|
|
1609
|
+
}
|
|
1610
|
+
async function removeStaleLockIfAllowed(params) {
|
|
1611
|
+
if (!params.shouldRemoveStaleLock) {
|
|
1612
|
+
return "not-approved";
|
|
1613
|
+
}
|
|
1614
|
+
if (params.snapshot.raw === undefined) {
|
|
1615
|
+
return "not-approved";
|
|
1616
|
+
}
|
|
1617
|
+
if (!(await params.shouldRemoveStaleLock({
|
|
1618
|
+
lockPath: params.lockPath,
|
|
1619
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
1620
|
+
raw: params.snapshot.raw,
|
|
1621
|
+
payload: params.snapshot.payload,
|
|
1622
|
+
}))) {
|
|
1623
|
+
return "not-approved";
|
|
1624
|
+
}
|
|
1625
|
+
const current = await readLockSnapshot(params.lockPath);
|
|
1626
|
+
if (!current || !snapshotMatches(current, params.snapshot)) {
|
|
1627
|
+
return "changed";
|
|
1628
|
+
}
|
|
1629
|
+
try {
|
|
1630
|
+
await promises_namespaceObject.rm(params.lockPath, { force: true });
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
if (err.code === "ENOENT") {
|
|
1634
|
+
return "changed";
|
|
1635
|
+
}
|
|
1636
|
+
return "not-approved";
|
|
1637
|
+
}
|
|
1638
|
+
return "removed";
|
|
1639
|
+
}
|
|
1640
|
+
function snapshotMatchesSync(lockPath, observed) {
|
|
1641
|
+
try {
|
|
1642
|
+
const stat = external_node_fs_namespaceObject.lstatSync(lockPath);
|
|
1643
|
+
if (observed.stat && !file_identity_sameFileIdentity(observed.stat, stat)) {
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
return observed.raw === undefined || external_node_fs_namespaceObject.readFileSync(lockPath, "utf8") === observed.raw;
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
1653
|
+
const resolved = external_node_path_namespaceObject.resolve(targetPath);
|
|
1654
|
+
const dir = external_node_path_namespaceObject.dirname(resolved);
|
|
1655
|
+
await promises_namespaceObject.mkdir(dir, { recursive: true });
|
|
1656
|
+
try {
|
|
1657
|
+
return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
return resolved;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function computeDelayMs(retry, attempt) {
|
|
1664
|
+
const minTimeout = retry.minTimeout ?? 50;
|
|
1665
|
+
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
1666
|
+
const factor = retry.factor ?? 1;
|
|
1667
|
+
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
1668
|
+
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
1669
|
+
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
1670
|
+
}
|
|
1671
|
+
async function defaultShouldReclaim(params) {
|
|
1672
|
+
const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
|
|
1673
|
+
const createdAtMs = Date.parse(createdAt);
|
|
1674
|
+
if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
try {
|
|
1678
|
+
const stat = await promises_namespaceObject.stat(params.lockPath);
|
|
1679
|
+
return params.nowMs - stat.mtimeMs > params.staleMs;
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function releaseAllLocksSync(state) {
|
|
1686
|
+
for (const [normalizedTargetPath, held] of state.held) {
|
|
1687
|
+
void held.handle.close().catch(() => undefined);
|
|
1688
|
+
try {
|
|
1689
|
+
if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
1690
|
+
external_node_fs_namespaceObject.rmSync(held.lockPath, { force: true });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
catch {
|
|
1694
|
+
// Best-effort process-exit cleanup.
|
|
1695
|
+
}
|
|
1696
|
+
state.held.delete(normalizedTargetPath);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
|
|
1700
|
+
const current = state.held.get(normalizedTargetPath);
|
|
1701
|
+
if (current !== held) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
if (opts.force) {
|
|
1705
|
+
held.count = 0;
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
held.count -= 1;
|
|
1709
|
+
if (held.count > 0) {
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (held.releasePromise) {
|
|
1714
|
+
await held.releasePromise.catch(() => undefined);
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
state.held.delete(normalizedTargetPath);
|
|
1718
|
+
held.releasePromise = (async () => {
|
|
1719
|
+
await held.handle.close().catch(() => undefined);
|
|
1720
|
+
await removeLockIfUnchanged(held.lockPath, held.snapshot);
|
|
1721
|
+
})();
|
|
1722
|
+
try {
|
|
1723
|
+
await held.releasePromise;
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
finally {
|
|
1727
|
+
held.releasePromise = undefined;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function createSidecarLockManager(key) {
|
|
1731
|
+
const state = resolveManagerState(key);
|
|
1732
|
+
function ensureExitCleanupRegistered() {
|
|
1733
|
+
if (state.cleanupRegistered) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
state.cleanupRegistered = true;
|
|
1737
|
+
process.on("exit", () => releaseAllLocksSync(state));
|
|
1738
|
+
}
|
|
1739
|
+
async function acquire(options) {
|
|
1740
|
+
ensureExitCleanupRegistered();
|
|
1741
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
1742
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
1743
|
+
const held = state.held.get(normalizedTargetPath);
|
|
1744
|
+
if (held && options.allowReentrant) {
|
|
1745
|
+
held.count += 1;
|
|
1746
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
|
|
1747
|
+
return {
|
|
1748
|
+
lockPath,
|
|
1749
|
+
normalizedTargetPath,
|
|
1750
|
+
release,
|
|
1751
|
+
[Symbol.asyncDispose]: release,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
const startedAt = Date.now();
|
|
1755
|
+
const retry = options.retry ?? {};
|
|
1756
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
1757
|
+
let attempt = 0;
|
|
1758
|
+
while (true) {
|
|
1759
|
+
let handle = null;
|
|
1760
|
+
try {
|
|
1761
|
+
handle = await promises_namespaceObject.open(lockPath, "wx");
|
|
1762
|
+
const payload = await options.payload();
|
|
1763
|
+
const raw = `${JSON.stringify(payload, null, 2)}\n`;
|
|
1764
|
+
await handle.writeFile(raw, "utf8");
|
|
1765
|
+
const snapshot = { raw, payload, stat: await handle.stat() };
|
|
1766
|
+
const createdHeld = {
|
|
1767
|
+
count: 1,
|
|
1768
|
+
handle,
|
|
1769
|
+
lockPath,
|
|
1770
|
+
snapshot,
|
|
1771
|
+
acquiredAt: Date.now(),
|
|
1772
|
+
metadata: options.metadata ?? {},
|
|
1773
|
+
};
|
|
1774
|
+
state.held.set(normalizedTargetPath, createdHeld);
|
|
1775
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
|
|
1776
|
+
return {
|
|
1777
|
+
lockPath,
|
|
1778
|
+
normalizedTargetPath,
|
|
1779
|
+
release,
|
|
1780
|
+
[Symbol.asyncDispose]: release,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
catch (err) {
|
|
1784
|
+
if (handle) {
|
|
1785
|
+
const failedSnapshot = { payload: null };
|
|
1786
|
+
try {
|
|
1787
|
+
failedSnapshot.stat = await handle.stat();
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
1791
|
+
}
|
|
1792
|
+
const current = state.held.get(normalizedTargetPath);
|
|
1793
|
+
if (current?.handle === handle) {
|
|
1794
|
+
state.held.delete(normalizedTargetPath);
|
|
1795
|
+
}
|
|
1796
|
+
// If payload serialization/write fails, the file may be empty or
|
|
1797
|
+
// partial JSON, so remove while our exclusive handle is still open.
|
|
1798
|
+
await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
|
|
1799
|
+
await handle.close().catch(() => undefined);
|
|
1800
|
+
// Windows can refuse removing an open file; retry after close but
|
|
1801
|
+
// only if the path still points at the file identity we created.
|
|
1802
|
+
await removeLockIfUnchanged(lockPath, failedSnapshot);
|
|
1803
|
+
}
|
|
1804
|
+
if (err.code !== "EEXIST") {
|
|
1805
|
+
throw err;
|
|
1806
|
+
}
|
|
1807
|
+
const nowMs = Date.now();
|
|
1808
|
+
const snapshot = await readLockSnapshot(lockPath);
|
|
1809
|
+
if (!snapshot) {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
|
|
1813
|
+
if (await shouldReclaim({
|
|
1814
|
+
lockPath,
|
|
1815
|
+
normalizedTargetPath,
|
|
1816
|
+
payload: snapshot?.payload ?? null,
|
|
1817
|
+
staleMs: options.staleMs,
|
|
1818
|
+
nowMs,
|
|
1819
|
+
heldByThisProcess: state.held.has(normalizedTargetPath),
|
|
1820
|
+
})) {
|
|
1821
|
+
if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
1825
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
1826
|
+
const removal = await removeStaleLockIfAllowed({
|
|
1827
|
+
lockPath,
|
|
1828
|
+
normalizedTargetPath,
|
|
1829
|
+
snapshot,
|
|
1830
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
1831
|
+
});
|
|
1832
|
+
if (removal === "removed" || removal === "changed") {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
1837
|
+
code: "file_lock_stale",
|
|
1838
|
+
lockPath,
|
|
1839
|
+
normalizedTargetPath,
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
const elapsed = Date.now() - startedAt;
|
|
1843
|
+
if ((options.timeoutMs !== undefined &&
|
|
1844
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
1845
|
+
elapsed >= options.timeoutMs) ||
|
|
1846
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
1847
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
1848
|
+
code: "file_lock_timeout",
|
|
1849
|
+
lockPath,
|
|
1850
|
+
normalizedTargetPath,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
1854
|
+
? Number.POSITIVE_INFINITY
|
|
1855
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
1856
|
+
const delay = Math.min(computeDelayMs(retry, attempt), remaining);
|
|
1857
|
+
attempt += 1;
|
|
1858
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async function withLock(options, fn) {
|
|
1863
|
+
const lock = await acquire(options);
|
|
1864
|
+
try {
|
|
1865
|
+
return await fn();
|
|
1866
|
+
}
|
|
1867
|
+
finally {
|
|
1868
|
+
await lock.release();
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
async function drain() {
|
|
1872
|
+
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
1873
|
+
await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function reset() {
|
|
1877
|
+
releaseAllLocksSync(state);
|
|
1878
|
+
}
|
|
1879
|
+
function heldEntries() {
|
|
1880
|
+
return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
|
|
1881
|
+
normalizedTargetPath,
|
|
1882
|
+
lockPath: held.lockPath,
|
|
1883
|
+
acquiredAt: held.acquiredAt,
|
|
1884
|
+
metadata: held.metadata,
|
|
1885
|
+
forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
|
|
1886
|
+
}));
|
|
1887
|
+
}
|
|
1888
|
+
return { acquire, withLock, drain, reset, heldEntries };
|
|
1889
|
+
}
|
|
1890
|
+
async function withSidecarLock(targetPath, options, fn) {
|
|
1891
|
+
const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
|
|
1892
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
1893
|
+
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
1897
|
+
let test_hooks_fsSafeTestHooks;
|
|
1898
|
+
function allowFsSafeTestHooks() {
|
|
1899
|
+
return false || process.env.VITEST === "true";
|
|
1900
|
+
}
|
|
1901
|
+
function getFsSafeTestHooks() {
|
|
1902
|
+
return test_hooks_fsSafeTestHooks;
|
|
1903
|
+
}
|
|
1904
|
+
function __setFsSafeTestHooksForTest(hooks) {
|
|
1905
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
1906
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
1907
|
+
}
|
|
1908
|
+
test_hooks_fsSafeTestHooks = hooks;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1532
1911
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
|
|
1533
1912
|
|
|
1534
1913
|
|
|
@@ -1542,6 +1921,8 @@ async function runPinnedPathHelper(params) {
|
|
|
1542
1921
|
|
|
1543
1922
|
|
|
1544
1923
|
|
|
1924
|
+
|
|
1925
|
+
|
|
1545
1926
|
function byteLength(input, encoding) {
|
|
1546
1927
|
return typeof input === "string"
|
|
1547
1928
|
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
@@ -1599,6 +1980,12 @@ async function runPinnedWriteHelper(params) {
|
|
|
1599
1980
|
validatePinnedOperationPayload({
|
|
1600
1981
|
relativeParentPath: params.relativeParentPath,
|
|
1601
1982
|
});
|
|
1983
|
+
// The Python helper deliberately enforces the strict post-rename inode
|
|
1984
|
+
// contract. The explicit compatibility policy therefore uses the guarded
|
|
1985
|
+
// Node fallback, where content verification can replace that one check.
|
|
1986
|
+
if (params.onRenameIdentityMismatch === "verify-content") {
|
|
1987
|
+
return await runPinnedWriteFallback(params);
|
|
1988
|
+
}
|
|
1602
1989
|
if (getFsSafePythonConfig().mode === "off") {
|
|
1603
1990
|
return await runPinnedWriteFallback(params);
|
|
1604
1991
|
}
|
|
@@ -1613,8 +2000,11 @@ async function runPinnedWriteHelper(params) {
|
|
|
1613
2000
|
throw error;
|
|
1614
2001
|
}
|
|
1615
2002
|
}
|
|
2003
|
+
const input = params.input.kind === "stream"
|
|
2004
|
+
? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
|
|
2005
|
+
: params.input;
|
|
1616
2006
|
const payload = {
|
|
1617
|
-
base64: await inputToBase64(
|
|
2007
|
+
base64: await inputToBase64(input, params.maxBytes),
|
|
1618
2008
|
basename: params.basename,
|
|
1619
2009
|
maxBytes: params.maxBytes ?? -1,
|
|
1620
2010
|
mkdir: params.mkdir,
|
|
@@ -1632,11 +2022,32 @@ async function runPinnedWriteHelper(params) {
|
|
|
1632
2022
|
}
|
|
1633
2023
|
catch (error) {
|
|
1634
2024
|
if (canFallbackFromPythonError(error)) {
|
|
1635
|
-
return await runPinnedWriteFallback(params);
|
|
2025
|
+
return await runPinnedWriteFallback({ ...params, input });
|
|
1636
2026
|
}
|
|
1637
2027
|
throw error;
|
|
1638
2028
|
}
|
|
1639
2029
|
}
|
|
2030
|
+
async function runPinnedWriteWithRenamePolicy(params) {
|
|
2031
|
+
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
2032
|
+
if (renameIdentity !== "verify-content-with-lock") {
|
|
2033
|
+
return await runPinnedWriteHelper(writeParams);
|
|
2034
|
+
}
|
|
2035
|
+
const relativeTargetPath = writeParams.relativeParentPath
|
|
2036
|
+
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
2037
|
+
: writeParams.basename;
|
|
2038
|
+
const lockPath = external_node_path_namespaceObject.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
|
|
2039
|
+
return await withSidecarLock(writeParams.rootPath, {
|
|
2040
|
+
managerKey: `fs-safe.write:${targetPath}`,
|
|
2041
|
+
lockPath,
|
|
2042
|
+
staleMs: 30_000,
|
|
2043
|
+
timeoutMs: 5_000,
|
|
2044
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
2045
|
+
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
2046
|
+
}, async () => await runPinnedWriteHelper({
|
|
2047
|
+
...writeParams,
|
|
2048
|
+
onRenameIdentityMismatch: "verify-content",
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
1640
2051
|
async function runPinnedCopyHelper(params) {
|
|
1641
2052
|
assertSafeBasename(params.basename);
|
|
1642
2053
|
validatePinnedOperationPayload({
|
|
@@ -1692,7 +2103,10 @@ async function runPinnedWriteFallback(params) {
|
|
|
1692
2103
|
else {
|
|
1693
2104
|
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
1694
2105
|
}
|
|
2106
|
+
await handle.sync();
|
|
1695
2107
|
const stat = await handle.stat();
|
|
2108
|
+
await handle.close().catch(() => undefined);
|
|
2109
|
+
await syncDirectoryBestEffort(parentPath);
|
|
1696
2110
|
created = false;
|
|
1697
2111
|
return { dev: stat.dev, ino: stat.ino };
|
|
1698
2112
|
}
|
|
@@ -1740,11 +2154,49 @@ async function runPinnedWriteFallback(params) {
|
|
|
1740
2154
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1741
2155
|
await promises_namespaceObject.rename(tempPath, targetPath);
|
|
1742
2156
|
renamed = true;
|
|
2157
|
+
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
1743
2158
|
await syncDirectoryBestEffort(parentPath);
|
|
1744
2159
|
targetStat = await promises_namespaceObject.lstat(targetPath);
|
|
1745
|
-
if (targetStat.isSymbolicLink()
|
|
2160
|
+
if (targetStat.isSymbolicLink()) {
|
|
1746
2161
|
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
1747
2162
|
}
|
|
2163
|
+
if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
|
|
2164
|
+
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
2165
|
+
// different inode from the source temp fd even with zero concurrency. The
|
|
2166
|
+
// caller must ensure mutual exclusion before passing "verify-content";
|
|
2167
|
+
// fall back to a content hash for this rename-boundary check only.
|
|
2168
|
+
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
2169
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2170
|
+
}
|
|
2171
|
+
if (params.input.kind !== "buffer") {
|
|
2172
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2173
|
+
}
|
|
2174
|
+
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
2175
|
+
const readFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
2176
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants
|
|
2177
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
2178
|
+
: 0);
|
|
2179
|
+
const readHandle = await promises_namespaceObject.open(targetPath, readFlags);
|
|
2180
|
+
let actualHash;
|
|
2181
|
+
let readHandleStat;
|
|
2182
|
+
try {
|
|
2183
|
+
// Capture fd-based identity before reading — this is stable across all
|
|
2184
|
+
// subsequent lookups (on FUSE and locally), unlike the lstat-based
|
|
2185
|
+
// targetStat that triggered this fallback.
|
|
2186
|
+
readHandleStat = await readHandle.stat();
|
|
2187
|
+
actualHash = sha256Hex(await readHandle.readFile());
|
|
2188
|
+
}
|
|
2189
|
+
finally {
|
|
2190
|
+
await readHandle.close().catch(() => undefined);
|
|
2191
|
+
}
|
|
2192
|
+
if (actualHash !== expectedHash) {
|
|
2193
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2194
|
+
}
|
|
2195
|
+
// Replace the unreliable lstat-based targetStat with the fd-based stat so
|
|
2196
|
+
// the returned identity is consistent with what subsequent verifications
|
|
2197
|
+
// (e.g. verifyAtomicWriteResult) will obtain by opening the same file.
|
|
2198
|
+
targetStat = readHandleStat;
|
|
2199
|
+
}
|
|
1748
2200
|
});
|
|
1749
2201
|
}
|
|
1750
2202
|
catch (error) {
|
|
@@ -2840,21 +3292,6 @@ function normalizePinnedPathError(error) {
|
|
|
2840
3292
|
});
|
|
2841
3293
|
}
|
|
2842
3294
|
|
|
2843
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
2844
|
-
let test_hooks_fsSafeTestHooks;
|
|
2845
|
-
function allowFsSafeTestHooks() {
|
|
2846
|
-
return false || process.env.VITEST === "true";
|
|
2847
|
-
}
|
|
2848
|
-
function getFsSafeTestHooks() {
|
|
2849
|
-
return test_hooks_fsSafeTestHooks;
|
|
2850
|
-
}
|
|
2851
|
-
function __setFsSafeTestHooksForTest(hooks) {
|
|
2852
|
-
if (hooks && !allowFsSafeTestHooks()) {
|
|
2853
|
-
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
2854
|
-
}
|
|
2855
|
-
test_hooks_fsSafeTestHooks = hooks;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
3295
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
|
|
2859
3296
|
function stringifyJsonDocument(value, replacer, space) {
|
|
2860
3297
|
const text = JSON.stringify(value, replacer, space);
|
|
@@ -3190,6 +3627,7 @@ class RootHandle {
|
|
|
3190
3627
|
data,
|
|
3191
3628
|
mkdir: this.defaults.mkdir,
|
|
3192
3629
|
mode: this.defaults.mode,
|
|
3630
|
+
renameIdentity: this.defaults.renameIdentity,
|
|
3193
3631
|
...options,
|
|
3194
3632
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
3195
3633
|
});
|
|
@@ -3624,21 +4062,23 @@ async function writeFileInRoot(root, params) {
|
|
|
3624
4062
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
3625
4063
|
let identity;
|
|
3626
4064
|
try {
|
|
3627
|
-
identity = await
|
|
4065
|
+
identity = await runPinnedWriteWithRenamePolicy({
|
|
3628
4066
|
rootPath: pinned.rootReal,
|
|
3629
4067
|
relativeParentPath: pinned.relativeParentPath,
|
|
3630
4068
|
basename: pinned.basename,
|
|
4069
|
+
targetPath: pinned.targetPath,
|
|
4070
|
+
renameIdentity: params.renameIdentity,
|
|
3631
4071
|
mkdir: params.mkdir !== false,
|
|
3632
4072
|
mode: params.mode ?? pinned.mode,
|
|
3633
4073
|
overwrite: params.overwrite,
|
|
3634
|
-
input: {
|
|
3635
|
-
kind: "buffer",
|
|
3636
|
-
data: params.data,
|
|
3637
|
-
encoding: params.encoding,
|
|
3638
|
-
},
|
|
4074
|
+
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
3639
4075
|
});
|
|
3640
4076
|
}
|
|
3641
4077
|
catch (error) {
|
|
4078
|
+
const errorCode = error?.code;
|
|
4079
|
+
if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
|
|
4080
|
+
throw error;
|
|
4081
|
+
}
|
|
3642
4082
|
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
3643
4083
|
throw new errors_FsSafeError("already-exists", "file already exists", {
|
|
3644
4084
|
cause: error instanceof Error ? error : undefined,
|
|
@@ -15272,4 +15712,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
15272
15712
|
// module factories are used so entry inlining is disabled
|
|
15273
15713
|
// startup
|
|
15274
15714
|
// Load entry module and return exports
|
|
15275
|
-
var __webpack_exports__ = __webpack_require__(
|
|
15715
|
+
var __webpack_exports__ = __webpack_require__(330);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lousy-agents/agent-shell",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.16.0",
|
|
4
4
|
"description": "A flight recorder for npm script execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"build": "rspack build --config rspack.config.ts && chmod +x dist/index.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@openclaw/fs-safe": "0.4.
|
|
39
|
+
"@openclaw/fs-safe": "0.4.1",
|
|
40
40
|
"zod": "4.4.3"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|