Security and data handling
Credentials
-
Every credential lives in one Secrets Manager secret,
agentcore-configs. Nothing is baked into an image, a template, or source code. -
Each consumer declares only the keys it needs in a Pydantic model with
extra="ignore", so an unrelated key cannot leak into an unrelated component. -
Secret reads are cached per container (
@lru_cachein Lambdas, a module global in the runtime): one Secrets Manager call per container lifetime. -
Libraries never read
os.environ.create_hubspot_mcp(),create_tavily_mcp(), andcreate_bigquery_client()all take credentials as explicit keyword arguments; the caller fetches them. This keeps credential provenance visible at the call site. -
.envfiles and service-account JSON must never be committed..gitignorecovers the common cases;enrichment_scraper/.env.exampledocuments the shape without values.
Two HubSpot auth modes are supported, in priority order: a Private App token
(HUBSPOT_PRIVATE_APP_TOKEN, preferred — static, no refresh) and the OAuth
triple (fallback). morpheus-hubspot validates at load time that at least one
complete mode is present and fails the cold start otherwise.
PII
The platform handles names, email addresses, phone numbers, and business identifiers for real people.
- Logging rules
-
Log identifiers (
sellerId,hubspot_contact_id, row UUIDs) and counts. Do not log message bodies, transcripts, or email content. Uselogger.debug()for per-record detail so it is off in production. Always passexc_info=Trueon a caught exception rather than reformatting the payload into the message.Known deviation: the
morpheus-hubspotdispatcher logs the full message body in its "Handler failed"ERRORline. - Sentry
-
Initialised with
send_default_pii=False, which strips IP addresses and user agents.LoggingIntegrationforwardsERROR-level records as events, so any PII placed in an error message reaches Sentry. Currently disabled —SENTRY_DSNis empty. - Email content
-
Inbound reply text is passed to Bedrock for classification and is not persisted by
email-agentbeyond the category, timestamp, and model label. Theemailstable (written by the web application) does hold bodies. - Suppression
-
Opt-outs are authoritative.
Contact.Unsubscribewrites HubSpot Communication Preferences; thecontact.propertyChangewebhook processor mirrorshs_email_optout_<id>intotarget_emails.newsletterOptOut,oneToOneOptOut, andmarketingOptOut. Re-engagement segments exclude blocked dispositions and do-not-contact reasons in SQL. See Email engagement.
Tenant isolation
Every domain table is scoped by subscriptionId. Isolation is enforced in three
different ways depending on the component:
| Component | Mechanism |
|---|---|
enrichment-scraper, enrichment-scorer, phone-agent, morpheus-hubspot |
|
email-agent |
No |
A mismatch is treated as bad data, never retried — a poison message from a misconfigured producer must not loop forever.
IAM posture
The Lambda functions use SAM-generated roles with narrow policies:
-
SQSPollerPolicy/SQSSendMessagePolicyscoped to the specific queue name. -
secretsmanager:GetSecretValuescoped toAgentcoreSecretonly.enrichment-scraperadditionally holdsPutSecretValueon the same secret because it caches the SmartScout session token there. -
bedrock:InvokeModelandInvokeModelWithResponseStreamon*for the two functions that call Bedrock (enrichment-scorer,email-agent). Bedrock model ARNs are not enumerable in advance for cross-region inference profiles.
The AgentCore runtime role is defined in the CDK stack with per-statement SIDs — ECR pull, CloudWatch Logs, X-Ray, namespaced CloudWatch metrics, AgentCore workload identity, Bedrock invocation, AWS Marketplace subscription (required for first-use model subscription), Secrets Manager read scoped to the secret ARN, and AgentCore Memory operations scoped to the memory ARN. Full statement list: IAM.
CDK owns all IAM. Importing an externally managed role with
iam.Role.fromRoleArn() is prohibited — see
Engineering conventions.
Prompt injection
Two agents consume untrusted text.
- Inbound email categorisation (
email-agent) -
inbound_textis verbatim sender content, interpolated into the prompt without sanitisation. This is accepted, with two controls:-
Structured output —
EmailReplyCategorisationResult.categoryis constrained to theEmailTargetCategoryenum, so the model cannot emit arbitrary JSON or invoke tools. -
The agent is constructed with
tools=lambda: []— no tool surface at all.Residual risk is bounded misclassification (a reply containing "always choose interested"), not code execution or data exfiltration. This trade-off is documented in
lambdas/email_agent/agents/prompts.py.
-
- Seller enrichment (
enrichment-scorer) -
The enrichment agent reads arbitrary web content through the Tavily MCP tool and has HubSpot MCP write access in the same session. Output is constrained to
SellerEnrichmentResult,max_turnsis capped at 6, and every result is validated before it reaches the database. Content fetched from the open web is the injection surface here.
strands_tools vended tools — shell, python_repl, editor, http_request —
are wired into the runtime’s dialler_agent. shell and python_repl grant
in-container code execution to the model. Only expose them to workflows whose
inputs you control.