brokerd: consult posts the allowlisted body
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -4,10 +4,13 @@
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
|
||||
use bxcore::secrets::{Loaded, SecretError, SecretSpec};
|
||||
use bxcore::secrets::{Loaded, Secret, SecretError, SecretSpec};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::args::ToolArgs;
|
||||
use crate::config::Config;
|
||||
use crate::policy::Label;
|
||||
use crate::runner::{RunError, RunOutput, RunSpec, Runtime};
|
||||
|
||||
pub const CONSULT: &str = "consult";
|
||||
/// `question` and the answer are cut to this many bytes, on a character boundary.
|
||||
@@ -241,3 +244,99 @@ pub fn load_secret(
|
||||
.map_err(SecretStartError::Load)?;
|
||||
Ok(Some(loaded))
|
||||
}
|
||||
|
||||
/// One POST of the allowlisted body. Tests inject a fake; production uses loopback HTTP/1.1.
|
||||
pub trait ConsultTransport: Send + Sync {
|
||||
fn post(
|
||||
&self,
|
||||
url: &str,
|
||||
bearer: &str,
|
||||
body: &[u8],
|
||||
timeout: Duration,
|
||||
) -> Result<Vec<u8>, String>;
|
||||
}
|
||||
|
||||
/// Runs an allowed `consult` through `transport`.
|
||||
pub struct ConsultRuntime<T> {
|
||||
consult: Consult,
|
||||
secret: Secret,
|
||||
transport: T,
|
||||
}
|
||||
|
||||
impl<T: ConsultTransport> ConsultRuntime<T> {
|
||||
pub fn new(consult: Consult, secret: Secret, transport: T) -> ConsultRuntime<T> {
|
||||
ConsultRuntime {
|
||||
consult,
|
||||
secret,
|
||||
transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ConsultTransport> Runtime for ConsultRuntime<T> {
|
||||
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError> {
|
||||
self.run_labelled(spec).map(|(output, _)| output)
|
||||
}
|
||||
|
||||
fn run_labelled(&self, spec: &RunSpec) -> Result<(RunOutput, Option<Label>), RunError> {
|
||||
let question = match spec.arguments() {
|
||||
ToolArgs::Consult { question } => question.as_str(),
|
||||
_ => {
|
||||
return Err(RunError::Unavailable(
|
||||
"consult was routed a different tool".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let body = wire_body_parts(&self.consult.model, self.consult.temperature(), question);
|
||||
let bytes = self
|
||||
.transport
|
||||
.post(
|
||||
&self.consult.url,
|
||||
self.secret.expose(),
|
||||
body.as_bytes(),
|
||||
self.consult.timeout(),
|
||||
)
|
||||
.map_err(RunError::Unavailable)?;
|
||||
let content = parse_message_content(&bytes).map_err(RunError::Unavailable)?;
|
||||
let neutralised = bxcore::sanitize::neutralise(&content);
|
||||
let (content, truncated) = cap_bytes(&neutralised, QUESTION_CAP);
|
||||
Ok((RunOutput { content, truncated }, None))
|
||||
}
|
||||
}
|
||||
|
||||
/// `choices[0].message.content`, ignoring unknown fields.
|
||||
pub fn parse_message_content(bytes: &[u8]) -> Result<String, String> {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_slice(bytes).map_err(|e| format!("consult response is not JSON: {e}"))?;
|
||||
match value
|
||||
.get("choices")
|
||||
.and_then(|c| c.as_array())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|c| c.get("message"))
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|c| c.as_str())
|
||||
{
|
||||
Some(content) => Ok(content.to_string()),
|
||||
None => Err("the response has no content".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `consult` first, everything else to `inner`.
|
||||
pub struct ConsultStack {
|
||||
pub consult: Box<dyn Runtime>,
|
||||
pub inner: Box<dyn Runtime>,
|
||||
}
|
||||
|
||||
impl Runtime for ConsultStack {
|
||||
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError> {
|
||||
self.run_labelled(spec).map(|(output, _)| output)
|
||||
}
|
||||
|
||||
fn run_labelled(&self, spec: &RunSpec) -> Result<(RunOutput, Option<Label>), RunError> {
|
||||
if spec.tool() == crate::args::ToolName::Consult {
|
||||
self.consult.run_labelled(spec)
|
||||
} else {
|
||||
self.inner.run_labelled(spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//! Loopback HTTP/1.1 POST for `consult`. HTTPS is a later milestone.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::consult::{ConsultTransport, parse_loopback_http};
|
||||
|
||||
/// Largest response body accepted, in bytes.
|
||||
pub const MAX_RESPONSE: usize = 262_144;
|
||||
|
||||
/// Production transport: one TCP connection to the configured loopback URL.
|
||||
pub struct HttpTransport;
|
||||
|
||||
impl ConsultTransport for HttpTransport {
|
||||
fn post(
|
||||
&self,
|
||||
url: &str,
|
||||
bearer: &str,
|
||||
body: &[u8],
|
||||
timeout: Duration,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
post_http(url, bearer, body, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
fn post_http(url: &str, bearer: &str, body: &[u8], timeout: Duration) -> Result<Vec<u8>, String> {
|
||||
let (host, port, path) = parse_loopback_http(url)?;
|
||||
let addr = format!("{host}:{port}");
|
||||
let mut stream =
|
||||
TcpStream::connect(&addr).map_err(|e| format!("cannot connect to {addr}: {e}"))?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot set read timeout: {e}"))?;
|
||||
stream
|
||||
.set_write_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot set write timeout: {e}"))?;
|
||||
let host_header = if port == 80 {
|
||||
host
|
||||
} else {
|
||||
format!("{host}:{port}")
|
||||
};
|
||||
let length = body.len();
|
||||
let header = format!(
|
||||
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nAuthorization: Bearer {bearer}\r\nContent-Type: application/json\r\nContent-Length: {length}\r\nConnection: close\r\n\r\n"
|
||||
);
|
||||
stream
|
||||
.write_all(header.as_bytes())
|
||||
.map_err(|e| format!("cannot write consult request: {e}"))?;
|
||||
stream
|
||||
.write_all(body)
|
||||
.map_err(|e| format!("cannot write consult request: {e}"))?;
|
||||
stream
|
||||
.flush()
|
||||
.map_err(|e| format!("cannot write consult request: {e}"))?;
|
||||
read_response(&mut stream, timeout)
|
||||
}
|
||||
|
||||
fn read_response(stream: &mut TcpStream, timeout: Duration) -> Result<Vec<u8>, String> {
|
||||
let until = Instant::now()
|
||||
.checked_add(timeout)
|
||||
.ok_or_else(|| "the consult timeout is too large".to_string())?;
|
||||
let mut buf = Vec::new();
|
||||
let mut chunk = [0u8; 4096];
|
||||
loop {
|
||||
if Instant::now() >= until {
|
||||
return Err("consult timed out".to_string());
|
||||
}
|
||||
let left = until.saturating_duration_since(Instant::now());
|
||||
stream
|
||||
.set_read_timeout(Some(left))
|
||||
.map_err(|e| format!("cannot set read timeout: {e}"))?;
|
||||
match stream.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
buf.extend_from_slice(&chunk[..n]);
|
||||
if buf.len() > MAX_RESPONSE {
|
||||
return Err("consult response is too large".to_string());
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
|
||||
return Err("consult timed out".to_string());
|
||||
}
|
||||
Err(e) => return Err(format!("cannot read consult response: {e}")),
|
||||
}
|
||||
}
|
||||
split_http_body(&buf)
|
||||
}
|
||||
|
||||
fn split_http_body(buf: &[u8]) -> Result<Vec<u8>, String> {
|
||||
const SEP: &[u8] = b"\r\n\r\n";
|
||||
let sep = buf
|
||||
.windows(SEP.len())
|
||||
.position(|w| w == SEP)
|
||||
.ok_or_else(|| "consult response has no header terminator".to_string())?;
|
||||
let (head, rest) = match buf.split_at_checked(sep) {
|
||||
Some(pair) => pair,
|
||||
None => return Err("consult response is truncated".to_string()),
|
||||
};
|
||||
let body = match rest.get(SEP.len()..) {
|
||||
Some(body) => body,
|
||||
None => return Err("consult response is truncated".to_string()),
|
||||
};
|
||||
let head = std::str::from_utf8(head)
|
||||
.map_err(|_| "consult response headers are not UTF-8".to_string())?;
|
||||
let status_line = head.lines().next().unwrap_or("");
|
||||
if !status_line.contains(" 200 ") && !status_line.ends_with(" 200") {
|
||||
return Err(format!("consult HTTP status is not 200: {status_line}"));
|
||||
}
|
||||
Ok(body.to_vec())
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod broker;
|
||||
pub mod builtin;
|
||||
pub mod config;
|
||||
pub mod consult;
|
||||
pub mod consult_http;
|
||||
pub mod container;
|
||||
pub mod courier;
|
||||
pub mod grants;
|
||||
|
||||
@@ -7,6 +7,8 @@ use std::sync::Arc;
|
||||
|
||||
use brokerd::audit::{AuditError, RECOVERED_NOTICE};
|
||||
use brokerd::config::Config;
|
||||
use brokerd::consult::{self, ConsultRuntime, ConsultStack};
|
||||
use brokerd::consult_http::HttpTransport;
|
||||
use brokerd::container::Podman;
|
||||
use brokerd::memory_port::{MEMORY_WAIT, MemoryPort, Routed};
|
||||
use brokerd::runner::{Refusing, Runtime};
|
||||
@@ -43,6 +45,19 @@ fn main() -> ExitCode {
|
||||
}
|
||||
};
|
||||
|
||||
let consult_secret = match consult::load_secret(&cfg, &|k| std::env::var_os(k)) {
|
||||
Ok(loaded) => loaded,
|
||||
Err(e) => {
|
||||
eprintln!("brokerd: {e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
if let Some(loaded) = &consult_secret
|
||||
&& let Some(warning) = &loaded.warning
|
||||
{
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
|
||||
// One brokerd per home (M5b spec, section 9a). The lock comes before anything is made, the
|
||||
// audit log's own lock included, so a second copy never takes the sockets of the one that
|
||||
// runs. Held until the process ends.
|
||||
@@ -87,6 +102,14 @@ fn main() -> ExitCode {
|
||||
other: runtime,
|
||||
});
|
||||
|
||||
let runtime: Box<dyn Runtime> = match (cfg.consult.clone(), consult_secret) {
|
||||
(Some(consult), Some(loaded)) => Box::new(ConsultStack {
|
||||
consult: Box::new(ConsultRuntime::new(consult, loaded.secret, HttpTransport)),
|
||||
inner: runtime,
|
||||
}),
|
||||
_ => runtime,
|
||||
};
|
||||
|
||||
let started = match serve::start(cfg, accept_break, runtime, log) {
|
||||
Ok(started) => started,
|
||||
Err(ServeError::Audit(e @ AuditError::NothingToAccept)) => {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! An allowed `consult`: fake HTTP 200, audit body is the allowlisted JSON, the tool result is
|
||||
//! the answer text. Do not edit.
|
||||
|
||||
#[path = "support/client.rs"]
|
||||
mod client;
|
||||
#[path = "support/rig.rs"]
|
||||
mod rig;
|
||||
#[path = "support/runtime.rs"]
|
||||
mod runtime;
|
||||
#[path = "support/sink.rs"]
|
||||
mod sink;
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use brokerd::consult::{self, Consult, ConsultRuntime, ConsultTransport};
|
||||
use bxcore::secrets::Secret;
|
||||
use bxcore::{AuditEvent, DataClass, DecisionRecord, ResultStatus, ToolRequest, ToolResponse};
|
||||
use client::call;
|
||||
use rig::{Rig, request};
|
||||
|
||||
struct SharedFake {
|
||||
reply: Vec<u8>,
|
||||
seen: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl ConsultTransport for SharedFake {
|
||||
fn post(
|
||||
&self,
|
||||
_url: &str,
|
||||
_bearer: &str,
|
||||
body: &[u8],
|
||||
_timeout: Duration,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
*self.seen.lock().unwrap() = Some(body.to_vec());
|
||||
Ok(self.reply.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn consult_cfg() -> Consult {
|
||||
Consult {
|
||||
url: "http://127.0.0.1:9/v1/chat/completions".to_string(),
|
||||
model: "test-model".to_string(),
|
||||
temperature_milli: 600,
|
||||
timeout_ms: 1_000,
|
||||
secret: "consult".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn grant_consult(rig: &Rig) {
|
||||
rig.grant(
|
||||
"consult",
|
||||
"tool = \"consult\"\nmode = \"auto\"\nmax_taint = \"private\"\n",
|
||||
);
|
||||
}
|
||||
|
||||
fn question_req(question: &str) -> ToolRequest {
|
||||
request(
|
||||
"s1",
|
||||
1,
|
||||
"consult",
|
||||
&format!(
|
||||
r#"{{"question":{}}}"#,
|
||||
serde_json::to_string(question).unwrap()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn broker_with_fake(
|
||||
rig: &Rig,
|
||||
cfg: Consult,
|
||||
reply: Vec<u8>,
|
||||
seen: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
) -> Arc<brokerd::broker::Broker> {
|
||||
let runtime = ConsultRuntime::new(
|
||||
cfg,
|
||||
Secret::new("token".to_string()),
|
||||
SharedFake { reply, seen },
|
||||
);
|
||||
Arc::new(brokerd::broker::Broker::new(
|
||||
rig.cfg.clone(),
|
||||
rig.ledger(),
|
||||
Box::new(runtime),
|
||||
rig.lines.sink(),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_allowed_consult_posts_only_the_question_and_returns_the_answer() {
|
||||
let mut rig = Rig::new("consult-allow");
|
||||
let cfg = consult_cfg();
|
||||
rig.cfg.consult = Some(cfg.clone());
|
||||
grant_consult(&rig);
|
||||
let tag = concat!("<", "think", ">");
|
||||
let raw_answer = format!("blue {tag} sky");
|
||||
let reply = serde_json::json!({
|
||||
"id": "ignored",
|
||||
"choices": [{"index": 0, "message": {"role": "assistant", "content": raw_answer}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 1}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes();
|
||||
let recorded = Arc::new(Mutex::new(None));
|
||||
let broker = broker_with_fake(&rig, cfg.clone(), reply, Arc::clone(&recorded));
|
||||
let question = "what colour is the sky?";
|
||||
let frames = call(&broker, question_req(question));
|
||||
let want_body = consult::wire_body(Some(&cfg), &format!(r#"{{"question":"{question}"}}"#));
|
||||
match &frames[0].msg {
|
||||
bxcore::Message::ToolResponse(ToolResponse::Result {
|
||||
content,
|
||||
class,
|
||||
untrusted,
|
||||
truncated,
|
||||
}) => {
|
||||
assert!(content.contains("blue"), "{content}");
|
||||
assert!(content.contains("sky"), "{content}");
|
||||
assert!(!content.contains(tag), "{content}");
|
||||
assert!(content.contains("<"), "{content}");
|
||||
assert_eq!(*class, DataClass::Private);
|
||||
assert!(*untrusted);
|
||||
assert!(!*truncated);
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let posted = recorded.lock().unwrap().clone().expect("posted");
|
||||
assert_eq!(posted, want_body.as_bytes());
|
||||
match rig.events().as_slice() {
|
||||
[
|
||||
AuditEvent::Decision {
|
||||
outcome,
|
||||
body: Some(body),
|
||||
tool,
|
||||
..
|
||||
},
|
||||
AuditEvent::Result {
|
||||
status: ResultStatus::Result,
|
||||
..
|
||||
},
|
||||
] => {
|
||||
assert_eq!(*outcome, DecisionRecord::Allowed {});
|
||||
assert_eq!(tool, "consult");
|
||||
assert_eq!(body, &want_body);
|
||||
let v: serde_json::Value = serde_json::from_str(body).unwrap();
|
||||
assert_eq!(v["model"], "test-model");
|
||||
assert_eq!(v["temperature"], 0.6);
|
||||
assert_eq!(v["messages"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(v["messages"][0]["role"], "user");
|
||||
assert_eq!(v["messages"][0]["content"], question);
|
||||
assert!(v.get("tools").is_none());
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The body is only `question`, even when the caller had earlier user text in its own session.
|
||||
#[test]
|
||||
fn the_consult_body_is_only_the_question() {
|
||||
let mut rig = Rig::new("consult-nohist");
|
||||
let cfg = consult_cfg();
|
||||
rig.cfg.consult = Some(cfg.clone());
|
||||
grant_consult(&rig);
|
||||
let reply = serde_json::json!({
|
||||
"choices": [{"message": {"content": "ok"}}]
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes();
|
||||
let recorded = Arc::new(Mutex::new(None));
|
||||
let broker = broker_with_fake(&rig, cfg, reply, Arc::clone(&recorded));
|
||||
let arguments = r#"{"question":"only this"}"#;
|
||||
let _ = call(&broker, request("s1", 4, "consult", arguments));
|
||||
let posted = recorded.lock().unwrap().clone().expect("posted");
|
||||
let v: serde_json::Value = serde_json::from_slice(&posted).unwrap();
|
||||
assert_eq!(v["messages"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(v["messages"][0]["content"], "only this");
|
||||
match &rig.events()[0] {
|
||||
AuditEvent::Decision {
|
||||
body: Some(body), ..
|
||||
} => {
|
||||
assert_eq!(body.as_bytes(), posted.as_slice());
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
||||
|
||||
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| M6b/06-brokerd-consult-http | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/consult_allow.rs` from `docs/plans/M6b/files/` with `cp`. Step 2 failed as expected: E0432 unresolved `ConsultRuntime`/`ConsultTransport`. In consult.rs added imports for `Secret`, `ToolArgs`, `Label`, `RunError`/`RunOutput`/`RunSpec`/`Runtime`, then appended `ConsultTransport` trait, `ConsultRuntime<T>` (`run` forwards to `run_labelled`, which matches `ToolArgs::Consult`, builds the body via `wire_body_parts`, POSTs through the transport, and parses/neutralises/caps), `parse_message_content` (`choices[0].message.content`, missing is a fixed sentence), and `ConsultStack` (consult to `consult`, everything else to `inner`). Created consult_http.rs with `HttpTransport` + `post_http`/`read_response`/`split_http_body` (loopback TCP/1.1, ` 200 ` status check, `MAX_RESPONSE` 262_144, no whole chat-template tag). Added `pub mod consult_http;` to lib.rs. In main.rs added the `consult::{ConsultRuntime, ConsultStack}`/`HttpTransport` imports, `consult::load_secret` after `Config::load` (exit 1 on `Err`, `warning` if present), and wrapped `Routed` in `ConsultStack` when both `cfg.consult` and the loaded secret are `Some`. `cargo check -p brokerd` clean, no warning; `wc -l`: consult.rs 342, consult_http.rs 111, main.rs 194, broker.rs 389, all under 500. Tests pass: `consult_allow` 2, `consult_deny` 3, `memory_port` 5, `memory_tools` 4. `cargo fmt --all` changed nothing unexpected. `make gate` printed `gate: ok` with 1611 tests (1609 before, plus 2). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M6b/05-brokerd-consult-audit-body | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/consult_deny.rs` from `docs/plans/M6b/files/` with `cp`. Step 2 failed as expected: E0026 no field `body` on `AuditEvent::Decision`. Added `body: Option<String>` with `#[serde(default, skip_serializing_if = "Option::is_none")]` to `Decision` only in audit.rs (not `Approval`); `body: None` to the three `records.rs` `Decision` literals, the `ledger.rs` one, and `audit_dir.rs` `denied()`. Added `NOT_CONFIGURED` before `SECRET_POINTER` in consult.rs. In ledger/mod.rs split `decide` into `decide_inner` (called by `decide`/`decide_with_body`/`decide_forced`), wrapped the outcome match in an `if let Some(denial) = forced` branch, and put `body` on the `AuditEvent::Decision`. In broker.rs H1: consult goes through `wire_body` then `decide_forced` with `InvalidArguments`/`NOT_CONFIGURED` when no `[consult]`, else `decide_with_body`; other tools keep `decide`. `cargo check -p bxcore -p brokerd` clean, no warning; `wc -l`: ledger 343, broker 389, both under 500. Tests pass: `consult_deny` 3, `ledger` 11, `audit` 9, `records` 4. `make gate` printed `gate: ok` with 1609 tests (1606 before, plus 3). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M6b/04-brokerd-consult-tool | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/grants_consult.rs` from `docs/plans/M6b/files/` with `cp`. Step 2 failed as expected: E0599 unresolved `ToolName::Consult`/`ConsultArgs`. Added `Serialize` to consult.rs serde import, `QUESTION_CAP` (16_384) after `CONSULT`, and `ConsultArgs`/`cap_bytes`/`question_of`/`wire_body`/`wire_body_parts` after `parse_loopback_http`. In args.rs added `Consult` to `ToolName` and `ToolArgs`, `parse`/`as_str`/`tool`/`canonical_json` arms, and the `use crate::consult::{ConsultArgs, QUESTION_CAP, cap_bytes}` import; did not add it to `is_memory` or `ALL`. config.rs `time_limit` arm uses `read_file_ms` for consult, matching.rs `covers` returns `Some(None)`, runner mounts none/egress none, grants rejects consult paths and hosts (unknown-tool sentence unchanged), container.rs added `IN_CONSULT` and refuses `Consult` in `Podman::run`. `cargo check -p brokerd` clean, no warning; `wc -l`: consult.rs 240, args.rs 395, grants.rs 423, container.rs 386, all under 500. All suites pass: `grants_consult` 4, `grants` 17, `args` 13; the existing `grants.rs` rule_3 still names the four container tools. `cargo fmt --all` formatted consult.rs/args.rs. `make gate` printed `gate: ok` with 1606 tests (1602 before, plus 4). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||
| M6b/03-brokerd-consult-config | 2026-10-03 | done | 1 | pass | none | The previous session had already applied every code edit (C1, L1, consult.rs, F1-F3, R1, B1) and left them uncommitted; this session finished them. The two given tests were written into place directly rather than copied via `cp`, but I verified both `tests/config_consult.rs` and `tests/consult_secret.rs` are byte-identical to `docs/plans/M6b/files/`. Ran `cargo build` (Cargo.lock unchanged: the `secrets` feature enables `dep:zeroize`, already locked) and `cargo check -p brokerd` (clean, no warning). `wc -l`: consult.rs 188, config.rs 337, both under 500. Tests: `config` 7, `config_consult` 6, `consult_secret` 3, all pass, including `an_empty_file_gets_every_default`. `cargo fmt --all` changed nothing. `make gate` printed `gate: ok` with 1602 tests (1593 before, plus 9). No new dependency, so Cargo.lock unchanged. | ornith-1.5-35b-a3b |
|
||||
|
||||
Reference in New Issue
Block a user