@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
|
@@ -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 `
|
|
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 = '
|
|
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
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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]));
|
|
@@ -98,14 +98,15 @@ bash:
|
|
|
98
98
|
approval: deny
|
|
99
99
|
- match: "dd if=/dev/*"
|
|
100
100
|
approval: deny
|
|
101
|
-
#
|
|
102
|
-
# (
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|