Skip to content

Security hardening: action budgets, dry-run mode, intent capsules - #38

Merged
ryaker merged 2 commits into
mainfrom
claude/zora-security-audit-6lYdy
Feb 13, 2026
Merged

Security hardening: action budgets, dry-run mode, intent capsules#38
ryaker merged 2 commits into
mainfrom
claude/zora-security-audit-6lYdy

Conversation

@ryaker

@ryaker ryaker commented Feb 13, 2026

Copy link
Copy Markdown
Owner

User description

Summary

Implements three critical security mitigations from OWASP LLM Top 10 (2025) and OWASP Agentic Top 10 (ASI-2026):

  1. Action Budgets (LLM06/LLM10) — Per-session limits on tool invocations to prevent unbounded autonomous loops
  2. Dry-Run Mode (ASI02) — Preview write operations without executing them
  3. Intent Capsules (ASI01) — Cryptographically signed mandate bundles for goal drift detection

Changes

Core Security Features

  • src/security/intent-capsule.ts (new) — IntentCapsuleManager creates HMAC-SHA256 signed intent capsules at task start, extracts mandate keywords, and detects goal hijacking via keyword overlap and action category matching
  • src/security/policy-engine.ts — Added budget tracking (recordAction, recordTokens, getBudgetStatus), dry-run mode integration, and intent capsule verification hooks
  • src/security/prompt-defense.ts — Added RAG/tool-output injection patterns (e.g., [IMPORTANT INSTRUCTION], NOTE TO AI) and sanitizeToolOutput() for defense against prompt injection via retrieved documents
  • src/security/security-types.ts — New types: BudgetStatus, DryRunResult, IntentCapsule, DriftCheckResult

Configuration & Policy

  • src/types.ts — Added BudgetPolicy and DryRunPolicy interfaces to ZoraPolicy
  • src/config/policy-loader.ts (new) — Centralized TOML → ZoraPolicy parsing with backward compatibility for missing sections
  • src/cli/presets.ts — Updated all presets (Locked, Safe, Balanced, Power) with budget and dry-run sections
  • specs/v5/docs/POLICY_PRESETS.md — Added "Locked" preset (fresh install default, zero access) and budget/dry-run config for all presets
  • specs/v5/docs/POLICY_REFERENCE.md — Complete reference for [budget] and [dry_run] policy sections

CLI & Daemon

  • src/cli/index.ts — Refactored to use centralized loadPolicy() from policy-loader
  • src/cli/daemon.ts — Refactored to use centralized loadPolicy() from policy-loader
  • src/orchestrator/orchestrator.ts — Added intent capsule creation at task start and budget/drift tracking integration

Documentation

  • SECURITY.md — Updated with v0.6 security hardening overview, four trust levels (added "Locked"), budget limits per preset, and dry-run mode explanation
  • CHANGELOG.md — Added v0.6.0 security hardening section detailing all three mitigations
  • SETUP_GUIDE.md — Added budget configuration section with OWASP references
  • README.md — Updated security callout to mention action budgets
  • docs/BEGINNERS_GUIDE.md — Updated security section to reference four presets
  • PRODUCTION_READINESS.md — Updated component count (27 → 29) and added IntentCapsuleManager and policy-loader to production-grade list
  • Archive docs (pre-hardening versions) — Preserved v0.5 documentation for reference

Tests

  • tests/unit/security/action-budget.test.ts (new) — 8 tests covering per-session limits, per-type limits, and budget status queries
  • tests/unit/security/dry-run.test.ts (new) — 9 tests covering dry-run mode enable/disable, write operation preview, and audit logging
  • tests/unit/security/intent-capsule.test.ts (new) — 8

https://claude.ai/code/session_017MTe9JTshvSB6eWtVyaQ24


CodeAnt-AI Description

Enforce action budgets, dry-run previews, and signed intent capsules; sanitize tool outputs for injection patterns

What Changed

  • Policy-enforced action and token budgets: sessions now count tool invocations and tokens and will block or flag actions when configured limits are exceeded (per-session and per-action-type limits).
  • Dry-run preview mode for write operations: when enabled, write/edit/Bash commands that modify state are intercepted and returned as "[DRY RUN] Would ..." messages instead of executing; intercepted actions are recorded in a dry-run log and can be optionally audited.
  • Intent capsules for goal integrity: each task can create a signed mandate bundle that is used to detect goal drift via category checks and keyword overlap; potential drift is flagged for human review rather than silently changing goals.
  • Tool-output and RAG injection defenses: input sanitization now includes RAG/tool-output patterns and a new tool-output sanitizer wraps suspicious tool results in <untrusted_tool_output> tags to prevent prompt injection from retrieved documents or tool responses.
  • Presets and policy loading: default policy presets now include budget and dry-run settings; a centralized policy loader parses TOML policies with backward-compatible defaults so existing configs keep working.
  • User-visible messages and reporting: policy summary now indicates budget and dry-run status; dry-run denials include human-readable "Would ..." descriptions; unit tests and docs updated to cover the new behaviors.

Impact

✅ Fewer runaway tool invocations
✅ Clearer previews for destructive operations
✅ Fewer prompt-injection surprises

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced action budgets with per-session and per-type limits
    • Added dry-run preview mode to test configurations safely without execution
    • Added intent verification with cryptographic signing for goal-drift detection
    • Expanded RAG and tool-output injection defenses
    • Enhanced audit logging with tamper-proof hash-chain tracking
    • Added fourth security preset (Locked) with explicit budgets for all presets
  • Documentation

    • Expanded security documentation with new hardening capabilities
    • Updated configuration guides with budget and dry-run sections
    • Updated security presets reference with explicit allowances and limits

… capsules, RAG defense

Implements security remediations from the Feb 2026 security audit against
OWASP LLM Top 10 (2025) and OWASP Agentic Top 10 (ASI-2026):

Phase 0: Centralized policy loader
- Extract duplicated TOML→ZoraPolicy parsing into src/config/policy-loader.ts
- Refactor src/cli/index.ts and src/cli/daemon.ts to use shared loadPolicy()

Phase 1 (LLM06/LLM10 — Excessive Agency / Unbounded Consumption):
- Add BudgetPolicy type with max_actions_per_session, per-type caps, token_budget
- Implement budget tracking and enforcement in PolicyEngine.createCanUseTool()
- Support 'block' and 'flag' modes when budget is exceeded
- Add budget defaults to all policy presets (locked/safe/balanced/power)

Phase 2 (ASI02 — Tool Misuse):
- Add DryRunPolicy type for previewing write operations without executing
- Implement dry-run interception in PolicyEngine for Write/Edit/Bash tools
- Skip read-only commands (ls, git status, etc.) in dry-run mode
- Optional audit logging of dry-run interceptions

Phase 3 (ASI01 — Agent Goal Hijack):
- Create IntentCapsuleManager with HMAC-SHA256 signed mandate bundles
- Implement keyword-based and category-based drift detection
- Integrate with PolicyEngine.createCanUseTool() for per-action drift checks
- Wire into Orchestrator.boot() and submitTask() lifecycle

Phase 4 (LLM01 — Prompt Injection):
- Add 10 RAG/tool-output injection patterns to PromptDefense
- Create sanitizeToolOutput() with distinct <untrusted_tool_output> tags
- Enhanced sanitizeInput() to include RAG injection patterns

All new features are backward-compatible: old policy.toml files without
budget/dry_run sections continue to work identically.

Tests: 502 passing (50 new tests across 3 new test files + existing tests updated)

https://claude.ai/code/session_017MTe9JTshvSB6eWtVyaQ24
Archive pre-hardening versions to docs/archive/2026-02/ and update all
user-facing documentation to reflect OWASP LLM/Agentic security features:
action budgets, dry-run preview mode, intent capsules, and RAG defense.

- SECURITY.md: Full rewrite with OWASP compliance matrix, security
  architecture table, new feature sections with config examples
- README.md: Security table in How Security Works, new status rows
- CHANGELOG.md: Detailed Security Hardening section for v0.6.0
- SETUP_GUIDE.md: [budget] and [dry_run] in example policy.toml
- POLICY_PRESETS.md: All 4 presets with budget/dry-run, summary table
- POLICY_REFERENCE.md: Full field reference for new sections
- PRODUCTION_READINESS.md: New security components, P2 progress
- BEGINNERS_GUIDE.md: Updated presets, key concepts table

https://claude.ai/code/session_017MTe9JTshvSB6eWtVyaQ24
@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A comprehensive security hardening update (v0.6) introducing action budgets, dry-run preview mode, intent capsules with drift detection, expanded RAG injection defenses, and a centralized policy-loader module. Includes new security components, policy configuration sections, orchestrator integration, extensive documentation updates, and test coverage.

Changes

Cohort / File(s) Summary
Documentation Core
CHANGELOG.md, README.md, SECURITY.md, SETUP_GUIDE.md, PRODUCTION_READINESS.md
Enhanced documentation describing v0.6 security hardening features: action budgets, dry-run mode, intent capsules, RAG injection defense. Updated capability tables and added new security sections with configuration examples.
Beginners & Reference Guides
docs/BEGINNERS_GUIDE.md, specs/v5/docs/POLICY_PRESETS.md, specs/v5/docs/POLICY_REFERENCE.md
Updated policy preset descriptions from 3 to 4 presets (added Locked), introduced budget and dry_run sections with per-preset behavior, expanded enforcement order and section references to include new configuration areas.
Documentation Archives
docs/archive/2026-02/*
Archived pre-hardening versions of guides and specifications (7 files) for historical reference, including BEGINNERS_GUIDE, CHANGELOG, POLICY_PRESETS, POLICY_REFERENCE, PRODUCTION_READINESS, README, SECURITY, and SETUP_GUIDE.
Security Core Components
src/security/intent-capsule.ts, src/security/policy-engine.ts, src/security/security-types.ts
Introduced IntentCapsuleManager for mandate signing and drift detection; expanded PolicyEngine with action budgeting (per-session, per-type, token), dry-run interception, and new public APIs for audit logging and capsule management; added types for BudgetStatus, DryRunResult, IntentCapsule, DriftCheckResult.
Security Utilities
src/security/prompt-defense.ts, src/security/index.ts
Added RAG_INJECTION_PATTERNS set and new sanitizeToolOutput function for tool-output sanitization; exported new security managers (IntentCapsuleManager, sanitizeToolOutput) and types.
Policy & Configuration Infrastructure
src/config/policy-loader.ts, src/types.ts, src/cli/presets.ts
Introduced centralized policy-loader module with loadPolicy and parsePolicy functions; added BudgetPolicy and DryRunPolicy type definitions; expanded PRESETS with budget and dry_run sections for all preset levels.
CLI & Orchestration
src/cli/daemon.ts, src/cli/index.ts, src/orchestrator/orchestrator.ts
Refactored policy loading to use centralized policy-loader instead of in-file TOML parsing; integrated IntentCapsuleManager into Orchestrator with per-session signing keys and drift detection on task submission.
Test Fixtures & Unit Tests
tests/fixtures/sample-policy.toml, tests/unit/security/action-budget.test.ts, tests/unit/security/dry-run.test.ts, tests/unit/security/intent-capsule.test.ts, tests/unit/security/prompt-defense.test.ts
Updated sample policy with new budget and dry_run sections; added comprehensive test suites for action budget enforcement (311 lines), dry-run mode interception (220 lines), intent capsule creation/drift detection (204 lines), and tool-output sanitization (79 lines).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant Orchestrator
    participant PolicyEngine
    participant IntentCapsuleManager
    participant AuditLogger

    User->>CLI: Submit Task
    CLI->>Orchestrator: Initialize Session
    Orchestrator->>IntentCapsuleManager: Create Signed Capsule
    IntentCapsuleManager-->>Orchestrator: Capsule with Mandate Hash
    Orchestrator->>PolicyEngine: Register Capsule & Start Session
    
    Orchestrator->>User: Ready to Execute
    
    User->>Orchestrator: Execute Action
    Orchestrator->>PolicyEngine: Check Intent Drift
    PolicyEngine->>IntentCapsuleManager: Verify Against Mandate
    IntentCapsuleManager-->>PolicyEngine: Drift Status
    
    alt Goal Drift Detected
        PolicyEngine->>AuditLogger: Log goal_drift Event
        PolicyEngine-->>User: Flag for Approval
    end
    
    PolicyEngine->>PolicyEngine: Check Action Budget
    
    alt Budget Exceeded
        PolicyEngine->>AuditLogger: Log budget_exceeded Event
        PolicyEngine-->>User: Flag or Block
    end
    
    PolicyEngine->>PolicyEngine: Check Dry-Run Mode
    
    alt Dry-Run Enabled
        PolicyEngine->>AuditLogger: Log dry_run Event
        PolicyEngine-->>User: Preview (No Execution)
    else Allowed
        PolicyEngine->>User: Execute Action
        PolicyEngine->>AuditLogger: Log audit_success
    end
Loading
sequenceDiagram
    participant Daemon
    participant PolicyLoader
    participant TOML Parser
    participant PolicyEngine

    Daemon->>PolicyLoader: loadPolicy(policyPath)
    
    alt Policy File Exists
        PolicyLoader->>TOML Parser: Parse policy.toml
        TOML Parser-->>PolicyLoader: Raw Config Object
        
        PolicyLoader->>PolicyLoader: parsePolicy(raw)
        Note over PolicyLoader: Extract [filesystem], [shell],<br/>[actions], [network],<br/>[budget], [dry_run]<br/>with defaults
        
        PolicyLoader-->>Daemon: ZoraPolicy Object
    else Policy Missing
        PolicyLoader-->>Daemon: Error with Location Info
    end
    
    Daemon->>PolicyEngine: Initialize with Policy
    PolicyEngine->>PolicyEngine: Expand Policy (include budget/dry_run)
    PolicyEngine-->>Daemon: Ready
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #8: Introduces core PolicyEngine, orchestrator, and security scaffolding that this PR directly extends with budget enforcement, dry-run mode, and intent capsule integration.
  • PR #20: Adds SDK permission callback mechanism (createCanUseTool) to control tool access; this PR integrates that with new budget and dry-run enforcement logic.
  • PR #25: Updates production-readiness documentation and release metadata for v0.6; this PR adds significant security capabilities documented in that parallel release update.

Suggested labels

size:XXL, security, feature, infrastructure


🐰 A hardened fortress built with care,
With budgets wise and capsules fair,
Intent verified, drift's laid bare,
RAG defenses everywhere—
Zora runs with caution in the air! 🔐

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (22 files):

⚔️ CHANGELOG.md (content)
⚔️ PRODUCTION_READINESS.md (content)
⚔️ QUICKSTART.md (content)
⚔️ README.md (content)
⚔️ SECURITY.md (content)
⚔️ SETUP_GUIDE.md (content)
⚔️ docs/BEGINNERS_GUIDE.md (content)
⚔️ specs/v5/docs/POLICY_PRESETS.md (content)
⚔️ specs/v5/docs/POLICY_REFERENCE.md (content)
⚔️ src/cli/daemon.ts (content)
⚔️ src/cli/index.ts (content)
⚔️ src/cli/presets.ts (content)
⚔️ src/dashboard/frontend/dist/index.html (content)
⚔️ src/dashboard/frontend/src/App.tsx (content)
⚔️ src/orchestrator/orchestrator.ts (content)
⚔️ src/security/index.ts (content)
⚔️ src/security/policy-engine.ts (content)
⚔️ src/security/prompt-defense.ts (content)
⚔️ src/security/security-types.ts (content)
⚔️ src/types.ts (content)
⚔️ tests/fixtures/sample-policy.toml (content)
⚔️ tests/unit/security/prompt-defense.test.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Security hardening: action budgets, dry-run mode, intent capsules' directly and accurately summarizes the three main security features added in this changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/zora-security-audit-6lYdy
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch claude/zora-security-audit-6lYdy
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Feb 13, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ryaker, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security posture of the Zora agent by integrating several critical mitigations aligned with OWASP LLM Top 10 (2025) and OWASP Agentic Top 10 (ASI-2026). The changes introduce robust controls for agent autonomy, including action budgets, a dry-run preview mode for sensitive operations, and cryptographically signed intent capsules to prevent goal hijacking. Additionally, prompt injection defenses have been expanded to cover Retrieval-Augmented Generation (RAG) and tool outputs, and the policy loading mechanism has been centralized for improved reliability and configuration management.

Highlights

  • Action Budgets: Implemented per-session and per-type limits on tool invocations and LLM token consumption to prevent unbounded autonomous loops, aligning with OWASP LLM06/LLM10.
  • Dry-Run Mode: Introduced a preview mode for write operations, allowing users to safely test policies and see what actions would be taken without actual execution, addressing OWASP ASI02.
  • Intent Capsules: Developed a system for cryptographically signed mandate bundles at task start to detect and flag goal drift or agent hijacking, mitigating OWASP ASI01.
  • RAG/Tool-Output Injection Defense: Enhanced prompt defense with 10 new RAG-specific injection patterns and a dedicated sanitizeToolOutput() function to protect against prompt injection via retrieved documents and tool outputs, addressing OWASP LLM01.
  • Centralized Policy Loader: Refactored policy loading into a single module (src/config/policy-loader.ts), improving maintainability, ensuring consistent application of defaults, and providing backward compatibility.
Changelog
  • CHANGELOG.md
    • Updated with a new "Security Hardening" section detailing action budgets, dry-run mode, intent capsules, RAG/tool-output injection defense, and the centralized policy loader.
    • Adjusted the comprehensive test suite count from 38 files/62+ tests to 48 files/500+ tests.
    • Updated the prompt defense description to reflect 20+ injection patterns (direct + RAG) and tool output sanitization.
  • PRODUCTION_READINESS.md
    • Increased the module count from 27 to 29 production-grade components.
    • Added IntentCapsuleManager and PolicyLoader to the list of production-grade security components.
    • Marked action budget enforcement, dry-run preview mode, intent capsule signing, RAG/tool-output injection defense, and centralized policy loader as "DONE in v0.6" under P2 hardening.
  • README.md
    • Updated the "Policy-Enforced Autonomy" description to mention action budgets, dry-run preview mode, and intent verification, referencing OWASP standards.
    • Added a new "v0.6 Security Hardening" table detailing Action Budgets, Dry-Run Mode, Intent Capsules, RAG Injection Defense, Hash-Chain Audit, and AES-256-GCM Secrets.
    • Updated the project status table to reflect the working status of action budgets, dry-run preview mode, intent capsules, and RAG/tool-output injection defense.
  • SECURITY.md
    • Added a v0.6.0 security hardening overview to the introduction.
    • Updated the "What Zora CAN'T Do" section to include not exceeding its action budget.
    • Changed "The Three Trust Levels" to "The Four Trust Levels" and introduced the "Locked" preset as the fresh install default.
    • Added budget information (actions/session, tokens, on_exceed behavior) to the "Safe", "Balanced", and "Power" trust levels.
    • Added new detailed sections for "Action Budgets (OWASP LLM06/LLM10)", "Dry-Run Preview Mode (OWASP ASI-02)", "Intent Verification / Mandate Signing (OWASP ASI-01)", and "RAG/Tool-Output Injection Defense (OWASP LLM01)".
    • Added new audit log event types: budget_exceeded, dry_run, and goal_drift.
    • Added a "Security Architecture Summary" table outlining all security layers and components.
    • Included an "OWASP Compliance Matrix" detailing Zora's mitigations for LLM01, LLM06, LLM07, LLM10, ASI-01, and ASI-02.
    • Updated the "v0.6 Implementation Status" table to reflect the status of new security features.
    • Expanded the summary section to include "Locked mode", action budgets, dry-run mode, intent verification, and injection defense.
  • SETUP_GUIDE.md
    • Added new [budget] and [dry_run] sections with example configurations to the policy.toml example.
    • Updated the "Understanding the Config Files" table for policy.toml to include [budget] and [dry_run] sections.
  • docs/BEGINNERS_GUIDE.md
    • Added a v0.6 security note referencing OWASP LLM Top 10 and Agentic Top 10.
    • Updated "The three security presets" to "The four security presets" and included the "Locked" preset with budget information.
    • Added action budgets, dry-run mode, and intent capsules to the "Key concepts in plain English" table.
  • docs/archive/2026-02/BEGINNERS_GUIDE-pre-hardening.md
    • Added an archived version of the beginner's guide from before the security hardening.
  • docs/archive/2026-02/CHANGELOG-pre-hardening.md
    • Added an archived version of the changelog from before the security hardening.
  • docs/archive/2026-02/POLICY_PRESETS-pre-hardening.md
    • Added an archived version of the policy presets documentation from before the security hardening.
  • docs/archive/2026-02/POLICY_REFERENCE-pre-hardening.md
    • Added an archived version of the policy reference documentation from before the security hardening.
  • docs/archive/2026-02/PRODUCTION_READINESS-pre-hardening.md
    • Added an archived version of the production readiness assessment from before the security hardening.
  • docs/archive/2026-02/README-pre-hardening.md
    • Added an archived version of the README from before the security hardening.
  • docs/archive/2026-02/SECURITY-pre-hardening.md
    • Added an archived version of the security guide from before the security hardening.
  • docs/archive/2026-02/SETUP_GUIDE-pre-hardening.md
    • Added an archived version of the setup guide from before the security hardening.
  • specs/v5/docs/POLICY_PRESETS.md
    • Updated the document title to "Zora v0.6 - Policy Presets" and added a v0.6 update note.
    • Introduced the "Locked" preset with zero access and budget/dry-run configurations.
    • Integrated [budget] and [dry_run] sections with default configurations into the "Safe", "Balanced", and "Power" presets.
    • Added a "Budget Summary by Preset" table for quick comparison of limits.
  • specs/v5/docs/POLICY_REFERENCE.md
    • Updated the document title to "Zora v0.6 - Policy Reference" and clarified its purpose.
    • Updated the policy file description to allow editing via text editor.
    • Added detailed "Section Reference" for [budget] and [dry_run] policies, including fields like max_actions_per_session, token_budget, on_exceed, enabled, tools, and audit_dry_runs.
    • Updated the "Enforcement order" to include checks for budget limits, intent capsule for goal drift, and dry-run interception.
    • Added a new section "Automatic security (no configuration needed)" detailing Intent Capsules, RAG Injection Defense, Leak Detection, Hash-Chain Audit, and Secrets Encryption.
    • Included a "Backward compatibility" note for [budget] and [dry_run] sections.
  • src/cli/daemon.ts
    • Refactored policy loading to use the new centralized policy-loader.ts module, removing duplicated TOML parsing logic.
  • src/cli/index.ts
    • Refactored policy loading to use the new centralized policy-loader.ts module, removing duplicated TOML parsing logic.
  • src/cli/presets.ts
    • Updated all defined policy presets (locked, safe, balanced, power) to include budget and dry_run configurations with appropriate default values.
  • src/config/policy-loader.ts
    • Added a new module to centralize the loading and parsing of ZoraPolicy from TOML files, ensuring backward compatibility with missing sections and providing safe defaults.
  • src/orchestrator/orchestrator.ts
    • Imported crypto and IntentCapsuleManager modules.
    • Initialized IntentCapsuleManager with a per-session signing key during Orchestrator setup.
    • Integrated IntentCapsuleManager to create signed intent capsules at the start of each task for goal drift detection.
  • src/security/index.ts
    • Exported the newly introduced IntentCapsuleManager.
    • Exported new security types: BudgetStatus, DryRunResult, IntentCapsule, and DriftCheckResult.
    • Exported sanitizeToolOutput from prompt-defense.ts.
  • src/security/intent-capsule.ts
    • Added a new module implementing IntentCapsuleManager for creating, verifying, and checking drift against cryptographically signed mandate bundles using HMAC-SHA256.
  • src/security/policy-engine.ts
    • Updated documentation to reflect new security hardening features: action budgeting, dry-run mode, and intent capsule integration.
    • Added internal state for budget tracking (_actionCounts, _totalActions, _tokensUsed, _sessionId) and dry-run logging (_dryRunLog).
    • Introduced WRITE_TOOLS and READ_ONLY_COMMANDS sets for dry-run logic.
    • Added optional _intentCapsuleManager and _auditLogger properties.
    • Implemented setAuditLogger and setIntentCapsuleManager methods.
    • Added budget management methods: startSession, recordAction, recordTokenUsage, getBudgetStatus, and resetBudget.
    • Implemented dry-run management methods: getDryRunLog, clearDryRunLog, _checkDryRun, _isReadOnlyCommand, and _describeAction.
    • Integrated budget enforcement into the createCanUseTool callback, allowing for blocking or flagging on exceed.
    • Integrated intent capsule drift checks into the createCanUseTool callback, flagging potential goal drift.
    • Integrated dry-run interception into the createCanUseTool callback, denying execution for intercepted actions.
    • Updated getPolicySummary to include budget and dry-run status.
    • Modified _persistPolicyExpansion to serialize [budget] and [dry_run] sections if present in the policy.
  • src/security/prompt-defense.ts
    • Added 10 new RAG-specific injection patterns to RAG_INJECTION_PATTERNS for detecting disguised instructions in retrieved documents and tool outputs.
    • Updated sanitizeInput to include RAG_INJECTION_PATTERNS in its scan.
    • Introduced sanitizeToolOutput function to aggressively sanitize tool output content by wrapping detected injection patterns in <untrusted_tool_output> tags.
  • src/security/security-types.ts
    • Extended AuditEntryEventType with new event types: budget_exceeded, dry_run, and goal_drift.
    • Added new interface BudgetStatus to represent the current state of action and token budgets.
    • Added new interface DryRunResult to describe intercepted actions in dry-run mode.
    • Added new interfaces IntentCapsule and DriftCheckResult for managing and evaluating signed mandate bundles.
  • src/types.ts
    • Added new interface BudgetPolicy to define budget limits for actions and tokens, including max_actions_per_session, max_actions_per_type, token_budget, and on_exceed.
    • Added new interface DryRunPolicy to configure dry-run mode, including enabled, tools, and audit_dry_runs.
    • Extended the ZoraPolicy interface to include optional budget and dry_run sections.
  • tests/fixtures/sample-policy.toml
    • Updated the sample policy file to include example [budget] and [dry_run] sections with default values.
  • tests/unit/security/action-budget.test.ts
    • Added a new unit test file to thoroughly test the action budget enforcement mechanisms in PolicyEngine, covering total session limits, per-type limits, token budgets, and integration with the canUseTool callback and flag system.
  • tests/unit/security/dry-run.test.ts
    • Added a new unit test file to verify the functionality of the dry-run mode, including interception of write tools, non-interception of read-only commands, specific tool targeting, and log management.
  • tests/unit/security/intent-capsule.test.ts
    • Added a new unit test file to cover the creation, verification, and drift checking capabilities of the IntentCapsuleManager, including handling of expired capsules, category mismatches, and keyword overlap.
  • tests/unit/security/prompt-defense.test.ts
    • Updated existing unit tests to include new RAG injection patterns for sanitizeInput.
    • Added new tests for the sanitizeToolOutput function, verifying its ability to wrap suspicious content in <untrusted_tool_output> tags.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • TOML parser / import errors not handled
    await import('smol-toml') and parseTOML(...) are used without try/catch. If the optional dependency is missing or TOML parsing fails (invalid file), the function will throw. Consider graceful error messages and guidance to fix the policy file or dependency.

  • TimingSafeEqual crash
    The code calls crypto.timingSafeEqual directly on two Buffers built from hex strings. If the provided capsule.signature is malformed or of a different length than the expected signature, timingSafeEqual will throw. This can cause runtime exceptions when verifying capsules (e.g., during tampering or format changes). Consider validating buffer lengths and catching errors to return false on verification failures.

  • Empty action detail logic
    An empty actionDetail results in actionKeywords.length === 0 and the code sets overlapRatio to 1.0 (treated as full match). This effectively disables drift detection for actions with empty details and can hide policy violations. The default should not imply perfect match — consider treating empty details as neutral/low overlap.

  • Dry-run Ordering
    Dry-run interception (_checkDryRun) is performed after budget enforcement and intent capsule checks. That means dry-run previews may still consume action/token budgets and trigger intent drift checks or audit events even though the action will not be executed. Dry-run interception should run before budget/intent enforcement so previews don't alter runtime state or budgets.

  • Budget Counting
    The budget recording increments counters (total and per-type) immediately in recordAction. If a downstream approval flow (e.g., on_exceed === 'flag' + human callback) rejects the action, the action has already been counted. This can cause denied actions to permanently consume the budget and skew metrics.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces significant security hardening measures for Zora, aligning with OWASP LLM Top 10 and Agentic Top 10 guidelines. Key changes include implementing action budgets (per-session and per-type limits, token budgets) to prevent unbounded autonomous loops, a dry-run preview mode for write operations, and intent capsules with mandate signing for goal drift detection. Additionally, RAG/tool-output injection defenses have been enhanced with new patterns and a sanitizeToolOutput() function. The policy loading mechanism has been centralized into src/config/policy-loader.ts to reduce code duplication and ensure backward compatibility for existing policy files. Documentation across CHANGELOG.md, PRODUCTION_READINESS.md, README.md, SECURITY.md, SETUP_GUIDE.md, and docs/BEGINNERS_GUIDE.md has been extensively updated to reflect these new features, including a new 'Locked' security preset and detailed explanations of the new security architecture and OWASP compliance. Review comments suggest refactoring the conditional property additions in policy-loader.ts for better readability and clarifying the hash chain calculation description in SECURITY.md for improved user understanding.

Comment on lines +37 to +76
return {
filesystem: {
allowed_paths: (fsPol?.['allowed_paths'] as string[]) ?? [],
denied_paths: (fsPol?.['denied_paths'] as string[]) ?? [],
resolve_symlinks: (fsPol?.['resolve_symlinks'] as boolean) ?? true,
follow_symlinks: (fsPol?.['follow_symlinks'] as boolean) ?? false,
},
shell: {
mode: (shPol?.['mode'] as 'allowlist' | 'denylist' | 'deny_all') ?? 'allowlist',
allowed_commands: (shPol?.['allowed_commands'] as string[]) ?? ['ls', 'npm', 'git'],
denied_commands: (shPol?.['denied_commands'] as string[]) ?? [],
split_chained_commands: (shPol?.['split_chained_commands'] as boolean) ?? true,
max_execution_time: (shPol?.['max_execution_time'] as string) ?? '1m',
},
actions: {
reversible: (actPol?.['reversible'] as string[]) ?? [],
irreversible: (actPol?.['irreversible'] as string[]) ?? [],
always_flag: (actPol?.['always_flag'] as string[]) ?? [],
},
network: {
allowed_domains: (netPol?.['allowed_domains'] as string[]) ?? [],
denied_domains: (netPol?.['denied_domains'] as string[]) ?? [],
max_request_size: (netPol?.['max_request_size'] as string) ?? '10mb',
},
...(budPol ? {
budget: {
max_actions_per_session: (budPol['max_actions_per_session'] as number) ?? 0,
max_actions_per_type: (budPol['max_actions_per_type'] as Record<string, number>) ?? {},
token_budget: (budPol['token_budget'] as number) ?? 0,
on_exceed: (budPol['on_exceed'] as 'block' | 'flag') ?? 'block',
},
} : {}),
...(dryPol ? {
dry_run: {
enabled: (dryPol['enabled'] as boolean) ?? false,
tools: (dryPol['tools'] as string[]) ?? [],
audit_dry_runs: (dryPol['audit_dry_runs'] as boolean) ?? true,
},
} : {}),
};

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.

medium

The use of spread syntax with a ternary operator (...(condition ? { ... } : {})) to conditionally add properties can be a bit difficult to read and is not a very common pattern.

A more straightforward and readable approach would be to construct the base object first and then use standard if statements to add the optional budget and dry_run properties. This would make the logic clearer to future maintainers.

  const policy: ZoraPolicy = {
    filesystem: {
      allowed_paths: (fsPol?.['allowed_paths'] as string[]) ?? [],
      denied_paths: (fsPol?.['denied_paths'] as string[]) ?? [],
      resolve_symlinks: (fsPol?.['resolve_symlinks'] as boolean) ?? true,
      follow_symlinks: (fsPol?.['follow_symlinks'] as boolean) ?? false,
    },
    shell: {
      mode: (shPol?.['mode'] as 'allowlist' | 'denylist' | 'deny_all') ?? 'allowlist',
      allowed_commands: (shPol?.['allowed_commands'] as string[]) ?? ['ls', 'npm', 'git'],
      denied_commands: (shPol?.['denied_commands'] as string[]) ?? [],
      split_chained_commands: (shPol?.['split_chained_commands'] as boolean) ?? true,
      max_execution_time: (shPol?.['max_execution_time'] as string) ?? '1m',
    },
    actions: {
      reversible: (actPol?.['reversible'] as string[]) ?? [],
      irreversible: (actPol?.['irreversible'] as string[]) ?? [],
      always_flag: (actPol?.['always_flag'] as string[]) ?? [],
    },
    network: {
      allowed_domains: (netPol?.['allowed_domains'] as string[]) ?? [],
      denied_domains: (netPol?.['denied_domains'] as string[]) ?? [],
      max_request_size: (netPol?.['max_request_size'] as string) ?? '10mb',
    },
  };

  if (budPol) {
    policy.budget = {
      max_actions_per_session: (budPol['max_actions_per_session'] as number) ?? 0,
      max_actions_per_type: (budPol['max_actions_per_type'] as Record<string, number>) ?? {},
      token_budget: (budPol['token_budget'] as number) ?? 0,
      on_exceed: (budPol['on_exceed'] as 'block' | 'flag') ?? 'block',
    };
  }

  if (dryPol) {
    policy.dry_run = {
      enabled: (dryPol['enabled'] as boolean) ?? false,
      tools: (dryPol['tools'] as string[]) ?? [],
      audit_dry_runs: (dryPol['audit_dry_runs'] as boolean) ?? true,
    };
  }

  return policy;

Comment thread SECURITY.md
Comment on lines +263 to +264
2. Entry 2: `hash_chain = hash(entry1_hash + entry2)`
3. Entry 3: `hash_chain = hash(entry2_hash + entry3)`

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.

medium

The description of how the hash chain is calculated is a bit ambiguous. For example, hash(entry1_hash + entry2) could be interpreted in a few ways. To improve clarity for users trying to understand or verify the audit log, I suggest describing the process more explicitly.

For example, you could rephrase to something like:

  1. Entry 1: hash_chain = hash(genesis_block_data + data_of_entry_1)
  2. Entry 2: hash_chain = hash(hash_from_entry_1 + data_of_entry_2)
  3. Entry 3: hash_chain = hash(hash_from_entry_2 + data_of_entry_3)

This makes it clearer that each new hash is a function of the previous hash and the current data.

Comment thread src/cli/daemon.ts
Comment on lines +52 to 53
} catch {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The catch block treats all loadPolicy failures as "policy not found", so if the policy file exists but is unreadable or contains invalid TOML, the real error is swallowed and the user is incorrectly told to rerun zora init, making debugging harder and potentially causing them to overwrite a broken policy instead of fixing it. [logic error]

Severity Level: Major ⚠️
- ⚠️ `zora start` misreports invalid policy as missing file.
- ⚠️ Users nudged to rerun `zora init` unnecessarily.
Suggested change
} catch {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('Policy file not found')) {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
} else {
console.error('Failed to load policy from ~/.zora/policy.toml:', message);
}
Steps of Reproduction ✅
1. Ensure Zora is initialized so that `~/.zora/config.toml` exists and is valid (checked
in `src/cli/daemon.ts:35-45` before loading the policy).

2. Create or edit `~/.zora/policy.toml` so the file exists but contains invalid TOML
(e.g., an unmatched bracket), causing TOML parsing to fail when read (this file path is
passed as `policyPath` in `src/cli/daemon.ts:38`).

3. Run `zora start`, which executes the CLI entrypoint in `src/cli/index.ts:188-232`; the
`start` command forks the daemon process by running `daemon.js` (see
`src/cli/index.ts:215-221`), which corresponds to `src/cli/daemon.ts` at runtime.

4. In the daemon process, `main()` in `src/cli/daemon.ts:35-55` executes:
`loadPolicy(policyPath)` at line 51 calls `loadPolicy` in
`src/config/policy-loader.ts:15-22`, which reads and parses `policy.toml`. The invalid
TOML causes `parseTOML` at `policy-loader.ts:20-21` to throw an error. This error is
caught by the bare `catch` in `daemon.ts:50-55`, which logs `Policy not found at
~/.zora/policy.toml. Run \`zora init\` first.` and exits with code 1, even though the file
exists and the real issue is a parse/IO error, making debugging the broken policy
difficult.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/cli/daemon.ts
**Line:** 52:53
**Comment:**
	*Logic Error: The catch block treats all `loadPolicy` failures as "policy not found", so if the policy file exists but is unreadable or contains invalid TOML, the real error is swallowed and the user is incorrectly told to rerun `zora init`, making debugging harder and potentially causing them to overwrite a broken policy instead of fixing it.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Comment thread src/cli/index.ts
} else {
try {
policy = await loadPolicy(policyPath);
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: In the shared setupContext path, any error from loadPolicy (including invalid or unreadable policy files) is caught and reported as "policy not found", which both hides the real failure mode and may prompt users to rerun zora init and overwrite their existing policy instead of fixing the underlying issue. [logic error]

Severity Level: Major ⚠️
- ⚠️ All CLI commands using setupContext misreport policy parse failures.
- ⚠️ Users may overwrite existing policies by rerunning `zora init`.
- ⚠️ Troubleshooting broken policy.toml becomes significantly harder.
Suggested change
} catch {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('Policy file not found')) {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
} else {
console.error('Failed to load policy from ~/.zora/policy.toml:', message);
}
Steps of Reproduction ✅
1. Start from a system where Zora has already been initialized so that
`~/.zora/config.toml` and `~/.zora/policy.toml` both exist (the config and policy paths
are constructed in `src/cli/index.ts:76-78` inside `setupContext()`).

2. Manually edit `~/.zora/policy.toml` and introduce a syntax error (for example, remove a
closing bracket), so the file still exists on disk but contains invalid TOML; this file is
later read by `loadPolicy()` in `src/config/policy-loader.ts:15-22` using
`fs.readFileSync` and `smol-toml`'s `parseTOML`.

3. Run any CLI command that invokes `setupContext()`, such as `zora ask "test"` defined at
`src/cli/index.ts:109-146`, which calls `setupContext()` at line 117, or the `status`
command at lines 149-186 which also calls `setupContext()` at line 162.

4. During `setupContext()` (`src/cli/index.ts:75-106`), the code imports `loadPolicy`
(line 89) and calls `policy = await loadPolicy(policyPath);` (line 92); `loadPolicy()`
reads and parses the existing but invalid `~/.zora/policy.toml`, causing `parseTOML` at
`policy-loader.ts:20-22` to throw a parse error. This error is caught by the bare `catch {
... }` block at `index.ts:91-95`, which logs `Policy not found at ~/.zora/policy.toml. Run
\`zora init\` first.` even though the file exists and the real problem is a parse/load
error, misleading the user toward re-running `zora init` (and potentially overwriting
their policy) instead of fixing the TOML.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/cli/index.ts
**Line:** 93:93
**Comment:**
	*Logic Error: In the shared `setupContext` path, any error from `loadPolicy` (including invalid or unreadable policy files) is caught and reported as "policy not found", which both hides the real failure mode and may prompt users to rerun `zora init` and overwrite their existing policy instead of fixing the underlying issue.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Comment on lines +290 to +298
const base = this._extractBaseCommand(command);
if (PolicyEngine.READ_ONLY_COMMANDS.has(base)) return true;
// git status, git log, git diff are read-only
if (base === 'git') {
const parts = command.trim().split(/\s+/);
const subCommand = parts[1] ?? '';
if (['status', 'log', 'diff', 'show', 'branch', 'remote', 'tag'].includes(subCommand)) {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The dry-run detector _isReadOnlyCommand treats any Bash command as read-only based only on its first binary (and some git subcommands), so chained commands like ls && rm -rf /tmp or pipelines starting with a "safe" command can bypass dry-run interception and execute destructive operations without preview. [security]

Severity Level: Critical 🚨
- ❌ Dry-run mode skips preview for destructive chained bash commands.
- ❌ ASI02 dry-run mitigation bypassed when chains start with ls.
- ⚠️ Users may suffer unintended filesystem changes despite dry-run enabled.
Suggested change
const base = this._extractBaseCommand(command);
if (PolicyEngine.READ_ONLY_COMMANDS.has(base)) return true;
// git status, git log, git diff are read-only
if (base === 'git') {
const parts = command.trim().split(/\s+/);
const subCommand = parts[1] ?? '';
if (['status', 'log', 'diff', 'show', 'branch', 'remote', 'tag'].includes(subCommand)) {
return true;
}
const trimmed = command.trim();
// If the command string contains chaining operators, treat it as potentially state-changing.
if (/[;&|]/.test(trimmed)) {
return false;
}
const base = this._extractBaseCommand(trimmed);
if (PolicyEngine.READ_ONLY_COMMANDS.has(base)) return true;
// Only obviously read-only git subcommands are treated as safe
if (base === 'git') {
const parts = trimmed.split(/\s+/);
const subCommand = parts[1] ?? '';
if (['status', 'log', 'diff', 'show'].includes(subCommand)) {
return true;
}
}
Steps of Reproduction ✅
1. Enable dry-run mode in the policy file used by the CLI:

   - Edit `~/.zora/policy.toml` to add a `[dry_run]` section with `enabled = true` and
   `tools = []` so dry-run applies to all write tools (as parsed in
   `src/config/policy-loader.ts:29-35,61-75`, which populates `ZoraPolicy.dry_run` and is
   used by `PolicyEngine` in `src/security/policy-engine.ts:244-246`).

2. Configure the shell allowlist so destructive commands are permitted while relying on
dry-run as the safety net:

   - In the same policy file, ensure `[shell]` includes `mode = "allowlist"`,
   `split_chained_commands = true` (the default in `policy-loader.ts:44-49`), and add `rm`
   to `allowed_commands` alongside `ls`.

   - This configuration is loaded via `loadPolicy()` in
   `src/config/policy-loader.ts:15-22` and passed into `new PolicyEngine(policy)` in
   `src/cli/index.ts:14,98`, establishing the policy used by
   `PolicyEngine.createCanUseTool()` in `src/security/policy-engine.ts:444-451`.

3. Run the real CLI flow that wires the Agent SDK through `PolicyEngine`:

   - Invoke the documented `ask` command described in `PRODUCTION_READINESS.md:117` (the
   only end-to-end path) via `zora ask "..."`, which uses `PolicyEngine` (imported and
   instantiated in `src/cli/index.ts:14,98`) to construct the SDK-compatible `canUseTool`
   callback (`createCanUseTool()` at `src/security/policy-engine.ts:444-451`).

   - During the session, prompt the agent to execute a Bash tool call like: `ls && rm -rf
   ./tmp/dry-run-bug`.

   - The SDK calls `createCanUseTool()`'s inner function (`policy-engine.ts:452-598`) with
   `toolName = 'Bash'` and `input.command = 'ls && rm -rf ./tmp/dry-run-bug'`.

4. Observe how the destructive part of the chain bypasses dry-run interception:

   - `createCanUseTool()` validates the command with `validateCommand()` at
   `src/security/policy-engine.ts:385-431`, which respects `shell.split_chained_commands =
   true` and uses `_splitChainedCommands()` (`policy-engine.ts:867-897`) so both `ls` and
   `rm -rf ./tmp/dry-run-bug` are allowed under the configured allowlist.

   - For dry-run, it then calls `_checkDryRun()` at `policy-engine.ts:241-258`, which for
   `toolName === 'Bash'` invokes `_isReadOnlyCommand(command)` (line `255-257`).

   - `_isReadOnlyCommand()` at `policy-engine.ts:286-300` only looks at the first binary:
   it calls `_extractBaseCommand()` (`policy-engine.ts:903-925`) on the full string `ls &&
   rm -rf ./tmp/dry-run-bug`, yielding base command `ls`. Since `ls` is in
   `READ_ONLY_COMMANDS` (defined at `policy-engine.ts:65-69`), `_isReadOnlyCommand()`
   returns `true`, causing `_checkDryRun()` to exit early (`return null;` at
   `policy-engine.ts:257`) and skip dry-run interception.

   - As a result, `createCanUseTool()` returns `{ behavior: 'allow', updatedInput: input
   }` (`policy-engine.ts:596-597`), the Bash tool executes the *full* chain including `rm
   -rf ./tmp/dry-run-bug`, no entry is added to the dry-run log (`getDryRunLog()` at
   `policy-engine.ts:230-231`), and the user sees only `getPolicySummary()`'s message `Dry
   Run: ENABLED (write operations will be previewed only)` (`policy-engine.ts:671-701`)
   despite a destructive write having executed without any preview.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/security/policy-engine.ts
**Line:** 290:298
**Comment:**
	*Security: The dry-run detector `_isReadOnlyCommand` treats any `Bash` command as read-only based only on its first binary (and some git subcommands), so chained commands like `ls && rm -rf /tmp` or pipelines starting with a "safe" command can bypass dry-run interception and execute destructive operations without preview.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/security/policy-engine.ts (1)

529-594: ⚠️ Potential issue | 🟠 Major

Dry-run intercepted actions still consume budget quota.

Budget enforcement (line 530-543) runs before dry-run interception (line 588-594). Actions that are intercepted by dry-run (never actually executed) still increment the budget counters via recordAction at line 532. In dry-run mode, this causes premature budget exhaustion — the budget tracks hypothetical actions rather than actually executed ones.

Consider moving the dry-run check before budget enforcement, or not recording budget for dry-run-intercepted actions.

Proposed fix: move dry-run check before budget enforcement
+      // ─── Dry-run interception (ASI02) ──────────────────────────────
+      const dryRunResult = this._checkDryRun(toolName, input);
+      if (dryRunResult) {
+        return {
+          behavior: 'deny' as const,
+          message: `[DRY RUN] ${dryRunResult.wouldExecute}`,
+        };
+      }
+
       // ─── Budget enforcement (LLM06/LLM10) ─────────────────────────
       if (this._policy.budget) {
         const actionType = this._classifyAction(toolName, input) ?? 'unknown';
         ...
       }
       ...
-      // ─── Dry-run interception (ASI02) ──────────────────────────────
-      const dryRunResult = this._checkDryRun(toolName, input);
-      if (dryRunResult) {
-        return {
-          behavior: 'deny' as const,
-          message: `[DRY RUN] ${dryRunResult.wouldExecute}`,
-        };
-      }
SECURITY.md (1)

431-438: ⚠️ Potential issue | 🟡 Minor

Update vulnerability reporting URL to match current repository.

Line 435: The advisories link points to https://github.com/ryaker/AgentDev/security/advisories, but this repository is ryaker/zora. Update the URL to https://github.com/ryaker/zora/security/advisories.

🤖 Fix all issues with AI agents
In `@docs/archive/2026-02/README-pre-hardening.md`:
- Around line 62-77: The fenced ASCII diagram block (the triple-backtick block
containing the ORCHESTRATOR CORE / LLM PROVIDER REGISTRY diagram) lacks a
language specifier causing linter warnings; fix it by changing the opening fence
from ``` to ```text (or ```ascii) so the block becomes a labeled code fence and
keep the closing ``` unchanged, ensuring the ASCII box content (lines with ┌┐│└┘
and headings like ORCHESTRATOR CORE, LLM PROVIDER REGISTRY) remains exactly
as-is.

In `@docs/archive/2026-02/SECURITY-pre-hardening.md`:
- Around line 254-258: Update the incorrect vulnerability reporting URLs that
currently point to "https://github.com/ryaker/AgentDev/security/advisories":
find and replace those occurrences in
docs/archive/2026-02/SECURITY-pre-hardening.md (the URL on/around line 256) and
in SECURITY.md (the URL on/around line 435) so they instead point to
"https://github.com/ryaker/zora/security/advisories"; ensure both files contain
the exact corrected URL string and no other references to the old repository
path remain.

In `@README.md`:
- Around line 56-67: Update the OWASP reference in the "v0.6 Security Hardening"
header line: replace the incorrect phrase "OWASP Agentic Top 10 (ASI-2026)" with
the official name "OWASP Top 10 for Agentic AI Applications (2026 edition)";
ensure the updated string appears in the same sentence that currently reads
"Audited against OWASP LLM Top 10 (2025) and OWASP Agentic Top 10 (ASI-2026)" so
the line becomes "Audited against OWASP LLM Top 10 (2025) and OWASP Top 10 for
Agentic AI Applications (2026 edition)".

In `@SECURITY.md`:
- Around line 449-464: Update the SECURITY.md entry for "always_flag interactive
approval" to reflect that enforcement is partially implemented: mention that
PolicyEngine.createCanUseTool() already enforces `_shouldFlag` and
`_flagCallback` when a flag callback is configured (so interactive approvals
work in that configuration), and clarify that full enforcement across all
runtime scenarios is still in progress; reference the
PolicyEngine.createCanUseTool(), `_shouldFlag`, and `_flagCallback` symbols in
the note so readers can find the implementation.

In `@specs/v5/docs/POLICY_REFERENCE.md`:
- Around line 104-106: The table row for the `split_chained_commands` policy
contains an unescaped pipe character in the inline code snippet (`` `&&`, `||`,
`;`, `|` ``) which breaks the Markdown table; update the content in
POLICY_REFERENCE.md for `split_chained_commands` to escape the pipe (e.g., `\|`)
or replace it with an HTML entity so the inline code becomes something like ``
`&&`, `||`, `;`, \| `` to keep the cell a single column and satisfy markdownlint
MD056.

In `@src/cli/daemon.ts`:
- Around line 50-55: The current bare catch around the loadPolicy call masks all
errors; change the catch to capture the exception (e.g., catch (err)) and
differentiate missing-file vs parse errors: if the error indicates a missing
file/ENOENT for policyPath, print the existing "Policy not found..." message and
exit, otherwise print the actual error (err.message or err.stack) so parse
errors are visible and then exit; update the catch block that wraps await
loadPolicy(policyPath) accordingly.

In `@src/cli/index.ts`:
- Around line 91-96: The catch block around loadPolicy(policyPath) incorrectly
treats all errors as "not found"; change it to inspect the thrown error (from
loadPolicy) and handle file-not-found vs parse/other errors: if the error code
or errno indicates ENOENT (file missing) keep the current "Policy not found..."
message, otherwise print a clear parse/validation error including error.message
(e.g., "Error loading policy: <error.message>") and exit; reference loadPolicy
and policyPath when updating the try/catch so parse errors are surfaced instead
of being masked.

In `@src/cli/presets.ts`:
- Around line 37-47: The locked preset's budget fields use 0 (which per
BudgetPolicy JSDoc means "unlimited") and thus contradict the "Zero access"
intent; update the locked preset's budget object in presets.ts (the budget
property on the locked preset) to explicit tight limits (e.g., set
max_actions_per_session to 10 and token_budget to 50000, and keep
max_actions_per_type as an empty map and on_exceed as 'block') so partial
relaxation of the preset doesn't remove budget guardrails; reference the
BudgetPolicy JSDoc in src/types.ts when selecting final numeric limits.
- Around line 74-78: The safe preset incorrectly sets shell_exec_destructive: 0
which is treated as "unlimited" because PolicyEngine checks per-type limits with
if (typeLimit > 0); change PolicyEngine's per-type check (the code that enforces
per-type limits in PolicyEngine / BudgetPolicy handling) to treat a numeric 0 as
"blocked" by checking for presence (e.g., typeLimit !== undefined) and then
explicitly handle typeLimit === 0 as a hard block, otherwise enforce numeric
limits, update the BudgetPolicy interface docs/comments to document that
per-type 0 means "blocked" (global 0 remains unlimited), and add a unit test
that uses the safe preset to assert shell_exec_destructive operations are
blocked.

In `@src/security/intent-capsule.ts`:
- Around line 90-93: The current call to crypto.timingSafeEqual may throw if the
buffers differ in length; update the verification (the code that reads
capsule.signature and compares to expectedSignature using
crypto.timingSafeEqual) to first safely construct both buffers inside a
try/catch (or validate hex), check that Buffer.byteLength(buf1) ===
Buffer.byteLength(buf2), and only then call crypto.timingSafeEqual; if buffer
construction fails or lengths differ, return false instead of letting the
RangeError propagate, ensuring the comparison routine (that references
capsule.signature, expectedSignature, and crypto.timingSafeEqual) fails
gracefully on malformed/tampered input.
🧹 Nitpick comments (13)
docs/archive/2026-02/README-pre-hardening.md (1)

1-1: Consider documenting the path context for archived content.

The relative paths for images (lines 1, 9) and documentation links (lines 110-116) reference locations relative to the repository root. Since this archived file is located at docs/archive/2026-02/, these paths won't resolve correctly when viewed from the archive location.

While this may be acceptable for historical snapshots, consider adding a brief note at the top indicating that paths reference the original structure, or updating paths to work from the archive location (e.g., ../../../specs/v5/assets/...).

Also applies to: 9-9, 110-116

src/security/prompt-defense.ts (1)

98-123: sanitizeToolOutput duplicates sanitizeInput — extract shared logic.

Both functions build the identical allPatterns array and apply the same global-flag + replace loop. The only difference is the wrapper tag. This violates DRY and means any future pattern or logic change must be applied in two places.

Also, the docstring claims this is "more aggressive than sanitizeInput()" but both functions apply the exact same pattern set — the only difference is the tag name. Either make it genuinely more aggressive (e.g., additional tool-specific patterns) or correct the docstring.

♻️ Proposed refactor to extract shared logic
+function wrapInjectionPatterns(content: string, tag: string): string {
+  let result = content;
+  const allPatterns = [
+    ...INJECTION_PATTERNS,
+    ...ENCODED_INJECTION_PATTERNS,
+    ...RAG_INJECTION_PATTERNS,
+  ];
+  for (const pattern of allPatterns) {
+    const globalPattern = pattern.global
+      ? pattern
+      : new RegExp(pattern.source, pattern.flags + 'g');
+    result = result.replace(globalPattern, (match) => `<${tag}>${match}</${tag}>`);
+  }
+  return result;
+}
+
 export function sanitizeInput(content: string): string {
-  let result = content;
-
-  const allPatterns = [...INJECTION_PATTERNS, ...ENCODED_INJECTION_PATTERNS, ...RAG_INJECTION_PATTERNS];
-
-  for (const pattern of allPatterns) {
-    // Ensure global flag is set so all occurrences are replaced, not just the first
-    const globalPattern = pattern.global
-      ? pattern
-      : new RegExp(pattern.source, pattern.flags + 'g');
-    result = result.replace(globalPattern, (match) => `<untrusted_content>${match}</untrusted_content>`);
-  }
-
-  return result;
+  return wrapInjectionPatterns(content, 'untrusted_content');
 }
 
 export function sanitizeToolOutput(content: string): string {
-  let result = content;
-
-  const allPatterns = [
-    ...INJECTION_PATTERNS,
-    ...ENCODED_INJECTION_PATTERNS,
-    ...RAG_INJECTION_PATTERNS,
-  ];
-
-  for (const pattern of allPatterns) {
-    const globalPattern = pattern.global
-      ? pattern
-      : new RegExp(pattern.source, pattern.flags + 'g');
-    result = result.replace(
-      globalPattern,
-      (match) => `<untrusted_tool_output>${match}</untrusted_tool_output>`,
-    );
-  }
-
-  return result;
+  return wrapInjectionPatterns(content, 'untrusted_tool_output');
 }
specs/v5/docs/POLICY_PRESETS.md (1)

35-46: Locked preset: dry-run is redundant given zero budget.

With max_actions_per_session = 0 and on_exceed = "block", the budget will block all actions before dry-run interception ever fires. Having dry_run.enabled = true is harmless (defense-in-depth), but could confuse users who think dry-run is doing the blocking. Consider adding a brief comment in the doc noting that the budget is the primary gate here.

src/orchestrator/orchestrator.ts (1)

99-105: Session ID uses Date.now() — not unique under concurrent boots.

session_${Date.now()} could collide if two sessions start within the same millisecond (unlikely but possible in automated/test scenarios). Consider appending a random suffix similar to the jobId pattern on line 235.

Proposed fix
-    this._policyEngine.startSession(`session_${Date.now()}`);
+    this._policyEngine.startSession(`session_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`);
src/cli/daemon.ts (1)

47-48: Both index.ts and daemon.ts duplicate the createProviders function and policy-loading boilerplate.

createProviders (lines 19-33 here, lines 44-63 in index.ts) and the policy-loading try/catch block are nearly identical. Consider extracting both into a shared setup utility to reduce duplication, especially since both files now depend on the same centralized loader.

src/types.ts (1)

1-6: Stale version in file header.

The comment references "v0.5" but this PR targets v0.6.

📝 Suggested fix
-/**
- * Zora Core Types — v0.5
+/**
+ * Zora Core Types — v0.6
tests/unit/security/intent-capsule.test.ts (1)

82-87: Minor grammar nit in test name.

'uses different signing keys produce different signatures' reads awkwardly.

📝 Suggested fix
-    it('uses different signing keys produce different signatures', () => {
+    it('rejects capsule verified with a different signing key', () => {
src/config/policy-loader.ts (2)

29-77: No runtime validation of TOML value types.

All fields use bare as casts (e.g., as string[], as boolean, as number). If a user writes allowed_paths = 42 in their TOML, the cast silently passes a number where a string[] is expected, producing confusing downstream errors rather than a clear parse-time message.

Since TOML is typed, this is unlikely in well-formed files, but a "fail-fast with a helpful message" approach would be more robust for a security-critical policy loader.


15-23: loadPolicy doesn't handle TOML parse errors gracefully.

If parseTOML throws on malformed TOML, the error propagates as an opaque smol-toml internal error. Consider wrapping it with a user-friendly message indicating the file path.

Proposed improvement
 export async function loadPolicy(policyPath: string): Promise<ZoraPolicy> {
   if (!fs.existsSync(policyPath)) {
     throw new Error(`Policy file not found at ${policyPath}. Run \`zora init\` first.`);
   }

   const { parse: parseTOML } = await import('smol-toml');
-  const raw = parseTOML(fs.readFileSync(policyPath, 'utf-8')) as Record<string, unknown>;
-  return parsePolicy(raw);
+  let raw: Record<string, unknown>;
+  try {
+    raw = parseTOML(fs.readFileSync(policyPath, 'utf-8')) as Record<string, unknown>;
+  } catch (err) {
+    throw new Error(`Failed to parse policy file at ${policyPath}: ${err instanceof Error ? err.message : String(err)}`);
+  }
+  return parsePolicy(raw);
 }
src/security/intent-capsule.ts (2)

158-160: getActiveCapsule returns a mutable reference, unlike getDriftHistory.

getDriftHistory() (line 166) defensively copies with [...this._driftHistory], but getActiveCapsule() returns the internal capsule object directly. A caller mutating the returned object would corrupt the manager's state and break subsequent checkDrift calls and signature verification.

Return a shallow copy for consistency
   getActiveCapsule(): IntentCapsule | null {
-    return this._activeCapsule;
+    return this._activeCapsule ? { ...this._activeCapsule } : null;
   }

131-153: Keyword overlap drift detection has a very low threshold (10%) and uses linear search.

The 10% keyword overlap threshold at line 138 means a single common keyword in a 10-word action detail is enough to pass. This is intentionally permissive (to avoid false positives in legitimate workflows), but it also makes it easy for an injected action to include one mandate keyword to evade drift detection. Just worth being aware of in threat modeling.

src/security/policy-engine.ts (2)

529-585: _classifyAction is called three times with identical arguments.

_classifyAction(toolName, input) is invoked at line 531 (budget), line 546 (always_flag), and line 566 (drift check) within the same canUseTool invocation. Compute it once and reuse the result.

Proposed consolidation
+      const action = this._classifyAction(toolName, input);
+
       // ─── Budget enforcement (LLM06/LLM10) ─────────────────────────
       if (this._policy.budget) {
-        const actionType = this._classifyAction(toolName, input) ?? 'unknown';
+        const actionType = action ?? 'unknown';
         const budgetResult = this.recordAction(actionType);
         ...
       }

       // Check always_flag for actions that require approval
-      const action = this._classifyAction(toolName, input);
       if (action && this._shouldFlag(action)) {
         ...
       }

       // ─── Intent capsule drift check (ASI01) ────────────────────────
       if (this._intentCapsuleManager) {
-        const driftAction = this._classifyAction(toolName, input) ?? 'unknown';
+        const driftAction = action ?? 'unknown';
         ...
       }

122-150: Budget counter increments even when the action is denied.

recordAction increments _totalActions and _actionCounts (lines 126-127) before checking limits. This means denied actions inflate the counters. If the intent is to track attempted actions, this is fine. If the intent is to track allowed actions, the increment should happen after the limit check passes.

Given the on_exceed = 'flag' flow where a callback can approve over-budget actions, the current approach means every subsequent action also requires approval once the limit is hit (since the counter keeps climbing). This seems intentional but is worth a doc comment to avoid confusion.

Comment on lines +62 to +77
```
┌─────────────────────────────────────────────────┐
│ ORCHESTRATOR CORE │
│ Router → Execution Loop → Failover Controller │
│ Retry Queue │ Session Manager │
├─────────────────────────────────────────────────┤
│ LLM PROVIDER REGISTRY │
│ Claude (Primary) │ Gemini (Secondary) │
│ Agent SDK (Native) │ CLI (Subprocess) │
├─────────────────────────────────────────────────┤
│ Tools │ Memory │ Security │
│ Shell │ MEMORY.md │ Policy Engine │
│ Filesystem │ Daily Notes │ Audit Log │
│ Web │ Context Loader │ Restrictive FS │
└─────────────────────────────────────────────────┘
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language specifier to the fenced code block.

The architecture diagram code block is missing a language identifier, which is flagged by the markdown linter. Adding text or ascii improves rendering consistency across different markdown parsers.

📝 Proposed fix
-```
+```text
 ┌─────────────────────────────────────────────────┐
 │                ORCHESTRATOR CORE                │
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
┌─────────────────────────────────────────────────┐
│ ORCHESTRATOR CORE │
│ Router → Execution Loop → Failover Controller │
│ Retry Queue │ Session Manager │
├─────────────────────────────────────────────────┤
│ LLM PROVIDER REGISTRY │
│ Claude (Primary) │ Gemini (Secondary) │
│ Agent SDK (Native) │ CLI (Subprocess) │
├─────────────────────────────────────────────────┤
│ Tools │ Memory │ Security │
│ Shell │ MEMORY.md │ Policy Engine │
│ Filesystem │ Daily Notes │ Audit Log │
│ Web │ Context Loader │ Restrictive FS │
└─────────────────────────────────────────────────┘
```
🤖 Prompt for AI Agents
In `@docs/archive/2026-02/README-pre-hardening.md` around lines 62 - 77, The
fenced ASCII diagram block (the triple-backtick block containing the
ORCHESTRATOR CORE / LLM PROVIDER REGISTRY diagram) lacks a language specifier
causing linter warnings; fix it by changing the opening fence from ``` to
```text (or ```ascii) so the block becomes a labeled code fence and keep the
closing ``` unchanged, ensuring the ASCII box content (lines with ┌┐│└┘ and
headings like ORCHESTRATOR CORE, LLM PROVIDER REGISTRY) remains exactly as-is.

Comment on lines +254 to +258
Please use GitHub Security Advisories for private disclosure:

**https://github.com/ryaker/AgentDev/security/advisories**

If GitHub advisories are not available to you, open a GitHub issue with the minimum necessary detail and note that you can provide a private report if contacted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the actual URL at line 256 in the target file
sed -n '254,258p' docs/archive/2026-02/SECURITY-pre-hardening.md

# Also check what repository this code is in
cat .git/config 2>/dev/null | grep -A 1 'url =' || echo "Git config not available"

# Check all SECURITY*.md files for security/advisories references
rg -n 'security/advisories' --glob '*.md' -A 1 -B 1

Repository: ryaker/zora

Length of output: 727


Vulnerability reporting URLs reference the wrong repository.

Lines 256 in docs/archive/2026-02/SECURITY-pre-hardening.md and line 435 in SECURITY.md both point to github.com/ryaker/AgentDev/security/advisories, but the repository is github.com/ryaker/zora. Update both URLs to https://github.com/ryaker/zora/security/advisories.

🤖 Prompt for AI Agents
In `@docs/archive/2026-02/SECURITY-pre-hardening.md` around lines 254 - 258,
Update the incorrect vulnerability reporting URLs that currently point to
"https://github.com/ryaker/AgentDev/security/advisories": find and replace those
occurrences in docs/archive/2026-02/SECURITY-pre-hardening.md (the URL on/around
line 256) and in SECURITY.md (the URL on/around line 435) so they instead point
to "https://github.com/ryaker/zora/security/advisories"; ensure both files
contain the exact corrected URL string and no other references to the old
repository path remain.

Comment thread README.md
Comment on lines +56 to +67
**v0.6 Security Hardening** — Audited against OWASP LLM Top 10 (2025) and OWASP Agentic Top 10 (ASI-2026):

| Defense | What It Does |
|---------|-------------|
| **Action Budgets** | Per-session limits on tool invocations and token spend prevent unbounded loops |
| **Dry-Run Mode** | Preview write operations without executing — test policies safely |
| **Intent Capsules** | HMAC-SHA256 signed mandates detect goal hijacking from injected instructions |
| **RAG Injection Defense** | 20+ patterns detect prompt injection in tool outputs and RAG documents |
| **Hash-Chain Audit** | SHA-256 chained append-only log with tamper detection |
| **AES-256-GCM Secrets** | Encrypted credential storage with PBKDF2 key derivation |

See **[SECURITY.md](SECURITY.md)** for the full security guide and OWASP compliance matrix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OWASP Agentic AI Security Top 10 ASI 2026 official name

💡 Result:

The official OWASP ASI “Agentic AI Security Top 10” (2026 edition) is named “OWASP Top 10 for Agentic Applications” (also referred to in OWASP materials as the “OWASP Top 10 for Agentic AI Applications”). [1], [2]


Update OWASP standard naming on line 56.

The reference "OWASP Agentic Top 10 (ASI-2026)" is incorrect. The official OWASP standard is named "OWASP Top 10 for Agentic AI Applications" (2026 edition). Update line 56 to use the correct official naming to maintain credibility in security documentation.

🤖 Prompt for AI Agents
In `@README.md` around lines 56 - 67, Update the OWASP reference in the "v0.6
Security Hardening" header line: replace the incorrect phrase "OWASP Agentic Top
10 (ASI-2026)" with the official name "OWASP Top 10 for Agentic AI Applications
(2026 edition)"; ensure the updated string appears in the same sentence that
currently reads "Audited against OWASP LLM Top 10 (2025) and OWASP Agentic Top
10 (ASI-2026)" so the line becomes "Audited against OWASP LLM Top 10 (2025) and
OWASP Top 10 for Agentic AI Applications (2026 edition)".

Comment thread SECURITY.md
Comment on lines +449 to 464
| Path allow/deny enforcement | Enforced via PolicyEngine |
| Shell command allow/deny enforcement | Enforced via PolicyEngine |
| Symlink boundary checks | Enforced |
| Agent sees its own policy boundaries | Policy injected into system prompt |
| `check_permissions` tool (agent self-checks) | Available to agent |
| Hash-chain audit trail | Working |
| Action budgets (per-session + per-type) | Enforced via PolicyEngine |
| Token budget enforcement | Enforced via PolicyEngine |
| Dry-run preview mode | Enforced via PolicyEngine |
| Intent capsules (mandate signing) | Active in orchestrator |
| Goal drift detection | Active with flag callback |
| RAG injection pattern detection | Active in PromptDefense |
| Tool output sanitization | Active via sanitizeToolOutput() |
| `always_flag` interactive approval | Config parsed, enforcement in progress |
| Runtime permission expansion (mid-task grants) | Planned |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

always_flag status may be understated.

Line 462 says "enforcement in progress," but PolicyEngine.createCanUseTool() (lines 546-562 in policy-engine.ts) already implements _shouldFlag + _flagCallback enforcement. Consider updating the status to reflect partial enforcement (works when a flag callback is configured).

🤖 Prompt for AI Agents
In `@SECURITY.md` around lines 449 - 464, Update the SECURITY.md entry for
"always_flag interactive approval" to reflect that enforcement is partially
implemented: mention that PolicyEngine.createCanUseTool() already enforces
`_shouldFlag` and `_flagCallback` when a flag callback is configured (so
interactive approvals work in that configuration), and clarify that full
enforcement across all runtime scenarios is still in progress; reference the
PolicyEngine.createCanUseTool(), `_shouldFlag`, and `_flagCallback` symbols in
the note so readers can find the implementation.

Comment on lines +104 to +106
| `denied_commands` | string[] | `[]` | Commands blocked. In `"denylist"` mode, these are the only ones blocked. |
| `split_chained_commands` | bool | `true` | Parse chained commands (`&&`, `||`, `;`, `|`) and validate each individually. |
| `max_execution_time` | string | `"1m"` | Maximum wall-clock time for a single command. Format: `"30s"`, `"5m"`, `"1h"`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pipe character | in table cell breaks Markdown rendering.

The | characters inside the description of split_chained_commands (within `&&`, `||`, `;`, `|`) are interpreted as table column delimiters, causing the row to have too many columns. This is confirmed by the markdownlint MD056 warning. Escape pipes inside table cells with \|.

📝 Proposed fix
-| `split_chained_commands` | bool | `true` | Parse chained commands (`&&`, `||`, `;`, `|`) and validate each individually. |
+| `split_chained_commands` | bool | `true` | Parse chained commands (`&&`, `\|\|`, `;`, `\|`) and validate each individually. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `denied_commands` | string[] | `[]` | Commands blocked. In `"denylist"` mode, these are the only ones blocked. |
| `split_chained_commands` | bool | `true` | Parse chained commands (`&&`, `||`, `;`, `|`) and validate each individually. |
| `max_execution_time` | string | `"1m"` | Maximum wall-clock time for a single command. Format: `"30s"`, `"5m"`, `"1h"`. |
| `denied_commands` | string[] | `[]` | Commands blocked. In `"denylist"` mode, these are the only ones blocked. |
| `split_chained_commands` | bool | `true` | Parse chained commands (`&&`, `\|\|`, `;`, `\|`) and validate each individually. |
| `max_execution_time` | string | `"1m"` | Maximum wall-clock time for a single command. Format: `"30s"`, `"5m"`, `"1h"`. |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 105-105: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 105-105: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 105-105: Table column count
Expected: 4; Actual: 7; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In `@specs/v5/docs/POLICY_REFERENCE.md` around lines 104 - 106, The table row for
the `split_chained_commands` policy contains an unescaped pipe character in the
inline code snippet (`` `&&`, `||`, `;`, `|` ``) which breaks the Markdown
table; update the content in POLICY_REFERENCE.md for `split_chained_commands` to
escape the pipe (e.g., `\|`) or replace it with an HTML entity so the inline
code becomes something like `` `&&`, `||`, `;`, \| `` to keep the cell a single
column and satisfy markdownlint MD056.

Comment thread src/cli/daemon.ts
Comment on lines +50 to 55
try {
policy = await loadPolicy(policyPath);
} catch {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same error-masking issue as in src/cli/index.ts.

The bare catch swallows parse errors from loadPolicy, reporting them all as "Policy not found." Apply the same fix as suggested for index.ts.

Proposed fix
-  try {
-    policy = await loadPolicy(policyPath);
-  } catch {
-    console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
+  try {
+    policy = await loadPolicy(policyPath);
+  } catch (err) {
+    console.error(err instanceof Error ? err.message : 'Failed to load policy at ~/.zora/policy.toml. Run `zora init` first.');
     process.exit(1);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
policy = await loadPolicy(policyPath);
} catch {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
process.exit(1);
}
try {
policy = await loadPolicy(policyPath);
} catch (err) {
console.error(err instanceof Error ? err.message : 'Failed to load policy at ~/.zora/policy.toml. Run `zora init` first.');
process.exit(1);
}
🤖 Prompt for AI Agents
In `@src/cli/daemon.ts` around lines 50 - 55, The current bare catch around the
loadPolicy call masks all errors; change the catch to capture the exception
(e.g., catch (err)) and differentiate missing-file vs parse errors: if the error
indicates a missing file/ENOENT for policyPath, print the existing "Policy not
found..." message and exit, otherwise print the actual error (err.message or
err.stack) so parse errors are visible and then exit; update the catch block
that wraps await loadPolicy(policyPath) accordingly.

Comment thread src/cli/index.ts
Comment on lines +91 to 96
try {
policy = await loadPolicy(policyPath);
} catch {
console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Catch block masks TOML parse errors as "not found".

If policy.toml exists but contains invalid TOML, loadPolicy will throw a parse error, but the catch block always prints "Policy not found." This will confuse users who have a policy file but with a syntax error.

Proposed fix
-  try {
-    policy = await loadPolicy(policyPath);
-  } catch {
-    console.error('Policy not found at ~/.zora/policy.toml. Run `zora init` first.');
+  try {
+    policy = await loadPolicy(policyPath);
+  } catch (err) {
+    console.error(err instanceof Error ? err.message : 'Failed to load policy at ~/.zora/policy.toml. Run `zora init` first.');
     process.exit(1);
   }
🤖 Prompt for AI Agents
In `@src/cli/index.ts` around lines 91 - 96, The catch block around
loadPolicy(policyPath) incorrectly treats all errors as "not found"; change it
to inspect the thrown error (from loadPolicy) and handle file-not-found vs
parse/other errors: if the error code or errno indicates ENOENT (file missing)
keep the current "Policy not found..." message, otherwise print a clear
parse/validation error including error.message (e.g., "Error loading policy:
<error.message>") and exit; reference loadPolicy and policyPath when updating
the try/catch so parse errors are surfaced instead of being masked.

Comment thread src/cli/presets.ts
Comment on lines +37 to +47
budget: {
max_actions_per_session: 0,
max_actions_per_type: {},
token_budget: 0,
on_exceed: 'block',
},
dry_run: {
enabled: true,
tools: [],
audit_dry_runs: true,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

locked preset budget values of 0 mean "unlimited" per BudgetPolicy docs — contradicts intent.

According to the BudgetPolicy JSDoc in src/types.ts (Line 346–350), max_actions_per_session: 0 and token_budget: 0 both mean unlimited. For a preset described as "Zero access," this is semantically contradictory and reduces defense-in-depth.

While the locked preset's other controls (empty allowed_paths, deny_all shell mode) prevent execution in practice, an operator who partially relaxes the locked preset (e.g., adding one allowed path) would inadvertently have no budget guardrails.

Consider setting tight explicit limits (e.g., max_actions_per_session: 10, token_budget: 50_000) rather than relying on 0 = unlimited.

🛡️ Proposed fix
     budget: {
-      max_actions_per_session: 0,
+      max_actions_per_session: 10,
       max_actions_per_type: {},
-      token_budget: 0,
+      token_budget: 50_000,
       on_exceed: 'block',
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
budget: {
max_actions_per_session: 0,
max_actions_per_type: {},
token_budget: 0,
on_exceed: 'block',
},
dry_run: {
enabled: true,
tools: [],
audit_dry_runs: true,
},
budget: {
max_actions_per_session: 10,
max_actions_per_type: {},
token_budget: 50_000,
on_exceed: 'block',
},
dry_run: {
enabled: true,
tools: [],
audit_dry_runs: true,
},
🤖 Prompt for AI Agents
In `@src/cli/presets.ts` around lines 37 - 47, The locked preset's budget fields
use 0 (which per BudgetPolicy JSDoc means "unlimited") and thus contradict the
"Zero access" intent; update the locked preset's budget object in presets.ts
(the budget property on the locked preset) to explicit tight limits (e.g., set
max_actions_per_session to 10 and token_budget to 50000, and keep
max_actions_per_type as an empty map and on_exceed as 'block') so partial
relaxation of the preset doesn't remove budget guardrails; reference the
BudgetPolicy JSDoc in src/types.ts when selecting final numeric limits.

Comment thread src/cli/presets.ts
Comment on lines +74 to +78
budget: {
max_actions_per_session: 100,
max_actions_per_type: { shell_exec: 20, shell_exec_destructive: 0 },
token_budget: 200_000,
on_exceed: 'block',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "policy-engine.ts" -o -name "*budget*.ts" | head -20

Repository: ryaker/zora

Length of output: 130


🏁 Script executed:

# Search for PolicyEngine and budget-related files
fd -e ts | grep -E "(policy|budget)" | head -30

Repository: ryaker/zora

Length of output: 196


🏁 Script executed:

# Check the BudgetPolicy interface and PolicyEngine implementation
rg -n 'max_actions_per_type' --type=ts -A 10 -B 5 | head -100

Repository: ryaker/zora

Length of output: 5824


🏁 Script executed:

# Look for per-type budget handling logic
rg -n 'per.type|per_type' --type=ts -A 5 -B 2 | head -150

Repository: ryaker/zora

Length of output: 9277


Per-type limit of 0 is treated as "unlimited" — not "blocked" — a bug in the safe preset.

The PolicyEngine checks per-type limits with the condition if (typeLimit > 0) at lines 138–139. When shell_exec_destructive: 0 in the safe preset, this check is skipped entirely, allowing unlimited destructive commands. This contradicts the intent to block them. The BudgetPolicy interface also lacks documentation of per-type 0 semantics, unlike the global limits which explicitly document 0 = unlimited. No test coverage exists for this scenario.

🤖 Prompt for AI Agents
In `@src/cli/presets.ts` around lines 74 - 78, The safe preset incorrectly sets
shell_exec_destructive: 0 which is treated as "unlimited" because PolicyEngine
checks per-type limits with if (typeLimit > 0); change PolicyEngine's per-type
check (the code that enforces per-type limits in PolicyEngine / BudgetPolicy
handling) to treat a numeric 0 as "blocked" by checking for presence (e.g.,
typeLimit !== undefined) and then explicitly handle typeLimit === 0 as a hard
block, otherwise enforce numeric limits, update the BudgetPolicy interface
docs/comments to document that per-type 0 means "blocked" (global 0 remains
unlimited), and add a unit test that uses the safe preset to assert
shell_exec_destructive operations are blocked.

Comment on lines +90 to +93
return crypto.timingSafeEqual(
Buffer.from(capsule.signature, 'hex'),
Buffer.from(expectedSignature, 'hex'),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

timingSafeEqual throws on length mismatch — tampered capsules crash instead of returning false.

If capsule.signature is malformed (not valid hex, truncated, or padded), Buffer.from(capsule.signature, 'hex') may produce a buffer of a different length than expectedSignature. crypto.timingSafeEqual throws a RangeError when buffer lengths differ, causing an unhandled exception instead of gracefully returning false.

Proposed fix: guard against length mismatch
   verifyCapsule(capsule: IntentCapsule): boolean {
     const payload = JSON.stringify({
       capsuleId: capsule.capsuleId,
       mandate: capsule.mandate,
       mandateHash: capsule.mandateHash,
       mandateKeywords: capsule.mandateKeywords,
       allowedActionCategories: capsule.allowedActionCategories,
       createdAt: capsule.createdAt,
       expiresAt: capsule.expiresAt,
     });

     const expectedSignature = crypto
       .createHmac('sha256', this._signingKey)
       .update(payload)
       .digest('hex');

+    const sigBuf = Buffer.from(capsule.signature, 'hex');
+    const expBuf = Buffer.from(expectedSignature, 'hex');
+
+    if (sigBuf.length !== expBuf.length) return false;
+
     return crypto.timingSafeEqual(
-      Buffer.from(capsule.signature, 'hex'),
-      Buffer.from(expectedSignature, 'hex'),
+      sigBuf,
+      expBuf,
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return crypto.timingSafeEqual(
Buffer.from(capsule.signature, 'hex'),
Buffer.from(expectedSignature, 'hex'),
);
const sigBuf = Buffer.from(capsule.signature, 'hex');
const expBuf = Buffer.from(expectedSignature, 'hex');
if (sigBuf.length !== expBuf.length) return false;
return crypto.timingSafeEqual(
sigBuf,
expBuf,
);
🤖 Prompt for AI Agents
In `@src/security/intent-capsule.ts` around lines 90 - 93, The current call to
crypto.timingSafeEqual may throw if the buffers differ in length; update the
verification (the code that reads capsule.signature and compares to
expectedSignature using crypto.timingSafeEqual) to first safely construct both
buffers inside a try/catch (or validate hex), check that Buffer.byteLength(buf1)
=== Buffer.byteLength(buf2), and only then call crypto.timingSafeEqual; if
buffer construction fails or lengths differ, return false instead of letting the
RangeError propagate, ensuring the comparison routine (that references
capsule.signature, expectedSignature, and crypto.timingSafeEqual) fails
gracefully on malformed/tampered input.

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

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants