@W-23448423: add user-license-reclamation-apply prompt (HITL-guarded)#518
Conversation
Akash-Rastogi
left a comment
There was a problem hiding this comment.
[Akash's agent] Self-review — 1 finding.
HITL-guarded apply prompt that chains 3A inform output into update-user downgrades to Unlicensed. Follows the same two-phase preview→confirm pattern as stale-content and extract-optimization apply prompts. Defaults to dry run; no mutation without explicit human approval at the Step 4 gate. - src/prompts/userLicenseReclamation/apply.ts (prompt impl) - src/prompts/userLicenseReclamation/apply.test.ts (23 tests incl. HITL-refusal adversarial cases) - src/prompts/index.ts (registration) - docs/docs/prompts/user-license-reclamation-apply.md - docs/docs/intro.md (table row) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Self-review finding: Steps 2 (ts-events) and 3 (site-content) told the model to call query-admin-insights but didn't provide VDS query objects with exact field captions. The model would have to guess Tableau-specific field names, causing runtime failure. Now provides deterministic JSON query blocks (same pattern as job-optimization-inform) with exact field captions and filters: - Step 2: ts-events with Login/Login (Embedded) filter, LASTN date - Step 3: site-content with Workbook/Datasource item-type filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix TS Events field names (Actor User Name, Event Date — not Actor User ID / Event Created At) - Use Access events instead of Login (catches embedded/API usage) - Add all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer) to defaults - Cap rangeN at 90 days (TC standard lookback limit) - Add limit: 10000 to prevent unbounded result sets - Add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES env var support - Handle null-lastLogin users (provisioned but never signed in) - Add ETL lag and lookback cap caveats to Fixed notes - Bump version to 3.5.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b63b29d to
280f8bc
Compare
|
[Alon's agent] ## Review — Reviewed against the repo's published standards and the review-feedback this codebase tends to flag. Verdict: ship-with-fixes — no blockers, but 3 MAJOR items should land before merge (the Heads-up on the PR descriptionThe body says this is "blocked on 3C / MAJOR
MINOR
NIT
Verified clean
For the later e2e pass (targets)
🤖 Review assisted by Claude Code (tmcp-lead + tmcp-reviewer). Findings verified against code on |
Alon-ST-DATA
left a comment
There was a problem hiding this comment.
Inline findings for user-license-reclamation-apply (companion to the summary comment above). Verdict: ship-with-fixes — no blockers, 3 MAJOR / 4 minor / 2 nit. Read-only review; no live e2e yet.
Heads-up: the PR body's "blocked on 3C / update-user doesn't exist on main" looks stale — update-user is fully registered on main and its token confirm contract (${userId}:${siteRole}, single-use) matches this prompt's call shape. Testable now.
MAJOR fixes: - Replace invalid `Owner LUID` caption with `Owner Email` (join by email) - Fix Step 2 inactivity rule: "whose lastLogin from Step 1 is null" not "absent from Step 1" - Docs: list all 6 license-consuming roles including compound variants MINOR fixes: - Clamp arg-path inactiveDays to max 3650 (matches env-path behavior) - Docs: reword Step 2 from "last login date" to "activity signals" - Docs: add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
[Alon's agent] Round-2 review — head Verdict: ship-with-fixes. Dry-run default ( Round-1 findings are addressedRound-1 reviewed 2 net-new MAJORs (block
|
- Require lastLogin to be null or older than inactiveDays threshold before flagging a user as inactive (prevents false positives from ETL lag on recently-active users) - Warn admin when TS Events or Site Content queries hit the 10k row limit (silent truncation could cause false-positive downgrades) - Tighten inactiveDays regex to reject 3651-9999 at schema level (previously only clamped at runtime via Math.min) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Alon-ST-DATA
left a comment
There was a problem hiding this comment.
[Alon's agent] Approve — head 8c734cfa.
Re-reviewed the fix commit ("add lastLogin pre-filter, truncation warnings, and tighten regex"). All four round-2 findings addressed:
- ✅ MAJOR-1 (recent-login downgradable): inactivity now requires both
lastLoginnull-or-older-than-inactiveDaysAND no Access event; explicit "users whose lastLogin is within the last N days are NOT candidates, even with no Access event." The false-positive-from-ETL-lag path is closed. - ✅ MAJOR-2 (silent ts-events truncation): admin is now warned when the result hits the 10k-row limit. Note: the query is still site-wide
limit:10000rather than candidate-scoped, so the mitigation is visibility not elimination — a reasonable prompt-only tradeoff, but candidate-scoping (Actor User NameSET filter) would be a stronger follow-up if this ever runs on very large sites. - ✅ MINOR (Step-3 truncation): same 10k-row warning in the ownership report.
- ✅ MINOR (regex): tightened to exactly 1–3650 at the schema level.
Verification: 34/34 unit tests pass (was 8 — 26 regression tests added). Diff scoped to the expected files; version 3.5.0 is one minor over main (3.4.0) — clean.
Destructive update-user two-phase token flow was proven live earlier (preview→apply→restore; cross-role / no-token / single-use-replay all rejected) and is unchanged by this commit. Since these changes are prompt-text + regex only, no fresh live e2e was needed for regression.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tasource-luid) (#570) * @W-23202034: Prevent sign-out teardown from masking the real REST error (#470) useRestApi signs out ephemeral (pat / direct-trust) sessions in a `finally` block. Because a throw inside `finally` replaces whatever the `try` was returning or throwing, an un-caught sign-out failure clobbered the callback's real outcome: a callback that 404s on a missing resource surfaced to the caller as the sign-out's 401 once the session was torn down. That is the misleading "delete-workbook returns 401 for a non-existent workbook LUID" report. Sign-out is best-effort cleanup (the ephemeral session expires on its own), so isolate it in its own try/catch and swallow-and-log any failure at 'warning'. The callback's result or error now always reaches the caller intact. Tests: two unit cases in restApiInstance.test.ts inject a rejecting signOut and assert the callback's error (and a successful result) survive teardown. Both fail without the fix. e2e is N/A — the defect only manifests when signOut itself fails at teardown, which a live run cannot force deterministically. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds (#473) * @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds get-stale-content-report accepts a projectIds list to scope the report. Two resolution paths silently dropped requested IDs: 1. resolveProjectScopeIds dropped IDs outside the server's bounded context (INCLUDE_PROJECT_IDS) with no signal. 2. resolveProjectIdsToNames intersected requested IDs against real site projects; IDs matching no project vanished. Worst case: when EVERY requested ID was invalid, the scope collapsed to empty and buildSiteContentQuery treated it identically to "no scope" — returning the ENTIRE site instead of nothing. A typo in a project scope silently widened the report to all content. Fix: - Track dropped IDs through both resolution paths (unknown-on-site vs not-permitted-by-config) and surface them in mcp.warnings, reusing the result convention established by query-datasource. - Widening guard: when a scope was requested but nothing resolved to a real project, return an empty report (0 rows) plus warnings — never fall through to the unscoped full-site query. - Back-compat: no mcp key is emitted when all requested IDs are valid. Test coverage: partial-invalid (scoped to valid subset + warning), all-invalid (empty report, asserts no VDS query fires), bounded-context drop, all-valid back-compat, plus direct unit tests for the new resolver return shapes and warning assembly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23202054: Branch warning wording on whether a valid scope remains; drop env-var name from output Addresses PR review (Matt): 1. The ignored-projectIds warning message was unconditional — "The report was scoped to the remaining valid projects." fired even in the all-invalid case, asserting a valid subset that does not exist. That false premise is exactly the silent-widening confusion this fix targets. The message now branches on whether any requested project survived: when none do, it states an empty report (0 rows) was returned instead of the full site, giving an LLM caller a self-correction signal. 2. The not-permitted-by-config message no longer names the INCLUDE_PROJECT_IDS env var — "outside this server's configured project scope" carries the same signal for a caller without coupling tool output to a config key. (The docs page still names the env var; that prose is admin-facing config reference.) Tests: added buildProjectIdWarnings cases for both message branches and the env-var-not-leaked assertion; strengthened the all-invalid callback test to assert the empty-report wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-22030892: Add friendly client-name mapping for OAuth client_id values in UIP telemetry (#479) * @W-22030892: Add friendly client-name mapping for OAuth client_id in tool telemetry Bearer clients now emit two additive tool_call properties: oauth_client_id (raw CIMD-URL client_id, canonical) and oauth_client_display_name (host-keyed static map -> Claude/Cursor/VS Code; unknown clients fall back to the raw value; absent auth emits '' matching existing optional-prop style). Telemetry/display only — no auth or request-handling changes; no network CIMD fetches on the hot path. Known-host map is best-effort (repo has no observed production client_ids); fail-safe by construction — wrong/missing entries surface raw, never corrupt. Single edit point: knownClientDisplayNamesByHost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * @W-22030892: bump version to 2.25.3 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * @W-22030892: cover embedded-OAuth client ids + bound telemetry values Self-review round fixes: - embedded-OAuth sessions (tableauAuthInfo normalized to X-Tableau-Auth) carry the client id on extra.authInfo.clientId — telemetry now derives Bearer clientId ?? authInfo.clientId (RED-verified: the path emitted blanks before). - client_id is an attacker-influenceable token claim: new sanitizeClientIdForTelemetry bounds it (URL -> origin+pathname, drops search/hash/userinfo; 200-char cap) on both emitted fields, following the accessTokenValidator claim-truncation precedent (divergence from its 256+marker documented in the module header). 150 files / 2,246 tests green, tsc + eslint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook (#416) * @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook Formalizes the hotfix release path so a livesite fix can ship off an already-deployed version without dragging main drift or moving the latest dist-tag. Andy's spec; continuity-critical (release-process holder departs 07-02). - publish.yml: prerelease-aware npm publish. A version with a SemVer prerelease identifier (a '-', e.g. 2.18.0-hotfix.1) publishes under a 'hotfix-v<version>' dist-tag, never latest; stable path unchanged (bare npm publish). Fails CLOSED if the version can't be read (refuses to risk moving latest). dist-tag is NOT the bare version — npm rejects a dist-tag that parses as valid SemVer. - docker-publish.yml: Docker 'latest' skips prerelease tags (mirror guard), so a hotfix gets its exact-version Docker tag but doesn't steal latest. - docs/RELEASE.md (new): normal + hotfix runbook (branch off tag, fix-on-main- then-cherry-pick, bump to -hotfix.N, release from hotfix branch, pin exact version in HF) + rationale; linked from CONTRIBUTING. Pre-push dual self-review (GPT-5.5 + Opus) caught 3 blockers on local code before this commit: npm rejecting a valid-SemVer dist-tag (fatal), Docker latest moving on hotfixes, and fail-open on empty version. All fixed here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * @W-23149256 preflight: release tag must equal v<package.json version> + runbook clarifications Review follow-up (self-review + Jaehun's walkthrough ask): npm decides prerelease-vs-stable from package.json while Docker decides from the release tag name — a tag/version mismatch could move Docker 'latest' while npm holds (or vice versa). Both workflows now fail closed on mismatch before publishing; the tag name reaches the scripts via env, not inline interpolation. Runbook: Release-publish (not tag push) is the trigger, mark hotfix releases pre-release, and rerun-after-partial-failure semantics (npm immutability). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * @W-23375797: consolidate admin-gated tools into polymorphic dispatchers (#475) * @W-23375797: consolidate admin-gated tools into polymorphic dispatchers Two new polymorphic tools reduce the model-visible admin tool surface: - `query-admin-insights` (kind discriminator) merges 4 insight readers: get-stale-workbooks, get-stale-datasources, get-content-usage-metrics, get-user-activity. - `delete-content` (resourceType discriminator) merges 3 preview→confirm destructive tools: delete-workbook, delete-datasource, delete-extract-refresh-task. Behavior preserved end-to-end. `delete-content` records AppApprovalEvidence under legacy namespaces so the mcp-apps HITL panel flow (flag-ON) continues to dispatch confirm-delete-workbook / confirm-delete-datasource / confirm-delete-extract-refresh-task via the existing iframe machinery. The flag-OFF preview→confirm tag/nonce evidence gates work unchanged. Auth: scope-based auth in scopes.ts adds delete-content and query-admin-insights, each carrying the union of its component tools' MCP + REST scopes. Both are dropped from `getEnabledToolNames()` when `ADMIN_TOOLS_ENABLED=false`. Legacy tools remain wired during the shim window. Bumps package version 2.25.0 → 2.26.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix self-review findings on consolidated tools - delete-content: replace three per-resource MCP scopes with the umbrella tableau:mcp:content:delete so authMiddleware's AND-enforcement doesn't 403 callers who only granted one legacy scope. Callers who need per-resource granularity keep using the legacy delete-{workbook, datasource,extract-refresh-task} tools during the shim window. - query-admin-insights: move the missing-query check inside logAndExecute so failed invocations are logged and product-telemetry emitted instead of being silently dropped by the pre-callback return. - query-admin-insights: honor per-kind MAX_RESULT_LIMITS entries for the four legacy admin-insights tool names by taking the tightest of consolidated cap, legacy cap, and caller-provided limit — prevents operators' existing cap config from silently disappearing on migration. * @W-23375797: adapt consolidated stale-content to new resolver shapes PR #473 changed getStaleContentReport's helpers to return richer objects: - _resolveProjectScopeIds now returns {scopeIds, boundedOutOfScopeIds} - _resolveProjectIdsToNames now returns {names, unknownIds} - new _buildProjectIdWarnings helper + _StaleReportWarning type Mirror the legacy get-stale-content-report tool in queryAdminInsights so projectId warnings and the all-invalid widening guard behave the same via kind=stale-content. Re-export the two helpers + type from getStaleContentReport so both tools share one implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix e2e tool-list test for gated consolidated tools The consolidated query-admin-insights and delete-content tools are disabled unless ADMIN_TOOLS_ENABLED=true, so they don't appear in the server's tool list in the default e2e run. Add them to the adminOnlyTools filter in the default and combined variant "should list tools" tests, matching how the legacy per-kind admin tools are already filtered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: lock in tightest-cap via legacy MAX_RESULT_LIMITS Add a queryAdminInsights.test.ts case that sets MAX_RESULT_LIMITS=query-admin-insights-ts-events:5, invokes the consolidated tool with kind=ts-events and limit=100, and asserts the underlying VDS query is issued with rowLimit=5 — so the legacy per-kind cap still wins after callers migrate to query-admin-insights. Guards against the silent-migration hazard flagged in the self-review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix oauth-embedded scopes_supported count for umbrella scope The new tableau:mcp:content:delete umbrella scope (added by the self-review fix on delete-content) bumps scopes_supported from 14 to 15. Update the three well-known-endpoint tests to expect the new scope by name and the new length. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: address review — trim descriptions, type logAndExecute, document flag-ON path - Trim query-admin-insights description from ~546 to ~120 tokens; remove inline JSON schema + per-kind param tables (Major, Alon). - Trim delete-content description from ~437 to ~100 tokens; remove per-resourceType parameter reference (Major, Alon). - Document that flag-ON preview path writes both TagEvidence AND AppApprovalEvidence in all three resource branches (Minor, Alon). - Add explicit <QueryOutput> type param to raw-VDS logAndExecute for symmetry with <StaleContentResult> branch (Minor, Alon). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: export QueryOutput type from adminInsightsToolBase The previous commit imported QueryOutput in queryAdminInsights.ts from adminInsightsToolBase.js, but that module only used it locally without re-exporting. Add `export type { QueryOutput }` so the consolidated tool's explicit type param compiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add documentation pages for query-admin-insights and delete-content - docs/docs/tools/admin-insights/query-admin-insights.md — consolidated tool - docs/docs/tools/content/delete-content.md — consolidated tool - docs/docs/tools/content/_category_.json — sidebar category - docs/docs/intro.md — 2 new table rows Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23415681: make FeatureGateProvider.isFeatureEnabled async (#497) * @W-23415681: make FeatureGateProvider.isFeatureEnabled async * @W-23415681: await async isFeatureEnabled in get-embed-token authz test * @W-23415681: prune redundant feature-gate init tests * @W-23375797: remove deprecated admin tool shims (Phase 2) (#498) * @W-23149256: fix publish.yml startup failure from empty ${{ }} in run: block (#506) The prerelease-aware comment block added in #416 included an empty ${{ }} expression inside a run: | script. GitHub Actions evaluates ${{ }} in run: text before the shell sees it, so the shell-comment # does not protect it; the empty expression is invalid and the workflow fails to load (startup failure, no publish job ever runs). Reword the comment to drop the token. actionlint clean; publish logic unchanged. * @W-12345678 Docs update 7-14 (#509) * 7/14 docs * version bump * @W-22350774: Add Tableau Prep flows tools (list-flows, get-flow) (#344) @W-22350774 Add Tableau Prep flows tools (list-flows, get-flow) Add two read-only MCP tools that wrap the public Tableau REST Flows API: - list-flows: lists Prep flows on a site with field:operator:value filtering (createdAt, name, ownerName, projectId, projectName, updatedAt) and sorting. Every successful response includes an always-present mcp.resultInfo { returnedCount, truncated, truncationReason?, totalAvailable? } so a caller can distinguish a complete list from one cut short by the caller's limit or the admin MAX_RESULT_LIMIT cap. totalAvailable is surfaced only when no server-side bounded context (PROJECT_IDS/TAGS) is configured, since that count is taken before the tool's allow-list filtering. - get-flow: retrieves a single flow with optional connections and run history. Sidecar OAuth scopes are requested only when the corresponding sidecar is included, and access is gated by the bounded-context resource checker (isFlowAllowed) so PROJECT_IDS/TAGS restrictions apply. Run history is bounded by flowRunLimit and emits a FLOW_RUNS_TRUNCATED warning when more runs exist. Supporting changes: - Gate all flow tools behind FLOW_TOOLS_ENABLED, disabled by default; set it to "true" to register list-flows and get-flow (mirrors ADMIN_TOOLS_ENABLED). Individual tools can still be excluded via EXCLUDE_TOOLS. - Add the flow REST API definitions, Zod types, and methods, plus the dedicated tableau:flows:read, tableau:flow_connections:read, and tableau:flow_runs:read scopes. - Harden parseAndValidateFilterString bracket / in: list handling (repo-wide fix). - Make project.description optional (Tableau returns it on embedded <project> elements). - Tools, types, unit tests, a description-quality eval test, and docs for both tools. * @W-23375797: fix unawaited async feature gate in confirmDeleteContent (#513) * fix: await async isFeatureEnabled in confirmDeleteContent disabled field PR #497 made isFeatureEnabled async but this call site was not updated, causing `!Promise` to always evaluate to `false` — the tool was never disabled when mcp-apps was OFF. Wrap in Provider(async () => ...) to match confirmUpdateCloudExtractRefreshTask. Update test mocks from mockReturnValue to mockResolvedValue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.1.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove flow:read from OAuth scope assertions Flow tools are gated behind FLOW_TOOLS_ENABLED (off by default), so tableau:mcp:flow:read is never advertised in scopes_supported. The assertions were incorrect as shipped by the flows PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23388458 - Add insight-cards MCP tool (deterministic insights from datasource context) (#502) * @W-23388458: Add generate-chiron-insight-cards tool + datasource LUID resolver Add a deterministic Chiron insight-cards path to tableau-mcp: - resolve-datasource-luid: exact case-sensitive contentUrl -> LUID resolution (name is not unique; REST contentUrl filter is case-insensitive). - generate-chiron-insight-cards: resolves a datasource, reads metadata to pick measures/time field/dimensions, runs period-over-period detail bundles via a shared runChironBundle helper, and shapes governed InsightCard JSON (headline, delta, direction, PoPC comparison, series, breakdown). - Register the tool + scopes; expose contentUrl on datasource types. Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23388458: Expand Chiron insight cards to full detail payload + drill-down Extend generate-chiron-insight-cards from a headline card into the full governed insight-detail payload and support recursive drill-down: - InsightCard now carries context, contributors (with % of lift), pronounced (most/least), unusual (anomaly), followUps, actions, and breakdownDimension; extractors read these from the Pulse detail bundle. - Add drill params: breakdownDimension (scopes the breakdown to one dimension) and filters (member scoping). Response returns the full dimension list so drilled cards still propose fresh follow-ups, while only the active dimension scopes allowed_dimensions. - Member-filter contract confirmed live against Pulse: OPERATOR_EQUAL with categorical_values [{ string_value }] only, in metric_specification (adding '='/bool_value/null_value or using basic_specification 400s). - Unit tests cover the detail payload, drill params, and the filter shape. Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23388458: Rename tool to generate-insight-cards; drop Chiron/Pulse from public name + description; fix tsc types; bump version * @W-23388458: Address PR review - bounded-context guards, surface bundle failures, validate drill args, fix nondeterministic/aliased fields * @W-23388458: De-Chiron internal names -> insight* (folder/files/functions/types); no behavior change * @W-23388458: Address review - indistinguishable not-found/not-allowed + gate before identity lookup; opt-in (disabled-by-default) registration; validate measures/timeField; deterministic series sort * @W-23388458: Gate insight tools behind INSIGHTS_TOOLS_ENABLED (off by default) + deterministic field selection --------- Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit (#529) * @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit Two defects in the consolidated delete-content flow: 1. Preview minted a confirmationToken for a non-existent extract-refresh-task. The task branch's resolveTarget was a pure echo with no REST read, so a well-formed but non-existent taskId passed the UUID check and got a live nonce. Unlike the workbook/datasource branches (which 404 early via getWorkbook/queryDatasource), the task branch never read the target and there is no single-get task endpoint. Fix: list-and-find existence check before minting the nonce (preview + confirm), returning a 404 not-found. Requires tableau:tasks:read, added to the delete-content scope map. 2. Confirm phase double-logged audit. guardMutation emitted an unconditional 'allowed' record plus the terminal completed/failed via recordOutcome, so every confirmed mutation logged twice (all resourceTypes / all preview-> confirm tools). Fix: suppress 'allowed' on the confirm phase so a confirm emits exactly one terminal record; preview still emits its single 'allowed'. Audit lifecycle doc comments updated to the one-record contract. Tests: 6 new pinning tests (nonexistent/empty-list task -> 404 + no token + no delete; confirm -> exactly one completed/failed; preview -> single allowed). Docs: delete-content.md documents the existence check and corrects the REST-API reference (List, not the non-existent Get, Extract Refresh Tasks). Version: 3.1.2 -> 3.1.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23377934: fix e2e list-tools to filter INSIGHTS_TOOLS_ENABLED-gated tools The e2e "should list tools" assertions (default + combined variants) built expectedToolNames from webToolNames but never filtered the insights tools (generate-insight-cards, resolve-datasource-luid) added in #502, which are gated behind INSIGHTS_TOOLS_ENABLED (off by default). The built server omits them, so the toHaveLength check failed (received 20/26 vs expected 22/28). This surfaced on this PR because it runs from the main repo (not a fork), so the fork-guarded E2E CI step actually executes. Added the insights filter to both variants, mirroring the existing FLOW_TOOLS_ENABLED gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23125362: complete mutation-tool safety layer — audit target enrichment + centralized HITL text + durability contract (#533) Scope-completes the common mutation-tool safety layer (guard + evidence + audit already merged). Finishes the in-scope remainder; auth-session tools (revoke/reset), a durable external sink, and e2e tiers are forked to a follow-up story. - AC-3 (Gap B): enrich extract-refresh-task audit target with name/project/ owner, derived from the underlying datasource (else workbook). New shared helper resolveExtractRefreshTaskTarget.ts; best-effort, degrades to id-only on Server sites or 403/404 and never blocks/fails the mutation. Applied to all four task paths (delete, confirm-delete, update, confirm-update). The delete branch reuses its existence-check task list (no extra round-trip). - AC-4 (Gap D): centralize tool-side HITL text into new hitlText.ts (NEXT-STEP preview, confirm-closed, PreviewNotRun recovery). Byte-for-byte behavior-preserving; rethreaded delete-content, update-cloud, and the guard's PreviewNotRun branch. Kept in tools/_lib to avoid a tools→prompts cross-layer import. - AC-3 (Gap C, doc): document the durability contract — audit records emit as structured JSON on the dedicated `audit` logger (bypasses LOG_LEVEL); durable retention is the deployment's responsibility (ship the stream to an immutable store). Added to auditRecord.ts and delete-content.md. - AC-6 (Gap E, unit): per-tool assertions that enriched target fields appear in audit records on allow and deny; forged-confirm reject and preview→confirm happy paths. Version 3.1.3 → 3.1.4. Full suite green (150 files / 2306 tests); tsc, lint, build clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23448424: add update-user tool — two-phase HITL site role update (#516) * feat: add update-user tool — two-phase HITL site role update - New `update-user` MCP tool with preview/confirm flow using RegistryEvidence - OAuth scope: `tableau:mcp:users:write` → `tableau:users:update` + `tableau:users:read` - Admin-gated, UUID-validated userId, enum-constrained siteRole - Primary use case: downgrade inactive users to Unlicensed for license reclamation @W-23448424 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: relax update-user response schema to accept partial user The Tableau PUT /users/:userId endpoint only echoes back changed fields (e.g. siteRole, name, email) without `id`. Using `userSchema.partial()` prevents Zodios response validation failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.2.0 for update-user feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings — idempotentHint, test coverage, scope phrasing - Set idempotentHint: false (nonce is single-use; repeated calls reject) - Add preview safety test: mockUpdateUser.not.toHaveBeenCalled() - Add partial-response fallback test (API omits siteRole) - Add missing siteRole/email fallback text test - Add RegistryEvidence binding-isolation test (role-swap protection) - Add e2e behavior test (preview, schema validation, bogus token rejection) - Fix scope phrasing: list both tableau:users:update and :read - Add comment explaining SupportUser/ServerAdministrator omission Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: gate update-user e2e tests behind UPDATE_USER_E2E_ID Unlike the sibling tool, update-user's resolveTarget calls queryUserOnSite which 404s for non-existent user IDs. Gate preview and bogus-token tests behind the env var so they skip gracefully when no real user LUID is configured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: split mutating e2e test behind UPDATE_USER_E2E_DESTRUCTIVE_ID Mirror sibling pattern (updateCloudExtractRefreshTask) — the preview and bogus-token tests use UPDATE_USER_E2E_ID, while the actual mutation uses a separate UPDATE_USER_E2E_DESTRUCTIVE_ID to prevent accidentally downgrading a real user. All guidance consistently says "disposable user". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23336786 ci: bump GitHub Actions to latest major versions (#507) ci: bump GitHub Actions to latest major versions Updates actions/checkout (v3/v4→v7), actions/setup-node (v4→v7), actions/upload-artifact (v4→v7), actions/upload-pages-artifact (v3→v5), actions/deploy-pages (v4→v5), actions/github-script (v8→v9), docker/setup-buildx-action (v3→v4), docker/login-action (v3→v4), docker/metadata-action (v5→v6), and docker/build-push-action (v5→v7) across all workflows. * @W-23336786 Add action to automatically label "feature" PRs (#451) * Ignore .DS_Store * Add .github/workflows/pr-feature-label.yml, which fires when a PR is opened against a base branch starting with "feature/" and labels it with the branch remainder prefixed "feature:" (e.g. base feature/rounded-corners -> label feature:rounded-corners). Uses actions/github-script to get-or-create the label (auto-created gray if missing) then apply it. The on.pull_request.branches: ['feature/**'] filter scopes the trigger to feature base branches; the job carries issues:write + pull-requests:write and the standard fork/dependabot guard. * Adjust based on source or destination branchs as "feature" * Operate consistently on PR actions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: pin system time in integration tests to eliminate date-dependent flakiness * Revert "fix: pin system time in integration tests to eliminate date-dependent flakiness" This reverts commit 4cb7d5f. Still want to fix this, but in a separate PR. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * @W-23448422: add user-license-reclamation-inform prompt (#517) * @W-23448422: add user-license-reclamation-inform prompt - New MCP prompt that identifies inactive licensed users as reclamation candidates by paginating list-users (role + lastLogin filters) and cross-referencing activity via query-admin-insights (ts-events) - Config knobs: LICENSE_RECLAIM_INACTIVE_DAYS, LICENSE_RECLAIM_ROLES - 13 unit tests covering defaults, arg overrides, env var wiring - Docs page + intro.md entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.3.0 New prompt feature warrants a minor version bump. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove broken markdown link to unreleased apply prompt doc The Docusaurus docs build fails when a markdown link targets a file that doesn't exist yet. Replace with inline code reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — input validation, lookback cap, docs - Add regex validation on `roles` arg to prevent prompt injection - Cap TS Events `rangeN` at 90-day lookback max (avoids false positives when inactiveDays > 90) - Add `limit: 10000` to ts-events query to bound result set - Align inactiveDays regex to 1–3650 (matches env var clamp) - Document MAX_RESULT_LIMIT caveat in docs workflow description - Add LICENSE_RECLAIM_INACTIVE_DAYS and LICENSE_RECLAIM_ROLES to env-vars.md configuration docs - Add source reference comment for TS Events field captions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct TS Events field captions and address live-verified findings BLOCKER fixes (live-verified against prod TS Events datasource): - `Created At` → `Event Date` (correct DATETIME field on TS Events) - `Actor User Id` → `Actor User Name` (STRING matching username/email, joinable with list-users; Actor User Id is an internal integer) MEDIUM fixes: - Add second-pass instruction for null-lastLogin users (never signed in — strongest reclamation candidates, previously dropped silently) - Add ETL lag warning (TS Events lags 24–48h behind live state, making the safety net leaky — candidates must be human-verified) - Strengthen "INFORM-only" framing in recommendation note Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23093467 @W-22551424: enforce stale-content row cap server-side + harden Admin Insights LUID cache (#553) Part A (W-23093467): move the stale-content-cleanup safety cap from prompt-text only to server-side enforcement. Add configurable STALE_CONTENT_MAX_ROWS (default 100, range 1-10000) and enforce it in the stale-content branch of query-admin-insights: above the cap the tool returns a structured success with rows:[], the TRUE totalStaleItems/totalStaleSizeBytes, and a ROW_CAP_EXCEEDED warning guiding the caller to narrow scope. Read-only inform prompt keeps working (no throw); the destructive apply flow can no longer receive a huge actionable set even if the model ignores the prompt guard (which remains as defense-in-depth). Part B (W-22551424): light hardening of the existing Admin Insights LUID resolver cache. Add an optional maxSize to ExpiringMap (default unbounded; other callers untouched) with oldest-inserted eviction that clears the pending timeout, wire a 256-entry bound into the resolver cache, and add hit/miss/resolve debug telemetry (no siteId/PII). Cache invalidation remains TTL + siteId-keying; an explicit refresh hook is deferred. Tests: below/at/above-cap + combined-warning cases, config precedence/validation, ExpiringMap eviction, resolver eviction. Docs: env-vars, request-overrides, site-settings, query-admin-insights tool page. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label) (#455) * @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label) * Improve case insensitive title matching Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Avoid race condition around label creation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Improve least-privilege for write permissions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Exclude dependabot PRs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * @W-23448423: add user-license-reclamation-apply prompt (HITL-guarded) (#518) * @W-23448423: add user-license-reclamation-apply prompt HITL-guarded apply prompt that chains 3A inform output into update-user downgrades to Unlicensed. Follows the same two-phase preview→confirm pattern as stale-content and extract-optimization apply prompts. Defaults to dry run; no mutation without explicit human approval at the Step 4 gate. - src/prompts/userLicenseReclamation/apply.ts (prompt impl) - src/prompts/userLicenseReclamation/apply.test.ts (23 tests incl. HITL-refusal adversarial cases) - src/prompts/index.ts (registration) - docs/docs/prompts/user-license-reclamation-apply.md - docs/docs/intro.md (table row) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: add deterministic VDS query templates for Steps 2-3 Self-review finding: Steps 2 (ts-events) and 3 (site-content) told the model to call query-admin-insights but didn't provide VDS query objects with exact field captions. The model would have to guess Tableau-specific field names, causing runtime failure. Now provides deterministic JSON query blocks (same pattern as job-optimization-inform) with exact field captions and filters: - Step 2: ts-events with Login/Login (Embedded) filter, LASTN date - Step 3: site-content with Workbook/Datasource item-type filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: align apply prompt with validated inform findings - Fix TS Events field names (Actor User Name, Event Date — not Actor User ID / Event Created At) - Use Access events instead of Login (catches embedded/API usage) - Add all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer) to defaults - Cap rangeN at 90 days (TC standard lookback limit) - Add limit: 10000 to prevent unbounded result sets - Add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES env var support - Handle null-lastLogin users (provisioned but never signed in) - Add ETL lag and lookback cap caveats to Fixed notes - Bump version to 3.5.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: address review findings — Owner LUID, inactivity rule, docs MAJOR fixes: - Replace invalid `Owner LUID` caption with `Owner Email` (join by email) - Fix Step 2 inactivity rule: "whose lastLogin from Step 1 is null" not "absent from Step 1" - Docs: list all 6 license-consuming roles including compound variants MINOR fixes: - Clamp arg-path inactiveDays to max 3650 (matches env-path behavior) - Docs: reword Step 2 from "last login date" to "activity signals" - Docs: add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prettier formatting for SITE_CONTENT_FIELDS array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add lastLogin pre-filter, truncation warnings, and tighten regex - Require lastLogin to be null or older than inactiveDays threshold before flagging a user as inactive (prevents false positives from ETL lag on recently-active users) - Warn admin when TS Events or Site Content queries hit the 10k row limit (silent truncation could cause false-positive downgrades) - Tighten inactiveDays regex to reject 3651-9999 at schema level (previously only clamped at runtime via Math.min) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prettier formatting for regex line break Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23518533: Group embed-viz and HITL MCP-App code into feature folders (#556) * refactor: group embed-viz and HITL MCP-App code into feature folders Split the flat src/web/apps/src/lib/ directory into feature-scoped folders so each browser-side MCP-App bundle is self-contained: - embed/ — Tableau viz embedding for get-view/get-workbook (mcp-app entry, handleToolResult, embedTableauViz, loadTableauEmbeddingApi, getEmbedTokenToolClient, openInTableauLink) + mcp-app.html - hitl/ — in-iframe confirm panel for delete/update preview tools (hitl-confirm entry, handleConfirmResult, *ConfirmClient renderers) + hitl-confirm.html - shared/ — mcp-app.css, showError, vite-env.d.ts, assets/, and a new vizContainer.ts holding TABLEAU_VIZ_CONTAINER_ID (extracted to keep shared/ free of a shared->embed dependency) HTML templates move next to their entry .ts with relative <script src>. Each Vite build's root is set to its feature folder in build.ts so single-file output still lands flat as dist/mcp-app.html and dist/hitl-confirm.html; appConfig.ts and server.web.ts are unchanged. All moves use git mv to preserve history. 2365 unit tests pass, lint clean, build produces flat dist. * chore: bump version to 3.4.1 * bump * remove unused export --------- Co-authored-by: Alon Siman Tov <asimantov@salesforce.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Akash Rastogi <akashrastogi12@gmail.com> Co-authored-by: jarhun88 <34752438+jarhun88@users.noreply.github.com> Co-authored-by: Joey Constantino <15789096+joeconstantino@users.noreply.github.com> Co-authored-by: Jialin Mao <30403691+showmethecold@users.noreply.github.com> Co-authored-by: Tony <3278731+tonythebest@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Brian Cantoni <bcantoni@salesforce.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Akash Rastogi <akash.rastogi@salesforce.com>
[Akash's agent]
Summary
user-license-reclamation-applyMCP prompt — a HITL-guarded destructive workflow that identifies inactive licensed users, surfaces their owned-content counts, and (only after explicit human approval) downgrades approved users to Unlicensed via theupdate-usertool.stale-content-cleanup-applyandextract-optimization-apply. Defaults to dry run.Key design decisions
Actor User Name,Event Date), usesAccessevents (catches embedded/API usage), includes all compound roles (SiteAdministratorCreator,SiteAdministratorExplorer), caps lookback at 90 days, and setslimit: 10000on queries.gateKind: 'token'— theupdate-usertool returns a per-userconfirmationTokenfrom the preview call, verified on confirm.LICENSE_RECLAIM_INACTIVE_DAYSandLICENSE_RECLAIM_ROLESfromprocess.env(matching inform prompt pattern).userIdsandsiteRolesargs are regex-constrained to safe character classes. The HITL gate block is rendered from hardcoded strings, never user-controlled values.Dependencies
Files changed
src/prompts/userLicenseReclamation/apply.tssrc/prompts/userLicenseReclamation/apply.test.tssrc/prompts/index.tsdocs/docs/prompts/user-license-reclamation-apply.mddocs/docs/intro.mdpackage.json/package-lock.jsonTest plan
npm run build— passesnpx tsc --noEmit— passesnpm run lint— passesnpm test— 2369 tests pass (153 files)🤖 Generated with Claude Code