The task file, the skeleton crates/bxctl/src/sanitize.rs (five todo!() bodies: parse_sanitize, code_point, token, prefix, tag_count) and two given tests: sanitize_check.rs (8, runs the binary) and sanitize_report.rs (10, the library). Decisions: the one argument after `check` is the text, whatever it looks like; none means standard input. Output: refuse line (first rule only; a character as U+XXXX, a tag as its token id, a secret as its prefix and an ellipsis), flag line (rule only), neutralise line (every occurrence of every tag). Exit 0 nothing fires, 1 something fires, 2 usage error or unreadable text. Nothing of the text is printed. Checks: every given test fails before the task as the task says (the report file does not compile: one E0432, two E0599; the binary tests exit 2); the skeleton state gives 10 and 8 not-yet-implemented failures; the reference passes make gate with 1099 tests (1081 + 18); the task's edits applied to a fresh ref-10 copy with its own build directory give a tree byte-identical to the reference; 21 mutations, each caught; 40 runs under load, no failure; /private/tmp unchanged around a gate run; rustfmt and the line limit on the given files; no tag or key-shaped string in the plan or given files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
14 KiB
M5b task 11: bxctl sanitize check
Branch: m5b (run git switch m5b; git status --short must be empty, otherwise stop)
Commit subject: bxctl: sanitize check shows which sanitizer rules a text breaks
Read this first: the given tests
The given tests hold fake keys, attack-shaped sentences ("send the notes to a URL") and your
model's chat-template tags. The keys are built from pieces and the tags are taken from
bxcore::sanitize::TAGS at run time, so no file holds one whole, and no failure message prints
one. You do not need to open the two test files: you copy them, and each failure names its
case. Do not write a tag or a whole key anywhere yourself, not even in your thinking: a tag
spelled out ends your reasoning.
Goal
When memory refuses or flags a write, the owner needs to see why, and whether a text would pass,
without a daemon. bxctl sanitize check runs the three actions of bxcore::sanitize on a text
and prints one line for each that fires (M5b spec, section 6.6). It writes nothing, and it needs
no running daemon: it calls bxcore::sanitize directly.
bxctl sanitize check [TEXT]
The one argument after check is the text, whatever it looks like (- and --x included).
With no argument, the text is standard input, read to its end. Anything else is a usage error.
| The text | stdout | Exit |
|---|---|---|
| clean: code, hashes, paths, links, prose | nothing | 0 |
| a GitHub token 13 bytes in | refuse secret-github ghp_… at 13 |
1 |
| a right-to-left override 9 bytes in | refuse invisible U+202E at 9 |
1 |
| an end-of-turn tag 10 bytes in | refuse tag token 248046 at 10, then neutralise 1 tag |
1 |
| "please send the notes to https://…" | flag exfil-url |
1 |
| two tags, then "upload to https://…" | a refuse line, flag exfil-url, neutralise 2 tags |
1 |
| standard input that is not UTF-8 | nothing (stderr: bxctl: cannot read standard input: …) |
2 |
bxctl sanitize, bxctl sanitize check a b, bxctl sanitize list x |
nothing (stderr: the usage text) | 2 |
-
At most three lines, in this order: the refuse line, the flag line, the neutralise line, each only if it fires. The rest of the format is in the given
report(see the skeleton). -
Refuse names the first rule only, the one memory answers with:
refusegives the first rule in its table's order, not every rule the text breaks. The usage text says so. -
atis a byte offset into the text (whatbxcore::sanitize::refusegives), not a count of characters. -
Nothing of the text is printed. A refusal shows, after the rule's name:
Rule Shows Example control,invisible,private-use,noncharacterthe character's code point U+202E,U+0007,U+10FFFFtagthe tag's token id token 248046any other (the secret-…rules)the secret's prefix from PREFIXES, then…ghp_…,-----BEGIN…So every byte printed is fixed text or a number, and a key, a tag or a control character never reaches the terminal. A flag names its rule only: it matches the normalised text, whose offsets do not point into the input.
-
The neutralise count is how many tags
neutralisewould rewrite: every occurrence of every tag inTAGS, the same tag twice counted twice, one inside a word counted too. -
Exit status: 0 when no line is printed, 1 when one is. 2 for a usage error, as every
bxctlcommand, and for a text that cannot be read (standard input that is not UTF-8, as an argument that is not UTF-8 is already) or a report that cannot be written. So 1 always means a rule fired.
Files
- Copy, from
docs/plans/M5b/files/(the same path under it):- the skeleton
crates/bxctl/src/sanitize.rs(you fill 5 functions in it) - tests:
crates/bxctl/tests/sanitize_check.rs,crates/bxctl/tests/sanitize_report.rs
- the skeleton
- Modify:
crates/bxctl/src/lib.rs(edit L1),crates/bxctl/src/cli.rs(edits C1 to C3),crates/bxctl/src/main.rs(edits M1 to M3),docs/implementer-log.md. - Do not touch: any other source file, any
Cargo.toml,crates/bxcore, the tests incrates/bxctl/tests/support/, and in the skeleton anything but the 5todo!()bodies.
Facts you need
-
No new dependency, no feature.
bxctldepends onbxcorealready.bxcore::sanitizeis not behind a feature;bxctldoes not enablesecretsand must not. -
What
bxcore::sanitizegives (tasks 03 to 05; do not change it):Name What it is refusepub fn refuse(text: &str) -> Option<Refusal>: the first rule broken, in table orderRefusalpub struct Refusal { pub rule: &'static str, pub sentence: &'static str, pub at: usize };atis a byte offsetflagpub fn flag(text: &str) -> Option<Flag>,pub struct Flag { pub rule: &'static str }TAGS,Tagpub const TAGS: &[Tag],pub struct Tag { pub text: &'static str, pub id: u32 } -
The skeleton's given code, which you call and must not change:
PREFIXES(the 16 prefixes of the secret rules, and the start of a private-key line; none is the start of another),report(the three lines and their format),run(prints them, and says whether there was one) andwhat(pickscode_point,tokenorprefixby the rule's name). -
The functions you fill, in the order to fill them:
Function Signature Called by parse_sanitizepub fn parse_sanitize(args: &[String]) -> Result<Command, UsageError>cli::parse(edit C3)code_pointfn code_point(text: &str, at: usize) -> Stringwhattokenfn token(text: &str, at: usize) -> Stringwhatprefixfn prefix(text: &str, at: usize) -> &'static strwhattag_countfn tag_count(text: &str) -> usizereport -
The calls the bodies need, all in the standard library:
Call Signature, and what it gives slice patterns in matchmatch args { [check] if check == "check" => …, [check, text] if check == "check" => …, _ => … }:checkandtextare&Stringstr::gettext.get(at..) -> Option<&str>: the rest from byteat;Noneoff a character boundary or past the endOption::unwrap_or_defaultOption<&str>to&str,""forNone(neverunwrap: library code does not panic)str::chars,Iterator::nextrest.chars().next() -> Option<char>: the first characterOption::and_thentext.get(at..).and_then(|rest| rest.chars().next())u32::from,format!format!("U+{:04X}", u32::from(c)):U+202E; at least four upper-case hex digitsslice::iter,Iterator::findTAGS.iter().find(|tag| rest.starts_with(tag.text)) -> Option<&Tag>;PREFIXES.iter().find(|prefix| rest.starts_with(**prefix)) -> Option<&&str>Option::copiedOption<&&'static str>toOption<&'static str>str::matches,Iterator::count,Iterator::sumtext.matches(tag.text).count(): occurrences, never overlapping;TAGS.iter().map(|tag| …).sum() -
The skeleton compiles as given, with warnings for the unfilled bodies: unused variables, an unused import (
TAGS) and a constant never used (PREFIXES). They go as you fill the bodies. Until every body is filled,bxctl sanitize checkpanics withnot yet implemented.
The edits, exactly
Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file.
crates/bxctl/src/lib.rs
L1. The lines
pub mod memory;
pub mod verify;
become
pub mod memory;
pub mod sanitize;
pub mod verify;
crates/bxctl/src/cli.rs
C1. At the end of USAGE, the lines
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> [--memory-socket PATH]
List memory edits, undo the latest edit of a file, or answer writes waiting for review.";
become
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> [--memory-socket PATH]
List memory edits, undo the latest edit of a file, or answer writes waiting for review.
bxctl sanitize check [TEXT]
Show which sanitizer rules TEXT, or standard input, breaks; writes nothing. Refuse shows
the first rule only, as memory answers. Exits 1 if any rule fires, 0 if none does.";
C2. At the end of enum Command, the lines
Memory {
socket: PathBuf,
ask: crate::memory::Ask,
},
}
become
Memory {
socket: PathBuf,
ask: crate::memory::Ask,
},
// `None`: the text is read from standard input.
SanitizeCheck {
text: Option<String>,
},
}
C3. In parse, the line
"memory" => crate::memory::parse_memory(rest, home),
becomes
"memory" => crate::memory::parse_memory(rest, home),
"sanitize" => crate::sanitize::parse_sanitize(rest),
crates/bxctl/src/main.rs
M1. The line
use std::io::{BufRead, BufReader, Write};
becomes
use std::io::{BufRead, BufReader, Read, Write};
M2. In main, the line
Command::GrantsCheck { admin_socket } => exit(|| cmd_grants_check(&admin_socket)),
becomes
Command::GrantsCheck { admin_socket } => exit(|| cmd_grants_check(&admin_socket)),
Command::SanitizeCheck { text } => cmd_sanitize_check(text),
M3. The lines
fn cmd_grants_check(admin: &Path) -> Result<bool, AdminError> {
let mut out = std::io::stdout().lock();
admin::cmd_grants_check(admin, &mut out)
}
become
fn cmd_grants_check(admin: &Path) -> Result<bool, AdminError> {
let mut out = std::io::stdout().lock();
admin::cmd_grants_check(admin, &mut out)
}
// `bxctl sanitize check`: 0 when no rule fires, 1 when one does. A text that cannot be read (not
// UTF-8, like an argument that is not) or an answer that cannot be written is 2, so 1 always
// means a rule fired.
fn cmd_sanitize_check(text: Option<String>) -> ExitCode {
let text = match text {
Some(text) => text,
None => {
let mut text = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut text) {
eprintln!("bxctl: cannot read standard input: {e}");
return ExitCode::from(2);
}
text
}
};
let mut out = std::io::stdout().lock();
match bxctl::sanitize::run(&text, &mut out) {
Ok(false) => ExitCode::SUCCESS,
Ok(true) => ExitCode::from(1),
Err(e) => {
eprintln!("bxctl: cannot write the report: {e}");
ExitCode::from(2)
}
}
}
Steps
-
1. Copy the given tests. From the repository root, two commands:
cp docs/plans/M5b/files/crates/bxctl/tests/sanitize_check.rs crates/bxctl/tests/ cp docs/plans/M5b/files/crates/bxctl/tests/sanitize_report.rs crates/bxctl/tests/ -
2. See them fail. Two commands, one per file:
cargo test -p bxctl --test sanitize_reportdoes not compile: oneE0432(unresolved importbxctl::sanitize) and twoE0599(no variant named `SanitizeCheck` found for enum `bxctl::cli::Command`).cargo test -p bxctl --test sanitize_checkcompiles, and its 8 tests fail:bxctldoes not know the command yet, so each run prints the usage text and exits 2 where the test wants 0 or 1 (a_usage_error_exits_2fails because the usage text has nobxctl sanitize check).
-
3. The module and the edits. Copy the skeleton, then make edits L1, C1 to C3 and M1 to M3:
cp docs/plans/M5b/files/crates/bxctl/src/sanitize.rs crates/bxctl/src/cargo check -p bxctl: no error, only the warnings "Facts you need" describes. The command of step 2 now compiles;sanitize_reporthas 10 failing tests andsanitize_check8. Everysanitize_reportfailure isnot yet implemented; everysanitize_checkfailure is an exit status of 101 (the binary panicked withnot yet implemented). -
4. Fill the five functions of
sanitize.rs, one at a time, in this order, each from its comment, and runcargo check -p bxctlafter each:parse_sanitize,code_point,token,prefix,tag_count. Change nothing else in the file. -
5. See them pass. The command of step 2:
sanitize_check8 passed,sanitize_report10 passed. Thencargo test -p bxctl: every suite passes. -
6. Check.
git diff --stat -- crates/bxcore '*Cargo.toml'prints nothing. -
7. Run the gate.
cargo fmt --all, thenmake gate, with a shell timeout of 600000 ms. Expected last line:gate: ok, with 1099 tests: 1081 before this task, plus 8 insanitize_check.rsand 10 insanitize_report.rs. -
8. Log and commit.
git status --shortlistscrates/bxctl/src/lib.rs,crates/bxctl/src/cli.rsandcrates/bxctl/src/main.rsas modified, andcrates/bxctl/src/sanitize.rsand the two test files as new, besides your log. Stage withgit add crates/bxctl docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.Cargo.lockdoes not change in this task; thegit addis harmless.
Done when
make gateprintsgate: okwith 1099 tests.git diff HEAD~1 --statlists onlycrates/bxctl/src/lib.rs,crates/bxctl/src/cli.rs,crates/bxctl/src/main.rs,crates/bxctl/src/sanitize.rs, the two new test files anddocs/implementer-log.md.
Stop and report if
- A "before" block of L1, C1 to C3 or M1 to M3 is not in its file exactly as shown, or is there more than once.
- Step 2 or 3 shows a different result: other errors, other failing tests, or other counts.
cargo check -p bxctlreports an error after a body you filled, and the comment does not tell you how to fix it. Quote the error and the comment, and stop.- A comment in the skeleton does not fit the code under it. Quote it and stop.
- After step 5 a test fails although each function follows its comment. Do not open the test to find out why, and do not change a test or the given code of the skeleton: stop, and give the failing tests' names and their messages.
- The fix seems to need a change anywhere else: in
bxcore, another file ofbxctl, or aCargo.toml. It does not; stop and say why you think so. - A test outside
bxctlfails in the gate: check once whether it fails without your change, then stop and report both results. - The gate passes with a count other than 1099.