Skip to content

Commit decab41

Browse files
committed
chore(docs): update roadmap and design
1 parent 93ea9c6 commit decab41

10 files changed

Lines changed: 143 additions & 70 deletions

File tree

cdk/src/handlers/shared/context-hydration.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -793,12 +793,12 @@ export interface HydrateContextOptions {
793793

794794
/**
795795
* Hydrate context for a task: resolve GitHub token, fetch issue/PR, enforce
796-
* token budget, assemble the user prompt, and (for PR tasks) screen through
797-
* Bedrock Guardrail for prompt injection.
796+
* token budget, assemble the user prompt, and screen through Bedrock Guardrail
797+
* for prompt injection (PR tasks; new_task when issue content is present).
798798
* @param task - the task record from DynamoDB.
799799
* @param options - optional per-repo overrides.
800-
* @returns the hydrated context. For PR tasks, `guardrail_blocked` is set when
801-
* the guardrail intervened.
800+
* @returns the hydrated context. `guardrail_blocked` is set when the guardrail
801+
* intervened (PR tasks: always screened; new_task: screened when issue content is present).
802802
* @throws GuardrailScreeningError when the Bedrock Guardrail API call fails
803803
* (fail-closed — propagated to prevent unscreened content from reaching the agent).
804804
*/
@@ -990,13 +990,19 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
990990
return prContext;
991991
}
992992

993-
// Standard task: existing behavior
993+
// Standard task
994994
const budgetResult = enforceTokenBudget(issue, task.task_description, USER_PROMPT_TOKEN_BUDGET);
995995
issue = budgetResult.issue;
996996

997997
userPrompt = assembleUserPrompt(task.task_id, task.repo, issue, budgetResult.taskDescription);
998998
const tokenEstimate = estimateTokens(userPrompt);
999999

1000+
// Screen assembled prompt when it includes GitHub issue content (attacker-controlled input).
1001+
// Skipped when no issue is present — task_description is already screened at submission time.
1002+
const guardrailAction = issue
1003+
? await screenWithGuardrail(userPrompt, task.task_id)
1004+
: undefined;
1005+
10001006
return {
10011007
version: 1,
10021008
user_prompt: userPrompt,
@@ -1005,6 +1011,9 @@ export async function hydrateContext(task: TaskRecord, options?: HydrateContextO
10051011
sources,
10061012
token_estimate: tokenEstimate,
10071013
truncated: budgetResult.truncated,
1014+
...(guardrailAction === 'GUARDRAIL_INTERVENED' && {
1015+
guardrail_blocked: 'Task context blocked by content policy',
1016+
}),
10081017
};
10091018
} catch (err) {
10101019
// Guardrail failures must propagate (fail-closed) — unscreened content must not reach the agent

cdk/test/handlers/shared/context-hydration.test.ts

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ describe('hydrateContext', () => {
546546
ok: true,
547547
json: async () => ({ number: 42, title: 'Bug', body: 'Details', comments: 0 }),
548548
});
549+
// Guardrail screens assembled prompt when issue content is present
550+
mockBedrockSend.mockResolvedValueOnce({ action: 'NONE' });
549551

550552
const task = { ...baseTask, issue_number: 42, task_description: 'Fix it' };
551553
const result = await hydrateContext(task as any);
@@ -571,6 +573,9 @@ describe('hydrateContext', () => {
571573
expect(result.sources).toContain('task_description');
572574
expect(result.issue).toBeUndefined();
573575
expect(result.user_prompt).toContain('Fix it');
576+
// No issue content fetched — guardrail should not be called (task_description already screened)
577+
expect(result.guardrail_blocked).toBeUndefined();
578+
expect(mockBedrockSend).not.toHaveBeenCalled();
574579
});
575580

576581
test('no issue number — assembles from task description only', async () => {
@@ -628,6 +633,8 @@ describe('hydrateContext', () => {
628633
ok: true,
629634
json: async () => ({ number: 10, title: 'Test', body: 'body', comments: 0 }),
630635
});
636+
// Guardrail screens assembled prompt when issue content is present
637+
mockBedrockSend.mockResolvedValueOnce({ action: 'NONE' });
631638

632639
const task = { ...baseTask, issue_number: 10, task_description: 'Fix' };
633640
const result = await hydrateContext(task as any, { githubTokenSecretArn: perRepoArn });
@@ -1027,7 +1034,7 @@ describe('screenWithGuardrail', () => {
10271034
});
10281035

10291036
// ---------------------------------------------------------------------------
1030-
// hydrateContext — guardrail screening for PR tasks
1037+
// hydrateContext — guardrail screening
10311038
// ---------------------------------------------------------------------------
10321039

10331040
describe('hydrateContext — guardrail screening', () => {
@@ -1113,29 +1120,70 @@ describe('hydrateContext — guardrail screening', () => {
11131120
expect(mockBedrockSend).toHaveBeenCalledTimes(1);
11141121
});
11151122

1116-
test('does not invoke guardrail for new_task type', async () => {
1123+
// --- new_task guardrail screening ---
1124+
1125+
const baseNewTask = {
1126+
task_id: 'TASK-NEW-001',
1127+
user_id: 'user-123',
1128+
status: 'SUBMITTED',
1129+
repo: 'org/repo',
1130+
branch_name: 'bgagent/TASK-NEW-001/fix',
1131+
channel_source: 'api',
1132+
status_created_at: 'SUBMITTED#2024-01-01T00:00:00Z',
1133+
created_at: '2024-01-01T00:00:00Z',
1134+
updated_at: '2024-01-01T00:00:00Z',
1135+
task_type: 'new_task',
1136+
task_description: 'Fix it',
1137+
};
1138+
1139+
function mockIssueFetch(): void {
11171140
mockSmSend.mockResolvedValueOnce({ SecretString: 'ghp_test' });
11181141
mockFetch.mockResolvedValueOnce({
11191142
ok: true,
11201143
json: async () => ({ number: 42, title: 'Bug', body: 'Details', comments: 0 }),
11211144
});
1145+
}
11221146

1123-
const newTask = {
1124-
task_id: 'TASK-NEW-001',
1125-
user_id: 'user-123',
1126-
status: 'SUBMITTED',
1127-
repo: 'org/repo',
1128-
branch_name: 'bgagent/TASK-NEW-001/fix',
1129-
channel_source: 'api',
1130-
status_created_at: 'SUBMITTED#2024-01-01T00:00:00Z',
1131-
created_at: '2024-01-01T00:00:00Z',
1132-
updated_at: '2024-01-01T00:00:00Z',
1133-
task_type: 'new_task',
1134-
issue_number: 42,
1135-
task_description: 'Fix it',
1136-
};
1137-
const result = await hydrateContext(newTask as any);
1147+
test('invokes guardrail for new_task with issue content', async () => {
1148+
mockIssueFetch();
1149+
mockBedrockSend.mockResolvedValueOnce({ action: 'NONE' });
1150+
1151+
const result = await hydrateContext({ ...baseNewTask, issue_number: 42 } as any);
1152+
expect(result.guardrail_blocked).toBeUndefined();
1153+
expect(mockBedrockSend).toHaveBeenCalledTimes(1);
1154+
});
1155+
1156+
test('does not invoke guardrail for new_task without issue_number', async () => {
1157+
const result = await hydrateContext(baseNewTask as any);
11381158
expect(result.guardrail_blocked).toBeUndefined();
11391159
expect(mockBedrockSend).not.toHaveBeenCalled();
11401160
});
1161+
1162+
test('returns guardrail_blocked when new_task issue context is blocked', async () => {
1163+
mockIssueFetch();
1164+
mockBedrockSend.mockResolvedValueOnce({ action: 'GUARDRAIL_INTERVENED' });
1165+
1166+
const result = await hydrateContext({ ...baseNewTask, issue_number: 42 } as any);
1167+
expect(result.guardrail_blocked).toBe('Task context blocked by content policy');
1168+
expect(mockBedrockSend).toHaveBeenCalledTimes(1);
1169+
});
1170+
1171+
test('proceeds normally when new_task issue context passes guardrail', async () => {
1172+
mockIssueFetch();
1173+
mockBedrockSend.mockResolvedValueOnce({ action: 'NONE' });
1174+
1175+
const result = await hydrateContext({ ...baseNewTask, issue_number: 42 } as any);
1176+
expect(result.guardrail_blocked).toBeUndefined();
1177+
expect(result.issue).toBeDefined();
1178+
expect(result.sources).toContain('issue');
1179+
});
1180+
1181+
test('throws when guardrail screening fails for new_task (fail-closed)', async () => {
1182+
mockIssueFetch();
1183+
mockBedrockSend.mockRejectedValueOnce(new Error('Bedrock timeout'));
1184+
1185+
await expect(
1186+
hydrateContext({ ...baseNewTask, issue_number: 42 } as any),
1187+
).rejects.toThrow('Guardrail screening unavailable: Bedrock timeout');
1188+
});
11411189
});

docs/design/OBSERVABILITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ Both are one-time, account-level setup steps — not managed by CDK.
129129
- **Guardrail screening events**`guardrail_blocked` (content blocked by Bedrock Guardrail during hydration, with metadata: `reason`, `task_type`, `pr_number`, `sources`, `token_estimate`). Screening failures are logged with structured `metric_type` fields (not emitted as task events).
130130
- Time in each state (e.g. time in HYDRATING, time RUNNING, cold start to first agent activity).
131131
- Correlation with a task id and user id so users and operators can filter by task or user.
132+
- **Planned (Iteration 5, Phase 1): `PolicyDecisionEvent`** — A unified event schema for all policy decisions across the task lifecycle: admission control, budget/quota resolution, guardrail screening, tool-call interception, and finalization. Each event carries: decision ID, policy name, version, phase, input hash, result (`allow` | `deny` | `modify`), reason codes, and enforcement mode (`enforced` | `observed` | `steered`). This normalizes the current mix of structured events (e.g. `admission_rejected`, `guardrail_blocked`) and silent HTTP errors into a single auditable event type. See [ROADMAP.md Iteration 5](../guides/ROADMAP.md) (Centralized policy framework) and [SECURITY.md](./SECURITY.md) (Policy enforcement and audit).
132133

133134
### Agent execution
134135

docs/design/ORCHESTRATOR.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ See the Admission control section for details. Validates that the task is allowe
179179

180180
#### Step 2: Context hydration (deterministic)
181181

182-
See the Context hydration section for details. Assembles the agent's prompt from multiple sources depending on task type. For `new_task`: user message, GitHub issue (title, body, comments), memory, repo configuration, and platform defaults. For `pr_iteration`: PR metadata, review comments, diff summary, and optional user instructions. An additional **pre-flight** sub-step verifies PR accessibility when `pr_number` is set (see [preflight.ts](../../cdk/src/handlers/shared/preflight.ts)). For PR tasks, the assembled prompt is screened through Amazon Bedrock Guardrails for prompt injection before the agent receives it. The output is a fully assembled prompt, ready to pass to the compute session.
182+
See the Context hydration section for details. Assembles the agent's prompt from multiple sources depending on task type. For `new_task`: user message, GitHub issue (title, body, comments), memory, repo configuration, and platform defaults. For `pr_iteration`: PR metadata, review comments, diff summary, and optional user instructions. An additional **pre-flight** sub-step verifies PR accessibility when `pr_number` is set (see [preflight.ts](../../cdk/src/handlers/shared/preflight.ts)). The assembled prompt is screened through Amazon Bedrock Guardrails for prompt injection before the agent receives it (PR tasks: always screened; `new_task`: screened when issue content is present). The output is a fully assembled prompt, ready to pass to the compute session.
183183

184184
#### Step 3: Session start and agent execution (deterministic start + agentic execution)
185185

@@ -253,6 +253,8 @@ Admission control runs immediately after the input gateway dispatches a "create
253253
- **Rejected.** Task transitions to `FAILED` with a reason (repo not onboarded, rate limit exceeded, concurrency limit, validation error). No counter change.
254254
- **Deduplicated.** Existing task ID returned. No new task created.
255255

256+
**Planned (Iteration 5):** Admission control checks will be governed by Cedar policies as part of the centralized policy framework. Cedar replaces the current inline admission logic with formally verifiable policy evaluation — the same Cedar policy store handles admission, budget/quota resolution, tool-call interception, and (when multi-user/team lands) tenant-scoped authorization. All admission decisions will emit a structured `PolicyDecisionEvent` for audit. See [ROADMAP.md Iteration 5](../guides/ROADMAP.md) (Centralized policy framework) and [SECURITY.md](./SECURITY.md) (Policy enforcement and audit).
257+
256258
---
257259

258260
## Context hydration
@@ -271,7 +273,7 @@ The orchestrator's `hydrateAndTransition()` function calls `hydrateContext()` (`
271273
4. **Assembles the user prompt** based on task type:
272274
- **`new_task`**: A structured markdown document with Task ID, Repository, GitHub Issue section, and Task section. The format mirrors the Python `assemble_prompt()` in `agent/entrypoint.py`.
273275
- **`pr_iteration`**: Assembled by `assemblePrIterationPrompt()` — includes PR metadata (number, title, body), the diff summary (changed files and patches), review comments (inline and conversation), and optional user instructions from `task_description`.
274-
5. **Screens through Bedrock Guardrail** (PR tasks only): For `pr_iteration` and `pr_review` tasks, the assembled user prompt is screened through Amazon Bedrock Guardrails (`screenWithGuardrail()`) using the `PROMPT_ATTACK` content filter. If the guardrail detects prompt injection, `guardrail_blocked` is set on the result and the orchestrator fails the task. If the Bedrock API is unavailable, a `GuardrailScreeningError` is thrown (fail-closed — unscreened content never reaches the agent). Task descriptions for all task types are screened at submission time in `create-task-core.ts`.
276+
5. **Screens through Bedrock Guardrail** (PR tasks; `new_task` when issue content is present): The assembled user prompt is screened through Amazon Bedrock Guardrails (`screenWithGuardrail()`) using the `PROMPT_ATTACK` content filter. For `new_task` tasks without issue content, screening is skipped because the task description was already screened at submission time. If the guardrail detects prompt injection, `guardrail_blocked` is set on the result and the orchestrator fails the task. If the Bedrock API is unavailable, a `GuardrailScreeningError` is thrown (fail-closed — unscreened content never reaches the agent). Task descriptions for all task types are screened at submission time in `create-task-core.ts`.
275277
6. **Returns a `HydratedContext` object** containing `version`, `user_prompt`, `issue`, `sources`, `token_estimate`, `truncated`, and for `pr_iteration`/`pr_review` tasks: `resolved_branch_name` and `resolved_base_branch`.
276278

277279
The hydrated context is passed to the agent as a new `hydrated_context` field in the invocation payload, alongside the existing legacy fields (`repo_url`, `task_id`, `branch_name`, `issue_number`, `prompt`). The agent checks for `hydrated_context` with `version == 1`; if present, it uses the pre-assembled `user_prompt` directly and skips in-container GitHub fetching and prompt assembly. If absent (e.g. during a deployment rollout or when the secret ARN isn't configured), the agent falls back to its existing behavior.

0 commit comments

Comments
 (0)