docs(credentials): record the third-review contracts across READMEs, catalogs, and a new Agent Note

Both provider READMEs state what actually holds: credentials-local now
documents the physical-line editor, the read-modify-write under the
writer lock, and a Security boundary section saying plainly that the file
mode stops other OS users and not the model. sandbox-policy documents
readDenyPaths and its per-backend enforcement. The llm READMEs carry the
registration handle, pi-ai's credential-miss semantics, and DeepSeek's
same-generation snapshot; app-boot and the CLI README stop describing
$DSH_HOME/.env as an environment layer.

A new Agent Note records the round (and the prior seam note cross-links
it); the sandbox and core catalog pages gain readDenyPaths and
AdapterRegistrationHandle with their manifest entries. The headless
missing-credential snapshot re-records for the reworded guidance, pi-ai
gains the Loader-composition guard its twin already had, and the
deliberate provider symmetry is marked for the clone detector.
This commit is contained in:
Yichen Jiang
2026-07-30 16:37:28 +08:00
parent 9626c15c6b
commit e7894f4152
31 changed files with 212 additions and 54 deletions

View File

@@ -405,8 +405,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
summary: 'The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.',
methods: [
{
signature: 'registerAdapter(providers: string[], adapter: LlmAdapter): () => void',
jsDoc: '/**\n * Register an adapter for the given provider routes. Throws `LlmError` with code\n * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).\n * Disposed with the fiber.\n * @param providers - every provider route this adapter should serve.\n * @param adapter - the adapter that streams calls for those providers.\n * @returns the disposer that unregisters all of them.\n */',
signature: 'registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle',
jsDoc: '/**\n * Register an adapter for the given provider routes. Throws `LlmError` with code\n * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).\n * Disposed with the fiber.\n * @param providers - every provider route this adapter should serve.\n * @param adapter - the adapter that streams calls for those providers.\n * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.\n */',
},
{
signature: 'listProviders(): LlmProviderInfo[]',
@@ -1297,7 +1297,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
name: 'credentials/updated',
mode: 'emit',
signature: '\'credentials/updated\'(ref: CredentialRef): void',
jsDoc: '/**\n * Committed change to a provider-managed credential source: a `set`, an\n * `unset`, or an external edit observed in storage. Ambient\n * process-environment changes are not observable and never emit.\n * @param ref - the reference whose stored value changed.\n * @mode emit\n */',
jsDoc: '/**\n * Committed change to a provider-managed credential source: a `set`, an\n * `unset`, or an external edit observed in storage. Ambient\n * process-environment changes are not observable and never emit. Listener\n * failures are contained and logged — a sync throw and an async rejection\n * alike — without changing the committed operation\'s outcome, except\n * `INVARIANT`-coded failures, which rethrow after every listener ran;\n * that rethrow reaches the emitter only from synchronous listeners, so\n * invariant checks on this event must not be async functions.\n * @param ref - the reference whose stored value changed.\n * @mode emit\n */',
summary: 'Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage.',
},
{
@@ -1521,6 +1521,10 @@ export const EVENT_API: readonly EventApiEntry[] = [
/** Shapes of every exported type the SERVICE_API signatures reference (transitively), sorted by name. */
export const TYPE_API: readonly TypeApiEntry[] = [
{
name: 'AdapterRegistrationHandle',
declaration: 'export interface AdapterRegistrationHandle {\n (): void;\n replace(providers: string[]): void;\n}',
},
{
name: 'Agent',
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly acceptsNextStep: boolean;\n readonly ctx: Context;\n send(message: UserMessage, options: SendOptions): void;\n updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n}',
@@ -2207,7 +2211,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SandboxExecutionPolicy',
declaration: 'export interface SandboxExecutionPolicy {\n mode: SandboxMode;\n workspaceRoot: string;\n}',
declaration: 'export interface SandboxExecutionPolicy {\n mode: SandboxMode;\n workspaceRoot: string;\n readDenyPaths?: readonly string[];\n}',
},
{
name: 'SandboxMode',

View File

@@ -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 packages/credentials/credentials-local/README.md
README.md: 277c7db02836819e34a5c2db8aaf542c8eeec162
README.zh.md: af1b840142d214b8cd8cf59690cb5773fae2e867
README.md: 2288d6d7133a7f356823e3e4f28746cfd28b2597
README.zh.md: 959322c9ec670ed76b89f1f3a19246191b3ec02c

View File

@@ -32,12 +32,12 @@ dotenv 格式,用 `dotenv` 解析;写回用物理行级编辑器,保留一
## 安全边界
文档位于 `0700` 目录下、权限 `0600`,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,因此在出厂默认的 `danger-full-access` 下,它们读这个文件与读该用户拥有的任何其他文件毫无二致。有两件事收窄了这一点:
文档在 `0700` 目录下以 `0600` 权限存放,这挡得住其他 OS 用户,**挡不住**模型。工具进程(bash、文件系统工具)以同一用户身份运行,因此在出厂默认的 `danger-full-access` 下,它们读这个文件与读该用户拥有的任何其他文件毫无二致。有两件事收窄了这一点:
- **约束型沙箱模式**会专门拒绝凭据文档:[`dsh-sandbox-policy`](../../sandbox/sandbox-policy/README.md) 把 `readDenyPaths` 默认为 `$DSH_HOME/.env`,Seatbelt 与 bwrap 后端会执行它(Landlock 无法从自己的 `/` 读授权中扣除,只能报 `partial`)。这条拒绝点名的是该文件而非整个 home,因此模型对自己会话日志的既定访问不受影响。
- **受限沙箱模式**会专门拒绝凭据文档:[`dsh-sandbox-policy`](../../sandbox/sandbox-policy/README.md) 把 `readDenyPaths` 默认为 `$DSH_HOME/.env`,Seatbelt 与 bwrap 后端会执行它(Landlock 无法从自己的 `/` 读授权中扣除,只能报 `partial`)。这条拒绝点名的是该文件而非整个 home,因此模型对自己会话日志的既定访问不受影响。
- harness 绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境(见 [app-boot 的个人配置](../../ui/app-boot/README.md#personal-config))。
这两者都不能让未受约束的 agent 变得安全。必须让提供方密钥远离自身 agent 的部署应当运行约束型模式;OS 钥匙串 provider——一个模型的进程根本读不到的存储——才是延后的答案,它应当作为平级包与本 provider 并列。
这两者都不能让未受限的 agent 变得安全。必须让提供方密钥远离自身 agent 的部署应当运行受限模式;OS 钥匙串 provider——一个模型的进程根本读不到的存储——才是延后的答案,它应当作为平级包与本 provider 并列。
## Model Experience
@@ -51,7 +51,7 @@ dotenv 格式,用 `dotenv` 解析;写回用物理行级编辑器,保留一
- **多行条目拒绝 `set`/`unset`**——行编辑器不改写会被它破坏的条目;`describe` 把它们报为 `writable: false`,编辑必须直接落到文件上。
- **同一引用的并发写入是后写胜出**——写锁加读-改-写让并发写入者不会丢掉彼此的条目,但两个写入者编辑同一个引用时仍以较后的写入为准;没有修订检查。
- **同 UID 进程可以读取该文档**——见[安全边界](#security-boundary):只有约束型沙箱模式会拒绝它,OS 钥匙串 provider 仍是延后项。
- **同 UID 进程可以读取该文档**——见[安全边界](#security-boundary):只有受限沙箱模式会拒绝它,OS 钥匙串 provider 仍是延后项。
- **无法表示的值响亮失败**——控制字符,或同时混用两种引号又含反斜杠的值,无法在 dotenv 行格式中往返。
- **环境变化不可见**——每次解析实时读取 `process.env`,但那里的变化不可能发出事件。
- **原子但不保证崩溃持久**——继承自 `dsh-atomic-write`;存储在启动时重新读取。

View File

@@ -302,6 +302,11 @@ export class CredentialsLocal extends Credentials {
await this.write(ref, undefined)
}
/* jscpd:ignore-start -- the operation-chain and reload lifecycle is the same
reviewed contract as settings-local, deliberately mirrored (prefer symmetry
for parallel values); the two providers own different documents and
failure policies, so extracting the shape would couple their teardown
semantics across packages for a handful of lines. */
/** Queue one exclusive document operation behind every earlier one. */
private enqueue<T>(operation: () => Promise<T>): Promise<T> {
const task = this.operations.then(operation)
@@ -319,6 +324,7 @@ export class CredentialsLocal extends Credentials {
this.ctx.logger.error(error)
})
}
/* jscpd:ignore-end */
/** Queue one line edit; entry checks reject early, the queue re-judges them at run time. */
private async write(ref: CredentialRef, value: string | undefined): Promise<void> {
@@ -390,6 +396,9 @@ export class CredentialsLocal extends Credentials {
this.values = new Map(Object.entries(parse(text)))
}
/* jscpd:ignore-start -- same deliberate mirror of settings-local's reload and
reconcile policy: warn-and-keep on a reload, throw on a write, invariant
failures propagate. */
/**
* Re-read the document after a watcher event. Unchanged content (including
* this provider's own writes) is a no-op; an unreadable document keeps the
@@ -430,6 +439,7 @@ export class CredentialsLocal extends Credentials {
this.values = next
for (const ref of changed) this.notifyUpdated(ref)
}
/* jscpd:ignore-end */
/** Seam-addressable entries whose effective (non-empty) value changed. */
private changedRefs(prev: Map<string, string>, next: Map<string, string>): CredentialRef[] {

View File

@@ -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 packages/llm/llm-deepseek/README.md
README.md: 88f4fd7c017a5dbb070bdaf8ee47bb5610b23303
README.zh.md: 386c695766a68c9054472bd5c9b9deb6746c52e6
README.md: ab44b61e300ca65cc4dd3507ad7262cd08edcfce
README.zh.md: 4ecaf361fdb396f9f8079476240b5e9353a73f5e

View File

@@ -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 packages/llm/llm-pi-ai/README.md
README.md: fb8145d58a7c74c70498468044282c740460a947
README.zh.md: 0d0e2152d27447705cdf19f5b36069314ff05b4d
README.md: 0099c9acd39cd2d471936505726d68423f351c76
README.zh.md: 7cb4f5fcbc1c7a67b77d690031cc7d553569433f

View File

@@ -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 packages/llm/llm/README.md
README.md: d343449d1530bf70a3a8c57f883894e29c42d18f
README.zh.md: 4dc4a0ca06378116d05fdb4b9b048738930511fd
README.md: 5b0c1b2dcafeefaad25f1714e4a1783430370118
README.zh.md: 5f5c8142ec829e8ca8cfd40e6caa341ae0a33c7d

View File

@@ -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 packages/sandbox/sandbox-policy/README.md
README.md: dca54330bc888af9ecac21aa92019d8a2b0140bd
README.zh.md: a201d48c81f563fc3d85495e964bb67432517a3c
README.md: 297dd7d5210bb30963a162c6a55a598c6d522aaf
README.zh.md: 1de92eb81409a7fabb25de94eb5372f0f16afb6f

View File

@@ -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 packages/ui/app-boot/README.md
README.md: 0282d3e9559d55c3fe5b07df133747750c06ebad
README.zh.md: b7121bbd288cd6e3f9ef2301de6018ceb380eb06
README.md: 47c5de35e6151b82f8d99c06618c42dfabe59f5e
README.zh.md: 9878567464b46865f9320582359f7baa0c97f30c

View File

@@ -26,8 +26,8 @@
开发者的机器本地偏好位于所有仓库之外的 Harness home 中(默认 `~/.dsh`,可由 `$DSH_HOME` 覆盖;统一由根级 [`resolveDshHome`](../../util/paths/README.md) 解析),并由 `dsh` CLI(命令行界面)的 TUI 界面([`apps/cli`](../../../apps/cli/README.md))使用;demo bin 会原样启动仓库中提交的树。这里有两个可选文件:
- **`.env`**:在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境中的值 > 项目 `.env` > 个人 `.env`。
- **`config.yaml`**:在发布的默认配置上应用 Loader overlay patch,语义与 include 条目的 `patches` 相同(以仓库提交的 Code Mode overlay 为模板):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值,因此个人 `apiKey` 可以引用个人 `.env`。如果 patch 指定的条目 id 不在已启动树中,Loader 会发出警告并跳过。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用 overlay,请使用 `[]` 或删除该文件。
- **`.env`**:[`dsh-credentials-local`](../../credentials/credentials-local/README.md) 的凭据存储,只由该 provider 读取。没有任何表层会把它提升进 `process.env`:那样做会让每个已存密钥在下次运行时看起来都像只读的启动时覆盖,从而阻断从 TUI 与 Web 页面轮换密钥。环境层次由环境中的值与调用目录的 `.env` 构成(由 bin 加载;`process.loadEnvFile` 从不覆盖已有值),没有凭据 provider 的组合仍然只从这两者解析密钥。
- **`config.yaml`**:在发布的默认配置上应用 Loader overlay patch,语义与 include 条目的 `patches` 相同(以仓库提交的 Code Mode overlay 为模板):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值。如果 patch 指定的条目 id 不在已启动树中,Loader 会发出警告并跳过。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用 overlay,请使用 `[]` 或删除该文件。
子进程测试 launcher 会把 `DSH_HOME` 指向逐测试隔离的目录,确保开发者的个人 overlay 不会泄漏到 fixture(测试前置数据)中。