@mutmutco/kilo-plugin 3.85.0 → 3.87.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/package.json +1 -1
- package/scripts/hook-run.mjs +1 -1
- package/scripts/secret-redact.mjs +11 -4
- package/server.mjs +22 -6
- package/skills/bootstrap/seeds/manifest.json +1 -1
package/package.json
CHANGED
package/scripts/hook-run.mjs
CHANGED
|
@@ -135,7 +135,7 @@ function normalizeInput(surface, gate, input) {
|
|
|
135
135
|
* and the deny was gone. So the line is elected by PARSING it, `hookSpecificOutput` carriers first, and
|
|
136
136
|
* the result reports `malformed` when stdout exists but cannot be reduced to exactly one — which
|
|
137
137
|
* runPolicyGate treats as a gate fault, i.e. the fail-closed fallback for vault-edit/command-ladder. */
|
|
138
|
-
function decisionEnvelope(stdout) {
|
|
138
|
+
export function decisionEnvelope(stdout) {
|
|
139
139
|
if (!stdout.trim()) return { envelope: '', stray: '', malformed: false };
|
|
140
140
|
const lines = stdout.split(/\r?\n/);
|
|
141
141
|
const objects = [];
|
|
@@ -493,21 +493,28 @@ export async function runHookGate({ input: buffered } = {}) {
|
|
|
493
493
|
}
|
|
494
494
|
const healed = Boolean(decision?.redactable) && canRewriteOutput;
|
|
495
495
|
const outcome = !decision ? 'ran' : healed ? 'heal' : 'observe';
|
|
496
|
+
// #4015/#4021: every operator- and log-facing string names the tool the HOST fired, never
|
|
497
|
+
// `decision.toolName` — that one is the name hook-run.mjs rewrote into the Claude vocabulary so it can
|
|
498
|
+
// be tested against UPDATABLE_TOOLS, and it collapsed Codex `shell` and `local_shell` into "PowerShell".
|
|
499
|
+
// #4012 fixed this call's `tool` FIELD and #4015 the stderr arms; the `action` TEXT below was converted
|
|
500
|
+
// by neither, so one row read `{"tool":"shell","action":"secret detected in PowerShell output …"}` and
|
|
501
|
+
// disagreed with itself — in the log doctor, the Stop summary and scrooge.md's detection counts read.
|
|
502
|
+
const alarmTool = tracedTool(input) || decision?.toolName || 'tool';
|
|
496
503
|
const action = !decision
|
|
497
504
|
? 'clean'
|
|
498
505
|
: healed
|
|
499
506
|
? 'redacted secrets from tool output'
|
|
500
507
|
: decision.redactable
|
|
501
|
-
? `secret detected in ${
|
|
502
|
-
: `secret detected in ${
|
|
508
|
+
? `secret detected in ${alarmTool} output and NOT masked — this host cannot rewrite tool output (no updatedToolOutput channel on this surface)`
|
|
509
|
+
: `secret detected in ${alarmTool} output but PostToolUse cannot redact this tool (harness limitation)`;
|
|
503
510
|
// Detection without masking must be audible, not just logged: on Codex the value is still on screen.
|
|
504
511
|
if (decision?.redactable && !canRewriteOutput) {
|
|
505
|
-
process.stderr.write(`[mmi-hook] secret-redact: a secret-shaped value was detected in ${
|
|
512
|
+
process.stderr.write(`[mmi-hook] secret-redact: a secret-shaped value was detected in ${alarmTool} output and could NOT be masked on this host — treat the transcript as exposed.\n`);
|
|
506
513
|
}
|
|
507
514
|
// #3630: same audibility for the non-redactable-TOOL arm. A detection in Read/WebFetch/Agent/mcp__*
|
|
508
515
|
// output landed only in the trace file — silent on the one surface where the value is still visible.
|
|
509
516
|
if (decision && !decision.redactable) {
|
|
510
|
-
process.stderr.write(`[mmi-hook] secret-redact: a secret-shaped value was detected in ${
|
|
517
|
+
process.stderr.write(`[mmi-hook] secret-redact: a secret-shaped value was detected in ${alarmTool} output and cannot be masked for this tool (no updatedToolOutput channel) — treat the transcript as exposed.\n`);
|
|
511
518
|
}
|
|
512
519
|
appendHookActivity({ event: 'PostToolUse', script: 'secret-redact', outcome, action, tool: tracedTool(input) });
|
|
513
520
|
} catch (err) {
|
package/server.mjs
CHANGED
|
@@ -24,6 +24,8 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync
|
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { decisionEnvelope } from './scripts/hook-run.mjs';
|
|
28
|
+
import { hookGate } from './scripts/hook-policy.mjs';
|
|
27
29
|
|
|
28
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
31
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'));
|
|
@@ -124,10 +126,22 @@ export function runGate(gate, payload) {
|
|
|
124
126
|
return { denied: false, reason: '', stdout: '', error: String(err && err.message) };
|
|
125
127
|
}
|
|
126
128
|
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
// #4018: elect the envelope with the RUNNER's own helper, never "the first line beginning with `{`".
|
|
130
|
+
// That weaker rule is exactly what #4016 measured failing OPEN on vault-edit: a stray line that is
|
|
131
|
+
// itself JSON wins over the real deny, and a stray write with no trailing newline merges with the
|
|
132
|
+
// envelope so nothing matches at all. It was safe here only because hook-run.mjs happens to emit one
|
|
133
|
+
// envelope or nothing — an invariant held by a different file and unasserted at this boundary.
|
|
134
|
+
const { envelope, malformed } = decisionEnvelope(stdout);
|
|
135
|
+
if (malformed) {
|
|
136
|
+
// The runner treats an unreducible stdout as a gate fault, i.e. fail-closed on the gates declared
|
|
137
|
+
// that way. Kilo is the surface where `permissionDecision` is honoured, so it must not be the one
|
|
138
|
+
// host that answers "allow" to a stream it could not read.
|
|
139
|
+
if (hookGate(gate).failure === 'closed') {
|
|
140
|
+
return { denied: true, reason: `${gate} produced no readable decision — failing closed`, stdout };
|
|
141
|
+
}
|
|
142
|
+
} else if (envelope) {
|
|
129
143
|
try {
|
|
130
|
-
const decision = JSON.parse(
|
|
144
|
+
const decision = JSON.parse(envelope).hookSpecificOutput;
|
|
131
145
|
if (decision?.permissionDecision === 'deny') {
|
|
132
146
|
return { denied: true, reason: decision.permissionDecisionReason || `${gate} denied the tool call`, stdout };
|
|
133
147
|
}
|
|
@@ -166,10 +180,12 @@ function editToolInput(args) {
|
|
|
166
180
|
/** The redactor's stdout is `{"hookSpecificOutput":{"updatedToolOutput": <redacted>}}` when it changed
|
|
167
181
|
* something, and nothing when clean. Returns the rewrite, or undefined when there is none. */
|
|
168
182
|
function readRedactorRewrite(stdout) {
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
// #4018: same election rule as runGate. `secret-output` is fail-OPEN, so an unreducible stream means
|
|
184
|
+
// "no rewrite" — the original output stands and the redactor's own stderr alarm stays the signal.
|
|
185
|
+
const { envelope, malformed } = decisionEnvelope(String(stdout ?? ''));
|
|
186
|
+
if (malformed || !envelope) return undefined;
|
|
171
187
|
try {
|
|
172
|
-
const updated = JSON.parse(
|
|
188
|
+
const updated = JSON.parse(envelope).hookSpecificOutput?.updatedToolOutput;
|
|
173
189
|
return typeof updated === 'string' || updated !== undefined ? updated : undefined;
|
|
174
190
|
} catch {
|
|
175
191
|
return undefined;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{ "target": ".github/ISSUE_TEMPLATE/config.yml", "source": "self", "ownership": "repo", "classes": ["deployable", "content"] },
|
|
9
9
|
{ "target": "scripts/next-version.mjs", "source": "self", "ownership": "org", "classes": ["deployable"] },
|
|
10
10
|
{ "target": ".github/workflows/gate.yml", "source": "seed:gate.template.yml", "ownership": "org", "classes": ["deployable"] },
|
|
11
|
-
{ "target": ".github/workflows/agent-pr.yml", "source": "self", "ownership": "org", "classes": ["deployable"] },
|
|
11
|
+
{ "target": ".github/workflows/agent-pr.yml", "source": "self", "ownership": "org", "classes": ["deployable"], "waivers": { "jerv-jervcode": "runs a hardened variant the org copy does not carry — pull_request_target off the trusted base branch, an immutable BASE..HEAD compare with a file-count equality wall, a repo-scoped App token, and a certified-head merge that re-asserts base/head/state. Its own scripts/workflow-boundary-core.test.mjs pins those properties, so a seed refresh both weakens the repo and reds its gate. Widen the org seed to match before lifting this (#4033)." } },
|
|
12
12
|
{ "target": ".github/rulesets/mmi-product-required-checks.json", "source": "seed:mmi-product-required-checks.template.json", "ownership": "org", "classes": ["deployable"] },
|
|
13
13
|
{ "target": ".gitignore", "source": "managed-block", "ownership": "org", "classes": ["deployable", "content"] },
|
|
14
14
|
{ "target": "README.md", "source": "seed:README.template.md", "ownership": "repo", "classes": ["deployable", "content"] },
|