Skip to content
Open
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
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ serde_json = "1"
toml = "0.8"
thiserror = "2"
unicode-normalization = "0.1"
unicode-security = "0.1"
async-trait = "0.1"
url = { version = "2", features = ["serde"] }
secrecy = { version = "0.8", features = ["serde"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2349,7 +2349,7 @@ fn make_publication_intent(
archive_hash: make_hash(hash_seed),
manifest_hash: make_hash(hash_seed.wrapping_add(1)),
file_inventory_hash: make_hash(hash_seed.wrapping_add(2)),
scan_schema_version: 1,
scan_schema_version: frameshift_publication::REPORT_SCHEMA_VERSION,
created_at,
expires_at,
consumed_at: None,
Expand Down
64 changes: 59 additions & 5 deletions crates/frameshift-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ enum Command {
/// Install from a local pack directory instead of the registry.
#[arg(long, value_name = "PATH")]
from_path: Option<PathBuf>,
/// Bypass final prompt-content policy for this explicitly local pack.
#[arg(long, requires = "from_path")]
trust_local_prompt_content: bool,
},

/// Activate an installed persona for this project.
Expand Down Expand Up @@ -281,13 +284,23 @@ fn run() -> Result<(), RunError> {
// ------------------------------------------------------------------
// M0 -- install
// ------------------------------------------------------------------
Command::Install { spec, from_path } => {
Command::Install {
spec,
from_path,
trust_local_prompt_content,
} => {
let client = make_client()?;
let (name, version) =
PersonaSpec::parse_loose(&spec).map_err(|e| RunError::General(e.to_string()))?;
let source = match from_path {
Some(path) => InstallSource::LocalPath(path),
None => InstallSource::Registry,
let source = match (from_path, trust_local_prompt_content) {
(Some(path), true) => {
eprintln!(
"warning: trusted-local prompt bypass enabled; final persona instructions will not be content-policy checked"
);
InstallSource::TrustedLocalPath(path)
}
(Some(path), false) => InstallSource::LocalPath(path),
(None, _) => InstallSource::Registry,
};
// A bare name (no `@version`) resolves to the registry's latest
// published version; local-path installs require an explicit
Expand All @@ -297,7 +310,7 @@ fn run() -> Result<(), RunError> {
(None, InstallSource::Registry) => client
.resolve_latest_version(&name)
.map_err(|e| RunError::General(e.to_string()))?,
(None, InstallSource::LocalPath(_)) => {
(None, InstallSource::LocalPath(_) | InstallSource::TrustedLocalPath(_)) => {
return Err(RunError::General(
"local installs require an explicit version".to_string(),
));
Expand Down Expand Up @@ -590,6 +603,47 @@ fn conformance_upgrade_warning(
}
}

/// CLI parsing regressions for explicit trusted-local prompt posture.
#[cfg(test)]
mod install_cli_tests {
use super::*;

/// Trusted-local prompt bypass cannot be selected without a local path.
#[test]
fn trusted_local_prompt_bypass_requires_path() {
let result = Cli::try_parse_from([
"frameshift",
"install",
"fixture@0.1.0",
"--trust-local-prompt-content",
]);

assert!(result.is_err());
}

/// Trusted-local prompt bypass parses only alongside an explicit local path.
#[test]
fn trusted_local_prompt_bypass_accepts_path() {
let cli = Cli::try_parse_from([
"frameshift",
"install",
"fixture@0.1.0",
"--from-path",
"/tmp/fixture",
"--trust-local-prompt-content",
])
.expect("trusted-local arguments should parse");

assert!(matches!(
cli.command,
Command::Install {
trust_local_prompt_content: true,
..
}
));
}
}

/// CLI parsing regressions for the human-reviewed publication surface.
#[cfg(test)]
mod publication_cli_tests {
Expand Down
43 changes: 43 additions & 0 deletions crates/frameshift-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,49 @@ pub enum ClientError {
#[error("no renderable markdown entry found in pack at {0}")]
MissingRenderSource(PathBuf),

/// The exact final rendered prompt failed the current deterministic policy.
#[error(
"persona {persona:?} failed rendered-prompt policy v{policy_version} with codes {codes:?}; inspect the pack (only explicitly trusted local-path installs can bypass this policy)"
)]
PromptPolicyViolation {
/// Persona whose final rendered output was rejected.
persona: String,
/// Exact deterministic policy version used for the decision.
policy_version: u32,
/// Sorted stable finding codes with no matched prompt excerpts.
codes: Vec<String>,
},

/// A staged persona could not be installed and its deterministic prior
/// state could not be restored to the canonical destination.
#[error(
"failed to replace materialized persona at {destination}: {install_error}; rollback also failed: {rollback_error}; recovery artifact remains at {backup}"
)]
MaterializationRollbackFailed {
/// Canonical persona directory that could not be replaced or restored.
destination: PathBuf,
/// Same-filesystem artifact containing a last-good tree or absence marker.
backup: PathBuf,
/// Failure returned while moving the validated staged tree into place.
install_error: std::io::Error,
/// Failure returned while restoring the last-good tree.
rollback_error: std::io::Error,
},

/// Recovery found both an interrupted backup and state that could not be
/// reconciled with the persisted lock hash.
#[error(
"cannot safely recover interrupted materialization for persona {persona:?}; preserved destination {destination} and backup {backup} for inspection"
)]
MaterializationRecoveryAmbiguous {
/// Persona whose interrupted transaction could not be resolved.
persona: String,
/// Canonical destination retained without destructive guessing.
destination: PathBuf,
/// Deterministic last-good backup retained without destructive guessing.
backup: PathBuf,
},

#[error("persona {0:?} is not present in frameshift.lock")]
PersonaNotInstalled(String),

Expand Down
Loading
Loading