diff --git a/README.ja.md b/README.ja.md
index dc5b3e6..5524eed 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -12,6 +12,25 @@
OpenAI 互換の LLM エンドポイント([Ollama](https://ollama.com/) / vLLM など)があれば単体で動作する。
+## スクリーンショット
+
+3ペイン構成のワークスペース。左にタスク一覧、中央にライブの会話とレンダリング済み成果物、右に状態・ミッションブリーフ・ファイル・トレースをまとめたサイドパネル:
+
+
+
+
+ほかのスクリーンショット — タスク一覧・設定
+
+実行中 / 待機中 / 完了をひと目で把握できるタスク一覧(クイックフィルタ付き):
+
+
+
+設定画面。`config.yaml` の各セクションがフォーム化(LLM ワーカー・サンドボックス・認証・ツールほか):
+
+
+
+
+
## 主な機能
- **タスク自動ルーティング** — タスク本文を LLM が分類し、最適な Piece(YAML ワークフロー)へ振り分け。
diff --git a/README.md b/README.md
index 6db35e8..de6e1e2 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,25 @@ English | [日本語](README.ja.md)
It works standalone as long as you have an OpenAI-compatible LLM endpoint ([Ollama](https://ollama.com/) / vLLM, etc.).
+## Screenshots
+
+The three-pane workspace — task list on the left, the live conversation and rendered output in the center, and a structured side panel (status, mission brief, files, trace) on the right:
+
+
+
+
+More screenshots — task list and settings
+
+Task list with at-a-glance status (running / waiting / done) and quick filters:
+
+
+
+Settings: every `config.yaml` section as a form — LLM workers, sandbox, auth, tools, and more:
+
+
+
+
+
## Key features
- **Automatic task routing** — the LLM classifies the task body and dispatches it to the best-fit Piece (a YAML workflow).
diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png
new file mode 100644
index 0000000..cd7c25d
Binary files /dev/null and b/docs/screenshots/settings.png differ
diff --git a/docs/screenshots/task-list.png b/docs/screenshots/task-list.png
new file mode 100644
index 0000000..b20ba2a
Binary files /dev/null and b/docs/screenshots/task-list.png differ
diff --git a/docs/screenshots/workspace.png b/docs/screenshots/workspace.png
new file mode 100644
index 0000000..79be7ad
Binary files /dev/null and b/docs/screenshots/workspace.png differ
diff --git a/package-lock.json b/package-lock.json
index c2be0b4..3db2f86 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3349,17 +3349,17 @@
}
},
"node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@@ -3605,9 +3605,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -3617,9 +3617,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.18",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
- "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
+ "version": "4.12.25",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
+ "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -6787,9 +6787,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.20.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/src/bridge/security-headers.test.ts b/src/bridge/security-headers.test.ts
new file mode 100644
index 0000000..074ab8c
--- /dev/null
+++ b/src/bridge/security-headers.test.ts
@@ -0,0 +1,58 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+ applySecurityHeaders,
+ securityHeadersMiddleware,
+ HSTS_MAX_AGE_SECONDS,
+} from './security-headers.js';
+import { type Request, type Response } from 'express';
+
+function fakeRes() {
+ const headers: Record = {};
+ const res = {
+ setHeader(name: string, value: string) {
+ headers[name] = value;
+ },
+ } as unknown as Response;
+ return { res, headers };
+}
+
+describe('applySecurityHeaders', () => {
+ it('sets the baseline hardening headers on every response', () => {
+ const { res, headers } = fakeRes();
+ applySecurityHeaders({ secure: false } as Request, res);
+ expect(headers['X-Content-Type-Options']).toBe('nosniff');
+ expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
+ expect(headers['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
+ });
+
+ it('keeps X-Frame-Options at SAMEORIGIN so the app can serve its own iframes', () => {
+ // PDF preview and noVNC browser sessions embed same-origin iframes; DENY
+ // would break them.
+ const { res, headers } = fakeRes();
+ applySecurityHeaders({ secure: true } as Request, res);
+ expect(headers['X-Frame-Options']).not.toBe('DENY');
+ expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
+ });
+
+ it('emits HSTS only when the response is served over TLS', () => {
+ const plain = fakeRes();
+ applySecurityHeaders({ secure: false } as Request, plain.res);
+ expect(plain.headers['Strict-Transport-Security']).toBeUndefined();
+
+ const tls = fakeRes();
+ applySecurityHeaders({ secure: true } as Request, tls.res);
+ expect(tls.headers['Strict-Transport-Security']).toBe(
+ `max-age=${HSTS_MAX_AGE_SECONDS}; includeSubDomains`,
+ );
+ });
+});
+
+describe('securityHeadersMiddleware', () => {
+ it('applies the headers and calls next()', () => {
+ const { res, headers } = fakeRes();
+ const next = vi.fn();
+ securityHeadersMiddleware()({ secure: false } as Request, res, next);
+ expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
+ expect(next).toHaveBeenCalledOnce();
+ });
+});
diff --git a/src/bridge/security-headers.ts b/src/bridge/security-headers.ts
new file mode 100644
index 0000000..b12771d
--- /dev/null
+++ b/src/bridge/security-headers.ts
@@ -0,0 +1,45 @@
+import { type Request, type Response, type NextFunction } from 'express';
+
+/**
+ * Baseline security response headers applied to every response.
+ *
+ * These are deliberately conservative so they cannot break the SPA, which
+ * serves same-origin `