# Live Artifacts via Agent Skills **Status:** Draft · 2026-04-29 **Parent:** [`docs/spec.md`](../../docs/spec.md) **Siblings:** [`docs/skills-protocol.md`](../../docs/skills-protocol.md) · [`docs/agent-adapters.md`](../../docs/agent-adapters.md) · [`docs/modes.md`](../../docs/modes.md) **Reference implementation:** `~/Projects/monet` connectors + live artifacts This spec defines how to bring Monet's **connectors** and **live artifacts** ideas into Open Design, but implement the agent-facing surface as **file-based agent skills plus daemon-owned local tools**, not as an in-process tool registry or MCP-first integration. --- ## 1. Product goal Open Design should let an agent create previewable artifacts that are not just one-off generated files, but **live, refreshable, auditable views** backed by external or local data sources. Examples: - “Create a live GitHub release dashboard.” - “Make a Notion project status page and let me refresh it tomorrow.” - “Turn this folder of JSON files into a polished stakeholder report.” - “Create a design-system coverage artifact that can be refreshed after code changes.” The user experience should feel like the existing OD artifact flow: 1. User chats with the selected agent. 2. Agent uses a skill to plan and create a live artifact. 3. OD persists the artifact as project-scoped files and metadata. 4. UI previews the artifact in the existing iframe/file viewer model. 5. User can refresh the artifact later without asking the agent to redesign it from scratch. ## 2. Key decision ### 2.1 Use `skill + daemon tool endpoint`, not MCP-first Monet exposes connectors and live artifacts through a controller-owned tool registry. OD should not copy that exact runtime shape because OD's core architecture is different: OD delegates to external CLI agents such as Claude Code, Codex, Cursor Agent, Gemini CLI, OpenCode, and Qwen. The agent-facing interface should therefore be: ```text skills/live-artifact/SKILL.md ↓ instructs the agent to call daemon local HTTP endpoints or wrapper CLI commands ↓ backed by daemon-owned connector + artifact services ↓ persisted as project workspace files + metadata ``` MCP may be added later as a wrapper over the same daemon services, but it should not be the first or only interface. Reasons: - **Multi-agent compatibility:** every supported agent can read a skill and execute shell commands; MCP support varies by agent and CLI version. - **Lower migration cost:** current daemon `/api/chat` does not support per-run MCP binding. - **Centralized safety:** daemon endpoints can enforce project, path, connector, and output-size policies consistently. - **Skill-native product model:** OD's extension point is already `skills/` + `SKILL.md`, so live artifacts should feel like another OD capability, not a separate agent protocol. ### 2.2 Keep live artifacts distinct, but project-native Live artifacts are a distinct persisted model integrated into the existing project UI. They must not be represented as a new static `ArtifactKind` in the existing artifact model, because they require ID-based identity, directory-shaped runtime storage, refresh/provenance history, connector permissions, locking, and server-rendered preview behavior. Product terms: - **Design / project:** the workspace container. - **Artifact:** a static generated file inside a design. - **Live artifact:** a refreshable, data-backed artifact inside a design. - **Connector:** an external or local data source available to live artifacts. Implementation boundaries: - Keep dedicated live-artifact storage under `.live-artifacts/`, dedicated `/api/live-artifacts/*` endpoints, and dedicated live-artifact DTOs in `packages/contracts`. - Reuse the existing project scope, workspace tabs, file tree, viewer primitives, chat SSE stream, and API error envelope so live artifacts feel native without polluting the simple static artifact path. - Do not expose `.live-artifacts/` through generic project file APIs; all mutation should go through live-artifact or tool endpoints. ## 3. What to migrate from Monet ### 3.1 Concepts to preserve From `~/Projects/monet`: - Static connector catalog plus dynamic connection status. - Connector tool safety classification. - Read-only-first connector policy. - Live artifact / tile / source / provenance separation. - HTML document template plus data-binding contract. - Declarative output mapping from tool output to `data.json` / render models. - Strict render JSON validation. - Refresh source validation before re-execution. - Refresh audit trail with step-level status. - Failure fallback: invalid refresh output should not blank the artifact. ### 3.2 Concepts to adapt Monet concept | OD adaptation ---|--- Controller `ToolRegistry` | Daemon service endpoints and optional CLI wrappers Chat tools `create_live_artifact`, `update_live_artifact`, `list_live_artifacts` | Skill instructions that call `od-tools live-artifacts ...` or localhost daemon endpoints Connector tools dynamically injected into tool registry | Connector catalog exposed through daemon endpoints; skill asks agent to query/use them explicitly SQLite-first artifact storage | Project-scoped metadata files first; SQLite optional later if indexing becomes necessary Controller-owned agent loop | External CLI agent loop; OD only injects skills and receives output/events ### 3.3 Monet files used as source material - `apps/controller/src/connectors/catalog.ts` - `apps/controller/src/connectors/service.ts` - `apps/controller/src/routes/connectors.ts` - `apps/controller/src/tools/connectors.ts` - `apps/controller/src/live-artifacts/schema.ts` - `apps/controller/src/live-artifacts/render.ts` - `apps/controller/src/live-artifacts/refresh.ts` - `apps/controller/src/routes/live-artifacts.ts` - `apps/controller/src/tools/live-artifacts.ts` - `apps/controller/src/chat-storage.ts` - `specs/2026-04-27-live-artifacts/spec.md` ## 4. Target architecture ```text ┌──────────────────────────────────────────────────────────────────┐ │ Web App │ │ chat · artifact tree · live artifact list · refresh button │ │ iframe preview · source/provenance panels │ └───────────────┬──────────────────────────────────────────────────┘ │ HTTP/SSE ┌───────────────▼──────────────────────────────────────────────────┐ │ Local Daemon │ │ │ │ Agent session broker │ │ Skill registry │ │ Built-in tool endpoints │ │ /api/tools/live-artifacts/* │ │ /api/tools/connectors/* │ │ /api/connectors/* │ │ Artifact store │ │ Connector service │ │ Refresh runner + audit log │ └───────────────┬──────────────────────────────────────────────────┘ │ spawn / stdio ┌───────────────▼──────────────────────────────────────────────────┐ │ External Agent CLI │ │ Claude Code · Codex · Cursor Agent · Gemini CLI · OpenCode · Qwen │ │ │ │ Receives SKILL.md instructions and calls daemon tools via shell │ └──────────────────────────────────────────────────────────────────┘ ``` ## 5. User-facing skill shape Add a built-in skill: ```text skills/live-artifact/ ├── SKILL.md ├── references/ │ ├── artifact-schema.md │ ├── connector-policy.md │ └── refresh-contract.md └── assets/ └── templates/ ├── dashboard.html └── report.html ``` ### 5.1 `SKILL.md` frontmatter ```yaml --- name: live-artifact description: | Create refreshable, auditable Open Design artifacts backed by connector or local data. Trigger when the user asks for live dashboards, refreshable reports, synced views, or reusable data-backed artifacts. triggers: - live artifact - refreshable dashboard - live report - synced view - 可刷新 - 实时看板 od: mode: prototype preview: type: html entry: index.html reload: debounce-100 design_system: requires: true outputs: primary: index.html secondary: - template.html - artifact.json - data.json - provenance.json capabilities_required: - shell - file_write --- ``` ### 5.2 Skill body responsibilities The skill should instruct the agent to: 1. Determine whether the user wants a live artifact or a normal static artifact. 2. Query available connectors and allowed read-only operations. 3. Fetch from the named connected connector/source when available; ask for a data source only when no matching connected source exists, multiple candidates are equally plausible, or the request lacks any searchable topic/page/database clue. 4. Create a safe render model, not raw provider output. 5. Write `template.html`, `data.json`, `artifact.json`, and `provenance.json` into the live artifact workspace directory; treat `index.html` as derived preview output. 6. Register the artifact through daemon tooling. 7. Include provenance and refresh source metadata. 8. Never store credentials, raw OAuth responses, headers, cookies, or tokens. ### 5.3 Agent-callable command surface Prefer a small `od` wrapper command over raw `curl` in the skill body: ```bash od tools live-artifacts create --input artifact.json od tools live-artifacts list --format compact od tools live-artifacts update --artifact-id "$ID" --input artifact.json od tools live-artifacts refresh --artifact-id "$ID" od tools connectors list --format compact od tools connectors execute --connector github --tool list_releases --input input.json ``` The wrapper should be implemented as TypeScript source under `apps/daemon/src` and call daemon endpoints using injected runtime values: - `OD_DAEMON_URL` - `OD_TOOL_TOKEN` The daemon injects these into the system prompt or skill preamble at runtime. The agent should not choose or override `projectId`; `/api/tools/*` derives project/run scope from `OD_TOOL_TOKEN`. If standalone JavaScript wrappers are later exposed, they must be generated build output from TypeScript source, not project-owned `.js` source files. Raw HTTP is for developer debugging only and must include the run-scoped bearer token: ```bash curl -s -X POST "$OD_DAEMON_URL/api/tools/live-artifacts/create" \ -H 'content-type: application/json' \ -H "authorization: Bearer $OD_TOOL_TOKEN" \ -d @artifact.json ``` ## 6. Daemon API design ### 6.1 Connector endpoints ```http GET /api/connectors GET /api/connectors/:connectorId POST /api/connectors/:connectorId/connect DELETE /api/connectors/:connectorId/connection ``` MVP may stub OAuth-backed connectors and start with local/read-only connectors, but the API should preserve Monet's split between catalog and connection status. OAuth callback routes are deferred until OAuth-backed connectors are implemented. Connector response shape: ```ts type ConnectorDetail = { id: string; label: string; category: 'code' | 'docs' | 'files' | 'analytics' | 'custom'; status: 'available' | 'connected' | 'error' | 'disabled'; accountLabel?: string; featuredTools: ConnectorToolSummary[]; allowedTools: ConnectorToolSummary[]; minimumApprovalPolicy: 'read_only_auto' | 'confirm_write' | 'disabled'; errorCode?: string; }; ``` ### 6.2 Connector tool endpoints Agent and refresh-runner connector execution must use the same daemon-owned execution path: ```http GET /api/tools/connectors/list POST /api/tools/connectors/execute ``` `/api/tools/connectors/list` returns a compact list of connected, allowed, read-only-first tools for the current run token. `/api/tools/connectors/execute` request: ```ts type ConnectorExecuteRequest = { connectorId: string; toolName: string; input: BoundedJsonObject; purpose: 'agent_preview' | 'artifact_refresh'; }; ``` Response: ```ts type ConnectorExecuteResponse = | { ok: true; connectorId: string; accountLabel?: string; toolName: string; safety: ConnectorToolSafety; output: BoundedJsonValue; outputSummary?: string; providerExecutionId?: string; metadata?: BoundedJsonObject; } | ApiErrorResponse; ``` Execution rules: - Require a valid `OD_TOOL_TOKEN` bound to the active run/project. - Reject tools that are not in the connector catalog allowlist. - Re-classify tool safety at execution time; catalog metadata alone is not authorization. - Reject `write`, `destructive`, and `unknown` tools for `artifact_refresh`. - Bound output size before it is returned to the agent. - Redact credentials and raw provider envelope fields before returning or persisting anything. - Record `providerExecutionId`, connector/account labels, and safety policy for provenance. ### 6.3 Live artifact endpoints Agent/tool endpoints: ```http POST /api/tools/live-artifacts/create GET /api/tools/live-artifacts/list POST /api/tools/live-artifacts/update POST /api/tools/live-artifacts/refresh ``` UI endpoints: ```http GET /api/live-artifacts?projectId=... POST /api/live-artifacts GET /api/live-artifacts/:artifactId PATCH /api/live-artifacts/:artifactId POST /api/live-artifacts/:artifactId/refresh GET /api/live-artifacts/:artifactId/preview ``` The `/api/tools/*` endpoints are optimized for agent consumption: compact JSON, concise errors, and explicit machine-readable validation failures. They never accept an arbitrary `projectId`; project/run scope comes from `OD_TOOL_TOKEN`. The `/api/live-artifacts/*` endpoints are optimized for UI state and use the web app's normal project context. Both endpoint families must call the same service-layer validation and storage code. Only authentication and response verbosity should differ; errors should share the `ApiErrorResponse` envelope from `packages/contracts`. Agent-facing tool endpoints should reuse the shared API error envelope from `packages/contracts/src/errors.ts` instead of introducing a parallel error type: ```ts type LiveArtifactToolResponse = TSuccess | ApiErrorResponse; ``` Add live-artifact and connector-specific `ApiErrorCode` values such as `TOOL_TOKEN_INVALID`, `TOOL_TOKEN_EXPIRED`, `CONNECTOR_NOT_CONNECTED`, `CONNECTOR_SAFETY_DENIED`, `REFRESH_LOCKED`, `REFRESH_TIMED_OUT`, `OUTPUT_TOO_LARGE`, `TEMPLATE_BINDING_INVALID`, and `REDACTION_REQUIRED`. Validation details should live in the existing error `details` field so web, daemon, and tests share one error model. ## 7. Data model ### 7.1 Storage layout Use project-scoped files under the daemon runtime data directory first. `OD_DATA_DIR` may override the default; otherwise `` is `/.od`: ```text /projects// └── .live-artifacts/ └── / ├── artifact.json ├── template.html ├── index.html ├── data.json ├── provenance.json ├── refreshes.jsonl └── snapshots/ └── / ├── data.json └── provenance.json ``` The dot-prefixed `.live-artifacts/` directory keeps implementation files out of the generic project file tree while preserving OD's file-first, inspectable-on-disk artifact philosophy. Add SQLite later only for cross-project indexing or high-volume refresh history. `index.html` is a generated preview artifact, not the source of truth for refreshable data. The UI should load live artifacts through: ```http GET /api/live-artifacts/:artifactId/preview ``` The preview route may serve the stored `index.html` for static cases, but for refreshable HTML it should render `template.html + data.json` and apply iframe sandbox/CSP headers. `snapshots/` should be hidden from the normal artifact tree unless the user explicitly opens refresh history. ### 7.2 Core types ```ts type BoundedJsonValue = | null | boolean | number | string | BoundedJsonValue[] | { [key: string]: BoundedJsonValue }; type BoundedJsonObject = { [key: string]: BoundedJsonValue }; type LiveArtifact = { schemaVersion: 1; id: string; projectId: string; sessionId?: string; createdByRunId?: string; title: string; slug: string; status: 'active' | 'archived' | 'error'; pinned: boolean; preview: { type: 'html' | 'jsx' | 'markdown'; entry: string; }; refreshStatus: 'never' | 'idle' | 'running' | 'succeeded' | 'failed'; createdAt: string; updatedAt: string; lastRefreshedAt?: string; document?: LiveArtifactDocument; }; type LiveArtifactDocument = { format: 'html_template_v1'; templatePath: 'template.html'; generatedPreviewPath: 'index.html'; dataPath: 'data.json'; dataJson: BoundedJsonObject; dataSchemaJson?: BoundedJsonObject; sourceJson?: LiveArtifactSource; }; type LiveArtifactSource = { type: 'local_file' | 'daemon_tool' | 'connector_tool'; toolName?: string; input: BoundedJsonObject; connector?: { connectorId: string; accountLabel?: string; toolName: string; approvalPolicy: 'read_only_auto' | 'manual_refresh_granted_for_read_only'; }; outputMapping?: { dataPaths?: Array<{ from: string; to: string }>; transform?: 'identity' | 'compact_table' | 'metric_summary'; }; refreshPermission: 'none' | 'manual_refresh_granted_for_read_only'; }; type LiveArtifactProvenance = { generatedAt: string; generatedBy: 'agent' | 'refresh_runner'; notes?: string; sources: Array<{ label: string; type: 'connector' | 'local_file' | 'user_input' | 'derived'; ref?: string; }>; }; ``` ### 7.3 Validation rules Port Monet's strict validation posture: - Apply the shared bounded JSON constraints in `packages/contracts` to every persisted or agent-supplied `BoundedJsonValue` / `BoundedJsonObject`. - Reject keys such as `raw`, `rawResponse`, `payload`, `body`, `headers`, `cookie`, `authorization`, `token`, `secret`, `credential`, `password`. - Redact suspicious source inputs before persistence. - Reject source inputs that still contain credential-like values after redaction. - HTML preview files must be generated from the document contract; refresh updates `data.json`, not arbitrary script. #### 7.3.1 Shared bounded JSON constraints The shared live-artifact JSON envelope is intentionally small enough to validate synchronously, store in project files, display in the UI, and include in agent-facing error details without leaking raw provider payloads. Define and export these constants from `packages/contracts` as `LIVE_ARTIFACT_BOUNDED_JSON_CONSTRAINTS`: | Constraint | Value | Applies to | | --- | ---: | --- | | Maximum object/array depth | `8` | Any `BoundedJsonValue`; count the root object or array as depth `1`. | | Maximum object keys | `100` | Any single object. | | Maximum array length | `500` | Any single array. | | Maximum string length | `16 KiB` | Any single string value, measured in UTF-16 code units before persistence. | | Maximum serialized payload size | `256 KiB` | Any complete bounded JSON document, measured as UTF-8 bytes of canonical `JSON.stringify` output. | Validation must fail closed when a value exceeds any limit. Persisted files and create/update inputs must use the same limits so valid agent input remains valid after storage round-trips. Future connector-specific limits may be stricter, but must not exceed this shared envelope for values persisted into live artifact files. ### 7.4 HTML document model MVP live HTML artifacts should use `html_template_v1`: ```text template.html + data.json → daemon render step → index.html / preview response ``` Rules: - `template.html` is authored by the agent during create/update. - Refreshable values must come from `data.json`, not be hardcoded only in HTML. - `html_template_v1` supports **Mustache-style escaped interpolation plus a minimal `data-od-repeat` structural directive**. It does not support arbitrary JavaScript, expression evaluation, helper functions, filters, partials, or raw HTML injection. - Refresh updates `data.json` and snapshots. It does not let connector output rewrite arbitrary HTML. - If a presentation redesign is needed, the user should ask the agent to update the artifact; refresh is for data changes. - `index.html` may be regenerated after successful refresh, but it is derived output. - The preview route must serve the document in a sandboxed iframe context with a restrictive CSP. External scripts are disallowed in MVP unless vendored and allowlisted. #### 7.4.1 `html_template_v1` binding contract MVP binding syntax is intentionally small and deterministic: - **Escaped interpolation:** `{{data.path.to.value}}` inserts a scalar value from `data.json`. - Paths must start with `data` and use dot-separated object keys, e.g. `{{data.summary.title}}`. - Numeric array indexes are allowed only as path segments, e.g. `{{data.metrics.0.value}}`. - Keys must match `/^[A-Za-z_][A-Za-z0-9_-]*$/`; bracket notation, computed paths, wildcards, function calls, and expressions are invalid. - Values render as strings. `null` and missing values render as an empty string. Objects and arrays are invalid interpolation targets except inside a supported repeat context. - **Repeat directive:** `data-od-repeat="item in data.items"` repeats the annotated element once for each object in an array. - The left side is a local alias matching `/^[A-Za-z_][A-Za-z0-9_]*$/`. - The right side must be a `data.*` path resolving to an array. - Inside the repeated element, interpolation may reference the local alias, e.g. `{{item.name}}`, using the same path grammar. - Nested `data-od-repeat` directives are disallowed in MVP. - `data-od-repeat` is removed from the generated output. - **Conditional directives:** none in MVP. Optional sections should be represented by empty strings, zero-length arrays, or separate agent-authored template variants during update. - **Attribute bindings:** interpolation may appear in text nodes and ordinary HTML attribute values, but not in attribute names, tag names, comments, `