@golden-frijoles/kit 0.4.0 → 0.5.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.
@@ -236,8 +236,11 @@ const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
236
236
  * Pure — does this key/value pair look like a credential rather than the NAME of an env var? A token prefix only
237
237
  * counts on a value long enough to be a token (`sk-shop` or `npm_utils` is a name, not a key).
238
238
  */
239
- export function looksLikeSecret(key, value) {
240
- if (typeof value !== 'string') return false;
239
+ export function looksLikeSecret(key, raw) {
240
+ if (typeof raw !== 'string') return false;
241
+ // Trimmed: the patterns are anchored, so ' sk-…' or a pasted token with a trailing newline slipped past them
242
+ // (security lens on golden-beans #164). Whitespace is never what makes a value safe.
243
+ const value = raw.trim();
241
244
  if (TOKEN_PREFIXES.test(value) && value.length >= 20) return true;
242
245
  if (TELEGRAM_BOT_TOKEN.test(value) || SLACK_WEBHOOK.test(value) || URL_WITH_PASSWORD.test(value)) return true;
243
246
  const leaf = key.split('.').pop();
package/dist/lib/jev.mjs CHANGED
@@ -14,6 +14,13 @@
14
14
  // 3. ONE COMMITTED SWITCH. `jev.config.json → rails.<rail>.mode: off | shadow | jev`. A missing file is
15
15
  // the defaults (everything off); a MALFORMED file throws, so a typo never silently means "off".
16
16
  // `egress:false` or no key makes the effective mode `off`, with the reason.
17
+ // 4. EGRESS IS A TRI-STATE, AND UNANSWERED BEHAVES AS NO (golden-frijoles-plugin D12, S5.4). `egress`
18
+ // is `true | false | null`. The template ships `null`: a stranger's diffs never leave their machine
19
+ // until they say so. `null` and `false` both make the effective mode `off`; `null` ADDITIONALLY
20
+ // emits the D11 ask (`needSetting('jev.egress', { blocking: false })`) once per process, so an agent
21
+ // asks the question instead of the guard silently staying off forever. The fallback reason for `null`
22
+ // is `egress not answered`: `review-guard.mjs` wraps it into the reason `jev could not look (egress not
23
+ // answered)`, and `prose-guard.mjs`'s judgeProse returns it as `why`. Existing consumers keep their committed `egress: true`.
17
24
  //
18
25
  // Zero deps — Node 18+ (global fetch). Everything with I/O takes injectable deps, so specs never touch the
19
26
  // network.
@@ -22,7 +29,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
22
29
  import { createHash } from 'node:crypto';
23
30
  import { join } from 'node:path';
24
31
  import { projectRoot } from './project-root.mjs';
25
- import { readSection } from './config.mjs';
32
+ import { needSetting, readSection } from './config.mjs';
26
33
 
27
34
  export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
28
35
  export const DEFAULT_MODEL = 'jev-1.13.0';
@@ -75,8 +82,13 @@ export function parseJevConfig(json) {
75
82
  throw new JevConfigError(
76
83
  `jev.config.json: model "${model}" is an alias — pin a version such as ${DEFAULT_MODEL}`
77
84
  );
78
- const egress = json.egress ?? true;
79
- if (typeof egress !== 'boolean') throw new JevConfigError('jev.config.json: egress must be true or false');
85
+ // Tri-state (D12): `true | false | null`. An explicit `null` must survive as `null` — `??` would
86
+ // silently turn it back into `true`, which is exactly the "unanswered defaults to send" bug this
87
+ // decision exists to close. Only an ABSENT key falls back to `true` (today's behaviour for a
88
+ // consumer's committed config that never mentions egress at all).
89
+ const egress = Object.prototype.hasOwnProperty.call(json, 'egress') ? json.egress : true;
90
+ if (egress !== true && egress !== false && egress !== null)
91
+ throw new JevConfigError('jev.config.json: egress must be true, false or null');
80
92
  if (json.rails !== undefined && !isObj(json.rails))
81
93
  throw new JevConfigError('jev.config.json: rails must be an object');
82
94
  const rails = {};
@@ -122,8 +134,16 @@ export function loadJevConfig({ root = repoRoot(), read = readFileSync, exists =
122
134
  throw new JevConfigError(`jev.config.json: unparseable (${e.message})`);
123
135
  },
124
136
  });
125
- // Absent everywhere → the defaults. A PRESENT legacy file holding JSON null is malformed: the parser throws.
126
- return parseJevConfig(present ? raw : {});
137
+ // Absent everywhere → the defaults (every rail off). A PRESENT legacy file holding JSON null is malformed: the
138
+ // parser throws.
139
+ if (!present) return parseJevConfig({});
140
+ // D12: a section that never gives egress a non-null value is UNANSWERED, never `true`. readSection treats a
141
+ // `null` in golden-frijoles.config.json as unset, so without this a new-file `egress: null` (or a migrated
142
+ // template config) reached parseJevConfig as a MISSING key and became `true` — sending with nobody's yes
143
+ // (review of the S5 diff). Only parseJevConfig called directly keeps the absent → true fallback.
144
+ if (raw && typeof raw === 'object' && !Array.isArray(raw) && (raw.egress === undefined || raw.egress === null))
145
+ return parseJevConfig({ ...raw, egress: null });
146
+ return parseJevConfig(raw);
127
147
  }
128
148
 
129
149
  /** Parse `KEY=value` lines. Enough for .env.local; quotes stripped. */
@@ -164,7 +184,10 @@ export function readApiKey({
164
184
  export function effectiveMode(config, rail, { key } = {}) {
165
185
  const configured = config.rails[rail].mode;
166
186
  if (configured === 'off') return { mode: 'off', configured, why: 'configured off' };
167
- if (!config.egress) return { mode: 'off', configured, why: 'egress disabled in jev.config.json' };
187
+ // Unanswered (D12): distinct from a deliberate `false` so the caller can ask once instead of
188
+ // staying silently off forever. `unanswered: true` is jevContext's cue to fire the D11 protocol.
189
+ if (config.egress === null) return { mode: 'off', configured, why: 'egress not answered', unanswered: true };
190
+ if (!config.egress) return { mode: 'off', configured, why: 'egress disabled (jev.egress: false)' };
168
191
  if (!key) return { mode: 'off', configured, why: 'no TYPESAFE_API_KEY' };
169
192
  return { mode: configured, configured, why: `configured ${configured}` };
170
193
  }
@@ -325,6 +348,18 @@ export function jevContext(rail, deps = {}) {
325
348
  const config = deps.config ?? loadJevConfig({ root });
326
349
  const key = 'key' in deps ? deps.key : readApiKey({ root });
327
350
  const eff = effectiveMode(config, rail, { key });
351
+ // D11's ask protocol, fired exactly once (needSetting's own dedup): egress was never answered, so
352
+ // tell the agent to ask instead of leaving the guard silently off forever. Non-blocking — a judge
353
+ // still returns a decision this run, via the regex fallback the caller already has for `mode:'off'`.
354
+ if (eff.unanswered) {
355
+ (deps.needSetting ?? needSetting)('jev.egress', {
356
+ blocking: false,
357
+ root,
358
+ ...(deps.read ? { read: deps.read } : {}),
359
+ ...(deps.exists ? { exists: deps.exists } : {}),
360
+ ...(deps.write ? { write: deps.write } : {}),
361
+ });
362
+ }
328
363
  return {
329
364
  root,
330
365
  config,
@@ -707,7 +707,9 @@ export async function judgeProse(draft, evidence = {}, deps = {}) {
707
707
  const units = proseUnits(draft);
708
708
  const questions = proseQuestions(units, evidence);
709
709
  if (ctx.mode === 'off' || !questions.length || !String(draft ?? '').trim()) {
710
- return { ...regex, decider: 'regex', mode: ctx.mode, regexCodes, jevCodes: null, fallback: [] };
710
+ // `why` names what kept Jev out (`egress not answered`, `no TYPESAFE_API_KEY`, …), the way the review judge's
711
+ // reason does, so a regex-decided draft never reads like the configured path.
712
+ return { ...regex, decider: 'regex', mode: ctx.mode, why: ctx.why, regexCodes, jevCodes: null, fallback: [] };
711
713
  }
712
714
 
713
715
  const chunks = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golden-frijoles/kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {