Skip to content

Architecture Overview

System Diagram

graph TB
    Client([Client / curl])

    subgraph AgentCore["BedrockAgentCoreApp (Fastify :8080)"]
        Ping["GET /ping"]
        Invoke["POST /invocations"]
    end

    subgraph Security["Prisma AIRS AI Runtime Security"]
        PromptScan["Prompt Scan<br/>(pre-LLM)"]
        ResponseScan["Response Scan<br/>(post-LLM)"]
    end

    subgraph StrandsAgent["Strands Agent"]
        Model["BedrockModel<br/>Claude Haiku 4.5"]
        SystemPrompt["System Prompt<br/>(extraction rules)"]
        FetchTool["fetch_url tool"]
    end

    subgraph Processing["Response Processing"]
        ExtractJSON["extractJson()<br/>parse LLM text output"]
        ZodValidation["RecipeSchema.parse()<br/>Zod v4 validation"]
    end

    subgraph FetchToolInternals["fetch_url internals"]
        HTTPFetch["Node fetch()"]
        LinkedOM["linkedom<br/>HTML parser"]
        JSONLD["JSON-LD extractor<br/>schema.org/Recipe"]
        StripHTML["Strip nav, header,<br/>footer, scripts"]
    end

    ExternalSite[(Recipe Website)]
    Bedrock[(Amazon Bedrock<br/>us-west-2)]
    AIRS[(Prisma AIRS API<br/>aisecurity.paloaltonetworks.com)]

    Client -->|"POST {url}"| Invoke
    Client -->|health check| Ping
    Invoke --> PromptScan
    PromptScan <-->|scan prompt| AIRS
    PromptScan -->|allow| StrandsAgent
    PromptScan -.->|block| Client
    Model <-->|Converse API| Bedrock
    Model -->|tool_use| FetchTool
    FetchTool --> HTTPFetch
    HTTPFetch -->|GET| ExternalSite
    ExternalSite -->|HTML| LinkedOM
    LinkedOM --> JSONLD
    LinkedOM --> StripHTML
    StripHTML -->|"text + jsonLd"| Model
    JSONLD -->|"text + jsonLd"| Model
    Model -->|JSON text| ExtractJSON
    ExtractJSON --> ZodValidation
    ZodValidation --> ResponseScan
    ResponseScan <-->|scan response| AIRS
    ResponseScan -->|allow| Client
    ResponseScan -.->|block| Client

    style AgentCore fill:#1a1a2e,stroke:#e94560,color:#fff
    style Security fill:#2d1b36,stroke:#e94560,color:#fff
    style StrandsAgent fill:#16213e,stroke:#0f3460,color:#fff
    style Processing fill:#0f3460,stroke:#533483,color:#fff
    style FetchToolInternals fill:#1a1a2e,stroke:#533483,color:#fff

Request Flow

sequenceDiagram
    participant C as Client
    participant App as BedrockAgentCoreApp
    participant AIRS as Prisma AIRS API
    participant A as Strands Agent
    participant B as Amazon Bedrock
    participant T as fetch_url Tool
    participant W as Recipe Website

    C->>+App: POST /invocations<br/>{"url": "https://..."}
    Note over App: Validate request via Zod<br/>Extract sessionId from header

    rect rgb(45, 27, 54)
        Note over App,AIRS: Pre-LLM Security Scan
        App->>+AIRS: scanPrompt(prompt, sessionId)
        AIRS-->>-App: {action: "allow" | "block"}
    end

    alt AIRS blocks prompt
        App-->>C: 200 {error: "blocked", category, scan_id}
    else AIRS allows prompt
        App->>+A: agent.invoke("Extract recipe from URL")

        A->>+B: Converse API<br/>(system prompt + user message)
        B-->>-A: tool_use: fetch_url({url})

        A->>+T: fetch_url({url})
        T->>+W: GET https://...
        W-->>-T: HTML response

        Note over T: Parse HTML with linkedom
        Note over T: Extract JSON-LD (schema.org/Recipe)
        Note over T: Strip script, style, nav, header, footer
        Note over T: Collapse whitespace, truncate at 30k chars

        T-->>-A: {text, jsonLd}

        A->>+B: Converse API<br/>(tool result with text + jsonLd)
        B-->>-A: Recipe JSON as text response

        A-->>-App: AgentResult

        Note over App: extractJson() — parse raw text,<br/>code block, or brace extraction
        Note over App: RecipeSchema.parse() — Zod validation

        rect rgb(45, 27, 54)
            Note over App,AIRS: Post-LLM Security Scan
            App->>+AIRS: scanResponse(recipeJSON, prompt, sessionId)
            AIRS-->>-App: {action: "allow" | "block"}
        end

        alt AIRS blocks response
            App-->>C: 200 {error: "blocked", category, scan_id}
        else AIRS allows response
            App-->>-C: 200 OK<br/>typed Recipe JSON
        end
    end

Key Design Decisions

Decision Rationale
Prisma AIRS pre+post scan Scans data from external sources — prompt injection, URL categorization, agent security, DLP — preventing the agent from acting on untrusted data
Fail-open on AIRS misconfiguration If API key missing, agent operates normally — no hard dependency on security service
Non-streaming handler Returns single JSON object — structured data doesn't benefit from SSE streaming
extractJson() fallback chain LLM may wrap JSON in markdown code blocks; tries direct parse, code block regex, then brace extraction
JSON-LD extraction Many recipe sites embed schema.org/Recipe structured data — improves accuracy
linkedom over jsdom ~200KB vs ~70MB; sufficient for text extraction and DOM traversal
Claude Haiku 4.5 Fast, cheap, accurate for structured extraction — ~5-9s per request
temperature: 0 Deterministic output for consistent JSON formatting

Project Structure

src/
  app.ts                 Agent logic, extractJson, processHandler, AIRS scanning
  main.ts                Bootstrap (Secrets Manager fetch) → import app → app.run()
  lib/
    cloudwatch-stream.ts Custom CloudWatch log stream
  schemas/
    recipe.ts            Zod schemas for Recipe and Ingredient
  tools/
    fetch-url.ts         Custom tool: fetch URL, strip HTML, extract JSON-LD
tests/
  unit/                  Schema, extractJson, fetch-url, cloudwatch-stream tests
  integration/           processHandler tests (mocked Agent + BedrockAgentCoreApp)
scripts/
  deploy.sh              First deploy + update AgentCore runtime
  setup-github-iam.sh    Create IAM role for GitHub Actions OIDC
  setup-secrets.sh       Store AIRS API key in Secrets Manager