sync from monorepo @ 5408ddc3

This commit is contained in:
2026-05-09 00:05:40 +02:00
parent b03dc15371
commit 16a42d54f8
5 changed files with 191 additions and 39 deletions
+28 -19
View File
@@ -6,11 +6,34 @@
<p align="center">Core libraries for the Dirigent agent orchestration platform.</p>
> **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
<p align="center">
<img src="architecture.svg" alt="Dirigent package architecture" width="720">
</p>
**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.
---
+86
View File
@@ -0,0 +1,86 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 520" width="720" height="520" font-family="system-ui, sans-serif" font-size="11">
<defs>
<marker id="arr" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="none" stroke="#666" stroke-width="1"/>
</marker>
</defs>
<rect width="720" height="520" rx="8" fill="#f8f9fa"/>
<text x="360" y="24" text-anchor="middle" font-size="14" font-weight="bold" fill="#1a1a2e">Dirigent package architecture</text>
<!-- Layer: External Tools (top) -->
<rect x="20" y="40" width="680" height="70" rx="8" fill="#e8f0fe" stroke="#4285f4" stroke-width="1.5"/>
<text x="30" y="58" font-size="10" font-weight="600" fill="#4285f4">STANDALONE TOOLS</text>
<rect x="40" y="66" width="130" height="32" rx="6" fill="#fff" stroke="#4285f4"/>
<text x="105" y="86" text-anchor="middle" fill="#333" font-weight="600">fermata</text>
<rect x="200" y="66" width="130" height="32" rx="6" fill="#fff" stroke="#4285f4"/>
<text x="265" y="86" text-anchor="middle" fill="#333" font-weight="600">dirigate</text>
<rect x="360" y="66" width="130" height="32" rx="6" fill="#fff" stroke="#4285f4"/>
<text x="425" y="86" text-anchor="middle" fill="#333" font-weight="600">anth</text>
<text x="530" y="80" fill="#4285f4" font-size="10" font-style="italic">← own repos, installable</text>
<!-- Layer: Orchestration -->
<rect x="20" y="130" width="680" height="80" rx="8" fill="#fef9e7" stroke="#f0ad4e" stroke-width="1.5"/>
<text x="30" y="148" font-size="10" font-weight="600" fill="#b9770e">ORCHESTRATION</text>
<rect x="40" y="158" width="140" height="36" rx="6" fill="#fff" stroke="#f0ad4e"/>
<text x="110" y="172" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_core</text>
<text x="110" y="185" text-anchor="middle" fill="#888" font-size="9">multi-connector runtime</text>
<rect x="210" y="158" width="140" height="36" rx="6" fill="#fff" stroke="#f0ad4e"/>
<text x="280" y="172" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_acp_api</text>
<text x="280" y="185" text-anchor="middle" fill="#888" font-size="9">ACP server</text>
<rect x="380" y="158" width="140" height="36" rx="6" fill="#fff" stroke="#f0ad4e"/>
<text x="450" y="172" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_taskrunner</text>
<text x="450" y="185" text-anchor="middle" fill="#888" font-size="9">background tasks</text>
<rect x="550" y="158" width="140" height="36" rx="6" fill="#fff" stroke="#f0ad4e"/>
<text x="620" y="172" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_archivist</text>
<text x="620" y="185" text-anchor="middle" fill="#888" font-size="9">session archival</text>
<!-- Layer: Foundation -->
<rect x="20" y="230" width="680" height="80" rx="8" fill="#e8f8f0" stroke="#1e8449" stroke-width="1.5"/>
<text x="30" y="248" font-size="10" font-weight="600" fill="#1e8449">FOUNDATION</text>
<rect x="40" y="258" width="140" height="36" rx="6" fill="#fff" stroke="#1e8449"/>
<text x="110" y="272" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_protocol</text>
<text x="110" y="285" text-anchor="middle" fill="#888" font-size="9">ACP types + messages</text>
<rect x="210" y="258" width="140" height="36" rx="6" fill="#fff" stroke="#1e8449"/>
<text x="280" y="272" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_tools</text>
<text x="280" y="285" text-anchor="middle" fill="#888" font-size="9">tool sandbox</text>
<rect x="380" y="258" width="140" height="36" rx="6" fill="#fff" stroke="#1e8449"/>
<text x="450" y="272" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_config</text>
<text x="450" y="285" text-anchor="middle" fill="#888" font-size="9">configuration</text>
<rect x="550" y="258" width="140" height="36" rx="6" fill="#fff" stroke="#1e8449"/>
<text x="620" y="272" text-anchor="middle" fill="#333" font-size="10" font-weight="600">dirigent_auth</text>
<text x="620" y="285" text-anchor="middle" fill="#888" font-size="9">authorization</text>
<!-- Layer: Integrations -->
<rect x="20" y="330" width="680" height="80" rx="8" fill="#f3e8fd" stroke="#8e44ad" stroke-width="1.5"/>
<text x="30" y="348" font-size="10" font-weight="600" fill="#8e44ad">INTEGRATIONS</text>
<rect x="40" y="358" width="100" height="36" rx="6" fill="#fff" stroke="#8e44ad"/>
<text x="90" y="372" text-anchor="middle" fill="#333" font-size="10" font-weight="600">matrix</text>
<text x="90" y="385" text-anchor="middle" fill="#888" font-size="9">session sharing</text>
<rect x="160" y="358" width="100" height="36" rx="6" fill="#fff" stroke="#8e44ad"/>
<text x="210" y="372" text-anchor="middle" fill="#333" font-size="10" font-weight="600">langfuse</text>
<text x="210" y="385" text-anchor="middle" fill="#888" font-size="9">observability</text>
<rect x="280" y="358" width="100" height="36" rx="6" fill="#fff" stroke="#8e44ad"/>
<text x="330" y="372" text-anchor="middle" fill="#333" font-size="10" font-weight="600">zed</text>
<text x="330" y="385" text-anchor="middle" fill="#888" font-size="9">editor</text>
<!-- Layer: Parsers -->
<rect x="20" y="430" width="680" height="70" rx="8" fill="#fdecea" stroke="#c0392b" stroke-width="1.5" stroke-dasharray="6,3"/>
<text x="30" y="448" font-size="10" font-weight="600" fill="#c0392b">PARSERS (third-party format readers)</text>
<rect x="40" y="458" width="120" height="30" rx="6" fill="#fff" stroke="#c0392b" stroke-dasharray="4,2"/>
<text x="100" y="477" text-anchor="middle" fill="#333" font-size="10">opencode_client</text>
<rect x="180" y="458" width="120" height="30" rx="6" fill="#fff" stroke="#c0392b" stroke-dasharray="4,2"/>
<text x="240" y="477" text-anchor="middle" fill="#333" font-size="10">dirigent_chatgpt</text>
<rect x="320" y="458" width="120" height="30" rx="6" fill="#fff" stroke="#c0392b" stroke-dasharray="4,2"/>
<text x="380" y="477" text-anchor="middle" fill="#333" font-size="10">dirigent_codex</text>
<rect x="460" y="458" width="120" height="30" rx="6" fill="#fff" stroke="#c0392b" stroke-dasharray="4,2"/>
<text x="520" y="477" text-anchor="middle" fill="#333" font-size="10">dirigent_inspector</text>
<!-- Dependency arrows -->
<line x1="105" y1="98" x2="110" y2="158" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="265" y1="98" x2="110" y2="158" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="265" y1="98" x2="280" y2="158" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="110" y1="194" x2="110" y2="258" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="110" y1="194" x2="280" y2="258" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="280" y1="194" x2="110" y2="258" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
<line x1="450" y1="194" x2="450" y2="258" stroke="#666" stroke-width="1" marker-end="url(#arr)"/>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

+9 -9
View File
@@ -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*"]
```
```
+13 -7
View File
@@ -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<PathBuf> {
let start = if target.is_file() {
target.parent()?
@@ -14,14 +16,18 @@ pub fn find_project_root(target: &Path) -> Option<PathBuf> {
target
};
let mut fallback: Option<PathBuf> = 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
}
+55 -4
View File
@@ -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();