Skip to content

Commit f520db7

Browse files
backnotpropclaude
andauthored
Feat/claude code updates (#74)
* feat(core): add ModificationObject and Modify decision variant Add support for the `modify` decision verb: - Add ModificationObject struct with priority field (1-100, higher wins) - Add modifications field to DecisionSet - Add FinalDecision::Modify variant with reason, updated_input, agent_messages - Add helper methods: has_modifications(), is_modify(), updated_input(), is_ask() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(core): add Modify synthesis with module refactoring Implement Modify decision synthesis with priority-based merge: - Add Modify between Ask and AllowOverride in priority hierarchy - Implement merge_modifications() with deep merge and priority resolution - Add collect_modification_agent_messages() for context extraction Refactor synthesis into module structure: - Extract tests (~340 lines) to synthesis/tests.rs - Extract merge logic (~80 lines) to synthesis/merge_input_updates.rs - Reduce mod.rs from 720 to ~310 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(harness): add EngineDecision::Modify variant Add Modify variant to EngineDecision enum with reason and updated_input fields for harness-level handling of modify decisions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(harness): handle Modify decision in all harness adapters Update adapt_decision() in all harnesses: - ClaudeHarness: Full support with EngineDecision::Modify - FactoryHarness: Full support with EngineDecision::Modify - CursorHarness: Treat Modify as Allow (no updatedInput support) - OpenCodeHarness: Treat Modify as Allow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(claude-code): add Modify support to response builders Handle EngineDecision::Modify in all Claude Code response builders: - pre_tool_use.rs: Set permissionDecision=allow with updatedInput - context_injection.rs: Treat as Allow with context - feedback_loop.rs: Treat as Allow - generic.rs: Treat as Allow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(factory): add Modify support to response builders Handle EngineDecision::Modify in all Factory response builders: - pre_tool_use.rs: Set permissionDecision=allow with updatedInput - context_injection.rs: Treat as Allow - feedback_loop.rs: Treat as Allow - generic.rs: Treat as Allow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cursor): handle Modify decision in response builders Treat EngineDecision::Modify as Allow in Cursor response builders (Cursor protocol does not support updatedInput): - before_mcp_execution.rs: Modify → Allow - before_read_file.rs: Modify → Allow - before_shell_execution.rs: Modify → Allow - before_submit_prompt.rs: Modify → Continue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(core): handle Modify in engine and debug output Update engine and debug modules for Modify decision: - engine/mod.rs: Add FinalDecision::Modify cases in match statements - debug.rs: Add Modify case for debug file output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(validator): add DecisionVerb::Modify for PreToolUse Add Modify to decision-event validation matrix: - Add DecisionVerb::Modify enum variant - Update all(), from_rego_name(), rego_name(), description() - Add Modify to PreToolUse compatibility list only - Add incompatibility message for non-PreToolUse events - Add test_modify_only_pre_tool_use test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(rego): add modifications collection to evaluate templates Add "modifications": collect_verbs("modify") to decision_set in all system evaluate.rego templates: - cupcake-cli/src/main.rs (SYSTEM_EVALUATE_TEMPLATE) - fixtures/{claude,cursor,factory,opencode}/system/evaluate.rego - examples/fixtures/system/evaluate.rego - cupcake-core/tests/fixtures/{system_evaluate,global_system_evaluate}.rego - cupcake-py/test-fixtures/.cupcake/policies/system/evaluate.rego - cupcake-ts/test-fixtures/.cupcake/policies/claude/system/evaluate.rego 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(factory): add modify decision integration test Add test_modify_decision_generates_allow_with_updated_input test to verify modify decisions correctly generate allow responses with updatedInput field in Factory harness. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add modify verb documentation Update documentation to reflect the new `modify` decision verb: - reference/policies/custom.md: Add modify to decision verbs table with example showing priority-based input transformation - reference/harnesses/claude-code.md: Add Can Modify column to events table, document updatedInput response format - reference/harnesses/factory-ai.md: Update to note Claude Code now also supports input modification - reference/harnesses/cursor.md: Add input modification row noting it's not supported in Cursor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(readme): add Modify decision to Decisions & Feedback section Update main README to document the new modify decision type that allows policies to transform tool input before execution. Also fixed context injection note to include Factory AI support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: remove allow_override decision verb Remove the allow_override decision verb which was designed for an unimplemented "deny-by-default" mode. In the current allow-by-default architecture, Claude never sees the permissionDecisionReason when the decision is "allow", making this feature non-functional. Changes: - Remove AllowOverride from DecisionVerb enum and FinalDecision - Remove allow_override from DecisionSet struct - Remove from synthesis logic and harness adapters - Update all evaluate.rego templates to remove allow_overrides - Update tests that used allow_override to use add_context instead - Update documentation to remove allow_override references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * readme updates * Fix SLSA badge link in README Updated SLSA badge link in README.md. * readme updates * readme updates * readme updates * readme updates * Add PermissionRequest hook event and deprecate block for PreToolUse PermissionRequest is a new Claude Code hook that fires when the user is shown a permission dialog, allowing policies to auto-approve or auto-deny. PermissionRequest implementation: - New event payload with tool_name, tool_input, tool_use_id fields - Response format uses nested decision object with behavior (allow/deny), updatedInput, message, and interrupt fields - No "ask" behavior - this IS the ask dialog - Supported verbs: halt, deny, block, modify (no ask, no add_context) Deprecation warning for block + PreToolUse: - Using 'block' for PreToolUse now emits a deprecation warning - Users should migrate to 'deny' for pre-execution rejection - 'block' is for post-execution feedback loops (PostToolUse) - This will become an error in a future version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add permission_mode, tool_use_id, and notification_type fields to Claude Code events Implements new input fields from Claude Code hooks specification: - permission_mode: Added to CommonEventData (all events) with PermissionMode enum - tool_use_id: Added to PreToolUse and PostToolUse payloads - notification_type: Added to Notification payload with NotificationType enum 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add SOC/SIEM telemetry export and consolidate docs Telemetry feature: - Add TelemetryConfig to rulebook (enabled, format, destination) - Add JSON serialization to DebugCapture for structured export - Wire up telemetry in CLI eval command Documentation: - Consolidate watchdog docs into single reference page - Add enterprise section (global-config, policy-registry, soc-siem, enterprise-pro) - Reorganize nav structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix clippy warnings and formatting in catalog code Auto-fixed uninlined format args warnings from clippy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * add amp to readme --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ed4375d commit f520db7

92 files changed

Lines changed: 2849 additions & 1049 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/debug-claude.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ jobs:
172172
"denials": [],
173173
"blocks": [],
174174
"asks": [],
175-
"allow_overrides": [],
175+
"modifications": [],
176176
"add_context": []
177177
}
178178
}

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,32 @@ Make AI agents follow the rules.
1818
**Policy enforcement** layer for AI agents; yielding better performance and security **without consuming model context**.
1919

2020
- **Deterministic rule-following** for your agents.
21-
- **Better performance** by moving rules out of context and into guarantees.
21+
- **Better performance** by moving rules out of context and into policy-as-code.
2222
- **LLM-as-a-judge** for more dynamic governance.
2323
- **Trigger alerts** and put _bad_ agents in timeout when they repeatedly violate rules.
2424

25-
Cupcake intercepts agent events and evaluates them against **user-defined rules** written in **[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) [Rego](https://www.openpolicyagent.org/docs/policy-language)**. Agent actions can be blocked, modified, and auto-corrected by providing the agent helpful feedback. Additional benefits include reactive automation for tasks you dont need to rely on the agent to conduct (like linting after a file edit).
25+
Cupcake intercepts agent events and evaluates them against **user-defined rules** written in **[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) [Rego](https://www.openpolicyagent.org/docs/policy-language)** or **Typescript policy programs** that abstract Rego. Agent actions can be blocked, modified, and auto-corrected by providing the agent helpful feedback. Additional benefits include reactive automation for tasks you dont need to rely on the agent to conduct (like linting after a file edit).
2626

2727
## Updates
2828

2929
**`2025-12-09`**: Official open source release. Roadmap will be produced in Q1 2026.
3030

31-
**`2025-04-04`**: We produce the [feature request](https://github.com/anthropics/claude-code/issues/712) for Claude Code Hooks. Runtime alignement requires integration into the agent harnesses, and we pivot away from filesystem and os-level monitoring of agent behavior (early cupcake PoC).
31+
**`2025-04-04`**: We produce the [feature request](https://github.com/anthropics/claude-code/issues/712) for Claude Code Hooks. Runtime alignment requires integration into the agent harnesses, and we pivot away from filesystem and os-level monitoring of agent behavior (early cupcake PoC).
3232

3333
## Supported Agent Harnesses
3434

35-
Cupcake provides **native integrations** for multiple AI coding agents:
35+
Cupcake provides lightweight **native integrations** for multiple AI coding agents:
3636

37-
| Harness | Status | Integration Guide |
38-
| --------------------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------------------- |
39-
| **[Claude Code](https://claude.ai/code)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/claude-code.md) |
40-
| **[Cursor](https://cursor.com)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/cursor.md) |
41-
| **[Factory AI](https://docs.factory.ai/welcome)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/factory.md) |
42-
| **[OpenCode](https://opencode.ai)** | ✅ Fully Supported | [Setup Guide](./docs/agents/opencode/quickstart.md) |
43-
| **[Gemini CLI](https://docs.cloud.google.com/gemini/docs/codeassist/gemini-cli)** | Coming soon | [Awaiting PR](https://github.com/google-gemini/gemini-cli/issues/2779) |
37+
| Harness | Status | Integration Guide |
38+
| --------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
39+
| **[Claude Code](https://claude.ai/code)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/claude-code.md) |
40+
| **[Cursor](https://cursor.com)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/cursor.md) |
41+
| **[Factory AI](https://docs.factory.ai/welcome)** | ✅ Fully Supported | [Setup Guide](./docs/user-guide/harnesses/factory.md) |
42+
| **[OpenCode](https://opencode.ai)** | ✅ Fully Supported | [Setup Guide](./docs/agents/opencode/quickstart.md) |
43+
| **[AMP](https://ampcode.com)** | Coming soon | [Awaiting release](https://ampcode.com/manual?internal#hooks) |
44+
| **[Gemini CLI](https://docs.cloud.google.com/gemini/docs/codeassist/gemini-cli)** | Coming soon | [Awaiting release](https://github.com/google-gemini/gemini-cli/issues/2779) |
4445

45-
Each harness uses native event formats. Similar to terraform, policies are separated by harness (`policies/claude/`, `policies/cursor/`, `policies/factory/`, `policies/opencode/`) to ensure clarity and full access to harness-specific capabilities.
46+
Each harness uses native event formats. Similar to terraform, policies are separated by harness (`policies/claude/`, `policies/cursor/`, `policies/factory/`, `policies/opencode/`) to ensure clarity and full access to harness-specific capabilities. If a particular harness is not supported, it is because it has no means for runtime integration.
4647

4748
#### Language Bindings
4849

cupcake-cli/src/catalog_cli.rs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ async fn execute_repo_command(command: RepoCommand) -> Result<()> {
173173
RepoCommand::Add { name, url } => {
174174
manager.add_registry(&name, &url)?;
175175
manager.save()?;
176-
println!("Added repository '{}' -> {}", name, url);
176+
println!("Added repository '{name}' -> {url}");
177177
}
178178
RepoCommand::List => {
179179
println!("Configured repositories:\n");
@@ -189,7 +189,7 @@ async fn execute_repo_command(command: RepoCommand) -> Result<()> {
189189
RepoCommand::Remove { name } => {
190190
manager.remove_registry(&name)?;
191191
manager.save()?;
192-
println!("Removed repository '{}'", name);
192+
println!("Removed repository '{name}'");
193193
}
194194
}
195195

@@ -305,7 +305,7 @@ async fn execute_show(name: &str, json_output: bool, force_refresh: bool) -> Res
305305

306306
let versions = index
307307
.get_versions(name)
308-
.context(format!("Rulebook '{}' not found in catalog", name))?;
308+
.context(format!("Rulebook '{name}' not found in catalog"))?;
309309

310310
let latest = versions.first().context("No versions available")?;
311311

@@ -338,7 +338,7 @@ async fn execute_show(name: &str, json_output: bool, force_refresh: bool) -> Res
338338
println!();
339339
println!("Description:");
340340
for line in latest.description.lines() {
341-
println!(" {}", line);
341+
println!(" {line}");
342342
}
343343

344344
println!();
@@ -393,9 +393,8 @@ async fn execute_install(name: &str, version: Option<&str>, force_refresh: bool)
393393
let entry = index
394394
.resolve_version(rulebook_name, version_spec)
395395
.context(format!(
396-
"No version matching '{}' found for '{}'.\n\
397-
Supported formats: 1.2.0 (exact), ^1.2 (compatible), ~1.2 (patch-level), latest",
398-
version_spec, rulebook_name
396+
"No version matching '{version_spec}' found for '{rulebook_name}'.\n\
397+
Supported formats: 1.2.0 (exact), ^1.2 (compatible), ~1.2 (patch-level), latest"
399398
))?;
400399

401400
// Show what version was resolved for non-exact specifiers
@@ -486,7 +485,7 @@ async fn execute_uninstall(name: &str) -> Result<()> {
486485
let mut lock = CatalogLock::load_or_default()?;
487486

488487
if !lock.is_installed(name) {
489-
println!("Rulebook '{}' is not installed.", name);
488+
println!("Rulebook '{name}' is not installed.");
490489
return Ok(());
491490
}
492491

@@ -504,7 +503,7 @@ async fn execute_uninstall(name: &str) -> Result<()> {
504503
lock.remove_installed(name);
505504
lock.save()?;
506505

507-
println!("Uninstalled '{}' v{}", name, version);
506+
println!("Uninstalled '{name}' v{version}");
508507

509508
Ok(())
510509
}
@@ -560,7 +559,7 @@ async fn execute_upgrade(name: Option<&str>, dry_run: bool, force_refresh: bool)
560559

561560
if to_check.is_empty() {
562561
if let Some(n) = name {
563-
println!("Rulebook '{}' is not installed.", n);
562+
println!("Rulebook '{n}' is not installed.");
564563
}
565564
return Ok(());
566565
}
@@ -621,10 +620,7 @@ async fn execute_upgrade(name: Option<&str>, dry_run: bool, force_refresh: bool)
621620
}
622621

623622
for name in &not_in_catalog {
624-
println!(
625-
"\nWarning: '{}' not found in catalog (may have been removed)",
626-
name
627-
);
623+
println!("\nWarning: '{name}' not found in catalog (may have been removed)");
628624
}
629625

630626
if dry_run {
@@ -663,7 +659,7 @@ async fn execute_upgrade(name: Option<&str>, dry_run: bool, force_refresh: bool)
663659
async fn execute_lint(path: &std::path::Path) -> Result<()> {
664660
use cupcake_core::catalog::RulebookManifest;
665661

666-
println!("Validating rulebook at {:?}...\n", path);
662+
println!("Validating rulebook at {path:?}...\n");
667663

668664
let mut errors: Vec<String> = Vec::new();
669665
let mut warnings: Vec<String> = Vec::new();
@@ -681,7 +677,7 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
681677
Ok(manifest) => {
682678
// Validate manifest fields
683679
if let Err(e) = manifest.validate() {
684-
errors.push(format!("Manifest validation failed: {}", e));
680+
errors.push(format!("Manifest validation failed: {e}"));
685681
}
686682

687683
// Check policies exist for declared harnesses
@@ -692,34 +688,30 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
692688
for harness in &manifest.metadata.harnesses {
693689
let harness_dir = policies_dir.join(harness);
694690
if !harness_dir.exists() {
695-
errors.push(format!(
696-
"Missing policies directory for harness: {}",
697-
harness
698-
));
691+
errors.push(format!("Missing policies directory for harness: {harness}"));
699692
continue;
700693
}
701694

702695
// Check for system/evaluate.rego
703696
let system_eval = harness_dir.join("system").join("evaluate.rego");
704697
if !system_eval.exists() {
705698
errors.push(format!(
706-
"Missing system/evaluate.rego for harness: {}",
707-
harness
699+
"Missing system/evaluate.rego for harness: {harness}"
708700
));
709701
}
710702

711703
// Check that at least some .rego files exist
712704
let rego_files = count_rego_files(&harness_dir);
713705
if rego_files == 0 {
714-
errors.push(format!("No .rego files found for harness: {}", harness));
706+
errors.push(format!("No .rego files found for harness: {harness}"));
715707
}
716708
}
717709
}
718710

719711
// Validate Rego namespaces
720712
if policies_dir.exists() {
721713
if let Err(e) = validate_rego_namespaces(path, &manifest.metadata.name) {
722-
errors.push(format!("Namespace validation failed: {}", e));
714+
errors.push(format!("Namespace validation failed: {e}"));
723715
}
724716
}
725717

@@ -740,7 +732,7 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
740732
println!();
741733
}
742734
Err(e) => {
743-
errors.push(format!("Failed to parse manifest.yaml: {}", e));
735+
errors.push(format!("Failed to parse manifest.yaml: {e}"));
744736
}
745737
}
746738

@@ -818,11 +810,11 @@ fn print_validation_results(errors: &[String], warnings: &[String]) {
818810
}
819811

820812
for error in errors {
821-
println!("ERROR: {}", error);
813+
println!("ERROR: {error}");
822814
}
823815

824816
for warning in warnings {
825-
println!("WARNING: {}", warning);
817+
println!("WARNING: {warning}");
826818
}
827819

828820
println!();
@@ -844,7 +836,7 @@ async fn execute_package(path: &std::path::Path, output: &std::path::Path) -> Re
844836
use sha2::{Digest, Sha256};
845837
use tar::Builder;
846838

847-
println!("Packaging rulebook at {:?}...\n", path);
839+
println!("Packaging rulebook at {path:?}...\n");
848840

849841
// Validate first
850842
let manifest_path = path.join("manifest.yaml");
@@ -859,15 +851,15 @@ async fn execute_package(path: &std::path::Path, output: &std::path::Path) -> Re
859851
let version = &manifest.metadata.version;
860852

861853
// Create tarball filename
862-
let tarball_name = format!("{}-{}.tar.gz", name, version);
854+
let tarball_name = format!("{name}-{version}.tar.gz");
863855
let tarball_path = output.join(&tarball_name);
864856

865857
// Ensure output directory exists
866858
std::fs::create_dir_all(output)?;
867859

868860
// Create tarball
869861
let tarball_file = std::fs::File::create(&tarball_path)
870-
.with_context(|| format!("Failed to create {:?}", tarball_path))?;
862+
.with_context(|| format!("Failed to create {tarball_path:?}"))?;
871863

872864
let encoder = GzEncoder::new(tarball_file, Compression::default());
873865
let mut builder = Builder::new(encoder);
@@ -884,11 +876,11 @@ async fn execute_package(path: &std::path::Path, output: &std::path::Path) -> Re
884876
let tarball_bytes = std::fs::read(&tarball_path)?;
885877
let digest = format!("sha256:{:x}", Sha256::digest(&tarball_bytes));
886878

887-
println!("Created: {:?}", tarball_path);
879+
println!("Created: {tarball_path:?}");
888880
println!("Size: {} bytes", tarball_bytes.len());
889-
println!("Digest: {}", digest);
881+
println!("Digest: {digest}");
890882
println!();
891-
println!("Rulebook: {} v{}", name, version);
883+
println!("Rulebook: {name} v{version}");
892884
println!("Harnesses: {}", manifest.metadata.harnesses.join(", "));
893885

894886
Ok(())

0 commit comments

Comments
 (0)