@westbayberry/dg 2.2.0 → 2.3.1

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.
Files changed (48) hide show
  1. package/dist/agents/claude-code.js +10 -0
  2. package/dist/agents/codex.js +1 -1
  3. package/dist/agents/cursor.js +6 -1
  4. package/dist/agents/gate-posture.js +21 -0
  5. package/dist/agents/persistence.js +68 -2
  6. package/dist/agents/registry.js +4 -3
  7. package/dist/agents/routing.js +118 -0
  8. package/dist/agents/windsurf.js +1 -1
  9. package/dist/audit/rules.js +6 -2
  10. package/dist/auth/store.js +9 -2
  11. package/dist/commands/agents.js +45 -1
  12. package/dist/commands/audit.js +6 -0
  13. package/dist/commands/setup.js +1 -2
  14. package/dist/commands/uninstall.js +2 -1
  15. package/dist/config/settings.js +35 -4
  16. package/dist/install-ui/LiveInstall.js +3 -2
  17. package/dist/install-ui/block-render.js +17 -13
  18. package/dist/launcher/agent-check.js +455 -25
  19. package/dist/launcher/agent-hook-exec.js +1 -1
  20. package/dist/launcher/agent-hook-io.js +12 -4
  21. package/dist/launcher/classify.js +27 -1
  22. package/dist/launcher/env.js +39 -7
  23. package/dist/launcher/install-preflight.js +15 -6
  24. package/dist/launcher/live-install.js +4 -3
  25. package/dist/launcher/manifest-screen.js +171 -0
  26. package/dist/launcher/output-redaction.js +4 -1
  27. package/dist/launcher/preflight-prompt.js +4 -3
  28. package/dist/launcher/run.js +90 -18
  29. package/dist/project/dgfile.js +2 -1
  30. package/dist/project/override-trust.js +0 -0
  31. package/dist/proxy/metadata-map.js +29 -13
  32. package/dist/proxy/server.js +130 -14
  33. package/dist/runtime/first-run.js +2 -1
  34. package/dist/sbom-ui/inventory.js +5 -1
  35. package/dist/scan/staged.js +31 -8
  36. package/dist/scan-ui/hooks/useScan.js +4 -1
  37. package/dist/security/sanitize.js +8 -4
  38. package/dist/service/state.js +1 -1
  39. package/dist/service/trust-store.js +5 -9
  40. package/dist/service/worker.js +3 -3
  41. package/dist/setup/plan.js +156 -12
  42. package/dist/setup/uninstall-standalone.js +25 -0
  43. package/dist/setup-ui/wizard.js +9 -1
  44. package/dist/standalone/uninstall.mjs +2123 -0
  45. package/dist/verify/local.js +2 -2
  46. package/dist/verify/package-check.js +3 -1
  47. package/npm-shrinkwrap.json +2 -2
  48. package/package.json +2 -1
@@ -1,22 +1,24 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
3
3
  import { chmodSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { randomBytes } from "node:crypto";
5
6
  import { homedir } from "node:os";
6
7
  import { createTheme } from "../presentation/theme.js";
7
- import { reverseAgentHookEntry } from "../agents/registry.js";
8
+ import { AGENTS, AGENT_IDS, agentLabel, resolveAgentHookContext, reverseAgentHookEntry } from "../agents/registry.js";
9
+ import { GATE_ENABLE_HINT, readNetworkGatePosture } from "../agents/gate-posture.js";
8
10
  import { acquireLockSync, findStaleSessionsSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
9
11
  import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
10
12
  import { dgVersion } from "../commands/version.js";
11
13
  import { compareVersions, readLatestVersion } from "../commands/update.js";
12
14
  import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
13
- import { ConfigError, loadUserConfig } from "../config/settings.js";
15
+ import { ConfigError, DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
14
16
  import { describeCooldownSettings } from "../policy/cooldown.js";
15
17
  import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
16
18
  import { packageManagerNames } from "../launcher/classify.js";
17
19
  import { readServiceState } from "../service/state.js";
18
20
  import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
19
- export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
21
+ export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pip3", "pipx", "uv", "uvx", "cargo"]);
20
22
  export const SHIM_SENTINEL = "dg-shim-v1";
21
23
  export const RC_SENTINEL = "dg-shell-rc-v1";
22
24
  export const RC_FUNCTIONS_SENTINEL = "dg-shim-functions-v1";
@@ -35,6 +37,14 @@ const LEGACY_PYTHON_HOOK_PTH_MARKER = "import dg_pip_hook";
35
37
  export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
36
38
  export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
37
39
  export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
40
+ function readShimFailClosed(env) {
41
+ try {
42
+ return loadUserConfig(env).policy.shimFailClosed;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
38
48
  const DOCTOR_GROUP_BY_NAME = {
39
49
  node: "environment",
40
50
  package: "environment",
@@ -56,6 +66,7 @@ const DOCTOR_GROUP_BY_NAME = {
56
66
  "commit-guard": "setup",
57
67
  "stale-sessions": "setup",
58
68
  service: "setup",
69
+ "agent-gate": "setup",
59
70
  auth: "account",
60
71
  api: "account"
61
72
  };
@@ -102,6 +113,7 @@ export function buildSetupPlan(options) {
102
113
  shell,
103
114
  shimDir,
104
115
  rcPath,
116
+ failClosed: readShimFailClosed(env),
105
117
  writes: [
106
118
  {
107
119
  kind: "directory",
@@ -147,13 +159,14 @@ export function applySetupPlan(plan, now = new Date()) {
147
159
  });
148
160
  for (const command of SHIM_COMMANDS) {
149
161
  const path = join(plan.shimDir, command);
150
- writeFileSync(path, shimSource(command), {
162
+ writeFileSync(path, shimSource(command, { failClosed: plan.failClosed }), {
151
163
  encoding: "utf8",
152
164
  mode: 0o755
153
165
  });
154
166
  chmodSync(path, 0o755);
155
167
  entries.push(cleanupEntry("shim", path, "mode1", now, SHIM_SENTINEL));
156
168
  }
169
+ installStandaloneUninstaller(plan.paths.homeDir);
157
170
  mkdirSync(dirname(plan.rcPath), {
158
171
  recursive: true,
159
172
  mode: 0o700
@@ -182,6 +195,45 @@ export function applySetupPlanWithLock(plan, now = new Date()) {
182
195
  lock.release();
183
196
  }
184
197
  }
198
+ // Shims written by a dg that predates the self-cleaning template carry no
199
+ // auto-cleanup, so a plain `npm uninstall -g` leaves them stranded forever.
200
+ // Rewriting the installed shims and reinstalling the standalone uninstaller on
201
+ // the first run after a version bump propagates the current template to setups
202
+ // created by older releases.
203
+ export function refreshSetupOnUpgrade(env = process.env) {
204
+ const paths = resolveDgPaths(env);
205
+ const shimDir = join(paths.homeDir, ".dg", "shims");
206
+ if (!isShimFile(join(shimDir, "npm"))) {
207
+ return false;
208
+ }
209
+ const failClosed = readShimFailClosed(env);
210
+ try {
211
+ const lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
212
+ staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
213
+ });
214
+ try {
215
+ for (const command of SHIM_COMMANDS) {
216
+ const shimPath = join(shimDir, command);
217
+ if (!isShimFile(shimPath)) {
218
+ continue;
219
+ }
220
+ writeFileSync(shimPath, shimSource(command, { failClosed }), {
221
+ encoding: "utf8",
222
+ mode: 0o755
223
+ });
224
+ chmodSync(shimPath, 0o755);
225
+ }
226
+ installStandaloneUninstaller(paths.homeDir);
227
+ }
228
+ finally {
229
+ lock.release();
230
+ }
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ return true;
236
+ }
185
237
  export function uninstallSetup(options) {
186
238
  const paths = resolveDgPaths(options.env ?? process.env);
187
239
  const hadCacheDirBeforeLock = existsSync(paths.cacheDir);
@@ -328,6 +380,7 @@ export function doctorReport(options = {}) {
328
380
  });
329
381
  checks.push(...unavailableDoctorChecks());
330
382
  checks.push(serviceCheck(env));
383
+ checks.push(agentGateCheck(env, paths.homeDir));
331
384
  return {
332
385
  version: dgVersion(),
333
386
  checks: checks.map(enrichDoctorCheck)
@@ -596,10 +649,10 @@ function reloadInstructions(shell) {
596
649
  // The baked absolute dg path stops a PATH-shadowing dg from hijacking the shim;
597
650
  // the fallbacks make it fail open — a removed or moved dg (uninstall, mid-upgrade)
598
651
  // runs the real manager instead of bricking it.
599
- export function shimSource(command) {
652
+ export function shimSource(command, options = {}) {
600
653
  const dg = escapeDoubleQuotedSh(dgEntrypoint());
601
654
  const nonce = '"${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}' + `${command}:$$"`;
602
- return [
655
+ const head = [
603
656
  "#!/bin/sh",
604
657
  `# ${SHIM_SENTINEL}`,
605
658
  `if [ -x "${dg}" ]; then`,
@@ -610,6 +663,27 @@ export function shimSource(command) {
610
663
  `dg_bin=$(PATH="$dg_path" command -v dg 2>/dev/null)`,
611
664
  `if [ -n "$dg_bin" ]; then`,
612
665
  ` DG_SHIM_ACTIVE=${nonce} exec "$dg_bin" ${command} "$@"`,
666
+ "fi"
667
+ ];
668
+ // dg is gone from this machine. fail-closed (managed/CI) refuses the command
669
+ // so an unscanned install can't slip through; the operator removes the guard
670
+ // explicitly via `dg uninstall` while dg is present. fail-open (default)
671
+ // self-cleans dg's footprint and falls back to the real binary, because a
672
+ // plain `npm uninstall -g dg` cannot run cleanup and must not brick npm.
673
+ const failClosedTail = [
674
+ `echo "dg: install firewall required by policy (policy.shimFailClosed) but dg is unavailable; refusing to run ${command}" >&2`,
675
+ `echo "dg: reinstall dg, or run 'dg uninstall' while dg is present, to remove this guard" >&2`,
676
+ "exit 127",
677
+ ""
678
+ ];
679
+ const failOpenTail = [
680
+ `dg_home=$(dirname "$shim_dir")`,
681
+ `if [ -f "$dg_home/uninstall.mjs" ] && command -v node >/dev/null 2>&1; then`,
682
+ ` ( sleep 10`,
683
+ ` if [ ! -x "${dg}" ] && ! PATH="$dg_path" command -v dg >/dev/null 2>&1; then`,
684
+ ` node "$dg_home/uninstall.mjs" --quiet >/dev/null 2>&1`,
685
+ ` fi`,
686
+ ` ) >/dev/null 2>&1 &`,
613
687
  "fi",
614
688
  `real_bin=$(PATH="$dg_path" command -v ${command} 2>/dev/null)`,
615
689
  `if [ -n "$real_bin" ]; then`,
@@ -618,7 +692,8 @@ export function shimSource(command) {
618
692
  `echo "dg: protection unavailable and no real ${command} found on PATH" >&2`,
619
693
  "exit 127",
620
694
  ""
621
- ].join("\n");
695
+ ];
696
+ return [...head, ...(options.failClosed ? failClosedTail : failOpenTail)].join("\n");
622
697
  }
623
698
  function escapeDoubleQuotedSh(value) {
624
699
  return value.replace(/[\\"$`]/g, "\\$&");
@@ -630,6 +705,25 @@ function dgEntrypoint() {
630
705
  const argv1 = process.argv[1];
631
706
  return argv1 ? resolve(argv1) : "dg";
632
707
  }
708
+ function standaloneUninstallerSource() {
709
+ try {
710
+ return fileURLToPath(new URL("../standalone/uninstall.mjs", import.meta.url));
711
+ }
712
+ catch {
713
+ return undefined;
714
+ }
715
+ }
716
+ function installStandaloneUninstaller(homeDir) {
717
+ const source = standaloneUninstallerSource();
718
+ if (!source || !existsSync(source)) {
719
+ return;
720
+ }
721
+ const dgRoot = join(homeDir, ".dg");
722
+ mkdirSync(dgRoot, { recursive: true, mode: 0o700 });
723
+ const dest = join(dgRoot, "uninstall.mjs");
724
+ copyFileSync(source, dest);
725
+ chmodSync(dest, 0o600);
726
+ }
633
727
  function withRcBlock(existing, plan) {
634
728
  const withoutExisting = stripRcBlock(existing);
635
729
  const block = plan.shell === "fish" ? fishRcBlock(plan.shimDir) : posixRcBlock(plan.shimDir);
@@ -846,7 +940,19 @@ function configCheck(paths, env) {
846
940
  }
847
941
  try {
848
942
  accessSync(paths.configDir, constants.R_OK);
849
- loadUserConfig(env);
943
+ const config = loadUserConfig(env);
944
+ if (config.api.baseUrl !== DEFAULT_CONFIG.api.baseUrl) {
945
+ // The verdict API is the firewall's source of truth. A non-default
946
+ // endpoint is legitimate for enterprise self-host, but a silent repoint
947
+ // is also how an attacker would make every verdict come back clean, so
948
+ // surface it on the standard health check rather than trusting it quietly.
949
+ return {
950
+ name: "config",
951
+ status: "warn",
952
+ message: `dg is fetching verdicts from a non-default API endpoint: ${config.api.baseUrl} (default ${DEFAULT_CONFIG.api.baseUrl})`,
953
+ fix: `if you did not set this, your verdict source may be tampered — restore with: dg config set api.baseUrl ${DEFAULT_CONFIG.api.baseUrl}`
954
+ };
955
+ }
850
956
  return {
851
957
  name: "config",
852
958
  status: "pass",
@@ -962,6 +1068,45 @@ function serviceCheck(env) {
962
1068
  message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
963
1069
  };
964
1070
  }
1071
+ function hookedAgentLabels(env, home) {
1072
+ const labels = [];
1073
+ for (const agent of AGENT_IDS) {
1074
+ const integration = AGENTS[agent];
1075
+ if (!integration.detect(home)) {
1076
+ continue;
1077
+ }
1078
+ const ctx = resolveAgentHookContext(agent, { env, home });
1079
+ const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
1080
+ if (hooked) {
1081
+ labels.push(agentLabel(agent));
1082
+ }
1083
+ }
1084
+ return labels;
1085
+ }
1086
+ function agentGateCheck(env, home) {
1087
+ const hooked = hookedAgentLabels(env, home);
1088
+ if (hooked.length === 0) {
1089
+ return {
1090
+ name: "agent-gate",
1091
+ status: "unavailable",
1092
+ message: "No AI agent has the dg install hook; run 'dg agents on' to protect agent-run installs"
1093
+ };
1094
+ }
1095
+ const posture = readNetworkGatePosture(env);
1096
+ if (posture.live) {
1097
+ return {
1098
+ name: "agent-gate",
1099
+ status: "pass",
1100
+ message: `${hooked.join(", ")} hooked; network gate live at ${posture.proxyUrl} (installs screened at fetch by artifact hash)`
1101
+ };
1102
+ }
1103
+ return {
1104
+ name: "agent-gate",
1105
+ status: "warn",
1106
+ message: `${hooked.join(", ")} hooked but the fetch-time network gate is OFF — static pre-screen only; absolute-path, manifest-only, dynamic, and unsupported-manager installs are NOT screened`,
1107
+ fix: GATE_ENABLE_HINT
1108
+ };
1109
+ }
965
1110
  function pathPrecedenceCheck(env, shimDir, functionsPresent) {
966
1111
  const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
967
1112
  const shimIndex = pathEntries.indexOf(shimDir);
@@ -1148,9 +1293,8 @@ function scriptGateCheck(env) {
1148
1293
  if (mode === "enforce") {
1149
1294
  return {
1150
1295
  name: "script-gate",
1151
- status: "warn",
1152
- message: "scriptGate.mode is enforce, but this dg release observes only installs report script-running packages without blocking",
1153
- fix: "enforcement ships in an upcoming release; dg config set scriptGate.mode observe clears this warning"
1296
+ status: "pass",
1297
+ message: "Install-script gate enforces: protected npm/yarn installs run with --ignore-scripts so packages cannot run lifecycle scripts (pnpm blocks them natively)"
1154
1298
  };
1155
1299
  }
1156
1300
  return {
@@ -0,0 +1,25 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { uninstallSetup } from "./plan.js";
4
+ import { LockBusyError, resolveDgPaths } from "../state/index.js";
5
+ function run() {
6
+ const quiet = process.argv.includes("--quiet");
7
+ try {
8
+ const result = uninstallSetup({ keepConfig: false, all: true });
9
+ if (!quiet) {
10
+ process.stderr.write(`Dependency Guardian cleaned up ${result.removed.length} leftover item(s) after the package was removed.\n`);
11
+ }
12
+ }
13
+ catch (error) {
14
+ if (!quiet && !(error instanceof LockBusyError)) {
15
+ process.stderr.write(`dg self-cleanup could not finish: ${error.message}\n`);
16
+ }
17
+ }
18
+ try {
19
+ rmSync(join(resolveDgPaths().homeDir, ".dg"), { recursive: true, force: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ }
25
+ run();
@@ -175,7 +175,8 @@ export const SetupWizardApp = (props) => {
175
175
  export async function runSetupWizard(plan, options = {}) {
176
176
  const env = options.env ?? process.env;
177
177
  const ci = process.env.CI;
178
- if (ci === "" || ci === "0" || ci === "false") {
178
+ const restoreCi = ci === "" || ci === "0" || ci === "false";
179
+ if (restoreCi) {
179
180
  delete process.env.CI;
180
181
  }
181
182
  const agents = collectAgentOffers();
@@ -193,11 +194,18 @@ export async function runSetupWizard(plan, options = {}) {
193
194
  } }), { exitOnCtrlC: true });
194
195
  await instance.waitUntilExit();
195
196
  leaveTui();
197
+ if (restoreCi) {
198
+ process.env.CI = ci;
199
+ }
196
200
  const theme = createTheme(resolvePresentation().color);
197
201
  const accent = (text) => theme.paint("accent", text);
198
202
  const muted = (text) => theme.paint("muted", text);
199
203
  const outcome = state.outcome;
200
204
  if (outcome === null) {
205
+ // Aborted (e.g. Ctrl-C). Record the skip so the wizard does not re-intercept
206
+ // every subsequent dg command until the user explicitly declines or applies it.
207
+ markWizardSkipped(env);
208
+ markFirstRunShown(env);
201
209
  return { exitCode: 0, stdout: "", stderr: "" };
202
210
  }
203
211
  if (outcome.kind === "declined") {