@integrity-labs/cloud-broker
v0.7.12
Published
Cloud Access Broker — MCP server that mints scoped, TTL-bounded cloud credentials per agent task. Ships AWS support (aws_request_access, aws_poll_grant, aws_release_access, aws_describe_scope, aws_preview_request, aws_get_credentials — STS AssumeRole unde
Downloads
1,617
Readme
@integrity-labs/cloud-broker
MCP server for the Augmented ephemeral cloud-access broker. Exposes per-cloud tool families that mint, poll, and release scoped, TTL-bounded credentials for a single task.
- AWS (STS
AssumeRoleunder the hood —aws_*tools, paired with theaws-clitoolkit or any AWS SDK). - GCP (IAM Credentials
generateAccessTokenunder the hood —gcp_*tools, paired with thegcloudtoolkit). Added in ENG-5074.
All tools are namespaced per cloud so additions are purely additive (ENG-4782). Azure / Cloudflare slots are reserved (azure_* / cf_*) but unimplemented.
See the PRDs (AWS, GCP) and toolkit docs (aws, gcloud) for the full design.
Tools
AWS
| Tool | Purpose |
|---|---|
| aws_describe_scope | Returns the team's resolved policy ceiling for an account — what the agent is allowed to ask for. Free, idempotent. |
| aws_preview_request | Dry-run a candidate request: auto_approve / route_to_approver / hard_deny. Writes nothing. |
| aws_request_access | Mint or queue a grant. On auto_approve you get secret_ref; on route_to_approver you get pending and a grant_id. |
| aws_poll_grant | Single-shot status check. Escape hatch — the broker pushes resolution via direct-chat. |
| aws_get_credentials | Fetch the AWS_* values for an active grant. Call after aws_request_access returns active or after the resolution-notification arrives. |
| aws_release_access | Voluntarily release a grant before TTL. Idempotent. |
| aws_check_approval_channel | Pre-flight Slack channel reachability check (ENG-4824). |
| aws_list_accounts | Live inventory of enrolled AWS accounts (ENG-5048). |
GCP
| Tool | Purpose |
|---|---|
| gcp_describe_scope | Resolved policy ceiling for a GCP project. Free, idempotent. |
| gcp_preview_request | Dry-run a candidate request. Writes nothing. |
| gcp_request_access | Mint or queue a grant. |
| gcp_poll_grant | Single-shot status check. Escape hatch. |
| gcp_get_credentials | Fetch the GCP access token for an active grant. |
| gcp_release_access | Voluntarily release a grant before TTL. Idempotent. |
| gcp_check_approval_channel | Pre-flight Slack channel reachability for the project's approval channel. |
| gcp_list_accounts | Live inventory of enrolled GCP projects. |
Supabase
Since ENG-7326 the agent holds no credential: it queries Supabase through server-side proxy tools that run the call under the grant's scope. supabase_rest (PostgREST, read + write) is the primary path; supabase_query runs read-only SQL. The operator stores, on the enrolment, the project's REST api_key (a new-format sb_secret_… key - legacy anon/service_role keys are disabled) and/or a scoped read-only Postgres connection string; the broker injects whichever the call needs at exec time. The scope (allowed_tables / read_only) is enforced at the broker, independent of RLS.
The legacy supabase_get_credentials still mints a short-lived Supabase JWT for direct PostgREST/psql use, but the proxy tools are preferred (they never hand the agent a secret, and they don't depend on RLS to enforce scope).
| Tool | Purpose |
|---|---|
| supabase_describe_scope | Resolved policy ceiling for a Supabase project (auto-approvable roles, schema ceiling, write gating). Free, idempotent. |
| supabase_preview_request | Dry-run a candidate request. Writes nothing. |
| supabase_request_access | Mint or queue a grant. service_role always routes to a human approver; privileged internal roles (postgres, supabase_admin, …) are hard-denied. |
| supabase_poll_grant | Single-shot status check. Escape hatch. |
| supabase_describe | Inspect what an active grant can do (role, allowed schemas/tables, read_only, and whether server-side SQL is enrolled) before querying. |
| supabase_rest | Primary. Call the project's PostgREST API (read + write) server-side under the grant's scope. The broker injects the api_key; the agent holds nothing. rpc/ is blocked. |
| supabase_query | Run read-only SQL server-side under the grant's scoped DB role (READ ONLY transaction + statement timeout). For system tables / joins REST can't express. Requires a DB connection string on the enrolment. |
| supabase_get_credentials | Legacy. Fetch a short-lived Supabase JWT + connection details for an active grant (direct PostgREST/psql). Prefer the proxy tools. |
| supabase_release_access | Voluntarily release a grant before TTL. Idempotent. (For the legacy JWT path the token stays valid until its exp.) |
| supabase_check_approval_channel | Pre-flight Slack channel reachability for the project's approval channel. |
| supabase_list_projects | Live inventory of enrolled Supabase projects. |
See docs/runbooks/supabase-broker-enrollment.md for operator enrollment (storing the REST api_key + DB connection string), the policy-ceiling shape, and the legacy augmented JWT-claim → RLS-policy enforcement guide.
Environment
| Var | Purpose |
|---|---|
| AGT_HOST | Augmented API base URL (e.g. https://api.augmented.example). |
| AGT_TOKEN | Pre-provisioned JWT (preferred). If set, used until expiry. |
| AGT_API_KEY | Host API key (tlk_…). Used to refresh the JWT via /host/exchange when AGT_TOKEN is missing or expired. |
At least one of AGT_TOKEN or AGT_API_KEY is required.
Worked example
An agent that needs to read one S3 object:
// 1. Optional: inspect the envelope before guessing.
aws_describe_scope({ account_id: "123456789012" })
// 2. Optional: dry-run.
aws_preview_request({
account_id: "123456789012",
actions: ["s3:GetObject", "s3:ListBucket"],
resources: ["arn:aws:s3:::reports/*", "arn:aws:s3:::reports"],
regions: ["us-east-1"],
ttl_seconds: 900
})
// → { "would": "auto_approve", "reason": null }
// 3. Mint.
aws_request_access({
agent_id: "11111111-1111-4111-8111-111111111111",
run_id: "22222222-2222-4222-8222-222222222222",
account_id: "123456789012",
actions: ["s3:GetObject", "s3:ListBucket"],
resources: ["arn:aws:s3:::reports/*", "arn:aws:s3:::reports"],
regions: ["us-east-1"],
ttl_seconds: 900,
reason: "fetch the daily reports CSV for the user"
})
// → { "grant_id": "...", "status": "active", "secret_ref": "secret_ref://aws/runs/<run_id>/<grant_id>", "expires_at": "..." }
// 4. Fetch the AWS_* values and use them.
aws_get_credentials({ grant_id: "..." })
// → { "grant_id": "...", "expires_at": "...", "credentials": { "access_key_id": "...", "secret_access_key": "...", "session_token": "..." } }
// Bash: AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... aws s3 cp s3://reports/today.csv -
// 5. (Optional) release early.
aws_release_access({ grant_id: "..." })If aws_request_access returns status: "pending", a human approver was paged. The broker pushes the resolution to you via direct-chat — save the grant_id, return control, and resume when the inbound message arrives. On denied, the denial_reason field explains why. aws_poll_grant is an escape hatch for explicit re-checks.
Worked example (GCP)
An agent reading one Cloud Storage object:
// 1. Inspect the envelope.
gcp_describe_scope({ project_id: "my-gcp-project" })
// 2. Dry-run.
gcp_preview_request({
project_id: "my-gcp-project",
permissions: ["storage.objects.get", "storage.objects.list"],
resources: ["projects/_/buckets/reports/objects/*", "projects/_/buckets/reports"],
ttl_seconds: 900
})
// → { "would": "auto_approve", "reason": null }
// 3. Mint.
gcp_request_access({
project_id: "my-gcp-project",
permissions: ["storage.objects.get", "storage.objects.list"],
resources: ["projects/_/buckets/reports/objects/*", "projects/_/buckets/reports"],
ttl_seconds: 900,
reason: "fetch the daily reports CSV for the user"
})
// → { "grant_id": "...", "status": "active", "secret_ref": "secret_ref://gcp/runs/<run_id>/<grant_id>", "expires_at": "..." }
// 4. Use the credentials. The runtime adapter resolves the secret_ref
// pointer to env vars at tool-call boundary — never substitute the
// raw token into shell history or logs.
// Inside an agent-spawned shell: gcloud storage cp gs://reports/today.csv -
// (CLOUDSDK_AUTH_ACCESS_TOKEN / GOOGLE_OAUTH_ACCESS_TOKEN are populated by
// the runtime; gcloud, gsutil, and bq pick them up automatically.)
//
// If you genuinely need the raw token (e.g. for non-gcloud HTTP calls),
// gcp_get_credentials({ grant_id: "..." }) returns it inline. The runtime
// MUST NOT echo the token to shell history; use it via a transient env
// only.
// 5. (Optional) release early.
gcp_release_access({ grant_id: "..." })Same pending / direct-chat-push resolution semantics as AWS. The runtime resolves secret_ref://gcp/runs/... to CLOUDSDK_AUTH_ACCESS_TOKEN and GOOGLE_OAUTH_ACCESS_TOKEN at exec time, so gcloud, gsutil, and bq all pick it up automatically without the agent ever handling the raw token.
Worked example (Supabase)
An agent that needs read-only access to two tables in public:
// 1. Inspect the envelope — which roles auto-approve, the schema ceiling, write gating.
supabase_describe_scope({ project_ref: "abcdefghijklmnopqrst" })
// 2. Dry-run.
supabase_preview_request({
project_ref: "abcdefghijklmnopqrst",
role: "authenticated",
allowed_schemas: ["public"],
allowed_tables: ["public.agents", "public.runs"], // optional — absent = all tables in allowed_schemas
read_only: true,
ttl_seconds: 900
})
// → { "would": "auto_approve", "reason": null }
// 3. Mint.
supabase_request_access({
project_ref: "abcdefghijklmnopqrst",
role: "authenticated",
allowed_schemas: ["public"],
allowed_tables: ["public.agents", "public.runs"],
read_only: true,
ttl_seconds: 900,
reason: "read the agents + runs tables to answer the user's status question"
})
// → { "grant_id": "...", "status": "active", "expires_at": "..." }
// `service_role` or write access (read_only:false, unless the project allows writes)
// returns "pending" instead and pages a human approver.
// 4. Learn your scope (recommended - also tells you if server-side SQL is enrolled).
supabase_describe({ grant_id: "..." })
// → { role, allowed_schemas, allowed_tables, read_only, sql_query: { available, ... } }
// 5. Query. You hold NO credential - the broker runs the call server-side and injects the key.
// REST (primary, read + write):
supabase_rest({ grant_id: "...", method: "GET", path: "agents?select=*&limit=10" })
// → { ok: true, status: 200, body: [ ... ] }
// Read-only SQL (system tables / joins REST can't express; needs a DB connection string enrolled):
supabase_query({ grant_id: "...", sql: "select count(*) from runs" })
// → { ok: true, rows: [{ count: 42 }], row_count: 1 }
// Both return { ok: false, error_code } on a denial - branch on error_code, don't treat it as a tool failure.
// 6. (Optional) release early.
supabase_release_access({ grant_id: "..." })With the proxy tools the grant's scope (allowed_tables / read_only) is enforced at the broker - read-only grants reject writes, calls outside allowed_tables are denied, and rpc/ is blocked - independent of RLS. (For the legacy supabase_get_credentials JWT path, scope instead rides the JWT's augmented claim and is only enforced if the project's RLS policies read it - see the enrollment runbook for those policy patterns.) Same pending / direct-chat-push resolution semantics as AWS.
Running locally
pnpm --filter @integrity-labs/cloud-broker build
AGT_HOST=http://api.agt.localhost:1355 \
AGT_AGENT_ID=<your-agent-uuid> \
AGT_RUN_ID=<your-run-uuid> \
AGT_TOKEN=$YOUR_JWT \
node packages/cloud-broker/dist/index.jsThe server speaks MCP over stdio. Wire it into your runtime adapter's .mcp.json — the toolkit ID is cloud-broker (registered in packages/supabase/seeds/toolkit-definitions.json).
Notes
- The MCP response strips inline credentials from
aws_request_access— only thesecret_refpointer reaches the LLM. The runtime adapter resolves the pointer at AWS-SDK call time (or viaaws_get_credentials) so credentials never enter the agent's transcript. - Per PRD §6.3 v1 ships TTL-bounded revocation only.
aws_release_accessmarks the grantrevokedin the broker DB and tears down thesecret_ref, but in-flight STS sessions remain valid in AWS until their TTL. v1.1 closes that gap with theaws:TokenIssueTimehard-revoke pattern. - 0.6.0 hard-renamed the tools to the
aws_*namespace (ENG-4782). Agents on cloud-broker ≤ 0.5.0 will see "tool not found" errors from the broker until they're re-provisioned with the new tool names. Re-runagt agent provisionfor any agent that has the cloud-broker MCP wired in.
