Skip to content

feat(gateway): persist tool approval decisions to audit log#72769

Open
chinadbo wants to merge 3 commits intoopenclaw:mainfrom
chinadbo:feat/tool-approval-audit-log
Open

feat(gateway): persist tool approval decisions to audit log#72769
chinadbo wants to merge 3 commits intoopenclaw:mainfrom
chinadbo:feat/tool-approval-audit-log

Conversation

@chinadbo
Copy link
Copy Markdown

Summary

  • Tool approval decisions (approved/denied) are now persisted to {stateDir}/audit/approvals.jsonl as append-only JSONL
  • Each entry captures: timestamp, approval ID, command text, decision, resolver identity (deviceId/clientId/connId), agentId, sessionKey
  • Adds exec.approval.auditLog gateway method (READ scope) for querying the log with optional since, limit, and agentId filters
  • Follows the existing audit log pattern from src/config/io.audit.ts
  • In-memory approval tracking behavior unchanged; audit log is additive
  • Sensitive argument values (keys matching key/token/password/secret/credential/auth/apikey) are redacted in persisted entries

Test plan

  • Approving a tool call writes an entry to approvals.jsonl
  • Denying a tool call writes an entry with decision: "denied"
  • exec.approval.auditLog returns persisted entries across gateway restarts
  • agentId, since, and limit filters work correctly
  • pnpm check:changed passes

Thanks @ioodu

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: M labels Apr 27, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR adds an append-only JSONL audit log for tool approval decisions and a new exec.approval.auditLog gateway method to query it. The core mechanism is correct, but the secret redaction does not protect commandArgv values because the redaction logic only handles plain objects and silently passes arrays through unchanged — meaning CLI arguments containing tokens or API keys are written to disk in plaintext.

Confidence Score: 3/5

Not safe to merge as-is: the advertised secret redaction is ineffective for the actual arg type used, causing credential leakage to disk.

One P1 security finding (secrets in commandArgv bypass redaction) blocks the merge. Two P2 findings (blocking sync read in async handler, unreachable error label) are non-blocking but worth addressing.

src/infra/exec-approval-audit.ts — redaction logic and synchronous file read both need attention.

Security Review

  • Secret leakage in audit log (src/infra/exec-approval-audit.ts): commandArgv is a string[] and bypasses redactSecretArgs entirely (the function returns arrays unmodified). Secrets passed as CLI arguments are persisted in plaintext to approvals.jsonl.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/exec-approval-audit.ts
Line: 40-47

Comment:
**Secret redaction does not apply to `commandArgv`**

`redactSecretArgs` short-circuits and returns the input unchanged when it receives an array (`Array.isArray(args)` check on line 42). At the call site, `args` is set to `resolvedEvent.request?.commandArgv`, which is typed `string[]`. Any CLI arguments carrying bearer tokens or API keys are therefore written to disk in plaintext. The PR description states that sensitive values are redacted, but because `commandArgv` is always an array rather than a plain object, no redaction ever occurs in practice.

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

---

This is a comment left during a code review.
Path: src/infra/exec-approval-audit.ts
Line: 75-110

Comment:
**Entire file read into memory on every query**

`readFileSync` loads the full `approvals.jsonl` into a single string before any filtering. Combined with the `limit` slice operating on the already-filtered `entries` array, both the I/O and the in-process filtering scale linearly with total file size. For a long-lived gateway instance with many tool calls, a single `exec.approval.auditLog` RPC will block the Node.js event loop for an unbounded duration. The existing `io.audit.ts` pattern uses `fs.promises` for async writes; a streaming line-by-line read (or at minimum an async `readFile`) would avoid blocking.

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

---

This is a comment left during a code review.
Path: src/gateway/server-methods/exec-approval.ts
Line: 407-418

Comment:
**`doAppend` is fire-and-forget; `errorLabel` is unreachable**

`appendApprovalAuditEntry` is synchronous and swallows all errors internally via `try/catch`. The `run` callback therefore never throws, so the `errorLabel: "exec approvals: audit log write failed"` will never be surfaced through whatever error-handling pipeline `extraResolvedHandlers` uses. If audit write failures need to be observable (e.g. via logging/metrics), `appendApprovalAuditEntry` would need to propagate the error rather than absorbing it, or the swallowing should be documented as intentional with a note that `errorLabel` is vestigial here.

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

Reviews (1): Last reviewed commit: "feat(gateway): persist tool approval dec..." | Re-trigger Greptile

Comment on lines +40 to +47
for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
result[key] = SECRET_KEY_PATTERNS.test(key) ? "[REDACTED]" : value;
}
return result;
}

export function appendApprovalAuditEntry(
entry: ApprovalAuditEntry,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Secret redaction does not apply to commandArgv

redactSecretArgs short-circuits and returns the input unchanged when it receives an array (Array.isArray(args) check on line 42). At the call site, args is set to resolvedEvent.request?.commandArgv, which is typed string[]. Any CLI arguments carrying bearer tokens or API keys are therefore written to disk in plaintext. The PR description states that sensitive values are redacted, but because commandArgv is always an array rather than a plain object, no redaction ever occurs in practice.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approval-audit.ts
Line: 40-47

Comment:
**Secret redaction does not apply to `commandArgv`**

`redactSecretArgs` short-circuits and returns the input unchanged when it receives an array (`Array.isArray(args)` check on line 42). At the call site, `args` is set to `resolvedEvent.request?.commandArgv`, which is typed `string[]`. Any CLI arguments carrying bearer tokens or API keys are therefore written to disk in plaintext. The PR description states that sensitive values are redacted, but because `commandArgv` is always an array rather than a plain object, no redaction ever occurs in practice.

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

Comment on lines +75 to +110
env: NodeJS.ProcessEnv = process.env,
homedir: () => string = () => os.homedir(),
): ApprovalAuditEntry[] {
const auditPath = resolveApprovalAuditLogPath(env, homedir);
let raw: string;
try {
raw = fs.readFileSync(auditPath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw err;
}

const lines = raw.split("\n").filter((line) => line.trim().length > 0);
const entries: ApprovalAuditEntry[] = [];
for (const line of lines) {
try {
const entry = JSON.parse(line) as ApprovalAuditEntry;
if (opts.since !== undefined && entry.ts < opts.since) {
continue;
}
if (opts.agentId !== undefined && entry.agentId !== opts.agentId) {
continue;
}
entries.push(entry);
} catch {
// skip malformed lines
}
}

if (opts.limit !== undefined && opts.limit > 0 && entries.length > opts.limit) {
return entries.slice(entries.length - opts.limit);
}
return entries;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Entire file read into memory on every query

readFileSync loads the full approvals.jsonl into a single string before any filtering. Combined with the limit slice operating on the already-filtered entries array, both the I/O and the in-process filtering scale linearly with total file size. For a long-lived gateway instance with many tool calls, a single exec.approval.auditLog RPC will block the Node.js event loop for an unbounded duration. The existing io.audit.ts pattern uses fs.promises for async writes; a streaming line-by-line read (or at minimum an async readFile) would avoid blocking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approval-audit.ts
Line: 75-110

Comment:
**Entire file read into memory on every query**

`readFileSync` loads the full `approvals.jsonl` into a single string before any filtering. Combined with the `limit` slice operating on the already-filtered `entries` array, both the I/O and the in-process filtering scale linearly with total file size. For a long-lived gateway instance with many tool calls, a single `exec.approval.auditLog` RPC will block the Node.js event loop for an unbounded duration. The existing `io.audit.ts` pattern uses `fs.promises` for async writes; a streaming line-by-line read (or at minimum an async `readFile`) would avoid blocking.

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

Comment on lines +407 to +418
command: resolvedEvent.request?.command ?? "",
decision: auditDecision,
resolvedBy: {
deviceId: client?.connect?.device?.id ?? null,
clientId: client?.connect?.client?.id ?? null,
connId: client?.connId ?? null,
},
agentId: resolvedEvent.request?.agentId ?? null,
sessionKey: resolvedEvent.request?.sessionKey ?? null,
args: resolvedEvent.request?.commandArgv,
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 doAppend is fire-and-forget; errorLabel is unreachable

appendApprovalAuditEntry is synchronous and swallows all errors internally via try/catch. The run callback therefore never throws, so the errorLabel: "exec approvals: audit log write failed" will never be surfaced through whatever error-handling pipeline extraResolvedHandlers uses. If audit write failures need to be observable (e.g. via logging/metrics), appendApprovalAuditEntry would need to propagate the error rather than absorbing it, or the swallowing should be documented as intentional with a note that errorLabel is vestigial here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/exec-approval.ts
Line: 407-418

Comment:
**`doAppend` is fire-and-forget; `errorLabel` is unreachable**

`appendApprovalAuditEntry` is synchronous and swallows all errors internally via `try/catch`. The `run` callback therefore never throws, so the `errorLabel: "exec approvals: audit log write failed"` will never be surfaced through whatever error-handling pipeline `extraResolvedHandlers` uses. If audit write failures need to be observable (e.g. via logging/metrics), `appendApprovalAuditEntry` would need to propagate the error rather than absorbing it, or the swallowing should be documented as intentional with a note that `errorLabel` is vestigial here.

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

@chinadbo
Copy link
Copy Markdown
Author

The failing CI checks (checks-fast-contracts-plugins, checks-node-agentic-agents, checks-node-core) are pre-existing failures on main unrelated to this PR:

  • checks-fast-contracts-plugins: extensions/qwen/vllm missing @mariozechner/pi-ai runtime dep declaration
  • checks-node-agentic-agents: model-auth.test.ts assertion failure (custom-local vs ollama-local) — pre-existing test flake
  • checks-node-core: pre-existing

@chinadbo chinadbo force-pushed the feat/tool-approval-audit-log branch from 33867e6 to 48c769d Compare April 27, 2026 12:47
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord extensions: memory-core Extension: memory-core commands Command implementations size: L and removed size: M labels Apr 27, 2026
@chinadbo chinadbo force-pushed the feat/tool-approval-audit-log branch from a670cec to 9c9c58f Compare April 27, 2026 13:34
@openclaw-barnacle openclaw-barnacle Bot added size: M and removed channel: discord Channel integration: discord extensions: memory-core Extension: memory-core commands Command implementations size: L labels Apr 27, 2026
@chinadbo chinadbo force-pushed the feat/tool-approval-audit-log branch 2 times, most recently from 7727bb0 to 88b8407 Compare April 27, 2026 14:20
@chinadbo
Copy link
Copy Markdown
Author

Addressed Greptile review:

  • ✅ Fixed commandArgv redaction: redactSecretArgs now handles arrays by scanning for KEY=value patterns and redacting values whose key matches the secret pattern (e.g. SECRET_KEY=abc123SECRET_KEY=[REDACTED])

邓波 and others added 2 commits April 28, 2026 11:13
Append approved/denied decisions to {stateDir}/audit/approvals.jsonl.
Adds exec.approval.auditLog gateway method (READ scope) for querying
with since/limit/agentId filters. Args with secret-looking keys are
redacted before writing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chinadbo chinadbo force-pushed the feat/tool-approval-audit-log branch from a286311 to 0cfe443 Compare April 28, 2026 03:13
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

Codex review: found issues before merge.

Summary
The PR adds an append-only approval-decision JSONL audit helper, writes exec approval resolve events to it, registers an exec.approval.auditLog RPC, and exports params validation.

Reproducibility: yes. A focused branch check can assert authorizeOperatorScopesForMethod("exec.approval.auditLog", ["operator.read"]) allows access, and unit coverage around appendApprovalAuditEntry can show --token secret, --password=secret, lowercase token=secret, bearer headers, and raw token-shaped argv values are not consistently masked.

Next step before merge
The PR is active and the remaining blockers are security-sensitive access-control and secret-persistence issues, so the source branch should be revised by the author or maintainer rather than queued as an autonomous replacement repair.

Security
Needs attention: The diff introduces concrete access-control, secret-redaction, and availability concerns in a new persisted approval audit surface.

Review findings

  • [P1] Gate audit-log reads with approval scope — src/gateway/method-scopes.ts:109
  • [P1] Reuse the shared argv redaction path — src/infra/exec-approval-audit.ts:36-43
  • [P2] Avoid blocking the gateway on audit reads — src/infra/exec-approval-audit.ts:90
Review details

Best possible solution:

Keep the feature direction, but revise the branch so approval history is approval-scoped, argv persistence uses the shared redaction contract, audit reads are async and bounded, protocol artifacts/docs are synchronized, and the user-facing change has a changelog entry.

Do we have a high-confidence way to reproduce the issue?

Yes. A focused branch check can assert authorizeOperatorScopesForMethod("exec.approval.auditLog", ["operator.read"]) allows access, and unit coverage around appendApprovalAuditEntry can show --token secret, --password=secret, lowercase token=secret, bearer headers, and raw token-shaped argv values are not consistently masked.

Is this the best way to solve the issue?

No. Persisting approval decisions is a reasonable additive gateway feature, but this implementation is not the narrowest maintainable solution until it uses the existing approval scope, shared redaction behavior, bounded async reads, and the full protocol update path.

Full review comments:

  • [P1] Gate audit-log reads with approval scope — src/gateway/method-scopes.ts:109
    This registers exec.approval.auditLog under operator.read, while live exec approval methods require operator.approvals. The returned history includes command text, args, agent/session IDs, and resolver identity, so read-only operator tokens would gain access to approval data they cannot see live.
    Confidence: 0.91
  • [P1] Reuse the shared argv redaction path — src/infra/exec-approval-audit.ts:36-43
    The array redactor only masks uppercase env-var style KEY=value entries. Common argv forms such as --token secret, --password=secret, lowercase token=secret, bearer headers, and raw token-shaped values can still be written to approvals.jsonl; reuse the existing audit/tool redaction helper or equivalent coverage before persisting argv.
    Confidence: 0.88
  • [P2] Avoid blocking the gateway on audit reads — src/infra/exec-approval-audit.ts:90
    readFileSync loads and parses the complete append-only audit file before applying limit. A long-lived gateway can accumulate a large file, and one exec.approval.auditLog request would block the Node event loop; use an async bounded or streaming read path instead.
    Confidence: 0.84
  • [P2] Register the audit method in generated protocol artifacts — src/gateway/protocol/schema/exec-approvals.ts:156-163
    Adding ExecApprovalAuditLogParamsSchema here and exporting a validator is not enough for a public gateway RPC. The new params/result surface is missing from the protocol schema registry, generated schema types, generated Swift models, and protocol docs, so generated clients will not learn the new contract.
    Confidence: 0.78
  • [P3] Add the required changelog entry — src/gateway/server-methods/exec-approval.ts:424
    This adds a user-facing gateway audit log and RPC, but the PR does not update CHANGELOG.md. Repo policy requires user-facing feat changes to include a changelog entry before merge.
    Confidence: 0.82

Overall correctness: patch is incorrect
Overall confidence: 0.89

Security concerns:

  • [high] Approval audit exposed to read-only tokens — src/gateway/method-scopes.ts:109
    The new audit-log RPC is registered under operator.read even though live exec approval methods and events are approval-scoped; this broadens access to historical approval command/session/resolver data.
    Confidence: 0.91
  • [high] CLI argv secrets can persist in plaintext — src/infra/exec-approval-audit.ts:36
    The custom redactor only masks uppercase env-var style array entries, leaving option-style, lowercase, bearer, and raw token-shaped argv values unmasked in the persisted JSONL file.
    Confidence: 0.88
  • [medium] Audit-log reads can block the gateway event loop — src/infra/exec-approval-audit.ts:90
    The query helper synchronously reads and parses the complete append-only file for each RPC, creating an availability risk as the audit log grows.
    Confidence: 0.84

Acceptance criteria:

  • pnpm test src/gateway/method-scopes.test.ts src/gateway/server-methods/server-methods.test.ts src/gateway/protocol/exec-approvals-validators.test.ts
  • pnpm protocol:gen
  • pnpm protocol:gen:swift
  • pnpm exec oxfmt --check --threads=1 src/gateway/method-scopes.ts src/gateway/server-methods/exec-approval.ts src/infra/exec-approval-audit.ts src/gateway/protocol/schema/exec-approvals.ts docs/gateway/protocol.md CHANGELOG.md

What I checked:

  • PR adds approval audit RPC to read scope: The PR head registers exec.approval.auditLog under the operator.read method group, which would make historical approval data available to read-scoped operators. (src/gateway/method-scopes.ts:109, a0c70f448d9d)
  • Current approval methods are approval-scoped: Current main keeps live exec approval get/list/request/wait/resolve methods under APPROVALS_SCOPE, not the read scope. (src/gateway/method-scopes.ts:43, 15bbf4f2f304)
  • Read scope is broader than approvals scope: Current authorization allows operator.read or operator.write for read-scoped methods, while non-admin approval methods require their exact approval scope. (src/gateway/method-scopes.ts:269, 15bbf4f2f304)
  • PR redaction only covers uppercase env-var array entries: The PR head array redactor only rewrites strings matching uppercase KEY=value style, leaving common option-style, lowercase, bearer, and token-shaped argv values outside this path. (src/infra/exec-approval-audit.ts:36, a0c70f448d9d)
  • Shared argv audit redaction already has broader coverage: The existing config audit helper redacts --flag=value, bare secret flags plus following values, env assignments, raw token shapes, bearer headers, and URL query secrets through the shared tool redaction path. (src/config/io.audit.ts:74, 15bbf4f2f304)
  • PR reads the append-only log synchronously: The PR head uses fs.readFileSync and parses all lines before applying limit, so one audit-log RPC can block the gateway event loop as the file grows. (src/infra/exec-approval-audit.ts:90, a0c70f448d9d)

Likely related people:

  • steipete: Current-main blame in this shallow checkout points central gateway approval and method-scope files to this maintainer, and GitHub commit history shows recent approval/protocol maintenance including fix(approvals): accept allowlist metadata. (role: recent maintainer and gateway approvals owner; confidence: medium; commits: 47375fd6dc98, ddac6f73e549; files: src/gateway/server-methods/exec-approval.ts, src/gateway/method-scopes.ts, src/gateway/protocol/schema/exec-approvals.ts)
  • pashpashpash: Recent merged gateway work routed diagnostics and trajectory export flows through exec approval and refreshed exec approval protocol models, which is directly adjacent to the approval audit path touched by this PR. (role: recent adjacent exec-approval maintainer; confidence: medium; commits: 6ce1058296cc; files: src/gateway/server-methods/exec-approval.ts, src/gateway/protocol/schema/exec-approvals.ts)
  • koshaji: Recent merged config-audit work added the broader argv redaction helper that this PR should likely reuse or match for persisted command arguments. (role: shared audit redaction contributor; confidence: medium; commits: a853c5e8c26f; files: src/config/io.audit.ts, src/logging/redact.ts, src/logging/redact.test.ts)

Remaining risk / open question:

  • The new audit-log RPC would expose historical command, args, session, agent, and resolver metadata to tokens that currently cannot access live approval data.
  • The custom argv redactor can persist common secret forms in plaintext unless it reuses or matches the shared redaction coverage.
  • Synchronous full-file audit reads create an availability risk for long-lived gateways with growing append-only logs.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 15bbf4f2f304.

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

Labels

app: web-ui App: web-ui gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant