Specify M4b (approvals over Mattermost); propose P16 on approval replies
The owner's decisions of 2026-09-24: commands only, no reactions; an approvers list that falls back to allow.users; approvals in the asking thread; on a gatewayd restart, old posts are marked dead and brokerd's courier posts pending approvals again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -6,6 +6,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
|
||||
|
||||
| Date | Decision | Reason |
|
||||
|---|---|---|
|
||||
| 2026-09-24 | M4b, changing the agreed outline (row of 2026-09-23): no reactions, an approval is answered only by `!approve <id>` or `!deny <id> [reason]` in its thread; an `[approvals] users` list in `gatewayd.toml`, which falls back to `allow.users` when empty; approvals are posted in the asking thread, channel threads included; on restart, `gatewayd` marks its old approval posts dead and `brokerd`'s courier posts pending approvals again. Spec `docs/specs/2026-09-24-m4b-approvals.md`. Brief change P16 proposed. | Owner: reactions are not used. Approvers are separate because an agent allowed to talk to Boxmaker should not be able to approve for it, but the default keeps one list for one person. Redacting secrets from arguments comes with the tools work. A restart must not leave posts that look answerable and are not. |
|
||||
| 2026-09-23 | M4a spec approved with two changes: a secret read from a file prints a warning at startup (and still starts); and in allowed channels and group messages shared with other agents, Boxmaker answers a post only if it names this bot, or it is a reply in one of this Boxmaker's threads that names no other user (`@channel`, `@here`, `@all` name nobody). One Boxmaker per machine, each with its own bot account; nothing is shared between them. | Owner's notes. The owner runs several agents in channels, possibly one Boxmaker per machine. A post's `from_bot` property is user-settable in v11.11.0, so bots are told apart by the allowlist, not by that property. |
|
||||
| 2026-09-23 | M4 is split: M4a is conversations over Mattermost (`gatewayd`, the secret store), M4b is approvals over Mattermost. Spec `docs/specs/2026-09-23-m4a-gateway.md`. | As with M2 and M3: the authority-bearing half is reviewed separately, and the open question about long approvals lands in M4b. |
|
||||
| 2026-09-23 | `gatewayd` reaches Mattermost over TLS, by default at the tailnet name `https://straylight.scylla-hammerhead.ts.net`; plain HTTP stays possible by URL. TLS is `rustls` 0.23 with `ring` and `rustls-native-certs` (the host's trust store, plus an optional CA file); `deny.toml` gains ISC and BSD-3-Clause. | Owner: portability, and a bigger picture that favours the tailnet name over host loopback. The host's store keeps a privately installed CA trusted; `webpki-roots` would not, and `aws-lc-rs` is a larger C build for nothing needed. |
|
||||
@@ -94,4 +95,6 @@ Later changes to the brief:
|
||||
|
||||
## Proposed changes to the design brief
|
||||
|
||||
None pending.
|
||||
| # | Change | Reason |
|
||||
|---|---|---|
|
||||
| P16 | Network isolation 3: "v0 approvals use replies or emoji reactions" becomes "approvals are commands (`!approve`, `!deny`) replied in the approval's thread". The rest of the item stands: they arrive over the Mattermost WebSocket, so no callback URL is needed. | Owner's decision of 2026-09-24: reactions are not used. |
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ One session per milestone. The design brief is `docs/design.md`.
|
||||
| M3a | `brokerd` decisions | Grants, deny by default, taint, hash-chained audit, `ask` approvals through `bxctl`, two sockets, `loopd`'s broker port; tools do not run yet | Tests for every deny path; audit tampering suite; a denial reaches Ornith on straylight as a tool result |
|
||||
| M3b | `brokerd` runner | Rootless container runner, four tools (read file, write file, sandboxed shell, HTTP fetch with host allowlist), egress proxy | A container with no network grant cannot reach the network; fetch refused for hosts outside the grant |
|
||||
| M4a | `gatewayd` + Mattermost conversations | Bot connection over TLS, user-ID allowlist, threads as sessions, bursts as one turn, typing while working, catching up after downtime, the secret store, no listening port | A direct message answered on the real server; a non-allowlisted user gets no response |
|
||||
| M4b | Approvals over Mattermost | `gateway.sock`, approval posts, answers by reaction or `!approve`/`!deny`, long approvals in parts | An `ask` call approved and refused from Mattermost; the audit log names the post |
|
||||
| M4b | Approvals over Mattermost | `gateway.sock`, approval posts, answers by `!approve`/`!deny` in the thread, approvers list, long approvals in parts | An `ask` call approved and refused from Mattermost; the audit log names the post |
|
||||
| M5 | Scheduler + memory | Heartbeat and cron on their own slot in isolated sessions; core block, `recall` and `remember` with provenance, FTS5 index, flush-then-compact at idle | Scheduled run leaves the main slot's cache intact; a fact from epoch 1 is recalled in epoch 2; a note written after reading untrusted content is marked on recall |
|
||||
| M5b | Embedding index (when justified) | Local embedding server, hybrid retrieval behind the `Retriever` trait, rebuild on model change | A paraphrased query finds a note that lexical search misses; no memory text leaves the host |
|
||||
| M6 | Subagents + cloud consult | Subagents on their slot returning summaries; consult tool gated by data-class grants with full payload log | A tainted session is refused; payload log matches what was sent |
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
# M4b design: approvals over Mattermost
|
||||
|
||||
Status: draft for the owner's review, 2026-09-24. It builds on M4a
|
||||
(`docs/specs/2026-09-23-m4a-gateway.md`, section 12 there is the agreed outline) and M3a's approval
|
||||
path (`docs/specs/2026-09-18-m3a-decision-path.md`, section 6). Owner's decisions of 2026-09-24,
|
||||
changing the outline: **no reactions**, answers are commands only; an `[approvals] users` list
|
||||
that falls back to `allow.users` when empty; approvals are posted in the asking thread, channel
|
||||
threads included (redacting secrets from tool arguments is later work, with the tools); a
|
||||
`gatewayd` restart marks its old approval posts dead and `brokerd` posts them again.
|
||||
|
||||
Facts this rests on, checked on 2026-09-24 against Mattermost's source and API reference at the
|
||||
server's version, v11.11.0:
|
||||
|
||||
- `PUT /api/v4/posts/{post_id}/patch` with `{"message": …}` replaces a post's text; the other
|
||||
fields are left alone. It needs the `edit_post` permission, which a user and a bot have for
|
||||
their own posts by default (to be confirmed on the owner's server at the device check).
|
||||
- `GET /api/v4/users/{user_id}` returns the user, with `username`.
|
||||
- M4a's facts (posts, `posted` events, threads, the 16,383-character limit) still hold.
|
||||
|
||||
## 1. What M4b proves
|
||||
|
||||
| Claim | Checked by |
|
||||
|---|---|
|
||||
| An `ask` call is posted in the asking thread, or for a session not from Mattermost in the first approver's direct messages, with its tool, grant, taint, expiry and full arguments | Tests against a fake Mattermost; on the owner's server |
|
||||
| `!approve <id>` from an approver, in that thread, after the post, while pending, runs the call exactly as `bxctl approve` would, and the audit record names the approver and the post | Tests; on the owner's server; `bxctl audit verify` |
|
||||
| `!deny <id> [reason]` refuses it the same way, with the reason recorded | Tests; on the owner's server |
|
||||
| Nobody else can answer: not a user outside the approvers, not a command elsewhere or before the post, not an id that is not pending there | Tests |
|
||||
| An approval answered in `bxctl`, expired, or abandoned by its requester is marked "no longer pending" in Mattermost | Tests; on the owner's server |
|
||||
| Arguments are never shortened: long ones come in up to 4 parts with a hash; longer ones are answerable only in `bxctl` | Tests |
|
||||
| A `gatewayd` restart leaves no post that looks answerable and is not; pending approvals are posted again | Tests; on the owner's server |
|
||||
| `gatewayd` still opens no TCP port, and never reaches `admin.sock` | On the owner's machine (`ss`); by construction |
|
||||
|
||||
Out of scope: reactions (dropped by the owner); redacting secrets from arguments (with the tools,
|
||||
later); approving grants or changing them from Mattermost (never: grants are the owner's files).
|
||||
|
||||
## 2. Components
|
||||
|
||||
```
|
||||
brokerd ──(one connection per pending approval)──> gateway.sock ── gatewayd ── Mattermost
|
||||
▲ │
|
||||
└── admin.sock ── bxctl (unchanged, plus `deny`) └── state.json (+ approvals)
|
||||
```
|
||||
|
||||
`gatewayd` listens on **`gateway.sock`**, a Unix socket in its own directory
|
||||
(`<home>/run/gateway/`, 0700; the socket 0600), made the way `brokerd` makes its sockets. It is
|
||||
still the only listening thing `gatewayd` has, and it is not TCP. `brokerd` is the client. Nothing
|
||||
else is meant to connect; a process that could would only be able to make the bot post an
|
||||
approval whose answer goes back to that process, never to `brokerd` (section 9).
|
||||
|
||||
`gatewayd` can answer only the approvals `brokerd` hands it, one per connection. It never learns
|
||||
of grants, never reaches `admin.sock`, and cannot create, list or widen anything.
|
||||
|
||||
### New and changed modules
|
||||
|
||||
| Crate | Module | Change |
|
||||
|---|---|---|
|
||||
| `proto` | `escape` | Moved from `bxctl`: `escape_json_text`, `escape_model_text`, unchanged. `bxctl` uses it from here. |
|
||||
| `proto` | `wire` | The four gateway messages (section 4). |
|
||||
| `brokerd` | `config` | `[gateway] socket` (section 3). |
|
||||
| `brokerd` | `courier` | New: one thread per pending approval, which carries it to `gatewayd` and its answer back (section 5). |
|
||||
| `brokerd` | `ledger`, `admin` | `Answer::Approved` and `Refused` gain `post`; the `Approval` record's `post` is filled from it. |
|
||||
| `brokerd` | `approvals` | `Table::contains(id)`. |
|
||||
| `gatewayd` | `config` | `[approvals] users` and `socket`. |
|
||||
| `gatewayd` | `approvals` | New: the pending approvals it has posted, the commands that answer them, the text of the posts. Pure. |
|
||||
| `gatewayd` | `gateway` | New: the listener and one thread per `brokerd` connection. |
|
||||
| `gatewayd` | `mm::rest` | `patch_post`, `username`. |
|
||||
| `gatewayd` | `sessions` | `!approve` and `!deny` go to `approvals` instead of the M4a notice. |
|
||||
| `gatewayd` | `state` | `approvals`: the posts of each open approval, for the restart. |
|
||||
| `gatewayd` | `deliver`, `serve` | Typing paused while a turn waits for approval; the M4a "waiting for approval" line only when `brokerd` did not send it (section 7). |
|
||||
| `bxctl` | `cli` | `deny` as another name for `refuse`. |
|
||||
|
||||
No new dependency. SHA-256 is `proto`'s (`emsha`).
|
||||
|
||||
## 3. Configuration
|
||||
|
||||
`brokerd.toml`:
|
||||
|
||||
```toml
|
||||
[gateway] # absent: Mattermost approvals are off, as in M3
|
||||
socket = "" # empty: <home>/run/gateway/gateway.sock
|
||||
```
|
||||
|
||||
`gatewayd.toml`:
|
||||
|
||||
```toml
|
||||
[approvals]
|
||||
users = [] # Mattermost user ids who may answer; empty: everyone in [allow] users
|
||||
socket = "" # empty: <home>/run/gateway/gateway.sock
|
||||
```
|
||||
|
||||
Every id in `approvals.users` must also be in `allow.users` (an approver who is not heard could
|
||||
never answer); anything else is a configuration error. The **first approver** is
|
||||
`approvals.users[0]`, or `allow.users[0]` when the list is empty.
|
||||
|
||||
## 4. The gateway messages
|
||||
|
||||
On one `gateway.sock` connection, every frame is a `proto` `Envelope` (`read_frame`,
|
||||
`write_frame`, as on the other sockets):
|
||||
|
||||
| # | From | Message (`kind`) | `final` | Body |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `brokerd` | `approval_request` | true | `{ approval: PendingApproval }` |
|
||||
| 2 | `gatewayd` | `approval_posted` | false | `{ posts: [String] }`, the post ids, first part first |
|
||||
| 3 | `gatewayd` | `approval_answer` | true | `{ approval: u64, approved: bool, by: String, post: String, reason: Option<String> }` |
|
||||
| 4 | `brokerd` | `approval_outcome` | true | `{ outcome: ApprovalOutcome }` |
|
||||
|
||||
```rust
|
||||
pub enum ApprovalOutcome { // tag "outcome", snake_case
|
||||
Runs, // approved, and the grants as they are now still allow it
|
||||
Denied { reason: DenyReason }, // approved, but denied on the re-check; or the record failed
|
||||
Refused, // the refusal is recorded
|
||||
Gone, // no longer pending when the answer came
|
||||
}
|
||||
```
|
||||
|
||||
Frames 1 and 4 carry `id` 1 and 2; frames 2 and 3 answer id 1. Instead of 2, `gatewayd` may send
|
||||
`error { code: internal, detail }` (final) when it cannot post; `brokerd` then tries again as in
|
||||
section 5. `by` and `post` are Mattermost ids (26 characters of `a-z0-9`), `reason` at most 500
|
||||
characters; `brokerd` refuses anything else as a bad frame and does not answer the approval.
|
||||
|
||||
Either side may close at any time. When `brokerd` closes before frame 3, the approval ended some
|
||||
other way. When `gatewayd` closes before frame 3, it went away.
|
||||
|
||||
## 5. `brokerd`: the courier
|
||||
|
||||
When an `ask` call enters the pending table and `[gateway]` is set, `brokerd` starts a **courier**
|
||||
thread for it (with `std::thread::Builder`; if that fails, it logs one line and the approval stays
|
||||
answerable in `bxctl`). The courier:
|
||||
|
||||
1. Stops as soon as the table no longer holds the approval (`Table::contains`).
|
||||
2. Connects to the socket and sends frame 1. If it cannot connect, or the answer is an `error`,
|
||||
it logs once for this approval, `brokerd: approval <id>: gatewayd is not reachable at <path>:
|
||||
<why>; bxctl can still answer it` and `see docs/runbook.md#gateway-unreachable`, waits 5 s
|
||||
(checking the table every second), and tries again from step 1. When a later attempt posts it,
|
||||
it logs `brokerd: approval <id> posted in Mattermost`.
|
||||
3. Waits for frame 3, with a read timeout of 1 s, and at every timeout checks the table: if the
|
||||
approval has gone (answered in `bxctl`, expired, requester gone), it closes the connection and
|
||||
stops. If `gatewayd` closes first, or sends anything but frame 2 then frame 3, the courier goes
|
||||
back to step 1: `gatewayd` restarted, or broke, and a fresh post is needed.
|
||||
4. On frame 3 for its own id: takes the entry out of the table. If it gets it, it answers it with
|
||||
`admin::answer`, exactly as `bxctl approve` and `refuse` do: `Answer::Approved { by, post }` or
|
||||
`Answer::Refused { by, post, reason }`, the `by` being the Mattermost user id. The `Approval`
|
||||
record then carries `by`, `post` and `reason`. It sends frame 4 with `Runs` or `Denied` (from
|
||||
the re-decision), or `Refused`. If the entry was already gone, frame 4 is `Gone` and nothing is
|
||||
written. Then it closes and stops.
|
||||
|
||||
"Whoever takes the entry out of the table answers it" (M3a) holds unchanged: the courier is one
|
||||
more such taker, beside the admin and expiry threads.
|
||||
|
||||
## 6. `gatewayd`: posting an approval
|
||||
|
||||
One thread per accepted `gateway.sock` connection. It reads frame 1 (bounded by `read_frame`),
|
||||
checks the ids in it, and decides **where**:
|
||||
|
||||
- If the session is `mm-<root>` and `state.in_flight` holds a turn of that session, the approval
|
||||
goes into that turn's thread (its channel and root). This covers direct messages and channels.
|
||||
- Otherwise (`bxctl chat`, a session no longer in flight, later scheduled jobs), it goes to the
|
||||
first approver's direct channel (`POST /channels/direct`), as a new top-level post whose own
|
||||
thread is where it is answered.
|
||||
|
||||
### The text
|
||||
|
||||
The arguments are `PendingApproval.arguments` (the re-serialised JSON `brokerd` shows `bxctl`),
|
||||
passed through `proto::escape::escape_json_text`, then fenced with a run of backticks one longer
|
||||
than the longest run inside them (at least three), so nothing in them renders as Markdown or
|
||||
mentions anyone. The grant name gets the same escaping, in backticks. Times are UTC.
|
||||
|
||||
One part, for arguments up to 15,000 characters after escaping:
|
||||
|
||||
````text
|
||||
**Approval 42** · tool `shell` · grant `ops-shell` · taint private · expires 2026-09-24 14:47 UTC
|
||||
```
|
||||
{"command":"ls -la /srv/backups","timeout_ms":10000}
|
||||
```
|
||||
Reply in this thread with `!approve 42` or `!deny 42 <reason>`.
|
||||
````
|
||||
|
||||
Longer arguments are split, at character boundaries, into parts of at most 15,000 characters,
|
||||
each posted in order in the same thread. Parts before the last:
|
||||
|
||||
````text
|
||||
**Approval 42**, part 1 of 3 of the arguments
|
||||
```
|
||||
{"command":"…
|
||||
```
|
||||
````
|
||||
|
||||
The last part adds, after its fence: `Approval 42: 3 parts, 41,207 characters, sha256 9f2c…e1a0
|
||||
(of the arguments as shown).` and the reply line. Only an answer after the **last** part counts
|
||||
(section 7), so nobody approves what they have not been shown in full.
|
||||
|
||||
More than 4 parts: one post, `**Approval 42** · tool … · the arguments are 91,234 characters
|
||||
(sha256 …), too long to show here; answer it with bxctl approvals`, and no command answers it.
|
||||
|
||||
### The outcome
|
||||
|
||||
When the approval ends, `gatewayd` edits the **last** part (the only answerable one) to its text
|
||||
plus one line:
|
||||
|
||||
| How it ended | Added line |
|
||||
|---|---|
|
||||
| `Runs` | `✅ Approved by @<username>: the call runs.` |
|
||||
| `Denied` | `Approved by @<username>, but denied on the re-check: <deny reason>.` |
|
||||
| `Refused` | `❌ Refused by @<username>: <reason, or "no reason given">.` |
|
||||
| `Gone`, or `brokerd` closed first | `No longer pending (answered elsewhere, expired, or its turn ended).` |
|
||||
| `gatewayd` restarted | `No longer answerable here: gatewayd restarted. If it is still pending it is posted again.` |
|
||||
|
||||
`<username>` comes from `GET /users/{id}`, falling back to the id. An edit that fails is logged and
|
||||
not retried.
|
||||
|
||||
## 7. `gatewayd`: answers, typing, and the old notice
|
||||
|
||||
**Answers.** M4a's routing is unchanged up to commands: only allowlisted users are heard, and in
|
||||
channels only in this Boxmaker's threads. A command `!approve <id>` or `!deny <id> [reason]`
|
||||
(exactly: one space, the id in decimal, then for `!deny` optionally one space and a reason of at
|
||||
most 500 characters) now goes to `approvals`, which accepts it only if **all** hold:
|
||||
|
||||
1. its author is an approver;
|
||||
2. approval `<id>` is pending in this `gatewayd`;
|
||||
3. the command is in that approval's thread (its root is the thread's root);
|
||||
4. it was posted after the approval's last part (`create_at` later).
|
||||
|
||||
Then the answer goes to the courier's connection as frame 3, and the thread says nothing more
|
||||
until the edit of section 6. Otherwise `gatewayd` replies in the thread with the first reason that
|
||||
applies: `only an approver can answer approvals`, `approval <id> is not pending here`, `approval
|
||||
<id> is answered in its own thread`, `approval <id> was answered before it was shown in full`, or
|
||||
`use !approve <id> or !deny <id> <reason>`. A command never reaches `loopd`. `!!` escapes as in
|
||||
M4a.
|
||||
|
||||
**Typing.** While a session has a pending approval in `approvals`, `user_typing` is not sent for
|
||||
its thread: the turn is waiting for the owner, not working.
|
||||
|
||||
**The M4a notice.** On an `approval_pending` event, `deliver` waits up to 10 s for `approvals` to
|
||||
hold that id. If it does, it posts nothing (the approval post is there). If not (no `[gateway]` in
|
||||
`brokerd`, or it could not connect), it posts M4a's line, reworded: `waiting for approval <id>:
|
||||
brokerd has not sent it here; answer it with bxctl approvals`.
|
||||
|
||||
## 8. State and restarts
|
||||
|
||||
`state.json` gains `approvals: [{ approval, channel, posts: [ids] }]`, written when an approval is
|
||||
posted and removed when it ends. The field is optional, so an M4a state file loads unchanged; like
|
||||
every other field, its ids must be valid.
|
||||
|
||||
At the first connection after a start, every entry left in `approvals` has its last part edited
|
||||
with the restart line of section 6, and is removed. The couriers of approvals still pending notice
|
||||
the dropped connection and post them again (section 5), so the owner sees one live post per
|
||||
pending approval.
|
||||
|
||||
`brokerd` restarts need nothing new: its pending table is in memory, its couriers die with it, and
|
||||
their connections close, so `gatewayd` marks the posts "no longer pending".
|
||||
|
||||
## 9. Security notes
|
||||
|
||||
- The approvers are the only people whose commands answer anything; an agent in `allow.users` can
|
||||
talk to Boxmaker but, if the owner lists the approvers, not approve for it.
|
||||
- Arguments are posted in full, in the thread, which in a channel is visible to its members,
|
||||
other agents included. Redacting secrets is deferred to the tools work (owner's decision).
|
||||
- Several Boxmakers in one channel have their own `brokerd`s, whose approval ids can coincide. A
|
||||
command counts only in the thread where this `gatewayd` posted that id; two Boxmakers with the
|
||||
same id pending in the same thread would both take it. This needs both named in one thread and
|
||||
both asking at once; it is accepted, and noted in the runbook.
|
||||
- A local process of the owner's user can connect to `gateway.sock` and have the bot post a fake
|
||||
approval; its answer goes back to that process only. The socket is 0600 in a 0700 directory;
|
||||
M7 keeps it out of `loopd`'s and the tools' reach.
|
||||
|
||||
## 10. Runbook
|
||||
|
||||
New: `gateway-unreachable` (`brokerd` cannot post an approval: `gatewayd` down, `[gateway]
|
||||
socket` wrong, or Mattermost unreachable; `bxctl approvals` still works). Changed:
|
||||
`gatewayd-start-failed` gains `gateway.sock` (directory, bind, permissions) and the approvers
|
||||
check. Every message for these states ends with its pointer.
|
||||
|
||||
## 11. Testing
|
||||
|
||||
Offline, as tasks with given tests:
|
||||
|
||||
- **`proto::escape`**: the moved tests pass unchanged, and `bxctl`'s output is byte-identical.
|
||||
- **Wire**: each gateway message round-trips; unknown fields and bad ids are refused.
|
||||
- **`brokerd` courier**, against a fake `gateway.sock`: posted and approved (the call runs, the
|
||||
record has `by` and `post`); refused with a reason; approved after the grant was removed
|
||||
(`Denied`); answered in `bxctl` meanwhile (the courier closes, frame 4 never sent); expired;
|
||||
the fake closes before answering (the courier posts again); unreachable at first, reachable 5 s
|
||||
later; a bad frame 3 (another id, bad `by`) answers nothing.
|
||||
- **`gatewayd` approvals**, pure: every rule of section 7 in both directions; the text of one part,
|
||||
of 3 parts with the hash, and of the refusal to show; fences around backtick runs; escaping of
|
||||
controls, bidi overrides and zero-width characters; every outcome line.
|
||||
- **`gatewayd` end to end**, against the fake Mattermost and a fake `brokerd`: the approval in a
|
||||
direct-message thread, in a channel thread, in the first approver's DMs; `!approve` from an
|
||||
approver runs it; from a non-approver, in another thread, before the last part, for an unknown
|
||||
id, each answered with its reason and nothing sent; `brokerd` closing marks the post; a restart
|
||||
marks old posts and the re-sent approval is posted fresh; typing stops while pending; the
|
||||
fallback notice when no approval arrives.
|
||||
- **`bxctl deny`**: the same output as `refuse`.
|
||||
|
||||
On the owner's server, by the design model with the owner answering: an approval in a direct
|
||||
message thread approved with `!approve` (the call runs; `bxctl audit verify` shows `by` and
|
||||
`post`), one in a channel thread denied with a reason, one answered in `bxctl` and marked in
|
||||
Mattermost, a long one in parts, one answered by a non-approver (refused with the reason), and a
|
||||
`gatewayd` restart while one is pending.
|
||||
Reference in New Issue
Block a user