mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
[codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives * refactor: use fs-safe for file access helpers * refactor: reuse fs-safe for media reads * refactor: use fs-safe for image reads * refactor: reuse fs-safe in qqbot media opener * refactor: reuse fs-safe for local media checks * refactor: consume cleaner fs-safe api * refactor: align fs-safe json option names * fix: preserve fs-safe migration contracts * refactor: use fs-safe primitive subpaths * refactor: use grouped fs-safe subpaths * refactor: align fs-safe api usage * refactor: adapt private state store api * chore: refresh proof gate * refactor: follow fs-safe json api split * refactor: follow reduced fs-safe surface * build: default fs-safe python helper off * fix: preserve fs-safe plugin sdk aliases * refactor: consolidate fs-safe usage * refactor: unify fs-safe store usage * refactor: trim fs-safe temp workspace usage * refactor: hide low-level fs-safe primitives * build: use published fs-safe package * fix: preserve outbound recovery durability after rebase * chore: refresh pr checks
This commit is contained in:
committed by
GitHub
parent
61481eb34f
commit
538605ff44
@@ -1,2 +1,2 @@
|
||||
fe061b6f35adb2b152d8f48244a94d4934b335143cc5f5aebb8cc96e5ba8b287 plugin-sdk-api-baseline.json
|
||||
495248d5981456192aaf7da2ed23d5951eaa6d9e59d70c716ab91c3da3620e73 plugin-sdk-api-baseline.jsonl
|
||||
1a06492fe05d1c9dc3194677f52d57ec90468b93023b70d0852ef01d87c7eae3 plugin-sdk-api-baseline.json
|
||||
c950a1923c0dc7d31120a3010e24217bcf22fd9cacbe102d3ae19b0120c0f648 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
"source": "Gateway RPC reference",
|
||||
"target": "Gateway RPC 参考"
|
||||
},
|
||||
{
|
||||
"source": "Secure file operations",
|
||||
"target": "安全文件操作"
|
||||
},
|
||||
{
|
||||
"source": "Sessions",
|
||||
"target": "会话"
|
||||
@@ -758,5 +762,9 @@
|
||||
{
|
||||
"source": "/cli/config",
|
||||
"target": "/cli/config"
|
||||
},
|
||||
{
|
||||
"source": "fs-safe Cleanup Plan",
|
||||
"target": "fs-safe Cleanup Plan"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1501,6 +1501,7 @@
|
||||
"group": "Security and sandboxing",
|
||||
"pages": [
|
||||
"gateway/security/index",
|
||||
"gateway/security/secure-file-operations",
|
||||
"gateway/security/audit-checks",
|
||||
"gateway/operator-scopes",
|
||||
"gateway/sandboxing",
|
||||
|
||||
@@ -65,6 +65,12 @@ OpenClaw assumes the host and config boundary are trusted:
|
||||
- Session identifiers (`sessionKey`, session IDs, labels) are routing selectors, not authorization tokens.
|
||||
- If several people can message one tool-enabled agent, each of them can steer that same permission set. Per-user session/memory isolation helps privacy, but does not convert a shared agent into per-user host authorization.
|
||||
|
||||
### Secure file operations
|
||||
|
||||
OpenClaw uses `@openclaw/fs-safe` for root-bounded file access, atomic writes, archive extraction, temp workspaces, and secret-file helpers. OpenClaw defaults fs-safe's optional POSIX Python helper to **off**; set `OPENCLAW_FS_SAFE_PYTHON_MODE=auto` or `require` only when you want the extra fd-relative mutation hardening and can support a Python runtime.
|
||||
|
||||
Details: [Secure file operations](/gateway/security/secure-file-operations).
|
||||
|
||||
### Shared Slack workspace: real risk
|
||||
|
||||
If "everyone in Slack can message the bot," the core risk is delegated tool authority:
|
||||
|
||||
76
docs/gateway/security/secure-file-operations.md
Normal file
76
docs/gateway/security/secure-file-operations.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
summary: "How OpenClaw handles local file access safely, and why the optional fs-safe Python helper is off by default"
|
||||
read_when:
|
||||
- Changing file access, archive extraction, workspace storage, or plugin filesystem helpers
|
||||
title: "Secure file operations"
|
||||
---
|
||||
|
||||
OpenClaw uses [`@openclaw/fs-safe`](https://github.com/openclaw/fs-safe) for security-sensitive local file operations: root-bounded reads/writes, atomic replacement, archive extraction, temp workspaces, JSON state, and secret-file handling.
|
||||
|
||||
The goal is a consistent **library guardrail** for trusted OpenClaw code that receives untrusted path names. It is not a sandbox. Host filesystem permissions, OS users, containers, and the agent/tool policy still define the real blast radius.
|
||||
|
||||
## Default: no Python helper
|
||||
|
||||
OpenClaw defaults the fs-safe POSIX Python helper to **off**.
|
||||
|
||||
Why:
|
||||
|
||||
- the gateway should not spawn a persistent Python sidecar unless an operator opted into it;
|
||||
- many installs do not need the extra parent-directory mutation hardening;
|
||||
- disabling Python keeps package/runtime behavior more predictable across desktop, Docker, CI, and bundled app environments.
|
||||
|
||||
OpenClaw only changes the default. If you explicitly set a mode, fs-safe honors it:
|
||||
|
||||
```bash
|
||||
# Default OpenClaw behavior: Node-only fs-safe fallbacks.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=off
|
||||
|
||||
# Opt into the helper when available, falling back if unavailable.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=auto
|
||||
|
||||
# Fail closed if the helper cannot start.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=require
|
||||
|
||||
# Optional explicit interpreter.
|
||||
OPENCLAW_FS_SAFE_PYTHON=/usr/bin/python3
|
||||
```
|
||||
|
||||
The generic fs-safe names also work: `FS_SAFE_PYTHON_MODE` and `FS_SAFE_PYTHON`.
|
||||
|
||||
## What stays protected without Python
|
||||
|
||||
With the helper off, OpenClaw still uses fs-safe's Node paths for:
|
||||
|
||||
- rejecting relative-path escapes such as `..`, absolute paths, and path separators where only names are allowed;
|
||||
- resolving operations through a trusted root handle instead of ad-hoc `path.resolve(...).startsWith(...)` checks;
|
||||
- refusing symlink and hardlink patterns on APIs that require that policy;
|
||||
- opening files with identity checks where the API returns or consumes file contents;
|
||||
- atomic sibling-temp writes for state/config files;
|
||||
- byte limits for reads and archive extraction;
|
||||
- private modes for secrets and state files where the API requires them.
|
||||
|
||||
These protections cover the normal OpenClaw threat model: trusted gateway code handling untrusted model/plugin/channel path input inside a single trusted operator boundary.
|
||||
|
||||
## What Python adds
|
||||
|
||||
On POSIX, fs-safe's optional helper keeps one persistent Python process and uses fd-relative filesystem operations for parent-directory mutations such as rename, remove, mkdir, stat/list, and some write paths.
|
||||
|
||||
That narrows same-UID race windows where another process can swap a parent directory between validation and mutation. It is defense in depth for hosts where untrusted local processes can modify the same directories OpenClaw is operating in.
|
||||
|
||||
If your deployment has that risk and Python is guaranteed to exist, use:
|
||||
|
||||
```bash
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=require
|
||||
```
|
||||
|
||||
Use `require` rather than `auto` when the helper is part of your security posture; `auto` intentionally falls back to Node-only behavior if the helper is unavailable.
|
||||
|
||||
## Plugin and core guidance
|
||||
|
||||
- Plugin-facing file access should go through `openclaw/plugin-sdk/*` helpers, not raw `fs`, when a path comes from a message, model output, config, or plugin input.
|
||||
- Core code should use the local fs-safe wrappers under `src/infra/*` so OpenClaw's process policy is applied consistently.
|
||||
- Archive extraction should use the fs-safe archive helpers with explicit size, entry-count, link, and destination limits.
|
||||
- Secrets should use OpenClaw secret helpers or fs-safe secret/private-state helpers; do not hand-roll mode checks around `fs.writeFile`.
|
||||
- If you need hostile local-user isolation, do not rely on fs-safe alone. Run separate gateways under separate OS users/hosts or use sandboxing.
|
||||
|
||||
Related: [Security](/gateway/security), [Sandboxing](/gateway/sandboxing), [Exec approvals](/tools/exec-approvals), [Secrets](/gateway/secrets).
|
||||
@@ -425,7 +425,7 @@ releases.
|
||||
| `plugin-sdk/approval-native-runtime` | Approval target helpers | Native approval target/account binding helpers |
|
||||
| `plugin-sdk/approval-reply-runtime` | Approval reply helpers | Exec/plugin approval reply payload helpers |
|
||||
| `plugin-sdk/channel-runtime-context` | Channel runtime-context helpers | Generic channel runtime-context register/get/watch helpers |
|
||||
| `plugin-sdk/security-runtime` | Security helpers | Shared trust, DM gating, external-content, and secret-collection helpers |
|
||||
| `plugin-sdk/security-runtime` | Security helpers | Shared trust, DM gating, root-bounded file/path helpers, external-content, and secret-collection helpers |
|
||||
| `plugin-sdk/ssrf-policy` | SSRF policy helpers | Host allowlist and private-network policy helpers |
|
||||
| `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers |
|
||||
| `plugin-sdk/system-event-runtime` | System event helpers | `enqueueSystemEvent`, `peekSystemEventEntries` |
|
||||
|
||||
@@ -161,7 +161,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
|
||||
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
|
||||
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
|
||||
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, sensitive text redaction, constant-time secret comparison, and secret-collection helpers |
|
||||
| `plugin-sdk/security-runtime` | Shared trust, DM gating, root-bounded file/path helpers including create-only writes, sync/async atomic file replacement, sibling temp writes, cross-device move fallback, private file-store helpers, symlink-parent guards, external-content, sensitive text redaction, constant-time secret comparison, and secret-collection helpers |
|
||||
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
|
||||
| `plugin-sdk/ssrf-dispatcher` | Narrow pinned-dispatcher helpers without the broad infra runtime surface |
|
||||
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, SSRF error, and SSRF policy helpers |
|
||||
@@ -210,7 +210,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/param-readers` | Common tool/CLI param readers |
|
||||
| `plugin-sdk/tool-payload` | Extract normalized payloads from tool result objects |
|
||||
| `plugin-sdk/tool-send` | Extract canonical send target fields from tool args |
|
||||
| `plugin-sdk/temp-path` | Shared temp-download path helpers |
|
||||
| `plugin-sdk/temp-path` | Shared temp-download path helpers and private secure temp workspaces |
|
||||
| `plugin-sdk/logging-core` | Subsystem logger and redaction helpers |
|
||||
| `plugin-sdk/markdown-table-runtime` | Markdown table mode and conversion helpers |
|
||||
| `plugin-sdk/model-session-runtime` | Model/session override helpers such as `applyModelOverrideToSessionEntry` and `resolveAgentMaxConcurrent` |
|
||||
|
||||
448
docs/refactor/fs-cleanup.md
Normal file
448
docs/refactor/fs-cleanup.md
Normal file
@@ -0,0 +1,448 @@
|
||||
---
|
||||
title: "fs-safe Cleanup Plan"
|
||||
summary: "Plan for consolidating OpenClaw filesystem helpers around @openclaw/fs-safe"
|
||||
read_when:
|
||||
- You are refactoring OpenClaw filesystem helpers
|
||||
- You are changing @openclaw/fs-safe imports, wrappers, or plugin SDK file APIs
|
||||
- You are deciding whether a local file helper belongs in OpenClaw or fs-safe
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
Implemented on `codex/extract-fs-safe-primitives`. Keep this file as the
|
||||
cleanup checklist for follow-up reviews and future fs-safe surface changes.
|
||||
|
||||
## Goal
|
||||
|
||||
Make OpenClaw's filesystem access boring and predictable:
|
||||
|
||||
- Core code uses one small set of OpenClaw wrappers that apply OpenClaw policy.
|
||||
- Plugin SDK compatibility aliases stay deliberate and documented.
|
||||
- fs-safe keeps a small public story centered on `root()`, with lower-level
|
||||
primitives behind explicit subpaths.
|
||||
- Duplicate JSON, temp, private-store, and path helper names disappear from
|
||||
OpenClaw internals.
|
||||
- Security-sensitive behavior keeps regression tests before names move.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not remove public plugin SDK exports in this cleanup. Keep deprecated
|
||||
aliases until a versioned SDK migration removes them.
|
||||
- Do not make fs-safe a sandbox. It remains a library guardrail for local file
|
||||
access, not OS isolation.
|
||||
- Do not convert all absolute-path reads to root-bounded reads. Some OpenClaw
|
||||
paths are trusted absolute paths and should stay explicit.
|
||||
- Do not chase cosmetic import churn without reducing helper count or clarifying
|
||||
trust boundaries.
|
||||
|
||||
## fs-safe Package Pin
|
||||
|
||||
`@openclaw/fs-safe` is published on npm and consumed through a semver range.
|
||||
Fresh checkouts and CI runners should install the package from the public
|
||||
registry, not from a local `link:../fs-safe` checkout or a GitHub tarball.
|
||||
|
||||
Current range:
|
||||
|
||||
- `^0.1.0`
|
||||
|
||||
The published package ships built `dist` files, so OpenClaw should not list it
|
||||
in `pnpm.onlyBuiltDependencies`.
|
||||
|
||||
## Current Shape
|
||||
|
||||
fs-safe's main entry is intentionally narrow:
|
||||
|
||||
- `root`
|
||||
- `FsSafeError`
|
||||
- `categorizeFsSafeError`
|
||||
- root option/result types
|
||||
- Python helper configuration
|
||||
|
||||
The wider surface lives behind subpaths:
|
||||
|
||||
- `/json`
|
||||
- `/store`
|
||||
- `/temp`
|
||||
- `/atomic`
|
||||
- `/root`
|
||||
- `/advanced`
|
||||
- `/archive`
|
||||
- `/walk`
|
||||
|
||||
OpenClaw now keeps fs-safe behind a small wrapper boundary:
|
||||
|
||||
- local `src/infra/*` wrappers for core policy defaults
|
||||
- public plugin SDK aliases, including older names from before fs-safe
|
||||
- package-local utility exports where importing `src/infra` would cross a
|
||||
package boundary
|
||||
|
||||
An import-boundary test rejects new direct fs-safe imports outside those
|
||||
allowed areas.
|
||||
|
||||
## Usage Map
|
||||
|
||||
### Root-bounded access
|
||||
|
||||
Representative use:
|
||||
|
||||
- `src/gateway/server-methods/agents.ts`
|
||||
- `src/agents/pi-tools.read.ts`
|
||||
- `src/agents/apply-patch.ts`
|
||||
- `src/plugins/install.ts`
|
||||
- `src/auto-reply/reply/stage-sandbox-media.ts`
|
||||
- `src/gateway/canvas-documents.ts`
|
||||
|
||||
Keep this family. `root()` is the fs-safe product surface OpenClaw should push
|
||||
callers toward.
|
||||
|
||||
### JSON helpers
|
||||
|
||||
OpenClaw still uses many names for the same operations:
|
||||
|
||||
- `readJsonFile`
|
||||
- `readJsonFileStrict`
|
||||
- `readDurableJsonFile`
|
||||
- `writeJsonAtomic`
|
||||
- `loadJsonFile`
|
||||
- `saveJsonFile`
|
||||
- `readJsonFileWithFallback`
|
||||
- `writeJsonFileAtomically`
|
||||
|
||||
fs-safe's canonical names are clearer:
|
||||
|
||||
- `tryReadJson`
|
||||
- `readJson`
|
||||
- `readJsonIfExists`
|
||||
- `writeJson`
|
||||
- `readJsonSync`
|
||||
- `tryReadJsonSync`
|
||||
- `writeJsonSync`
|
||||
|
||||
This was the highest-value cleanup because it removed naming drift without
|
||||
changing semantics. Compatibility aliases stay in `src/infra/json-files.ts` and
|
||||
plugin SDK barrels.
|
||||
|
||||
### Private state and stores
|
||||
|
||||
Representative use:
|
||||
|
||||
- `src/commitments/store.ts`
|
||||
- `src/agents/models-config.ts`
|
||||
- `src/agents/pi-auth-json.ts`
|
||||
- `src/cron/run-log.ts`
|
||||
- `src/secrets/shared.ts`
|
||||
- `src/infra/device-auth-store.ts`
|
||||
- `src/infra/device-identity.ts`
|
||||
|
||||
Current overlap:
|
||||
|
||||
- `fileStore`
|
||||
- `fileStore({ private: true })`
|
||||
- plugin SDK private-state aliases
|
||||
|
||||
The concepts are now one family. fs-safe exposes private mode through
|
||||
`fileStore({ private: true })`; OpenClaw internals and bundled plugins use
|
||||
store-shaped wrappers instead of standalone private JSON/text helpers.
|
||||
|
||||
### Temp workspaces
|
||||
|
||||
Representative use:
|
||||
|
||||
- `src/media/qr-image.ts`
|
||||
- `extensions/discord/src/send.voice.ts`
|
||||
- `extensions/discord/src/voice/audio.ts`
|
||||
- `extensions/qa-lab/src/temp-dir.test-helper.ts`
|
||||
|
||||
`tempWorkspace` is the stable useful primitive. One-shot temp targets and
|
||||
sibling-temp helpers are lower-level implementation tools.
|
||||
|
||||
### Atomic writes
|
||||
|
||||
Representative use:
|
||||
|
||||
- config and session stores
|
||||
- cron stores
|
||||
- plugin install paths
|
||||
- extension state files
|
||||
|
||||
Keep atomic replacement as a public fs-safe subpath. OpenClaw should use the
|
||||
same canonical JSON/text helpers where possible instead of hand-picking lower
|
||||
level atomic calls for ordinary JSON state.
|
||||
|
||||
### Regular, secure, and root file reads
|
||||
|
||||
These are not true duplicates:
|
||||
|
||||
- `root()` protects root-relative untrusted paths.
|
||||
- regular-file helpers read trusted absolute paths with regular-file checks.
|
||||
- secure-file helpers add ownership and mode checks for secret references.
|
||||
|
||||
Keep them separate. Document the trust boundary instead of hiding it behind one
|
||||
generic "read file" helper.
|
||||
|
||||
### Archive helpers
|
||||
|
||||
Representative use:
|
||||
|
||||
- plugin install
|
||||
- skill install
|
||||
- marketplace and ClawHub archive flows
|
||||
|
||||
Keep as a separate fs-safe subpath. Do not leak archive entry plumbing into
|
||||
OpenClaw core call sites unless the caller is actually validating archive
|
||||
metadata.
|
||||
|
||||
## Target Design
|
||||
|
||||
### OpenClaw imports
|
||||
|
||||
Core OpenClaw code should use local policy wrappers:
|
||||
|
||||
- `src/infra/fs-safe.ts` for common root/error helpers
|
||||
- `src/infra/json-files.ts` for the temporary JSON compatibility layer
|
||||
- `src/infra/private-file-store.ts` until private stores are unified
|
||||
- `src/infra/replace-file.ts` for low-level atomic replacement
|
||||
- `src/infra/boundary-file-read.ts` for loader/package boundary reads
|
||||
- `src/infra/archive.ts` for archive extraction policy
|
||||
- `src/infra/file-lock-manager.ts` for the rare core service that needs
|
||||
manager-style lock lifecycle/diagnostics
|
||||
|
||||
New direct imports from `@openclaw/fs-safe/*` should be reserved for:
|
||||
|
||||
- package-level utilities outside core that cannot import `src/infra`
|
||||
- compatibility shims
|
||||
- code that intentionally consumes a narrow fs-safe subpath, such as
|
||||
`openclaw/plugin-sdk/file-lock` using `@openclaw/fs-safe/file-lock`
|
||||
|
||||
### Plugin SDK exports
|
||||
|
||||
Plugin SDK exports are contractual. Keep aliases even when OpenClaw internals
|
||||
move to canonical names.
|
||||
|
||||
Mark older names as deprecated in types/docs when the replacement is stable:
|
||||
|
||||
- `readJsonFileWithFallback` -> `readJsonIfExists` or a store method
|
||||
- `writeJsonFileAtomically` -> `writeJson`
|
||||
- `loadJsonFile` -> `tryReadJson`
|
||||
- `saveJsonFile` -> `writeJson`
|
||||
- `readFileWithinRoot` -> `root(...).read*`
|
||||
- `writeFileWithinRoot` -> `root(...).write`
|
||||
|
||||
### fs-safe stores
|
||||
|
||||
Move toward one store family:
|
||||
|
||||
```ts
|
||||
const store = fileStore({
|
||||
rootDir,
|
||||
private: true,
|
||||
mode: 0o600,
|
||||
dirMode: 0o700,
|
||||
});
|
||||
```
|
||||
|
||||
or a thin alias:
|
||||
|
||||
```ts
|
||||
const store = stateStore({ rootDir, private: true });
|
||||
```
|
||||
|
||||
The store family should cover:
|
||||
|
||||
- `read`
|
||||
- `readText`
|
||||
- `readJson`
|
||||
- `readTextIfExists`
|
||||
- `readJsonIfExists`
|
||||
- `write`
|
||||
- `writeJson`
|
||||
- `remove`
|
||||
- `exists`
|
||||
- `open`
|
||||
- `copyIn`
|
||||
- `writeStream`
|
||||
- `pruneExpired`
|
||||
|
||||
This cleanup added that store shape in fs-safe, removed the unshipped
|
||||
`privateStateStore` surface, and moved OpenClaw internals and bundled plugins
|
||||
onto explicit store reads/writes.
|
||||
|
||||
### Temp
|
||||
|
||||
Keep stable public temp surface small:
|
||||
|
||||
```ts
|
||||
await using workspace = await tempWorkspace({ prefix: "openclaw-" });
|
||||
const target = workspace.path("payload.bin");
|
||||
```
|
||||
|
||||
Move one-shot temp target helpers and sibling-temp helpers to advanced/internal
|
||||
unless a concrete OpenClaw caller needs the public contract.
|
||||
|
||||
## Refactor Phases
|
||||
|
||||
### Phase 1: Inventory and Guards
|
||||
|
||||
- Add a small import-boundary test that lists allowed direct
|
||||
`@openclaw/fs-safe/*` imports in OpenClaw core.
|
||||
- Add regression tests for the JSON symlink behavior kept by
|
||||
`src/infra/json-file.ts`.
|
||||
- Add regression tests for public plugin SDK aliases that must keep resolving.
|
||||
- Add a doc note to the plugin SDK runtime docs once aliases are marked
|
||||
deprecated.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- The current compatibility surface is executable-tested.
|
||||
- New direct fs-safe imports are visible in review.
|
||||
|
||||
### Phase 2: JSON Name Cleanup
|
||||
|
||||
- Convert OpenClaw internal callers from old JSON names to canonical fs-safe
|
||||
names where the semantics are identical.
|
||||
- Keep plugin SDK aliases unchanged.
|
||||
- Collapse `src/infra/json-file.ts` and `src/infra/json-files.ts` into one
|
||||
compatibility module if that reduces indirection without losing symlink
|
||||
semantics.
|
||||
- Keep `saveJsonFile` symlink-target behavior until every caller/test is
|
||||
intentionally migrated.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Core internal code no longer imports `readJsonFileStrict`,
|
||||
`readDurableJsonFile`, or `writeJsonAtomic` unless it is a compatibility shim.
|
||||
- Plugin SDK aliases still pass import/type tests.
|
||||
|
||||
### Phase 3: Store Unification
|
||||
|
||||
- Add the unified private mode to fs-safe's store API.
|
||||
- Remove the unshipped `privateStateStore` surface instead of keeping a second
|
||||
store family.
|
||||
- Migrate OpenClaw private-state internals to the unified store shape in small
|
||||
groups:
|
||||
- auth/profile state
|
||||
- device identity and device auth
|
||||
- cron/run logs
|
||||
- commitments
|
||||
- extension state
|
||||
- Regenerate the plugin SDK API baseline for the intentional pre-release
|
||||
private-helper removal.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- OpenClaw internals and bundled plugins do not call standalone private
|
||||
JSON/text helpers.
|
||||
- `fileStore({ private: true })` is the only private multi-file store API.
|
||||
|
||||
### Phase 4: Temp Simplification
|
||||
|
||||
- Replace OpenClaw one-shot temp target call sites with `tempWorkspace`.
|
||||
- Keep `resolvePreferredOpenClawTmpDir` as OpenClaw policy.
|
||||
- Move one-shot temp and sibling-temp helpers out of the curated OpenClaw
|
||||
wrapper surface.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- OpenClaw uses `tempWorkspace` for temporary file lifetimes unless a low-level
|
||||
atomic helper owns the temp path.
|
||||
|
||||
### Phase 5: Shim Reduction
|
||||
|
||||
- Group one-line fs-safe shims into a smaller number of named OpenClaw policy
|
||||
modules.
|
||||
- Delete shims that are no longer imported.
|
||||
- Keep shims that preserve public SDK names or OpenClaw-specific defaults.
|
||||
|
||||
Candidate stable shims:
|
||||
|
||||
- `src/infra/fs-safe.ts`
|
||||
- `src/infra/json-files.ts`
|
||||
- `src/infra/private-file-store.ts`
|
||||
- `src/infra/replace-file.ts`
|
||||
- `src/infra/boundary-file-read.ts`
|
||||
- `src/infra/archive.ts`
|
||||
|
||||
Candidate advanced-only grouping:
|
||||
|
||||
- path guards
|
||||
- symlink parent guards
|
||||
- hardlink guards
|
||||
- move-path helpers
|
||||
- file identity helpers
|
||||
- sibling temp helpers
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- The local wrapper list has policy meaning, not one file per fs-safe module.
|
||||
|
||||
### Phase 6: fs-safe Public Surface Finalization
|
||||
|
||||
- Keep `@openclaw/fs-safe` main entry curated.
|
||||
- Keep `root()` as the primary README/API story.
|
||||
- Keep `openPinnedFileSync` internal. Use `readSecureFile`, `root().open`, or
|
||||
`openRootFile*` wrappers instead of exposing the fd-level pinned primitive.
|
||||
- Keep `createSidecarLockManager` internal. Public callers should use
|
||||
`acquireFileLock` / `withFileLock`; `createFileLockManager` is subpath-only
|
||||
for long-lived services that need held-lock inspection or drain/reset.
|
||||
- Move rare root escape hatches such as `openWritable` to advanced only if API
|
||||
checks show no supported caller needs the main root interface.
|
||||
- Keep `regular-file`, `secure-file`, archive, and root helpers separate
|
||||
because their trust models differ.
|
||||
- Remove or mark unstable any standalone helper that is fully covered by root or
|
||||
store methods.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- fs-safe has a stable pre-1.0 public surface.
|
||||
- OpenClaw imports only stable fs-safe APIs outside compatibility shims.
|
||||
|
||||
## Verification
|
||||
|
||||
Use targeted proof per phase:
|
||||
|
||||
- JSON cleanup:
|
||||
- JSON symlink tests
|
||||
- plugin SDK JSON-store import tests
|
||||
- representative extension tests that use JSON store aliases
|
||||
- Store unification:
|
||||
- private mode tests in fs-safe
|
||||
- auth profile persistence tests
|
||||
- device identity tests
|
||||
- cron/run-log tests
|
||||
- Temp cleanup:
|
||||
- media temp tests
|
||||
- Discord voice temp tests
|
||||
- QA-lab temp helper tests
|
||||
- Shim reduction:
|
||||
- plugin SDK API generation/check
|
||||
- import-boundary tests
|
||||
- `pnpm build`
|
||||
|
||||
Before merging a broad cleanup batch, run the changed gate and build:
|
||||
|
||||
```sh
|
||||
pnpm check:changed
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Implementation proof from this cleanup:
|
||||
|
||||
- `pnpm test src/infra/fs-safe-import-boundary.test.ts src/plugin-sdk/temp-path.test.ts src/agents/models-config.write-serialization.test.ts src/infra/json-file.test.ts src/infra/json-files.test.ts`
|
||||
- `pnpm test src/infra/fs-safe-import-boundary.test.ts src/infra/device-auth-store.test.ts src/infra/device-identity.test.ts src/infra/exec-approvals.test.ts src/agents/models-config.write-serialization.test.ts src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts src/agents/harness/native-hook-relay.test.ts`
|
||||
- `pnpm test src/infra/fs-safe-import-boundary.test.ts src/infra/hardlink-guards.test.ts src/infra/file-identity.test.ts src/plugin-sdk/fs-safe-compat.test.ts src/plugin-sdk/temp-path.test.ts`
|
||||
- `pnpm plugin-sdk:api:check`
|
||||
- `pnpm build`
|
||||
- Blacksmith Testbox `pnpm install --frozen-lockfile --config.minimum-release-age=0 && pnpm check:changed`
|
||||
- In `../fs-safe`: `pnpm docs:site && pnpm build && pnpm test test/api-coverage.test.ts test/new-primitives.test.ts`
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Does this change reduce a public name, local wrapper, or duplicated semantic
|
||||
family?
|
||||
- Is the old name public plugin SDK surface? If yes, keep a deprecated alias.
|
||||
- Does the replacement preserve symlink, hardlink, mode, and missing-file
|
||||
behavior?
|
||||
- Is the caller using an untrusted relative path, trusted absolute path, secret
|
||||
path, archive entry, or temp lifetime? Pick the helper that says that out
|
||||
loud.
|
||||
- Are docs and plugin SDK API snapshots updated when exported names change?
|
||||
Reference in New Issue
Block a user