@@ -24,16 +25,28 @@ export function ArtifactDetail({ artifact, onClose }: ArtifactDetailProps) {
- {onClose && (
-
diff --git a/apps/desktop/src/features/artifacts/hooks/useArtifacts.ts b/apps/desktop/src/features/artifacts/hooks/useArtifacts.ts
index a91a7230..b0512fa0 100644
--- a/apps/desktop/src/features/artifacts/hooks/useArtifacts.ts
+++ b/apps/desktop/src/features/artifacts/hooks/useArtifacts.ts
@@ -1,6 +1,6 @@
// Hooks for artifacts.
-import { useQuery } from "@tanstack/react-query";
+import { useMutation, useQuery } from "@tanstack/react-query";
import { desktopApi, type ArtifactFilters } from "@/lib/desktop-api";
export const ARTIFACT_KEYS = {
@@ -18,3 +18,12 @@ export function useArtifacts(filters: ArtifactFilters = {}) {
queryFn: () => desktopApi.artifacts.listArtifacts(filters),
});
}
+
+/**
+ * Hook to open an artifact in its own window.
+ */
+export function useOpenArtifactWindow() {
+ return useMutation({
+ mutationFn: (artifactId: string) => desktopApi.artifacts.openArtifactWindow(artifactId),
+ });
+}
diff --git a/apps/desktop/src/features/artifacts/index.ts b/apps/desktop/src/features/artifacts/index.ts
index da6f3d12..a84d181b 100644
--- a/apps/desktop/src/features/artifacts/index.ts
+++ b/apps/desktop/src/features/artifacts/index.ts
@@ -9,4 +9,4 @@ export type {
ParsedArtifactData,
TimelineData,
} from "./components/parseArtifactData";
-export { ARTIFACT_KEYS, useArtifacts } from "./hooks/useArtifacts";
+export { ARTIFACT_KEYS, useArtifacts, useOpenArtifactWindow } from "./hooks/useArtifacts";
diff --git a/apps/desktop/src/lib/desktop-api/artifacts.ts b/apps/desktop/src/lib/desktop-api/artifacts.ts
index ebfe4009..fc455492 100644
--- a/apps/desktop/src/lib/desktop-api/artifacts.ts
+++ b/apps/desktop/src/lib/desktop-api/artifacts.ts
@@ -34,3 +34,10 @@ export async function listArtifacts(filters: ArtifactFilters = {}): Promise
{
+ return invoke("open_artifact_window", { artifactId });
+}
diff --git a/apps/desktop/src/pages/artifacts/ArtifactsPage.test.tsx b/apps/desktop/src/pages/artifacts/ArtifactsPage.test.tsx
index 57b19f0d..41f16c18 100644
--- a/apps/desktop/src/pages/artifacts/ArtifactsPage.test.tsx
+++ b/apps/desktop/src/pages/artifacts/ArtifactsPage.test.tsx
@@ -1,11 +1,12 @@
// Tests for the Artifacts page.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@testing-library/jest-dom";
import { invoke } from "@tauri-apps/api/core";
+import { ToastProvider } from "@/components/common/ToastProvider";
import { ArtifactsPage } from "./ArtifactsPage";
import type { Artifact } from "@/lib/desktop-api";
@@ -38,7 +39,9 @@ function renderPage() {
return render(
-
+
+
+
,
);
}
@@ -94,4 +97,27 @@ describe("ArtifactsPage", () => {
expect(await screen.findByText("Failed to load artifacts")).toBeInTheDocument();
});
+
+ it("opens an artifact window", async () => {
+ vi.mocked(invoke).mockImplementation(((command: string) => {
+ if (command === "list_artifacts") {
+ return Promise.resolve([comparisonArtifact]);
+ }
+ if (command === "open_artifact_window") {
+ return Promise.resolve(null);
+ }
+ return Promise.reject(new Error(`Unexpected command: ${command}`));
+ }) as unknown as typeof invoke);
+
+ renderPage();
+
+ fireEvent.click(await screen.findByText("Tesla vs Nvidia"));
+ fireEvent.click(await screen.findByRole("button", { name: /open in window/i }));
+
+ await waitFor(() => {
+ expect(vi.mocked(invoke)).toHaveBeenCalledWith("open_artifact_window", {
+ artifactId: "artifact-1",
+ });
+ });
+ });
});
diff --git a/apps/desktop/src/pages/artifacts/ArtifactsPage.tsx b/apps/desktop/src/pages/artifacts/ArtifactsPage.tsx
index 090e109b..12a9a6b7 100644
--- a/apps/desktop/src/pages/artifacts/ArtifactsPage.tsx
+++ b/apps/desktop/src/pages/artifacts/ArtifactsPage.tsx
@@ -5,11 +5,20 @@ import { Boxes } from "lucide-react";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { EmptyState } from "@/components/common/EmptyState";
import { ErrorState } from "@/components/common/ErrorState";
-import { ArtifactDetail, ArtifactList, useArtifacts } from "@/features/artifacts";
+import { useToast } from "@/components/common/useToast";
+import {
+ ArtifactDetail,
+ ArtifactList,
+ useArtifacts,
+ useOpenArtifactWindow,
+} from "@/features/artifacts";
+import { getErrorMessage } from "@/lib/errors";
import type { Artifact } from "@/lib/desktop-api";
export function ArtifactsPage() {
+ const { toast } = useToast();
const { data: artifacts, isLoading, error, refetch } = useArtifacts();
+ const openArtifact = useOpenArtifactWindow();
const [selectedArtifactId, setSelectedArtifactId] = useState(null);
const selectedArtifact =
@@ -55,6 +64,17 @@ export function ArtifactsPage() {
onClose={() => {
setSelectedArtifactId(null);
}}
+ onOpen={() => {
+ openArtifact.mutate(selectedArtifact.id, {
+ onError: (error) => {
+ toast({
+ title: "Failed to open artifact window",
+ description: getErrorMessage(error),
+ variant: "destructive",
+ });
+ },
+ });
+ }}
/>
)}
diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts
index d717c74e..816939e0 100644
--- a/apps/desktop/vite.config.ts
+++ b/apps/desktop/vite.config.ts
@@ -20,5 +20,11 @@ export default defineConfig({
target: "esnext",
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_DEBUG,
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, "index.html"),
+ artifact: path.resolve(__dirname, "artifact.html"),
+ },
+ },
},
});
From 140f524f8c648662dc9a3af6ee9ab532607e73e0 Mon Sep 17 00:00:00 2001
From: Berry Wahlberg <40695099+BerryUIKI@users.noreply.github.com>
Date: Thu, 5 Nov 2026 17:20:11 +0800
Subject: [PATCH 3/3] docs: document artifact windows and permission isolation
- describe the dedicated artifact entry, payload injection, and core:default capability
- note the residual app-command ACL limitation
- mark temporary window creation and permission isolation on the roadmap
---
CHANGELOG.md | 1 +
CLAUDE.md | 3 ++-
docs/ARCHITECTURE.md | 3 ++-
docs/ARTIFACT_SYSTEM.md | 5 +++--
docs/MILESTONE_ROADMAP.md | 8 ++++----
docs/SECURITY.md | 1 +
6 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73e680df..3bc9d0f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `alphaforge` CLI for workspaces and agent tasks, with human/JSON output, live progress, timeout, cancellation, and retry.
- Agent context and tools: workspace and recent-task context in prompts, an OpenAI tool-calling loop (max 4 rounds), and read-only tools for listing/searching tasks, reading task output, and arithmetic.
- Agent artifacts: validated comparison-table and timeline outputs are generated with tasks, persisted in SQLite, and rendered on the Artifacts page with predefined React components.
+- Artifact windows: validated artifacts can open in a dedicated minimal-permission WebView (`core:default`) with the payload injected as parsed JSON.
### Fixed
- Root `pnpm typecheck` now uses the pnpm workspace instead of a non-existent npm workspace.
diff --git a/CLAUDE.md b/CLAUDE.md
index b16577cd..ec090a80 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -136,7 +136,8 @@ React component
- `agent/runtime.rs` defines `AgentRuntime`, `ProgressReporter`, and a simulated `LocalDemoRuntime` for tests/demos; `agent/openai_runtime.rs` is the production runtime — it assembles context, runs up to 4 OpenAI tool-calling rounds against a `ToolRegistry`, validates the final JSON against `ResearchOutput`, and persists canonical JSON. `agent/context.rs` supplies the workspace name and summaries of recent completed tasks; `agent/tools/` provides `list_tasks`, `search_tasks`, `get_task_output`, and `calculate`. Tool execution errors are returned to the model as `{"error": ...}` so it can self-correct. `agent/events.rs` publishes `agent-task-event` through an injected `EventPublisher` (Tauri-backed in production).
- The frontend `useAgentTaskEvents` hook subscribes to `agent-task-event` and invalidates agent queries so task status and results update live. `AgentWorkspacePanel` (rendered by `TodayPage` once a workspace is selected) combines task creation, start/cancel actions, and a detail view with the execution timeline, output, and error.
- Agent artifacts: `agent/openai_runtime.rs` accepts an optional `artifacts` array in the final JSON; `artifacts/schema.rs` validates the `comparison-table` and `timeline` schemas, and `services/artifact_service.rs` persists them (title/workspace on the row, payload in `artifacts.input`). `commands/artifacts.rs` exposes `list_artifacts` with workspace/task filters, and `features/artifacts` renders them with predefined React components in the main window.
-- Artifact windows, the plugin runtime, and the remaining built-in artifact types are still scaffolding. Rendering never injects model-generated HTML; treat artifact payloads as untrusted data even after Rust validation. The intended contracts are documented in `docs/ARTIFACT_SYSTEM.md`, `docs/PLUGIN_SPEC.md`, and the manifests in `plugins/*/manifest.json` (`id`, `name`, `version`, `entry`, `inputSchema`, `permissions`, `window`).
+- Artifacts can also open in a dedicated WebView via `open_artifact_window` (`windows/artifact_window.rs`). The window loads `artifact.html` / `src/artifact-main.tsx`, receives the validated payload through an initialization script as a JSON string parsed with `JSON.parse`, and runs with only `core:default` (`capabilities/artifact-window.json`). The artifact entry must not import `desktopApi` or app-shell code.
+- The plugin runtime and the remaining built-in artifact types are still scaffolding. Rendering never injects model-generated HTML; treat artifact payloads as untrusted data even after Rust validation. The intended contracts are documented in `docs/ARTIFACT_SYSTEM.md`, `docs/PLUGIN_SPEC.md`, and the manifests in `plugins/*/manifest.json` (`id`, `name`, `version`, `entry`, `inputSchema`, `permissions`, `window`).
- Treat agent output as untrusted: validate it against a schema and render it with predefined React plugins. Never inject agent-generated HTML into the privileged main-window DOM.
- Artifact windows receive only `core:default` (`capabilities/artifact-window.json`); the main window additionally gets opener, shell-open, and store permissions (`capabilities/main-window.json`).
- The research, journal, portfolio, and artifact list commands (`commands/research.rs`, `journal.rs`, `portfolio.rs`, `artifacts.rs`) are stubs returning empty collections.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 413e9481..d2b20e6a 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -165,7 +165,7 @@ since the desktop app may be open at the same time.
| `list_research_documents` | research | Stub — returns [] |
| `list_theses` | journal | Stub — returns [] |
| `list_portfolio_accounts` | portfolio | Stub — returns [] |
-| `list_artifacts` (optional `workspace_id` / `task_id` filters) | artifacts | ✅ Active (SQLite; comparison-table and timeline renderers) |
+| `list_artifacts` (optional `workspace_id` / `task_id` filters), `open_artifact_window` | artifacts | ✅ Active (SQLite; comparison-table and timeline renderers; dedicated minimal-permission WebView) |
## Database
@@ -178,6 +178,7 @@ since the desktop app may be open at the same time.
## Security
- API keys: stored in the OS keychain (`security/credentials.rs`, `keyring` crate); status is exposed to React, the key is never returned
+- Artifact windows: dedicated `artifact.html` entry with only `core:default`; validated JSON is injected through an initialization script and parsed with `JSON.parse`
- Artifact windows: Minimal permissions (`capabilities/artifact-window.json`)
- Main window: Controlled permissions (`capabilities/main-window.json`)
- Input validation: Will use Zod (TS) + manual validation (Rust)
diff --git a/docs/ARTIFACT_SYSTEM.md b/docs/ARTIFACT_SYSTEM.md
index efb4ddf2..33d1d625 100644
--- a/docs/ARTIFACT_SYSTEM.md
+++ b/docs/ARTIFACT_SYSTEM.md
@@ -31,14 +31,15 @@ User submits task
## Current Implementation (MVP)
-The first slice renders artifacts inside the main window rather than a separate WebView:
+The MVP supports main-window rendering and dedicated artifact windows:
- The agent may include an `artifacts` array in its final structured output; each entry has `type`, `title`, and `data`.
- `artifacts/schema.rs` validates `comparison-table` and `timeline` payloads; invalid artifacts are discarded before persistence.
- `ArtifactService` stores validated artifacts and `list_artifacts` exposes them with workspace/task filters.
- `features/artifacts` renders the payloads with predefined React components. No model-generated HTML is used.
+- `open_artifact_window` opens a dedicated WebView (`artifact.html`) with only `core:default`. The payload is injected by an initialization script as a JSON string parsed with `JSON.parse`; the artifact entry does not import `desktopApi` or the app shell.
-Separate artifact windows, the plugin SDK, and the remaining artifact types are still future work.
+Residual risk: Tauri application commands are not ACL-gated per window, so isolation relies on the artifact entry containing no privileged code paths and on the minimal capability set. The plugin SDK and the remaining artifact types are still future work.
## Lifecycle
diff --git a/docs/MILESTONE_ROADMAP.md b/docs/MILESTONE_ROADMAP.md
index 46023645..ae8739c9 100644
--- a/docs/MILESTONE_ROADMAP.md
+++ b/docs/MILESTONE_ROADMAP.md
@@ -302,8 +302,8 @@ Enable Agents to create interactive research experiences.
#### Artifact Runtime
- [x] Structured output schema (comparison-table, timeline)
- [x] Renderer system (predefined React components in the main window)
-- [ ] Temporary window creation
-- [ ] Permission isolation
+- [x] Temporary window creation (dedicated `artifact.html` WebView)
+- [x] Permission isolation (only `core:default`; dedicated entry with no privileged imports)
#### Built-in Artifacts
- [x] Comparison table
@@ -312,8 +312,8 @@ Enable Agents to create interactive research experiences.
- [ ] Valuation model
- [ ] Risk dashboard
-Artifacts currently render inside the main window with predefined components;
-separate artifact WebViews and the plugin runtime remain future work.
+Artifacts render in the main window and can open in a dedicated minimal-permission
+WebView. The plugin runtime and remaining built-in artifact types remain future work.
### Artifact Flow
```text
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 9dde2d36..590ceaef 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -67,6 +67,7 @@ Plugins are internal-only in the MVP. Even so, every plugin is untrusted:
- Tool results are treated as untrusted data: the system prompt tells the model not to follow instructions inside them, and tool arguments are validated against their JSON Schema before execution.
- Agent-generated artifacts are validated against their input schema before rendering.
- Artifacts are rendered by predefined React components in the main window; model-generated HTML is never injected.
+- Artifact windows load a dedicated `artifact.html` entry with only `core:default`, receive validated JSON through an initialization script, and do not import `desktopApi` or app-shell code.
- The agent cannot generate arbitrary code for execution.
## Input Validation