Skip to content

feat(tasks): channel feed messages — durable, multiplayer system announcements - #70320

Merged
adamleithp merged 8 commits into
masterfrom
feat/channel-feed-messages
Jul 14, 2026
Merged

feat(tasks): channel feed messages — durable, multiplayer system announcements#70320
adamleithp merged 8 commits into
masterfrom
feat/channel-feed-messages

Conversation

@adamleithp

Copy link
Copy Markdown
Contributor

The first of its kind

A task channel's feed has always been just a task list (GET /tasks/?channel=<uuid>). There has never been a way to put anything into that feed that isn't a task — no lifecycle events, no agent- or system-authored rows, no "X created this channel". This PR introduces the first durable, team-visible, non-task feed entry for channels: ChannelFeedMessage.

What it adds

Model (ChannelFeedMessage) — deliberately generic so it's a foundation, not a one-off:

  • author (FK user) + author_kind (human | system | agent) — the first system/agent-authored rows in a channel feed
  • event (stable key) + payload (JSON) — structured and rename/i18n-safe
  • content — freeform escape hatch for ad-hoc text
  • optional client-supplied created_at so a burst of announcements orders deterministically instead of racing on insert time

EndpointGET/POST /api/projects/{id}/task_channels/{channel_id}/feed/, nested under the channel. Public channels are team-visible; personal (#me) channels are owner-only.

First event, wired end-to-endchannel_created, emitted server-side in resolve_channel the moment a public channel is created. So "Ann created this context" shows up in the feed regardless of which client or integration made the channel — not something a single client has to remember to post. (The desktop app posts a companion context_md_building row when it kicks off the CONTEXT.md planning session.)

Shape / layering

Follows the tasks app's existing layering exactly: model → facade (frozen DTOs) → DRF serializers → viewset → route. Nothing bespoke.

Tests

Full API coverage in test_channels_api.py::ChannelFeedMessageAPITestCase (7 passing): post + list, team visibility, personal-channel isolation, event validation (400), unknown channel (404), and the server-emitted channel_created — including no re-emit when an existing channel is resolved again.

Migration

0056_channelfeedmessage — additive CreateModel + one index; makemigrations --check clean, applied and exercised locally.

Client

Consumed by the Code desktop app (posthog/code) — separate PR — which renders these as "PostHog agent" feed rows alongside task cards.

🤖 Generated with Claude Code

…uncements

Introduces ChannelFeedMessage: the first durable, team-visible "feed message"
model for task channels. Until now a channel's feed was purely a task list
(GET /tasks/?channel=<uuid>) with no way to record anything that isn't a task —
no lifecycle events, no agent/system rows, nothing.

ChannelFeedMessage adds that surface, deliberately generic:
  - author (FK user) + author_kind (human | system | agent) — the first
    system/agent-authored rows in a channel feed, not just human tasks
  - event (stable key) + payload (JSON) — structured + rename/i18n-safe
  - content — freeform escape hatch
  - optional client-supplied created_at so a burst of announcements orders
    deterministically instead of racing on insert time

Endpoint: GET/POST /api/projects/{id}/task_channels/{channel_id}/feed/
(nested under the channel, personal channels owner-only, public team-visible).

First event wired end-to-end: channel_created — emitted server-side in
resolve_channel the moment a public channel is created, so "Ann created this
context" appears in the feed no matter which client or integration created it.
The desktop app (posthog/code) posts a companion context_md_building row when it
launches the CONTEXT.md planning session.

Layered per the tasks app conventions: model -> facade (frozen DTOs) -> DRF
serializers -> viewset -> route. Full API test coverage for post/list, team
visibility, personal-channel isolation, event validation, and the
server-emitted channel_created (including no-reemit on resolve of an existing
channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread products/tasks/backend/facade/api.py
@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(tasks): channel feed messages — dur..." | Re-trigger Greptile

Comment thread products/tasks/backend/presentation/serializers.py Outdated
Comment thread products/tasks/backend/presentation/serializers.py Outdated
@trunk-io

trunk-io Bot commented Jul 12, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@adamleithp

Copy link
Copy Markdown
Contributor Author

Review

Context: First non-task feed row for channels — aligns with the channels architecture plan (code#3328) as a precursor to workstream F (real-time feed). One tension worth tracking: the plan sequences workstream A (unified channel model) first, and this table FKs the task-Channel row that A may replace as the canonical identity. The generic event/payload design keeps that future migration cheap, so this is a reasonable tracer bullet — just remember this table when A settles the canonical row.

Correctness — solid:

  • resolve_channel race handled: only the get_or_create winner emits; IntegrityError loser refetches. No-re-emit is tested. ✅
  • Team scoping: fail-closed TeamScopedRootMixin manager + explicit team_id filters. ✅
  • Personal-channel isolation via _visible_channel, tested; UUID validated → 404 not 500. ✅
  • Migration additive, db_constraint=False on hot-table FKs per convention, index name fits the 30-char limit. ✅

Findings:

  1. Client-supplied created_at is unbounded — any team member can backdate or future-date rows anywhere in the feed's history. The curated event set limits abuse, but it should be clamped to a small window around now (or rejected outside it).
  2. The created_at feature is untested — the PR body highlights deterministic burst ordering, but no test posts explicit timestamps and asserts the resulting order.
  3. No pagination on list — fine for rare lifecycle events; a time bomb if per-task events land here later (the plan suggests they will). Worth a note in code.
  4. content escape hatch is dead — model/DTO carry it but the write serializer doesn't, so it's unreachable via the API. Harmless as a response field; wire it up or drop it when its first consumer appears.
  5. Silent-miss risk for future callers_emit_channel_created swallows all exceptions; TeamScopedManager raises without team context, so a future non-request caller (temporal, MCP) would get a channel with a silently missing announcement. Deserves a comment pointing callers at team_scope().
  6. Missing test: cross-team access (another team's channel id → 404). Personal isolation is covered; the team boundary currently rests on the manager alone.

Style: layering exactly matches the tasks app (model → facade DTO → serializer → viewset → route), no inline logic in views, constraint comments justify themselves. channel_created kept out of the client-postable whitelist is the right call.

Verdict: high quality, nothing blocking. Fixing 1/2/5/6 on the branch now.

Clamp created_at to ±10min of now (it exists for burst ordering, not
backdating), test the ordering it was added for, test the cross-team
boundary, and document the team_scope() requirement for future
non-request emitters plus the unpaginated-list assumption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread products/tasks/backend/facade/api.py Outdated
Comment thread products/tasks/backend/facade/api.py
@veria-ai

veria-ai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR adds durable channel feed messages for task-related system announcements, including publishing announcements when filesystem-backed task or canvas activity occurs. The changes support multiplayer visibility for these task announcements in public channels.

One issue remains open after one prior issue was addressed. The remaining concern is that ordinary write requests can supply a task header that causes an announcement to be recorded as an agent message, allowing a team member to create trusted-looking system activity in a public channel. The impact is limited to message provenance and teammate-facing announcement integrity, but it is directly attacker-controlled within the app’s normal permissions.

Open issues (1)

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

…ompletion (#70371)

Co-authored-by: tests-posthog[bot] <250237707+tests-posthog[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit df73a4c · box box-35615b1e21c8 · ready in 1002s (push → usable) · build log · rebuilds on every push, torn down on close

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

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

Total: 64.43 MiB · no change

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.21 MiB · 22 files no change ███░░░░░░░ 28.2% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.12 MiB · 2,974 files no change █████████░ 87.7% of 9.25 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
668 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
278.6 KiB ../node_modules/.pnpm/posthog-js@1.399.5/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
234.9 KiB src/taxonomy/core-filter-definitions-by-group.json
222.1 KiB ../node_modules/.pnpm/posthog-js@1.399.5/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.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.9 KiB src/lib/api.ts
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

⚠️ Dist folder size — 🔺 +10.6 KiB (+0.0%)

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

Total: 1284.45 MiB · 🔺 +10.6 KiB (+0.0%)

ℹ️ MCP UI apps size — 31 app(s), 16561.5 KB JS

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

App JS CSS
debug 598.2 KB 179.2 KB
action 456.5 KB 179.2 KB
action-list 563.0 KB 179.2 KB
cohort 455.4 KB 179.2 KB
cohort-list 562.0 KB 179.2 KB
email-template 455.3 KB 179.2 KB
error-details 471.1 KB 179.2 KB
error-issue 456.1 KB 179.2 KB
error-issue-list 562.9 KB 179.2 KB
experiment 560.1 KB 179.2 KB
experiment-list 563.8 KB 179.2 KB
experiment-results 561.8 KB 179.2 KB
feature-flag 565.8 KB 179.2 KB
feature-flag-list 569.5 KB 179.2 KB
feature-flag-testing 459.4 KB 179.2 KB
insight-actors 560.8 KB 179.2 KB
invite-email-preview 454.7 KB 179.2 KB
llm-costs 558.1 KB 179.2 KB
session-recording 457.2 KB 179.2 KB
session-summary 462.5 KB 179.2 KB
survey 457.0 KB 179.2 KB
survey-global-stats 560.9 KB 179.2 KB
survey-list 563.7 KB 179.2 KB
survey-stats 560.8 KB 179.2 KB
trace-span 455.8 KB 179.2 KB
trace-span-list 562.9 KB 179.2 KB
workflow 455.8 KB 179.2 KB
workflow-list 562.4 KB 179.2 KB
query-results 743.7 KB 179.2 KB
render-ui 824.0 KB 179.2 KB
visual-review-snapshots 460.3 KB 179.2 KB
⚠️ Playwright — 1 failed

🎭 Playwright report · View test results →

1 failed test:

  • View persons list, navigate to detail, and browse tabs (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!

⚠️ Backend coverage — 93.0% of changed backend lines covered — 32 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 93.0% (482 / 514)

File Patch Uncovered changed lines
products/tasks/backend/temporal/process_task/activities/tests/test_relay_sandbox_events.py 0.0% 560
products/tasks/backend/temporal/process_task/activities/relay_sandbox_events.py 50.0% 334, 406, 412, 421, 428–429, 485, 507, 520, 658
products/tasks/backend/facade/api.py 83.9% 4760–4761, 4882, 5097–5098, 5104, 5110–5112, 5143–5144, 5160, 5184–5185
products/tasks/backend/presentation/views/channels_api.py 88.2% 120, 123–124, 162
products/tasks/backend/presentation/serializers.py 96.2% 1343
products/tasks/backend/models.py 96.3% 931
posthog/api/file_system/file_system.py 97.1% 1229

🤖 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 29334057965 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.6% 8,411 / 21,215
demo ███████████░░░░░░░░░ 55.2% 1,436 / 2,601
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 67.1% 25,151 / 37,465
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
data_modeling ████████████████░░░░ 78.4% 4,696 / 5,987
signals ████████████████░░░░ 78.5% 18,101 / 23,059
cdp ████████████████░░░░ 80.6% 3,105 / 3,851
wizard ████████████████░░░░ 82.5% 772 / 936
cohorts █████████████████░░░ 82.9% 3,048 / 3,675
notebooks █████████████████░░░ 83.8% 6,086 / 7,259
agent_platform █████████████████░░░ 84.1% 3,095 / 3,678
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
engineering_analytics ██████████████████░░ 87.6% 4,417 / 5,040
exports ██████████████████░░ 88.3% 6,857 / 7,763
visual_review ██████████████████░░ 88.5% 5,565 / 6,287
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 88.9% 15,924 / 17,921
mcp_analytics ██████████████████░░ 89.1% 2,485 / 2,790
dashboards ██████████████████░░ 89.1% 5,648 / 6,337
error_tracking ██████████████████░░ 89.5% 9,611 / 10,734
streamlit_apps ██████████████████░░ 90.4% 2,499 / 2,764
slack_app ██████████████████░░ 90.6% 9,460 / 10,444
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.7% 11,476 / 12,646
product_analytics ██████████████████░░ 91.0% 5,450 / 5,989
managed_migrations ██████████████████░░ 91.9% 908 / 988
workflows ██████████████████░░ 92.0% 4,795 / 5,210
mcp_store ██████████████████░░ 92.1% 3,665 / 3,981
data_warehouse ██████████████████░░ 92.1% 17,298 / 18,781
alerts ██████████████████░░ 92.1% 3,389 / 3,678
web_analytics ███████████████████░ 92.7% 13,702 / 14,787
notifications ███████████████████░ 92.7% 1,026 / 1,107
ai_observability ███████████████████░ 92.7% 14,670 / 15,822
surveys ███████████████████░ 92.9% 5,660 / 6,094
posthog_ai ███████████████████░ 93.2% 1,311 / 1,407
tracing ███████████████████░ 93.2% 2,423 / 2,599
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
early_access_features ███████████████████░ 93.8% 848 / 904
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
skills ███████████████████░ 94.4% 2,827 / 2,995
revenue_analytics ███████████████████░ 94.4% 3,586 / 3,797
messaging ███████████████████░ 94.5% 2,530 / 2,677
review_hog ███████████████████░ 94.5% 6,429 / 6,802
growth ███████████████████░ 94.9% 2,393 / 2,522
logs ███████████████████░ 95.3% 9,441 / 9,907
experiments ███████████████████░ 95.6% 24,017 / 25,124
replay_vision ███████████████████░ 95.6% 12,690 / 13,272
feature_flags ███████████████████░ 96.0% 14,600 / 15,203
warehouse_sources ███████████████████░ 96.1% 213,644 / 222,336
annotations ███████████████████░ 96.2% 732 / 761
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
access_control ███████████████████░ 96.8% 849 / 877
data_catalog ███████████████████░ 97.2% 1,642 / 1,689
customer_analytics ███████████████████░ 97.3% 7,396 / 7,600
analytics_platform ████████████████████ 98.2% 2,098 / 2,137
metrics ████████████████████ 98.3% 2,363 / 2,403
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

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.

ClickHouse migration SQL — none

No ClickHouse migrations in the latest push.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Migration SQL Changes

Hey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:

products/tasks/backend/migrations/0057_channelfeedmessage.py

BEGIN;
--
-- Create model ChannelFeedMessage
--
CREATE TABLE "posthog_task_channel_feed_message" ("id" uuid NOT NULL PRIMARY KEY, "author_kind" varchar(16) NOT NULL, "event" varchar(64) NOT NULL, "payload" jsonb NOT NULL, "content" text NOT NULL, "deleted" boolean NOT NULL, "created_at" timestamp with time zone NOT NULL, "author_id" integer NULL, "channel_id" uuid NOT NULL, "team_id" integer NOT NULL);
--
-- Create index task_channel_feed_msg_created on field(s) channel, created_at of model channelfeedmessage
--
CREATE INDEX "task_channel_feed_msg_created" ON "posthog_task_channel_feed_message" ("channel_id", "created_at");
ALTER TABLE "posthog_task_channel_feed_message" ADD CONSTRAINT "posthog_task_channel_channel_id_fb7264ac_fk_posthog_t" FOREIGN KEY ("channel_id") REFERENCES "posthog_task_channel" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "posthog_task_channel_feed_message_author_id_05cf1b63" ON "posthog_task_channel_feed_message" ("author_id");
CREATE INDEX "posthog_task_channel_feed_message_channel_id_fb7264ac" ON "posthog_task_channel_feed_message" ("channel_id");
CREATE INDEX "posthog_task_channel_feed_message_team_id_39f4896d" ON "posthog_task_channel_feed_message" ("team_id");
COMMIT;

products/tasks/backend/migrations/0058_taskthreadmessage_agent_fields.py

BEGIN;
--
-- Add field author_kind to taskthreadmessage
--
ALTER TABLE "posthog_task_thread_message" ADD COLUMN "author_kind" varchar(16) DEFAULT 'human' NOT NULL;
ALTER TABLE "posthog_task_thread_message" ALTER COLUMN "author_kind" DROP DEFAULT;
--
-- Add field event to taskthreadmessage
--
ALTER TABLE "posthog_task_thread_message" ADD COLUMN "event" varchar(64) DEFAULT '' NOT NULL;
ALTER TABLE "posthog_task_thread_message" ALTER COLUMN "event" DROP DEFAULT;
--
-- Add field payload to taskthreadmessage
--
ALTER TABLE "posthog_task_thread_message" ADD COLUMN "payload" jsonb DEFAULT '{}'::jsonb NOT NULL;
ALTER TABLE "posthog_task_thread_message" ALTER COLUMN "payload" DROP DEFAULT;
COMMIT;

Last updated: 2026-07-14 12:56 UTC (df73a4c)

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🔍 Migration Risk Analysis

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 1 Needs Review | 0 Blocked

⚠️ Needs Review

May have performance impact

tasks.0058_taskthreadmessage_agent_fields
  └─ #1 ✅ AddField
     Adding NOT NULL field with constant default (safe in PG11+)
     model: taskthreadmessage, field: author_kind
  └─ #2 ✅ AddField
     Adding NOT NULL field with constant default (safe in PG11+)
     model: taskthreadmessage, field: event
  └─ #3 ⚠️ AddField
     Adding NOT NULL field with callable default (dict) - verify it's stable
     model: taskthreadmessage, field: payload, default: dict

✅ Safe

Brief or no lock, backwards compatible

tasks.0057_channelfeedmessage
  └─ #1 ✅ CreateModel
     Creating new table is safe
     model: ChannelFeedMessage
  │
  └──> ℹ️  INFO:
       ℹ️  Skipped operations on newly created tables (empty tables
       don't cause lock contention).

Last updated: 2026-07-14 12:56 UTC (df73a4c)

Comment thread products/tasks/backend/facade/api.py
by the requesting user — the header can't point the announcement at someone
else's task thread. No header (a human or app save) means no announcement.
"""
raw_task_id = (request.headers.get("X-PostHog-Task-Id") or "").strip()

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.

Medium: Caller-controlled header can forge agent messages

X-PostHog-Task-Id is accepted on ordinary file_system:write requests, but the downstream path stores the announcement with author_kind=AGENT and no author. Any team member can set this header to a public-channel task they created, publish a canvas's first version, and create a trusted-looking agent announcement visible to teammates; matching the user to the task creator does not prove the request came from the sandbox. Require sandbox-specific authenticated provenance for this path, or record requests without it as human-authored.

adamleithp and others added 2 commits July 14, 2026 10:52
- write agent thread messages via for_team so temporal-relay callers pass
  the fail-closed manager (CI TeamScopeError)
- client feed posts are marked human-authored; system/agent kinds reserved
  for server-side writers
- cap feed payloads at 8 KB and channels at 500 feed rows; bound the list
  path to the newest rows
- only announce canvas creation for requests bearing a sandbox-app OAuth
  token, so the X-PostHog-Task-Id header alone can't forge agent messages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…essages

# Conflicts:
#	products/tasks/backend/migrations/max_migration.txt
@github-actions
github-actions Bot requested a deployment to preview-pr-70320 July 14, 2026 10:02 In progress
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +4876 to +4880
if (
ChannelFeedMessage.objects.filter(channel_id=channel_id, team_id=team_id, deleted=False).count()
>= CHANNEL_FEED_MAX_MESSAGES
):
return "full"

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.

Race condition allows exceeding the CHANNEL_FEED_MAX_MESSAGES limit. Between checking the count and creating the message, concurrent requests can create additional messages, causing the channel to exceed the cap.

To fix, use a database transaction with SELECT FOR UPDATE or implement row-level locking:

from django.db import transaction

with transaction.atomic():
    count = ChannelFeedMessage.objects.filter(
        channel_id=channel_id, team_id=team_id, deleted=False
    ).select_for_update().count()
    if count >= CHANNEL_FEED_MAX_MESSAGES:
        return "full"
    message = ChannelFeedMessage.objects.create(**fields)

Alternatively, add a database constraint or use optimistic locking if the soft limit is acceptable.

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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.

not a big deal

@adamleithp
adamleithp enabled auto-merge (squash) July 14, 2026 10:40
@adamleithp
adamleithp merged commit a05027f into master Jul 14, 2026
246 checks passed
@adamleithp
adamleithp deleted the feat/channel-feed-messages branch July 14, 2026 13:13
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-14 13:50 UTC Run
prod-us ✅ Deployed 2026-07-14 14:02 UTC Run
prod-eu ✅ Deployed 2026-07-14 14:03 UTC Run

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