Skip to content

Commit 36e118f

Browse files
authored
Merge branch 'main' into feat/96-linear-prefix-routing
2 parents 36fe8e5 + 8c0f5e3 commit 36e118f

132 files changed

Lines changed: 10050 additions & 703 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ jobs:
153153
github-token: ${{ github.token }}
154154

155155
- name: Configure AWS credentials
156-
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
156+
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
157157
with:
158158
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
159159
aws-region: ${{ vars.AWS_REGION }}
@@ -228,7 +228,7 @@ jobs:
228228
github-token: ${{ github.token }}
229229

230230
- name: Configure AWS credentials
231-
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
231+
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
232232
with:
233233
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
234234
aws-region: ${{ vars.AWS_REGION }}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ A documentation site is available containing all design documents, roadmap and g
146146

147147
The example provided in this repository is for experimental and educational purposes only. It demonstrates concepts and techniques but is not intended for direct use in production environments.
148148

149+
## Operational Metrics Collection
150+
151+
Autonomous Background Coding Agent samples may collect anonymous operational metrics, including: the region a construct is deployed, the name and version of the construct deployed, and related information. We may use the metrics to maintain, provide, develop, and improve the constructs and AWS services.
152+
149153
## License
150154

151155
This library is licensed under the MIT-0 License. See the [LICENSE](./LICENSE) file.

agent/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ COPY agent/policies/ /app/policies/
7474
# by ``cdk/src/handlers/shared/types.ts`` at synth time. See
7575
# ``contracts/README.md`` for the contract.
7676
COPY contracts/ /app/contracts/
77+
# First-party workflow files (#248). ``agent/src/workflow/loader.py`` resolves
78+
# ``_WORKFLOWS_ROOT`` to ``/app/workflows`` (parents[2] of /app/src/workflow/)
79+
# and loads ``<domain>/<name>-vN.yaml`` plus ``schema/workflow.schema.json`` at
80+
# task time; without these files every workflow load fails with
81+
# ``WorkflowValidationError: workflow '...' not found at /app/workflows/...``.
82+
COPY agent/workflows/ /app/workflows/
7783
COPY agent/prepare-commit-msg.sh /app/
7884
COPY agent/test_sdk_smoke.py agent/test_subprocess_threading.py /app/
7985

agent/policies/hard_deny.cedar

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,30 @@
1212
@rule_id("base_permit")
1313
permit (principal, action, resource);
1414

15-
// pr_review tasks may never invoke Write. Absolute; cannot be overridden
16-
// by per-blueprint customization or --pre-approve.
15+
// Read-only workflows may never invoke Write. Absolute; cannot be overridden
16+
// by per-blueprint customization or --pre-approve. Keyed on the read_only
17+
// context attribute (not a principal literal) so the deny attaches to the
18+
// *property* and fires for every read-only workflow uniformly — not just
19+
// coding/pr-review. (#248 Phase 2a — replaces the literal
20+
// Agent::TaskAgent::"pr_review" match; see ADR-014 addendum 2026-06-08.)
1721
@tier("hard")
18-
@rule_id("pr_review_forbid_write")
22+
@rule_id("read_only_forbid_write")
1923
forbid (
20-
principal == Agent::TaskAgent::"pr_review",
24+
principal,
2125
action == Agent::Action::"invoke_tool",
2226
resource == Agent::Tool::"Write"
23-
);
27+
)
28+
when { context.read_only == true };
2429

25-
// pr_review tasks may never invoke Edit.
30+
// Read-only workflows may never invoke Edit.
2631
@tier("hard")
27-
@rule_id("pr_review_forbid_edit")
32+
@rule_id("read_only_forbid_edit")
2833
forbid (
29-
principal == Agent::TaskAgent::"pr_review",
34+
principal,
3035
action == Agent::Action::"invoke_tool",
3136
resource == Agent::Tool::"Edit"
32-
);
37+
)
38+
when { context.read_only == true };
3339

3440
// Reject `rm -rf /` and similar absolute-root destructive commands.
3541
@tier("hard")

agent/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ dependencies = [
3434
# commit. See docs/design/CEDAR_HITL_GATES.md §15.6 (decision #23) and
3535
# the parity-contract banner in mise.toml.
3636
"cedarpy==4.8.4", #https://github.com/k9securityio/cedar-py — EXACT pin (no ^/~), parity with @cedar-policy/cedar-wasm@4.8.2 (both Cedar Rust 4.8.2)
37+
# Workflow-driven tasks (#248): the step runner loads YAML workflow files
38+
# and validates them against agent/workflows/schema/workflow.schema.json.
39+
# Both were previously only transitively present; declared directly so the
40+
# workflow loader does not depend on another package's transitive pin.
41+
"pyyaml==6.0.3", #https://pypi.org/project/PyYAML/
42+
"jsonschema==4.26.0", #https://pypi.org/project/jsonschema/
3743
]
3844

3945
[tool.uv]

agent/src/config.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@
55
import uuid
66
from datetime import UTC
77

8-
from models import AttachmentConfig, TaskConfig, TaskType
8+
from models import AttachmentConfig, TaskConfig
99
from shell import log
1010

1111
AGENT_WORKSPACE = os.environ.get("AGENT_WORKSPACE", "/workspace")
1212

13-
# Task types that operate on an existing pull request.
14-
PR_TASK_TYPES = frozenset(("pr_iteration", "pr_review"))
13+
# The platform default workflow id used when a payload omits resolved_workflow
14+
# (local/batch runs). Mirrors the create-task boundary's coding default.
15+
DEFAULT_WORKFLOW_ID = "coding/new-task-v1"
16+
# The repo-less platform default workflow (#248 Phase 3) — the one first-party
17+
# id whose ``requires_repo`` is false. Used by the load-failure fallback to
18+
# decide repo-optionality without loading the file.
19+
REPO_LESS_DEFAULT_WORKFLOW_ID = "default/agent-v1"
20+
# First-party workflow ids that operate on an existing pull request.
21+
PR_WORKFLOW_IDS = frozenset(("coding/pr-iteration-v1", "coding/pr-review-v1"))
22+
# First-party workflow ids that are writeable (NOT read-only). Used only by the
23+
# load-failure fallback to bias an unrecognised id toward read-only (fail closed
24+
# on the write-deny invariant). pr-review-v1 is intentionally excluded (it is
25+
# read-only); default/agent-v1 is excluded because its conservative posture
26+
# should fail closed too.
27+
_KNOWN_WRITEABLE_WORKFLOW_IDS = frozenset(("coding/new-task-v1", "coding/pr-iteration-v1"))
1528

1629

1730
def resolve_github_token() -> str:
@@ -314,7 +327,7 @@ def _refresh(current: dict) -> dict | None:
314327

315328

316329
def build_config(
317-
repo_url: str,
330+
repo_url: str = "",
318331
task_description: str = "",
319332
issue_number: str = "",
320333
github_token: str = "",
@@ -325,7 +338,7 @@ def build_config(
325338
dry_run: bool = False,
326339
task_id: str = "",
327340
system_prompt_overrides: str = "",
328-
task_type: str = "new_task",
341+
resolved_workflow: dict | None = None,
329342
branch_name: str = "",
330343
pr_number: str = "",
331344
channel_source: str = "",
@@ -351,22 +364,59 @@ def build_config(
351364
"ANTHROPIC_MODEL", "us.anthropic.claude-sonnet-4-6"
352365
)
353366

367+
# Resolve the workflow id (the create-task boundary already pinned it; local
368+
# batch runs default to the coding workflow). Required-input validation is
369+
# owned by the create-task boundary now; the agent re-checks only the
370+
# pr_number/issue/description shape needed to run.
371+
workflow = resolved_workflow or {"id": DEFAULT_WORKFLOW_ID, "version": "1.0.0"}
372+
workflow_id = workflow.get("id", DEFAULT_WORKFLOW_ID)
373+
is_pr_workflow = workflow_id in PR_WORKFLOW_IDS
374+
375+
# Load the workflow up-front: it drives the Cedar principal, the read_only
376+
# flag, AND whether a repo is required (#248 Phase 3). Fall back to id-based
377+
# mapping when the file can't be loaded (e.g. a registry-only id in a future
378+
# phase) — a repo-less default is the safe assumption only for non-coding.
379+
from workflow import WorkflowValidationError, load_workflow, policy_principal_for
380+
381+
try:
382+
workflow_obj = load_workflow(workflow_id)
383+
policy_principal = policy_principal_for(workflow_obj)
384+
workflow_read_only = workflow_obj.read_only
385+
workflow_requires_repo = workflow_obj.resolved_requires_repo
386+
workflow_allowed_tools = list(workflow_obj.agent_config.allowed_tools)
387+
except WorkflowValidationError as exc:
388+
# The pinned workflow file failed to load (corrupt YAML, schema drift, a
389+
# future registry-only id). This is the one place read_only/requires_repo
390+
# can be wrong without a loud failure, so: (1) log it, and (2) fail
391+
# *closed* — assume read-only (deny writes) for any id we don't recognise
392+
# as a known writeable coding workflow, rather than fail-open to writeable.
393+
log("ERROR", f"workflow {workflow_id!r} failed to load ({exc}); using fallback policy")
394+
policy_principal = "pr_review" if workflow_id == "coding/pr-review-v1" else "new_task"
395+
# Known writeable coding workflows are the only ids that fall back to
396+
# writeable; everything else (incl. an unrecognised id) is read-only.
397+
workflow_read_only = workflow_id not in _KNOWN_WRITEABLE_WORKFLOW_IDS
398+
# requires_repo: the repo-less platform default is the only id that does
399+
# NOT require a repo; every other id (coding or unknown) requires one.
400+
workflow_requires_repo = workflow_id != REPO_LESS_DEFAULT_WORKFLOW_ID
401+
# Tool surface is unknown without the file; empty = the runner falls back
402+
# to its built-in full surface. read_only (above, fail-closed) still drops
403+
# Write/Edit, so the write-deny invariant holds even on this path.
404+
workflow_allowed_tools = []
405+
354406
errors = []
355-
if not resolved_repo_url:
356-
errors.append("repo_url is required (e.g., 'owner/repo')")
357-
if not resolved_github_token:
358-
errors.append("github_token is required")
407+
# Repo + GitHub token are required only for repo-bound workflows; a repo-less
408+
# workflow (requires_repo:false) runs from task_description/attachments alone.
409+
if workflow_requires_repo:
410+
if not resolved_repo_url:
411+
errors.append("repo_url is required (e.g., 'owner/repo')")
412+
if not resolved_github_token:
413+
errors.append("github_token is required")
359414
if not resolved_aws_region:
360415
errors.append("aws_region is required for Bedrock")
361-
try:
362-
task = TaskType(task_type)
363-
except ValueError:
364-
errors.append(f"Invalid task_type: '{task_type}'")
365-
task = None
366-
if task and task.is_pr_task:
416+
if is_pr_workflow:
367417
if not pr_number:
368-
errors.append("pr_number is required for pr_iteration/pr_review task type")
369-
elif task and not resolved_issue_number and not resolved_task_description:
418+
errors.append(f"pr_number is required for the {workflow_id!r} workflow")
419+
elif not resolved_issue_number and not resolved_task_description:
370420
errors.append("Either issue_number or task_description is required")
371421

372422
if errors:
@@ -394,7 +444,12 @@ def build_config(
394444
max_turns=max_turns,
395445
max_budget_usd=max_budget_usd,
396446
system_prompt_overrides=system_prompt_overrides,
397-
task_type=task_type,
447+
resolved_workflow=workflow,
448+
policy_principal=policy_principal,
449+
read_only=workflow_read_only,
450+
allowed_tools=workflow_allowed_tools,
451+
requires_repo=workflow_requires_repo,
452+
is_pr_workflow=is_pr_workflow,
398453
branch_name=branch_name,
399454
pr_number=pr_number,
400455
task_id=task_id or uuid.uuid4().hex[:12],

agent/src/entrypoint.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from config import ( # noqa: F401
1616
AGENT_WORKSPACE,
17-
PR_TASK_TYPES,
17+
PR_WORKFLOW_IDS,
1818
build_config,
1919
get_config,
2020
resolve_github_token,
@@ -29,7 +29,6 @@
2929
RepoSetup,
3030
TaskConfig,
3131
TaskResult,
32-
TaskType,
3332
TokenUsage,
3433
)
3534
from pipeline import main, run_task # noqa: F401

agent/src/memory.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ def _validate_repo(repo: str) -> None:
5353
)
5454

5555

56+
def _validate_actor(actor: str) -> None:
57+
"""Validate a memory actorId: an ``owner/repo`` OR a ``user:{id}`` namespace.
58+
59+
Repo-less knowledge tasks (#248 Phase 3) key memory on ``user:{user_id}``
60+
instead of a repo (ADR-014 addendum). Accept both forms so the same write
61+
path serves coding and knowledge tasks; reject anything else.
62+
"""
63+
if actor.startswith("user:"):
64+
if not actor[len("user:") :]:
65+
raise ValueError("actor 'user:' namespace requires a non-empty user id")
66+
return
67+
_validate_repo(actor)
68+
69+
5670
def _log_error(func_name: str, err: Exception, memory_id: str, task_id: str) -> None:
5771
"""Log memory write failure with severity based on exception type."""
5872
is_programming_error = isinstance(err, (TypeError, ValueError, AttributeError, KeyError))
@@ -67,7 +81,7 @@ def _log_error(func_name: str, err: Exception, memory_id: str, task_id: str) ->
6781

6882
def write_task_episode(
6983
memory_id: str,
70-
repo: str,
84+
actor: str,
7185
task_id: str,
7286
status: str,
7387
pr_url: str | None = None,
@@ -81,17 +95,18 @@ def write_task_episode(
8195
status, PR URL, cost, duration, and any self-feedback from the
8296
agent's "## Agent notes" section in the PR body.
8397
84-
Uses actorId=repo and sessionId=task_id so the extraction strategy
85-
namespace templates (/{actorId}/episodes/{sessionId}/) place records
86-
into the correct per-repo, per-task namespace.
98+
Uses actorId=``actor`` and sessionId=task_id so the extraction strategy
99+
namespace templates (/{actorId}/episodes/{sessionId}/) place records into
100+
the correct namespace. ``actor`` is an ``owner/repo`` for coding tasks or a
101+
``user:{user_id}`` namespace for repo-less knowledge tasks (#248 Phase 3).
87102
88103
Metadata includes source_type='agent_episode' for provenance tracking
89104
and content_sha256 for integrity auditing on read (schema v3).
90105
91106
Returns True on success, False on failure (fail-open).
92107
"""
93108
try:
94-
_validate_repo(repo)
109+
_validate_actor(actor)
95110
client = _get_client()
96111

97112
parts = [
@@ -124,7 +139,7 @@ def write_task_episode(
124139

125140
client.create_event(
126141
memoryId=memory_id,
127-
actorId=repo,
142+
actorId=actor,
128143
sessionId=task_id,
129144
eventTimestamp=_iso_now(),
130145
payload=[

0 commit comments

Comments
 (0)