#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""The `msg` shell command: read and write the message spool.**Five verbs, one per thing a person does with a message** — `list` what is waiting,`read` one, `send` one, `dismiss` one, and `act` on one. There were seven, and the twoextra ones were the confusing part rather than the useful part: `queue` was `list` with astaff-only filter over a mailbox that already shows staff the queue, and `notice` was`send` with a different recipient. Both were labelled STAFF in the menu and both refused*after* walking a `contributor` through subject, body and recipients — a menu that offerswhat it will then refuse teaches the ladder in the worst possible place. Now the ladder isin what a verb *asks*: `to` is only asked of staff, because for everyone else there is onepossible answer.`act` is the other half of that: `save` was a verb for one message kind, which meant thereader had to know which of seven verbs their message wanted. A message knows what it isfor — :func:`~euler_webshell.msg.verb_for` decides it from the subject — so `act` takes the idand does the thing: take the key, grant the key, merge the pull request, walk throughregistering unregistered answers, or, for prose, simply read it. The header chip labels itsrows from that same function, so what the button says and what the command does cannot drift.Registered at the **reader** floor — the terminal is the front door for every rung, and anew invitee's first need is often to ask a question. The two acts that are staff's alone(`authorize`, `merge`) are reachable only through :func:`~euler_webshell.msg.verb_for`, whichhands them out at :data:`~euler_webshell.msg.identity.STAFF_FLOOR` and hands everyone else aplain read. `dismiss` is not floored here at all: the service already answers that questionbetter — staff may drop anything, anyone else a message they are a party to, which is whatlets `act` clean up after the key it just took.**Two channels, one renderer.** Which plane answers — the web shell's direct`msg.sock`, or the operator's `sudo` fallback — is :mod:`euler_webshell.msg.client`'sproblem, not this module's. Both come back as the same JSON, and everything belowrenders it identically; a `None` from the client means neither plane answered, which:func:`_unreachable` says once for every verb.That client is shared: `user-authorize <thread-id>` reads and answers a key-requestthread through the same two planes (:mod:`solver.crypto.keys`), which is why the calllives beside the spool rather than inside this command."""from__future__importannotations__all__=['msg']fromtypingimportAnnotated,Any,Literal,NamedTuplefromsolver.auth.subjectimportrankfromsolver.configimportExitCodes,configfromeuler_webshell.shellimportoscfromsolver.shellimportconsole,registerfromsolver.shell.commandimportContextfromsolver.core.noticesimportunregistered_problemsfromsolver.shell.dialogueimportAbort,Ask,Choice,interactivefromsolver.shell.variablesimportvariable,variablesfromeuler_webshell.msg.clientimportcallas_callfromeuler_webshell.msgimport(KEY_ISSUE_SUBJECT,KEY_REQUEST_SUBJECT,KEY_SHARE_SUBJECT,PR_REVIEW_SUBJECT,UNREGISTERED_SUBJECT,verb_for)fromeuler_webshellimportrosterfromeuler_webshell.msg.identityimportSTAFF_FLOOR#: What each verb means, for the menu `msg` shows when no verb was typed. The `Literal` stays#: the source of what is *valid*; this only says what each option does. No "STAFF:" markers:#: every verb here is one every rung can run, and the one whose *reach* widens with the#: ladder (`send`) says so by asking staff a question it does not ask anyone else._ACTIONS:dict[str,str]={'list':'everything waiting for you, newest first','read':'open one message, and mark it read','send':'write to staff — or, as staff, to users','dismiss':'drop a message you are done with','act':'do what a message asks: take a key, complete one, grant one, merge a pull request, ''register answers',}#: The verbs that name an existing message._ON_THREAD:tuple[str,...]=('read','dismiss','act')#: The wire form for "everyone", which the service resolves to every mapped identity, and#: the word the menu offers for it — nobody should have to know that `*` is the wire._EVERYONE:str='*'_EVERYONE_LABEL:str='everyone'#: The default recipient, and the only one below the staff floor: a message to the#: maintainers and admins, which is what the spool is for._STAFF_LABEL:str='staff'classThread(NamedTuple):"""One message in this caller's mailbox, as a `loop` body and a menu both want it."""id:strsubject:strauthor:strupdated:strunread:bool#: What `act` would do to it, for this caller — `read`, `save`, `merge`, …verb:str@propertydefnote(self)->str:"""The reading line beside the subject: who it is from, when, and whether it is new."""returnf'{self.author} · {_when(self.updated)}'+(' · unread'ifself.unreadelse'')def__choice__(self)->Choice:"""The row, without the verb: see :func:`_thread_choices` for why that is separate."""returnChoice(self.id,self.subject,self.note)@variable("this caller's messages, newest first")defthreads()->list[Thread]:"""This caller's mailbox — the same read `msg list` prints. Always the mailbox, for every thread verb. `dismiss` used to read the *staff queue*, which holds inbound questions only (`store.inbound` filters on `kind == 'inbound'`), so a notice could be listed by `msg list` and still leave `msg dismiss` saying "no messages". The mailbox is also the wider set for staff — `_visible_to` lets them see inbound threads too — and it is exactly what the service will accept: staff may dismiss anything, anyone else a thread they are a party to. An empty list means an empty mailbox. A spool that cannot be reached **raises**, for the reason `open_prs` does: "no messages" is the one answer that is certainly wrong when nothing answered, and a `loop {threads}:` over a dead service must not run zero times in silence. """result=_call('mailbox')ifresultisNoneorresult[0]!=200ornotisinstance(result[1],dict):raiseAbort('the message service is not reachable (is euler-msg.service running?)',rc=ExitCodes.EXIT_ERROR)return[Thread(id=str(thread.get('id','')),subject=str(thread.get('subject','')),author=str(thread.get('author_name','')),updated=str(thread.get('updated','')),unread=bool(thread.get('unread')),verb=_verb_of(thread))forthreadinresult[1].get('threads')or[]]@variable('the key-authorization requests waiting to be granted')defkey_requests()->list[Thread]:"""The messages asking for a master key, which `user-authorize` acts on by id. A slice of the same mailbox read: staff see every collaborator's request, so this is the queue a maintainer works through. Filtered on the subject the `user` command files them under, which is also what `user-authorize` checks again before it grants anything — a menu is not a gate. """return[threadforthreadinthreads()ifthread.subject.startswith(KEY_REQUEST_SUBJECT)]def_thread_choices(_:Context,bound:dict[str,Any])->list[Choice]:"""The mailbox as menu options, labelled by the verb when the verb is the question. The one choice set that stays a callable: for `act` each row also names **what acting on it would do**, which depends on the answer already bound to `action` — and a variable cannot see that. Without it the menu would be a list of subjects and the reader would have to guess which one carries a key. Only when there is something to name: `read` on every other line is the noise the label exists to cut through, exactly as on the header chip's rows. The read itself is the `threads` variable, so this and `loop {threads}:` cannot drift. """action=str(bound.get('action')or'')mailbox=threads()ifnotmailbox:raiseAbort(f'no messages to {actionor"act on"}',rc=ExitCodes.EXIT_ERROR)return[thread.__choice__()ifnot(action=='act'andthread.verb!='read')elseChoice(thread.id,thread.subject,f'{thread.note} · {thread.verb}')forthreadinmailbox]def_recipient_choices(_:Context,__:dict[str,Any])->list[Choice]:"""Who a message can go to: two audiences, then every collaborator in the roster. A callable rather than a variable, and not a filter on `{accounts}` either: `staff` and `everyone` are **not accounts**. They are routing keys the spool accepts — which is why this cannot be `choices='accounts', which=…`, and why naming the whole thing as a variable would name a set that only a menu has any use for. Asked of staff only (:func:`_needs_recipients`), so `staff` leads but is not the whole menu. The people come from the roster (`/etc/euler/roster/users.json`), a plain file read — so a maintainer over the web gets the same menu the operator's terminal does, where the old admin-plane read left them the two words and nothing else. They are **slugs**, and that is what the spool wants: it routes by box key, and for a web identity the box key *is* the slug (:func:`~euler_webshell.msg.identity.box_of`). So no e-mail address has to be published anywhere to name a recipient. Still not strict — a slug the roster has not caught up with can be typed — and it never raises: a menu is not a gate, and the service decides what it will actually accept. """options=[Choice(_STAFF_LABEL,_STAFF_LABEL,'the maintainers and admins'),Choice(_EVERYONE_LABEL,_EVERYONE_LABEL,'every mapped identity')]try:options.extend(account.__choice__()foraccountinroster.accounts(scope='web'))exceptException:# noqa: BLE001 — a menu, not a gatepassreturnoptionsdef_needs_recipients(bound:dict[str,Any])->bool:"""Whether this `send` has a recipient worth asking about. Only staff are asked. Below that floor there is exactly one possible answer — staff — so the question would be a menu of one, and the two other options on it would be offers the service then refuses. A verb must not ask what it cannot honour. """returnbound.get('action')=='send'andnotbound.get('to')and_is_staff()def_needs_thread(bound:dict[str,Any])->bool:"""Whether the chosen verb names a message."""returnbound.get('action')in_ON_THREADdef_is_send(bound:dict[str,Any])->bool:"""Whether the chosen verb carries a message out, and so needs something to say."""returnbound.get('action')=='send'def_unreachable()->int:"""The one message for "no spool answered", on either plane."""console.print('[error]error:[/error] the message service is not reachable ''(is euler-msg.service running?)')return1def_is_staff()->bool:"""Whether this shell's subject is at or above the staff floor."""returnrank(config.subject.profile)>=rank(STAFF_FLOOR)def_verb_of(thread:dict[str,Any])->str:"""What `act` would do to *thread*, for this caller — the menu's hint and the dispatch."""returnverb_for(str(thread.get('subject','')),is_staff=_is_staff(),is_own=str(thread.get('author_name',''))==config.subject.user,from_staff=thread.get('kind')=='notice')# ── rendering ───────────────────────────────────────────────────────────────────────def_when(stamp:str)->str:"""An ISO-8601 timestamp trimmed to the minute — the row has to fit one line."""returnstamp[:16].replace('T',' ')def_print_thread_line(thread:dict[str,Any])->None:"""One line per message: unread marker, id, when, who, subject."""mark='[accent]●[/accent]'ifthread.get('unread')else' 'verb=_verb_of(thread)tail=f' [muted]({verb})[/muted]'ifverb!='read'else''console.print(f' {mark} [accent]{thread.get("id","")}[/accent] 'f'[muted]{_when(str(thread.get("updated","")))}[/muted] 'f'{str(thread.get("kind","")):7} 'f'{str(thread.get("author_name",""))[:18]:18}{thread.get("subject","")}{tail}')def_print_thread(thread:dict[str,Any])->None:"""One message in full — there is nothing else to it."""console.print(f'\n[accent]{thread.get("subject","")}[/accent] 'f'[muted]({thread.get("kind","")} · {thread.get("id","")})[/muted]')console.print(f'[muted]from[/muted] {thread.get("author_name","")} 'f'[muted]{_when(str(thread.get("created","")))}[/muted]')forlineinstr(thread.get('body')or'').splitlines():console.print(f' {line}')# ── verbs ───────────────────────────────────────────────────────────────────────────def_fetch(thread_id:str)->dict[str,Any]|None:"""One message as this caller may see it, or None with the reason already printed. The single read behind both `read` and `act`: `act` has to know what a message *is* before it can do anything with it, and fetching twice to decide and then to print would let the two answers differ. """result=_call('thread',thread_id=thread_id)ifresultisNone:_unreachable()returnNonestatus,data=resultifstatus!=200ornotisinstance(data,dict):console.print(f'[error]error:[/error] {status}{data}')returnNonereturndatadef_list()->int:"""Every message this caller may read, newest first. One list, for everybody. Staff see the inbound queue here as well — the store shows every inbound thread to any staff reader — so the separate `queue` verb was a second name for a subset of this, with its own ordering and its own emptiness message. """result=_call('mailbox')ifresultisNone:return_unreachable()status,data=resultifstatus!=200ornotisinstance(data,dict):console.print(f'[error]error:[/error] {status}{data}')return1threads=data.get('threads')or[]ifnotthreads:console.print('[muted]no messages[/muted]')return0unread=int(data.get('unread',0))console.print(f'[accent]{len(threads)}[/accent] message(s), 'f'[accent]{unread}[/accent] unread — read one with `msg read <id>`, ''or `msg act <id>` to do what it asks')forthreadinthreads:_print_thread_line(thread)return0def_read(thread_id:str)->int:"""Show one message and mark it read."""data=_fetch(thread_id)ifdataisNone:return1return_show(data)def_show(data:dict[str,Any])->int:"""Print an already-fetched message and mark it read — `read`, and `act`'s default."""_print_thread(data)_call('read',thread_id=str(data.get('id','')))# attention, not activity — failure is harmlessosc.messages_changed()# the unread count just droppedreturn0def_act(ctx:Context,thread_id:str)->int:"""Do what this message asks — and, for prose, that is to read it. The dispatch is :func:`~euler_webshell.msg.verb_for` on the subject, which is also what labels the header chip's rows, so the button and the command cannot disagree. The two staff acts are handed out by that function at :data:`STAFF_FLOOR` and never below it, which is what makes calling `user_authorize` / `gh_merge` as plain functions safe here: a caller who is not staff is never routed to them, and both commands hold their own floor for every other way in. Each act cleans up after itself, so there is nothing left to dismiss: taking a key dismisses the message that carried it, granting one dismisses the request, a merge dismisses the notice that announced the pull request, and a register walk that confirmed every answer dismisses the notice that listed them. """data=_fetch(thread_id)ifdataisNone:return1match_verb_of(data):case'save':rc=_save(thread_id,data)osc.git_changed()# a key landed: solutions/private re-checked-outreturnrccase'reconstruct':rc=_reconstruct(thread_id,data)osc.git_changed()returnrccase'authorize':fromeuler_webshell.shell.keysimportuser_authorizereturnuser_authorize(thread_id)case'merge':# The branch out of the subject, not the thread id: the queue this act works is# GitHub's, and `merge_pr_for_branch` turns the branch into the number the gate# needs — so acting on *this* notice lands *this* request, rather than offering# the whole queue as a menu. It dismisses this message itself, by the same# branch (`solver.core.git._dismiss_pr_notice`).fromsolver.core.gitimportmerge_pr_for_branchrc=merge_pr_for_branch(str(data.get('subject','')).removeprefix(PR_REVIEW_SUBJECT))osc.git_changed()# master movedosc.messages_changed()# the landed request's notice is dismissedreturnrccase'register':return_register(ctx,thread_id,data)case_:return_show(data)#: The walk `msg act` runs on an unregistered-answer notice, one problem per pass: open it,#: confirm it is registered, then — unless it was the last — wait a random 15 to 45 minutes#: before the next. A command block rather than Python, so it is the same block a person could#: type. `{unregistered}` holds the problems still to confirm (seeded by :func:`_register`) and#: loses its head at each confirmation, so a walk cut short (`show` failing, the confirmation#: left with Ctrl-C) leaves exactly the rest in it — and the next `msg act` resumes there,#: without walking the ones already registered. Ctrl-C during the wait only skips the wait.#: Run without the interpreter's statement echo: what shows is the commands' own output._REGISTER_BLOCK:str='''loop {unregistered}: { show {loop} || break pause "please confirm the answer for problem {loop} is registered" || break unregistered = {unregistered}[1:]{unregistered} && { wait = random 2700 900; sleep {wait} }}'''#: The shell variable :data:`_REGISTER_BLOCK` walks and leaves behind._UNREGISTERED_VAR:str='unregistered'def_register(ctx:Context,thread_id:str,data:dict[str,Any])->int:"""Walk through registering each answer an unregistered-answer notice names; dismiss it after. `summary` files the notice when problems recorded as solved here are not solved on the progress page — the answer was never given to projecteuler.net. Acting on it runs :data:`_REGISTER_BLOCK` over those problems in this shell, and only when the walk has confirmed every one is the message dismissed: a walk cut short leaves it in place, and the problems not yet confirmed in `{unregistered}`, so acting on it again picks up there. Leftovers from some other notice are ignored — only this one's problems are walked. Interactive only, because the walk ends in a confirmation per problem — and `pause`, with nobody to ask, is a no-op that would dismiss the notice on nobody's word. """ifnotstr(data.get('subject','')).startswith(UNREGISTERED_SUBJECT):console.print(f'[error]error:[/error] message [accent]{thread_id}[/accent] is not an ''unregistered-answer notice')return1numbers=unregistered_problems(str(data.get('body','')))ifnotnumbers:console.print(f'[error]error:[/error] message [accent]{thread_id}[/accent] names no ''problems to register')return1ifnotinteractive():console.print('[error]error:[/error] registering answers needs an interactive shell — ''each one is confirmed by hand')returnExitCodes.EXIT_USAGEifctx.shellisNone:console.print('[error]error:[/error] no shell to run the walk in')return1_print_thread(data)left=_left_to_register()resumed=[numberfornumberinleftifnumberinnumbers]variables[_UNREGISTERED_VAR]=resumedornumbersifresumed:console.print(f'[muted]resuming: {len(numbers)-len(resumed)} of {len(numbers)} ''already confirmed[/muted]')rc=int(ctx.shell.dispatch(_REGISTER_BLOCK,echo=False))if_left_to_register():console.print(f'[muted]Not every answer was confirmed — message [accent]{thread_id}''[/accent] is kept; `msg act` it again to carry on.[/muted]')returnrcor1delvariables[_UNREGISTERED_VAR]if_call('dismiss',thread_id=thread_id)isNone:console.print(f'[muted]All confirmed, but could not dismiss [accent]{thread_id}[/accent] — ''`msg dismiss` it yourself.[/muted]')osc.messages_changed()return0def_left_to_register()->list[int]:"""The problems `{unregistered}` still holds — `[]` when unset or not a list of numbers."""left=variables[_UNREGISTERED_VAR]if_UNREGISTERED_VARinvariableselseNoneifnotisinstance(left,list)ornotall(isinstance(number,int)fornumberinleft):return[]returnleftdef_send(subject:str,body:str,to:str)->int:"""Send one message: to staff, to everyone, or to named identities. One verb for what used to be two. `send` and `notice` differed only in *who receives*, which is an argument, not a verb — and making it a verb put the ladder in the menu, where a `contributor` was offered a staff-only option and refused after answering three questions. Below the staff floor `to` is never asked and defaults to `staff`, so nothing changes for the rung that has one possible answer. Above it, anything other than `staff` is a notice, which the service floors on its own; the check here only says so in the command's own words rather than as a bare 403. """parts=[part.strip()forpartinto.split(',')ifpart.strip()]or[_STAFF_LABEL]if_STAFF_LABELinparts:iflen(parts)>1:raiseAbort(f'`to={_STAFF_LABEL}` is the whole audience — it cannot be combined ''with named recipients',rc=ExitCodes.EXIT_USAGE)return_to_staff(subject,body)ifnot_is_staff():console.print(f'[error]error:[/error] only {STAFF_FLOOR}s may write to users 'f'(you are {config.subject.profile}) — `to={_STAFF_LABEL}` is yours')return77targets:str|list[str]=(_EVERYONEif_EVERYONEinpartsor_EVERYONE_LABELinpartselseparts)return_notice(targets,subject,body)def_to_staff(subject:str,body:str)->int:"""Ask staff something — the inbound half of `send`."""result=_call('send',body={'subject':subject,'body':body})ifresultisNone:return_unreachable()status,data=resultifstatus!=201ornotisinstance(data,dict):console.print(f'[error]error:[/error] {status}{data}')return1console.print(f'[success]sent[/success] [muted]({data.get("id")})[/muted] — ''staff will see it in their messages')osc.messages_changed()return0def_notice(targets:str|list[str],subject:str,body:str)->int:"""Send to named recipients, or to everyone — the outbound half of `send` (staff)."""result=_call('notice',body={'to':targets,'subject':subject,'body':body})ifresultisNone:return_unreachable()status,data=resultifstatus!=201ornotisinstance(data,dict):console.print(f'[error]error:[/error] {status}{data}')return1count=data.get('recipients',0)console.print(f'[success]sent[/success] to [accent]{count}[/accent] 'f'recipient(s) [muted]({data.get("id")})[/muted]')osc.messages_changed()return0def_save(thread_id:str,data:dict[str,Any])->int:"""Write the master key delivered in *thread_id* to this machine's enc-key file. The receiving half of key distribution, and what `msg act` does with a key message. `user-authorize` (or `key-rekey`) wraps the master key to your public key and sends it; this takes it. Nothing else can write that file, and nothing writes it without proving the payload first: - the message's **subject** must be the key-issue one, so no other message can be mined for something that looks like key material. That is how `act` chose this branch, and it is checked again here rather than trusted — the dispatch is a routing decision, the check is the gate; - the payload must **unwrap with your private key** and its `verify` must decrypt to the known text. Only then does it replace what you have. The message is **dismissed** once the key is written: its whole content is now in your enc-key file, and a spool that keeps every taken key is storing key material for nobody. That order matters. The file it overwrites may be the only thing standing between this machine and the whole private tree, so a bad or mistargeted payload has to fail *before* the write, not be discovered after it. """fromsolver.crypto.keysimportsave_issued_key# An issued key is always its own message: with no replies there is nowhere else for it# to be, and one subject to check.ifnotstr(data.get('subject','')).startswith(KEY_ISSUE_SUBJECT):console.print(f'[error]error:[/error] message [accent]{thread_id}[/accent] does not carry ''a master key — taking one writes key material and nothing else')return1ifnotsave_issued_key(str(data.get('body',''))):return1# Worked, so gone. A message whose whole content is now in your enc-key file has nothing# left to say, and leaving it would mean a mailbox that fills with keys you have already# taken — each one a copy of key material sitting in the spool for no reason. Failure to# dismiss is not failure to save: the key is written either way.if_call('dismiss',thread_id=thread_id)isNone:console.print(f'[muted]Saved, but could not dismiss [accent]{thread_id}[/accent] — ''`msg dismiss` it yourself.[/muted]')osc.messages_changed()return0def_reconstruct(thread_id:str,data:dict[str,Any])->int:"""Complete the half key delivered in *thread_id* with the repository's half, and store it. The receiving half of `key-split`, and the sibling of :func:`_save`: same destination, one step longer. What arrives here is not a key but a share sealed to this holder's public key, worth nothing until it is unwrapped *and* put together with the one committed in the clone — so the work is `key-reconstruct`'s, and this only decides that the message is one to hand it. The same two rules as a key: the **subject** must be the share one (checked again here rather than trusted from the dispatch), and the body must carry exactly one share. Every proof that the result is really the master key is `key-reconstruct`'s own, before it writes anything. Dismissed once it worked, for the reason a taken key is: the share's whole content is now in the enc-key file, and a spool that keeps used key material keeps it for nobody. """fromsolver.crypto.keysimportkey_reconstruct,share_in_messageifnotstr(data.get('subject','')).startswith(KEY_SHARE_SUBJECT):console.print(f'[error]error:[/error] message [accent]{thread_id}[/accent] does not carry ''half a master key — reconstructing writes key material and nothing else')return1share=share_in_message(str(data.get('body','')))ifshareisNoneorkey_reconstruct(share)!=0:return1if_call('dismiss',thread_id=thread_id)isNone:console.print(f'[muted]Reconstructed, but could not dismiss [accent]{thread_id}[/accent] — ''`msg dismiss` it yourself.[/muted]')osc.messages_changed()return0def_dismiss(thread_id:str)->int:"""Drop a worked message from the spool. Not floored in the command: the service decides, and it decides better — staff may drop anything, anyone else a message they are a party to. A `reader` clearing a notice they have finished with is the case the floor used to get wrong. """result=_call('dismiss',thread_id=thread_id)ifresultisNone:return_unreachable()status,data=resultifstatus!=200:console.print(f'[error]error:[/error] {status}{data}')return1console.print(f'[success]dismissed[/success] [muted]({thread_id})[/muted]')osc.messages_changed()return0@register(requires='reader',aliases=('messages',),pass_ctx=True)defmsg(ctx:Context,action:Annotated[Literal['list','read','send','dismiss','act'],Ask('What would you like to do?',labels=_ACTIONS)]='list',thread:Annotated[str,Ask('Which message?',choices=_thread_choices,when=_needs_thread,empty='no messages')]='',subject:Annotated[str,Ask('Subject',when=_is_send)]='',body:Annotated[str,Ask('Message',when=_is_send,multiline=True)]='',to:Annotated[str,Ask('Who should receive it?',choices=_recipient_choices,when=_needs_recipients,strict=False)]='')->int:"""Read and send messages: what is waiting for you, and what it asks you to do. Every message has staff (`maintainer`+) at one end: you can ask them something, they can answer, and they can send notices. There is deliberately no user-to-user messaging. Delivery is asynchronous — the spool holds the message until you read it. Five verbs, and each is one thing a person does with a message. `act` is the one worth knowing: a message *knows* what it is for, so acting on one takes the key it carries, grants the key it asks for, merges the pull request it announces, or walks you through registering the answers `summary` found unregistered — and on anything else it simply reads it. The header's message chip labels its rows from the same rule. Typed bare, it walks you through the rest: pick a verb, then whatever that verb needs — a message from your own list, or a subject and a body. Every answer can be given on the command line instead, and a non-interactive shell asks nothing. Args: ctx: [injected] The live shell context; the decorator supplies it. action: [asked] What to do — `list` everything waiting for you, newest first; `read` one message and mark it read; `send` a message; `dismiss` one you are done with; `act` on one, doing what it asks. Defaults to `list`. thread: [asked] The message to act on, for `read` / `dismiss` / `act`. Offered as a menu of your own messages, so the id never has to be typed out. subject: [asked] The subject line, for `send`. body: [asked] The message text, for `send`. to: [asked] Who a `send` goes to: `staff` (the default, and the only audience below the `maintainer` floor), `everyone`, or one or more identities, comma-separated. Staff are offered the known accounts as a menu where the roster can be read. """ifaction=='list':return_list()ifactionin_ON_THREAD:ifnotthread:# Reached only non-interactively: an interactive shell was offered the menu.raiseAbort(f'msg {action} needs a message id (see `msg list`)',rc=ExitCodes.EXIT_USAGE)ifaction=='read':return_read(thread)ifaction=='dismiss':return_dismiss(thread)return_act(ctx,thread)ifnotsubjectornotbody:raiseAbort('msg send needs subject="…" and body="…"',rc=ExitCodes.EXIT_USAGE)return_send(subject,body,to)