gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
//! `Clans`: ordinary accounts grouped, each giving part of its own room
//! (owner, 2026-10-03 and 2026-10-05).
//!
//! # Why every verb is the self-scope
//!
//! A clan verb is always the caller acting on its OWN standing: making a clan,
//! answering its own invitation, giving or taking back its own room, leaving —
//! or, as the clan's admin, administering the clan. None of that is a grant on
//! the server's table, so none of it is asked of [`ServiceContext::require`]:
//! [`ServiceContext::require_self`] keeps what is not a grant (a browse
//! replica's refusal, a narrowed principal's list, the audit record) and the
//! identity table decides the rest from the clan itself — who its admin is,
//! whether an account is an ordinary one, whether the room fits. A server
//! admin is not a clan's admin: the clan's verbs refuse it like anybody else.
//!
//! What only the stores know is measured HERE and handed in, as
//! `Accounts.EnrollEmployee` does with its room: the caller's own cap and what
//! it stores ([`account_room`]), and what the clan stores (its namespace's
//! `pack_bytes`, the meter a push is refused against). A clan whose usage
//! could not be read is never treated as empty.
//!
//! [`ServiceContext::require`]: crate::service::ServiceContext::require
//! [`ServiceContext::require_self`]: crate::service::ServiceContext::require_self

use gunnar_proto::clans_server::Clans;
use gunnar_proto::{
    AcceptClanInviteRequest, Clan, ClanResponse, ClanRole, CreateClanRequest, DeclineClanInviteRequest,
    DissolveClanRequest, DissolveClanResponse, GetClanRequest, GrantLevel, InviteToClanRequest,
    LeaveClanRequest, ListClansRequest, ListClansResponse, RemoveClanMemberRequest, SetClanAdminRequest,
    SetClanContributionRequest,
};
use gunnar_proto::{AnswerRepoOfferRequest, AnswerRepoOfferResponse, OfferRepoRequest};
use tonic::{Request, Response, Status};

use crate::auth::Caller;
use crate::clans::{ClanPlane, ClanRoom};
use crate::service::accounts::account_room;
use crate::service::ClansService;

/// A required name, trimmed, or `INVALID_ARGUMENT`.
fn named(raw: &str, field: &'static str) -> Result<String, Status> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(Status::invalid_argument(format!("`{field}` is required")));
    }
    Ok(trimmed.to_owned())
}

impl ClansService {
    /// The plane's clan half, or UNIMPLEMENTED — never an empty answer.
    fn plane(&self) -> Result<&dyn ClanPlane, Status> {
        self.ctx
            .control
            .clans()
            .ok_or_else(|| Status::unimplemented("Clans: this server keeps no clans"))
    }

    /// The caller's own account's room, measured: its cap and what it
    /// stores. Uncapped (`cap: None`) wherever nothing caps it.
    fn room(&self, account: &str) -> Result<ClanRoom, Status> {
        Ok(match account_room(&self.ctx, account)? {
            Some((cap, stored)) => ClanRoom {
                cap: Some(cap),
                stored,
            },
            None => ClanRoom::default(),
        })
    }

    /// What the clan stores: its namespace's `pack_bytes` AND `lfs_bytes`
    /// (round 2, owner 2026-10-05: "LFS counts in clan room") — what a push
    /// or an LFS upload into it is refused against. `None` when it could not
    /// be measured.
    fn used(&self, clan: &str) -> Option<u64> {
        match self.ctx.control.usage(Some(clan)) {
            Ok(rows) => rows
                .into_iter()
                .find(|row| row.namespace == clan)
                .map(|row| row.pack_bytes.saturating_add(row.lfs_bytes)),
            Err(_) => None,
        }
    }

    /// One line on the audit target for a repository offered out of a clan,
    /// taken, declined or withdrawn (round 2) — beside the `Clans.*` record
    /// every call already leaves in the audit ledger — and the entry in the
    /// ACCOUNT audit logs it belongs to (2026-10-06): the clan's own, the
    /// offering admin's account's (`offered_by`), the receiving account's
    /// (`to`) and the acting account's, each log once.
    fn audit_offer(&self, caller: &Caller, event: &'static str, clan: &str, repo: &str, to: &str, offered_by: &str) {
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event,
            principal = %caller.principal_id,
            clan = %clan,
            repo = %repo,
            to = %to,
            "a repository offered out of a clan"
        );
        let detail = if event == "clan.repo_moved" {
            format!("clan {clan}, repository {repo}, now {to}/{repo}")
        } else {
            format!("clan {clan}, repository {repo}, account {to}")
        };
        let mut logs = vec![clan.to_owned()];
        let offerer = self.ctx.account_of_principal(offered_by);
        let actor = self.ctx.account_of_principal(&caller.principal_id);
        for account in [offerer, Some(to.to_owned()), actor].into_iter().flatten() {
            if !logs.contains(&account) {
                logs.push(account);
            }
        }
        let target = format!("{clan}/{repo}");
        for account in logs {
            self.ctx
                .account_event(Some(account), crate::enterprise::AccountAuditEvent::by(caller, event, target.clone()).detail(detail.clone()));
        }
    }

    /// **The clan as the caller sees it**: its standing, its own
    /// contribution, how low that may go, what its account could still give
    /// — and the invitations an admin sees (a plain member sees none, an
    /// invited account its own).
    fn seen(&self, caller: &str, mut clan: Clan, room_left: Option<u64>) -> Clan {
        let role = match clan.members.iter().find(|m| m.principal_id == caller) {
            Some(m) if m.admin => ClanRole::Admin,
            Some(_) => ClanRole::Member,
            None if clan.invites.iter().any(|i| i.principal_id == caller) => ClanRole::Invited,
            None => ClanRole::Unspecified,
        };
        if role != ClanRole::Admin {
            clan.invites.retain(|i| i.principal_id == caller);
            clan.offers.clear();
        }
        let own = clan
            .members
            .iter()
            .find(|m| m.principal_id == caller)
            .map_or(0, |m| m.contribution_bytes);
        let used = self.used(&clan.name);
        clan.used_bytes = used;
        clan.your_role = role as i32;
        clan.your_contribution_bytes = own;
        // The floor counts the OTHERS' shares in force (round 2): a lapsed
        // share holds nothing up, and a lapsed member's own is out of force.
        let me_lapsed = clan.members.iter().any(|m| m.principal_id == caller && m.lapsed);
        let others_in_force: u64 = clan
            .members
            .iter()
            .filter(|m| m.principal_id != caller && !m.lapsed)
            .map(|m| m.contribution_bytes)
            .fold(0, u64::saturating_add);
        clan.your_floor_bytes = match role {
            ClanRole::Admin | ClanRole::Member if me_lapsed => used.map(|_| 0),
            ClanRole::Admin | ClanRole::Member => used.map(|used| used.saturating_sub(others_in_force)),
            _ => None,
        };
        clan.your_room_left_bytes = room_left;
        clan
    }

    /// What the caller could still give, or `None` (uncapped, or the room
    /// could not be read — the number is a reading for the page, never a
    /// decision; every decision measures again).
    fn room_left(&self, caller: &str) -> Option<u64> {
        let room = self.room(caller).ok()?;
        self.plane().ok()?.clan_room_left(caller, room).ok().flatten()
    }

    /// The answer every write gives: the clan as the caller now sees it.
    fn answer(&self, caller: &Caller, clan: Clan) -> Response<ClanResponse> {
        let left = self.room_left(&caller.principal_id);
        Response::new(ClanResponse {
            clan: Some(self.seen(&caller.principal_id, clan, left)),
        })
    }

    /// One line on the audit target per clan write, naming who and what —
    /// and the entry in the ACCOUNT audit logs it belongs to (clans + the
    /// enterprise list, 2026-10-05): the clan's own (a clan is an account to
    /// its admins), the acting account's own, and the other account's when
    /// the verb named one (invited, removed, made admin).
    fn audit(&self, caller: &Caller, event: &'static str, clan: &str, other: &str, bytes: u64) {
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event,
            principal = %caller.principal_id,
            clan = %clan,
            other = %other,
            bytes,
            "a clan changed"
        );
        let detail = match (other.is_empty(), event) {
            (_, "clan.created" | "clan.joined" | "clan.contribution") => format!("clan {clan}, giving {bytes} bytes"),
            (false, _) => format!("clan {clan}, account {other}"),
            (true, _) => format!("clan {clan}"),
        };
        let target = if other.is_empty() { clan } else { other };
        let entry = |target: &str| crate::enterprise::AccountAuditEvent::by(caller, event, target).detail(detail.clone());
        self.ctx.account_event(Some(clan.to_owned()), entry(target));
        let mine = self.ctx.account_of_principal(&caller.principal_id);
        if let Some(own) = mine.clone().filter(|own| own != clan) {
            self.ctx.account_event(Some(own), entry(target));
        }
        if !other.is_empty() && other != caller.principal_id {
            if let Some(theirs) = self.ctx.account_of_principal(other).filter(|a| a != clan && Some(a) != mine.as_ref()) {
                self.ctx.account_event(Some(theirs), entry(clan));
            }
        }
    }
}

#[tonic::async_trait]
impl Clans for ClansService {
    /// **`Clans.Create`**: a free name becomes the clan and its private
    /// namespace; the caller is its admin and gives `contribution_bytes` of
    /// its own room. A console's reserved name is refused as for an employee.
    async fn create(&self, request: Request<CreateClanRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Create")?;
        let request = request.into_inner();
        let name = named(&request.name, "name")?;
        if crate::service::access::is_reserved_principal_name(&name) {
            return Err(crate::service::access::reserved_name_refusal(&name, "a clan"));
        }
        // The name filter (clans + the enterprise list, 2026-10-05): a clan's
        // name is a namespace, named here — refused when offensive, and a
        // borderline one is refused too (a new clan has no admin to ask yet).
        self.ctx.filter_name(&caller, "namespace", &name, None, false)?;
        let room = self.room(&caller.principal_id)?;
        let clan = self
            .plane()?
            .clan_create(&name, &caller.principal_id, request.contribution_bytes, room)?;
        self.audit(&caller, "clan.created", &name, "", request.contribution_bytes);
        Ok(self.answer(&caller, clan))
    }

    /// **`Clans.Get`**: to a member or an invited account; to anybody else
    /// the clan does not exist, in the words a clan that does not exist gets.
    async fn get(&self, request: Request<GetClanRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Read, "Clans.Get")?;
        let name = named(&request.into_inner().clan, "clan")?;
        let absent = || Status::not_found(format!("there is no clan {name} you are in or invited to"));
        let clan = self.plane()?.clan_get(&name)?.ok_or_else(absent)?;
        let inside = clan.members.iter().any(|m| m.principal_id == caller.principal_id)
            || clan.invites.iter().any(|i| i.principal_id == caller.principal_id);
        if !inside {
            return Err(absent());
        }
        Ok(self.answer(&caller, clan))
    }

    /// **`Clans.List`**: the caller's clans and invitations.
    async fn list(&self, request: Request<ListClansRequest>) -> Result<Response<ListClansResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Read, "Clans.List")?;
        let left = self.room_left(&caller.principal_id);
        let plane = self.plane()?;
        let clans = plane
            .clans_of(&caller.principal_id)?
            .into_iter()
            .map(|clan| self.seen(&caller.principal_id, clan, left))
            .collect();
        // Repositories offered to the caller's OWN account (round 2).
        let offers = plane.clan_offers_to(&caller.principal_id)?;
        Ok(Response::new(ListClansResponse { clans, offers }))
    }

    /// **`Clans.Invite`**: the clan's admin invites an ordinary account.
    async fn invite(&self, request: Request<InviteToClanRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Invite")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let principal = named(&request.principal_id, "principal_id")?;
        let row = self.plane()?.clan_invite(&clan, &caller.principal_id, &principal)?;
        self.audit(&caller, "clan.invited", &clan, &principal, 0);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.Accept`**: the invited account joins, giving
    /// `contribution_bytes` of its own room.
    async fn accept(&self, request: Request<AcceptClanInviteRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Accept")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let room = self.room(&caller.principal_id)?;
        let row = self.plane()?.clan_answer(
            &clan,
            &caller.principal_id,
            true,
            request.contribution_bytes,
            room,
        )?;
        self.audit(&caller, "clan.joined", &clan, "", request.contribution_bytes);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.Decline`**: the caller declines its own invitation — or, as
    /// the clan's admin, withdraws another account's.
    async fn decline(&self, request: Request<DeclineClanInviteRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Decline")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let target = match request.principal_id.trim() {
            "" => caller.principal_id.clone(),
            other => other.to_owned(),
        };
        let plane = self.plane()?;
        if target == caller.principal_id {
            let row = plane.clan_answer(&clan, &caller.principal_id, false, 0, ClanRoom::default())?;
            self.audit(&caller, "clan.declined", &clan, "", 0);
            // The caller is no longer invited: it sees the clan no more.
            let mut row = self.seen(&caller.principal_id, row, None);
            row.members.clear();
            return Ok(Response::new(ClanResponse { clan: Some(row) }));
        }
        let row = plane.clan_withdraw(&clan, &caller.principal_id, &target)?;
        self.audit(&caller, "clan.invite_withdrawn", &clan, &target, 0);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.SetContribution`**: up out of the member's own room; down no
    /// lower than what the clan stores leaves room for.
    async fn set_contribution(
        &self,
        request: Request<SetClanContributionRequest>,
    ) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.SetContribution")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let room = self.room(&caller.principal_id)?;
        let used = self.used(&clan);
        let row = self.plane()?.clan_set_contribution(
            &clan,
            &caller.principal_id,
            request.contribution_bytes,
            room,
            used,
        )?;
        self.audit(&caller, "clan.contribution", &clan, "", request.contribution_bytes);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.Leave`**: the contribution goes back — refused while the clan
    /// uses it, and to the clan's last admin.
    async fn leave(&self, request: Request<LeaveClanRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Leave")?;
        let clan = named(&request.into_inner().clan, "clan")?;
        let used = self.used(&clan);
        let row = self.plane()?.clan_leave(&clan, &caller.principal_id, used)?;
        self.audit(&caller, "clan.left", &clan, "", 0);
        // Out of it now: what the caller may still see is that it left.
        let mut row = self.seen(&caller.principal_id, row, None);
        row.members.clear();
        row.invites.clear();
        Ok(Response::new(ClanResponse { clan: Some(row) }))
    }

    /// **`Clans.RemoveMember`**: the clan's admin removes a member.
    async fn remove_member(&self, request: Request<RemoveClanMemberRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.RemoveMember")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let member = named(&request.principal_id, "principal_id")?;
        let used = self.used(&clan);
        let row = self.plane()?.clan_remove(&clan, &caller.principal_id, &member, used)?;
        self.audit(&caller, "clan.member_removed", &clan, &member, 0);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.SetAdmin`**: the clan's admin makes a member admin, or not.
    async fn set_admin(&self, request: Request<SetClanAdminRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.SetAdmin")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let member = named(&request.principal_id, "principal_id")?;
        let row = self
            .plane()?
            .clan_set_admin(&clan, &caller.principal_id, &member, request.admin)?;
        self.audit(
            &caller,
            if request.admin { "clan.admin_made" } else { "clan.admin_unmade" },
            &clan,
            &member,
            0,
        );
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.Dissolve`**: the clan's admin ends an EMPTY clan.
    async fn dissolve(&self, request: Request<DissolveClanRequest>) -> Result<Response<DissolveClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.Dissolve")?;
        let clan = named(&request.into_inner().clan, "clan")?;
        let row = self.plane()?.clan_dissolve(&clan, &caller.principal_id)?;
        self.audit(&caller, "clan.dissolved", &clan, "", 0);
        Ok(Response::new(DissolveClanResponse { clan: Some(row) }))
    }

    /// **`Clans.OfferRepo`** (round 2): the clan's admin offers one of the
    /// clan's repositories to an account, to move it out.
    async fn offer_repo(&self, request: Request<OfferRepoRequest>) -> Result<Response<ClanResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.OfferRepo")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let repo = named(&request.repo, "repo")?;
        let account = named(&request.account, "account")?;
        let row = self.plane()?.clan_offer_repo(&clan, &caller.principal_id, &repo, &account)?;
        self.audit_offer(&caller, "clan.repo_offered", &clan, &repo, &account, &caller.principal_id);
        Ok(self.answer(&caller, row))
    }

    /// **`Clans.AnswerRepoOffer`** (round 2): the offered account's admin —
    /// gunnar's own rule, Admin over the account's namespace — takes the
    /// repository (it moves now, if the account's room holds it) or declines;
    /// the clan's admin withdraws. To anybody else there is no such offer.
    async fn answer_repo_offer(
        &self,
        request: Request<AnswerRepoOfferRequest>,
    ) -> Result<Response<AnswerRepoOfferResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        self.ctx.require_self(&caller, GrantLevel::Admin, "Clans.AnswerRepoOffer")?;
        let request = request.into_inner();
        let clan = named(&request.clan, "clan")?;
        let repo = named(&request.repo, "repo")?;
        let plane = self.plane()?;
        let absent = || Status::not_found(format!("there is no offer of {clan}/{repo} you may answer"));
        let offer = plane.clan_repo_offer(&clan, &repo)?.ok_or_else(absent)?;
        let to = offer.to_account.clone();
        let offered_by = offer.offered_by.clone();
        let account_admin = self
            .ctx
            .check(&caller, crate::auth::Access::namespace(&to), GrantLevel::Admin)?
            .is_none();
        if !account_admin {
            // Not the offered account's: the clan's admin may withdraw, and
            // may not take it for the account; anybody else meets no offer.
            let clan_admin = plane.clan_role(&caller.principal_id, &clan)? == Some(ClanRole::Admin);
            if !clan_admin {
                return Err(absent());
            }
            if request.accept {
                return Err(Status::permission_denied(format!(
                    "{clan}/{repo} is offered to account {to}: only its admin takes it"
                )));
            }
            plane.clan_withdraw_offer(&clan, &caller.principal_id, &repo)?;
            self.audit_offer(&caller, "clan.repo_offer_withdrawn", &clan, &repo, &to, &offered_by);
            return Ok(Response::new(AnswerRepoOfferResponse { offer: Some(offer), repo: None }));
        }
        // The account's admin, asked again as the verb, so the ledger holds it.
        self.ctx
            .require(&caller, crate::auth::Access::namespace(&to), GrantLevel::Admin, "Clans.AnswerRepoOffer")?;
        if !request.accept {
            let offer = plane.clan_decline_offer(&clan, &repo, &to)?;
            self.audit_offer(&caller, "clan.repo_offer_declined", &clan, &repo, &to, &offered_by);
            return Ok(Response::new(AnswerRepoOfferResponse { offer: Some(offer), repo: None }));
        }
        // The account's room must hold what the repository stores: it comes
        // out of the account's own room, as a contribution does.
        let room = self.room(&to)?;
        if let (Some(stored), Some(left)) = (offer.stored_bytes, plane.clan_room_left(&to, room)?) {
            if stored > left {
                return Err(Status::failed_precondition(format!(
                    "account {to} has {left} bytes of its own room unused, and {clan}/{repo} holds {stored}: \
                     {} bytes short. Add room to the account, or free some, before taking it",
                    stored - left
                )));
            }
        }
        let (offer, moved) = plane.clan_take_offer(&clan, &repo, &to, &caller.principal_id)?;
        // To everything watching: a name stopped existing, another began.
        self.ctx.bus.publish(gunnar_proto::event::Payload::RepoDeleted(gunnar_proto::RepoDeleted {
            repo: Some(gunnar_proto::RepoPath::new(&clan, &repo)),
            principal_id: caller.principal_id.clone(),
        }));
        self.ctx.bus.publish(gunnar_proto::event::Payload::RepoCreated(gunnar_proto::RepoCreated {
            repo: Some(moved.clone()),
            principal_id: caller.principal_id.clone(),
        }));
        self.audit_offer(&caller, "clan.repo_moved", &clan, &repo, &to, &offered_by);
        Ok(Response::new(AnswerRepoOfferResponse { offer: Some(offer), repo: Some(moved) }))
    }
}