Skip to content

feat(tasks): add loop skill bundles seeded into fired runs#73069

Open
charlesvien wants to merge 12 commits into
masterfrom
feat/loop-skill-bundles
Open

feat(tasks): add loop skill bundles seeded into fired runs#73069
charlesvien wants to merge 12 commits into
masterfrom
feat/loop-skill-bundles

Conversation

@charlesvien

@charlesvien charlesvien commented Jul 23, 2026

Copy link
Copy Markdown
Member

Problem

posthog-code is adding "run a skill instead of instructions" to Loops. Cloud task runs already support skills as skill_bundle run artifacts, but a client bundles and uploads them at task creation. Loop fires happen server-side with no client attached, so a loop needs to store its skill bundles once and have every fired run seeded with them.

Changes

  • Loop.skill_bundles JSONField (migration 0071): manifest entries in the same shape as TaskRun.artifacts, bytes under a loop-scoped S3 prefix so run retention never reaps them.
  • PUT /loops/{id}/skill_bundles/: wholesale replace. Verifies base64 and sha256, caps count (10) and size (30MB), writes to S3 and deletes superseded objects best-effort. Gated like other identity-bearing config: skill_bundles joins IDENTITY_FIELDS, so on team loops only the owner may change it.
  • fire_loop seeds each stored bundle into the new run: S3 copy into the run's artifact prefix plus a matching skill_bundle manifest entry. The sandbox agent-server already installs those, so no sandbox changes. A failed copy fails the fire, which rolls back and retries cleanly rather than running with silently missing skills.
  • LoopSerializer exposes skill_bundles (skill name, source, size, sha256, uploaded_at; no storage paths).

Client side: PostHog/code#3741.

How did you test this code?

pytest products/tasks/backend/tests/test_loops_api.py products/tasks/backend/tests/test_loop_runs.py (119 passed). New coverage, each catching a regression no existing test did:

  • LoopSkillBundlesAPITest: replace + readback + clear (manifest and S3 delete of superseded objects), sha256 mismatch is a 400 with no S3 write, count cap, non-owner replace on a team loop is a 403, someone else's personal loop is a 404.
  • TestFireLoopSeedsSkillBundles: a fire copies bundles into the run manifest under the run prefix, a bundle-less fire never touches storage, a failed S3 copy fails the fire with no orphaned run.

makemigrations --check clean, ruff and mypy clean on touched files.

Automatic notifications

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

Docs update

N/A

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Built with Claude Code. Design was chosen against two rejected alternatives: copying the skill body into loop.instructions (loses companion files, drifts) and resolving team LLMSkills at fire time (needs a new skills-to-sandbox path; today nothing installs LLMSkills into sandboxes). Snapshot-at-save reuses the exact skill_bundle artifact mechanic the sandbox agent-server already consumes. A dedicated replace endpoint (instead of extending the loop write serializer) keeps multi-megabyte base64 payloads out of every PATCH and gives the client one idempotent call.

@trunk-io

trunk-io Bot commented Jul 23, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
products/tasks/backend/facade/loops.py:1046
**Partial replacements leak uploads**

When an earlier bundle is uploaded and a later bundle fails validation or storage writing, the manifest update never runs and the earlier object remains unreferenced under the persistent loop prefix with no retention TTL or subsequent cleanup path.

### Issue 2 of 4
products/tasks/backend/logic/services/loop_runs.py:501
**Rollback leaves copied artifacts**

When one bundle copy succeeds before a later copy or manifest save fails, the database transaction rolls back but the copied object remains under the discarded run prefix. Temporal retries create new run IDs, so each failed attempt accumulates another set of unreachable objects.

### Issue 3 of 4
products/tasks/backend/facade/loops.py:1023-1027
**Concurrent replacements orphan bundles**

When two replacement requests overlap, both read the same previous manifest and upload separate objects without locking the loop. The last database write wins, leaving the other request's newly uploaded objects unreferenced and outside later manifest-based cleanup.

### Issue 4 of 4
products/tasks/backend/facade/loops.py:1046
**Deleted loops retain bundle objects**

When a loop with attached bundles is soft-deleted, these persistent loop-scoped objects are not removed or assigned a retention TTL, so deleting the loop leaves its bundle data stored indefinitely.

Reviews (1): Last reviewed commit: "add loop skill bundles seeded into fired..." | Re-trigger Greptile

Comment thread products/tasks/backend/facade/loops.py Outdated
Comment thread products/tasks/backend/logic/services/loop_runs.py Outdated
Comment thread products/tasks/backend/facade/loops.py Outdated
Comment thread products/tasks/backend/facade/loops.py Outdated
Comment thread products/tasks/backend/facade/loops.py Outdated
@veria-ai

veria-ai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR adds loop skill bundles to tasks and seeds those bundles into fired runs, including when runs are recovered.

One significant security issue remains: a teammate who takes ownership of a loop can replace its bundles, causing an orphaned run to execute new instructions with the previous owner’s OAuth, GitHub, or MCP credentials during recovery. Although four issues have been addressed, recovery still needs to use an immutable bundle snapshot captured when the run was fired.

Open issues (1)

Fixed/addressed: 4 · PR risk: 8/10

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +554 B (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.38 MiB · 🔺 +554 B (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.24 MiB · 22 files no change ███░░░░░░░ 27.5% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.06 MiB · 3,009 files 🟢 -5 B (-0.0%) ████████░░ 83.0% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.5 KiB ../node_modules/.pnpm/posthog-js@1.407.2/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
236.0 KiB src/taxonomy/core-filter-definitions-by-group.json
226.1 KiB ../node_modules/.pnpm/posthog-js@1.407.2/node_modules/posthog-js/dist/module.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.8 KiB src/lib/api.ts
94.0 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.18 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.18 MiB · 17 files no change ████░░░░░░ 38.1% of 5.72 MiB
Deferred (lazy) 2.07 MiB · 33 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
713.8 KiB dist/toolbar/toolbar-app-HLTHZTTF.css
545.0 KiB dist/toolbar/chunk-chunk-JGF7ZYC3.js
484.2 KiB dist/toolbar/chunk-chunk-YMFODIKP.js
133.6 KiB dist/toolbar/chunk-chunk-YSVTUFEA.js
131.8 KiB dist/toolbar/chunk-chunk-T5KY5WYR.js
71.0 KiB dist/toolbar/toolbar-app-Y3ZKEAFE.js
69.0 KiB dist/toolbar/chunk-chunk-27JL52RE.js
35.6 KiB dist/toolbar/chunk-chunk-ALQOU6WO.js
20.9 KiB dist/toolbar/chunk-chunk-VMBXRLPQ.js
12.2 KiB dist/toolbar/chunk-chunk-PIK3PADE.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — 🔺 +10.8 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1352.13 MiB · 🔺 +10.8 KiB (+0.0%)

ℹ️ MCP UI apps size — 32 app(s), 17065.5 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 599.5 KB 187.7 KB
action 457.8 KB 187.7 KB
action-list 564.3 KB 187.7 KB
cohort 456.8 KB 187.7 KB
cohort-list 563.3 KB 187.7 KB
email-template 456.6 KB 187.7 KB
error-details 472.4 KB 187.7 KB
error-issue 457.5 KB 187.7 KB
error-issue-list 564.2 KB 187.7 KB
experiment 561.5 KB 187.7 KB
experiment-list 565.1 KB 187.7 KB
experiment-results 563.2 KB 187.7 KB
feature-flag 567.1 KB 187.7 KB
feature-flag-list 570.9 KB 187.7 KB
feature-flag-testing 461.0 KB 187.7 KB
insight-actors 562.1 KB 187.7 KB
invite-email-preview 456.0 KB 187.7 KB
llm-costs 559.5 KB 187.7 KB
session-recording 458.6 KB 187.7 KB
session-summary 463.9 KB 187.7 KB
survey 458.4 KB 187.7 KB
survey-global-stats 562.2 KB 187.7 KB
survey-list 565.0 KB 187.7 KB
survey-stats 562.2 KB 187.7 KB
trace-span 457.2 KB 187.7 KB
trace-span-list 564.2 KB 187.7 KB
workflow 457.1 KB 187.7 KB
workflow-list 563.7 KB 187.7 KB
loops-review 461.2 KB 187.7 KB
query-results 745.5 KB 187.7 KB
render-ui 826.2 KB 187.7 KB
visual-review-snapshots 461.6 KB 187.7 KB
⚠️ Backend coverage — 97.0% of changed backend lines covered — 10 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 97.0% (363 / 373)

File Patch Uncovered changed lines
products/tasks/backend/facade/loops.py 91.1% 451, 1011–1012, 1050–1051, 1053, 1073–1074
products/tasks/backend/logic/services/loop_runs.py 94.7% 564–565

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29993870183 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
tasks ██████████████░░░░░░ 69.4% 29,727 / 42,859
signals ████████████████░░░░ 79.2% 20,576 / 25,978
cdp ████████████████░░░░ 80.7% 3,117 / 3,863
data_modeling █████████████████░░░ 82.8% 5,529 / 6,675
notebooks █████████████████░░░ 85.3% 7,266 / 8,522
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
cohorts ██████████████████░░ 87.8% 4,488 / 5,114
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
exports ██████████████████░░ 88.4% 6,949 / 7,861
data_warehouse ██████████████████░░ 88.8% 11,867 / 13,358
engineering_analytics ██████████████████░░ 89.2% 5,641 / 6,323
conversations ██████████████████░░ 89.3% 16,820 / 18,834
dashboards ██████████████████░░ 89.5% 5,989 / 6,693
error_tracking ██████████████████░░ 89.7% 10,174 / 11,342
alerts ██████████████████░░ 89.9% 4,054 / 4,508
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
mcp_analytics ██████████████████░░ 90.1% 2,763 / 3,065
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
slack_app ██████████████████░░ 90.7% 9,025 / 9,948
marketing_analytics ██████████████████░░ 91.0% 11,792 / 12,964
stamphog ██████████████████░░ 91.1% 4,056 / 4,450
product_analytics ██████████████████░░ 91.3% 5,811 / 6,362
ai_observability ███████████████████░ 92.7% 14,995 / 16,168
surveys ███████████████████░ 93.0% 5,730 / 6,163
web_analytics ███████████████████░ 93.0% 14,178 / 15,237
posthog_ai ███████████████████░ 93.2% 1,326 / 1,422
approvals ███████████████████░ 93.3% 3,437 / 3,682
reminders ███████████████████░ 93.4% 468 / 501
workflows ███████████████████░ 93.5% 6,337 / 6,775
endpoints ███████████████████░ 94.1% 8,640 / 9,177
skills ███████████████████░ 94.5% 2,893 / 3,061
revenue_analytics ███████████████████░ 94.6% 3,651 / 3,858
review_hog ███████████████████░ 94.7% 6,814 / 7,199
logs ███████████████████░ 95.4% 9,935 / 10,416
experiments ███████████████████░ 95.7% 25,036 / 26,159
replay_vision ███████████████████░ 96.2% 14,973 / 15,572
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.5% 17,110 / 17,734
user_interviews ███████████████████░ 96.5% 2,638 / 2,734
warehouse_sources ███████████████████░ 97.1% 328,027 / 337,701
customer_analytics ███████████████████░ 97.3% 8,380 / 8,610
data_catalog ███████████████████░ 97.5% 2,368 / 2,429
pulse ████████████████████ 98.4% 2,017 / 2,049

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/tasks/backend/migrations/0072_loop_skill_bundles.py

BEGIN;
--
-- Add field skill_bundles to loop
--
ALTER TABLE "posthog_task_loop" ADD COLUMN "skill_bundles" jsonb DEFAULT '[]'::jsonb NOT NULL;
ALTER TABLE "posthog_task_loop" ALTER COLUMN "skill_bundles" DROP DEFAULT;
COMMIT;

Last updated: 2026-07-24 05:58 UTC (c06f9b4)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 0 Safe | 1 Needs Review | 0 Blocked

⚠️ Needs Review

May have performance impact

tasks.0072_loop_skill_bundles
  └─ #1 ⚠️ AddField
     Adding NOT NULL field with callable default (list) - verify it's stable
     model: loop, field: skill_bundles, default: list

Last updated: 2026-07-24 05:58 UTC (c06f9b4)

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · 🔧 building…

Spinning up PostHog for this PR on a hogland hogbox — this comment updates in place when it's ready (usually a few minutes).

commit c06f9b4 · build log

Comment thread products/tasks/backend/presentation/views/loops.py
Comment thread products/tasks/backend/facade/loops.py Outdated
@posthog-bot-comment-resolver

posthog-bot-comment-resolver Bot commented Jul 23, 2026

Copy link
Copy Markdown

🔀 Tried to auto-resolve conflicts with master but this one needs a human.

I won't retry until the branch or master moves.

Comment thread products/tasks/backend/presentation/views/loops.py
Comment thread products/tasks/backend/facade/loops.py
@github-actions
github-actions Bot requested a deployment to preview-pr-73069 July 24, 2026 04:54 In progress
Comment thread products/tasks/backend/facade/loops.py Outdated
@github-actions
github-actions Bot requested a deployment to preview-pr-73069 July 24, 2026 05:05 In progress
@github-actions
github-actions Bot requested a deployment to preview-pr-73069 July 24, 2026 05:18 In progress
Comment thread products/tasks/backend/facade/loops.py Outdated
@github-actions
github-actions Bot requested a deployment to preview-pr-73069 July 24, 2026 05:22 In progress
Comment thread products/tasks/backend/facade/loops.py Outdated
@github-actions
github-actions Bot requested a deployment to preview-pr-73069 July 24, 2026 05:54 In progress
if task.origin_product != Task.OriginProduct.LOOP or task.loop_id is None:
return True
# No ambient team scope on reconciler/Celery paths, same as Loop._get_before_update.
loop = Loop.objects.unscoped().filter(pk=task.loop_id).first()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Do not seed recovered runs from the current loop manifest

The reconciler loads Loop.skill_bundles at recovery time, rather than the manifest that existed when this run was created. A team member can take ownership after an on-commit dispatch is lost, upload a malicious skill bundle, and wait for this path to start the already-created run. That task retains the previous owner's created_by identity and its sandbox/OAuth/GitHub credentials, so the new owner-controlled bundle executes with the former owner's access.

Prompt To Fix With AI
Snapshot the validated loop skill-bundle manifest (including immutable source storage paths and digest) into TaskRun state when creating the run. Make both the post-commit seeder and ensure_loop_skill_bundles_seeded copy exclusively from that run-scoped snapshot, never from the mutable Loop row. For legacy queued runs without a snapshot, fail closed or only recover when no skills are required. Add a test where ownership and bundles change before orphan reconciliation and verify the recovered run cannot execute the new bundle under the original task creator.

Severity: high | Confidence: 94% | React with 👍 if useful or 👎 if not

if task.origin_product != Task.OriginProduct.LOOP or task.loop_id is None:
return True
# No ambient team scope on reconciler/Celery paths, same as Loop._get_before_update.
loop = Loop.objects.unscoped().filter(pk=task.loop_id).first()

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.

High: Recovery crosses the run's credential boundary

This reloads the current loop after the run and its credential owner were captured. A teammate can take ownership of a team loop, replace its bundles, and have those new instructions execute when an orphaned run is recovered with the previous owner's OAuth/GitHub/MCP credentials. Persist the bundle manifest snapshot on the run at fire time and make recovery seed only from that immutable snapshot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants