Skip to content
Closed
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
1 change: 1 addition & 0 deletions crates/webcodex-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod job_observation;
pub mod lsp_bridge;
pub mod mcp_gateway;
pub mod memory_contract;
pub mod model_reference;
pub mod plugin;
pub mod project_context_contract;
pub mod project_instructions;
Expand Down
77 changes: 77 additions & 0 deletions crates/webcodex-core/src/model_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Typed, model-facing short-reference syntax.
//!
//! Short refs are selectors only. Persistence, authority, lifecycle and
//! canonical identity remain owned by the target domain.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModelReferenceKind {
Project,
Session,
}

impl ModelReferenceKind {
pub const fn prefix(self) -> &'static str {
match self {
Self::Project => "~p",
Self::Session => "~s",
}
}
}

pub fn parse_model_reference(raw: &str, kind: ModelReferenceKind) -> Option<Result<u64, ()>> {
let digits = raw.strip_prefix(kind.prefix())?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Some(Err(()));
}
if digits.len() > 1 && digits.starts_with('0') {
return Some(Err(()));
}
Some(
digits
.parse::<u64>()
.ok()
.filter(|index| *index > 0)
.ok_or(()),
)
}

pub fn format_model_reference(kind: ModelReferenceKind, ref_index: u64) -> String {
debug_assert!(ref_index > 0);
format!("{}{ref_index}", kind.prefix())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn typed_refs_have_disjoint_strict_syntax() {
assert_eq!(
parse_model_reference("~p12", ModelReferenceKind::Project),
Some(Ok(12))
);
assert_eq!(
parse_model_reference("~s7", ModelReferenceKind::Session),
Some(Ok(7))
);
assert_eq!(
parse_model_reference("~s7", ModelReferenceKind::Project),
None
);
for invalid in ["~s", "~s0", "~s01", "~s-1", "~s1x"] {
assert_eq!(
parse_model_reference(invalid, ModelReferenceKind::Session),
Some(Err(())),
"{invalid}"
);
}
assert_eq!(
format_model_reference(ModelReferenceKind::Project, 3),
"~p3"
);
assert_eq!(
format_model_reference(ModelReferenceKind::Session, 3),
"~s3"
);
}
}
5 changes: 4 additions & 1 deletion crates/webcodex-store/src/connection_observation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub(crate) enum StoreDomain {
JobReceipts,
JobTerminalWait,
Memory,
ModelReference,
OAuth,
ProjectReference,
Schema,
Expand All @@ -31,7 +32,7 @@ pub(crate) enum StoreDomain {

impl StoreDomain {
#[cfg(test)]
pub(crate) const ALL: [Self; 17] = [
pub(crate) const ALL: [Self; 18] = [
Self::Accounts,
Self::Activity,
Self::AdminProjectLifecycle,
Expand All @@ -45,6 +46,7 @@ impl StoreDomain {
Self::JobReceipts,
Self::JobTerminalWait,
Self::Memory,
Self::ModelReference,
Self::OAuth,
Self::ProjectReference,
Self::Schema,
Expand All @@ -66,6 +68,7 @@ impl StoreDomain {
Self::JobReceipts => "job_receipts",
Self::JobTerminalWait => "job_terminal_wait",
Self::Memory => "memory",
Self::ModelReference => "model_reference",
Self::OAuth => "oauth",
Self::ProjectReference => "project_reference",
Self::Schema => "schema",
Expand Down
1 change: 1 addition & 0 deletions crates/webcodex-store/src/db_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ fn store_connection_domains_and_metric_names_are_closed_and_stable() {
"job_receipts",
"job_terminal_wait",
"memory",
"model_reference",
"oauth",
"project_reference",
"schema",
Expand Down
4 changes: 4 additions & 0 deletions crates/webcodex-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod job_terminal_wait;
#[cfg(test)]
mod job_terminal_wait_tests;
mod memory;
mod model_reference;
pub mod models;
mod oauth;
mod peer_collaboration;
Expand Down Expand Up @@ -111,6 +112,7 @@ pub use self::memory::{
memory_definition_hash, memory_state_revision, validate_memory_body, validate_memory_key,
validate_memory_summary, MAX_MEMORIES_PER_PROJECT, MEMORY_SCOPE_IDENTITY_ATTRIBUTED,
};
pub use self::model_reference::{ModelReferenceRecord, ModelReferenceStoreError};
pub use self::oauth::RotateResult;
pub use self::peer_collaboration::{
NewPeerMessage, PeerAttentionBatch, PeerMessageDelivery, PeerMessageDeliveryOutcome,
Expand Down Expand Up @@ -182,6 +184,8 @@ mod goal_tests;
#[cfg(test)]
mod memory_tests;
#[cfg(test)]
mod model_reference_tests;
#[cfg(test)]
mod project_reference_tests;

#[cfg(test)]
Expand Down
207 changes: 207 additions & 0 deletions crates/webcodex-store/src/model_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
use super::{Database, ProjectReferenceStoreError, StoreDomain};
use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
use webcodex_core::{
model_reference::ModelReferenceKind, workflow_session_contract::is_valid_session_id,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelReferenceRecord {
pub ref_index: u64,
pub canonical_id: String,
pub incarnation_fingerprint: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelReferenceStoreError {
InvalidInput,
Unavailable,
}

impl std::fmt::Display for ModelReferenceStoreError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidInput => "invalid model reference input",
Self::Unavailable => "model reference store unavailable",
})
}
}

impl std::error::Error for ModelReferenceStoreError {}

impl From<ProjectReferenceStoreError> for ModelReferenceStoreError {
fn from(error: ProjectReferenceStoreError) -> Self {
match error {
ProjectReferenceStoreError::InvalidInput => Self::InvalidInput,
ProjectReferenceStoreError::Unavailable => Self::Unavailable,
}
}
}

fn store_error(error: rusqlite::Error) -> ModelReferenceStoreError {
tracing::warn!(error = %error, "durable model reference store operation failed");
ModelReferenceStoreError::Unavailable
}

fn valid_session_input(
principal_key: &str,
canonical_session_id: &str,
incarnation_fingerprint: &str,
) -> bool {
!principal_key.is_empty()
&& principal_key.len() <= 128
&& is_valid_session_id(canonical_session_id)
&& incarnation_fingerprint.len() == 64
&& incarnation_fingerprint
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
}

fn project_record(record: super::ProjectReferenceRecord) -> ModelReferenceRecord {
ModelReferenceRecord {
ref_index: record.ref_index,
canonical_id: record.canonical_project_id,
incarnation_fingerprint: record.root_fingerprint,
}
}

impl Database {
pub(super) fn ensure_model_reference_schema(conn: &mut Connection) -> anyhow::Result<()> {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS session_references (
principal_key TEXT NOT NULL,
ref_index INTEGER NOT NULL CHECK(ref_index >= 1),
canonical_session_id TEXT NOT NULL,
incarnation_fingerprint TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY(principal_key, ref_index),
UNIQUE(principal_key, canonical_session_id, incarnation_fingerprint)
);
",
)?;
Ok(())
}

pub fn get_or_create_model_reference(
&self,
principal_key: &str,
kind: ModelReferenceKind,
canonical_id: &str,
incarnation_fingerprint: &str,
created_at: i64,
) -> Result<ModelReferenceRecord, ModelReferenceStoreError> {
if kind == ModelReferenceKind::Project {
return self
.get_or_create_project_reference(
principal_key,
canonical_id,
incarnation_fingerprint,
created_at,
)
.map(project_record)
.map_err(Into::into);
}

if !valid_session_input(principal_key, canonical_id, incarnation_fingerprint) {
return Err(ModelReferenceStoreError::InvalidInput);
}
let mut conn = self.lock_connection(StoreDomain::ModelReference);
let transaction = conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(store_error)?;
let existing = transaction
.query_row(
"SELECT ref_index, canonical_session_id, incarnation_fingerprint
FROM session_references
WHERE principal_key = ?1
AND canonical_session_id = ?2
AND incarnation_fingerprint = ?3",
params![principal_key, canonical_id, incarnation_fingerprint],
|row| {
let ref_index: i64 = row.get(0)?;
Ok(ModelReferenceRecord {
ref_index: ref_index as u64,
canonical_id: row.get(1)?,
incarnation_fingerprint: row.get(2)?,
})
},
)
.optional()
.map_err(store_error)?;
if let Some(existing) = existing {
transaction.commit().map_err(store_error)?;
return Ok(existing);
}

let next_index: i64 = transaction
.query_row(
"SELECT COALESCE(MAX(ref_index), 0) + 1
FROM session_references
WHERE principal_key = ?1",
params![principal_key],
|row| row.get(0),
)
.map_err(store_error)?;
if next_index <= 0 {
return Err(ModelReferenceStoreError::Unavailable);
}
transaction
.execute(
"INSERT INTO session_references (
principal_key, ref_index, canonical_session_id,
incarnation_fingerprint, created_at
) VALUES (?1, ?2, ?3, ?4, ?5)",
params![
principal_key,
next_index,
canonical_id,
incarnation_fingerprint,
created_at
],
)
.map_err(store_error)?;
transaction.commit().map_err(store_error)?;
Ok(ModelReferenceRecord {
ref_index: next_index as u64,
canonical_id: canonical_id.to_string(),
incarnation_fingerprint: incarnation_fingerprint.to_string(),
})
}

pub fn lookup_model_reference(
&self,
principal_key: &str,
kind: ModelReferenceKind,
ref_index: u64,
) -> Result<Option<ModelReferenceRecord>, ModelReferenceStoreError> {
if kind == ModelReferenceKind::Project {
return self
.lookup_project_reference(principal_key, ref_index)
.map(|record| record.map(project_record))
.map_err(Into::into);
}

if principal_key.is_empty() || principal_key.len() > 128 || ref_index == 0 {
return Err(ModelReferenceStoreError::InvalidInput);
}
let ref_index =
i64::try_from(ref_index).map_err(|_| ModelReferenceStoreError::InvalidInput)?;
let conn = self.lock_connection(StoreDomain::ModelReference);
conn.query_row(
"SELECT ref_index, canonical_session_id, incarnation_fingerprint
FROM session_references
WHERE principal_key = ?1 AND ref_index = ?2",
params![principal_key, ref_index],
|row| {
let ref_index: i64 = row.get(0)?;
Ok(ModelReferenceRecord {
ref_index: ref_index as u64,
canonical_id: row.get(1)?,
incarnation_fingerprint: row.get(2)?,
})
},
)
.optional()
.map_err(store_error)
}
}
Loading
Loading