Skip to content

Add optional format support to the re-identify tool - #28

Merged
jstjoe merged 7 commits into
mainfrom
claude/reidentification-format-support-winl3u
Jul 24, 2026
Merged

jstjoe merged 7 commits into
mainfrom
claude/reidentification-format-support-winl3u

Conversation

@jstjoe

@jstjoe jstjoe commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional format input to the re-identify tool so the calling agent can specify, per entity type, how tokens are rendered on the way out — fully restored (plaintext), partially masked (masked), or fully redacted (redacted). This mirrors the Skyflow Detect API format object and maps to the skyflow-node SDK's ReidentifyTextOptions (setRedactedEntities / setMaskedEntities / setPlainTextEntities).

Entity types not listed in any bucket default to full plaintext restoration, so existing calls that omit format behave exactly as before.

Example

{
  "inputString": "email [EMAIL_ADDRESS_a1b2], ssn [SSN_c3d4], name [NAME_e5f6]",
  "format": {
    "masked": ["ssn"],
    "redacted": ["email_address"],
    "plaintext": ["name"]
  }
}

SSNs come back partially masked, emails fully redacted, names restored in full, and any other detected entity type defaults to plaintext.

Changes

  • Handler (src/lib/tools/reIdentify.ts) — new optional format parameter; builds ReidentifyTextOptions (only for non-empty buckets, falling back to no options otherwise) and echoes the applied format back in the output. Invalid entity types surface as a clean error via the existing getEntityEnum mapping.
  • Server (src/server.ts) — format added to the re-identify input and output Zod schemas (input uses the shared ENTITY_KEYS enum for validation) and threaded through to the handler.
  • Types (src/lib/tools/types.ts) — new ReIdentifyFormat interface; ReIdentifyOutput extended with an optional format.
  • UI (ui/re-identify/main.ts, ui/shared/types.ts, ui/shared/styles.css) — the re-identify app now renders a "Format Applied" summary grouping entity types by treatment when a format was provided.
  • DocsCLAUDE.md, README.md, and the wrapping guide (docs/wrapping-mcp-tools-with-skyflow.md, both SDK and REST sections) updated; CHANGELOG.md entry added.

Testing

  • 8 new unit tests in tests/unit/tools/reIdentify.test.ts cover entity routing to the correct setters, echo-back, single-bucket usage, empty-bucket/no-op handling, invalid entity errors, and backward compatibility (no options passed when format is omitted).
  • Full suite green: 174 tests passing.
  • pnpm build (UI + server) passes with no type errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ


Generated by Claude Code

Adds an optional `format` input to the re-identify tool so the calling
agent can specify, per entity type, how tokens are rendered on the way
out: fully restored (plaintext), partially masked, or fully redacted.
This mirrors the Skyflow Detect API `format` object and maps to the
skyflow-node SDK's `ReidentifyTextOptions`. Entity types not listed
default to full plaintext restoration, preserving existing behavior.

- handler: build `ReidentifyTextOptions` from the format and echo the
  applied format back in the output
- server: add `format` to the re-identify input/output Zod schemas and
  thread it through to the handler
- types: add `ReIdentifyFormat`, extend `ReIdentifyOutput`
- UI: summarize the applied format treatment in the re-identify app
- tests: 8 new cases (entity routing, echo-back, empty/invalid buckets,
  backward compatibility)
- docs: CLAUDE.md, README, wrapping guide, CHANGELOG

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sky-mcp-streamable Ready Ready Preview, Comment Jul 21, 2026 5:53pm

Request Review

@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review: Add optional format support to the re-identify tool

Nice, focused change. It's backward compatible, follows the "Modifying Tools" checklist in CLAUDE.md end-to-end (handler → Zod schemas → call site → tests → UI types → UI rendering → docs), and the tests are well-structured. A few things worth a look, none blocking.

Correctness / questions

1. The "unlisted entities default to plaintext" claim is the load-bearing assumption — is it verified against the SDK/API? 🔍
The whole feature (and the README/CLAUDE.md/wrapping-guide docs) rests on: if you set only setMaskedEntities(["ssn"]), every other detected entity type comes back as full plaintext. If the Detect API actually defaults unlisted entities to redacted (or leaves them tokenized) when any bucket is set, the documented behavior would be wrong and callers could silently over-expose or under-expose data. Worth confirming with a live round-trip against a real vault, since the unit tests mock the SDK and can't catch this.

2. No handling of an entity type appearing in multiple buckets. buildReidentifyOptions will happily forward e.g. { redacted: ["ssn"], masked: ["ssn"] } — both setters get ssn. The resulting SDK behavior is undefined/last-wins. Consider either rejecting overlaps with a clear error or documenting the precedence.

Minor

3. src/lib/tools/reIdentify.ts — invalid-entity error path is a bit indirect. buildReidentifyOptions throws from inside the try, so an invalid entity surfaces through the generic catch as a server-style error rather than a distinct validation failure. It works (the test asserts "Invalid entity type"), and in practice the z.enum(ENTITY_KEYS) input schema in server.ts rejects bad entities before the handler ever runs — so this branch is only reachable via direct/unit calls. Fine as belt-and-suspenders, just noting the handler-level check is largely redundant with the schema.

4. ui/re-identify/main.ts:176getEntityClass(e.toUpperCase()) is redundant. getEntityClass already lowercases its input (entityType.toLowerCase()), so the .toUpperCase() is a no-op round-trip. Also, getEntityClass always returns entity-${key} and never falls back to .entity-default, so a format entity type without a dedicated .entity-* rule in styles.css renders a colorless badge-dot (background: var(--entity-color) with no fallback). Only ~28 of the 59 ENTITY_MAP keys have CSS classes. This mirrors the existing token-table pattern, so it's pre-existing, but the format summary makes it easier to hit since the user picks the entity types directly.

5. Echo-back includes empty buckets verbatim. ...(format && { format }) echoes {} or { redacted: [], masked: ["ssn"] } as-is. Harmless (the UI renderFormatSummary filters empty groups), just slightly noisy in the structured output.

Test coverage

Good coverage of routing, single-bucket, empty-bucket no-op, echo-back, and the invalid-entity error. The DetectEntities Proxy trick to make getEntityEnum round-trip through the mock is clean. The main gap is behavioral (point 1) — nothing here verifies the SDK actually treats unlisted entities as plaintext, which is exactly the guarantee the docs promise.

Overall: solid, well-documented, ship-ready pending confirmation of the default-plaintext behavior.

- Reject a format where the same entity type appears in more than one
  bucket (redacted/masked/plaintext), which would otherwise forward the
  entity to multiple SDK setters with undefined last-wins behavior.
- Normalize the echoed-back format to omit empty buckets so the response
  reflects only what was actually applied.
- UI: drop the redundant `.toUpperCase()` before `getEntityClass` (which
  lowercases internally) and give the format-summary badge dot a neutral
  color fallback so entity types without a dedicated CSS class still show.
- Docs/tests: note the overlap rejection in CLAUDE.md; add unit tests for
  overlap rejection, empty-bucket normalization, and intra-bucket dups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ
@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

jstjoe commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — addressed the actionable points in b036192:

  • Add Claude Code GitHub Workflow #2 (multi-bucket overlap): now rejected with a clear validation error before any SDK call ({ redacted: ["ssn"], masked: ["ssn"] } → error). Duplicates within one bucket are still allowed. Added tests.
  • rename tools #4 (UI): dropped the no-op .toUpperCase() before getEntityClass, and gave the format-summary badge dot a neutral color fallback so entity types without a dedicated .entity-* rule still render a visible dot.
  • readme updats: title, try it out section #5 (echo noise): the echoed-back format is now normalized to omit empty buckets.
  • add simple authentication and a sample .env #3: left the handler-level invalid-entity check in place intentionally as belt-and-suspenders for direct/unit calls, as you noted.

#1 (unlisted-entities → plaintext default) — flagging for a human/live check. I couldn't verify this end-to-end from here: the hosted Detect API reference returned 403 to automated fetch, and there's no live vault in this environment for a round-trip. My confidence rests on (a) the no-format baseline already restores everything to plaintext (verified by the existing tool), and (b) the SDK models format as three optional override buckets on top of that baseline, so unlisted types staying plaintext is the natural reading — and the risk direction is under-exposure (unlisted coming back less-revealed), not a leak. That said, since the unit tests mock the SDK, a quick live round-trip against a real vault before merge would be worth it to confirm unlisted entities aren't left tokenized or redacted. Happy to adjust the docs if it behaves differently.


Generated by Claude Code

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Code Review — Add optional format support to re-identify

Nicely scoped, well-tested PR. The handler-first pattern is respected, both server-side Zod (z.enum(ENTITY_KEYS)) and handler-side (getEntityEnum) validation give good defense-in-depth, the overlap-rejection hardening in the follow-up commit is a genuinely thoughtful catch, and the CLAUDE.md Modifying Tools checklist (handler / schemas / call site / tests / UI types / UI render / docs) is fully covered. Backward compatibility is preserved and explicitly tested. Nothing here blocks merge — just a few minor observations.

Correctness / behavior

  • "applied" vs "requested" echo. The docs, CHANGELOG, and code comment describe the echoed format as "the format that was applied," but it is really the callers requested format (just normalized to drop empty buckets). If the input text contains no tokens of a listed entity type, or the caller lists an entity that never appears, the echo still reports it — the response does not reflect what the SDK actually rendered. This is fine as an echo, but consider rewording to "the requested format" to avoid implying the server verified application.

  • Duplicates within a bucket survive to the SDK and the echo. findFormatOverlaps correctly de-dupes intra-bucket (via new Set) so masked: ["ssn","ssn"] is allowed — and there is a test locking that in — but that duplicate then flows through buildReidentifyOptions to setMaskedEntities(["ssn","ssn"]) and into the echoed format. Harmless, but de-duping in normalizeFormat/buildReidentifyOptions would keep the SDK call and the response tidy.

Minor / style

  • Empty format: {} in the response. Passing format: {} (or all-empty buckets) sends no options to the SDK (correct) but still echoes an empty format: {} object. There is an explicit test for this, so it is intentional — but omitting format entirely when the normalized result is empty (Object.keys(normalized).length > 0) would read cleaner. Cosmetic.

  • Duplicated non-empty-bucket logic. buildReidentifyOptions and normalizeFormat each independently repeat the bucket && bucket.length > 0 check across all three buckets. A small shared helper (or iterating over a ["redacted","masked","plaintext"] key list) would remove the repetition and keep the two in lockstep if a fourth bucket is ever added.

Tests

Coverage is strong: routing to each setter, single-bucket, empty/absent buckets, no-op forwarding, echo-back, empty-bucket normalization, intra-bucket dups, cross-bucket overlap rejection, invalid entity, and the omitted-format backward-compat path. One small addition worth considering: the invalid-entity path is validated via getEntityEnum inside the try block — a test that pairs an invalid entity with an overlap would confirm the overlap guard (which runs first, before try) short-circuits as intended.

Security

No concerns. Entity strings are enum-constrained at the schema layer, error messages only echo those constrained values, and the UI escapes labels via escapeHtml. The getEntityClass .toUpperCase() cleanup and the --entity-color neutral fallback are correct.

Review based on reading the diff against main; I was unable to run the suite in this environment, but the PR reports 174 tests green and a clean pnpm build.

Follow-up to the second review pass:

- Refactor buildReidentifyOptions / normalizeFormat / findFormatOverlaps
  around a shared FORMAT_BUCKETS list so the SDK options and the echoed
  format derive from one normalization step (no duplicated per-bucket
  logic that could drift).
- normalizeFormat now de-duplicates entity types within a bucket, so a
  value listed twice (masked: ["ssn","ssn"]) reaches the SDK and the echo
  once.
- Omit `format` from the response entirely when the normalized result is
  empty, instead of echoing `{}`.
- Reword docs/schema/type comments from "applied" to "requested": the
  echo reflects the caller's requested (normalized) format, not what the
  SDK verified it rendered.
- Tests: dedupe now expects a single entry; empty-format case asserts the
  field is omitted; add a test that the overlap guard short-circuits
  before entity-name validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ
@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Code Review — Add optional format support to the re-identify tool

Thanks for this — it's a clean, well-scoped change. The helper decomposition (normalizeFormat / findFormatOverlaps / buildReidentifyOptions) is easy to follow, backward compatibility is genuinely preserved (no formatundefined options → prior code path), and the CLAUDE.md "modifying a tool" checklist was followed end to end (handler, both Zod schemas, call-site destructure, unit tests, UI types + render, and all four docs). Test coverage on the handler is excellent — routing to each setter, echo-back, intra-bucket dedup, cross-bucket overlap, invalid-entity, ordering of the overlap guard vs. entity validation, and the no-format backward-compat case are all covered.

A few things worth addressing, one of which I'd consider blocking-until-verified.

🔴 Please verify: does the SDK actually default unlisted entities to plaintext when options are set?

This is the central behavioral promise of the PR ("Entity types not listed default to full plaintext restoration"), and it's the one thing the test suite cannot catch because every test mocks reidentifyText — the mock records which setters were called but never exercises the real Detect API's defaulting semantics.

If the Skyflow SDK / Detect API instead defaults unlisted entities to redacted (or anything other than plaintext) once a format object is present, then a caller who passes { masked: ["ssn"] } expecting every other entity to come back in full would silently get a different result — and the "preserves existing behavior" claim in the CHANGELOG would be wrong. Could you confirm this against the SDK source or a live vault, and ideally add one integration test (or a manual verification note in the PR) that exercises a real reidentifyText call with a partial format? Everything else hinges on this being true.

🟡 Minor: masked output wording may over-promise

The schema description and docs use ***-**-6789 as the masked example. The actual partial-mask output depends on the vault's masking configuration, so the exact shape may differ per vault/entity. Consider softening to "partially masked (exact format depends on vault configuration)" to avoid setting a specific expectation the vault may not meet.

🟡 Minor (cosmetic, UI): badge-dot color falls back to gray for most entities

getEntityClass returns entity-<key> for every entity, but only ~30 of the ~70 entity types define --entity-color in styles.css. Types without one (e.g. organization, age) will render their "Format Applied" badge dot in the gray fallback rather than routing to .entity-default. Purely cosmetic and the fallback is handled — just flagging in case consistent coloring is desired.

🟢 Note (not a change request): two error shapes for an invalid entity

Through the MCP server path, an invalid entity name is rejected by the z.enum(ENTITY_KEYS) input schema (protocol validation error) before reaching the handler; on a direct handler call it surfaces as { error: true, message: "Invalid entity type: …" } via the try/catch. Both are reasonable, and the handler-level guard is good defense-in-depth — just noting the divergence so it's intentional. The overlap guard correctly running before getEntityEnum (with a dedicated test) is a nice touch.

Overall: solid work. My only real ask is confirming the unlisted-entity defaulting behavior against the real API before merge, since the unit tests structurally can't. 👍

Third review pass (doc accuracy):

- Reword the "unlisted entities → plaintext" behavior across the tool
  description, output schema, README, CLAUDE.md, CHANGELOG, and wrapping
  guide to attribute it to the Detect API's default rather than stating it
  as a verified invariant. The unit tests mock the SDK and can't exercise
  the real defaulting semantics, so the docs no longer over-claim.
- Decouple the (verified) backward-compat claim — omitting `format`
  behaves exactly as before — from the API-governed default for unlisted
  entities.
- Soften the partial-mask example: drop the specific "***-**-6789" shape
  in favor of noting the masked form depends on the vault's masking
  configuration.

No behavior change — the handler still forwards the format to the SDK
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ

jstjoe commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the doc-accuracy points in 9190abd:

  • 🟡 masked wording: dropped the specific ***-**-6789 shape from the schema description and wrapping guide; it now says the masked form depends on the vault's masking configuration.
  • 🔴 unlisted-entity default: reworded the tool description, output schema, README, CLAUDE.md, CHANGELOG, and wrapping guide so the "unlisted → plaintext" behavior is attributed to the Detect API's default rather than stated as a verified invariant, and decoupled it from the genuinely-verified backward-compat claim (omitting format behaves exactly as before).

On actually verifying the default (the 🔴 ask): I can't close it from here, and want to be transparent about why rather than imply otherwise. I inspected the skyflow-node source — reidentifyText just forwards format: { redacted, masked, plaintext } to the Detect REST API; the defaulting for unlisted entities is server-side, so it can't be confirmed from the SDK, and the unit tests mock the transport so they structurally can't exercise it either. A real round-trip needs live vault credentials, which aren't available in this environment. So this remains a pre-merge check for someone with vault access: re-identify text containing a mix of entity types with e.g. { "masked": ["ssn"] } and confirm the non-SSN entities come back as full plaintext (not left tokenized/redacted). If it behaves differently, the fix is docs-only — the handler forwards the format faithfully either way.

🟡 badge-dot fallback and 🟢 dual error shapes: left as-is, per your note that both are acceptable/non-blocking.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Code Review — PR 28: Add optional format support to re-identify

Overall this is a clean, well-scoped, and genuinely well-hardened change. The multi-pass iteration visible in the commit history (overlap rejection, normalize-once refactor, doc-accuracy softening) resolved most of what I would otherwise have flagged. Backward compatibility is preserved correctly (no options forwarded and no format echoed when the arg is omitted), and the CLAUDE.md checklist for changing a tool input/output schema was followed end-to-end (handler, Zod schemas, call site, types, UI types, UI rendering, tests, docs). Nice work.

I could not run the suite locally (sandbox blocked pnpm test/vitest), so I am relying on CI for the "174 passing" claim, but the test file itself reads as comprehensive.

Strengths

  • Single normalization step (normalizeFormat) feeding both the SDK options and the echoed-back format is the right call — it eliminates the drift risk the second commit calls out.
  • Overlap guard runs before entity-name validation, with an explicit test locking that ordering in. Good defensive detail.
  • Test coverage is strong: routing to each setter, single-bucket, empty-bucket no-op, intra-bucket dedup, cross-bucket rejection, invalid entity, echo-back, and omit-when-empty are all covered.
  • Docs were appropriately softened — attributing the unlisted-to-plaintext behavior to the Detect API default rather than asserting it as a verified invariant is honest, given the tests mock the SDK.

Minor findings / nits (none blocking)

  1. UI heading wording is inconsistent with the deliberately-chosen "requested" framing. The output-schema description, the ReIdentifyOutput.format doc comment, and the CHANGELOG were all reworded from "applied" to "requested" (the echo reflects what the caller asked for, not what the SDK verified it rendered). But ui/re-identify/main.ts still renders the heading Format Applied, and renderFormatSummary param comment in ui/shared/types.ts says "formatting applied during re-identification." For consistency, consider "Requested Format".

  2. Invalid-entity error shape is indistinguishable from an API failure. In handleReIdentify, getEntityEnum throws inside the try, so an invalid entity type surfaces through the generic catch as { error: true, message: "Invalid entity type: ..." } with no code/flag. In practice the server-layer Zod z.enum(ENTITY_KEYS) rejects bad entity names before the handler is reached, so this only matters for direct handler calls (the unit test) — but a caller reading the structured output cannot tell a client-side validation error from a Skyflow API error. Optional: validate/short-circuit before the try, alongside the overlap guard.

  3. Output schema format is unconstrained (z.array(z.string())) while the input uses z.enum(ENTITY_KEYS). Fine since the echo is just normalized, already-validated input — noting it only so it is a conscious choice rather than an oversight.

  4. REST-doc format example claims lowercase entity names and the unlisted-to-plaintext default for the raw /v1/detect/reidentify endpoint. Reasonable inference from the SDK mapping, but not exercised by the mocked tests — worth a quick sanity check against a live vault before anyone relies on the REST snippet verbatim.

Security / performance

No concerns. format only controls output rendering; there is no new credential or injection surface, and the added normalization/overlap work is trivial (small fixed-size buckets).

Happy to see this merge once the (cosmetic) heading wording in item 1 is addressed — the rest are optional.

Fourth review pass:

- UI: rename the re-identify summary heading "Format Applied" ->
  "Requested Format" and the shared-type comment to match the "requested"
  framing used in the schema, type docs, and CHANGELOG (the echo reflects
  what the caller asked for, not what the SDK verified it rendered).
- Handler: validate entity names before the try, alongside the overlap
  guard, so an invalid entity type returns a distinct client-side
  validation error (no HTTP `code`) instead of surfacing through the
  generic catch looking like a Skyflow API error. Overlap still checked
  first (ordering test preserved).
- Test: assert the invalid-entity error carries no `code`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ

jstjoe commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 58820e3:

  • Jstjoe/2025 10 09 #1 (merge-gating): the re-identify summary heading is now "Requested Format" and the shared-type comment matches, consistent with the "requested" framing in the schema/type docs/CHANGELOG.
  • Add Claude Code GitHub Workflow #2: invalid entity names are now validated before the try (alongside the overlap guard), so they return a distinct client-side validation error with no HTTP code — distinguishable from a Skyflow API error. Overlap is still checked first (ordering test preserved); added an assertion locking in the no-code shape.

#3 (lenient output schema) and #4 (live-vault sanity check of the REST/default behavior) left as-is — #3 is the intended input-strict/output-lenient split, and #4 is the same pre-merge live-vault check already flagged above. Thanks for the thorough passes.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Review: Add optional format support to the re-identify tool

Overall this is a high-quality, well-scoped PR. The change is additive and backward-compatible, the handler logic is factored cleanly around a single FORMAT_BUCKETS normalization path, validation is layered sensibly, and test/doc coverage is thorough. The commit history shows the design was hardened across several review passes (overlap rejection, dedupe, echo normalization, clean client-side validation errors, "requested" vs "applied" wording). Nice work.

Strengths

  • Single source of truth for normalizationnormalizeFormat / buildReidentifyOptions / findFormatOverlaps all iterate the shared FORMAT_BUCKETS, so the SDK options and the echoed-back format can't drift. This directly addresses the "last-wins/undefined behavior" risk of an entity appearing in two buckets.
  • Validation ordering is deliberate and tested — the overlap guard runs before entity-name validation, and both run before any Skyflow call. Invalid entities surface as a client-side error with no HTTP code, keeping them distinguishable from a real Skyflow API error. should reject an overlap before validating entity names locks this in.
  • Backward compatibility is airtightoptions is undefined when no (non-empty) buckets are provided, and format is omitted from the output entirely rather than echoed as {}. Both are covered by tests.
  • Docs are appropriately honest — attributing "unlisted → plaintext" to the Detect API default (rather than a verified invariant the mocked tests can't exercise), and noting the masked form depends on vault config, avoids over-claiming.

Minor suggestions (non-blocking)

  1. The plaintext bucket is functionally redundant with the default. Since unlisted entities already restore to plaintext, listing an entity under plaintext is a no-op relative to omitting it. Harmless and it mirrors the SDK's setPlainTextEntities, but a one-line note in the tool description that plaintext exists mainly for explicitness would prevent callers from assuming it's required to get restoration.

  2. UI bucket ordering differs from the handler. renderFormatSummary orders Plaintext → Masked → Redacted, while FORMAT_BUCKETS is redacted → masked → plaintext. This looks like a deliberate least-→-most-restrictive UI choice and is purely cosmetic, but a brief comment would save a future reader from wondering whether it's a bug.

  3. No server-side wiring test for the new format arg. The handler is well covered, but nothing exercises server.ts threading format from the Zod input through to handleReIdentify. Consistent with the existing "test handlers as pure functions" pattern, so it's fine — just noting a schema/handler-signature mismatch on this path relies on the TypeScript build rather than a test to catch it.

Could not independently verify

  • node_modules isn't present in this environment, so I couldn't run the suite or type-check the reidentifyText(request, options) second-arg signature / the setRedactedEntities|setMaskedEntities|setPlainTextEntities methods against the real skyflow-node types. The PR reports 174 tests green and a clean pnpm build, which covers this — worth a reviewer with deps installed confirming.

Checklist against CLAUDE.md

The modifying-a-tool checklist looks complete: handler, inputSchema/outputSchema, handler call site, unit tests, UI types, UI rendering, and CLAUDE.md all updated. 👍

Looks good to merge once the minor notes above are considered.

🤖 Generated with Claude Code

Fifth review pass (clarity only, no behavior change):

- Note in the `plaintext` schema description that it is redundant with the
  default treatment for unlisted entities and exists only for explicitness,
  so callers don't assume they must list entities to get restoration.
- Comment renderFormatSummary's least-to-most-restrictive group ordering,
  which intentionally differs from the handler's FORMAT_BUCKETS order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ
@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review: Add optional format support to the re-identify tool

Thorough, well-executed change. The six-commit progression shows genuine iterative hardening (overlap rejection, single-normalization lockstep, doc-accuracy corrections, distinct client-side validation errors), and the result is clean and consistent across handler, schemas, types, UI, docs, and tests. It also follows the "Modifying Tools" checklist in CLAUDE.md end-to-end. A few notes below, mostly minor.

Strengths

  • Backward compatible. Omitting format calls the SDK with no options exactly as before; verified by a dedicated test asserting the second SDK arg is undefined.
  • Single normalization step (normalizeFormat) feeds both the SDK options and the echoed-back value, so they cannot drift — a real improvement over the earlier per-bucket duplication.
  • Cross-bucket overlap rejection is the right call: forwarding one entity to multiple SDK setters is ambiguous, and rejecting with a clear message beats silent last-wins. Good that the guard runs before entity-name validation and that ordering is pinned by a test.
  • Echo semantics were correctly reframed from "applied" to "requested (normalized)" across docs/schema/types — the response reflects what the caller asked for, not what the vault actually rendered. Nice attention to not over-claiming.
  • Schema/handler/UI/type parity is complete, and the format input reuses the shared ENTITY_KEYS enum so it stays in sync with de-identify.
  • Test coverage is strong: routing to each setter, single-bucket, empty/absent buckets, intra-bucket dedup, cross-bucket overlap, invalid entity (with code absent to distinguish from API errors), guard ordering, and echo-back.

Minor observations (non-blocking)

  1. Double validation of entity names. The server inputSchema already constrains each bucket with z.enum(ENTITY_KEYS), so findInvalidEntities in the handler is effectively unreachable via the MCP path — it only fires on direct handler calls (e.g. tests). That is defensible: the handler is a pure, independently-testable/reusable function and should not assume the caller pre-validated. Worth a one-line comment noting the handler intentionally re-validates, so a future reader does not simplify it away.

  2. cls injected unescaped into the class attribute in renderFormatSummary (class="badge ${cls}"). It is safe here because cls derives from getEntityClass over an enum-validated entity name, and this mirrors the existing highlight code in the same file — so no action needed, just flagging that the safety rests on that upstream validation rather than local escaping.

  3. UI group ordering vs. handler FORMAT_BUCKETS ordering intentionally differ (least-to-most restrictive for display vs. output order). Good that the final commit added a comment; two orderings living in separate files is the kind of thing that quietly diverges, so the comment earns its keep.

  4. Masked-form docs. Softening the *--6789 example to "depends on the vault masking configuration" is accurate and avoids implying a guaranteed shape.

Verification

Relied on the PR-stated results (174 tests passing, pnpm build clean) plus a read of the final state — the code type-checks by inspection (FORMAT_BUCKETS typing, format[bucket] indexing, and the conditional format spread all line up with ReIdentifyFormat), and the test mock lowercase DetectEntities proxy correctly round-trips through ENTITY_MAP / getEntityEnum. I was not able to run the suite in this sandbox to independently confirm.

Overall: ship it. The observations above are polish, not blockers.

Sixth review pass (comment only): note that handleReIdentify re-validates
entity names even though the server inputSchema already enum-constrains
them, because the handler is a standalone reusable function — so a future
reader doesn't remove the guard as "redundant."

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZZMCL2fjoR3FvvoM3vofZ
@github-actions

Copy link
Copy Markdown

Gitleaks Findings: No secrets detected. Safe to proceed!

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review: Add optional format support to the re-identify tool

Nice, focused change. It's well-structured, backward-compatible, and the documentation + test coverage are unusually thorough for a feature of this size. The CLAUDE.md "Modifying Tools" checklist looks fully honored (handler, inputSchema/outputSchema, call site, UI types, UI rendering, docs). Below is feedback grouped by priority.

🔴 Worth confirming before merge — the load-bearing semantic assumption

The PR repeatedly asserts that entity types not listed in any bucket "fall back to the Detect API's default and are restored as full plaintext." Everything downstream (docs, tool description, the "listing under plaintext is only for explicitness" note) depends on that being true.

Every test mocks the SDK, so none of them actually verify this default — they confirm routing to the right setters, not what Skyflow does with unlisted entities once a format/ReidentifyTextOptions object is present. This is security-relevant in both directions:

  • If the real API defaults unlisted entities to redacted when options are supplied, a caller passing plaintext: ["name"] would silently lose other data they expected restored.
  • Conversely, someone passing only redacted: [...] to hide a couple of types could have other PII come back in full plaintext unexpectedly.

Recommend confirming this against the Skyflow Detect API docs (or a one-off manual call against a real vault) and, ideally, leaving a comment/citation in reIdentify.ts pinning down the documented default. If it can't be guaranteed, the safer design would be to explicitly set every unlisted detected entity's treatment rather than relying on an implicit default.

🟡 Minor

  1. Validation ordering (reIdentify.ts:105-134): findFormatOverlaps runs before findInvalidEntities. An invalid entity name that happens to appear in two buckets is reported as "...appear in more than one [bucket]" rather than "invalid entity type" — confusing for a name that isn't valid to begin with. The should reject an overlap before validating entity names test locks this order in as intended, so it's a deliberate choice, but validating names first would give the clearer message. Not blocking.

  2. Redundant iteration (reIdentify.ts): normalizeFormat, findInvalidEntities, findFormatOverlaps, and buildReidentifyOptions each walk the buckets independently, and buildReidentifyOptions re-derives the emptiness check already computed as hasFormat. The format object is tiny so there's zero perf concern — just noting the logic could be consolidated (e.g. validate + normalize in one pass) if you touch this again.

  3. Test asserts mock-derived enum values (reIdentify.test.ts): the DetectEntities Proxy returns the lowercased prop name, so expect(mockSetMaskedEntities).toHaveBeenCalledWith(["ssn"]) really only asserts the mock echoes back what getEntityEnum returns. The correctness of the string→enum mapping itself isn't covered here (presumably it lives in the entityMaps tests) — fine, but the routing tests aren't proving the values forwarded to Skyflow are the real enum members.

🟢 Nits

  • outputSchema.format uses z.array(z.string()) while inputSchema uses z.enum(ENTITY_KEYS) — reasonable (output echoes already-validated data), just flagging the intentional asymmetry.
  • UI renderFormatSummary badge dots use var(--entity-color, …); only ~10 of the 59 entity types define --entity-color in styles.css, so most types render a gray dot. Graceful fallback, purely cosmetic.

✅ Things done well

  • Handler re-validates independently of the Zod schema, with a clear comment explaining why — good for the "standalone testable function" pattern this repo follows.
  • Overlap detection prevents genuinely ambiguous input (same type in two buckets) rather than silently picking a winner.
  • Normalize-once so the SDK options and the echoed-back format stay in lockstep — echo behavior (drop empty buckets, dedupe, omit when absent) is well covered by tests.
  • Backward compatibility is explicit and tested (no options arg, no format field when omitted).

Overall: LGTM pending confirmation of the unlisted-entity default semantics. 👍

Reviewed by Claude Code.

@jstjoe
jstjoe merged commit ed842a9 into main Jul 24, 2026
6 checks passed
jstjoe pushed a commit that referenced this pull request Jul 25, 2026
main advanced after this PR was opened: PR #28 (optional output-format
support for the re-identify text tool) merged. This merge brings the PR
branch up to date so it stays cleanly mergeable.

Conflicts resolved in CHANGELOG.md and README.md by unioning both features'
documentation (file de-identify/re-identify tools + re-identify format
control), keeping the re-identify format example in the re-identify section
and the file-tool sections after it. Source files (src/lib/tools/types.ts,
src/server.ts, ui/shared/types.ts) auto-merged with no conflicts.

Verified: full build green (tsc + 4 UI apps) and the complete unit suite
passes (270 tests) on the merged tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv6ppGHwgQFFZSd32FBiZA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants