Skip to content

Commit 222a15c

Browse files
authored
fix: show decrypted row preview for arbitrary disclosure schemas (#81)
v0.5.0 only read .input and .output from the decrypted parameters when populating the row tooltip cache, so disclosures that capture other top-level keys — for example MCP tool wrappers writing command / arguments / result — looked the same as before decryption: the fallback "Additional disclosure fields present" message. Build the cache entry from whichever keys are present, capped at two to fit the tooltip's width budget. Input and output still come first so plaintext-and-encrypted rows render identically when the schema matches the server's preview convention. When the decrypt succeeded but every value is empty, the tooltip now says "🔓 Decrypted (empty parameters)" instead of pretending nothing happened.
1 parent 3433ce4 commit 222a15c

2 files changed

Lines changed: 31 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **Decrypted row preview now works for arbitrary disclosure schemas** — v0.5.0 only surfaced the `input` and `output` keys in the row tooltip, so MCP tool wrappers that capture parameters under other names (e.g. `command`, `arguments`, `result`) fell back to the generic "Additional disclosure fields present" message even when the forensic key successfully decrypted the envelope. The hydrator now keeps the first two non-empty top-level keys regardless of their names, with `input`/`output` still preferred for parity with the server's plaintext preview.
13+
1014
## [0.5.0] - 2026-06-03
1115

1216
### Added

internal/server/static/index.html

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,23 +1156,23 @@ <h2 class="modal-title" id="forensic-modal-title">Forensic decryption key</h2>
11561156
// and finally a generic "see detail view" fallback.
11571157
function disclosureTooltipInnerHTML(r) {
11581158
const cached = disclosurePreviewCache.get(r.id);
1159-
let input = '', output = '', decrypted = false;
11601159
if (cached && cached.state === 'decrypted') {
1161-
input = cached.input || '';
1162-
output = cached.output || '';
1163-
decrypted = true;
1164-
} else {
1165-
input = r.parameters_input_preview || '';
1166-
output = r.parameters_output_preview || '';
1160+
const head = '<span class="label">🔓 Decrypted</span>';
1161+
if (!cached.entries.length) {
1162+
return head + '<span class="muted text-xs">(empty parameters)</span>';
1163+
}
1164+
const body = cached.entries.map(e =>
1165+
`<span class="label">${escapeHtml(e.key)}</span><span class="value">${escapeHtml(e.value)}</span>`
1166+
).join('');
1167+
return head + body;
11671168
}
1169+
const input = r.parameters_input_preview || '';
1170+
const output = r.parameters_output_preview || '';
11681171
const parts = [];
1169-
if (decrypted) parts.push('<span class="label">🔓 Decrypted</span>');
11701172
if (input) parts.push(`<span class="label">Input</span><span class="value">${escapeHtml(input)}</span>`);
11711173
if (output) parts.push(`<span class="label">Output</span><span class="value">${escapeHtml(output)}</span>`);
1172-
if (parts.length === (decrypted ? 1 : 0)) {
1173-
return '<span class="muted text-xs">Additional disclosure fields present — see detail view</span>';
1174-
}
1175-
return parts.join('');
1174+
if (parts.length) return parts.join('');
1175+
return '<span class="muted text-xs">Additional disclosure fields present — see detail view</span>';
11761176
}
11771177

11781178
function tbodyIdFor(containerId) { return 'tbody-' + containerId; }
@@ -1266,14 +1266,24 @@ <h2 class="modal-title" id="forensic-modal-title">Forensic decryption key</h2>
12661266

12671267
// Extract a compact list-preview entry from the disclosure API response.
12681268
// Returns null for non-decrypted states so the cache only holds usable data.
1269+
// The two-key cap matches the row tooltip's width budget; we prefer
1270+
// input/output for parity with the server-side plaintext preview, then
1271+
// fall back to whatever keys the disclosure actually carries (e.g.
1272+
// command/arguments/result for MCP tool wrappers).
12691273
function disclosureCacheEntryFromResponse(resp) {
12701274
if (!resp || resp.state !== 'decrypted') return null;
12711275
const p = resp.parameters || {};
1272-
return {
1273-
state: 'decrypted',
1274-
input: formatDisclosurePreviewValue(p.input),
1275-
output: formatDisclosurePreviewValue(p.output),
1276-
};
1276+
const ordered = Object.keys(p).sort((a, b) => {
1277+
const rank = k => k === 'input' ? 0 : k === 'output' ? 1 : 2;
1278+
return rank(a) - rank(b) || a.localeCompare(b);
1279+
});
1280+
const entries = [];
1281+
for (const k of ordered) {
1282+
const v = formatDisclosurePreviewValue(p[k]);
1283+
if (v) entries.push({ key: k, value: v });
1284+
if (entries.length >= 2) break;
1285+
}
1286+
return { state: 'decrypted', entries };
12771287
}
12781288

12791289
// formatDisclosurePreviewValue mirrors the server's truncation rule for

0 commit comments

Comments
 (0)