Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
## Commands

- Environment:
- This is a full dev environment, not a restricted patch-editing sandbox — it has `node`, `pnpm`, a package mirror, and `apt`, so tools and dependencies that aren't present yet can be installed, and tests, Storybook, and the app can actually be run. A missing `node_modules`, browser binary, or flox usually just means setup hasn't run yet (`pnpm install`, `npx playwright install --with-deps chromium`, or building the nested `@posthog/quill` workspace that `global.scss` imports), rather than that running things is impossible.
- So the absence of a tool isn't evidence that a task can't be done — installing it is the first step. The honest signal that something genuinely can't run is an attempt that fails for a specific, nameable reason (no network access, `apt` unavailable, out of memory), which is worth reporting alongside whatever fallback you take.
- This matters most for visual and UX work, where reading the code isn't the same as seeing the result. Rendering the affected surface (for example in Storybook via a headless browser) and comparing before and after is what actually confirms such a change, and is usually worth the setup cost.
- Use flox when available — prefer `flox activate -- bash -c "<command>"` if commands fail
- Never use `flox activate` in interactive sessions (it hangs if you try)
- Tests:
Expand Down
10 changes: 9 additions & 1 deletion products/tasks/backend/facade/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2728,7 +2728,15 @@ def finalize_task_run_artifact_uploads(
for storage_path in new_storage_paths:
_tag_artifact_object(run, storage_path)

return finalized_entries, None
# Mint a fresh presigned download URL per response entry so the caller (e.g. the
# upload_artifact tool) can surface a link to the file. Presigned URLs expire, so
# they are attached to the response only and never written back to the manifest.
response_entries: list[dict] = []
for entry in finalized_entries:
presigned_url = object_storage.get_presigned_url(entry["storage_path"])
response_entries.append({**entry, "url": presigned_url} if presigned_url else dict(entry))

return response_entries, None


def list_task_run_living_artifacts(run_id: str | UUID, task_id: str | UUID, team_id: int) -> list[dict] | None:
Expand Down
7 changes: 7 additions & 0 deletions products/tasks/backend/presentation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ class TaskRunArtifactResponseSerializer(serializers.Serializer):
)
storage_path = serializers.CharField(help_text="S3 object key for the artifact")
uploaded_at = serializers.CharField(help_text="Timestamp when the artifact was uploaded")
url = serializers.URLField(
required=False,
help_text=(
"Presigned download URL for the artifact. Populated on the finalize-upload response so "
"the caller can link to the file directly; it is time-limited and not persisted on the manifest."
),
)


class TaskRunDetailSerializer(DataclassSerializer):
Expand Down
6 changes: 5 additions & 1 deletion products/tasks/backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6394,7 +6394,11 @@ def test_finalize_artifact_uploads_is_idempotent_for_existing_entry(self, mock_t
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["artifacts"], run.artifacts)
returned_artifacts = response.json()["artifacts"]
# The finalize response augments each entry with a presigned download URL that is not
# persisted on the manifest, so compare the stored fields separately from the URL.
self.assertEqual([{k: v for k, v in a.items() if k != "url"} for a in returned_artifacts], run.artifacts)
self.assertTrue(all(a.get("url") for a in returned_artifacts))
mock_head_object.assert_not_called()
mock_tag.assert_not_called()

Expand Down
2 changes: 2 additions & 0 deletions products/tasks/frontend/generated/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions services/mcp/src/api/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading