Skip to content
Merged
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
261 changes: 178 additions & 83 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ documentation = "https://docs.rs/nvisy-server"
#
# See for more details: https://github.com/rust-lang/cargo/issues/11329

# Elide crates
elide-pipeline = { git = "https://github.com/nvisycom/elide-runtime", branch = "main", default-features = false }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Internal crates
nvisy-engine = { git = "https://github.com/nvisycom/runtime", branch = "main" }
nvisy-core = { path = "./crates/nvisy-core", version = "0.1.0" }
nvisy-nats = { path = "./crates/nvisy-nats", version = "0.1.0" }
nvisy-inference = { path = "./crates/nvisy-inference", version = "0.1.0" }
Expand Down
4 changes: 2 additions & 2 deletions crates/nvisy-postgres/src/model/workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use crate::types::{Handle, HasCreatedAt, HasDeletedAt, HasUpdatedAt};

/// Workspace policy representing a structured redaction governance policy.
///
/// The `definition` holds a `nvisy_schema` Policy (rules, labels, fallback,
/// retention) that the redaction engine consumes.
/// The `definition` holds an `elide-governance` `PolicyDefinition` (rules,
/// labels, fallback, retention) that the redaction engine consumes.
#[derive(Debug, Clone, PartialEq, Queryable, Selectable)]
#[diesel(table_name = workspace_policies)]
#[diesel(check_for_backend(diesel::pg::Pg))]
Expand Down
10 changes: 9 additions & 1 deletion crates/nvisy-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,16 @@ default = []
cli = ["dep:clap", "dep:humantime"]

[dependencies]
# Elide crates. The workspace dep disables default features, so re-enable every
# modality (all-modalities: tabular/image/audio/document) plus JSON/CSV audit
# export. elide-pipeline re-exports every engine type this crate uses.
elide-pipeline = { workspace = true, features = [
"all-modalities",
"audit-json",
"audit-csv",
] }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Internal crates
nvisy-engine = { workspace = true, features = ["audit-json", "audit-csv"] }
nvisy-core = { workspace = true, features = ["schema"] }
nvisy-nats = { workspace = true, features = [] }
nvisy-inference = { workspace = true, features = ["schema"] }
Expand Down
6 changes: 3 additions & 3 deletions crates/nvisy-server/src/handler/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
//! target) and the recognizers the engine has registered. Both are
//! deployment-owned reference data, not persisted rows: labels come from the
//! runtime's built-in [`LabelCatalog`], recognizers from the configured
//! [`Engine`](nvisy_engine::Engine) lineup.
//! [`Engine`](elide_pipeline::Engine) lineup.

use aide::axum::ApiRouter;
use aide::transform::TransformOperation;
use axum::extract::State;
use nvisy_engine::entity::LabelCatalog;
use elide_pipeline::entity::LabelCatalog;

use crate::extract::{AuthState, Json};
use crate::handler::response::{ErrorResponse, RecognizerCatalog};
Expand Down Expand Up @@ -69,7 +69,7 @@ pub fn routes() -> ApiRouter<ServiceState> {

#[cfg(test)]
mod tests {
use nvisy_engine::entity::LabelCatalog;
use elide_pipeline::entity::LabelCatalog;

#[test]
fn builtin_labels_are_non_empty() {
Expand Down
25 changes: 17 additions & 8 deletions crates/nvisy-server/src/handler/error/engine_error.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
//! Redaction-engine error to HTTP error conversion.
//!
//! Maps `nvisy_engine::Error` onto an HTTP error. An analyze/anonymize failure
//! is a server-side processing fault, so it surfaces as an internal error with
//! the engine's own message as context.
//! Maps `elide_pipeline::Error` onto an HTTP error by its kind: a
//! `MalformedInput` is a bad document the caller supplied (a client error),
//! while every other kind — including `CapabilityUnavailable` (a codec/renderer
//! the build does not ship) — is a server-side processing fault. The engine's
//! own message travels along as context.

use elide_pipeline::ErrorKind as EngineErrorKind;

use super::http_error::{Error as HttpError, ErrorKind};

impl<'a> From<nvisy_engine::Error> for HttpError<'a> {
fn from(error: nvisy_engine::Error) -> Self {
ErrorKind::InternalServerError
.with_message("Redaction engine failed")
.with_context(error.to_string())
impl<'a> From<elide_pipeline::Error> for HttpError<'a> {
fn from(error: elide_pipeline::Error) -> Self {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
match error.kind() {
EngineErrorKind::MalformedInput => ErrorKind::BadRequest
.with_message("Document could not be processed")
.with_context(error.to_string()),
_ => ErrorKind::InternalServerError
.with_message("Redaction engine failed")
.with_context(error.to_string()),
}
}
}
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/pipeline_audits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use axum::body::Body;
use axum::extract::State;
use axum::http::header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use nvisy_engine::Audit;
use elide_pipeline::Audit;
use nvisy_postgres::PgClient;
use zip::write::SimpleFileOptions;

Expand Down
21 changes: 15 additions & 6 deletions crates/nvisy-server/src/handler/policies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use aide::axum::ApiRouter;
use aide::transform::TransformOperation;
use axum::extract::State;
use axum::http::StatusCode;
use elide_pipeline::policy::PolicyDefinition;
use nvisy_postgres::model::{NewWorkspacePolicy, UpdateWorkspacePolicy, WorkspacePolicy};
use nvisy_postgres::query::WorkspacePolicyRepository;
use nvisy_postgres::types::WithAccountRef;
Expand Down Expand Up @@ -56,10 +57,9 @@ async fn create_policy(
.authorize_workspace(&mut conn, workspace.id, Permission::ManagePolicies)
.await?;

// Resolve the body (inline or a built-in template) and give it a fresh id so
// policies from one template stay independent.
let mut definition = request.body.into_definition();
definition.id = Uuid::now_v7();
// Resolve the body (inline or a built-in template). `into_definition` mints a
// fresh id and stamps the template origin (server-owned).
let definition = request.body.into_definition();

let display_name = request
.display_name
Expand Down Expand Up @@ -249,8 +249,17 @@ async fn update_policy(
.await?
.item;

let definition = match &request.definition {
Some(definition) => Some(crypto.encrypt_json(workspace.id, definition)?),
// A replaced body keeps the policy's server-owned template origin: the caller
// authored new rules, but where the policy came from is provenance the client
// cannot set or clear. Carry the stored origin forward onto the new draft.
let definition = match request.definition {
Some(draft) => {
let template = crypto
.decrypt_json::<PolicyDefinition>(workspace.id, &existing.definition)?
.template;
let definition = draft.into_definition(template);
Some(crypto.encrypt_json(workspace.id, &definition)?)
}
None => None,
};

Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/request/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::borrow::Cow;
use std::collections::BTreeSet;

use derive_more::{AsRef, Into};
use nvisy_engine::FormatRegistry;
use elide_pipeline::FormatRegistry;
use nvisy_postgres::model::UpdateWorkspaceFile as UpdateFileModel;
use nvisy_postgres::types::FileFilter;
use schemars::{JsonSchema, Schema, SchemaGenerator};
Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/request/pipeline_runs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Pipeline run request types (detect).

use nvisy_engine::plan::ScopeParams;
use elide_pipeline::plan::ScopeParams;
use nvisy_postgres::types::{PipelineRunStatus, PipelineTriggerType, RunFilter};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/request/pipelines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! creation, updates, and filtering. All request types support JSON serialization
//! and validation.

use nvisy_engine::plan::ScopeParams;
use elide_pipeline::plan::ScopeParams;
use nvisy_postgres::model::{NewWorkspacePipeline, UpdateWorkspacePipeline as UpdatePipelineModel};
use nvisy_postgres::types::{Handle, Json, PipelineMetadata, PipelineStatus, RetentionOverride};
use schemars::JsonSchema;
Expand Down
86 changes: 74 additions & 12 deletions crates/nvisy-server/src/handler/request/policies.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Policy request types.

use nvisy_engine::policy::PolicyDefinition;
use nvisy_engine::template::PolicyTemplate;
use elide_pipeline::policy::redaction::ModalityRedactions;
use elide_pipeline::policy::{LabelGroup, Labels, PolicyDefinition, PolicyRule, TemplateOrigin};
use elide_pipeline::template::PolicyTemplate;
use nvisy_postgres::types::Handle;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use validator::Validate;

/// Path parameters for policy operations.
Expand All @@ -21,39 +23,97 @@ pub struct PolicyPathParams {
pub policy_slug: String,
}

/// A client-authored policy body: the parts of a policy definition a caller may
/// set, without the fields the server owns.
///
/// The engine's `PolicyDefinition` also carries an `id` and a `template` origin.
/// Both are server-owned — the `id` is minted at creation and the `template`
/// records which built-in a policy was seeded from (provenance). Neither is
/// representable here, so a client cannot mint ids or forge provenance; the
/// server stamps them in [`into_definition`](PolicyDraft::into_definition).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PolicyDraft {
/// Human-readable name. Display-only.
pub name: String,
/// Optional description for reviewers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Vocabulary the policy operates over: builtins picked by name plus
/// caller-authored custom label schemas.
#[serde(default, skip_serializing_if = "Labels::is_empty")]
pub labels: Labels,
/// Named clusters of labels this policy's rules may reference by name.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<LabelGroup>,
/// Ordered rules. First match wins within this policy.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<PolicyRule>,
/// Per-policy catch-all, fired when no rule matched.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<ModalityRedactions>,
}

impl PolicyDraft {
/// Builds a full engine [`PolicyDefinition`] from this draft, stamping the
/// server-owned fields: a fresh `id`, and the given `template` origin
/// (`None` for a hand-authored body, the built-in's origin when seeded from
/// a template).
pub fn into_definition(self, template: Option<TemplateOrigin>) -> PolicyDefinition {
PolicyDefinition {
id: Uuid::now_v7(),
name: self.name.into(),
description: self.description.map(Into::into),
template,
labels: self.labels,
groups: self.groups,
rules: self.rules,
fallback: self.fallback,
}
}
}

/// Where a new policy's body comes from: exactly one source, enforced by the
/// type so neither-nor-both is unrepresentable.
///
/// Tagged by `source`: `{ "source": "template", "template": "hipaa_safe_harbor" }`
/// Tagged by `source`: `{ "source": "template", "template": { ... } }`
/// or `{ "source": "inline", "definition": { ... } }`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "source", rename_all = "camelCase")]
pub enum PolicyBody {
/// Seed the body from a built-in policy template.
///
/// The template's body is copied into a normal, independently-editable
/// policy at creation time.
/// policy at creation time, tagged with the template's origin.
Template {
/// The built-in policy template to seed from.
template: PolicyTemplate,
},
/// An inline structured policy body consumed by the engine.
Inline {
/// The structured policy body.
/// The client-authored policy body.
///
/// Boxed to keep the enum small: an inline body is much larger than a
/// template id, and most requests use a template.
definition: Box<PolicyDefinition>,
definition: Box<PolicyDraft>,
},
}

impl PolicyBody {
/// Resolves the body source into a concrete policy definition: the inline
/// body as-is, or the template's body materialized from the runtime.
/// Resolves the body source into a concrete policy definition with a fresh
/// `id`, so two policies seeded from the same template stay independent.
///
/// An inline body is hand-authored, so it carries no template origin; a
/// template body keeps the template's own origin (stamped by `build`).
pub fn into_definition(self) -> PolicyDefinition {
match self {
PolicyBody::Inline { definition } => *definition,
PolicyBody::Template { template } => template.build().policy,
PolicyBody::Inline { definition } => definition.into_definition(None),
PolicyBody::Template { template } => PolicyDefinition {
// `build()` bakes a stable constant id; re-mint so each created
// policy is distinct.
id: Uuid::now_v7(),
..template.build().policy
},
}
}
}
Expand Down Expand Up @@ -81,7 +141,9 @@ pub struct CreatePolicy {

/// Request payload for updating an existing workspace policy.
///
/// Replacing the `definition` replaces the whole policy body.
/// Replacing the `definition` replaces the whole policy body. The policy's
/// template origin is server-owned and preserved across updates — it is not
/// settable here.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePolicy {
Expand All @@ -92,5 +154,5 @@ pub struct UpdatePolicy {
#[validate(length(max = 4096))]
pub description: Option<Option<String>>,
/// New policy body (replaces the stored definition).
pub definition: Option<PolicyDefinition>,
pub definition: Option<PolicyDraft>,
}
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/response/catalog.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Deployment catalog response types.

use nvisy_engine::RegisteredRecognizer;
use elide_pipeline::RegisteredRecognizer;
use schemars::JsonSchema;
use serde::Serialize;

Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/handler/response/policies.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Policy response types.

use elide_pipeline::policy::PolicyDefinition;
use jiff::Timestamp;
use nvisy_engine::policy::PolicyDefinition;
use nvisy_postgres::model::WorkspacePolicy;
use nvisy_postgres::types::Handle;
use schemars::JsonSchema;
Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/service/detection/job.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Detection job and run-status event types.

use nvisy_engine::plan::ScopeParams;
use elide_pipeline::plan::ScopeParams;
use nvisy_postgres::types::PipelineRunStatus;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/service/detection/support.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Shared detection helpers used by both the create-run handler and the worker.

use nvisy_engine::policy::PolicyDefinition;
use elide_pipeline::policy::PolicyDefinition;
use nvisy_postgres::model::UpdateWorkspacePipelineRun;
use nvisy_postgres::query::{
PipelineReferenceRepository, WorkspacePipelineRunRepository, WorkspacePolicyRepository,
Expand Down
15 changes: 8 additions & 7 deletions crates/nvisy-server/src/service/detection/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use std::time::Duration;

use nvisy_engine::OcrMode;
use elide_pipeline::RasterMode;
use nvisy_nats::stream::DetectionStream;
use nvisy_postgres::PgConn;
use nvisy_postgres::model::{UpdateWorkspacePipelineRun, WorkspacePipeline, WorkspacePipelineRun};
Expand Down Expand Up @@ -272,7 +272,7 @@ impl DetectionWorker {
let settings = workspace.settings.or_default();
let params =
self.engine
.analyzer_params(&definition, job.scope.clone(), ocr_mode_of(&settings));
.analyzer_params(&definition, job.scope.clone(), raster_mode_of(&settings));

let document = self.blob.build_document(&file, run.id).await?;

Expand Down Expand Up @@ -340,11 +340,12 @@ enum JobOutcome {
Retry,
}

/// Maps a workspace's OCR policy to the engine's per-run OCR mode.
fn ocr_mode_of(settings: &WorkspaceSettings) -> OcrMode {
/// Maps a workspace's OCR policy to the engine's per-run page-rasterisation
/// mode.
fn raster_mode_of(settings: &WorkspaceSettings) -> RasterMode {
match settings.ocr {
OcrPolicy::Auto => OcrMode::Auto,
OcrPolicy::Force => OcrMode::force(),
OcrPolicy::Never => OcrMode::Never,
OcrPolicy::Auto => RasterMode::Auto,
OcrPolicy::Force => RasterMode::always(),
OcrPolicy::Never => RasterMode::Never,
}
}
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/service/engine/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use std::path::Path;

use nvisy_engine::provider::{
use elide_pipeline::provider::{
LlmConfig, LlmRecognizerConfig, NerConfig, NerRecognizerConfig, OcrConfig, OcrEnricherConfig,
SttConfig, SttEnricherConfig,
};
Expand Down
Loading