mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-12 04:01:20 +00:00
Merge codex/subprocess-win32-process-primitives into codex/subprocess-native-containment
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/client-modules.md
|
||||
client-modules.md: 52fa4ef73766e8e7e7601e604f78aded9940328f
|
||||
client-modules.zh.md: e830349be77000759684a76b2834478a803e3e11
|
||||
client-modules.md: d939cbbafa186b82d9110a6ccc41e9892b53eb95
|
||||
client-modules.zh.md: 8f407de0bd44ea2cc95b6d29ca7e149648785c0f
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
English | [中文](client-modules.zh.md)
|
||||
|
||||
The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModules` (`ClientModuleRegistry`). It scans the host Loader's entries for packages declaring `dsh.client`, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins/<id>/client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [web-server.md](web-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
|
||||
The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModules` (`ClientModuleRegistry`). It scans the host Loader's entries for packages declaring `dsh.client`, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins/<id>/client.js`, and answers every index-injection collection with the boot manifest rows — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [web-server.md](web-server.md) supplies the prefix route and the `webserver/index-inject` event this service answers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
|
||||
|
||||
Source: [`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
|
||||
|
||||
## The wire
|
||||
|
||||
The graph is the wire single source between the Node and browser halves: the host composes `WebBootEntry` rows from scanned packages, injects the graph as the first script in `<head>` (`window.__DSH_BOOT__`, with `<` escaped so plugin-controlled strings cannot break out of the script element), and the shell parses it before booting anything. A page without a valid manifest cannot boot — the browser-side parser throws loud on a missing or malformed graph.
|
||||
The graph is the wire single source between the Node and browser halves: the host composes `WebBootEntry` rows from scanned packages, publishes the graph as a `global` injection row rendered ahead of later script rows (`globalThis["__DSH_BOOT__"]`, with `<` escaped so plugin-controlled strings cannot break out of the script element), and the shell parses it before booting anything. A page without a valid manifest cannot boot — the browser-side parser throws loud on a missing or malformed graph.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -60,9 +60,9 @@ Scanning is incremental per package; there is no full-rescan code path. Every co
|
||||
|
||||
Package metadata — including the negative "not a client package" verdict — is cached per name and never expires: plugin-set changes take effect on restart. A fiber restart reuses its row and rev untouched; bundle content changes reach the graph only through `rebuilt()`.
|
||||
|
||||
## The bundle route and index tap
|
||||
## The bundle route and index injection
|
||||
|
||||
`GET`/`HEAD /plugins/<id>/client.js` serves the registered bundle from disk with `no-cache` (the rev query, not HTTP caching, anchors consistency); other methods are 405. An unknown id — or a registered row whose bundle is unreadable because it has not been built yet — answers a loud 404 rather than letting the carrier's SPA fallback ship HTML as JavaScript. The index tap injects the current graph on every index render, so a reload always boots against the live composition.
|
||||
`GET`/`HEAD /plugins/<id>/client.js` serves the registered bundle from disk with `no-cache` (the rev query, not HTTP caching, anchors consistency); other methods are 405. An unknown id — or a registered row whose bundle is unreadable because it has not been built yet — answers a loud 404 rather than letting the carrier's SPA fallback ship HTML as JavaScript. The injection rows carry the current graph on every index render, so a reload always boots against the live composition.
|
||||
|
||||
## The service
|
||||
|
||||
@@ -82,7 +82,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.clientModules` — `ClientModuleRegistry`
|
||||
|
||||
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
|
||||
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -122,5 +122,5 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
|
||||
onGraphChanged(listener: () => void): () => void
|
||||
```
|
||||
|
||||
Source: [`packages/client/modules/src/index.ts:295`](../../packages/client/modules/src/index.ts)
|
||||
Source: [`packages/client/modules/src/index.ts:282`](../../packages/client/modules/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](client-modules.md) | 中文
|
||||
|
||||
Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModules`(`ClientModuleRegistry`)形式提供。它扫描宿主 Loader 的 entry,找出声明了 `dsh.client` 的包,组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins/<id>/client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[web-server.md](web-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
|
||||
Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModules`(`ClientModuleRegistry`)形式提供。它扫描宿主 Loader 的 entry,找出声明了 `dsh.client` 的包,组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins/<id>/client.js` 提供各个 bundle,并以启动 manifest(元数据清单)行回应每次 index 注入收集——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[web-server.md](web-server.md) 所述的载体提供本服务注册的前缀路由与其回应的 `webserver/index-inject` 事件。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
|
||||
|
||||
源码:[`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
|
||||
|
||||
## wire
|
||||
|
||||
图是 Node 半与浏览器半之间协议层的唯一真源:宿主从扫描到的包组合出 `WebBootEntry` 行,把图作为 `<head>` 中的第一个脚本注入(`window.__DSH_BOOT__`,其中 `<` 已转义,插件可控的字符串因此无法逃出 script 元素),壳则在启动任何东西之前先解析它。没有有效 manifest 的页面无法启动——浏览器侧的解析器在图缺失或畸形时大声抛错。
|
||||
图是 Node 半与浏览器半之间协议层的唯一真源:宿主从扫描到的包组合出 `WebBootEntry` 行,把图发布为一条 `global` 注入行、渲染在后续 script 行之前(`globalThis["__DSH_BOOT__"]`,其中 `<` 已转义,插件可控的字符串因此无法逃出 script 元素),壳则在启动任何东西之前先解析它。没有有效 manifest 的页面无法启动——浏览器侧的解析器在图缺失或畸形时大声抛错。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -60,9 +60,9 @@ interface WebBootGraph {
|
||||
|
||||
包元数据——包括「非 client 包」这一否定结论——按名缓存且永不过期:插件集合的变更在重启后生效。fiber 重启原样复用其行与 rev;bundle 内容变更只经 `rebuilt()` 到达图。
|
||||
|
||||
## bundle 路由与 index 转换
|
||||
## bundle 路由与 index 注入
|
||||
|
||||
`GET`/`HEAD /plugins/<id>/client.js` 以 `no-cache` 从磁盘提供已注册的 bundle(锚定一致性的是 rev 查询参数,而非 HTTP 缓存);其他方法返回 405。未知 id——或已注册、但 bundle 因尚未构建而不可读的行——回应一个大声的 404,而不是让载体的 SPA 回退把 HTML 当作 JavaScript 发出。index 转换在每次 index 渲染时注入当前图,因此刷新页面总是针对实时组合启动。
|
||||
`GET`/`HEAD /plugins/<id>/client.js` 以 `no-cache` 从磁盘提供已注册的 bundle(锚定一致性的是 rev 查询参数,而非 HTTP 缓存);其他方法返回 405。未知 id——或已注册、但 bundle 因尚未构建而不可读的行——回应一个大声的 404,而不是让载体的 SPA 回退把 HTML 当作 JavaScript 发出。注入行在每次 index 渲染时携带当前图,因此刷新页面总是针对实时组合启动。
|
||||
|
||||
## 服务
|
||||
|
||||
@@ -82,7 +82,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.clientModules` — `ClientModuleRegistry`
|
||||
|
||||
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
|
||||
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -122,5 +122,5 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
|
||||
onGraphChanged(listener: () => void): () => void
|
||||
```
|
||||
|
||||
Source: [`packages/client/modules/src/index.ts:295`](../../packages/client/modules/src/index.ts)
|
||||
Source: [`packages/client/modules/src/index.ts:282`](../../packages/client/modules/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/credentials.md
|
||||
credentials.md: fe480c4311dd5f7e0b1f26996ad53d5f0cd79e7d
|
||||
credentials.zh.md: 1d8712f8b01a6d6b92e2036172c151d22728f2a4
|
||||
credentials.md: 459511965d796cd34d1a817110016bcb922d2988
|
||||
credentials.zh.md: d833f7897008dd9f9c062d8fbefc1e4ba9c0d959
|
||||
|
||||
@@ -47,7 +47,7 @@ interface CredentialInfo {
|
||||
|
||||
## Change commits
|
||||
|
||||
`credentials/updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge.
|
||||
`credentials/reference-updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -57,11 +57,76 @@ interface CredentialInfo {
|
||||
|
||||
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`) — this section is byte-identical in both language sides of the page. 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).
|
||||
|
||||
<a id="ctxauthorization--authorizationservice"></a>
|
||||
|
||||
### `ctx.authorization` — `AuthorizationService`
|
||||
|
||||
`ctx.authorization`: a registry of credential-obtaining flows, one attempt at a time per key.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Offer a way to obtain one credential. One flow per key: two plugins
|
||||
* claiming the same key would each write a record in their own format, and
|
||||
* whichever ran last would leave the other reading a payload it cannot parse.
|
||||
*
|
||||
* @param flow - the key it writes, its label, its methods, and its runner.
|
||||
* @returns Disposer that withdraws this flow.
|
||||
* @throws {AuthorizationError} code `DUPLICATE_FLOW` when the key is already claimed.
|
||||
*/
|
||||
registerFlow(flow: AuthorizationFlow): () => void
|
||||
|
||||
/**
|
||||
* Every registered flow, for a surface listing what can be authorized.
|
||||
* @returns one entry per flow, in registration order.
|
||||
*/
|
||||
list(): readonly AuthorizationEntry[]
|
||||
|
||||
/**
|
||||
* One registered flow.
|
||||
* @param key - the credential record to ask about.
|
||||
* @returns the entry, or undefined when no flow claims that key.
|
||||
*/
|
||||
describe(key: CredentialKey): AuthorizationEntry | undefined
|
||||
|
||||
/**
|
||||
* Withdraw the attempt running for a key, if any. Separate from the
|
||||
* request's own signal because a request/response transport answers a Cancel
|
||||
* button on a second call, with no handle on the first one's signal.
|
||||
* @param key - the credential record whose attempt should stop.
|
||||
*/
|
||||
cancel(key: CredentialKey): void
|
||||
|
||||
/**
|
||||
* Run one attempt to authorize a key, and report how it ended.
|
||||
*
|
||||
* One attempt per key at a time. A second caller is refused rather than
|
||||
* joined: the two would be prompting different humans through the same flow,
|
||||
* and the second would answer questions the first was asked.
|
||||
*
|
||||
* @param request - the key, the method, the surface, and the cancel signal.
|
||||
* @returns `authorized` once the flow's record is committed during this
|
||||
* attempt and observed, or `cancelled` when the human declined or the
|
||||
* caller withdrew.
|
||||
* @throws {AuthorizationError} code `NO_FLOW` when nothing claims the key,
|
||||
* `UNKNOWN_METHOD` when the named method is not one the flow offers,
|
||||
* `ALREADY_IN_FLIGHT` when an attempt is already running for the key, or
|
||||
* `NOT_COMMITTED` when the flow resolved without committing a record
|
||||
* during the attempt.
|
||||
*/
|
||||
async begin(request: AuthorizationRequest): Promise<AuthorizationOutcome>
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/authorization/src/index.ts:182`](../../packages/credentials/authorization/src/index.ts)
|
||||
|
||||
<a id="ctxcredentials--credentialprovider-abstract-seam"></a>
|
||||
|
||||
### `ctx.credentials` — `CredentialProvider` (abstract seam)
|
||||
|
||||
Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
|
||||
Abstract credential service over two key spaces that answer two questions.
|
||||
|
||||
A CredentialRef answers "what is behind this environment-variable name", layered over the process environment, the provider-managed store, and `.env` files. One seam-wide rule binds that half: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
|
||||
|
||||
A CredentialKey answers "what credential does this plugin hold for this id". Nothing can layer here — an authorization grant has no environment to be read from — so presence of the record is the whole fact, and modifyRecord is the only write path because a correct write depends on the current value (a token refresh is read-decide-replace under one lock).
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -99,17 +164,107 @@ abstract set(ref: CredentialRef, value: string): Promise<void>
|
||||
* @param ref - the reference to remove.
|
||||
*/
|
||||
abstract unset(ref: CredentialRef): Promise<void>
|
||||
|
||||
/**
|
||||
* Read one stored record. The value is returned as its owner wrote it; a
|
||||
* {@link GrantRecord} payload is not interpreted on the way out.
|
||||
* @param key - the record to read.
|
||||
* @returns the record, or `undefined` while none is stored.
|
||||
*/
|
||||
abstract readRecord(key: CredentialKey): Promise<CredentialRecord | undefined>
|
||||
|
||||
/**
|
||||
* Describe one record for configuration surfaces without exposing its value.
|
||||
* @param key - the record to describe.
|
||||
* @returns presence, discriminant, and writability.
|
||||
*/
|
||||
abstract describeRecord(key: CredentialKey): Promise<CredentialRecordInfo>
|
||||
|
||||
/**
|
||||
* Enumerate every stored record's address and tag. Unlike the reference
|
||||
* half, which has no enumeration because configuration surfaces learn which
|
||||
* references exist from settings schemas, records have no such discovery
|
||||
* path: a surface that cannot list them cannot show what a user is
|
||||
* authorized for, nor find an orphan left by an uninstalled plugin.
|
||||
* @returns every stored record, values excluded.
|
||||
*/
|
||||
abstract listRecords(): Promise<readonly CredentialRecordEntry[]>
|
||||
|
||||
/**
|
||||
* Serialized read-modify-write over one record — the only write path.
|
||||
* `mutate` sees the record as it stands at the moment the write is
|
||||
* exclusive, and returning `undefined` leaves the entry untouched. Exclusion
|
||||
* holds across processes where the backing store supports it, which is what
|
||||
* makes a token refresh safe: two processes rotating one refresh token
|
||||
* concurrently would otherwise lose whichever wrote first.
|
||||
* @param key - the record to modify.
|
||||
* @param mutate - receives the current record and returns its replacement, or `undefined` to leave it.
|
||||
* @returns the record after the write, or the current one when `mutate` declined.
|
||||
*/
|
||||
abstract modifyRecord( key: CredentialKey, mutate: (current: CredentialRecord | undefined) => Promise<CredentialRecord | undefined>, ): Promise<CredentialRecord | undefined>
|
||||
|
||||
/**
|
||||
* Remove one record; removing an absent record is a no-op.
|
||||
* @param key - the record to remove.
|
||||
*/
|
||||
abstract deleteRecord(key: CredentialKey): Promise<void>
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts:60`](../../packages/credentials/credentials/src/index.ts)
|
||||
Source: [`packages/credentials/credentials/src/index.ts:177`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
<a id="authorization-events"></a>
|
||||
|
||||
### `authorization/*` events
|
||||
|
||||
<a id="authorizationsettled--emit"></a>
|
||||
|
||||
#### `authorization/settled` — emit
|
||||
|
||||
One authorization attempt has finished and released its key. Fires for every terminal outcome, failures included, so a surface watching a key it did not start (a second browser tab) learns the attempt is over.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* One authorization attempt has finished and released its key. Fires for
|
||||
* every terminal outcome, failures included, so a surface watching a key it
|
||||
* did not start (a second browser tab) learns the attempt is over.
|
||||
* @mode emit
|
||||
* @param key - the credential record the finished attempt was authorizing.
|
||||
* @param settlement - how it ended, including the `failed` case its caller sees as a thrown error.
|
||||
*/
|
||||
'authorization/settled'(key: CredentialKey, settlement: AuthorizationSettlement): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/authorization/src/index.ts:57`](../../packages/credentials/authorization/src/index.ts)
|
||||
|
||||
<a id="credentials-events"></a>
|
||||
|
||||
### `credentials/*` events
|
||||
|
||||
<a id="credentialsupdated--emit"></a>
|
||||
<a id="credentialsrecord-updated--emit"></a>
|
||||
|
||||
#### `credentials/updated` — emit
|
||||
#### `credentials/record-updated` — emit
|
||||
|
||||
Committed change to a stored credential record: a `modifyRecord` that wrote, a `deleteRecord` that removed, or an external edit observed in storage. Separate from `credentials/reference-updated` because the two key grammars are disjoint — a listener that received both on one event could not tell which space a subject belongs to. Listener failures are contained on the same terms as `credentials/reference-updated`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Committed change to a stored credential record: a `modifyRecord` that
|
||||
* wrote, a `deleteRecord` that removed, or an external edit observed in
|
||||
* storage. Separate from `credentials/reference-updated` because the two key
|
||||
* grammars are disjoint — a listener that received both on one event could
|
||||
* not tell which space a subject belongs to. Listener failures are
|
||||
* contained on the same terms as `credentials/reference-updated`.
|
||||
* @param key - the record whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
'credentials/record-updated'(key: CredentialKey): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/types.ts:87`](../../packages/credentials/credentials/src/types.ts)
|
||||
|
||||
<a id="credentialsreference-updated--emit"></a>
|
||||
|
||||
#### `credentials/reference-updated` — emit
|
||||
|
||||
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
|
||||
|
||||
@@ -126,8 +281,8 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o
|
||||
* @param ref - the reference whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
'credentials/updated'(ref: CredentialRef): void
|
||||
'credentials/reference-updated'(ref: CredentialRef): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/types.ts:29`](../../packages/credentials/credentials/src/types.ts)
|
||||
Source: [`packages/credentials/credentials/src/types.ts:75`](../../packages/credentials/credentials/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -47,7 +47,7 @@ interface CredentialInfo {
|
||||
|
||||
## 已提交的变更
|
||||
|
||||
`credentials/updated (ref)` 在提供方管理的来源发生已提交变更后发出——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。
|
||||
`credentials/reference-updated (ref)` 在提供方管理的来源发生已提交变更后发出——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -57,11 +57,76 @@ interface CredentialInfo {
|
||||
|
||||
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`) — this section is byte-identical in both language sides of the page. 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).
|
||||
|
||||
<a id="ctxauthorization--authorizationservice"></a>
|
||||
|
||||
### `ctx.authorization` — `AuthorizationService`
|
||||
|
||||
`ctx.authorization`: a registry of credential-obtaining flows, one attempt at a time per key.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Offer a way to obtain one credential. One flow per key: two plugins
|
||||
* claiming the same key would each write a record in their own format, and
|
||||
* whichever ran last would leave the other reading a payload it cannot parse.
|
||||
*
|
||||
* @param flow - the key it writes, its label, its methods, and its runner.
|
||||
* @returns Disposer that withdraws this flow.
|
||||
* @throws {AuthorizationError} code `DUPLICATE_FLOW` when the key is already claimed.
|
||||
*/
|
||||
registerFlow(flow: AuthorizationFlow): () => void
|
||||
|
||||
/**
|
||||
* Every registered flow, for a surface listing what can be authorized.
|
||||
* @returns one entry per flow, in registration order.
|
||||
*/
|
||||
list(): readonly AuthorizationEntry[]
|
||||
|
||||
/**
|
||||
* One registered flow.
|
||||
* @param key - the credential record to ask about.
|
||||
* @returns the entry, or undefined when no flow claims that key.
|
||||
*/
|
||||
describe(key: CredentialKey): AuthorizationEntry | undefined
|
||||
|
||||
/**
|
||||
* Withdraw the attempt running for a key, if any. Separate from the
|
||||
* request's own signal because a request/response transport answers a Cancel
|
||||
* button on a second call, with no handle on the first one's signal.
|
||||
* @param key - the credential record whose attempt should stop.
|
||||
*/
|
||||
cancel(key: CredentialKey): void
|
||||
|
||||
/**
|
||||
* Run one attempt to authorize a key, and report how it ended.
|
||||
*
|
||||
* One attempt per key at a time. A second caller is refused rather than
|
||||
* joined: the two would be prompting different humans through the same flow,
|
||||
* and the second would answer questions the first was asked.
|
||||
*
|
||||
* @param request - the key, the method, the surface, and the cancel signal.
|
||||
* @returns `authorized` once the flow's record is committed during this
|
||||
* attempt and observed, or `cancelled` when the human declined or the
|
||||
* caller withdrew.
|
||||
* @throws {AuthorizationError} code `NO_FLOW` when nothing claims the key,
|
||||
* `UNKNOWN_METHOD` when the named method is not one the flow offers,
|
||||
* `ALREADY_IN_FLIGHT` when an attempt is already running for the key, or
|
||||
* `NOT_COMMITTED` when the flow resolved without committing a record
|
||||
* during the attempt.
|
||||
*/
|
||||
async begin(request: AuthorizationRequest): Promise<AuthorizationOutcome>
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/authorization/src/index.ts:182`](../../packages/credentials/authorization/src/index.ts)
|
||||
|
||||
<a id="ctxcredentials--credentialprovider-abstract-seam"></a>
|
||||
|
||||
### `ctx.credentials` — `CredentialProvider` (abstract seam)
|
||||
|
||||
Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
|
||||
Abstract credential service over two key spaces that answer two questions.
|
||||
|
||||
A CredentialRef answers "what is behind this environment-variable name", layered over the process environment, the provider-managed store, and `.env` files. One seam-wide rule binds that half: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
|
||||
|
||||
A CredentialKey answers "what credential does this plugin hold for this id". Nothing can layer here — an authorization grant has no environment to be read from — so presence of the record is the whole fact, and modifyRecord is the only write path because a correct write depends on the current value (a token refresh is read-decide-replace under one lock).
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -99,17 +164,107 @@ abstract set(ref: CredentialRef, value: string): Promise<void>
|
||||
* @param ref - the reference to remove.
|
||||
*/
|
||||
abstract unset(ref: CredentialRef): Promise<void>
|
||||
|
||||
/**
|
||||
* Read one stored record. The value is returned as its owner wrote it; a
|
||||
* {@link GrantRecord} payload is not interpreted on the way out.
|
||||
* @param key - the record to read.
|
||||
* @returns the record, or `undefined` while none is stored.
|
||||
*/
|
||||
abstract readRecord(key: CredentialKey): Promise<CredentialRecord | undefined>
|
||||
|
||||
/**
|
||||
* Describe one record for configuration surfaces without exposing its value.
|
||||
* @param key - the record to describe.
|
||||
* @returns presence, discriminant, and writability.
|
||||
*/
|
||||
abstract describeRecord(key: CredentialKey): Promise<CredentialRecordInfo>
|
||||
|
||||
/**
|
||||
* Enumerate every stored record's address and tag. Unlike the reference
|
||||
* half, which has no enumeration because configuration surfaces learn which
|
||||
* references exist from settings schemas, records have no such discovery
|
||||
* path: a surface that cannot list them cannot show what a user is
|
||||
* authorized for, nor find an orphan left by an uninstalled plugin.
|
||||
* @returns every stored record, values excluded.
|
||||
*/
|
||||
abstract listRecords(): Promise<readonly CredentialRecordEntry[]>
|
||||
|
||||
/**
|
||||
* Serialized read-modify-write over one record — the only write path.
|
||||
* `mutate` sees the record as it stands at the moment the write is
|
||||
* exclusive, and returning `undefined` leaves the entry untouched. Exclusion
|
||||
* holds across processes where the backing store supports it, which is what
|
||||
* makes a token refresh safe: two processes rotating one refresh token
|
||||
* concurrently would otherwise lose whichever wrote first.
|
||||
* @param key - the record to modify.
|
||||
* @param mutate - receives the current record and returns its replacement, or `undefined` to leave it.
|
||||
* @returns the record after the write, or the current one when `mutate` declined.
|
||||
*/
|
||||
abstract modifyRecord( key: CredentialKey, mutate: (current: CredentialRecord | undefined) => Promise<CredentialRecord | undefined>, ): Promise<CredentialRecord | undefined>
|
||||
|
||||
/**
|
||||
* Remove one record; removing an absent record is a no-op.
|
||||
* @param key - the record to remove.
|
||||
*/
|
||||
abstract deleteRecord(key: CredentialKey): Promise<void>
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts:60`](../../packages/credentials/credentials/src/index.ts)
|
||||
Source: [`packages/credentials/credentials/src/index.ts:177`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
<a id="authorization-events"></a>
|
||||
|
||||
### `authorization/*` events
|
||||
|
||||
<a id="authorizationsettled--emit"></a>
|
||||
|
||||
#### `authorization/settled` — emit
|
||||
|
||||
One authorization attempt has finished and released its key. Fires for every terminal outcome, failures included, so a surface watching a key it did not start (a second browser tab) learns the attempt is over.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* One authorization attempt has finished and released its key. Fires for
|
||||
* every terminal outcome, failures included, so a surface watching a key it
|
||||
* did not start (a second browser tab) learns the attempt is over.
|
||||
* @mode emit
|
||||
* @param key - the credential record the finished attempt was authorizing.
|
||||
* @param settlement - how it ended, including the `failed` case its caller sees as a thrown error.
|
||||
*/
|
||||
'authorization/settled'(key: CredentialKey, settlement: AuthorizationSettlement): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/authorization/src/index.ts:57`](../../packages/credentials/authorization/src/index.ts)
|
||||
|
||||
<a id="credentials-events"></a>
|
||||
|
||||
### `credentials/*` events
|
||||
|
||||
<a id="credentialsupdated--emit"></a>
|
||||
<a id="credentialsrecord-updated--emit"></a>
|
||||
|
||||
#### `credentials/updated` — emit
|
||||
#### `credentials/record-updated` — emit
|
||||
|
||||
Committed change to a stored credential record: a `modifyRecord` that wrote, a `deleteRecord` that removed, or an external edit observed in storage. Separate from `credentials/reference-updated` because the two key grammars are disjoint — a listener that received both on one event could not tell which space a subject belongs to. Listener failures are contained on the same terms as `credentials/reference-updated`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Committed change to a stored credential record: a `modifyRecord` that
|
||||
* wrote, a `deleteRecord` that removed, or an external edit observed in
|
||||
* storage. Separate from `credentials/reference-updated` because the two key
|
||||
* grammars are disjoint — a listener that received both on one event could
|
||||
* not tell which space a subject belongs to. Listener failures are
|
||||
* contained on the same terms as `credentials/reference-updated`.
|
||||
* @param key - the record whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
'credentials/record-updated'(key: CredentialKey): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/types.ts:87`](../../packages/credentials/credentials/src/types.ts)
|
||||
|
||||
<a id="credentialsreference-updated--emit"></a>
|
||||
|
||||
#### `credentials/reference-updated` — emit
|
||||
|
||||
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
|
||||
|
||||
@@ -126,8 +281,8 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o
|
||||
* @param ref - the reference whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
'credentials/updated'(ref: CredentialRef): void
|
||||
'credentials/reference-updated'(ref: CredentialRef): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/credentials/src/types.ts:29`](../../packages/credentials/credentials/src/types.ts)
|
||||
Source: [`packages/credentials/credentials/src/types.ts:75`](../../packages/credentials/credentials/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/web-server.md
|
||||
web-server.md: 414863d88cdc34163882cdc3dfe8dc7c1471ac77
|
||||
web-server.zh.md: d756192ce733a3fe79c9eaf44e5d556c4bac90a8
|
||||
web-server.md: 6a33eb2bacb7dd9e85a13d7f2ef9cf870b5c44d9
|
||||
web-server.zh.md: 5dcc4b6e002c4bdeb0d839e67a750f67f40eb0a6
|
||||
|
||||
@@ -42,7 +42,7 @@ interface Config {
|
||||
|
||||
## The service
|
||||
|
||||
`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. `tapIndex(transform)` adds a pure html-to-html transform applied to every index response — `/` and each SPA fallback — in registration order; [dsh-client-modules](../../packages/client/modules) uses it to inject the boot manifest. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0.
|
||||
`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. `collectIndexInjections()` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit, and `renderIndex(html)` renders them into every index response — `/` and each SPA fallback — 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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -88,8 +88,9 @@ registerUpgrade(route: WebUpgradeRoute): () => void
|
||||
registerFallback(handler: WebRoute['handler']): () => void
|
||||
|
||||
/**
|
||||
* Register an index.html transform, applied by the fallback owner to every
|
||||
* index response ({@link applyIndexTaps}) in registration order.
|
||||
* Register a raw-HTML index transform, the escape hatch for markup no
|
||||
* {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
|
||||
* registration order after rendering the structured rows.
|
||||
* @param transform - pure html-to-html function.
|
||||
* @returns the disposer removing the transform.
|
||||
*/
|
||||
@@ -102,7 +103,46 @@ tapIndex(transform: (html: string) => string): () => void
|
||||
* @returns the transformed body.
|
||||
*/
|
||||
applyIndexTaps(html: string): string
|
||||
|
||||
/**
|
||||
* Gather the structured injection table: one `webserver/index-inject` emit,
|
||||
* every subscriber pushes its current rows. Fresh per call, so subscribers
|
||||
* read live state (module graph, theme preference) at emit time.
|
||||
* @returns rows in subscriber activation order.
|
||||
*/
|
||||
collectIndexInjections(): IndexInjection[]
|
||||
|
||||
/**
|
||||
* Render one index.html body: the structured injection table first, then
|
||||
* the raw `tapIndex` transforms over the result.
|
||||
* @param html - the raw index.html body.
|
||||
* @returns the transformed body.
|
||||
*/
|
||||
renderIndex(html: string): string
|
||||
```
|
||||
|
||||
Source: [`packages/host/webserver/src/index.ts:59`](../../packages/host/webserver/src/index.ts)
|
||||
Source: [`packages/host/webserver/src/index.ts:73`](../../packages/host/webserver/src/index.ts)
|
||||
|
||||
<a id="webserver-events"></a>
|
||||
|
||||
### `webserver/*` events
|
||||
|
||||
<a id="webserverindex-inject--emit"></a>
|
||||
|
||||
#### `webserver/index-inject` — emit
|
||||
|
||||
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.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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.
|
||||
* @param table - Mutable row table; listeners append in activation order.
|
||||
* @mode emit
|
||||
*/
|
||||
'webserver/index-inject'(table: IndexInjection[]): void
|
||||
```
|
||||
|
||||
Source: [`packages/host/webserver/src/index.ts:34`](../../packages/host/webserver/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -42,7 +42,7 @@ interface Config {
|
||||
|
||||
## 服务
|
||||
|
||||
`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。`tapIndex(transform)` 添加一个纯 HTML 到 HTML 转换函数,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest(元数据清单)。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口。
|
||||
`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。`collectIndexInjections()` 经一次 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html)` 把它们渲染进每个 index 响应(`/` 和每次 SPA 回退),随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口。
|
||||
|
||||
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。该包从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.md) 中。
|
||||
|
||||
@@ -88,8 +88,9 @@ registerUpgrade(route: WebUpgradeRoute): () => void
|
||||
registerFallback(handler: WebRoute['handler']): () => void
|
||||
|
||||
/**
|
||||
* Register an index.html transform, applied by the fallback owner to every
|
||||
* index response ({@link applyIndexTaps}) in registration order.
|
||||
* Register a raw-HTML index transform, the escape hatch for markup no
|
||||
* {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
|
||||
* registration order after rendering the structured rows.
|
||||
* @param transform - pure html-to-html function.
|
||||
* @returns the disposer removing the transform.
|
||||
*/
|
||||
@@ -102,7 +103,46 @@ tapIndex(transform: (html: string) => string): () => void
|
||||
* @returns the transformed body.
|
||||
*/
|
||||
applyIndexTaps(html: string): string
|
||||
|
||||
/**
|
||||
* Gather the structured injection table: one `webserver/index-inject` emit,
|
||||
* every subscriber pushes its current rows. Fresh per call, so subscribers
|
||||
* read live state (module graph, theme preference) at emit time.
|
||||
* @returns rows in subscriber activation order.
|
||||
*/
|
||||
collectIndexInjections(): IndexInjection[]
|
||||
|
||||
/**
|
||||
* Render one index.html body: the structured injection table first, then
|
||||
* the raw `tapIndex` transforms over the result.
|
||||
* @param html - the raw index.html body.
|
||||
* @returns the transformed body.
|
||||
*/
|
||||
renderIndex(html: string): string
|
||||
```
|
||||
|
||||
Source: [`packages/host/webserver/src/index.ts:59`](../../packages/host/webserver/src/index.ts)
|
||||
Source: [`packages/host/webserver/src/index.ts:73`](../../packages/host/webserver/src/index.ts)
|
||||
|
||||
<a id="webserver-events"></a>
|
||||
|
||||
### `webserver/*` events
|
||||
|
||||
<a id="webserverindex-inject--emit"></a>
|
||||
|
||||
#### `webserver/index-inject` — emit
|
||||
|
||||
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.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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.
|
||||
* @param table - Mutable row table; listeners append in activation order.
|
||||
* @mode emit
|
||||
*/
|
||||
'webserver/index-inject'(table: IndexInjection[]): void
|
||||
```
|
||||
|
||||
Source: [`packages/host/webserver/src/index.ts:34`](../../packages/host/webserver/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
Reference in New Issue
Block a user