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
10 changes: 10 additions & 0 deletions products/tasks/backend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ def vm_sandbox_allowed_origins(*, distinct_id: str, organization_id: str) -> set
}
)

# Stripped from the agent-server's process environment at launch (env -u).
# Two categories:
# - code-injection vectors a resume snapshot could smuggle in (NODE_*, LD_*, DYLD_*);
# - the GitHub token, so the agent-server holds no frozen copy of the acting user's
# credentials. The token is delivered per command via the live /tmp/agent-env file
# (re-sourced by BASH_ENV, seeded before this unset), so git/gh still authenticate;
# removing the static process-env copy is what lets a mid-session logout or rebind
# actually take effect instead of being resurrected from os.environ.
SANDBOX_AGENT_LAUNCH_UNSET_ENV_VARS: tuple[str, ...] = (
"NODE_OPTIONS",
"NODE_REPL_EXTERNAL_MODULE",
Expand All @@ -298,6 +306,8 @@ def vm_sandbox_allowed_origins(*, distinct_id: str, organization_id: str) -> set
"LD_AUDIT",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"GITHUB_TOKEN",
"GH_TOKEN",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
PrAuthorshipMode,
get_github_token,
get_pr_authorship_mode,
get_sandbox_github_identity_user,
get_sandbox_github_token,
get_task_run_credential_user,
is_caller_token_run,
is_slack_interaction_state,
resolve_user_github_integration_for_task,
sandbox_identity_scope,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -159,7 +161,14 @@ def _live_sandboxes_for_user_integration(user_integration_id: int) -> list[tuple
task__github_user_integration_id=user_integration_id,
)
.select_related("task")
.only("id", "state", "task__repository", "task__github_user_integration_id", "task__origin_product")
.only(
"id",
"state",
"task__repository",
"task__github_user_integration_id",
"task__origin_product",
"task__created_by_id",
)
)
for run in runs:
sandbox_id = (run.state or {}).get("sandbox_id")
Expand All @@ -172,6 +181,13 @@ def _live_sandboxes_for_user_integration(user_integration_id: int) -> list[tuple
continue
if is_caller_token_run(str(run.id), run.state):
continue
# A per-message actor transition may have rebound (or logged out) this sandbox's GitHub
# identity to someone other than the run owner. This loop carries the owner's token, so
# re-applying it would undo that transition and resurrect the owner's identity for the
# current actor. Skip when the sandbox is bound to a different actor.
bound_actor = get_sandbox_github_identity_user(sandbox_identity_scope(str(run.id), run.state))
if bound_actor is not None and bound_actor != run.task.created_by_id:
continue
rows.append((str(run.id), sandbox_id, run.task.repository))
return rows

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,43 @@ def _task(repo):
result = _live_sandboxes_for_user_integration(integration.id)

assert result == [(str(live_run.id), "sb-live", "org/live")]

@pytest.mark.parametrize("marker,included", [("none", True), ("owner", True), ("other", False)])
def test_actor_transition_gates_owner_token_propagation(self, marker, included):
from posthog.models import Organization, Team
from posthog.models.user import User
from posthog.models.user_integration import UserIntegration

from products.tasks.backend.models import Task, TaskRun
from products.tasks.backend.temporal.process_task.sandbox_credentials import (
_live_sandboxes_for_user_integration,
)
from products.tasks.backend.temporal.process_task.utils import mark_sandbox_github_identity

org = Organization.objects.create(name="o")
team = Team.objects.create(organization=org, name="t")
owner = User.objects.create(email="owner@test.com")
other = User.objects.create(email="other@test.com")
integration = UserIntegration.objects.create(
user=owner, kind=UserIntegration.IntegrationKind.GITHUB, integration_id="i1", config={}, sensitive_config={}
)
task = Task.objects.create(
team=team, created_by=owner, repository="org/repo", github_user_integration=integration
)
run = TaskRun.objects.create(
task=task,
team=team,
status=TaskRun.Status.IN_PROGRESS,
state={"sandbox_id": "sb-x", "pr_authorship_mode": "user"},
)
# An unset marker (no transition yet) and one bound to the owner both propagate; a marker
# bound to a different per-message actor means the sandbox was logged out / rebound, so the
# owner's rotating token must not overwrite it.
if marker == "owner":
mark_sandbox_github_identity("sb-x", owner.id)
elif marker == "other":
mark_sandbox_github_identity("sb-x", other.id)

result = _live_sandboxes_for_user_integration(integration.id)

assert (result == [(str(run.id), "sb-x", "org/repo")]) is included
Loading