@khorsheed/dsh-ankh-guard 0.3.0 → 0.3.2
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 +16 -0
- package/README.en.md +8 -3
- package/README.i18n.yaml +2 -2
- package/README.md +8 -3
- package/lib/index.js +65 -6
- package/lib/preflight-runner.js +39 -4
- package/lib/types/browser-handoff.d.ts +6 -0
- package/lib/types/browser-handoff.js +39 -0
- package/lib/types/index.d.ts +34 -8
- package/lib/types/index.js +32 -10
- package/lib/types/preflight-runner.d.ts +21 -1
- package/lib/types/preflight-runner.js +100 -9
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +13 -19
- package/skills/dsh-self-restart-guard/SKILL.md +2 -0
- package/lib/restart-context-DUyExi9O.js +0 -1245
|
@@ -131,7 +131,7 @@ class PreflightInfraError extends Error {
|
|
|
131
131
|
* bundle layers in `dsh.profile.bundles` order, the profile user layer, the
|
|
132
132
|
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
133
133
|
* (rc host line only; the 0.1.2 line's preset package self-ships its root),
|
|
134
|
-
* then the telemetry switch.
|
|
134
|
+
* then the telemetry switch. Three host API generations are mirrored and
|
|
135
135
|
* feature-detected per run — see the `hostLine` branch below. Exported so
|
|
136
136
|
* the drift tripwire can compare this assembly against the launcher's own
|
|
137
137
|
* dump without booting anything.
|
|
@@ -158,6 +158,8 @@ export async function composePreflightPatches(profile, patchFiles, root, home, b
|
|
|
158
158
|
}
|
|
159
159
|
const composeEntries = appBoot.composeEntries;
|
|
160
160
|
const healProfilesModuleFallback = appBoot.healProfilesModuleFallback;
|
|
161
|
+
const createProfileResolutionGeneration = appBoot.createProfileResolutionGeneration;
|
|
162
|
+
const createRuntimeResolution = appBoot.createRuntimeResolution;
|
|
161
163
|
const loadOptionalPatches = appBoot.loadOptionalPatches;
|
|
162
164
|
const loadOverlayPatches = appBoot.loadOverlayPatches;
|
|
163
165
|
const loadProfile = appBoot.loadProfile;
|
|
@@ -174,12 +176,21 @@ export async function composePreflightPatches(profile, patchFiles, root, home, b
|
|
|
174
176
|
// the profile composition: the heal moved behind the async options API and
|
|
175
177
|
// below the profile load (see HealProfilesModuleFallback), and the launcher
|
|
176
178
|
// dropped its agent-presets shipped-root overlay because the preset package
|
|
177
|
-
// now self-ships its root.
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
179
|
+
// now self-ships its root. The 0.1.6 line flipped the default resolution
|
|
180
|
+
// mode from link to runtime: its compose computes an immutable resolution
|
|
181
|
+
// generation instead of materializing fallback links, and it removed
|
|
182
|
+
// DEFAULT_PROFILE_PATCH_RELOAD — the 0.1.2 line's marker. The 0.1.7 line
|
|
183
|
+
// deleted the fallback projections outright (in-memory runtime resolution
|
|
184
|
+
// interception): its compose calls createRuntimeResolution and carries
|
|
185
|
+
// neither older marker, so the markers must be probed newest-first. Each
|
|
186
|
+
// marker is a value export only its line carries; a version parse would
|
|
187
|
+
// break on exactly the unreleased builds this runner must dry-run. All
|
|
188
|
+
// lines stay supported: prod hosts run 0.1.5 until the next npm line lands.
|
|
189
|
+
const hostLine = 'createRuntimeResolution' in appBoot
|
|
190
|
+
? '0.1.7'
|
|
191
|
+
: 'createProfileResolutionGeneration' in appBoot
|
|
192
|
+
? '0.1.6'
|
|
193
|
+
: 'DEFAULT_PROFILE_PATCH_RELOAD' in appBoot ? '0.1.2' : 'rc';
|
|
183
194
|
if (hostLine === 'rc')
|
|
184
195
|
healProfilesModuleFallback(anchor, resolvedHome);
|
|
185
196
|
const composed = loadProfile(NAME, profile, anchor, resolvedHome, { userLayer: true });
|
|
@@ -189,11 +200,57 @@ export async function composePreflightPatches(profile, patchFiles, root, home, b
|
|
|
189
200
|
// the dry-run would otherwise fail on exactly the tree a first boot
|
|
190
201
|
// composes fine.
|
|
191
202
|
writeFileSync(join(composed.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG);
|
|
203
|
+
let pluginPackagesConfig;
|
|
192
204
|
if (hostLine === '0.1.2') {
|
|
193
205
|
await healProfilesModuleFallback({ installAnchor: anchor, profile: composed, home: resolvedHome });
|
|
194
206
|
}
|
|
207
|
+
else if (hostLine === '0.1.6') {
|
|
208
|
+
// The launcher compose's runtime-mode step (apps/cli composeProfile):
|
|
209
|
+
// compute the generation AFTER the profile load and root-config rewrite,
|
|
210
|
+
// materializing nothing. Awaited, so a resolution-graph failure rejects
|
|
211
|
+
// the compose instead of escaping as an unhandled rejection. The explicit
|
|
212
|
+
// home keeps the recorded profilesDir on the deployment under check. The
|
|
213
|
+
// generation is kept: the launcher hands it to the boot's PluginPackages
|
|
214
|
+
// mount (`{ generation }`), and so must the dry-run — computing it and
|
|
215
|
+
// dropping it leaves profile-tree imports to native Node resolution,
|
|
216
|
+
// which finds nothing in a tarball profile's node_modules.
|
|
217
|
+
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, profile: composed, home: resolvedHome });
|
|
218
|
+
pluginPackagesConfig = { generation };
|
|
219
|
+
}
|
|
220
|
+
else if (hostLine === '0.1.7') {
|
|
221
|
+
// The 0.1.7 launcher compose (apps/cli composeProfile): the resolution is
|
|
222
|
+
// computed in memory right after the profile load and root-config rewrite
|
|
223
|
+
// — the older lines' fallback projections are gone for good, so this
|
|
224
|
+
// interception is the ONLY way profile-tree imports resolve. Awaited, so
|
|
225
|
+
// a resolution-graph failure rejects the compose instead of escaping as
|
|
226
|
+
// an unhandled rejection. The resolution is kept for the boot's
|
|
227
|
+
// PluginPackages mount (`{ resolution }`), exactly as the launcher's
|
|
228
|
+
// runProfile hands it over; discarding it is the tarball-profile false
|
|
229
|
+
// FAIL (every official entry reports "failed to import" on a profile
|
|
230
|
+
// whose real boot is clean).
|
|
231
|
+
const resolution = await createRuntimeResolution({ installAnchor: anchor, profile: composed });
|
|
232
|
+
pluginPackagesConfig = { resolution };
|
|
233
|
+
}
|
|
195
234
|
const homePatches = loadOptionalPatches(NAME, join(resolvedHome, HOME_PATCH_FILENAME)) ?? [];
|
|
196
235
|
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)));
|
|
236
|
+
// The launcher provides a data-only profileContext service before the tree
|
|
237
|
+
// mounts (since 0.1.6-alpha.2; the heal-based lines had no such service).
|
|
238
|
+
// The 0.1.7 settings service injects it, so a dry-run without it leaves
|
|
239
|
+
// `settings` — and everything injecting it — pending: applies the contract
|
|
240
|
+
// promises to exercise never run, on a profile the real boot runs clean.
|
|
241
|
+
const profileContext = hostLine === '0.1.6' || hostLine === '0.1.7'
|
|
242
|
+
? {
|
|
243
|
+
name: profile,
|
|
244
|
+
dir: composed.dir,
|
|
245
|
+
patchPath: composed.patchPath,
|
|
246
|
+
installAnchor: anchor,
|
|
247
|
+
startedBundles: composed.layers.map(layer => layer.packageName),
|
|
248
|
+
cwd: process.cwd(),
|
|
249
|
+
home: resolvedHome,
|
|
250
|
+
overlays,
|
|
251
|
+
telemetryDisabledEnv: process.env.DSH_TELEMETRY_DISABLED,
|
|
252
|
+
}
|
|
253
|
+
: undefined;
|
|
197
254
|
const bundlePatches = composed.layers.flatMap(layer => layer.patches);
|
|
198
255
|
const patches = [...bundlePatches, ...composed.patches, ...homePatches, ...overlays];
|
|
199
256
|
const rows = new Map();
|
|
@@ -231,6 +288,15 @@ export async function composePreflightPatches(profile, patchFiles, root, home, b
|
|
|
231
288
|
},
|
|
232
289
|
});
|
|
233
290
|
}
|
|
291
|
+
if (profileContext !== undefined && rows.has('hmr')) {
|
|
292
|
+
// Providing profileContext satisfies the hmr row's disable expression
|
|
293
|
+
// (`!ctx.get('profileContext')`) on the runtime-resolution lines. A
|
|
294
|
+
// dry-run is one-shot — no HMR, no user-patch watchers: the boot's own
|
|
295
|
+
// tree write-back would queue a config refresh on hmr's operations queue,
|
|
296
|
+
// and dispose then awaits a queue that never drains (observed: preflight
|
|
297
|
+
// hung past boot and the process exited 13 on an unsettled await).
|
|
298
|
+
composedOverlays.push({ id: 'hmr', disabled: true });
|
|
299
|
+
}
|
|
234
300
|
if (rows.has('ankh-guard')) {
|
|
235
301
|
// The guard plugin writes state at apply (the instance-launch record,
|
|
236
302
|
// snapshots). A dry-run is NOT the real instance — isolate its state to a
|
|
@@ -252,7 +318,13 @@ export async function composePreflightPatches(profile, patchFiles, root, home, b
|
|
|
252
318
|
if (telemetryPatch !== undefined)
|
|
253
319
|
composedOverlays.push(telemetryPatch);
|
|
254
320
|
patches.push(...composedOverlays);
|
|
255
|
-
return {
|
|
321
|
+
return {
|
|
322
|
+
patches,
|
|
323
|
+
rows,
|
|
324
|
+
profileDir: composed.dir,
|
|
325
|
+
...(profileContext === undefined ? {} : { profileContext }),
|
|
326
|
+
...(pluginPackagesConfig === undefined ? {} : { pluginPackagesConfig }),
|
|
327
|
+
};
|
|
256
328
|
}
|
|
257
329
|
/**
|
|
258
330
|
* The launcher's readiness signal, mirrored: 0.1.2's runProfile provides an
|
|
@@ -336,6 +408,7 @@ export async function runPreflight(profile, patchFiles = [], root = resolveHarne
|
|
|
336
408
|
const loadLayeredEnv = appBoot.loadLayeredEnv;
|
|
337
409
|
const launchEnvironmentKey = launchEnvironment.DSH_LAUNCH_ENVIRONMENT_KEY;
|
|
338
410
|
const provideCmdline = cmdline.provideCmdline;
|
|
411
|
+
const PluginPackages = appBoot.PluginPackages;
|
|
339
412
|
let environment;
|
|
340
413
|
try {
|
|
341
414
|
environment = loadLayeredEnv(NAME);
|
|
@@ -362,8 +435,26 @@ export async function runPreflight(profile, patchFiles = [], root = resolveHarne
|
|
|
362
435
|
const appReady = createAppReadyStub();
|
|
363
436
|
// Cloned for the same insert-aliasing reason the launcher documents: boot
|
|
364
437
|
// application mutates rows by reference.
|
|
365
|
-
const ctx = await boot(NAME, rootConfig, structuredClone(patches), (hostCtx) => {
|
|
438
|
+
const ctx = await boot(NAME, rootConfig, structuredClone(patches), async (hostCtx) => {
|
|
439
|
+
// Mirror runProfile's prepare order: profileContext, launch environment,
|
|
440
|
+
// PluginPackages, cmdline — all before boot() mounts the root include.
|
|
441
|
+
if (composed.profileContext !== undefined)
|
|
442
|
+
hostCtx.provide?.('profileContext', composed.profileContext);
|
|
366
443
|
hostCtx.provide?.(launchEnvironmentKey, environment);
|
|
444
|
+
// On the runtime-resolution lines the composed resolution/generation
|
|
445
|
+
// must be mounted in-process through PluginPackages BEFORE the config
|
|
446
|
+
// tree mounts — boot() awaits prepare before the root include, so every
|
|
447
|
+
// entry import resolves through the interception. Skipping the mount is
|
|
448
|
+
// not a neutral shortcut: without it Node resolves profile-tree imports
|
|
449
|
+
// natively, a tarball profile's node_modules holds no official packages,
|
|
450
|
+
// and the dry-run reports a wall of "failed to import" on a tree the
|
|
451
|
+
// real launcher boots clean.
|
|
452
|
+
if (composed.pluginPackagesConfig !== undefined) {
|
|
453
|
+
if (PluginPackages === undefined || hostCtx.plugin === undefined) {
|
|
454
|
+
throw new Error('host line requires a PluginPackages mount but the loaded app-boot does not export PluginPackages');
|
|
455
|
+
}
|
|
456
|
+
await hostCtx.plugin(PluginPackages, composed.pluginPackagesConfig);
|
|
457
|
+
}
|
|
367
458
|
provideCmdline(hostCtx, { args: [], exit: () => { }, ready: appReady.service });
|
|
368
459
|
});
|
|
369
460
|
// The launcher commits readiness once boot and host setup settle; a
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khorsheed/dsh-ankh-guard",
|
|
3
3
|
"description": "Hard gate for self-modification restarts: a green-build credential bound to the git HEAD, checked before any restart of the running instance",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"types": "./lib/types/client/index.d.ts",
|
|
26
26
|
"default": "./lib/client.js"
|
|
27
27
|
},
|
|
28
|
-
"./package.json": "./package.json"
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
"./locale/*.json": "./locale/*.json"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"lib/*.js",
|
|
@@ -37,12 +38,14 @@
|
|
|
37
38
|
"scripts/install-systemd.sh",
|
|
38
39
|
"skills/**/*.md",
|
|
39
40
|
"cordis.patch.yml",
|
|
41
|
+
"locale/*.json",
|
|
40
42
|
"CHANGELOG.md"
|
|
41
43
|
],
|
|
42
44
|
"license": "MIT",
|
|
43
45
|
"peerDependencies": {
|
|
44
46
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
45
47
|
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
48
|
+
"@deepseek-ai/dsh-agent-preset-registry": "^0.1.0-rc.6",
|
|
46
49
|
"@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
|
|
47
50
|
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
48
51
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -52,6 +55,12 @@
|
|
|
52
55
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
56
|
},
|
|
54
57
|
"peerDependenciesMeta": {
|
|
58
|
+
"@deepseek-ai/dsh-agent-preset-registry": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@deepseek-ai/dsh-agent-presets": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
55
64
|
"@deepseek-ai/dsh-client-connection": {
|
|
56
65
|
"optional": true
|
|
57
66
|
},
|
|
@@ -59,21 +68,6 @@
|
|
|
59
68
|
"optional": true
|
|
60
69
|
}
|
|
61
70
|
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
65
|
-
"@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
|
|
66
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.1",
|
|
67
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
|
|
68
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
69
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.1",
|
|
70
|
-
"@deepseek-ai/dsh-skill": "^0.1.5-rc.1",
|
|
71
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
72
|
-
"@types/node": "^22.0.0",
|
|
73
|
-
"tsdown": "^0.22.2",
|
|
74
|
-
"typescript": "^5.9.0",
|
|
75
|
-
"vitest": "^3.0.0"
|
|
76
|
-
},
|
|
77
71
|
"keywords": [
|
|
78
72
|
"dsh",
|
|
79
73
|
"dsh-plugin",
|
|
@@ -96,8 +90,8 @@
|
|
|
96
90
|
},
|
|
97
91
|
"compat": {
|
|
98
92
|
"minHost": "0.1.5-rc.1",
|
|
99
|
-
"notes": "composition-preflight runs via the standalone preflight-runner and degrades to a notice when no live harness checkout resolves; original-tab browser handoff feature-probes optional WebServer/connection auth seams; reversible state quarantine was exercised in a live 0.1.1-rc.2 to 0.1.2-alpha.4 cutover",
|
|
100
|
-
"verifiedHost": "0.1.
|
|
93
|
+
"notes": "0.1.5-rc.1 full-line boot-verified 2026-09-25 (42 packages including capture) through the three compat layers: preset-registry dual-name probe, dual-shape typert codecs, and typert faces carrying zod@4; 0.1.7-rc.2 is the 3080 production-verified line; composition-preflight runs via the standalone preflight-runner and degrades to a notice when no live harness checkout resolves; original-tab browser handoff feature-probes optional WebServer/connection auth seams; reversible state quarantine was exercised in a live 0.1.1-rc.2 to 0.1.2-alpha.4 cutover",
|
|
94
|
+
"verifiedHost": "0.1.7-rc.2"
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
}
|
|
@@ -52,6 +52,8 @@ $GUARD verify --repo <repo> --state-dir "$DSH_HOME/state"
|
|
|
52
52
|
|
|
53
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
54
|
|
|
55
|
+
Scope the evidence command to the change being proved, and run it against the repo that change lives in. When the change lives OUTSIDE the credential repo — plugin packages shipped to a profile as tarballs, say — the credential repo's monorepo-wide suite proves nothing about the change and may carry unrelated red (a 35k-test suite with hundreds of pre-existing failures in experimental packages takes ~11 minutes to fail and records nothing); that is the deployment driver's job (e.g. `pnpm deploy:3080` records trust-command evidence from its own green gate — see below). Hand `record --run` is for changes inside the credential repo itself, scoped to the suites the change touches whenever that is honest. And because `record` clears the old credential and the reusable proof before running, a failed evidence command leaves the gate with zero valid evidence — `verify` first, record only what you must.
|
|
56
|
+
|
|
55
57
|
`--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
58
|
|
|
57
59
|
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:
|