From 98b1c46a247cb472355468274ec920e4feb392b7 Mon Sep 17 00:00:00 2001
From: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Date: Mon, 31 Aug 2026 09:00:26 +0000
Subject: [PATCH 01/11] docs: prototype interactive sandbox diagram
---
assets/css/interactive-diagram.css | 374 ++++++++++++++++++
assets/css/style.css | 1 +
assets/js/interactive-diagram.js | 300 ++++++++++++++
content/manuals/ai/sandboxes/architecture.md | 8 +
.../diagrams/credential-injection.yaml | 149 +++++++
.../_partials/interactive-diagram-script.html | 4 +
layouts/_shortcodes/interactive-diagram.html | 77 ++++
.../interactive-diagram.markdown.md | 13 +
layouts/baseof.html | 4 +
9 files changed, 930 insertions(+)
create mode 100644 assets/css/interactive-diagram.css
create mode 100644 assets/js/interactive-diagram.js
create mode 100644 content/manuals/ai/sandboxes/diagrams/credential-injection.yaml
create mode 100644 layouts/_partials/interactive-diagram-script.html
create mode 100644 layouts/_shortcodes/interactive-diagram.html
create mode 100644 layouts/_shortcodes/interactive-diagram.markdown.md
diff --git a/assets/css/interactive-diagram.css b/assets/css/interactive-diagram.css
new file mode 100644
index 000000000000..4cfcc1bb85f1
--- /dev/null
+++ b/assets/css/interactive-diagram.css
@@ -0,0 +1,374 @@
+.interactive-diagram {
+ --diagram-accent: var(--color-blue-500);
+ --diagram-accent-soft: var(--color-blue-50);
+ --diagram-border: var(--color-gray-200);
+ --diagram-ink: var(--color-gray-900);
+ --diagram-muted: var(--color-gray-600);
+ overflow: hidden;
+ border: 1px solid var(--diagram-border);
+ border-radius: 0.5rem;
+ background: white;
+ color: var(--diagram-ink);
+}
+
+.dark .interactive-diagram {
+ --diagram-accent: var(--color-blue-300);
+ --diagram-accent-soft: var(--color-blue-950);
+ --diagram-border: var(--color-gray-700);
+ --diagram-ink: var(--color-gray-100);
+ --diagram-muted: var(--color-gray-400);
+ background: var(--color-gray-950);
+}
+
+.interactive-diagram__header {
+ padding: 1.25rem 1.5rem 1rem;
+ border-bottom: 1px solid var(--diagram-border);
+}
+
+.interactive-diagram__title,
+.interactive-diagram__description,
+.interactive-diagram__step-title,
+.interactive-diagram__step-body,
+.interactive-diagram__state {
+ margin: 0;
+}
+
+.interactive-diagram__title {
+ color: var(--diagram-ink);
+ font-size: 1rem;
+ font-weight: 650;
+}
+
+.interactive-diagram__description {
+ margin-top: 0.25rem;
+ color: var(--diagram-muted);
+ font-size: 0.875rem;
+ line-height: 1.45;
+}
+
+.interactive-diagram__stage {
+ overflow-x: auto;
+ background-color: var(--diagram-accent-soft);
+ background-image:
+ linear-gradient(
+ to right,
+ color-mix(in srgb, var(--diagram-border) 28%, transparent) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ color-mix(in srgb, var(--diagram-border) 28%, transparent) 1px,
+ transparent 1px
+ );
+ background-size: 24px 24px;
+}
+
+.interactive-diagram__svg {
+ display: block;
+ width: 100%;
+ min-width: 760px;
+ height: auto;
+ font-family: "Roboto Flex", sans-serif;
+}
+
+.interactive-diagram__boundary rect {
+ fill: color-mix(in srgb, white 72%, transparent);
+ stroke: var(--diagram-border);
+ stroke-width: 1.5;
+ stroke-dasharray: 5 5;
+}
+
+.dark .interactive-diagram__boundary rect {
+ fill: color-mix(in srgb, var(--color-gray-900) 82%, transparent);
+}
+
+.interactive-diagram__boundary--sandbox rect {
+ stroke: color-mix(in srgb, var(--diagram-accent) 65%, var(--diagram-border));
+ stroke-width: 2;
+}
+
+.interactive-diagram__boundary-label {
+ fill: var(--diagram-muted);
+ font-family: "Roboto Mono", monospace;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.045em;
+ text-transform: uppercase;
+}
+
+.interactive-diagram__edge {
+ stroke: var(--color-gray-400);
+ stroke-width: 2;
+ stroke-dasharray: 5 7;
+ opacity: 0.5;
+ transition:
+ opacity 180ms ease,
+ stroke 180ms ease,
+ stroke-width 180ms ease;
+}
+
+.interactive-diagram__svg marker path {
+ fill: context-stroke;
+}
+
+.interactive-diagram__edge.is-active {
+ stroke: var(--diagram-accent);
+ stroke-width: 3;
+ stroke-dasharray: none;
+ opacity: 1;
+}
+
+.interactive-diagram__node rect {
+ fill: white;
+ stroke: var(--diagram-border);
+ stroke-width: 1.5;
+ transition:
+ fill 180ms ease,
+ stroke 180ms ease,
+ stroke-width 180ms ease;
+}
+
+.dark .interactive-diagram__node rect {
+ fill: var(--color-gray-900);
+}
+
+.interactive-diagram__node--context {
+ opacity: 0.64;
+}
+
+.interactive-diagram__node--secret rect {
+ fill: var(--color-green-50);
+ stroke: var(--color-green-400);
+}
+
+.dark .interactive-diagram__node--secret rect {
+ fill: var(--color-green-950);
+ stroke: var(--color-green-700);
+}
+
+.interactive-diagram__node.is-active rect {
+ fill: var(--diagram-accent-soft);
+ stroke: var(--diagram-accent);
+ stroke-width: 3;
+}
+
+.interactive-diagram__node-label {
+ fill: var(--diagram-ink);
+ font-size: 14px;
+ font-weight: 650;
+}
+
+.interactive-diagram__node-description {
+ fill: var(--diagram-muted);
+ font-size: 11px;
+}
+
+.interactive-diagram__token rect {
+ fill: var(--diagram-ink);
+ stroke: white;
+ stroke-width: 2;
+ filter: drop-shadow(0 2px 3px rgb(18 21 31 / 0.18));
+}
+
+.interactive-diagram__token-label {
+ fill: white;
+ font-family: "Roboto Mono", monospace;
+ font-size: 11px;
+ font-weight: 600;
+ text-anchor: middle;
+}
+
+.interactive-diagram__status {
+ min-height: 7.5rem;
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--diagram-border);
+ border-bottom: 1px solid var(--diagram-border);
+ background: white;
+}
+
+.dark .interactive-diagram__status {
+ background: var(--color-gray-950);
+}
+
+.interactive-diagram__step-copy {
+ display: grid;
+ grid-template-columns: 3.5rem 1fr;
+ gap: 0.75rem;
+}
+
+.interactive-diagram__step-number {
+ color: var(--diagram-accent);
+ font-family: "Roboto Mono", monospace;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+}
+
+.interactive-diagram__step-title {
+ color: var(--diagram-ink);
+ font-size: 0.95rem;
+ font-weight: 650;
+}
+
+.interactive-diagram__step-body {
+ margin-top: 0.25rem;
+ color: var(--diagram-muted);
+ font-size: 0.85rem;
+ line-height: 1.45;
+}
+
+.interactive-diagram__state {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: 0.75rem;
+ margin-left: 4.25rem;
+ color: var(--diagram-muted);
+ font-size: 0.78rem;
+}
+
+.interactive-diagram__state code {
+ padding: 0.05rem 0.35rem;
+ border-radius: 0.2rem;
+ background: var(--diagram-accent-soft);
+ color: var(--diagram-ink);
+ font-size: 0.75rem;
+}
+
+.interactive-diagram__controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.8rem 1rem;
+}
+
+.interactive-diagram__button {
+ min-height: 2.25rem;
+ padding: 0.4rem 0.7rem;
+ border: 1px solid var(--diagram-border);
+ border-radius: 0.25rem;
+ background: transparent;
+ color: var(--diagram-ink);
+ font-size: 0.78rem;
+ font-weight: 600;
+}
+
+.interactive-diagram__button:hover:not(:disabled) {
+ border-color: var(--diagram-accent);
+ background: var(--diagram-accent-soft);
+}
+
+.interactive-diagram__button:focus-visible,
+.interactive-diagram__progress-step:focus-visible {
+ outline: 3px solid var(--color-blue-focusvisible);
+ outline-offset: 2px;
+}
+
+.interactive-diagram__button:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+}
+
+.interactive-diagram__button--play {
+ min-width: 3.8rem;
+ border-color: var(--diagram-accent);
+ color: var(--diagram-accent);
+}
+
+.interactive-diagram__progress {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ gap: 0.45rem;
+}
+
+.interactive-diagram__progress-step {
+ position: relative;
+ width: 1.75rem;
+ height: 1.75rem;
+ padding: 0;
+ border: 0;
+ background: transparent;
+}
+
+.interactive-diagram__progress-step::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 0.6rem;
+ height: 0.6rem;
+ border: 1px solid var(--color-gray-400);
+ border-radius: 50%;
+ background: transparent;
+ content: "";
+ transform: translate(-50%, -50%);
+}
+
+.interactive-diagram__progress-step.is-active::before {
+ border-color: var(--diagram-accent);
+ background: var(--diagram-accent);
+ box-shadow: 0 0 0 3px var(--color-blue-focus);
+}
+
+.interactive-diagram__fallback {
+ padding: 1rem 1.5rem;
+}
+
+.interactive-diagram.is-enhanced .interactive-diagram__fallback {
+ display: none;
+}
+
+.interactive-diagram:not(.is-enhanced) .interactive-diagram__stage,
+.interactive-diagram:not(.is-enhanced) .interactive-diagram__status,
+.interactive-diagram:not(.is-enhanced) .interactive-diagram__controls {
+ display: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .interactive-diagram__token.is-entering {
+ animation: interactive-diagram-token 280ms ease-out;
+ }
+
+ @keyframes interactive-diagram-token {
+ from {
+ opacity: 0;
+ }
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .interactive-diagram__edge,
+ .interactive-diagram__node rect {
+ transition: none;
+ }
+}
+
+@media (max-width: 640px) {
+ .interactive-diagram__header,
+ .interactive-diagram__status {
+ padding-inline: 1rem;
+ }
+
+ .interactive-diagram__step-copy {
+ grid-template-columns: 1fr;
+ gap: 0.35rem;
+ }
+
+ .interactive-diagram__state {
+ margin-left: 0;
+ }
+
+ .interactive-diagram__controls {
+ flex-wrap: wrap;
+ }
+
+ .interactive-diagram__progress {
+ order: 4;
+ flex-basis: 100%;
+ padding-top: 0.35rem;
+ }
+
+ .interactive-diagram__button:last-child {
+ margin-left: auto;
+ }
+}
diff --git a/assets/css/style.css b/assets/css/style.css
index f63fc6f5aec2..d1a2620a8fef 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -42,6 +42,7 @@
@import "syntax-dark.css";
@import "syntax-light.css";
@import "components.css";
+@import "interactive-diagram.css";
@import "highlight-github-dark.css";
@variant dark (&:where(.dark, .dark *));
diff --git a/assets/js/interactive-diagram.js b/assets/js/interactive-diagram.js
new file mode 100644
index 000000000000..3d429f6739e3
--- /dev/null
+++ b/assets/js/interactive-diagram.js
@@ -0,0 +1,300 @@
+const svgNamespace = "http://www.w3.org/2000/svg";
+
+document.querySelectorAll("[data-interactive-diagram]").forEach((root) => {
+ const configElement = root.querySelector("[data-interactive-diagram-config]");
+ if (!configElement) return;
+
+ let config;
+ try {
+ config = JSON.parse(configElement.textContent);
+ } catch {
+ return;
+ }
+
+ const stage = root.querySelector("[data-diagram-stage]");
+ if (!stage || !config.canvas || !config.steps?.length) return;
+
+ const state = buildDiagram(stage, config);
+ const previousButton = root.querySelector("[data-step-previous]");
+ const nextButton = root.querySelector("[data-step-next]");
+ const playButton = root.querySelector("[data-step-play]");
+ const progress = root.querySelector("[data-step-progress]");
+ let currentStep = 0;
+ let playTimer;
+
+ const stepButtons = config.steps.map((step, index) => {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "interactive-diagram__progress-step";
+ button.setAttribute("aria-label", `Show step ${index + 1}: ${step.label}`);
+ button.addEventListener("click", () => {
+ stopPlaying();
+ showStep(index);
+ });
+ progress?.append(button);
+ return button;
+ });
+
+ function showStep(index) {
+ currentStep = Math.max(0, Math.min(index, config.steps.length - 1));
+ const step = config.steps[currentStep];
+ const activeNodes = new Set(step.activeNodes ?? []);
+ const activeEdges = new Set(step.activeEdges ?? []);
+
+ state.nodes.forEach((element, id) => {
+ element.classList.toggle("is-active", activeNodes.has(id));
+ });
+ state.edges.forEach((element, id) => {
+ element.classList.toggle("is-active", activeEdges.has(id));
+ });
+ positionToken(state, config, step.token);
+
+ setText(
+ root,
+ "[data-step-number]",
+ `${String(currentStep + 1).padStart(2, "0")} / ${String(config.steps.length).padStart(2, "0")}`,
+ );
+ setText(root, "[data-step-title]", step.label);
+ setText(root, "[data-step-body]", step.body);
+ setText(root, "[data-state-label]", step.stateLabel);
+ setText(root, "[data-state-value]", step.stateValue);
+
+ if (previousButton) previousButton.disabled = currentStep === 0;
+ if (nextButton)
+ nextButton.disabled = currentStep === config.steps.length - 1;
+ stepButtons.forEach((button, buttonIndex) => {
+ const selected = buttonIndex === currentStep;
+ button.classList.toggle("is-active", selected);
+ button.setAttribute("aria-current", selected ? "step" : "false");
+ });
+ }
+
+ function stopPlaying() {
+ window.clearInterval(playTimer);
+ playTimer = undefined;
+ playButton?.setAttribute("aria-pressed", "false");
+ if (playButton) playButton.textContent = "Play";
+ }
+
+ previousButton?.addEventListener("click", () => {
+ stopPlaying();
+ showStep(currentStep - 1);
+ });
+ nextButton?.addEventListener("click", () => {
+ stopPlaying();
+ showStep(currentStep + 1);
+ });
+ playButton?.addEventListener("click", () => {
+ if (playTimer) {
+ stopPlaying();
+ return;
+ }
+ if (currentStep === config.steps.length - 1) showStep(0);
+ playButton.setAttribute("aria-pressed", "true");
+ playButton.textContent = "Pause";
+ playTimer = window.setInterval(() => {
+ if (currentStep === config.steps.length - 1) {
+ stopPlaying();
+ } else {
+ showStep(currentStep + 1);
+ }
+ }, 2600);
+ });
+
+ root.classList.add("is-enhanced");
+ showStep(0);
+});
+
+function buildDiagram(stage, config) {
+ const svg = createSvgElement("svg", {
+ viewBox: `0 0 ${config.canvas.width} ${config.canvas.height}`,
+ role: "img",
+ "aria-label": config.description,
+ });
+ svg.classList.add("interactive-diagram__svg");
+
+ const definitions = createSvgElement("defs");
+ const marker = createSvgElement("marker", {
+ id: `arrow-${Math.random().toString(36).slice(2)}`,
+ viewBox: "0 0 10 10",
+ refX: "8",
+ refY: "5",
+ markerWidth: "7",
+ markerHeight: "7",
+ orient: "auto-start-reverse",
+ });
+ marker.append(createSvgElement("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
+ definitions.append(marker);
+ svg.append(definitions);
+
+ config.boundaries?.forEach((boundary) => {
+ const group = createSvgElement("g");
+ group.classList.add(
+ "interactive-diagram__boundary",
+ `interactive-diagram__boundary--${boundary.kind}`,
+ );
+ group.append(
+ createSvgElement("rect", {
+ x: boundary.x,
+ y: boundary.y,
+ width: boundary.width,
+ height: boundary.height,
+ rx: "8",
+ }),
+ createSvgText(
+ boundary.label,
+ boundary.x + 16,
+ boundary.y + 25,
+ "interactive-diagram__boundary-label",
+ ),
+ );
+ svg.append(group);
+ });
+
+ const nodesById = new Map(config.nodes.map((node) => [node.id, node]));
+ const edges = new Map();
+ config.edges.forEach((edge) => {
+ const from = nodesById.get(edge.from);
+ const to = nodesById.get(edge.to);
+ if (!from || !to) return;
+ const points = edgePoints(from, to, edge.offset);
+ const line = createSvgElement("line", {
+ x1: points.x1,
+ y1: points.y1,
+ x2: points.x2,
+ y2: points.y2,
+ "marker-end": `url(#${marker.id})`,
+ });
+ line.classList.add("interactive-diagram__edge");
+ svg.append(line);
+ edges.set(edge.id, line);
+ });
+
+ const nodes = new Map();
+ config.nodes.forEach((node) => {
+ const group = createSvgElement("g");
+ group.classList.add(
+ "interactive-diagram__node",
+ `interactive-diagram__node--${node.kind}`,
+ );
+ group.append(
+ createSvgElement("rect", {
+ x: node.x,
+ y: node.y,
+ width: node.width,
+ height: node.height,
+ rx: "7",
+ }),
+ createSvgText(
+ node.label,
+ node.x + 14,
+ node.y + 29,
+ "interactive-diagram__node-label",
+ ),
+ createSvgText(
+ node.description,
+ node.x + 14,
+ node.y + 51,
+ "interactive-diagram__node-description",
+ ),
+ );
+ svg.append(group);
+ nodes.set(node.id, group);
+ });
+
+ const token = createSvgElement("g");
+ token.classList.add("interactive-diagram__token");
+ const tokenRect = createSvgElement("rect", { height: "28", rx: "14" });
+ const tokenText = createSvgText("", 0, 0, "interactive-diagram__token-label");
+ token.append(tokenRect, tokenText);
+ svg.append(token);
+ stage.append(svg);
+
+ return { nodes, edges, nodesById, token, tokenRect, tokenText };
+}
+
+function positionToken(state, config, tokenConfig) {
+ if (!tokenConfig) {
+ state.token.hidden = true;
+ return;
+ }
+
+ let x;
+ let y;
+ if (tokenConfig.node) {
+ const node = state.nodesById.get(tokenConfig.node);
+ if (node) {
+ x = node.x + node.width / 2;
+ y = node.y + node.height / 2;
+ }
+ } else if (tokenConfig.edge) {
+ const edge = config.edges.find(
+ (candidate) => candidate.id === tokenConfig.edge,
+ );
+ const from = state.nodesById.get(edge?.from);
+ const to = state.nodesById.get(edge?.to);
+ if (from && to) {
+ const points = edgePoints(from, to, edge.offset);
+ x = (points.x1 + points.x2) / 2;
+ y = (points.y1 + points.y2) / 2;
+ }
+ }
+ if (x === undefined || y === undefined) return;
+
+ const width = Math.max(70, tokenConfig.label.length * 7.2 + 24);
+ state.token.hidden = false;
+ state.token.setAttribute(
+ "transform",
+ `translate(${x - width / 2} ${y - 14})`,
+ );
+ state.tokenRect.setAttribute("width", width);
+ state.tokenText.setAttribute("x", width / 2);
+ state.tokenText.setAttribute("y", "18");
+ state.tokenText.textContent = tokenConfig.label;
+ state.token.classList.remove("is-entering");
+ window.requestAnimationFrame(() => state.token.classList.add("is-entering"));
+}
+
+function edgePoints(from, to, offset = 0) {
+ const fromCenter = {
+ x: from.x + from.width / 2,
+ y: from.y + from.height / 2,
+ };
+ const toCenter = { x: to.x + to.width / 2, y: to.y + to.height / 2 };
+ const dx = toCenter.x - fromCenter.x;
+ const dy = toCenter.y - fromCenter.y;
+ if (Math.abs(dx) >= Math.abs(dy)) {
+ return {
+ x1: dx >= 0 ? from.x + from.width : from.x,
+ y1: fromCenter.y + offset,
+ x2: dx >= 0 ? to.x : to.x + to.width,
+ y2: toCenter.y + offset,
+ };
+ }
+ return {
+ x1: fromCenter.x + offset,
+ y1: dy >= 0 ? from.y + from.height : from.y,
+ x2: toCenter.x + offset,
+ y2: dy >= 0 ? to.y : to.y + to.height,
+ };
+}
+
+function createSvgElement(name, attributes = {}) {
+ const element = document.createElementNS(svgNamespace, name);
+ Object.entries(attributes).forEach(([key, value]) =>
+ element.setAttribute(key, value),
+ );
+ return element;
+}
+
+function createSvgText(value, x, y, className) {
+ const text = createSvgElement("text", { x, y });
+ text.classList.add(className);
+ text.textContent = value;
+ return text;
+}
+
+function setText(root, selector, value) {
+ const element = root.querySelector(selector);
+ if (element) element.textContent = value;
+}
diff --git a/content/manuals/ai/sandboxes/architecture.md b/content/manuals/ai/sandboxes/architecture.md
index 88c8da4bdebe..fa7462e4b74e 100644
--- a/content/manuals/ai/sandboxes/architecture.md
+++ b/content/manuals/ai/sandboxes/architecture.md
@@ -61,6 +61,14 @@ proxy also handles [credential injection](configuration/credentials.md). See
works and [Default security posture](security/defaults.md) for what is
allowed out of the box.
+### Follow an authenticated request
+
+The following diagram shows where Docker Sandboxes checks network policy and
+replaces a sentinel credential with the real value. The real credential stays
+outside the sandbox throughout the request.
+
+{{< interactive-diagram src="diagrams/credential-injection.yaml" >}}
+
### Upstream proxy
The host-side proxy makes its outbound connections using your host's network
diff --git a/content/manuals/ai/sandboxes/diagrams/credential-injection.yaml b/content/manuals/ai/sandboxes/diagrams/credential-injection.yaml
new file mode 100644
index 000000000000..d93bfbc96847
--- /dev/null
+++ b/content/manuals/ai/sandboxes/diagrams/credential-injection.yaml
@@ -0,0 +1,149 @@
+title: Follow an authenticated request
+description: Step through a request as Docker Sandboxes applies network policy and injects a credential outside the microVM.
+canvas:
+ width: 920
+ height: 430
+boundaries:
+ - id: host
+ label: Host machine
+ x: 18
+ y: 24
+ width: 682
+ height: 382
+ kind: host
+ - id: sandbox
+ label: Sandbox microVM
+ x: 42
+ y: 68
+ width: 348
+ height: 310
+ kind: sandbox
+nodes:
+ - id: agent
+ label: AI agent
+ description: Sends the API request
+ x: 78
+ y: 122
+ width: 168
+ height: 76
+ kind: process
+ - id: docker-engine
+ label: Docker Engine
+ description: Private to this sandbox
+ x: 78
+ y: 267
+ width: 128
+ height: 64
+ kind: context
+ - id: workspace
+ label: Workspace
+ description: Shared with the host
+ x: 226
+ y: 267
+ width: 128
+ height: 64
+ kind: context
+ - id: policy
+ label: Network policy
+ description: Checks the destination
+ x: 446
+ y: 106
+ width: 176
+ height: 70
+ kind: control
+ - id: proxy
+ label: Host-side proxy
+ description: Rewrites the auth header
+ x: 446
+ y: 218
+ width: 176
+ height: 70
+ kind: control
+ - id: credentials
+ label: Credential store
+ description: Real token stays here
+ x: 446
+ y: 326
+ width: 176
+ height: 56
+ kind: secret
+ - id: provider
+ label: Model provider
+ description: Receives the request
+ x: 746
+ y: 192
+ width: 156
+ height: 84
+ kind: external
+edges:
+ - id: agent-policy
+ from: agent
+ to: policy
+ - id: policy-proxy
+ from: policy
+ to: proxy
+ - id: credentials-proxy
+ from: credentials
+ to: proxy
+ - id: proxy-provider
+ from: proxy
+ to: provider
+ offset: -7
+ - id: provider-proxy
+ from: provider
+ to: proxy
+ offset: 7
+steps:
+ - label: The agent prepares the request
+ body: The agent sees a sentinel value instead of the real API credential.
+ activeNodes: [agent]
+ token:
+ node: agent
+ label: proxy-managed
+ stateLabel: Auth header inside the sandbox
+ stateValue: "Bearer proxy-managed"
+ - label: The request leaves the microVM
+ body: Outbound HTTP and HTTPS traffic crosses the sandbox boundary through the host network path.
+ activeNodes: [agent, policy]
+ activeEdges: [agent-policy]
+ token:
+ edge: agent-policy
+ label: proxy-managed
+ stateLabel: Credential exposed to the microVM
+ stateValue: No
+ - label: Network policy checks the destination
+ body: The request continues only when an active policy permits the provider domain.
+ activeNodes: [policy]
+ activeEdges: [agent-policy]
+ token:
+ node: policy
+ label: allowed
+ stateLabel: Policy decision
+ stateValue: Allow destination
+ - label: The proxy retrieves the credential
+ body: The host-side proxy resolves the matching credential without copying it into the sandbox.
+ activeNodes: [proxy, credentials]
+ activeEdges: [policy-proxy, credentials-proxy]
+ token:
+ edge: credentials-proxy
+ label: host lookup
+ stateLabel: Credential location
+ stateValue: Host credential store
+ - label: The proxy rewrites the header
+ body: The proxy replaces the sentinel with the real credential after the request has left the microVM.
+ activeNodes: [proxy, provider]
+ activeEdges: [proxy-provider]
+ token:
+ edge: proxy-provider
+ label: Bearer ••••••
+ stateLabel: Auth header sent to the provider
+ stateValue: Real credential
+ - label: The response returns
+ body: The provider response returns through the host proxy to the agent. The credential remains on the host.
+ activeNodes: [provider, proxy, agent]
+ activeEdges: [provider-proxy]
+ token:
+ edge: provider-proxy
+ label: 200 OK
+ stateLabel: Credential after the request
+ stateValue: Still on the host
diff --git a/layouts/_partials/interactive-diagram-script.html b/layouts/_partials/interactive-diagram-script.html
new file mode 100644
index 000000000000..84c0ec2fa443
--- /dev/null
+++ b/layouts/_partials/interactive-diagram-script.html
@@ -0,0 +1,4 @@
+{{ $diagram := resources.Get "js/interactive-diagram.js"
+ | js.Build (dict "minify" true "targetPath" "interactive-diagram.js")
+}}
+
diff --git a/layouts/_shortcodes/interactive-diagram.html b/layouts/_shortcodes/interactive-diagram.html
new file mode 100644
index 000000000000..ce64bde4fd47
--- /dev/null
+++ b/layouts/_shortcodes/interactive-diagram.html
@@ -0,0 +1,77 @@
+{{- $src := .Get "src" -}}
+{{- if not $src -}}
+ {{- errorf "interactive-diagram shortcode: missing src: %s" .Position -}}
+{{- end -}}
+{{- $filePath := path.Join .Page.File.Dir $src -}}
+{{- if not (fileExists $filePath) -}}
+ {{- errorf "interactive-diagram shortcode: file %q not found: %s" $filePath .Position -}}
+{{- end -}}
+{{- $diagram := readFile $filePath | transform.Unmarshal -}}
+{{- $id := printf "interactive-diagram-%s-%d" .Page.File.UniqueID .Ordinal -}}
+{{- .Page.Store.Set "interactive-diagram" true -}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{- range $diagram.steps }}
+ - {{ .label }}: {{ .body }}
+ {{- end }}
+
+
+
+
diff --git a/layouts/_shortcodes/interactive-diagram.markdown.md b/layouts/_shortcodes/interactive-diagram.markdown.md
new file mode 100644
index 000000000000..c50b4317a993
--- /dev/null
+++ b/layouts/_shortcodes/interactive-diagram.markdown.md
@@ -0,0 +1,13 @@
+{{- $src := .Get "src" -}}
+{{- $filePath := path.Join .Page.File.Dir $src -}}
+{{- if not (fileExists $filePath) -}}
+ {{- errorf "interactive-diagram shortcode: file %q not found: %s" $filePath .Position -}}
+{{- end -}}
+{{- $diagram := readFile $filePath | transform.Unmarshal -}}
+{{ $diagram.title }}
+
+{{ $diagram.description }}
+
+{{ range $index, $step := $diagram.steps -}}
+{{ add $index 1 }}. {{ $step.label }}: {{ $step.body }}
+{{ end -}}
diff --git a/layouts/baseof.html b/layouts/baseof.html
index 823d4a087912..b862091a975e 100644
--- a/layouts/baseof.html
+++ b/layouts/baseof.html
@@ -95,5 +95,9 @@
{{ with .Store.Get "mermaid" }}
{{ partialCached "mermaid-script.html" "-" "-" }}
{{ end }}
+ {{/* Load interactive diagrams only on pages that contain the shortcode */}}
+ {{ with .Store.Get "interactive-diagram" }}
+ {{ partialCached "interactive-diagram-script.html" "-" "-" }}
+ {{ end }}