swarmctl: drop agent mint-identity and its controller route
Re-creating an agent mints its store identity the same way, so the
backfill verb and the `POST /api/agents/{name}/identity` route that only
it called go. The `MintAgentIdentity` job node stays: agent creation and
the renewal pass still queue it.
Part of #4765 (c).
This commit is contained in:
parent
cf763da178
commit
207484dfa6
7 changed files with 6 additions and 302 deletions
|
|
@ -95,7 +95,7 @@ hive-c0re creates the manager agent, ruth, on its own at startup — so
|
||||||
unlike agents made through the swarm, she has no store identity yet:
|
unlike agents made through the swarm, she has no store identity yet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
swarmctl agent mint-identity ruth # on the swarm-controller's host
|
swarmctl agent create ruth --hive <hive> # on the swarm-controller's host
|
||||||
hivectl agent ruth rebuild # on ruth's hive: hands her the identity
|
hivectl agent ruth rebuild # on ruth's hive: hands her the identity
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,27 +102,7 @@ carries it, and no hive ever needs the capability to mint an identity.
|
||||||
`swarm-controller` proves the leaf it publishes before the creation job
|
`swarm-controller` proves the leaf it publishes before the creation job
|
||||||
reports success, by logging in with it and reading the row back.
|
reports success, by logging in with it and reading the row back.
|
||||||
|
|
||||||
**Backfilling an agent that predates a credential.** Agent creation at swarm
|
`swarm-controller` checks every forge token itself: it looks at every agent
|
||||||
level is purely event-driven — `swarm-controller` mints an agent's store
|
|
||||||
identity on the job graph `POST /api/agents` inserts, and nothing sweeps for
|
|
||||||
agents that already exist. An agent created before a credential joined that
|
|
||||||
mint therefore never receives one, and nothing will ever come back around to
|
|
||||||
it. Re-run the mint for one agent with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
swarmctl agent mint-identity <agent>
|
|
||||||
```
|
|
||||||
|
|
||||||
The queue secret half is idempotent — an agent that already has one keeps exactly the
|
|
||||||
value it holds, so running this against an already-migrated agent doesn't drop
|
|
||||||
its queue connection. The certificate half isn't: the agent gets a fresh leaf
|
|
||||||
and picks it up on its next boot.
|
|
||||||
|
|
||||||
The renewal pass in the table doesn't replace this step: it only re-issues a
|
|
||||||
certificate or re-mints a queue secret that already exists, and only for an
|
|
||||||
agent some hive's wanted state declares as anything but `destroyed`.
|
|
||||||
|
|
||||||
The forge token needs no such step: `swarm-controller` checks every agent
|
|
||||||
that has a store identity at start and every five minutes. For any whose
|
that has a store identity at start and every five minutes. For any whose
|
||||||
stored token is missing or stale it creates the forge user if there isn't one,
|
stored token is missing or stale it creates the forge user if there isn't one,
|
||||||
then mints the token. To check one agent now:
|
then mints the token. To check one agent now:
|
||||||
|
|
@ -132,13 +112,8 @@ swarmctl agent mint-forge-token <agent>
|
||||||
```
|
```
|
||||||
|
|
||||||
An agent without a store identity gets no swarm token and keeps using the
|
An agent without a store identity gets no swarm token and keeps using the
|
||||||
`forge-token` file in its state dir, if it has one; run `mint-identity` for it
|
`forge-token` file in its state dir, if it has one; re-create it with
|
||||||
first.
|
`swarmctl agent create` to give it one.
|
||||||
|
|
||||||
⚠️ **Run this for every existing agent before deploying a hive-side change
|
|
||||||
that makes a container require a credential it may not have.** A container
|
|
||||||
whose credential is absent doesn't start — that's deliberate, and it's what
|
|
||||||
makes the backfill a step rather than a suggestion.
|
|
||||||
|
|
||||||
**Revoking an agent's queue credential.** Declaring an agent `destroyed` —
|
**Revoking an agent's queue credential.** Declaring an agent `destroyed` —
|
||||||
`PUT /api/hives/{hive}/agents/{agent}/state` — deletes
|
`PUT /api/hives/{hive}/agents/{agent}/state` — deletes
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ This document contains the help content for the `swarmctl` command-line program.
|
||||||
* [`swarmctl`↴](#swarmctl)
|
* [`swarmctl`↴](#swarmctl)
|
||||||
* [`swarmctl agent`↴](#swarmctl-agent)
|
* [`swarmctl agent`↴](#swarmctl-agent)
|
||||||
* [`swarmctl agent create`↴](#swarmctl-agent-create)
|
* [`swarmctl agent create`↴](#swarmctl-agent-create)
|
||||||
* [`swarmctl agent mint-identity`↴](#swarmctl-agent-mint-identity)
|
|
||||||
* [`swarmctl agent mint-forge-token`↴](#swarmctl-agent-mint-forge-token)
|
* [`swarmctl agent mint-forge-token`↴](#swarmctl-agent-mint-forge-token)
|
||||||
* [`swarmctl agent inference`↴](#swarmctl-agent-inference)
|
* [`swarmctl agent inference`↴](#swarmctl-agent-inference)
|
||||||
* [`swarmctl agent inference get`↴](#swarmctl-agent-inference-get)
|
* [`swarmctl agent inference get`↴](#swarmctl-agent-inference-get)
|
||||||
|
|
@ -65,7 +64,6 @@ Manage agents across the swarm
|
||||||
###### **Subcommands:**
|
###### **Subcommands:**
|
||||||
|
|
||||||
* `create` — Queue creation of a new agent on a hive in this swarm
|
* `create` — Queue creation of a new agent on a hive in this swarm
|
||||||
* `mint-identity` — Queue a re-mint of an existing agent's identity at the swarm's secret store
|
|
||||||
* `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale
|
* `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale
|
||||||
* `inference` — Show or change the model, effort and subagent models the swarm sets for an agent
|
* `inference` — Show or change the model, effort and subagent models the swarm sets for an agent
|
||||||
|
|
||||||
|
|
@ -100,30 +98,6 @@ No approval gate guards this: the caller is root on the controller's host, or lo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `swarmctl agent mint-identity`
|
|
||||||
|
|
||||||
Queue a re-mint of an existing agent's identity at the swarm's secret store.
|
|
||||||
|
|
||||||
**The backfill verb.** Agent creation is event-driven and nothing at swarm level sweeps for agents that are missing a credential, so an agent created before a credential joined the mint never receives one. This re-runs the mint for one agent that already exists.
|
|
||||||
|
|
||||||
⚠️ **It re-mints the agent's store certificate**, which that agent picks up the next time its container boots. It leaves an existing queue secret exactly as it stands, so running this against an already-migrated agent doesn't disturb its queue connection.
|
|
||||||
|
|
||||||
Queues and returns, the same way `agent create` does — watch the swarm UI's job view for the outcome.
|
|
||||||
|
|
||||||
**Usage:** `swarmctl agent mint-identity [OPTIONS] <NAME>`
|
|
||||||
|
|
||||||
###### **Arguments:**
|
|
||||||
|
|
||||||
* `<NAME>` — Name of an agent that already exists
|
|
||||||
|
|
||||||
###### **Options:**
|
|
||||||
|
|
||||||
* `--controller-socket <PATH>` — swarm-controller's unix socket.
|
|
||||||
|
|
||||||
Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `swarmctl agent mint-forge-token`
|
## `swarmctl agent mint-forge-token`
|
||||||
|
|
||||||
Check one agent's forge token, and mint it if it's missing or stale.
|
Check one agent's forge token, and mint it if it's missing or stale.
|
||||||
|
|
|
||||||
|
|
@ -2312,79 +2312,6 @@ async fn get_agent_config_pr(
|
||||||
Ok(Json(cache.get(&name)))
|
Ok(Json(cache.get(&name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Success body of `POST /api/agents/{name}/identity`.
|
|
||||||
#[derive(Clone, Debug, Serialize, ToSchema)]
|
|
||||||
struct MintAgentIdentityResponse {
|
|
||||||
/// The queued node, so a caller can follow it in the job view.
|
|
||||||
node_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-run the store-identity mint for an agent that already exists.
|
|
||||||
///
|
|
||||||
/// **This route exists for backfill.** Agent creation is event-driven and
|
|
||||||
/// there is no reconcile sweep at swarm level, so an agent created before a
|
|
||||||
/// credential was part of the mint never gets one — nothing would ever come
|
|
||||||
/// back around to it. Without a way to re-run the node by hand, the only way
|
|
||||||
/// to give an existing agent its queue credential would be to delete and
|
|
||||||
/// recreate the agent.
|
|
||||||
///
|
|
||||||
/// A job node rather than an inline write, and specifically the *same* node
|
|
||||||
/// `POST /api/agents` declares: a second code path that mints an identity is a
|
|
||||||
/// second place for the four strings in [`agent_identity`] to disagree.
|
|
||||||
///
|
|
||||||
/// ⚠️ **Not idempotent in one respect**: the node re-mints the agent's mTLS
|
|
||||||
/// leaf, which the agent picks up on its next boot. Its queue secret is left
|
|
||||||
/// alone — see [`agent_identity::mint_and_verify`] for which half is which and
|
|
||||||
/// why.
|
|
||||||
#[utoipa::path(
|
|
||||||
post,
|
|
||||||
path = "/api/agents/{name}/identity",
|
|
||||||
params(("name" = String, Path, description = "agent name")),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "mint queued", body = MintAgentIdentityResponse),
|
|
||||||
(status = 400, description = "`name` is not a valid identifier (problem+json)", body = String),
|
|
||||||
(status = 500, description = "the job could not be queued (problem+json)", body = String),
|
|
||||||
),
|
|
||||||
tag = "agents"
|
|
||||||
)]
|
|
||||||
async fn mint_agent_identity(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Path(name): Path<String>,
|
|
||||||
) -> Result<Json<MintAgentIdentityResponse>, problem_details::ProblemDetails> {
|
|
||||||
// The name is interpolated into store paths and policy documents
|
|
||||||
// downstream, so it is validated here as well as there.
|
|
||||||
let agent = hive_types::Ident::parse(&name)
|
|
||||||
.map_err(|reason| error_problem(axum::http::StatusCode::BAD_REQUEST, reason))?
|
|
||||||
.into_string();
|
|
||||||
|
|
||||||
// No reserved-name or collision warnings here, unlike `create_agent`:
|
|
||||||
// those answer "is this name available", and this route is only ever
|
|
||||||
// pointed at a name that was already taken.
|
|
||||||
let mut sched = state
|
|
||||||
.jobq
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
let ids = sched
|
|
||||||
.insert_job(None, |b| {
|
|
||||||
vec![
|
|
||||||
b.node(SwarmNodeKind::MintAgentIdentity {
|
|
||||||
agent: agent.clone(),
|
|
||||||
})
|
|
||||||
.guid(),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.map_err(|e| {
|
|
||||||
error_problem(
|
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
&e.to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let [id] = ids[..] else {
|
|
||||||
unreachable!("exactly one handle was asked for");
|
|
||||||
};
|
|
||||||
Ok(Json(MintAgentIdentityResponse { node_id: id.get() }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Success body of `POST /api/agents/{name}/forge-token`.
|
/// Success body of `POST /api/agents/{name}/forge-token`.
|
||||||
#[derive(Clone, Debug, Serialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, ToSchema)]
|
||||||
struct MintAgentForgeTokenResponse {
|
struct MintAgentForgeTokenResponse {
|
||||||
|
|
@ -3312,7 +3239,6 @@ fn build_app(state: AppState) -> axum::Router {
|
||||||
))
|
))
|
||||||
.routes(routes!(get_config_prs))
|
.routes(routes!(get_config_prs))
|
||||||
.routes(routes!(create_agent))
|
.routes(routes!(create_agent))
|
||||||
.routes(routes!(mint_agent_identity))
|
|
||||||
.routes(routes!(mint_agent_forge_token))
|
.routes(routes!(mint_agent_forge_token))
|
||||||
.routes(routes!(make_forge_admin))
|
.routes(routes!(make_forge_admin))
|
||||||
.routes(routes!(get_agents))
|
.routes(routes!(get_agents))
|
||||||
|
|
@ -4286,67 +4212,6 @@ mod tests {
|
||||||
assert_eq!(resp.change, super::ForgeAdminChange::AlreadyAdmin);
|
assert_eq!(resp.change, super::ForgeAdminChange::AlreadyAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A name that is not an identifier is refused before it can reach a
|
|
||||||
/// store path. `create_agent` gets this for free from the roster check
|
|
||||||
/// on the hive; the agent name has no roster to check against, so this
|
|
||||||
/// is the only thing standing between `../beta` and a path built out of
|
|
||||||
/// it.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_backfill_for_a_name_that_is_not_an_identifier_queues_nothing() {
|
|
||||||
let (state, sched) = state_with_roster();
|
|
||||||
|
|
||||||
super::mint_agent_identity(
|
|
||||||
axum::extract::State(state),
|
|
||||||
axum::extract::Path("../beta".to_owned()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect_err("a traversal in the agent name must be refused");
|
|
||||||
|
|
||||||
let queued = sched
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.graph()
|
|
||||||
.nodes()
|
|
||||||
.count();
|
|
||||||
assert_eq!(queued, 0, "a refused backfill must queue no work");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The accept arm, and the property that makes this a *backfill* route
|
|
||||||
/// rather than a second creation route: it queues the mint node **and
|
|
||||||
/// nothing else**. `create_agent`'s graph also scaffolds a config repo
|
|
||||||
/// and publishes a deploy; running either of those against an agent that
|
|
||||||
/// already exists is the failure this assertion exists to catch.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_backfill_queues_the_mint_node_and_only_the_mint_node() {
|
|
||||||
let (state, sched) = state_with_roster();
|
|
||||||
|
|
||||||
let queued = super::mint_agent_identity(
|
|
||||||
axum::extract::State(state),
|
|
||||||
axum::extract::Path("atlas".to_owned()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("a valid agent name must be accepted");
|
|
||||||
|
|
||||||
let guard = sched
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
let nodes: Vec<_> = guard.graph().nodes().collect();
|
|
||||||
let [node] = nodes[..] else {
|
|
||||||
panic!("a backfill queues exactly one node, got {}", nodes.len());
|
|
||||||
};
|
|
||||||
assert!(
|
|
||||||
matches!(
|
|
||||||
&node.payload,
|
|
||||||
SwarmNodeKind::MintAgentIdentity { agent } if agent == "atlas"
|
|
||||||
),
|
|
||||||
"the one node must be the mint, for the named agent: {:?}",
|
|
||||||
node.payload
|
|
||||||
);
|
|
||||||
// The id the operator is told to watch has to be the node that was
|
|
||||||
// actually inserted, or `swarmctl` prints a handle to nothing.
|
|
||||||
assert_eq!(queued.node_id, node.id.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every node whose first request goes to the forge, run against a forge
|
/// Every node whose first request goes to the forge, run against a forge
|
||||||
/// that accepts the connection and never answers: each ends `Failed`
|
/// that accepts the connection and never answers: each ends `Failed`
|
||||||
/// with an error naming the node instead of holding its executor slot.
|
/// with an error naming the node instead of holding its executor slot.
|
||||||
|
|
|
||||||
|
|
@ -117,19 +117,6 @@ crate does not link it, and there is no wire-type crate between them.
|
||||||
Two fields out, two in, both ends validating — a drift shows up as a
|
Two fields out, two in, both ends validating — a drift shows up as a
|
||||||
400 naming the field.
|
400 naming the field.
|
||||||
|
|
||||||
## `agent mint-identity`
|
|
||||||
|
|
||||||
```console
|
|
||||||
# swarmctl agent mint-identity ruth
|
|
||||||
```
|
|
||||||
|
|
||||||
`POST /api/agents/{name}/identity` on the swarm-controller. Re-runs the
|
|
||||||
store-identity mint for one agent that already exists — the backfill for an
|
|
||||||
agent the swarm never created, such as the manager agent `hive-c0re` makes at
|
|
||||||
startup. It re-mints the agent's store certificate, which the agent picks up
|
|
||||||
the next time its container boots, and leaves an existing queue secret alone.
|
|
||||||
Queues and returns, like `agent create`.
|
|
||||||
|
|
||||||
## `agent mint-forge-token`
|
## `agent mint-forge-token`
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! `swarmctl agent create`, `swarmctl agent mint-identity` and `swarmctl agent
|
//! `swarmctl agent create` and `swarmctl agent mint-forge-token` — queue work
|
||||||
//! mint-forge-token` — queue work on the swarm-controller's job graph.
|
//! on the swarm-controller's job graph.
|
||||||
//!
|
//!
|
||||||
//! Each POSTs and returns as soon as the work is *inserted*. Both verbs
|
//! Each POSTs and returns as soon as the work is *inserted*. Both verbs
|
||||||
//! print the queued node id and stop: creation's last node only *publishes*
|
//! print the queued node id and stop: creation's last node only *publishes*
|
||||||
|
|
@ -53,12 +53,6 @@ struct CreateAgentResponse {
|
||||||
warnings: Vec<String>,
|
warnings: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Success body of `POST /api/agents/{name}/identity`.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct MintIdentityResponse {
|
|
||||||
node_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Success body of `POST /api/agents/{name}/forge-token`, which takes no body.
|
/// Success body of `POST /api/agents/{name}/forge-token`, which takes no body.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct MintForgeTokenResponse {
|
struct MintForgeTokenResponse {
|
||||||
|
|
@ -108,35 +102,6 @@ pub(crate) fn parse_ident(value: &str, what: &str) -> Result<String> {
|
||||||
.map_err(|reason| anyhow::anyhow!("invalid {what} {value:?}: {reason}"))
|
.map_err(|reason| anyhow::anyhow!("invalid {what} {value:?}: {reason}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `swarmctl agent mint-identity`.
|
|
||||||
///
|
|
||||||
/// Synchronous for the same reason [`create`] is, and built on the same
|
|
||||||
/// round trip.
|
|
||||||
pub(crate) fn mint_identity(target: &Target, name: &str) -> Result<()> {
|
|
||||||
// Client-side first, so a typo is a local error rather than a 400 the
|
|
||||||
// operator waits for. The controller validates it again.
|
|
||||||
let name = parse_ident(name, "agent name")?;
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.context("starting a tokio runtime for the controller request")?;
|
|
||||||
let resp: MintIdentityResponse = rt.block_on(post(
|
|
||||||
target,
|
|
||||||
&format!("/api/agents/{name}/identity"),
|
|
||||||
&serde_json::json!({}),
|
|
||||||
"mint-identity",
|
|
||||||
))?;
|
|
||||||
|
|
||||||
println!("queued: job node {}", resp.node_id);
|
|
||||||
println!(
|
|
||||||
"agent {name:?} will have its identity re-minted once the job graph runs; `swarmctl` \
|
|
||||||
does not wait for it. An existing queue secret is kept as it is; the store \
|
|
||||||
certificate is re-minted and the agent picks the new one up on its next boot"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run `swarmctl agent mint-forge-token`.
|
/// Run `swarmctl agent mint-forge-token`.
|
||||||
///
|
///
|
||||||
/// Synchronous for the same reason [`create`] is, and built on the same
|
/// Synchronous for the same reason [`create`] is, and built on the same
|
||||||
|
|
|
||||||
|
|
@ -237,22 +237,6 @@ enum AgentVerb {
|
||||||
/// No approval gate guards this: the caller is root on the controller's
|
/// No approval gate guards this: the caller is root on the controller's
|
||||||
/// host, or logged in through the swarm domain's SSO gate.
|
/// host, or logged in through the swarm domain's SSO gate.
|
||||||
Create(AgentCreateArgs),
|
Create(AgentCreateArgs),
|
||||||
/// Queue a re-mint of an existing agent's identity at the swarm's
|
|
||||||
/// secret store.
|
|
||||||
///
|
|
||||||
/// **The backfill verb.** Agent creation is event-driven and nothing at
|
|
||||||
/// swarm level sweeps for agents that are missing a credential, so an
|
|
||||||
/// agent created before a credential joined the mint never receives one.
|
|
||||||
/// This re-runs the mint for one agent that already exists.
|
|
||||||
///
|
|
||||||
/// ⚠️ **It re-mints the agent's store certificate**, which that agent
|
|
||||||
/// picks up the next time its container boots. It leaves an existing
|
|
||||||
/// queue secret exactly as it stands, so running this against an
|
|
||||||
/// already-migrated agent doesn't disturb its queue connection.
|
|
||||||
///
|
|
||||||
/// Queues and returns, the same way `agent create` does — watch the
|
|
||||||
/// swarm UI's job view for the outcome.
|
|
||||||
MintIdentity(AgentMintIdentityArgs),
|
|
||||||
/// Check one agent's forge token, and mint it if it's missing or stale.
|
/// Check one agent's forge token, and mint it if it's missing or stale.
|
||||||
///
|
///
|
||||||
/// swarm-controller does this for every agent with a store identity at
|
/// swarm-controller does this for every agent with a store identity at
|
||||||
|
|
@ -353,19 +337,6 @@ struct AgentMintForgeTokenArgs {
|
||||||
controller_socket: Option<PathBuf>,
|
controller_socket: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
struct AgentMintIdentityArgs {
|
|
||||||
/// Name of an agent that already exists.
|
|
||||||
name: String,
|
|
||||||
/// swarm-controller's unix socket.
|
|
||||||
///
|
|
||||||
/// Supplied by the nix module that installs this binary, from the same
|
|
||||||
/// `socketPath` option the daemon binds; falls back to
|
|
||||||
/// `SWARM_CONTROLLER_SOCKET`.
|
|
||||||
#[arg(long, value_name = "PATH")]
|
|
||||||
controller_socket: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum ForgeVerb {
|
enum ForgeVerb {
|
||||||
/// Make an existing forge user a site admin.
|
/// Make an existing forge user a site admin.
|
||||||
|
|
@ -589,13 +560,6 @@ fn main() -> Result<()> {
|
||||||
agent::create(&target, &args.name, &args.hive)
|
agent::create(&target, &args.name, &args.hive)
|
||||||
}
|
}
|
||||||
// Same socket-resolution reasoning as `Create` above.
|
// Same socket-resolution reasoning as `Create` above.
|
||||||
Verb::Agent {
|
|
||||||
command: AgentVerb::MintIdentity(args),
|
|
||||||
} => {
|
|
||||||
let target = remote.target(args.controller_socket)?;
|
|
||||||
agent::mint_identity(&target, &args.name)
|
|
||||||
}
|
|
||||||
// Same socket-resolution reasoning as `Create` above.
|
|
||||||
Verb::Agent {
|
Verb::Agent {
|
||||||
command: AgentVerb::MintForgeToken(args),
|
command: AgentVerb::MintForgeToken(args),
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -1141,32 +1105,6 @@ mod tests {
|
||||||
assert!(args.controller_socket.is_none());
|
assert!(args.controller_socket.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn the_backfill_verb_takes_an_agent_and_no_hive() {
|
|
||||||
let cli = Cli::try_parse_from(["swarmctl", "agent", "mint-identity", "scribe"])
|
|
||||||
.expect("the minimal form parses");
|
|
||||||
let Verb::Agent {
|
|
||||||
command: AgentVerb::MintIdentity(args),
|
|
||||||
} = cli.command
|
|
||||||
else {
|
|
||||||
panic!("expected `agent mint-identity`");
|
|
||||||
};
|
|
||||||
assert_eq!(args.name, "scribe");
|
|
||||||
assert!(args.controller_socket.is_none());
|
|
||||||
assert!(
|
|
||||||
Cli::try_parse_from([
|
|
||||||
"swarmctl",
|
|
||||||
"agent",
|
|
||||||
"mint-identity",
|
|
||||||
"scribe",
|
|
||||||
"--hive",
|
|
||||||
"a"
|
|
||||||
])
|
|
||||||
.is_err(),
|
|
||||||
"the identity has no hive, so the verb must not take one"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_forge_token_verb_takes_an_agent_and_no_hive() {
|
fn the_forge_token_verb_takes_an_agent_and_no_hive() {
|
||||||
let cli = Cli::try_parse_from(["swarmctl", "agent", "mint-forge-token", "scribe"])
|
let cli = Cli::try_parse_from(["swarmctl", "agent", "mint-forge-token", "scribe"])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue