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
2 changes: 2 additions & 0 deletions website/content/docs/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Architecture />

## Configuration

```bash
Expand Down
2 changes: 2 additions & 0 deletions website/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Architecture />

## Where to start

- **[Getting started](/docs/getting-started)** - install, point at a light-run instance, run the example.
Expand Down
4 changes: 4 additions & 0 deletions website/content/docs/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Dag />

## Folder structure

```
Expand Down Expand Up @@ -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**.

<NetworkServices />

```json
{
"id": "claude-wf",
Expand Down
109 changes: 109 additions & 0 deletions website/src/components/diagrams/Architecture.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<figure className={styles.figure}>
<svg
className={styles.svg}
viewBox={`0 0 680 ${height}`}
role="img"
aria-label="light-process delegates over HTTP to light-run, which calls the light-runner Docker SDK, which runs the container in Docker."
>
{/* down rail (request) */}
<line className={styles.edge} x1={120} y1={railTop} x2={120} y2={railBottom} />
{/* up rail (result) */}
<line className={styles.edge} x1={552} y1={railTop} x2={552} y2={railBottom} />

{LAYERS.map((l, i) => {
const y = TOP + i * (ROW_H + GAP);
return (
<g key={l.name}>
<rect
className={l.current ? styles.node : styles.nodeMuted}
x={BOX_X}
y={y}
width={BOX_W}
height={ROW_H}
rx={3}
/>
<text className={styles.label} x={BOX_X + 18} y={y + 23}>
{l.name}
</text>
<text className={styles.sub} x={BOX_X + 18} y={y + 39}>
{l.sub}
</text>
{/* rail entry/exit ticks */}
<line className={styles.edge} x1={120} y1={y + ROW_H / 2} x2={BOX_X} y2={y + ROW_H / 2} />
<line
className={styles.edge}
x1={BOX_X + BOX_W}
y1={y + ROW_H / 2}
x2={552}
y2={y + ROW_H / 2}
/>
</g>
);
})}

{/* arrow captions */}
<text className={styles.tag} x={28} y={railTop - 6}>
request
</text>
<text className={styles.tag} x={28} y={railTop + 8}>
POST /run
</text>
<text className={styles.tag} x={566} y={railTop - 6}>
result
</text>
<text className={styles.tag} x={566} y={railTop + 8}>
RunState
</text>

{/* traveling tokens */}
<circle
className={`${styles.travelDown} ${styles.glowDot}`}
style={{ ['--travel' as string]: `${travel}px` }}
cx={120}
cy={railTop}
r={4}
/>
<circle
className={styles.travelUp}
style={{ ['--travel' as string]: `${travel}px` }}
cx={552}
cy={railBottom}
r={4}
/>
</svg>
<figcaption className={styles.caption}>
<span>Fig. - one request, three layers, one isolation boundary.</span>
<span className={styles.captionRight}>no upward coupling</span>
</figcaption>
</figure>
);
}
106 changes: 106 additions & 0 deletions website/src/components/diagrams/Dag.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<figure className={styles.figure}>
<svg
className={styles.svg}
viewBox="0 0 680 256"
role="img"
aria-label="A workflow graph: fetch fans out to parse and store in parallel; parse links to report only when its score exceeds a threshold."
>
<defs>
<marker id="dag-arrow" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path className={styles.arrowLive} d="M0 0 L8 4 L0 8 z" />
</marker>
</defs>

{/* edges (drawn first so nodes sit on top) */}
<path className={styles.edge} d={edge('fetch', 'parse')} markerEnd="url(#dag-arrow)" />
<path className={styles.edgeLive} d={edge('fetch', 'parse')} />
<path className={styles.edge} d={edge('fetch', 'store')} markerEnd="url(#dag-arrow)" />
<path className={styles.edgeLive} d={edge('fetch', 'store')} />
<path className={styles.edge} d={edge('parse', 'report')} markerEnd="url(#dag-arrow)" />
<path className={styles.edgeLive} d={edge('parse', 'report')} />

{/* edge condition labels */}
<text className={styles.tag} x={196} y={68}>
when ok:true
</text>
<text className={styles.tag} x={196} y={170}>
fan-out
</text>
<text className={styles.tag} x={430} y={28}>
when score &gt; 0.8
</text>

{/* nodes */}
{NODES.map((n) => (
<g key={n.id}>
<rect
className={`${styles.node} ${n.pulse ? styles[n.pulse] : ''}`}
x={n.x}
y={n.y}
width={W}
height={H}
rx={3}
/>
<text className={styles.label} x={n.x + 16} y={n.y + 21}>
{n.label}
</text>
<text className={styles.sub} x={n.x + 16} y={n.y + 36}>
{n.sub}
</text>
</g>
))}
</svg>
<figcaption className={styles.caption}>
<span>Fig. - nodes fan out; a link fires only when its condition holds.</span>
<span className={styles.captionRight}>data + conditions</span>
</figcaption>
</figure>
);
}
104 changes: 104 additions & 0 deletions website/src/components/diagrams/NetworkServices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import styles from './diagrams.module.css';

/*
* The networks/services proxy pattern. A run-scoped Docker network
* (`lp-<run>-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 (
<figure className={styles.figure}>
<svg
className={styles.svg}
viewBox="0 0 680 240"
role="img"
aria-label="A run-scoped network contains the workload and a proxy service that holds the API key. The workload reaches the proxy by hostname with no secret; only the proxy attaches the Bearer key on the way out to the external API."
>
<defs>
<marker id="ns-arrow-live" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path className={styles.arrowLive} d="M0 0 L8 4 L0 8 z" />
</marker>
<marker id="ns-arrow-warm" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path className={styles.arrowWarm} d="M0 0 L8 4 L0 8 z" />
</marker>
</defs>

{/* run-scoped network boundary */}
<rect className={styles.panelDashed} x={BOX.x} y={BOX.y} width={BOX.w} height={BOX.h} rx={4} />
<text className={styles.tag} x={BOX.x + 12} y={BOX.y + 20}>
network: lp-&lt;run&gt;-svc-net
</text>

{/* edges first */}
<line className={styles.edge} x1={a.x} y1={a.y} x2={b.x} y2={b.y} markerEnd="url(#ns-arrow-live)" />
<line className={styles.edgeLive} x1={a.x} y1={a.y} x2={b.x} y2={b.y} />
<line className={styles.edge} x1={c.x} y1={c.y} x2={d.x} y2={d.y} markerEnd="url(#ns-arrow-warm)" />
<line className={styles.edgeWarm} x1={c.x} y1={c.y} x2={d.x} y2={d.y} />

{/* edge labels */}
<text className={styles.tag} x={a.x + 6} y={a.y - 8}>
http://proxy
</text>
<text className={styles.tag} x={a.x + 6} y={a.y + 18}>
no secret
</text>
<text className={styles.tagWarm} x={c.x + 6} y={c.y - 8}>
+ Bearer key
</text>

{/* workload node */}
<rect className={styles.node} x={WORKLOAD.x} y={WORKLOAD.y} width={WORKLOAD.w} height={WORKLOAD.h} rx={3} />
<text className={styles.label} x={WORKLOAD.x + 16} y={WORKLOAD.y + 24}>
workload
</text>
<text className={styles.sub} x={WORKLOAD.x + 16} y={WORKLOAD.y + 41}>
no API key in env
</text>

{/* proxy node (holds the secret) */}
<rect className={`${styles.nodeWarm} ${styles.pulse}`} x={PROXY.x} y={PROXY.y} width={PROXY.w} height={PROXY.h} rx={3} />
<text className={styles.label} x={PROXY.x + 16} y={PROXY.y + 24}>
proxy
</text>
<text className={styles.sub} x={PROXY.x + 16} y={PROXY.y + 41}>
holds the API key
</text>

{/* external API (outside the boundary) */}
<rect className={styles.nodeMuted} x={EXTERNAL.x} y={EXTERNAL.y} width={EXTERNAL.w} height={EXTERNAL.h} rx={3} />
<text className={styles.label} x={EXTERNAL.x + 16} y={EXTERNAL.y + 24}>
external API
</text>
<text className={styles.sub} x={EXTERNAL.x + 16} y={EXTERNAL.y + 41}>
api.example.com
</text>
</svg>
<figcaption className={styles.caption}>
<span>Fig. - the workload reaches the API by hostname; the key never leaves the proxy.</span>
<span className={styles.captionRight}>secret isolation</span>
</figcaption>
</figure>
);
}
Loading
Loading