@ngockhoale/ukit 2.7.5 → 2.7.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.7.6 - 2026-09-20
6
+
7
+ Permission posture work — cycle C41 (TASK-001..004): omp prompts aligned to
8
+ Claude Code — routine commands auto-allow, gates kept only for destructive
9
+ operations.
10
+
11
+ - `rm -rf *` static catch-all removed from `templates/.omp/config.yml`
12
+ `bash.patterns`, `templates/.claude/settings.json`, and
13
+ `permissionPolicy.CANONICAL_DENY` (16→15). The `block-dangerous` hook remains
14
+ the authority for generic `rm -rf <dir>` (fail-closed), so coverage for
15
+ real destructive targets is unchanged while routine/safe cleanup paths no
16
+ longer hit the static deny on both hosts.
17
+ - `ompConfigMerge` now coerces preserved legacy `prompt`/`ask` bash.patterns
18
+ to `allow` instead of `deny` — under rendered `yolo` a broad preserved
19
+ pattern (worst case `*`) could globally brick bash. Preserved `deny` still
20
+ passes through; canonical deny + the hook still gate destructive commands.
21
+ - `permissionDoctor` new FAIL check `omp-approval-mode-not-pinned`: under
22
+ `orchestration.permissionMode: unattended`, fires when omp `approvalMode`
23
+ is missing or not `yolo`; plus an advisory audit of preserved bash.patterns.
24
+ `applicable:false` when `.omp/config.yml` is absent or mode ≠ unattended.
25
+ - `sensitive-data-guard` added to `CANONICAL_PROTECTED` (no precedence
26
+ change); new `tests/manifest/c41OmpPosture.test.js` asserts routine
27
+ commands are not prompted, all destructive classes stay denied, and
28
+ enumerates the full prompt-producing gate inventory (stale-spec-guard,
29
+ sensitive-data-guard, bridge, FAIL_CLOSED).
30
+
5
31
  ## 2.7.5 - 2026-09-20
6
32
 
7
33
  Hook performance work — cycle C40 (TASK-001..007): `.mjs` module steps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.7.5",
3
+ "version": "2.7.6",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -130,7 +130,12 @@ export function mergeOmpConfig(existingText, renderedText) {
130
130
  }
131
131
 
132
132
  // §7.4 — bash.patterns union: existing-only entries (dedupe by match) appended
133
- // before the trailing `- match: "*"` allow; `prompt` re-emitted as `deny`.
133
+ // before the trailing `- match: "*"` allow; `prompt` re-emitted as `allow`.
134
+ // Under rendered yolo a `deny` is a silent hard block, so coercing prompt→deny
135
+ // would only ADD false bricks (worst case a legacy `*` pattern denying all
136
+ // bash) without adding real safety — the canonical deny set + block-dangerous
137
+ // hook still gate destructive commands. prompt→allow is behavior-identical
138
+ // to the trailing `*` allow and stays honest about what yolo can run.
134
139
  const existingPatterns = Array.isArray(existing?.bash?.patterns) ? existing.bash.patterns : [];
135
140
  const renderedPatterns = Array.isArray(rendered?.bash?.patterns) ? rendered.bash.patterns : [];
136
141
  const renderedMatches = new Set(renderedPatterns.map((p) => p?.match));
@@ -140,7 +145,7 @@ export function mergeOmpConfig(existingText, renderedText) {
140
145
  if (renderedMatches.has(pattern.match)) continue;
141
146
  let approval = pattern.approval;
142
147
  if (approval === 'prompt') {
143
- approval = 'deny';
148
+ approval = 'allow';
144
149
  report.coercedPromptPatterns.push(pattern.match);
145
150
  }
146
151
  extraPatterns.push({ ...pattern, approval });
@@ -17,10 +17,11 @@ import { findOpencodeArtifacts, opencodeSteerMessage } from './opencodeSteer.js'
17
17
  // doctor section and `--json`.
18
18
  //
19
19
  // FAIL classes (each → named failing check + report.failures entry + exit 1):
20
- // unattended-prompt prompt/ask reachable while permissionMode=unattended
21
- // deny-loss canonical deny entry missing from a host surface
22
- // unsafe-eval-mismatch surface eval approval ≠ compiled evalPosture
23
- // unrendered-token literal {{token}} left in a rendered artifact
20
+ // unattended-prompt prompt/ask reachable while permissionMode=unattended
21
+ // omp-approval-mode-not-pinned omp approvalMode ≠ 'yolo' under unattended
22
+ // deny-loss canonical deny entry missing from a host surface
23
+ // unsafe-eval-mismatch surface eval approval ≠ compiled evalPosture
24
+ // unrendered-token literal {{token}} left in a rendered artifact
24
25
  // INFO: reachable prompts under interactive/safe-auto. SKIP: absent host surface.
25
26
 
26
27
  const UNRESOLVED_TOKEN_PATTERN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
@@ -210,7 +211,32 @@ export async function inspectPermissions({ projectRoot, ompPath = 'omp' }) {
210
211
  .filter((w) => w !== 'unsafe-eval-mismatch' && w !== 'prompt-reachable-under-unattended')
211
212
  .map((w) => `${h}:${w}`));
212
213
 
214
+ // TASK-003 / cycle-41 — C34/C35 blind spot: literal prompt/ask counting never
215
+ // sees `approvalMode: write` or a missing pin, yet unattended still needs the
216
+ // yolo pin (C35 decided; here we only detect drift, never re-litigate it).
217
+ const pinApplicable = omp.exists && declared === 'unattended';
218
+ const pinFail = pinApplicable && ompTools.approvalMode !== 'yolo';
219
+
220
+ // Preserved-pattern audit — omp bash.patterns deny/allow entries outside the
221
+ // canonical set (post-TASK-001 shape) are merges carrying legacy/custom
222
+ // rules; visible as advisory, never a FAIL.
223
+ const canonicalOmpMatches = new Set(
224
+ CANONICAL_DENY.map((c) => c.omp).filter((m) => typeof m === 'string'),
225
+ );
226
+ const preservedPatternMatches = bashPatterns
227
+ .filter(
228
+ (e) =>
229
+ e &&
230
+ (e.approval === 'deny' || e.approval === 'allow') &&
231
+ typeof e.match === 'string' &&
232
+ !canonicalOmpMatches.has(e.match) &&
233
+ // the template's own trailing catch-all allow is rendered, not preserved
234
+ !(e.match === '*' && e.approval === 'allow'),
235
+ )
236
+ .map((e) => e.match);
237
+
213
238
  if (promptsFail) failures.push('unattended-prompt');
239
+ if (pinFail) failures.push('omp-approval-mode-not-pinned');
214
240
  if (denyMissingAll.length > 0) failures.push('deny-loss');
215
241
  if (evalMismatch) failures.push('unsafe-eval-mismatch');
216
242
  if (unresolvedTokens.length > 0) failures.push('unrendered-token');
@@ -248,6 +274,35 @@ export async function inspectPermissions({ projectRoot, ompPath = 'omp' }) {
248
274
  : 'no host permission surfaces installed',
249
275
  },
250
276
  ),
277
+ check(
278
+ 'omp approvalMode pinned under unattended',
279
+ !pinFail,
280
+ 'install-repairable',
281
+ 'Run ukit install to re-pin approvalMode to yolo.',
282
+ {
283
+ ...(pinApplicable ? {} : { applicable: false }),
284
+ detail: !omp.exists
285
+ ? '.omp/config.yml absent'
286
+ : declared !== 'unattended'
287
+ ? `mode ${declared} allows prompts — pin not required`
288
+ : `tools.approvalMode=${ompTools.approvalMode ?? 'absent'} (required: yolo)`,
289
+ },
290
+ ),
291
+ check(
292
+ 'preserved omp bash.patterns audit',
293
+ true,
294
+ 'advisory',
295
+ 'Review preserved entries; drop them or rerun ukit install after pruning the source config.',
296
+ {
297
+ ...(omp.exists ? {} : { applicable: false }),
298
+ ...(preservedPatternMatches.length > 0 ? { severity: 'warning' } : {}),
299
+ detail: !omp.exists
300
+ ? '.omp/config.yml absent'
301
+ : preservedPatternMatches.length === 0
302
+ ? 'no non-canonical bash.patterns entries preserved'
303
+ : `non-canonical preserved entries: ${preservedPatternMatches.join(', ')}`,
304
+ },
305
+ ),
251
306
  check(
252
307
  'deny coverage vs canonical set',
253
308
  denyMissingAll.length === 0,
@@ -35,7 +35,6 @@ export const CANONICAL_DENY = Object.freeze([
35
35
  { id: 'rm-rf-home', claude: 'Bash(rm -rf ~:*)', omp: 'rm -rf ~*', codex: 'rm -rf ~', hardDeny: true },
36
36
  { id: 'rm-rf-parent', claude: 'Bash(rm -rf ..:*)', omp: 'rm -rf ..', codex: 'rm -rf ..', hardDeny: true },
37
37
  { id: 'rm-rf-dot', claude: 'Bash(rm -rf .:*)', omp: 'rm -rf .', codex: 'rm -rf .', hardDeny: true },
38
- { id: 'rm-rf-any', claude: 'Bash(rm -rf:*)', omp: 'rm -rf *', codex: 'rm -rf' },
39
38
  { id: 'dd-raw-device', claude: 'Bash(dd if=/dev/:*)', omp: 'dd if=/dev/*', codex: 'dd if=/dev/', hardDeny: true },
40
39
  { id: 'write-raw-device', claude: 'Bash(*> /dev/sda:*)', omp: '*> /dev/sda*', codex: '> /dev/sda', hardDeny: true },
41
40
  { id: 'mkfs', claude: 'Bash(mkfs:*)', omp: 'mkfs.*', codex: 'mkfs', hardDeny: true },
@@ -48,6 +47,7 @@ export const CANONICAL_PROTECTED = Object.freeze([
48
47
  'release-credentials', // npm tokens, signing keys, .env secrets
49
48
  'protected-files', // protect-files.sh guarded paths
50
49
  'stale-spec-guard', // spec/source drift guard on edits
50
+ 'sensitive-data-guard', // secret/PII scrub gate on reads, bash, prompts
51
51
  ]);
52
52
 
53
53
  const TIER_RANK = new Map(PERMISSION_PRECEDENCE.map((tier, i) => [tier, i]));
@@ -48,7 +48,6 @@
48
48
  "Bash(rm -rf ~:*)",
49
49
  "Bash(rm -rf ..:*)",
50
50
  "Bash(rm -rf .:*)",
51
- "Bash(rm -rf:*)",
52
51
  "Bash(dd if=/dev/:*)",
53
52
  "Bash(*> /dev/sda:*)",
54
53
  "Bash(mkfs:*)",
@@ -98,14 +98,15 @@ bash:
98
98
  approval: deny
99
99
  - match: "dd if=/dev/*"
100
100
  approval: deny
101
- # block-dangerous.sh also carries a safe-cleanup allowlist for recursive force-deletes
102
- # (dist/build/coverage/.next/.nuxt/.turbo/tmp/temp/.cache/node_modules) and blocks every other
103
- # `rm -rf`. bash.patterns has no conditional/allowlist syntax to express that distinction, so
104
- # this layer is deliberately coarser: it denies ALL remaining `rm -rf` rather than approximating
105
- # the allowlist. The real script still runs via TASK-004's bridge and is the authority here —
106
- # bash.patterns is defence in depth, not the only line.
107
- - match: "rm -rf *"
108
- approval: deny
101
+ # Generic `rm -rf <dir>` is intentionally NOT denied here — the
102
+ # block-dangerous hook (running via TASK-004's bridge) is the authority:
103
+ # its safe-cleanup allowlist (dist/build/coverage/.next/.nuxt/.turbo/
104
+ # tmp/temp/.cache/node_modules, realpath-contained in the project root)
105
+ # passes routine cleanup, and every other `rm -rf` is gated fail-closed
106
+ # (ask → block on omp, deny on claude). A static `rm -rf *` deny used to
107
+ # sit here but outranked that allowlist, so allowlisted cleanup could
108
+ # never run. The hard rm denies above (/, ~, ., ..) stay — those are
109
+ # catastrophic regardless of allowlist.
109
110
  - match: "*"
110
111
  approval: allow
111
112