@khorsheed/dsh-ankh-guard 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.en.md +75 -29
- package/README.i18n.yaml +2 -2
- package/README.md +74 -29
- package/lib/cli.js +2383 -209
- package/lib/client.js +257 -0
- package/lib/exit-agent.js +5 -2
- package/lib/index.js +722 -38
- package/lib/invariant.js +1 -1
- package/lib/preflight-runner.js +125 -47
- package/lib/processes-BjZgJjQr.js +344 -0
- package/lib/restart-context-D6nISh28.js +1245 -0
- package/lib/restart-context-DUyExi9O.js +1245 -0
- package/lib/{state-Dhx9VG44.js → state-4f7yny39.js} +60 -13
- package/lib/state-CZMypGkB.js +323 -0
- package/lib/test-seam-DnvLWTeO.js +119 -0
- package/lib/test-seam-cli.js +24 -0
- package/lib/test-seam-dwvaKjRp.js +459 -0
- package/lib/test-seam.js +2 -0
- package/lib/types/browser-handoff.d.ts +55 -0
- package/lib/types/browser-handoff.js +489 -0
- package/lib/types/cli.d.ts +34 -4
- package/lib/types/cli.js +1487 -225
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +264 -0
- package/lib/types/deployment-proof.d.ts +24 -0
- package/lib/types/deployment-proof.js +314 -0
- package/lib/types/exit-agent.js +2 -0
- package/lib/types/git.d.ts +12 -3
- package/lib/types/git.js +69 -7
- package/lib/types/index.d.ts +66 -3
- package/lib/types/index.js +157 -39
- package/lib/types/launch-spec.d.ts +263 -0
- package/lib/types/launch-spec.js +823 -0
- package/lib/types/preflight-runner.d.ts +23 -12
- package/lib/types/preflight-runner.js +152 -57
- package/lib/types/processes.d.ts +38 -6
- package/lib/types/processes.js +236 -10
- package/lib/types/restart-context.d.ts +50 -0
- package/lib/types/restart-context.js +106 -0
- package/lib/types/restart-request.d.ts +32 -0
- package/lib/types/restart-request.js +128 -0
- package/lib/types/state-files.d.ts +30 -0
- package/lib/types/state-files.js +55 -0
- package/lib/types/state.d.ts +29 -2
- package/lib/types/state.js +52 -7
- package/lib/types/temp-artifact.d.ts +15 -0
- package/lib/types/temp-artifact.js +17 -0
- package/lib/types/test-seam-cli.d.ts +3 -0
- package/lib/types/test-seam-cli.js +27 -0
- package/lib/types/test-seam.d.ts +55 -0
- package/lib/types/test-seam.js +112 -0
- package/lib/types/transition.d.ts +118 -0
- package/lib/types/transition.js +717 -0
- package/package.json +29 -9
- package/scripts/dsh-watchdog.sh +1388 -80
- package/scripts/install-launchd.sh +43 -5
- package/scripts/install-systemd.sh +43 -5
- package/scripts/on-install.js +1 -1
- package/skills/dsh-self-restart-guard/SKILL.md +38 -12
- package/lib/processes-hCAmwma-.js +0 -127
- package/lib/restart-context-DmnQXNf-.js +0 -421
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
#
|
|
19
19
|
# Usage:
|
|
20
20
|
# install-launchd.sh --start "CMD" [--port N] [--home DIR] [--repo DIR]
|
|
21
|
+
# [--harness-root DIR] [--profile NAME]
|
|
22
|
+
# --preflight-surface source|built --preflight-install-anchor FILE
|
|
23
|
+
# [--preflight-runner FILE]
|
|
21
24
|
# [--cli "CMD"] [--label NAME] [--force]
|
|
22
25
|
# install-launchd.sh --uninstall [--label NAME]
|
|
23
26
|
#
|
|
@@ -27,7 +30,14 @@
|
|
|
27
30
|
# --home DIR dsh root: state/, logs, DSH_HOME for the job (default
|
|
28
31
|
# $DSH_HOME, else $HOME/.dsh-official).
|
|
29
32
|
# --repo DIR checkout the guard credential/rollback binds to (default
|
|
30
|
-
#
|
|
33
|
+
# current directory); this may be a plugin migration repo.
|
|
34
|
+
# --harness-root DIR dsh host checkout used by preflight and exported to
|
|
35
|
+
# the child (default DSH_HARNESS, else conventional host).
|
|
36
|
+
# --profile NAME dsh profile used for preflight/canary metadata (default
|
|
37
|
+
# $DSH_PROFILE, else web).
|
|
38
|
+
# --preflight-surface source|built execution surface used by this start command.
|
|
39
|
+
# --preflight-install-anchor FILE exact @deepseek-ai/dsh/package.json used by it.
|
|
40
|
+
# --preflight-runner FILE matching ankh-guard runner (normally auto-resolved).
|
|
31
41
|
# --cli "CMD" guard CLI invocation prefix, e.g. "node lib/cli.js" or
|
|
32
42
|
# "node --import <tsx> src/cli.ts" (default: this package's
|
|
33
43
|
# built lib/cli.js run via node).
|
|
@@ -44,7 +54,12 @@ set -u
|
|
|
44
54
|
LABEL="${DSH_WD_LABEL:-com.dsh.watchdog}"
|
|
45
55
|
PORT="${DSH_WD_PORT:-3080}"
|
|
46
56
|
HOME_DIR="${DSH_WD_HOME:-${DSH_HOME:-$HOME/.dsh-official}}"
|
|
47
|
-
REPO="${DSH_WD_REPO:-$
|
|
57
|
+
REPO="${DSH_WD_REPO:-$PWD}"
|
|
58
|
+
HARNESS_ROOT="${DSH_WD_HARNESS_ROOT:-${DSH_HARNESS:-$HOME/code/deepseek-harness}}"
|
|
59
|
+
PROFILE="${DSH_WD_PROFILE:-${DSH_PROFILE:-web}}"
|
|
60
|
+
PREFLIGHT_SURFACE="${DSH_WD_PREFLIGHT_SURFACE:-}"
|
|
61
|
+
PREFLIGHT_INSTALL_ANCHOR="${DSH_WD_PREFLIGHT_INSTALL_ANCHOR:-}"
|
|
62
|
+
PREFLIGHT_RUNNER="${DSH_WD_PREFLIGHT_RUNNER:-}"
|
|
48
63
|
START=""
|
|
49
64
|
CLI=""
|
|
50
65
|
FORCE=0
|
|
@@ -70,6 +85,11 @@ while [ $# -gt 0 ]; do
|
|
|
70
85
|
--port) PORT="${2:-}"; shift 2 ;;
|
|
71
86
|
--home) HOME_DIR="${2:-}"; shift 2 ;;
|
|
72
87
|
--repo) REPO="${2:-}"; shift 2 ;;
|
|
88
|
+
--harness-root) HARNESS_ROOT="${2:-}"; shift 2 ;;
|
|
89
|
+
--profile) PROFILE="${2:-}"; shift 2 ;;
|
|
90
|
+
--preflight-surface) PREFLIGHT_SURFACE="${2:-}"; shift 2 ;;
|
|
91
|
+
--preflight-install-anchor) PREFLIGHT_INSTALL_ANCHOR="${2:-}"; shift 2 ;;
|
|
92
|
+
--preflight-runner) PREFLIGHT_RUNNER="${2:-}"; shift 2 ;;
|
|
73
93
|
--cli) CLI="${2:-}"; shift 2 ;;
|
|
74
94
|
--label) LABEL="${2:-}"; shift 2 ;;
|
|
75
95
|
--force) FORCE=1; shift ;;
|
|
@@ -100,6 +120,16 @@ fi
|
|
|
100
120
|
|
|
101
121
|
STATE_DIR="$HOME_DIR/state"
|
|
102
122
|
PIDFILE="$STATE_DIR/watchdog.pid"
|
|
123
|
+
if [ ! -f "$STATE_DIR/launch-spec.json" ]; then
|
|
124
|
+
if [ "$PREFLIGHT_SURFACE" != "source" ] && [ "$PREFLIGHT_SURFACE" != "built" ]; then
|
|
125
|
+
echo "install-launchd.sh: fresh launch state requires --preflight-surface source|built" >&2
|
|
126
|
+
exit 2
|
|
127
|
+
fi
|
|
128
|
+
if [ -z "$PREFLIGHT_INSTALL_ANCHOR" ]; then
|
|
129
|
+
echo "install-launchd.sh: fresh launch state requires --preflight-install-anchor FILE for the actual dsh execution surface" >&2
|
|
130
|
+
exit 2
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
103
133
|
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
104
134
|
OLD_PID="$(cat "$PIDFILE")"
|
|
105
135
|
if [ "$FORCE" = "1" ]; then
|
|
@@ -129,9 +159,17 @@ xml_escape() {
|
|
|
129
159
|
printf '%s' "$1" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g; s/'"'"'/\'/g'
|
|
130
160
|
}
|
|
131
161
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
162
|
+
# Initialize the durable launch specification once, then always start from its
|
|
163
|
+
# selected side. Reinstalling an OS service must not silently overwrite a
|
|
164
|
+
# cutover/rollback decision; launch changes go through `reconfigure`.
|
|
165
|
+
PREFLIGHT_FLAGS=""
|
|
166
|
+
if [ -n "$PREFLIGHT_SURFACE" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-surface $(printf '%q' "$PREFLIGHT_SURFACE")"; fi
|
|
167
|
+
if [ -n "$PREFLIGHT_INSTALL_ANCHOR" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-install-anchor $(printf '%q' "$PREFLIGHT_INSTALL_ANCHOR")"; fi
|
|
168
|
+
if [ -n "$PREFLIGHT_RUNNER" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-runner $(printf '%q' "$PREFLIGHT_RUNNER")"; fi
|
|
169
|
+
INIT="$CLI configure-launch --if-absent --port $PORT --start $(printf '%q' "$START") --state-dir $(printf '%q' "$STATE_DIR") --repo $(printf '%q' "$REPO") --harness-root $(printf '%q' "$HARNESS_ROOT") --home $(printf '%q' "$HOME_DIR") --profile $(printf '%q' "$PROFILE")$PREFLIGHT_FLAGS"
|
|
170
|
+
# One bash -c line: the CLI process (and not this setup shell) becomes the
|
|
171
|
+
# watchdog's parent, so launchd observes the watchdog's eventual exit status.
|
|
172
|
+
PROGRAM="$INIT && exec $CLI supervise --foreground --state-dir $(printf '%q' "$STATE_DIR")"
|
|
135
173
|
|
|
136
174
|
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
137
175
|
mkdir -p "$(dirname "$PLIST")"
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
#
|
|
32
32
|
# Usage:
|
|
33
33
|
# install-systemd.sh --start "CMD" [--port N] [--home DIR] [--repo DIR]
|
|
34
|
+
# [--harness-root DIR] [--profile NAME]
|
|
35
|
+
# --preflight-surface source|built --preflight-install-anchor FILE
|
|
36
|
+
# [--preflight-runner FILE]
|
|
34
37
|
# [--cli "CMD"] [--label NAME] [--force] [--print]
|
|
35
38
|
# install-systemd.sh --uninstall [--label NAME]
|
|
36
39
|
#
|
|
@@ -40,7 +43,14 @@
|
|
|
40
43
|
# --home DIR dsh root: state/, logs, DSH_HOME for the unit (default
|
|
41
44
|
# $DSH_HOME, else $HOME/.dsh-official).
|
|
42
45
|
# --repo DIR checkout the guard credential/rollback binds to (default
|
|
43
|
-
#
|
|
46
|
+
# current directory); this may be a plugin migration repo.
|
|
47
|
+
# --harness-root DIR dsh host checkout used by preflight and exported to
|
|
48
|
+
# the child (default DSH_HARNESS, else conventional host).
|
|
49
|
+
# --profile NAME dsh profile used for preflight/canary metadata (default
|
|
50
|
+
# $DSH_PROFILE, else web).
|
|
51
|
+
# --preflight-surface source|built execution surface used by this start command.
|
|
52
|
+
# --preflight-install-anchor FILE exact @deepseek-ai/dsh/package.json used by it.
|
|
53
|
+
# --preflight-runner FILE matching ankh-guard runner (normally auto-resolved).
|
|
44
54
|
# --cli "CMD" guard CLI invocation prefix (default: this package's built
|
|
45
55
|
# lib/cli.js run via an absolute node path).
|
|
46
56
|
# --label NAME unit name without .service (default dsh-watchdog).
|
|
@@ -60,7 +70,12 @@ set -u
|
|
|
60
70
|
LABEL="${DSH_WD_LABEL:-dsh-watchdog}"
|
|
61
71
|
PORT="${DSH_WD_PORT:-3080}"
|
|
62
72
|
HOME_DIR="${DSH_WD_HOME:-${DSH_HOME:-$HOME/.dsh-official}}"
|
|
63
|
-
REPO="${DSH_WD_REPO:-$
|
|
73
|
+
REPO="${DSH_WD_REPO:-$PWD}"
|
|
74
|
+
HARNESS_ROOT="${DSH_WD_HARNESS_ROOT:-${DSH_HARNESS:-$HOME/code/deepseek-harness}}"
|
|
75
|
+
PROFILE="${DSH_WD_PROFILE:-${DSH_PROFILE:-web}}"
|
|
76
|
+
PREFLIGHT_SURFACE="${DSH_WD_PREFLIGHT_SURFACE:-}"
|
|
77
|
+
PREFLIGHT_INSTALL_ANCHOR="${DSH_WD_PREFLIGHT_INSTALL_ANCHOR:-}"
|
|
78
|
+
PREFLIGHT_RUNNER="${DSH_WD_PREFLIGHT_RUNNER:-}"
|
|
64
79
|
START=""
|
|
65
80
|
CLI=""
|
|
66
81
|
FORCE=0
|
|
@@ -86,6 +101,11 @@ while [ $# -gt 0 ]; do
|
|
|
86
101
|
--port) PORT="${2:-}"; shift 2 ;;
|
|
87
102
|
--home) HOME_DIR="${2:-}"; shift 2 ;;
|
|
88
103
|
--repo) REPO="${2:-}"; shift 2 ;;
|
|
104
|
+
--harness-root) HARNESS_ROOT="${2:-}"; shift 2 ;;
|
|
105
|
+
--profile) PROFILE="${2:-}"; shift 2 ;;
|
|
106
|
+
--preflight-surface) PREFLIGHT_SURFACE="${2:-}"; shift 2 ;;
|
|
107
|
+
--preflight-install-anchor) PREFLIGHT_INSTALL_ANCHOR="${2:-}"; shift 2 ;;
|
|
108
|
+
--preflight-runner) PREFLIGHT_RUNNER="${2:-}"; shift 2 ;;
|
|
89
109
|
--cli) CLI="${2:-}"; shift 2 ;;
|
|
90
110
|
--label) LABEL="${2:-}"; shift 2 ;;
|
|
91
111
|
--force) FORCE=1; shift ;;
|
|
@@ -119,12 +139,30 @@ if [ -z "$CLI" ]; then
|
|
|
119
139
|
fi
|
|
120
140
|
|
|
121
141
|
STATE_DIR="$HOME_DIR/state"
|
|
142
|
+
if [ ! -f "$STATE_DIR/launch-spec.json" ]; then
|
|
143
|
+
if [ "$PREFLIGHT_SURFACE" != "source" ] && [ "$PREFLIGHT_SURFACE" != "built" ]; then
|
|
144
|
+
echo "install-systemd.sh: fresh launch state requires --preflight-surface source|built" >&2
|
|
145
|
+
exit 2
|
|
146
|
+
fi
|
|
147
|
+
if [ -z "$PREFLIGHT_INSTALL_ANCHOR" ]; then
|
|
148
|
+
echo "install-systemd.sh: fresh launch state requires --preflight-install-anchor FILE for the actual dsh execution surface" >&2
|
|
149
|
+
exit 2
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
122
152
|
LOG_OUT="$STATE_DIR/watchdog.log"
|
|
123
153
|
LOG_ERR="$STATE_DIR/watchdog.stderr.log"
|
|
124
154
|
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
155
|
+
# Initialize the durable launch specification once, then always start from its
|
|
156
|
+
# selected side. Reinstalling an OS service must not silently overwrite a
|
|
157
|
+
# cutover/rollback decision; launch changes go through `reconfigure`.
|
|
158
|
+
PREFLIGHT_FLAGS=""
|
|
159
|
+
if [ -n "$PREFLIGHT_SURFACE" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-surface $(printf '%q' "$PREFLIGHT_SURFACE")"; fi
|
|
160
|
+
if [ -n "$PREFLIGHT_INSTALL_ANCHOR" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-install-anchor $(printf '%q' "$PREFLIGHT_INSTALL_ANCHOR")"; fi
|
|
161
|
+
if [ -n "$PREFLIGHT_RUNNER" ]; then PREFLIGHT_FLAGS="$PREFLIGHT_FLAGS --preflight-runner $(printf '%q' "$PREFLIGHT_RUNNER")"; fi
|
|
162
|
+
INIT="$CLI configure-launch --if-absent --port $PORT --start $(printf '%q' "$START") --state-dir $(printf '%q' "$STATE_DIR") --repo $(printf '%q' "$REPO") --harness-root $(printf '%q' "$HARNESS_ROOT") --home $(printf '%q' "$HOME_DIR") --profile $(printf '%q' "$PROFILE")$PREFLIGHT_FLAGS"
|
|
163
|
+
# One bash -c line: the CLI process (and not this setup shell) becomes the
|
|
164
|
+
# watchdog's parent, so systemd observes the watchdog's eventual exit status.
|
|
165
|
+
PROGRAM="$INIT && exec $CLI supervise --foreground --state-dir $(printf '%q' "$STATE_DIR")"
|
|
128
166
|
|
|
129
167
|
# Quote one value as a single systemd argument. systemd does NOT parse shell
|
|
130
168
|
# quoting: it splits on whitespace and understands double quotes with backslash
|
package/scripts/on-install.js
CHANGED
|
@@ -16,7 +16,7 @@ if (process.cwd().includes(`${sep}node_modules${sep}`)) {
|
|
|
16
16
|
the running instance has not loaded the plugin and no watchdog exists yet, so a bare
|
|
17
17
|
exit leaves the service DOWN:
|
|
18
18
|
node_modules/@khorsheed/dsh-ankh-guard/lib/cli.js check-env
|
|
19
|
-
node_modules/@khorsheed/dsh-ankh-guard/lib/cli.js record build+test --state-dir "$DSH_HOME/state" --repo "$PWD"
|
|
19
|
+
node_modules/@khorsheed/dsh-ankh-guard/lib/cli.js record build+test --state-dir "$DSH_HOME/state" --repo "$PWD" --run -- sh -c 'pnpm run build && pnpm run test'
|
|
20
20
|
node_modules/@khorsheed/dsh-ankh-guard/lib/cli.js restart --port <port> --start "<start command>"
|
|
21
21
|
or establish the watchdog first with \`supervise\`. NEVER hand-roll sleep/kill/nohup restart
|
|
22
22
|
scripts — they die with the instance (its teardown reaps managed processes).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dsh-self-restart-guard
|
|
3
|
-
description: Use before
|
|
3
|
+
description: Use before a self-modification batch, an agent-driven restart, or a launch-configuration cutover of the running dsh web instance. Enforces execution-backed green evidence, clean-tree/HEAD binding, authoritative supervisor/child/listener ownership, the checkpoint policy for real edits, durable abort/full-spec recovery, and the post-restart canary. Also consult it to query guard state or recorded deployment pitfalls.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Self-Restart Guard: atomic self-modification protocol
|
|
@@ -21,50 +21,76 @@ Never restart "the instance" from a recipe — restart the one you are actually
|
|
|
21
21
|
|
|
22
22
|
1. **State dir**: `$DSH_HOME/state` (already in your environment). If `DSH_HOME` is unset, the deployment is custom — ask the user rather than guessing.
|
|
23
23
|
2. **Start command and repo**: read `$DSH_HOME/state/instance-launch.json` once the plugin has booted there — the plugin records the instance's exact launch command (cwd, env, full argv incl. `--import` chains) at every boot. `restart`/`supervise` consume it automatically, so `--start` is usually unnecessary. Only construct a start command yourself when the record does not exist (plugin not loaded yet).
|
|
24
|
-
3. **Port**: the GUI address in your context (e.g. the system prompt's URL)
|
|
24
|
+
3. **Port**: use the GUI address in your context (e.g. the system prompt's URL); otherwise run `/usr/sbin/lsof -a -p <instance-pid> -iTCP -sTCP:LISTEN -P` on macOS (or the platform's absolute `lsof` path). The `restart` verb also defaults `--port` from the launch record.
|
|
25
25
|
|
|
26
26
|
Run `$GUARD check-env --state-dir "$DSH_HOME/state" --repo <repo>` for a one-shot readout of all of the above plus sandbox status and watchdog presence.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Classify the operation first
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
- **Pure same-launch restart**: no source, dependency, profile, generated output, installed artifact, or launch-spec input changed. Skip the Git checkpoint because there is no pre-edit state to preserve. Run `verify` first: a fresh credential passes, and a `proven deployment valid` result may reuse the build/test evidence that a previous successful watchdog canary promoted for this exact deployment fingerprint. Missing legacy proof or any fingerprint drift requires build/test + record again. Preflight, watchdog ownership, readiness, and canary are never skipped.
|
|
31
|
+
- **Modification followed by restart**: take the checkpoint before editing, then commit the logical change and prove the resulting clean HEAD.
|
|
32
|
+
- **Launch configuration changes**: use the modification protocol as applicable, then `reconfigure`; never smuggle a new command/repo/host root/profile through `schedule-exit`.
|
|
33
|
+
|
|
34
|
+
## The protocol
|
|
35
|
+
|
|
36
|
+
1. **Checkpoint real edits only** — before touching anything, record the rollback point of the repo you are about to change. A clean tree records the existing HEAD without creating an empty commit. A dirty tree is refused by default; inspect every path and, only with explicit approval and where repository policy permits committing the complete snapshot, rerun with `--include-dirty`:
|
|
31
37
|
|
|
32
38
|
```sh
|
|
33
39
|
$GUARD checkpoint --message "before <batch-name>" --repo <repo> --state-dir "$DSH_HOME/state"
|
|
40
|
+
# reviewed dirty snapshot only:
|
|
41
|
+
$GUARD checkpoint --message "before <batch-name>" --include-dirty --repo <repo> --state-dir "$DSH_HOME/state"
|
|
34
42
|
```
|
|
35
43
|
|
|
36
44
|
2. **Modify** — make the change; register every surface the change needs (package `files`, `dsh.bundle.patch`, identity triangle, dependencies). Missing registrations are the single most common failure class — an unregistered package is invisible to every gate.
|
|
37
45
|
|
|
38
|
-
3. **Prove green** — run the
|
|
46
|
+
3. **Prove green on the final clean commit** — after any modification, use `record --run --` to make the guard execute the exact argv and observe exit 0. The guard clears any old credential and proven-deployment reuse before the command, and records only if HEAD is unchanged and staged, unstaged, and untracked inputs are all absent afterward. For multiple shell steps, invoke the shell explicitly as the evidence program:
|
|
39
47
|
|
|
40
48
|
```sh
|
|
41
|
-
$GUARD record build --
|
|
49
|
+
$GUARD record build+test --repo <repo> --state-dir "$DSH_HOME/state" --run -- sh -c 'pnpm run build && pnpm run test'
|
|
42
50
|
$GUARD verify --repo <repo> --state-dir "$DSH_HOME/state"
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
For a classified pure same-launch restart, run only the `verify` line first. If it reports either a fresh green credential or `proven deployment valid`, continue without rerunning the expensive command. The reusable proof exists only after this guard version has observed a complete watchdog restart and canary; `last-good-boot.json` by itself, an older guard state, or a matching HEAD without the runtime fingerprint does not qualify. `schedule-exit` recomputes the fingerprint and pins the selected evidence SHA into the short-lived restart marker, and the successor watchdog revalidates it before accepting canary. A refusal is not bypassable: run the full evidence command above.
|
|
54
|
+
|
|
55
|
+
`--trust-command --command "..."` is reserved for an external orchestrator that already observed the command's real exit status (for example, the repository's deployment driver). It is not an agent shortcut.
|
|
56
|
+
|
|
57
|
+
4. **Composition preflight** — `restart`, `schedule-exit`, and `reconfigure` each run this gate internally exactly once and refuse before stopping the healthy host. For an ordinary same-launch restart, call the stop-capable verb directly; use the standalone verb only to diagnose an already-observed failure, never as a speculative duplicate immediately before that verb:
|
|
46
58
|
|
|
47
59
|
```sh
|
|
48
60
|
$GUARD preflight --profile web
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
The caller deadline is a separate safety budget from the guard's `--preflight-timeout-ms` (default 120000 ms). A managed Bash/tool call MUST wait longer than the internal gate: with the default, set the call's execution option to `timeoutMs: 180000`; with a custom preflight timeout, keep at least 30000 ms of caller margin. `timeoutMs` is tool metadata, not a guard CLI flag. Never wrap the command in a shorter shell timeout. The CLI prints `composition preflight START` before the wait and a PASS/refusal only after the runner settles. If the caller nevertheless kills the CLI, absence of `exit scheduled` means no restart was authorized; inspect the restart marker and receipt before deciding whether a retry is safe.
|
|
64
|
+
|
|
65
|
+
5. **Restart or launch cutover** — the path depends on supervision and whether the complete launch specification changes:
|
|
66
|
+
- **No watchdog yet** (e.g. right after installing the plugin): `schedule-exit` hard-refuses because killing the host would guarantee an outage. Establish supervision, or drive the first bounce with the `restart` verb — it owns stop → start → canary in a detached driver and self-detaches from the dying instance. `--start` defaults to the launch record when one exists.
|
|
67
|
+
- **Watchdog-supervised, same command, home, credential repo, harness root, and profile**: run `schedule-exit --delay-ms 5000 --state-dir "$DSH_HOME/state"` (`--port` may confirm the durable port on legacy state), with `timeoutMs: 180000` on the managed Bash/tool call. It reloads the durable active spec, rejects explicit repo/host-root/profile/port conflicts, verifies that the live supervisor record owns the same command, accepts either the active repo's fresh credential or an exact canary-promoted deployment proof, runs one composition preflight against the active host root, then exits the child. Proof reuse never skips preflight or canary. The watchdog respawns + canaries automatically. Do NOT use bare `restart` against a supervised instance — it fights the supervisor and lacks the durable ownership needed for proof reuse. **Never pass `--initiator` by hand**: it defaults to `$DSH_SESSION_ID`, which the shell environment already sets to THIS session's id — that is what routes the post-restart wake-up report back to you. An invented value sends the report to a session that does not exist and you are never woken (a branch name is not a session id).
|
|
68
|
+
- **Watchdog-supervised, command, home, credential repo, harness root, profile, or execution surface changes**: first ensure `launch-status` shows a complete durable previous spec. If it does not, initialize the real current values with `configure-launch`; never infer previous from the target repo or legacy command record. Explicitly classify the command actually used by the successor: `built` for an npm toolchain `.../@deepseek-ai/dsh/lib/bin.js`, `source` for checkout TypeScript. Pass `--preflight-surface <source|built>` and `--preflight-install-anchor <the exact successor @deepseek-ai/dsh/package.json>`; pass `--preflight-runner` only when the matching installed guard runner cannot be auto-resolved. Do not infer the surface from `process.execArgv`, `DSH_HARNESS`, or the guard's own launch mode. Treat `--candidate-probe-command` as a caller-supplied trust boundary: the guard hash-binds it beside `--start` but cannot prove that arbitrary shell text is semantically derived from the target argv. Construct it from the same target executable and launcher argv (for the official dsh CLI, preserve the same launcher flags/order and replace the long-running action with one-shot `--dump-config`); never use an unrelated success command, because it will not catch misplaced or unknown final argv. Then use `reconfigure --start "<complete target command>" --repo <target-credential-repo> --harness-root <target-host-root> --preflight-surface ... --preflight-install-anchor ... --candidate-probe-command "..." --on-failure <policy> --browser-handoff required`. Before running it, obtain the user's explicit recovery choice: `restore-previous` restores the entire previous launch specification; `wait-for-user` parks without resetting a repository. `reconfigure` rebuilds internal pnpm/Cordis links against copied nodes, keeps contained dependency cycles, maps external targets into a deduplicated snapshot-owned area, and rejects dangling/unresolvable links, special files, or any writable target outside the snapshot before it can run candidate or stop previous. Candidate and composition checks then run there, after which the pidfile transfers atomically. It persists start identities for the old supervisor/direct child/listener plus the new driver/watchdog. The successor consumes abort/restore while waiting, bounds old-supervisor yield (default 15 seconds), and may stop only a frozen then revalidated identity; it never treats a process found by port as the target. Online port changes are refused; deploy a separately supervised authority and cut traffic over instead.
|
|
69
|
+
- **Candidate rejects reconstructible old state**: derive the incompatible path from source/schema evidence, classify it as reconstructible rather than authoritative, and show the user the exact plan before approval. Pass a schema-v1 JSON file through `reconfigure --transition-file <plan>`; do not put `mv`, deletion, or migration logic in `--start`. The only supported operation is `{"kind":"quarantine","path":"<home-relative path>","expect":"present|absent"}`. Build the recovery closure: list both old paths target must not see and target output paths previous must not see after a failed attempt, including paths absent before cutover. `expect` binds that observation across isolated preflight and live apply; drift refuses rather than silently changing rollback meaning. The guard also rejects traversal, symlinks, overlapping paths, guard-state overlap, cross-filesystem moves, or a plan whose bytes change after preparation. It preflights the target against an isolated transitioned copy while previous remains live, applies the hash-bound plan only after previous is proven stopped, and on recovery retains target replacements before restoring exact previous bytes. A rollback failure parks without starting previous. Never quarantine session logs, credentials, or other authority; content conversion requires a separate reviewed migration mechanism.
|
|
54
70
|
- Never hand-roll `sleep; kill; nohup start` scripts — they die with the instance (teardown reaps managed processes).
|
|
55
71
|
|
|
56
|
-
6. **Verify after** — the port must listen again and the canary must PASS (`$DSH_HOME/state/restart.log` for the verb path, `watchdog.log` for the supervised path). On repeated boot failure the watchdog rolls the checkout back to the last known-good revision
|
|
72
|
+
6. **Verify after** — the port must listen again and the canary must PASS (`$DSH_HOME/state/restart.log` for the verb path, timestamped `watchdog.log` for the supervised path). For `reconfigure`, wait for `launch-status` to show a terminal receipt, read `$DSH_HOME/state/launch-cutover.json`, and report its supervisor/child/listener identities, redacted launch summaries, `preflight` surface/runner/install-anchor/target-command bindings and both PASS outcomes, server authentication readiness, stable-window/retry-zero proof, the later page-authored `browserHandoff` acknowledgement, per-role failure counts, transition digest/count/terminal phase when present, and recovery outcome. Read target readiness/canary from `targetValidation`; after restore, read previous readiness and its pass/fail/explicitly-skipped recovery canary from `recovery.validation`. Never describe a retained target canary failure as the restored previous host's health. A port response is never sufficient: the receipt must prove that the selected child remained alive, uniquely owned the listener through the stability window, completed at retry zero, passed or explicitly classified its role-specific canary before any browser URL was issued, and—when browser handoff is required—received an authenticated ACK from the original or fallback page. On repeated ordinary boot failure the watchdog rolls the checkout back to the last known-good revision; a cutover follows only its pre-approved full-spec recovery policy.
|
|
57
73
|
|
|
58
74
|
## Querying state
|
|
59
75
|
|
|
60
76
|
```sh
|
|
61
77
|
$GUARD status --state-dir "$DSH_HOME/state" # credentials, checkpoints, restart records
|
|
78
|
+
$GUARD launch-status --state-dir "$DSH_HOME/state" # redacted selected spec + durable cutover receipt
|
|
62
79
|
$GUARD canary --port <port> --state-dir "$DSH_HOME/state" --repo <repo>
|
|
80
|
+
# In-flight cutover controls:
|
|
81
|
+
$GUARD abort-cutover --state-dir "$DSH_HOME/state" # execute its pre-approved policy
|
|
82
|
+
$GUARD restore-previous --state-dir "$DSH_HOME/state" # explicit full-previous restore authorization
|
|
63
83
|
```
|
|
64
84
|
|
|
65
85
|
## Pitfalls this protocol exists for
|
|
66
86
|
|
|
67
|
-
- **Credential/
|
|
87
|
+
- **Credential/deployment mismatch**: `verify` and `schedule-exit` prefer the fresh credential; after it expires they accept only a proof written by a successful canary for the exact clean credential/harness HEADs, launch spec, profile/install artifacts, and bound preflight execution surface. `restart` still requires the fresh credential. Any changed HEAD, staged/unstaged/untracked input, profile package, linked target, archive, runner, or anchor refuses. Commit/remove the input, then rerun the execution-backed evidence. Record with `--state-dir` pointing at the SAME state the restart reads (`$DSH_HOME/state`); a record without it lands in a stray `<cwd>/.dsh-guard-state` and the gate will not see it.
|
|
68
88
|
- **Sandboxed sessions**: restart verbs refuse in a sandboxed turn (a detached driver would be reaped). Escalate through your host's per-command approval, or the user runs `/permission danger-full-access` in the session — settings pages only affect NEW sessions.
|
|
69
89
|
- **Preflight FAIL is information, not friction**: it has caught unbootable profile patches and duplicate loader entry ids before they could take prod down. Fix the composition; never bypass.
|
|
90
|
+
- **Caller timeout is not a guard verdict**: the default internal preflight budget is 120 seconds, while managed shell tools may default to 60 seconds. Set `timeoutMs: 180000` (or at least the configured preflight timeout plus 30000 ms). `composition preflight START` without PASS/refusal or `exit scheduled` means the caller interrupted the gate before any restart was authorized; check durable markers before retrying.
|
|
70
91
|
- **Profile `link:`/`file:` deps**: after rebuilding a plugin, refresh the profile install (`dsh plugin --profile web add <path-or-tarball>`) before restarting — a restart serves whatever the profile's `node_modules` currently contains.
|
|
92
|
+
- **401 is transport, not readiness**: require a protected final process to announce a same-authority launch URL; the watchdog proves URL → 303 → cookie-authenticated `/` → 200 with a temporary jar, holds one child/listener identity stable at retry zero, and passes canary before browser handoff. With `--browser-handoff required`, every responsive tab uses a held same-origin poll to register a per-tab capability before shutdown and waits. The final listener asks each tab to reload when its cookie is valid, or returns its own one-time same-origin URL in memory for `location.replace()` after 401; after ACK, the page returns to the original credential-free pathname. One real ACK gates terminal readiness, while slower registered tabs remain recoverable. Only if no original tab registers or none acknowledges before timeout does the watchdog request a system-open fallback, and opener success never substitutes for an authenticated page ACK. Never copy a candidate instance's URL into the final restart, and never persist or print bearer URLs or raw capabilities in state, logs, receipts, or reports.
|
|
93
|
+
- **First handoff rollout uses fallback**: a page loaded by an older ankh-guard has no handoff client. To test original-tab recovery, first deploy this plugin to the previous host and refresh that page, then run the host cutover. A fallback tab during the bootstrap deployment is expected.
|
|
94
|
+
- **An arbitrary 200 is not target identity**: the spawned direct child must still be alive; the port must have exactly one listener in that child tree; child/listener PID and start identity must remain unchanged through the stability window at retry zero. `EADDRINUSE`, a stale old listener, or a child that exits after provisional readiness fails the attempt and invokes the approved recovery path.
|
|
95
|
+
- **Restricted PATH is expected**: the guard resolves `/usr/sbin/lsof`, `/usr/bin/lsof`, and corresponding system `ps`/`pgrep` paths before PATH. Do not replace ownership proof with a bare `lsof` shell check or a port-only kill.
|
|
96
|
+
- **Intervene durably, never by signal alone**: use `abort-cutover` to execute the policy approved before the stop, or `restore-previous` to explicitly authorize a complete previous-spec restore. Each writes its own atomic marker before waking the watchdog; restore is monotonically stronger even when sessions race. Do not manually kill supervisor or child PIDs.
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
//#region lib/types/processes.js
|
|
3
|
-
/**
|
|
4
|
-
* Process primitives shared by the guard CLI and the exit agent: locating the
|
|
5
|
-
* listener on a port and killing a process tree. One owner — the watchdog's
|
|
6
|
-
* bash side keeps its own copy (different runtime), but every TypeScript
|
|
7
|
-
* caller goes through here.
|
|
8
|
-
*/
|
|
9
|
-
/** The first process listening on a TCP port, or null when none is (via lsof). */
|
|
10
|
-
function findPidOnPort(port) {
|
|
11
|
-
try {
|
|
12
|
-
const first = execFileSync("lsof", [
|
|
13
|
-
`-tiTCP:${port}`,
|
|
14
|
-
"-sTCP:LISTEN",
|
|
15
|
-
"-P"
|
|
16
|
-
], {
|
|
17
|
-
encoding: "utf8",
|
|
18
|
-
stdio: "pipe"
|
|
19
|
-
}).trim().split("\n")[0];
|
|
20
|
-
return first !== void 0 && first !== "" ? first : null;
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/** POSIX single-quote one word for a shell command line. */
|
|
26
|
-
function shellQuote(word) {
|
|
27
|
-
return `'${word.replace(/'/g, "'\\''")}'`;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Discover how the process on a port was launched — its exact argv from
|
|
31
|
-
* `ps -o command=`, its cwd from lsof, and its DSH_* environment from
|
|
32
|
-
* `ps eww` — rendered as a shell command. This exists because the agent's
|
|
33
|
-
* sandbox blocks ps entirely, so every fresh-machine agent fell into a
|
|
34
|
-
* process-tree archaeology loop before its first restart; the CLI (running
|
|
35
|
-
* unsandboxed) answers the same question mechanically and reliably. Returns
|
|
36
|
-
* null when the process is gone or ps/lsof are unavailable.
|
|
37
|
-
* @param pid - the listener's pid.
|
|
38
|
-
*/
|
|
39
|
-
function discoverLaunchCommand(pid) {
|
|
40
|
-
let argv;
|
|
41
|
-
let cwd;
|
|
42
|
-
try {
|
|
43
|
-
argv = execFileSync("ps", [
|
|
44
|
-
"-o",
|
|
45
|
-
"command=",
|
|
46
|
-
"-p",
|
|
47
|
-
pid
|
|
48
|
-
], {
|
|
49
|
-
encoding: "utf8",
|
|
50
|
-
stdio: "pipe"
|
|
51
|
-
}).trim();
|
|
52
|
-
if (argv === "") return null;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw new Error(`ps unavailable: ${String(error)}`);
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
const out = execFileSync("lsof", [
|
|
58
|
-
"-a",
|
|
59
|
-
"-p",
|
|
60
|
-
pid,
|
|
61
|
-
"-d",
|
|
62
|
-
"cwd",
|
|
63
|
-
"-Fn"
|
|
64
|
-
], {
|
|
65
|
-
encoding: "utf8",
|
|
66
|
-
stdio: "pipe"
|
|
67
|
-
});
|
|
68
|
-
const match = /^n(.+)$/m.exec(out);
|
|
69
|
-
if (match === null) return null;
|
|
70
|
-
cwd = match[1] ?? "";
|
|
71
|
-
} catch (error) {
|
|
72
|
-
throw new Error(`lsof cwd unavailable: ${String(error)}`);
|
|
73
|
-
}
|
|
74
|
-
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
75
|
-
"DSH_ANKH_RESTART_DRIVER",
|
|
76
|
-
"DSH_SESSION_ID",
|
|
77
|
-
"DSH_SESSION_JSONL",
|
|
78
|
-
"DSH_WEB_URL",
|
|
79
|
-
"DSH_SHELL"
|
|
80
|
-
]);
|
|
81
|
-
const env = {};
|
|
82
|
-
try {
|
|
83
|
-
const out = execFileSync("ps", [
|
|
84
|
-
"eww",
|
|
85
|
-
"-o",
|
|
86
|
-
"command",
|
|
87
|
-
"-p",
|
|
88
|
-
pid
|
|
89
|
-
], {
|
|
90
|
-
encoding: "utf8",
|
|
91
|
-
stdio: "pipe"
|
|
92
|
-
});
|
|
93
|
-
for (const token of out.split(/\s+/)) {
|
|
94
|
-
const eq = token.indexOf("=");
|
|
95
|
-
if (eq > 0 && token.slice(0, eq).startsWith("DSH_") && !TRANSIENT.has(token.slice(0, eq))) env[token.slice(0, eq)] = token.slice(eq + 1);
|
|
96
|
-
}
|
|
97
|
-
} catch {}
|
|
98
|
-
const envPart = Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
|
|
99
|
-
return `cd ${shellQuote(cwd)} && ${envPart !== "" ? `${envPart} ` : ""}${argv}`;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Kill a pid AND its descendants, deepest first (best effort). The supervised
|
|
103
|
-
* instance may have forked children; a plain signal on the pid alone would
|
|
104
|
-
* orphan them (the EADDRINUSE race the watchdog's EADDRINUSE branch exists
|
|
105
|
-
* for). The process-group model is NOT assumed — the instance is not
|
|
106
|
-
* setsid'd — so the sweep walks `pgrep -P` instead. `pgrep` missing or
|
|
107
|
-
* returning nothing is fine: the pid itself still gets the signal.
|
|
108
|
-
*/
|
|
109
|
-
function killPidTree(pid, signal) {
|
|
110
|
-
let children = [];
|
|
111
|
-
try {
|
|
112
|
-
const out = execFileSync("pgrep", ["-P", String(pid)], {
|
|
113
|
-
encoding: "utf8",
|
|
114
|
-
stdio: "pipe"
|
|
115
|
-
}).trim();
|
|
116
|
-
children = out === "" ? [] : out.split("\n");
|
|
117
|
-
} catch {}
|
|
118
|
-
for (const raw of children) {
|
|
119
|
-
const child = Number(raw);
|
|
120
|
-
if (Number.isInteger(child) && child > 0) killPidTree(child, signal);
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
process.kill(pid, signal);
|
|
124
|
-
} catch {}
|
|
125
|
-
}
|
|
126
|
-
//#endregion
|
|
127
|
-
export { findPidOnPort as n, killPidTree as r, discoverLaunchCommand as t };
|