diff --git a/website/content/docs/docker.mdx b/website/content/docs/docker.mdx index 0d31fd5..193a0f8 100644 --- a/website/content/docs/docker.mdx +++ b/website/content/docs/docker.mdx @@ -4,6 +4,8 @@ title: "Execution (light-run)" light-process no longer talks to Docker directly. Every node runs inside a container spawned by the [light-run](https://github.com/enixCode/light-run) HTTP service. + + ## Configuration ```bash diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx index 13f22ef..5dc3b87 100644 --- a/website/content/docs/index.mdx +++ b/website/content/docs/index.mdx @@ -7,6 +7,8 @@ description: A DAG workflow engine that orchestrates code in Docker containers v Execution itself is delegated: each node runs on a [light-run](https://github.com/enixCode/light-run) instance over HTTP, which in turn drives [light-runner](https://github.com/enixCode/light-runner) and Docker. light-process never touches Docker directly; it owns orchestration, identity, and structured results. + + ## Where to start - **[Getting started](/docs/getting-started)** - install, point at a light-run instance, run the example. diff --git a/website/content/docs/workflows.mdx b/website/content/docs/workflows.mdx index f02975c..07856d2 100644 --- a/website/content/docs/workflows.mdx +++ b/website/content/docs/workflows.mdx @@ -4,6 +4,8 @@ title: "Workflows" A workflow is a directed acyclic graph (DAG) of nodes connected by links. Each node runs code in a Docker container. + + ## Folder structure ``` @@ -168,6 +170,8 @@ Isolation is topological: put each group of nodes on its own `networkDef` and th A `service` is a long-running container started **before** the DAG and stopped **after** it, living outside the DAG flow (it is never a node and never an entry node). The canonical use is a network proxy that holds a provider API key so a workload node can reach another provider by hostname **without the secret ever entering the workload container**. + + ```json { "id": "claude-wf", diff --git a/website/src/components/diagrams/Architecture.tsx b/website/src/components/diagrams/Architecture.tsx new file mode 100644 index 0000000..b91ddf3 --- /dev/null +++ b/website/src/components/diagrams/Architecture.tsx @@ -0,0 +1,109 @@ +import styles from './diagrams.module.css'; + +/* + * The strict top-down delegation chain: + * light-process (graph) -> light-run (HTTP) -> light-runner (Docker SDK) -> Docker + * A request token travels down the left rail, a result token back up the right. + * Pure CSS animation, no client JS - safe in static export. + */ + +const LAYERS = [ + { name: 'light-process', sub: 'DAG orchestrator - evaluates the graph', current: true }, + { name: 'light-run', sub: 'HTTP wrapper - POST /run, serves artifacts' }, + { name: 'light-runner', sub: 'Docker SDK - dockerode, extract, state' }, + { name: 'Docker', sub: 'isolation boundary - the container runs here' }, +]; + +const ROW_H = 52; +const GAP = 18; +const TOP = 16; +const BOX_X = 150; +const BOX_W = 372; + +export function Architecture() { + const height = TOP * 2 + LAYERS.length * ROW_H + (LAYERS.length - 1) * GAP; + const railTop = TOP + ROW_H / 2; + const railBottom = TOP + (LAYERS.length - 1) * (ROW_H + GAP) + ROW_H / 2; + const travel = railBottom - railTop; + + return ( +
+ + {/* down rail (request) */} + + {/* up rail (result) */} + + + {LAYERS.map((l, i) => { + const y = TOP + i * (ROW_H + GAP); + return ( + + + + {l.name} + + + {l.sub} + + {/* rail entry/exit ticks */} + + + + ); + })} + + {/* arrow captions */} + + request + + + POST /run + + + result + + + RunState + + + {/* traveling tokens */} + + + +
+ Fig. - one request, three layers, one isolation boundary. + no upward coupling +
+
+ ); +} diff --git a/website/src/components/diagrams/Dag.tsx b/website/src/components/diagrams/Dag.tsx new file mode 100644 index 0000000..36efef1 --- /dev/null +++ b/website/src/components/diagrams/Dag.tsx @@ -0,0 +1,106 @@ +import styles from './diagrams.module.css'; + +/* + * The DAG model: entry node fans out into a parallel batch; links carry output + * to the next node, optionally gated by a `when` condition. Live edges animate + * a flowing dash; the conditional edge is labelled with its predicate. Pure CSS, + * no client JS - safe in static export. Reused (scaled) as the landing hero. + */ + +type NodeDef = { + id: string; + x: number; + y: number; + label: string; + sub: string; + cls?: string; + pulse?: string; +}; + +const W = 124; +const H = 46; + +const NODES: NodeDef[] = [ + { id: 'fetch', x: 40, y: 110, label: 'fetch', sub: 'node:24', pulse: 'pulse' }, + { id: 'parse', x: 286, y: 40, label: 'parse', sub: 'python', pulse: 'pulseB' }, + { id: 'store', x: 286, y: 180, label: 'store', sub: 'node:24', pulse: 'pulseC' }, + { id: 'report', x: 516, y: 40, label: 'report', sub: 'python', pulse: 'pulseB' }, +]; + +function center(n: NodeDef) { + return { cx: n.x + W / 2, cy: n.y + H / 2 }; +} + +export function Dag() { + const byId = Object.fromEntries(NODES.map((n) => [n.id, n])); + const edge = (from: string, to: string) => { + const a = center(byId[from]); + const b = center(byId[to]); + const x1 = byId[from].x + W; + const y1 = a.cy; + const x2 = byId[to].x; + const y2 = b.cy; + const mx = (x1 + x2) / 2; + return `M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`; + }; + + return ( +
+ + + + + + + + {/* edges (drawn first so nodes sit on top) */} + + + + + + + + {/* edge condition labels */} + + when ok:true + + + fan-out + + + when score > 0.8 + + + {/* nodes */} + {NODES.map((n) => ( + + + + {n.label} + + + {n.sub} + + + ))} + +
+ Fig. - nodes fan out; a link fires only when its condition holds. + data + conditions +
+
+ ); +} diff --git a/website/src/components/diagrams/NetworkServices.tsx b/website/src/components/diagrams/NetworkServices.tsx new file mode 100644 index 0000000..8903a0a --- /dev/null +++ b/website/src/components/diagrams/NetworkServices.tsx @@ -0,0 +1,104 @@ +import styles from './diagrams.module.css'; + +/* + * The networks/services proxy pattern. A run-scoped Docker network + * (`lp--svc-net`) holds the workload and a sidecar proxy service. The + * secret (API key) lives only in the proxy: the workload reaches it by + * hostname, no credential in the workload's env. The proxy is the single hop + * that attaches the key on the way out to the external API. Pure CSS animation, + * no client JS - safe in static export. + */ + +const BOX = { x: 20, y: 52, w: 432, h: 140 }; + +const WORKLOAD = { x: 44, y: 100, w: 150, h: 54 }; +const PROXY = { x: 258, y: 100, w: 150, h: 54 }; +const EXTERNAL = { x: 512, y: 100, w: 148, h: 54 }; + +function rightMid(n: { x: number; y: number; w: number; h: number }) { + return { x: n.x + n.w, y: n.y + n.h / 2 }; +} +function leftMid(n: { x: number; y: number; w: number; h: number }) { + return { x: n.x, y: n.y + n.h / 2 }; +} + +export function NetworkServices() { + const a = rightMid(WORKLOAD); + const b = leftMid(PROXY); + const c = rightMid(PROXY); + const d = leftMid(EXTERNAL); + + return ( +
+ + + + + + + + + + + {/* run-scoped network boundary */} + + + network: lp-<run>-svc-net + + + {/* edges first */} + + + + + + {/* edge labels */} + + http://proxy + + + no secret + + + + Bearer key + + + {/* workload node */} + + + workload + + + no API key in env + + + {/* proxy node (holds the secret) */} + + + proxy + + + holds the API key + + + {/* external API (outside the boundary) */} + + + external API + + + api.example.com + + +
+ Fig. - the workload reaches the API by hostname; the key never leaves the proxy. + secret isolation +
+
+ ); +} diff --git a/website/src/components/diagrams/diagrams.module.css b/website/src/components/diagrams/diagrams.module.css new file mode 100644 index 0000000..0101819 --- /dev/null +++ b/website/src/components/diagrams/diagrams.module.css @@ -0,0 +1,266 @@ +/* + * Self-contained design tokens + animations for the light-process docs + * diagrams. The docs site (Fumadocs) runs a neutral/blue theme; these figures + * deliberately carry the light-* landing aesthetic (near-black canvas, ice-blue + * accent, Fraunces/JetBrains Mono voice) as a framed "diagram panel", so they + * read the same in light and dark docs. Tokens are hardcoded here on purpose: + * the landing CSS variables are not in scope inside the docs. + */ + +.figure { + --d-ink: #050608; + --d-ink-2: #0a0c10; + --d-line: #1a1d24; + --d-line-hot: #242933; + --d-paper: #edeef0; + --d-paper-dim: #8a8f9a; + --d-paper-dimmer: #4d525c; + --d-halo: #c7e8ff; + --d-halo-soft: #7fddff; + --d-amber: #ffb855; + --d-ember: #ff6b35; + + position: relative; + margin: 1.75rem 0; + padding: 1.4rem 1.4rem 1rem; + background: radial-gradient(120% 140% at 50% 0%, var(--d-ink-2), var(--d-ink) 70%); + border: 1px solid var(--d-line); + border-radius: 3px; + overflow: hidden; + color: var(--d-paper); + font-family: + 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; +} + +/* Corner brackets, the landing's signature motif. */ +.figure::before, +.figure::after { + content: ''; + position: absolute; + width: 14px; + height: 14px; + border: 1px solid var(--d-halo-soft); + opacity: 0.5; + pointer-events: none; +} +.figure::before { + top: 8px; + left: 8px; + border-right: 0; + border-bottom: 0; +} +.figure::after { + bottom: 8px; + right: 8px; + border-left: 0; + border-top: 0; +} + +.svg { + display: block; + width: 100%; + height: auto; +} + +.caption { + margin-top: 0.9rem; + display: flex; + justify-content: space-between; + gap: 1rem; + font-size: 10.5px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--d-paper-dimmer); +} +.captionRight { + color: var(--d-halo-soft); + opacity: 0.7; +} + +/* --- SVG primitives (applied via className on SVG elements) --- */ + +.panel { + fill: var(--d-ink-2); + stroke: var(--d-line-hot); + stroke-width: 1; +} +.panelDashed { + fill: rgba(199, 232, 255, 0.03); + stroke: var(--d-halo-soft); + stroke-width: 1; + stroke-dasharray: 5 4; + opacity: 0.8; +} +.node { + fill: var(--d-ink-2); + stroke: var(--d-halo); + stroke-width: 1.25; +} +.nodeMuted { + fill: var(--d-ink-2); + stroke: var(--d-line-hot); + stroke-width: 1.25; +} +.nodeWarm { + fill: var(--d-ink-2); + stroke: var(--d-amber); + stroke-width: 1.25; +} + +.label { + fill: var(--d-paper); + font-family: 'Fraunces', Georgia, serif; + font-style: italic; + font-size: 15px; +} +.labelMono { + fill: var(--d-paper); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 12px; +} +.sub { + fill: var(--d-paper-dim); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 10px; + letter-spacing: 0.04em; +} +.tag { + fill: var(--d-halo-soft); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 9.5px; + letter-spacing: 0.06em; +} +.tagWarm { + fill: var(--d-amber); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 9.5px; + letter-spacing: 0.06em; +} + +.arrowLive { + fill: var(--d-halo); +} +.arrowMuted { + fill: var(--d-paper-dimmer); +} +.arrowWarm { + fill: var(--d-amber); +} + +.edge { + fill: none; + stroke: var(--d-line-hot); + stroke-width: 1.5; +} +.edgeLive { + fill: none; + stroke: var(--d-halo); + stroke-width: 1.5; + stroke-dasharray: 4 5; + animation: flow 1.1s linear infinite; +} +.edgeWarm { + fill: none; + stroke: var(--d-amber); + stroke-width: 1.5; + stroke-dasharray: 4 5; + animation: flow 1.4s linear infinite; +} + +/* A glowing pulse on a node outline. */ +.pulse { + animation: pulse 2.4s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +.pulseB { + animation: pulse 2.4s ease-in-out infinite 0.8s; + transform-box: fill-box; + transform-origin: center; +} +.pulseC { + animation: pulse 2.4s ease-in-out infinite 1.6s; + transform-box: fill-box; + transform-origin: center; +} + +/* A token that travels along a path (architecture rails / proxy hop). */ +.travelDown { + fill: var(--d-halo); + animation: travelDown 2.6s ease-in-out infinite; +} +.travelUp { + fill: var(--d-halo-soft); + animation: travelUp 2.6s ease-in-out infinite 1.3s; +} +.glowDot { + fill: var(--d-halo); + filter: drop-shadow(0 0 4px var(--d-halo-soft)); +} + +@keyframes flow { + to { + stroke-dashoffset: -18; + } +} +@keyframes pulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} +@keyframes travelDown { + 0% { + opacity: 0; + transform: translateY(0); + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(var(--travel, 210px)); + } +} +@keyframes travelUp { + 0% { + opacity: 0; + transform: translateY(0); + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--travel, 210px))); + } +} + +@media (prefers-reduced-motion: reduce) { + .edgeLive, + .edgeWarm, + .pulse, + .pulseB, + .pulseC, + .travelDown, + .travelUp { + animation: none; + } + .edgeLive, + .edgeWarm { + stroke-dasharray: 4 5; + } + .travelDown, + .travelUp { + opacity: 0; + } +} diff --git a/website/src/components/diagrams/index.ts b/website/src/components/diagrams/index.ts new file mode 100644 index 0000000..a6ad7d3 --- /dev/null +++ b/website/src/components/diagrams/index.ts @@ -0,0 +1,3 @@ +export { Architecture } from './Architecture'; +export { Dag } from './Dag'; +export { NetworkServices } from './NetworkServices'; diff --git a/website/src/components/mdx.tsx b/website/src/components/mdx.tsx index a640575..dc68724 100644 --- a/website/src/components/mdx.tsx +++ b/website/src/components/mdx.tsx @@ -1,9 +1,13 @@ import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; +import { Architecture, Dag, NetworkServices } from './diagrams'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, + Architecture, + Dag, + NetworkServices, ...components, } satisfies MDXComponents; }