types: reserve the protocol names an agent must not be called
An agent's name was checked for shape and never for meaning: `Ident::parse` is 1-63 chars of [a-z0-9-] and there was no reserved-name list anywhere in the tree. So an agent could be called `operator`, `forge` or `todo` -- names the message layer already produces as a sender -- and a wake from that component became indistinguishable, at the broker, from a message sent by the agent. Adds `RESERVED_NAMES` + `is_reserved_name` to `hive-types`, the zero-dep leaf both `hive-c0re` and `swarm-controller` already depend on, so neither grows a dependency to use it. Every entry is a value some component actually produces as a message `from`/`to`, taken from `hive-sh4re`'s own sentinel constants rather than guessed: operator, system, reminder, forge, scheduled, todo, compact, graceful-stop. Two sentinels are deliberately absent -- `<parent>` and `<children>` are unreachable as agent names because the charset rejects them, and `ruth` is a real agent, so wanting that name is a name being *taken*, which the roster answers. Deliberately not enforced inside `Ident::parse`: parsing runs on every read of an already-created name, so rejecting there would make existing agents unreadable rather than un-creatable -- and it would be a refusal, which is a stronger action than the warning this is used for today. `create_agent` now warns on both halves -- a reserved name, and a name that is also a hive in the roster -- and does not refuse. The warnings ride on `CreateAgentResponse` rather than only the daemon's log, because the person who can still fix the name in one keystroke is holding the response, not reading the journal. `skip_serializing_if` keeps the no-warning JSON byte-identical to before, so this is a non-breaking first step toward refusing later. `hive-sh4re` gains a drift test tying its sentinel constants to the list: two crates that cannot import each other's intent now fail loudly if a sentinel is added without being reserved. Mutation-verified -- forcing the predicate false, forcing it true, and dropping a single entry each turn a different test red.
This commit is contained in:
parent
43ae164d8b
commit
5202e5c5ba
3 changed files with 215 additions and 2 deletions
|
|
@ -131,3 +131,52 @@ pub struct SchedulePromptPayload {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reserved_name_tests {
|
||||
use super::{
|
||||
CHILDREN_RECIPIENT, MANAGER_AGENT, OPERATOR_RECIPIENT, PARENT_RECIPIENT, SYSTEM_SENDER,
|
||||
};
|
||||
use hive_types::{Ident, is_reserved_name};
|
||||
|
||||
/// The sentinels declared here and the reserved-name list in
|
||||
/// `hive-types` are two spellings of one fact, in crates that cannot
|
||||
/// import each other's intent. This pins them together: adding a
|
||||
/// sentinel without reserving it now fails here rather than years
|
||||
/// later, when an agent takes the name.
|
||||
#[test]
|
||||
fn ident_shaped_sentinels_are_reserved() {
|
||||
for sentinel in [OPERATOR_RECIPIENT, SYSTEM_SENDER] {
|
||||
assert!(
|
||||
is_reserved_name(sentinel),
|
||||
"{sentinel:?} is a sentinel an agent could be named — it must be in RESERVED_NAMES"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The other half, and the reason the test above is not vacuous: these
|
||||
/// sentinels are *unreachable* as agent names because the charset
|
||||
/// rejects them, so they are correctly absent from the list. If a
|
||||
/// charset change ever made one parseable, it would become a real
|
||||
/// collision and this test is what notices.
|
||||
#[test]
|
||||
fn bracketed_recipients_cannot_be_agent_names() {
|
||||
for sentinel in [PARENT_RECIPIENT, CHILDREN_RECIPIENT] {
|
||||
assert!(
|
||||
Ident::parse(sentinel).is_err(),
|
||||
"{sentinel:?} parses as an ident now — it is reachable as an agent name and must be reserved"
|
||||
);
|
||||
assert!(!is_reserved_name(sentinel));
|
||||
}
|
||||
}
|
||||
|
||||
/// `ruth` is a real agent, not a protocol literal, so it is not
|
||||
/// reserved: a second agent wanting the name is a *taken* name, which
|
||||
/// the roster check answers. Recorded as a test so the distinction is
|
||||
/// enforced rather than remembered.
|
||||
#[test]
|
||||
fn manager_name_is_taken_not_reserved() {
|
||||
assert!(Ident::parse(MANAGER_AGENT).is_ok());
|
||||
assert!(!is_reserved_name(MANAGER_AGENT));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue