Skip to content

feat(canvas): build and securely deliver cloud artifacts - #76649

Merged
trunk-io[bot] merged 30 commits into
masterfrom
posthog-code/port-canvas-build-pipeline-phase-6
Aug 3, 2026
Merged

feat(canvas): build and securely deliver cloud artifacts#76649
trunk-io[bot] merged 30 commits into
masterfrom
posthog-code/port-canvas-build-pipeline-phase-6

Conversation

@k11kirky

@k11kirky k11kirky commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Canvases relied on the desktop file-system abstraction for source, versions, channels, and task filing. The paired backend remodel in #73874 replaces that surface with first-class canvas and channel APIs, so the desktop client must move to the new contract at the same time.

Ports PostHog/code#3835 into products/desktop/ after the monorepo migration.

Changes

  • Move canvases onto typed canvas records, server-side source versions, publish/build records, and signed artifacts.
  • Use the backend channel UUID consistently for Spaces navigation, stars, context, feeds, and task filing.
  • Make build progress, failures, diagnostics, artifact refresh, undo, and revert observable in the canvas UI.
  • Restrict built artifact frames to their configured artifact origin and revoke their data bridge after navigation.
  • Remove the retired desktop file-system and local canvas-preview paths.
  • Preserve the imported Spaces dialog behavior while reconciling the port with the singular task repository contract.
  • Require the artifact hostname ingress to forward only /canvas-artifacts/; infrastructure configuration handles this deployment boundary.

How did you test this code?

  • pnpm install --frozen-lockfile --lockfile-only
  • pnpm typecheck
  • pnpm --filter @posthog/core test – 2,996 tests passed
  • pnpm --filter @posthog/ui test – 2,528 tests passed
  • pnpm --filter @posthog/shared test – 780 tests passed
  • node scripts/check-host-boundaries.mjs – no new violations
  • Biome check on the monorepo reconciliation files
  • pnpm exec vitest run src/features/canvas/freeform/BuiltCanvas.test.tsx – 3 tests passed
  • pnpm --filter @posthog/ui typecheck
  • Biome check on BuiltCanvas.tsx and its test

I did not run a manual end-to-end generate → build → artifact flow.

👉 Stay up-to-date with PostHog coding conventions for a smoother review.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No documentation change. This ports an existing client implementation into the imported desktop tree.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Codex applied the /porting-code-prs skill to preserve the source patch series and authorship, map it under products/desktop/, retain the monorepo lockfile security overrides, and reconcile conflicts against the imported Spaces tree. The original 27 commits were republished as GitHub-verified commits, followed by verified migration-reconciliation and artifact-isolation commits.


Created with PostHog Code

k11kirky and others added 28 commits August 3, 2026 13:55
Rewire the client off the retired desktop file-system surface. Canvases are
now rows on the canvases API: the record carries pointers only, source lives
in server-side versions (source/versions/revert endpoints), and the rendered
output is the published build's artifact. One channel identity everywhere —
the backend channel UUID keys the sidebar, stars, CONTEXT.md instructions,
feeds, task filing (a PATCH of the task's channel), routes, and deep links;
the name-matching bridge (useBackendChannel) is gone. The home canvas is an
is_home row seeded by a real publish, and its board reads system.canvases /
system.tasks.

The freeform store shrinks to view state (context buffer, version browsing,
runtime errors); undo/redo browse server versions and revert republishes. The
build lifecycle is now observable end-to-end: builds poll while a generation
runs, the artifact URL is pinned per build (no iframe reloads on poll ticks),
queued/building states show elapsed time, failures surface in both modes with
diagnostics and an "Ask agent to fix" affordance, and the completion toast
checks the build before announcing success. Agent prompts use the canvas-*/
channel-* tools; the unused local-preview pipeline (canvas-preview, local
canvas builder, its contract) is deleted, with the platform manifest vendored
from the server's single source of truth for the whitelist contract test.
assertCanvasCapability moves to @posthog/core and tolerates a not-yet-loaded
manifest instead of rejecting every request.

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
Restore the canvas context editor's save path (it lost its writer in the
remodel): the editor now buffers locally and commits through the saveContext
mutation, and the freeform store shrinks to runtime-error + version-browse
state only. Scope revert/reset/sync invalidations to the one canvas via a
shared helper; poll builds at the record cadence until a build actually
starts; request explicit page sizes on paginated lists. Collapse the
duplicated channels query onto useTaskChannels, find the personal channel by
type everywhere, and collapse ChannelTaskRecord's duplicate id/taskId.
Extract the version-navigation arithmetic into a tested pure helper, replace
the currentCode sentinel with a real isEdit flag, seed the home canvas from
the shared contract constants, and delete dead code (orphaned freeform
version schemas, unreferenced channel client methods, the pruned
canvas-contracts validators, a stray local redis dump).

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
On OAuth apps with a seeded scope ceiling, /oauth/authorize narrows a wildcard request to the ceiling enumerated at grant time, and refresh never widens it — so sessions granted before the server grew the canvas scope 403 on every /canvases/ call forever. Bumping the version forces those sessions through one re-auth, whose fresh grant includes the scope.

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
Canvas-mode submits from the channel composer pre-created an "Untitled canvas" and pinned it in the generation prompt, so the skill's resolve-or-create step never ran and every prompt minted a new husk. The composer now just starts the task (staying on the feed with an optimistic kickoff card); the prompt routes the agent into listing the channel's canvases, building on a clearly-matching one — saying so in its reply — or creating one with a descriptive name. Canvas-side effects (generation-task recording, auto-naming, completion-toast tracking) only run when the surface pre-resolved a target.

While a generation runs on a not-yet-renderable canvas, the run's chat panel now opens by default — in view mode too — instead of stranding the user on the "Generating…" spinner; a minimize sticks for the visit and each new run reopens it.

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
…ud runs

A cloud sandbox lingers after the agent's turn so follow-ups can reuse it — the run record only completes on the inactivity timeout, long after generation ended — so keying the canvas's "Generating…" state (and the loading bar, disabled undo/redo, and record/build polling) on run status pinned it for the whole linger window. When a live session mirror exists, its turn-level prompt-pending signal (armed on session/prompt, cleared on turn_complete) now decides, with "connecting" covering the attach/replay window; without a mirror the run status remains the only signal.

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
The `quality` CI job (`biome ci .`) failed with two format errors on lines
this branch added: a wrapped `expect(...).not.toContain(...)` call in
prompt-builder.test.ts and the collapsed-panel JSX guard in
FreeformCanvasView.tsx. Reformatted with `biome format --write` so
`biome ci` exits clean; no behaviour change.

Generated-By: PostHog Code
Task-Id: 4928140a-2ab8-4fb3-86c1-e02d25f60cca
…ffect

react-doctor blocked CI with a `no-adjust-state-on-prop-change` error at
CanvasBuildStatus.tsx: the effect called `setNow(Date.now())` to restart the
clock when a new build became active, so the first commit after a build change
briefly rendered the previous build's elapsed time.

The `now` state was only ever read to compute the label, so drop it: the
interval now just bumps a tick to force a re-render and the label is computed
from `Date.now()` during render. This removes the duplicated state (one of the
two remedies react-doctor suggests) rather than adding a `prev`-prop
comparison, and it also means the label can never be stale.

react-doctor and `biome ci` are clean; @posthog/ui typecheck plus the
@posthog/ui (2323) and @posthog/core (2595) unit suites pass.

Generated-By: PostHog Code
Task-Id: 4928140a-2ab8-4fb3-86c1-e02d25f60cca
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
8 updated
Run: 556c77f3-ca53-4b8c-b076-ce14c09c6b1b

Co-authored-by: k11kirky <4571455+k11kirky@users.noreply.github.com>
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Reuse: use escapeXmlAttr from @posthog/shared for prompt metadata; derive
platform pins (react version, canvasSdkVersion) from CANVAS_PLATFORM_MANIFEST;
route the dashboards-grid delete through deleteCanvasWithUndo.

Simplification: single build-record mapping shared by toBuildRecord and
tryToBuildRecord; de-duplicate the ActivityView feed body across layouts;
drop the freeformChatStore LRU/mount bookkeeping now that per-thread state
is tiny; inline the one-line canvasSourcePresentation helpers; delete the
orphaned desktop file-system client methods.

Efficiency: stop re-validating build records in polled getBuilds; bound
canvas data payloads without a per-request TextEncoder; keep onDataRequest
referentially stable across builds polls; resolve the home canvas from the
query cache before hitting ensureHomeCanvas.

Altitude: extract a shared canvas host message router used by BuiltCanvas
and FreeformCanvas (both paths now get the concurrency/size/timeout guards);
move capability gating onto BuiltCanvas via a capabilities prop; extract the
pinned-artifact lifecycle into usePinnedArtifact.

Generated-By: PostHog Code
Task-Id: aea6c5f4-02ae-448b-b4dc-f10f581ad597
Align the port with the imported Spaces UI and singular task repository contract. Regenerate the nested lockfile entry while retaining the monorepo security overrides.

Generated-By: PostHog Code
Task-Id: 636487f1-66a7-476a-aa18-3b6340ce2c34
@k11kirky k11kirky self-assigned this Aug 3, 2026
@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Hey @k11kirky! 👋

It looks like your git author email on this PR isn't your @posthog.com address (k11kirky@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@k11kirky
k11kirky marked this pull request as ready for review August 3, 2026 13:12
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (131 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

Comment thread products/desktop/packages/ui/src/features/canvas/freeform/FreeformCanvasView.tsx Outdated
@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@veria-ai

veria-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

k11kirky commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Required CI is green. The remaining review finding concerns the published artifact capability/permission boundary. I am not changing security-sensitive permission enforcement during this automated CI-feedback turn based on review guidance alone; this needs explicit human review and direction.

Generated-By: PostHog Code
Task-Id: 474defa1-1ea2-4e0e-ad8c-542180ecf339

@Piccirello Piccirello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agent-driven security review — findings inline.

// assert allows that transient window instead of hard-failing
// every read. The interactive path never gates: the author's
// own client keeps full data access while iterating.
capabilities={

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Medium · authz · confidence 5/5

Capability enforcement is switched off by a local UI toggle any viewer can click.

interactive comes from useIsDashboardEditing(dashboardId), which reads dashboardEditStore — a plain Zustand store holding editing: Record<string, boolean>. It's flipped by the "Edit" button in components/WebsiteLayout.tsx, and FreeformEditControls renders that button unconditionally: no authorship check, no access-control check, no server involvement.

The published manifest's capabilities are presented as a security control across both halves of this work. The server's CanvasSourceProjectSerializer help text says "the host enforces these at runtime and validation rejects undeclared ph calls", and validate_source_project blocks a publish whose code calls undeclared ph.*. This line is that runtime enforcement, and it's one click from off for any viewer on any canvas.

Impact — user B opens a canvas that user A's agent published, clicks Edit (a normal, always-visible affordance), and A's canvas gets ungated ph.query (arbitrary HogQL), ph.loadInsight (any short id), and ph.capture (any event name), all executed with B's credentials against B's project. Canvas source is agent-authored, so a prompt-injected generation is the realistic way a canvas comes to want more than it declared. Egress stays capped by the artifact CSP and the posthog.com-only openExternal allowlist, so this is undeclared data access and event writes rather than exfiltration.

Fix — derive the ungated path from something authoritative (authorship, or project write access on the canvas) rather than from a client-side view-mode toggle. If the toggle is meant to stay the trigger, the gate has to move somewhere the canvas's viewer can't flip.

Agent-driven security review.

method: string,
payload: unknown,
): void {
if (!capabilities) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Low · authz · confidence 5/5

assertCanvasCapability is an allowlist of rejections rather than a default-deny, so it fails open in two directions.

  1. if (!capabilities) return; — no manifest means every method is allowed.
  2. Any method outside {query, loadInsight, capture} passes unchecked, because the function is a sequence of per-method rejections with no terminal throw.

(2) is already primed to matter: canvasToHostMessageSchema accepts method: "run" (core/src/canvas/freeformSchemas.ts), and the comments there and in freeformDataBridge.ts say Phase 3 makes run the view-mode data path — the one that most needs gating. Today run throws "not available yet" in handleFreeformDataRequest, so there's no live hole; the shape is the defect.

On (1), the docstring justifies fail-open as covering the window where "the manifest hasn't loaded yet". That window doesn't exist on this path: capabilities and artifactUrl are both read off the same publishedBuild in the same render (FreeformCanvasView.tsxusePinnedArtifact), so a rendered artifact always has its manifest. The remaining routes to undefined are the interactive toggle (separate comment) and the pinned artifact outliving publishedBuild when lifecycle goes undefined — worth checking whether a canvas→canvas route change remounts FreeformCanvasView, since WebsiteDashboard.tsx puts no key on it.

Fix — switch on the method and throw for anything unrecognized, and make "ungated" an explicit value (a sentinel or a separate prop) so a missing manifest can't be spelled the same way as a deliberate bypass:

export function assertCanvasCapability(
  capabilities: CanvasCapabilities | "ungated",
  method: string,
  payload: unknown,
): void {
  if (capabilities === "ungated") return;
  switch (method) {
    case "query": ...
    case "loadInsight": ...
    case "capture": ...
    default:
      throw new Error(`Method "${method}" is not allowed by this canvas`);
  }
}

Agent-driven security review.

onRendered: () => latest.current.onRendered?.(),
onNavigate: (intent) => latest.current.onNavigate?.(intent),
}),
isFrameFocused: () => document.activeElement === iframeRef.current,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Low · availability · confidence 3/5

The focus gate on open-external doesn't hold — canvas code can satisfy it without any user interaction.

document.activeElement === iframeRef.current is checked against the host document. Canvas code can call .focus() on an element inside the artifact frame at load; focus moving into a nested browsing context makes the containing iframe the ancestor document's activeElement. So the guarantee stated in canvasHostMessageRouter.ts — "a real link click moves focus into the iframe; requiring it stops canvas code from auto-opening URLs on load" — isn't one. The window.confirm below is what actually holds.

Impact — a canvas can drive a modal confirm dialog at the throttle rate (1/s) from the moment it loads, which blocks the desktop window. The isSafePostHogUrl allowlist caps everything beyond the nuisance, so this is availability rather than exfiltration.

Fix — key the gate on a real user-activation signal (a recent trusted pointer or key event on the host) instead of activeElement. If the focus check stays as-is, drop the guarantee from its doc comment so the next reader doesn't treat it as the gate.

Agent-driven security review. 3/5: the activeElement behaviour is standard but I didn't run it, and whether the dialog spam sustains depends on the artifact frame being site-isolated into its own process, which isn't visible from the repo.

Generated-By: PostHog Code
Task-Id: 88e5c6c3-a3f3-4800-85d9-9e02cc4b08ea
@k11kirky k11kirky added the create desktop release Auto-release desktop on merge: desktop-tag.yml tags, desktop-release.yml builds and ships label Aug 3, 2026
@k11kirky

k11kirky commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

/trunk merge

1 similar comment
@adamleithp

Copy link
Copy Markdown
Contributor

/trunk merge

@trunk-io
trunk-io Bot merged commit 91c6fa2 into master Aug 3, 2026
242 of 246 checks passed
@trunk-io
trunk-io Bot deleted the posthog-code/port-canvas-build-pipeline-phase-6 branch August 3, 2026 23:10
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Could not verify team-posthog-code membership (GitHub API error). Auto-release is limited to members of the team-posthog-code GitHub team, so this will ship with the next scheduled release instead.

@deployment-status-posthog

deployment-status-posthog Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-08-03 23:54 UTC Run
prod-us ✅ Deployed 2026-08-04 00:08 UTC Run
prod-eu ✅ Deployed 2026-08-04 00:10 UTC Run

k11kirky added a commit that referenced this pull request Aug 5, 2026
…ng clobbered by the canvas port

The canvas artifacts port (#76649) was built from a stale snapshot and overwrote newer master work when it merged. This restores the artifacts page's list/grid/masonry view toggle, page header, and delete-undo handling; the Task.repositories field and its three consumers (task normalization, cloud-run instructions/checkpointing, local handoff) from #76448 that #77252 did not cover; the deleted multi-repo handoff tests; converts the new CanvasBuildStatus component off banned Radix imports; and drops the stale common/alerting entry #73874's conflict resolution re-added to .dockerignore.

Generated-By: PostHog Code
Task-Id: 167fa10a-0a98-447b-bc44-34d4433de1f0
joethreepwood added a commit that referenced this pull request Aug 5, 2026
Co-authored-by: k11kirky <4571455+k11kirky@users.noreply.github.com>
k11kirky added a commit that referenced this pull request Aug 5, 2026
…text editor fill

Second adversarial audit pass over the #76649 diff found four more still-broken downgrades: the canvas toolbar's Delete… flow (destructive confirm + undo toast + navigate back to artifacts) was removed, the pin menu label was hardcoded to "channel" under the Spaces layout, the menu lost the w-auto min-w-fit width fix that prevented "Unpin from space" clipping, and the CONTEXT.md edit textarea stopped filling the viewport (fixed rows inside a scroll area).

Generated-By: PostHog Code
Task-Id: 167fa10a-0a98-447b-bc44-34d4433de1f0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create desktop release Auto-release desktop on merge: desktop-tag.yml tags, desktop-release.yml builds and ships

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants