Add governed directory inventory sight
This commit is contained in:
parent
bc47ab616c
commit
31dcd351a2
3 changed files with 229 additions and 21 deletions
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Reference in a new issue