better-dsh 0.2.3-e → 0.2.3-g
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/50_test-reports/2026-09-13-preact-ui-shell/345/256/236/346/265/213/346/212/245/345/221/212.md +1 -1
- package/docs/50_test-reports/2026-09-14-4999-skill/346/270/205/345/215/225/344/270/216lsp-gate/345/256/236/346/265/213/346/212/245/345/221/212.md +54 -0
- package/docs/specs/agent/spec.md +54 -0
- package/docs/specs/ast/spec.md +34 -0
- package/docs/specs/compaction-recall/spec.md +46 -0
- package/docs/specs/ctx/spec.md +107 -0
- package/docs/specs/dsh/spec.md +47 -0
- package/docs/specs/dvc/spec.md +87 -0
- package/docs/specs/escalation-guidance/spec.md +44 -0
- package/docs/specs/fs-scheme-resolution/spec.md +37 -0
- package/docs/specs/hash-edit/spec.md +41 -0
- package/docs/specs/http-read/spec.md +73 -0
- package/docs/specs/kernel-provisioning/spec.md +53 -0
- package/docs/specs/lsp/spec.md +121 -0
- package/docs/specs/mobile-layout/spec.md +108 -0
- package/docs/specs/model-failover/spec.md +20 -0
- package/docs/specs/preact-ui-shell/spec.md +22 -0
- package/docs/specs/repl-dispatch-resilience/spec.md +21 -0
- package/docs/specs/skill/spec.md +58 -0
- package/docs/specs/tool-surface/spec.md +222 -0
- package/docs/specs/url-schema/spec.md +148 -0
- package/docs/specs/web-trust-fence/spec.md +43 -0
- package/dsh-docs/AGENTS.md +75 -0
- package/dsh-docs/agent-lifecycle.md +84 -0
- package/dsh-docs/agent-lifecycle.zh.md +86 -0
- package/dsh-docs/api-gateway.md +164 -0
- package/dsh-docs/api-gateway.zh.md +164 -0
- package/dsh-docs/architecture.md +150 -0
- package/dsh-docs/architecture.zh.md +154 -0
- package/dsh-docs/capability-seams.md +543 -0
- package/dsh-docs/capability-seams.zh.md +545 -0
- package/dsh-docs/config-catalog.md +3473 -0
- package/dsh-docs/config-catalog.zh.md +3474 -0
- package/dsh-docs/cookbook/adding-a-package.md +117 -0
- package/dsh-docs/cookbook/adding-a-package.zh.md +119 -0
- package/dsh-docs/cookbook/adding-a-remote-api.md +197 -0
- package/dsh-docs/cookbook/adding-a-remote-api.zh.md +197 -0
- package/dsh-docs/cookbook/adding-a-settings-card.md +102 -0
- package/dsh-docs/cookbook/adding-a-settings-card.zh.md +102 -0
- package/dsh-docs/cookbook/adding-a-tool.md +101 -0
- package/dsh-docs/cookbook/adding-a-tool.zh.md +103 -0
- package/dsh-docs/cookbook/adding-a-vendored-package.md +59 -0
- package/dsh-docs/cookbook/adding-a-vendored-package.zh.md +59 -0
- package/dsh-docs/cookbook/adding-an-llm-adapter.md +43 -0
- package/dsh-docs/cookbook/adding-an-llm-adapter.zh.md +43 -0
- package/dsh-docs/cookbook/extension-cookbook.md +132 -0
- package/dsh-docs/cookbook/extension-cookbook.zh.md +136 -0
- package/dsh-docs/cookbook/maintaining-dsh-code-review.md +64 -0
- package/dsh-docs/cookbook/maintaining-dsh-code-review.zh.md +64 -0
- package/dsh-docs/cookbook/responding-to-pr-review-on-a-stack.md +32 -0
- package/dsh-docs/cookbook/responding-to-pr-review-on-a-stack.zh.md +32 -0
- package/dsh-docs/cordis-api/context.md +364 -0
- package/dsh-docs/cordis-api/context.zh.md +366 -0
- package/dsh-docs/cordis-api/events.md +207 -0
- package/dsh-docs/cordis-api/events.zh.md +209 -0
- package/dsh-docs/cordis-api/fiber.md +375 -0
- package/dsh-docs/cordis-api/fiber.zh.md +377 -0
- package/dsh-docs/cordis-api/inherited.md +39 -0
- package/dsh-docs/cordis-api/registry.md +152 -0
- package/dsh-docs/cordis-api/registry.zh.md +154 -0
- package/dsh-docs/cordis-api/service.md +102 -0
- package/dsh-docs/cordis-api/service.zh.md +104 -0
- package/dsh-docs/cordis-primer.md +45 -0
- package/dsh-docs/cordis-primer.zh.md +51 -0
- package/dsh-docs/cordis-tutorial/01-first-plugin.md +95 -0
- package/dsh-docs/cordis-tutorial/01-first-plugin.zh.md +95 -0
- package/dsh-docs/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
- package/dsh-docs/cordis-tutorial/02-lifecycle-and-effects.zh.md +98 -0
- package/dsh-docs/cordis-tutorial/03-services.md +98 -0
- package/dsh-docs/cordis-tutorial/03-services.zh.md +98 -0
- package/dsh-docs/cordis-tutorial/04-events.md +144 -0
- package/dsh-docs/cordis-tutorial/04-events.zh.md +144 -0
- package/dsh-docs/cordis-tutorial/05-config.md +84 -0
- package/dsh-docs/cordis-tutorial/05-config.zh.md +84 -0
- package/dsh-docs/cordis-tutorial/06-composition-and-hmr.md +113 -0
- package/dsh-docs/cordis-tutorial/06-composition-and-hmr.zh.md +113 -0
- package/dsh-docs/cordis-tutorial/07-into-the-harness.md +108 -0
- package/dsh-docs/cordis-tutorial/07-into-the-harness.zh.md +108 -0
- package/dsh-docs/cordis-tutorial/index.md +60 -0
- package/dsh-docs/cordis-tutorial/index.zh.md +62 -0
- package/dsh-docs/deepseek-llm-api-wire-extensions.md +163 -0
- package/dsh-docs/deepseek-llm-api-wire-extensions.zh.md +163 -0
- package/dsh-docs/defensive-patterns.md +33 -0
- package/dsh-docs/defensive-patterns.zh.md +35 -0
- package/dsh-docs/development.md +167 -0
- package/dsh-docs/development.zh.md +173 -0
- package/dsh-docs/event-producer-consumer.md +86 -0
- package/dsh-docs/event-producer-consumer.zh.md +88 -0
- package/dsh-docs/glossary.md +45 -0
- package/dsh-docs/glossary.zh.md +45 -0
- package/dsh-docs/graph-atlas.md +22 -0
- package/dsh-docs/graph-atlas.zh.md +24 -0
- package/dsh-docs/i18n/README.md +60 -0
- package/dsh-docs/i18n/README.zh.md +62 -0
- package/dsh-docs/i18n/style-samples.md +87 -0
- package/dsh-docs/i18n/terminology.md +214 -0
- package/dsh-docs/i18n/translation-prompt.md +263 -0
- package/dsh-docs/i18n/translation-rules.md +69 -0
- package/dsh-docs/i18n/translation-rules.zh.md +69 -0
- package/dsh-docs/module-graph.md +1411 -0
- package/dsh-docs/module-graph.zh.md +1413 -0
- package/dsh-docs/persistence-catalog.md +1075 -0
- package/dsh-docs/persistence-catalog.zh.md +1077 -0
- package/dsh-docs/postmortem/0001-acp-default-export-drops-inject.md +113 -0
- package/dsh-docs/postmortem/0001-acp-default-export-drops-inject.zh.md +113 -0
- package/dsh-docs/postmortem/0002-js-expression-disabled-filesystem-tools.md +47 -0
- package/dsh-docs/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +47 -0
- package/dsh-docs/postmortem/0003-web-agent-gui-feedback-loop.md +53 -0
- package/dsh-docs/postmortem/0003-web-agent-gui-feedback-loop.zh.md +53 -0
- package/dsh-docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +55 -0
- package/dsh-docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +55 -0
- package/dsh-docs/postmortem/README.md +18 -0
- package/dsh-docs/postmortem/README.zh.md +18 -0
- package/dsh-docs/rescope.md +53 -0
- package/dsh-docs/rescope.zh.md +53 -0
- package/dsh-docs/subsystems/README.md +61 -0
- package/dsh-docs/subsystems/README.zh.md +61 -0
- package/dsh-docs/subsystems/agent-team.md +207 -0
- package/dsh-docs/subsystems/agent-team.zh.md +207 -0
- package/dsh-docs/subsystems/approval.md +170 -0
- package/dsh-docs/subsystems/approval.zh.md +170 -0
- package/dsh-docs/subsystems/attachment.md +351 -0
- package/dsh-docs/subsystems/attachment.zh.md +351 -0
- package/dsh-docs/subsystems/client-modules.md +168 -0
- package/dsh-docs/subsystems/client-modules.zh.md +168 -0
- package/dsh-docs/subsystems/code-runtime.md +195 -0
- package/dsh-docs/subsystems/code-runtime.zh.md +195 -0
- package/dsh-docs/subsystems/commands.md +219 -0
- package/dsh-docs/subsystems/commands.zh.md +219 -0
- package/dsh-docs/subsystems/compaction.md +238 -0
- package/dsh-docs/subsystems/compaction.zh.md +238 -0
- package/dsh-docs/subsystems/conversation.md +258 -0
- package/dsh-docs/subsystems/conversation.zh.md +258 -0
- package/dsh-docs/subsystems/core.md +1209 -0
- package/dsh-docs/subsystems/core.zh.md +1219 -0
- package/dsh-docs/subsystems/credentials.md +329 -0
- package/dsh-docs/subsystems/credentials.zh.md +329 -0
- package/dsh-docs/subsystems/extensions.md +382 -0
- package/dsh-docs/subsystems/extensions.zh.md +382 -0
- package/dsh-docs/subsystems/feedback.md +266 -0
- package/dsh-docs/subsystems/feedback.zh.md +266 -0
- package/dsh-docs/subsystems/filesystem.md +505 -0
- package/dsh-docs/subsystems/filesystem.zh.md +505 -0
- package/dsh-docs/subsystems/goal.md +277 -0
- package/dsh-docs/subsystems/goal.zh.md +277 -0
- package/dsh-docs/subsystems/invariants.md +88 -0
- package/dsh-docs/subsystems/invariants.zh.md +88 -0
- package/dsh-docs/subsystems/jobs.md +290 -0
- package/dsh-docs/subsystems/jobs.zh.md +290 -0
- package/dsh-docs/subsystems/llm-streaming.md +1080 -0
- package/dsh-docs/subsystems/llm-streaming.zh.md +1086 -0
- package/dsh-docs/subsystems/lsp.md +202 -0
- package/dsh-docs/subsystems/lsp.zh.md +202 -0
- package/dsh-docs/subsystems/permission-presets.md +131 -0
- package/dsh-docs/subsystems/permission-presets.zh.md +131 -0
- package/dsh-docs/subsystems/persistence.md +395 -0
- package/dsh-docs/subsystems/persistence.zh.md +395 -0
- package/dsh-docs/subsystems/plan.md +87 -0
- package/dsh-docs/subsystems/plan.zh.md +87 -0
- package/dsh-docs/subsystems/sandbox.md +220 -0
- package/dsh-docs/subsystems/sandbox.zh.md +220 -0
- package/dsh-docs/subsystems/schedule.md +192 -0
- package/dsh-docs/subsystems/schedule.zh.md +192 -0
- package/dsh-docs/subsystems/scope.md +59 -0
- package/dsh-docs/subsystems/scope.zh.md +59 -0
- package/dsh-docs/subsystems/session-projection.md +354 -0
- package/dsh-docs/subsystems/session-projection.zh.md +354 -0
- package/dsh-docs/subsystems/session-query.md +509 -0
- package/dsh-docs/subsystems/session-query.zh.md +509 -0
- package/dsh-docs/subsystems/session-reference.md +219 -0
- package/dsh-docs/subsystems/session-reference.zh.md +219 -0
- package/dsh-docs/subsystems/session-telemetry.md +194 -0
- package/dsh-docs/subsystems/session-telemetry.zh.md +194 -0
- package/dsh-docs/subsystems/session-title.md +204 -0
- package/dsh-docs/subsystems/session-title.zh.md +204 -0
- package/dsh-docs/subsystems/session.md +1155 -0
- package/dsh-docs/subsystems/session.zh.md +1159 -0
- package/dsh-docs/subsystems/settings.md +405 -0
- package/dsh-docs/subsystems/settings.zh.md +405 -0
- package/dsh-docs/subsystems/shell.md +303 -0
- package/dsh-docs/subsystems/shell.zh.md +303 -0
- package/dsh-docs/subsystems/skills.md +354 -0
- package/dsh-docs/subsystems/skills.zh.md +354 -0
- package/dsh-docs/subsystems/slots.md +175 -0
- package/dsh-docs/subsystems/slots.zh.md +175 -0
- package/dsh-docs/subsystems/spill.md +117 -0
- package/dsh-docs/subsystems/spill.zh.md +117 -0
- package/dsh-docs/subsystems/storage.md +260 -0
- package/dsh-docs/subsystems/storage.zh.md +260 -0
- package/dsh-docs/subsystems/subagent.md +766 -0
- package/dsh-docs/subsystems/subagent.zh.md +770 -0
- package/dsh-docs/subsystems/subprocess.md +324 -0
- package/dsh-docs/subsystems/subprocess.zh.md +324 -0
- package/dsh-docs/subsystems/system-prompt.md +220 -0
- package/dsh-docs/subsystems/system-prompt.zh.md +220 -0
- package/dsh-docs/subsystems/terminal.md +184 -0
- package/dsh-docs/subsystems/terminal.zh.md +184 -0
- package/dsh-docs/subsystems/todo.md +32 -0
- package/dsh-docs/subsystems/todo.zh.md +32 -0
- package/dsh-docs/subsystems/token-meter.md +105 -0
- package/dsh-docs/subsystems/token-meter.zh.md +105 -0
- package/dsh-docs/subsystems/tools.md +720 -0
- package/dsh-docs/subsystems/tools.zh.md +720 -0
- package/dsh-docs/subsystems/typert.md +343 -0
- package/dsh-docs/subsystems/typert.zh.md +343 -0
- package/dsh-docs/subsystems/user-questions.md +178 -0
- package/dsh-docs/subsystems/user-questions.zh.md +178 -0
- package/dsh-docs/subsystems/web-client.md +95 -0
- package/dsh-docs/subsystems/web-client.zh.md +95 -0
- package/dsh-docs/subsystems/web-server.md +154 -0
- package/dsh-docs/subsystems/web-server.zh.md +154 -0
- package/dsh-docs/subsystems/web.md +206 -0
- package/dsh-docs/subsystems/web.zh.md +206 -0
- package/dsh-docs/subsystems/webhook.md +70 -0
- package/dsh-docs/subsystems/webhook.zh.md +70 -0
- package/dsh-docs/subsystems/workflow.md +278 -0
- package/dsh-docs/subsystems/workflow.zh.md +278 -0
- package/dsh-docs/subsystems/workspace.md +321 -0
- package/dsh-docs/subsystems/workspace.zh.md +321 -0
- package/dsh-docs/testing.md +54 -0
- package/dsh-docs/testing.zh.md +54 -0
- package/dsh-docs/tool-catalog.md +2225 -0
- package/dsh-docs/tool-catalog.zh.md +2233 -0
- package/dsh-docs/tool-execution-pipeline.md +62 -0
- package/dsh-docs/tool-execution-pipeline.zh.md +64 -0
- package/dsh-docs/user/develop/basic/config.md +106 -0
- package/dsh-docs/user/develop/basic/config.zh.md +106 -0
- package/dsh-docs/user/develop/basic/index.md +144 -0
- package/dsh-docs/user/develop/basic/index.zh.md +144 -0
- package/dsh-docs/user/develop/basic/publish.md +183 -0
- package/dsh-docs/user/develop/basic/publish.zh.md +183 -0
- package/dsh-docs/user/develop/basic/tool.md +52 -0
- package/dsh-docs/user/develop/basic/tool.zh.md +52 -0
- package/dsh-docs/user/develop/framework/events.md +143 -0
- package/dsh-docs/user/develop/framework/events.zh.md +143 -0
- package/dsh-docs/user/develop/framework/index.md +137 -0
- package/dsh-docs/user/develop/framework/index.zh.md +137 -0
- package/dsh-docs/user/develop/framework/service.md +148 -0
- package/dsh-docs/user/develop/framework/service.zh.md +150 -0
- package/dsh-docs/user/develop/practice/dynamic-cordis.md +15 -0
- package/dsh-docs/user/develop/practice/dynamic-cordis.zh.md +15 -0
- package/dsh-docs/user/develop/practice/index.md +155 -0
- package/dsh-docs/user/develop/practice/index.zh.md +155 -0
- package/dsh-docs/user/develop/practice/llm-adapter.md +189 -0
- package/dsh-docs/user/develop/practice/llm-adapter.zh.md +189 -0
- package/dsh-docs/user/guide/github-review.md +102 -0
- package/dsh-docs/user/guide/github-review.zh.md +102 -0
- package/dsh-docs/user/guide/index.md +30 -0
- package/dsh-docs/user/guide/index.zh.md +30 -0
- package/dsh-docs/user/guide/mcp-memory.md +101 -0
- package/dsh-docs/user/guide/mcp-memory.zh.md +101 -0
- package/dsh-docs/user/guide/network-proxy.md +85 -0
- package/dsh-docs/user/guide/network-proxy.zh.md +85 -0
- package/dsh-docs/user/guide/providers.md +190 -0
- package/dsh-docs/user/guide/providers.zh.md +190 -0
- package/dsh-docs/user/guide/python-sdk.md +150 -0
- package/dsh-docs/user/guide/python-sdk.zh.md +150 -0
- package/dsh-docs/user/guide/schedule.md +21 -0
- package/dsh-docs/user/guide/schedule.zh.md +21 -0
- package/dsh-docs/user/index.md +11 -0
- package/dsh-docs/user/index.zh.md +11 -0
- package/dsh-docs/web-styling.md +29 -0
- package/dsh-docs/web-styling.zh.md +29 -0
- package/lib/client/index.js +268 -38
- package/lib/fs-aware/sandbox-plugin.js +1 -1
- package/lib/index.js +1112 -1261
- package/lib/lsp-server-registry-B8DNonhS.js +3 -0
- package/lib/lsp-server-registry-BexQagaK.js +943 -0
- package/lib/{wrap-DC8O3SYz.js → wrap-JFjcWwZf.js} +42 -16
- package/package.json +2 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# HTTP Server
|
|
2
|
+
|
|
3
|
+
English | [中文](web-server.zh.md)
|
|
4
|
+
|
|
5
|
+
[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.webServer`, a named-route registry, optional gzip response compression, index.html transform callbacks, and one fallback handler that a plugin may claim. It is not part of the agent loop and not a capability seam; it knows no harness concepts, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering note](../../.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server.
|
|
6
|
+
|
|
7
|
+
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
8
|
+
|
|
9
|
+
## Routes
|
|
10
|
+
|
|
11
|
+
```ts type-equiv
|
|
12
|
+
/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
|
|
13
|
+
type WebRouteKind = 'exact' | 'prefix'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts type-equiv
|
|
17
|
+
/** One named route registration. */
|
|
18
|
+
interface WebRoute {
|
|
19
|
+
kind: WebRouteKind
|
|
20
|
+
/** Absolute pathname, no trailing slash. */
|
|
21
|
+
path: string
|
|
22
|
+
/** Owns the full response lifecycle (may hold the response open, e.g. SSE). */
|
|
23
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with [`dsh-host-frontend-static`](../../packages/host/frontend-static/src/index.ts), the SPA dist server with locked semantics: Connection authenticates the dist root and configured index before their HTML is read; non-index assets remain public; non-GET/HEAD is 405, traversal outside the dist root is 403, existing files are served directly, absent or non-file targets are empty 404 responses, and unknown extensions ship as octet-stream.
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
```ts type-equiv
|
|
32
|
+
/** Web server listen and response-compression config. */
|
|
33
|
+
interface Config {
|
|
34
|
+
/** Listen host; the two supported values are loopback and all-interfaces. */
|
|
35
|
+
host: '127.0.0.1' | '0.0.0.0'
|
|
36
|
+
/** Listen port; zero requests an OS-assigned port. */
|
|
37
|
+
port: number
|
|
38
|
+
/** Response compression for socket-backed HTTP requests. @default 'none' */
|
|
39
|
+
compression?: 'none' | 'gzip'
|
|
40
|
+
/** Gzip DEFLATE level from 0 through 9. @default 1 */
|
|
41
|
+
compressionLevel?: number
|
|
42
|
+
/** Minimum known response length eligible for gzip; unknown-length streams are eligible. @default 1024 */
|
|
43
|
+
compressionThresholdBytes?: number
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure). The carrier itself owns no TLS, authentication, or Origin policy, so a non-loopback bind exposes the server unless the composition supplies those controls. `compression` defaults to `none`; the shipped Web bundle selects gzip level 1 with a 1024-byte threshold. The shipped `dsh web` command selects loopback and rejects `--host 0.0.0.0`; its Connection plugin supplies Host/Origin checks plus browser-session authentication for every Host API route and stream. Other compositions own their bind and route-authentication policy. The dist location is an assembly fact of the frontend plugin that claims the seat.
|
|
48
|
+
|
|
49
|
+
## The service
|
|
50
|
+
|
|
51
|
+
`WebServer` (`ctx.webServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. Gzip wraps eligible socket-backed responses inside the server, so route handlers retain direct `ServerResponse` ownership and no response-writing API is added to the service. Existing content encodings, `Cache-Control: no-transform`, ranges, SSE, ZIP, and the packaged `.gz` Worker image remain identity responses. `collectIndexInjections()` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit, and `renderIndex(html)` renders them into successful root and configured index responses before applying the raw `tapIndex(transform)` escape-hatch transforms in registration order; [dsh-client-modules](../../packages/client/modules) answers the event with the boot manifest rows. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0.
|
|
52
|
+
|
|
53
|
+
A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md).
|
|
54
|
+
|
|
55
|
+
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
56
|
+
|
|
57
|
+
<a id="cordis-surface"></a>
|
|
58
|
+
|
|
59
|
+
## Cordis API
|
|
60
|
+
|
|
61
|
+
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
|
62
|
+
|
|
63
|
+
<a id="ctxwebserver--webserver"></a>
|
|
64
|
+
|
|
65
|
+
### `ctx.webServer` — `WebServer`
|
|
66
|
+
|
|
67
|
+
The browser HTTP carrier service. Activation listens immediately. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. A listen failure rejects initialization, and the boot process reports the failed fiber.
|
|
68
|
+
|
|
69
|
+
```ts cordis-catalog
|
|
70
|
+
/**
|
|
71
|
+
* Register a named route. Duplicate (kind, path) throws — route patterns are
|
|
72
|
+
* a composition-level contract, so a collision is a misconfiguration.
|
|
73
|
+
* @param route - kind, path, and the owning handler.
|
|
74
|
+
* @returns the disposer removing the route.
|
|
75
|
+
*/
|
|
76
|
+
register(route: WebRoute): () => void
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
|
|
80
|
+
* one socket can have only one protocol owner.
|
|
81
|
+
* @param route - pathname and handler owning negotiation plus socket use.
|
|
82
|
+
* @returns the disposer removing the route.
|
|
83
|
+
*/
|
|
84
|
+
registerUpgrade(route: WebUpgradeRoute): () => void
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Claim the fallback seat: the handler answering every request no named
|
|
88
|
+
* route matches (the SPA dist server in the shipped Web composition). One
|
|
89
|
+
* owner only — a second registration throws, because two fallbacks cannot
|
|
90
|
+
* compose.
|
|
91
|
+
* @param handler - owns the full response lifecycle of unmatched requests.
|
|
92
|
+
* @returns the disposer releasing the seat.
|
|
93
|
+
*/
|
|
94
|
+
registerFallback(handler: WebRoute['handler']): () => void
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a raw-HTML index transform, the escape hatch for markup no
|
|
98
|
+
* {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
|
|
99
|
+
* registration order after rendering the structured rows.
|
|
100
|
+
* @param transform - pure html-to-html function.
|
|
101
|
+
* @returns the disposer removing the transform.
|
|
102
|
+
*/
|
|
103
|
+
tapIndex(transform: (html: string) => string): () => void
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run an index.html body through the registered taps in registration order
|
|
107
|
+
* — called by the fallback owner on every index response it renders.
|
|
108
|
+
* @param html - the raw index.html body.
|
|
109
|
+
* @returns the transformed body.
|
|
110
|
+
*/
|
|
111
|
+
applyIndexTaps(html: string): string
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gather the structured injection table: one `webserver/index-inject` emit,
|
|
115
|
+
* every subscriber pushes its current rows. Fresh per call, so subscribers
|
|
116
|
+
* read live state (module graph, theme preference) at emit time.
|
|
117
|
+
* @returns rows in subscriber activation order.
|
|
118
|
+
*/
|
|
119
|
+
collectIndexInjections(): IndexInjection[]
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render one index.html body: the structured injection table first, then
|
|
123
|
+
* the raw `tapIndex` transforms over the result.
|
|
124
|
+
* @param html - the raw index.html body.
|
|
125
|
+
* @returns the transformed body.
|
|
126
|
+
*/
|
|
127
|
+
renderIndex(html: string): string
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
131
|
+
|
|
132
|
+
<a id="webserver-events"></a>
|
|
133
|
+
|
|
134
|
+
### `webserver/*` events
|
|
135
|
+
|
|
136
|
+
<a id="webserverindex-inject--emit"></a>
|
|
137
|
+
|
|
138
|
+
#### `webserver/index-inject` — emit
|
|
139
|
+
|
|
140
|
+
Collect the structured index injection table. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
|
|
141
|
+
|
|
142
|
+
```ts cordis-catalog
|
|
143
|
+
/**
|
|
144
|
+
* Collect the structured index injection table. Emitted on every index
|
|
145
|
+
* render and every worker boot-payload request; listeners push their
|
|
146
|
+
* current rows, so a row's data is read fresh at emit time.
|
|
147
|
+
* @param table - Mutable row table; listeners append in activation order.
|
|
148
|
+
* @mode emit
|
|
149
|
+
*/
|
|
150
|
+
'webserver/index-inject'(table: IndexInjection[]): void
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
154
|
+
<!-- END GENERATED cordis-surface -->
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# HTTP 服务器
|
|
2
|
+
|
|
3
|
+
[English](web-server.md) | 中文
|
|
4
|
+
|
|
5
|
+
[dsh-host-webserver](../../packages/host/webserver) 是 GUI Host 的浏览器 HTTP 载体:它是一个提供 `ctx.webServer` 的 `node:http` 插件,包含具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退处理器。它不属于 agent loop(智能体循环),也不是能力 seam;它不了解任何 harness 概念。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.zh.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。
|
|
6
|
+
|
|
7
|
+
源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
8
|
+
|
|
9
|
+
## 路由
|
|
10
|
+
|
|
11
|
+
```ts type-equiv
|
|
12
|
+
/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
|
|
13
|
+
type WebRouteKind = 'exact' | 'prefix'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts type-equiv
|
|
17
|
+
/** One named route registration. */
|
|
18
|
+
interface WebRoute {
|
|
19
|
+
kind: WebRouteKind
|
|
20
|
+
/** Absolute pathname, no trailing slash. */
|
|
21
|
+
path: string
|
|
22
|
+
/** Owns the full response lifecycle (may hold the response open, e.g. SSE). */
|
|
23
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到已注册的回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,任何未被具名路由认领的请求都由回退席位应答;席位只有一个所有者,第二次注册会抛出异常。发布的 Web 组合用 [`dsh-host-frontend-static`](../../packages/host/frontend-static/src/index.ts) 认领席位,即遵循固定语义的 SPA dist 服务器:Connection 在读取 dist 根目录和配置 index 的 HTML 前完成认证;非 index 资产保持公开;非 GET/HEAD 返回 405,越出 dist 根目录的遍历返回 403,现有文件直接提供,缺失或不是文件的目标返回空的 404,未知扩展名按 octet-stream 发送。
|
|
28
|
+
|
|
29
|
+
## 配置
|
|
30
|
+
|
|
31
|
+
```ts type-equiv
|
|
32
|
+
/** Web server listen and response-compression config. */
|
|
33
|
+
interface Config {
|
|
34
|
+
/** Listen host; the two supported values are loopback and all-interfaces. */
|
|
35
|
+
host: '127.0.0.1' | '0.0.0.0'
|
|
36
|
+
/** Listen port; zero requests an OS-assigned port. */
|
|
37
|
+
port: number
|
|
38
|
+
/** Response compression for socket-backed HTTP requests. @default 'none' */
|
|
39
|
+
compression?: 'none' | 'gzip'
|
|
40
|
+
/** Gzip DEFLATE level from 0 through 9. @default 1 */
|
|
41
|
+
compressionLevel?: number
|
|
42
|
+
/** Minimum known response length eligible for gzip; unknown-length streams are eligible. @default 1024 */
|
|
43
|
+
compressionThresholdBytes?: number
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露)。载体本身不拥有 TLS、认证或 Origin 策略,因此绑定到非回环地址会暴露服务器,除非组合层提供这些控制。`compression` 默认为 `none`;随附的 Web 组合选择 gzip level 1 和 1024 字节阈值。随附的 `dsh web` 命令选择 loopback 并拒绝 `--host 0.0.0.0`;其 Connection 插件为每个 Host API route 与 stream 提供 Host/Origin 校验和浏览器会话认证。其他组合自行拥有绑定与路由认证策略。dist 位置是认领席位的前端插件的组装事实。
|
|
48
|
+
|
|
49
|
+
## 服务
|
|
50
|
+
|
|
51
|
+
`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。Gzip 在服务器内部包装符合条件且基于 socket 的响应,因此 route handler 继续直接持有 `ServerResponse`,服务也不新增响应写出 API。已有内容编码、`Cache-Control: no-transform`、范围响应、SSE、ZIP 与打包后的 `.gz` Worker 镜像均保持 identity 响应。`collectIndexInjections()` 经一次 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html)` 把它们渲染进成功的根路径和配置 index 响应,随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口。
|
|
52
|
+
|
|
53
|
+
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。该包从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.zh.md) 中。
|
|
54
|
+
|
|
55
|
+
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
56
|
+
|
|
57
|
+
<a id="cordis-surface"></a>
|
|
58
|
+
|
|
59
|
+
## Cordis API
|
|
60
|
+
|
|
61
|
+
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
|
62
|
+
|
|
63
|
+
<a id="ctxwebserver--webserver"></a>
|
|
64
|
+
|
|
65
|
+
### `ctx.webServer` — `WebServer`
|
|
66
|
+
|
|
67
|
+
The browser HTTP carrier service. Activation listens immediately. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. A listen failure rejects initialization, and the boot process reports the failed fiber.
|
|
68
|
+
|
|
69
|
+
```ts cordis-catalog
|
|
70
|
+
/**
|
|
71
|
+
* Register a named route. Duplicate (kind, path) throws — route patterns are
|
|
72
|
+
* a composition-level contract, so a collision is a misconfiguration.
|
|
73
|
+
* @param route - kind, path, and the owning handler.
|
|
74
|
+
* @returns the disposer removing the route.
|
|
75
|
+
*/
|
|
76
|
+
register(route: WebRoute): () => void
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
|
|
80
|
+
* one socket can have only one protocol owner.
|
|
81
|
+
* @param route - pathname and handler owning negotiation plus socket use.
|
|
82
|
+
* @returns the disposer removing the route.
|
|
83
|
+
*/
|
|
84
|
+
registerUpgrade(route: WebUpgradeRoute): () => void
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Claim the fallback seat: the handler answering every request no named
|
|
88
|
+
* route matches (the SPA dist server in the shipped Web composition). One
|
|
89
|
+
* owner only — a second registration throws, because two fallbacks cannot
|
|
90
|
+
* compose.
|
|
91
|
+
* @param handler - owns the full response lifecycle of unmatched requests.
|
|
92
|
+
* @returns the disposer releasing the seat.
|
|
93
|
+
*/
|
|
94
|
+
registerFallback(handler: WebRoute['handler']): () => void
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a raw-HTML index transform, the escape hatch for markup no
|
|
98
|
+
* {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
|
|
99
|
+
* registration order after rendering the structured rows.
|
|
100
|
+
* @param transform - pure html-to-html function.
|
|
101
|
+
* @returns the disposer removing the transform.
|
|
102
|
+
*/
|
|
103
|
+
tapIndex(transform: (html: string) => string): () => void
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run an index.html body through the registered taps in registration order
|
|
107
|
+
* — called by the fallback owner on every index response it renders.
|
|
108
|
+
* @param html - the raw index.html body.
|
|
109
|
+
* @returns the transformed body.
|
|
110
|
+
*/
|
|
111
|
+
applyIndexTaps(html: string): string
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gather the structured injection table: one `webserver/index-inject` emit,
|
|
115
|
+
* every subscriber pushes its current rows. Fresh per call, so subscribers
|
|
116
|
+
* read live state (module graph, theme preference) at emit time.
|
|
117
|
+
* @returns rows in subscriber activation order.
|
|
118
|
+
*/
|
|
119
|
+
collectIndexInjections(): IndexInjection[]
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render one index.html body: the structured injection table first, then
|
|
123
|
+
* the raw `tapIndex` transforms over the result.
|
|
124
|
+
* @param html - the raw index.html body.
|
|
125
|
+
* @returns the transformed body.
|
|
126
|
+
*/
|
|
127
|
+
renderIndex(html: string): string
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
131
|
+
|
|
132
|
+
<a id="webserver-events"></a>
|
|
133
|
+
|
|
134
|
+
### `webserver/*` events
|
|
135
|
+
|
|
136
|
+
<a id="webserverindex-inject--emit"></a>
|
|
137
|
+
|
|
138
|
+
#### `webserver/index-inject` — emit
|
|
139
|
+
|
|
140
|
+
Collect the structured index injection table. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
|
|
141
|
+
|
|
142
|
+
```ts cordis-catalog
|
|
143
|
+
/**
|
|
144
|
+
* Collect the structured index injection table. Emitted on every index
|
|
145
|
+
* render and every worker boot-payload request; listeners push their
|
|
146
|
+
* current rows, so a row's data is read fresh at emit time.
|
|
147
|
+
* @param table - Mutable row table; listeners append in activation order.
|
|
148
|
+
* @mode emit
|
|
149
|
+
*/
|
|
150
|
+
'webserver/index-inject'(table: IndexInjection[]): void
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
|
|
154
|
+
<!-- END GENERATED cordis-surface -->
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Web Access
|
|
2
|
+
|
|
3
|
+
English | [中文](web.zh.md)
|
|
4
|
+
|
|
5
|
+
The web access seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) that spans **two operations** (search and fetch) on one `ctx.web` service, split across packages: Service Definition ([dsh-web](../../packages/web/web), `ctx.web` + the provider registries), Service Providers ([dsh-web-search-exa](../../packages/web/web-search-exa), [dsh-web-search-perplexity](../../packages/web/web-search-perplexity), [dsh-web-search-deepseek](../../packages/web/web-search-deepseek), [dsh-web-fetch-http](../../packages/web/web-fetch-http)), and Consumer ([dsh-tool-web](../../packages/web/tool-web), the `web_search`/`web_fetch` tool schemas). Web is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A search-provider swap does not change how the model asks for a query, and a fetch-provider swap does not change how the model asks for a URL.
|
|
6
|
+
|
|
7
|
+
Source: [`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts)
|
|
8
|
+
|
|
9
|
+
## Why one capability has two operations
|
|
10
|
+
|
|
11
|
+
Search and fetch share no request schema and no business logic, but they are deliberately one `ctx.web` middle layer: one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" configuration API. The cost is the parallel `searchX`/`fetchX` method pairs on the service; that parallelism is intentional, not a missed extraction. Providers register **capabilities** (a `WebSearchProvider` or `WebFetchProvider`), not tools; the model-facing names, schemas, prompt guidance, and presentation all live in the single `dsh-tool-web` consumer.
|
|
12
|
+
|
|
13
|
+
## Search request and result
|
|
14
|
+
|
|
15
|
+
Each seam request carries exactly one `query`. The `dsh-tool-web` consumer accepts a required `queries` array and fans it out into separate seam requests; a one-item array performs one search. `maxResults` is a consumer-owned bound (`dsh-tool-web`'s `searchMaxResults` config, default `8`) passed through the seam and enforced on the way back — if a provider over-returns, the seam truncates `sources[]` and sets `truncated`.
|
|
16
|
+
|
|
17
|
+
```ts type-equiv
|
|
18
|
+
/**
|
|
19
|
+
* What one search-capable backend is asked to search. Each request carries one
|
|
20
|
+
* query; a consumer may issue several requests. `maxResults` is a
|
|
21
|
+
* `dsh-tool-web`-layer bound passed through unchanged and enforced on the way
|
|
22
|
+
* back by the seam (see {@link WebSearchResult}).
|
|
23
|
+
*/
|
|
24
|
+
interface WebSearchRequest {
|
|
25
|
+
readonly query: string
|
|
26
|
+
/**
|
|
27
|
+
* Upper bound on returned sources; the seam truncates to it. Omitted = no
|
|
28
|
+
* bound. `dsh-tool-web` always sets it. A provider whose API supports a
|
|
29
|
+
* result-count control (Exa's `numResults`) should apply it at the request
|
|
30
|
+
* layer as a cost/latency optimization; the seam enforces the bound
|
|
31
|
+
* regardless.
|
|
32
|
+
*/
|
|
33
|
+
readonly maxResults?: number
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts type-equiv
|
|
38
|
+
/**
|
|
39
|
+
* Normalized search outcome. `content` is optional provider-generated answer
|
|
40
|
+
* text or summary (Exa and DeepSeek return none; Perplexity returns a
|
|
41
|
+
* generated answer).
|
|
42
|
+
* `sources[]` is the portable citation shape. `truncated` is set by the seam
|
|
43
|
+
* when it cut `sources[]` down to `maxResults`.
|
|
44
|
+
*/
|
|
45
|
+
interface WebSearchResult {
|
|
46
|
+
/** Optional provider-generated answer text, search context, or summary. */
|
|
47
|
+
readonly content?: string
|
|
48
|
+
/** Citeable sources, already truncated to the request's `maxResults`. */
|
|
49
|
+
readonly sources: readonly WebSearchSource[]
|
|
50
|
+
/** True when the seam dropped sources to honor `maxResults`. */
|
|
51
|
+
readonly truncated: boolean
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts type-equiv
|
|
56
|
+
/**
|
|
57
|
+
* One citeable source. A source always has a URL; `title`, `snippet`, and
|
|
58
|
+
* `publishedAt` are optional because not every provider returns them — forcing
|
|
59
|
+
* adapters to invent them would make the seam lie (Perplexity citations may be
|
|
60
|
+
* URL-only). `dsh-tool-web` renders `title ?? hostname(url)` for display.
|
|
61
|
+
*/
|
|
62
|
+
interface WebSearchSource {
|
|
63
|
+
readonly url: string
|
|
64
|
+
readonly title?: string
|
|
65
|
+
readonly snippet?: string
|
|
66
|
+
/** Publication/crawl timestamp as a provider-supplied ISO-8601 string. */
|
|
67
|
+
readonly publishedAt?: string
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Fetch request and result
|
|
72
|
+
|
|
73
|
+
```ts type-equiv
|
|
74
|
+
/**
|
|
75
|
+
* What one fetch-capable backend is asked to retrieve. The request deliberately
|
|
76
|
+
* omits timeout, format, prompt, and extraction controls: cancellation is a
|
|
77
|
+
* direct execution argument, while presentation and higher-level LLM concerns
|
|
78
|
+
* belong outside safe retrieval.
|
|
79
|
+
*/
|
|
80
|
+
interface WebFetchRequest {
|
|
81
|
+
readonly url: string
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
HTTP status is part of the fetched resource state, not automatically a failure: a successful network fetch of a `404`/`500` returns a `WebFetchResult` with the status code and a bounded decoded body. `url` is the final URL after allowed redirects. `WebError` is reserved for failures to safely retrieve or represent the resource.
|
|
86
|
+
|
|
87
|
+
```ts type-equiv
|
|
88
|
+
/**
|
|
89
|
+
* Normalized fetch outcome. A successful network fetch of a non-2xx response is
|
|
90
|
+
* a result, not an error: the status code is part of the fetched resource
|
|
91
|
+
* state. {@link WebError} is reserved for failures to safely retrieve or
|
|
92
|
+
* represent the resource.
|
|
93
|
+
*/
|
|
94
|
+
interface WebFetchResult {
|
|
95
|
+
/** The final URL after allowed redirects (the request URL is in the request). */
|
|
96
|
+
readonly url: string
|
|
97
|
+
/** HTTP status code of the fetched response. */
|
|
98
|
+
readonly statusCode: number
|
|
99
|
+
/** Decoded body, classified by content kind. */
|
|
100
|
+
readonly body: WebFetchBody
|
|
101
|
+
/** True when the provider capped the decoded body. */
|
|
102
|
+
readonly truncated: boolean
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```ts type-equiv
|
|
107
|
+
/**
|
|
108
|
+
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
|
|
109
|
+
* `dsh-web`: the provider decodes the kind and `dsh-tool-web` renders it, so a
|
|
110
|
+
* new kind is a coordinated change across known packages, not a plugin
|
|
111
|
+
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
|
|
112
|
+
* so adding a kind breaks compilation at every consumer until handled. Each arm
|
|
113
|
+
* stays its own object literal even where fields coincide, so an arm can gain
|
|
114
|
+
* fields the others lack.
|
|
115
|
+
*/
|
|
116
|
+
type WebFetchBody =
|
|
117
|
+
| { readonly kind: 'html'; readonly content: string }
|
|
118
|
+
| { readonly kind: 'text'; readonly content: string }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Provider availability
|
|
122
|
+
|
|
123
|
+
A provider's `available(): boolean` is a cheap LOCAL check (credential presence, parseable config) and **must not make network calls**. It is an input to execution-time selection, not a health system: `search()`/`fetch()` read it to pick a usable provider, and a selection failure surfaces as the structured `WebError` the caller routes on — which carries the branchable detail (the missing id or ambiguous candidate set) in its code and message.
|
|
124
|
+
|
|
125
|
+
Selection never depends on registration, config, or HMR order: a capability has an explicit provider id (config `searchProvider`/`fetchProvider`, or the matching env var feeding the same field), or auto-selects when exactly one usable provider is registered; multiple usable providers with no configured id is `WEB_PROVIDER_AMBIGUOUS`, not first-wins.
|
|
126
|
+
|
|
127
|
+
## Fetch network policy
|
|
128
|
+
|
|
129
|
+
The shipped Cordis, Code, and Standard presets expose `web_fetch` in every sandbox and approval mode without per-call confirmation. File sandbox presets do not govern Web network access. A deployment that needs confirmation must add a `tools/pre-execute` policy or disable fetch.
|
|
130
|
+
|
|
131
|
+
The HTTP provider resolves each actual request, rejects non-public answers including private IPv4 reached through the active DNS64 prefix, pins the validated address set, and repeats enforcement for each same-origin redirect. A cross-origin redirect requires a new tool call and fresh public-address validation. These checks prevent SSRF access to non-public destinations but do not stop a model from sending data to a public URL.
|
|
132
|
+
|
|
133
|
+
## Errors
|
|
134
|
+
|
|
135
|
+
`WebError extends HarnessError` ([core.md](core.md) error taxonomy) with a `code: string` (open, like every other seam's error — `LlmError`, `SubagentError`), not a closed union: a provider may raise its own codes without editing `dsh-web`, and consumers must tolerate an unknown code. The codes split by owner. Seam-neutral codes are raised by the shared `WebRuntime` contract: `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_CONFIGURED_MISSING`, `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`, `WEB_DUPLICATE_PROVIDER` (a registration-time programming error, the analogue of `LlmRuntime`'s `DUPLICATE_ADAPTER`), `WEB_ABORTED`, and `WEB_PROVIDER_ERROR` (the catch-all for a provider's own failure surfaced through the seam, including network/transport failure — DNS, connection refused, TLS). Fetch-transport codes are owned by the `dsh-web-fetch-http` implementation and a different fetch backend need not raise them: `WEB_INVALID_URL`, `WEB_BLOCKED_URL`, `WEB_REDIRECT_BLOCKED`, `WEB_FETCH_TOO_LARGE`, `WEB_FETCH_TIMEOUT`, `WEB_UNSUPPORTED_CONTENT_TYPE`.
|
|
136
|
+
|
|
137
|
+
## The service
|
|
138
|
+
|
|
139
|
+
`WebRuntime` registers search and fetch providers, rejects duplicate ids with `WEB_DUPLICATE_PROVIDER`, and resolves providers at execution time with structured selection errors. The local fetch backend accepts only HTTP(S), rejects credentials, resolves each hostname once, rejects any answer set containing a non-public IPv4 or IPv6 destination or an active-prefix NAT64 translation to non-public IPv4, pins the request connection to the validated addresses, repeats those checks for every same-origin redirect hop, caps redirects, bytes, characters, and time, and decodes the body; the tool owns presentation.
|
|
140
|
+
|
|
141
|
+
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
142
|
+
|
|
143
|
+
<a id="cordis-surface"></a>
|
|
144
|
+
|
|
145
|
+
## Cordis API
|
|
146
|
+
|
|
147
|
+
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
|
148
|
+
|
|
149
|
+
<a id="ctxweb--webruntime"></a>
|
|
150
|
+
|
|
151
|
+
### `ctx.web` — `WebRuntime`
|
|
152
|
+
|
|
153
|
+
The web access service. Registered as `ctx.web` (one instance per context).
|
|
154
|
+
|
|
155
|
+
Selection semantics (resolved at execution time, never order-dependent):
|
|
156
|
+
|
|
157
|
+
- A configured id that is registered and `available()` → that provider.
|
|
158
|
+
- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
159
|
+
- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
160
|
+
- No id configured, exactly one registered usable provider → that provider.
|
|
161
|
+
- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
162
|
+
- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
163
|
+
|
|
164
|
+
```ts cordis-catalog
|
|
165
|
+
/**
|
|
166
|
+
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
167
|
+
* if its id is already registered for search. Returns a disposer; disposed
|
|
168
|
+
* with the calling fiber.
|
|
169
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
170
|
+
* @returns the disposer that unregisters the provider.
|
|
171
|
+
*/
|
|
172
|
+
registerSearchProvider(provider: WebSearchProvider): () => void
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
176
|
+
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
177
|
+
* with the calling fiber.
|
|
178
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
179
|
+
* @returns the disposer that unregisters the provider.
|
|
180
|
+
*/
|
|
181
|
+
registerFetchProvider(provider: WebFetchProvider): () => void
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Run one search through the selected provider. Resolves the provider at call
|
|
185
|
+
* time with the selection rules above; throws {@link WebError} when the
|
|
186
|
+
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
187
|
+
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
188
|
+
* @param request - the query and optional result limit.
|
|
189
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
190
|
+
* @returns the provider's results, capped to `request.maxResults`.
|
|
191
|
+
*/
|
|
192
|
+
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
196
|
+
* call time with the selection rules above; throws {@link WebError} when the
|
|
197
|
+
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
198
|
+
* @param request - the URL plus retrieval options.
|
|
199
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
200
|
+
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
201
|
+
*/
|
|
202
|
+
async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Source: [`packages/web/web/src/index.ts`](../../packages/web/web/src/index.ts)
|
|
206
|
+
<!-- END GENERATED cordis-surface -->
|