@sema-agent/core 7.49.4 → 7.49.5
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 +14 -0
- package/dist/agents/peer-directory.d.ts +8 -3
- package/dist/agents/peer-directory.js +5 -3
- package/dist/core/posix-mode-face.d.ts +10 -0
- package/dist/core/posix-mode-face.js +3 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/stores/file/permission-rule-store.d.ts +13 -1
- package/dist/stores/file/permission-rule-store.js +40 -21
- package/package.json +1 -1
- package/wire-changes/core@7.49.5.json +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 7.49.5 — 2026-10-13
|
|
2
|
+
|
|
3
|
+
**Hard-breaking changes (#1701):** none. **Closed sets:** none. **Package exports:** +0 / −0 names (`posixModeBitsMeaningful` lives in `src/core/posix-mode-face.ts` and is not on the package index); member changes, all optional: `vetPeerRegistryDirectory(dir, { platform? })`, `judgePeerRecordFile(stat, selfUid, platform?)`, `new FileDurableRulePartitionProvider(dir, onError, { platform? })`. **Behavior:** on Windows the rule store and the peer registry stop reading the platform's `0o666` as "writable by group or others" — a loosening on exactly one platform, and the one that restores the store's purpose there (open to veto: it departs from "refuse what you cannot prove safe" for that platform); POSIX hosts are unchanged.
|
|
4
|
+
|
|
5
|
+
### A platform with no POSIX permission bits is not "writable by everyone" (#1701; fix; security fail-open on Windows; hard-breaking: none, open to veto; exports: +0 / −0) (@test @server @cli @client-core)
|
|
6
|
+
|
|
7
|
+
- **Before (7.49.4 and earlier).** The permission-rule store refused its whole directory when `(mode & 0o022) !== 0` — "writable by group or others" — and loaded ZERO rules. Windows has no POSIX permission bits: Node reports `0o666` for every file and directory there (a directory created with mode `0o700` too), so on a Windows host the check fired on every run. A persisted `deny` / `ask` row never applied (the engine disclosed "loading zero rules" and went on), no `allow` row was read either, the write face refused every write (an "always allow" could not be saved), and the org state file in the same directory was refused with it. The peer registry's directory chain (`vetPeerRegistryDirectory`) and a registration file (`judgePeerRecordFile`) judged the same bits and refused with `group_or_world_writable` on every Windows path. A POSIX host loaded normally.
|
|
8
|
+
- **Now.** ONE predicate, `posixModeBitsMeaningful(platform = process.platform)` (`src/core/posix-mode-face.ts`: `platform !== "win32"`), is read by every site that tests those bits: the store's directory vetting (bucket read, org state read, the write face's lock, the adoption marker writer), the peer registry's chain walk and its file judge, and the execution env's file-mode report (`HAS_POSIX_MODE_FACE`, which already asked this question for itself and now reads the same predicate — no second spelling of the fact). Where the bits are not meaningful the group/other-writable judgment is not made. The owner judgment is unchanged (it was already skipped where `process.getuid` is absent); symlink, not-a-directory, unreadable, checksum and shape refusals are unchanged. On a platform with bits (macOS, Linux) nothing changes: a `0o777` directory is still refused whole. The store says it ONCE per store instance through its existing error disclosure (`onError`): the directory is not checked for being writable by other accounts, this platform reports no POSIX permission bits, so who else can write there is not verified here. The peer registry's two functions are pure verdicts with no disclosure seat; they only skip. There is no ACL-level check (KL-1701-a).
|
|
9
|
+
- **Who needs it.** Every Windows host that persists permission rules (a `deny` / `ask` row, an org snapshot, a rule imported through the consent flow) — until now none of them took part in a decision there.
|
|
10
|
+
- **Who is hurt.** None by design. What changes for a Windows host: persisted rows now take part — `allow` rows silence the asks they were approved to silence, `deny` / `ask` rows apply, org state loads — which is the contract on every other platform. A host that was (unknowingly) running with zero persisted rules will see asks and refusals appear and approved commands stop asking. A directory another Windows account can write to is no longer refused by this check (KL-1701-a).
|
|
11
|
+
- **Compensation.** Nothing to do on POSIX. A Windows deployment that wants the old refusal on a shared machine has no switch for it (the old refusal was this bug, not a policy); restrict the rule directory's ACL to the engine's account.
|
|
12
|
+
- **Consumers.** server / cli / client-core on Windows: the `onError` sink of `FileDurableRulePartitionProvider` now receives one extra sentence per provider instance on win32 (it contains the directory); a host that treats every `onError` line as an error should read this one as a notice. Nothing else on the wire or the card changes.
|
|
13
|
+
- Pins: `test/backlog1701-win32-mode-bits.test.ts` (① the predicate; ② the rule store on a REAL `0o777` directory with the platform injected — win32 loads the deny row, says it once however many reads, says it again per instance, still refuses a symlink / corrupt bucket, the write face and the org read follow it; darwin / linux still refuse whole with no skip notice; a `0o700` darwin directory loads with no notice; a throwing sink changes nothing; ③ the same deny row on the real Runner — win32 refuses the command, a `0o777` darwin directory is the pre-fix reading and runs it; ④ the peer registry — the file judge and the chain walk with a `0o777` ancestor, the other arms unchanged; ⑤ no source file outside the predicate tests those bits without importing it, and the execution env's mode face flips with the platform its module loads under).
|
|
14
|
+
|
|
1
15
|
## 7.49.4 — 2026-10-13
|
|
2
16
|
|
|
3
17
|
**Rebuild of 7.49.3 — no source change.** The npm tarball of 7.49.3 (shasum `77a7fd37…`, now deprecated) carried the `dist/` of 7.49.2: it was published with `--ignore-scripts` from a checkout whose `dist/` had not been rebuilt (the release-candidate build and gates had run on a detached gate copy). None of the four 7.49.3 entries below (#1685, #1691, #1692, #1693) were in that package. 7.49.4 is the same source as tag `v7.49.3` built in the publishing checkout and compared byte-for-byte with the gated dist before publish; every 7.49.3 entry, criterion (`docs/release-criteria/7.49.3.json`) and wire manifest applies to 7.49.4. **Hard-breaking changes:** none. **Closed sets:** none. **Package exports:** +0 / −0 (`npm run handoff:diff v7.49.3 HEAD`). Consumers pinned to 7.49.3 must move to 7.49.4 (@server @cli @web-admin @client-core @test).
|
|
@@ -216,20 +216,25 @@ export type PeerRegistryDirectoryVerdict = {
|
|
|
216
216
|
path: string;
|
|
217
217
|
};
|
|
218
218
|
/** Vet a registry directory and every ancestor. `uid` defaults to the process uid; on a platform with
|
|
219
|
-
* no uid (win32) the owner arm is skipped. Refuses on the FIRST failing ancestor (closest first).
|
|
219
|
+
* no uid (win32) the owner arm is skipped. Refuses on the FIRST failing ancestor (closest first).
|
|
220
|
+
* `platform` (default: this process's) decides whether the permission bits are judged at all
|
|
221
|
+
* ({@link posixModeBitsMeaningful}): where they are not meaningful the group/world-writable arm is skipped,
|
|
222
|
+
* and nothing in this verdict says so — the caller that reads it owns any disclosure. */
|
|
220
223
|
export declare function vetPeerRegistryDirectory(dir: string, opts?: {
|
|
221
224
|
uid?: number;
|
|
222
225
|
stopAt?: string;
|
|
226
|
+
platform?: NodeJS.Platform;
|
|
223
227
|
}): Promise<PeerRegistryDirectoryVerdict>;
|
|
224
228
|
/** §2.1 ③ — the read-side judgment on ONE registration file's stat facts (pure; the reader supplies
|
|
225
229
|
* the stat): owner must be this uid, mode must carry no group/world write bit, and it must be a
|
|
226
|
-
* regular file under the read cap. A refused file is skipped LOUDLY by the reader.
|
|
230
|
+
* regular file under the read cap. A refused file is skipped LOUDLY by the reader. The mode arm is judged
|
|
231
|
+
* only where `platform` (default: this process's) has permission bits to judge ({@link posixModeBitsMeaningful}). */
|
|
227
232
|
export declare function judgePeerRecordFile(stat: {
|
|
228
233
|
uid?: number;
|
|
229
234
|
mode: number;
|
|
230
235
|
isFile: boolean;
|
|
231
236
|
size: number;
|
|
232
|
-
}, selfUid: number | undefined): {
|
|
237
|
+
}, selfUid: number | undefined, platform?: NodeJS.Platform): {
|
|
233
238
|
ok: true;
|
|
234
239
|
} | {
|
|
235
240
|
ok: false;
|
|
@@ -2,6 +2,7 @@ import { lstat } from "node:fs/promises";
|
|
|
2
2
|
import { isAbsolute, dirname, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { formatPeerNameRef, mintPeerRef, normalizePeerName, parsePeerNameRef, PEER_REF_RE } from "./cross-session-ref.js";
|
|
4
4
|
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
5
|
+
import { posixModeBitsMeaningful } from "../core/posix-mode-face.js";
|
|
5
6
|
export const PEER_SESSION_RECORD_SCHEMA_VERSION = 1;
|
|
6
7
|
export const PEER_SESSION_RECORD_MAX_BYTES = 262_144;
|
|
7
8
|
export const PEER_PRESENCE_MAX_CHARS = 120;
|
|
@@ -252,6 +253,7 @@ export async function vetPeerRegistryDirectory(dir, opts) {
|
|
|
252
253
|
if (!isAbsolute(dir))
|
|
253
254
|
return { ok: false, code: "not_absolute", path: dir };
|
|
254
255
|
const uid = opts?.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
|
|
256
|
+
const judgeModeBits = posixModeBitsMeaningful(opts?.platform);
|
|
255
257
|
const stopAt = opts?.stopAt !== undefined ? resolvePath(opts.stopAt) : undefined;
|
|
256
258
|
let cursor = resolvePath(dir);
|
|
257
259
|
for (;;) {
|
|
@@ -271,7 +273,7 @@ export async function vetPeerRegistryDirectory(dir, opts) {
|
|
|
271
273
|
if (!isRoot) {
|
|
272
274
|
if (uid !== undefined && st.uid !== uid)
|
|
273
275
|
return { ok: false, code: "foreign_owner", path: cursor };
|
|
274
|
-
if ((st.mode & 0o022) !== 0)
|
|
276
|
+
if (judgeModeBits && (st.mode & 0o022) !== 0)
|
|
275
277
|
return { ok: false, code: "group_or_world_writable", path: cursor };
|
|
276
278
|
}
|
|
277
279
|
if (isRoot)
|
|
@@ -279,12 +281,12 @@ export async function vetPeerRegistryDirectory(dir, opts) {
|
|
|
279
281
|
cursor = parent;
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
|
-
export function judgePeerRecordFile(stat, selfUid) {
|
|
284
|
+
export function judgePeerRecordFile(stat, selfUid, platform) {
|
|
283
285
|
if (!stat.isFile)
|
|
284
286
|
return { ok: false, code: "not_regular" };
|
|
285
287
|
if (selfUid !== undefined && stat.uid !== undefined && stat.uid !== selfUid)
|
|
286
288
|
return { ok: false, code: "foreign_owner" };
|
|
287
|
-
if ((stat.mode & 0o022) !== 0)
|
|
289
|
+
if (posixModeBitsMeaningful(platform) && (stat.mode & 0o022) !== 0)
|
|
288
290
|
return { ok: false, code: "group_or_world_writable" };
|
|
289
291
|
if (stat.size > PEER_SESSION_RECORD_MAX_BYTES)
|
|
290
292
|
return { ok: false, code: "too_large" };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this platform report POSIX permission bits worth judging? Windows' `Stats.mode` is a projection of
|
|
3
|
+
* the read-only attribute (`0o666` / `0o444`; a directory created with mode `0o700` reads `0o666` too), not
|
|
4
|
+
* permission bits — so a group/world-writable test over it fires on every file and says nothing about who may
|
|
5
|
+
* write. Every reader of `mode & 0o022` (and the file-mode report of the execution env) asks THIS predicate,
|
|
6
|
+
* so a platform never judges a bit it cannot have.
|
|
7
|
+
*
|
|
8
|
+
* `platform` is a parameter so a caller can ask about the platform it was told, not only the one it runs on.
|
|
9
|
+
*/
|
|
10
|
+
export declare function posixModeBitsMeaningful(platform?: NodeJS.Platform): boolean;
|
|
@@ -9,6 +9,7 @@ import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.j
|
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
10
|
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
11
|
import { physicalPathOfAsync } from "../../core/physical-path.js";
|
|
12
|
+
import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js";
|
|
12
13
|
import { walkSymlinkChain } from "./symlink-chain.js";
|
|
13
14
|
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
14
15
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
@@ -41,7 +42,7 @@ function fileKindFromStats(stats) {
|
|
|
41
42
|
}
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
|
-
const HAS_POSIX_MODE_FACE =
|
|
45
|
+
const HAS_POSIX_MODE_FACE = posixModeBitsMeaningful();
|
|
45
46
|
function fileInfoFromStats(path, stats) {
|
|
46
47
|
const kind = fileKindFromStats(stats);
|
|
47
48
|
if (!kind) {
|
|
@@ -113,15 +113,23 @@ export type RuleAdoptionFile = {
|
|
|
113
113
|
export declare function readRuleAdoptionFile(dir: string): RuleAdoptionFile | undefined;
|
|
114
114
|
/** Atomically publish the adoption marker (same write-temp → fsync → rename discipline as the store). */
|
|
115
115
|
export declare function writeRuleAdoptionFile(dir: string, content: RuleAdoptionFile): void;
|
|
116
|
+
/** What a directory vetting reads from its host. Absent ⇒ this process's platform, and a skipped judgment goes unsaid. */
|
|
117
|
+
interface DirVetting {
|
|
118
|
+
/** The platform whose permission bits are read (default: this process's — see {@link posixModeBitsMeaningful}). */
|
|
119
|
+
readonly platform?: NodeJS.Platform;
|
|
120
|
+
/** Said when the group/other-writable judgment was NOT made because the platform reports no permission bits. */
|
|
121
|
+
readonly modeBitsUnjudged?: (dir: string) => void;
|
|
122
|
+
}
|
|
116
123
|
/** One principal's rule file. */
|
|
117
124
|
declare class FileDurableRulePartition implements WritableDurableRulePartition {
|
|
118
125
|
private readonly dir;
|
|
119
126
|
private readonly file;
|
|
120
127
|
private readonly acquireWriteLock;
|
|
121
128
|
private readonly onError?;
|
|
129
|
+
private readonly vetting?;
|
|
122
130
|
readonly durability: StoreDurability;
|
|
123
131
|
readonly fidelity: StoreFidelity;
|
|
124
|
-
constructor(dir: string, file: string, acquireWriteLock: () => void, onError?: ((message: string) => void) | undefined);
|
|
132
|
+
constructor(dir: string, file: string, acquireWriteLock: () => void, onError?: ((message: string) => void) | undefined, vetting?: DirVetting | undefined);
|
|
125
133
|
/** Read and validate. Any refusal answers with an empty set AND says so — never a silent empty store. The answer's
|
|
126
134
|
* sentence names no location ({@link refusalText}); the disclosure that accompanies it does. */
|
|
127
135
|
private read;
|
|
@@ -198,9 +206,13 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
|
|
|
198
206
|
private readonly onError?;
|
|
199
207
|
private lock;
|
|
200
208
|
private orgPersistence;
|
|
209
|
+
/** What every directory vetting of this instance reads; it says an unjudged mode check ONCE per instance. */
|
|
210
|
+
private readonly vetting;
|
|
201
211
|
constructor(dir: string, onError?: ((message: string) => void) | undefined, opts?: {
|
|
202
212
|
/** The data root whose adoption marker gates construction (design/183 I6). Absent ⇒ no root gate. */
|
|
203
213
|
dataRoot?: string;
|
|
214
|
+
/** The platform whose permission bits the directory vetting reads. Absent ⇒ this process's platform. */
|
|
215
|
+
platform?: NodeJS.Platform;
|
|
204
216
|
});
|
|
205
217
|
/** The org partition's durable home in this directory (design/182 §7.4 contract; `org.json`). Shares
|
|
206
218
|
* this provider's writer lock — an install is a write. */
|
|
@@ -2,6 +2,7 @@ import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { isRuleBehavior } from "../../core/permission-rule-model.js";
|
|
5
|
+
import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js";
|
|
5
6
|
import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, unknownRuleWriteDeltaError, } from "../../core/permission-rule-store.js";
|
|
6
7
|
import { canonicalize } from "../../core/canonical-json.js";
|
|
7
8
|
import { BootLock, FileStoreLockError } from "./fs-atomic.js";
|
|
@@ -118,13 +119,17 @@ const EMPTY_READ = { rules: [], tombstones: [], rev: 0 };
|
|
|
118
119
|
function checksumOf(body) {
|
|
119
120
|
return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`;
|
|
120
121
|
}
|
|
121
|
-
function unsafeDirFault(dir) {
|
|
122
|
+
function unsafeDirFault(dir, vetting) {
|
|
122
123
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
123
124
|
const st = lstatSync(dir);
|
|
124
125
|
if (st.isSymbolicLink() || !st.isDirectory())
|
|
125
126
|
return "is a symlink or not a directory";
|
|
126
127
|
if (typeof process.getuid === "function" && st.uid !== process.getuid())
|
|
127
128
|
return "is owned by another user";
|
|
129
|
+
if (!posixModeBitsMeaningful(vetting?.platform)) {
|
|
130
|
+
vetting?.modeBitsUnjudged?.(dir);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
128
133
|
if ((st.mode & 0o022) !== 0)
|
|
129
134
|
return `is writable by group or others (mode ${(st.mode & 0o777).toString(8)}) — anyone who can write there can grant themselves permissions`;
|
|
130
135
|
return undefined;
|
|
@@ -135,15 +140,18 @@ function assertSafeDir(dir) {
|
|
|
135
140
|
throw new Error(`permission-rule store directory ${dir} ${fault}; refusing to use it`);
|
|
136
141
|
}
|
|
137
142
|
function refusalText(onError, sentence, location) {
|
|
143
|
+
discloseTo(onError, `${sentence} (at ${location})`);
|
|
144
|
+
return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`;
|
|
145
|
+
}
|
|
146
|
+
function discloseTo(onError, message) {
|
|
138
147
|
try {
|
|
139
|
-
onError?.(
|
|
148
|
+
onError?.(message);
|
|
140
149
|
}
|
|
141
150
|
catch {
|
|
142
151
|
}
|
|
143
|
-
return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`;
|
|
144
152
|
}
|
|
145
|
-
function assertSafeDirAtRequest(dir, onError, act) {
|
|
146
|
-
const fault = unsafeDirFault(dir);
|
|
153
|
+
function assertSafeDirAtRequest(dir, onError, act, vetting) {
|
|
154
|
+
const fault = unsafeDirFault(dir, vetting);
|
|
147
155
|
if (fault !== undefined)
|
|
148
156
|
throw new Error(refusalText(onError, `the permission-rule store directory ${fault}; refusing to ${act} it`, dir));
|
|
149
157
|
}
|
|
@@ -171,13 +179,15 @@ class FileDurableRulePartition {
|
|
|
171
179
|
file;
|
|
172
180
|
acquireWriteLock;
|
|
173
181
|
onError;
|
|
182
|
+
vetting;
|
|
174
183
|
durability = "durable";
|
|
175
184
|
fidelity = "json";
|
|
176
|
-
constructor(dir, file, acquireWriteLock, onError) {
|
|
185
|
+
constructor(dir, file, acquireWriteLock, onError, vetting) {
|
|
177
186
|
this.dir = dir;
|
|
178
187
|
this.file = file;
|
|
179
188
|
this.acquireWriteLock = acquireWriteLock;
|
|
180
189
|
this.onError = onError;
|
|
190
|
+
this.vetting = vetting;
|
|
181
191
|
}
|
|
182
192
|
read() {
|
|
183
193
|
const bucket = "this owner's permission-rule bucket";
|
|
@@ -185,7 +195,7 @@ class FileDurableRulePartition {
|
|
|
185
195
|
const unreadable = (sentence, location = this.file) => ({ unreadable: refusalText(this.onError, sentence, location) });
|
|
186
196
|
let raw;
|
|
187
197
|
try {
|
|
188
|
-
const dirFault = unsafeDirFault(this.dir);
|
|
198
|
+
const dirFault = unsafeDirFault(this.dir, this.vetting);
|
|
189
199
|
if (dirFault !== undefined)
|
|
190
200
|
return unreadable(`the permission-rule store directory ${dirFault}; loading zero rules`, this.dir);
|
|
191
201
|
const st = lstatSync(this.file);
|
|
@@ -228,11 +238,7 @@ class FileDurableRulePartition {
|
|
|
228
238
|
return { file: parsed };
|
|
229
239
|
}
|
|
230
240
|
disclose(message) {
|
|
231
|
-
|
|
232
|
-
this.onError?.(message);
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
}
|
|
241
|
+
discloseTo(this.onError, message);
|
|
236
242
|
}
|
|
237
243
|
screenDurableRows(file) {
|
|
238
244
|
const isSession = (scope) => scope?.kind === "session";
|
|
@@ -246,7 +252,7 @@ class FileDurableRulePartition {
|
|
|
246
252
|
return { rules, tombstones, quarantined };
|
|
247
253
|
}
|
|
248
254
|
write(next) {
|
|
249
|
-
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
255
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
|
|
250
256
|
atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
|
|
251
257
|
}
|
|
252
258
|
async list() {
|
|
@@ -392,11 +398,22 @@ export class FileDurableRulePartitionProvider {
|
|
|
392
398
|
onError;
|
|
393
399
|
lock;
|
|
394
400
|
orgPersistence;
|
|
401
|
+
vetting;
|
|
395
402
|
constructor(dir, onError, opts) {
|
|
396
403
|
this.dir = dir;
|
|
397
404
|
this.onError = onError;
|
|
398
405
|
if (opts?.dataRoot !== undefined)
|
|
399
406
|
assertAdoptionBootGate(opts.dataRoot, "FileDurableRulePartitionProvider");
|
|
407
|
+
let said = false;
|
|
408
|
+
this.vetting = {
|
|
409
|
+
platform: opts?.platform,
|
|
410
|
+
modeBitsUnjudged: (where) => {
|
|
411
|
+
if (said)
|
|
412
|
+
return;
|
|
413
|
+
said = true;
|
|
414
|
+
discloseTo(this.onError, `the permission-rule store directory is not checked for being writable by other accounts: this platform reports no POSIX permission bits, so who else can write there is not verified here (at ${where})`);
|
|
415
|
+
},
|
|
416
|
+
};
|
|
400
417
|
}
|
|
401
418
|
adoptionMarker() {
|
|
402
419
|
const judged = judgeRuleAdoptionFile(this.dir);
|
|
@@ -410,13 +427,13 @@ export class FileDurableRulePartitionProvider {
|
|
|
410
427
|
return this.fileOrgPersistence();
|
|
411
428
|
}
|
|
412
429
|
fileOrgPersistence() {
|
|
413
|
-
this.orgPersistence ??= new FileOrgRuleStatePersistence(this.dir, this.acquireWriteLock, this.onError);
|
|
430
|
+
this.orgPersistence ??= new FileOrgRuleStatePersistence(this.dir, this.acquireWriteLock, this.onError, this.vetting);
|
|
414
431
|
return this.orgPersistence;
|
|
415
432
|
}
|
|
416
433
|
acquireWriteLock = () => {
|
|
417
434
|
if (this.lock !== undefined)
|
|
418
435
|
return;
|
|
419
|
-
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
436
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
|
|
420
437
|
const lock = new BootLock(join(this.dir, "PERMISSION-RULES-WRITER-LOCK"));
|
|
421
438
|
try {
|
|
422
439
|
lock.acquire();
|
|
@@ -437,11 +454,11 @@ export class FileDurableRulePartitionProvider {
|
|
|
437
454
|
return this.readOnlySourceView(principalRuleFileName(principal), principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
|
|
438
455
|
}
|
|
439
456
|
}
|
|
440
|
-
return new FileDurableRulePartition(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError);
|
|
457
|
+
return new FileDurableRulePartition(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError, this.vetting);
|
|
441
458
|
}
|
|
442
459
|
readOnlySourceView(sourceName, targetName, flipped) {
|
|
443
460
|
const pick = flipped ? targetName : existsSync(join(this.dir, sourceName)) ? sourceName : targetName;
|
|
444
|
-
return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError));
|
|
461
|
+
return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError, this.vetting));
|
|
445
462
|
}
|
|
446
463
|
forLocalOwner() {
|
|
447
464
|
const adoption = this.adoptionMarker();
|
|
@@ -451,7 +468,7 @@ export class FileDurableRulePartitionProvider {
|
|
|
451
468
|
return this.readOnlySourceView(LOCAL_OWNER_FILE, principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
|
|
452
469
|
}
|
|
453
470
|
}
|
|
454
|
-
return new FileDurableRulePartition(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError);
|
|
471
|
+
return new FileDurableRulePartition(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError, this.vetting);
|
|
455
472
|
}
|
|
456
473
|
dispose() {
|
|
457
474
|
this.lock?.release();
|
|
@@ -462,12 +479,14 @@ class FileOrgRuleStatePersistence {
|
|
|
462
479
|
dir;
|
|
463
480
|
acquireWriteLock;
|
|
464
481
|
onError;
|
|
482
|
+
vetting;
|
|
465
483
|
file;
|
|
466
484
|
writeChain = Promise.resolve();
|
|
467
|
-
constructor(dir, acquireWriteLock, onError) {
|
|
485
|
+
constructor(dir, acquireWriteLock, onError, vetting) {
|
|
468
486
|
this.dir = dir;
|
|
469
487
|
this.acquireWriteLock = acquireWriteLock;
|
|
470
488
|
this.onError = onError;
|
|
489
|
+
this.vetting = vetting;
|
|
471
490
|
this.file = join(dir, ORG_STATE_FILE);
|
|
472
491
|
}
|
|
473
492
|
readSync() {
|
|
@@ -477,7 +496,7 @@ class FileOrgRuleStatePersistence {
|
|
|
477
496
|
const ioFault = (err) => `could not be read (${err?.code ?? err?.name ?? "unknown"})`;
|
|
478
497
|
let dirFault;
|
|
479
498
|
try {
|
|
480
|
-
dirFault = unsafeDirFault(this.dir);
|
|
499
|
+
dirFault = unsafeDirFault(this.dir, this.vetting);
|
|
481
500
|
}
|
|
482
501
|
catch (err) {
|
|
483
502
|
return refuse(`the permission-rule store directory ${ioFault(err)}`, `${this.dir}: ${thrownText(err)}`);
|
|
@@ -524,7 +543,7 @@ class FileOrgRuleStatePersistence {
|
|
|
524
543
|
}
|
|
525
544
|
installOrgStateSync(state) {
|
|
526
545
|
this.acquireWriteLock();
|
|
527
|
-
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
546
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
|
|
528
547
|
state = jsonForm(state);
|
|
529
548
|
const cur = this.readSync();
|
|
530
549
|
if (cur !== undefined && state.revisionHighWater < cur.revisionHighWater) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.49.5",
|
|
4
|
+
"from": "40a79ac3341fc6750aae1664c64840a44abaa205",
|
|
5
|
+
"to": "9ba02c33f3eea253894b0132dca7ed98c390fb08",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [],
|
|
10
|
+
"closedSetMembers": [],
|
|
11
|
+
"closedSetsUnreadable": [],
|
|
12
|
+
"typeKeys": [],
|
|
13
|
+
"typeKeysUnreadableArms": [],
|
|
14
|
+
"typeKeysFlattenedCount": 223,
|
|
15
|
+
"signatures": [
|
|
16
|
+
{
|
|
17
|
+
"name": "FileDurableRulePartitionProvider.constructor",
|
|
18
|
+
"at": [
|
|
19
|
+
"src/stores/file/permission-rule-store.ts:671"
|
|
20
|
+
],
|
|
21
|
+
"from": [
|
|
22
|
+
"(dir: string, onError?: (message: string) => void, opts?: { dataRoot?: string; })"
|
|
23
|
+
],
|
|
24
|
+
"to": [
|
|
25
|
+
"(dir: string, onError?: (message: string) => void, opts?: { dataRoot?: string; platform?: NodeJS.Platform; })"
|
|
26
|
+
],
|
|
27
|
+
"changes": [
|
|
28
|
+
{
|
|
29
|
+
"aspect": "param",
|
|
30
|
+
"detail": "param 3 `opts` type widened: { dataRoot?: string; [ -> platform?: NodeJS.Platform; ]}",
|
|
31
|
+
"breaking": false
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"breaking": false,
|
|
35
|
+
"criteriaCovered": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "judgePeerRecordFile",
|
|
39
|
+
"at": [
|
|
40
|
+
"src/agents/peer-directory.ts:484"
|
|
41
|
+
],
|
|
42
|
+
"from": [
|
|
43
|
+
"(stat: { uid?: number; mode: number; isFile: boolean; size: number; }, selfUid: number | undefined): { ok: false; code: \"not_regular\" | \"foreign_owner\" | \"group_or_world_writable\" | \"too_large\"; } | { ok: true; }"
|
|
44
|
+
],
|
|
45
|
+
"to": [
|
|
46
|
+
"(stat: { uid?: number; mode: number; isFile: boolean; size: number; }, selfUid: number | undefined, platform?: NodeJS.Platform): { ok: false; code: \"not_regular\" | \"foreign_owner\" | \"group_or_world_writable\" | \"too_large\"; } | { ok: true; }"
|
|
47
|
+
],
|
|
48
|
+
"changes": [
|
|
49
|
+
{
|
|
50
|
+
"aspect": "param",
|
|
51
|
+
"detail": "+optional param `platform?: NodeJS.Platform`",
|
|
52
|
+
"breaking": false
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"breaking": false,
|
|
56
|
+
"criteriaCovered": false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "vetPeerRegistryDirectory",
|
|
60
|
+
"at": [
|
|
61
|
+
"src/agents/peer-directory.ts:452"
|
|
62
|
+
],
|
|
63
|
+
"from": [
|
|
64
|
+
"(dir: string, opts?: { uid?: number; stopAt?: string; }): Promise<PeerRegistryDirectoryVerdict>"
|
|
65
|
+
],
|
|
66
|
+
"to": [
|
|
67
|
+
"(dir: string, opts?: { uid?: number; stopAt?: string; platform?: NodeJS.Platform; }): Promise<PeerRegistryDirectoryVerdict>"
|
|
68
|
+
],
|
|
69
|
+
"changes": [
|
|
70
|
+
{
|
|
71
|
+
"aspect": "param",
|
|
72
|
+
"detail": "param 2 `opts` type widened: …umber; stopAt?: string; [ -> platform?: NodeJS.Platform; ]}",
|
|
73
|
+
"breaking": false
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"breaking": false,
|
|
77
|
+
"criteriaCovered": false
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"declarationMoves": [],
|
|
81
|
+
"mintMoves": [],
|
|
82
|
+
"criteriaObserves": [
|
|
83
|
+
{
|
|
84
|
+
"kind": "typeMember",
|
|
85
|
+
"name": "AskRequest.classes",
|
|
86
|
+
"assertions": [
|
|
87
|
+
"G1.a",
|
|
88
|
+
"G2.a"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"consumersHint": {
|
|
93
|
+
"cli": [
|
|
94
|
+
"FileDurableRulePartitionProvider"
|
|
95
|
+
],
|
|
96
|
+
"server": [
|
|
97
|
+
"FileDurableRulePartitionProvider"
|
|
98
|
+
],
|
|
99
|
+
"sdk": [],
|
|
100
|
+
"note": "scanned cli@7006be65 server@72f3abe9 sdk@f1b30d4 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it. Ledger files (**/docs/core-consumption/**, **/docs/wire-consumption/**, **/*LEDGER*.json) are not evidence of use — server: 2 name(s) matched ONLY in bookkeeping files and were set aside (judgePeerRecordFile, vetPeerRegistryDirectory)"
|
|
101
|
+
},
|
|
102
|
+
"breakingCount": 0,
|
|
103
|
+
"breakingItems": []
|
|
104
|
+
}
|