dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env node
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
function render(overrides = {}) {
const env = {
...process.env,
DSH_DEFAULT_WORKSPACE: '/workspace',
DSH_IMAGE_TAG: 'latest',
DSH_LISTEN_ADDRESS: '0.0.0.0',
DSH_PORT: '3080',
DSH_PROXY_POLYFILL: 'true',
DSH_TELEMETRY_DISABLED: 'true',
DSH_TRUSTED_HOSTS: '',
DSH_WORKSPACE: './workspace',
...overrides,
}
if (!Object.hasOwn(overrides, 'DSH_SUDO_ENABLED')) delete env.DSH_SUDO_ENABLED
const result = spawnSync(
'docker',
['compose', '--env-file', '/dev/null', '-f', 'docker-compose.yaml', 'config', '--format', 'json'],
{
encoding: 'utf8',
env,
},
)
assert.equal(result.status, 0, result.stderr)
return JSON.parse(result.stdout).services['deepseek-harness']
}
const defaults = render()
assert.equal(defaults.image, 'szcq/deepseek-harness:latest')
assert.equal(defaults.container_name, 'deepseek-harness')
assert.equal(defaults.ports[0].host_ip, '0.0.0.0')
assert.equal(defaults.ports[0].published, '3080')
assert.equal(Object.hasOwn(defaults.environment, 'DSH_PROXY_USERNAME'), false)
assert.equal(Object.hasOwn(defaults.environment, 'DSH_PROXY_PASSWORD'), false)
assert.equal(Object.hasOwn(defaults.environment, 'DSH_PLATFORM_PASSWORD'), false)
assert.equal(defaults.environment.DSH_PROXY_POLYFILL, 'true')
assert.equal(defaults.environment.DSH_TRUSTED_HOSTS, '')
assert.equal(Object.hasOwn(defaults.environment, 'DSH_UPDATE_METADATA_URL'), false)
assert.deepEqual(defaults.group_add, ['dsh-sudo-false'])
assert.equal(defaults.environment.DSH_EXPERIMENTAL_PROBATION_SECONDS, '120')
assert.equal(defaults.environment.DSH_LOG_MAX_BYTES, '104857600')
assert.equal(defaults.environment.DSH_PLATFORM_DATA, '/data/platform')
assert.equal(defaults.environment.DSH_HOME, '/data/dsh')
assert.deepEqual(defaults.volumes.map(volume => volume.target).sort(), ['/data/dsh', '/data/platform', '/workspace'])
assert.equal(defaults.volumes.find(volume => volume.target === '/data/dsh').type, 'volume')
assert.equal(defaults.volumes.find(volume => volume.target === '/data/platform').type, 'volume')
assert.equal(defaults.volumes.find(volume => volume.target === '/workspace').source, resolve('./workspace'))
const configured = render({
DSH_IMAGE_TAG: '1.0.0',
DSH_LISTEN_ADDRESS: '0.0.0.0',
DSH_PORT: '4080',
DSH_PROXY_POLYFILL: 'false',
DSH_SUDO_ENABLED: 'true',
DSH_TELEMETRY_DISABLED: 'false',
DSH_TRUSTED_HOSTS: '192.168.1.10,dsh.example:8443',
})
assert.equal(configured.image, 'szcq/deepseek-harness:1.0.0')
assert.equal(configured.ports[0].host_ip, '0.0.0.0')
assert.equal(configured.ports[0].published, '4080')
assert.equal(Object.hasOwn(configured.environment, 'DSH_PROXY_USERNAME'), false)
assert.equal(Object.hasOwn(configured.environment, 'DSH_PROXY_PASSWORD'), false)
assert.equal(Object.hasOwn(configured.environment, 'DSH_PLATFORM_PASSWORD'), false)
assert.equal(configured.environment.DSH_PROXY_POLYFILL, 'false')
assert.equal(configured.environment.DSH_TELEMETRY_DISABLED, 'false')
assert.equal(configured.environment.DSH_TRUSTED_HOSTS, '192.168.1.10,dsh.example:8443')
assert.deepEqual(configured.group_add, ['dsh-sudo-true'])
console.log('Compose configuration checks passed')
+936
View File
@@ -0,0 +1,936 @@
#!/bin/sh
set -eu
if [ "$#" -gt 1 ]; then
echo "usage: $0 [image]" >&2
exit 64
fi
image="${1:-dsh-docker:smoke}"
container="dsh-gateway-smoke-$$"
platform_volume="dsh-platform-smoke-$$"
home_volume="dsh-home-smoke-$$"
readiness_timeout_ms=30000
cleanup() {
docker rm -f "$container" >/dev/null 2>&1 || true
docker volume rm "$platform_volume" "$home_volume" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
wait_platform_ready() {
started="$(date +%s%3N)"
while ! docker exec "$container" sh -c '
curl --fail --silent http://127.0.0.1:3080/_dsh_gateway/health >/dev/null \
&& test "$(curl --silent --output /dev/null --write-out "%{http_code}" --noproxy "*" http://127.0.0.1:3079/)" -ge 200 \
&& test "$(curl --silent --output /dev/null --write-out "%{http_code}" --noproxy "*" http://127.0.0.1:3079/)" -lt 500 \
&& dsh-platform status | jq -e '\''.dshLifecycle.state == "running"'\'' >/dev/null
' >/dev/null 2>&1; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "platform readiness exceeded 30 seconds" >&2
exit 1
fi
sleep 0.2
done
now="$(date +%s%3N)"
echo "$((now - started))"
}
wait_control_ready() {
started="$(date +%s%3N)"
while ! docker exec "$container" curl --fail --silent \
--header 'Host: smoke.example' --header 'Accept: text/html' \
http://127.0.0.1:3080/_dsh_platform/auth/ >/dev/null 2>&1; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "control-plane readiness exceeded 30 seconds" >&2
exit 1
fi
sleep 0.2
done
}
establish_sessions() {
action="$1"
expected=200
[ "$action" = login ] || expected=201
marker='autocomplete="current-password"'
[ "$action" = login ] || marker='autocomplete="new-password"'
session_cookie=/tmp/dsh-smoke.cookies
auth_page=/tmp/dsh-smoke-auth.html
started="$(date +%s%3N)"
while ! docker exec "$container" sh -c '
curl --fail --silent --header "Host: smoke.example" --header "Accept: text/html" \
http://127.0.0.1:3080/_dsh_platform/auth/ > "$1" \
&& grep -F "$2" "$1" >/dev/null
' _ "$auth_page" "$marker"; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker exec "$container" cat "$auth_page" >&2 || true
docker logs "$container" >&2
echo "authentication state did not become $action-ready" >&2
exit 1
fi
sleep 0.2
done
docker exec "$container" rm -f "$session_cookie"
docker exec "$container" curl --fail --silent \
--cookie-jar "$session_cookie" --header 'Host: smoke.example' --header 'Accept: text/html' \
http://127.0.0.1:3080/_dsh_platform/auth/ >/dev/null
auth_csrf="$(docker exec "$container" awk '$6 == "dsh_auth_csrf" { print $7 }' "$session_cookie")"
[ -n "$auth_csrf" ]
authentication_context="$(docker exec "$container" sed -n 's/.*authenticationContextId=\("[^"]*"\).*/\1/p' "$auth_page" | sed 's/^"//; s/"$//')"
[ -n "$authentication_context" ]
authentication_body="$(jq -nc --arg username smoke-admin --arg password smoke-password --arg authenticationContext "$authentication_context" '{username:$username,password:$password,authenticationContext:$authenticationContext}')"
auth_response=/tmp/dsh-smoke-auth-response.json
status="$(docker exec "$container" curl --silent --output "$auth_response" --write-out '%{http_code}' \
--cookie "$session_cookie" --cookie-jar "$session_cookie" \
--header 'Host: smoke.example' --header 'Origin: http://smoke.example' \
--header 'Content-Type: application/json' --header "X-DSH-CSRF: $auth_csrf" \
--data "$authentication_body" \
http://127.0.0.1:3080/_dsh_platform/auth/session)"
if [ "$status" != "$expected" ]; then
docker exec "$container" cat "$auth_response" >&2 || true
echo "DSH authentication returned HTTP $status, expected $expected" >&2
exit 1
fi
status="$(docker exec "$container" curl --fail --silent --location \
--output /dev/null --write-out '%{http_code}' \
--cookie "$session_cookie" --cookie-jar "$session_cookie" \
--header 'Host: smoke.example' --header 'Accept: text/html' \
http://127.0.0.1:3080/_dsh_platform/auth/management/start)"
[ "$status" = 200 ]
management_csrf="$(docker exec "$container" awk '$6 == "dsh_management_csrf" { print $7 }' "$session_cookie")"
[ -n "$management_csrf" ]
}
if [ "$#" -eq 0 ]; then
docker build --tag "$image" .
fi
docker run --detach --name "$container" \
--group-add dsh-sudo-false \
--env DSH_TRUSTED_HOSTS=smoke.example \
--volume "$platform_volume:/data/platform" \
--volume "$home_volume:/data/dsh" \
"$image" >/dev/null
wait_control_ready
establish_sessions initialize
startup_one="$(wait_platform_ready)"
if docker exec --user node "$container" sudo -n true >/dev/null 2>&1; then
echo "DSH node identity unexpectedly retained sudo while DSH_SUDO_ENABLED=false" >&2
exit 1
fi
status="$(docker exec --user node "$container" curl --silent --output /dev/null --write-out '%{http_code}' \
--unix-socket /run/dsh-platform/maintenance.sock \
http://localhost/_dsh_platform/api/v1/files/config)"
[ "$status" = 401 ]
attempt=0
until docker logs "$container" 2>&1 \
| grep -E '"source":"bootstrap".*"stream":"platform".*"message":"platform.ready"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 50 ] || exit 1
sleep 0.2
done
docker logs "$container" 2>&1 \
| grep -E '"source":"stage0".*"stream":"platform".*"message":"stage0.ready"' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' 'http://127.0.0.1:3080/_dsh_platform/api/v1/logs?limit=5000' \
| jq -e 'any(.entries[]; .source == "stage0" and .message == "stage0.ready")
and any(.entries[]; .source == "bootstrap" and .message == "platform.ready")
and any(.entries[]; .source == "gateway" and .message == "gateway.ready")' >/dev/null
docker exec "$container" sh -c '
set -eu
command -v g++ >/dev/null
command -v make >/dev/null
command -v python3 >/dev/null
venv="$(mktemp -d)/venv"
python3 -m venv "$venv"
"$venv/bin/python" -c "import sys; assert sys.prefix != sys.base_prefix"
"$venv/bin/pip" --version >/dev/null
'
sh "$(dirname "$0")/profile-package-compatibility-smoke.sh" "$container"
status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' \
--header 'Host: evil.example' http://127.0.0.1:3080/)"
[ "$status" = 403 ]
status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' \
--header 'Accept: text/html' --header 'Host: smoke.example' http://127.0.0.1:3080/)"
[ "$status" = 303 ]
status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' \
--header 'Host: smoke.example' http://127.0.0.1:3080/)"
[ "$status" = 401 ]
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Host: smoke.example' http://127.0.0.1:3080/ >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/console/ \
| grep -F 'DSH Management Console' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e '.updateChannel == "stable"
and .automaticCheck == {"enabled":true,"intervalSeconds":21600,"notificationsEnabled":true}
and .latestAutomatic == {"stable":null,"upstream":null}' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins \
| jq -e '.plugins == [{
"id":"platform-management",
"artifactId":"system-plugin-platform-management",
"sha256":.plugins[0].sha256,
"description":{"zh":"管理 DSH Docker 更新、运行维护、系统资源与登录会话。","en":"Manage DSH Docker updates, runtime maintenance, system resources, and login sessions."},
"installed":true,
"enabled":true,
"activeInstalled":true,
"activeEnabled":true,
"pendingRestart":false,
"protected":true,
"reason":null
},{
"id":"settings-navigation",
"artifactId":"system-plugin-settings-navigation",
"sha256":.plugins[1].sha256,
"description":{"zh":"优化 DSH 设置目录滚动和窄屏分级导航。","en":"Improve DSH Settings directory scrolling and compact navigation."},
"installed":true,
"enabled":true,
"activeInstalled":true,
"activeEnabled":true,
"pendingRestart":false,
"protected":false,
"reason":null
},{
"id":"settings-document-editor",
"artifactId":"system-plugin-settings-document-editor",
"sha256":.plugins[2].sha256,
"description":{"zh":"在浏览器中查看和编辑 DSH 配置文件。","en":"View and edit the DSH settings document in the browser."},
"installed":true,
"enabled":true,
"activeInstalled":true,
"activeEnabled":true,
"pendingRestart":false,
"protected":false,
"reason":null
}]' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/system-skills \
| jq -e '.skills == [{
"id":"dsh-docker-operations",
"sha256":.skills[0].sha256,
"description":{"zh":"在 DSH Docker 环境中使用受支持的命令完成开发、插件、维护、更新、恢复与诊断。","en":"Use supported commands for development, plugins, maintenance, updates, recovery, and diagnostics in the DSH Docker Environment."},
"installed":true,
"enabled":true
}]' >/dev/null
skill_view="/run/dsh-platform/views/skills/dsh-docker-operations"
docker exec "$container" sh -c "test -L '$skill_view' && test -f '$skill_view/SKILL.md'"
docker exec -i --user node \
--env DSH_HOME=/data/dsh \
--env DSH_DEFAULT_WORKSPACE=/workspace \
--env DSH_BUNDLED_SKILL_DIR=/run/dsh-platform/views/skills \
"$container" /usr/local/bin/node --input-type=module \
< "$(dirname "$0")/system-skill-discovery-smoke.mjs" \
| jq -e '.name == "dsh-docker-operations" and .source == "bundled" and .provider == "filesystem" and .contentBytes > 500' >/dev/null
skill_dsh_pid="$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )"
skill_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"skillId":"dsh-docker-operations","action":"disable"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/system-skills/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$skill_task" '.systemSkillOperation.taskId == $task and .systemSkillOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.1
done
if docker exec "$container" test -e "$skill_view"; then
echo "disabled System Skill remained in the active view" >&2
exit 1
fi
[ "$skill_dsh_pid" = "$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )" ]
skill_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"skillId":"dsh-docker-operations","action":"enable"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/system-skills/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$skill_task" '.systemSkillOperation.taskId == $task and .systemSkillOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.1
done
docker exec "$container" sh -c "test -L '$skill_view' && test -f '$skill_view/SKILL.md'"
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"system-skill.disable.completed"' >/dev/null
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"system-skill.enable.completed"' >/dev/null
docker exec -i --user node \
--env DSH_HOME=/data/dsh \
--env DSH_DEFAULT_WORKSPACE=/workspace \
--env DSH_BUNDLED_SKILL_DIR=/run/dsh-platform/views/skills \
"$container" /usr/local/bin/node --input-type=module \
< "$(dirname "$0")/user-skill-watcher-smoke.mjs" \
| jq -e '. == {"discovered":true,"disabled":true,"enabled":true,"deleted":true}' >/dev/null
docker exec --user node "$container" sh -c '
set -eu
skill=/data/dsh/skills/smoke-managed-user-skill
mkdir -p "$skill"
cat > "$skill/SKILL.md" <<"EOF"
---
name: smoke-managed-user-skill
description: Smoke-test Management user skill actions.
---
# Smoke Managed User Skill
EOF
'
user_skill_inventory="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills)"
user_skill_id="$(echo "$user_skill_inventory" | jq -er '.skills[] | select(.name == "smoke-managed-user-skill") | .entryId')"
user_skill_revision="$(echo "$user_skill_inventory" | jq -er .revision)"
user_skill_dsh_pid="$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )"
user_skill_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data "$(jq -nc --arg entryId "$user_skill_id" --arg revision "$user_skill_revision" \
'{entryId:$entryId,revision:$revision,action:"disable"}')" \
http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$user_skill_task" '.userSkillOperation.taskId == $task and .userSkillOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.1
done
docker exec "$container" test -f /data/dsh/skills/.disabled/smoke-managed-user-skill/SKILL.md
[ "$user_skill_dsh_pid" = "$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )" ]
user_skill_inventory="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills)"
user_skill_id="$(echo "$user_skill_inventory" | jq -er '.skills[] | select(.name == "smoke-managed-user-skill") | .entryId')"
user_skill_revision="$(echo "$user_skill_inventory" | jq -er .revision)"
user_skill_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data "$(jq -nc --arg entryId "$user_skill_id" --arg revision "$user_skill_revision" \
'{entryId:$entryId,revision:$revision,action:"enable"}')" \
http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$user_skill_task" '.userSkillOperation.taskId == $task and .userSkillOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.1
done
docker exec "$container" test -f /data/dsh/skills/smoke-managed-user-skill/SKILL.md
[ "$user_skill_dsh_pid" = "$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )" ]
user_skill_inventory="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills)"
user_skill_id="$(echo "$user_skill_inventory" | jq -er '.skills[] | select(.name == "smoke-managed-user-skill") | .entryId')"
user_skill_revision="$(echo "$user_skill_inventory" | jq -er .revision)"
user_skill_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data "$(jq -nc --arg entryId "$user_skill_id" --arg revision "$user_skill_revision" \
'{entryId:$entryId,revision:$revision,action:"delete"}')" \
http://127.0.0.1:3080/_dsh_platform/api/v1/user-skills/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$user_skill_task" '.userSkillOperation.taskId == $task and .userSkillOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.1
done
if docker exec "$container" test -e /data/dsh/skills/smoke-managed-user-skill; then
echo "deleted User Skill remained in the active user root" >&2
exit 1
fi
[ "$user_skill_dsh_pid" = "$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ' )" ]
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"user-skill.disable.completed"' >/dev/null
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"user-skill.enable.completed"' >/dev/null
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"user-skill.delete.completed"' >/dev/null
settings_document="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/settings-document)"
[ "$(echo "$settings_document" | jq -r .exists)" = false ]
settings_revision="$(echo "$settings_document" | jq -r .revision)"
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' --request PUT \
--data "$(jq -nc --arg revision "$settings_revision" '{content:"{}\n",revision:$revision}')" \
http://127.0.0.1:3080/_dsh_platform/api/v1/settings-document \
| jq -e '.content == "{}\n" and .exists == true' >/dev/null
docker exec "$container" sh -c '
[ "$(cat /data/dsh/settings.yaml)" = "{}" ]
[ "$(stat -c %U /data/dsh/settings.yaml)" = node ]
grep -F "@dsh-docker/settings-navigation" /run/dsh-platform/views/system-plugins/cordis.patch.yml >/dev/null
test -s /run/dsh-platform/views/system-plugins/packages/settings-navigation/lib/client.bundle.js
grep -F "data-dsh-settings-navigation-role" \
/run/dsh-platform/views/system-plugins/packages/settings-navigation/lib/client.bundle.js >/dev/null
grep -F "@dsh-docker/settings-document-editor" /run/dsh-platform/views/system-plugins/cordis.patch.yml >/dev/null
'
dsh_pid_before_plugin_changes="$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')"
plugin_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"id":"platform-management","action":"uninstall"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$plugin_task" \
'.systemPluginOperation.taskId == $task
and .systemPluginOperation.status == "failed"
and (.systemPluginOperation.error | contains("managed by the platform"))' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 50 ] || exit 1
sleep 0.2
done
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins \
| jq -e '.plugins[0] | .installed and .enabled and .protected' >/dev/null
recovery_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"id":"platform-management","action":"uninstall"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/recovery-action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$recovery_task" \
'.systemPluginOperation.taskId == $task and .systemPluginOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.2
done
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/console/ \
| grep -F 'Standalone console' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins \
| jq -e '.plugins[0] | (.installed | not) and (.enabled | not) and .protected' >/dev/null
recovery_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"id":"platform-management","action":"install"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/recovery-action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$recovery_task" \
'.systemPluginOperation.taskId == $task and .systemPluginOperation.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.2
done
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins \
| jq -e '.plugins[0] | .installed and .enabled and .protected' >/dev/null
plugin_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"id":"settings-document-editor","action":"disable"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$plugin_task" \
'.systemPluginOperation.taskId == $task and .systemPluginOperation.status == "success"
and .systemPluginOperation.restartRequired == true' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.2
done
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins \
| jq -e '.plugins[] | select(.id == "settings-document-editor") | .installed and (.enabled | not)' >/dev/null
docker exec "$container" grep -F '@dsh-docker/settings-document-editor' \
/run/dsh-platform/views/system-plugins/cordis.patch.yml >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e '.systemPluginOperation.restartRequired == true' >/dev/null
dsh_pid_before="$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')"
[ "$dsh_pid_before_plugin_changes" = "$dsh_pid_before" ]
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --request POST \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/discard \
| jq -e '.plugins[] | select(.id == "settings-document-editor") | .installed and .enabled and (.pendingRestart | not)' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e '.systemPluginOperation.restartRequired == false' >/dev/null
[ "$dsh_pid_before" = "$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')" ]
plugin_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--data '{"id":"settings-document-editor","action":"disable"}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/bundled-plugins/action | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$plugin_task" \
'.systemPluginOperation.taskId == $task and .systemPluginOperation.status == "success"
and .systemPluginOperation.restartRequired == true' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.2
done
restart_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --request POST \
http://127.0.0.1:3080/_dsh_platform/api/v1/restart-dsh | jq -r .taskId)"
started="$(date +%s%3N)"
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$restart_task" \
'.dshLifecycle.taskId == $task and .dshLifecycle.state == "running"
and .systemPluginOperation.restartRequired == false' >/dev/null; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "DSH plugin-change restart did not complete within the readiness timeout" >&2
exit 1
fi
sleep 0.2
done
dsh_pid_after="$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')"
[ "$dsh_pid_before" != "$dsh_pid_after" ]
if docker exec "$container" grep -F '@dsh-docker/settings-document-editor' \
/run/dsh-platform/views/system-plugins/cordis.patch.yml >/dev/null; then
exit 1
fi
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/ >/dev/null
docker exec "$container" sh -c "
grep -F '\"message\":\"component.stopping\"' /data/platform/logs/dsh-runtime.jsonl >/dev/null
grep -F '\"message\":\"component.ready\"' /data/platform/logs/dsh-runtime.jsonl >/dev/null
grep -F '\"message\":\"system-plugin.changes.discarded\"' /data/platform/logs/audit.jsonl >/dev/null
"
bootstrap_pid="$(docker exec "$container" pgrep -f '/platform/bootstrap/index.mjs')"
dsh_pid="$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')"
docker exec "$container" kill -9 "$dsh_pid"
started="$(date +%s%3N)"
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e '.dshLifecycle.state == "running" and .recoveryMode == null' >/dev/null \
&& [ "$(docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web')" != "$dsh_pid" ]; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "DSH automatic recovery did not complete within the readiness timeout" >&2
exit 1
fi
sleep 0.1
done
[ "$(docker exec "$container" pgrep -f '/platform/bootstrap/index.mjs')" = "$bootstrap_pid" ]
[ "$(docker inspect --format '{{.RestartCount}}' "$container")" = 0 ]
stop_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --request POST \
http://127.0.0.1:3080/_dsh_platform/api/v1/stop-dsh | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$stop_task" '.dshLifecycle.taskId == $task and .dshLifecycle.state == "stopped"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 50 ] || exit 1
sleep 0.1
done
sleep 1
if docker exec "$container" pgrep -f '^node /run/dsh-platform/views/runtime/bin/dsh web' >/dev/null; then
echo "DSH recovered after an explicit stop" >&2
exit 1
fi
wait_status="$(docker exec "$container" curl --silent --show-error --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' \
--output /tmp/dsh-smoke-wait.html --write-out '%{http_code}' \
'http://127.0.0.1:3080/_dsh_gateway/wait?return=%2Fsessions%2Fcurrent')"
[ "$wait_status" = 200 ]
if ! docker exec "$container" grep -E 'DeepSeek Harness is stopped|DeepSeek Harness 已停止' \
/tmp/dsh-smoke-wait.html >/dev/null; then
docker exec "$container" cat /tmp/dsh-smoke-wait.html >&2
exit 1
fi
docker exec -i --user node --env "SMOKE_MANAGEMENT_CSRF=$management_csrf" \
"$container" /usr/local/bin/node --input-type=module \
< container/test/standalone-file-management-smoke.mjs \
| jq -e '.range == "3456" and .searched >= 2 and .attributes == 488 and .dshUnavailable == true' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"source":"file-manager".*"message":"file-task.copy.completed"' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"source":"audit".*"message":"files.content.saved"' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"error":"size target changed".*"source":"audit".*"message":"files.size.failed"' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"source":"audit".*"message":"files.attributes.completed"' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Accept: text/html' --header 'Host: smoke.example' http://127.0.0.1:3080/ >/dev/null
if docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' 'http://127.0.0.1:3080/_dsh_platform/api/v1/logs?source=gateway&limit=5000' \
| jq -e 'any(.entries[]; .message == "gateway.upstream.failed" and .upstream == "dsh")' >/dev/null; then
echo "explicit DSH stop was reported as an upstream failure" >&2
exit 1
fi
managed_start_output="$(docker exec --user node "$container" dsh web --no-open)"
printf '%s\n' "$managed_start_output" | grep -F 'requested managed DSH start' >/dev/null
start_task="$(printf '%s\n' "$managed_start_output" | sed -n 's/.*(task \([^)]*\)).*/\1/p')"
[ -n "$start_task" ]
started="$(date +%s%3N)"
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$start_task" \
'.dshLifecycle.taskId == $task and .dshLifecycle.state == "running" and .recoveryMode == null' >/dev/null; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "managed DSH start did not complete within the readiness timeout" >&2
exit 1
fi
sleep 0.1
done
internal_status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' \
http://127.0.0.1:3079/)"
[ "$internal_status" -ge 200 ] && [ "$internal_status" -lt 500 ]
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/ >/dev/null
if docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' 'http://127.0.0.1:3080/_dsh_platform/api/v1/logs?source=gateway&limit=5000' \
| jq -e 'any(.entries[]; .message == "gateway.upstream.recovered" and .upstream == "dsh")' >/dev/null; then
echo "explicit DSH start was reported as outage recovery" >&2
exit 1
fi
docker exec -i --user node --env "SMOKE_MANAGEMENT_CSRF=$management_csrf" \
"$container" /usr/local/bin/node --input-type=module \
< container/test/standalone-recovery-smoke.mjs \
| jq -e '.faultNames == ["smoke-fault-one","smoke-fault-two"]' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"source":"terminal".*"message":"terminal.session.created"' >/dev/null
docker logs "$container" 2>&1 \
| grep -E '"source":"audit".*"message":"user-plugin.apply.completed"' >/dev/null
internal_status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' --noproxy '*' \
http://127.0.0.1:3079/)"
[ "$internal_status" -ge 200 ] && [ "$internal_status" -lt 500 ]
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--request PUT --data '{"enabled":false,"intervalSeconds":3600,"notificationsEnabled":false}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/automatic-check \
| jq -e '. == {"enabled":false,"intervalSeconds":3600,"notificationsEnabled":false}' >/dev/null
loopback_patch_count="$(docker exec "$container" sh -c \
"grep -F -o 'isLoopback: true,' /run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-client-connection/lib/client.js | wc -l")"
[ "$loopback_patch_count" = 1 ]
docker exec "$container" sh -c '
set -eu
pgrep -f "^/usr/local/bin/node /opt/dsh-platform/runtime/platform/stage0/index.mjs$" >/dev/null
pgrep -f "/opt/dsh-platform/seed/bootstrap/.*/platform/bootstrap/index.mjs" >/dev/null
pgrep -f "^node /run/dsh-platform/views/runtime/bin/dsh web --patch /run/dsh-platform/views/system-plugins/cordis.patch.yml --host 127.0.0.1 --port 3079$" >/dev/null
pgrep -f "^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/management/index.mjs$" >/dev/null
pgrep -f "^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/gateway/index.mjs$" >/dev/null
dsh-platform trust status | jq -e ".keyringGeneration == 1" >/dev/null
dsh-platform status | jq -e ".trust.keyringGeneration == 1 and .platformLayout == 1 and .current.source == \"image\"" >/dev/null
[ "$(stat -c %a /run/dsh-platform/recovery.sock)" = 600 ]
[ "$(stat -c %U /run/dsh-platform/recovery.sock)" = root ]
[ "$(readlink /usr/local/bin/dsh 2>/dev/null || true)" = "" ]
grep -F "exec /run/dsh-platform/views/runtime/bin/dsh" /usr/local/bin/dsh >/dev/null
'
docker exec "$container" sh -c 'printf runtime-reset-smoke > /data/dsh/runtime-reset-sentinel'
docker exec "$container" cp /data/platform/state/deployments/slots.json /tmp/slots-before-runtime-reset.json
runtime_hash="$(docker exec "$container" sha256sum /run/dsh-platform/views/runtime/package/package.json | cut -d ' ' -f 1)"
docker exec "$container" sh -c 'printf "\ncorrupt-runtime-smoke" >> /run/dsh-platform/views/runtime/package/package.json'
runtime_reset_task="$(docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --request POST \
http://127.0.0.1:3080/_dsh_platform/api/v1/runtime/reset | jq -r .taskId)"
attempt=0
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$runtime_reset_task" '.runtimeReset.taskId == $task and .runtimeReset.status == "success"' >/dev/null; do
attempt=$((attempt + 1))
if [ "$attempt" -ge 600 ]; then
docker logs "$container" >&2
echo "Runtime reset did not complete" >&2
exit 1
fi
sleep 0.2
done
[ "$(docker exec "$container" sha256sum /run/dsh-platform/views/runtime/package/package.json | cut -d ' ' -f 1)" = "$runtime_hash" ]
[ "$(docker exec "$container" cat /data/dsh/runtime-reset-sentinel)" = runtime-reset-smoke ]
docker exec "$container" jq -e --slurpfile before /tmp/slots-before-runtime-reset.json \
'.generation == ($before[0].generation + 1) and .previous == $before[0].previous' \
/data/platform/state/deployments/slots.json >/dev/null
docker exec "$container" dsh-platform status \
| jq -e '.current.source == "managed" and .recoveryMode == null' >/dev/null
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"runtime.reset.started"' >/dev/null
docker logs "$container" 2>&1 | grep -E '"source":"audit".*"message":"runtime.reset.completed"' >/dev/null
stage0_pid="$(docker exec "$container" pgrep -f '^/usr/local/bin/node /opt/dsh-platform/runtime/platform/stage0/index.mjs$')"
bootstrap_pid="$(docker exec "$container" pgrep -f '/opt/dsh-platform/seed/bootstrap/.*/platform/bootstrap/index.mjs')"
management_pid="$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/management/index.mjs$')"
gateway_pid="$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/gateway/index.mjs$')"
dsh_pid="$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ')"
docker exec --user node "$container" node -e '
const fs = require("node:fs")
const path = "/data/dsh/profiles/web/package.json"
const manifest = JSON.parse(fs.readFileSync(path, "utf8"))
manifest.dsh.profile.bundles.push("smoke-removed-plugin")
fs.writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n")
'
restart_task="$(docker exec "$container" dsh-platform restart | jq -r .taskId)"
started="$(date +%s%3N)"
until docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e --arg task "$restart_task" '.dshLifecycle.taskId == $task and .dshLifecycle.state == "running"' >/dev/null; do
now="$(date +%s%3N)"
if [ $((now - started)) -ge "$readiness_timeout_ms" ]; then
docker logs "$container" >&2
echo "DSH orphaned-bundle restart did not complete within the readiness timeout" >&2
exit 1
fi
sleep 0.2
done
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /opt/dsh-platform/runtime/platform/stage0/index.mjs$')" = "$stage0_pid" ]
[ "$(docker exec "$container" pgrep -f '/opt/dsh-platform/seed/bootstrap/.*/platform/bootstrap/index.mjs')" = "$bootstrap_pid" ]
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/management/index.mjs$')" = "$management_pid" ]
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/gateway/index.mjs$')" = "$gateway_pid" ]
[ "$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ')" != "$dsh_pid" ]
docker exec "$container" jq -e '.dsh.profile.bundles | index("smoke-removed-plugin") == null' \
/data/dsh/profiles/web/package.json >/dev/null
docker logs "$container" 2>&1 \
| grep -F 'dsh: removed orphaned profile bundle(s): smoke-removed-plugin' >/dev/null
dsh_pid="$(docker exec "$container" pgrep -o -f '^node /run/dsh-platform/views/runtime/bin/dsh web ')"
managed_restart_result="$(docker exec -i --user node --env CURRENT_DSH_PID="$dsh_pid" \
"$container" /usr/local/bin/node --input-type=module \
< "$(dirname "$0")/managed-lifecycle-restart-smoke.mjs")"
printf '%s\n' "$managed_restart_result" | jq -e \
'.helperExitCode == 0 and .sawRestarting == true and .dshCount == 1
and (.taskId | type == "string" and length > 0) and .oldPid != .newPid' >/dev/null
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /opt/dsh-platform/runtime/platform/stage0/index.mjs$')" = "$stage0_pid" ]
[ "$(docker exec "$container" pgrep -f '/opt/dsh-platform/seed/bootstrap/.*/platform/bootstrap/index.mjs')" = "$bootstrap_pid" ]
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/management/index.mjs$')" = "$management_pid" ]
[ "$(docker exec "$container" pgrep -f '^/usr/local/bin/node /run/dsh-platform/views/bootstrap/control-plane/services/gateway/index.mjs$')" = "$gateway_pid" ]
[ "$(docker exec "$container" pgrep -c -f '^node /run/dsh-platform/views/runtime/bin/dsh web ')" = 1 ]
if docker exec --user node "$container" curl --silent --unix-socket /run/dsh-platform/recovery.sock \
http://localhost/v1/status >/dev/null 2>&1; then
echo "node user unexpectedly accessed the Stage-0 recovery socket" >&2
exit 1
fi
internal_status="$(docker exec "$container" curl --silent --output /dev/null --write-out '%{http_code}' --noproxy '*' \
http://127.0.0.1:3079/)"
[ "$internal_status" -ge 200 ] && [ "$internal_status" -lt 500 ]
container_ip="$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")"
if docker exec "$container" curl --fail --silent --max-time 2 --noproxy '*' \
"http://${container_ip}:3079/" >/dev/null 2>&1; then
echo "DSH unexpectedly accepts non-loopback connections on port 3079" >&2
exit 1
fi
docker rm -f "$container" >/dev/null
docker run --detach --name "$container" \
--group-add dsh-sudo-true \
--env DSH_TRUSTED_HOSTS=smoke.example \
--volume "$platform_volume:/data/platform" \
--volume "$home_volume:/data/dsh" \
"$image" >/dev/null
wait_control_ready
startup_two="$(wait_platform_ready)"
establish_sessions login
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' --header 'Content-Type: application/json' \
--request PUT --data '{"enabled":false,"intervalSeconds":3600,"notificationsEnabled":false}' \
http://127.0.0.1:3080/_dsh_platform/api/v1/automatic-check >/dev/null
docker exec --user node "$container" sh -c 'printf platform > /data/platform/state/updater/smoke && printf home > /data/dsh/smoke'
stop_task="$(docker exec "$container" dsh-platform stop | jq -r .taskId)"
attempt=0
until docker exec "$container" dsh-platform status \
| jq -e --arg task "$stop_task" '.dshLifecycle.taskId == $task and .dshLifecycle.state == "stopped"' >/dev/null; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 0.2
done
docker exec "$container" curl --fail --silent --noproxy '*' \
http://127.0.0.1:3080/_dsh_gateway/health >/dev/null
if health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$container")" \
&& [ -n "$health_status" ] && [ "$health_status" != healthy ] && [ "$health_status" != starting ]; then
echo "container health became $health_status after an intentional DSH stop" >&2
exit 1
fi
docker exec "$container" chown root:root /data/platform/cache/npm
docker restart "$container" >/dev/null
wait_control_ready
startup_three="$(wait_platform_ready)"
establish_sessions login
docker exec "$container" sh -c '[ "$(stat -c %u:%g /data/platform/cache/npm)" = 1000:1000 ]'
docker exec "$container" sh -c '[ "$(cat /data/platform/state/updater/smoke)" = platform ] && [ "$(cat /data/dsh/smoke)" = home ]'
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status \
| jq -e '.automaticCheck == {"enabled":false,"intervalSeconds":3600,"notificationsEnabled":false}
and .dshLifecycle.state == "running"' >/dev/null
echo "Cold readiness (ms): $startup_one, $startup_two, $startup_three"
docker exec "$container" dsh-platform channel experimental >/dev/null
[ "$(docker exec "$container" dsh-platform channel)" = experimental ]
attempt=0
until docker exec --user node "$container" curl --fail --silent --unix-socket /run/dsh-platform/bootstrap.sock \
http://localhost/v1/status >/dev/null 2>&1; do
attempt=$((attempt + 1))
[ "$attempt" -lt 60 ] || exit 1
sleep 1
done
docker exec --user node "$container" curl --fail --silent --unix-socket /run/dsh-platform/bootstrap.sock \
--request POST http://localhost/v1/components/dsh-runtime/suspend >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/console/ \
| grep -F 'DSH Management Console' >/dev/null
docker exec "$container" curl --fail --silent --cookie "$session_cookie" \
--header 'Origin: http://smoke.example' --header "X-DSH-CSRF: $management_csrf" \
--header 'Host: smoke.example' http://127.0.0.1:3080/_dsh_platform/api/v1/status >/dev/null
docker exec -i --user node "$container" /usr/local/bin/node --input-type=module <<'NODE'
import { readFile } from 'node:fs/promises'
import { LocalApiClient } from '/run/dsh-platform/views/bootstrap/control-plane/modules/updater/lib/client.mjs'
import { UpdateJournal } from '/run/dsh-platform/views/bootstrap/control-plane/modules/updater/lib/journal.mjs'
import { PersistentStateSnapshots } from '/run/dsh-platform/views/bootstrap/control-plane/modules/updater/lib/snapshots.mjs'
const bootstrap = new LocalApiClient('/run/dsh-platform/bootstrap.sock')
const { record } = await bootstrap.request('GET', '/v1/deployments/current')
const metadata = JSON.parse(await readFile('/run/dsh-platform/views/runtime/package/package.json', 'utf8'))
const snapshots = new PersistentStateSnapshots({ root: '/data/platform/store/snapshots', sourceRoot: '/data/dsh' })
await snapshots.create({
id: 'smoke-recovery', runtimeId: record.id,
environmentVersion: record.environmentVersion, dshVersion: metadata.version,
})
const journal = new UpdateJournal('/data/platform/state/updater/transaction.json')
await journal.begin({
transactionId: 'smoke-recovery', mode: 'experimental',
from: {
dsh: metadata.version, environment: record.environmentVersion, runtime: record.id,
dataSnapshot: null, receiptTokens: record.receiptTokens,
},
to: { dsh: metadata.version, environment: record.environmentVersion, runtime: record.id },
})
await journal.transition('candidate-ready', { receiptTokens: record.receiptTokens })
await journal.transition('suspended')
await journal.transition('snapshot-created', { snapshotId: 'smoke-recovery' })
await journal.transition('switched')
await journal.transition('probation', { probationUntil: '2099-01-01T00:00:00.000Z' })
NODE
docker exec --user node "$container" sh -c 'printf changed-after-snapshot > /data/dsh/smoke'
docker restart "$container" >/dev/null
attempt=0
until docker exec "$container" sh -c '
test "$(cat /data/dsh/smoke 2>/dev/null)" = home \
&& test "$(jq -r .phase /data/platform/state/updater/transaction.json 2>/dev/null)" = rolled-back \
&& test "$(dsh-platform channel 2>/dev/null)" = experimental
' >/dev/null 2>&1; do
attempt=$((attempt + 1))
if [ "$attempt" -ge 60 ]; then
docker logs "$container" >&2
echo "Experimental restart recovery did not complete" >&2
exit 1
fi
sleep 0.2
done
docker exec "$container" sh -c '
set -eu
[ "$(cat /data/dsh/smoke)" = home ]
[ "$(jq -r .phase /data/platform/state/updater/transaction.json)" = rolled-back ]
[ "$(dsh-platform channel)" = experimental ]
'
docker logs "$container" 2>&1 | node -e '
const input = require("node:fs").readFileSync(0, "utf8").split("\n")
const entries = input.flatMap(line => { try { return [JSON.parse(line)] } catch { return [] } })
const recovery = entries.filter(entry => entry.message === "startup-recovery.completed").at(-1)
const dshStart = entries.filter(entry => entry.message === "component.starting" && entry.componentId === "dsh-runtime").at(-1)
if (recovery?.updatePhase !== "rolled-back" || dshStart === undefined
|| Date.parse(recovery.timestamp) > Date.parse(dshStart.timestamp)) {
throw new Error("DSH started before interrupted update recovery completed")
}
'
cleanup
trap - EXIT INT TERM
platform_volume="dsh-platform-legacy-$$"
home_volume="dsh-home-legacy-$$"
trap cleanup EXIT INT TERM
docker run --rm --entrypoint sh \
--volume "$platform_volume:/data/platform" \
--volume "$home_volume:/data/dsh" \
"$image" -c 'mkdir -p /data/platform/runtime && printf preserved > /data/dsh/sentinel'
set +e
legacy_output="$(timeout 15s docker run --rm \
--volume "$platform_volume:/data/platform" \
--volume "$home_volume:/data/dsh" \
"$image" 2>&1)"
status=$?
set -e
[ "$status" -ne 0 ]
echo "$legacy_output" | grep -F 'clear only /data/platform' >/dev/null
echo "$legacy_output" | grep -F 'Do not delete /data/dsh' >/dev/null
docker run --rm --entrypoint sh --volume "$home_volume:/data/dsh" "$image" \
-c '[ "$(cat /data/dsh/sentinel)" = preserved ]'
cleanup
trap - EXIT INT TERM
echo "Container smoke checks passed"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu
if [ "$#" -ne 1 ]; then
echo "usage: $0 image" >&2
exit 64
fi
image="$1"
docker run --rm --entrypoint sh "$image" -c '
set -eu
for package in \
bash-completion build-essential dnsutils file htop iproute2 iputils-ping less lsof \
nano netcat-openbsd openssl pkg-config python3 python3-venv rsync tmux tree unzip \
vim wget xz-utils zip; do
dpkg-query --show "$package" >/dev/null
done
for command in \
bash curl dig file g++ git htop ip jq less lsof make nano nc openssl ping \
pkg-config ps python3 rg rsync ssh tmux tree unzip uv uvx vim wget xz zip; do
command -v "$command" >/dev/null
done
[ "$(command -v python3)" = /usr/bin/python3 ]
! command -v pip >/dev/null
! command -v pip3 >/dev/null
[ ! -e /opt/dsh-python ]
uv --version | grep "^uv 0\.11\.32 (x86_64-unknown-linux-gnu)$" >/dev/null
uvx --version | grep "^uvx 0\.11\.32 (x86_64-unknown-linux-gnu)$" >/dev/null
grep '\''^python-downloads = "manual"$'\'' /etc/uv/uv.toml >/dev/null
uv run --isolated --no-python-downloads -- python -c \
"import sys; assert sys.prefix != sys.base_prefix"
venv="$(mktemp -d)/venv"
python3 -m venv "$venv"
"$venv/bin/python" --version >/dev/null
'
echo "Devtools smoke checks passed"
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict'
import { readdir, readFile } from 'node:fs/promises'
import { request } from 'node:http'
import { spawn } from 'node:child_process'
const MANAGEMENT_SOCKET = '/run/dsh-platform/management-cli.sock'
const DSH_EXECUTABLE = '/run/dsh-platform/views/runtime/bin/dsh'
const oldPid = Number.parseInt(process.env.CURRENT_DSH_PID ?? '', 10)
assert.ok(Number.isInteger(oldPid) && oldPid > 1, 'CURRENT_DSH_PID must identify the running DSH')
function delay(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function status() {
return new Promise((resolve, reject) => {
const req = request({
socketPath: MANAGEMENT_SOCKET,
method: 'GET',
path: '/_dsh_platform/api/v1/status',
}, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.on('end', () => {
if (response.statusCode !== 200) {
reject(new Error(`Management status returned HTTP ${String(response.statusCode)}`))
return
}
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
} catch (error) {
reject(error)
}
})
})
req.setTimeout(2_000, () => req.destroy(new Error('Management status timed out')))
req.once('error', reject)
req.end()
})
}
async function dshPids() {
const entries = await readdir('/proc', { withFileTypes: true })
const matches = []
await Promise.all(entries.filter(entry => entry.isDirectory() && /^\d+$/.test(entry.name)).map(async entry => {
try {
const command = await readFile(`/proc/${entry.name}/cmdline`)
const args = command.toString('utf8').split('\0').filter(Boolean)
if (args[1] === DSH_EXECUTABLE && args[2] === 'web') matches.push(Number.parseInt(entry.name, 10))
} catch {}
}))
return matches.sort((left, right) => left - right)
}
function runUnauthorizedHelper() {
return new Promise((resolve, reject) => {
const child = spawn(DSH_EXECUTABLE, ['web', '--no-open'], {
detached: true,
env: { ...process.env, DSH_PLATFORM_LAUNCH_TOKEN: '' },
stdio: ['ignore', 'pipe', 'pipe'],
})
const output = []
child.stdout.on('data', chunk => output.push(chunk))
child.stderr.on('data', chunk => output.push(chunk))
const timeout = setTimeout(() => {
child.kill('SIGKILL')
reject(new Error('unauthorized detached DSH helper did not exit'))
}, 10_000)
child.once('error', error => {
clearTimeout(timeout)
reject(error)
})
child.once('exit', code => {
clearTimeout(timeout)
resolve({ code, output: Buffer.concat(output).toString('utf8') })
})
})
}
const before = await status()
assert.equal(before.dshLifecycle?.state, 'running')
assert.deepEqual(await dshPids(), [oldPid])
const helper = await runUnauthorizedHelper()
assert.equal(helper.code, 0)
assert.match(helper.output, /managed DSH is running; no second Web instance was started/)
assert.deepEqual(await dshPids(), [oldPid])
process.kill(oldPid, 'SIGTERM')
let taskId = null
let sawRestarting = false
let finalStatus = null
let newPids = []
const deadline = Date.now() + 25_000
while (Date.now() < deadline) {
const current = await status()
if (current.dshLifecycle?.taskId && current.dshLifecycle.taskId !== before.dshLifecycle?.taskId) {
taskId = current.dshLifecycle.taskId
}
if (current.dshLifecycle?.state === 'restarting') sawRestarting = true
newPids = await dshPids()
if (taskId !== null && current.dshLifecycle?.state === 'running'
&& newPids.length === 1 && newPids[0] !== oldPid) {
finalStatus = current
break
}
await delay(100)
}
assert.ok(taskId, 'unregistered SIGTERM did not create a managed restart task')
assert.equal(sawRestarting, true, 'managed restart state was not observed')
assert.equal(finalStatus?.dshLifecycle?.state, 'running')
assert.equal(finalStatus?.recoveryMode, null)
assert.equal(newPids.length, 1)
assert.notEqual(newPids[0], oldPid)
process.stdout.write(`${JSON.stringify({
helperExitCode: helper.code,
oldPid,
newPid: newPids[0],
taskId,
sawRestarting,
dshCount: newPids.length,
})}\n`)
@@ -0,0 +1,73 @@
#!/bin/sh
set -eu
if [ "$#" -ne 1 ]; then
echo "usage: $0 <container>" >&2
exit 64
fi
container="$1"
profile=/data/dsh/profiles/web
legacy_store=/workspace/.pnpm-store
stable_store=/data/dsh/.pnpm-store
fixture=/workspace/profile-package-smoke
docker exec "$container" dsh-platform stop --wait >/dev/null
docker exec --user node "$container" sh -c "
set -eu
test -f '$profile/package.json'
mkdir -p '$legacy_store'
if [ -d '$stable_store' ]; then cp -a '$stable_store/.' '$legacy_store/'; fi
"
docker exec --user node --env PROFILE="$profile" --env LEGACY_STORE="$legacy_store" \
"$container" node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs"
const path = process.env.PROFILE + "/pnpm-workspace.yaml"
const source = readFileSync(path, "utf8")
const line = `storeDir: ${JSON.stringify(process.env.LEGACY_STORE)}\n`
const pattern = /^storeDir:[^\r\n]*(?:\r?\n|$)/m
writeFileSync(path, pattern.test(source)
? source.replace(pattern, line)
: source.replace(/(?:\r?\n)?$/, `\n\n${line}`))
'
docker exec --user node "$container" sh -c "
set -eu
rm -rf '$profile/node_modules'
cd '$profile'
pnpm install --offline --frozen-lockfile --reporter=append-only >/dev/null
"
docker exec --user node --env PROFILE="$profile" --env LEGACY_STORE="$legacy_store" \
"$container" node -e '
const value = JSON.parse(require("fs").readFileSync(process.env.PROFILE + "/node_modules/.modules.yaml", "utf8"))
if (!value.storeDir.startsWith(process.env.LEGACY_STORE + "/")) throw new Error(value.storeDir)
'
docker exec "$container" dsh-platform start --wait >/dev/null
docker exec --user node --env PROFILE="$profile" --env STABLE_STORE="$stable_store" \
"$container" node -e '
const value = JSON.parse(require("fs").readFileSync(process.env.PROFILE + "/node_modules/.modules.yaml", "utf8"))
if (!value.storeDir.startsWith(process.env.STABLE_STORE + "/")) throw new Error(value.storeDir)
'
docker exec --user node "$container" sh -c "
set -eu
mkdir -p '$fixture'
printf '%s\n' '{\"name\":\"profile-package-smoke\",\"version\":\"1.0.0\",\"type\":\"module\",\"main\":\"index.mjs\",\"dsh\":{\"bundle\":{\"patch\":\"./cordis.patch.yml\"}}}' > '$fixture/package.json'
printf '%s\n' 'export default function () {}' > '$fixture/index.mjs'
printf '%s\n' '- insert:' ' - id: profile-package-smoke' ' name: file://$fixture/index.mjs' > '$fixture/cordis.patch.yml'
"
docker exec \
--env npm_config_userconfig=/root/.config/npm/npmrc \
--env npm_config_cache=/root/.npm \
"$container" dsh plugin --profile web add "$fixture" >/dev/null
docker exec "$container" sh -c "
set -eu
test \"\$(stat -c %U '$profile/package.json')\" = node
test \"\$(stat -c %U '$profile/pnpm-lock.yaml')\" = node
test \"\$(stat -c %U '$profile/node_modules/profile-package-smoke')\" = node
"
docker exec "$container" dsh plugin --profile web remove profile-package-smoke >/dev/null
docker exec --user node "$container" rm -rf "$fixture"
docker logs "$container" 2>&1 \
| grep -F "dsh: migrated Profile package storage from $legacy_store/" >/dev/null
@@ -0,0 +1,204 @@
import assert from 'node:assert/strict'
import { mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
import { request as httpRequest } from 'node:http'
const API = 'http://127.0.0.1:3080/_dsh_platform/api/v1'
const cookie = (await readFile('/tmp/dsh-smoke.cookies', 'utf8'))
.split('\n')
.map(line => line.startsWith('#HttpOnly_') ? line.slice('#HttpOnly_'.length) : line)
.filter(line => line !== '' && !line.startsWith('#'))
.map(line => line.split('\t'))
.filter(fields => fields.length === 7)
.map(fields => `${fields[5]}=${fields[6]}`)
.join('; ')
const headers = {
cookie,
host: 'smoke.example',
origin: 'http://smoke.example',
'x-dsh-csrf': process.env.SMOKE_MANAGEMENT_CSRF,
}
async function request(path, { method = 'GET', body, raw = false, extraHeaders = {} } = {}) {
const encoded = body === undefined ? undefined : raw ? body : Buffer.from(JSON.stringify(body))
const response = await new Promise((resolve, reject) => {
const outgoing = httpRequest(`${API}/${path}`, {
method,
headers: {
...headers,
...extraHeaders,
...(body !== undefined && !raw ? { 'content-type': 'application/json' } : {}),
...(encoded === undefined ? {} : { 'content-length': Buffer.byteLength(encoded) }),
},
}, incoming => {
const chunks = []
incoming.on('data', chunk => chunks.push(chunk))
incoming.once('end', () => resolve(new Response(Buffer.concat(chunks), {
status: incoming.statusCode,
headers: incoming.headers,
})))
})
outgoing.once('error', reject)
outgoing.end(encoded)
})
if (!response.ok) throw new Error(`${method} ${path} returned ${String(response.status)}: ${await response.text()}`)
return raw ? response : response.json()
}
async function task(body) {
const started = await request('files/tasks', { method: 'POST', body })
for (let attempt = 0; attempt < 200; attempt += 1) {
const state = await request(`files/tasks/${started.taskId}`)
if (state.status === 'success') return state
if (!['queued', 'running'].includes(state.status)) throw new Error(`file task ${state.taskId} ${state.status}: ${state.error}`)
await new Promise(resolve => setTimeout(resolve, 20))
}
throw new Error(`file task ${started.taskId} timed out`)
}
async function item(path) { return request(`files/stat?path=${encodeURIComponent(path)}`) }
const config = await request('files/config')
assert.equal(config.privileged, true)
const rootFile = `/root/dsh-maintenance-smoke-${String(process.pid)}.txt`
await request('files/content', {
method: 'PUT', body: { path: rootFile, revision: null, content: 'root maintenance\n', create: true },
})
const rootItem = await item(rootFile)
assert.equal(rootItem.user, 'root')
assert.equal(rootItem.group, 'root')
assert.equal((await request(`files/content?path=${encodeURIComponent(rootFile)}`)).content, 'root maintenance\n')
await task({ operation: 'delete', sources: [{ path: rootItem.path, revision: rootItem.revision }] })
await assert.rejects(item(rootFile), /returned 404/)
const workspaceRoot = `/workspace/file-management-smoke-${String(process.pid)}`
const dshRoot = `/data/dsh/file-management-smoke-${String(process.pid)}`
await mkdir(workspaceRoot)
await task({ operation: 'mkdir', destination: dshRoot })
const uploadedPath = `${workspaceRoot}/uploaded-中文.txt`
const upload = await request(`files/upload?path=${encodeURIComponent(uploadedPath)}&conflict=reject`, {
method: 'POST', body: Buffer.from('0123456789'), raw: true, extraHeaders: { 'content-length': '10' },
}).then(response => response.json())
assert.equal(upload.size, 10)
const range = await request(`files/download?path=${encodeURIComponent(uploadedPath)}&revision=${encodeURIComponent(upload.revision)}`, {
raw: true, extraHeaders: { range: 'bytes=3-6' },
})
assert.equal(range.status, 206)
assert.equal(await range.text(), '3456')
const before = await request(`files/content?path=${encodeURIComponent(uploadedPath)}`)
const saved = await request('files/content', {
method: 'PUT', body: { path: uploadedPath, revision: before.revision, content: 'edited in recovery mode\n' },
})
assert.notEqual(saved.revision, before.revision)
assert.equal((await request(`files/content?path=${encodeURIComponent(uploadedPath)}`)).content, 'edited in recovery mode\n')
const copyRoot = `${workspaceRoot}/copies`
await task({ operation: 'mkdir', destination: copyRoot })
const source = await item(uploadedPath)
await task({ operation: 'copy', sources: [{ path: source.path, revision: source.revision }], destination: copyRoot, conflict: 'reject' })
const copiedPath = `${copyRoot}/uploaded-中文.txt`
assert.equal((await request(`files/content?path=${encodeURIComponent(copiedPath)}`)).content, 'edited in recovery mode\n')
const linkPath = `${workspaceRoot}/relative-link`
await symlink('uploaded-中文.txt', linkPath)
const link = await item(linkPath)
assert.equal(link.type, 'symlink')
await task({ operation: 'copy', sources: [{ path: link.path, revision: link.revision }], destination: copyRoot, conflict: 'reject' })
assert.equal((await item(`${copyRoot}/relative-link`)).type, 'symlink')
const movedRoot = `${workspaceRoot}/moved`
await task({ operation: 'mkdir', destination: movedRoot })
const copied = await item(copiedPath)
await task({ operation: 'move', sources: [{ path: copied.path, revision: copied.revision }], destination: movedRoot, conflict: 'reject' })
assert.equal((await request(`files/content?path=${encodeURIComponent(`${movedRoot}/uploaded-中文.txt`)}`)).content, 'edited in recovery mode\n')
const dshFile = `${dshRoot}/recovery.json`
await request('files/content', { method: 'PUT', body: { path: dshFile, revision: null, content: '{"recovered":true}\n', create: true } })
assert.equal((await request(`files/content?path=${encodeURIComponent(dshFile)}`)).content, '{"recovered":true}\n')
const archiveSource = `${workspaceRoot}/archive-source`
await task({ operation: 'mkdir', destination: archiveSource })
await request('files/content', {
method: 'PUT', body: { path: `${archiveSource}/entry.txt`, revision: null, content: 'archive fixture\n', create: true },
})
for (const [format, extension] of [['zip', 'zip'], ['7z', '7z'], ['tar.gz', 'tar.gz']]) {
const source = await item(archiveSource)
const archivePath = `${workspaceRoot}/fixture-${format}.${extension}`
await task({
operation: 'archive', archiveFormat: format,
sources: [{ path: source.path, revision: source.revision }], destination: archivePath,
})
const extraction = `${workspaceRoot}/extracted-${format}`
await task({ operation: 'mkdir', destination: extraction })
const archive = await item(archivePath)
await task({
operation: 'extract', archiveFormat: format,
sources: [{ path: archive.path, revision: archive.revision }], destination: extraction,
})
assert.equal((await request(`files/content?path=${encodeURIComponent(`${extraction}/archive-source/entry.txt`)}`)).content, 'archive fixture\n')
}
const directoryDownload = await request(`files/download?path=${encodeURIComponent(archiveSource)}&revision=${encodeURIComponent((await item(archiveSource)).revision)}`, { raw: true })
assert.equal(directoryDownload.headers.get('content-disposition').includes('archive-source.zip'), true)
assert.deepEqual([...new Uint8Array(await directoryDownload.arrayBuffer()).slice(0, 2)], [0x50, 0x4b])
const pagingRoot = `${workspaceRoot}/paging`
await mkdir(pagingRoot)
await Promise.all(Array.from({ length: 125 }, (_, index) => writeFile(`${pagingRoot}/${String(index).padStart(3, '0')}.txt`, 'x')))
const firstPage = await request(`files/list?path=${encodeURIComponent(pagingRoot)}&limit=50`)
assert.equal(firstPage.entries.length, 50)
assert.equal(firstPage.total, 125)
assert.equal(typeof firstPage.nextCursor, 'string')
const secondPage = await request(`files/list?path=${encodeURIComponent(pagingRoot)}&limit=50&cursor=${encodeURIComponent(firstPage.nextCursor)}`)
assert.equal(secondPage.entries.length, 50)
assert.equal(secondPage.entries[0].name, '050.txt')
const searchRoot = await request(`files/list?path=${encodeURIComponent(workspaceRoot)}`)
assert.ok(searchRoot.entries.every(entry => typeof entry.user === 'string' && typeof entry.group === 'string'))
const workspaceBeforeAttributes = await item(workspaceRoot)
await task({
operation: 'attributes',
sources: [{ path: workspaceRoot, revision: workspaceBeforeAttributes.revision }],
attributes: { user: 'root', group: 'root', mode: '0750', recursive: true },
})
const workspaceAttributes = await item(workspaceRoot)
assert.equal(workspaceAttributes.user, 'root')
assert.equal(workspaceAttributes.group, 'root')
assert.equal(workspaceAttributes.mode, 0o750)
assert.equal((await item(uploadedPath)).mode, 0o750)
const sizeTask = await request('files/tasks', { method: 'POST', body: { operation: 'size', path: workspaceRoot, revision: workspaceAttributes.revision } })
let sizeResult
for (let attempt = 0; attempt < 100; attempt += 1) {
sizeResult = await request(`files/tasks/${sizeTask.taskId}`)
if (sizeResult.status !== 'running') break
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.equal(sizeResult.status, 'success')
assert.ok(sizeResult.bytes >= 10)
const staleSize = await request('files/tasks', { method: 'POST', body: { operation: 'size', path: workspaceRoot, revision: searchRoot.revision } })
let staleSizeResult
for (let attempt = 0; attempt < 100; attempt += 1) {
staleSizeResult = await request(`files/tasks/${staleSize.taskId}`)
if (staleSizeResult.status !== 'running') break
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.equal(staleSizeResult.status, 'failed')
assert.match(staleSizeResult.error, /changed/)
const searchRootAfterAttributes = await request(`files/list?path=${encodeURIComponent(workspaceRoot)}`)
const search = await request('files/tasks', { method: 'POST', body: { operation: 'search', path: workspaceRoot, revision: searchRootAfterAttributes.revision, query: 'uploaded' } })
let searchResult
for (let attempt = 0; attempt < 100; attempt += 1) {
searchResult = await request(`files/tasks/${search.taskId}?limit=1000`)
if (searchResult.status !== 'running') break
await new Promise(resolve => setTimeout(resolve, 10))
}
assert.equal(searchResult.status, 'success')
assert.ok(searchResult.results.length >= 2)
const workspace = await item(workspaceRoot)
const dsh = await item(dshRoot)
await task({ operation: 'delete', sources: [{ path: workspace.path, revision: workspace.revision }, { path: dsh.path, revision: dsh.revision }] })
await assert.rejects(item(workspaceRoot), /returned 404/)
await assert.rejects(item(dshRoot), /returned 404/)
console.log(JSON.stringify({ range: '3456', searched: searchResult.results.length, directoryBytes: sizeResult.bytes, attributes: workspaceAttributes.mode, archives: 3, paged: firstPage.total, dshUnavailable: true }))
@@ -0,0 +1,231 @@
import assert from 'node:assert/strict'
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
import { request as httpRequest } from 'node:http'
import { join } from 'node:path'
import WebSocket from '/run/dsh-platform/views/bootstrap/control-plane/services/management/node_modules/ws/wrapper.mjs'
const API = '/_dsh_platform/api/v1/'
const cookie = (await readFile('/tmp/dsh-smoke.cookies', 'utf8'))
.split('\n')
.map(line => line.startsWith('#HttpOnly_') ? line.slice('#HttpOnly_'.length) : line)
.filter(line => line !== '' && !line.startsWith('#'))
.map(line => line.split('\t'))
.filter(fields => fields.length === 7)
.map(fields => `${fields[5]}=${fields[6]}`)
.join('; ')
const headers = Object.freeze({
cookie,
host: 'smoke.example',
origin: 'http://smoke.example',
'x-dsh-csrf': process.env.SMOKE_MANAGEMENT_CSRF,
})
function request(method, path, body) {
return new Promise((resolve, reject) => {
const encoded = body === undefined ? undefined : Buffer.from(JSON.stringify(body))
const outgoing = httpRequest({
host: '127.0.0.1', port: 3080, method, path,
headers: {
...headers,
...(encoded === undefined ? {} : { 'content-type': 'application/json', 'content-length': encoded.byteLength }),
},
}, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.once('end', () => {
const text = Buffer.concat(chunks).toString('utf8')
const value = text === '' ? undefined : JSON.parse(text)
if ((response.statusCode ?? 500) >= 400) {
const error = new Error(value?.error ?? `HTTP ${String(response.statusCode)}`)
error.statusCode = response.statusCode
reject(error)
} else resolve(value)
})
})
outgoing.once('error', reject)
outgoing.end(encoded)
})
}
async function waitFor(check, label, attempts = 300) {
let last
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
last = await check()
if (last) return last
} catch (error) {
last = error
}
await new Promise(resolve => setTimeout(resolve, 200))
}
throw new Error(`${label} timed out: ${last instanceof Error ? last.message : JSON.stringify(last)}`)
}
function connectTerminal(sessionId) {
const socket = new WebSocket(`ws://127.0.0.1:3080${API}terminal/sessions/${sessionId}/stream`, {
headers,
})
let output = ''
const waiters = new Set()
socket.on('message', data => {
const message = JSON.parse(data.toString())
if (message.type === 'output') output += message.data
for (const waiter of waiters) waiter()
})
const opened = new Promise((resolve, reject) => {
socket.once('open', resolve)
socket.once('error', reject)
})
const waitOutput = async (marker, occurrences = 1) => {
for (let attempt = 0; attempt < 300; attempt += 1) {
if (output.split(marker).length - 1 >= occurrences) return output
await new Promise(resolve => {
const timer = setTimeout(() => { waiters.delete(done); resolve() }, 100)
const done = () => { clearTimeout(timer); waiters.delete(done); resolve() }
waiters.add(done)
})
}
throw new Error(`terminal output did not contain ${marker}: ${output}`)
}
return { socket, opened, waitOutput }
}
async function waitTask(kind, taskId, expected) {
let lastOperation
try {
return await waitFor(async () => {
const status = await request('GET', `${API}status`)
const operation = status[kind]
lastOperation = operation
const operationState = operation?.status ?? operation?.state
return operation?.taskId === taskId && expected.includes(operationState) ? operation : false
}, `${kind} ${taskId}`, 600)
} catch (error) {
throw new Error(`${error.message}; last operation: ${JSON.stringify(lastOperation)}`)
}
}
const terminal = await request('POST', `${API}terminal/sessions`, { cols: 100, rows: 32 })
assert.equal(terminal.privileged, true)
const first = connectTerminal(terminal.sessionId)
await first.opened
first.socket.send(JSON.stringify({
type: 'input',
data: "printf 'terminal-before-restart\\n'; printf 'uid=%s\\n' \"$(id -u)\"; printf 'gids=%s\\n' \"$(id -G)\"; printf 'cwd=%s\\n' \"$PWD\"; printf 'home=%s\\n' \"$HOME\"; printf 'dsh=%s\\n' \"$DSH_HOME\"; sudo -n true && printf 'sudo=ok\\n'\n",
}))
const identity = await first.waitOutput('sudo=ok', 3)
assert.match(identity, /terminal-before-restart/)
assert.match(identity, /uid=0/)
assert.match(identity, /gids=.+/)
assert.match(identity, /cwd=\/root/)
assert.match(identity, /home=\/root/)
assert.match(identity, /dsh=\/data\/dsh/)
const restart = await request('POST', `${API}restart-dsh`)
await waitTask('dshLifecycle', restart.taskId, ['running'])
first.socket.send(JSON.stringify({ type: 'input', data: "printf 'terminal-after-restart-中文\\n'\n" }))
await first.waitOutput('terminal-after-restart-中文', 2)
first.socket.close()
await new Promise(resolve => first.socket.once('close', resolve))
const second = connectTerminal(terminal.sessionId)
await second.opened
await second.waitOutput('terminal-after-restart-中文')
await request('DELETE', `${API}terminal/sessions/${terminal.sessionId}`)
await new Promise(resolve => second.socket.once('close', resolve))
await assert.rejects(request('GET', `${API}terminal/sessions/${terminal.sessionId}`), error => error.statusCode === 404)
const profileRoot = '/data/dsh/profiles/web'
const manifestPath = join(profileRoot, 'package.json')
const faultNames = ['smoke-fault-one', 'smoke-fault-two']
const pluginRoots = new Map()
for (const name of faultNames) {
const source = join('/workspace/smoke-user-plugin-sources', name)
const installed = join(profileRoot, 'node_modules', name)
pluginRoots.set(name, source)
await mkdir(source, { recursive: true })
await writeFile(join(source, 'package.json'), `${JSON.stringify({
name,
version: '1.0.0',
type: 'module',
main: './index.mjs',
dsh: { bundle: { patch: './cordis.patch.yml' } },
}, null, 2)}\n`)
await writeFile(join(source, 'index.mjs'), `throw new Error('${name} startup failure')\n`)
await writeFile(join(source, 'cordis.patch.yml'), `- insert:\n - id: ${name}\n name: ${JSON.stringify(`file://${join(source, 'index.mjs')}`)}\n`)
await cp(source, installed, { recursive: true })
}
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
manifest.dependencies ??= {}
manifest.dsh ??= {}
manifest.dsh.profile ??= {}
manifest.dsh.profile.bundles ??= []
for (const name of faultNames) {
manifest.dependencies[name] = `file:${pluginRoots.get(name)}`
if (!manifest.dsh.profile.bundles.includes(name)) manifest.dsh.profile.bundles.push(name)
}
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
const brokenRestart = await request('POST', `${API}restart-dsh`)
await waitTask('dshLifecycle', brokenRestart.taskId, ['failed'])
await waitFor(async () => (await request('GET', `${API}status`)).recoveryMode ?? false, 'DSH recovery mode')
let inventory = await request('GET', `${API}user-plugins`)
for (const name of faultNames) {
const plugin = inventory.plugins.find(value => value.name === name)
assert.equal(plugin?.enabled, true)
assert.equal(plugin?.damaged, false)
}
const firstRecovery = await request('POST', `${API}user-plugins/apply`, {
profile: 'web', revision: inventory.revision,
actions: [{ name: faultNames[0], action: 'disable' }],
})
const firstOutcome = await waitTask('userPluginOperation', firstRecovery.taskId, ['success'])
assert.equal(firstOutcome.strategy, 'next-start')
assert.equal(firstOutcome.activationError, null)
inventory = await request('GET', `${API}user-plugins`)
assert.equal(inventory.plugins.find(value => value.name === faultNames[0])?.enabled, false)
assert.equal(inventory.plugins.find(value => value.name === faultNames[1])?.enabled, true)
const secondRecovery = await request('POST', `${API}user-plugins/apply`, {
profile: 'web', revision: inventory.revision,
actions: [{ name: faultNames[1], action: 'disable' }],
})
const secondOutcome = await waitTask('userPluginOperation', secondRecovery.taskId, ['success'])
assert.equal(secondOutcome.strategy, 'next-start')
assert.equal(secondOutcome.activationError, null)
const recoveredStart = await request('POST', `${API}start-dsh`)
await waitTask('dshLifecycle', recoveredStart.taskId, ['running'])
await waitFor(async () => {
const status = await request('GET', `${API}status`)
return status.recoveryMode === null ? status : false
}, 'DSH recovery completion')
inventory = await request('GET', `${API}user-plugins`)
for (const name of faultNames) assert.equal(inventory.plugins.find(value => value.name === name)?.enabled, false)
const uninstall = await request('POST', `${API}user-plugins/apply`, {
profile: 'web', revision: inventory.revision,
actions: [{ name: faultNames[0], action: 'uninstall' }],
})
const uninstallOutcome = await waitTask('userPluginOperation', uninstall.taskId, ['success', 'failed'])
if (uninstallOutcome.status !== 'success') {
const diagnostics = await request('GET', `${API}logs?limit=50`)
throw new Error(JSON.stringify({
error: uninstallOutcome.error,
manifest: JSON.parse(await readFile(manifestPath, 'utf8')),
logs: diagnostics.entries.slice(-10),
}))
}
inventory = await request('GET', `${API}user-plugins`)
assert.equal(inventory.plugins.some(value => value.name === faultNames[0]), false)
const logEntries = (await request('GET', `${API}logs?limit=5000`)).entries
for (const message of [
'terminal.session.created', 'terminal.session.connected', 'terminal.session.disconnected',
'terminal.session.reconnected', 'terminal.session.closed', 'user-plugin.apply.started',
'user-plugin.apply.completed',
]) assert.ok(logEntries.some(entry => entry.message === message), `missing platform log ${message}`)
assert.doesNotMatch(JSON.stringify(logEntries.filter(entry => entry.source === 'terminal')), /terminal-before-restart|terminal-after-restart/)
process.stdout.write(`${JSON.stringify({ terminalSession: terminal.sessionId, faultNames })}\n`)
@@ -0,0 +1,37 @@
import { Context } from "/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/cordis/lib/index.js";
import SkillRegistry from "/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-skill/lib/index.js";
import * as SkillFileSystem from "/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-skill-filesystem/lib/index.js";
import LocalFileSystem from "/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-fs-local/lib/index.js";
const context = new Context();
context.plugin(SkillRegistry);
context.plugin(LocalFileSystem, { cwd: process.env.DSH_DEFAULT_WORKSPACE ?? "/workspace" });
context.plugin(SkillFileSystem, {
dshHome: process.env.DSH_HOME ?? "/data/dsh",
agentsHome: process.env.DSH_AGENTS_HOME ?? "/home/node/.agents",
bundledSkillDir: process.env.DSH_BUNDLED_SKILL_DIR ?? "/run/dsh-platform/views/skills",
});
await new Promise((resolve) => setTimeout(resolve, 100));
const summaries = await context.skills.list({
cwd: process.env.DSH_DEFAULT_WORKSPACE ?? "/workspace",
});
const summary = summaries.find((entry) => entry.name === "dsh-docker-operations");
if (summary?.source !== "bundled" || summary.provider !== "filesystem") {
throw new Error(`bundled operations skill was not discovered: ${JSON.stringify(summary)}`);
}
const definition = await context.skills.get("dsh-docker-operations", {
cwd: process.env.DSH_DEFAULT_WORKSPACE ?? "/workspace",
});
if (!definition || definition.name !== "dsh-docker-operations" || definition.content.length < 500) {
throw new Error("bundled operations skill did not load a complete definition");
}
console.log(JSON.stringify({
name: definition.name,
source: summary.source,
provider: summary.provider,
contentBytes: Buffer.byteLength(definition.content),
}));
process.exit(0);
@@ -0,0 +1,53 @@
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { Context } from '/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/cordis/lib/index.js'
import SkillRegistry from '/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-skill/lib/index.js'
import * as SkillFileSystem from '/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-skill-filesystem/lib/index.js'
import LocalFileSystem from '/run/dsh-platform/views/runtime/package/node_modules/@deepseek-ai/dsh-fs-local/lib/index.js'
const dshHome = process.env.DSH_HOME ?? '/data/dsh'
const workspace = process.env.DSH_DEFAULT_WORKSPACE ?? '/workspace'
const skillRoot = join(dshHome, 'skills')
const activePath = join(skillRoot, 'smoke-user-skill')
const disabledPath = join(skillRoot, '.disabled', 'smoke-user-skill')
async function waitForSkill(context, expected) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const values = await context.skills?.list({ cwd: workspace })
if (values !== undefined && values.some(value => value.name === 'smoke-user-skill') === expected) return
await new Promise(resolve => setTimeout(resolve, 50))
}
throw new Error(`user skill watcher did not report expected=${expected}`)
}
await rm(activePath, { recursive: true, force: true })
await rm(disabledPath, { recursive: true, force: true })
await mkdir(activePath, { recursive: true })
await writeFile(join(activePath, 'SKILL.md'), `---
name: smoke-user-skill
description: Smoke-test user skill watcher behavior.
---
# Smoke User Skill
`)
const context = new Context()
context.plugin(SkillRegistry)
context.plugin(LocalFileSystem, { cwd: workspace })
context.plugin(SkillFileSystem, {
dshHome,
agentsHome: process.env.DSH_AGENTS_HOME ?? '/home/node/.agents',
bundledSkillDir: process.env.DSH_BUNDLED_SKILL_DIR ?? '/run/dsh-platform/views/skills',
})
await waitForSkill(context, true)
await mkdir(join(skillRoot, '.disabled'), { recursive: true })
await rename(activePath, disabledPath)
await waitForSkill(context, false)
await rename(disabledPath, activePath)
await waitForSkill(context, true)
await rm(activePath, { recursive: true })
await waitForSkill(context, false)
console.log(JSON.stringify({ discovered: true, disabled: true, enabled: true, deleted: true }))
process.exit(0)