Merge remote-tracking branch 'origin/master' into feat/loader-entry-disabled-interpolation

This commit is contained in:
Huanqi Cao
2026-08-11 13:40:24 +08:00
535 changed files with 12687 additions and 1999 deletions
+31 -2
View File
@@ -113,20 +113,42 @@ describe('E2B e2e workflow', () => {
})
describe('Issue lifecycle workflow', () => {
it('uses review signals instead of rerunning when a draft becomes ready', () => {
it('uses explicit review handoff events without rerunning when a draft becomes ready', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
const policyPullRequest = workflowEvent(policy, 'pull_request')
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
expect(lifecyclePullRequest.types).toContain('review_requested')
expect(lifecycleReview.types).toContain('submitted')
expect(lifecycleReview.types).toEqual(['submitted'])
expect(lifecycleJob.if).toBe(
"${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }}",
)
expect(policyPullRequest.types).toContain('ready_for_review')
})
})
describe('Git hooks', () => {
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
const lefthook = loadWorkflow('lefthook.yml')
for (const hookName of ['pre-commit', 'pre-merge-commit']) {
const hook = lefthook[hookName]
if (!isRecord(hook) || !Array.isArray(hook.jobs)) {
throw new TypeError(`lefthook must define ${hookName} jobs`)
}
const pairing: unknown = hook.jobs.find(
(job: unknown) => isRecord(job) && job.name === 'translation pairing (staged records)',
)
expect(pairing).toMatchObject({ exclude: ['.agents/notes/archived/**'] })
}
})
})
function loadWorkflow(path: string): Record<string, unknown> {
const workflow: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
if (!isRecord(workflow)) throw new TypeError(`${path} must define a workflow`)
@@ -140,6 +162,13 @@ function workflowEvent(workflow: Record<string, unknown>, event: string): Record
return workflow.on[event]
}
function workflowJob(workflow: Record<string, unknown>, job: string): Record<string, unknown> {
if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs[job])) {
throw new TypeError(`workflow must define the ${job} job`)
}
return workflow.jobs[job]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
+22
View File
@@ -63,6 +63,7 @@ export const SERVICE_PAGE: Record<string, string> = {
httpServer: 'http-server.md',
invariants: 'invariants.md',
llm: 'llm-streaming.md',
messageFeedback: 'feedback.md',
permission: 'permission.md',
planMode: 'plan.md',
pty: 'pty.md',
@@ -229,6 +230,25 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ResolvedRetryPolicy: 'llm-streaming.md',
Message: 'llm-streaming.md',
MessageSource: 'llm-streaming.md',
MessageFeedbackDeleteRequest: 'feedback.md',
MessageFeedbackDeleteResult: 'feedback.md',
MessageFeedbackDeleteValue: 'feedback.md',
MessageFeedbackFailure: 'feedback.md',
MessageFeedbackItem: 'feedback.md',
MessageFeedbackListRequest: 'feedback.md',
MessageFeedbackListResult: 'feedback.md',
MessageFeedbackListValue: 'feedback.md',
MessageFeedbackNoteBlank: 'feedback.md',
MessageFeedbackNoteTooLarge: 'feedback.md',
MessageFeedbackPutRequest: 'feedback.md',
MessageFeedbackPutResult: 'feedback.md',
MessageFeedbackRating: 'feedback.md',
MessageFeedbackRejected: 'feedback.md',
MessageFeedbackSessionNotFound: 'feedback.md',
MessageFeedbackSuccess: 'feedback.md',
MessageFeedbackTargetNotFound: 'feedback.md',
MessageFeedbackVersion: 'feedback.md',
MessageFeedbackVersionConflict: 'feedback.md',
UserMessage: 'session.md',
PreStepDecision: 'core.md',
PreStepContext: 'core.md',
@@ -384,6 +404,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
TaskRead: 'tasks.md',
TaskSnapshot: 'tasks.md',
TaskStart: 'tasks.md',
TasksChangedListener: 'tasks.md',
TokenMeasurement: 'token-meter.md',
CodeDispatchLog: 'tools.md',
PostToolDecision: 'tools.md',
@@ -462,6 +483,7 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
'Promise',
'Record',
'Readonly',
'Uint8Array',
])
/** Project types deliberately documented outside the subsystems catalog. */
+10 -3
View File
@@ -135,7 +135,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants', 'message-feedback'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
@@ -167,7 +167,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite', 'message-feedback'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
@@ -211,9 +211,16 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'storage-domain',
title: 'Domain data facility',
mode: 'core',
consumers: ['workspace'],
consumers: ['workspace', 'message-feedback'],
note: 'Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state.',
},
{
key: 'messageFeedback',
pkg: 'message-feedback',
title: 'Lifecycle-bound message feedback',
mode: 'core',
note: 'Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry.',
},
{
key: 'workspace',
pkg: 'workspace',
+67 -18
View File
@@ -13,6 +13,7 @@ import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/persistence-catalog.md'
const OUT_RUNTIME_TYPES = 'packages/core/session/src/known-event-types.ts'
/** The fenced-block info string for generated declaration blocks (skipped by
* doc-typecheck, since their imported types are not standalone-compilable). */
@@ -359,7 +360,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
'',
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).',
'',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'',
'## Event envelope',
'',
@@ -382,31 +383,79 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
return lines.join('\n')
}
/** CLI entry: default writes the catalog, `--check` fails if the committed copy
/**
* Render the runtime known-vocabulary module: every event type the packages in
* this repo can write, as a generated `ReadonlySet` the read path checks
* unknown-type refusal against (`SessionEvent.ignorable` contract).
*/
export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string {
const names = [...new Set(events.map(e => e.name))].sort()
return [
'/**',
' * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run',
' * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by',
' * `pnpm run verify-persistence-catalog`, part of `doc-sync`).',
' * @module @deepseek-ai/dsh-session/known-event-types',
' */',
'',
'/**',
' * Every `SessionEventMap` member declared in this repository — the event',
' * vocabulary this build understands. The persistence read path refuses to',
' * interpret a log containing a type outside this set unless the event',
' * carries the envelope\'s `ignorable` marker (see `SessionEvent.ignorable`',
' * in `./types.ts`): such a log was likely written by a newer harness, and',
' * silently skipping a required event would reconstruct a wrong session.',
' * Downstream (out-of-repo) plugin events are outside this list by',
' * construction; a registration surface for them is deferred until such a',
' * consumer exists.',
' */',
'export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([',
...names.map(name => ` '${name}',`),
'])',
'',
].join('\n')
}
/** One generated artifact: repo-relative target and its freshly-rendered content. */
interface GeneratedArtifact {
readonly out: string
readonly content: string
}
/** CLI entry: default writes the artifacts, `--check` fails if a committed copy
* is stale. Guarded behind an entry-point check so importing this module for
* tests neither regenerates the committed file nor calls process.exit. */
* tests neither regenerates the committed files nor calls process.exit. */
function main(): void {
const content = render(annotateSurface(collectLogEvents(), collectSurfaceEventTypes()), collectEventEnvelopeTypes())
const events = annotateSurface(collectLogEvents(), collectSurfaceEventTypes())
const artifacts: GeneratedArtifact[] = [
{ out: OUT, content: render(events, collectEventEnvelopeTypes()) },
{ out: OUT_RUNTIME_TYPES, content: renderKnownEventTypes(events) },
]
if (process.argv.includes('--check')) {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, OUT), 'utf8')
} catch {
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
committed = null
}
if (committed === content) {
console.log(`gen-persistence-catalog: ${OUT} is up to date.`)
const stale = artifacts.filter((artifact) => {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, artifact.out), 'utf8')
} catch {
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
committed = null
}
return committed !== artifact.content
})
if (stale.length === 0) {
console.log(`gen-persistence-catalog: ${artifacts.map(a => a.out).join(', ')} are up to date.`)
process.exit(0)
}
console.error(`gen-persistence-catalog: ${OUT} is stale. Run \`pnpm run gen-persistence-catalog\` and commit ${OUT}.`)
console.error(`gen-persistence-catalog: ${stale.map(a => a.out).join(', ')} stale. Run \`pnpm run gen-persistence-catalog\` and commit the result.`)
process.exit(1)
}
writeFileSync(resolve(root, OUT), content)
console.log(`gen-persistence-catalog: wrote ${OUT}.`)
for (const artifact of artifacts) {
writeFileSync(resolve(root, artifact.out), artifact.content)
console.log(`gen-persistence-catalog: wrote ${artifact.out}.`)
}
}
// Run only when invoked as a script, not when imported by a test.
+4 -4
View File
@@ -27,8 +27,8 @@ const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'p
* root manifest), test infrastructure, the documentation site, the runnable
* demo leaves, and the native launcher's build workspace. A runtime
* declaration by anything outside these areas is a disclosure-relevant
* runtime dependency, because `scripts/install.sh` installs the repository
* itself and any plugin package can be mounted from a user's `cordis.yml`.
* runtime dependency because any plugin package can be mounted from a user's
* `cordis.yml`.
*/
const DEV_ONLY_AREAS = [
'package.json',
@@ -369,7 +369,7 @@ function collectNpmDeps(): ExternalDep[] {
*/
export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<string>): Map<string, boolean> {
const tiers = new Map<string, boolean>()
// `tsx` is runtime by fiat: `bin/dsh` execs the CLI through its ESM hook.
// `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook.
tiers.set('tsx', true)
for (const [path, manifest] of manifests) {
const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area))
@@ -707,7 +707,7 @@ ${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.u
## Runtime npm dependencies
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
${renderNpmTable(runtimeDeps)}
+31 -4
View File
@@ -24,6 +24,8 @@ import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
import WebService from '@deepseek-ai/dsh-web'
@@ -60,6 +62,29 @@ import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
/** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */
class CatalogAttachmentStore extends AttachmentStore {
readonly imageLimits: ImageAttachmentLimits = Object.freeze({
maxImageBytes: 1,
maxImagesPerMessage: 1,
maxMessageImageBytes: 1,
maxImagePixels: 1,
mediaTypes: Object.freeze(['image/png'] as const),
})
override validateImage(_input: SaveImageAttachment): Promise<void> {
return Promise.reject(new Error('gen-tool-catalog: attachment validation is unreachable during schema harvest'))
}
override saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
return Promise.reject(new Error('gen-tool-catalog: attachment writes are unreachable during schema harvest'))
}
override readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
return Promise.reject(new Error('gen-tool-catalog: attachment reads are unreachable during schema harvest'))
}
}
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
@@ -265,16 +290,18 @@ const TOOL_PACKAGES: ToolPackage[] = [
pkg: '@deepseek-ai/dsh-tool-fs',
dir: 'tool-fs',
source: 'packages/fs/tool-fs/src/index.ts',
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt'],
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful mutation', 'tool/result'],
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt', 'ctx.attachments (read_image registration)', 'ctx.llm + an image-capable route (read_image execution)'],
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful file operation', 'durable attachment (read_image)', 'tool/result'],
async mount(ctx) {
// The tool needs `fs`; the bare provider is sufficient because policy
// changes behavior, not schema shape.
// changes behavior, not schema shape. The catalog seam marker opts into
// the attachments-conditional read_image schema without attachment I/O.
await ctx.plugin(LocalFileSystem)
await ctx.plugin(CatalogAttachmentStore)
await ctx.plugin(ToolFs)
},
note:
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.',
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs-search',
-425
View File
@@ -1,425 +0,0 @@
#!/bin/sh
# dsh one-line installer.
#
# curl -fsSL https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/scripts/install.sh | sh
#
# It clones the harness under ~/.dsh/source (the master clone at
# ~/.dsh/source/master), adds a per-install staging worktree at
# ~/.dsh/source/staging-<timestamp> on branch dsh-staging/<timestamp>, checks
# host dependencies (git, Node, pnpm) and offers to install a missing pnpm, runs
# `pnpm install`, points the stable `~/.dsh/source/current` symlink
# at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`,
# records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot,
# builds the repository artifacts, and launches the Web UI. Keeping every
# checkout under ~/.dsh/source keeps successive
# upgrades in one place instead of scattered sibling clones, and lets staging
# worktrees share the master clone's object store. The PATH symlink resolves through
# `current`, so an upgrade repoints one stable symlink instead of relinking PATH:
# the `dsh` on PATH never moves and can never dangle.
#
# When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
# than `curl ... | sh`) it never clones and never touches that working tree;
# DSH_REF is ignored. Instead it *adopts* the checkout: `git rev-parse
# --git-common-dir` resolves the repository behind it (for a linked worktree that
# is the real clone, not the worktree), and a fresh staging worktree branched
# from the checkout's HEAD lands in the source container beside `current`. The
# container owns staging worktrees and `current`; the clone is discovered, not
# owned, so an arbitrary clone (~/src/dsh) and a managed one converge on one
# layout and stay upgradable. Adoption carries committed work only: the staging
# worktree branches from HEAD, so uncommitted changes stay in the checkout.
# Setting DSH_SOURCE to a different directory opts back into the normal
# clone/worktree path.
#
# Adopting an arbitrary clone leaves the container not self-contained: its
# staging worktrees hold an absolute gitdir pointer into that clone, so deleting
# it breaks them. `git worktree list` in that clone is the record of which
# worktrees depend on it.
#
# When run through `curl | sh` the script text arrives on stdin, so every
# prompt and the final launch read the controlling terminal (/dev/tty) directly;
# with no terminal the script prints the manual next steps instead.
#
# Overridable via environment:
# DSH_REF branch or tag to clone/checkout (default: master)
# DSH_REPO clone URL (default: the GitHub repo)
# DSH_SOURCE source container directory (default: ~/.dsh/source)
# DSH_MASTER master clone directory (default: $DSH_SOURCE/master)
# DSH_CURRENT stable symlink to the active worktree (default: $DSH_SOURCE/current)
# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh)
set -eu
DSH_REF=${DSH_REF:-master}
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-ai/deepseek-harness-sdk.git}
# DSH_SOURCE is the staging-worktree container and the default home of `current`.
# DSH_MASTER names the main clone: clone mode defaults it inside DSH_SOURCE,
# while adoption discovers an existing clone anywhere on disk. Remember whether
# DSH_SOURCE was explicit so a different path selects clone mode.
if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi
DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source}
DSH_MASTER=${DSH_MASTER:-$DSH_SOURCE/master}
# The stable symlink the PATH launcher resolves through: PATH/dsh ->
# current/bin/dsh -> <staging>/bin/dsh. Installs and upgrades repoint `current`;
# the PATH target remains current/bin/dsh.
DSH_CURRENT=${DSH_CURRENT:-$DSH_SOURCE/current}
DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin}
# One UTC basic timestamp names this install's staging branch and worktree.
DSH_STAMP=$(date -u +%Y%m%dT%H%M%SZ)
DSH_STAGING_BRANCH=dsh-staging/$DSH_STAMP
DSH_STAGING=$DSH_SOURCE/staging-$DSH_STAMP
# --- path helpers ---------------------------------------------------------------
# Every path comparison below runs on physical paths. Git always reports resolved
# paths, so comparing one against an unresolved path disagrees whenever a symlink
# sits anywhere above the checkout — a symlinked home directory is enough, and
# macOS reaches every mktemp path that way through /var -> private/var. The
# mismatch silently misclassifies an existing managed install as a foreign clone
# and builds a second container beside the real one.
# `git rev-parse --path-format=absolute` would do this, but it needs git 2.31+.
#
# A not-yet-created directory (the container on a fresh install) has no physical
# path. Falling back here rather than at each call site keeps every caller a
# plain assignment, so no site can compare against an empty path by forgetting
# its own fallback.
resolve_dir() { CDPATH= cd -- "$1" 2>/dev/null && pwd -P || printf '%s\n' "$1"; }
# --- in-repo detection ---------------------------------------------------------
# Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell
# name and no file path resolves; running a checked-out copy (`sh
# scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose
# parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present),
# this is in-repo mode: never clone, never touch that working tree. An explicit
# DSH_SOURCE pointing elsewhere opts back into the clone/worktree path.
IN_REPO=0
DSH_CHECKOUT=''
if [ -f "$0" ]; then
_self_dir=$(resolve_dir "$(dirname -- "$0")")
if [ -n "$_self_dir" ]; then
# Physical without its own resolve_dir: dirname is textual, so trimming a
# resolved path leaves one. The comparison below depends on that.
_repo_root=$(dirname -- "$_self_dir")
if [ "$(basename -- "$_self_dir")" = scripts ] \
&& [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then
# Compare the explicit DSH_SOURCE physically: an unresolved but equivalent
# path must still count as "the caller meant this checkout".
_src_resolved=$(resolve_dir "$DSH_SOURCE")
if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$_src_resolved" = "$_repo_root" ]; then
IN_REPO=1
DSH_CHECKOUT=$_repo_root
fi
fi
fi
fi
# --- terminal-aware prompting --------------------------------------------------
# stdin is the piped script, so read the controlling terminal for input.
if { true </dev/tty; } 2>/dev/null; then
HAS_TTY=1
# Restore terminal echo on exit or interrupt: ask_secret disables echo between
# its stty toggles, and dash (a common `sh`) does not run an EXIT trap when the
# shell is killed by a signal, so the fatal signals need their own handler. A
# successful run ends in exec, which replaces this process and drops the traps.
trap 'stty echo </dev/tty 2>/dev/null || true' EXIT
trap 'stty echo </dev/tty 2>/dev/null || true; exit 130' INT TERM HUP
else
HAS_TTY=0
fi
# Colour only when writing to a terminal.
if [ -t 1 ]; then
B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m')
GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m')
else
B=''; DIM=''; RED=''; GRN=''; YEL=''; RST=''
fi
info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$1"; }
step() { printf '\n%s==>%s %s%s%s\n' "$GRN" "$RST" "$B" "$1" "$RST"; }
warn() { printf '%s warn%s %s\n' "$YEL" "$RST" "$1" >&2; }
die() { printf '%serror%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; }
# ask PROMPT [DEFAULT] -> answer on stdout (plain-text line).
ask() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
IFS= read -r _ans </dev/tty || _ans=''
[ -n "$_ans" ] || _ans=${2:-}
printf '%s' "$_ans"
}
# ask_secret PROMPT -> answer on stdout, with terminal echo suppressed.
ask_secret() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
stty -echo </dev/tty 2>/dev/null || true
IFS= read -r _sec </dev/tty || _sec=''
stty echo </dev/tty 2>/dev/null || true
printf '\n' >/dev/tty
printf '%s' "$_sec"
}
# confirm PROMPT [Y] -> exit 0 on yes. Default is no unless second arg is "Y".
confirm() {
_def=${2:-N}
if [ "$HAS_TTY" != 1 ]; then
[ "$_def" = Y ] # non-interactive: take the default
return
fi
if [ "$_def" = Y ]; then _hint='[Y/n]'; else _hint='[y/N]'; fi
printf '%s%s%s %s ' "$B" "$1" "$RST" "$_hint" >/dev/tty
IFS= read -r _r </dev/tty || _r=''
[ -n "$_r" ] || _r=$_def
case "$_r" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
}
printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
if [ "$IN_REPO" = 1 ]; then
printf '%scheckout %s%s\n' "$DIM" "$DSH_CHECKOUT" "$RST"
else
printf '%smaster %s @ %s%s\n' "$DIM" "$DSH_MASTER" "$DSH_REF" "$RST"
printf '%sstaging %s%s\n' "$DIM" "$DSH_STAGING" "$RST"
printf '%scurrent %s%s\n' "$DIM" "$DSH_CURRENT" "$RST"
fi
# --- 1. dependency check -------------------------------------------------------
step "Checking dependencies"
command -v git >/dev/null 2>&1 || die "git is required but not found. Install git, then re-run."
info "git ... ok"
# Node ^22.19.0 || >=24.0.0 (see the root package.json "engines" field).
node_ok() {
command -v node >/dev/null 2>&1 || return 1
_v=$(node -v 2>/dev/null) || return 1
_v=${_v#v}
_major=${_v%%.*}
_rest=${_v#*.}
_minor=${_rest%%.*}
case "$_major" in ''|*[!0-9]*) return 1 ;; esac
case "$_minor" in ''|*[!0-9]*) _minor=0 ;; esac
[ "$_major" -ge 24 ] && return 0
[ "$_major" -eq 22 ] && [ "$_minor" -ge 19 ] && return 0
return 1
}
if node_ok; then
info "node $(node -v) ... ok"
else
if command -v node >/dev/null 2>&1; then
die "Node $(node -v) is unsupported. dsh needs ^22.19.0 || >=24.0.0 — upgrade Node, then re-run."
fi
die "Node is required but not found. Install Node ^22.19.0 || >=24, then re-run."
fi
# pnpm is the only dependency we offer to install for you.
if command -v pnpm >/dev/null 2>&1; then
info "pnpm $(pnpm --version) ... ok"
else
warn "pnpm is not installed."
if confirm "Install pnpm now?" Y; then
if command -v corepack >/dev/null 2>&1 && corepack enable pnpm >/dev/null 2>&1; then
info "enabled pnpm via corepack"
elif command -v npm >/dev/null 2>&1 && npm install -g pnpm >/dev/null 2>&1; then
info "installed pnpm via npm"
else
die "could not install pnpm automatically. Install it (https://pnpm.io/installation), then re-run."
fi
command -v pnpm >/dev/null 2>&1 || die "pnpm still not on PATH after install. Open a new shell, then re-run."
else
die "pnpm is required. Install it (https://pnpm.io/installation), then re-run."
fi
fi
# --- 2. resolve the repository and lay out the staging worktree ---------------
# The source container owns staging worktrees and `current`; the repository is
# *discovered*, not owned. A curl install discovers it by cloning to $DSH_MASTER;
# in-repo adoption discovers it from the checkout. Both then run one shared
# worktree/exclude/lock path, so an arbitrary clone and a managed install
# converge on the same layout.
#
# REPO_COMMON is the shared git directory every worktree of the repository
# points at; REPO_ROOT is the working tree that owns it (the master clone).
REPO_COMMON=''
REPO_ROOT=''
if [ "$IN_REPO" = 1 ]; then
step "Using existing checkout at $DSH_CHECKOUT"
info "running from inside the repo — never cloning, and DSH_REF is ignored"
# Resolve the repository behind the checkout. --git-common-dir returns the
# SHARED git dir, so a linked worktree resolves to the real clone rather than
# itself; it is relative for a plain clone, so anchor it before resolving.
# Require the resolved git dir to exist: resolve_dir echoes its argument back
# for a missing path, so test the directory rather than the returned string.
if _common=$(git -C "$DSH_CHECKOUT" rev-parse --git-common-dir 2>/dev/null) && [ -n "$_common" ]; then
case "$_common" in /*) ;; *) _common=$DSH_CHECKOUT/$_common ;; esac
[ -d "$_common" ] && REPO_COMMON=$(resolve_dir "$_common")
fi
[ -n "$REPO_COMMON" ] || die "$DSH_CHECKOUT is not a git repository — cannot adopt it."
REPO_ROOT=$(dirname -- "$REPO_COMMON")
# Reuse the container when the repository already lives inside it (the normal
# managed install re-running its own script); otherwise treat that clone as
# its own master and keep worktrees in the default container.
_src_resolved=$(resolve_dir "$DSH_SOURCE")
case "$REPO_ROOT/" in
"$_src_resolved"/*) info "repository $REPO_ROOT is already inside $DSH_SOURCE" ;;
*) info "adopting clone $REPO_ROOT as its own master" ;;
esac
DSH_MASTER=$REPO_ROOT
else
step "Fetching source into $DSH_MASTER"
if [ -d "$DSH_MASTER/.git" ]; then
info "existing master clone found — updating"
git -C "$DSH_MASTER" fetch origin "$DSH_REF"
# Reset the master checkout to the freshly fetched tip. FETCH_HEAD (not
# origin/<ref>) so this resolves for a tag as well as a branch, and -B makes
# the re-run idempotent whether or not DSH_REF changed since the last install.
git -C "$DSH_MASTER" checkout -q -B "$DSH_REF" FETCH_HEAD
else
mkdir -p "$DSH_SOURCE"
git clone --branch "$DSH_REF" "$DSH_REPO" "$DSH_MASTER"
fi
# Physical on both branches: REPO_ROOT is compared against resolved paths
# below, and REPO_COMMON stays symmetric with it so neither can be read as
# carrying a different kind of path.
REPO_COMMON=$(resolve_dir "$DSH_MASTER/.git")
REPO_ROOT=$(resolve_dir "$DSH_MASTER")
fi
step "Adding staging worktree at $DSH_STAGING"
[ -e "$DSH_STAGING" ] && die "staging path $DSH_STAGING already exists — remove it or set DSH_SOURCE elsewhere, then re-run."
mkdir -p "$DSH_SOURCE"
# The staging worktree owns the branch dsh runs from; the repository stays as
# the fetch/upgrade base and is never a launcher target. A clone install
# branches from the ref it just fetched; adoption branches from the checkout's
# HEAD so the contributor's committed work is what runs.
if [ "$IN_REPO" = 1 ]; then
git -C "$DSH_CHECKOUT" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD
else
git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" FETCH_HEAD 2>/dev/null \
|| git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD
fi
# Exclude the per-worktree merge lock in the shared git dir's info/exclude,
# which every linked worktree inherits.
_exclude="$REPO_COMMON/info/exclude"
if [ -f "$_exclude" ] && ! grep -qxF '.agents/merge.lock' "$_exclude" 2>/dev/null; then
printf '.agents/merge.lock\n' >>"$_exclude"
fi
mkdir -p "$DSH_STAGING/.agents"
: >"$DSH_STAGING/.agents/merge.lock"
# --- 3. install dependencies (no build; the launcher runs from source) --------
step "Installing dependencies with pnpm (this can take a while)"
( cd "$DSH_STAGING" && pnpm install )
[ -x "$DSH_STAGING/bin/dsh" ] || die "launcher $DSH_STAGING/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
# --- 4. put `dsh` on PATH ------------------------------------------------------
# Every install goes through a stable `current` symlink so an upgrade repoints
# one symlink (current -> new worktree) and the PATH launcher never moves:
# PATH/dsh -> current/bin/dsh -> <staging>/bin/dsh.
step "Linking dsh into $DSH_BIN_DIR"
mkdir -p "$DSH_BIN_DIR"
# The launcher must resolve to a staging worktree, never to the repository
# itself: an upgrade repoints `current`, so aliasing it onto the master clone
# would make every upgrade rewrite the fetch/upgrade base. Compare physical
# paths — a symlinked or unresolved path would slip past a string compare.
_staging_resolved=$(resolve_dir "$DSH_STAGING")
[ "$_staging_resolved" = "$REPO_ROOT" ] \
&& die "refusing to point $DSH_CURRENT at the repository $REPO_ROOT — the launcher must resolve to a staging worktree."
# Point `current` at this staging worktree with `ln -sfn`: -f replaces an
# existing `current` (re-run or upgrade) and -n stops `ln` from dereferencing
# an existing symlink-to-directory and dropping the new link *inside* the old
# worktree. `mv` is unusable here — BSD/macOS `mv` follows the existing dir
# symlink the same way. The swap is one unlink+symlink pair on a local fs; the
# installer holds no other process racing this path.
ln -sfn "$DSH_STAGING" "$DSH_CURRENT"
info "pointed $DSH_CURRENT -> $DSH_STAGING"
DSH_LAUNCH_TARGET=$DSH_CURRENT/bin/dsh
ln -sf "$DSH_LAUNCH_TARGET" "$DSH_BIN_DIR/dsh"
info "linked $DSH_BIN_DIR/dsh -> $DSH_LAUNCH_TARGET"
case ":$PATH:" in
*":$DSH_BIN_DIR:"*) ON_PATH=1 ;;
*) ON_PATH=0 ;;
esac
if [ "$ON_PATH" = 0 ]; then
warn "$DSH_BIN_DIR is not on your PATH."
_line="export PATH=\"$DSH_BIN_DIR:\$PATH\""
_rc=''
_sh=${SHELL:-} # SHELL may be unset; word-removal on an unset var trips set -u under dash.
case "${_sh##*/}" in
zsh) _rc="$HOME/.zshrc" ;;
bash) _rc="$HOME/.bashrc" ;;
esac
if [ -n "$_rc" ] && [ -f "$_rc" ] && grep -qF "$_line" "$_rc" 2>/dev/null; then
info "$_rc already exports $DSH_BIN_DIR — open a new shell to pick it up"
elif [ -n "$_rc" ] && confirm "Add it to $_rc?" Y; then
printf '\n# Added by the dsh installer\n%s\n' "$_line" >>"$_rc"
info "updated $_rc — run 'source $_rc' or open a new shell to pick it up"
else
warn "add this line to your shell profile yourself:"
printf ' %s\n' "$_line"
fi
fi
# --- 5. credentials ------------------------------------------------------------
# Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them.
if [ -n "${DSH_HOME:-}" ]; then
CONF="$DSH_HOME"
else
CONF="$HOME/.dsh"
fi
ENV_FILE="$CONF/.env"
step "Configuring credentials"
if [ -f "$ENV_FILE" ] && grep -q '^DEEPSEEK_API_KEY=' "$ENV_FILE" 2>/dev/null; then
info "DEEPSEEK_API_KEY already set in $ENV_FILE"
if ! confirm "Replace it?" N; then
SKIP_CREDS=1
fi
fi
if [ "${SKIP_CREDS:-0}" != 1 ]; then
if [ "$HAS_TTY" = 1 ]; then
API_KEY=$(ask_secret "DeepSeek API key (input hidden):")
if [ -z "$API_KEY" ]; then
warn "no key entered — skipping. Set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
else
BASE_URL=$(ask "DeepSeek base URL (optional, Enter to skip):")
mkdir -p "$CONF"
# The installer owns exactly the two DEEPSEEK_* lines; any other lines the
# user keeps in this .env are preserved. The rewrite happens in a subshell
# so umask 077 (which closes the create-time permission race) does not leak
# into the exec'd dsh, and lands atomically via a same-dir temp + mv.
_tmp="$ENV_FILE.dsh.$$"
(
umask 077
if [ -f "$ENV_FILE" ]; then
grep -v -e '^DEEPSEEK_API_KEY=' -e '^DEEPSEEK_BASE_URL=' "$ENV_FILE" >"$_tmp" || true
else
: >"$_tmp"
fi
printf 'DEEPSEEK_API_KEY=%s\n' "$API_KEY" >>"$_tmp"
if [ -n "$BASE_URL" ]; then printf 'DEEPSEEK_BASE_URL=%s\n' "$BASE_URL" >>"$_tmp"; fi
)
mv "$_tmp" "$ENV_FILE"
chmod 600 "$ENV_FILE" 2>/dev/null || true
info "wrote $ENV_FILE"
fi
else
warn "no terminal for credential input — set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
fi
fi
# --- 6. build and launch the Web interface -------------------------------------
step "Done"
if [ "$HAS_TTY" = 1 ]; then
step "Building DeepSeek Harness for Web UI"
( cd "$DSH_STAGING" && pnpm run build )
info "launching Web UI — run 'dsh web' anytime to start again"
exec "$DSH_BIN_DIR/dsh" web </dev/tty
else
info "install complete. Build and start the Web UI with:"
printf ' (cd %s && pnpm run build)\n' "$DSH_STAGING"
printf ' %s web\n' "$DSH_BIN_DIR/dsh"
fi
+3 -3
View File
@@ -104,7 +104,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[B](./reference/b.md#part) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/packages/tool.ts#L2) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
+ '[web](https://example.com)\n',
)
})
@@ -130,7 +130,7 @@ describe('rewriteMarkdown', () => {
pages,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/abc123/packages/logo.svg)\n')
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/abc123/packages/logo.svg)\n')
})
it('hands an image to the placer and uses the URL it returns', () => {
@@ -209,7 +209,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[title](./reference/b.md "b.md") '
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/docs/x(y).md)\n',
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/docs/x(y).md)\n',
)
})
+2 -2
View File
@@ -15,7 +15,7 @@ import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness-sdk'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness'
const root = resolve(import.meta.dirname, '..')
const generatedRoot = resolve(root, 'website/.generated')
@@ -209,7 +209,7 @@ function githubTarget(
image: boolean,
): string {
const path = repoPath(absPath, repoRoot)
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/${repositoryRef}/${path}${suffix}`
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/${repositoryRef}/${path}${suffix}`
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
const lineSuffix = line === undefined ? suffix : `#L${line}`
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
File diff suppressed because one or more lines are too long
+100
View File
@@ -1620,6 +1620,11 @@
"symbol": "WebBootGraph",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/telemetry.md",
"symbol": "TelemetrySharingStatus",
"source": "packages/session/session-telemetry/src/index.ts"
},
{
"doc": "docs/subsystems/telemetry.md",
"symbol": "TelemetrySeverity",
@@ -1734,6 +1739,101 @@
"doc": "docs/subsystems/core.md",
"symbol": "AgentOptions",
"source": "packages/core/agent/src/runtime-types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackVersion",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackRating",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackItem",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListValue",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackPutRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteValue",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackSessionNotFound",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackTargetNotFound",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackVersionConflict",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackNoteBlank",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackNoteTooLarge",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackFailure",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackSuccess",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackRejected",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListResult",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackPutResult",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteResult",
"source": "packages/feedback/message-feedback/src/types.ts"
}
]
}
@@ -72,6 +72,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-tool': { kind: 'none', reason: 'Browser-side Tool presentation layer; renders logged calls without changing model context.' },
'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-task': { kind: 'none', reason: 'Browser-side read-only projection of ctx.tasks records; dsh-tool-tasks owns the model-facing surface.' },
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the Host snapshots the selection at the next prompt-assembly boundary and owns the model-visible effect.' },
+26 -41
View File
@@ -1,60 +1,45 @@
import { describe, expect, it } from 'vitest'
import { findInternalRepositoryReferences } from './verify-public-repository-links.ts'
import { findUnavailableRepositoryReferences } from './verify-public-repository-links.ts'
describe('public repository link policy', () => {
it('rejects encoded and case-varied internal identities without blocking public repositories', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const encodedRepository = internalRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = internalRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = internalRepository.replace('/', '\\/')
const unicodeEscapedRepository = internalRepository.replace('/', String.raw`\u002f`)
describe('repository link policy', () => {
it('rejects encoded and case-varied references to the unavailable repository', () => {
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableName}`
const encodedRepository = unavailableRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = unavailableRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = unavailableRepository.replace('/', '\\/')
const unicodeEscapedRepository = unavailableRepository.replace('/', String.raw`\u002f`)
const source = [
'https://github.com/deepseek-ai/deepseek-harness-sdk',
`https://github.com/${internalOwner}/cordis`,
`https://github.com/${internalRepository.toUpperCase()}/issues/1`,
'https://github.com/deepseek-ai/deepseek-harness',
`https://github.com/${unavailableRepository.toUpperCase()}/issues/1`,
`https://github.com/${encodedRepository}/issues/2`,
`https://github.com/${htmlEncodedRepository}/issues/3`,
`"https:\\/\\/github.com\\/${jsonEscapedRepository}\\/issues\\/4"`,
`"https:\\/\\/github.com\\/${unicodeEscapedRepository}\\/issues\\/5"`,
`${internalOwner.toUpperCase()}#6`,
`https://github.com/${unavailableOwner}/cordis`,
`https://github.com/example/${unavailableName}`,
].join('\n')
expect(findInternalRepositoryReferences('subject.md', source)).toEqual([
expect(findUnavailableRepositoryReferences('subject.md', source)).toEqual([
{ file: 'subject.md', line: 2 },
{ file: 'subject.md', line: 3 },
{ file: 'subject.md', line: 4 },
{ file: 'subject.md', line: 5 },
{ file: 'subject.md', line: 6 },
{ file: 'subject.md', line: 7 },
{ file: 'subject.md', line: 8 },
])
})
it('allows only the exact audited trusted-publishing repository declarations', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const repositoryUrl = `git+https://github.com/${internalRepository}.git`
const manifestLine = ` "url": "${repositoryUrl}",`
const constraintLine = `const repositoryUrl = '${repositoryUrl}'`
const allowedDeclarations = [
['native/landlock-run/packages/entry/package.json', manifestLine],
['native/landlock-run/packages/linux-arm64/package.json', manifestLine],
['native/landlock-run/packages/linux-x64/package.json', manifestLine],
['scripts/check-workspace-constraints.ts', constraintLine],
] as const
it('preserves frozen archived Agent Notes', () => {
const unavailableRepository = ['deepseek-ai', 'deepseek-harness-sdk'].join('/')
for (const [file, source] of allowedDeclarations) {
expect(findInternalRepositoryReferences(file, source)).toEqual([])
}
const wrongFile = 'native/landlock-run/package.json'
expect(findInternalRepositoryReferences(wrongFile, manifestLine)).toEqual([{ file: wrongFile, line: 1 }])
const manifestFile = 'native/landlock-run/packages/entry/package.json'
const wrongField = ` "homepage": "${repositoryUrl}",`
expect(findInternalRepositoryReferences(manifestFile, wrongField)).toEqual([{ file: manifestFile, line: 1 }])
const encodedLine = manifestLine.replace('github.com/', 'github.com\\/')
expect(findInternalRepositoryReferences(manifestFile, encodedLine)).toEqual([{ file: manifestFile, line: 1 }])
expect(findUnavailableRepositoryReferences(
'.agents/notes/archived/process/historical-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([])
expect(findUnavailableRepositoryReferences(
'.agents/notes/implemented/process/active-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([{ file: '.agents/notes/implemented/process/active-record.md', line: 1 }])
})
})
+19 -30
View File
@@ -1,4 +1,4 @@
/** Reject tracked files that expose the internal repository identity outside audited publishing declarations. */
/** Reject tracked files that reference an unavailable legacy repository. */
import { execFileSync } from 'node:child_process'
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
@@ -6,22 +6,13 @@ import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '..')
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const internalIssueShorthand = `${internalOwner}#`
const trustedPublishingRepositoryUrl = `git+https://github.com/${internalRepository}.git`
/** Exact declarations that intentionally expose the source repository for trusted publishing. */
const allowedInternalRepositoryLineByFile: Readonly<Record<string, string>> = {
'native/landlock-run/packages/entry/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-arm64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-x64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'scripts/check-workspace-constraints.ts': `const repositoryUrl = '${trustedPublishingRepositoryUrl}'`,
}
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableRepositoryName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableRepositoryName}`
const archivedAgentNotePrefix = '.agents/notes/archived/'
const namedReferenceCharacters: Readonly<Record<string, string>> = {
hyphen: '-',
num: '#',
sol: '/',
}
@@ -40,8 +31,8 @@ function canonicalReferenceText(source: string): string {
.toLowerCase()
}
/** One tracked reference to the internal repository. */
export interface InternalRepositoryReference {
/** One tracked reference to the unavailable repository. */
export interface UnavailableRepositoryReference {
/** Repository-relative file path. */
file: string
/** One-based source line. */
@@ -49,20 +40,18 @@ export interface InternalRepositoryReference {
}
/**
* Locate unaudited internal-repository references in one text file.
* Locate unavailable-repository references in one active text file.
* @param file - Repository-relative path used in diagnostics.
* @param source - Text to inspect.
* @returns every matching source line.
* @returns every matching source line, excluding frozen archived Agent Notes.
*/
export function findInternalRepositoryReferences(file: string, source: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
export function findUnavailableRepositoryReferences(file: string, source: string): UnavailableRepositoryReference[] {
if (file.startsWith(archivedAgentNotePrefix)) return []
const references: UnavailableRepositoryReference[] = []
for (const [index, line] of source.split('\n').entries()) {
const canonicalLine = canonicalReferenceText(line)
const isAllowedPublishingDeclaration = line.trim() === allowedInternalRepositoryLineByFile[file]
if (!isAllowedPublishingDeclaration
&& (canonicalLine.includes(internalRepository) || canonicalLine.includes(internalIssueShorthand))) {
references.push({ file, line: index + 1 })
}
if (canonicalLine.includes(unavailableRepository)) references.push({ file, line: index + 1 })
}
return references
}
@@ -73,8 +62,8 @@ function trackedFiles(repoRoot: string): string[] {
.filter(file => file !== '')
}
function scanRepository(repoRoot: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
function scanRepository(repoRoot: string): UnavailableRepositoryReference[] {
const references: UnavailableRepositoryReference[] = []
for (const file of trackedFiles(repoRoot)) {
const path = resolve(repoRoot, file)
if (!existsSync(path)) continue
@@ -82,7 +71,7 @@ function scanRepository(repoRoot: string): InternalRepositoryReference[] {
if (!stat.isFile() && !stat.isSymbolicLink()) continue
const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
if (source.includes('\0')) continue
references.push(...findInternalRepositoryReferences(file, source))
references.push(...findUnavailableRepositoryReferences(file, source))
}
return references
}
@@ -92,9 +81,9 @@ const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(re
if (isMain) {
const references = scanRepository(root)
if (references.length === 0) {
console.log('verify-public-repository-links: tracked files expose no unexpected internal repository identity.')
console.log('verify-public-repository-links: tracked files reference no unavailable repository.')
} else {
console.error('verify-public-repository-links: unexpected internal repository references found:')
console.error('verify-public-repository-links: unavailable repository references found:')
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
process.exitCode = 1
}