New crates: stratum-orchestrator (Cedar authz, Vault secrets, Nu/agent executors, saga runner), stratum-graph (petgraph DAG + SurrealDB repo), stratum-state (SurrealDB tracker), platform-nats (NKey auth client), ncl-import-resolver. Updates: stratum-embeddings (SurrealDB store + persistent cache), stratum-llm circuit breaker. Adds Nickel action-nodes, schemas, config, Nushell scripts, docker-compose dev stack, and ADR-003.
1 line
38 KiB
HTML
1 line
38 KiB
HTML
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title data-en="Stratum Orchestrator — Graph-Driven Workflow Engine" data-es="Stratum Orchestrator — Motor de Workflows Basado en Grafos" > Stratum Orchestrator </title><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&display=swap" rel="stylesheet" /><style>:root{--bg-primary:#0a0c10;--bg-secondary:rgba(255,255,255,0.03);--text-primary:#ffffff;--text-secondary:#cbd5e1;--text-muted:#94a3b8;--border-color:rgba(99,102,241,0.35);--border-color-b:rgba(59,130,246,0.3);--accent-indigo:#6366f1;--accent-cyan:#22d3ee;--accent-green:#10b981;--accent-blue:#3b82f6;--accent-purple:#8b5cf6;--accent-amber:#f59e0b;--card-bg:rgba(255,255,255,0.03);--card-border:rgba(99,102,241,0.3);--highlight-color:#6366f1;--footer-text:#64748b;}html.light-mode{--bg-primary:#f0f4f8;--bg-secondary:rgba(0,0,0,0.02);--text-primary:#1e293b;--text-secondary:#374151;--text-muted:#6b7280;--border-color:rgba(79,70,229,0.35);--border-color-b:rgba(59,130,246,0.35);--accent-indigo:#4338ca;--accent-cyan:#0891b2;--accent-green:#059669;--accent-blue:#2563eb;--accent-purple:#7c3aed;--accent-amber:#d97706;--card-bg:rgba(0,0,0,0.02);--card-border:rgba(79,70,229,0.25);--highlight-color:#4338ca;--footer-text:#4b5563;}*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;}body{font-family:"JetBrains Mono",monospace;background:var(--bg-primary);color:var(--text-primary);overflow-x:hidden;transition:background-color 0.3s ease,color 0.3s ease;}.gradient-bg{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;background:radial-gradient( circle at 15% 40%,var(--accent-indigo) 0%,transparent 50% ),radial-gradient( circle at 80% 70%,var(--accent-cyan) 0%,transparent 50% ),radial-gradient( circle at 50% 95%,var(--accent-purple) 0%,transparent 50% );opacity:0.12;}.nav-pill{position:fixed;top:2rem;right:2rem;z-index:100;display:flex;align-items:center;gap:0.375rem;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:24px;padding:0.3rem 0.4rem;backdrop-filter:blur(12px);box-shadow:0 8px 32px rgba(0,0,0,0.18);}.nav-btn{font-family:"JetBrains Mono",monospace;font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.42rem 0.9rem;border-radius:18px;border:none;cursor:pointer;text-decoration:none;color:var(--text-muted);background:transparent;transition:color 0.25s,background 0.25s;display:inline-flex;align-items:center;gap:0.3rem;}.nav-btn.active{background:linear-gradient(135deg,#6366f1,#22d3ee);color:#fff;}.nav-btn:hover{color:var(--accent-indigo);}.nav-sep{width:1px;height:20px;background:var(--border-color);}.nav-theme{font-size:1.1rem;padding:0.42rem 0.6rem;color:var(--text-muted);}.container{max-width:1200px;margin:0 auto;padding:2rem;position:relative;}header{text-align:center;padding:5rem 0 2rem;animation:fadeInUp 0.8s ease-out;}@keyframes fadeInUp{from{opacity:0;transform:translateY(30px);}to{opacity:1;transform:translateY(0);}}.status-badge{display:inline-block;background:rgba(99,102,241,0.18);border:1px solid var(--accent-indigo);color:var(--accent-indigo);padding:0.5rem 1.5rem;border-radius:50px;font-size:0.85rem;font-weight:700;margin-bottom:1.5rem;}.logo-container{margin-bottom:2rem;}.logo-container img{max-width:420px;width:100%;height:auto;filter:drop-shadow(0 0 28px rgba(99,102,241,0.4));}.tagline{font-size:0.95rem;color:var(--accent-indigo);font-weight:400;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:1rem;}h1{font-size:2.8rem;font-weight:800;line-height:1.2;margin-bottom:1.5rem;background:linear-gradient( 135deg,#6366f1 0%,#22d3ee 50%,#10b981 100% );-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}.hero-subtitle{font-size:1.1rem;color:var(--text-secondary);max-width:800px;margin:0 auto 2rem;line-height:1.8;}.highlight{color:var(--highlight-color);font-weight:700;}.section{margin:4rem 0;animation:fadeInUp 0.8s ease-out;}.section-title{font-size:2rem;font-weight:800;margin-bottom:2rem;color:var(--accent-indigo);text-align:center;}.section-title span{background:linear-gradient(135deg,#8b5cf6 0%,#22d3ee 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}.chars-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1.5rem;margin-top:2rem;}.char-card{background:var(--card-bg);border:1px solid var(--card-border);border-radius:12px;padding:1.8rem;transition:all 0.3s ease;position:relative;overflow:hidden;}.char-card:hover{transform:translateY(-4px);background:rgba(99,102,241,0.06);border-color:rgba(99,102,241,0.5);}.char-number{font-size:2rem;font-weight:800;background:linear-gradient(135deg,#6366f1 0%,#22d3ee 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:1;margin-bottom:0.5rem;}.char-card h3{color:var(--accent-purple);font-size:1rem;margin-bottom:0.6rem;font-weight:700;}.char-card p{color:var(--text-secondary);font-size:0.88rem;line-height:1.65;}.t{color:var(--accent-cyan);font-weight:700;}.features-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:2rem;margin-top:2rem;}.feature-box{background:linear-gradient( 135deg,rgba(99,102,241,0.08) 0%,rgba(34,211,238,0.08) 100% );border-radius:12px;padding:2rem;border-left:4px solid var(--accent-indigo);transition:all 0.3s ease;}.feature-box:hover{background:linear-gradient( 135deg,rgba(99,102,241,0.13) 0%,rgba(34,211,238,0.13) 100% );transform:translateY(-3px);}.feature-icon{font-size:2.2rem;margin-bottom:1rem;}.feature-title{font-size:1.1rem;font-weight:700;color:var(--accent-indigo);margin-bottom:0.7rem;}.feature-text{color:var(--text-secondary);font-size:0.92rem;line-height:1.7;}.code-block{background:rgba(0,0,0,0.25);border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:0.9rem 1rem;margin-top:1rem;font-size:0.8rem;color:#a5b4fc;white-space:pre;overflow-x:auto;line-height:1.5;}html.light-mode .code-block{background:rgba(0,0,0,0.06);border-color:rgba(0,0,0,0.1);color:#4338ca;}.arch-preview{display:flex;flex-direction:column;align-items:center;gap:1.5rem;}.arch-pair{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;width:100%;}.arch-img-link{display:block;border-radius:12px;overflow:hidden;border:1px solid var(--border-color);transition:all 0.3s ease;cursor:pointer;}.arch-img-link:hover{border-color:rgba(99,102,241,0.6);box-shadow:0 8px 32px rgba(99,102,241,0.15);transform:translateY(-2px);}.arch-img{display:block;width:100%;height:auto;}.arch-label{font-size:0.78rem;font-weight:700;text-align:center;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-top:0.6rem;}html.light-mode .arch-dark{display:none;}html:not(.light-mode) .arch-light{display:none;}.arch-btn-row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;}.arch-btn{display:inline-block;border:2px solid var(--accent-indigo);color:var(--accent-indigo);padding:0.9rem 2.2rem;border-radius:50px;text-decoration:none;font-weight:800;font-size:0.9rem;font-family:"JetBrains Mono",monospace;transition:all 0.3s ease;text-transform:uppercase;letter-spacing:0.05em;}.arch-btn:hover{background:var(--accent-indigo);color:#fff;box-shadow:0 10px 30px rgba(99,102,241,0.3);transform:translateY(-2px);}.components-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin-top:2rem;}.component-item{background:rgba(99,102,241,0.1);padding:1.4rem;border-radius:8px;border:1px solid rgba(99,102,241,0.28);transition:all 0.2s ease;text-align:center;}.component-item:hover{background:rgba(99,102,241,0.16);transform:translateY(-2px);}.component-name{color:var(--accent-indigo);font-weight:700;display:block;margin-bottom:0.35rem;font-size:0.95rem;}.component-domain{color:var(--accent-cyan);font-size:0.78rem;font-weight:700;display:block;margin-bottom:0.3rem;text-transform:uppercase;letter-spacing:0.05em;}.component-role{color:var(--text-muted);font-size:0.82rem;line-height:1.4;}.tech-stack{display:flex;flex-wrap:wrap;gap:0.9rem;margin-top:2rem;justify-content:center;}.tech-badge{background:rgba(99,102,241,0.14);border:1px solid rgba(99,102,241,0.4);padding:0.45rem 0.95rem;border-radius:20px;font-size:0.8rem;color:var(--accent-indigo);font-weight:700;}.cta-section{text-align:center;margin:5rem 0 3rem;padding:4rem 2rem;background:linear-gradient( 135deg,rgba(99,102,241,0.1) 0%,rgba(139,92,246,0.1) 100% );border-radius:20px;border:1px solid rgba(99,102,241,0.3);}.cta-title{font-size:2rem;font-weight:800;margin-bottom:1rem;background:linear-gradient(135deg,#6366f1 0%,#10b981 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}.cta-button{display:inline-block;background:linear-gradient( 135deg,#6366f1 0%,#22d3ee 50%,#10b981 100% );color:#fff;padding:1.1rem 2.8rem;border-radius:50px;text-decoration:none;font-weight:800;font-size:1rem;font-family:"JetBrains Mono",monospace;transition:all 0.3s ease;box-shadow:0 10px 30px rgba(99,102,241,0.3);text-transform:uppercase;letter-spacing:0.05em;border:none;cursor:pointer;}.cta-button:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 20px 50px rgba(99,102,241,0.5);}footer{text-align:center;padding:3rem 0 2rem;color:var(--footer-text);border-top:1px solid var(--border-color);margin-top:4rem;font-size:0.9rem;}footer p:first-child{font-weight:700;color:#94a3b8;}footer p:last-child{margin-top:0.5rem;font-size:0.85rem;}.hidden{display:none;}@media (max-width:768px){h1{font-size:2rem;}.hero-subtitle{font-size:0.95rem;}.logo-container img{max-width:320px;}.section-title{font-size:1.6rem;}.cta-title{font-size:1.6rem;}.nav-pill{top:1rem;right:1rem;}.chars-grid{grid-template-columns:1fr;}.arch-pair{grid-template-columns:1fr;}}</style></head><body><div class="gradient-bg"></div><div class="nav-pill"><button class="nav-btn active" data-lang="en" onclick="switchLanguage('en')" > EN </button><button class="nav-btn" data-lang="es" onclick="switchLanguage('es')"> ES </button><div class="nav-sep"></div><button class="nav-btn nav-theme" onclick="toggleTheme()" title="Toggle light/dark" ><span id="theme-icon">🌙</span></button><div class="nav-sep"></div><a href="stratumiops-arch.html" class="nav-btn" data-en="Arch →" data-es="Arq →" style=" background: rgba(99, 102, 241, 0.18); border: 1px solid rgba(99, 102, 241, 0.4); " >Arch →</a ><a href="stratumiops-pipeline.html" class="nav-btn" data-en="Pipeline →" data-es="Pipeline →" style=" display: none; background: rgba(34, 211, 238, 0.12); border: 1px solid rgba(34, 211, 238, 0.3); " >Pipeline →</a ></div><div class="container"><header><span class="status-badge" style="display: none" data-en="✅ ADR-003 | Accepted" data-es="✅ ADR-003 | Aceptado" >✅ ADR-003 | Accepted</span ><div class="logo-container"><img src="stratumiops-h.svg" alt="StratumIOps" width="420" /></div><p class="tagline" data-en="Graph-Driven Workflow Orchestration" data-es="Orquestación de Workflows Basada en Grafos" > Graph-Driven Workflow Orchestration </p><h1 data-en="Stratum Orchestrator —<br>Stateless, Agnostic, Auditable" data-es="Stratum Orchestrator —<br>Sin Estado, Agnóstico, Auditable" > Stratum Orchestrator —<br />Stateless, Agnostic, Auditable </h1><p class="hero-subtitle"><span class="highlight" data-en="Cross-project event-driven pipelines" data-es="Pipelines event-driven multi-proyecto" >Cross-project event-driven pipelines</span ><span data-en=" declared in Nickel, executed by Nushell, coordinated via NATS JetStream, and persisted in SurrealDB. The orchestrator binary never changes when workflows do." data-es=" declaradas en Nickel, ejecutadas por Nushell, coordinadas vía NATS JetStream y persistidas en SurrealDB. El binario del orquestador nunca cambia cuando los workflows evolucionan." > declared in Nickel, executed by Nushell, coordinated via NATS JetStream, and persisted in SurrealDB. The orchestrator binary never changes when workflows do.</span ></p></header><section class="section"><h2 class="section-title"><span data-en="10 Fundamental Design Characteristics" data-es="10 Características Fundamentales de Diseño" >10 Fundamental Design Characteristics</span ></h2><div class="chars-grid"><div class="char-card"><div class="char-number">01</div><h3 data-en="Graph-Driven — No Routing Tables" data-es="Basado en Grafos — Sin Tablas de Rutas" > Graph-Driven — No Routing Tables </h3><p data-en="NATS subjects are matched against an ActionGraph built from Nickel node definitions. Each ActionNode declares trigger, input_schemas, output_schemas, and compensate. Adding a workflow means adding .ncl files — zero binary changes." data-es="Los sujetos NATS se comparan con un ActionGraph construido desde definiciones Nickel. Cada ActionNode declara trigger, input_schemas, output_schemas y compensate. Añadir un workflow implica añadir .ncl — cero cambios en el binario." > NATS subjects are matched against an ActionGraph built from Nickel node definitions. Each ActionNode declares trigger, input_schemas, output_schemas, and compensate. Adding a workflow means adding .ncl files — zero binary changes. </p></div><div class="char-card"><div class="char-number">02</div><h3 data-en="Stateless Orchestrator — DB-First" data-es="Orquestador Sin Estado — DB-First" > Stateless Orchestrator — DB-First </h3><p data-en="Every PipelineContext write goes to SurrealDB first, then to an in-memory DashMap cache. On crash, the instance reconstructs from DB. Enables horizontal scaling and crash-free pipeline resumption from last persisted capability." data-es="Cada escritura de PipelineContext va primero a SurrealDB, luego al caché DashMap en memoria. Ante un crash, la instancia se reconstruye desde la DB. Permite escalado horizontal y reanudación sin pérdida desde la última capacidad persistida." > Every PipelineContext write goes to SurrealDB first, then to an in-memory DashMap cache. On crash, the instance reconstructs from DB. Enables horizontal scaling and crash-free pipeline resumption from last persisted capability. </p></div><div class="char-card"><div class="char-number">03</div><h3 data-en="Nickel as Single Source of Truth" data-es="Nickel como Única Fuente de Verdad" > Nickel as Single Source of Truth </h3><p data-en="Action nodes, capability schemas, and startup config are all defined in Nickel. No DB copy of node definitions exists at runtime. The ActionGraph is built in-memory at startup via nickel export, then kept live via a notify file watcher for hot-reload." data-es="Nodos, esquemas de capacidad y config de inicio están todos en Nickel. No existe copia en DB de las definiciones de nodo en tiempo de ejecución. El ActionGraph se construye en memoria al inicio vía nickel export y se mantiene vivo con un watcher de archivos." > Action nodes, capability schemas, and startup config are all defined in Nickel. No DB copy of node definitions exists at runtime. The ActionGraph is built in-memory at startup via nickel export, then kept live via a notify file watcher for hot-reload. </p></div><div class="char-card"><div class="char-number">04</div><h3 data-en="Capability Model — DI at Execution Level" data-es="Modelo de Capacidad — DI a Nivel de Ejecución" > Capability Model — DI at Execution Level </h3><p data-en="Nodes do not depend on each other directly — they declare capabilities they produce and consume. The graph engine resolves dependencies. build-crate does not know about lint-crate; it only needs linted-code. Nodes can be swapped or parallelized without changing consumers." data-es="Los nodos no dependen directamente entre sí — declaran capacidades que producen y consumen. El motor de grafos resuelve dependencias. build-crate no conoce lint-crate; solo necesita linted-code. Los nodos pueden intercambiarse o paralelizarse sin cambiar consumidores." > Nodes do not depend on each other directly — they declare capabilities they produce and consume. The graph engine resolves dependencies. build-crate does not know about lint-crate; it only needs linted-code. Nodes can be swapped or parallelized without changing consumers. </p></div><div class="char-card"><div class="char-number">05</div><h3 data-en="Three Independent Auth Planes" data-es="Tres Planos de Autenticación Independientes" > Three Independent Auth Planes </h3><p data-en="Publisher auth via NATS NKeys (ed25519) controls who can emit events. Workflow authz via Cedar policies controls which pipelines a principal can trigger. Execution credentials via SecretumVault are scoped per-node with TTL equal to the node timeout — revoked on failure." data-es="Auth de publicador vía NATS NKeys (ed25519) controla quién puede emitir eventos. Authz de workflow vía políticas Cedar controla qué pipelines puede activar un principal. Credenciales de ejecución vía SecretumVault tienen alcance por nodo con TTL igual al timeout del nodo." > Publisher auth via NATS NKeys (ed25519) controls who can emit events. Workflow authz via Cedar policies controls which pipelines a principal can trigger. Execution credentials via SecretumVault are scoped per-node with TTL equal to the node timeout — revoked on failure. </p></div><div class="char-card"><div class="char-number">06</div><h3 data-en="Saga Atomicity — Compensation Not Transactions" data-es="Atomicidad Saga — Compensación, No Transacciones" > Saga Atomicity — Compensation Not Transactions </h3><p data-en="Pipelines execute forward through stages. If a stage fails, the orchestrator runs compensate.nu scripts in reverse order through all previously successful stages. Compensation is best-effort; failures are logged and auditable in SurrealDB — the pipeline still reaches Compensated status." data-es="Los pipelines ejecutan hacia adelante por etapas. Si una etapa falla, el orquestador ejecuta scripts compensate.nu en orden inverso por todas las etapas previas exitosas. La compensación es best-effort; los fallos se registran y son auditables en SurrealDB." > Pipelines execute forward through stages. If a stage fails, the orchestrator runs compensate.nu scripts in reverse order through all previously successful stages. Compensation is best-effort; failures are logged and auditable in SurrealDB — the pipeline still reaches Compensated status. </p></div><div class="char-card"><div class="char-number">07</div><h3 data-en="Parallel Stages — JoinSet + CancellationToken" data-es="Etapas Paralelas — JoinSet + CancellationToken" > Parallel Stages — JoinSet + CancellationToken </h3><p data-en="Within each stage, nodes with no capability dependencies execute in parallel via tokio::task::JoinSet. Fail-fast is implemented via CancellationToken: the first node failure cancels the token, aborting all sibling tasks in that stage immediately." data-es="En cada etapa, los nodos sin dependencias de capacidad entre sí se ejecutan en paralelo con tokio::task::JoinSet. Fail-fast se implementa mediante CancellationToken: el primer fallo de nodo cancela el token, abortando todos los hermanos de la etapa." > Within each stage, nodes with no capability dependencies execute in parallel via tokio::task::JoinSet. Fail-fast is implemented via CancellationToken: the first node failure cancels the token, aborting all sibling tasks in that stage immediately. </p></div><div class="char-card"><div class="char-number">08</div><h3 data-en="OCI for Everything — Content-Addressed" data-es="OCI para Todo — Direccionamiento por Contenido" > OCI for Everything — Content-Addressed </h3><p data-en="Node definitions and the Nickel base library are published as OCI artifacts to a Zot registry. The ncl-import-resolver binary pulls each OCI layer at startup, verifies digest against annotated sha256, then exposes a local path for Nickel imports. Prevents loading unverified definitions." data-es="Las definiciones de nodo y la librería base de Nickel se publican como artefactos OCI en un registro Zot. El binario ncl-import-resolver descarga cada capa OCI al inicio, verifica el digest contra el sha256 anotado y expone un path local para imports de Nickel." > Node definitions and the Nickel base library are published as OCI artifacts to a Zot registry. The ncl-import-resolver binary pulls each OCI layer at startup, verifies digest against annotated sha256, then exposes a local path for Nickel imports. Prevents loading unverified definitions. </p></div><div class="char-card"><div class="char-number">09</div><h3 data-en="Nushell as Execution Unit — Agnostic" data-es="Nushell como Unidad de Ejecución — Agnóstica" > Nushell as Execution Unit — Agnostic </h3><p data-en="Each node's handler is a Nushell script. The executor spawns nu --no-config-file script.nu, passes PipelineContext inputs as JSON on stdin, reads output JSON from stdout. The orchestrator has no knowledge of what the script does — hot-replaceable without recompiling." data-es="El handler de cada nodo es un script Nushell. El executor invoca nu --no-config-file script.nu, pasa las entradas del PipelineContext como JSON por stdin y lee el JSON de salida por stdout. El orquestador no sabe qué hace el script — reemplazable en caliente sin recompilar." > Each node's handler is a Nushell script. The executor spawns nu --no-config-file script.nu, passes PipelineContext inputs as JSON on stdin, reads output JSON from stdout. The orchestrator has no knowledge of what the script does — hot-replaceable without recompiling. </p></div><div class="char-card"><div class="char-number">10</div><h3 data-en="TypeDialog — Startup Config Only" data-es="TypeDialog — Solo Configuración de Inicio" > TypeDialog — Startup Config Only </h3><p data-en="TypeDialog is used exclusively for orchestrator startup configuration: SurrealDB URL, NATS URL, Zot URL, Vault URL, log level, feature flags. Not for workflow definitions or node configurations — those live in Nickel. Bounded scope, single responsibility." data-es="TypeDialog se usa exclusivamente para la configuración de inicio del orquestador: URLs de SurrealDB, NATS, Zot, Vault, nivel de log y feature flags. No para definiciones de workflows ni configuraciones de nodo — esas viven en Nickel. Alcance acotado, responsabilidad única." > TypeDialog is used exclusively for orchestrator startup configuration: SurrealDB URL, NATS URL, Zot URL, Vault URL, log level, feature flags. Not for workflow definitions or node configurations — those live in Nickel. Bounded scope, single responsibility. </p></div></div></section><section class="section"><h2 class="section-title"><span data-en="Key Architectural Decisions" data-es="Decisiones Arquitectónicas Clave" >Key Architectural Decisions</span ></h2><div class="features-grid"><div class="feature-box" style="border-left-color: #6366f1"><div class="feature-icon">🕸️</div><h3 class="feature-title" style="color: #6366f1" data-en="Capability DAG — not a rule router" data-es="DAG de Capacidades — no un router de reglas" > Capability DAG — not a rule router </h3><p class="feature-text" data-en="Topological sort of the capability graph produces a staged execution plan. Each stage is a set of nodes with no inter-dependency. The orchestrator evaluates the graph; it does not hardcode any workflow logic." data-es="El ordenamiento topológico del grafo de capacidades produce un plan de ejecución en etapas. Cada etapa es un conjunto de nodos sin interdependencia. El orquestador evalúa el grafo; no tiene hardcodeada ninguna lógica de workflow." > Topological sort of the capability graph produces a staged execution plan. Each stage is a set of nodes with no inter-dependency. The orchestrator evaluates the graph; it does not hardcode any workflow logic. </p><div class="code-block"> lint-crate → produces: linted-code fmt-crate → produces: formatted-code build-crate → consumes: linted-code, formatted-code → produces: built-artifact </div></div><div class="feature-box" style="border-left-color: #f59e0b"><div class="feature-icon">🔐</div><h3 class="feature-title" style="color: #f59e0b" data-en="Three-Plane Auth — non-substitutable" data-es="Auth en Tres Planos — no sustituibles" > Three-Plane Auth — non-substitutable </h3><p class="feature-text" data-en="NKeys authenticate the publisher on the transport. Cedar authorizes the workflow at the orchestrator boundary. Vault scopes execution credentials per-node with TTL = node timeout, injected as env vars and revoked on failure — never in NATS messages or logs." data-es="NKeys autentican al publicador en el transporte. Cedar autoriza el workflow en el límite del orquestador. Vault limita las credenciales de ejecución por nodo con TTL = timeout del nodo, inyectadas como env vars y revocadas ante fallo — nunca en mensajes NATS o logs." > NKeys authenticate the publisher on the transport. Cedar authorizes the workflow at the orchestrator boundary. Vault scopes execution credentials per-node with TTL = node timeout, injected as env vars and revoked on failure — never in NATS messages or logs. </p><div class="code-block"> Transport: NATS NKeys (ed25519) Authz: Cedar policies Execution: Vault lease, TTL-scoped </div></div><div class="feature-box" style="border-left-color: #10b981"><div class="feature-icon">🔄</div><h3 class="feature-title" style="color: #10b981" data-en="Saga Compensation — auditable rollback" data-es="Compensación Saga — rollback auditable" > Saga Compensation — auditable rollback </h3><p class="feature-text" data-en="Compensation scripts run in reverse stage order through all previously succeeded stages. The full trace — including compensation failures — is recorded in SurrealDB. 2PC is not feasible across Nushell subprocesses; Saga is the only realistic atomicity model here." data-es="Los scripts de compensación se ejecutan en orden de etapa inverso por todas las etapas previamente exitosas. La traza completa — incluyendo fallos de compensación — queda en SurrealDB. 2PC no es viable entre subprocesos Nushell; Saga es el único modelo de atomicidad realista." > Compensation scripts run in reverse stage order through all previously succeeded stages. The full trace — including compensation failures — is recorded in SurrealDB. 2PC is not feasible across Nushell subprocesses; Saga is the only realistic atomicity model here. </p><div class="code-block"> Stage 0: lint ✓ fmt ✓ → executed Stage 1: build ✗ → compensate Stage 0 ←: undo fmt, undo lint </div></div><div class="feature-box" style="border-left-color: #3b82f6"><div class="feature-icon">⚡</div><h3 class="feature-title" style="color: #3b82f6" data-en="DB-First State — crash recovery is free" data-es="Estado DB-First — recuperación ante crash gratuita" > DB-First State — crash recovery is free </h3><p class="feature-text" data-en="PipelineContext is written to SurrealDB before updating the in-memory DashMap. On restart, the cache is reconstructed from the DB. Multiple instances share one SurrealDB — no split-brain, no coordinator needed. Horizontal scaling is structural, not bolted on." data-es="El PipelineContext se escribe en SurrealDB antes de actualizar el DashMap en memoria. Al reiniciar, el caché se reconstruye desde la DB. Múltiples instancias comparten una SurrealDB — sin split-brain, sin coordinador. El escalado horizontal es estructural." > PipelineContext is written to SurrealDB before updating the in-memory DashMap. On restart, the cache is reconstructed from the DB. Multiple instances share one SurrealDB — no split-brain, no coordinator needed. Horizontal scaling is structural, not bolted on. </p></div><div class="feature-box" style="border-left-color: #8b5cf6"><div class="feature-icon">📦</div><h3 class="feature-title" style="color: #8b5cf6" data-en="OCI Distribution — verified imports" data-es="Distribución OCI — imports verificados" > OCI Distribution — verified imports </h3><p class="feature-text" data-en="nickel typecheck → gitleaks detect → nickel export → sha256sum → oras push with content-hash annotations. The ncl-import-resolver verifies each layer's digest against the annotated hash before exposing it to the Nickel import path — tampered definitions cannot load." data-es="nickel typecheck → gitleaks detect → nickel export → sha256sum → oras push con anotaciones de hash de contenido. El ncl-import-resolver verifica el digest de cada capa contra el hash anotado antes de exponerlo al path de imports Nickel — las definiciones adulteradas no se cargan." > nickel typecheck → gitleaks detect → nickel export → sha256sum → oras push with content-hash annotations. The ncl-import-resolver verifies each layer's digest against the annotated hash before exposing it to the Nickel import path — tampered definitions cannot load. </p></div><div class="feature-box" style="border-left-color: #ec4899"><div class="feature-icon">🐚</div><h3 class="feature-title" style="color: #ec4899" data-en="Nushell Executor — domain-agnostic subprocess" data-es="Executor Nushell — subproceso agnóstico al dominio" > Nushell Executor — domain-agnostic subprocess </h3><p class="feature-text" data-en="Each node spawns nu --no-config-file script.nu. PipelineContext inputs arrive as JSON on stdin; outputs return as JSON on stdout. Each node gets its own process with scoped Vault credentials. Scripts can be tested independently: echo '{}' | nu script.nu." data-es="Cada nodo invoca nu --no-config-file script.nu. Las entradas del PipelineContext llegan como JSON por stdin; las salidas retornan como JSON por stdout. Cada nodo obtiene su propio proceso con credenciales Vault de alcance limitado. Los scripts son testables de forma independiente." > Each node spawns nu --no-config-file script.nu. PipelineContext inputs arrive as JSON on stdin; outputs return as JSON on stdout. Each node gets its own process with scoped Vault credentials. Scripts can be tested independently: echo '{}' | nu script.nu. </p></div></div></section><section class="section"><h2 class="section-title"><span data-en="Architecture & Pipeline Diagrams" data-es="Diagramas de Arquitectura y Pipeline" >Architecture & Pipeline Diagrams</span ></h2><div class="arch-preview"><div class="arch-pair"><div><a href="stratumiops-arch.html" class="arch-img-link"><img class="arch-img arch-dark" src="arch-stratum-orchestrator.svg" alt="Orchestrator Architecture — Dark" /><img class="arch-img arch-light" src="w-arch-stratum-orchestrator.svg" alt="Orchestrator Architecture — Light" /></a><p class="arch-label" data-en="Orchestrator Architecture" data-es="Arquitectura del Orquestador" > Orchestrator Architecture </p></div><div><a href="stratumiops-pipeline.html" class="arch-img-link"><img class="arch-img arch-dark" src="flow-stratum-build-pipeline.svg" alt="Build Pipeline Flow — Dark" /><img class="arch-img arch-light" src="w-flow-stratum-build-pipeline.svg" alt="Build Pipeline Flow — Light" /></a><p class="arch-label" data-en="Build Pipeline Flow" data-es="Flujo del Pipeline de Build" > Build Pipeline Flow </p></div></div><div class="arch-btn-row"><a href="stratumiops-arch.html" class="arch-btn" data-en="Explore Architecture →" data-es="Explorar Arquitectura →" >Explore Architecture →</a ><a href="stratumiops-pipeline.html" class="arch-btn" style=" border-color: var(--accent-cyan); color: var(--accent-cyan); " data-en="Explore Pipeline →" data-es="Explorar Pipeline →" >Explore Pipeline →</a ></div></div></section><section class="section"><h2 class="section-title"><span data-en="Crate Structure" data-es="Estructura de Crates" >Crate Structure</span ></h2><div class="components-grid"><div class="component-item"><span class="component-name">stratum-graph</span><span class="component-domain" data-en="Knowledge" data-es="Conocimiento" >Knowledge</span ><span class="component-role" data-en="ActionNode · Capability · GraphRepository trait" data-es="ActionNode · Capability · trait GraphRepository" >ActionNode · Capability · GraphRepository trait</span ></div><div class="component-item"><span class="component-name">stratum-state</span><span class="component-domain" data-en="Operational" data-es="Operacional" >Operational</span ><span class="component-role" data-en="PipelineRun · StepRecord · StateTracker trait" data-es="PipelineRun · StepRecord · trait StateTracker" >PipelineRun · StepRecord · StateTracker trait</span ></div><div class="component-item"><span class="component-name">platform-nats</span><span class="component-domain" data-en="Transport" data-es="Transporte" >Transport</span ><span class="component-role" data-en="JetStream consumer · NKey auth" data-es="Consumidor JetStream · Auth NKey" >JetStream consumer · NKey auth</span ></div><div class="component-item"><span class="component-name">stratum-orchestrator</span><span class="component-domain" data-en="Coordination" data-es="Coordinación" >Coordination</span ><span class="component-role" data-en="ActionGraph · PipelineContext · StageRunner · auth · executor" data-es="ActionGraph · PipelineContext · StageRunner · auth · executor" >ActionGraph · PipelineContext · StageRunner · auth · executor</span ></div></div></section><section class="section"><h2 class="section-title"><span data-en="Technology Stack" data-es="Stack Tecnológico" >Technology Stack</span ></h2><div class="tech-stack"><span class="tech-badge">Rust</span><span class="tech-badge">Nickel</span><span class="tech-badge">Nushell</span><span class="tech-badge">NATS JetStream</span><span class="tech-badge">SurrealDB</span><span class="tech-badge">Cedar</span><span class="tech-badge">SecretumVault</span><span class="tech-badge">OCI / Zot</span><span class="tech-badge">TypeDialog</span><span class="tech-badge">tokio JoinSet</span><span class="tech-badge">notify (hot-reload)</span><span class="tech-badge">oras</span></div></section><section class="section"><h2 class="section-title"><span data-en="Startup Sequence" data-es="Secuencia de Inicio" >Startup Sequence</span ></h2><div class="features-grid"><div class="feature-box" style="border-left-color: #22d3ee"><div class="feature-icon">🚀</div><h3 class="feature-title" style="color: #22d3ee" data-en="Boot order — deterministic" data-es="Orden de arranque — determinista" > Boot order — deterministic </h3><p class="feature-text" data-en="Each step is a hard dependency for the next. No lazy initialization, no optional deferred loading." data-es="Cada paso es una dependencia fuerte del siguiente. Sin inicialización lazy, sin carga diferida opcional." > Each step is a hard dependency for the next. No lazy initialization, no optional deferred loading. </p><div class="code-block"> 1. TypeDialog config load 2. SurrealDB connect 3. NATS JetStream connect 4. OCI Nickel import resolve 5. ActionGraph build 6. notify watcher start 7. Cedar policies init 8. HTTP server (health + agent cb) 9. JetStream pull loop </div></div><div class="feature-box" style="border-left-color: #a78bfa"><div class="feature-icon">📊</div><h3 class="feature-title" style="color: #a78bfa" data-en="Accepted trade-offs" data-es="Trade-offs aceptados" > Accepted trade-offs </h3><p class="feature-text" data-en="nickel export is a subprocess call per file at startup — ~50ms per node file, mitigated by parallel JoinSet load. Each node execution spawns a Nushell process — observable latency for sub-second scripts, acceptable for CI/CD. OCI cold starts require layer pulls, mitigated by local digest cache." data-es="nickel export es una llamada a subproceso por archivo al inicio — ~50ms por archivo de nodo, mitigado con carga paralela en JoinSet. Cada ejecución de nodo invoca un proceso Nushell — latencia observable para scripts sub-segundo, aceptable para CI/CD. Los cold starts OCI requieren descargar capas, mitigado con caché local de digests." > nickel export is a subprocess call per file at startup — ~50ms per node file, mitigated by parallel JoinSet load. Each node execution spawns a Nushell process — observable latency for sub-second scripts, acceptable for CI/CD. OCI cold starts require layer pulls, mitigated by local digest cache. </p></div></div></section><div class="cta-section"><h2 class="cta-title" data-en="New workflows — zero orchestrator changes." data-es="Nuevos workflows — cero cambios en el orquestador." > New workflows — zero orchestrator changes. </h2><p style="color: #94a3b8; margin-bottom: 2rem; font-size: 1.05rem" data-en="Add .ncl files. The graph resolves the rest." data-es="Añade archivos .ncl. El grafo resuelve el resto." > Add .ncl files. The graph resolves the rest. </p><a href="stratumiops-arch.html" class="cta-button" data-en="Explore Architecture →" data-es="Explorar Arquitectura →" >Explore Architecture →</a ></div><footer><p data-en="StratumIOps — Stratum Orchestrator" data-es="StratumIOps — Stratum Orchestrator" > StratumIOps — Stratum Orchestrator </p><p data-en="Graph-driven · Stateless · Auditable · OCI-distributed" data-es="Basado en grafos · Sin estado · Auditable · Distribuido vía OCI" > Graph-driven · Stateless · Auditable · OCI-distributed </p><p style="margin-top: 0.8rem; font-size: 0.8rem" data-en="ADR-003 | Rust + Nickel + Nushell + NATS + SurrealDB" data-es="ADR-003 | Rust + Nickel + Nushell + NATS + SurrealDB" > ADR-003 | Rust + Nickel + Nushell + NATS + SurrealDB </p></footer></div><script> const THEME_KEY = "stratumiops-theme";const LANG_KEY = "stratumiops-lang";const TERMS_RE = /\b(ActionGraph|ActionNode|PipelineContext|StageRunner|RuleEngine|DashMap|JoinSet|CancellationToken|SurrealDB|NKeys|Cedar|SecretumVault|Vault|Nickel|Nushell|TypeDialog|Zot|JetStream|ncl-import-resolver|compensate\.nu|build-crate|lint-crate|fmt-crate|install-crate|linted-code|formatted-code|built-artifact|ed25519|sha256|tokio::task::JoinSet|input_schemas|output_schemas|notify)\b|\.ncl\b/g;function applyTermHighlight(){document.querySelectorAll(".char-card p").forEach((el)=>{el.innerHTML = el.textContent.replace(TERMS_RE,'<span class="t">$&</span>',);});}function setTheme(theme){localStorage.setItem(THEME_KEY,theme);const isDark = theme !== "light";document.documentElement.classList.toggle("light-mode",!isDark);document.getElementById("theme-icon").textContent = isDark ? "🌙" : "☀️";}function toggleTheme(){setTheme(localStorage.getItem(THEME_KEY)=== "light" ? "dark" : "light",);}function switchLanguage(lang){localStorage.setItem(LANG_KEY,lang);document.querySelectorAll(".nav-btn[data-lang]").forEach((btn)=>{btn.classList.toggle("active",btn.dataset.lang === lang);});document.querySelectorAll("[data-en][data-es]").forEach((el)=>{const content = el.dataset[lang];if(!content)return;if(["H1","H2","H3"].includes(el.tagName)){el.innerHTML = content;}else{el.textContent = content;}});document.documentElement.lang = lang;applyTermHighlight();}document.addEventListener("DOMContentLoaded",()=>{setTheme(localStorage.getItem(THEME_KEY)|| "dark");switchLanguage(localStorage.getItem(LANG_KEY)|| "en");});</script></body></html> |