Skip to content

[superlog] Fix insights agent annotation creation: resolve domain→UUID and add manage:config scope#472

Open
superlog-app[bot] wants to merge 1 commit into
stagingfrom
superlog/fix-insights-annotation-domain-uuid-scope
Open

[superlog] Fix insights agent annotation creation: resolve domain→UUID and add manage:config scope#472
superlog-app[bot] wants to merge 1 commit into
stagingfrom
superlog/fix-insights-annotation-domain-uuid-scope

Conversation

@superlog-app

@superlog-app superlog-app Bot commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Every scheduled insights-generate-website job that calls create_annotation fails with NOT_FOUND: website not found. The insight itself is generated and delivered correctly, but the chart timeline annotation is silently dropped.

The insights agent's prompt identifies websites by domain name (e.g., abeeducation.edu.au), so the LLM passes that domain as the websiteId parameter to create_annotation. The backend withWorkspace resolves websites by UUID only — db.query.websites.findFirst({ where: { id } }) — so a domain name never matches and throws NOT_FOUND.

A second latent issue would surface after fixing the first: the insights service auth had scopes: ["read:data"], but annotation create/update/delete trigger effectiveResource = "website" inside withWorkspace (because websiteId is in the options), and the scope override maps website+update → manage:websites — a scope the service auth doesn't hold. Annotations are a config-level operation, not website management.

Changes:

  • packages/ai/src/ai/tools/annotations.ts — add resolveWebsiteId(ctx, inputId) that maps a domain name back to the UUID from the app context (ctx.websiteDomain → ctx.websiteId, also checks accessibleWebsites by domain). Apply it in list_annotations and create_annotation.
  • packages/rpc/src/procedures/with-workspace.ts — add scopeResource?: string option; when set, it overrides the resource type used for API-key scope checks only (user role checks are unaffected), allowing annotation endpoints to require manage:config instead of manage:websites.
  • packages/rpc/src/routers/annotations.ts — pass scopeResource: "organization" to withWorkspace in create, update, and delete handlers (organization+update → manage:config).
  • apps/insights/src/generation.ts — extend insights service auth scopes to ["read:data", "manage:config"].

An alternative remediation would be to include the website UUID explicitly in the insights agent's system prompt (via formatContextForLLM) so the LLM always has the correct identifier — this would fix Bug 1 without code changes to the annotation tool, but Bug 2 (scope) still needs a fix.

Incident on Superlog


Was this PR helpful? Leave feedback — goes straight to the Superlog team.


Summary by cubic

Fixes failed insights annotations by resolving domain names to website UUIDs and using config-level scopes for annotation CRUD. Scheduled insights jobs now create timeline annotations successfully.

  • Bug Fixes
    • Resolve websiteId from domain via context in list_annotations and create_annotation (packages/ai/src/ai/tools/annotations.ts).
    • Add scopeResource?: string to withWorkspace to override API-key scope checks (packages/rpc/src/procedures/with-workspace.ts); routers pass scopeResource: "organization" for annotation create/update/delete (packages/rpc/src/routers/annotations.ts) so they require manage:config instead of manage:websites.
    • Extend insights service auth scopes to ["read:data", "manage:config"] (apps/insights/src/generation.ts).

Written for commit 670f607. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Jun 12, 2026 9:26am
databuddy-status Ready Ready Preview, Comment Jun 12, 2026 9:26am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
documentation Skipped Skipped Jun 12, 2026 9:26am

@unkey-deploy

unkey-deploy Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Unkey Deploy

Name Status Preview Inspect Updated (UTC)
api (preview) Ready Visit Preview Inspect Jun 12, 2026 9:25am

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two compounding failures that silently dropped every annotation created by the insights agent: the LLM passes domain names where the backend expects UUIDs, and the service auth lacked the manage:config scope that annotation write endpoints require. The domain resolution helper and the scopeResource override together close both gaps cleanly.

  • resolveWebsiteId maps LLM-supplied domain strings to UUIDs via the app context before any RPC call is made; applied to list_annotations and create_annotation where websiteId is caller-supplied.
  • scopeResource: \"organization\" on annotation create/update/delete routes the API-key scope check through organization+update/delete → manage:config instead of website+update → manage:websites; the insights service auth is updated to carry that scope.
  • The resolveWebsiteId branch 2 guard (&& ctx.websiteId) misses contexts where only defaultWebsiteId is populated, and all domain comparisons are case-sensitive — both worth addressing before this helper is reused in non-insights agents.

Confidence Score: 3/5

The scope and routing fixes are safe; the domain-resolution helper has a logic gap that could silently pass raw domain strings to the RPC layer in non-insights contexts.

The resolveWebsiteId branch 2 guard (&& ctx.websiteId) causes a silent passthrough when defaultWebsiteId is set but websiteId is not — a valid AppContext shape for non-insights chat agents. If the helper is called in such a context with a domain string and an empty accessibleWebsites list, the raw domain reaches withWorkspace, which throws NOT_FOUND exactly like the bug being fixed. The scope and router changes are straightforward and correct.

packages/ai/src/ai/tools/annotations.ts — the resolveWebsiteId helper has two issues: branch 2 should return ctx.websiteId ?? ctx.defaultWebsiteId rather than just ctx.websiteId, and all domain comparisons should be case-insensitive.

Important Files Changed

Filename Overview
packages/ai/src/ai/tools/annotations.ts Adds resolveWebsiteId helper to map domain names to UUIDs before RPC calls; applied correctly to list and create, but branch 2 silently misses contexts where defaultWebsiteId is set but websiteId is not, and domain comparisons are case-sensitive.
packages/rpc/src/procedures/with-workspace.ts Adds scopeResource option that correctly overrides only the API-key scope resolution path, leaving role-based user permission checks unaffected; typed as string which is intentionally permissive.
packages/rpc/src/routers/annotations.ts Adds scopeResource: "organization" to create, update, and delete handlers; scope mapping (organization+update/delete → manage:config) is correct per the scopes.ts table.
apps/insights/src/generation.ts Extends service auth scopes to include manage:config alongside read:data; minimal, correct change that unblocks annotation creation for the insights agent.

Sequence Diagram

sequenceDiagram
    participant LLM as Insights LLM
    participant Tool as create_annotation tool
    participant Resolve as resolveWebsiteId()
    participant RPC as annotations.create RPC
    participant WW as withWorkspace()
    participant Scope as requiredScopesForResource()
    participant DB as Database

    LLM->>Tool: "websiteId = "example.com" (domain)"
    Tool->>Resolve: resolveWebsiteId(ctx, "example.com")
    Resolve-->>Tool: "uuid-abc-123" (matched ctx.websiteDomain → ctx.websiteId)
    Tool->>RPC: "{ websiteId: "uuid-abc-123", ... }"
    RPC->>WW: "withWorkspace({ websiteId: "uuid-abc-123", permissions: ["update"], scopeResource: "organization" })"
    WW->>DB: getWebsiteById("uuid-abc-123")
    DB-->>WW: website row ✓
    WW->>Scope: requiredScopesForResource("organization", ["update"])
    Scope-->>WW: ["manage:config"]
    WW-->>WW: hasKeyScope(apiKey, "manage:config") ✓
    WW-->>RPC: workspace ✓
    RPC->>DB: INSERT annotation
    DB-->>RPC: new annotation row
    RPC-->>LLM: annotation created ✓
Loading

Reviews (1): Last reviewed commit: "[superlog] Fix insights agent annotation..." | Re-trigger Greptile

Comment on lines +102 to +104
// Input is the context domain → return the context UUID.
if (ctx.websiteDomain && inputId === ctx.websiteDomain && ctx.websiteId) {
return ctx.websiteId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Domain→UUID mapping silently misses defaultWebsiteId-only contexts

Branch 2 returns ctx.websiteId, but the codebase uses defaultWebsiteId ?? websiteId as the canonical primary identifier (see requireWebsiteId in context.ts). When ctx.websiteId is falsy but ctx.defaultWebsiteId holds the correct UUID — a valid AppContext for non-insights chat agents — the guard && ctx.websiteId causes a fallthrough to the accessibleWebsites scan. If that list is also empty (common in lightweight contexts), the raw domain string passes through to the RPC layer, reproducing the same NOT_FOUND error this PR was written to fix. The fix is one line: return ctx.websiteId ?? ctx.defaultWebsiteId instead of just ctx.websiteId.

Comment on lines +103 to +109
if (ctx.websiteDomain && inputId === ctx.websiteDomain && ctx.websiteId) {
return ctx.websiteId;
}
// Input matches an accessible website's domain → return its UUID.
const byDomain = (ctx.accessibleWebsites ?? []).find(
(w) => w.domain === inputId
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Domain comparisons use strict equality, making them case-sensitive. LLMs occasionally return domains with mixed capitalisation (e.g. AbcEducation.edu.au vs the stored abceducation.edu.au), which would cause both branch 2 and branch 3 to miss and silently fall through to returning the raw domain string.

Suggested change
if (ctx.websiteDomain && inputId === ctx.websiteDomain && ctx.websiteId) {
return ctx.websiteId;
}
// Input matches an accessible website's domain → return its UUID.
const byDomain = (ctx.accessibleWebsites ?? []).find(
(w) => w.domain === inputId
);
if (ctx.websiteDomain && inputId.toLowerCase() === ctx.websiteDomain.toLowerCase() && ctx.websiteId) {
return ctx.websiteId;
}
// Input matches an accessible website's domain → return its UUID.
const byDomain = (ctx.accessibleWebsites ?? []).find(
(w) => w.domain?.toLowerCase() === inputId.toLowerCase()
);

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.

0 participants