Skip to content

Add creative context and framework workflow updates#2190

Merged
steve8708 merged 49 commits into
mainfrom
changes-489
Jul 16, 2026
Merged

Add creative context and framework workflow updates#2190
steve8708 merged 49 commits into
mainfrom
changes-489

Conversation

@steve8708

Copy link
Copy Markdown
Contributor

Summary

  • add the public @agent-native/creative-context package and integrate it with the relevant templates
  • update shared core chat, agent, ingestion, provider, workspace-connection, and extension surfaces
  • harden Clips, Analytics, Design, Plan, Slides, Assets, and Content workflows with focused tests and changelog entries
  • update publish, desktop-release, package metadata, lockfile, skills, and marketplace sync surfaces

Validation

  • corepack pnpm run guards
  • corepack pnpm --filter @agent-native/core build
  • corepack pnpm --filter @agent-native/creative-context build
  • focused dispatch, docs, and Slides tests pass serially
  • full prep typecheck passed; repo-wide test concurrency exposed dispatch/docs timeout and environment-only errors, which passed in focused reruns

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

# Conflicts:
#	templates/content/actions/create-document.test.ts
#	templates/content/actions/create-document.ts
#	templates/content/actions/edit-document.ts
#	templates/content/actions/update-document.ts
@steve8708 steve8708 closed this Jul 16, 2026
@steve8708 steve8708 reopened this Jul 16, 2026
builder-io-integration[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor Author

One additional packaging concern not covered by the existing threads:

packages/creative-context/package.json declares @agent-native/core as >=0.8.0, but the published package imports the new @agent-native/core/ingestion and @agent-native/core/search-utils subpaths introduced by this PR. A consumer on an older core release can therefore satisfy npm's peer range and still fail to build/load because those exports do not exist.

Please set the peer lower bound to the first core release that ships these subpaths (or otherwise enforce the compatible core pairing), and add a package-install/compatibility check so the published metadata cannot drift from the runtime requirement.

Copy link
Copy Markdown
Contributor Author

Another concrete data-integrity issue in the Figma resync path:

figma-native.ts deliberately keeps each asset's ccm_... ID stable across source versions because forArtifact() hashes media.contentHash (falling back to the source version only when there is no content hash). But normalize.ts includes sourceVersion in the item content hash, so editing a Figma file creates a new immutable item version even when an embedded image is unchanged. ingestItems() then tries to insert that same media ID into creative_context_media, whose id is the global primary key. The transaction fails on the duplicate key, and the unique-conflict retry repeats the same insert.

Impact: a normal Figma resync can fail to persist the new version as soon as it contains an unchanged embedded asset. Please make the persisted media row identity version-aware (while keeping blob identity stable), or include the source version in the row ID and add a resync test covering unchanged assets.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

Large diff — this recap is a summarized view (top files + schema/API deltas).

builder-io-integration[bot]

This comment was marked as outdated.

@steve8708

Copy link
Copy Markdown
Contributor Author

Review notes

Reviewed the full 612-file diff against merge-base. Everything below was verified by reading the code — and in three cases by executing it — rather than inferred from the diff. There's a "verified clean" section at the end so the checks that passed are on record and don't get re-litigated.


Blocking-ish

1. creative-context's peer range is wrong — every install available today breaks at runtime

packages/creative-context/package.json declares "@agent-native/core": ">=0.8.0", but the package imports @agent-native/core/ingestion (21×) and @agent-native/core/search-utils (3×). Both subpaths are added to core's exports map by this PR — neither exists on main (core 0.103.0).

So installing @agent-native/creative-context against any core published today satisfies the peer range (no warning, no error), then throws ERR_PACKAGE_PATH_NOT_EXPORTED on first import. The floor should be the core version this release ships (>=0.104.0).

2. desktop-release.yml — the stable-channel safety net is dead code

The new resolver's own comment says the remote-tag lookup exists so "a reset workflow counter can never move the stable channel backwards." It cannot do that:

for (const line of tags.split("\\n")) {

Through the YAML block scalar → shell single quotes → node -e, that's the JS literal "\\n" — backslash + n, not a newline. git ls-remote output is newline-separated, so it stays one chunk, the $-anchored regex never matches, and releasedPatch is always -1.

Ran the exact script with a stubbed tag list (v1.2.40, v1.2.41; base 1.2.3; run number 5):

split() produced 1 chunk(s)
releasedPatch detected = -1   (should be 41)
chosen version = 1.2.5        (should be 1.2.42)

It reduces to nextPatch = max(patch + 1, runNumber), so a reset run counter can move the channel backwards or collide with an existing tag. This matters more because the same hunk moves auto builds off prereleases onto the stable update channel. Fix: split("\n").


Should-fix

3. content — a membership list is used as a write-authorization gate

actions/_content-spaces.ts:478 returns spaceIds = personal + every org membership regardless of role. But provisionableOrganizationSpaceIds (:301) only provisions for owner/admin, and resolveContentSpaceAccess (_content-space-access.ts:121) maps org role memberviewer and rejects editor.

Three write paths gate on spaceIds.includes(...); only import-content-source.ts:207 follows with resolveContentSpaceAccess(..., "editor"). The other two never import it at all:

  • _builder-docs-client.ts:539 — the error text literally says "does not have a writable Content space" while only checking membership
  • share-local-file-document.ts:99

An org member calling pull-builder-doc passes the guard, and :593 inserts a document into an org space that resolveContentSpaceAccess(spaceId, "editor") would reject. Not a cross-tenant leak, but the editor gate this module defines is enforced on one of three paths.

4. content — migration v74 deletes rows at boot (additive-only rule)

server/plugins/db.ts:748-849 runs DROP INDEX ×2 and three DELETE FROM … WHERE id NOT IN (SELECT MIN(id) …) at startup for every tenant. CLAUDE.md: "Never drop, rename, truncate, or destructively alter tables or columns in migrations or startup code."

The survivor is MIN(id), and these ids are sha256-derived hex (_content-spaces.ts:31), so "MIN" is effectively random rather than oldest. Two content_database_items rows for the same (database_id, document_id) at positions 3 and 17 → the survivor is whichever hash sorts lower, so the row's position silently changes on boot and the discarded row's body_hydration_status/body_hydration_error are lost with no record. Worth a second reviewer before this touches a production DB.

5. content — dead guard produces phantom workspace pages

actions/_content-spaces.ts:352:

const accessibleIds = new Set(spaces.map((space) => space.id));
for (const [index, space] of spaces.entries()) {
  if (!accessibleIds.has(space.id)) continue;   // can never be true

The set is built from the same array it guards, so the continue is unreachable — it looks intended to skip org spaces :310 refused to provision. A plain member of an unprovisioned org gets a reference document named after the org created in their personal space, plus a content_space_catalog_items row pointing at a nonexistent content_spaces.id. list-content-spaces innerJoins so it's hidden from the switcher, but the sidebar/Files DB show a page that opens empty and belongs to no workspace.

6. plan — the new destructive-collapse guard misses columns blocks

actions/update-visual-plan.ts:150collectPlanBlockIds recurses into tabs only, but PlanColumnsBlock (shared/plan-content.ts:415) has the identical data.columns[].blocks: PlanBlock[] shape. A full content replacement or a replace-blocks patch that drops blocks nested in a columns block (while preserving the outer id) never registers in removedBlockIds, so the new allowDestructive gate silently no-ops — defeating the guard this PR is adding. No test covers a columns-nested case.

7. clips — the idempotent-retry shortcut can report a good upload as "incomplete"

server/routes/api/uploads/[recordingId]/chunk.post.ts:260-273 reads sourceSizeBytes only from readAppState('recording-upload-<id>') with no fallback, unlike the equivalent shortcut in actions/finalize-recording.ts:541 which falls back to existing.videoSizeBytes ?? 0.

If the final chunk POST succeeds server-side but the client never sees the response (the exact case this PR hardens) and retries, a missing/failed app-state read returns sourceSizeBytes: undefined. desktop/src/lib/upload-verification.ts:29 then treats Number(undefined) as non-finite and throws "Clip may be incomplete… unknown number of bytes" for a recording that uploaded fine. Fails safe (keeps the backup), but it's a false alarm in the area this PR targets.

8. creative-context — a stale mediaId silently serves the wrong image

src/server/media.ts:99-105: media?.storageKey ?? detail.item.thumbnailBlobRef. If input.mediaId matches nothing (deleted asset, stale reference in a previously-rendered native artifact), it falls back to the item thumbnail and returns 200 with a different image instead of 404.

9. core — newly-public ingestion API is unbounded

Both ship as public API here (ingestion/index.ts), so creative-context can reach them:

  • ingestion/office.ts, pptx.ts:78, docx.ts — no input-size or decompressed-size bound before handing untrusted bytes to mammoth/jszip/officeparser, and no per-entry cap while iterating zip entries; a ~50KB crafted PPTX/DOCX can inflate to GBs. Sibling media.ts:96 already enforces maxInputBytes for the equivalent untrusted-image path.
  • ingestion/figma.ts:257,324summarizeFigmaFrame/visitFigmaNode recurse over node.children with no depth cap, node cap, or cycle guard, while summarizeFigmaNode in the same file takes maxDepth/maxNodes and figma-node-to-html.ts gates every entry point on assertFigmaNodeTreeComplexity.

10. analytics — bar charts lose date gap-filling at every grain

SqlChart.tsx:1229 keys gap-filling off panel.chartType rather than bucket granularity:

const pivoted = pivotRows(rawRows, panel.config.pivot, {
  fillDateGaps: panel.chartType !== "bar",
});

Added for the new weekly-bucketed panel, but it applies to all bar+pivot charts. A daily-grain bar+pivot chart (e.g. a week with 5 zero-signup days) previously got zero-value rows from fillMissingDailyRows; now those days are dropped, compressing the x-axis and diverging from the equivalent line/area chart. pivot.spec.ts only covers the weekly case.

11. analytics — duplicate panel ids can be saved, orphaning a panel forever

Filters are deduped by id (update-dashboard.ts:301, explicit seen set), but the parallel panel-validation loop has no equivalent check (dashboard-mutation-api.ts:943, update-dashboard.ts:337). dashboard.set({"panels":[...]}) can save two panels sharing an id; since requirePanel/findPanelIndex use findIndex (first match), a later dashboard.panel("a").setTitle(...) edits only one and the other becomes permanently unaddressable. Untested.

12. core client — new agent tabs hardcode English

client/agent-page/AgentTabsPage.tsx:136-152 (RESOURCE_TAB_COPY) plus literal title="Connections" (:326) / title="Access" (:506) bypass useT(), while sibling new code in this same PR (AgentJobsTab.tsx, AgentAskPopover.tsx) routes copy through it correctly.

13. core client — ambient resource-context ownership collision

client/MultiTabAssistantChat.tsx:1155-1194ownsContextItem keys only on whether the stored item starts with Resource context: ${type}:${id}, not on component identity. Clips mounts two legitimate owners of the same default contextKey for one recording (library-layout.tsx:562 sidebar + r.$recordingId.tsx:1147 embedded panel). Both claim ownership; whichever unmounts first removes the chip out from under the still-mounted one, silently dropping the agent's grounding.


Nits

  • untrusted-reference.ts:12 — the delimiter defense works, but only by accident. A literal <<<END_UNTRUSTED_REFERENCE>>> in ingested text is neutralized — but only because the generic /<[^>]+>/g HTML-tag strip happens to consume <<<END_UNTRUSTED_REFERENCE> and leave >> (verified by executing the real function; no escape is achievable). Nothing in sanitizeUntrustedReference strips the delimiter tokens on purpose, so the guarantee rests on the delimiters incidentally looking like an HTML tag. Change the delimiter to anything without </> and the boundary silently becomes forgeable. Worth an explicit replaceAll of both tokens in the shared helper (generation-context.ts:253 already does this per-callsite) plus a test pinning the property.
  • creative-context/package.jsonofficeparser is in optionalDependencies but unused anywhere in the package (only core uses it, and core declares it itself). optionalDeps install by default, so every consumer pulls it for nothing.
  • creative-context/src/connectors/google-slides-native.visual.spec.ts:4 imports ../../../core/src/ingestion/media.js, reaching outside the package root; the sibling figma-native.visual.spec.ts correctly uses @agent-native/core/ingestion. Since files: ["src"] ships specs, the published tarball carries a dangling relative path.
  • creative-context/src/connectors/figma-native.ts:1142flattenTransform emits [a, b, tx, c, d, ty], but CSS matrix(a,b,c,d,e,f) wants (a, c, b, d, tx, ty)b/c are swapped. Inert today (nothing reads manifestChild.transform), but it's persisted as durable manifest metadata in a public package, so a future consumer treating it as a CSS matrix renders rotated/skewed elements distorted.
  • .changeset/quiet-inline-chat-chrome.md is patch but changes a default (AgentChatHome showHeaderfalse). No shipped template is affected (all pass it explicitly), but for external consumers of a published package a changed default is minor, not a fix.
  • connections/catalog.ts:96 — Notion credentialKeys narrowed to [] while NOTION_API_KEY still functions; a self-hosted deployment that configured it manually now sees zero credential fields for Notion in settings.
  • clips i18n — en-US.ts reworded agentTitle/openAgentSettings to "Agent workspace", but the locale files (ja-JP, de-DE, ar-SA, …) didn't pick up those keys, so non-English UI still reads "agent settings".
  • analytics/.../SqlChart.tsx:137isAnimationActive latches false on the first size change and is never restored, so after one window resize every later data change in that chart renders unanimated for the rest of the mount.
  • analytics/.../types.ts:91 + dashboard-mutation-api.ts:45 — an orphaned JSDoc for a barWidth knob was added with no field declaration and no reader anywhere; it documents a nonexistent option to the agent via the mutation API's tool description.
  • analytics/server/routes/sessions/lookup.get.ts — correctly authenticated and scoped, but has zero callers repo-wide and no test; resolveSessionReplayLink (session-replay.ts:1692) also bypasses the shared resolveAccess model every other reader here uses, requiring an exact ownerEmail match (over-restrictive, so an org teammate 404s).
  • plan/actions/patch-visual-plan-source.ts:226 — the force: true snapshot is taken before the conditional UPDATE … WHERE updatedAt = …; when the update loses the race and throws, the snapshot isn't rolled back, leaving a spurious "Before source patch" version for a patch that never applied.
  • mail/.../EmailThread.tsx:2547measureEmailDocumentHeight does body.querySelectorAll("*") + getBoundingClientRect() per descendant on every trigger, now including a new ResizeObserver and window resize. Table-heavy marketing emails have hundreds–thousands of nodes, and this runs per expanded message.

Verified clean (recording so these don't get re-litigated)

  • design's ~2,229 deletions are a clean extraction, not a loss. figma-node-to-html.ts (1859 → 1 line) now re-exports @agent-native/core/ingestion; diffed old vs new — the only difference is an added relativeTransform field, and all 21 prior exports are re-exported from core. Tweaks/Code/comment/annotate surfaces untouched.
  • The local-folder connector is genuinely path-safe. _local-folder-source.ts performs zero filesystem access (opaque connectionId + metadata only). Client-supplied paths go through normalizeSourcePath, which rejects \0, leading slashes, and any ./../empty segment, and requires .md/.mdx; the server never opens them. reveal-local-source-file.ts:30 uses spawn with no shell.
  • The prompt-injection boundary holds — attempted delimiter-escape payloads against the real delimitUntrustedReference produce no early close (see the nit above for why that's more fragile than it looks).
  • clips recording hardening is real. The compression swap now requires a duration match from the worker's own completion payload instead of the flaky HEAD/Range probe behind the half-written-derivative incident, and fails closed via markCompressionFailed; finalize verifies served bytes against uploaded bytes; desktop recovery now keeps local backups on uncertainty rather than deleting them; app.tsx's loadPendingUploads switch to Promise.allSettled fixes a real bug where one source's failure hid the other's pending uploads.
  • public-recording.get.ts:221 is a genuine access fix — now routes through resolveAccess("recording", ...); anonymous/unauthorized viewers stay blocked, and explicitly-shared non-owners are no longer wrongly blocked.
  • SSR hard-cache shell contract — no SSR/cache file touched anywhere in the diff.
  • Managed OAuth scoping — session/org resolved and access-checked before resolveSecret, all inside runWithRequestContext; state HMAC verified via timingSafeEqual, cross-tenant mismatch rejection covered by tests.
  • stop-zombie-run-events SQL is correct — the spec test is a SQL-string mock that proves nothing about execution, so the guard was executed against real PGlite: terminal → 0 rows, running → 1 row, missing row → 1 row. All three branches correct.
  • Dashboard mutations are race-safeupsertDashboardWithRetry fences every write with WHERE id=? AND updated_at=? and recomputes from fresh state on retry.
  • Optional deps load safely — every new optionalDependency is behind await import() in try/catch with a clear fallback; no static top-level import.
  • Schema otherwise additive; no hardcoded secrets anywhere in the diff; core docs updated across all 10 locales for both changed pages; 27 changelog entries present.

One process note

At 612 files / ~62k additions, this bundles a brand-new public package, a desktop release-channel change, and 12 template surfaces into a single revert unit. Both blocking-ish findings sit in the packaging/release-plumbing slice — the part least exercised by CI and hardest to see in a diff this size. That slice might be worth splitting out and landing on its own.

@builder-io-integration

Copy link
Copy Markdown
Contributor

⚠️ Review Agent ran into a problem and couldn't finish reviewing the latest commit.

We've been automatically notified and are looking into it. Push a new commit to re-trigger the review, or contact support@builder.io if this keeps happening.

Error ID: 0024afa1b78e4b1882092839d2c193c9

builder-io-integration[bot]

This comment was marked as outdated.

@builder-io-integration builder-io-integration Bot left a comment

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.

Builder reviewed your changes and found 2 potential issues 🟡

Review Details

Code Review Summary

This incremental review covers the latest fix batch in PR #2190. The five previously-open findings were rechecked and are fixed: context jobs now apply organization scope, OAuth callbacks no longer add a revocation-bypassing app allow-list entry, Google Drive preserves incremental scopes, the Picker Promise has a timeout, and SQL validation now covers metadata/media text. Those stale review threads were resolved.

The new access-control and storage-boundary hardening is directionally sound, with focused tests added around A2A access, design context access, provider OAuth, and normalization. I found two remaining storage-boundary gaps in the expanded validation: provenance is still serialized directly into SQL without a size check, and media locator fields can carry arbitrarily large values. Both can bypass the intended raw-payload/blob-storage boundary.

Key Findings

  • 🟡 MEDIUM — Bound item provenance before SQL persistence.
  • 🟡 MEDIUM — Bound or reject oversized media locator fields before SQL persistence.

Risk classification remains High because this PR spans OAuth, tenant authorization, public agent access, and persistent data handling.

🧪 Browser testing: Could not verify — the dev server was healthy, but all 17 planned cases were blocked because browser-testing executors had no Chrome/navigation tools available.

Comment on lines +132 to +141
export function assertContextItemSqlTextLimits(
item: Pick<
NormalizedContextItem,
| "content"
| "summary"
| "mimeType"
| "metadata"
| "chunks"
| "media"
| "edges"

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.

🟡 Apply the SQL payload limit to item provenance

assertContextItemSqlTextLimits now checks metadata, chunk/media/edge fields, and text limits, but it still omits NormalizedContextItem.provenance. The ingest path serializes provenance directly into the SQL provenance column, so an importer can place an oversized raw/provider payload there and bypass the storage/blob boundary. Include provenance in the validation and apply the JSON byte limit (or move oversized provenance to private blob storage).

Additional Info
Reported by 1 of 3 review agents; verified in the current normalization and ingest paths.

Fix in Builder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 810a3b6. The SQL guard now applies the structured metadata limit to item provenance as well, so oversized provider payloads fail closed before provenance is persisted.

"move raw payloads to private blob storage before ingest",
);
}
for (const media of item.media ?? []) {

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.

🟡 Bound media locator fields before SQL persistence

The expanded media validation bounds OCR/caption/alt text and metadata, but not url, storageKey, or provenanceUrl. These values are persisted by the ingest path, so an oversized locator (including a data: URL) can still bypass the SQL/blob storage boundary. Apply a byte limit to persisted locators and reject inline data URLs rather than storing them.

Additional Info
Reported by 1 of 3 review agents; verified in the current normalization and ingest paths.

Fix in Builder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 810a3b6. Media URL, storage-key, and provenance-URL locators now have byte limits, and inline data URLs are rejected before SQL persistence.

@steve8708
steve8708 merged commit 2625de5 into main Jul 16, 2026
87 of 89 checks passed
@steve8708
steve8708 deleted the changes-489 branch July 16, 2026 23:37
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.

1 participant