Skip to content

feat(core): add governAction() for non-LLM action governance#10

Merged
c-1k merged 2 commits into
masterfrom
ship/action-governance
Mar 31, 2026
Merged

feat(core): add governAction() for non-LLM action governance#10
c-1k merged 2 commits into
masterfrom
ship/action-governance

Conversation

@c-1k

@c-1k c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add governAction<R>() method to TrustedClient — governs arbitrary agent actions (tool use, file access, shell commands, API requests) through the same pipeline as LLM calls
  • New types: ActionKind, ActionDescriptor, GovernedActionResult — exported from usertrust
  • TrustReceipt gains optional actionKind field (backward compatible)
  • Security: AUD-466 validates action.cost >= 0 to prevent budget inflation via negative values
  • Safety: AUD-465 uses holdReleased guard to prevent double-decrement of in-flight hold tracking

API

const client = await trust(new Anthropic(), { budget: 50_000 });

// LLM calls still work exactly as before
const { response, receipt } = await client.messages.create({ ... });

// NEW: Govern any agent action through the same pipeline
const { result, receipt: actionReceipt } = await client.governAction(
  { kind: "tool_use", name: "file_read", cost: 10, params: { path: "/data" } },
  async () => fs.readFile("/data", "utf-8"),
);

Governance pipeline for actions

mutex → circuit breaker → policy gate → PII check → PENDING hold → execute → POST/VOID → audit → receipt

Test plan

  • 14 unit tests: policy denial, budget enforcement, PII detection, audit trail, multiple action kinds, budget tracking, custom actor, destroyed client, negative/NaN/Infinity cost rejection, zero cost
  • 5 E2E tests: mixed LLM+action lifecycle, shared budget enforcement, audit chain integrity across mixed calls, action receipt shape, destroy cleanup
  • All 1084 tests pass (46 test files)
  • TypeScript: clean
  • Biome: clean

🤖 Generated with Claude Code

Add `governAction<R>()` method to `TrustedClient` that runs arbitrary
agent actions (tool use, file access, shell commands, API requests)
through the same governance pipeline as LLM calls:

  mutex → circuit breaker → policy gate → PII check →
  PENDING hold → execute → POST/VOID → audit → receipt

New types: ActionKind, ActionDescriptor, GovernedActionResult.
TrustReceipt gains optional `actionKind` field (backward compat).

Security: AUD-466 validates cost >= 0 to prevent budget inflation.
Safety: AUD-465 uses holdReleased guard to prevent double-decrement.

14 unit tests + 5 E2E tests covering policy denial, budget enforcement,
PII detection, audit chain integrity, mixed LLM+action governance,
negative cost rejection, and destroyed client behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@c-1k

c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Codex Review (gpt-5.3-codex)

Severity: High
File:Line: packages/core/src/govern.ts:851 (within governActionImpl, audit event data spread)
Description: action.params is written verbatim into the immutable audit log:

...(action.params != null ? { params: action.params } : {}),

This can persist raw sensitive payloads (including prompts/secrets/PII) to audit storage, which conflicts with your security model (“only SHA-256 hashes allowed” for sensitive raw content). Even with PII detection, pii: "off" or non-detected sensitive fields still get stored in cleartext.
Suggestion: Store a deterministic hash of params (and optionally a schema/keys preview), not full raw params.

const paramsHash =
	action.params != null
		? createHash("sha256").update(stableStringify(action.params)).digest("hex")
		: null;

// ...
data: {
	actionName: action.name,
	cost: action.cost,
	settled,
	transferId,
	...(paramsHash != null ? { paramsHash } : {}),
},

(Use the project’s canonical serialization utility if available; otherwise ensure stable key ordering.)

@c-1k

c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Codex Remediation Summary

# Finding Severity Classification Action Commit
1 action.params written verbatim to audit log High Deferred Deferred — same pattern as existing LLM path

Reasoning for deferral

The suggestion to hash action.params would break auditability — params contain operational context ({path: "/data", query: "users"}) that is explicitly designed to be logged. The existing LLM path logs model, cost, settled, transferId to the audit chain in the same way (govern.ts:603-610).

The "only hashes allowed" principle applies to raw prompt content, not structured action metadata. The PII detector already catches sensitive data in params when pii: "block" or pii: "warn" is configured.

If we change this pattern, it should be a separate PR that addresses both the LLM and action paths consistently.

Deliberations: 0 of 2 used
Result: 1 finding deferred (pre-existing pattern, out of scope)

@c-1k c-1k marked this pull request as ready for review March 31, 2026 01:34
@c-1k

c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

CI Results (Final)

Step Status Duration
Typecheck ✓ pass 11s
Lint ✓ pass 13s
Tests + Coverage ✓ pass 23s
Codex Review ✓ pass 1m23s

Commit: 2d88f70

@c-1k

c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Ship Pipeline Complete

Branch: ship/action-governance
Pipeline stages: 0-15 ✓

Audit Trail

  • Internal code review: 2 findings — 1 fixed (AUD-465 hold guard), 1 verified false positive (stage 6-7)
  • Security review: 3 findings — 1 fixed (AUD-466 negative cost validation), 2 pre-existing/deferred (stage 6-7)
  • Codex findings: 1 found, 0 fixed, 1 deferred (pre-existing audit pattern) (stage 11-12)
  • Deliberations: 0 of 2 used
  • CI: ✓ all 4 checks pass (stage 13)

Summary

  • Added governAction<R>() to TrustedClient — governs tool use, file access, shell commands, API requests through the same governance pipeline as LLM calls
  • 14 unit tests + 5 E2E tests + 4 security tests = 23 new tests (1084 total)
  • Two security hardening fixes: AUD-465 (double-decrement guard) + AUD-466 (negative cost validation)

… log

AUD-467: Spread action.params BEFORE governance fields in policy context
so callers cannot shadow budget_remaining, estimated_cost, or tier with
user-supplied values. Adds regression test.

Also adds stderr log for audit degradation in governAction (failure mode
15.3 consistency with interceptCall).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@c-1k

c-1k commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Remediation Round 2 — Review Findings

Fixed (this commit: a0cf27a)

Finding Source Severity Fix
Policy context field shadowing via params Code Review #5, Security Review #6 Important/Medium AUD-467: Spread params FIRST so governance fields always win
Missing stderr log for audit degradation Code Review #6 Important Added process.stderr.write matching interceptCall() pattern

Previously fixed (commit 2d88f70)

Finding Source Severity Fix
Double-decrement of inFlightHoldTotal Code Review #1 Critical AUD-465: holdReleased boolean guard
Negative cost bypasses budget Security Review #1 Critical AUD-466: Validation at entry

Verified false positive

Finding Source Why
inFlightHoldTotal decremented when never incremented Code Review #2 Traced error flow — policy/PII errors exit mutex finally and bypass execute try-catch entirely

Deferred (pre-existing, out of scope)

Finding Source Severity Reason
DEFAULT_RULES never loaded Security Review #2 Critical Pre-existing in interceptCall() path, not introduced by this PR
PII in warn mode logged to audit Both reviews High Pre-existing in LLM path, same pattern
Credential patterns not detected by PII scanner Security Review #4 High PII scanner scope — future enhancement (P1 roadmap)
TOCTOU on budget commit outside mutex Security Review #5 High Same structure as interceptCall(), pre-existing

CI Results (Final — commit a0cf27a)

Step Status
Typecheck ✓ pass
Lint ✓ pass
Tests (1085) ✓ pass
Codex Review ✓ pass

@c-1k c-1k merged commit c217a0b into master Mar 31, 2026
4 checks passed
@c-1k c-1k deleted the ship/action-governance branch March 31, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant