REQ-C-002: Command Declares Danger Level
Tier: Command Contract | Priority: P0
Source: §23 Side Effects & Destructive Operations · §12 Idempotency & Safe Retries
Addresses: Severity: Critical / Token Spend: Medium / Time: High / Context: Medium
Description
Every command MUST declare a danger_level as part of its registration metadata, chosen from: safe (read-only, no side effects on state), mutating (creates or modifies state), or destructive (permanently deletes or irreversibly modifies state). The framework MUST refuse to register a command without this declaration. The framework MUST use this declaration to enforce related behaviors (e.g., requiring --dry-run for destructive commands per REQ-C-004). A streaming command (REQ-O-004) MUST NOT declare destructive, and the framework refuses to register one that does.
Writes of declared cache, log, temp, and output paths (REQ-C-011) are not state changes: a command whose only writes are of those kinds stays safe. A report generator that writes its product to a declared output path is safe, not mutating. Writing a credential or config path, or any undeclared path, is a state change.
A command MAY also declare idempotent: true: a repeat with the same arguments converges on the same state, whatever a previous attempt left behind. A rewrite of one snapshot file per server, a PUT of a full resource, and a delete of a named resource (danger_level: "destructive") all qualify; an append, a counter increment, and a message send do not. Absent means false. The declaration does not change any ExitCodeEntry: an exit after some writes stays retryable: false with side_effects: "partial", because retryable: true still guarantees that nothing was written (REQ-C-001). What it adds is an agent rule: on a non-retryable exit whose entry declares side_effects: "partial", rerunning the identical command is the recovery, without inspecting state first. The rule never covers ARG_ERROR (2) or an error that carries fix_required or fix_command, which fail until the caller changes something. idempotent on a safe command is redundant (a read-only command converges trivially) and the framework accepts it without warning.
idempotent is distinct from --idempotency-key (REQ-C-007), which deduplicates one request so that its repeat does nothing and returns effect: "noop" (REQ-C-003). A command can deduplicate without converging (each new key creates another order) and converge without deduplicating (each rerun rewrites the same files); declaring idempotent does not exempt a mutating command from accepting --idempotency-key.
Acceptance Criteria
- Attempting to register a command without
danger_levelraises a framework error - The
--schemaoutput for every command includesdanger_level - Commands with
danger_level: "destructive"trigger framework-level dry-run enforcement (REQ-C-004) - Commands with
danger_level: "safe"do not require--idempotency-key(REQ-C-007) - Commands with
danger_level: "mutating"require--idempotency-keyunless they stream, and a streaming one that accepts it raises a registration error (REQ-C-007) - Registering a streaming command with
danger_level: "destructive"raises a framework error (REQ-O-004) - A command whose only declared filesystem writes are
cache,log,temp, oroutputpaths may register assafe - A command registered with
idempotent: trueshows"idempotent": truein--schemaoutput and the manifest; a command registered without it omits the field or showsfalse - Registering
idempotent: truewith anydanger_level, includingsafeanddestructive, succeeds and leaves every declaredExitCodeEntryunchanged - For a command declared
idempotent: true, a rerun of the identical invocation after each declared exit withside_effects: "partial"exits0and leaves the state a single clean run leaves
Schema
Types: manifest-response.md
The danger_level field appears on every command entry in --schema output and in the manifest. Allowed values: "safe" · "mutating" · "destructive". The optional idempotent field appears when the command declares it; absent means false.
{
"danger_level": {
"type": "string",
"enum": ["safe", "mutating", "destructive"],
"description": "Indicates the mutation risk level of the command"
},
"idempotent": {
"type": "boolean",
"default": false,
"description": "A repeat with the same arguments converges on the same state, whatever a previous attempt left behind"
}
}
Wire Format
$ tool delete-account --schema
{
"command": "delete-account",
"danger_level": "destructive",
"reversible": false,
"requires_confirmation": true,
"flags": {
"user": { "type": "integer", "required": true, "description": "User ID to delete" },
"dry-run": { "type": "boolean", "required": false, "default": false, "description": "Validate without deleting" }
},
"exit_codes": {
"0": { "name": "SUCCESS", "description": "User account deleted", "retryable": false, "side_effects": "complete" },
"2": { "name": "ARG_ERROR", "description": "Invalid user ID", "retryable": false, "side_effects": "none" },
"5": { "name": "NOT_FOUND", "description": "User not found", "retryable": false, "side_effects": "none" }
}
}
Example
A command declares its danger level at registration time. The framework uses this declaration to enforce related behaviors automatically.
register command "delete-account":
danger_level: destructive
exit_codes:
SUCCESS (0): description: "User account deleted", retryable: false, side_effects: complete
NOT_FOUND(5): description: "User not found", retryable: false, side_effects: none
register command "list-users":
danger_level: safe
exit_codes:
SUCCESS(0): description: "User list returned", retryable: false, side_effects: none
register command "observe":
danger_level: mutating
idempotent: true
exit_codes:
SUCCESS (0): description: "Every snapshot file is rewritten", retryable: false, side_effects: complete
PARTIAL_FAILURE(3): description: "Some snapshot files are rewritten and others are not", retryable: false, side_effects: partial
→ after exit 3, the agent reruns "tool observe" unchanged; no state inspection first
register command "update-email":
(no danger_level)
→ framework error: danger_level declaration is required
Related
| Requirement | Tier | Relationship |
|---|---|---|
| REQ-C-001 | C | Composes: danger_level and idempotent are part of the same --schema output as exit_codes; idempotent leaves the retryable ⇒ side_effects: "none" invariant unchanged |
| REQ-C-003 | C | Extends: danger_level: mutating/destructive triggers effect field requirement |
| REQ-C-004 | C | Enforces: danger_level: destructive requires --dry-run support |
| REQ-C-007 | C | Enforces: danger_level: mutating/destructive requires --idempotency-key on every non-streaming command, which idempotent does not replace |
| REQ-O-004 | O | Enforces: a streaming command is never destructive |
| REQ-O-041 | O | Aggregates: manifest exposes danger_level for every registered command |