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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
.scratch
node_modules
**/node_modules
**/dist
coverage
*.tgz
.env*
47 changes: 46 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
path: |
packages/web/package.json
packages/web/dist
packages/web/dist/openapi.yaml
packages/pi-web/package.json
packages/pi-web/dist
packages/dsh-web/package.json
Expand Down Expand Up @@ -161,8 +162,41 @@ jobs:
working-directory: packages/${{ matrix.directory }}
run: npm publish --no-git-checks --access public --provenance --tag "${{ steps.version.outputs.dist-tag }}"

container:
needs: preflight
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24

- name: Synchronize image package version
run: node scripts/sync-version.mjs "${GITHUB_REF_NAME#v}"

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build and publish immutable release image
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}
run: |
image="${IMAGE_NAME,,}:${GITHUB_REF_NAME}"
docker build --tag "$image" .
docker push "$image"

github-release:
needs: publish
needs: [publish, container]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -171,3 +205,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" --generate-notes --repo "$GITHUB_REPOSITORY"

- name: Download generated OpenAPI artifact
uses: actions/download-artifact@v5
with:
name: release-dist
path: packages

- name: Attach generated OpenAPI contract
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$GITHUB_REF_NAME" packages/web/dist/openapi.yaml#openapi.yaml --repo "$GITHUB_REPOSITORY"
16 changes: 16 additions & 0 deletions .scratch/defered/public-http-service-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Public HTTP service hardening

Before Guion Web's container is presented as a public or multi-tenant service, define and implement the security boundary for outbound requests and rendered browsing. The current first release is a Personal Web Service only.

Required decisions and work:

- Block private, loopback, link-local, and otherwise non-public network targets across redirects and DNS resolution, including DNS rebinding.
- Run `agent-browser` in an isolated browser/process and restrict its network egress consistently with direct fetch.
- Set resource and concurrency limits appropriate for a remotely reachable renderer.
- Define the authentication and tenant boundary when the service stops being personal.

This deferred work does not change the v1 search policy: the Personal Web
Service tries the server-configured Bridge Route first, preserves successful
empty results, and falls back exactly once to the server-configured Exa
credential only for a non-cancellation Bridge failure. Typed Bridge Data
Operations report Bridge failure directly.
141 changes: 141 additions & 0 deletions .scratch/http-web-service/implementation-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Guion Web page-reading contract implementation report

## Scope

- Repository: `guionai/web`
- Branch: `http-web-service`
- Fixed point: `9fff0766001f5ebcad91045c34958af72841da09`
- Implementation commits: `bb4900a5dba9b5d71fdd64363ce860953b2dc098` (`feat(http): unify page-reading contract`), `90de2fa9d77a6dc2ac8c34284f469e1d722e3d12` (`fix(http): repair navigation and renderer validation`), and `19b63287601c5c7472cb85cf0dae1080513b212d` (`fix(core): guard automatic tree without headings`)
- Delivery boundary: the complete `http-web-service` spec and tickets 01 and 02.
Code review and deployment were excluded.

The tickets were implemented in dependency order: the shared Core page-reading
seam first, then every adapter, HTTP/OpenAPI contract, tests, and documentation.

## Ticket outcomes

### 01 — Unify the page-reading module interface

- Core Fetch and Links now accept only `render: "http" | "browser"`.
HTTP is the default; browser requires `waitMs` from 0 through 30,000, while
HTTP forbids `waitMs`. Capability errors suggest `render: "browser"` with
`waitMs: 2000`.
- Public `tree` and `tree_threshold` inputs were removed. The module owns the
fixed 5,000-character policy: non-full, unsectioned long content with
navigable headings returns a navigation tree, while headingless long content
uses the normal bounded response. `full: true` returns complete extracted
Markdown without the Core 30,000-character truncation, and `section_id`
retrieves one tree section.
- `full: true` with `section_id` is rejected. Core validation also rejects
unknown legacy fields and invalid navigation values.
- Core tests use injected cache, HTTP, and browser seams; no live provider or
browser state is required.
- H1-only long documents now list their H1 as a selectable tree node with a
stable `section_id`; a Core behavior test retrieves the emitted ID and checks
the complete section content.

### 02 — Align adapters, HTTP, and reference documentation

- CLI flags/help, stdio MCP schemas, Pi TypeBox schemas/prompts, DSH tool
definitions, and HTTP schemas now expose the same renderer and navigation
vocabulary without compatibility aliases.
- HTTP remains exactly `/v1/search`, `/v1/fetch`, and `/v1/links`. Search keeps
server-selected Kepos Bridge-first behavior with one Exa retry; Fetch and
Links enforce the shared renderer rules.
- The build generates the version-matched OpenAPI 3.1 artifact at
`packages/web/dist/openapi.yaml`; the release artifact test parses a
test-owned generated file and asserts exactly the three retained routes and
the unified request schemas.
- Added [`docs/http-service.md`](../../docs/http-service.md), a standalone
human-readable reference covering requests, responses, validation, provider
behavior, renderer behavior, errors, and the OpenAPI release asset. README
and ADR 0001 link to and describe the same contract. Operator setup may still
name the installed browser executable.
- CONTEXT.md now uses the public `render: "http" | "browser"` vocabulary for
glossary and HTTP guidance; `agent-browser` appears only as an operator
implementation/setup detail. Pi and DSH each consolidate their repeated
renderer/wait checks in one local helper while preserving validation order,
messages, and forwarding behavior.

## Review-fix batch

The post-review repairs were applied in the scoped commits `90de2fa` and
`19b6328`:

- Core tree output for an H1-only long document emits its deterministic heading
ID, and the behavior test uses that ID to retrieve the section.
- CONTEXT.md describes the public HTTP/browser renderer contract and reserves
the `agent-browser` name for operator implementation/setup wording.
- Pi and DSH each own a local `validateRenderOptions` helper; no
cross-adapter abstraction was added.
- Core now enters automatic tree mode only when headings exist. A headingless
long document stays in bounded `mode: "full"`, while `full: true` still
returns the complete content; the Core behavior test covers both paths.

## Verification

All checks completed successfully against the implementation commit:

- `pnpm format:check` and explicit Prettier checks for the ADR, DSH README, and
standalone HTTP reference.
- `pnpm typecheck`.
- `pnpm test` — 19 test files and 117 tests passed. The existing DSH source-map
warning was non-fatal.
- `pnpm build` — all workspace packages built and generated
`packages/web/dist/openapi.yaml`.
- `pnpm test:release` — release version synchronization fixtures passed.
- `pnpm test:pack` — packed-installation/host-loading checks passed for
`@guionai/web`, `@guionai/pi-web`, and `@guionai/dsh-web`.
- Docker validation with the supported image: `docker build --tag
guionai-web-http-smoke .` passed; a disposable container returned HTTP 400
for invalid Fetch input and HTTP 200 for a real browser-rendered
`https://example.com` Fetch with `full: true`.

No live credentials were used. The Docker smoke used a disposable container
and a non-secret placeholder startup key; it was stopped after verification.

The final Core edge repair was additionally verified with:

- `pnpm exec vitest run packages/web-core/test/markdown.test.ts` — 4 tests
passed.
- `pnpm typecheck`, `pnpm format:check`, and explicit Prettier checks for the
changed Core source/test files and HTTP/CONTEXT documentation.

The review-fix commit was additionally verified with:

- `pnpm typecheck`.
- `pnpm format:check` and explicit Prettier checks for the changed source files,
CONTEXT.md, and the HTTP reference.
- `pnpm test` — 19 test files and 118 tests passed. The existing DSH source-map
warning was non-fatal.
- `pnpm build` — all workspace packages built and generated
`packages/web/dist/openapi.yaml`.
- `pnpm test:release` — release version synchronization fixtures passed.
- `pnpm test:pack` — packed-installation/host-loading checks passed for
`@guionai/web`, `@guionai/pi-web`, and `@guionai/dsh-web`.

## Changed paths and size

Against the fixed point, excluding generated `dist` output, lockfiles, and
this report:

- Product code: 328 additions and 254 deletions (582 changed lines) across
Core, CLI, MCP, Pi, DSH, and HTTP.
- Tests: 227 additions and 166 deletions (393 changed lines), including Core
navigation/renderer seams, adapter forwarding/validation, and OpenAPI
artifact parsing.
- Documentation/configuration: 229 additions and 53 deletions (282 changed
lines), including the 154-line standalone HTTP reference and aligned README,
ADR, package metadata, and DSH guide.
- Total: 784 additions and 473 deletions (1,257 changed lines), within the
spec estimate of 780–1,400 total changed lines. Product code is modestly
above its 250–450 estimate because each owned adapter now performs explicit
input normalization and legacy-field rejection at its boundary; the review
repair remains a small local delta.

## Remaining concerns

- Public or multi-tenant HTTP hardening remains out of scope as documented by
ADR 0001 and the existing deferred security note.
- Code review and deployment were intentionally not run for this implementation
task.
52 changes: 52 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Guion Web

Guion Web provides read-only research operations through local integrations and a self-hosted HTTP service. This glossary keeps the interfaces and their deployment boundary distinct.

## Language

**Personal Web Service**:
A self-hosted, single-trust-boundary deployment of Guion Web for its operator and their agents. It is not a public or multi-tenant hosted service.
_Avoid_: Public service, SaaS

**Research Operation**:
One of Guion Web's read-only capabilities: Search, Fetch, Links, Documentation Resolve or Fetch, or Public-source Search. It is independent of the transport that exposes it.
_Avoid_: Tool, endpoint

**Search**:
The general web-retrieval Research Operation. In the Personal Web Service, it uses Kepos Bridge first and transparently falls back to Exa only when the Bridge is unavailable; its response identifies the provider that supplied results.
_Avoid_: Bridge search, provider-selected search

**Bridge Data Operation**:
A typed, Bridge-only lookup for weather, sports, finance, or time. It has no Exa fallback because Exa search is not an equivalent result source.
_Avoid_: Generic Bridge command, special search

**Bridge Route**:
The server-local URL used by the Kepos Bridge search provider. The service operator configures it; API callers never supply it.
_Avoid_: Bridge URL parameter

**Page Rendering**:
Fetch and Links use `render: "http"` by default or explicit `render: "browser"`.
Browser rendering requires `waitMs` from 0 through 30,000 and is never selected
automatically. The operator-installed `agent-browser` runtime is an implementation
and setup detail, not an adapter-facing request value.
_Avoid_: Backend-specific renderer labels, automatic fallback

**Page Navigation**:
The shared page-reading module owns its fixed 5,000-character policy. A long,
unsectioned request with navigable headings returns a navigation tree; a
headingless long document uses the normal bounded response. `full: true`
returns complete Markdown, and a tree's `section_id` retrieves one section.
`full: true` and `section_id` are mutually exclusive.
_Avoid_: Caller-selected tree thresholds, public `tree` controls

**Release Contract**:
The versioned public distribution of Guion Web: its npm packages, GHCR container image, and the generated `openapi.yaml` attached to the matching GitHub Release.
_Avoid_: Checked-in OpenAPI file, independently versioned schema

**HTTP Service**:
The Hono-based `/v1` JSON API shipped by `web serve` and the GHCR image. It
uses server-local credentials and Bridge Route configuration; clients do not
select providers or submit a generic Bridge command. Its page-reading routes use
the same `render: "http" | "browser"`, `full`, and `section_id` contract; the
browser executable name appears only in operator setup.
_Avoid_: Remote MCP, public service
36 changes: 36 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Guion Web's supported HTTP-service image. The build stage bundles the web
# executable; the runtime stage adds the optional rendered-fetch capability.
FROM node:24-bookworm-slim AS build

WORKDIR /workspace
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/web-core/package.json packages/web-core/package.json
COPY packages/web/package.json packages/web/package.json
COPY packages/pi-web/package.json packages/pi-web/package.json
COPY packages/dsh-web/package.json packages/dsh-web/package.json
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm --filter @guionai/web run build

FROM node:24-bookworm

ENV NODE_ENV=production
# Chrome for Testing has no Linux ARM64 distribution. Debian's Chromium works
# on both released container architectures, and agent-browser documents this
# executable override for containers.
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app

# Rendering is deliberately explicit at the HTTP contract. The executable and
# browser runtime are image capabilities, while credentials remain env-only.
ARG AGENT_BROWSER_VERSION=0.36.0
RUN apt-get update \
&& apt-get install --yes --no-install-recommends chromium \
&& rm -rf /var/lib/apt/lists/* \
&& npm install --global agent-browser@${AGENT_BROWSER_VERSION}

COPY --from=build /workspace/packages/web/dist ./dist

EXPOSE 8787
ENTRYPOINT ["node", "/app/dist/cli.js", "serve"]
Loading
Loading