diff --git a/.dockerignore b/.dockerignore index feb3681..79817f5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,4 +21,3 @@ dist *.sqlite3 *.db tests -docs diff --git a/README.md b/README.md index b07f1d6..9ee177e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ same independently evolvable Agent loop; its scheduler, context policy, memory, tools, evidence rules, and child Agents are owned by this repository. The production instance is available at . +Architecture, Agent internals, and the experiment board are published at +. ## Architecture @@ -120,9 +122,14 @@ Open WebUI and re-signed for the Workspace Gateway; no internal key is exposed. ## Documentation +- [Documentation index](docs/README.md) - [Architecture and Agent loop](docs/architecture.md) +- [Agent loop implementation](docs/agent-loop.md) +- [Experiment system](docs/experiments.md) +- [Project history and decisions](docs/project-history.md) - [Open WebUI integration](docs/openwebui-integration.md) - [Infrastructure map](docs/infrastructure.md) +- [Development and verification](docs/development.md) - [Operations runbook](docs/operations.md) - [Security model](docs/security.md) diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile index 9b09cc0..bc35221 100644 --- a/docker/web.Dockerfile +++ b/docker/web.Dockerfile @@ -80,6 +80,7 @@ WORKDIR /app/backend COPY --from=web-build /src/backend /app/backend COPY --from=web-build /src/build /app/build COPY --from=web-build /src/CHANGELOG.md /app/CHANGELOG.md +COPY docs/site /app/build/doc COPY --from=web-build /src/LICENSE /app/OPEN_WEBUI_LICENSE COPY THIRD_PARTY_NOTICES.md /app/THIRD_PARTY_NOTICES.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..57c6cb3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,55 @@ +# K1412 Agent documentation + +This directory is the canonical technical record for K1412 Agent. The public +documentation portal at is a curated rendering +of the same design. When behavior, infrastructure, or an experiment changes, +update the relevant Markdown document and the portal snapshot in the same +commit. + +## Start here + +| Document | Audience | What it answers | +| --- | --- | --- | +| [Architecture](architecture.md) | Everyone | What runs where, and why are the boundaries drawn this way? | +| [Agent loop](agent-loop.md) | Agent researchers and backend engineers | How do context, tools, scheduling, evidence, memory, and child Agents work? | +| [Experiment system](experiments.md) | Agent researchers | How do we change the loop without losing comparability or production safety? | +| [Project history](project-history.md) | Maintainers | Which product and architecture decisions have already been made? | +| [Open WebUI integration](openwebui-integration.md) | Frontend and platform engineers | What belongs to Open WebUI, and what remains K1412-owned? | +| [Security model](security.md) | Security and platform engineers | How are users, credentials, workspaces, and Docker separated? | +| [Infrastructure map](infrastructure.md) | Operators | Which services are shared infrastructure and which are Agent core? | +| [Development and verification](development.md) | Contributors | How do I run, test, and safely change the system? | +| [Operations runbook](operations.md) | Operators | How is production built, deployed, verified, backed up, and migrated? | + +## Current product contract + +- There is one user-facing mode: the K1412 Agent loop. The earlier Chat/Work + split was removed deliberately. +- Open WebUI owns accounts, sessions, approval, conversation storage, and the + chat shell. K1412 owns Agent behavior. +- Four server-defined model choices are exposed: Luna, Terra, Sol, and + DeepSeek V4 Pro. Model identity and thinking capability are displayed + separately. +- Every Open WebUI user receives an isolated Docker container, network, and + persistent volume on the dedicated physical execution host. +- Generated files can be browsed and downloaded through the authenticated Web + UI. Internal dependency directories are hidden. +- Provider credentials, internal prompts, Docker access, SSH configuration, + and tool-service credentials are never browser-configurable. + +## What is experimental + +The stable platform boundary is authentication, user/workspace isolation, +durable storage, and the public OpenAI-compatible Runtime API. The intended +experimental surface is: + +- context selection and compaction; +- memory retrieval and lifecycle; +- tool descriptions, normalization, and routing; +- parallel scheduling and mutation serialization; +- child-Agent delegation policy; +- evidence and completion gates; +- recovery prompts and retry policy; +- model routing and per-tier budgets. + +Every experiment should be versioned in run events and evaluated against a +fixed task set before production rollout. See [experiments.md](experiments.md). diff --git a/docs/agent-loop.md b/docs/agent-loop.md new file mode 100644 index 0000000..ecb2757 --- /dev/null +++ b/docs/agent-loop.md @@ -0,0 +1,223 @@ +# Agent loop implementation + +## Purpose + +The K1412 loop turns a model completion API into an accountable coding Agent. +Its primary rule is simple: a textual claim is not proof that work happened. +Files, commands, reports, and tests must be backed by successful tool events +from the current run. + +The current event metadata identifies the implementation as: + +- strategy: `agent-loop-v3`; +- scheduler: `safe-parallel-v1`; +- context policy: `recent-visible-v1`. + +These identifiers are part of the experiment contract, not marketing version +numbers. Change them when behavior changes in a way that can affect evaluation. + +## Run state machine + +```mermaid +stateDiagram-v2 + [*] --> BuildContext + BuildContext --> RequestModel + RequestModel --> ValidateCalls: tool calls + RequestModel --> CheckCompletion: no tool calls + ValidateCalls --> Schedule + Schedule --> Execute + Execute --> RecordEvidence + RecordEvidence --> RequestModel + CheckCompletion --> RequestModel: evidence missing and budget remains + CheckCompletion --> Completed: evidence sufficient + CheckCompletion --> Unverified: evidence missing and retry budget exhausted + Completed --> [*] + Unverified --> [*] +``` + +Every top-level run receives a unique `run_id`. All events include the user, +chat, monotonic sequence, timestamp, event type, and a sanitized payload. + +## Context policy + +`ContextPolicy.prepare`: + +1. accepts only system, developer, user, and assistant messages; +2. strips rendered `
` blocks from old assistant + messages so UI markup is not fed back as model context; +3. prepends the K1412 system contract; +4. appends up to eight durable user memories as context, explicitly not as + higher-priority instructions; +5. walks visible messages newest-first until the model's character budget is + reached; +6. records how many old messages were dropped. + +The policy is intentionally simple and inspectable. It does not yet summarize +old conversation branches, retrieve semantic workspace context, or estimate +provider-specific tokenization. Those are future experiment candidates. + +## Model contract + +`ModelSpec` is the server-side source of truth for: + +- public and provider model IDs; +- provider selection; +- thinking capability and display label; +- reasoning effort when the provider supports it; +- maximum output tokens; +- maximum loop iterations; +- context character budget. + +Luna, Terra, and Sol are separate local models with boolean thinking support; +they are not three effort settings for one model. DeepSeek V4 Pro uses the +DeepSeek provider with thinking enabled and `reasoning_effort=max`. + +Runtime preserves provider `reasoning_content` across tool turns when required +for protocol correctness, but does not publish hidden reasoning as user-facing +content. + +## Tool catalog + +Workspace tools: + +- status, list, read, search, write, and patch files; +- execute foreground commands; +- inspect Git status and diff; +- start, poll, and cancel background processes. + +Runtime state tools: + +- update a per-chat plan; +- remember, recall, and forget durable user memory. + +Delegation tool: + +- start one bounded child Agent with a role, task, and explicit write policy. + +Tool schemas use `additionalProperties: false` so malformed model arguments +fail early. Arguments are normalized before policy checks. Content and patch +bodies are omitted from public run-event payloads. + +## Intent-based tool selection + +The root loop does not always send every tool. Lightweight request classifiers +detect whether the task concerns artifacts, execution, source files, reports, +comparisons, processes, Git, memory, or delegation. The selected catalog keeps +the model's tool decision smaller while retaining the core workspace tools. + +This is heuristic routing, not authorization. Gateway remains the enforcement +boundary. + +## Scheduler + +Each model response can request at most eight tool calls. Calls are parsed in +their original order and divided into consecutive groups: + +- tools marked `parallel_safe` execute concurrently; +- mutations and other non-parallel tools execute serially; +- read-only child Agents may run in parallel; +- a child with write access is serialized; +- child Agents cannot delegate again. + +Gateway also serializes workspace mutations per user. This second lock is +important because multiple Runtime requests or browser actions can address the +same workspace. + +The scheduler preserves result order even when a parallel group completes out +of order. + +## Defensive normalization and retry guards + +The loop repairs a small set of common model formatting mistakes, then applies +policy: + +- reject bare interactive commands such as `python3`, `bash`, or `node`; +- reject attempts to run documentation/data files as executable source; +- reject syntactically invalid complete Python files before writing; +- decode repeated escaped source-layout newlines produced by weak models; +- skip identical successful writes; +- block an identical failed write until content changes; +- block an unchanged failed command until a mutation or materially different + diagnostic occurs; +- deduplicate repeated commands in one batch; +- cap generation, iteration, and tool-batch sizes. + +At the execution layer, Bash runs with `pipefail` and returns the true exit +code. Foreground tools default to a 900-second ceiling; background processes +have explicit start/poll/cancel lifecycle. + +## Evidence-gated completion + +The loop derives required evidence from the user request. + +| Request shape | Minimum evidence | +| --- | --- | +| Concrete artifact | A successful file mutation and a later successful verification | +| Execute/test/analyze | At least one successful execution or inspection | +| Source-code request | A separate executable source file | +| Report request | A separate report written after execution | +| Benchmark/comparison report | Exact numeric measurements copied from successful execution output | + +Verification includes a later file read, Git diff/status, command execution, or +background-process poll. A failed command remains unresolved until a later +execution succeeds. + +If a model tries to finish too early, Runtime emits `completion.rejected`, +returns a focused recovery instruction to the model, and continues. After +repeated rejected checkpoints or iteration exhaustion, Runtime returns an +explicit incomplete result instead of laundering an unverified claim into a +success. + +## Delegation + +A root Agent can delegate a bounded independent task. The child receives: + +- a role and exact task; +- a reduced iteration budget of at most eight; +- no delegation tool; +- read-only tools unless write access was explicitly requested. + +Child events share the root run stream with an increased `depth`. The current +implementation is recursive execution inside one Runtime process, not a +distributed queue or a persistent autonomous worker. + +## Memory and plans + +Durable memories are scoped by user ID. Recall is currently recency-first with +optional case-insensitive substring filtering. Plans are scoped by user and +chat and allow at most one `in_progress` item. + +Memory is useful but intentionally conservative: the system does not +automatically extract facts from every conversation and does not yet perform +embedding retrieval, confidence scoring, expiry, or conflict resolution. + +## Run event model + +Important event types include: + +- `run.created`, `context.built`, `run.completed`, `run.failed`, + `run.cancelled`; +- `model.requested`, `model.responded`; +- `tool.started`, `tool.completed`, `tool.batch_limited`; +- `completion.rejected`, `completion.unverified`; +- `agent.spawned`, `agent.completed`. + +The event stream powers UI detail blocks today and is the basis for future +replay, evaluation, cost analysis, and A/B assignment. Events store public +arguments and concise summaries, not credentials, full write bodies, or hidden +reasoning. + +## Known limitations + +- Context compaction drops old messages instead of summarizing them. +- Memory retrieval is lexical rather than semantic. +- Tool intent classifiers are regular-expression based. +- The scheduler parallelizes only consecutive safe calls and has no resource + cost model. +- Child Agents are not durable across Runtime restarts. +- Experiment assignment and aggregate dashboards are not yet implemented. +- Provider token accounting is retained when returned but is not yet converted + into a unified cost model. + +These are deliberate experiment targets, documented in +[experiments.md](experiments.md). diff --git a/docs/architecture.md b/docs/architecture.md index 587e876..0cc284c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,74 +1,185 @@ # Architecture -## Single Agent path +## Design objective -Open WebUI owns authenticated users, conversation history, and rendering. -Every normal model request is delegated to the K1412 Runtime. There is no -second Chat loop and no per-conversation mode state. +K1412 Agent is a multi-user web coding Agent whose loop can be changed +independently of its account system and user interface. The architecture keeps +the high-cost experimental surface small: researchers should be able to change +context, memory, scheduling, tools, delegation, or completion policy without +forking authentication, chat history, Docker lifecycle, or the whole frontend. -## Agent loop +The system deliberately has one Agent path. The earlier idea of a lightweight +Chat loop beside a custom Work loop was removed because it duplicated product +behavior, confused model selection, and increased the amount of frontend and +backend code that had to remain compatible. -The first strategy is intentionally modular: +## System context -- `ModelProvider` owns provider transport. -- `ContextPolicy` owns visible-message cleanup and compaction. -- `ToolRegistry` owns tool schemas and dispatch. -- `AgentLoop` owns iteration, safe parallel scheduling, and delegation. -- `RuntimeStore` owns events, plans, and durable Agent memory. -- `ExecutionProvider` owns local or SSH Docker execution. +```mermaid +flowchart LR + U["Browser user"] --> P["DNS, TLS and Nginx Proxy Manager"] + P --> W["Open WebUI
auth, RBAC, history, UI"] + W --> R["Agent Runtime
loop, context, memory, model routing"] + R --> M["Model providers
K1412 API or DeepSeek"] + R --> G["Workspace Gateway
identity and execution policy"] + G -->|Tailscale + SSH Docker| X["Per-user workspace container
home-node-itx"] + W --> DB["PostgreSQL
users and chats"] + R --> DB + W --> Q["Redis
coordination"] +``` -Every run records its model ID, strategy version, scheduler version, context -policy, model/tool events, failures, and completion. This event stream is the -foundation for replay, evaluation, and future experiment assignment. +Only Web is publicly reachable. Runtime, Gateway, PostgreSQL, and Redis live on +private Compose networks. Workspace containers run on a separate physical +Docker host, receive no application secrets, and expose no host ports. -Luna, Terra, and Sol are three independent Ollama models. Each advertises -boolean thinking support, but none advertises adjustable reasoning effort. -DeepSeek V4 Pro is a fourth model configured with thinking enabled and maximum -reasoning effort. Provider selection and credentials remain server-side. -Thinking state is preserved across tool turns when the provider requires the -assistant's `reasoning_content` to be returned with following tool results. -The Agent never publishes hidden reasoning as its own user-facing output. +## Responsibility boundaries -Completion is evidence-gated. Requests for scripts, reports, code, or -other concrete artifacts are not accepted as complete until a file mutation -has succeeded and a later read, diff, or command has verified the latest -mutation. Bare interactive shell commands are rejected. If the iteration -budget is exhausted without evidence, Runtime returns an explicit incomplete -checkpoint instead of repeating an unverified model claim. +| Layer | Owns | Must not own | +| --- | --- | --- | +| Open WebUI | Registration, login, sessions, admin approval, RBAC, chat persistence, rendering | Agent scheduling, provider secrets, Docker access | +| Agent Runtime | Model catalog, context policy, Agent loop, tools, scheduling, delegation, memory, plans, run events, evidence gates | Browser sessions, Docker socket | +| Workspace Gateway | Signed identity enforcement, workspace path policy, per-user locks, Docker lifecycle, file delivery | Model calls, public authentication UI | +| Workspace container | User code, dependencies, generated files, command execution | Other users' data, service credentials, Docker socket | +| PostgreSQL | Open WebUI durable state plus Runtime events, plans, and memories | Execution | +| Shared infrastructure | TLS, registry, source hosting, private routing, model serving | Agent behavior | -Read-only tools and read-only child Agents may run concurrently. Workspace -mutations are serialized. A child Agent cannot delegate again, and read-only -children receive only read-only tools. +This separation lets the Web UI be upgraded on its own schedule while the +Agent loop can iterate frequently. -## Execution providers +## Request lifecycle -`local-docker` connects the Workspace Gateway to the local Docker daemon. -`ssh-docker` connects the same Docker SDK operations to a remote physical -machine through SSH. Both create the same workspace image, persistent per-user -volume, and dedicated per-user bridge network, so moving execution off the web -host changes configuration rather than Agent behavior. +### Agent request -For SSH mode, layer `compose.ssh.yaml` over the development Compose file, or -`deploy/docker-compose.ssh.yml` over the two production Compose files. The -override removes the local Docker socket and mounts only the selected SSH -configuration directory read-only. +```mermaid +sequenceDiagram + participant B as Browser + participant W as Open WebUI + participant R as Runtime + participant M as Model provider + participant G as Gateway + participant X as User workspace -Only Open WebUI publishes a host port. Runtime, Gateway, Postgres, and Redis -remain unpublishing services. Runtime and Gateway also join an egress-only -bridge so the model API and a future SSH Docker host are reachable without -exposing either service on the host. + B->>W: Authenticated chat request + W->>R: OpenAI-compatible request + service key + signed user identity + R->>R: Verify identity, build context, record run.created + loop Until evidence-backed completion or budget exhausted + R->>M: Recent context + tools + provider policy + M-->>R: Assistant content and/or tool calls + R->>R: Normalize, validate, limit, and schedule calls + R->>G: Tool request + service key + freshly signed identity + G->>G: Verify identity and bind user workspace + G->>X: Execute with resource and path constraints + X-->>G: Exit code, output, or file result + G-->>R: Structured tool result + R->>R: Persist events and evaluate completion evidence + end + R-->>W: SSE answer and completed tool detail blocks + W-->>B: Rendered Agent response +``` -## Workspace file path +The identity sent by Open WebUI is verified once when Runtime accepts the +request. Runtime then signs the already verified identity again for every +Gateway call. This prevents a long Agent run from reusing an identity token +that expired while the model or a tool was working. + +### Workspace file request ```text browser with Open WebUI session -> /api/v1/k1412/workspace/* - -> Open WebUI backend verifies the user - -> backend mints a short-lived signed identity JWT - -> Workspace Gateway verifies service key + user JWT - -> provider selects only that user's hashed container and volume + -> Open WebUI verifies the logged-in user + -> Web signs a short-lived user identity + -> Gateway verifies service key + signed identity + -> Gateway selects only the hashed container and volume for that user + -> browse, download, or streamed archive response ``` -The file API exposes read-only browse, file download, and streamed directory -archive operations. Paths pass through the same `/workspace` normalization as -Agent tools. The browser never talks to Gateway or Docker directly. +The browser never receives a Gateway service key and never connects to Docker. + +## Runtime internals + +Runtime is an OpenAI-compatible provider from Open WebUI's point of view, but +normal chat completions are implemented by the K1412 loop: + +- `ModelProvider` translates the internal model spec to a provider request and + normalizes streamed completion responses. +- `ContextPolicy` removes rendered tool-detail markup, prepends the system + contract and durable memories, and keeps the newest visible messages within + a per-model character budget. +- `ToolRegistry` declares tool schemas and routes workspace tools to Gateway or + state tools to `RuntimeStore`. +- `AgentLoop` owns iteration, tool normalization, validation, scheduling, + recovery, delegation, and evidence-gated completion. +- `RuntimeStore` persists run events, per-chat plans, and user memories. + +More detail is in [agent-loop.md](agent-loop.md). + +## Workspace architecture + +`local-docker` and `ssh-docker` implement the same `ExecutionProvider` +contract. Production uses `ssh-docker`: Gateway talks to Docker on +`home-node-itx` through a dedicated SSH configuration mounted read-only. + +Each immutable Open WebUI user ID maps to a SHA-256-derived workspace ID and +therefore to exactly one: + +- container named `k1412-ws-`; +- named volume `k1412-ws-data-`; +- bridge network `k1412-ws-net-`. + +Containers run as UID/GID 1000 with a read-only root filesystem, all Linux +capabilities dropped, `no-new-privileges`, PID/CPU/memory limits, no Docker +socket, and no published ports. `/workspace` is the only persistent writable +filesystem. `/tmp` is writable but mounted `noexec`. + +Python environments live at `/workspace/.venv`. User-level home and package +caches live below `/workspace/.agent`. The image puts the virtual environment +and user binary directories first on `PATH`. Commands run under Bash with +`pipefail`, so a failed install cannot be hidden by a successful `tail` or +similar pipeline stage. + +When `WORKSPACE_IMAGE` changes to a new immutable tag, Gateway replaces a stale +container on its next access but reattaches the existing named volume. Image +upgrades therefore change the execution environment without deleting user +files. + +## Networks and deployment + +Production runs as one Unraid Compose Manager project: + +- `public`: Web ingress; +- `internal`: private service-to-service traffic; +- `egress`: Runtime provider access and Gateway SSH access. + +Only Web publishes NAS port `12004`. Nginx Proxy Manager on the VPS terminates +TLS for `agent.k1412.top` and forwards over Tailscale. Images are built for +`linux/amd64`, pushed to `docker.k1412.top/wuyang/*` with immutable +timestamp-and-commit tags, and deployed by updating only the relevant service. + +The documentation portal is packaged inside the Web image at `/doc/`. It does +not add a public service, port, database, or proxy rule. + +## Stable and experimental surfaces + +Stable platform contracts: + +- authenticated Web-to-Runtime request; +- signed Runtime/Web-to-Gateway identity; +- `ExecutionProvider` workspace semantics; +- per-user volume isolation; +- run-event persistence; +- public model IDs; +- workspace file API. + +Expected experiment surfaces: + +- prompt and context policy; +- memory selection; +- tool catalog and intent routing; +- scheduler and parallelism; +- delegation; +- recovery and evidence rules; +- model budgets and routing. + +Version experiment-sensitive behavior in `run.created` so results remain +comparable after the implementation changes. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..870745f --- /dev/null +++ b/docs/development.md @@ -0,0 +1,129 @@ +# Development and verification + +## Repository layout + +```text +agent_platform/ + runtime/ Agent API, loop, context, model transport, and tools + gateway/ Authenticated workspace and file service + auth.py Internal service and signed identity verification + models.py Public model catalog and server-side budgets + store.py Run events, memories, and plans +docker/ + web/ Open WebUI components and reproducible patch set + *.Dockerfile Web, Runtime, Gateway, and Workspace images +docs/ + site/ Static source for /doc/ +deploy/ Production Compose files and SSH override +e2e/ Deterministic full-stack verifier and fake provider +scripts/ Verification, evaluation, audit, and secret helpers +tests/ Unit and real Docker integration tests +``` + +## Local environment + +```bash +python3 -m venv .venv +.venv/bin/python -m pip install -e '.[dev]' +cp .env.example .env +./scripts/init-secrets.sh +``` + +Keep real provider credentials in a protected local environment or deployment +file. Never add them to Git, Docker build arguments, frontend code, test +fixtures, or logs. + +Build the user workspace, then start the stack: + +```bash +docker compose --profile build-only build workspace-image +docker compose up --build +``` + +Open . New registrations have the `pending` role until +approved by the bootstrap administrator. + +## Fast feedback + +```bash +.venv/bin/ruff check agent_platform tests e2e +.venv/bin/ruff format --check agent_platform tests e2e +.venv/bin/pytest +``` + +Unit tests are deterministic and do not require a model provider. + +## Real Docker integration + +```bash +docker build -f docker/workspace.Dockerfile \ + -t k1412-agent-workspace:test . +RUN_DOCKER_INTEGRATION=1 \ + .venv/bin/pytest tests/test_docker_workspace.py +``` + +The test creates unique containers, networks, and volumes and removes only +those resources in cleanup. + +## Full-stack E2E + +```bash +./scripts/verify-e2e.sh +``` + +The script builds a disposable six-service stack, uses a deterministic model +stub, performs authentication and isolation checks, and removes the stack and +test volumes on exit. + +## Live Agent evaluation + +```bash +set -a +source /path/to/protected/provider.env +set +a +.venv/bin/python scripts/eval-live-work.py --model deepseek-v4-pro +``` + +Use `--keep-workspace` only when inspecting a failed case; remove that uniquely +named evaluation workspace afterward. + +## Image audit + +```bash +./scripts/audit-images.sh +``` + +The audit requires consistent Python environments and no fixable +High/Critical vulnerabilities in the finished images. Web also receives a +Python dependency audit. + +## Documentation portal + +`docs/site/` is dependency-free static HTML, CSS, JavaScript, and JSON. It is +copied into `/app/build/doc` by `docker/web.Dockerfile` after Open WebUI's +frontend build completes. + +Validate it locally with: + +```bash +python3 -m http.server 4173 --directory docs/site +``` + +The production path is `/doc/`; all internal assets therefore use relative +URLs. Keep the Markdown documents and portal content aligned in the same +commit. Add experiment cards through `docs/site/experiments.json`. + +## Change checklist + +Before pushing: + +1. keep unrelated working-tree changes untouched; +2. update architecture or behavior documentation; +3. add deterministic tests for policy changes; +4. run unit and formatting checks; +5. run Docker integration for workspace/Gateway changes; +6. run E2E for public model, auth, loop, or file-delivery changes; +7. audit every rebuilt image; +8. use immutable production image tags; +9. verify public health and one representative user flow; +10. retain the previous image and deployment backup until verification passes. diff --git a/docs/experiments.md b/docs/experiments.md new file mode 100644 index 0000000..3b71539 --- /dev/null +++ b/docs/experiments.md @@ -0,0 +1,133 @@ +# Agent experiment system + +## Goal + +The project exists to make Agent-loop iteration cheap, observable, and +reversible. An experiment should change one behavioral hypothesis while +holding authentication, workspace isolation, and delivery infrastructure +constant. + +The public portal at contains a +curated experiment board. This Markdown document defines the durable process; +the portal catalog is a presentation layer. + +## Experiment unit + +Every experiment should define: + +1. **Question** — the behavior we want to improve. +2. **Hypothesis** — the concrete loop change and expected direction. +3. **Variant identifier** — a stable string recorded in `run.created`. +4. **Task set** — fixed prompts, starting workspace fixtures, and model tier. +5. **Metrics** — success, quality, latency, model/tool usage, and regressions. +6. **Safety constraints** — behavior that must not worsen. +7. **Decision rule** — promote, iterate, or reject. +8. **Artifact** — code commit, configuration, raw event export, and report. + +Do not compare variants from different task inputs or silently change the +model, prompt, context budget, or workspace fixture. + +## Recommended metrics + +Primary: + +- task success under explicit evidence checks; +- verified-deliverable rate; +- unresolved tool-failure rate; +- human or rubric quality score. + +Efficiency: + +- end-to-end latency; +- model calls and iterations; +- prompt/completion tokens when available; +- tool-call count; +- duplicated or policy-blocked calls; +- child-Agent count and concurrency; +- estimated provider and electricity cost. + +Reliability and safety: + +- incomplete checkpoints correctly rejected; +- false-success rate; +- workspace escape or cross-user access failures; +- non-zero command exits incorrectly recorded as success; +- identity/authentication failures; +- Runtime or workspace restart recovery. + +## Evaluation layers + +### Unit and policy tests + +Fast deterministic tests cover parsing, tool normalization, evidence gates, +authentication, context selection, event persistence, and provider response +normalization. + +### Real Docker integration + +The integration test creates two disposable users and proves: + +- separate containers, networks, and volumes; +- workspace path confinement; +- file browse/download/archive behavior; +- read-only root and dropped capabilities; +- persistent Python virtual environments; +- foreground and background `pipefail` exit codes. + +### Disposable full-stack E2E + +The E2E stack uses a deterministic fake model provider and exercises: + +- registration and admin approval; +- the exact four-model public catalog; +- the custom Agent loop; +- evidence-backed file generation; +- file delivery; +- per-user isolation. + +### Live-model evaluation + +`scripts/eval-live-work.py` runs a real model against a unique disposable +workspace and records latency, token usage, events, tool counts, and output +files. Live evaluations complement deterministic tests; they do not replace +them. + +### Production smoke test + +After deployment, use a low-impact authenticated task to confirm public Web, +Runtime, Gateway, the remote workspace host, and file retrieval. Remove only +the smoke artifact after verification. + +## Current baselines + +| ID | Area | Status | Result | +| --- | --- | --- | --- | +| `loop-v3-evidence` | Completion policy | Production baseline | Artifacts require mutation followed by verification; reports and benchmarks require stronger evidence. | +| `safe-parallel-v1` | Scheduler | Production baseline | Consecutive read-only calls and read-only delegates run concurrently; mutations remain serialized. | +| `recent-visible-v1` | Context | Production baseline | Keep recent visible messages under a per-model character budget and attach up to eight memories. | +| `workspace-python-v1` | Execution | Validated | Persistent `.venv`, persistent user caches, 900-second tool ceiling, and true pipeline exits. | +| `fresh-gateway-identity-v1` | Authentication | Validated | Runtime re-signs verified identity for each tool request so long runs survive the original token expiry. | + +## Initial backlog + +1. Semantic or hybrid memory retrieval with conflict and expiry policy. +2. Structured old-context summarization with replay comparison. +3. Dependency-aware tool DAG scheduling rather than consecutive safe groups. +4. Child-Agent result contracts and budget allocation. +5. Experiment assignment stored per run and an aggregate comparison API. +6. Cost accounting across local electricity and paid provider tokens. +7. Workspace quotas, egress policy, abuse monitoring, and backup drills. + +## Adding an experiment + +1. Add the hypothesis and metrics to this document or a dedicated + `docs/experiments/.md` file. +2. Add the experiment to `docs/site/experiments.json` for the public board. +3. Add or update the relevant strategy/scheduler/context identifier in + `run.created`. +4. Add deterministic tests before a live run. +5. Run the standard verification and capture a report. +6. Promote with an immutable image tag; retain the previous tag for rollback. + +Never put provider keys, private event payloads, user identifiers, prompts from +real users, or internal credentials in an experiment artifact. diff --git a/docs/operations.md b/docs/operations.md index c79d4bc..63e4cf8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -3,6 +3,7 @@ ## Production layout - Public URL: `https://agent.k1412.top` +- Public documentation: `https://agent.k1412.top/doc/` - NAS project: `/boot/config/plugins/compose.manager/projects/k1412-agent` - Public host port: `12004` - Execution provider: `ssh-docker` @@ -47,6 +48,11 @@ Compose file, image, repository, or log. The SSH override removes `/var/run/docker.sock` from Gateway and mounts the dedicated SSH directory read-only at `/root/.ssh`. +The documentation portal is copied into the existing Web image at +`/app/build/doc`. It uses the same host port and proxy route as Open WebUI, so +it does not require another Compose project, NAS port, DNS record, or Nginx +Proxy Manager host. + The Workspace image keeps the user home, package caches, and Python user base under `/workspace/.agent`; the conventional project virtual environment is `/workspace/.venv`. Updating `WORKSPACE_IMAGE` to a new immutable tag causes diff --git a/docs/project-history.md b/docs/project-history.md new file mode 100644 index 0000000..524cf56 --- /dev/null +++ b/docs/project-history.md @@ -0,0 +1,136 @@ +# Project history and decisions + +This record summarizes the rebuild that began on 2026-07-26. Git remains the +source of truth for exact changes; this document captures product intent and +architecture decisions that are otherwise difficult to reconstruct from +individual commits. + +## 1. Clean-slate web Agent + +The existing business/data-specific application was declared a new project +with no backward-compatibility requirement. Business skills, domain workflows, +and user-configurable provider settings were removed from scope. The retained +core requirement was general coding-Agent capability with an independently +evolvable loop. + +Decision: + +- build one general multi-user Web Agent; +- preserve coding, shell, file, Git, process, planning, memory, and delegation + capabilities; +- keep infrastructure and product shell separate from Agent intelligence. + +## 2. Open WebUI as product shell + +Instead of maintaining a second full account/chat frontend, Open WebUI v0.9.6 +was pinned and lightly patched. It supplies registration, login, RBAC, +administrator approval, conversation history, and familiar chat interaction. + +K1412 kept ownership of the Agent loop, provider routing, memory, tools, +scheduling, events, and workspace execution. This reduced the cost of Agent +experiments without making the loop dependent on Open WebUI internals. + +## 3. One Agent path, not Chat versus Work + +An early design considered a switch between Open WebUI's native Chat loop and +the K1412 Work loop. The product was simplified to one Agent path because: + +- users should not need to understand two orchestration engines; +- conversation continuation between modes creates ambiguous state; +- two loops double frontend states and verification behavior; +- the custom loop is the main research asset. + +The remaining interface is an Agent-first chat with server-owned model +selection. + +## 4. Model and thinking semantics + +The original “light / medium / high reasoning” labels conflated different +models with adjustable effort. The UI and backend now distinguish model +identity from thinking capability: + +- Luna, Terra, and Sol are separate local K1412/Ollama models; +- each exposes thinking as enabled but without adjustable effort levels; +- DeepSeek V4 Pro is the cloud extreme tier with maximum reasoning effort. + +Provider model IDs, URLs, credentials, and tuning remain server-side. + +## 5. Multi-user remote workspaces + +Each user received a dedicated Docker container, network, and persistent volume +derived from a hashed immutable user ID. Execution moved from the NAS to the +physical `home-node-itx` through SSH Docker. + +The physical node was prepared with: + +- an ext4 500 GB data disk mounted at `/srv/k1412-data`; +- Docker root under `/srv/k1412-data/docker`; +- strict SSH host verification; +- no application/database secrets. + +The local and SSH execution providers retain one interface so the host can be +replaced without changing Agent behavior. + +## 6. File delivery and focused UI + +The frontend was simplified around the Agent: + +- K1412 product title and assets; +- a compact model selector with separate thinking status; +- no exposed provider/tool/system-prompt settings; +- an authenticated workspace file browser; +- single-file download and streamed directory archive. + +Open WebUI attribution and license requirements remain documented. + +## 7. Loop hardening through failed-task analysis + +Real sorting-script/report tasks exposed model weaknesses and platform bugs. +The loop was progressively hardened to: + +- require actual files rather than textual claims; +- require executable source separately from reports; +- run the source before writing a report; +- require measured benchmark values; +- reject invalid Python and escaped-layout corruption; +- recover from failed verification; +- prevent unchanged retry loops and duplicate batches; +- preserve failures even when output is piped through another command; +- bound iteration, tool-batch, model-output, and retry budgets. + +These constraints are not assumed permanently optimal. They are the current +baseline and should be removed or changed only through measured experiments. + +## 8. Python environment and long-run reliability + +Production debugging found three platform problems: + +1. the container root was read-only while `pip` defaulted to `/home/agent`; +2. `/tmp` was `noexec`; +3. shell pipelines returned the last command's status, hiding failed installs. + +The fix moved home, caches, user packages, and virtual environments to the +persistent workspace, enabled Bash `pipefail`, extended tool timeouts to 900 +seconds, and taught the Agent the supported `.venv` workflow. + +The same investigation found that a five-minute identity token was reused for +every tool in a long run. Runtime now signs a fresh short-lived identity for +each Gateway call after the original request identity has been verified. + +Production validation created a real `.venv`, installed and imported +`psutil 7.2.2`, downloaded the generated proof file through the Web API, and +then removed the temporary proof file. + +## 9. Documentation and experiment portal + +The repository documentation was reorganized around architecture, Agent +implementation, security, operations, history, development, and experiments. +The curated public portal is shipped inside the existing Web image at +`/doc/`, avoiding a second service or proxy rule. + +## Current direction + +The platform is now suitable for controlled Agent-loop experimentation. The +next work should prioritize evaluation data, context/memory experiments, +scheduler design, delegation contracts, cost accounting, and hardening for +hostile multi-tenant use rather than adding business-specific workflows. diff --git a/docs/site/app.js b/docs/site/app.js new file mode 100644 index 0000000..c39418f --- /dev/null +++ b/docs/site/app.js @@ -0,0 +1,265 @@ +const flows = { + agent: { + kicker: 'AGENT REQUEST / 7 HOPS', + description: + '浏览器的已登录请求由 Web 转换成兼容 OpenAI 的流式调用;Runtime 独立完成上下文构建、模型调用、工具循环和证据校验。', + nodes: ['browser', 'web', 'runtime', 'provider', 'gateway', 'workspace', 'store'] + }, + identity: { + kicker: 'IDENTITY / SHORT-LIVED & SERVER-SIDE', + description: + 'Web 先验证登录用户并签发短期身份;Runtime 验证一次后,在每次 Gateway 工具调用前重新签发,避免长任务复用过期令牌。', + nodes: ['browser', 'web', 'runtime', 'gateway', 'workspace'] + }, + tool: { + kicker: 'TOOL EXECUTION / POLICY BEFORE DOCKER', + description: + 'Runtime 校验并调度工具,Gateway 再执行身份、路径和资源策略,最后才通过 SSH Docker 进入该用户唯一的工作区。', + nodes: ['runtime', 'gateway', 'workspace', 'store'] + }, + file: { + kicker: 'FILE DELIVERY / AUTHENTICATED STREAM', + description: + '用户在工作区抽屉发起浏览或下载;Web 验证会话后请求 Gateway,文件或归档以流式响应返回,浏览器从不接触内部服务密钥。', + nodes: ['browser', 'web', 'gateway', 'workspace'] + } +}; + +const stages = { + context: { + kicker: 'CONTEXT / recent-visible-v1', + title: '把有限上下文留给真正影响下一步的内容', + description: + '移除旧的工具详情渲染,按模型预算保留最新可见消息,再注入最多 8 条持久记忆。上下文策略和 Agent Loop 分别版本化。', + points: [ + '去掉历史消息中的工具 UI 标记', + '最近消息优先,按字符预算截断', + '记忆只作为补充上下文,不直接取得执行权限' + ] + }, + model: { + kicker: 'MODEL / SERVER-SIDE CATALOG', + title: '模型提出下一步,平台控制它能看到和能花费什么', + description: + 'Runtime 根据公开模型 ID 选择固定 provider、上下文预算、输出预算和循环上限。用户不能从前端注入密钥或任意模型参数。', + points: ['支持流式内容与工具调用', '模型和推理强度明确标注', 'provider 差异在传输层统一'] + }, + validate: { + kicker: 'VALIDATE / NORMALIZE BEFORE TRUST', + title: '先把模型输出变成可约束的数据,再决定是否执行', + description: + '工具名称、参数、批次数量和权限都在执行前检查。一次模型响应最多接受 8 个工具调用,未知或畸形调用会成为可见错误。', + points: ['JSON 参数规范化', '工具白名单与参数校验', '重复无进展调用守卫'] + }, + schedule: { + kicker: 'SCHEDULER / safe-parallel-v1', + title: '读取可以加速,写入必须保持可预测', + description: + '连续且标记为 parallel_safe 的工具并发执行;任何文件写入、命令或状态变更都会形成串行边界,结果按请求顺序返回模型。', + points: ['只读调用成组并发', '变更操作严格串行', '批次结果仍保持稳定顺序'] + }, + execute: { + kicker: 'EXECUTE / TOOLS + BOUNDED DELEGATION', + title: '所有能力都通过可记录、可验证的工具边界发生', + description: + '文件、命令、Git、计划和记忆都有结构化工具。Workspace 工具经 Gateway 进入用户容器;只读子任务可以受限委派。', + points: ['工作区工具与状态工具分离', '每次调用记录开始和结果事件', '子 Agent 不得再次委派'] + }, + evidence: { + kicker: 'EVIDENCE / COMPLETION IS A POLICY', + title: '“完成”不是一句自然语言,而是一组已经发生的事实', + description: + '如果任务要求生成文件、修改代码或给出测量报告,循环必须看到相应的写入、验证和精确结果。证据不足时会继续循环或明确失败。', + points: ['变更后必须有后续验证', '失败命令必须修复并重跑', '报告必须晚于来源或实验结果'] + } +}; + +const statusLabels = { + baseline: '当前基线', + validated: '已验证', + backlog: '待实验' +}; + +let experimentData = []; +let activeExperimentFilter = 'all'; + +function activateFlow(flowName) { + const flow = flows[flowName]; + if (!flow) return; + + document.querySelectorAll('.flow-tab').forEach((button) => { + const active = button.dataset.flow === flowName; + button.classList.toggle('is-active', active); + button.setAttribute('aria-selected', String(active)); + }); + + document.querySelectorAll('.system-node').forEach((node) => { + node.classList.toggle('is-flow-active', flow.nodes.includes(node.dataset.node)); + }); + + document.querySelector('.system-map')?.classList.add('has-selection'); + document.querySelector('#flow-kicker').textContent = flow.kicker; + document.querySelector('#flow-description').textContent = flow.description; +} + +function activateStage(stageName) { + const stage = stages[stageName]; + if (!stage) return; + + document.querySelectorAll('.loop-stage').forEach((button) => { + const active = button.dataset.stage === stageName; + button.classList.toggle('is-active', active); + button.setAttribute('aria-selected', String(active)); + }); + + document.querySelector('#stage-kicker').textContent = stage.kicker; + document.querySelector('#stage-title').textContent = stage.title; + document.querySelector('#stage-description').textContent = stage.description; + + const list = document.querySelector('#stage-points'); + list.replaceChildren( + ...stage.points.map((point) => { + const item = document.createElement('li'); + item.textContent = point; + return item; + }) + ); +} + +function experimentCard(experiment) { + const article = document.createElement('article'); + article.className = 'experiment-card'; + + const meta = document.createElement('div'); + meta.className = 'experiment-meta'; + + const id = document.createElement('span'); + id.textContent = experiment.id; + + const badge = document.createElement('span'); + badge.className = `status-badge status-${experiment.status}`; + badge.textContent = statusLabels[experiment.status]; + meta.append(id, badge); + + const title = document.createElement('h3'); + title.textContent = experiment.title; + + const summary = document.createElement('p'); + summary.textContent = experiment.summary; + + const footer = document.createElement('div'); + footer.className = 'experiment-footer'; + [experiment.area, experiment.version, ...(experiment.metrics || [])].filter(Boolean).forEach((text) => { + const tag = document.createElement('span'); + tag.textContent = text; + footer.append(tag); + }); + + article.append(meta, title, summary, footer); + return article; +} + +function renderExperiments() { + const query = document.querySelector('#experiment-search').value.trim().toLocaleLowerCase('zh-CN'); + const filtered = experimentData.filter((experiment) => { + const statusMatches = + activeExperimentFilter === 'all' || experiment.status === activeExperimentFilter; + const haystack = [ + experiment.id, + experiment.title, + experiment.summary, + experiment.area, + experiment.version, + ...(experiment.metrics || []) + ] + .join(' ') + .toLocaleLowerCase('zh-CN'); + return statusMatches && (!query || haystack.includes(query)); + }); + + const grid = document.querySelector('#experiment-grid'); + if (filtered.length === 0) { + const empty = document.createElement('article'); + empty.className = 'experiment-empty'; + empty.textContent = '没有匹配的实验记录。'; + grid.replaceChildren(empty); + } else { + grid.replaceChildren(...filtered.map(experimentCard)); + } + + document.querySelector('#experiment-count').textContent = + `${String(filtered.length).padStart(2, '0')} / ${String(experimentData.length).padStart(2, '0')} RECORDS`; +} + +async function loadExperiments() { + const grid = document.querySelector('#experiment-grid'); + try { + const response = await fetch('./experiments.json', { cache: 'no-cache' }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + experimentData = await response.json(); + renderExperiments(); + } catch (error) { + const failure = document.createElement('article'); + failure.className = 'experiment-empty'; + failure.textContent = '实验台账暂时无法读取,请查看仓库中的 docs/experiments.md。'; + grid.replaceChildren(failure); + document.querySelector('#experiment-count').textContent = 'DATA UNAVAILABLE'; + } +} + +function setActiveNavigation(sectionId) { + document.querySelectorAll('.nav-link').forEach((link) => { + link.classList.toggle('is-active', link.getAttribute('href') === `#${sectionId}`); + }); +} + +document.querySelectorAll('.flow-tab').forEach((button) => { + button.addEventListener('click', () => activateFlow(button.dataset.flow)); +}); + +document.querySelectorAll('.loop-stage').forEach((button) => { + button.addEventListener('click', () => activateStage(button.dataset.stage)); +}); + +document.querySelectorAll('.filter-button').forEach((button) => { + button.addEventListener('click', () => { + activeExperimentFilter = button.dataset.filter; + document + .querySelectorAll('.filter-button') + .forEach((candidate) => candidate.classList.toggle('is-active', candidate === button)); + renderExperiments(); + }); +}); + +document.querySelector('#experiment-search').addEventListener('input', renderExperiments); + +const toggle = document.querySelector('.nav-toggle'); +const sidebar = document.querySelector('.sidebar'); +toggle.addEventListener('click', () => { + const isOpen = sidebar.classList.toggle('is-open'); + toggle.setAttribute('aria-expanded', String(isOpen)); +}); + +document.querySelectorAll('.nav-link').forEach((link) => { + link.addEventListener('click', () => { + sidebar.classList.remove('is-open'); + toggle.setAttribute('aria-expanded', 'false'); + }); +}); + +if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio); + if (visible[0]) setActiveNavigation(visible[0].target.id); + }, + { rootMargin: '-20% 0px -62% 0px', threshold: [0.05, 0.3] } + ); + document.querySelectorAll('main > section[id]').forEach((section) => observer.observe(section)); +} + +activateFlow('agent'); +activateStage('context'); +loadExperiments(); diff --git a/docs/site/experiments.json b/docs/site/experiments.json new file mode 100644 index 0000000..15cc2c3 --- /dev/null +++ b/docs/site/experiments.json @@ -0,0 +1,92 @@ +[ + { + "id": "BASE-001", + "title": "统一 Agent 路径", + "summary": "删除 Chat / Work 双模式,所有对话通过同一套可观测 Agent Loop;减少前端状态和后端分叉。", + "area": "product", + "status": "validated", + "version": "agent-loop-v3", + "metrics": ["维护面", "交互一致性"] + }, + { + "id": "BASE-002", + "title": "安全读取并行,变更串行", + "summary": "连续 parallel_safe 调用并发执行,写文件、命令和状态变更维持模型给出的顺序。", + "area": "scheduler", + "status": "baseline", + "version": "safe-parallel-v1", + "metrics": ["wall time", "determinism"] + }, + { + "id": "BASE-003", + "title": "最近可见上下文", + "summary": "清除旧工具详情,按模型字符预算保留最近消息,并注入最多 8 条持久记忆。", + "area": "context", + "status": "baseline", + "version": "recent-visible-v1", + "metrics": ["token proxy", "task success"] + }, + { + "id": "BASE-004", + "title": "证据驱动的完成判定", + "summary": "文件、代码和报告任务必须出现写入与后续验证;未解决的命令失败会阻止虚假完成。", + "area": "reliability", + "status": "validated", + "version": "evidence-gate-v1", + "metrics": ["false completion", "artifact validity"] + }, + { + "id": "BASE-005", + "title": "持久 Python 工具链", + "summary": "用户依赖安装到 /workspace/.venv;镜像升级保留环境,pipefail 防止安装失败被管道尾部掩盖。", + "area": "workspace", + "status": "validated", + "version": "workspace-python-v2", + "metrics": ["install success", "restart survival"] + }, + { + "id": "EXP-006", + "title": "基于相关性的记忆召回", + "summary": "比较固定最近记忆、关键词召回和向量召回对长期偏好遵循、上下文成本与错误注入的影响。", + "area": "memory", + "status": "backlog", + "version": "proposal", + "metrics": ["recall@k", "task success", "context cost"] + }, + { + "id": "EXP-007", + "title": "依赖感知工具调度", + "summary": "从显式工具属性推进到调用间读写集分析,评估是否能扩大安全并行范围。", + "area": "scheduler", + "status": "backlog", + "version": "proposal", + "metrics": ["wall time", "race failures", "parallel ratio"] + }, + { + "id": "EXP-008", + "title": "自适应模型路由", + "summary": "以任务复杂度和中途失败信号决定 Luna、Terra、Sol 或 DeepSeek,而不是让一次选择锁定整个任务。", + "area": "model-routing", + "status": "backlog", + "version": "proposal", + "metrics": ["task success", "latency", "API cost"] + }, + { + "id": "EXP-009", + "title": "受限多 Agent 拓扑", + "summary": "比较单循环、只读并行委派和角色化子 Agent,在复杂代码任务中的收益与上下文整合成本。", + "area": "delegation", + "status": "backlog", + "version": "proposal", + "metrics": ["task success", "wall time", "tool calls"] + }, + { + "id": "EXP-010", + "title": "上下文压缩与可逆摘要", + "summary": "在最近消息预算之外保存结构化任务状态,并允许模型按需取回原始证据,减少长任务遗忘。", + "area": "context", + "status": "backlog", + "version": "proposal", + "metrics": ["long-horizon success", "context cost", "retrieval count"] + } +] diff --git a/docs/site/index.html b/docs/site/index.html new file mode 100644 index 0000000..882be00 --- /dev/null +++ b/docs/site/index.html @@ -0,0 +1,570 @@ + + + + + + + K1412 Agent · 架构、实现与实验 + + + + + + + + + + + + + + + +
+ + + + K1412 Agent + Architecture / Lab Notes + + +
+ 当前架构 · 2026.07 + + 查看源码 + + 进入 Agent + +
+
+ +
+ + +
+
+
ONE AGENT PATH / MULTI-USER / SELF-HOSTED
+

一套可以被持续
实验的 Web Agent

+

+ K1412 Agent 把账户、界面和执行隔离做成稳定平台,把上下文、工具调度、并行、多 Agent、 + 记忆和完成判定留在我们自己的 Agent Loop 中。这里既是系统说明,也是后续实验的公开台账。 +

+ + +
+
+ 01 + 统一 Agent 路径 + 没有 Chat / Work 分叉 +
+
+ 04 + 可选模型 + 模型与推理强度分离 +
+
+ 1 : 1 + 用户执行空间 + 容器、网络与持久卷 +
+
+ SSE + 运行事件流 + 模型、工具与完成证据 +
+
+
+ +
+
+
+ 02 / ARCHITECTURE +

稳定外壳与
实验核心解耦

+
+

+ 点击一条链路,观察一次请求如何穿过系统。公开入口只有 Web;模型密钥、Docker + 权限和用户执行环境始终留在服务端。 +

+
+ +
+
+ + + + +
+ +
+
+ PUBLIC EDGE +
+ 01 / CLIENT + Browser + 登录、会话、文件下载 +
+ +
+ 02 / STABLE SHELL + Open WebUI + Auth · RBAC · History · UI +
+
+ +
+ PRIVATE APPLICATION NETWORK +
+ 03 / EXPERIMENT CORE + Agent Runtime + Context · Loop · Tools · Memory +
+ +
+ 04 / INFERENCE + Model Provider + K1412 API · DeepSeek +
+
+ 05 / POLICY GATE + Workspace Gateway + Identity · Path · Docker lifecycle +
+
+ STATE + PostgreSQL + Redis + Chats · Events · Plans · Memory +
+
+ +
+ TAILSCALE / PHYSICAL WORKER +
+ 06 / USER BOUNDARY + Per-user Workspace + Container · Network · Persistent volume +
+
+
+ +
+ AGENT REQUEST / 7 HOPS +

+ 浏览器的已登录请求由 Web 转换成兼容 OpenAI 的流式调用;Runtime + 独立完成上下文构建、模型调用、工具循环和证据校验。 +

+
+
+ +
+
+ A +

Open WebUI

+

负责用户系统、会话、历史和呈现。它是产品外壳,不决定 Agent 如何思考与执行。

+ STABLE / UPSTREAM-BASED +
+
+ B +

Agent Runtime

+

负责模型、上下文、工具、并行、委派、记忆和完成判定,是我们主要的实验面。

+ FAST-MOVING / OWNED +
+
+ C +

Gateway + Workspace

+

把已验证用户绑定到唯一执行空间,控制路径、资源和 Docker 生命周期。

+ SECURITY BOUNDARY +
+
+
+ +
+
+
+ 03 / AGENT LOOP +

模型不是 Agent,
循环才是。

+
+

+ 当前基线为 agent-loop-v3。模型每轮可以回答或提出工具调用;Runtime + 负责校验、调度、恢复,并拒绝没有证据的“已完成”。 +

+
+ +
+
+ + + + + + +
+ +
+
+ CONTEXT / recent-visible-v1 +

把有限上下文留给真正影响下一步的内容

+
+

+ 移除旧的工具详情渲染,按模型预算保留最新可见消息,再注入最多 8 + 条持久记忆。上下文策略和 Agent Loop 分别版本化。 +

+
    +
  • 去掉历史消息中的工具 UI 标记
  • +
  • 最近消息优先,按字符预算截断
  • +
  • 记忆只作为补充上下文,不直接取得执行权限
  • +
+
+
+ +
+
+ PARALLELISM +

只并行已知安全的读取

+

+ 连续的只读、parallel_safe 工具可以并发;写入和状态变更保持顺序, + 避免输出更快但结果不可复现。 +

+
+
+ RECOVERY +

失败不是上下文噪声

+

命令失败必须被修复并重新运行,重复无进展调用会被守卫阻止,模型不能用文字绕过失败。

+
+
+ DELEGATION +

委派是受限的工具

+

只读子任务可并行,子 Agent 不能继续委派。父循环仍然负责整合证据和最终完成判定。

+
+
+ +
+
+ 声称完成的类型最低证据阻止条件 +
+
+ 生成文件成功写入 + 随后读取或列出只有模型文字 +
+
+ 修改代码变更操作 + 相关验证未解决的失败命令 +
+
+ 生成报告来源或执行结果 + 报告文件验证报告早于实验 +
+
+ 性能比较可解析的精确数值只有定性结论 +
+
+
+ +
+
+
+ 04 / EXECUTION SPACE +

每个用户,一套
长期存在的工具台

+
+

+ 生产执行发生在物理机 home-node-itx。Gateway 通过 Tailscale + SSH + 管理 Docker;用户得到独立容器、网络和持久卷,而不是宿主机账号。 +

+
+ +
+
+ IMMUTABLE INPUT + Open WebUI user_id + sha256(user_id) → workspace_id +
+ +
+
CONTAINERk1412-ws-<id>
+
NETWORKk1412-ws-net-<id>
+
VOLUMEk1412-ws-data-<id>
+
+
+ +
+
+ 01 +

身份不下放给浏览器

+

Web 验证登录,Runtime 每次调用 Gateway 前重新签发短期身份。内部服务密钥从不进入前端。

+
+
+ 02 +

容器不是主机边界的替代品

+

只读根目录、丢弃 capabilities、no-new-privileges、资源限制、无宿主端口和 Docker socket。

+
+
+ 03 +

依赖和文件都会留下

+

/workspace 由命名卷持久化;Python 位于 .venv,镜像升级不会删除用户数据。

+
+
+ 04 +

输出可以直接取回

+

工作区抽屉支持浏览、下载文件和流式归档。用户无需接触 Gateway、SSH 或容器名称。

+
+
+
+ +
+
+
+ 05 / MODEL POLICY +

先标清模型,
再标真实思考能力

+
+

+ Luna、Terra、Sol 是三个独立模型,不是同一模型的轻、中、高强度。前端只显示后端确认支持的 + 思考语义;provider、循环和上下文预算仍由服务端模型目录固定。 +

+
+ +
+
+
LOCAL MODEL
+

Luna

+

思考:开启(不分档)

+
循环上限
8
上下文预算
120k chars
+
+
+
LOCAL / DEFAULT
+

Terra

+

思考:开启(不分档)

+
循环上限
16
上下文预算
240k chars
+
+
+
LOCAL MODEL
+

Sol

+

思考:开启(不分档)

+
循环上限
24
上下文预算
400k chars
+
+
+
CLOUD / MAX EFFORT
+

DeepSeek V4 Pro

+

思考强度:极高(max)

+
循环上限
32
上下文预算
800k chars
+
+
+ +
+ 命名规则 +

+ Luna / Terra / Sol / DeepSeek V4 Pro 是四个独立模型。前三者目前只暴露 + “思考开启”的布尔能力,不能严谨地标成轻、中、高;DeepSeek 才明确使用 + reasoning_effort=max,界面标注为“极高”。 +

+
+
+ +
+
+
+ 06 / EXPERIMENT LOG +

每次改进都应该
留下可比较的记录

+
+

+ 实验以问题、基线、变量、数据集、指标和结论为最小单元。页面读取仓库里的 + experiments.json,与实现一起版本化。 +

+
+ +
+
+ + + + +
+ +
+ +
+
正在读取实验台账…
+
+

+
+ +
+
+
+ 07 / OPERATIONS +

一条公开入口,
多层私有服务

+
+

+ 生产栈运行于 Unraid NAS,公开流量经 VPS 上的 Nginx Proxy Manager 和 Tailscale + 进入 NAS。文档和聊天由同一个 Web 镜像提供。 +

+
+ +
+
GITgit.k1412.top
+ +
OCIdocker.k1412.top
+ +
COMPOSEUnraid NAS :12004
+ +
PUBLICagent.k1412.top
+
+ +
+
+ 01 / BUILD +

不可变镜像

+

镜像按 UTC 时间与 Git commit 标记;发布只替换发生变化的服务。

+
+
+ 02 / VERIFY +

分层验证

+

静态检查、单元测试、真实 Docker、完整 E2E、镜像审计与公开路径冒烟。

+
+
+ 03 / OBSERVE +

事件优先

+

每轮上下文、模型、工具和完成状态进入运行事件,便于重放与实验比较。

+
+
+ 04 / ROLLBACK +

保留上个版本

+

配置先备份,旧镜像不覆盖;验证失败时恢复镜像引用即可回退。

+
+
+
+ +
+
+
+ 08 / PROJECT HISTORY +

从业务 Agent 到
通用实验平台

+
+

+ 项目按“干净重建”推进,不保留旧业务 Skill 和旧 API 兼容层。下面记录已经改变系统性质的关键决策。 +

+
+ +
    +
  1. + 01 +
    FOUNDATION

    重建为多人 Web Agent

    Open WebUI 承担账户和聊天外壳,自研 Runtime 承担 Agent 能力。

    +
  2. +
  3. + 02 +
    PRODUCT

    收敛为单一 Agent 路径

    删除 Chat / Work 双模式,降低交互和后端维护成本。

    +
  4. +
  5. + 03 +
    EXECUTION

    迁移到远端持久工作区

    接入 home-node-itx,打通每用户容器、持久卷与文件交付。

    +
  6. +
  7. + 04 +
    RELIABILITY

    建立证据驱动的完成策略

    加入恢复、重试守卫、精确报告证据和结构化事件。

    +
  8. +
  9. + 05 +
    PLATFORM

    补齐 Python 环境与长期身份

    持久化虚拟环境、pipefail、900 秒工具超时和每次调用的新鲜身份令牌。

    +
  10. +
  11. + 06 +
    NOW

    文档与实验成为产品界面

    架构、实现、运维和实验台账随代码发布到同域门户。

    +
  12. +
+
+ +
+
+
+ 09 / REPOSITORY +

网页负责建立心智模型,
仓库文档负责成为事实来源。

+

+ 所有设计说明、限制、运维步骤和实验规范都与代码一同提交。门户是摘要, + Markdown 文档是评审与变更时的正式依据。 +

+ + 打开 Git 仓库 + +
+ +
+
+
+
+ + + + diff --git a/docs/site/og.png b/docs/site/og.png new file mode 100644 index 0000000..1f88c9a Binary files /dev/null and b/docs/site/og.png differ diff --git a/docs/site/styles.css b/docs/site/styles.css new file mode 100644 index 0000000..5d655d3 --- /dev/null +++ b/docs/site/styles.css @@ -0,0 +1,1688 @@ +:root { + --paper: #f2f0e9; + --paper-deep: #e7e3d8; + --ink: #151713; + --muted: #676a62; + --line: rgba(21, 23, 19, 0.16); + --green: #b8f23d; + --green-dark: #83b611; + --blue: #5c77ff; + --orange: #ff6c3d; + --dark: #141713; + --dark-raised: #20241e; + --white: #fffefa; + --sidebar-width: 224px; + --topbar-height: 78px; + --radius: 3px; + font-family: + Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + color: var(--ink); + background: var(--paper); + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: calc(var(--topbar-height) + 18px); +} + +body { + margin: 0; + background: + linear-gradient(rgba(21, 23, 19, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(21, 23, 19, 0.035) 1px, transparent 1px), var(--paper); + background-size: 24px 24px; + color: var(--ink); +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input { + font: inherit; +} + +button { + color: inherit; +} + +code { + padding: 0.12em 0.32em; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.46); + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.88em; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.skip-link { + position: fixed; + z-index: 200; + top: 8px; + left: 8px; + padding: 10px 14px; + transform: translateY(-150%); + background: var(--green); + font-weight: 800; +} + +.skip-link:focus { + transform: translateY(0); +} + +.topbar { + position: fixed; + z-index: 100; + top: 0; + right: 0; + left: 0; + height: var(--topbar-height); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px; + border-bottom: 1px solid var(--line); + background: rgba(242, 240, 233, 0.93); + backdrop-filter: blur(16px); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; +} + +.brand-mark { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border: 1px solid var(--ink); + background: var(--green); + font-size: 20px; + font-weight: 900; +} + +.brand strong, +.brand small { + display: block; +} + +.brand strong { + font-size: 14px; + letter-spacing: -0.01em; +} + +.brand small { + margin-top: 2px; + color: var(--muted); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 9px; + letter-spacing: 0.08em; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.system-status { + display: inline-flex; + align-items: center; + gap: 7px; + margin-right: 8px; + color: var(--muted); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 10px; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.system-status i { + width: 7px; + height: 7px; + border-radius: 999px; + background: var(--green-dark); + box-shadow: 0 0 0 4px rgba(131, 182, 17, 0.12); +} + +.button { + display: inline-flex; + min-height: 40px; + align-items: center; + justify-content: center; + gap: 18px; + padding: 0 16px; + border: 1px solid var(--ink); + border-radius: var(--radius); + font-size: 12px; + font-weight: 750; + transition: + transform 160ms ease, + box-shadow 160ms ease, + background 160ms ease; +} + +.button:hover { + transform: translateY(-2px); + box-shadow: 3px 3px 0 var(--ink); +} + +.button-primary { + background: var(--green); +} + +.button-quiet { + border-color: var(--line); + background: transparent; +} + +.button-dark { + min-height: 48px; + padding: 0 20px; + background: var(--dark); + color: var(--white); +} + +.button-light { + margin-top: 28px; + background: var(--white); + color: var(--ink); +} + +.nav-toggle { + display: none; + width: 42px; + height: 40px; + border: 1px solid var(--ink); + background: transparent; +} + +.nav-toggle span { + display: block; + width: 18px; + height: 1px; + margin: 5px auto; + background: currentColor; +} + +.page-shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + padding-top: var(--topbar-height); +} + +.sidebar { + position: sticky; + top: var(--topbar-height); + height: calc(100vh - var(--topbar-height)); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 36px 22px 24px; + border-right: 1px solid var(--line); + background: rgba(242, 240, 233, 0.75); +} + +.nav-label { + margin: 0 0 10px 12px; + color: #8a8c84; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 9px; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.nav-label:not(:first-child) { + margin-top: 30px; +} + +.nav-link { + position: relative; + display: flex; + align-items: center; + gap: 11px; + padding: 8px 11px; + border-radius: var(--radius); + color: #565950; + font-size: 12px; + font-weight: 650; + transition: + background 140ms ease, + color 140ms ease; +} + +.nav-link span { + color: #999b94; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 9px; +} + +.nav-link:hover, +.nav-link.is-active { + background: var(--ink); + color: var(--white); +} + +.nav-link.is-active span { + color: var(--green); +} + +.sidebar-note { + padding: 15px; + border: 1px solid var(--line); + border-left: 3px solid var(--green-dark); + background: rgba(255, 255, 255, 0.36); +} + +.sidebar-note span { + color: var(--green-dark); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 9px; + letter-spacing: 0.12em; +} + +.sidebar-note p { + margin: 8px 0 0; + font-size: 12px; + line-height: 1.55; +} + +main { + min-width: 0; +} + +.section { + position: relative; + padding: 100px clamp(40px, 7vw, 110px); + border-bottom: 1px solid var(--line); +} + +.section-heading { + display: grid; + grid-template-columns: minmax(420px, 1.15fr) minmax(280px, 0.85fr); + gap: 70px; + align-items: end; + margin-bottom: 60px; +} + +.section-heading h2 { + max-width: 780px; + margin: 15px 0 0; + font-size: clamp(43px, 5.4vw, 76px); + font-weight: 720; + letter-spacing: -0.06em; + line-height: 0.98; +} + +.section-heading > p { + max-width: 520px; + margin: 0; + color: var(--muted); + font-size: 14px; + line-height: 1.8; +} + +.section-index, +.eyebrow { + color: var(--green-dark); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.hero { + min-height: calc(100vh - var(--topbar-height)); + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + padding-top: 115px; + padding-bottom: 58px; +} + +.hero::after { + position: absolute; + z-index: -1; + top: 70px; + right: -120px; + width: min(36vw, 500px); + aspect-ratio: 1; + border: 1px solid rgba(21, 23, 19, 0.1); + border-radius: 50%; + box-shadow: + 0 0 0 80px rgba(184, 242, 61, 0.07), + 0 0 0 160px rgba(92, 119, 255, 0.035); + content: ""; +} + +.eyebrow span { + padding: 5px 8px; + background: var(--green); + color: var(--ink); +} + +.hero h1 { + max-width: 1040px; + margin: 26px 0 28px; + font-size: clamp(64px, 8.3vw, 124px); + font-weight: 720; + letter-spacing: -0.075em; + line-height: 0.86; +} + +.hero-copy { + max-width: 740px; + margin: 0; + color: #4e5149; + font-size: clamp(16px, 1.45vw, 20px); + line-height: 1.75; +} + +.hero-actions { + display: flex; + align-items: center; + gap: 28px; + margin-top: 36px; +} + +.text-link { + display: inline-flex; + gap: 12px; + align-items: center; + padding: 7px 0; + border-bottom: 1px solid var(--ink); + font-size: 13px; + font-weight: 750; +} + +.metric-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-top: auto; + padding-top: 70px; +} + +.metric-strip > div { + min-height: 112px; + padding: 18px 20px; + border-top: 1px solid var(--ink); + border-right: 1px solid var(--line); +} + +.metric-strip > div:first-child { + padding-left: 0; +} + +.metric-strip strong, +.metric-strip span, +.metric-strip small { + display: block; +} + +.metric-strip strong { + margin-bottom: 17px; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 20px; + letter-spacing: -0.04em; +} + +.metric-strip span { + font-size: 13px; + font-weight: 760; +} + +.metric-strip small { + margin-top: 5px; + color: var(--muted); + font-size: 10px; +} + +.architecture-lab { + border: 1px solid var(--line); + background: #e9e6dd; + box-shadow: 8px 8px 0 rgba(21, 23, 19, 0.06); +} + +.flow-tabs { + display: flex; + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.38); +} + +.flow-tab { + padding: 15px 22px; + border: 0; + border-right: 1px solid var(--line); + background: transparent; + color: var(--muted); + font-size: 11px; + font-weight: 750; + cursor: pointer; +} + +.flow-tab:hover, +.flow-tab.is-active { + background: var(--ink); + color: var(--white); +} + +.system-map { + display: grid; + grid-template-columns: 0.8fr 1.35fr 0.85fr; + min-height: 500px; + padding: 32px; + gap: 14px; +} + +.map-lane { + position: relative; + display: flex; + gap: 12px; + flex-direction: column; + justify-content: center; + padding: 44px 16px 20px; + border: 1px dashed rgba(21, 23, 19, 0.2); + background: rgba(255, 255, 255, 0.35); +} + +.lane-label { + position: absolute; + top: 14px; + left: 16px; + color: #8b8d86; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 8px; + letter-spacing: 0.12em; +} + +.map-arrow { + align-self: center; + color: #8c8e87; + font-family: monospace; +} + +.system-node { + position: relative; + z-index: 1; + padding: 16px; + border: 1px solid rgba(21, 23, 19, 0.28); + background: var(--paper); + transition: + border-color 180ms ease, + box-shadow 180ms ease, + transform 180ms ease, + opacity 180ms ease; +} + +.system-node small, +.system-node strong, +.system-node span { + display: block; +} + +.system-node small { + margin-bottom: 14px; + color: #898b83; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 8px; + letter-spacing: 0.08em; +} + +.system-node strong { + font-size: 16px; +} + +.system-node span { + margin-top: 5px; + color: var(--muted); + font-size: 9px; + line-height: 1.45; +} + +.node-runtime { + border-color: var(--green-dark); + background: #e4f9b7; +} + +.node-web { + background: #dfe4ff; +} + +.node-workspace { + background: #ffe1d8; +} + +.node-store { + margin-top: 14px; +} + +.system-map.has-selection .system-node:not(.is-flow-active) { + opacity: 0.3; +} + +.system-node.is-flow-active { + z-index: 2; + border-color: var(--ink); + box-shadow: 4px 4px 0 var(--ink); + transform: translate(-2px, -2px); +} + +.flow-explainer { + display: grid; + grid-template-columns: 210px 1fr; + gap: 22px; + padding: 22px 28px; + border-top: 1px solid var(--line); + background: var(--ink); + color: var(--white); +} + +.flow-explainer span { + color: var(--green); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 9px; + letter-spacing: 0.1em; +} + +.flow-explainer p { + max-width: 780px; + margin: 0; + color: #d6d7d1; + font-size: 12px; + line-height: 1.7; +} + +.boundary-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 34px; +} + +.boundary-grid article { + position: relative; + min-height: 245px; + padding: 32px; + border-top: 1px solid var(--ink); + border-right: 1px solid var(--line); + background: rgba(255, 255, 255, 0.3); +} + +.boundary-number { + display: grid; + width: 28px; + height: 28px; + place-items: center; + border: 1px solid var(--ink); + font-family: monospace; + font-size: 10px; +} + +.boundary-grid h3 { + margin: 35px 0 12px; + font-size: 20px; +} + +.boundary-grid p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.7; +} + +.boundary-grid small { + position: absolute; + bottom: 24px; + color: var(--green-dark); + font-family: monospace; + font-size: 8px; + letter-spacing: 0.1em; +} + +.loop-section, +.operations-section { + background: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), var(--dark); + background-size: 24px 24px; + color: var(--white); +} + +.inverse-heading > p { + color: #afb2aa; +} + +.inverse-heading code { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.08); +} + +.loop-console { + border: 1px solid rgba(255, 255, 255, 0.16); + background: #1b1e1a; +} + +.loop-stages { + display: grid; + grid-template-columns: repeat(6, 1fr); + border-bottom: 1px solid rgba(255, 255, 255, 0.14); +} + +.loop-stage { + min-width: 0; + padding: 22px 16px; + border: 0; + border-right: 1px solid rgba(255, 255, 255, 0.1); + background: transparent; + color: #8e9289; + text-align: left; + cursor: pointer; +} + +.loop-stage span, +.loop-stage strong, +.loop-stage small { + display: block; +} + +.loop-stage span { + margin-bottom: 24px; + font-family: monospace; + font-size: 9px; +} + +.loop-stage strong { + color: #c9cbc5; + font-size: 12px; +} + +.loop-stage small { + margin-top: 5px; + font-size: 9px; +} + +.loop-stage:hover, +.loop-stage.is-active { + background: var(--green); + color: #587d08; +} + +.loop-stage.is-active strong, +.loop-stage:hover strong { + color: var(--ink); +} + +.loop-detail { + display: grid; + grid-template-columns: 1.1fr 1fr 0.85fr; + gap: 55px; + min-height: 240px; + padding: 42px; + align-items: start; +} + +.detail-label { + color: var(--green); + font-family: monospace; + font-size: 9px; + letter-spacing: 0.1em; +} + +.loop-detail h3 { + margin: 16px 0 0; + font-size: clamp(25px, 2.5vw, 38px); + font-weight: 660; + letter-spacing: -0.035em; + line-height: 1.16; +} + +.loop-detail > p { + margin: 24px 0 0; + color: #b1b4ad; + font-size: 12px; + line-height: 1.75; +} + +.loop-detail ul { + margin: 24px 0 0; + padding: 0; + list-style: none; +} + +.loop-detail li { + position: relative; + padding: 9px 0 9px 17px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + color: #d7d9d3; + font-size: 10px; + line-height: 1.55; +} + +.loop-detail li::before { + position: absolute; + top: 14px; + left: 0; + width: 5px; + height: 5px; + background: var(--green); + content: ""; +} + +.loop-principles { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 28px; +} + +.loop-principles article { + min-height: 230px; + padding: 29px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-right: 0; +} + +.loop-principles article:last-child { + border-right: 1px solid rgba(255, 255, 255, 0.14); +} + +.loop-principles span, +.ops-grid span { + color: var(--green); + font-family: monospace; + font-size: 9px; + letter-spacing: 0.1em; +} + +.loop-principles h3, +.ops-grid h3 { + margin: 44px 0 12px; + font-size: 19px; +} + +.loop-principles p, +.ops-grid p { + margin: 0; + color: #aeb1a9; + font-size: 11px; + line-height: 1.7; +} + +.loop-principles code { + border-color: rgba(255, 255, 255, 0.17); + background: transparent; +} + +.evidence-table { + margin-top: 46px; + border-top: 1px solid rgba(255, 255, 255, 0.34); +} + +.table-row { + display: grid; + grid-template-columns: 0.7fr 1.25fr 1fr; + padding: 16px 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + color: #aeb1a9; + font-size: 11px; +} + +.table-row strong { + color: var(--white); + font-size: 11px; +} + +.table-head { + color: var(--green); + font-family: monospace; + font-size: 9px; + letter-spacing: 0.07em; +} + +.isolation-visual { + display: grid; + grid-template-columns: minmax(230px, 0.8fr) auto minmax(330px, 1.2fr); + gap: 30px; + align-items: center; + max-width: 1000px; + margin: 0 auto 52px; +} + +.identity-card, +.workspace-stack { + border: 1px solid var(--ink); + box-shadow: 6px 6px 0 rgba(21, 23, 19, 0.1); +} + +.identity-card { + padding: 30px; + background: var(--green); +} + +.identity-card small, +.identity-card strong, +.identity-card code { + display: block; +} + +.identity-card small { + font-family: monospace; + font-size: 8px; + letter-spacing: 0.1em; +} + +.identity-card strong { + margin: 27px 0 12px; + font-size: 20px; +} + +.identity-card code { + border-color: rgba(21, 23, 19, 0.4); + background: transparent; + font-size: 10px; +} + +.identity-arrow { + font-size: 24px; +} + +.workspace-stack { + padding: 12px; + background: var(--ink); + color: var(--white); +} + +.workspace-stack > div { + display: grid; + grid-template-columns: 100px 1fr; + gap: 12px; + padding: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.workspace-stack > div:last-child { + border-bottom: 0; +} + +.workspace-stack span { + color: var(--green); + font-family: monospace; + font-size: 8px; + letter-spacing: 0.1em; +} + +.workspace-stack code { + border: 0; + background: transparent; + color: #cfd1ca; + font-size: 10px; +} + +.security-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); +} + +.security-grid article { + min-height: 250px; + padding: 24px; + border-top: 1px solid var(--ink); + border-right: 1px solid var(--line); +} + +.security-icon { + display: inline-block; + padding: 4px 6px; + background: var(--ink); + color: var(--green); + font-family: monospace; + font-size: 9px; +} + +.security-grid h3 { + margin: 46px 0 12px; + font-size: 17px; + line-height: 1.3; +} + +.security-grid p { + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.7; +} + +.models-section { + background: #e8e5dc; +} + +.model-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +.model-card { + min-height: 310px; + padding: 25px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.55); +} + +.model-card.is-default { + border-color: var(--ink); + box-shadow: 5px 5px 0 var(--ink); + transform: translateY(-5px); +} + +.model-card > div:first-child { + display: flex; + align-items: center; + justify-content: space-between; +} + +.model-card small { + font-family: monospace; + font-size: 8px; + letter-spacing: 0.08em; +} + +.model-dot { + width: 10px; + height: 10px; + border: 1px solid var(--ink); + border-radius: 50%; +} + +.model-dot.luna { + background: #c8c8c8; +} + +.model-dot.terra { + background: var(--green); +} + +.model-dot.sol { + background: var(--orange); +} + +.model-dot.deepseek { + background: var(--blue); +} + +.model-card h3 { + margin: 55px 0 7px; + font-size: 30px; + letter-spacing: -0.04em; +} + +.model-card p { + margin: 0; + color: var(--muted); + font-size: 11px; +} + +.model-card dl { + margin: 55px 0 0; +} + +.model-card dl div { + display: flex; + justify-content: space-between; + padding: 9px 0; + border-top: 1px solid var(--line); +} + +.model-card dt, +.model-card dd { + margin: 0; + font-size: 9px; +} + +.model-card dt { + color: var(--muted); +} + +.model-card dd { + font-family: monospace; +} + +.model-deepseek { + background: #e4e8ff; +} + +.policy-note { + display: grid; + grid-template-columns: 130px 1fr; + gap: 40px; + margin-top: 40px; + padding: 24px 0; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); +} + +.policy-note span { + font-family: monospace; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.1em; +} + +.policy-note p { + max-width: 800px; + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.7; +} + +.experiment-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid var(--ink); +} + +.filter-group { + display: flex; + gap: 6px; +} + +.filter-button { + padding: 9px 14px; + border: 1px solid var(--line); + background: transparent; + font-size: 10px; + font-weight: 700; + cursor: pointer; +} + +.filter-button:hover, +.filter-button.is-active { + border-color: var(--ink); + background: var(--ink); + color: var(--white); +} + +.experiment-search { + position: relative; +} + +.experiment-search input { + width: min(300px, 34vw); + padding: 10px 34px 10px 12px; + border: 1px solid var(--line); + border-radius: 0; + outline: none; + background: rgba(255, 255, 255, 0.45); + font-size: 11px; +} + +.experiment-search input:focus { + border-color: var(--ink); + box-shadow: 2px 2px 0 var(--ink); +} + +.experiment-search > span:last-child { + position: absolute; + top: 7px; + right: 12px; + font-size: 17px; +} + +.experiment-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.experiment-card { + display: grid; + grid-template-rows: auto auto 1fr auto; + min-height: 295px; + padding: 25px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.48); + transition: + transform 160ms ease, + box-shadow 160ms ease; +} + +.experiment-card:hover { + transform: translate(-3px, -3px); + box-shadow: 5px 5px 0 var(--ink); +} + +.experiment-meta { + display: flex; + align-items: center; + justify-content: space-between; + font-family: monospace; + font-size: 8px; + letter-spacing: 0.07em; +} + +.status-badge { + padding: 5px 7px; + border: 1px solid var(--ink); +} + +.status-baseline { + background: #dddcd5; +} + +.status-validated { + background: var(--green); +} + +.status-backlog { + background: #e3e7ff; +} + +.experiment-card h3 { + max-width: 530px; + margin: 37px 0 11px; + font-size: 22px; + letter-spacing: -0.03em; + line-height: 1.2; +} + +.experiment-card > p { + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.7; +} + +.experiment-footer { + display: flex; + flex-wrap: wrap; + gap: 7px; + align-items: end; + margin-top: 28px; +} + +.experiment-footer span { + padding: 5px 7px; + border: 1px solid var(--line); + color: var(--muted); + font-family: monospace; + font-size: 8px; +} + +.experiment-loading, +.experiment-empty { + grid-column: 1 / -1; + padding: 50px; + border: 1px dashed var(--line); + color: var(--muted); + text-align: center; + font-size: 12px; +} + +.experiment-count { + margin: 18px 0 0; + color: var(--muted); + font-family: monospace; + font-size: 9px; + text-align: right; +} + +.deployment-path { + display: grid; + grid-template-columns: repeat(7, auto); + align-items: center; + justify-content: space-between; + padding: 26px; + border: 1px solid rgba(255, 255, 255, 0.16); + background: var(--dark-raised); +} + +.deployment-path div small, +.deployment-path div strong { + display: block; +} + +.deployment-path div small { + color: var(--green); + font-family: monospace; + font-size: 8px; + letter-spacing: 0.1em; +} + +.deployment-path div strong { + margin-top: 9px; + font-family: monospace; + font-size: 11px; +} + +.deployment-path > span { + color: #686d63; +} + +.ops-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-top: 28px; +} + +.ops-grid article { + min-height: 220px; + padding: 26px; + border-top: 1px solid rgba(255, 255, 255, 0.24); + border-right: 1px solid rgba(255, 255, 255, 0.12); +} + +.timeline { + margin: 0; + padding: 0; + border-top: 1px solid var(--ink); + list-style: none; +} + +.timeline li { + display: grid; + grid-template-columns: 90px 1fr; + min-height: 145px; + padding: 28px 0; + border-bottom: 1px solid var(--line); +} + +.timeline li > span { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid var(--ink); + font-family: monospace; + font-size: 9px; +} + +.timeline li.is-current > span { + background: var(--green); + box-shadow: 3px 3px 0 var(--ink); +} + +.timeline div { + display: grid; + grid-template-columns: 130px 0.8fr 1fr; + gap: 35px; + align-items: start; +} + +.timeline small { + color: var(--green-dark); + font-family: monospace; + font-size: 8px; + letter-spacing: 0.1em; +} + +.timeline h3 { + margin: 0; + font-size: 18px; +} + +.timeline p { + max-width: 540px; + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.7; +} + +.repository-section { + padding: 70px clamp(40px, 7vw, 110px); + background: var(--green); +} + +.repository-panel { + display: grid; + grid-template-columns: 1fr 0.85fr; + gap: 80px; + padding: 60px; + background: var(--ink); + color: var(--white); +} + +.repository-panel h2 { + margin: 20px 0 22px; + font-size: clamp(34px, 4.4vw, 62px); + font-weight: 680; + letter-spacing: -0.055em; + line-height: 1.02; +} + +.repository-panel > div:first-child > p { + max-width: 610px; + margin: 0; + color: #b5b8b0; + font-size: 12px; + line-height: 1.75; +} + +.doc-list { + border-top: 1px solid rgba(255, 255, 255, 0.32); +} + +.doc-list a { + position: relative; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + padding: 17px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.13); + transition: padding 150ms ease; +} + +.doc-list a::after { + position: absolute; + right: 0; + color: var(--green); + content: "↗"; +} + +.doc-list a:hover { + padding-left: 8px; +} + +.doc-list span { + font-family: monospace; + font-size: 10px; +} + +.doc-list small { + padding-right: 20px; + color: #8f9389; + font-size: 9px; +} + +footer { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 30px; + padding: 34px 36px; + background: var(--ink); + color: var(--white); +} + +footer span, +footer a { + font-family: monospace; + font-size: 9px; + letter-spacing: 0.09em; +} + +footer p { + margin: 0; + color: #8f928a; + font-size: 10px; +} + +footer a { + color: var(--green); +} + +@media (max-width: 1120px) { + :root { + --sidebar-width: 194px; + } + + .section { + padding-right: 48px; + padding-left: 48px; + } + + .system-map { + grid-template-columns: 1fr; + } + + .map-lane { + display: grid; + grid-template-columns: repeat(2, 1fr); + padding-top: 52px; + } + + .map-arrow { + display: none; + } + + .loop-stages { + grid-template-columns: repeat(3, 1fr); + } + + .loop-detail { + grid-template-columns: 1fr 1fr; + } + + .loop-detail ul { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + } + + .security-grid, + .ops-grid, + .model-grid { + grid-template-columns: repeat(2, 1fr); + } + + .deployment-path { + grid-template-columns: 1fr; + gap: 12px; + } + + .deployment-path > span { + transform: rotate(90deg); + justify-self: start; + } +} + +@media (max-width: 820px) { + :root { + --topbar-height: 66px; + } + + .topbar { + padding: 0 14px; + } + + .system-status, + .hide-mobile { + display: none; + } + + .button-primary { + min-height: 38px; + padding: 0 12px; + } + + .nav-toggle { + display: block; + } + + .page-shell { + display: block; + } + + .sidebar { + position: fixed; + z-index: 90; + top: var(--topbar-height); + right: 0; + left: 0; + height: calc(100vh - var(--topbar-height)); + transform: translateX(-105%); + padding: 25px; + background: var(--paper); + transition: transform 220ms ease; + } + + .sidebar.is-open { + transform: translateX(0); + } + + .sidebar-note { + max-width: 280px; + } + + .section { + padding: 72px 23px; + } + + .section-heading { + grid-template-columns: 1fr; + gap: 28px; + margin-bottom: 42px; + } + + .section-heading h2 { + font-size: clamp(40px, 12vw, 64px); + } + + .hero { + min-height: calc(100svh - var(--topbar-height)); + padding-top: 86px; + } + + .hero h1 { + font-size: clamp(58px, 17vw, 92px); + } + + .metric-strip { + grid-template-columns: repeat(2, 1fr); + padding-top: 60px; + } + + .metric-strip > div:nth-child(3) { + padding-left: 0; + } + + .flow-tabs { + display: grid; + grid-template-columns: repeat(2, 1fr); + } + + .system-map { + padding: 14px; + } + + .map-lane { + grid-template-columns: 1fr; + } + + .flow-explainer { + grid-template-columns: 1fr; + } + + .boundary-grid, + .loop-principles, + .experiment-grid { + grid-template-columns: 1fr; + } + + .loop-stages { + grid-template-columns: repeat(2, 1fr); + } + + .loop-detail { + grid-template-columns: 1fr; + padding: 28px; + } + + .loop-detail ul { + grid-column: auto; + grid-template-columns: 1fr; + } + + .isolation-visual { + grid-template-columns: 1fr; + } + + .identity-arrow { + transform: rotate(90deg); + text-align: center; + } + + .model-grid, + .security-grid, + .ops-grid { + grid-template-columns: 1fr; + } + + .model-card.is-default { + transform: none; + } + + .experiment-toolbar { + align-items: stretch; + flex-direction: column; + } + + .filter-group { + overflow-x: auto; + } + + .experiment-search input { + width: 100%; + } + + .timeline li { + grid-template-columns: 48px 1fr; + } + + .timeline div { + grid-template-columns: 1fr; + gap: 10px; + } + + .repository-section { + padding: 20px; + } + + .repository-panel { + grid-template-columns: 1fr; + gap: 54px; + padding: 35px 25px; + } + + .doc-list a { + grid-template-columns: 1fr; + gap: 6px; + } + + footer { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/e2e/verify_stack.py b/e2e/verify_stack.py index 4325b00..db6e2a5 100644 --- a/e2e/verify_stack.py +++ b/e2e/verify_stack.py @@ -149,6 +149,27 @@ def main() -> None: health.raise_for_status() assert health.json().get("status") is True + docs_entry = client.get("/doc", follow_redirects=False) + assert docs_entry.status_code in {307, 308} + assert docs_entry.headers["location"].endswith("/doc/") + docs = client.get("/doc/") + docs.raise_for_status() + assert "K1412 Agent · 架构、实现与实验" in docs.text + assert docs.headers["content-type"].startswith("text/html") + docs_style = client.get("/doc/styles.css") + docs_style.raise_for_status() + assert "--green: #b8f23d" in docs_style.text + docs_script = client.get("/doc/app.js") + docs_script.raise_for_status() + assert "safe-parallel-v1" in docs_script.text + docs_experiments = client.get("/doc/experiments.json") + docs_experiments.raise_for_status() + assert {item["status"] for item in docs_experiments.json()} == { + "baseline", + "validated", + "backlog", + } + admin = _signin(client, ADMIN_EMAIL, ADMIN_PASSWORD) admin_token = admin["token"] @@ -276,8 +297,8 @@ def main() -> None: assert legacy.status_code == 404, legacy.text print( - "E2E passed: auth approval, four Agent models, custom loop evidence, file downloads, " - "and per-user workspace isolation." + "E2E passed: public documentation, auth approval, four Agent models, custom loop " + "evidence, file downloads, and per-user workspace isolation." ) diff --git a/tests/test_docs_site.py b/tests/test_docs_site.py new file mode 100644 index 0000000..294a65e --- /dev/null +++ b/tests/test_docs_site.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import json +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import urlparse + +SITE_ROOT = Path(__file__).parents[1] / "docs" / "site" +ALLOWED_STATUSES = {"baseline", "validated", "backlog"} + + +class SiteParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.ids: set[str] = set() + self.references: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + values = dict(attrs) + if values.get("id"): + self.ids.add(values["id"]) + attribute = "href" if tag in {"a", "link"} else "src" if tag in {"script", "img"} else None + if attribute and values.get(attribute): + self.references.append(values[attribute]) + + +def _parser() -> SiteParser: + parser = SiteParser() + parser.feed((SITE_ROOT / "index.html").read_text()) + return parser + + +def test_docs_site_required_files_are_present() -> None: + for relative_path in ("index.html", "styles.css", "app.js", "experiments.json", "og.png"): + path = SITE_ROOT / relative_path + assert path.is_file(), relative_path + assert path.stat().st_size > 0, relative_path + assert (SITE_ROOT / "og.png").read_bytes().startswith(b"\x89PNG\r\n\x1a\n") + + +def test_docs_site_local_links_and_anchors_resolve() -> None: + parser = _parser() + for reference in parser.references: + if reference.startswith("#"): + assert reference[1:] in parser.ids, reference + continue + + parsed = urlparse(reference) + if parsed.scheme or parsed.netloc or reference.startswith("/"): + continue + if reference == "../static/favicon.svg": + continue + + local_path = (SITE_ROOT / parsed.path).resolve() + assert local_path.is_relative_to(SITE_ROOT.resolve()), reference + assert local_path.is_file(), reference + + +def test_docs_site_navigation_targets_every_top_level_section() -> None: + parser = _parser() + html = (SITE_ROOT / "index.html").read_text() + expected_sections = { + "overview", + "architecture", + "agent-loop", + "workspace", + "models", + "experiments", + "operations", + "timeline", + "repository", + } + assert expected_sections <= parser.ids + for section_id in expected_sections: + assert f'href="#{section_id}"' in html + + +def test_experiment_ledger_has_unique_stable_records() -> None: + experiments = json.loads((SITE_ROOT / "experiments.json").read_text()) + assert len(experiments) >= 5 + + ids = [item["id"] for item in experiments] + assert len(ids) == len(set(ids)) + for item in experiments: + assert item["status"] in ALLOWED_STATUSES + assert item["title"].strip() + assert item["summary"].strip() + assert item["area"].strip() + assert item["version"].strip() + assert isinstance(item["metrics"], list)