@nanhara/hara 0.130.0 → 0.130.1
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/CHANGELOG.md +10 -0
- package/dist/fs-permissions.js +14 -0
- package/dist/security/private-state.js +6 -23
- package/dist/serve/server.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.130.1 — 2026-07-21 — Windows private-state portability
|
|
9
|
+
|
|
10
|
+
- `hara serve` no longer calls the inapplicable POSIX `fchmod` operation on Windows discovery
|
|
11
|
+
directory and file handles. Official Windows standalone builds can now create the authenticated
|
|
12
|
+
`serve.json` record instead of exiting with `EPERM`.
|
|
13
|
+
- The same descriptor-mode boundary is shared by all private-state readers and writers: Windows
|
|
14
|
+
retains file type, identity, no-replace, atomic-write, and symlink/reparse-point checks while
|
|
15
|
+
omitting only POSIX owner bits; macOS and Linux still fail closed if permission tightening fails.
|
|
16
|
+
- Upgrade with `npm i -g @nanhara/hara@0.130.1`.
|
|
17
|
+
|
|
8
18
|
## 0.130.0 — 2026-07-20 — ordered task-state delivery
|
|
9
19
|
|
|
10
20
|
- Typed `event.task_state` notifications now carry a server-stream identity and a positive,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fchmodSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Tighten an already-verified descriptor on platforms that implement POSIX ownership modes.
|
|
4
|
+
*
|
|
5
|
+
* Windows Node/Bun may expose `fchmodSync`, but the underlying handle can reject it with EPERM and
|
|
6
|
+
* Windows ACLs do not implement these POSIX owner bits. Callers must retain their type, identity,
|
|
7
|
+
* no-replace, and atomic-write checks; only the inapplicable mode operation is omitted on Windows.
|
|
8
|
+
* POSIX errors deliberately propagate so a failed private-state repair remains fail-closed.
|
|
9
|
+
*/
|
|
10
|
+
export function tightenPrivateDescriptorMode(fd, mode, platform = process.platform, writeMode = fchmodSync) {
|
|
11
|
+
if (platform === "win32")
|
|
12
|
+
return;
|
|
13
|
+
writeMode(fd, mode);
|
|
14
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Owner-only migration for Hara's local control plane. New writers should still create private files
|
|
2
2
|
// directly, but this repairs installations created by older releases and makes ~/.hara non-traversable by
|
|
3
3
|
// other local users before credentials/session state are read.
|
|
4
|
-
import { closeSync, chmodSync, constants,
|
|
4
|
+
import { closeSync, chmodSync, constants, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { FileReadLimitError, MAX_EDIT_READ_BYTES, decodeUtf8Strict, openVerifiedRegularFileNoFollow, verifyOpenedRegularFileSync, } from "../fs-read.js";
|
|
9
9
|
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
10
|
+
import { tightenPrivateDescriptorMode } from "../fs-permissions.js";
|
|
10
11
|
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
11
12
|
const PRIVATE_TREES = new Set(["sessions", "checkpoints", "index", "gateway", "cron", "weixin", "tool-results", "plugin-receipts", "artifacts"]);
|
|
12
13
|
const tightenedHomes = new Set();
|
|
@@ -81,7 +82,7 @@ function verifyAndTightenPrivateDirectory(path) {
|
|
|
81
82
|
if (!opened.isDirectory() || opened.dev !== inspected.dev || opened.ino !== inspected.ino) {
|
|
82
83
|
throw new Error(`private Hara state directory changed while opening: '${path}'`);
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
+
tightenPrivateDescriptorMode(fd, 0o700);
|
|
85
86
|
}
|
|
86
87
|
finally {
|
|
87
88
|
closeSync(fd);
|
|
@@ -190,13 +191,7 @@ export function readPrivateStateFileSnapshotSync(path, maxBytes = MAX_EDIT_READ_
|
|
|
190
191
|
rejectHardLinks: true,
|
|
191
192
|
protectSensitive: false,
|
|
192
193
|
});
|
|
193
|
-
|
|
194
|
-
fchmodSync(fd, 0o600);
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
if (process.platform !== "win32")
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
194
|
+
tightenPrivateDescriptorMode(fd, 0o600);
|
|
200
195
|
// chmod may update ctime; capture the authoritative baseline afterwards.
|
|
201
196
|
info = fstatSync(fd);
|
|
202
197
|
if (info.size > limit)
|
|
@@ -334,13 +329,7 @@ export function writePrivateStateBytesOnceSync(binding, bytes) {
|
|
|
334
329
|
try {
|
|
335
330
|
fd = openSync(temp, "wx", 0o600);
|
|
336
331
|
writeFileSync(fd, bytes);
|
|
337
|
-
|
|
338
|
-
fchmodSync(fd, 0o600);
|
|
339
|
-
}
|
|
340
|
-
catch (error) {
|
|
341
|
-
if (process.platform !== "win32")
|
|
342
|
-
throw error;
|
|
343
|
-
}
|
|
332
|
+
tightenPrivateDescriptorMode(fd, 0o600);
|
|
344
333
|
fsyncSync(fd);
|
|
345
334
|
const stagedInfo = fstatSync(fd);
|
|
346
335
|
if (!stagedInfo.isFile() || stagedInfo.nlink !== 1) {
|
|
@@ -422,13 +411,7 @@ export function writePrivateStateFileSync(binding, text, options = {}) {
|
|
|
422
411
|
// Bun's Windows numeric-open compatibility can otherwise turn this valid create into a false ENOENT.
|
|
423
412
|
fd = openSync(temp, "wx", 0o600);
|
|
424
413
|
writeFileSync(fd, text, "utf8");
|
|
425
|
-
|
|
426
|
-
fchmodSync(fd, 0o600);
|
|
427
|
-
}
|
|
428
|
-
catch (error) {
|
|
429
|
-
if (process.platform !== "win32")
|
|
430
|
-
throw error;
|
|
431
|
-
}
|
|
414
|
+
tightenPrivateDescriptorMode(fd, 0o600);
|
|
432
415
|
fsyncSync(fd);
|
|
433
416
|
const stagedInfo = fstatSync(fd);
|
|
434
417
|
if (!stagedInfo.isFile() || stagedInfo.nlink !== 1)
|
package/dist/serve/server.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (no import cycle back into the CLI entry).
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import { randomBytes, randomUUID, timingSafeEqual, createHash } from "node:crypto";
|
|
8
|
-
import { closeSync, constants as fsConstants,
|
|
8
|
+
import { closeSync, constants as fsConstants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
11
11
|
import "../tools/all.js"; // register the full built-in toolset — serve must work as a standalone entry
|
|
@@ -35,6 +35,7 @@ import { parseFrame, rpcResult, rpcError, rpcNotify, ERR, PROTOCOL_VERSION } fro
|
|
|
35
35
|
import { taskLifecycleEvent, } from "./task-events.js";
|
|
36
36
|
import { readModelContextFileSync } from "../fs-read.js";
|
|
37
37
|
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
38
|
+
import { tightenPrivateDescriptorMode } from "../fs-permissions.js";
|
|
38
39
|
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
39
40
|
import { redactSensitiveText, redactSensitiveValue } from "../security/secrets.js";
|
|
40
41
|
import { ArtifactStoreError, commitArtifact, getArtifact, importArtifact, listArtifactRevisions, listArtifacts, revertArtifact, } from "../artifacts/store.js";
|
|
@@ -81,7 +82,7 @@ const ensurePrivateDiscoveryDir = (dir) => {
|
|
|
81
82
|
throw new Error(`${dir} must be a private directory, not a symlink`);
|
|
82
83
|
// mkdir's mode does not affect a legacy directory. Operate through the verified descriptor so a path
|
|
83
84
|
// replacement cannot redirect chmod to a symlink target between validation and permission tightening.
|
|
84
|
-
|
|
85
|
+
tightenPrivateDescriptorMode(fd, 0o700);
|
|
85
86
|
}
|
|
86
87
|
finally {
|
|
87
88
|
if (fd !== undefined)
|
|
@@ -173,7 +174,7 @@ const writeDiscovery = async (dir, path, record) => {
|
|
|
173
174
|
let fd;
|
|
174
175
|
try {
|
|
175
176
|
fd = openSync(temp, "wx", 0o600);
|
|
176
|
-
|
|
177
|
+
tightenPrivateDescriptorMode(fd, 0o600);
|
|
177
178
|
writeFileSync(fd, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
178
179
|
fsyncSync(fd);
|
|
179
180
|
closeSync(fd);
|