What it enables
- Operations-shaped scenarios (AWS Pulse, Incident Response, Lead Qualifier) that need to read live state from outside the project workspace.
- Per-tenant credential management with an audit-friendly indirection: the database holds a reference to the secret, never the secret value itself.
- Per-scenario tool whitelisting via the previously-unused
Level.intra_toolsfield — a typo surfaces as a clearKeyErrorat level start, not a silent miss. - Shared HTTP plumbing (retry, per-host rate limit, structured logging) so each connector doesn’t reinvent it.
Architecture
finish, read_file, edit_file,
…) stay in worker/runtime/file_tools.py; external tools live in this
new registry. They’re layered together at dispatch time — file tools
are always available; external tools come from Level.intra_tools.
This separation is deliberate: file tools take a project workspace and
mutate the tree; external tools take a ToolContext (tenant + secrets
- HTTP) and reach out to remote services. Forcing one signature would compromise both.
Tool definition pattern
default_registry.register(SPEC) at import
time. tools/__init__.py pulls in builtins for the side-effect, so
a single from worker.worker.runtime.tools import default_registry
exposes every shipped tool.
Tenant secrets
Schema:tenant_tool_secrets (added to PostgresRepo.ensure_schema).
| Column | Notes |
|---|---|
tenant_id, secret_name | Composite PK |
ref_kind | 'env' (Phase 1), 'secrets_manager', 'inline' (Phase 2+) |
ref_value | Env var name OR Secrets Manager ARN OR encrypted blob id |
metadata, notes, created_by, timestamps | Audit trail |
ref_kind='env'. EnvToolSecretProvider reads
the row, looks up os.environ[row['ref_value']], JSON-parses if the
payload looks like JSON, falls back to {"value": <raw>}. A 300s
in-process TTL cache absorbs repeat lookups within a single run.
Seed a row:
CEREBRUM_AWS_DEFAULT='{"access_key_id":"...","secret_access_key":"...","region":"ap-east-1"}'.
SecretsManagerToolProvider is a stub that raises NotImplementedError
loudly so a premature switch in Phase 2 is loud, not silent.
Runner integration
Two surgical edits inworker/worker/graph/scenarios/runners.py:
_build_tool_list(level, registry)— combined file + external tool schema list per level. Unknownintra_toolsraises at level start with the full list of registered names._dispatch_tool— registry-aware:ask_user(interrupt) → external tool registry → file toolexecute_toolfallthrough. Crashes in external handlers becomeToolResult(ok=False); they never abort the loop.
_redact() — any string under a key matching
/(secret|token|password|api[_-]?key|access[_-]?key)/i is masked to
<redacted>. Belt-and-braces in case a token leaks into args.
Workspace-less levels (AWS Pulse style — intra_tools set, no
_project_cwd) get a scratch tempfile.TemporaryDirectory so
finish still works.
Shipped connectors
AWS — 12 read-only audit tools.aws_ec2_inventory,
aws_ebs_inventory, aws_cost_breakdown, aws_cost_anomalies,
aws_compute_optimizer_recs, aws_s3_inventory, aws_iam_audit,
aws_security_groups_audit, aws_cloudtrail_coverage,
aws_nat_gateways_audit, aws_eips_audit, aws_rds_inventory.
Cost Explorer / Compute Optimizer require account opt-in;
OptInRequired surfaces as ToolResult(ok=False, error=…) so the
loop continues.
Slack — 4 tools. slack_post_message, slack_post_blocks,
slack_post_threaded, slack_channel_history. Auth resolves either
a bot_token (xoxb-…) for the full Web API or a webhook_url for
post-only delivery; bot token wins when both are present.
Impact scope
- Pure addition. Existing scenarios (
code_build,document_writing,quick_qa,security_review) are unaffected — file tools dispatch unchanged. A legacyintra_tools: [heartbeat_l4]declaration incode_build.yamlworks because we registered a no-op sentinel. - New Postgres table;
ensure_schema()is idempotent. - Production wiring lives in
worker/worker/main.py: builds oneEnvToolSecretProvider(repo=postgres)and passes it toWorkerOrchestrator. - New dependencies: none. Uses existing
httpx,boto3,psycopg.
Tests
| File | Coverage |
|---|---|
test_tool_registry.py | Register/get/clone/duplicate, helpful KeyError |
test_tool_http_client.py | Retry on 5xx, Retry-After, idempotent=False, token bucket |
test_tool_secrets.py | Env resolution, convention fallback, TTL cache |
test_runners_external_tools.py | _build_tool_list, dispatch, redaction |
test_tools_aws.py | One happy + one error path per AWS tool (botocore.stub) |
test_tools_slack.py | Bot vs webhook routing, Block Kit limits, errors |
test_scenario_aws_pulse_load.py | YAML parses, prompt_refs resolve, intra_tools registered |
test_scenario_aws_pulse.py | End-to-end orchestrator with stubbed AWS + Slack |
Phase 2 plans
- Real
SecretsManagerToolProviderbacking (currently stubbed). - Cross-tenant AWS via
sts:AssumeRole. - More connectors: GitHub, PagerDuty, CRM (Attio / HubSpot), ATS (Greenhouse / Lever).
- Admin UI for per-tenant secret seeding (currently CLI-only).