mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-08 07:37:48 +02:00
fix(gateguard): surface graduated recovery hints
Change-Id: I6ade0a2a54a26bd5721c62edf7efa462e8043a08 Co-authored-by: TRAE CLI <traecli@bytedance.com>
This commit is contained in:
committed by
haelyra
co-authored by
TRAE CLI
parent
c40d0e4f7c
commit
a4d72b2271
@@ -42,6 +42,10 @@ const MAX_SESSION_KEYS = 50;
|
||||
const ROUTINE_BASH_SESSION_KEY = '__bash_session__';
|
||||
const EDIT_WRITE_HOOK_ID = 'pre:edit-write:gateguard-fact-force';
|
||||
const BASH_HOOK_ID = 'pre:bash:gateguard-fact-force';
|
||||
const EDIT_WRITE_NARROW_RECOVERY_HINT =
|
||||
'Narrow recovery: add a matching path glob to `GATEGUARD_EXEMPT_GLOBS` to skip first-touch Edit/Write checks without disabling destructive Bash checks.';
|
||||
const ROUTINE_BASH_NARROW_RECOVERY_HINT =
|
||||
'Narrow recovery: set `GATEGUARD_BASH_ROUTINE_DISABLED=1`; destructive Bash checks remain active.';
|
||||
const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable']);
|
||||
const ECC_ENABLE_VALUES = new Set(['1', 'true', 'on', 'enabled', 'enable', 'yes']);
|
||||
|
||||
@@ -1097,7 +1101,7 @@ function condensedGateMsg(action, filePath, ordinal) {
|
||||
return (
|
||||
`[Fact-Forcing Gate] (denial #${ordinal} this session) First ${action} of ${safe}: ` +
|
||||
"briefly state importers/callers, affected API, data schemas if any, and the user's verbatim instruction, then retry. " +
|
||||
'(ECC_GATEGUARD=off disables this gate.)'
|
||||
'(Use GATEGUARD_EXEMPT_GLOBS for path-scoped exemptions; ECC_GATEGUARD=off disables this gate.)'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1128,9 +1132,15 @@ function routineBashMsg() {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) {
|
||||
function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID], narrowRecoveryHint = '') {
|
||||
const disableTargets = hookIds.map(hookId => `\`${hookId}\``).join(' or ');
|
||||
return [message, '', `Recovery: if GateGuard is blocking setup or repair work, run this session with \`ECC_GATEGUARD=off\` or add ${disableTargets} to \`ECC_DISABLED_HOOKS\`.`].join('\n');
|
||||
const recoveryLines = narrowRecoveryHint ? [narrowRecoveryHint, ''] : [];
|
||||
return [
|
||||
message,
|
||||
'',
|
||||
...recoveryLines,
|
||||
`Recovery: if GateGuard is blocking setup or repair work, run this session with \`ECC_GATEGUARD=off\` or add ${disableTargets} to \`ECC_DISABLED_HOOKS\`.`
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function isSubagentInvocation(data) {
|
||||
@@ -1148,12 +1158,15 @@ function isSubagentInvocation(data) {
|
||||
function denyResult(reason, options = {}) {
|
||||
const includeRecoveryHint = options.includeRecoveryHint !== false;
|
||||
const hookIds = Array.isArray(options.hookIds) && options.hookIds.length > 0 ? options.hookIds : [EDIT_WRITE_HOOK_ID];
|
||||
const narrowRecoveryHint = typeof options.narrowRecoveryHint === 'string' ? options.narrowRecoveryHint : '';
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'deny',
|
||||
permissionDecisionReason: includeRecoveryHint ? withRecoveryHint(reason, hookIds) : reason
|
||||
permissionDecisionReason: includeRecoveryHint
|
||||
? withRecoveryHint(reason, hookIds, narrowRecoveryHint)
|
||||
: reason
|
||||
}
|
||||
}),
|
||||
exitCode: 0
|
||||
@@ -1210,7 +1223,9 @@ function run(rawInput) {
|
||||
const action = toolName === 'Edit' ? 'edit' : 'creation';
|
||||
return denyResult(condensedGateMsg(action, filePath, denials), { includeRecoveryHint: false });
|
||||
}
|
||||
return denyResult(toolName === 'Edit' ? editGateMsg(filePath) : writeGateMsg(filePath));
|
||||
return denyResult(toolName === 'Edit' ? editGateMsg(filePath) : writeGateMsg(filePath), {
|
||||
narrowRecoveryHint: EDIT_WRITE_NARROW_RECOVERY_HINT
|
||||
});
|
||||
}
|
||||
|
||||
return rawInput; // allow
|
||||
@@ -1232,7 +1247,9 @@ function run(rawInput) {
|
||||
if (denials > getFullDenialBudget()) {
|
||||
return denyResult(condensedGateMsg('edit', filePath, denials), { includeRecoveryHint: false });
|
||||
}
|
||||
return denyResult(editGateMsg(filePath));
|
||||
return denyResult(editGateMsg(filePath), {
|
||||
narrowRecoveryHint: EDIT_WRITE_NARROW_RECOVERY_HINT
|
||||
});
|
||||
}
|
||||
}
|
||||
return rawInput; // allow
|
||||
@@ -1268,7 +1285,10 @@ function run(rawInput) {
|
||||
if (!markChecked(ROUTINE_BASH_SESSION_KEY)) {
|
||||
return allowWithStateWarning();
|
||||
}
|
||||
return denyResult(routineBashMsg(), { hookIds: [BASH_HOOK_ID] });
|
||||
return denyResult(routineBashMsg(), {
|
||||
hookIds: [BASH_HOOK_ID],
|
||||
narrowRecoveryHint: ROUTINE_BASH_NARROW_RECOVERY_HINT
|
||||
});
|
||||
}
|
||||
|
||||
return rawInput; // allow
|
||||
|
||||
@@ -145,6 +145,8 @@ function runTests() {
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Fact-Forcing Gate'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('import/require'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('/src/app.js'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'Edit denial should show the path-scoped exemption control');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_BASH_ROUTINE_DISABLED'), 'Edit denial should not suggest the routine Bash control');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -538,6 +540,8 @@ function runTests() {
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('ECC_GATEGUARD=off'), 'denial reason should show the direct recovery env toggle');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('ECC_DISABLED_HOOKS'), 'denial reason should mention the existing hook-id disable control');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'Edit/Write denial should show the path-scoped exemption control');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_BASH_ROUTINE_DISABLED'), 'Edit/Write denial should not suggest the routine Bash control');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -558,6 +562,9 @@ function runTests() {
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(reason.includes('pre:bash:gateguard-fact-force'), 'routine Bash denial should show the Bash hook ID');
|
||||
assert.ok(!reason.includes('pre:edit-write:gateguard-fact-force'), 'routine Bash denial should not show the Edit/Write hook ID as the targeted disable');
|
||||
assert.ok(reason.includes('GATEGUARD_BASH_ROUTINE_DISABLED=1'), 'routine Bash denial should show the narrow routine-gate control');
|
||||
assert.ok(reason.includes('destructive Bash checks remain active'), 'routine Bash denial should preserve the destructive-check safety boundary');
|
||||
assert.ok(!reason.includes('GATEGUARD_EXEMPT_GLOBS'), 'routine Bash denial should not suggest the Edit/Write path control');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -577,6 +584,9 @@ function runTests() {
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive command detected'));
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('ECC_GATEGUARD=off'), 'destructive gate should not advertise disabling GateGuard');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('ECC_DISABLED_HOOKS'), 'destructive gate should not advertise disabling its hook');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_BASH_ROUTINE_DISABLED'), 'destructive gate should not advertise the routine-only bypass');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'destructive gate should not advertise the Edit/Write path exemption');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -602,6 +612,7 @@ function runTests() {
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Fact-Forcing Gate'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('/src/multi-a.js'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'MultiEdit denial should show the path-scoped exemption control');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -2556,6 +2567,7 @@ function runTests() {
|
||||
assert.ok(!reason.includes('present these facts'), 'no repeated four-fact block');
|
||||
assert.ok(!reason.includes('\n'), 'condensed message is a single line');
|
||||
assert.ok(reason.includes('ECC_GATEGUARD=off'), 'condensed message keeps a recovery hint');
|
||||
assert.ok(reason.includes('GATEGUARD_EXEMPT_GLOBS'), 'condensed Edit denial keeps the path-scoped recovery hint');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -2571,6 +2583,8 @@ function runTests() {
|
||||
const secondReason = second.hookSpecificOutput.permissionDecisionReason;
|
||||
assert.ok(firstReason.includes('denial #6'), `expected ordinal 6, got: ${firstReason}`);
|
||||
assert.ok(secondReason.includes('denial #7'), `expected ordinal 7, got: ${secondReason}`);
|
||||
assert.ok(firstReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'condensed Write denial keeps the path-scoped recovery hint');
|
||||
assert.ok(!firstReason.includes('GATEGUARD_BASH_ROUTINE_DISABLED'), 'condensed Write denial should not suggest the routine Bash control');
|
||||
assert.notStrictEqual(firstReason, secondReason, 'successive denials must differ so they cannot compound verbatim');
|
||||
})
|
||||
)
|
||||
@@ -2635,6 +2649,7 @@ function runTests() {
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('denial #5'));
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('present these facts'));
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('GATEGUARD_EXEMPT_GLOBS'), 'condensed MultiEdit denial keeps the path-scoped recovery hint');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
|
||||
Reference in New Issue
Block a user