Design Spec: Active-Transmit Safety Model¶
Document Control¶
| Field | Value |
|---|---|
| Status | Planned |
| Command surface | canarchy send, generate, gateway, replay, uds scan, fuzz payload, fuzz replay, fuzz arbitration-id |
| Primary area | CLI, transport, safety |
| Related specs | docs/design/active-command-safety.md, docs/design/transport-core-commands.md, docs/design/generate-command.md |
Goal¶
Define the safety controls that every active-transmit command in
CANarchy must honour, including the placeholder fuzz command tree
that was removed in #292 pending this design. The model layers
on top of the existing --ack-active preflight gate (active-command-safety.md)
with five additional controls: rate caps, target allowlists, a
kill-switch, a --dry-run planning mode, and run-id provenance on
every emitted event.
User-Facing Motivation¶
Active CAN transmission can disturb a live vehicle or test rig. The
existing acknowledgement gate is enough for one-shot send and
generate calls, but high-rate or long-running workflows
(fuzz payload, gateway, mutated replay) need finer-grained
controls so a misconfigured invocation cannot saturate the bus,
target the wrong arbitration ID space, or continue past an operator
interrupt. Agents calling these workflows over MCP need an explicit
opt-in token so a confused agent cannot accidentally invoke an
active workflow.
Requirements¶
| ID | Type | Requirement |
|---|---|---|
REQ-ATS-01 |
Ubiquitous | The system shall treat send, generate, gateway, replay, uds scan, fuzz payload, fuzz replay, and fuzz arbitration-id as active-transmit commands and apply the controls in this spec. |
REQ-ATS-02 |
Event-driven | When an active-transmit command is invoked, the system shall enforce the existing active-command-safety.md acknowledgement gate before applying any control in this spec. |
REQ-ATS-03 |
Optional feature | Where --ack-active is supplied non-interactively (stdin is not a TTY), the system shall accept the flag itself as the acknowledgement and skip the interactive YES prompt. |
REQ-ATS-04 |
Event-driven | When an active-transmit command begins transmission, the system shall stamp every emitted event with a stable run_id field derived from a cryptographically random 128-bit identifier. |
REQ-ATS-05 |
Optional feature | Where --rate <hz> is supplied or implied by command defaults, the system shall enforce a per-command rate cap measured in frames per second. |
REQ-ATS-06 |
Unwanted behaviour | If the rate cap would be exceeded by the requested workload, the system shall return a structured error with code ACTIVE_TRANSMIT_RATE_EXCEEDED and exit code 1 before any frame is transmitted. |
REQ-ATS-07 |
Optional feature | Where --targets <path> is supplied (or [safety].targets_file is set in ~/.canarchy/config.toml), the system shall load an allowlist of arbitration IDs and refuse to transmit frames whose ID is not present. |
REQ-ATS-08 |
Unwanted behaviour | If a target allowlist is in force and the requested transmission targets an ID outside the allowlist, the system shall return a structured error with code ACTIVE_TRANSMIT_TARGET_BLOCKED and exit code 1 before any frame is transmitted. |
REQ-ATS-09 |
State-driven | While an active-transmit command is running, the system shall stop transmission cleanly when SIGINT is received, emitting a final alert event with reason KILL_SWITCH_TRIGGERED. |
REQ-ATS-09a |
State-driven | While an active-transmit command is running and stdin was a live pipe at command start and has not yet reached EOF, the system shall also stop cleanly when stdin transitions to EOF. |
REQ-ATS-09b |
Unwanted behaviour | If stdin is a TTY, closed, or already at EOF at command start (for example a CI runner that supplies stdin=/dev/null, an MCP subprocess, or a cron job), the system shall ignore stdin EOF as a kill condition so non-interactive automation can run to completion. |
REQ-ATS-10 |
Optional feature | Where --dry-run is supplied, the system shall plan the transmission and emit the would-send frames as JSONL events to stdout without opening a transport, and the response shall carry the structured warning code ACTIVE_TRANSMIT_DRY_RUN. |
REQ-ATS-11 |
Optional feature | Where the command is invoked via the MCP server, the system shall require an explicit ack_active=true argument in the tool call. |
REQ-ATS-12 |
Unwanted behaviour | If an MCP active-transmit tool is invoked without ack_active=true, the system shall return a structured error with code ACTIVE_TRANSMIT_REQUIRES_ACK and exit code 1 without invoking the underlying command. |
REQ-ATS-13 |
State-driven | While the MCP server is the caller, the system shall default --dry-run to true when the agent omits an explicit dry_run field, so safe planning is the default for agent-initiated active workflows. |
REQ-ATS-14 |
Ubiquitous | The system shall keep machine-readable stdout output free of duplicated safety prompts; rate-cap, target-block, kill-switch, and dry-run signals shall appear in the canonical envelope's warnings[] or errors[] arrays, not as raw text. |
REQ-ATS-15 |
Ubiquitous | The system shall record the active-transmit safety configuration (require_active_ack, rate cap default, target allowlist path, MCP dry-run default) in canarchy config show output with [safety] source attribution. |
Command Surface¶
Existing active commands gain four shared flags:
canarchy <active-command> [...] [--ack-active] [--rate <hz>] [--targets <path>] [--dry-run] [--run-id <uuid>]
Specific examples:
canarchy send <interface> <frame-id> <hex-data> [--ack-active] [--rate <hz>] [--targets <path>] [--dry-run]
canarchy generate <interface> [--id <hex|R>] [--count <n>] [--rate <hz>]
[--ack-active] [--targets <path>] [--dry-run] [--run-id <uuid>]
canarchy gateway <src> <dst> [--bidirectional] [--rate <hz>]
[--ack-active] [--targets <path>] [--dry-run] [--run-id <uuid>]
canarchy fuzz payload <iface> --id <hex> --strategy {bitflip,random,boundary}
--rate <hz> --max <n> [--ack-active] [--dry-run] [--run-id <uuid>]
canarchy fuzz replay --file <capture> --strategy {timing,payload-bitflip}
[--rate <hz>] [--ack-active] [--dry-run] [--run-id <uuid>]
canarchy fuzz arbitration-id <iface> --range <start>:<end> --rate <hz>
[--extended] [--ack-active] [--dry-run] [--run-id <uuid>]
--gap on generate remains the canonical inter-frame spacing knob;
--rate <hz> is the new safety cap and overrides any implied rate
that would exceed it. Both flags coexist for backwards compatibility.
Responsibilities And Boundaries¶
In scope:
- shared safety controls applied uniformly across every active-transmit command
- a single source of truth for the gate (
require_active_ack, rate cap default, target allowlist) in~/.canarchy/config.toml - MCP-side opt-in token that is independent of the CLI
--ack-activeprompt - dry-run mode that planning agents can use to inspect a workflow before authorising it
run_idprovenance so post-mortem analysis can match an emitted frame to the invocation that produced it
Out of scope:
- passive commands (
capture,uds trace,filter,stats) — these never transmit and do not need the gate - offensive tooling beyond what the listed active commands already do (e.g. UDS write-by-DID, ECU reprogramming) — those would need their own spec extending this one
- hardware-level interlocks (relay-cut bus isolation, ignition lockout) — outside the CANarchy process boundary
Data Model¶
Target allowlist file¶
TOML format, located at the path supplied via --targets <path> or
[safety].targets_file.
[allowlist]
description = "Lab bench arbitration IDs"
ids = ["0x100", "0x200-0x2FF", "0x18FEEE31"]
extended = true # apply to 29-bit IDs
default = "block" # block any ID not listed
Ranges use a single hyphen with inclusive bounds. IDs may be supplied
as hex (0x100) or decimal (256). When extended = false, the
allowlist applies only to 11-bit IDs.
run_id field¶
Every active-transmit-emitted event (frame, alert, summary
active_transmit event) carries:
{ "run_id": "0193bf6e-1e3e-7a8c-b6b1-d0e7d3a8f4f0" }
run_id is also reported in the final summary envelope under
data.run_id.
Output Contracts¶
--dry-run¶
Planning mode. The command emits JSONL events with event_type:
"frame" and an additional payload.frame.dry_run = true field per
frame that would have been transmitted. The summary envelope carries
warnings: ["ACTIVE_TRANSMIT_DRY_RUN: <n> frames planned; no transport opened."]
and data.dry_run = true. No transport is opened; rate-cap and
allowlist checks still run.
Kill switch¶
The kill switch fires on SIGINT unconditionally and on stdin EOF only
when stdin was a live pipe at command start. When triggered, the
system emits a final alert event with
payload.reason = "KILL_SWITCH_TRIGGERED" and payload.frames_sent =
<count>, then exits with EXIT_PARTIAL_SUCCESS (4). The canonical
summary still includes data.frames_sent and data.run_id.
A pipe is considered "live at command start" when sys.stdin is not
a TTY and select.select([sys.stdin], [], [], 0) does not already
report EOF at startup. In any other case (TTY input, closed pipe,
stdin=/dev/null, MCP subprocess) stdin EOF is silently ignored so
long-running commands (gateway, replay) can still run to
completion in non-interactive contexts. SIGINT remains the universal
stop signal.
Rate-cap violations¶
If the requested workload exceeds the configured rate cap, the
system fails fast with ACTIVE_TRANSMIT_RATE_EXCEEDED before opening
the transport. No frames are sent.
Target-allowlist violations¶
If a planned frame's ID is outside the allowlist, the system fails
fast with ACTIVE_TRANSMIT_TARGET_BLOCKED and reports the blocked
IDs in errors[0].detail.blocked_ids.
Error Contracts¶
| Code | Trigger | Exit code |
|---|---|---|
ACTIVE_ACK_REQUIRED |
existing — see active-command-safety.md |
1 |
ACTIVE_CONFIRMATION_DECLINED |
existing — see active-command-safety.md |
1 |
ACTIVE_TRANSMIT_RATE_EXCEEDED |
requested workload exceeds the configured rate cap | 1 |
ACTIVE_TRANSMIT_TARGET_BLOCKED |
requested ID is outside the active target allowlist | 1 |
ACTIVE_TRANSMIT_REQUIRES_ACK |
MCP tool invoked without ack_active=true |
1 |
ACTIVE_TRANSMIT_INVALID_TARGETS |
--targets <path> was supplied but the file failed to parse or had no ids |
1 |
ACTIVE_TRANSMIT_INVALID_RUN_ID |
--run-id <uuid> was supplied but did not parse as a UUID |
1 |
Each error carries an actionable hint per the convention in
docs/event-schema.md.
Configuration Surface¶
~/.canarchy/config.toml:
[safety]
require_active_ack = false
targets_file = "~/labs/bench-allowlist.toml"
[safety.rate_cap]
default_hz = 200 # applied when no command-specific cap is configured
maximum_hz = 5000 # hard ceiling that even --rate cannot exceed
fuzz_payload_hz = 100 # per-command override
fuzz_replay_hz = 50
fuzz_arbitration_id_hz = 200
Environment overrides:
CANARCHY_REQUIRE_ACTIVE_ACK— booleanCANARCHY_ACTIVE_TARGETS_FILE— path to the target allowlistCANARCHY_ACTIVE_RATE_CAP_HZ— overrides[safety.rate_cap].default_hz
canarchy config show shall report all of the above under
data.safety with the same source attribution it already uses for
transport keys.
MCP Surface¶
The fuzz tools expose:
{
"name": "fuzz_payload",
"inputSchema": {
"type": "object",
"required": ["interface", "id", "strategy", "ack_active"],
"properties": {
"interface": { "type": "string" },
"id": { "type": "string", "description": "hex CAN ID" },
"strategy": { "enum": ["bitflip", "random", "boundary"] },
"rate": { "type": "number", "description": "frames per second; capped by [safety.rate_cap]" },
"max": { "type": "integer" },
"ack_active":{ "type": "boolean", "const": true },
"dry_run": { "type": "boolean", "default": true }
}
}
}
ack_active=true is the only acceptable value; omitting the field or
passing false returns ACTIVE_TRANSMIT_REQUIRES_ACK. dry_run
defaults to true so agent-initiated calls plan rather than
transmit; setting dry_run=false requires both ack_active=true and
the existing CLI safety controls to pass.
Audit Of Existing Commands Against This Spec¶
| Command | --ack-active |
Rate cap | Target allowlist | Kill switch | --dry-run |
run_id |
MCP ack_active=true |
|---|---|---|---|---|---|---|---|
send |
✓ | n/a (single frame) | gap | n/a | + | + | + |
generate |
✓ | + | + | + | + | + | + |
gateway |
✓ | + | + | + | + | + | + |
replay |
✓ | + | + | + | ✓ | + | ✓ |
uds scan |
✓ | + | + | + | + | + | + |
fuzz payload |
+ | + | + | + | + | + | + |
fuzz replay |
+ | + | + | + | + | + | + |
fuzz arbitration-id |
+ | + | + | + | + | + | + |
Legend: ✓ already implemented; + to be added in the
implementation issues that follow; partial means the gate exists but
needs to extend to the new flags.
Deferred Decisions¶
- whether
--rateshould be a hard ceiling (current proposal) or a soft target with backoff - whether to expose target allowlists as a separate
canarchy safety targetssubcommand - whether to ship a default
safety.targets.lab.tomlexample file or leave it as an operator artefact - SIGTERM handling — current proposal treats SIGINT and stdin EOF; SIGTERM is left to follow-up
- whether to record
run_idincanarchy session savefor full-session replay