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ペイン構成のワークスペース。左にタスク一覧、中央にライブの会話とレンダリング済み成果物、右に状態・ミッションブリーフ・ファイル・トレースをまとめたサイドパネル: + +![タスクワークスペース](docs/screenshots/workspace.png) + +
+ほかのスクリーンショット — タスク一覧・設定 + +実行中 / 待機中 / 完了をひと目で把握できるタスク一覧(クイックフィルタ付き): + +![タスク一覧](docs/screenshots/task-list.png) + +設定画面。`config.yaml` の各セクションがフォーム化(LLM ワーカー・サンドボックス・認証・ツールほか): + +![設定](docs/screenshots/settings.png) + +
+ ## 主な機能 - **タスク自動ルーティング** — タスク本文を 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: + +![Task workspace](docs/screenshots/workspace.png) + +
+More screenshots — task list and settings + +Task list with at-a-glance status (running / waiting / done) and quick filters: + +![Task list](docs/screenshots/task-list.png) + +Settings: every `config.yaml` section as a form — LLM workers, sandbox, auth, tools, and more: + +![Settings](docs/screenshots/settings.png) + +
+ ## 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 `