The review of tasks 01 to 15 goes in the implementer log, with the device check against the owner's Mattermost. Three follow-ups: - 16: the refusal without a runner says why. - 17: the texts that say who asked. - 18: approvals from non-approvers in direct or group messages go to an approver. Task 14's given serve_approvals.rs becomes serve_approvals.rs-task14, since task 18 hands over a new version. Tip T31 is added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
116 lines
4.6 KiB
Markdown
116 lines
4.6 KiB
Markdown
# M4b task 17: the texts of a redirected approval
|
|
|
|
**Branch:** `m4b` (run `git switch m4b`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `gatewayd: the texts that say who asked for a redirected approval`
|
|
|
|
## Goal
|
|
|
|
Texts only; task 18 posts them. When someone who may talk to the bot but not approve asks for
|
|
something in a direct or group message, no approver can answer the approval there, so it goes to
|
|
the first approver's direct messages (M4b spec, section 6, as changed after the review of
|
|
2026-09-24). The approver must see who asked, where, and what they asked. Three texts:
|
|
|
|
1. `asked_post`: the first post of a redirected approval. The asker's message is escaped with
|
|
`proto::escape::escape_model_text` and fenced (`fence`, already in this file). It is context,
|
|
not what is approved, so only its first `PROMPT_SHOWN` (2,000) characters are shown, then a
|
|
line saying how many more there were. For example:
|
|
|
|
````text
|
|
**Approval 3** was asked by @alex in a direct message with the bot. Their message:
|
|
```
|
|
Read the file /srv/notes/q3.txt and summarise it.
|
|
```
|
|
````
|
|
|
|
2. `unfollowed_post`: the first post of an approval whose session `gatewayd` does not follow
|
|
(`bxctl chat`, or a turn from before a restart).
|
|
3. `sent_to_approver`: the line left in the asking thread.
|
|
|
|
## Files
|
|
|
|
- Copy: `crates/gatewayd/tests/approvals_asked.rs`
|
|
- Modify: `crates/gatewayd/src/approvals/text.rs` (append only), `docs/implementer-log.md`
|
|
|
|
## The change, exactly
|
|
|
|
Append this to the end of `crates/gatewayd/src/approvals/text.rs`, after `with_ending`, exactly
|
|
as it is. Nothing else in the file changes, and there are no new imports: `proto` is already a
|
|
dependency, and `code` and `fence` are in this file.
|
|
|
|
```rust
|
|
|
|
/// The most characters of an asker's message shown to the approver: it is context, not what is
|
|
/// approved, so it may be cut.
|
|
pub const PROMPT_SHOWN: usize = 2_000;
|
|
|
|
/// Who asked for an approval that is shown elsewhere (M4b spec, section 6).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Asked {
|
|
/// `@username`, or the user id when the name could not be found.
|
|
pub by: String,
|
|
/// The channel's type: `D` a direct message, `G` a group message.
|
|
pub channel_type: String,
|
|
/// The text of the turn that asked.
|
|
pub prompt: String,
|
|
}
|
|
|
|
/// The first post of a redirected approval: who asked, where, and their message.
|
|
pub fn asked_post(id: u64, asked: &Asked) -> String {
|
|
let place = if asked.channel_type == "G" {
|
|
"a group message"
|
|
} else {
|
|
"a direct message"
|
|
};
|
|
let escaped = proto::escape::escape_model_text(&asked.prompt);
|
|
let total = escaped.chars().count();
|
|
let shown: String = escaped.chars().take(PROMPT_SHOWN).collect();
|
|
let mut post = format!(
|
|
"**Approval {id}** was asked by {} in {place} with the bot. Their message:\n{}",
|
|
asked.by,
|
|
fence(&shown)
|
|
);
|
|
if total > PROMPT_SHOWN {
|
|
post.push_str(&format!("\n(… {} more characters)", total - PROMPT_SHOWN));
|
|
}
|
|
post
|
|
}
|
|
|
|
/// The first post of an approval whose session `gatewayd` does not follow.
|
|
pub fn unfollowed_post(id: u64, session: &str) -> String {
|
|
format!(
|
|
"**Approval {id}** was asked in session {}, which gatewayd does not follow now (a restart, or a session from outside Mattermost).",
|
|
code(session)
|
|
)
|
|
}
|
|
|
|
/// The line left in the asking thread when its approval goes to an approver.
|
|
pub fn sent_to_approver(id: u64) -> String {
|
|
format!("waiting for approval {id}: it was sent to an approver")
|
|
}
|
|
```
|
|
|
|
Characters are counted with `chars()`, never `len()`: `len()` counts bytes, and a message in any
|
|
language but English would be cut short.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy.** `git switch m4b`, then
|
|
`cp docs/plans/M4b/files/crates/gatewayd/tests/approvals_asked.rs crates/gatewayd/tests/`
|
|
- [ ] **2. See it fail.** `cargo test -p gatewayd --test approvals_asked`. Expected: it does not
|
|
compile (`Asked` and the three functions do not exist yet).
|
|
- [ ] **3. Make the change.** Run `cargo fmt --all`.
|
|
- [ ] **4. See it pass.** `cargo test -p gatewayd --test approvals_asked`: 4 passed. Then
|
|
`cargo test -p gatewayd`: every suite passes.
|
|
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`, with 4 more tests than
|
|
before this task (about 850).
|
|
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
|
|
|
|
## Done when
|
|
|
|
- `cargo test -p gatewayd` passes; `make gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A test in `approvals_asked.rs` still fails with the code above appended exactly. Do not change
|
|
the test or the texts to make it pass.
|