Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
26 changes: 26 additions & 0 deletions products/tasks/backend/facade/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2877,6 +2877,8 @@ def bootstrap_task_run(
github_user_token = validated_data.get("github_user_token")
initial_permission_mode = validated_data.get("initial_permission_mode")
home_quick_action = validated_data.get("home_quick_action")
imported_mcp_servers = validated_data.get("imported_mcp_servers")
relayed_mcp_servers = validated_data.get("relayed_mcp_servers")
if run_source == RunSource.SIGNAL_REPORT:
pr_authorship_mode = PrAuthorshipMode.BOT

Expand Down Expand Up @@ -2975,6 +2977,17 @@ def bootstrap_task_run(
)
run = task.create_run(environment=environment, mode=mode, branch=branch, extra_state=extra_state)

if imported_mcp_servers or relayed_mcp_servers:
update_fields = ["updated_at"]
if imported_mcp_servers:
# Kept out of `state` (a plain JSONField) because header values carry credentials.
run.imported_mcp_servers = imported_mcp_servers
update_fields.append("imported_mcp_servers")
if relayed_mcp_servers:
run.relayed_mcp_servers = relayed_mcp_servers
update_fields.append("relayed_mcp_servers")
run.save(update_fields=update_fields)

if github_user_token and pr_authorship_mode == PrAuthorshipMode.USER:
cache_github_user_token(str(run.id), github_user_token)

Expand Down Expand Up @@ -4104,6 +4117,8 @@ def run_task(
reasoning_effort = validated_data.get("reasoning_effort")
github_user_token = validated_data.get("github_user_token")
initial_permission_mode = validated_data.get("initial_permission_mode")
imported_mcp_servers = validated_data.get("imported_mcp_servers")
relayed_mcp_servers = validated_data.get("relayed_mcp_servers")
if run_source == RunSource.SIGNAL_REPORT:
pr_authorship_mode = PrAuthorshipMode.BOT

Expand Down Expand Up @@ -4280,6 +4295,17 @@ def run_task(
logger.info("Creating task run for task %s with mode=%s, branch=%s", task.id, mode, branch)
task_run = task.create_run(mode=mode, branch=branch, extra_state=extra_state)

if imported_mcp_servers or relayed_mcp_servers:
update_fields = ["updated_at"]
if imported_mcp_servers:
# Kept out of `state` (a plain JSONField) because header values carry credentials.
task_run.imported_mcp_servers = imported_mcp_servers
update_fields.append("imported_mcp_servers")
if relayed_mcp_servers:
task_run.relayed_mcp_servers = relayed_mcp_servers
update_fields.append("relayed_mcp_servers")
task_run.save(update_fields=update_fields)

if pending_user_artifact_ids:
_attach_staged_artifacts_to_run(
task_run, task, staged_artifacts=staged_artifacts, artifact_ids=pending_user_artifact_ids
Expand Down
11 changes: 10 additions & 1 deletion products/tasks/backend/logic/services/docker_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ def _build_agent_server_command(
reasoning_effort: str | None = None,
initial_permission_mode: str | None = None,
mcp_servers_arg: str = "",
relay_mcp_servers_arg: str = "",
allowed_domains: list[str] | None = None,
event_ingest_token: str | None = None,
event_ingest_url: str | None = None,
Expand Down Expand Up @@ -814,7 +815,8 @@ def _build_agent_server_command(
f"env {unset_flags}BASH_ENV={shlex.quote(BASH_ENV_SCRIPT)} "
f"{env_prefix}./node_modules/.bin/agent-server --port {AGENT_SERVER_PORT}{repo_flag} "
f"--taskId {shlex.quote(task_id)} --runId {shlex.quote(run_id)} --mode {shlex.quote(mode)}"
f"{create_pr_flag}{auto_publish_flag}{branch_flag}{mcp_servers_arg}{domains_flag}{repo_ready_flag}"
f"{create_pr_flag}{auto_publish_flag}{branch_flag}{mcp_servers_arg}{relay_mcp_servers_arg}"
f"{domains_flag}{repo_ready_flag}"
)

# agentsh injects HTTP_PROXY pointing at a per-session egress proxy port; undici
Expand Down Expand Up @@ -863,6 +865,7 @@ def start_agent_server(
reasoning_effort: str | None = None,
initial_permission_mode: str | None = None,
mcp_configs: list[McpServerConfig] | None = None,
relayed_mcp_servers: list[str] | None = None,
allowed_domains: list[str] | None = None,
event_ingest_token: str | None = None,
event_ingest_url: str | None = None,
Expand Down Expand Up @@ -901,6 +904,10 @@ def start_agent_server(
mcp_json = json.dumps([c.to_dict() for c in mcp_configs])
mcp_servers_arg = f" --mcpServers {shlex.quote(mcp_json)}"

relay_mcp_servers_arg = ""
if relayed_mcp_servers:
relay_mcp_servers_arg = f" --relayMcpServers {shlex.quote(json.dumps(relayed_mcp_servers))}"

if auto_publish and not self.agent_server_supports_auto_publish():
logger.warning(f"Installed agent-server in sandbox {self.id} predates --autoPublish; starting review-first")
auto_publish = False
Expand All @@ -920,6 +927,7 @@ def start_agent_server(
reasoning_effort,
initial_permission_mode,
mcp_servers_arg,
relay_mcp_servers_arg,
allowed_domains=allowed_domains,
event_ingest_token=event_ingest_token,
event_ingest_url=event_ingest_url,
Expand Down Expand Up @@ -969,6 +977,7 @@ def start_agent_server(
reasoning_effort=reasoning_effort,
initial_permission_mode=initial_permission_mode,
mcp_servers_arg=mcp_servers_arg,
relay_mcp_servers_arg=relay_mcp_servers_arg,
allowed_domains=allowed_domains,
event_ingest_token=event_ingest_token,
event_ingest_url=event_ingest_url,
Expand Down
10 changes: 9 additions & 1 deletion products/tasks/backend/logic/services/modal_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ def _build_agent_server_command(
reasoning_effort: str | None = None,
initial_permission_mode: str | None = None,
mcp_servers_arg: str = "",
relay_mcp_servers_arg: str = "",
allowed_domains: list[str] | None = None,
event_ingest_token: str | None = None,
event_ingest_url: str | None = None,
Expand Down Expand Up @@ -927,7 +928,8 @@ def _build_agent_server_command(
f"env {unset_flags}BASH_ENV={shlex.quote(BASH_ENV_SCRIPT)} "
f"{env_prefix}./node_modules/.bin/agent-server --port {AGENT_SERVER_PORT}{repo_flag} "
f"--taskId {shlex.quote(task_id)} --runId {shlex.quote(run_id)} --mode {shlex.quote(mode)}"
f"{create_pr_flag}{auto_publish_flag}{branch_flag}{mcp_servers_arg}{domains_flag}{repo_ready_flag}"
f"{create_pr_flag}{auto_publish_flag}{branch_flag}{mcp_servers_arg}{relay_mcp_servers_arg}"
f"{domains_flag}{repo_ready_flag}"
)

if repo_ready_file:
Expand Down Expand Up @@ -1009,6 +1011,7 @@ def start_agent_server(
reasoning_effort: str | None = None,
initial_permission_mode: str | None = None,
mcp_configs: list[McpServerConfig] | None = None,
relayed_mcp_servers: list[str] | None = None,
allowed_domains: list[str] | None = None,
event_ingest_token: str | None = None,
event_ingest_url: str | None = None,
Expand Down Expand Up @@ -1046,6 +1049,10 @@ def start_agent_server(
mcp_json = json.dumps([c.to_dict() for c in mcp_configs])
mcp_servers_arg = f" --mcpServers {shlex.quote(mcp_json)}"

relay_mcp_servers_arg = ""
if relayed_mcp_servers:
relay_mcp_servers_arg = f" --relayMcpServers {shlex.quote(json.dumps(relayed_mcp_servers))}"

if auto_publish and not self.agent_server_supports_auto_publish():
logger.warning(f"Installed agent-server in sandbox {self.id} predates --autoPublish; starting review-first")
auto_publish = False
Expand All @@ -1065,6 +1072,7 @@ def start_agent_server(
reasoning_effort,
initial_permission_mode,
mcp_servers_arg,
relay_mcp_servers_arg,
allowed_domains=allowed_domains,
event_ingest_token=event_ingest_token,
event_ingest_url=event_ingest_url,
Expand Down
1 change: 1 addition & 0 deletions products/tasks/backend/logic/services/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ def start_agent_server(
reasoning_effort: str | None = None,
initial_permission_mode: str | None = None,
mcp_configs: list[McpServerConfig] | None = None,
relayed_mcp_servers: list[str] | None = None,
allowed_domains: list[str] | None = None,
event_ingest_token: str | None = None,
event_ingest_url: str | None = None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import migrations

import posthog.helpers.encrypted_fields


class Migration(migrations.Migration):
dependencies = [
("tasks", "0058_taskthreadmessage_agent_fields"),
]

operations = [
migrations.AddField(
model_name="taskrun",
name="imported_mcp_servers",
field=posthog.helpers.encrypted_fields.EncryptedJSONStringField(
blank=True,
default=None,
help_text="Client-imported MCP server configs (type/name/url/headers) to make available in the sandbox",
null=True,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("tasks", "0059_taskrun_imported_mcp_servers"),
]

operations = [
migrations.AddField(
model_name="taskrun",
name="relayed_mcp_servers",
field=models.JSONField(
blank=True,
default=None,
help_text="Names of desktop-only MCP servers the creating client relays into this run (docs/cloud-mcp-relay.md). Names only — configuration never crosses the wire.",
null=True,
),
),
]
2 changes: 1 addition & 1 deletion products/tasks/backend/migrations/max_migration.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0058_taskthreadmessage_agent_fields
0060_taskrun_relayed_mcp_servers
17 changes: 17 additions & 0 deletions products/tasks/backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,23 @@ class Environment(models.TextChoices):
help_text="Run state data for resuming or tracking execution state",
)

# Local url-based MCP servers imported from the creating client (PostHog Code),
# merged into the sandbox agent server's --mcpServers at spawn. Encrypted because
# header values carry credentials; never exposed through API responses.
imported_mcp_servers = EncryptedJSONStringField(
blank=True,
null=True,
default=None,
help_text="Client-imported MCP server configs (type/name/url/headers) to make available in the sandbox",
)

relayed_mcp_servers = models.JSONField(
blank=True,
null=True,
default=None,
help_text="Names of desktop-only MCP servers the creating client relays into this run (docs/cloud-mcp-relay.md). Names only — configuration never crosses the wire.",
)

created_at = models.DateTimeField(default=django_timezone.now)
updated_at = models.DateTimeField(auto_now=True)
completed_at = models.DateTimeField(null=True, blank=True)
Expand Down
Loading
Loading