feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand

Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.

- Extract the `Branded<B>` primitive into a new standalone type-only package
  `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
  dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
  dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
  dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
  generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
  the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
  the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
  SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
  that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
  agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
  at the config boundary and the inner create()/resume casts disappear (only the
  genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
  Map keys and public params/exports (SessionStore, AgentRegistry + factory
  options, the ACP session-id surface + ToolPresenter CallId map, the
  persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
  the Branded type-equiv at dsh-brand, fix stale param types in the session/
  agent/bash READMEs, regenerate the cordis catalog + module graph.

Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
This commit is contained in:
Tianyi Cui
2026-06-21 07:19:59 +08:00
parent 24168aee70
commit d6a2ab30c8
75 files changed
+644 -445

No files matched your search

+6 -5
View File
@@ -15,8 +15,9 @@
*/
import { Context, Service } from 'cordis'
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskListener, BashTaskRead } from './types.ts'
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskId, BashTaskListener, BashTaskRead, OwnerToken } from './types.ts'
export { BashTaskId, OwnerToken } from './types.ts'
export type {
BashExecRequest,
BashExecSpec,
@@ -86,7 +87,7 @@ export abstract class BashExecutor extends Service {
abstract start(spec: BashExecSpec): BashTask
/** Look up a background task by id. */
abstract get(id: string): BashTask | undefined
abstract get(id: BashTaskId): BashTask | undefined
/**
* The opaque OWNER token recorded for a background task at {@link start}
@@ -101,19 +102,19 @@ export abstract class BashExecutor extends Service {
* Storing ownership in the executor (disposed with ITS fiber) — not in the
* tool plugin — is what makes ownership survive a `tool-bash` HMR reload.
*/
abstract ownerOf(id: string): string | undefined
abstract ownerOf(id: BashTaskId): OwnerToken | undefined
/** All tracked background tasks (insertion order). */
abstract list(): BashTask[]
/** Read output produced since the previous read. Throws for unknown ids. */
abstract readOutput(id: string): BashTaskRead
abstract readOutput(id: BashTaskId): BashTaskRead
/**
* Kill a running background task. Returns false when it had already
* finished (no-op). Throws for unknown ids.
*/
abstract kill(id: string): boolean
abstract kill(id: BashTaskId): boolean
/**
* Register a background-task completion listener (disposed with the
+28 -3
View File
@@ -6,6 +6,31 @@
* @module dsh-bash/types
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
/** Identifies one background task within an executor (generated `bash-N`). */
export type BashTaskId = Branded<'BashTaskId'>
/** Brand a string as a {@link BashTaskId}. */
export function BashTaskId(id: string): BashTaskId {
return id as BashTaskId
}
/**
* A background task's opaque isolation key — the CONSUMER's owner identity, not
* the bash seam's. The executor stores and returns it verbatim and never
* interprets it; the access policy lives in the consumer (`dsh-tool-bash`),
* which is the single boundary that casts its own id vocabulary into one. A
* DISTINCT brand (not a `SessionId` alias) keeps the seam decoupled — a
* sandboxed/remote executor inherits no session dependency.
*/
export type OwnerToken = Branded<'OwnerToken'>
/** Brand a string as an {@link OwnerToken}. */
export function OwnerToken(id: string): OwnerToken {
return id as OwnerToken
}
/**
* A caller's execution REQUEST: `workdir` and `timeoutMs` are optional and
* filled by {@link BashExecutor.resolve} from the implementation's config.
@@ -28,7 +53,7 @@ export interface BashExecRequest {
* seam — that is the consumer's job). Absent for foreground runs and for an
* ownerless background start (a non-agent caller).
*/
owner?: string | undefined
owner?: OwnerToken | undefined
}
/**
@@ -53,7 +78,7 @@ export interface BashExecSpec {
* silently-absent property that yields an unowned (cross-session-readable)
* task. `start()` stores it; `run()` (foreground) ignores it.
*/
owner: string | undefined
owner: OwnerToken | undefined
}
/** One captured stream: the (possibly truncated) text plus recovery info. */
@@ -87,7 +112,7 @@ export type BashTaskStatus = 'running' | 'completed' | 'killed'
/** A tracked background task handle. */
export interface BashTask {
readonly id: string
readonly id: BashTaskId
readonly command: string
status: BashTaskStatus
/** Exit code once finished (null = killed by signal / still running). */