Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ and this project adheres to
in general (see `CLAUDE.md`). GFM task list items survive sanitization as
real `<input type="checkbox" disabled>` elements with correct checked
state, so a screen reader announces them as checkboxes rather than as
decorative text. No full-text search yet — that lands with the planned
`okf serve` companion. (This started life as `okf`'s `render` verb; it
decorative text. No full-text search: a static site has no server to run
the shared `ConceptSearch` scorer, and mirroring its weights in JavaScript
would fork it. (This started life as `okf`'s `render` verb; it
Comment on lines +81 to +83
moved to its own binary before ever shipping in a release, so there is no
deprecated verb or shim to call out here.)
- **A `sources[]` entry can now carry its own `usage_window` override
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Requires .NET SDK 10.0+. CI (ci.yml) runs build+test on Linux/Windows/macOS, `do
- **`src/OKF4net.Agents/`** — Microsoft Agent Framework layer exposing OKF bundle operations as function tools (e.g. `OkfBundleTools`) plus `OkfContextProvider`, an `AIContextProvider` that auto-injects budget-bounded bundle context and captures deterministic per-day memory concepts; the only project depending on `Microsoft.Agents.AI`.
- **`src/OKF4net.Catalog/`** — knowledge-catalog model and logic, referencing only `OKF4net` (BCL otherwise; zero `PackageReference`). Depended on by `OKF4net.Catalog.Hosting`. Each manifest source carries a `role` (`SourceRole`): `Knowledge` (read-only, searched by `IKnowledgeResolver`) or `Memory` (writable, scoped by a required `tier` — `session`/`user`/`tenant`, all three backed by `FileMemoryStore`, fed by `IMemoryStore`, never searched by the resolver); any other `role` string in `catalog.json` is rejected (`CatalogDiagnosticCode.IllegalRole`).
- **`src/OKF4net.Catalog.Hosting/`** — host-integration layer for the catalog, referencing only `OKF4net.Catalog`. This is the sole project allowed a `Microsoft.Extensions.*` package (`Microsoft.Extensions.DependencyInjection.Abstractions`) — an explicit, narrowly-scoped exception to the zero-dependency rule so catalog sources can register with a host's `IServiceCollection`; the core dependency graph (`OKF4net.Catalog` → `OKF4net`) stays zero-dependency and acyclic.
- **`src/OKF4net.Viewer/`** — static HTML site generation for a bundle, referencing only `OKF4net` (BCL otherwise; zero `PackageReference`). Backs the `okf-render` binary (`src/OKF4net.Render/`), not `okf` itself. Three units: `SiteModel` (pure `Bundle` → display-model projection), `HtmlWriter` (the only I/O), `ViewerAssets` (embedded CSS/JS). Markdown is rendered **client-side** by a vendored copy of marked (MIT, v15.0.12, credited in `NOTICE`) — the generated page carries its raw markdown plus a link-rewiring table as an HTML-safe JSON payload, escaped by `HtmlSafeJson` so untrusted bundle content cannot break out of the `<script>` container. Raw HTML is neutralized by sanitizing the **parsed DOM** in `viewer.js`, not by marked itself (it has no `sanitize` option any more) and not by patching marked's renderer hooks (tried and dropped — see below): an element allowlist (gated for a handful of tags by an attribute-value constraint table, e.g. `<input>` survives only as `type="checkbox"`, forced `disabled`, since a screen reader announces a real checkbox with its state where a decorative glyph would lose it), a per-tag attribute allowlist that drops every `on*` handler, URL-scheme validation on `href`/`src`, and an opaque-tags table (`<script>`/`<style>`) dropped with no text kept, since their content is source, not prose. This sanitizer is the whole defense, not one layer of it — renderer-hook patching (suppressing marked's `html` renderer output) was tried and measured against the vendored build plus the hostile-payload battery in `tools/viewer-security-check/`: it stopped nothing the sanitizer alone didn't already stop, while it silently deleted benign wrapped content (e.g. `<details><summary>...</summary>body</details>` rendered as `""` instead of keeping "body"), because marked's `Renderer.image()` interpolates the `alt` attribute with no escaping at all — `![foo" onerror="alert(1)](x.png)` breaks out of the attribute with no raw-HTML token involved, a class of bug no renderer-hook override can see, let alone stop; no amount of patching marked's hooks bounds that class in general. **Do not reintroduce renderer-hook patching as "extra defense in depth"** — it buys no security property the sanitizer lacks and reintroduces the content-loss bug. xunit runs on .NET and cannot execute JavaScript, so `tests/OKF4net.Tests/Viewer/ViewerAssetsTests.cs` only smoke-checks for source-text markers (allowlist names, rejected schemes) — those tests stay green even if the sanitizer is gutted, and are **not** proof it works. The real guard is `tools/viewer-security-check/`, a Node/jsdom harness that runs the actual vendored `marked.min.js` and `viewer.js` against hostile payloads; CI runs it as the `viewer sanitizer (JS)` job. **Whenever you re-vendor `marked.min.js` or edit `viewer.js`, that harness is what tells you whether the defense still holds** — add a case to it for any new payload class you discover. No full-text search by design: a static site has no server to run `ConceptSearch`, and mirroring its weights in JS would fork the scorer — search lands with the planned `okf serve`.
- **`src/OKF4net.Viewer/`** — static HTML site generation for a bundle, referencing only `OKF4net` (BCL otherwise; zero `PackageReference`). Backs the `okf-render` binary (`src/OKF4net.Render/`), not `okf` itself. Three units: `SiteModel` (pure `Bundle` → display-model projection), `HtmlWriter` (the only I/O), `ViewerAssets` (embedded CSS/JS). Markdown is rendered **client-side** by a vendored copy of marked (MIT, v15.0.12, credited in `NOTICE`) — the generated page carries its raw markdown plus a link-rewiring table as an HTML-safe JSON payload, escaped by `HtmlSafeJson` so untrusted bundle content cannot break out of the `<script>` container. Raw HTML is neutralized by sanitizing the **parsed DOM** in `viewer.js`, not by marked itself (it has no `sanitize` option any more) and not by patching marked's renderer hooks (tried and dropped — see below): an element allowlist (gated for a handful of tags by an attribute-value constraint table, e.g. `<input>` survives only as `type="checkbox"`, forced `disabled`, since a screen reader announces a real checkbox with its state where a decorative glyph would lose it), a per-tag attribute allowlist that drops every `on*` handler, URL-scheme validation on `href`/`src`, and an opaque-tags table (`<script>`/`<style>`) dropped with no text kept, since their content is source, not prose. This sanitizer is the whole defense, not one layer of it — renderer-hook patching (suppressing marked's `html` renderer output) was tried and measured against the vendored build plus the hostile-payload battery in `tools/viewer-security-check/`: it stopped nothing the sanitizer alone didn't already stop, while it silently deleted benign wrapped content (e.g. `<details><summary>...</summary>body</details>` rendered as `""` instead of keeping "body"), because marked's `Renderer.image()` interpolates the `alt` attribute with no escaping at all — `![foo" onerror="alert(1)](x.png)` breaks out of the attribute with no raw-HTML token involved, a class of bug no renderer-hook override can see, let alone stop; no amount of patching marked's hooks bounds that class in general. **Do not reintroduce renderer-hook patching as "extra defense in depth"** — it buys no security property the sanitizer lacks and reintroduces the content-loss bug. xunit runs on .NET and cannot execute JavaScript, so `tests/OKF4net.Tests/Viewer/ViewerAssetsTests.cs` only smoke-checks for source-text markers (allowlist names, rejected schemes) — those tests stay green even if the sanitizer is gutted, and are **not** proof it works. The real guard is `tools/viewer-security-check/`, a Node/jsdom harness that runs the actual vendored `marked.min.js` and `viewer.js` against hostile payloads; CI runs it as the `viewer sanitizer (JS)` job. **Whenever you re-vendor `marked.min.js` or edit `viewer.js`, that harness is what tells you whether the defense still holds** — add a case to it for any new payload class you discover. No full-text search by design: a static site has no process to run `ConceptSearch` in, and mirroring its weights in JS would fork the scorer. Search is planned to arrive via the VS Code extension in `ROADMAP.md`, whose extension host can call the .NET side — **there is no `okf serve` and none is planned** (#40's live-server half was dropped in its favour).
- **`src/OKF4net.Render/`** — the `okf-render` binary (`okf-render <bundle> --out <dir>`), published Native AOT (`PublishAot`, `InvariantGlobalization`) like `OKF4net.Cli`, referencing only `OKF4net.Viewer` (and `OKF4net`). Split out of `OKF4net.Cli` so `okf validate` — the small, dependency-free CI validator distributed via winget — does not carry `OKF4net.Viewer`'s vendored viewer JavaScript, which every `okf validate` run paid for (+164 KB / +4.0% on the AOT binary) but never executed; the same "leaf owns its own dependencies" pattern `OKF4net.Mcp` already established. All logic lives in `OkfRenderCli.Run(args, out, err)`, mirroring `OkfCli`, but hand-rolls a small scanner for its one command rather than porting `OkfCli`'s multi-verb `VerbSpec`/`CliArgs` machinery. `render` never shipped in a release, so `okf` carries no shim or hint pointing at this binary — `okf render` is an ordinary unknown subcommand.
- **`src/OKF4net.Mcp/`** — a local [Model Context Protocol](https://modelcontextprotocol.io) server exposing one OKF bundle over stdio, published as the `okf-mcp` `dotnet tool` (`PackAsTool`, no Native AOT). Thin entry point: `Program.cs` resolves the bundle root + read-only flag via `OkfMcpConfig` (testable static; on misconfig it prints a one-line usage/error to stderr and exits non-zero) and starts a stdio host serving `OKF4net.Agents`' `OkfBundleTools`. **stdio invariant: stdout is reserved for the JSON-RPC stream, every log line goes to stderr.** Tests live in `tests/OKF4net.Tests/Mcp/`.
- **`tests/OKF4net.Tests/`** — xunit. `GoldenParityTests` diffs CLI output byte-for-byte against `tests/fixtures/golden/`; tests locate the repo root by walking up from the test assembly to `OKF4net.sln`. Some parity tests temporarily set the CWD to the repo root because goldens embed the relative bundle path as given on the command line. Catalog and Catalog.Hosting tests live here too (`Catalog/`) rather than in separate test projects.
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,11 @@ okf-render bundles/ga4 --out /tmp/ga4-site
```

The generated site is self-contained and opens straight off the filesystem —
no server needed. It is read-only; full-text search arrives with the planned
`okf serve` companion. `okf-render` has its own winget package,
no server needed. It is read-only, and has no full-text search: a static site
has no server to run the shared `ConceptSearch` scorer, and mirroring its
weights in JavaScript would fork it. Interactive browsing with search is
Comment on lines +262 to +264
planned as a VS Code extension instead (see `ROADMAP.md`), not as a local
server. `okf-render` has its own winget package,
**`Coderise.OKF4net.Render`**, built and attached to each Release the same way
as `okf`'s — but its *first* submission to `winget-pkgs` is a manual, one-time
step (see [`packaging/winget/README.md`](packaging/winget/README.md)) that has
Expand Down
17 changes: 12 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,15 @@ are the concrete entry points.
binary (`OKF4net.Render`, over `OKF4net.Viewer`) — split out of `okf`
itself so the CI-facing validator does not carry the viewer's JavaScript.
The live-server half of [#40](https://github.com/jchable/okf4net/issues/40)
remains open — it is what unlocks full-text search in the viewer, since a
server can run `ConceptSearch` directly instead of mirroring its weights in
JavaScript. Its implementation approach (zero-dep `HttpListener`, ASP.NET
Core, or a standalone web tool) is still open.
was **dropped, and the issue closed** — the interactive, always-fresh
viewing it was meant to provide is being pursued as the VS Code extension
below instead, which reaches the same goal from inside the editor without
a local HTTP server, and reaches full-text search by the same route (an
extension host is a process, so it can have the .NET side run
`ConceptSearch` rather than mirroring its weights in JavaScript). What the
server would have added over `okf-render` alone was one saved command
invocation per edit; search was the only capability that genuinely
required it, and the extension gets that too.
- **The client-side XSS defense is guarded by a JS harness, not by xunit.**
xunit runs on .NET and cannot execute JavaScript, so
`tests/OKF4net.Tests/Viewer/ViewerAssetsTests.cs` only smoke-checks for
Expand Down Expand Up @@ -99,7 +104,9 @@ are the concrete entry points.
page. Better fit: `SiteModel.Build` is a pure `Bundle` → model projection
with no I/O, so a JSON output mode emitting exactly the `{ body, links }`
payload for one concept would let the extension re-render a single page
per save, and shares its plumbing with the live-server half of #40 above.
per save. That JSON payload mode is now the *only* consumer of this
plumbing, since the live-server half of #40 was dropped in favour of
this extension.
`HtmlWriter` and `HtmlSafeJson` do not transfer at all: output layout and
write-containment guards are static-site concerns, and a webview receives
the payload by `postMessage` as a real object rather than escaping it
Expand Down