use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Gate {
pub name: &'static str,
pub signal: &'static str,
pub condition: &'static str,
pub action: &'static str,
pub memory: &'static str,
pub reversibility: &'static str,
}
pub const GATES: &[Gate] = &[
Gate {
name: "prompt_drift",
signal: "commit touches bundled prompts or prompt source tree",
condition: "source bytes != bundled asset bytes",
action: "bin/check runs bin/check-prompt-drift and fails",
memory: "test stdout plus src/crates/netsky-prompts/tests/prompt_drift.rs:49",
reversibility: "trivial: sync source, rebuild, rerun gate",
},
Gate {
name: "mutation_ledger",
signal: "MCP mutating tool call",
condition: "args_hash seen recently or call times out after dispatch",
action: "dedupe exact retry; surface timeout_race with request_id",
memory: "mutation JSONL with request_id and args_hash at src/crates/netsky-io/src/mcp.rs:51",
reversibility: "compensating: read-before-retry, then decide next mutation",
},
Gate {
name: "email_draft_sentinel_consume",
signal: "send_draft with NETSKY_EMAIL_AUTO_SEND=1",
condition: "fresh approval sentinel exists for composite_draft_id",
action: "rename sentinel to .used before POST",
memory: ".used sentinel and success delete at src/crates/netsky-io/src/sources/email/send.rs:643",
reversibility: "repairable: restore fresh sentinel only on pre-send failure",
},
Gate {
name: "drive_empty_trash_surface",
signal: "request to empty Google Drive trash",
condition: "call arrives through MCP tool surface",
action: "reject unknown tool; allow CLI-only path",
memory: "test proves absence at src/crates/netsky-io/src/sources/drive/mod.rs:491",
reversibility: "irreversible if executed; gate keeps execution off MCP",
},
Gate {
name: "graceful_down_preflight",
signal: "netsky down without --force",
condition: "target tmux sessions exist",
action: "send shutdown envelopes, wait for ack or close, then force fallback",
memory: "shutdown request envelope at src/crates/netsky-cli/src/cmd/down.rs:221",
reversibility: "repairable: rerun agent startup for any missing session",
},
Gate {
name: "watchdog_clone_idle_discriminator",
signal: "clone pane hash is stable past hang threshold",
condition: "inbox history == Drained and pane_stable_since >= drain_since",
action: "suppress hang page only with proof of drained work",
memory: "state file agentN-inbox.state at src/crates/netsky-cli/src/cmd/watchdog.rs:1406",
reversibility: "trivial: delete malformed state or let next tick rewrite it",
},
Gate {
name: "escalate_retry_failed_marker",
signal: "netsky escalate sends an owner page",
condition: "first osascript attempt times out or exits nonzero",
action: "retry once; if second fails, write failed marker and return error",
memory: "marker plus event at src/crates/netsky-cli/src/cmd/escalate.rs:105",
reversibility: "repairable: owner or doctor reads marker, fixes path, retries page",
},
Gate {
name: "channel_watch_delivery",
signal: "netsky channel watch agent --tmux session",
condition: "pending envelope validates and tmux paste succeeds",
action: "claim, wrap, paste, submit, ack, archive",
memory: "received ack JSONL at src/crates/netsky-cli/src/cmd/channel.rs:306",
reversibility: "repairable: failed submit leaves claimed file for retry",
},
Gate {
name: "envelope_wrapper_token_validator",
signal: "inbound or outbound agent-bus envelope",
condition: "body contains </channel> or <channel source=",
action: "refuse send or quarantine read before model framing",
memory: "poison/quarantine record at src/crates/netsky-cli/src/cmd/channel.rs:230",
reversibility: "trivial: remove wrapper token from body, resend",
},
Gate {
name: "atomic_envelope_write",
signal: "producer writes envelope into inbox",
condition: "final filename may collide or writer may die mid-write",
action: "write tempfile, hardlink create-new to final, unlink tempfile",
memory: "final JSON file or leftover tmp at src/crates/netsky-core/src/envelope.rs:94",
reversibility: "repairable: retry with fresh filename, inspect leftover tmp",
},
];
pub fn run(json: bool, name: Option<&str>) -> netsky_core::Result<()> {
match (json, name) {
(true, Some(name)) => {
let gate = find_gate(name)?;
println!("{}", serde_json::to_string_pretty(gate)?);
}
(true, None) => println!("{}", serde_json::to_string_pretty(GATES)?),
(false, Some(name)) => print_gate(find_gate(name)?),
(false, None) => {
for gate in GATES {
println!("{}: {}", gate.name, gate.signal);
}
}
}
Ok(())
}
fn find_gate(name: &str) -> netsky_core::Result<&'static Gate> {
GATES.iter().find(|gate| gate.name == name).ok_or_else(|| {
let known = GATES.iter().map(|g| g.name).collect::<Vec<_>>().join(", ");
netsky_core::Error::Invalid(format!(
"unknown gate '{name}' — known gates: {known} (run `netsky gates` to list)"
))
})
}
fn print_gate(gate: &Gate) {
println!("{}", gate.name);
println!(" signal: {}", gate.signal);
println!(" condition: {}", gate.condition);
println!(" action: {}", gate.action);
println!(" memory: {}", gate.memory);
println!(" reversibility: {}", gate.reversibility);
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn every_gate_memory_citation_exists() {
let root = repo_root();
for gate in GATES {
let (path, line) = memory_citation(gate.memory)
.unwrap_or_else(|| panic!("{} memory lacks path:line citation", gate.name));
let full_path = root.join(path);
let content = std::fs::read_to_string(&full_path)
.unwrap_or_else(|err| panic!("{} citation missing {path}: {err}", gate.name));
assert!(
content.lines().nth(line - 1).is_some(),
"{} citation {path}:{line} is beyond EOF",
gate.name
);
}
}
fn memory_citation(memory: &str) -> Option<(&str, usize)> {
memory.split_whitespace().find_map(|token| {
let (path, line) = token.rsplit_once(':')?;
if !path.ends_with(".rs") || !Path::new(path).is_relative() {
return None;
}
Some((path, line.parse().ok()?))
})
}
fn repo_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(3)
.expect("repo root")
}
}