Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/webcodex-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub mod model_reference;
pub mod plugin;
pub mod project_build;
pub mod project_context_contract;
pub mod project_format;
#[cfg(test)]
mod project_format_tests;
pub mod project_instructions;
pub mod project_listing;
mod project_operation;
Expand Down
293 changes: 293 additions & 0 deletions crates/webcodex-core/src/project_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
//! Closed, explicit-file project formatting plans. Source and candidate bytes
//! remain private to the Runner; plans carry bounded content witnesses only.

use crate::runner_protocol::ShellProcessArgv;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

pub const PROJECT_FORMAT_MAX_FILES: usize = 8;
pub const PROJECT_FORMAT_FILE_MAX_BYTES: usize = 64 * 1024;
pub const PROJECT_FORMAT_TOTAL_MAX_BYTES: usize = 256 * 1024;
pub const PROJECT_FORMAT_MANIFEST_MAX_BYTES: usize = 256 * 1024;
pub const PROJECT_FORMAT_PLAN_MAX_BYTES: usize = 32 * 1024;
pub const PROJECT_FORMAT_TIMEOUT_MAX_SECS: u64 = 120;

#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ProjectFormatAdapter {
#[default]
Auto,
Rust,
Python,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectFormatRequest {
pub project_id: String,
pub cwd: Option<String>,
#[serde(default)]
pub adapter: ProjectFormatAdapter,
/// Ordered paths relative to the registered Project, not the recipe cwd.
pub files: Vec<String>,
}

pub fn valid_format_relative_path(path: &str, directory: bool) -> bool {
(directory && path == ".")
|| (!path.is_empty()
&& path.len() <= 1024
&& !path.contains(['\\', ':'])
&& !path.chars().any(char::is_control)
&& path.split('/').all(|part| !matches!(part, "" | "." | "..")))
}

impl ProjectFormatRequest {
pub fn validate(&self) -> Result<(), &'static str> {
if self.project_id.is_empty()
|| self.project_id.len() > 200
|| self.project_id.chars().any(char::is_control)
|| !valid_format_relative_path(self.cwd.as_deref().unwrap_or("."), true)
|| !(1..=PROJECT_FORMAT_MAX_FILES).contains(&self.files.len())
{
return Err("invalid project format request");
}
let mut backend = None;
for (index, file) in self.files.iter().enumerate() {
if !valid_format_relative_path(file, false) || self.files[..index].contains(file) {
return Err("invalid project format files");
}
let selected = if file.ends_with(".rs") {
ProjectFormatAdapter::Rust
} else if file.ends_with(".py") || file.ends_with(".pyi") {
ProjectFormatAdapter::Python
} else {
return Err("unsupported project format extension");
};
if backend.is_some_and(|backend| backend != selected)
|| (self.adapter != ProjectFormatAdapter::Auto && self.adapter != selected)
{
return Err("project format scope mismatch");
}
backend = Some(selected);
}
Ok(())
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "backend", rename_all = "snake_case", deny_unknown_fields)]
pub enum ProjectFormatProfile {
Rust { edition: String },
Python { target_version: String },
}

impl ProjectFormatProfile {
pub fn adapter(&self) -> ProjectFormatAdapter {
match self {
Self::Rust { .. } => ProjectFormatAdapter::Rust,
Self::Python { .. } => ProjectFormatAdapter::Python,
}
}

pub fn is_valid(&self) -> bool {
match self {
Self::Rust { edition } => matches!(edition.as_str(), "2015" | "2018" | "2021" | "2024"),
Self::Python { target_version } => matches!(
target_version.as_str(),
"py37" | "py38" | "py39" | "py310" | "py311" | "py312" | "py313" | "py314"
),
}
}

/// The Runner must resolve/probe the trusted executable separately. This
/// command shape never accepts executable, flags or scripts from the model.
pub fn process(&self, recipe_relative_file: &str) -> Result<ShellProcessArgv, &'static str> {
if !self.is_valid() || !valid_format_relative_path(recipe_relative_file, false) {
return Err("invalid project format profile");
}
let (executable, args) = match self {
Self::Rust { edition } if recipe_relative_file.ends_with(".rs") => (
"rustfmt",
vec![
"--emit=stdout".into(),
format!("--edition={edition}"),
"--config".into(),
"skip_children=true".into(),
],
),
Self::Python { .. }
if recipe_relative_file.ends_with(".py")
|| recipe_relative_file.ends_with(".pyi") =>
{
(
"python",
vec![
"-I",
"-B",
"-m",
"ruff",
"format",
"--no-cache",
"--config",
"pyproject.toml",
"--stdin-filename",
recipe_relative_file,
"-",
]
.into_iter()
.map(str::to_string)
.collect(),
)
}
_ => return Err("project format scope mismatch"),
};
Ok(ShellProcessArgv {
executable: executable.into(),
args,
})
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectFormatFileWitness {
pub path: String,
pub bytes: usize,
pub sha256: String,
pub identity_digest: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectFormatPlan {
pub request: ProjectFormatRequest,
pub recipe_root: String,
pub root_digest: String,
pub manifest_digest: String,
pub profile: ProjectFormatProfile,
pub files: Vec<ProjectFormatFileWitness>,
}

impl ProjectFormatPlan {
pub fn is_valid(&self) -> bool {
let digest = |s: &str| {
s.len() == 64
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
};
self.request.validate().is_ok()
&& valid_format_relative_path(&self.recipe_root, true)
&& digest(&self.root_digest)
&& digest(&self.manifest_digest)
&& self.profile.is_valid()
&& (self.request.adapter == ProjectFormatAdapter::Auto
|| self.request.adapter == self.profile.adapter())
&& self.files.len() == self.request.files.len()
&& self
.files
.iter()
.zip(&self.request.files)
.all(|(witness, path)| {
witness.path == *path
&& witness.bytes <= PROJECT_FORMAT_FILE_MAX_BYTES
&& digest(&witness.sha256)
&& digest(&witness.identity_digest)
&& self
.recipe_relative_file(path)
.is_some_and(|relative| self.profile.process(relative).is_ok())
})
&& self
.files
.iter()
.try_fold(0usize, |total, file| total.checked_add(file.bytes))
.is_some_and(|total| total <= PROJECT_FORMAT_TOTAL_MAX_BYTES)
}

pub fn recipe_relative_file<'a>(&self, path: &'a str) -> Option<&'a str> {
if self.recipe_root == "." {
Some(path)
} else {
path.strip_prefix(&self.recipe_root)?.strip_prefix('/')
}
}

pub fn digest(&self) -> Result<String, &'static str> {
if !self.is_valid() {
return Err("invalid project format plan");
}
let bytes = serde_json::to_vec(self).map_err(|_| "invalid project format plan")?;
if bytes.len() > PROJECT_FORMAT_PLAN_MAX_BYTES {
return Err("project format plan exceeds payload bound");
}
Ok(format!("{:x}", Sha256::digest(bytes)))
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
pub enum ProjectFormatPlanningResult {
Ready { plan: ProjectFormatPlan },
Unavailable { code: String },
}

/// Executor-owned evidence, never derived from process exit status. Missing
/// historical evidence is unknown, including after a crash or lost delivery.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectFormatMutationReport {
#[default]
Unknown,
Unchanged,
Changed,
}

impl ProjectFormatMutationReport {
pub fn state_changed(self) -> Option<bool> {
match self {
Self::Unknown => None,
Self::Unchanged => Some(false),
Self::Changed => Some(true),
}
}
}

/// Safe Job correlation only; never executable instructions or source bytes.
pub fn structured_metadata() -> crate::runner_protocol::ShellJobStructuredExecutionMetadata {
crate::runner_protocol::ShellJobStructuredExecutionMetadata {
execution_source: "project_format".into(),
language: None,
script_bytes: None,
arg_count: 0,
stdin_present: false,
validation_identity: None,
validation_tool: None,
assertion_name: None,
}
}

/// Missing historical receipts remain unknown. Known reports require complete
/// executor success and can never be attached to another kind of Job.
pub fn validate_mutation_report(
kind: &str,
status: &str,
exit_code: Option<i32>,
execution: Option<crate::runner_protocol::ShellCommandExecutionState>,
report: Option<ProjectFormatMutationReport>,
error: Option<&str>,
) -> Result<(), &'static str> {
if let Some(report) = report {
if kind != "project_format"
|| (report != ProjectFormatMutationReport::Unknown
&& (status != "completed"
|| exit_code != Some(0)
|| execution
!= Some(crate::runner_protocol::ShellCommandExecutionState::Completed)
|| error.is_some()))
{
return Err("format_mutation_invalid");
}
}
Ok(())
}
Loading
Loading