Skip to content

Security: harden gateway timeouts and auth store sealing#39059

Open
vincentkoc wants to merge 5 commits intomainfrom
vincentkoc-code/security-hardening-auth-timeouts
Open

Security: harden gateway timeouts and auth store sealing#39059
vincentkoc wants to merge 5 commits intomainfrom
vincentkoc-code/security-hardening-auth-timeouts

Conversation

@vincentkoc
Copy link
Member

Summary

  • Problem: GatewayClient.request() could hang indefinitely, auth stores were always plaintext at rest, and mirrored transcript writes did not re-assert restrictive permissions after append.
  • Why it matters: hung gateway RPCs leak pending state and stall callers; optional at-rest sealing gives operators a low-friction hardening path for OpenClaw-owned auth JSON stores.
  • What changed: added bounded gateway request timeouts, added optional OPENCLAW_PASSPHRASE sealing for auth-profiles.json and legacy oauth.json, added regression coverage, and re-applied 0600 perms after mirrored transcript writes.
  • What did NOT change (scope boundary): this PR does not implement full session transcript encryption because @mariozechner/pi-coding-agent and multiple local readers still depend on raw JSONL session files.
  • Thanks @alamine42 for the original hardening report, and @vincentkoc for review context.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • GatewayClient requests now fail with gateway request timeout for <method> instead of hanging forever.
  • When OPENCLAW_PASSPHRASE is set, OpenClaw-owned auth stores are written in sealed form and require that same env var to be read.
  • Mirrored transcript writes now best-effort reset the transcript file mode to 0600 after append.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) Yes
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:
    Optional auth-store sealing adds a new env-based operator hardening path. The format is backward compatible because plaintext reads still work when sealing is not enabled, and encrypted reads fail closed with a warning if the passphrase env var is missing.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: n/a
  • Integration/channel (if any): n/a
  • Relevant config (redacted): optional OPENCLAW_PASSPHRASE

Steps

  1. Run the focused test suite for gateway client, auth store, oauth migration, and transcript paths.
  2. Run repo-level pnpm check.
  3. Run repo-level pnpm build.

Expected

  • Gateway requests time out cleanly.
  • Auth stores round-trip in sealed form when OPENCLAW_PASSPHRASE is set.
  • Build and lint stay green.

Actual

  • Verified locally.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: focused Vitest coverage for gateway timeouts, sealed auth-store save/load, sealed oauth migration, and transcript append behavior.
  • Edge cases checked: timeout cleanup removes pending entries; encrypted auth stores do not contain plaintext secrets; legacy oauth migration still works when the source file is sealed.
  • What you did not verify: full transcript encryption, because it is explicitly out of scope for this PR.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) Yes
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:
    Operators who want auth-store sealing can set OPENCLAW_PASSPHRASE before writing credentials and keep it set for future reads.

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: unset OPENCLAW_PASSPHRASE before writing new auth store files, or revert the branch.
  • Files/config to restore: auth-profiles.json / oauth.json from backup if an operator loses the passphrase.
  • Known bad symptoms reviewers should watch for: warning that an encrypted auth store requires OPENCLAW_PASSPHRASE when the env var is missing.

Risks and Mitigations

  • Risk: operator enables auth-store sealing and later starts OpenClaw without OPENCLAW_PASSPHRASE.
    • Mitigation: reads fail closed with a targeted warning instead of silently parsing garbage.
  • Risk: timeout defaults could surface latent long-running RPC assumptions.
    • Mitigation: timeout is bounded but configurable per client/request.

@vincentkoc vincentkoc self-assigned this Mar 7, 2026
@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime agents Agent runtime and tooling size: M maintainer Maintainer-authored PR labels Mar 7, 2026
@vincentkoc vincentkoc marked this pull request as ready for review March 7, 2026 17:48
@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 7, 2026

🔒 Aisle Security Analysis

We found 4 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Encrypted auth store can be overwritten in plaintext when OPENCLAW_PASSPHRASE is missing
2 🟡 Medium Sealed JSON encryption uses implicit scrypt defaults (weak/unstable KDF parameters) and omits KDF metadata
3 🟡 Medium Auth store written with insecure permissions window (chmod after write)
4 🟡 Medium Symlink/hardlink TOCTOU when writing sealed JSON files (arbitrary file clobber/chmod)

1. 🟡 Encrypted auth store can be overwritten in plaintext when OPENCLAW_PASSPHRASE is missing

Property Value
Severity Medium
CWE CWE-312
Location src/infra/sealed-json-file.ts:111-114

Description

saveSealedJsonFile() will write plaintext JSON whenever OPENCLAW_PASSPHRASE is not set. Combined with the new auth store loading behavior (which swallows SealedJsonPassphraseRequiredError and continues with an empty/coerced store), this enables a downgrade-to-plaintext and data loss scenario:

  • If auth-profiles.json (or oauth.json) is sealed and OPENCLAW_PASSPHRASE is missing, loadSealedJsonFile() throws SealedJsonPassphraseRequiredError.
  • loadProtectedAuthJson() catches that error and returns undefined, causing the code to proceed as if no store exists.
  • Later code paths (e.g., external CLI sync, legacy migration, OAuth merge, or any updater that persists) call saveSealedJsonFile().
  • With no passphrase, saveSealedJsonFile() writes the new store in plaintext, overwriting the previously encrypted file (destroying the encrypted data) and potentially persisting newly merged credentials/tokens unencrypted.

Concrete reproduction (one example):

  1. Create a sealed auth-profiles.json by running the app once with OPENCLAW_PASSPHRASE set.
  2. Unset OPENCLAW_PASSPHRASE.
  3. Ensure a write-trigger occurs, e.g.:
    • external CLI credentials are present so syncExternalCliCredentials() returns true, or
    • a legacy auth.json exists (migration), or
    • a plaintext oauth.json exists (merge), or
    • any command that updates the store via saveAuthProfileStore() / upsert...WithLock().
  4. Run the app/load path that persists; observe auth-profiles.json becomes plaintext JSON (no sealed prefix).

Vulnerable code:

const plaintext = `${JSON.stringify(data, null, 2)}\n`;
const passphrase = resolvePassphrase(env);
fs.writeFileSync(pathname, passphrase ? sealUtf8(plaintext, passphrase) : plaintext, "utf8");

Recommendation

Prevent silent downgrade/overwrite when an encrypted file exists but the passphrase is missing.

Recommended hardening in saveSealedJsonFile():

  • If OPENCLAW_PASSPHRASE is missing and the destination file already exists and is sealed, refuse to write (throw a SealedJsonPassphraseRequiredError).
  • Optionally, add a separate explicit “allow plaintext write” flag for non-secret uses.

Example fix:

export function saveSealedJsonFile(pathname: string, data: unknown, env: NodeJS.ProcessEnv = process.env): void {
  const dir = path.dirname(pathname);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });

  const passphrase = resolvePassphrase(env);

  if (!passphrase && fs.existsSync(pathname)) {
    const existingRaw = fs.readFileSync(pathname, "utf8");
    if (isSealedJsonText(existingRaw)) {// Do not overwrite encrypted content with plaintext.
      throw new SealedJsonPassphraseRequiredError(pathname);
    }
  }

  const plaintext = `${JSON.stringify(data, null, 2)}\n`;
  fs.writeFileSync(pathname, passphrase ? sealUtf8(plaintext, passphrase) : plaintext, "utf8");
  fs.chmodSync(pathname, 0o600);
}

Additionally (defense in depth): in auth-profiles/store.ts, when SealedJsonPassphraseRequiredError occurs, consider failing the operation or forcing readOnly/disabling any subsequent persistence for that run.


2. 🟡 Sealed JSON encryption uses implicit scrypt defaults (weak/unstable KDF parameters) and omits KDF metadata

Property Value
Severity Medium
CWE CWE-326
Location src/infra/sealed-json-file.ts:38-40

Description

The sealed JSON file format derives the AES-256-GCM key from OPENCLAW_PASSPHRASE using scryptSync() without specifying work factors and without recording KDF parameters in the envelope.

Impacts:

  • Offline brute-force risk: if an attacker obtains the sealed file, security depends heavily on passphrase entropy. Node’s default scrypt parameters may be insufficient against GPU/ASIC guessing when operators choose weak env-var passphrases.
  • Future decryptability/upgrade risk: because KDF parameters are not stored, any future change in Node’s scrypt defaults (or a desire to tune them) can break decryption compatibility or silently weaken/strengthen encryption across versions.

Vulnerable code:

function deriveKey(passphrase: string, salt: Buffer): Buffer {
  return scryptSync(passphrase, salt, 32);
}

Recommendation

Make KDF parameters explicit and store them in the envelope so encryption strength and decryptability are stable across releases.

Example:

type SealedJsonEnvelopeV1 = {
  v: 1;
  alg: "aes-256-gcm";
  kdf: { name: "scrypt"; N: number; r: number; p: number };
  salt: string;
  iv: string;
  tag: string;
  ciphertext: string;
};

const SCRYPT_PARAMS = { N: 1 << 18, r: 8, p: 1 } as const; // tune for your perf budget

function deriveKey(passphrase: string, salt: Buffer, kdf = SCRYPT_PARAMS): Buffer {
  return scryptSync(passphrase, salt, 32, kdf);
}// when sealing:// kdf: { name: "scrypt", ...SCRYPT_PARAMS }// when unsealing:// validate envelope.kdf and pass into deriveKey(...)

Additionally:

  • Enforce a minimum passphrase length/entropy (or require a randomly generated key) to reduce real-world brute-force risk.
  • Consider allowing operators to configure work factors (while still writing the chosen factors into the envelope).

3. 🟡 Auth store written with insecure permissions window (chmod after write)

Property Value
Severity Medium
CWE CWE-732
Location src/infra/sealed-json-file.ts:111-114

Description

saveSealedJsonFile() writes sensitive auth material (API keys / OAuth refresh tokens) to disk using fs.writeFileSync() and only then applies chmod(0o600).

On POSIX systems, newly-created files are typically created with mode 0o666 & ~umask (often 0644). This creates a TOCTOU window where another local user on the same host (who can traverse the directory, e.g. if the state dir is under a 0755 home directory or an overridden shared OPENCLAW_STATE_DIR/OPENCLAW_AGENT_DIR) can open/read the file before permissions are restricted.

Vulnerable code:

fs.writeFileSync(pathname, passphrase ? sealUtf8(plaintext, passphrase) : plaintext, "utf8");
fs.chmodSync(pathname, 0o600);

Recommendation

Write with secure permissions at creation time and do an atomic temp-file + rename update.

You already have an atomic helper (writeTextAtomic) that best-effort applies chmod and uses rename:

import { writeTextAtomic } from "./json-files.js";

export function saveSealedJsonFile(pathname: string, data: unknown, env: NodeJS.ProcessEnv = process.env): void {
  const plaintext = `${JSON.stringify(data, null, 2)}\n`;
  const passphrase = resolvePassphrase(env);
  const payload = passphrase ? sealUtf8(plaintext, passphrase) : plaintext;// write with mode=0o600 and atomic rename// (ensureDirMode should be 0o700)
  await writeTextAtomic(pathname, payload, { mode: 0o600, ensureDirMode: 0o700, appendTrailingNewline: false });
}

If you must keep sync APIs, use fs.openSync() with an explicit mode and write to the returned FD (or implement a sync temp+rename path) so there is no world-readable intermediate state.


4. 🟡 Symlink/hardlink TOCTOU when writing sealed JSON files (arbitrary file clobber/chmod)

Property Value
Severity Medium
CWE CWE-61
Location src/infra/sealed-json-file.ts:107-115

Description

saveSealedJsonFile() performs non-atomic filesystem operations (existsSync/mkdirSync, writeFileSync, then chmodSync) on a caller-provided pathname without any protection against symlink/hardlink attacks.

If an attacker can influence either:

  • the pathname itself (via env overrides such as OPENCLAW_AGENT_DIR, OPENCLAW_STATE_DIR, OPENCLAW_OAUTH_DIR), or
  • the directory contents (e.g., they can create/replace the target file with a symlink between the checks and the write),

then they can cause OpenClaw to:

  • overwrite an arbitrary file that the OpenClaw process has permission to write (via symlink followed by writeFileSync), and/or
  • change permissions of an arbitrary file to 0600 (via symlink followed by chmodSync).

This is particularly risky if OpenClaw is run with elevated privileges or in shared directories, and it is realistic in multi-user systems when the state dir location is overridden to a world-writable location (e.g., /tmp/...).

Vulnerable code:

if (!fs.existsSync(dir)) {
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
fs.writeFileSync(pathname, payload, "utf8");
fs.chmodSync(pathname, 0o600);

loadSealedJsonFile() similarly uses existsSync + readFileSync without validating that the file is a regular file (not a symlink), enabling an attacker who can swap in a symlink to redirect reads to other files (less severe but still a confused-deputy / integrity concern).

Recommendation

Harden against symlink/hardlink attacks and TOCTOU:

  • Prefer an atomic temp-file + rename approach inside the same directory.
  • When opening/writing, use fs.open/fs.openSync with flags that avoid following symlinks where available.
    • On Linux, you can use O_NOFOLLOW via fs.constants.O_NOFOLLOW.
  • Validate the target is a regular file using lstat/fstat.
  • For directory creation, consider validating that each path component is a directory and not a symlink.

Example (conceptual; platform-conditional):

import { constants as c } from "node:fs";

const fd = fs.openSync(pathname, c.O_WRONLY | c.O_CREAT | c.O_TRUNC | (c.O_NOFOLLOW ?? 0), 0o600);
try {
  fs.writeFileSync(fd, payload, "utf8");
  fs.fsyncSync(fd);
} finally {
  fs.closeSync(fd);
}

Even better, reuse the existing atomic writer (writeTextAtomic) but extend it to prevent symlink following (e.g., write to a newly-created temp file with mode 0600 and then rename over an existing file only after verifying it is not a symlink). Also, wrap chmodSync in a try/catch to avoid crashes on platforms where chmod is unsupported.


Analyzed PR: #39059 at commit 9761a71

Last updated on: 2026-03-07T18:01:35Z

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR adds three security hardening measures: bounded GatewayClient request timeouts (30 s default, clamped to [1 ms, 300 s]), optional AES-256-GCM at-rest encryption for auth-profiles.json / oauth.json gated on OPENCLAW_PASSPHRASE, and a best-effort chmod(0o600) re-assertion after mirrored transcript writes.

The gateway timeout and transcript permission changes are well-implemented. The sealed-JSON infrastructure is cryptographically sound, but two correctness issues were found in how the new sealed reader is wired into the auth-store load paths:

  • Subagent auth inheritance broken when passphrase is set (store.ts line 412): the subagent fallback path still calls loadJsonFile(mainAuthPath) rather than loadProtectedAuthJson. Because loadJsonFile cannot parse sealed content, subagents silently fail to inherit credentials from the main store whenever OPENCLAW_PASSPHRASE is configured.
  • Sealed store silently downgraded to plaintext (store.ts lines 435–438): if OpenClaw starts without OPENCLAW_PASSPHRASE while a sealed store exists, loadCoercedStore returns null and the legacy/OAuth/CLI migration write path triggers with no passphrase, overwriting the sealed file in plaintext — contrary to the PR's "fail-closed" claim.
  • The scryptSync call in sealed-json-file.ts uses implicit Node.js defaults (N=16384); explicitly documenting the cost parameters would improve auditability.

Confidence Score: 2/5

  • Not safe to merge as-is due to a subagent auth regression and a sealed-store downgrade path introduced by the PR.
  • The gateway timeout and transcript permission changes are correct and well-tested. However, the sealed auth-store integration has two logic bugs: subagent credential inheritance silently breaks when OPENCLAW_PASSPHRASE is set (wrong reader still used on line 412), and a write-path can silently convert a sealed store back to plaintext when the passphrase is absent at startup. Both issues undermine the security guarantees the PR is trying to provide.
  • src/agents/auth-profiles/store.ts — two separate correctness/security issues in the sealed store read and write paths.

Comments Outside Diff (1)

  1. src/agents/auth-profiles/store.ts, line 412-416 (link)

    Subagent auth inheritance silently fails when passphrase is set

    loadJsonFile is used here to read the main store for subagent inheritance, but this PR now writes the main store in sealed format when OPENCLAW_PASSPHRASE is set. loadJsonFile will attempt to parse the sealed prefix string (openclaw-sealed-json-v1:{...}) as raw JSON, fail, and return null, causing coerceAuthStore(null) to return null — so the inheritance block is silently skipped every time a passphrase is configured.

    The fix is to use loadProtectedAuthJson (introduced in this same PR) instead:

    const mainRaw = loadProtectedAuthJson(mainAuthPath, "auth-profiles.json");
    

    This brings the subagent inheritance read path in line with all the other store read paths changed in this PR.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/auth-profiles/store.ts
    Line: 412-416
    
    Comment:
    **Subagent auth inheritance silently fails when passphrase is set**
    
    `loadJsonFile` is used here to read the main store for subagent inheritance, but this PR now writes the main store in sealed format when `OPENCLAW_PASSPHRASE` is set. `loadJsonFile` will attempt to parse the sealed prefix string (`openclaw-sealed-json-v1:{...}`) as raw JSON, fail, and return `null`, causing `coerceAuthStore(null)` to return `null` — so the inheritance block is silently skipped every time a passphrase is configured.
    
    The fix is to use `loadProtectedAuthJson` (introduced in this same PR) instead:
    
    ```
    const mainRaw = loadProtectedAuthJson(mainAuthPath, "auth-profiles.json");
    ```
    
    This brings the subagent inheritance read path in line with all the other store read paths changed in this PR.
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 9761a71

Comment on lines 435 to 439
const forceReadOnly = process.env.OPENCLAW_AUTH_STORE_READONLY === "1";
const shouldWrite = !readOnly && !forceReadOnly && (legacy !== null || mergedOAuth || syncedCli);
if (shouldWrite) {
saveJsonFile(authPath, store);
saveSealedJsonFile(authPath, store);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sealed store can be silently downgraded to plaintext

When OPENCLAW_PASSPHRASE is not set but a sealed auth-profiles.json already exists, loadCoercedStore returns null (the SealedJsonPassphraseRequiredError is caught and undefined is returned). The code then falls through to the legacy/oauth/external-CLI migration path. If any of legacy !== null, mergedOAuth, or syncedCli is true, shouldWrite becomes true and saveSealedJsonFile is called without a passphrase — writing plaintext over the sealed file.

Concrete scenario:

  1. Operator sets OPENCLAW_PASSPHRASE, runs once — credentials are sealed.
  2. Operator later starts OpenClaw without OPENCLAW_PASSPHRASE (e.g., a deploy that forgot the env var).
  3. oauth.json or external CLI credentials exist → mergedOAuth/syncedCli is true.
  4. The sealed auth-profiles.json is overwritten with plaintext, with no warning.

The PR description states "reads fail closed with a warning if the passphrase env var is missing" but this write path silently removes the sealing. A guard is needed before writing: if the file already contains sealed content and no passphrase is available, the write should be skipped (or at least an explicit warning logged).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/auth-profiles/store.ts
Line: 435-439

Comment:
**Sealed store can be silently downgraded to plaintext**

When `OPENCLAW_PASSPHRASE` is not set but a sealed `auth-profiles.json` already exists, `loadCoercedStore` returns `null` (the `SealedJsonPassphraseRequiredError` is caught and `undefined` is returned). The code then falls through to the legacy/oauth/external-CLI migration path. If any of `legacy !== null`, `mergedOAuth`, or `syncedCli` is true, `shouldWrite` becomes `true` and `saveSealedJsonFile` is called without a passphrase — writing **plaintext** over the sealed file.

Concrete scenario:
1. Operator sets `OPENCLAW_PASSPHRASE`, runs once — credentials are sealed.
2. Operator later starts OpenClaw without `OPENCLAW_PASSPHRASE` (e.g., a deploy that forgot the env var).
3. `oauth.json` or external CLI credentials exist → `mergedOAuth`/`syncedCli` is `true`.
4. The sealed `auth-profiles.json` is overwritten with plaintext, with no warning.

The PR description states *"reads fail closed with a warning if the passphrase env var is missing"* but this write path silently removes the sealing. A guard is needed before writing: if the file already contains sealed content and no passphrase is available, the write should be skipped (or at least an explicit warning logged).

How can I resolve this? If you propose a fix, please make it concise.

}

function deriveKey(passphrase: string, salt: Buffer): Buffer {
return scryptSync(passphrase, salt, 32);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrypt cost parameters not explicitly set

scryptSync is called with only the key length — no options object is passed. Node.js defaults to N=16384, r=8, p=1 which is modest. Explicitly documenting (and potentially raising) the cost parameters would make the security posture clearer and easier to audit or tune later:

Suggested change
return scryptSync(passphrase, salt, 32);
return scryptSync(passphrase, salt, 32, { N: 65536, r: 8, p: 1 });

N=65536 is a more conservative baseline for at-rest credential encryption, and having the values in code makes future tuning (and a version bump) straightforward.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/sealed-json-file.ts
Line: 39

Comment:
**scrypt cost parameters not explicitly set**

`scryptSync` is called with only the key length — no `options` object is passed. Node.js defaults to `N=16384, r=8, p=1` which is modest. Explicitly documenting (and potentially raising) the cost parameters would make the security posture clearer and easier to audit or tune later:

```suggestion
  return scryptSync(passphrase, salt, 32, { N: 65536, r: 8, p: 1 });
```

`N=65536` is a more conservative baseline for at-rest credential encryption, and having the values in code makes future tuning (and a version bump) straightforward.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9761a71a5a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 436 to +438
const shouldWrite = !readOnly && !forceReadOnly && (legacy !== null || mergedOAuth || syncedCli);
if (shouldWrite) {
saveJsonFile(authPath, store);
saveSealedJsonFile(authPath, store);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent overwrite when encrypted store cannot be decrypted

When loadCoercedStore returns null because auth-profiles.json is sealed and OPENCLAW_PASSPHRASE is missing/invalid, this path can still set shouldWrite via mergeOAuthFileIntoStore/syncExternalCliCredentials and immediately persist store back to authPath; with no passphrase set, saveSealedJsonFile writes plaintext, so a valid encrypted store can be silently replaced with partial plaintext credentials. This breaks the intended fail-closed behavior and can cause credential loss/leakage in environments that still have oauth.json or external CLI creds.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling gateway Gateway runtime maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: GatewayClient.request() has no timeout, causing indefinite hangs

1 participant