Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/dsh-plugin-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Publish DSH Plugin to npm

on:
workflow_dispatch:
inputs:
release_tag:
description: "Existing published DSH plugin release tag"
required: true
type: string

permissions:
contents: read

concurrency:
group: dsh-plugin-npm-publication
cancel-in-progress: false

jobs:
publish:
if: github.repository == 'loopx-project/loopx' && github.ref_type == 'tag' && github.ref_name == inputs.release_tag
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: packages/dsh-loopx-plugin
env:
RELEASE_TAG: ${{ inputs.release_tag }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: "24"
- name: Require a merged, published release with matching package identity
run: |
git fetch origin main
git merge-base --is-ancestor HEAD origin/main
node --input-type=module <<'NODE'
import assert from 'node:assert/strict'
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
const manifest = JSON.parse(readFileSync('package.json', 'utf8'))
assert.equal(process.env.RELEASE_TAG, `dsh-loopx-plugin-v${manifest.version}`)
mkdirSync('output', { recursive: true })
appendFileSync(process.env.GITHUB_ENV, `DSH_ARTIFACT=output/${manifest.name}-${manifest.version}.tgz\n`)
NODE
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json tagName,isDraft > output/release.json
node -e 'const r=require("./output/release.json"); require("node:assert/strict").equal(r.isDraft, false)'
- name: Download the immutable GitHub package
run: |
gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$(basename "$DSH_ARTIFACT")" --dir output
tar -xOf "$DSH_ARTIFACT" package/package.json > output/manifest.json
node --input-type=module <<'NODE'
import assert from 'node:assert/strict'
import { appendFileSync, readFileSync } from 'node:fs'
const source = JSON.parse(readFileSync('package.json', 'utf8'))
const packed = JSON.parse(readFileSync('output/manifest.json', 'utf8'))
for (const field of ['name', 'version', 'main', 'exports', 'dsh', 'peerDependencies', 'repository', 'keywords']) {
assert.deepEqual(packed[field], source[field], `release artifact ${field} differs from its tag`)
}
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(source.name)}`, {
signal: AbortSignal.timeout(15_000),
})
assert(response.ok || response.status === 404, `registry lookup failed: HTTP ${response.status}`)
const exists = response.ok && !!(await response.json()).versions?.[source.version]
appendFileSync(process.env.GITHUB_ENV, `DSH_ALREADY_PUBLISHED=${exists}\n`)
NODE
- name: Publish the same bytes with npm trusted publishing
if: env.DSH_ALREADY_PUBLISHED != 'true'
run: npm publish "$DSH_ARTIFACT" --access public --tag latest --ignore-scripts --provenance
- name: Read back npm bytes, latest, and marketplace repository discovery
run: node scripts/verify-distribution.mjs --tarball "$DSH_ARTIFACT"
77 changes: 77 additions & 0 deletions .github/workflows/dsh-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: DSH Plugin Distribution

on:
pull_request:
paths:
- "packages/dsh-loopx-plugin/**"
- ".github/workflows/dsh-plugin.yml"
- ".github/workflows/dsh-plugin-publish.yml"
push:
branches: [main]
paths:
- "packages/dsh-loopx-plugin/**"
- ".github/workflows/dsh-plugin.yml"
- ".github/workflows/dsh-plugin-publish.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: dsh-plugin-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
distribution:
name: dsh-plugin (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
defaults:
run:
working-directory: packages/dsh-loopx-plugin
shell: bash
env:
LOOPX_USAGE_PING: "0"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: "24"
- uses: pnpm/action-setup@v4
with:
version: "10.33.0"
- uses: astral-sh/setup-uv@v7
- name: Prepare source-checkout integration interpreter
if: runner.os == 'Linux'
working-directory: .
run: uv sync --extra test
- name: Install frozen plugin dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Validate typed host and client contracts
run: pnpm typecheck && pnpm smoke:peer-range
- name: Validate unit and real LoopX admission contracts
if: runner.os == 'Linux'
run: PATH="$GITHUB_WORKSPACE/.venv/bin:$PATH" pnpm test
- name: Build the npm publication artifact
run: |
node -e 'require("node:fs").mkdirSync("output", { recursive: true })'
npm pack --pack-destination output
node -e 'require("node:fs").appendFileSync(process.env.GITHUB_ENV, "DSH_ARTIFACT=output/dsh-loopx-plugin-" + require("./package.json").version + ".tgz\n")'
- name: Qualify the real package-name install and uninstall
run: node smoke/dsh-registry-smoke.mjs --tarball "$DSH_ARTIFACT"
- name: Qualify packed Client and real web runtime
if: runner.os == 'Linux'
run: |
node smoke/dsh-client-artifact-smoke.mjs --tarball "$DSH_ARTIFACT"
node smoke/dsh-profile-smoke.mjs --tarball "$DSH_ARTIFACT"
node smoke/dsh-goalbar-runtime-smoke.mjs --tarball "$DSH_ARTIFACT"
- name: Preserve the qualified package for review and local recovery
uses: actions/upload-artifact@v7
with:
name: dsh-loopx-plugin-${{ matrix.os }}
path: packages/dsh-loopx-plugin/${{ env.DSH_ARTIFACT }}
if-no-files-found: error
40 changes: 22 additions & 18 deletions docs/architecture/rfcs/harness-selection-dsh-pi-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,24 +313,28 @@ These are integration-cost and contract observations, not claims that Pi lacks
events or DSH cannot support other models. Both expose control-capable APIs;
passivity is a property of the selected adapter and its loaded dependencies.

The two LoopX surfaces that depend on dsh do not move together. The bounded Turn
host uses the Python SDK/runtime pin recorded above (`0.1.5rc1`, the released
channel). The dsh-side plugin (`packages/dsh-loopx-plugin`) now builds its
development, host, and client surfaces on the same released `0.1.5-rc.2` line
instead of the retired `0.1.1-rc.2` one, and its npm peer ranges admit only
`>=0.1.5-rc.1`. Three upstream moves forced that, so it is a new release line
rather than a patch: the 0.1.5 line no longer publishes
`@deepseek-ai/dsh-client-runtime` (last released 0.1.1-rc.2), which moves the
`slots` service seat to `@deepseek-ai/dsh-client-ui-renderer` — the package this
manifest now names in `dsh.client.inject`; `Session.events` became
`Session.snapshotEvents()` and `Inbox.hasPending` became the two pending queues;
and the shared `/api` bridge addresses Remote methods as `<namespace>/<method>`
with a single `args` payload field. One `dsh.client.inject` list cannot order
boot rows for both generations at once, so the plugin cannot claim both. The L1
observer contract above is unchanged: the observer still consumes only
`session/created`, `session/event`, and `session/disposed`, and now treats
token-level `assistant/chunk` rows as retired input replayed from older durable
logs instead of a live event type.
The bounded Turn host's Python SDK/runtime pin remains separate from the
independently versioned `packages/dsh-loopx-plugin`. The plugin source now pins
its development, host, and Client packages to `0.2.0-rc.2`, retaining the supported
0.1.5 and explicit 0.1.7 prerelease peer ranges. It registers its initialization
message source, resets Session state at `agent/created`, and keeps Connection
Peer admission in the upstream transport. Client revisions remain opaque.
Bootstrap and runtime consumers require LoopX 1.2.4 or newer, including the
released Windows peer-file fix; an explicit outdated CLI fails before install.
The retired 0.1.1 Client runtime is still unsupported; the renderer owns slots,
and the shared `/api` carrier retains `<namespace>/<method>` and `args`.

The npm distribution channel requires the exact GitHub release artifact,
the qualified `latest` tag, and repository search selecting `dsh-loopx-plugin`
instead of the monorepo root. The registry smoke qualifies package-name
installation and removal locally; CI covers Linux and Windows. Direct release
installation is proposed in [Hub PR #93](https://github.com/dshplugin/dsh-plugin-hub/pull/93),
using the existing authoritative catalog command. It needs a released Hub and
verified online-catalog adoption before it replaces the npm requirement for
that route. Public distribution, marketplace adoption, and browser-mounted
Start/Pause remain independent release evidence. The L1 observer still consumes only
`session/created`, `session/event`, and `session/disposed`; this compatibility
repair does not close its separately budgeted C0/C1 or overhead qualification.

## Data and Authority Flow

Expand Down
30 changes: 16 additions & 14 deletions docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,22 @@ tag:PyPI 上的 `deepseek-harness-sdk==0.1.5rc1` /
这是接入成本和合同差异,不是说 Pi 没有事件,或 DSH 不能使用其他模型。
两个 harness 都有控制 API;“被动”是具体 adapter 和实际加载依赖的性质。

依赖 dsh 的两个 LoopX 面并不一起移动:有界 Turn 宿主使用上文记录的 Python
SDK/runtime 固定版本(`0.1.5rc1`,已发布通道);而 dsh 侧插件
(`packages/dsh-loopx-plugin`)的开发、宿主与客户端面现已统一构建在同一已发布的
`0.1.5-rc.2` 线上,不再停留在 `0.1.1-rc.2`,其 npm peer 范围只接受
`>=0.1.5-rc.1`。这是上游三处变化逼出来的,因此它是一条新的发布线而不是原地补丁:
0.1.5 线不再发布 `@deepseek-ai/dsh-client-runtime`(最后发布版本为 `0.1.1-rc.2`),
`slots` service 座位随之移到 `@deepseek-ai/dsh-client-ui-renderer`,也就是本 manifest
现在写入 `dsh.client.inject` 的包;`Session.events` 变为 `Session.snapshotEvents()`,
`Inbox.hasPending` 变为两个 pending 队列;共享 `/api` bridge 用
`<namespace>/<method>` 寻址 Remote 方法,并只接受一个 `args` payload 字段。一份
`dsh.client.inject` 无法同时为两代排序 boot row,所以插件不能同时声明两代。上文的
L1 observer 契约不变:observer 仍只消费 `session/created`、`session/event`、
`session/disposed`,只是把 token 级 `assistant/chunk` 行视为旧 durable 日志重放出来的
已退场输入,而不是现存事件类型。
有界 Turn 宿主的 Python SDK/runtime 固定版本,与独立版本化的
`packages/dsh-loopx-plugin` 分开维护。插件源码现将开发、Host、Client 包固定为
`0.2.0-rc.2`,保留已支持的 0.1.5 和显式 0.1.7 预发布 peer 范围。插件注册自己的
初始化消息来源,在 `agent/created` 重置 Session 状态,并由上游 Connection transport
继续负责 Peer 准入;Client revision 保持不透明。
初始化与运行时统一要求包含 Windows peer-file 修复的 LoopX 1.2.4 或更新版本;
显式指定的旧 CLI 在安装前失败。已退场的 0.1.1 Client runtime 仍不支持,slots 由
renderer 提供,共享 `/api` 保留 `<namespace>/<method>` 和 `args` 契约。

npm 分发通道要求包与 GitHub release artifact 完全一致,`latest` 指向已验证版本,
仓库搜索选中 `dsh-loopx-plugin` 而不是 monorepo 根目录。registry smoke 在本地验证
包名安装与卸载,CI 覆盖 Linux 和 Windows。
[Hub PR #93](https://github.com/dshplugin/dsh-plugin-hub/pull/93) 提议按现有权威目录命令
直接安装 release 包;该通道须待 Hub 发布并验证在线目录采用后,才能替代对 npm 的要求。
公开分发、市场目录采用,以及浏览器挂载后的 Start/Pause 仍各自需要发布证据。L1 observer 仍只消费 `session/created`、
`session/event`、`session/disposed`;兼容性修复不关闭另行预算的 C0/C1 或开销验收。

## 数据流与权限

Expand Down
Loading
Loading