@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.
- package/dist/agents/claude-code.js +10 -0
- package/dist/agents/codex.js +1 -1
- package/dist/agents/cursor.js +6 -1
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/persistence.js +68 -2
- package/dist/agents/registry.js +4 -3
- package/dist/agents/routing.js +118 -0
- package/dist/agents/windsurf.js +1 -1
- package/dist/audit/rules.js +6 -2
- package/dist/auth/store.js +9 -2
- package/dist/commands/agents.js +45 -1
- package/dist/commands/audit.js +6 -0
- package/dist/commands/setup.js +1 -2
- package/dist/commands/uninstall.js +2 -1
- package/dist/config/settings.js +35 -4
- package/dist/install-ui/LiveInstall.js +3 -2
- package/dist/install-ui/block-render.js +17 -13
- package/dist/launcher/agent-check.js +455 -25
- package/dist/launcher/agent-hook-exec.js +1 -1
- package/dist/launcher/agent-hook-io.js +12 -4
- package/dist/launcher/classify.js +27 -1
- package/dist/launcher/env.js +39 -7
- package/dist/launcher/install-preflight.js +15 -6
- package/dist/launcher/live-install.js +4 -3
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +4 -1
- package/dist/launcher/preflight-prompt.js +4 -3
- package/dist/launcher/run.js +90 -18
- package/dist/project/dgfile.js +2 -1
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/metadata-map.js +29 -13
- package/dist/proxy/server.js +130 -14
- package/dist/runtime/first-run.js +2 -1
- package/dist/sbom-ui/inventory.js +5 -1
- package/dist/scan/staged.js +31 -8
- package/dist/scan-ui/hooks/useScan.js +4 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +1 -1
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +3 -3
- package/dist/setup/plan.js +156 -12
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/wizard.js +9 -1
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/verify/local.js +2 -2
- package/dist/verify/package-check.js +3 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
|
@@ -9,13 +9,24 @@ const pipxProtected = new Set(["install", "upgrade", "inject", "run"]);
|
|
|
9
9
|
const uvProtected = new Set(["add", "sync"]);
|
|
10
10
|
const cargoProtected = new Set(["add", "install", "fetch", "update", "build", "test", "check", "run"]);
|
|
11
11
|
const passthrough = new Set(["help", "--help", "-h", "version", "--version", "-v", "list", "ls", "show", "freeze"]);
|
|
12
|
+
// pip3, pip3.12, python3, python3.11 are the canonical install commands on
|
|
13
|
+
// macOS/Homebrew and many Linux distros (see BINARY_FALLBACKS in
|
|
14
|
+
// resolve-real-binary.ts). The classifier keys on the base name, so a
|
|
15
|
+
// version-suffixed spelling must normalize back to pip/python or it falls
|
|
16
|
+
// through to a silent allow. `pipx`/`pipenv` are left untouched — only a bare
|
|
17
|
+
// numeric/dotted version suffix is stripped.
|
|
18
|
+
export function normalizeManagerName(name) {
|
|
19
|
+
const m = /^(pip|python)(\d+(?:\.\d+)*)?$/.exec(name);
|
|
20
|
+
return m && m[1] ? m[1] : name;
|
|
21
|
+
}
|
|
12
22
|
export function packageManagerNames() {
|
|
13
23
|
return [...supportedManagers, ...gatedManagers];
|
|
14
24
|
}
|
|
15
25
|
export function isSupportedPackageManager(manager) {
|
|
16
26
|
return supportedManagers.includes(manager);
|
|
17
27
|
}
|
|
18
|
-
export function classifyPackageManagerInvocation(
|
|
28
|
+
export function classifyPackageManagerInvocation(rawManager, args) {
|
|
29
|
+
const manager = normalizeManagerName(rawManager);
|
|
19
30
|
if (!isSupportedPackageManager(manager)) {
|
|
20
31
|
return unsupportedClassification(manager, args);
|
|
21
32
|
}
|
|
@@ -29,6 +40,13 @@ export function classifyPackageManagerInvocation(manager, args) {
|
|
|
29
40
|
return classifyByCommand(manager, args, "pnpm", pnpmProtected, "pnpm install/fetch command", "javascript");
|
|
30
41
|
}
|
|
31
42
|
if (manager === "yarn") {
|
|
43
|
+
// A bare `yarn` (no sub-command) runs `yarn install`, fetching every lockfile
|
|
44
|
+
// dependency; classify it protected rather than passthrough. Read-only
|
|
45
|
+
// version/help invocations stay passthrough.
|
|
46
|
+
const readOnlyFlag = args.some((a) => a === "--version" || a === "-v" || a === "--help" || a === "-h");
|
|
47
|
+
if (firstCommand(args) === "" && !readOnlyFlag) {
|
|
48
|
+
return protectedClassification("yarn", args, "yarn", "bare yarn installs all lockfile dependencies");
|
|
49
|
+
}
|
|
32
50
|
return classifyByCommand(manager, args, "yarn", yarnProtected, "Yarn classic install/fetch command", "javascript");
|
|
33
51
|
}
|
|
34
52
|
if (manager === "pip") {
|
|
@@ -73,6 +91,11 @@ function classifyUv(args) {
|
|
|
73
91
|
if (action === "tool" && ["run", "install", "upgrade"].includes(args[1] ?? "")) {
|
|
74
92
|
return protectedClassification("uv", args, "uv", "uv tool run/install/upgrade fetches package artifacts");
|
|
75
93
|
}
|
|
94
|
+
if (action === "run" && uvRunFetchesPackage(args)) {
|
|
95
|
+
// `uv run --with <pkg> …` fetches <pkg> from PyPI and runs it: a real
|
|
96
|
+
// fetch-and-execute path that the bare `run` verb otherwise waves through.
|
|
97
|
+
return protectedClassification("uv", args, "uv", "uv run --with fetches and runs a package");
|
|
98
|
+
}
|
|
76
99
|
if (uvProtected.has(action) || containsFetchSpec(args)) {
|
|
77
100
|
return protectedClassification("uv", args, "uv", "uv install/fetch command");
|
|
78
101
|
}
|
|
@@ -122,6 +145,9 @@ function initFetchesPackage(action, args, protectedCommands) {
|
|
|
122
145
|
const commandIndex = args.findIndex((arg) => !arg.startsWith("-"));
|
|
123
146
|
return args.slice(commandIndex + 1).some((arg) => arg !== "--" && !arg.startsWith("-"));
|
|
124
147
|
}
|
|
148
|
+
export function uvRunFetchesPackage(args) {
|
|
149
|
+
return args.some((a) => a === "--with" || a === "--with-editable" || a === "--with-requirements" || a.startsWith("--with="));
|
|
150
|
+
}
|
|
125
151
|
function containsFetchSpec(args) {
|
|
126
152
|
return args.some((arg) => /^https?:\/\//.test(arg) || /^git\+https?:\/\//.test(arg) || /\.t(ar\.)?gz(?:$|[#?])/.test(arg));
|
|
127
153
|
}
|
package/dist/launcher/env.js
CHANGED
|
@@ -13,18 +13,50 @@ function clientReadTimeoutSeconds(env) {
|
|
|
13
13
|
const verdictSeconds = Number.isFinite(verdictMs) && verdictMs > 0 ? verdictMs / 1000 : 240;
|
|
14
14
|
return Math.ceil(verdictSeconds + 60);
|
|
15
15
|
}
|
|
16
|
+
// The wrapped package manager and every lifecycle/postinstall script it runs are
|
|
17
|
+
// untrusted. The dg account credential must never reach them; only the trusted
|
|
18
|
+
// proxy worker (which makes the authenticated API calls) keeps it. Applied on
|
|
19
|
+
// EVERY spawn path, not just the proxy one.
|
|
20
|
+
export function scrubChildSecrets(env) {
|
|
21
|
+
delete env.DG_API_KEY;
|
|
22
|
+
delete env.DG_API_TOKEN;
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
// The union of proxy-routing vars for an AGENT environment. Unlike a single
|
|
26
|
+
// wrapped install, an agent runs arbitrary package managers, so every manager's
|
|
27
|
+
// proxy + CA var is set at once (npm/node, pip, uv, cargo), plus DG_PROXY_ACTIVE
|
|
28
|
+
// so the install-hook pre-screen defers statically-undecidable commands to the
|
|
29
|
+
// proxy. Every value is a literal (the proxy URL, the CA path) — no shell
|
|
30
|
+
// expansion — so it is safe to drop into an agent's settings `env` block.
|
|
31
|
+
export function buildAgentRoutingEnv(proxyUrl, caPath) {
|
|
32
|
+
const authToken = readProxyAuthToken(dirname(caPath));
|
|
33
|
+
const url = authToken ? proxyUrlWithAuth(proxyUrl, authToken) : proxyUrl;
|
|
34
|
+
const noProxy = "127.0.0.1,localhost";
|
|
35
|
+
return {
|
|
36
|
+
DG_PROXY_ACTIVE: "1",
|
|
37
|
+
NO_PROXY: noProxy,
|
|
38
|
+
no_proxy: noProxy,
|
|
39
|
+
HTTP_PROXY: url,
|
|
40
|
+
HTTPS_PROXY: url,
|
|
41
|
+
http_proxy: url,
|
|
42
|
+
https_proxy: url,
|
|
43
|
+
ALL_PROXY: url,
|
|
44
|
+
npm_config_proxy: url,
|
|
45
|
+
npm_config_https_proxy: url,
|
|
46
|
+
NODE_EXTRA_CA_CERTS: caPath,
|
|
47
|
+
REQUESTS_CA_BUNDLE: caPath,
|
|
48
|
+
PIP_CERT: caPath,
|
|
49
|
+
SSL_CERT_FILE: caPath,
|
|
50
|
+
CARGO_HTTP_CAINFO: caPath,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
16
53
|
export function buildProxyChildEnv(options) {
|
|
17
54
|
const authToken = readProxyAuthToken(dirname(options.caBundlePath));
|
|
18
55
|
const proxyUrl = authToken ? proxyUrlWithAuth(options.proxyUrl, authToken) : options.proxyUrl;
|
|
19
|
-
const env = {
|
|
56
|
+
const env = scrubChildSecrets({
|
|
20
57
|
...options.baseEnv,
|
|
21
58
|
DG_PROXY_ACTIVE: "1"
|
|
22
|
-
};
|
|
23
|
-
// The wrapped package manager and every lifecycle/postinstall script it runs
|
|
24
|
-
// are untrusted. The dg account credential must never reach them; only the
|
|
25
|
-
// trusted proxy worker (which makes the authenticated API calls) keeps it.
|
|
26
|
-
delete env.DG_API_KEY;
|
|
27
|
-
delete env.DG_API_TOKEN;
|
|
59
|
+
});
|
|
28
60
|
// dg fully controls NO_PROXY (loopback only). An inherited NO_PROXY that names
|
|
29
61
|
// the registry (or a `*`/`.org` glob) would route the manager straight past the
|
|
30
62
|
// firewall, so the inherited value is dropped, not merged.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { analyzePackages } from "../api/analyze.js";
|
|
4
|
-
import { loadUserConfig } from "../config/settings.js";
|
|
4
|
+
import { DEFAULT_CONFIG, loadUserConfig, trustsProjectOverrides } from "../config/settings.js";
|
|
5
|
+
import { honoredOverrides } from "../project/override-trust.js";
|
|
5
6
|
import { matchDecision, packageKey } from "../decisions/apply.js";
|
|
6
7
|
import { offerRememberOnIo } from "../decisions/remember-prompt.js";
|
|
7
8
|
import { provenanceDowngradeLine } from "../presentation/provenance.js";
|
|
@@ -11,14 +12,17 @@ import { findProjectRoot, loadDgFile, resolveAcceptedBy, warnUnreadableDgFile }
|
|
|
11
12
|
import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
|
|
12
13
|
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
13
14
|
export function resolvePreflightCooldown(env, ecosystem) {
|
|
15
|
+
// A corrupt user config must not silently disable the cooldown gate; fall back
|
|
16
|
+
// to defaults so any env-configured cooldown is still applied.
|
|
17
|
+
let config;
|
|
14
18
|
try {
|
|
15
|
-
|
|
16
|
-
const param = cooldownRequestParam(config, env, ecosystem, "");
|
|
17
|
-
return param ? { param, exempt: config.cooldown.exempt, ecosystem } : undefined;
|
|
19
|
+
config = loadUserConfig(env);
|
|
18
20
|
}
|
|
19
21
|
catch {
|
|
20
|
-
|
|
22
|
+
config = DEFAULT_CONFIG;
|
|
21
23
|
}
|
|
24
|
+
const param = cooldownRequestParam(config, env, ecosystem, "");
|
|
25
|
+
return param ? { param, exempt: config.cooldown.exempt, ecosystem } : undefined;
|
|
22
26
|
}
|
|
23
27
|
function isQuarantined(pkg, context, dgExemptions = []) {
|
|
24
28
|
if (!context || !pkg.cooldown) {
|
|
@@ -41,7 +45,12 @@ export function resolvePreflightDecisions(ecosystem, cwd, env = process.env) {
|
|
|
41
45
|
if (!file.readable) {
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
const honored = honoredOverrides(file, root, env, trustsProjectOverrides(env));
|
|
49
|
+
if (honored.droppedExemptions > 0 || honored.droppedDecisions > 0) {
|
|
50
|
+
process.stderr.write(`dg: ignoring ${honored.droppedExemptions} cooldown exemption(s) and ${honored.droppedDecisions} decision(s) in ${file.path} not authored on this machine — re-add with 'dg cooldown add' / 'dg decisions', or 'dg config set policy.trustProjectAllowlists true' to trust this repo\n`);
|
|
51
|
+
}
|
|
52
|
+
const gatedFile = { ...file, cooldownExemptions: honored.exemptions, decisions: honored.decisions };
|
|
53
|
+
return { root, file: gatedFile, ecosystem, env };
|
|
45
54
|
}
|
|
46
55
|
const PROCEED = { proceed: true };
|
|
47
56
|
const UNRESOLVED = { set: undefined, count: undefined };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
2
2
|
import { runInstallPreflight } from "./install-preflight.js";
|
|
3
|
-
import { isSupportedPackageManager } from "./classify.js";
|
|
3
|
+
import { isSupportedPackageManager, normalizeManagerName } from "./classify.js";
|
|
4
4
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
5
5
|
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
6
6
|
const FALL_THROUGH = { handled: false };
|
|
@@ -9,8 +9,9 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
9
9
|
if (!process.stdout.isTTY || isCiEnv(env) || resolvePresentation().mode !== "rich") {
|
|
10
10
|
return FALL_THROUGH;
|
|
11
11
|
}
|
|
12
|
-
const [
|
|
13
|
-
|
|
12
|
+
const [rawManager, ...rest] = args;
|
|
13
|
+
const manager = normalizeManagerName(rawManager ?? "");
|
|
14
|
+
if (!rawManager || !isSupportedPackageManager(manager)) {
|
|
14
15
|
return FALL_THROUGH;
|
|
15
16
|
}
|
|
16
17
|
const { childArgs, forceOverride } = stripControlArgs(rest);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
// A bare `npm install` resolves the whole dependency tree, but enumerating every
|
|
4
|
+
// transitive package in the hook would blow its time budget — that breadth is
|
|
5
|
+
// the runtime network gate's job. The static hook screens the DIRECT
|
|
6
|
+
// dependencies declared in the manifest, which is where a cloned hostile repo
|
|
7
|
+
// plants a malicious package, and caps the count so a giant manifest defers
|
|
8
|
+
// instead of silently passing.
|
|
9
|
+
const MAX_MANIFEST_SPECS = 100;
|
|
10
|
+
const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
function readBoundedFile(path) {
|
|
12
|
+
try {
|
|
13
|
+
if (statSync(path).size > MAX_MANIFEST_BYTES) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return readFileSync(path, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
24
|
+
}
|
|
25
|
+
// package-lock v2/v3 keys direct deps as "node_modules/<name>"; v1 nests them
|
|
26
|
+
// under `dependencies`. Either way, pinning to the lock means the version we
|
|
27
|
+
// screen is the version that installs.
|
|
28
|
+
function lockfileVersions(cwd) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
for (const file of ["package-lock.json", "npm-shrinkwrap.json"]) {
|
|
31
|
+
const raw = readBoundedFile(join(cwd, file));
|
|
32
|
+
if (!raw) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
const packages = asRecord(parsed.packages);
|
|
38
|
+
for (const [key, meta] of Object.entries(packages)) {
|
|
39
|
+
if (key.startsWith("node_modules/")) {
|
|
40
|
+
const version = asRecord(meta).version;
|
|
41
|
+
if (typeof version === "string") {
|
|
42
|
+
out.set(key.slice("node_modules/".length), version);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const deps = asRecord(parsed.dependencies);
|
|
47
|
+
for (const [name, meta] of Object.entries(deps)) {
|
|
48
|
+
const version = asRecord(meta).version;
|
|
49
|
+
if (typeof version === "string" && !out.has(name)) {
|
|
50
|
+
out.set(name, version);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Unparsable lockfile: fall back to range resolution, don't crash.
|
|
56
|
+
}
|
|
57
|
+
if (out.size > 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
export function readNpmManifestSpecs(cwd) {
|
|
64
|
+
const raw = readBoundedFile(join(cwd, "package.json"));
|
|
65
|
+
if (!raw) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const names = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
78
|
+
for (const name of Object.keys(asRecord(parsed[field]))) {
|
|
79
|
+
if (!seen.has(name)) {
|
|
80
|
+
seen.add(name);
|
|
81
|
+
names.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (names.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const truncated = names.length > MAX_MANIFEST_SPECS;
|
|
89
|
+
const locks = lockfileVersions(cwd);
|
|
90
|
+
const specs = names.slice(0, MAX_MANIFEST_SPECS).map((name) => ({ name, version: locks.get(name) ?? null }));
|
|
91
|
+
return { specs, truncated };
|
|
92
|
+
}
|
|
93
|
+
function parseRequirementLine(line) {
|
|
94
|
+
const stripped = line.split(/(?<!\\)#/)[0]?.trim() ?? "";
|
|
95
|
+
if (stripped.length === 0 || stripped.startsWith("-")) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// Drop environment markers (`; python_version < "3.9"`) and inline options.
|
|
99
|
+
const core = (stripped.split(";")[0] ?? "").trim();
|
|
100
|
+
if (core.length === 0 || core.includes("://") || core.startsWith("git+")) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const exact = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?==([^,;\s]+)$/.exec(core);
|
|
104
|
+
if (exact && exact[1] && exact[2]) {
|
|
105
|
+
return { name: exact[1], version: exact[2] };
|
|
106
|
+
}
|
|
107
|
+
const ranged = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?\s*(?:===|>=|<=|~=|!=|<|>|$)/.exec(core);
|
|
108
|
+
if (ranged && ranged[1]) {
|
|
109
|
+
return { name: ranged[1], version: null };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
export function readPipRequirementSpecs(args, cwd) {
|
|
114
|
+
const files = [];
|
|
115
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
116
|
+
const a = args[i];
|
|
117
|
+
if (a === undefined) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (a === "-r" || a === "--requirement") {
|
|
121
|
+
const v = args[i + 1];
|
|
122
|
+
i += 1;
|
|
123
|
+
if (v !== undefined) {
|
|
124
|
+
files.push(v);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (a.startsWith("--requirement=")) {
|
|
128
|
+
files.push(a.slice("--requirement=".length));
|
|
129
|
+
}
|
|
130
|
+
else if (a.startsWith("-r=")) {
|
|
131
|
+
files.push(a.slice("-r=".length));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (files.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const specs = [];
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
let truncated = false;
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const path = isAbsolute(file) ? file : join(cwd, file);
|
|
142
|
+
if (!existsSync(path)) {
|
|
143
|
+
// A missing requirements file means the install will fail anyway — nothing
|
|
144
|
+
// to screen, and no reason to nag the user. Leave it alone.
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const raw = readBoundedFile(path);
|
|
148
|
+
if (!raw) {
|
|
149
|
+
// The file exists but we could not read it (oversized / permissions) — that
|
|
150
|
+
// is a real unscreened install, so defer rather than pass it clean.
|
|
151
|
+
truncated = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
155
|
+
const spec = parseRequirementLine(line);
|
|
156
|
+
if (!spec || seen.has(spec.name)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (specs.length >= MAX_MANIFEST_SPECS) {
|
|
160
|
+
truncated = true;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
seen.add(spec.name);
|
|
164
|
+
specs.push(spec);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (specs.length === 0) {
|
|
168
|
+
return truncated ? { specs, truncated } : null;
|
|
169
|
+
}
|
|
170
|
+
return { specs, truncated };
|
|
171
|
+
}
|
|
@@ -7,7 +7,10 @@ const npmrcAuthPattern = /(_authToken|_auth|_password)\s*=\s*("[^"]*"|[^\s;,]+)/
|
|
|
7
7
|
const jsonSecretPattern = /("[A-Za-z0-9_.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
|
|
8
8
|
const bearerPattern = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g;
|
|
9
9
|
// Bare tokens (not in URL userinfo or KEY=value form), by their published shape.
|
|
10
|
-
|
|
10
|
+
// Alnum-only look-around instead of \b so a fixed-length token immediately
|
|
11
|
+
// adjacent to an underscore-word (e.g. npm_<36>_ci) is still redacted — \b fails
|
|
12
|
+
// there because the boundary would fall between two word characters.
|
|
13
|
+
const knownTokenShapePattern = /(?<![A-Za-z0-9])(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|hf_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|sk_(?:live|test)_[A-Za-z0-9]{20,}|rk_live_[A-Za-z0-9]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|xox[abdeprs]-[A-Za-z0-9-]{10,})(?![A-Za-z0-9])/g;
|
|
11
14
|
export function redactSecrets(text) {
|
|
12
15
|
return text
|
|
13
16
|
.replace(credentialUrlPattern, "$1<redacted>@")
|
|
@@ -7,7 +7,7 @@ import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
|
7
7
|
import { normalizePypiName } from "../policy/pypi-name.js";
|
|
8
8
|
import { resolveAcceptedBy } from "../project/dgfile.js";
|
|
9
9
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
10
|
-
import { classifyPackageManagerInvocation, isSupportedPackageManager } from "./classify.js";
|
|
10
|
+
import { classifyPackageManagerInvocation, isSupportedPackageManager, normalizeManagerName } from "./classify.js";
|
|
11
11
|
import { actionRank, promptPreflightYesNo, recordPreflightApprovals, renderCoveredWarns, renderProvenanceDowngrades, resolvePreflightDecisions } from "./install-preflight.js";
|
|
12
12
|
import { redactSecrets } from "./output-redaction.js";
|
|
13
13
|
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
@@ -26,8 +26,9 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
26
26
|
if (!io.isTTY || isCiEnv(env)) {
|
|
27
27
|
return FALL_THROUGH;
|
|
28
28
|
}
|
|
29
|
-
const [
|
|
30
|
-
|
|
29
|
+
const [rawManager, ...rest] = args;
|
|
30
|
+
const manager = normalizeManagerName(rawManager ?? "");
|
|
31
|
+
if (!rawManager || !isSupportedPackageManager(manager)) {
|
|
31
32
|
return FALL_THROUGH;
|
|
32
33
|
}
|
|
33
34
|
const ecosystem = ECOSYSTEM_BY_MANAGER[manager];
|
package/dist/launcher/run.js
CHANGED
|
@@ -3,8 +3,9 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { constants as osConstants } from "node:os";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { EXIT_UNAVAILABLE } from "../commands/types.js";
|
|
6
|
-
import { loadUserConfig } from "../config/settings.js";
|
|
6
|
+
import { loadUserConfig, trustsProjectOverrides } from "../config/settings.js";
|
|
7
7
|
import { findProjectRoot, loadDgFile } from "../project/dgfile.js";
|
|
8
|
+
import { honoredOverrides } from "../project/override-trust.js";
|
|
8
9
|
import { writeCooldownExemptionsFile } from "../proxy/cooldown-exemptions-file.js";
|
|
9
10
|
import { writePreverifiedFile } from "../proxy/preverified.js";
|
|
10
11
|
import { describeBlockedInstall, describeFlaggedWarn, renderInstallDecision } from "../install-ui/block-render.js";
|
|
@@ -13,21 +14,66 @@ import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
|
13
14
|
import { maybeSetupNudge } from "../runtime/nudges.js";
|
|
14
15
|
import { createTheme } from "../presentation/theme.js";
|
|
15
16
|
import { readProxySessionState } from "../proxy/server.js";
|
|
17
|
+
import { readServiceState } from "../service/state.js";
|
|
16
18
|
import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
|
|
17
19
|
import { classifyPackageManagerInvocation } from "./classify.js";
|
|
18
|
-
import { buildProxyChildEnv } from "./env.js";
|
|
20
|
+
import { buildProxyChildEnv, scrubChildSecrets } from "./env.js";
|
|
19
21
|
import { prepareCargoHome, userCargoHome } from "./cargo-cache.js";
|
|
20
22
|
import { cachedPipResolution } from "./install-preflight.js";
|
|
21
23
|
import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
|
|
22
24
|
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
23
25
|
import { resolveRealBinary } from "./resolve-real-binary.js";
|
|
24
|
-
import { runScriptGateAfterInstall } from "../scripts/gate.js";
|
|
26
|
+
import { runScriptGateAfterInstall, scriptGateChildEnv, scriptGateInstallArgs } from "../scripts/gate.js";
|
|
25
27
|
export const EXIT_INSTALL_BLOCKED = 2;
|
|
26
28
|
export { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
27
29
|
export function shimDepth(env) {
|
|
28
30
|
const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
|
|
29
31
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
30
32
|
}
|
|
33
|
+
// A genuine parent dg proxy sets DG_PROXY_ACTIVE=1 and routes the child through a
|
|
34
|
+
// loopback proxy URL that carries the proxy's auth token (buildProxyChildEnv ->
|
|
35
|
+
// proxyUrlWithAuth). All of these must hold for a nested invocation to safely run
|
|
36
|
+
// the real package manager directly (its traffic still flows through the live
|
|
37
|
+
// parent proxy). DG_PROXY_ACTIVE + a bare loopback URL are both forgeable, so:
|
|
38
|
+
// - the URL must carry a dg auth token (a forged HTTPS_PROXY=http://127.0.0.1:1
|
|
39
|
+
// with no credential is rejected), and
|
|
40
|
+
// - when a persistent dg service is the parent (the agent-routing case), the URL
|
|
41
|
+
// must match that live service's proxy, so a forged token cannot impersonate it.
|
|
42
|
+
// If neither matches, we start our own verifying proxy rather than trust the env.
|
|
43
|
+
export function inheritedDgProxyActive(env) {
|
|
44
|
+
if (env.DG_PROXY_ACTIVE !== "1") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const proxyUrl = env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
|
|
48
|
+
if (!proxyUrl) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(proxyUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const host = url.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
59
|
+
if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (!url.username && !url.password) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { state } = readServiceState(env);
|
|
67
|
+
if (state.running && state.proxy) {
|
|
68
|
+
const live = new URL(state.proxy.proxyUrl);
|
|
69
|
+
return live.hostname.replace(/^\[(.*)\]$/, "$1") === host && live.port === url.port;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// No readable service state — fall through to the session-proxy path below.
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
31
77
|
export function rootUnprotectedNotice(env, uid = process.getuid?.()) {
|
|
32
78
|
if (uid !== 0) {
|
|
33
79
|
return "";
|
|
@@ -59,11 +105,11 @@ export function createLaunchPlan(manager, args, env = process.env) {
|
|
|
59
105
|
classification,
|
|
60
106
|
realBinary,
|
|
61
107
|
startsProxy: classification.kind === "protected",
|
|
62
|
-
childEnv: {
|
|
108
|
+
childEnv: scrubChildSecrets({
|
|
63
109
|
...env,
|
|
64
110
|
DG_SHIM_ACTIVE: shimNonce(manager, env),
|
|
65
111
|
DG_SHIM_DEPTH: String(shimDepth(env) + 1)
|
|
66
|
-
}
|
|
112
|
+
})
|
|
67
113
|
};
|
|
68
114
|
}
|
|
69
115
|
export async function runPackageManager(manager, args, options = {}) {
|
|
@@ -84,15 +130,20 @@ export async function runPackageManager(manager, args, options = {}) {
|
|
|
84
130
|
stderr: `dg: ${manager} shim exec loop detected (DG_SHIM_DEPTH=${depth}) — refusing to re-enter\n`
|
|
85
131
|
};
|
|
86
132
|
}
|
|
87
|
-
if (depth === 1 && plan.startsProxy) {
|
|
88
|
-
const child = await spawnPackageManager(plan, args, options);
|
|
89
|
-
return {
|
|
90
|
-
exitCode: child.exitCode,
|
|
91
|
-
stdout: streamedOut(child.stdout, options),
|
|
92
|
-
stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
133
|
if (plan.startsProxy) {
|
|
134
|
+
// Reuse a parent dg proxy ONLY when one is genuinely live in this environment
|
|
135
|
+
// (the parent set DG_PROXY_ACTIVE and a loopback proxy URL). An env var such as
|
|
136
|
+
// a forged or stale DG_SHIM_DEPTH must never be what decides to skip
|
|
137
|
+
// verification: if no live dg proxy is detected, start one rather than run the
|
|
138
|
+
// install unproxied. DG_SHIM_DEPTH is only the >=2 infinite-recursion backstop.
|
|
139
|
+
if (inheritedDgProxyActive(options.env ?? process.env)) {
|
|
140
|
+
const child = await spawnPackageManager(plan, args, options);
|
|
141
|
+
return {
|
|
142
|
+
exitCode: child.exitCode,
|
|
143
|
+
stdout: streamedOut(child.stdout, options),
|
|
144
|
+
stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
96
147
|
if (!options.proxyVerdict) {
|
|
97
148
|
return runWithProductionProxy(plan, args, options);
|
|
98
149
|
}
|
|
@@ -271,6 +322,7 @@ export async function runWithProductionProxyLive(plan, args, options, onView, pr
|
|
|
271
322
|
cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
|
|
272
323
|
});
|
|
273
324
|
const spawner = options.spawner ?? defaultSpawner;
|
|
325
|
+
const hardened = applyScriptGateHardening(plan, args, childEnv, env);
|
|
274
326
|
const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
|
|
275
327
|
const poll = setInterval(() => {
|
|
276
328
|
onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
|
|
@@ -279,8 +331,8 @@ export async function runWithProductionProxyLive(plan, args, options, onView, pr
|
|
|
279
331
|
try {
|
|
280
332
|
finished = await spawner({
|
|
281
333
|
binary: plan.realBinary.path ?? "",
|
|
282
|
-
args,
|
|
283
|
-
env:
|
|
334
|
+
args: hardened.args,
|
|
335
|
+
env: hardened.env
|
|
284
336
|
});
|
|
285
337
|
}
|
|
286
338
|
finally {
|
|
@@ -326,6 +378,22 @@ function installOutcome(stdout) {
|
|
|
326
378
|
/^(added|changed|removed|updated) \d/.test(line.trim()));
|
|
327
379
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
328
380
|
}
|
|
381
|
+
// scriptGate.mode === "enforce" is applied HERE, at the spawn boundary: it
|
|
382
|
+
// rewrites the install args (--ignore-scripts) and child env
|
|
383
|
+
// (npm_config_ignore_scripts) for npm/yarn so a reputation-clean package can't
|
|
384
|
+
// run a lifecycle script. observe/off leave the install untouched. Without this
|
|
385
|
+
// the mode setting was inert — the verdict screens what is fetched, this gates
|
|
386
|
+
// what the fetched package is allowed to execute.
|
|
387
|
+
function applyScriptGateHardening(plan, args, childEnv, configEnv) {
|
|
388
|
+
const mode = loadUserConfig(configEnv).scriptGate.mode;
|
|
389
|
+
const manager = plan.classification.manager;
|
|
390
|
+
const hardenedArgs = scriptGateInstallArgs({ mode, manager, args, env: childEnv });
|
|
391
|
+
const gateEnv = scriptGateChildEnv({ mode, manager, args, env: childEnv });
|
|
392
|
+
return {
|
|
393
|
+
args: hardenedArgs,
|
|
394
|
+
env: Object.keys(gateEnv).length > 0 ? { ...childEnv, ...gateEnv } : childEnv
|
|
395
|
+
};
|
|
396
|
+
}
|
|
329
397
|
async function spawnPackageManager(plan, args, options) {
|
|
330
398
|
if (!plan.realBinary.path) {
|
|
331
399
|
return {
|
|
@@ -335,10 +403,11 @@ async function spawnPackageManager(plan, args, options) {
|
|
|
335
403
|
};
|
|
336
404
|
}
|
|
337
405
|
const spawner = options.spawner ?? defaultSpawner;
|
|
406
|
+
const hardened = applyScriptGateHardening(plan, args, plan.childEnv, options.env ?? process.env);
|
|
338
407
|
return spawner({
|
|
339
408
|
binary: plan.realBinary.path,
|
|
340
|
-
args,
|
|
341
|
-
env:
|
|
409
|
+
args: hardened.args,
|
|
410
|
+
env: hardened.env,
|
|
342
411
|
onStdout: options.onStdout,
|
|
343
412
|
onStderr: options.onStderr
|
|
344
413
|
});
|
|
@@ -410,7 +479,10 @@ export function loadProjectCooldownExemptions(env, cwd = process.cwd()) {
|
|
|
410
479
|
return [];
|
|
411
480
|
}
|
|
412
481
|
const file = loadDgFile(root);
|
|
413
|
-
|
|
482
|
+
if (!file.readable) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
return honoredOverrides(file, root, env, trustsProjectOverrides(env)).exemptions;
|
|
414
486
|
}
|
|
415
487
|
catch {
|
|
416
488
|
return [];
|
package/dist/project/dgfile.js
CHANGED
|
@@ -6,6 +6,7 @@ import { gitTrimmed } from "../util/git.js";
|
|
|
6
6
|
import { writeJsonAtomic } from "../util/json-file.js";
|
|
7
7
|
import { canonicalCooldownName } from "../policy/pypi-name.js";
|
|
8
8
|
import { acquireLockSyncWithRetry, resolveDgPaths } from "../state/index.js";
|
|
9
|
+
import { stampAuthoredEntries } from "./override-trust.js";
|
|
9
10
|
const DG_FILE_LOCK_STALE_MS = 10_000;
|
|
10
11
|
const DG_FILE_LOCK_TIMEOUT_MS = 5_000;
|
|
11
12
|
export function withDgFileLock(root, env, action) {
|
|
@@ -29,7 +30,7 @@ export function mutateDgFile(root, env, mutate) {
|
|
|
29
30
|
if (!file.readable) {
|
|
30
31
|
throw new Error(`refusing to rewrite ${file.path}: ${file.failure ?? "unreadable"}`);
|
|
31
32
|
}
|
|
32
|
-
const next = mutate(file);
|
|
33
|
+
const next = stampAuthoredEntries(mutate(file), file, root, env);
|
|
33
34
|
saveDgFile(next);
|
|
34
35
|
return next;
|
|
35
36
|
});
|
|
Binary file
|