Skip to content

Commit b3adb97

Browse files
authored
Merge pull request #14 from aws-samples/mem
chore(docs): update docs, add guardrails
2 parents 808554c + 06002e2 commit b3adb97

19 files changed

Lines changed: 412 additions & 90 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ ABCA is under active development. The platform ships iteratively — each iterat
5656
|---|---|---|
5757
| **1** | Done | Agent runs on AWS, CLI submit, branch + PR |
5858
| **2** | Done | Production orchestrator, API contract, task management, observability, security, webhooks |
59-
| **3a** | Done | Repo onboarding, per-repo GitHub App credentials, turn caps, prompt guide |
59+
| **3a** | Done | Repo onboarding, per-repo credentials, turn caps, prompt guide |
6060
| **3b** | Done | Memory Tier 1, insights, agent self-feedback, prompt versioning, commit attribution |
6161
| **3bis** | Done | Hardening — reconciler error tracking, error serialization, test coverage gaps |
6262
| **3c** | WIP | Pre-flight checks, persistent session storage, deterministic validation, PR review task type, multi-modal input, input guardrail screening |
63-
| **3d** | Planned | Review feedback loop, PR outcome tracking, evaluation pipeline |
63+
| **3d** | Planned | Review feedback loop, PR outcome tracking, evaluation pipeline, memory input hardening |
6464
| **4** | Planned | GitLab, visual proof, Slack, control panel, WebSocket streaming |
6565
| **5** | Planned | Pre-warming, multi-user/team, cost management, output guardrails, alternate runtime |
6666
| **6** | Planned | Skills learning, multi-repo, iterative feedback, multiplayer, CDK constructs |

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/API_CONTRACT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,8 @@ Rate limit status is communicated via response headers (see Standard response he
617617
| `WEBHOOK_NOT_FOUND` | 404 | Webhook does not exist or belongs to a different user. |
618618
| `WEBHOOK_ALREADY_REVOKED` | 409 | Webhook is already revoked. |
619619
| `REPO_NOT_ONBOARDED` | 422 | Repository is not registered with the platform. Repos are onboarded via CDK deployment, not via a runtime API. There are no `/v1/repos` endpoints. |
620+
| `GITHUB_UNREACHABLE` | 502 | The GitHub API was unreachable during the orchestrator's pre-flight check. The task fails fast without consuming compute. Transient — retry with backoff. |
621+
| `REPO_NOT_FOUND_OR_NO_ACCESS` | 422 | The target repository does not exist or the configured credentials lack access. Checked during the orchestrator's pre-flight step (`GET /repos/{owner}/{repo}`). Distinct from `REPO_NOT_ONBOARDED` — the repo is onboarded but the credential cannot reach it. |
620622
| `PR_NOT_FOUND_OR_CLOSED` | 422 | For `pr_iteration` and `pr_review` tasks: the specified PR does not exist, is not open, or is not accessible with the configured GitHub token. Checked during the orchestrator's pre-flight step. |
621623
| `INVALID_STEP_SEQUENCE` | 500 | The blueprint's step sequence is invalid (missing required steps or incorrect ordering). This indicates a CDK configuration error that slipped past synth-time validation. Visible via `GET /v1/tasks/{id}` as `error_code`. See [REPO_ONBOARDING.md](./REPO_ONBOARDING.md#step-sequence-validation). |
622624
| `GUARDRAIL_BLOCKED` | 400 | Task description was blocked by Bedrock Guardrail content screening (prompt injection detected). Revise the task description and retry. |

docs/design/ARCHITECTURE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ Cost efficiency is a design principle. The following estimates are based on **50
129129
| **API Gateway** (REST API, ~2K requests/day) | ~$5–15 | Per-request pricing |
130130
| **AgentCore Memory** (events, records, retrieval) | TBD | Pricing not fully public; proportional to usage |
131131
| **CloudWatch** (logs, metrics, traces, Transaction Search) | ~$20–50 | Log ingestion + storage |
132-
| **Secrets Manager** (GitHub App keys, webhook secrets) | ~$5–10 | Per-secret/month + API calls |
132+
| **Secrets Manager** (GitHub token or App private key, webhook secrets) | ~$5–10 | Per-secret/month + API calls |
133+
| **AgentCore Identity** (planned — WorkloadIdentity, Token Vault credential provider) | TBD | Token vending API calls; replaces per-task Secrets Manager reads for GitHub tokens |
133134
| **S3** (artifacts, memory backups) | ~$1–5 | Storage + requests |
134135
| **Total** | **~$700–1,600/month** | |
135136

@@ -201,10 +202,17 @@ Each concept has a **source-of-truth document** and one or more documents that r
201202
| Agent swarm orchestration | ROADMAP.md (Iter 6) ||
202203
| Adaptive model router | ROADMAP.md (Iter 5) | COST_MODEL.md |
203204
| Capability-based security | ROADMAP.md (Iter 5) | SECURITY.md |
205+
| Centralized policy framework | ROADMAP.md (Iter 5), SECURITY.md (Policy enforcement and audit) | ORCHESTRATOR.md, OBSERVABILITY.md |
206+
| GitHub App + AgentCore Token Vault | ROADMAP.md (Iter 3c), SECURITY.md (Authentication) | ORCHESTRATOR.md (context hydration), COMPUTE.md |
204207
| Live session replay | ROADMAP.md (Iter 4) | API_CONTRACT.md |
205208
| PR iteration task type | API_CONTRACT.md, ORCHESTRATOR.md | USER_GUIDE.md, PROMPT_GUIDE.md, SECURITY.md, AGENT_HARNESS.md |
206209
| PR review task type | API_CONTRACT.md, ORCHESTRATOR.md | USER_GUIDE.md, PROMPT_GUIDE.md, SECURITY.md, AGENT_HARNESS.md |
210+
| Orchestrator pre-flight checks | ORCHESTRATOR.md (Context hydration, pre-flight sub-step) | API_CONTRACT.md (Error codes: GITHUB_UNREACHABLE, REPO_NOT_FOUND_OR_NO_ACCESS), ROADMAP.md (3c), SECURITY.md |
207211
| Bedrock Guardrail input screening | SECURITY.md (Input validation and guardrails) | ORCHESTRATOR.md (Context hydration), API_CONTRACT.md (Error codes), OBSERVABILITY.md (Alarms), ROADMAP.md (3c) |
212+
| Memory input hardening (3e Phase 1) | ROADMAP.md (Iter 3e Phase 1, co-ships with 3d) | MEMORY.md, SECURITY.md (Memory-specific threats) |
213+
| Per-tool-call structured telemetry | ROADMAP.md (Iter 3d) | SECURITY.md (Mid-execution enforcement), EVALUATION.md, OBSERVABILITY.md |
214+
| Mid-execution behavioral monitoring | ROADMAP.md (Iter 5), SECURITY.md (Mid-execution enforcement) | OBSERVABILITY.md |
215+
| Tool-call interceptor (Guardian pattern) | SECURITY.md (Mid-execution enforcement), ROADMAP.md (Iter 5) | REPO_ONBOARDING.md (Blueprint security props) |
208216

209217
### Per-repo model selection
210218

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

0 commit comments

Comments
 (0)