Skip to content
Merged
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
4 changes: 3 additions & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ runtime dependency on the paper's `skillopt/` experiment package.

## Available integrations

Five integrations wrap the shared `skillopt_sleep` CLI. OpenClaw is a separate
Six integrations wrap the shared `skillopt_sleep` CLI. OpenClaw is a separate
reference adaptation with its own backend and setup assumptions.

| Platform | Folder | Mechanism | Status |
Expand All @@ -20,6 +20,7 @@ reference adaptation with its own backend and setup assumptions.
| **Cursor** | [`cursor/`](cursor) | native command and skill, project skill target, and shared runner | installable shared-engine integration |
| **GitHub Copilot** | [`copilot/`](copilot) | MCP server exposing seven `sleep_*` tools | shared-engine MCP integration |
| **Devin** | [`devin/`](devin) | MCP server plus Devin transcript conversion | shared-engine MCP integration |
| **DeepSeek Harness** | [`dsh/`](dsh) | Cordis plugin: 7 native `skillopt_*` tools, skill, bundle patch layer | installable shared-engine integration |
| **OpenClaw** | [`openclaw/`](openclaw) | custom DeepSeek/Ollama wrapper | independent reference adaptation; review and adapt before use |

## Install
Expand All @@ -34,6 +35,7 @@ for your workflow.
| **Cursor** | `bash plugins/cursor/install.sh` (macOS/Linux) or `powershell -File plugins/cursor/install.ps1` (Windows) | `/skillopt-sleep status` |
| **Copilot** | register `plugins/copilot/mcp_server.py` using its example MCP config | ask Copilot to run `sleep_status` |
| **Devin** | register `plugins/devin/mcp_server.py` using its example MCP config | ask Devin to run `sleep_status` |
| **DeepSeek Harness** | add `dsh-skillopt` to the profile's bundles, or patch it in — from a DSH source checkout: `pnpm dsh web --patch ./plugins/dsh/cordis.patch.yml`; with global dsh: `dsh web --patch ./plugins/dsh/cordis.patch.yml` | ask the agent to use `skillopt_status` |
| **OpenClaw** | follow and adapt [`openclaw/README.md`](openclaw/README.md) | validate paths, credentials, and tasks locally |

Python 3.10 or newer is required. Real CLI backends also require the selected
Expand Down
21 changes: 21 additions & 0 deletions plugins/dsh/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Microsoft Corporation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
119 changes: 119 additions & 0 deletions plugins/dsh/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SkillOpt-Sleep — DeepSeek Harness (dsh) integration

Give your **DeepSeek Harness** agent a nightly **sleep cycle**: it reviews past
sessions offline, replays your recurring tasks on your own API budget, and
consolidates what it learns into validated skills behind a held-out gate. Same
engine as the Claude Code / Codex / Cursor integrations (`skillopt_sleep`),
wired into dsh's plugin system as native tools plus a bundled skill.

DeepSeek Harness is the "everything is a plugin" agent framework
([deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)).
Plugins are TypeScript modules exporting an `apply(ctx)` function that register
capabilities (tools, services, events, settings) on the Cordis context.

## What this integration adds

| Component | Purpose |
|---|---|
| `src/index.js` | dsh plugin entry: registers 7 `skillopt_*` tools + Schemastery config |
| `cordis.patch.yml` | bundle patch layer — drop `dsh-skillopt` into any profile's bundles |
| `skills/skillopt-sleep/SKILL.md` | agent skill: when to use the tools, operating rules, data-boundary rules |
| `scripts/sleep.py` | bootstrap/self-check runner (same command shape the tools use) |
| `package.json` | npm package metadata (bundle manifest) |

## Tools

| Tool | skillopt_sleep action | Behavior |
|---|---|---|
| `skillopt_status` | `status` | state, engine availability, latest staged proposal & report |
| `skillopt_dry_run` | `dry-run` | full preview (harvest+mine+replay), stages nothing |
| `skillopt_run` | `run` | full cycle, stages a proposal (live files unchanged) |
| `skillopt_adopt` | `adopt` | apply latest staged proposal (with backup) — the live-change boundary |
| `skillopt_harvest` | `harvest` | read-only show/export of mined tasks |
| `skillopt_schedule` / `skillopt_unschedule` | `schedule` / `unschedule` | install/remove the nightly cron entry |

## Prerequisites

- DeepSeek Harness (dsh) installed
- Python 3.10+ with the SkillOpt-Sleep engine:

```bash
pip install skillopt # or use this source checkout
```

## Install

### As a bundle in a profile

Add `dsh-skillopt` to the profile's bundles, or in the profile `cordis.patch.yml`:

```yaml
- insert:
- id: skillopt
name: './src/index.js'
config:
backend: mock # or codex / claude / cursor / pi / opencode / handoff …
project: /path/to/project
preferences: 'Always use async/await'
```

### Local patch overlay (dev)

Run from a DeepSeek Harness **source checkout** (the official dev workflow,
`pnpm` resolves the workspace `dsh` bin):

```bash
pnpm dsh web --patch ./plugins/dsh/cordis.patch.yml
```

If `dsh` is installed **globally** (npm install -g), use it directly:

```bash
dsh web --patch ./plugins/dsh/cordis.patch.yml
```

Either way the patch inserts the `skillopt` plugin row into the profile; then
ask the agent: "Use skillopt_status to check the sleep cycle state."

## Config keys

| Key | Default | Purpose |
|---|---|---|
| `pythonCmd` | `python` | Python interpreter for the engine |
| `module` | — (bootstrap) | engine Python module override (`python -m <module>`) |
| `engineScript` | — (scripts/sleep.py) | engine bootstrap script override |
| `project` | — | default project directory |
| `scope` | — | harvest scope: `all` \| `invoked` |
| `backend` | — | `mock\|claude\|codex\|copilot\|cursor\|pi\|opencode\|handoff\|azure_openai` |
| `source` | — | `claude\|codex\|copilot\|cursor\|pi\|opencode\|auto` |
| `model` | — | backend model override |
| `maxTasks` / `maxSessions` | — | mine/harvest caps |
| `editBudget` | — | bounded edits per cycle |
| `preferences` | — | house rules for the reflection prior |
| `jsonOutput` | `false` | machine-readable JSON output |
| `autoAdopt` | `false` | OPERATOR-ONLY: auto-adopt a passed proposal without asking |
| `unscheduleAll` | `false` | OPERATOR-ONLY: allow `skillopt_unschedule` to remove every managed entry |
| `timeoutMs` | `600000` | per-call engine timeout in milliseconds |

Advanced engine keys (`gate_mode`, `gate_metric`, `gate_no_regression`,
`dream_rollouts`, `recall_k`, `evolve_memory`/`evolve_skill`) go in
`~/.skillopt-sleep/config.json` — the same file shared by all integrations.

## Data boundary

- Harvest is read-only; `mock`/`handoff` make no network calls.
- `run` stages proposals; `adopt` is the normal live-change boundary and backs up first.
- Real backends send truncated transcript excerpts and derived tasks to the
selected provider. For sensitive sessions, export tasks first (`skillopt_harvest`
with `output=`), redact, set `"reviewed": true`, then replay — real backends
refuse unreviewed task files.
- Outbound prompts are not guaranteed secret-free; review source & provider policy.

## Validate (no API spend)

```bash
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
```

See the [SkillOpt-Sleep documentation](../../docs/sleep/README.md) for recorded
results, limitations, and the supported integration surface.
36 changes: 36 additions & 0 deletions plugins/dsh/README.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# dsh-skillopt 文档

## 快速上手

1. 安装引擎:`pip install skillopt`(或克隆 [microsoft/SkillOpt](https://github.com/microsoft/SkillOpt) 并把其根目录加入 `PYTHONPATH`)
2. 在 profile 的 `cordis.patch.yml` 插入插件(见根 README)
3. 启动 dsh 后向 agent 提问:"用 skillopt_status 查看睡眠循环状态"

## 工具与引擎命令对照

| dsh 工具 | skillopt_sleep 动作 | 说明 |
|---|---|---|
| `skillopt_status` | `status` | 状态与暂存提案 |
| `skillopt_dry_run` | `dry-run` | 预览,不暂存 |
| `skillopt_run` | `run` | 完整循环并暂存 |
| `skillopt_adopt` | `adopt` | 应用提案(先备份) |
| `skillopt_harvest` | `harvest` | 只读导出任务 |
| `skillopt_schedule` | `schedule` | 安装夜间 cron |
| `skillopt_unschedule` | `unschedule` | 移除 cron |

## 引擎进阶配置(`~/.skillopt-sleep/config.json`)

```json
{
"gate_mode": "on",
"gate_metric": "mixed",
"gate_no_regression": false,
"dream_rollouts": 1,
"recall_k": 0,
"evolve_memory": true,
"evolve_skill": true,
"preferences": "Prefer pytest. Keep commits imperative."
}
```

详见上游文档:https://github.com/microsoft/SkillOpt/tree/main/docs/sleep
18 changes: 18 additions & 0 deletions plugins/dsh/cordis.patch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# dsh-skillopt bundle patch layer.
# When a profile lists this bundle, this patch inserts the plugin rows below.
#
# Usage in a profile's cordis.patch.yml / dsh.profile bundles list:
# bundles:
# - dsh-skillopt
# or with a local checkout:
# - insert:
# - id: skillopt
# name: './src/index.js'

- insert:
- id: skillopt
name: './src/index.js'
# config:
# backend: mock # mock = no provider calls (default)
# project: /path/to/project
# preferences: 'Prefer pytest. Keep commits imperative.'
43 changes: 43 additions & 0 deletions plugins/dsh/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "dsh-skillopt",
"version": "0.1.0",
"description": "Microsoft SkillOpt-Sleep integration for DeepSeek Harness: give your dsh agent a nightly sleep cycle that harvests past sessions, replays recurring tasks, and consolidates validated skills behind a held-out gate.",
"type": "module",
"main": "src/index.js",
"files": [
"src",
"skills",
"scripts",
"cordis.patch.yml",
"README.md",
"README.zh.md"
],
"keywords": [
"dsh",
"deepseek-harness",
"cordis",
"plugin",
"skillopt",
"skill-optimization",
"self-improvement",
"memory-consolidation",
"sleep"
],
"license": "MIT",
"dsh": {
"bundle": {
"patch": "cordis.patch.yml"
}
},
"dependencies": {
"@deepseek-ai/schemastery": "^3.18.1"
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8"
},
"peerDependenciesMeta": {
"@deepseek-ai/cordis": { "optional": true },
"@deepseek-ai/dsh-tools": { "optional": true }
}
}
41 changes: 41 additions & 0 deletions plugins/dsh/scripts/audit-control-chars.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 安全审核:控制字符/换行不能逃逸单引号边界,且不产生副作用。
// 修复后的期望:控制字符被剥离为空格,值仍作为单个参数到达,无文件/命令执行。
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
const m = await import('../src/index.js')
const { quoteArgv } = m
const BASH = process.env.BASH_PATH || 'C:/Program Files/Git/bin/bash.exe'
const payloads = [
'x\n touch /tmp/nl-pwned',
'x\r echo PWNED',
'x\ttab',
'x`id`',
'x\x00null',
"'; touch /tmp/semi-pwned;'",
'normal\r\ntouch /tmp/crnl-pwned',
]
let fail = 0
for (const p of payloads) {
const argv = ['python', '-m', 'skillopt_sleep', 'run', '--preferences', p]
const quoted = quoteArgv(argv)
const script = `for a in ${quoted}; do printf '[%s]\\n' "$a"; done`
const out = execFileSync(BASH, ['-c', script], { encoding: 'utf8' })
const args = out.trim().split('\n').map((l) => l.slice(1, -1))
// 安全断言 1:--preferences 后恰好一个参数(无参数分裂)
const idx = args.indexOf('--preferences')
const singleArg = idx >= 0 && idx + 1 < args.length && idx + 2 === args.length
// 安全断言 2:到达的值不含原始控制字符(已被剥离为空格)
const arrived = idx >= 0 ? args[idx + 1] : ''
const stripped = !/[\r\n\t\x00]/.test(arrived)
// 安全断言 3:没有注入命令出现在参数列表外
const noInjection = !args.some((a) => /touch|PWNED|rm\s/.test(a) && a !== arrived)
if (!singleArg || !stripped || !noInjection) {
fail++
console.log('FAIL:', JSON.stringify(p), '-> args:', JSON.stringify(args))
}
}
for (const f of ['/tmp/nl-pwned', '/tmp/semi-pwned', '/tmp/crnl-pwned']) {
if (existsSync(f)) { fail++; console.log('FILE CREATED:', f) }
}
console.log(fail === 0 ? 'ALL CONTROL-CHAR PAYLOADS NEUTRALIZED' : `${fail} FAILURES`)
process.exit(fail === 0 ? 0 : 1)
31 changes: 31 additions & 0 deletions plugins/dsh/scripts/audit-injection.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 独立注入审计:各种恶意 payload 过 quoteArgv → 真实 bash → 验证不逃逸
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
const m = await import('../src/index.js')
const { quoteArgv } = m
const BASH = process.env.BASH_PATH || 'C:/Program Files/Git/bin/bash.exe'
const payloads = [
'x; touch /tmp/pwned',
'x$(touch /tmp/pwned2)',
'x`touch /tmp/pwned3`',
'x|cat /etc/passwd',
'x&&rm -rf /',
"' OR 1=1 --",
'x > /tmp/redirected',
]
let fail = 0
for (const p of payloads) {
const argv = ['python', '-m', 'skillopt_sleep', 'run', '--preferences', p]
const quoted = quoteArgv(argv)
const script = `for a in ${quoted}; do printf '[%s]\\n' "$a"; done`
const out = execFileSync(BASH, ['-c', script], { encoding: 'utf8' })
const args = out.trim().split('\n').map((l) => l.slice(1, -1))
const pref = args[args.indexOf('--preferences') + 1]
const ok = pref === p
if (!ok) { fail++; console.log('FAIL:', JSON.stringify(p), '->', JSON.stringify(pref)) }
}
for (const f of ['/tmp/pwned', '/tmp/pwned2', '/tmp/pwned3', '/tmp/redirected', '/tmp/pwnedx']) {
if (existsSync(f)) { fail++; console.log('FILE CREATED:', f) }
}
console.log(fail === 0 ? 'ALL 7 INJECTION PAYLOADS INERT' : `${fail} FAILURES`)
process.exit(fail === 0 ? 0 : 1)
Loading