Add governed directory inventory sight

This commit is contained in:
Meta Vens 2026-08-11 06:56:16 +02:00
commit 31dcd351a2
3 changed files with 229 additions and 21 deletions

View file

@ -593,11 +593,11 @@ pub fn build_worker_model_request(
let mut instructions =
worker_instructions(launch_request, &launch_document, &output_schema_json);
if launch_request.binding_id == epiphany_core::EPIPHANY_VERIFICATION_ROLE_BINDING_ID {
instructions.push_str("\n\nTool mandate: before returning `needs-evidence` because source files, command artifacts, commit diffs, or Hands receipt bodies are not inspectable, call the read-only source tools available on this request. Use `mcp__epiphany_source__read_file` for cited source/artifact paths, `mcp__epiphany_source__git_show` for commit diffs, and `mcp__epiphany_source__read_hands_receipt` for Hands patch/command/commit receipts. If a tool fails, cite that failed tool result and the exact remaining blocker.");
instructions.push_str("\n\nTool mandate: before returning `needs-evidence` because source files, artifact directories, command artifacts, commit diffs, or Hands receipt bodies are not inspectable, call the read-only source tools available on this request. Use `mcp__epiphany_source__read_file` for cited source/artifact files, `mcp__epiphany_source__directory_inventory` for bounded workspace directory counts and bytes, `mcp__epiphany_source__git_show` for commit diffs, and `mcp__epiphany_source__read_hands_receipt` for Hands patch/command/commit receipts. Directory totals are authoritative only when the tool reports `complete=true`. If a tool fails, cite that failed tool result and the exact remaining blocker.");
} else if launch_request.binding_id == epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID {
instructions.push_str("\n\nTool mandate: Eyes must inspect current repository sources before emitting evidence. Call the bounded read-only file or Git tools, cite the exact inspected paths/revisions in filesInspected and evidence, and report a source gap if the required body cannot be observed.");
instructions.push_str("\n\nTool mandate: Eyes must inspect current repository sources before emitting evidence. Call the bounded read-only file, directory-inventory, or Git tools, cite the exact inspected paths/revisions in filesInspected and evidence, and report a source gap if the required body cannot be observed. Directory totals are authoritative only when the inventory reports `complete=true`.");
} else if launch_request.binding_id == epiphany_core::EPIPHANY_MODELING_ROLE_BINDING_ID {
instructions.push_str("\n\nTool mandate: Modeling must inspect current repository sources before proposing repository anatomy. Call the bounded read-only file or Git tools, cite the exact inspected paths/revisions in filesInspected and evidence, and emit regather-needed instead of inventing unobserved structure.");
instructions.push_str("\n\nTool mandate: Modeling must inspect current repository sources before proposing repository anatomy. Call the bounded read-only file, directory-inventory, or Git tools, cite the exact inspected paths/revisions in filesInspected and evidence, and emit regather-needed instead of inventing unobserved structure. Directory totals are authoritative only when the inventory reports `complete=true`.");
}
let mut request = EpiphanyModelRequest::new(
request_id,
@ -1106,6 +1106,22 @@ fn repository_source_tools() -> Vec<EpiphanyModelToolDefinition> {
})
.to_string(),
},
EpiphanyModelToolDefinition {
name: "mcp__epiphany_source__directory_inventory".to_string(),
description: "Measure a workspace-confined directory with deterministic bounded counts, regular-file bytes, and path samples. Totals are usable as complete evidence only when complete=true; symlinks are counted but never followed.".to_string(),
parameters_json: serde_json::json!({
"type": "object",
"additionalProperties": false,
"properties": {
"path": {"type": "string"},
"maxDepth": {"type": "integer", "minimum": 0, "maximum": 8},
"maxEntries": {"type": "integer", "minimum": 1, "maximum": 4096},
"maxSamples": {"type": "integer", "minimum": 1, "maximum": 100}
},
"required": ["path"]
})
.to_string(),
},
EpiphanyModelToolDefinition {
name: "mcp__epiphany_source__git_show".to_string(),
description: "Read a bounded git show/diff preview for a commit or revision in the current workspace.".to_string(),
@ -2822,6 +2838,10 @@ mod tests {
.tools
.iter()
.any(|tool| tool.name == "mcp__epiphany_source__read_file"));
assert!(model_request
.tools
.iter()
.any(|tool| tool.name == "mcp__epiphany_source__directory_inventory"));
assert!(model_request.instructions.contains("Modeling must inspect"));
let openai_summary = EpiphanyOpenAiRuntimeRunSummary {
store: store.display().to_string(),
@ -2970,11 +2990,15 @@ mod tests {
.collect::<Vec<_>>();
assert!(tool_names.contains(&"mcp__epiphany_source__read_file"));
assert!(tool_names.contains(&"mcp__epiphany_source__directory_inventory"));
assert!(tool_names.contains(&"mcp__epiphany_source__git_show"));
assert!(tool_names.contains(&"mcp__epiphany_source__read_hands_receipt"));
assert!(model_request
.instructions
.contains("mcp__epiphany_source__read_file"));
assert!(model_request
.instructions
.contains("mcp__epiphany_source__directory_inventory"));
Ok(())
}

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Result, anyhow};
use epiphany_tool_adapter::EpiphanyToolInvocationIntent;
use serde_json::{Value, json};
use std::fs::File;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
@ -19,12 +19,114 @@ pub fn execute_epiphany_source(
}
match intent.tool_name.as_str() {
"read_file" => read_file(cwd, &arguments),
"directory_inventory" => directory_inventory(cwd, &arguments),
"git_show" => git_show(cwd, &arguments),
"read_hands_receipt" => read_hands_receipt(store, &arguments),
other => Err(anyhow!("unknown epiphany_source tool {other:?}")),
}
}
fn directory_inventory(cwd: &Path, arguments: &Value) -> Result<Value> {
let requested = required_string(arguments, "path")?;
let maximum_depth = optional_u64(arguments, "maxDepth")?
.unwrap_or(3)
.clamp(0, 8) as usize;
let maximum_entries = optional_u64(arguments, "maxEntries")?
.unwrap_or(1_024)
.clamp(1, 4_096) as usize;
let maximum_samples = optional_u64(arguments, "maxSamples")?
.unwrap_or(40)
.clamp(1, 100) as usize;
let root = confined_path(cwd, requested)?;
if !root.is_dir() {
return Err(anyhow!("directory inventory path is not a directory"));
}
let mut pending = vec![(root.clone(), 0usize)];
let mut entries = Vec::new();
let mut entry_limit_hit = false;
let mut depth_limit_hit = false;
while let Some((directory, depth)) = pending.pop() {
let mut children = fs::read_dir(&directory)
.with_context(|| format!("reading directory {}", directory.display()))?
.collect::<std::io::Result<Vec<_>>>()?;
children.sort_by_key(|entry| entry.file_name());
for child in children {
if entries.len() == maximum_entries {
entry_limit_hit = true;
break;
}
let path = child.path();
let metadata = fs::symlink_metadata(&path)
.with_context(|| format!("reading metadata for {}", path.display()))?;
let kind = if metadata.file_type().is_symlink() {
"symlink"
} else if metadata.is_dir() {
"directory"
} else if metadata.is_file() {
"file"
} else {
"other"
};
entries.push((path.clone(), kind, metadata.len()));
if kind == "directory" && depth < maximum_depth {
pending.push((path, depth + 1));
} else if kind == "directory"
&& fs::read_dir(&path)
.with_context(|| format!("testing directory depth at {}", path.display()))?
.next()
.transpose()?
.is_some()
{
depth_limit_hit = true;
}
}
if entry_limit_hit {
break;
}
pending.sort_by(|left, right| right.0.cmp(&left.0));
}
entries.sort_by(|left, right| left.0.cmp(&right.0));
let file_count = entries.iter().filter(|entry| entry.1 == "file").count();
let directory_count = entries
.iter()
.filter(|entry| entry.1 == "directory")
.count();
let symlink_count = entries.iter().filter(|entry| entry.1 == "symlink").count();
let total_file_bytes = entries
.iter()
.filter(|entry| entry.1 == "file")
.map(|entry| entry.2)
.sum::<u64>();
let sample = entries
.iter()
.take(maximum_samples)
.map(|(path, kind, bytes)| {
json!({
"path": path.strip_prefix(&root).unwrap_or(path).display().to_string(),
"kind": kind,
"bytes": if *kind == "file" { Some(*bytes) } else { None },
})
})
.collect::<Vec<_>>();
let complete = !entry_limit_hit && !depth_limit_hit;
Ok(json!({
"path": root.display().to_string(),
"maxDepth": maximum_depth,
"maxEntries": maximum_entries,
"complete": complete,
"entryLimitHit": entry_limit_hit,
"depthLimitHit": depth_limit_hit,
"entryCount": entries.len(),
"fileCount": file_count,
"directoryCount": directory_count,
"symlinkCount": symlink_count,
"totalFileBytes": total_file_bytes,
"sample": sample,
}))
}
fn read_file(cwd: &Path, arguments: &Value) -> Result<Value> {
let requested = required_string(arguments, "path")?;
let start = optional_u64(arguments, "startLine")?.unwrap_or(1).max(1) as usize;
@ -230,4 +332,71 @@ mod tests {
assert_eq!(read_retained(&b"abcdef"[..], 3)?, "abc\n...<truncated>");
Ok(())
}
#[test]
fn inventories_workspace_directory_with_deterministic_totals() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("artifacts"))?;
fs::create_dir(dir.path().join("artifacts/pulse-2"))?;
fs::create_dir(dir.path().join("artifacts/pulse-1"))?;
fs::write(dir.path().join("artifacts/pulse-1/a.txt"), b"awake")?;
fs::write(dir.path().join("artifacts/pulse-2/b.txt"), b"machine")?;
let value = execute_epiphany_source(
&intent(
"directory_inventory",
r#"{"path":"artifacts","maxDepth":2,"maxEntries":10,"maxSamples":10}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(value["complete"], true);
assert_eq!(value["entryCount"], 4);
assert_eq!(value["directoryCount"], 2);
assert_eq!(value["fileCount"], 2);
assert_eq!(value["totalFileBytes"], 12);
assert_eq!(value["sample"][0]["path"], "pulse-1");
assert!(
execute_epiphany_source(
&intent("directory_inventory", r#"{"path":"../escape"}"#),
dir.path(),
dir.path(),
)
.is_err()
);
Ok(())
}
#[test]
fn directory_inventory_reports_truncation_instead_of_partial_truth() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("artifacts"))?;
for name in ["a", "b", "c"] {
fs::write(dir.path().join("artifacts").join(name), name)?;
}
let value = execute_epiphany_source(
&intent(
"directory_inventory",
r#"{"path":"artifacts","maxEntries":2}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(value["complete"], false);
assert_eq!(value["entryCount"], 2);
fs::create_dir(dir.path().join("nested"))?;
fs::create_dir(dir.path().join("nested/child"))?;
fs::write(dir.path().join("nested/child/body"), b"hidden by depth")?;
let depth_limited = execute_epiphany_source(
&intent(
"directory_inventory",
r#"{"path":"nested","maxDepth":0,"maxEntries":10}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(depth_limited["complete"], false);
assert_eq!(depth_limited["depthLimitHit"], true);
Ok(())
}
}

View file

@ -4,24 +4,39 @@ Disposable working memory for one bounded rite.
## Current Subgoal
Resume autonomous cross-organ turnover from copied c006 state now that release
iteration is no longer the active machine fault.
Give Eyes, Modeling, and Soul a governed read-only directory inventory so they
can source-ground artifact-retention claims instead of trusting operator prose.
## Accepted boundary
## Live finding
Exact `aa841873` makes legacy coordinator repair one full-snapshot transaction.
The full core suite passes 670 tests with one intentional ignore. Its genuine
core-source package completed in 37.27s wall/Cargo 29.54s versus the 5m02s
baseline; identical replay took 8.65s wall/Cargo 0.69s. Both produced exact
24-binary release
`sha256-dd4f05cb2e7f1b26156e7c9d5ed58f6d97ce5808d123b1bec343bfac59bf2ce7`
and witness
`sha256-793b29fcb7af4f8314a46f0fe0013cc8f5f382e7490a32c160385d29359adca5`.
Exact `aa841873` on copied c006 accepted one fresh user proposal and issued one
proposal-bound Modeling grant. Self held `awaiting-fulfillment`, then completed
once at revision 46 with zero retry. Runtime ended at 48 jobs/results, zero
open jobs, and no Hands/Eyes frontier. Modeling used five governed source calls
and correctly refused the operator-supplied `72 directories / 4.2 MiB` claim:
the native source adapter could read files and Git objects but could not
enumerate or measure `.epiphany-smoke/heartbeat-c006-c9a2a979`. The copied brake
was re-engaged and both direct PID containers exited zero. Actual c006 was not
touched.
## Next boundary
## Authority map
Keep actual c006 alive and cognitively braked. Inspect operator-safe current
frontier state, then choose a copied-state turn that extends Self, Eyes,
Modeling, or Mind coordination without minting Hands, Persona, or deployment
authority. Separately recheck the Yggdrasil credential boundary before any
public Persona consequence. Yggdrasil never builds.
- Owner: the native `epiphany_source` adapter owns bounded read-only workspace
observation.
- Inputs: one canonical workspace-confined directory plus explicit depth,
entry, and sample ceilings.
- Outputs: deterministic entry/file/directory/symlink counts, total regular-file
bytes, bounded samples, and explicit entry/depth truncation state.
- Derived state: samples are display only. Totals are admissible evidence only
when `complete=true`.
- Forbidden writers: shell execution, symlink traversal, outside-workspace
reads, mutation, operator counts, and Heartbeat-specific policy.
- Shared paths: Eyes, Modeling, and Soul receive the same model tool definition,
native execution path, and typed intent/receipt boundary.
- Cut line: add one general `directory_inventory` beside `read_file`; do not
add a special Heartbeat inspector or teach prompts to trust proposal prose.
- Verification: focused native execution tests must prove exact totals,
deterministic ordering, confinement, and explicit truncation; OpenAI request
tests must prove the tool and completeness mandate are visible to workers.
Then package and replay the copied proposal with the original operator counts
omitted so Epiphany itself measures the artifact family.