diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..14bf554 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,49 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "brokerd" +version = "0.1.0" +dependencies = [ + "proto", +] + +[[package]] +name = "bxctl" +version = "0.1.0" +dependencies = [ + "proto", +] + +[[package]] +name = "gatewayd" +version = "0.1.0" +dependencies = [ + "proto", +] + +[[package]] +name = "inferproxy" +version = "0.1.0" +dependencies = [ + "proto", +] + +[[package]] +name = "loopd" +version = "0.1.0" +dependencies = [ + "proto", +] + +[[package]] +name = "proto" +version = "0.1.0" + +[[package]] +name = "toolkit" +version = "0.1.0" +dependencies = [ + "proto", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..733ae69 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +resolver = "3" +members = [ + "crates/proto", + "crates/loopd", + "crates/brokerd", + "crates/gatewayd", + "crates/inferproxy", + "crates/toolkit", + "crates/bxctl", +] + +[workspace.package] +edition = "2024" +rust-version = "1.95" +publish = false + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.dependencies] +proto = { path = "crates/proto" } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a9c14f3 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# Boxmaker gate. `make gate` must pass before any work is called done. It needs no network. + +.PHONY: gate audit verify-device + +gate: + cargo fmt --all --check + cargo clippy --workspace --all-targets --locked --offline -- -D warnings + cargo test --workspace --locked --offline + cargo deny --offline check bans licenses sources + sh scripts/check-lines.sh + sh scripts/check-crate-deps.sh + sh scripts/check-dep-docs.sh + sh scripts/test-gate-scripts.sh + @echo "gate: ok" + +# Fetches the RustSec advisory database. Listed in docs/egress.md. +audit: + cargo deny check advisories + +# Checks that need straylight. Filled in from M2. +verify-device: + @echo "verify-device: nothing to check until M2" diff --git a/crates/brokerd/Cargo.toml b/crates/brokerd/Cargo.toml new file mode 100644 index 0000000..829bb8d --- /dev/null +++ b/crates/brokerd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "brokerd" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/brokerd/src/lib.rs b/crates/brokerd/src/lib.rs new file mode 100644 index 0000000..330d8ce --- /dev/null +++ b/crates/brokerd/src/lib.rs @@ -0,0 +1 @@ +//! The broker: the only role that holds authority. diff --git a/crates/brokerd/src/main.rs b/crates/brokerd/src/main.rs new file mode 100644 index 0000000..5dc6c7e --- /dev/null +++ b/crates/brokerd/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("brokerd: not implemented until M3"); + std::process::exit(2); +} diff --git a/crates/bxctl/Cargo.toml b/crates/bxctl/Cargo.toml new file mode 100644 index 0000000..c75c375 --- /dev/null +++ b/crates/bxctl/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bxctl" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/bxctl/src/lib.rs b/crates/bxctl/src/lib.rs new file mode 100644 index 0000000..13a8c97 --- /dev/null +++ b/crates/bxctl/src/lib.rs @@ -0,0 +1 @@ +//! The owner's command-line tool. diff --git a/crates/bxctl/src/main.rs b/crates/bxctl/src/main.rs new file mode 100644 index 0000000..75459df --- /dev/null +++ b/crates/bxctl/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("bxctl: not implemented until M2"); + std::process::exit(2); +} diff --git a/crates/gatewayd/Cargo.toml b/crates/gatewayd/Cargo.toml new file mode 100644 index 0000000..8b1d220 --- /dev/null +++ b/crates/gatewayd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gatewayd" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/gatewayd/src/lib.rs b/crates/gatewayd/src/lib.rs new file mode 100644 index 0000000..f78d946 --- /dev/null +++ b/crates/gatewayd/src/lib.rs @@ -0,0 +1 @@ +//! The Mattermost channel. Outbound connections only. diff --git a/crates/gatewayd/src/main.rs b/crates/gatewayd/src/main.rs new file mode 100644 index 0000000..9a276c4 --- /dev/null +++ b/crates/gatewayd/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("gatewayd: not implemented until M4"); + std::process::exit(2); +} diff --git a/crates/inferproxy/Cargo.toml b/crates/inferproxy/Cargo.toml new file mode 100644 index 0000000..54ec5fa --- /dev/null +++ b/crates/inferproxy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "inferproxy" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/inferproxy/src/lib.rs b/crates/inferproxy/src/lib.rs new file mode 100644 index 0000000..575af64 --- /dev/null +++ b/crates/inferproxy/src/lib.rs @@ -0,0 +1 @@ +//! Forwards bytes between `infer.sock` and the llama-server router. It logs nothing. diff --git a/crates/inferproxy/src/main.rs b/crates/inferproxy/src/main.rs new file mode 100644 index 0000000..11f585e --- /dev/null +++ b/crates/inferproxy/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("inferproxy: not implemented until M2"); + std::process::exit(2); +} diff --git a/crates/loopd/Cargo.toml b/crates/loopd/Cargo.toml new file mode 100644 index 0000000..9c812db --- /dev/null +++ b/crates/loopd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "loopd" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/loopd/src/lib.rs b/crates/loopd/src/lib.rs new file mode 100644 index 0000000..6d2a343 --- /dev/null +++ b/crates/loopd/src/lib.rs @@ -0,0 +1 @@ +//! The agent loop: sessions, prompt assembly and memory. It holds no authority. diff --git a/crates/loopd/src/main.rs b/crates/loopd/src/main.rs new file mode 100644 index 0000000..081fcd6 --- /dev/null +++ b/crates/loopd/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("loopd: not implemented until M2"); + std::process::exit(2); +} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml new file mode 100644 index 0000000..aef1fae --- /dev/null +++ b/crates/proto/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "proto" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs new file mode 100644 index 0000000..953bde6 --- /dev/null +++ b/crates/proto/src/lib.rs @@ -0,0 +1 @@ +//! Shared data types and the frame codec for Boxmaker. No policy and no I/O beyond frames. diff --git a/crates/toolkit/Cargo.toml b/crates/toolkit/Cargo.toml new file mode 100644 index 0000000..644c499 --- /dev/null +++ b/crates/toolkit/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "toolkit" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +proto.workspace = true diff --git a/crates/toolkit/src/lib.rs b/crates/toolkit/src/lib.rs new file mode 100644 index 0000000..a70e307 --- /dev/null +++ b/crates/toolkit/src/lib.rs @@ -0,0 +1 @@ +//! Entry points that run inside tool containers. diff --git a/crates/toolkit/src/main.rs b/crates/toolkit/src/main.rs new file mode 100644 index 0000000..9a439df --- /dev/null +++ b/crates/toolkit/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + eprintln!("toolkit: not implemented until M3"); + std::process::exit(2); +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..b081bd2 --- /dev/null +++ b/deny.toml @@ -0,0 +1,25 @@ +# cargo-deny configuration. `make gate` runs bans, licenses and sources offline. +# `make audit` runs advisories, which fetches the RustSec database. + +[graph] +all-features = true + +[licenses] +allow = ["MIT", "Apache-2.0", "Unicode-3.0"] +confidence-threshold = 0.9 + +[licenses.private] +ignore = true + +[bans] +multiple-versions = "deny" +wildcards = "deny" +allow-wildcard-paths = true + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] + +[advisories] +yanked = "deny" diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 0000000..660cac5 --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,6 @@ +# Dependencies + +Every external crate has a row here. `scripts/check-dep-docs.sh` enforces it. + +| Crate | Version | Used by | Why | +|---|---|---|---| diff --git a/docs/egress.md b/docs/egress.md new file mode 100644 index 0000000..8ef352c --- /dev/null +++ b/docs/egress.md @@ -0,0 +1,9 @@ +# Egress + +Every outbound network call the project makes, at run time or in development. Nothing else is +allowed. + +| When | From | To | What | +|---|---|---|---| +| Development | `cargo` | crates.io | Downloading the crates listed in `docs/dependencies.md` | +| Development | `make audit` | github.com/rustsec/advisory-db | The RustSec advisory database, fetched by `cargo deny check advisories` | diff --git a/docs/implementer-log.md b/docs/implementer-log.md index 781b7a1..798d7cd 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -5,5 +5,6 @@ reviewer adds findings under "Reviews" once per milestone. | Task | Date | Status | Gate runs | First gate | Deviations | Notes | |---|---|---|---|---|---|---| +| M1/01-workspace-and-gate | 2026-09-17 | done | 1 | pass | none | Crate skeletons, Cargo files and the given Makefile/deny.toml/test-gate-scripts.sh were already present untracked from a prior attempt; I verified them against the plan and created only the missing gate scripts, dependencies.md, egress.md and this log row. | ## Reviews diff --git a/scripts/check-crate-deps.sh b/scripts/check-crate-deps.sh new file mode 100644 index 0000000..c38ee8c --- /dev/null +++ b/scripts/check-crate-deps.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Fails if `proto` depends on any workspace crate, or if any other crate depends on +# a workspace crate other than `proto`. Every manifest section whose name contains +# "dependencies" (including [dev-dependencies]) is inspected. A dependency is the +# key in such a section, however it is written: `x.workspace = true`, +# `x = { path = ".." }` or `x = "1"`. +ROOT="${1:-.}" +status=0 + +# Workspace crate names: directories under ROOT/crates that contain a Cargo.toml. +workspace="" +for d in "$ROOT"/crates/*/; do + [ -d "$d" ] || continue + [ -f "$d/Cargo.toml" ] || continue + workspace="$workspace $(basename "$d")" +done + +for crate in "$ROOT"/crates/*/; do + [ -d "$crate" ] || continue + crate_name=$(basename "$crate") + [ -f "$crate/Cargo.toml" ] || continue + keys=$(awk ' + BEGIN { in_deps = 0; depth = 0 } + /^\[/ { + sec = $0 + sub(/^\[[ \t]*/, "", sec) + sub(/[ \t]*\][ \t]*.*$/, "", sec) + in_deps = (sec ~ /dependencies/) + depth = 0 + next + } + in_deps { + if (depth == 0) { + t = $0 + sub(/^[ \t]+/, "", t) + if (t ~ /^[A-Za-z0-9][A-Za-z0-9_-]*(\.workspace)?[ \t]*=/) { + if (match(t, /[A-Za-z0-9][A-Za-z0-9_-]*/)) { + k = substr(t, RSTART, RLENGTH) + if (RSTART == 1) print k + } + } + } + s = $0 + while (match(s, /[{}]/)) { + c = substr(s, RSTART, 1) + if (c == "{") depth++ + else if (depth > 0) depth-- + s = substr(s, RSTART + 1) + } + } + ' "$crate/Cargo.toml" 2>/dev/null) + for key in $keys; do + hit=0 + for w in $workspace; do + if [ "$key" = "$w" ]; then hit=1; break; fi + done + if [ "$hit" -eq 1 ]; then + if [ "$crate_name" = "proto" ]; then + echo "check-crate-deps: $crate_name depends on workspace crate $key" >&2 + status=1 + elif [ "$key" != "proto" ]; then + echo "check-crate-deps: $crate_name depends on workspace crate $key" >&2 + status=1 + fi + fi + done +done +exit $status diff --git a/scripts/check-dep-docs.sh b/scripts/check-dep-docs.sh new file mode 100644 index 0000000..503362c --- /dev/null +++ b/scripts/check-dep-docs.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# Fails if a key in [workspace.dependencies] of ROOT/Cargo.toml that is not a +# workspace crate has no table row starting `| `name` |` in ROOT/docs/dependencies.md. +# Also fails if any dependency line in any ROOT/crates/*/Cargo.toml lacks +# `workspace = true`. +ROOT="${1:-.}" +status=0 + +# Workspace crate names: directories under ROOT/crates that contain a Cargo.toml. +workspace="" +for d in "$ROOT"/crates/*/; do + [ -d "$d" ] || continue + [ -f "$d/Cargo.toml" ] || continue + workspace="$workspace $(basename "$d")" +done + +root_toml="$ROOT/Cargo.toml" +docs="$ROOT/docs/dependencies.md" + +# Extract dependency keys from every [dependencies*]-ish section. +dep_keys=$(awk ' + BEGIN { in_deps = 0; depth = 0 } + /^\[/ { + sec = $0 + sub(/^\[[ \t]*/, "", sec) + sub(/[ \t]*\][ \t]*.*$/, "", sec) + in_deps = (sec ~ /dependencies/) + depth = 0 + next + } + in_deps { + if (depth == 0) { + t = $0 + sub(/^[ \t]+/, "", t) + if (t ~ /^[A-Za-z0-9][A-Za-z0-9_-]*(\.workspace)?[ \t]*=/) { + if (match(t, /[A-Za-z0-9][A-Za-z0-9_-]*/)) { + k = substr(t, RSTART, RLENGTH) + if (RSTART == 1) print k + } + } + } + s = $0 + while (match(s, /[{}]/)) { + c = substr(s, RSTART, 1) + if (c == "{") depth++ + else if (depth > 0) depth-- + s = substr(s, RSTART + 1) + } + } +' "$root_toml" 2>/dev/null) + +# Part 1: non-workspace-crate workspace dependencies must have a docs table row. +for key in $dep_keys; do + hit=0 + for w in $workspace; do + if [ "$key" = "$w" ]; then hit=1; break; fi + done + if [ "$hit" -eq 0 ]; then + if ! awk -v k="$key" 'index($0, "| `" k "` |") == 1 { found = 1; exit } END { exit !found }' "$docs" 2>/dev/null; then + echo "check-dep-docs: $key has no row in $docs" >&2 + status=1 + fi + fi +done + +# Part 2: every dependency line in a crate manifest must contain `workspace = true`. +for crate in "$ROOT"/crates/*/; do + [ -d "$crate" ] || continue + crate_name=$(basename "$crate") + [ -f "$crate/Cargo.toml" ] || continue + bad=$(awk ' + BEGIN { in_deps = 0; depth = 0 } + /^\[/ { + sec = $0 + sub(/^\[[ \t]*/, "", sec) + sub(/[ \t]*\][ \t]*.*$/, "", sec) + in_deps = (sec ~ /dependencies/) + depth = 0 + next + } + in_deps { + if (depth == 0) { + t = $0 + sub(/^[ \t]+/, "", t) + if (t ~ /^[A-Za-z0-9][A-Za-z0-9_-]*(\.workspace)?[ \t]*=/) { + print $0 + } + } + s = $0 + while (match(s, /[{}]/)) { + c = substr(s, RSTART, 1) + if (c == "{") depth++ + else if (depth > 0) depth-- + s = substr(s, RSTART + 1) + } + } + ' "$crate/Cargo.toml" 2>/dev/null | grep -v 'workspace = true') + if [ -n "$bad" ]; then + echo "$bad" | while read -r line; do + [ -n "$line" ] && echo "check-dep-docs: $crate_name declares a dependency without workspace = true: $line" >&2 + done + status=1 + fi +done +exit $status diff --git a/scripts/check-lines.sh b/scripts/check-lines.sh new file mode 100644 index 0000000..9187321 --- /dev/null +++ b/scripts/check-lines.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Fails if any *.rs file under ROOT/crates has more than 500 lines. +# Files under any target/ directory are ignored. Test files count. +ROOT="${1:-.}" + +bad=$(find "$ROOT/crates" -type f -name '*.rs' ! -path '*/target/*' \ + -exec awk 'END { exit !(NR > 500) }' {} \; -print 2>/dev/null) +if [ -n "$bad" ]; then + echo "$bad" | while read -r f; do + [ -n "$f" ] && echo "check-lines: $f" >&2 + done + exit 1 +fi +exit 0 diff --git a/scripts/test-gate-scripts.sh b/scripts/test-gate-scripts.sh new file mode 100644 index 0000000..f51d127 --- /dev/null +++ b/scripts/test-gate-scripts.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Self-test for the three gate scripts. It builds small fake trees in a temporary +# directory and checks that each script passes the good tree and fails the bad ones. +# Do not edit: this file defines the required behaviour of the scripts. +set -eu +here=$(cd "$(dirname "$0")" && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +fails=0 + +expect() { # expect pass|fail NAME SCRIPT ROOT + want="$1"; name="$2"; script="$3"; root="$4" + if sh "$here/$script" "$root" >/dev/null 2>&1; then got=pass; else got=fail; fi + if [ "$got" != "$want" ]; then + echo "test-gate-scripts: $name: expected $want, got $got" >&2 + fails=$((fails + 1)) + fi +} + +manifest() { # manifest DIR NAME [DEPENDENCY-LINES...] + dir="$1"; name="$2"; shift 2 + mkdir -p "$dir/src" + { + printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[dependencies]\n' "$name" + for line in "$@"; do printf '%s\n' "$line"; done + } > "$dir/Cargo.toml" +} + +tree() { # tree ROOT: a good workspace with proto, loopd and brokerd + root="$1" + mkdir -p "$root/docs" + printf '[workspace]\nmembers = ["crates/*"]\n\n[workspace.dependencies]\nproto = { path = "crates/proto" }\nserde = { version = "1", features = ["derive"] }\n' > "$root/Cargo.toml" + printf '# Dependencies\n\n| Crate | Why |\n|---|---|\n| `serde` | types |\n' > "$root/docs/dependencies.md" + manifest "$root/crates/proto" proto 'serde.workspace = true' + manifest "$root/crates/loopd" loopd 'proto.workspace = true' + manifest "$root/crates/brokerd" brokerd 'proto = { workspace = true }' 'serde.workspace = true' +} + +lines() { # lines N FILE + mkdir -p "$(dirname "$2")" + i=0; : > "$2" + while [ "$i" -lt "$1" ]; do echo "// line" >> "$2"; i=$((i + 1)); done +} + +# check-lines.sh +tree "$tmp/l-ok"; lines 500 "$tmp/l-ok/crates/loopd/src/lib.rs" +expect pass "500 lines is allowed" check-lines.sh "$tmp/l-ok" +tree "$tmp/l-bad"; lines 501 "$tmp/l-bad/crates/loopd/src/deep/mod.rs" +expect fail "501 lines in a nested file" check-lines.sh "$tmp/l-bad" +tree "$tmp/l-test"; lines 501 "$tmp/l-test/crates/proto/tests/big.rs" +expect fail "501 lines in a test file" check-lines.sh "$tmp/l-test" +tree "$tmp/l-tgt"; lines 501 "$tmp/l-tgt/crates/proto/target/debug/gen.rs" +expect pass "files under target/ are ignored" check-lines.sh "$tmp/l-tgt" + +# check-crate-deps.sh +tree "$tmp/c-ok" +expect pass "roles depend only on proto" check-crate-deps.sh "$tmp/c-ok" +tree "$tmp/c-role"; manifest "$tmp/c-role/crates/loopd" loopd 'proto.workspace = true' 'brokerd.workspace = true' +expect fail "role depends on another role" check-crate-deps.sh "$tmp/c-role" +tree "$tmp/c-tbl"; manifest "$tmp/c-tbl/crates/loopd" loopd 'brokerd = { path = "../brokerd" }' +expect fail "role depends on another role by path" check-crate-deps.sh "$tmp/c-tbl" +tree "$tmp/c-dev"; printf '\n[dev-dependencies]\nbrokerd.workspace = true\n' >> "$tmp/c-dev/crates/loopd/Cargo.toml" +expect fail "role dev-depends on another role" check-crate-deps.sh "$tmp/c-dev" +tree "$tmp/c-proto"; manifest "$tmp/c-proto/crates/proto" proto 'loopd.workspace = true' +expect fail "proto depends on a role" check-crate-deps.sh "$tmp/c-proto" + +# check-dep-docs.sh +tree "$tmp/d-ok" +expect pass "every dependency is documented" check-dep-docs.sh "$tmp/d-ok" +tree "$tmp/d-miss"; printf 'rand = "0.9"\n' >> "$tmp/d-miss/Cargo.toml" +expect fail "workspace dependency without a docs row" check-dep-docs.sh "$tmp/d-miss" +tree "$tmp/d-prose"; printf 'rand = "0.9"\n' >> "$tmp/d-prose/Cargo.toml"; printf '\nWe do not use rand.\n' >> "$tmp/d-prose/docs/dependencies.md" +expect fail "a mention in prose is not a table row" check-dep-docs.sh "$tmp/d-prose" +tree "$tmp/d-loose"; manifest "$tmp/d-loose/crates/loopd" loopd 'proto.workspace = true' 'rand = "0.9"' +expect fail "crate declares a dependency outside the workspace table" check-dep-docs.sh "$tmp/d-loose" + +if [ "$fails" -ne 0 ]; then + echo "test-gate-scripts: $fails failure(s)" >&2 + exit 1 +fi +echo "test-gate-scripts: ok"