Implement reversible PII de-identification round trip (T1–T7)
Build the AWS side of the HNCB demo end to end (region ap-southeast-1): - T1 /tokenize + T2 /restore: Lambdas behind a public API Gateway (shared-secret auth), Presidio detection, random per-request tokens, DynamoDB vault; overlap resolution so a ROC ID stays TW_ROC_ID. - T3: Presidio made private (SG-locked to the tokenize Lambda in-VPC; DynamoDB gateway endpoint); only /tokenize + /restore are public. - T4: RAG Lambda registered as an MCP tool on an AgentCore Gateway (AWS_IAM/SigV4); agentcore_setup.sh + a SigV4 MCP invoke test. - T5: Strands agent deployed to AgentCore Runtime; SigV4 gateway auth, apac inference profile, pinned deps. - T6: advisor UI on S3+CloudFront with a Fusion-less demo orchestrator (/demo) chaining tokenize -> runtime -> restore. - T7: README runbook + trace check; teardown deletes gateway/runtime/memory/ECR. Verified live: the cloud AgentCore/Bedrock trace shows only tokens, never the real name. Secrets stay in gitignored local.auto.tfvars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
125
CLAUDE.md
125
CLAUDE.md
@@ -60,30 +60,107 @@ fusion/ POLICY_SETUP.md (SaaS console config — human does this, not y
|
||||
## Task backlog (pick these up)
|
||||
Each task: keep it demo-grade, add a note in the file, and update this list.
|
||||
|
||||
- **T1 — `/tokenize` endpoint** (`gateway_api/tokenize/`). HTTPS endpoint Fusion
|
||||
calls with `{query}`. Steps: POST query to Presidio `/analyze`; for each finding
|
||||
mint a random token (`CUST_<rand>` for PERSON→customer, `TW_<rand>` for TW_ROC_ID);
|
||||
write vault items `{token, type, value, session_id, expires_at}`; splice tokens
|
||||
into the text. Return `{deidentified_prompt, session_id}`.
|
||||
*Accept:* real name in → tokenized text + session_id out; vault rows written;
|
||||
same input yields different tokens on repeat calls.
|
||||
- **T2 — `/restore` endpoint** (`gateway_api/restore/`). Input `{session_id, text}`.
|
||||
Look up this session's tokens in the vault, re-attach identity (envelope-level:
|
||||
prepend the real name; also swap any inline tokens). Return `{final}`.
|
||||
*Accept:* tokenized answer + session_id → answer with `王小明` restored.
|
||||
- **T3 — reachability from SaaS.** Make Presidio (or fold detection into T1 so only
|
||||
`/tokenize` + `/restore` are public) reachable from Fusion SaaS with auth.
|
||||
*Accept:* Fusion SaaS can call the endpoints over HTTPS; nothing else is public.
|
||||
- **T4 — AgentCore Gateway target.** Wire the RAG Lambda as the MCP tool; confirm
|
||||
the agent can call `get_customer_activity_summary(CUST_000123)` and get the package.
|
||||
*Accept:* a manual `agentcore` invoke returns the evidence package.
|
||||
- **T5 — Agent runtime.** Pin deps, `agentcore launch`, capture the Runtime ARN.
|
||||
*Accept:* invoking the runtime with a tokenized prompt returns talking points.
|
||||
- **T6 — UI wiring.** Point `ui/index.html` GATEWAY_URL at the Fusion SaaS entry
|
||||
(or `/tokenize` for a Fusion-less dry run); host on S3+CloudFront.
|
||||
*Accept:* the split view renders restored vs tokenized.
|
||||
- **T7 — E2E rehearsal.** Run the full script in README; confirm the Bedrock/
|
||||
AgentCore trace shows only tokens. Then verify `teardown.sh` leaves nothing paid-for.
|
||||
- **T1 — `/tokenize` endpoint** ✅ **done + live-verified** (`gateway_api/tokenize/handler.py`,
|
||||
terraform `terraform/gateway.tf`). Lambda behind a **public API Gateway HTTP API**
|
||||
(not a Function URL — this account's SCP blocks unauthenticated Function URLs),
|
||||
shared-secret auth (`tokenize_api_key`, sent as `x-api-key` / bearer, checked
|
||||
in-handler). Calls Presidio `/analyze`, resolves overlapping findings (specific
|
||||
entity wins, so the ROC ID stays `TW_ROC_ID` not `PERSON`), mints random
|
||||
`CUST_<rand>`/`TW_<rand>` tokens, writes vault rows
|
||||
`{token, type, value, session_id, expires_at [, customer_id]}` (`value` = original
|
||||
PII for /restore; `customer_id` best-effort resolved for the RAG tool), splices
|
||||
right-to-left, returns `{deidentified_prompt, session_id}`. **Verified live** in
|
||||
`ap-southeast-1`: `王小明`→`CUST_*`, `A123456789`→`TW_*`, clean prompt, vault rows
|
||||
written with resolved `customer_id`, repeat→different tokens, no/bad key→401.
|
||||
Infra changes made while deploying: Presidio task bumped to **2GB** (1GB OOM'd on
|
||||
the 603MB zh model → connection refused), **awslogs** added, **ARM64/Graviton**
|
||||
runtime (built on Apple Silicon via podman). All AWS objects tagged
|
||||
`Owner="conan hncb demo"`. *Caveats:* Presidio has no stable endpoint — its
|
||||
Fargate public IP changes per task launch, so `presidio_url` in
|
||||
`terraform/local.auto.tfvars` must be refreshed and the tokenize Lambda re-applied
|
||||
(an ALB/Cloud Map would fix this; out of scope for the demo). The default VPC was
|
||||
created by hand and tagged `maintenance=manual-cleanup-required` (Terraform doesn't
|
||||
own it, so `teardown.sh` won't remove it).
|
||||
- **T2 — `/restore` endpoint** ✅ **done + live-verified** (`gateway_api/restore/handler.py`,
|
||||
terraform in `terraform/gateway.tf`). `POST /restore` on the same API Gateway,
|
||||
same shared-secret auth; IAM is read-only `dynamodb:Scan` on the vault. Input
|
||||
`{session_id, text}` → scans the vault for THIS session's tokens (scoped, so one
|
||||
session can't restore another's), swaps inline tokens back to the original PII
|
||||
(longest-token-first), and prepends the customer's real name at the envelope
|
||||
level (`(客戶:王小明)`). Returns `{final, restored}`. **Verified live** with the
|
||||
full T1→T2 round trip: tokenized prompt out → simulated token-only cloud answer →
|
||||
`王小明` + `A123456789` restored, no tokens left; no/bad key → 401; unknown
|
||||
session → text unchanged, `restored:0` (no cross-session leak). `restore_url`
|
||||
is a Terraform output.
|
||||
- **T3 — reachability from SaaS** ✅ **done + live-verified** (`terraform/ecs.tf`,
|
||||
`terraform/gateway.tf`). Presidio is now private: its SG allows 5001 only from the
|
||||
tokenize Lambda's SG (no `0.0.0.0/0`). The tokenize Lambda runs **in the VPC**
|
||||
(`vpc_config` + `AWSLambdaVPCAccessExecutionRole`) and reaches Presidio on its
|
||||
**private** IP; DynamoDB is reached via a **gateway VPC endpoint** (no NAT). The
|
||||
Fargate task keeps a public IP only to pull from ECR, but all inbound is SG-locked.
|
||||
Only the API Gateway (`/tokenize` + `/restore`, shared-secret auth) is public.
|
||||
**Verified live:** Presidio's public IP `:5001` now times out from the internet;
|
||||
the full round trip still works through the private path. *Caveat update:* the
|
||||
tokenize Lambda now targets Presidio's **private** IP (`172.31.2.216` currently),
|
||||
which still changes per task launch — refresh `presidio_url` in
|
||||
`terraform/local.auto.tfvars` and re-apply `aws_lambda_function.tokenize` if the
|
||||
task restarts (Cloud Map/ALB would give a stable name; out of scope for the demo).
|
||||
- **T4 — AgentCore Gateway target** ✅ **done + live-verified** (`terraform/agentcore.tf`,
|
||||
`scripts/agentcore_setup.sh`, `scripts/gateway_invoke_test.py`). Created an MCP
|
||||
Gateway (`hncb-rag-gateway`, `authorizerType=AWS_IAM` so inbound is SigV4 — no
|
||||
Cognito) with the RAG Lambda registered as a `lambda` MCP target; the Gateway
|
||||
assumes a Terraform-managed IAM role that can only `lambda:InvokeFunction` the RAG
|
||||
tool. Built the Gateway/target with `aws bedrock-agentcore-control` (Terraform
|
||||
lags here). **Verified live:** `gateway_invoke_test.py` (SigV4 MCP
|
||||
initialize→tools/list→tools/call) returned the full de-identified evidence package
|
||||
for `CUST_000123` through the Gateway; the tool shows up namespaced as
|
||||
`rag___get_customer_activity_summary`. Gotchas handled: the Gateway `inputSchema`
|
||||
is a restricted JSON-Schema subset (strip `default`/unknown keys); MCP responses
|
||||
can be SSE; tool names are target-prefixed. Gateway tagged `Owner="conan hncb demo"`;
|
||||
`teardown.sh` deletes it (not in Terraform state). MCP URL is printed by the setup
|
||||
script. *Note:* `.venv-t4/` (gitignored) holds boto3 for the invoke test.
|
||||
- **T5 — Agent runtime** ✅ **done + live-verified** (`agent/agent.py`,
|
||||
`agent/requirements.txt`, `scripts/agentcore_setup.sh` T5 section). Pinned deps
|
||||
(bedrock-agentcore 1.16.0 / strands-agents 1.45.0 / mcp 1.28.1 / boto3 1.43.38),
|
||||
`agentcore configure --non-interactive` + `agentcore launch` (cloud CodeBuild,
|
||||
auto-created execution role + ECR + STM memory). **Runtime ARN:**
|
||||
`arn:aws:bedrock-agentcore:ap-southeast-1:286171702468:runtime/hncb_advisor_agent-duWZOT5Far`.
|
||||
**Verified live:** `agentcore invoke {"prompt": "...CUST_000123..."}` returns
|
||||
numbered talking points built only from the de-identified evidence — no name, no
|
||||
leaked token. Three gotchas fixed (all captured in code/scripts):
|
||||
1. In `ap-southeast-1` the model is **INFERENCE_PROFILE-only** → use
|
||||
`apac.anthropic.claude-3-5-sonnet-20241022-v2:0`, not the raw id.
|
||||
2. Our Gateway is **AWS_IAM**, so `agent.py` SigV4-signs MCP requests (httpx.Auth)
|
||||
instead of using a bearer token.
|
||||
3. The auto-created runtime **execution role can't call the Gateway** by default →
|
||||
must attach `bedrock-agentcore:InvokeGateway` on the gateway ARN (the launch
|
||||
step in `agentcore_setup.sh` shows the exact command). Runtime + memory added to
|
||||
`teardown.sh`; agent logic was de-risked locally before launch.
|
||||
- **T6 — UI wiring** ✅ **done + live-verified** (`ui/index.html`, `terraform/ui.tf`,
|
||||
`gateway_api/orchestrator/`). Hosted on **S3 + CloudFront** (private bucket, OAC).
|
||||
For the Fusion-less dry run I added a thin **demo orchestrator** (`POST /demo`, a
|
||||
Lambda on the same API) that runs the full chain server-side —
|
||||
`/tokenize` → SigV4 `InvokeAgentRuntime` → `/restore` — and returns
|
||||
`{final, deidentified_prompt, agent_tokenized}` (this stands in for Fusion ONLY
|
||||
for the dry run; we still never build/host Fusion). `index.html` reads the live
|
||||
endpoint from a generated `config.js` (so no ephemeral URL is committed) and
|
||||
renders the split view. CORS enabled on the API. **UI:**
|
||||
`https://d3n89cj9w7ynf0.cloudfront.net`. **Verified live:** CloudFront serves
|
||||
`index.html` + `config.js` (200); `/demo` returns `王小明`→`CUST_*` on the cloud
|
||||
side and `王小明`-restored talking points on the advisor side. Only the browser
|
||||
render itself is left as a human eyeball check. All resources tagged.
|
||||
- **T7 — E2E rehearsal** ✅ **done + live-verified** (README demo/deploy sections
|
||||
rewritten with the real commands). Ran the full round trip via `/demo`
|
||||
(`王小明`→`CUST_317499` out, agent talking points on tokens only, `王小明` restored).
|
||||
**Money shot proven:** in the cloud runtime CloudWatch trace for that session the
|
||||
token appears in **13 events** while `王小明` and `A123456789` appear in **0** —
|
||||
the Bedrock/AgentCore trace is token-only. **Teardown verified (non-destructively):**
|
||||
`terraform destroy` targets 43 resources (Fargate, CloudFront, S3, DynamoDB, all
|
||||
Lambdas, API GW, VPC endpoint, SGs, IAM) and `teardown.sh` additionally deletes the
|
||||
Gateway/targets, Runtime, Memory, and both ECR repos — i.e. every billed resource.
|
||||
Residual is free-only: the hand-made default VPC, the AgentCore SDK exec role, an
|
||||
idle CodeBuild project. The actual `teardown.sh` run is left for the human to fire
|
||||
after recording (didn't auto-destroy the live demo).
|
||||
|
||||
## Known caveats (don't "fix" these silently)
|
||||
- zh-TW detection is demo-narrow (tuned to the scripted entities), not production recall.
|
||||
|
||||
Reference in New Issue
Block a user