From 16a42d54f80cdcf92c91c1f2914b4d7fd04a5a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Sat, 9 May 2026 00:05:40 +0200 Subject: [PATCH] sync from monorepo @ 5408ddc3 --- README.md | 47 ++++++---- architecture.svg | 86 +++++++++++++++++++ crates/dirigent_fermata/README.md | 18 ++-- crates/dirigent_fermata/src/core/project.rs | 20 +++-- crates/dirigent_fermata/tests/core_project.rs | 59 ++++++++++++- 5 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 architecture.svg diff --git a/README.md b/README.md index d266bdc..71f5e07 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,34 @@

Core libraries for the Dirigent agent orchestration platform.

+> **Under construction.** Dirigent is in active development. Most crates are experimental — APIs will change. There is nothing to install from this repository yet. The standalone tools listed below have their own repositories. + --- -Dirigent is a multi-agent orchestration platform built around the Agent-Client Protocol (ACP). This repository contains the foundational library crates — the building blocks used by downstream tools such as [dirigate](https://git.g4b.org/dirigence/dirigate) and [fermata](https://git.g4b.org/dirigence/fermata). +## Standalone Tools -> **Downstream mirror.** Active development happens in an upstream monorepo. This repository is an export of the core library crates and is updated on each release. Issues and contributions should be directed to the upstream project. +These tools are developed in this monorepo but distributed as independent repositories. Install them from their own repos: + +| Tool | Repository | Description | +|------|-----------|-------------| +| **fermata** | [git.g4b.org/dirigence/fermata](https://git.g4b.org/dirigence/fermata) | Policy gate for AI coding agents — `.botignore` enforcement | +| **anth** | [git.g4b.org/dirigence/dirigent_anth](https://git.g4b.org/dirigence/dirigent_anth) | Tools for working with Claude Code session data | +| **dirigate** | [git.g4b.org/dirigence/dirigate](https://git.g4b.org/dirigence/dirigate) | ACP bridge connecting stdio agents to Dirigent | + +--- + +## Architecture + +

+ Dirigent package architecture +

+ +**Layers top-to-bottom:** +- **Standalone Tools** — installable from their own repositories; depend on foundation crates +- **Orchestration** — multi-connector runtime, ACP server, task management, archival +- **Foundation** — protocol types, tool sandbox, configuration, auth +- **Integrations** — Matrix, Langfuse, Zed, and other external system connectors +- **Parsers** — readers for third-party session formats (OpenCode, ChatGPT, Codex) --- @@ -41,9 +64,7 @@ Dirigent is a multi-agent orchestration platform built around the Agent-Client P --- -## Usage - -### Library crates (via git dependency) +## Library Usage Add a crate to your `Cargo.toml`: @@ -53,21 +74,9 @@ dirigent_protocol = { git = "https://git.g4b.org/dirigence/dirigent", path = "cr dirigent_core = { git = "https://git.g4b.org/dirigence/dirigent", path = "crates/dirigent_core" } ``` -Replace `dirigent_protocol` / `dirigent_core` with the crate you need. All crates follow the same pattern. +Replace the crate name and path with the one you need. All crates follow the same pattern. -### Binary crates (cargo install) - -**fermata** — policy gate CLI and Claude hook adapter: - -```bash -cargo install --git https://git.g4b.org/dirigence/dirigent --features cli -``` - -**anth** — Claude Code session inspector: - -```bash -cargo install --git https://git.g4b.org/dirigence/dirigent --bin anth_bear --features dirigent-paths -``` +> **Expect breakage.** These are internal library crates under active development. Pin to a specific commit if you depend on stability. --- diff --git a/architecture.svg b/architecture.svg new file mode 100644 index 0000000..55bbb32 --- /dev/null +++ b/architecture.svg @@ -0,0 +1,86 @@ + + + + + + + + Dirigent package architecture + + + + STANDALONE TOOLS + + fermata + + dirigate + + anth + ← own repos, installable + + + + ORCHESTRATION + + dirigent_core + multi-connector runtime + + dirigent_acp_api + ACP server + + dirigent_taskrunner + background tasks + + dirigent_archivist + session archival + + + + FOUNDATION + + dirigent_protocol + ACP types + messages + + dirigent_tools + tool sandbox + + dirigent_config + configuration + + dirigent_auth + authorization + + + + INTEGRATIONS + + matrix + session sharing + + langfuse + observability + + zed + editor + + + + PARSERS (third-party format readers) + + opencode_client + + dirigent_chatgpt + + dirigent_codex + + dirigent_inspector + + + + + + + + + + diff --git a/crates/dirigent_fermata/README.md b/crates/dirigent_fermata/README.md index 7dc4df3..ae03652 100644 --- a/crates/dirigent_fermata/README.md +++ b/crates/dirigent_fermata/README.md @@ -8,7 +8,7 @@ Drop a `.botignore` file in your project root. Fermata reads it and blocks your .env .env.* secrets/** -conf/localsettings.yaml +conf/settings.local.yaml ``` That's all it takes. @@ -90,8 +90,8 @@ Create a `.botignore` at your project root. Gitignore syntax. Blocks both reads secrets/** # Local config overrides -conf/localsettings.yaml -conf/localtestsettings.yaml +conf/settings.local.yaml +conf/settings.test.yaml # Generated files — let the tools rebuild them, not patch them dist/** @@ -107,7 +107,7 @@ For cases where `.botignore`'s uniform read+write block isn't granular enough: ```toml [read] # Block reading secrets outright -patterns = [".env*", "secrets/**", "conf/localsettings.yaml"] +patterns = [".env*", "secrets/**", "conf/settings.local.yaml"] [write] # Allow reading vendor code but block patching it @@ -156,12 +156,12 @@ When Claude attempts a `Read(.env)`, `Write(vendor/foo.js)`, or `Bash(rm ./secre ## Real-world scenario -A project has `.env`, `conf/localsettings.yaml`, and a `vendor/` tree it doesn't want patched. With `.botignore`: +A project has `.env`, `conf/settings.local.yaml`, and a `vendor/` tree it doesn't want patched. With `.botignore`: ```gitignore .env .env.* -conf/localsettings.yaml +conf/settings.local.yaml vendor/** ``` @@ -169,8 +169,8 @@ Claude attempts to read credentials: ``` Tool: Read -Path: ./conf/localsettings.yaml -Decision: BLOCK — matched rule "conf/localsettings.yaml" (.botignore) +Path: ./conf/settings.local.yaml +Decision: BLOCK — matched rule "conf/settings.local.yaml" (.botignore) ``` Claude attempts to read application code: @@ -186,7 +186,7 @@ Claude attempts to run `cat .env` via bash — which would bypass a path-only ch ```toml # botignore.toml [bash] -deny = ["cat .env*", "cat conf/localsettings*"] +deny = ["cat .env*", "cat conf/settings.local*"] ``` ``` diff --git a/crates/dirigent_fermata/src/core/project.rs b/crates/dirigent_fermata/src/core/project.rs index bceba07..91beed6 100644 --- a/crates/dirigent_fermata/src/core/project.rs +++ b/crates/dirigent_fermata/src/core/project.rs @@ -1,12 +1,14 @@ use std::path::{Path, PathBuf}; -/// Markers checked in priority order when walking up from a target path. -const MARKERS: &[&str] = &["botignore.toml", ".botignore", ".git"]; +/// Strong markers that definitively identify a project root. +const STRONG_MARKERS: &[&str] = &["botignore.toml", ".botignore.toml", ".git"]; /// Walk upward from `target` (or its parent if `target` is a file) looking -/// for the nearest project root. Roots are identified by the presence of -/// any marker in `MARKERS`. Walks from the **target file's location**, not -/// from cwd, because agents `cd` around. +/// for the nearest project root. Strong markers (`botignore.toml`, +/// `.botignore.toml`, `.git`) stop the walk immediately. A `.botignore` +/// file is remembered as a fallback but does not stop the walk — the search +/// continues upward for a stronger boundary. If none is found, the +/// `.botignore` location is used. pub fn find_project_root(target: &Path) -> Option { let start = if target.is_file() { target.parent()? @@ -14,14 +16,18 @@ pub fn find_project_root(target: &Path) -> Option { target }; + let mut fallback: Option = None; let mut current = Some(start); while let Some(dir) = current { - for marker in MARKERS { + for marker in STRONG_MARKERS { if dir.join(marker).exists() { return Some(dir.to_path_buf()); } } + if fallback.is_none() && dir.join(".botignore").exists() { + fallback = Some(dir.to_path_buf()); + } current = dir.parent(); } - None + fallback } diff --git a/crates/dirigent_fermata/tests/core_project.rs b/crates/dirigent_fermata/tests/core_project.rs index b4a3dc3..98baa10 100644 --- a/crates/dirigent_fermata/tests/core_project.rs +++ b/crates/dirigent_fermata/tests/core_project.rs @@ -8,7 +8,7 @@ fn finds_botignore_toml_first() { let root = tmp.path(); fs::create_dir_all(root.join("sub/deep")).unwrap(); fs::write(root.join("botignore.toml"), "").unwrap(); - fs::write(root.join(".botignore"), "").unwrap(); + fs::write(root.join(".botignore.toml"), "").unwrap(); fs::create_dir_all(root.join(".git")).unwrap(); let target = root.join("sub/deep/file.rs"); @@ -19,11 +19,11 @@ fn finds_botignore_toml_first() { } #[test] -fn falls_back_to_botignore() { +fn finds_dot_botignore_toml() { let tmp = TempDir::new().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("sub")).unwrap(); - fs::write(root.join(".botignore"), "").unwrap(); + fs::write(root.join(".botignore.toml"), "").unwrap(); let target = root.join("sub/file.rs"); fs::write(&target, "").unwrap(); @@ -32,6 +32,57 @@ fn falls_back_to_botignore() { assert_eq!(found, root); } +#[test] +fn botignore_alone_does_not_stop_walk() { + // A bare .botignore is a policy file, not a project boundary. + // The walk should continue past it to find a real root marker. + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("a/b")).unwrap(); + fs::create_dir_all(root.join(".git")).unwrap(); + fs::write(root.join("a/.botignore"), "*.secret").unwrap(); + + let target = root.join("a/b/file.rs"); + fs::write(&target, "").unwrap(); + + // Should find root (with .git), NOT root/a (with .botignore) + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + +#[test] +fn botignore_used_as_fallback() { + // If only .botignore exists (no strong marker), it serves as a fallback + // root so that policy is still enforced. + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("sub")).unwrap(); + fs::write(root.join(".botignore"), "*.secret").unwrap(); + + let target = root.join("sub/file.rs"); + fs::write(&target, "").unwrap(); + + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + +#[test] +fn strong_marker_preferred_over_botignore_fallback() { + // .botignore at a/b/, .git at root — walk past .botignore, use root. + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("a/b/c")).unwrap(); + fs::create_dir_all(root.join(".git")).unwrap(); + fs::write(root.join("a/b/.botignore"), "*.key").unwrap(); + + let target = root.join("a/b/c/file.rs"); + fs::write(&target, "").unwrap(); + + // Should find root (with .git), not a/b (with .botignore) + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + #[test] fn falls_back_to_git() { let tmp = TempDir::new().unwrap(); @@ -59,7 +110,7 @@ fn walks_up_from_file_path_not_cwd() { let tmp = TempDir::new().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("a/b/c")).unwrap(); - fs::write(root.join("a/.botignore"), "").unwrap(); + fs::write(root.join("a/botignore.toml"), "").unwrap(); let target = root.join("a/b/c/file.rs"); fs::write(&target, "").unwrap();