Specialized

Permissions

Permissions

@openclaw/fs-safe/permissions contains the curated mode and permission inspection helpers used by secure file reads and by applications that want to report actionable permission problems.

import {
  formatPermissionDetail,
  formatPermissionRemediation,
  inspectPathPermissions,
} from "@openclaw/fs-safe/permissions";

const perms = await inspectPathPermissions("/var/lib/app/auth.token");
console.log(formatPermissionDetail("/var/lib/app/auth.token", perms));
if (perms.ok && (perms.groupReadable || perms.worldReadable)) {
  console.log(
    formatPermissionRemediation({
      targetPath: "/var/lib/app/auth.token",
      perms,
      isDir: false,
      posixMode: 0o600,
    }),
  );
}

#POSIX helpers

safeStat(path);
inspectPathPermissions(path, options?);
formatPermissionDetail(path, check);
formatPermissionRemediation({ targetPath, perms, isDir, posixMode });
modeBits(mode);
formatOctal(bits);
isWorldWritable(bits);
isGroupWritable(bits);
isWorldReadable(bits);
isGroupReadable(bits);

POSIX remediation strings shell-quote paths with whitespace or metacharacters and protect option-like paths with --, so they can be presented as commands without letting the inspected pathname add shell syntax.

inspectPathPermissions() follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports ownerSid plus ownerTrusted when ownership can be verified. ownerTrusted is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. readSecureFile() obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged PowerShell/C# bridge in auto and off modes.

#Advanced Windows ACL helpers

The low-level Windows ACL parser and icacls command builders live in @openclaw/fs-safe/advanced:

import {
  createIcaclsResetCommand,
  formatIcaclsResetCommand,
  formatWindowsAclSummary,
  inspectWindowsAcl,
  parseIcaclsOutput,
  resolveWindowsUserPrincipal,
  summarizeWindowsAcl,
} from "@openclaw/fs-safe/advanced";

inspectWindowsAcl(path, { env, exec });
parseIcaclsOutput(output, targetPath);
summarizeWindowsAcl(entries, env);
formatWindowsAclSummary(summary);
formatIcaclsResetCommand(targetPath, { isDir, env });
createIcaclsResetCommand(targetPath, { isDir, env });
resolveWindowsUserPrincipal(env);

The fallback Windows inspector reads the owner and DACL together through one built-in Windows PowerShell/.NET query. The query addresses its JSON command by module name and limits module discovery to PowerShell's bundled system modules. It returns canonical SIDs and numeric access masks, so Unicode paths and account names do not pass through lossy console display text. inspectWindowsAcl() uses native descriptor facts for complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the optional Windows binding is available. It applies the same classifier to native facts and the fallback query, returning canonical SIDs in its principal fields with normalized rights tokens. Explicit env or exec options retain the query path. Disabled or unavailable native helpers, remote or incomplete descriptors, leaf symbolic links, and native query errors use the fallback. Explicit ACEs and zero-mask entries also retain the query so .NET continues to own its ACE ordering and normalization. Structured ACLs containing only canonical SIDs are classified directly from the current-user SID without requiring a separate account-name lookup. The advanced options retain currentUserSid as an explicit classification override and principalTranslationFailed: true as an immediate unverified result. The optional principalSids translation cache is still accepted but is no longer needed because the query returns SIDs directly. The existing classifier assigns principals to trusted, world, or group; trusted defaults include the current user, SYSTEM, and Administrators. The built-in query has a fixed 30-second process deadline. A command failure or timeout returns an unverified result (source: "unknown") so callers fail closed. Advanced callers that inject a custom exec implementation own that executor's deadline. Failed owner and ACL inspections retain error text and an optional errorDetail: PermissionCommandFailure with command, integer durationMs, timedOut, exitCode, signal, and stderr. The type is exported from both @openclaw/fs-safe/permissions and @openclaw/fs-safe/advanced. Built-in execution measures elapsed time; injected execFile-shaped failures receive best-effort command diagnostics. Plain errors have no errorDetail. Display reasons and stderr escape control characters and are limited to 400 characters, including a trailing when truncated. Diagnostics do not copy stdout or read target file contents. The separate errorCause retains the original exception for restricted local diagnosis; do not serialize or expose it as display text. Custom executors may reject with any JavaScript value. The fallback display formatter handles primitives directly and reads only string-valued name and message data descriptors through a small, fixed prototype budget. It does not coerce objects, invoke accessors, or inspect proxy targets; unavailable display facts use a bounded generic reason. Command fields follow the same best-effort data-descriptor rule. Raw string, Buffer, or genuine Uint8Array stderr retains the sanitization above. Byte stderr is copied through captured typed-array intrinsics into a private bounded snapshot before replacement-based UTF-8 decoding; receiver properties, iterators, constructors, and altered prototypes are not consulted. Detached or out-of-bounds byte views contribute no stderr detail. These diagnostic limits do not relax permission policy: incomplete owner or ACL inspection remains unverified, and errorCause remains the exact rejected value even when no display metadata is safe to obtain. The parser and remediation command builders remain on the advanced surface for CLIs processing captured icacls output or presenting an explicit repair. Runtime inspection does not parse that display text. A null DACL reports unrestricted access; an empty DACL grants nothing. Inherit-only ACEs do not apply to the inspected object, and deny ACEs never subtract coarse grants or claim effective-access evaluation. Unsupported ACE layouts remain unverified.

When the native binding is available, inspectPathPermissions() reads the owner and DACL directly with Windows security APIs. It classifies the current user, LocalSystem, and built-in Administrators as trusted and reports the world/group read/write facts consumed by secure reads. Descriptor forms it cannot classify equivalently fall back to the structured .NET query; mode: "off" exercises that fallback deterministically.

#Policy-free owner and DACL facts

readOwnerAndDacl() exposes the direct Windows descriptor facts needed by a consumer that owns a principal allowlist. It deliberately does not decide which SID is trusted or calculate effective access. For example, snapshot staging can reject an incomplete descriptor and ignore inherit-only ACEs before applying its own exact SID policy:

import { readOwnerAndDacl } from "@openclaw/fs-safe/permissions";

const facts = readOwnerAndDacl(stagingDirectory);
if (facts.status === "unsupported-platform") {
  throw new Error(`Windows ACL facts unavailable on ${facts.platform}`);
}
if (!facts.isLocal || !facts.daclPresent || !facts.complete) {
  throw new Error("staging DACL cannot be evaluated completely");
}

for (const ace of facts.aces) {
  if (ace.flags.inheritOnly) continue;
  if (!trustedSids.has(ace.sid)) {
    throw new Error(`unexpected staging principal: ${ace.sid}`);
  }
  evaluateMaskAndDenyOrder(ace.aceType, ace.mask);
}

On Windows the supported result contains ownerSid, currentUserSid, daclPresent, isLocal, complete, unsupportedAceTypes, and ordered basic allow/deny aces. currentUserSid is the process token's TokenUser SID, so callers can compare it with the owner or their own allowlist without fs-safe applying trust policy. Each ACE has { sid, mask, aceType, flags }; flags retains the raw byte and decoded objectInherit, containerInherit, noPropagateInherit, inheritOnly, inherited, successfulAccess, and failedAccess facts. SID strings are lowercase Windows SID notation. daclPresent: false represents a null DACL, which grants unrestricted access; it must not be mistaken for an empty DACL.

Object-specific and other ACE layouts are not guessed: they are omitted, complete becomes false, and their numeric types appear in unsupportedAceTypes, allowing a security-sensitive caller to fail closed. Non-Windows systems return { status: "unsupported-platform", platform }. Windows prefers the native binding. In native auto or off mode, a missing binding or capability uses the packaged PowerShell/C# bridge with the same raw ACE projection. Native require rejects either absence with FsSafeError("helper-unavailable") and starts no command. An available native query's failure is terminal. The existing coarse inspectPathPermissions() API still owns its compatibility fallback and trust classification.

#Private directories

import path from "node:path";
import { createPrivateDirectory } from "@openclaw/fs-safe/permissions";

const sqliteDirectory =
  "C:\\Users\\me\\AppData\\Local\\OpenClaw\\private-databases";
await createPrivateDirectory(sqliteDirectory);
await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));

On Windows, this creates the directory and applies a protected owner + LocalSystem + Administrators full-control DACL directly with an atomic security descriptor. The native route launches no command. When its binding or capability is unavailable, native auto and off modes use the packaged PowerShell/C# bridge. Both routes retain the parent and exact created-directory handles through ACL and final pathname validation. If validation fails, it attempts only nonrecursive deletion through the created handle, preserving any pathname replacement. If cleanup also fails, the error retains the original failure and includes the cleanup failure.

Directory association checks compare the complete 64-bit volume serial and 128-bit FILE_ID_INFO identity, including on ReFS. If that identity class is unavailable, the operation fails closed without a narrower file-index fallback. Validation confirms that the created directory is local, its DACL is protected from inheritance, and its final public pathname opens the same local directory.

This is a point-in-time pathname association check. The function closes its handles before returning; callers must keep the pathname's ancestry trusted during subsequent use, including opening SQLite databases in the example above. The immediate parent and final directory must not be reparse points. Earlier ancestor reparse points can be followed; this API does not reject every reparse point in the full ancestry.

Path components ending in a space or period are rejected before filesystem operations to avoid differing Win32 and native pathname interpretations. This also rejects explicit . and .. components, including spellings such as .\private and parent\..\private, as a compatibility restriction. Simple relative names without these components remain supported.

This API is Windows-only; it fails closed with FsSafeError("helper-unavailable") on other platforms. Native require also fails if the binding or capability is missing and never starts a command. An available native operation's failure is terminal. POSIX callers should create private directories through their existing trusted-root creation policy rather than a pathname-only compatibility shim. Existing Windows permission inspection still retains its structured .NET compatibility fallback.

The raw owner/DACL and private-directory fallbacks each emit one path-free FS_SAFE_NATIVE_FALLBACK warning per process. PowerShell startup and C# compilation add overhead to each call; install the native package for frequent operations. These routes run the package's readable, fixed scripts under normal system PowerShell policy; see the Windows security fallback prerequisites. If command support is unavailable, disallowed, or fails, the operation rejects. Private-directory creation never falls back to inherited permissions. The asynchronous creation command has a 30-second deadline. After a timeout or transport failure, fs-safe requests termination and waits at most one further second before rejecting and closing its output pipes. The error distinguishes observed process exit from an unconfirmed termination attempt. If the OS refuses termination, the command can still create the directory after rejection. An already-created object retains its protected DACL, but pathname validation and owned-handle cleanup may not finish. An error therefore does not prove the pathname is absent; a retry can report EEXIST. Before retrying or using the pathname, establish that the earlier operation stopped and verify any existing directory's security. The library does not attempt pathname-based cleanup.

Use createIcaclsResetCommand() when you need a structured command and argv pair. Use formatIcaclsResetCommand() when you only need a remediation string for a user-facing message.

#Types

type PermissionCheck = {
  ok: boolean;
  isSymlink: boolean;
  isDir: boolean;
  mode: number | null;
  bits: number | null;
  source: "posix" | "windows-acl" | "unknown";
  worldWritable: boolean;
  groupWritable: boolean;
  worldReadable: boolean;
  groupReadable: boolean;
  ownerSid?: string;
  ownerTrusted?: boolean;
  ownerError?: string;
  aclSummary?: string;
  error?: string;
};

ok: false means the path itself could not be inspected. ok: true with source: "unknown" means basic stat information was available, but the platform-specific permission source could not be verified.

#See also