@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.
@@ -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. Two host API generations are mirrored and
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. DEFAULT_PROFILE_PATCH_RELOAD is a value export
178
- // only the new line carries, so it is the feature marker; a version parse
179
- // would break on exactly the unreleased builds this runner must dry-run.
180
- // Both lines stay supported: prod hosts run the rc line until 0.1.2 lands
181
- // on npm.
182
- const hostLine = 'DEFAULT_PROFILE_PATCH_RELOAD' in appBoot ? '0.1.2' : 'rc';
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 { patches, rows, profileDir: composed.dir };
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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Ankh Guard",
4
+ "description": "Gate self-modification restarts on a green build and test credential."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "重启守卫",
4
+ "description": "先证明构建与测试通过,才放行自我修改的重启。"
5
+ }
6
+ }
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.0",
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.5-rc.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: