@attalabs/vinaya 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1470,11 +1470,79 @@ function installSignalForwarding() {
1470
1470
  process.on("SIGINT", () => forward("SIGINT", 130));
1471
1471
  process.on("SIGTERM", () => forward("SIGTERM", 143));
1472
1472
  }
1473
- async function runOne(spec, timeoutMs) {
1473
+ var ENV_BASELINE_KEYS = ["PATH", "LANG", "HOME", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "TMPDIR"];
1474
+ function buildCheckEnv(env, callerEnv = process.env) {
1475
+ const out = {};
1476
+ for (const key of ENV_BASELINE_KEYS) {
1477
+ const value = callerEnv[key];
1478
+ if (value !== undefined)
1479
+ out[key] = value;
1480
+ }
1481
+ if (!env)
1482
+ return out;
1483
+ for (const [key, decl] of Object.entries(env)) {
1484
+ if (decl === true || typeof decl === "object" && "optional" in decl) {
1485
+ const value = callerEnv[key];
1486
+ if (value !== undefined)
1487
+ out[key] = value;
1488
+ } else if (typeof decl === "object" && "anyOf" in decl) {
1489
+ for (const member of decl.anyOf) {
1490
+ const value = callerEnv[member];
1491
+ if (value !== undefined)
1492
+ out[member] = value;
1493
+ }
1494
+ } else if (typeof decl === "string") {
1495
+ out[key] = decl;
1496
+ }
1497
+ }
1498
+ return out;
1499
+ }
1500
+ function missingEnvErrors(spec, callerEnv) {
1501
+ if (!spec.env)
1502
+ return [];
1503
+ const errors = [];
1504
+ for (const [key, decl] of Object.entries(spec.env)) {
1505
+ if (decl === true) {
1506
+ if (callerEnv[key] === undefined) {
1507
+ errors.push({
1508
+ schema: CHECK_SCHEMA_VERSION,
1509
+ check: spec.name,
1510
+ severity: "error",
1511
+ message: `Could not run check "${spec.name}": required environment variable \`${key}\` is not set.`,
1512
+ agent_recovery_prompt: `Set \`${key}\` in the environment before re-running \`vinaya check ${spec.name}\`, or relax its declaration to \`{ optional: true }\` in its \`env\` registration if the check can tolerate absence.`
1513
+ });
1514
+ }
1515
+ } else if (typeof decl === "object" && decl !== null && "anyOf" in decl) {
1516
+ const satisfied = decl.anyOf.some((member) => callerEnv[member] !== undefined);
1517
+ if (!satisfied) {
1518
+ errors.push({
1519
+ schema: CHECK_SCHEMA_VERSION,
1520
+ check: spec.name,
1521
+ severity: "error",
1522
+ message: `Could not run check "${spec.name}": none of its required environment variables (${decl.anyOf.join(", ")}) are set.`,
1523
+ agent_recovery_prompt: `Set one of ${decl.anyOf.join(", ")} in the environment before re-running \`vinaya check ${spec.name}\`.`
1524
+ });
1525
+ }
1526
+ }
1527
+ }
1528
+ return errors;
1529
+ }
1530
+ async function runOne(spec, timeoutMs, callerEnv) {
1474
1531
  const start = performance.now();
1532
+ const envErrors = missingEnvErrors(spec, callerEnv);
1533
+ if (envErrors.length > 0) {
1534
+ return {
1535
+ name: spec.name,
1536
+ status: "error",
1537
+ exitCode: null,
1538
+ errors: envErrors,
1539
+ durationMs: performance.now() - start
1540
+ };
1541
+ }
1475
1542
  const proc = spawn(spec.run, spec.args ?? [], {
1476
1543
  stdio: ["ignore", "pipe", "pipe"],
1477
- detached: true
1544
+ detached: true,
1545
+ env: buildCheckEnv(spec.env, callerEnv)
1478
1546
  });
1479
1547
  function killTree(signal) {
1480
1548
  const pid = proc.pid;
@@ -1590,6 +1658,7 @@ async function runOne(spec, timeoutMs) {
1590
1658
  return { name: spec.name, status, exitCode, errors, durationMs };
1591
1659
  }
1592
1660
  async function runChecks(specs, opts) {
1661
+ const callerEnv = opts.callerEnv ?? process.env;
1593
1662
  const results = new Array(specs.length);
1594
1663
  const toRun = [];
1595
1664
  for (let i = 0;i < specs.length; i++) {
@@ -1606,7 +1675,7 @@ async function runChecks(specs, opts) {
1606
1675
  const idx = toRun[cursor];
1607
1676
  cursor += 1;
1608
1677
  const spec = specs[idx];
1609
- results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs);
1678
+ results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs, callerEnv);
1610
1679
  }
1611
1680
  };
1612
1681
  const workerCount = Math.max(1, Math.min(opts.parallel, toRun.length));
@@ -1781,31 +1850,6 @@ function loadConfigChecked() {
1781
1850
  return { ok: true, config: parsed.data };
1782
1851
  }
1783
1852
 
1784
- // src/lib/env-lint.ts
1785
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
1786
- var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
1787
- function checksMissingEnvDeclaration(specs) {
1788
- const names = [];
1789
- for (const spec of specs) {
1790
- if (spec.env)
1791
- continue;
1792
- if (!existsSync5(spec.run))
1793
- continue;
1794
- let source;
1795
- try {
1796
- source = readFileSync2(spec.run, "utf8");
1797
- } catch {
1798
- continue;
1799
- }
1800
- if (ENV_READ_PATTERN.test(source))
1801
- names.push(spec.name);
1802
- }
1803
- return names;
1804
- }
1805
- function envDeclarationWarning(name) {
1806
- return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
1807
- }
1808
-
1809
1853
  // src/commands/check.ts
1810
1854
  function git(args) {
1811
1855
  try {
@@ -1868,9 +1912,6 @@ async function checkCommand(args) {
1868
1912
  console.error(`Unknown check: ${requestedName}`);
1869
1913
  process.exit(2);
1870
1914
  }
1871
- for (const name of checksMissingEnvDeclaration(specsToRun)) {
1872
- console.error(`⚠ ${envDeclarationWarning(name)}`);
1873
- }
1874
1915
  const changed = diffOnly ? changedFiles() : null;
1875
1916
  const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
1876
1917
  parallel: requestedParallel ?? defaultParallelism(),
@@ -1901,7 +1942,7 @@ async function checkCommand(args) {
1901
1942
 
1902
1943
  // src/commands/demo.ts
1903
1944
  import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
1904
- import { existsSync as existsSync6, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1945
+ import { existsSync as existsSync5, readFileSync as readFileSync2, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1905
1946
  import { isAbsolute, join as join5 } from "node:path";
1906
1947
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
1907
1948
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
@@ -1944,9 +1985,9 @@ function gitDirAbs(repoRoot) {
1944
1985
  function recoverFromCrash(repoRoot, statePath) {
1945
1986
  const currentBranch = currentBranchName(repoRoot);
1946
1987
  let state = null;
1947
- if (existsSync6(statePath)) {
1988
+ if (existsSync5(statePath)) {
1948
1989
  try {
1949
- state = JSON.parse(readFileSync3(statePath, "utf-8"));
1990
+ state = JSON.parse(readFileSync2(statePath, "utf-8"));
1950
1991
  } catch {
1951
1992
  state = null;
1952
1993
  }
@@ -2016,14 +2057,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
2016
2057
  gitCapture(repoRoot, ["clean", "-fd"]);
2017
2058
  gitCapture(repoRoot, ["checkout", originalBranch]);
2018
2059
  gitCapture(repoRoot, ["branch", "-D", demoBranch]);
2019
- if (existsSync6(statePath))
2060
+ if (existsSync5(statePath))
2020
2061
  rmSync(statePath, { force: true });
2021
2062
  }
2022
2063
  async function runDemoBreak(repoRoot, args) {
2023
2064
  const keep = args.includes("--keep");
2024
2065
  const hookDir = resolveHookDir(repoRoot);
2025
2066
  const hookPath = join5(repoRoot, hookDir, "pre-commit");
2026
- if (!existsSync6(hookPath)) {
2067
+ if (!existsSync5(hookPath)) {
2027
2068
  console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
2028
2069
  return 1;
2029
2070
  }
@@ -2081,7 +2122,7 @@ Attempting to commit…
2081
2122
  process.stdout.write(`✓ Commit passed — the fix worked.
2082
2123
  `);
2083
2124
  if (keep) {
2084
- if (existsSync6(statePath))
2125
+ if (existsSync5(statePath))
2085
2126
  unlinkSync(statePath);
2086
2127
  process.stdout.write(`
2087
2128
  --keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
@@ -2477,6 +2518,31 @@ function buildInitProductOps(name) {
2477
2518
  ];
2478
2519
  }
2479
2520
 
2521
+ // src/lib/env-lint.ts
2522
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
2523
+ var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
2524
+ function checksMissingEnvDeclaration(specs) {
2525
+ const names = [];
2526
+ for (const spec of specs) {
2527
+ if (spec.env)
2528
+ continue;
2529
+ if (!existsSync6(spec.run))
2530
+ continue;
2531
+ let source;
2532
+ try {
2533
+ source = readFileSync3(spec.run, "utf8");
2534
+ } catch {
2535
+ continue;
2536
+ }
2537
+ if (ENV_READ_PATTERN.test(source))
2538
+ names.push(spec.name);
2539
+ }
2540
+ return names;
2541
+ }
2542
+ function envDeclarationWarning(name) {
2543
+ return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
2544
+ }
2545
+
2480
2546
  // src/lib/ops.ts
2481
2547
  import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2482
2548
  import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
@@ -4300,7 +4366,7 @@ var COMMANDS = [
4300
4366
  { flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
4301
4367
  ],
4302
4368
  details: [
4303
- "Warns (stderr, every output mode, never affecting the exit code) for any check whose executable reads `process.env`/`Bun.env`/`Deno.env` directly with no `env` declared on its `CheckSpec` those checks will lose environment access once the env allowlist is wired as the spawn default in a later minor. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) to silence the warning ahead of that flip."
4369
+ "Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwardsnever the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't."
4304
4370
  ],
4305
4371
  status: "shipped"
4306
4372
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attalabs/vinaya",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Vinaya — Agentic Engineering Harness. Deterministic checks every AI coding agent must satisfy before merge.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",