use std::collections::HashSet;
use std::path::Path;
use anyhow::Result;
use rusqlite::Connection;
use sha2::{Digest, Sha256};
use super::discovery::DiscoveredFile;
use super::parser::{parse_rollout_summary, CodexRecord, FORMAT_ID, FORMAT_VERSION};
pub(super) const SOURCE_KIND: &str = "codex_native";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Classification {
PendingReview,
Quarantined,
Dedup,
}
impl Classification {
fn as_str(self) -> &'static str {
match self {
Self::PendingReview => "pending_review",
Self::Quarantined => "quarantined",
Self::Dedup => "dedup",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum DestinationRoute {
Project(String),
ToolOwned,
}
impl DestinationRoute {
pub(super) fn key(&self) -> String {
match self {
Self::Project(project) => format!("project:{project}"),
Self::ToolOwned => "tool:codex-cli".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub(super) struct PlanEntry {
pub record: CodexRecord,
pub identity: String,
pub classification: Classification,
pub route: DestinationRoute,
}
#[derive(Debug)]
pub(super) struct ImportPlan {
pub entries: Vec<PlanEntry>,
pub secret_blocked: usize,
pub secret_blocked_files: Vec<String>,
pub plan_digest: String,
}
impl ImportPlan {
pub(super) fn source_state(&self) -> &'static str {
if self.secret_blocked > 0 {
"blocked"
} else {
"ready"
}
}
pub(super) fn format_versions(&self) -> Vec<String> {
vec![format!("{FORMAT_ID}/{FORMAT_VERSION}")]
}
pub(super) fn file_ids(&self) -> Vec<String> {
if self.secret_blocked > 0 {
return self.secret_blocked_files.clone();
}
self.entries
.iter()
.map(|entry| entry.record.rel_id.clone())
.collect()
}
pub(super) fn planned_import(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.classification == Classification::PendingReview)
.count()
}
pub(super) fn dedup(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.classification == Classification::Dedup)
.count()
}
pub(super) fn quarantine(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.classification == Classification::Quarantined)
.count()
}
}
pub(super) fn build_plan(conn: &Connection, files: &[DiscoveredFile]) -> Result<ImportPlan> {
let mut records = Vec::new();
for file in files {
records.push(parse_rollout_summary(&file.rel_id, &file.content)?);
}
let mut secret_blocked_files = Vec::new();
for record in &records {
if crate::adapter::redaction::redact_sensitive_text(record.body.as_str()) != record.body {
secret_blocked_files.push(record.rel_id.clone());
}
}
if !secret_blocked_files.is_empty() {
return Ok(ImportPlan {
entries: Vec::new(),
secret_blocked: secret_blocked_files.len(),
secret_blocked_files,
plan_digest: String::new(),
});
}
let mut entries = Vec::new();
let mut seen_identities: HashSet<String> = HashSet::new();
for record in records {
let route = resolve_route(&record.cwd);
let identity = record_identity(&record.body, &route);
let topic_key = topic_key_for(&identity);
let quarantine_match = crate::memory::poisoning::scan_instruction_pattern(&record.body);
let classification = if seen_identities.contains(&identity) {
Classification::Dedup
} else {
let (source_project, owner_scope, owner_key, target_project) = match &route {
DestinationRoute::Project(project) => (
project.as_str(),
"repo",
project.as_str(),
Some(project.as_str()),
),
DestinationRoute::ToolOwned => ("tool:codex-cli", "tool", "codex-cli", None),
};
match crate::memory_candidate::route::external_candidate_disposition(
conn,
&crate::memory_candidate::route::ExternalCandidateIdentity {
source_kind: SOURCE_KIND,
memory_type: "discovery",
semantic_discriminator_sha256: None,
source_project,
owner_scope,
owner_key,
target_project,
topic_key: &topic_key,
text: &record.body,
},
quarantine_match,
)? {
crate::memory_candidate::route::ExternalCandidateDisposition::PendingReview => {
Classification::PendingReview
}
crate::memory_candidate::route::ExternalCandidateDisposition::Quarantined => {
Classification::Quarantined
}
crate::memory_candidate::route::ExternalCandidateDisposition::Duplicate => {
Classification::Dedup
}
}
};
seen_identities.insert(identity.clone());
entries.push(PlanEntry {
record,
identity,
classification,
route,
});
}
let plan_digest = digest_entries(&entries);
Ok(ImportPlan {
entries,
secret_blocked: 0,
secret_blocked_files: Vec::new(),
plan_digest,
})
}
fn resolve_route(record_cwd: &str) -> DestinationRoute {
let path = Path::new(record_cwd);
if path.is_absolute() && path.is_dir() {
DestinationRoute::Project(crate::db::project_from_cwd(record_cwd))
} else {
DestinationRoute::ToolOwned
}
}
fn record_identity(body: &str, route: &DestinationRoute) -> String {
let mut hasher = Sha256::new();
hasher.update(FORMAT_ID.as_bytes());
hasher.update([0]);
hasher.update(FORMAT_VERSION.as_bytes());
hasher.update([0]);
hasher.update(body.as_bytes());
hasher.update([0]);
hasher.update(route.key().as_bytes());
format!("{:x}", hasher.finalize())
}
pub(super) fn topic_key_for(identity: &str) -> String {
format!("codex-native-{}", &identity[..identity.len().min(32)])
}
fn digest_entries(entries: &[PlanEntry]) -> String {
let mut hasher = Sha256::new();
for entry in entries {
hasher.update(entry.record.rel_id.as_bytes());
hasher.update(b"\t");
hasher.update(entry.identity.as_bytes());
hasher.update(b"\t");
hasher.update(entry.classification.as_str().as_bytes());
hasher.update(b"\t");
hasher.update(entry.route.key().as_bytes());
hasher.update(b"\n");
}
format!("{:x}", hasher.finalize())
}