Repository navigation
Feature: import and export controls via JSON files #128
Description
Activity
Import/export via JSON is the right primitives for shareable control bundles. A few considerations from the policy-as-data perspective:
Schema versioning: The JSON schema should include a
versionfield and aschemaURI. Without it, forward compatibility breaks silently when the control model adds fields. Something like:{ "version": "1.0.0", "schema": "https://agentcontrol.dev/schemas/control-bundle/v1.json", "controls": [...] }Policy as file, not just export: The import/export flow is great for sharing. But there is also a case for repo-local policy files (e.g.
.agentowners.ymlor.agent-control.yml) that live alongside code and are version-controlled. This enables CI/CD enforcement where policy changes are reviewed through the same PR workflow as code changes. The import flow could accept both UI exports and repo-local policy files.Control-only vs policy membership: For v1, controls-only is the right scope. But the JSON format should leave room for policy membership later (e.g. which agents or namespaces a control applies to). A
scopeortargetfield in the bundle schema would make that extension clean.Conflict resolution: For import conflicts, I would bias toward explicit over implicit — surface all conflicts to the user with suggested resolutions, never silently skip or overwrite. The policy author needs to know which controls changed and why. This is especially important when controls encode security invariants — silently dropping a
denycontrol during import is a security regression.Hi! I'd like to pick this up. Before writing code, here is a concrete v1 proposal covering the open questions in the description. Feedback welcome, especially on the conflict handling default.
Scope for v1: server-side API only, controls only
- Two endpoints, no UI changes yet. The Control Store buttons can be a follow-up PR once the API shape settles.
- Controls only, no policy membership, agent attachments, or bindings. Those reference server-local entities (agent names, policy ids) that generally do not exist in the target environment, so including them invites confusing partial failures. The envelope leaves room to add them later.
Export
POST /api/v1/controls/exportwith an optional list of control ids (absent means all controls visible in the caller's namespace). Returns a versioned envelope:{ "format_version": 1, "exported_at": "2026-07-19T00:00:00Z", "controls": [ { "name": "Block leaked secrets", "enabled": true, "condition": { ... }, "action": { "decision": "deny" } } ] }Only portable definition fields are exported: name, description, enabled, condition, action, and for template-backed controls the template plus template_values (so they stay editable as templates after import, rather than being flattened). Server-assigned identity is excluded: ids, timestamps, namespace, version history, clone lineage, bindings.
One thing to decide: evaluator configs can contain sensitive values (API keys for cloud evaluators). I'd propose v1 exports configs as-is and the docs call out that exports may contain secrets, with key redaction as a possible follow-up. Alternative is to redact known secret fields on export, but that makes round-tripping lossy.
Import
POST /api/v1/controls/importaccepting the same envelope, plus two options:on_conflict:skip(default),overwrite, orrename(imports as "Name (imported)", numbered on repeat). Names conflict per namespace, matching current create behavior.overwritegoes through the existing update path so it creates a new control version rather than destroying history.dry_run: validate everything and return the report without persisting.
Each control is validated through the existing definition validation path (same code the create and validate endpoints use), so import cannot smuggle in anything the UI would reject. Import is per-item, not all-or-nothing: valid controls import, invalid ones are reported. The response makes every outcome explicit, nothing fails silently:
{ "imported": ["Block leaked secrets"], "skipped": [{"name": "PII filter", "reason": "name_conflict"}], "errors": [{"name": "Bad control", "reason": "unknown evaluator 'foo.bar'"}] }If all-or-nothing semantics are preferred instead, that is an easy switch, but per-item with a report seems friendlier for the "import a published control bundle" use case where one stale evaluator reference should not block the other nine controls.
Format versioning
format_versionis an integer, starting at 1. Import rejects unknown versions with a clear error. Future format changes bump the version and import keeps reading older versions.Testing
Round-trip tests (export then import into a clean namespace yields equivalent controls, including template-backed ones), conflict behavior for all three modes, dry run, per-item validation failures, unknown format_version, and namespace isolation.
If this direction looks right, I'll start with the export endpoint and envelope models, then import in the same PR.
Keesan12 commented
on Jul 21, 2026 More actionsImport and export sounds like a convenience feature until controls become part of how a team proves what was actually allowed. Then versioning, reviewability, and a clear diff of policy changes become the real product.
That is a lesson we are learning with MartinLoop. The trust layer around coding agents has to be inspectable before a run and accountable afterward. Teams need to see what rules were in force, what the agent did inside them, and whether the work passed a real finish line.
Would love to hear how you want teams to review a controls change before it lands. We are building MartinLoop in the open: https://github.com/Keesan12/martin-loop. support@martinloop.com is open for direct feedback.
Summary
Motivation
Current behavior
Expected behavior
Reproduction (if bug)
Proposed solution (optional)
Additional context