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
7 changes: 6 additions & 1 deletion agent/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Literal, Self
from typing import Any, Literal, Self

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

Expand Down Expand Up @@ -228,6 +228,11 @@ class TaskConfig(BaseModel):
trace: bool = False
# Enriched mid-flight by pipeline.py:
cedar_policies: list[str] = []
# Registry assets (#246) resolved by the orchestrator and threaded in the
# payload. Each entry is ``{kind, namespace, name, version, runtime}``; the
# per-kind loaders (registry.loader) apply them — mcp_server merges into
# ``.mcp.json`` (PR 2); cedar_policy_module / skill land in PR 3.
resolved_assets: list[dict[str, Any]] = Field(default_factory=list)
# Cedar human-in-the-loop approvals. Per-task approval defaults threaded
# from the orchestrator payload; consumed by PolicyEngine at
# construction so the engine seeds ApprovalAllowlist and adopts
Expand Down
31 changes: 31 additions & 0 deletions agent/src/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ def run_task(
trace: bool = False,
user_id: str = "",
attachments: list[dict] | None = None,
resolved_assets: list[dict] | None = None,
) -> dict:
"""Run the full agent pipeline and return a serialized result dict.

Expand Down Expand Up @@ -887,6 +888,11 @@ def run_task(
if cedar_policies:
config.cedar_policies = cedar_policies

# Registry assets (#246) resolved by the orchestrator — applied by the
# per-kind loaders below (mcp_server → .mcp.json in PR 2).
if resolved_assets:
config.resolved_assets = resolved_assets

# Export session-tag values so tenant-data boto3 clients (DDB/S3) assume
# the per-task SessionRole with {user_id, repo, task_id} tags. No-op when
# AGENT_SESSION_ROLE_ARN is unset (local/dev/tests).
Expand Down Expand Up @@ -1147,6 +1153,31 @@ def _on_trace_truncated(max_bytes: int, first_dropped: int) -> None:
# matches Jira's own entry.
strip_linear_mcp_servers(setup.repo_dir)

# Registry assets (#246): merge resolved mcp_server configs into
# .mcp.json alongside the channel MCP entry, before the project scan.
# Fail-closed (#246 Option C): apply_resolved_assets raises
# RegistryAssetLoadError for any condition that would leave a pinned
# asset unloaded (missing repo_dir, empty/invalid runtime, structurally
# invalid config, or a write error) — we let it propagate so the task
# fails rather than running with a pinned-but-absent asset while the
# audit record claims it was loaded.
if config.resolved_assets:
from registry.loader import apply_resolved_assets

loaded_mcp_keys = apply_resolved_assets(setup.repo_dir, config.resolved_assets)
log("TASK", f"Registry: applied {len(loaded_mcp_keys)} mcp_server asset(s)")
# ADR-016 ENFORCEMENT (re-apply after the merge): the registry
# merge writes servers into .mcp.json AFTER the strip above, so a
# registry-published Linear server would otherwise slip back in and
# run under bypassPermissions. Re-strip so the enforcement covers
# registry-sourced entries too, not just repo-committed ones.
if strip_linear_mcp_servers(setup.repo_dir):
log(
"WARN",
"Registry: stripped a Linear MCP server introduced by a resolved "
"asset (ADR-016 — the agent must have no Linear tools)",
)

# Download attachments from S3 (version-pinned, integrity-verified)
prepared_attachments: list = []
if config.attachments:
Expand Down
23 changes: 18 additions & 5 deletions agent/src/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,13 +879,26 @@ def __init__(
if legacy_extra:
soft_text = soft_text + "\n" + "\n".join(legacy_extra)

# 64 KB cap on combined blueprint text (finding #12). Built-ins do
# not count against the cap — they are trusted platform content.
blueprint_text = "".join(filter(None, [blueprint_hard_policies, blueprint_soft_policies]))
if len(blueprint_text.encode("utf-8")) > POLICIES_MAX_BYTES:
# 64 KB cap on combined operator-supplied policy text (finding #12).
# Built-ins do not count — they are trusted platform content. Registry
# cedar_policy_module assets arrive via the legacy ``extra_policies``
# path, so they MUST be counted here too; otherwise a large registry
# policy bypasses the cap entirely (#246 review). Count the raw operator
# text (pre-synthetic-wrapper) so the bound reflects authored bytes.
operator_text = "".join(
filter(
None,
[
blueprint_hard_policies,
blueprint_soft_policies,
*(extra_policies or []),
],
)
)
if len(operator_text.encode("utf-8")) > POLICIES_MAX_BYTES:
raise ValueError(
f"cedar_policies exceeds {POLICIES_MAX_BYTES // 1024} KB cap "
f"({len(blueprint_text.encode('utf-8'))} bytes)"
f"({len(operator_text.encode('utf-8'))} bytes)"
)

# Parse + validate annotations on each tier.
Expand Down
12 changes: 12 additions & 0 deletions agent/src/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ def build_system_prompt(
if channel_addendum:
system_prompt += channel_addendum

# Registry skill assets (#246, PR 3): append resolved prompt fragments. Placed
# after channel guidance so operator-attached skills sit at the recency end.
if config.resolved_assets:
from registry.loader import build_skill_prompt_fragment

system_prompt += build_skill_prompt_fragment(config.resolved_assets)

return system_prompt


Expand Down Expand Up @@ -113,6 +120,11 @@ def build_repoless_system_prompt(
if channel_addendum:
system_prompt += channel_addendum

if config.resolved_assets:
from registry.loader import build_skill_prompt_fragment

system_prompt += build_skill_prompt_fragment(config.resolved_assets)

return system_prompt


Expand Down
Loading
Loading