diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f2f1db6..9adf4c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -71,6 +71,42 @@ jobs:
run: cargo clippy --workspace --all-targets -- -D warnings
- name: cargo test --workspace
run: cargo test --workspace --locked
+ - name: Validar schemas Tauri (SRC-004)
+ run: |
+ python3 -m pip install --no-input --disable-pip-version-check "jsonschema==4.23.0"
+ python3 tools/validate_tauri_schemas.py
+
+ security-runtime:
+ name: Security runtime deny (G8/G9 / NFR-SEC-001)
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Toolchain pinned (rust-toolchain.toml)
+ run: rustup show active-toolchain
+ - name: Instalar deps sistema Tauri/GTK (glib/gdk)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ pkg-config \
+ libglib2.0-dev \
+ libgtk-3-dev \
+ libwebkit2gtk-4.1-dev \
+ libayatana-appindicator3-dev \
+ librsvg2-dev \
+ patchelf
+ - name: Stub frontendDist para generate_context
+ run: |
+ mkdir -p apps/web/dist
+ printf '%s\n' '
archmaker' \
+ > apps/web/dist/index.html
+ # Nombre de test obligatorio: si falta, cargo test falla (no skip-if-missing).
+ - name: Prueba negativa runtime (capability denegada)
+ run: |
+ cargo test -p archmaker --locked \
+ security_runtime_deny_ungranted_command \
+ -- --nocapture
frontend:
name: Frontend pnpm check (G9)
diff --git a/apps/web/package.json b/apps/web/package.json
index aa341bf..de3a6a9 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -15,7 +15,7 @@
"test-a11y:all": "pnpm test-a11y && pnpm test-a11y:axe && pnpm test-a11y:visual && pnpm test-a11y:flow"
},
"dependencies": {
- "@tauri-apps/api": "^2",
+ "@tauri-apps/api": "2.12.1",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
diff --git a/crates/archmaker-core/tests/negative_capabilities.rs b/crates/archmaker-core/tests/negative_capabilities.rs
index 894a2ea..847b908 100644
--- a/crates/archmaker-core/tests/negative_capabilities.rs
+++ b/crates/archmaker-core/tests/negative_capabilities.rs
@@ -149,18 +149,20 @@ fn neg_capability_deny_by_default() {
"permissions vacia: denegacion definitiva; los TOML no conceden"
);
- // Check dialog and picker capabilities have empty permissions (reserved for v1)
+ // MVP: solo main.json. dialog/picker son reserva v1 sin fichero en el árbol.
+ let caps_dir = repo_root().join("src-tauri/capabilities");
+ let mut names: Vec<_> = std::fs::read_dir(&caps_dir)
+ .expect("capabilities dir")
+ .filter_map(|e| e.ok())
+ .map(|e| e.file_name().to_string_lossy().into_owned())
+ .filter(|n| n.ends_with(".json"))
+ .collect();
+ names.sort();
+ assert_eq!(names, vec!["main.json".to_string()], "solo main en MVP");
for cap in ["dialog", "picker"] {
- let raw = read_repo(&format!("src-tauri/capabilities/{}.json", cap));
- let v: serde_json::Value = serde_json::from_str(&raw).expect("capability JSON invalido");
- let perms = v["permissions"]
- .as_array()
- .expect("permissions debe ser lista");
- assert_eq!(
- perms.len(),
- 0,
- "{} capability reservado para v1 sin permisos",
- cap
+ assert!(
+ !caps_dir.join(format!("{cap}.json")).exists(),
+ "{cap}.json no debe existir en MVP (reserva v1)"
);
}
}
@@ -408,19 +410,12 @@ fn neg_unknown_window_yields_typed_proto_error() {
}
#[test]
-fn dialog_and_picker_deny_everything_by_default() {
- // Dialog and picker capabilities are reserved for v1 and have no permissions
+fn dialog_and_picker_capability_files_absent_in_mvp() {
+ let caps_dir = repo_root().join("src-tauri/capabilities");
for cap in ["dialog", "picker"] {
- let raw = read_repo(&format!("src-tauri/capabilities/{}.json", cap));
- let v: serde_json::Value = serde_json::from_str(&raw).expect("capability JSON invalido");
- let perms = v["permissions"]
- .as_array()
- .expect("permissions debe ser lista");
- assert_eq!(
- perms.len(),
- 0,
- "{} capability reservado para v1 sin permisos",
- cap
+ assert!(
+ !caps_dir.join(format!("{cap}.json")).exists(),
+ "{cap}.json ausente en MVP; reserva documentada en tauri-policy.md"
);
}
}
diff --git a/docs/00-governance/source-register.md b/docs/00-governance/source-register.md
index 3fa4a95..7c49a09 100644
--- a/docs/00-governance/source-register.md
+++ b/docs/00-governance/source-register.md
@@ -34,18 +34,19 @@ Toda afirmación mutable (paquetes, versiones, compatibilidad, comportamiento de
## Detalle de captura
-| ID | URL canónica | Versión objetivo | Fecha de captura | Verificador | Caducidad / revisión |
-| ------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------- | ----------------- | ------------------------------------------------------------------------------------- |
-| SRC-002 | | Contenido wiki vigente a la fecha | 2026-10-05 | Gobierno | Revisar por release de soporte o si cambia el flujo de instalación |
-| SRC-003 | | Repositorio oficial vigente a la fecha | 2026-10-05 | Gobierno | Revisar antes de fijar el adapter v1 (DEC-003) |
-| SRC-004 | | Tauri 2.x | 2026-10-05 (previa) | Seguridad | Revisar por minor; fijar permisos por ventana |
-| SRC-005 | · | Rust estable + RustSec Advisory DB | 2026-10-05 | Seguridad | Revisar por release estable y por aviso relevante en `cargo-audit` |
-| SRC-006 | | Draft 2020-12 | 2026-10-05 | Arquitectura | Estable; revisar notas de errata |
-| SRC-007 | · | **W3C Recommendation 12 December 2024** + ARIA APG vigente | 2026-10-05 | UX/Accesibilidad | Estable; revisar APG por cambios de patrón |
-| SRC-008 | · · | **SLSA v1.2** (v1.0 retirada) · **NIST SSDF SP 800-218 v1.1** · Sigstore vigente | 2026-10-05 | Seguridad/Release | Revisar por nueva versión de especificación; fijar versiones antes de firmar releases |
+| ID | URL canónica | Versión objetivo | Fecha de captura | Verificador | Caducidad / revisión |
+| ------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------- | ------------------------------------------------------------------------------------- |
+| SRC-002 | | Contenido wiki vigente a la fecha | 2026-10-05 | Gobierno | Revisar por release de soporte o si cambia el flujo de instalación |
+| SRC-003 | | Repositorio oficial vigente a la fecha | 2026-10-05 | Gobierno | Revisar antes de fijar el adapter v1 (DEC-003) |
+| SRC-004 | · | crate `tauri` **=2.12.1** · `tauri-build` **=2.7.1** · CLI/API `@tauri-apps/{cli,api}` **2.12.1** · claimId `CLAIM-SRC-004-CAPABILITIES-2.12.1` · SHA-256 `4a946fe48fbb0c7c535ea53e6070bd8276f3f3d74f96efbaa6d85bd7b53b0f05` | 2026-10-14 | Seguridad | Revisar por minor; regenerar schema y SHA al fijar otra versión |
+| SRC-005 | · | Rust estable + RustSec Advisory DB | 2026-10-05 | Seguridad | Revisar por release estable y por aviso relevante en `cargo-audit` |
+| SRC-006 | | Draft 2020-12 | 2026-10-05 | Arquitectura | Estable; revisar notas de errata |
+| SRC-007 | · | **W3C Recommendation 12 December 2024** + ARIA APG vigente | 2026-10-05 | UX/Accesibilidad | Estable; revisar APG por cambios de patrón |
+| SRC-008 | · · | **SLSA v1.2** (v1.0 retirada) · **NIST SSDF SP 800-218 v1.1** · Sigstore vigente | 2026-10-05 | Seguridad/Release | Revisar por nueva versión de especificación; fijar versiones antes de firmar releases |
## Notas de verificación
+- **SRC-004 (Tauri 2.12.1):** captura de capabilities/permissions en . Versiones fijadas en `src-tauri/Cargo.toml` (`tauri` `=2.12.1`, `tauri-build` `=2.7.1` según `Cargo.lock`), `package.json` y `apps/web/package.json` (`@tauri-apps/cli` / `@tauri-apps/api` `2.12.1`). Claim `CLAIM-SRC-004-CAPABILITIES-2.12.1`. Schema ACL: `src-tauri/gen/schemas/desktop-schema.json` con SHA-256 `4a946fe48fbb0c7c535ea53e6070bd8276f3f3d74f96efbaa6d85bd7b53b0f05` (regenerado con `cargo build -p archmaker` el 2026-10-14).
- **SRC-008 (SLSA):** la especificación `v1.0` está **retired**; la versión actual es **v1.2** (). Toda referencia a SLSA en la documentación debe apuntar a v1.2, no a v1.0.
- **SRC-008 (SSDF):** capturado como **NIST SP 800-218, SSDF Version 1.1** (no v1.0). Las referencias de SDLC se anclan a v1.1.
- **SRC-008 (Sigstore):** capturado el overview oficial; firma/verificación de artefactos (release files, imágenes, binarios, SBOM) conforme a DEC-006.
diff --git a/docs/08-security/tauri-policy.md b/docs/08-security/tauri-policy.md
index 8a250d8..bc2e83f 100644
--- a/docs/08-security/tauri-policy.md
+++ b/docs/08-security/tauri-policy.md
@@ -5,7 +5,7 @@ priority: P0
documentStatus: in-review
approvalStatus: pending
implementationStatus: complete
-verificationStatus: not-verified
+verificationStatus: partial
releaseStatus: ineligible
owners:
- security
@@ -16,13 +16,14 @@ reviewers:
# Política Tauri
- Deny-by-default.
-- La allow-list JSON de `main`, `dialog` y `picker` permanece vacía: es la denegación definitiva del slice. Los ocho ficheros `src-tauri/permissions/*.toml` definen identificadores `core:*` y no los conceden. `dialog` y `picker` siguen reservadas, sin permisos.
+- En MVP solo existe la ventana `main` y su capability (`src-tauri/capabilities/main.json`) con `permissions: []`: esa lista vacía es la denegación definitiva del slice. Los ocho ficheros `src-tauri/permissions/*.toml` definen identificadores `core:*` y no se conceden en el JSON. No hay campo raíz `denied` (el schema Tauri 2 no lo soporta; la ausencia de permiso basta).
+- Reserva v1: las ventanas `dialog` y `picker` no tienen ficheros de capability en el árbol MVP; se reintroducirán solo con alcance `accepted` de v1, cada una con allow-list vacía hasta conceder permisos tipados.
- Capabilities separadas por ventana y plataforma.
-- Permisos custom con allow/deny explícitos.
+- Permisos custom con allow/deny explícitos cuando se concedan.
- Scopes de filesystem mínimos; negar rutas sensibles.
- Sin `shell:*` en MVP.
- Updater solo tras firma y ADR de canal/rollback.
- CSP sin CDN en producción; `default-src 'self'`.
- Devtools desactivadas en release salvo build de diagnóstico controlada.
- Navegación externa mediante allowlist y confirmación.
-- Toda capability tiene test negativo.
+- Toda capability tiene test negativo (estático + `security-runtime`).
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a93875f..776194a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,7 +27,7 @@ importers:
apps/web:
dependencies:
"@tauri-apps/api":
- specifier: ^2
+ specifier: 2.12.1
version: 2.12.1
react:
specifier: ^19.1.0
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 1b347db..f0cfc54 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -6,10 +6,10 @@ license = "Apache-2.0"
rust-version = "1.82"
[build-dependencies]
-tauri-build = { version = "2", features = [] }
+tauri-build = { version = "=2.7.1", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "=2.12.1", features = ["test"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
archmaker-core = { path = "../crates/archmaker-core" }
diff --git a/src-tauri/capabilities/dialog.json b/src-tauri/capabilities/dialog.json
deleted file mode 100644
index 3f355c4..0000000
--- a/src-tauri/capabilities/dialog.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "$schema": "../gen/schemas/desktop-schema.json",
- "identifier": "dialog-capability",
- "description": "Deny-by-default: ventana dialog reservada (v1). Sin comandos CorePort en MVP; solo denegaciones explicitas.",
- "windows": ["dialog"],
- "permissions": []
-}
diff --git a/src-tauri/capabilities/picker.json b/src-tauri/capabilities/picker.json
deleted file mode 100644
index 6b728a5..0000000
--- a/src-tauri/capabilities/picker.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "$schema": "../gen/schemas/desktop-schema.json",
- "identifier": "picker-capability",
- "description": "Deny-by-default: ventana picker reservada (v1). Sin comandos CorePort en MVP; solo denegaciones explicitas.",
- "windows": ["picker"],
- "permissions": []
-}
diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json
index fd51484..aa3d615 100644
--- a/src-tauri/gen/schemas/capabilities.json
+++ b/src-tauri/gen/schemas/capabilities.json
@@ -1,23 +1,9 @@
{
- "dialog-capability": {
- "identifier": "dialog-capability",
- "description": "Deny-by-default: ventana dialog reservada (v1). Sin comandos CorePort en MVP; solo denegaciones explicitas.",
- "local": true,
- "windows": ["dialog"],
- "permissions": []
- },
"main-capability": {
"identifier": "main-capability",
"description": "Deny-by-default: permissions vacia es la denegacion definitiva del slice. Los 8 identificadores core:* viven en TOML y no se conceden aqui. Sin shell, sin FS global.",
"local": true,
"windows": ["main"],
"permissions": []
- },
- "picker-capability": {
- "identifier": "picker-capability",
- "description": "Deny-by-default: ventana picker reservada (v1). Sin comandos CorePort en MVP; solo denegaciones explicitas.",
- "local": true,
- "windows": ["picker"],
- "permissions": []
}
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
new file mode 100644
index 0000000..865c4e0
--- /dev/null
+++ b/src-tauri/src/lib.rs
@@ -0,0 +1,204 @@
+//! Crate de escritorio Tauri: comandos CorePort y denegación deny-by-default.
+//!
+//! La allow-list de `capabilities/main.json` está vacía: Tauri deniega IPC no
+//! concedido. El gate de aplicación (`check_invoke`) es una segunda capa.
+
+use std::sync::Mutex;
+
+use archmaker_core::{
+ check_invoke, unknown_command_error, BuildManifestInput, CoreError, CoreOp, CorePort,
+ CreateDraftInput, ExportArtifactInput, ImportDraftInput, LoadCatalogInput, RealCore,
+ ResolveDraftInput, SaveDraftInput, ValidateDraftInput, Window,
+};
+use tauri::Manager;
+
+struct CoreState(Mutex);
+
+fn core_error_value(err: CoreError) -> serde_json::Value {
+ serde_json::to_value(err).expect("CoreError is serializable")
+}
+
+fn invalid_input(op: CoreOp, detail: &str) -> serde_json::Value {
+ core_error_value(CoreError::schema_invalid(&format!(
+ "archmaker-tauri.{}: {detail}",
+ op.command_name()
+ )))
+}
+
+fn gate(window: &tauri::Window, op: CoreOp) -> Result<(), serde_json::Value> {
+ let label = window.label().to_string();
+ let parsed = Window::parse(&label)
+ .ok_or_else(|| core_error_value(unknown_command_error(&label, op.command_name())))?;
+ check_invoke(parsed, op).map_err(core_error_value)?;
+ Ok(())
+}
+
+fn parse_input(op: CoreOp, input: serde_json::Value) -> Result
+where
+ T: serde::de::DeserializeOwned,
+{
+ serde_json::from_value(input).map_err(|e| invalid_input(op, &e.to_string()))
+}
+
+macro_rules! core_command {
+ ($name:ident, $op:expr, $input_ty:ty, $method:ident) => {
+ #[tauri::command]
+ fn $name(
+ window: tauri::Window,
+ state: tauri::State<'_, CoreState>,
+ input: serde_json::Value,
+ ) -> Result {
+ gate(&window, $op)?;
+ let typed: $input_ty = parse_input($op, input)?;
+ let core = state
+ .0
+ .lock()
+ .map_err(|_| core_error_value(CoreError::io_failed("archmaker-tauri.state")))?;
+ core.$method(typed)
+ .map(|out| serde_json::to_value(out).expect("DTO is serializable"))
+ .map_err(core_error_value)
+ }
+ };
+}
+
+core_command!(
+ create_draft,
+ CoreOp::CreateDraft,
+ CreateDraftInput,
+ create_draft
+);
+core_command!(
+ load_catalog,
+ CoreOp::LoadCatalog,
+ LoadCatalogInput,
+ load_catalog
+);
+core_command!(save_draft, CoreOp::SaveDraft, SaveDraftInput, save_draft);
+core_command!(
+ resolve_draft,
+ CoreOp::ResolveDraft,
+ ResolveDraftInput,
+ resolve_draft
+);
+core_command!(
+ validate_draft,
+ CoreOp::ValidateDraft,
+ ValidateDraftInput,
+ validate_draft
+);
+core_command!(
+ build_manifest,
+ CoreOp::BuildManifest,
+ BuildManifestInput,
+ build_manifest
+);
+core_command!(
+ export_artifact,
+ CoreOp::ExportArtifact,
+ ExportArtifactInput,
+ export_artifact
+);
+core_command!(
+ import_draft,
+ CoreOp::ImportDraft,
+ ImportDraftInput,
+ import_draft
+);
+
+/// Arranque de producción (runtime Wry).
+pub fn run() {
+ tauri::Builder::default()
+ .setup(|app| {
+ let data_dir = app.path().app_data_dir().expect("app data dir resolves");
+ std::fs::create_dir_all(&data_dir).expect("app data dir is writable");
+ let core = RealCore::with_embedded()
+ .expect("embedded catalog loads")
+ .with_store_dir(data_dir);
+ app.manage(CoreState(Mutex::new(core)));
+ Ok(())
+ })
+ .invoke_handler(tauri::generate_handler![
+ create_draft,
+ load_catalog,
+ save_draft,
+ resolve_draft,
+ validate_draft,
+ build_manifest,
+ export_artifact,
+ import_draft
+ ])
+ .run(tauri::generate_context!())
+ .expect("error while running ArchMaker");
+}
+
+#[cfg(test)]
+mod tests {
+ use tauri::webview::InvokeRequest;
+ use tauri::WebviewWindowBuilder;
+
+ fn invoke_url() -> tauri::Url {
+ if cfg!(any(windows, target_os = "android")) {
+ "http://tauri.localhost".parse().unwrap()
+ } else {
+ "tauri://localhost".parse().unwrap()
+ }
+ }
+
+ /// Negativo runtime: comando no concedido en capability → denegación IPC de Tauri.
+ /// El job CI `security-runtime` ejecuta este nombre; si falta, falla.
+ #[test]
+ fn security_runtime_deny_ungranted_command() {
+ // MockRuntime + contexto real (capabilities/main.json con permissions: []).
+ let app = tauri::test::mock_builder()
+ .build(tauri::generate_context!())
+ .expect("mock app builds with real capabilities");
+
+ let webview = WebviewWindowBuilder::new(&app, "main", Default::default())
+ .build()
+ .expect("main webview");
+
+ let err = tauri::test::get_ipc_response(
+ &webview,
+ InvokeRequest {
+ cmd: "create_draft".into(),
+ callback: tauri::ipc::CallbackFn(0),
+ error: tauri::ipc::CallbackFn(1),
+ url: invoke_url(),
+ body: tauri::ipc::InvokeBody::default(),
+ headers: Default::default(),
+ invoke_key: tauri::test::INVOKE_KEY.to_string(),
+ },
+ )
+ .expect_err("create_draft must be denied: main capability grants nothing");
+
+ let msg = format!("{err:?}").to_lowercase();
+ assert!(
+ msg.contains("denied")
+ || msg.contains("not allowed")
+ || msg.contains("forbidden")
+ || msg.contains("permission")
+ || msg.contains("capability")
+ || msg.contains("acl")
+ || msg.contains("command"),
+ "expected ACL/permission denial, got: {err:?}"
+ );
+
+ let unknown = tauri::test::get_ipc_response(
+ &webview,
+ InvokeRequest {
+ cmd: "shell_execute".into(),
+ callback: tauri::ipc::CallbackFn(0),
+ error: tauri::ipc::CallbackFn(1),
+ url: invoke_url(),
+ body: tauri::ipc::InvokeBody::default(),
+ headers: Default::default(),
+ invoke_key: tauri::test::INVOKE_KEY.to_string(),
+ },
+ )
+ .expect_err("shell_execute must be denied");
+ assert!(
+ !format!("{unknown:?}").is_empty(),
+ "unknown command must produce an error"
+ );
+ }
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index dcca67f..469ec85 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -1,128 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-use std::sync::Mutex;
-
-use archmaker_core::{
- check_invoke, unknown_command_error, BuildManifestInput, CoreError, CoreOp, CorePort,
- CreateDraftInput, ExportArtifactInput, ImportDraftInput, LoadCatalogInput, RealCore,
- ResolveDraftInput, SaveDraftInput, ValidateDraftInput, Window,
-};
-use tauri::Manager;
-
-struct CoreState(Mutex);
-
-fn core_error_value(err: CoreError) -> serde_json::Value {
- serde_json::to_value(err).expect("CoreError is serializable")
-}
-
-fn invalid_input(op: CoreOp, detail: &str) -> serde_json::Value {
- core_error_value(CoreError::schema_invalid(&format!(
- "archmaker-tauri.{}: {detail}",
- op.command_name()
- )))
-}
-
-fn gate(window: &tauri::Window, op: CoreOp) -> Result<(), serde_json::Value> {
- let label = window.label().to_string();
- let parsed = Window::parse(&label)
- .ok_or_else(|| core_error_value(unknown_command_error(&label, op.command_name())))?;
- check_invoke(parsed, op).map_err(core_error_value)?;
- Ok(())
-}
-
-fn parse_input(op: CoreOp, input: serde_json::Value) -> Result
-where
- T: serde::de::DeserializeOwned,
-{
- serde_json::from_value(input).map_err(|e| invalid_input(op, &e.to_string()))
-}
-
-macro_rules! core_command {
- ($name:ident, $op:expr, $input_ty:ty, $method:ident) => {
- #[tauri::command]
- fn $name(
- window: tauri::Window,
- state: tauri::State<'_, CoreState>,
- input: serde_json::Value,
- ) -> Result {
- gate(&window, $op)?;
- let typed: $input_ty = parse_input($op, input)?;
- let core = state
- .0
- .lock()
- .map_err(|_| core_error_value(CoreError::io_failed("archmaker-tauri.state")))?;
- core.$method(typed)
- .map(|out| serde_json::to_value(out).expect("DTO is serializable"))
- .map_err(core_error_value)
- }
- };
-}
-
-core_command!(
- create_draft,
- CoreOp::CreateDraft,
- CreateDraftInput,
- create_draft
-);
-core_command!(
- load_catalog,
- CoreOp::LoadCatalog,
- LoadCatalogInput,
- load_catalog
-);
-core_command!(save_draft, CoreOp::SaveDraft, SaveDraftInput, save_draft);
-core_command!(
- resolve_draft,
- CoreOp::ResolveDraft,
- ResolveDraftInput,
- resolve_draft
-);
-core_command!(
- validate_draft,
- CoreOp::ValidateDraft,
- ValidateDraftInput,
- validate_draft
-);
-core_command!(
- build_manifest,
- CoreOp::BuildManifest,
- BuildManifestInput,
- build_manifest
-);
-core_command!(
- export_artifact,
- CoreOp::ExportArtifact,
- ExportArtifactInput,
- export_artifact
-);
-core_command!(
- import_draft,
- CoreOp::ImportDraft,
- ImportDraftInput,
- import_draft
-);
-
fn main() {
- tauri::Builder::default()
- .setup(|app| {
- let data_dir = app.path().app_data_dir().expect("app data dir resolves");
- std::fs::create_dir_all(&data_dir).expect("app data dir is writable");
- let core = RealCore::with_embedded()
- .expect("embedded catalog loads")
- .with_store_dir(data_dir);
- app.manage(CoreState(Mutex::new(core)));
- Ok(())
- })
- .invoke_handler(tauri::generate_handler![
- create_draft,
- load_catalog,
- save_draft,
- resolve_draft,
- validate_draft,
- build_manifest,
- export_artifact,
- import_draft
- ])
- .run(tauri::generate_context!())
- .expect("error while running ArchMaker");
+ archmaker::run();
}
diff --git a/tools/check_negative_capabilities.py b/tools/check_negative_capabilities.py
index 5aabefb..b36b946 100644
--- a/tools/check_negative_capabilities.py
+++ b/tools/check_negative_capabilities.py
@@ -49,12 +49,17 @@ def check(name: str, ok: bool, detail: str = "") -> None:
found == CORE_CMDS,
str(found),
)
+caps_dir = ROOT / "src-tauri/capabilities"
+cap_files = sorted(p.name for p in caps_dir.glob("*.json"))
+check(
+ "NEG-001 solo capability main en MVP",
+ cap_files == ["main.json"],
+ str(cap_files),
+)
for reserved in ["dialog", "picker"]:
- ocap = json.loads((ROOT / f"src-tauri/capabilities/{reserved}.json").read_text())
check(
- f"NEG-001 {reserved} reservado sin permisos (v1)",
- ocap.get("permissions") == [],
- str(ocap.get("permissions")),
+ f"NEG-001 {reserved}.json ausente (reserva v1 sin fichero)",
+ not (caps_dir / f"{reserved}.json").exists(),
)
raw_conf = (ROOT / "src-tauri/tauri.conf.json").read_text()
diff --git a/tools/validate_tauri_schemas.py b/tools/validate_tauri_schemas.py
new file mode 100755
index 0000000..4b52b81
--- /dev/null
+++ b/tools/validate_tauri_schemas.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""Valida tauri.conf.json y capabilities contra schemas Tauri 2 (SRC-004 / #30 / #32).
+
+- Rechaza app.security.devtools (fuera de SecurityConfig v2).
+- Valida cada capability JSON contra src-tauri/gen/schemas/desktop-schema.json.
+- Exige solo main.json en capabilities/ (MVP).
+"""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+fails: list[str] = []
+
+
+def fail(msg: str) -> None:
+ print(f"FAIL {msg}")
+ fails.append(msg)
+
+
+def ok(msg: str) -> None:
+ print(f"PASS {msg}")
+
+
+def main() -> int:
+ conf_path = ROOT / "src-tauri/tauri.conf.json"
+ schema_path = ROOT / "src-tauri/gen/schemas/desktop-schema.json"
+ caps_dir = ROOT / "src-tauri/capabilities"
+
+ conf = json.loads(conf_path.read_text(encoding="utf-8"))
+ ok(f"parse {conf_path.relative_to(ROOT)}")
+
+ security = conf.get("app", {}).get("security", {})
+ if "devtools" in security:
+ fail("app.security.devtools presente (rechazado por schema Tauri 2)")
+ else:
+ ok("app.security sin devtools")
+
+ windows = conf.get("app", {}).get("windows", [])
+ labels = [w.get("label") for w in windows if isinstance(w, dict)]
+ if labels != ["main"]:
+ fail(f"ventanas esperadas ['main'], got {labels}")
+ else:
+ ok("tauri.conf.json solo ventana main")
+
+ if not schema_path.is_file():
+ fail(f"falta {schema_path.relative_to(ROOT)}; regenerar con cargo build -p archmaker")
+ print(f"\n{len(fails)} fallos.")
+ return 1
+
+ try:
+ from jsonschema import Draft7Validator
+ except ImportError:
+ fail("falta jsonschema (pip install jsonschema==4.23.0)")
+ print(f"\n{len(fails)} fallos.")
+ return 1
+
+ desktop_schema = json.loads(schema_path.read_text(encoding="utf-8"))
+ # CapabilityFile (raíz): anyOf acepta una sola Capability.
+ validator = Draft7Validator(desktop_schema)
+
+ cap_files = sorted(caps_dir.glob("*.json"))
+ names = [p.name for p in cap_files]
+ if names != ["main.json"]:
+ fail(f"capabilities MVP debe ser solo main.json, got {names}")
+ else:
+ ok("capabilities/ solo main.json")
+
+ for cap_path in cap_files:
+ instance = json.loads(cap_path.read_text(encoding="utf-8"))
+ # $schema es metadato del fichero; el schema CapabilityFile no lo declara.
+ instance.pop("$schema", None)
+ if "denied" in instance:
+ fail(f"{cap_path.name}: campo raíz 'denied' no soportado por schema")
+ errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path))
+ if errors:
+ for err in errors[:5]:
+ fail(f"{cap_path.name}: {err.message}")
+ else:
+ ok(f"capability valida contra desktop-schema: {cap_path.name}")
+ if instance.get("permissions") != []:
+ fail(f"{cap_path.name}: permissions debe ser [] en MVP")
+ else:
+ ok(f"{cap_path.name} permissions vacía (deny-by-default)")
+
+ caps_gen = ROOT / "src-tauri/gen/schemas/capabilities.json"
+ if caps_gen.is_file():
+ gen = json.loads(caps_gen.read_text(encoding="utf-8"))
+ keys = sorted(gen.keys())
+ if keys != ["main-capability"]:
+ fail(f"gen/schemas/capabilities.json keys={keys}, esperado ['main-capability']")
+ else:
+ ok("gen/schemas/capabilities.json solo main-capability")
+
+ print(f"\n{len(fails)} fallos.")
+ return 1 if fails else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())