Skip to main content

Call AWS and GCP APIs from an Agent

A static Authorization header on an HTTP tool does not cover AWS (Signature Version 4 HMAC per request over method, URL, headers and body) or Google Cloud (short-lived OAuth 2.0 access token minted from a signed service account assertion, expiring in about an hour).

execute.auth is an authentication strategy on the http transport, so guardrails, approvals, body_mode and output_mapping behave as usual. You build a SigV4-signed S3 tool and a GCP service-account BigQuery tool, hand one to an agent, and read the failure modes.

Prerequisites​

  • SOAT running locally (Quick Start); Key Concepts.
  • SECRETS_ENCRYPTION_KEY set — secrets refuses to start without it (Configuration).
  • Ollama with qwen2.5:0.5b; other providers: Connect Third-Party LLMs.
  • CLI or SDK; server at http://localhost:5047.
  • Real cloud credentials: an AWS key pair with s3:GetObject on one bucket, and/or a GCP service account key file with BigQuery access. Only Steps 3 and 7 call the live APIs.
export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in and create a project​

See Projects and Users.

ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN

PROJECT_ID=$(soat create-project --name "Cloud Tools" | jq -r '.id')
echo "PROJECT_ID: $PROJECT_ID"

Step 2 — Store the AWS credentials as secrets​

GET /tools/{tool_id} echoes execute verbatim to every project reader, so store credentials as secrets and reference them; the reference is resolved only immediately before signing.

AWS_KEY_ID_SECRET=$(soat create-secret --project-id "$PROJECT_ID" \
--name "aws-access-key-id" --value "$AWS_ACCESS_KEY_ID" | jq -r '.id')

AWS_SECRET_SECRET=$(soat create-secret --project-id "$PROJECT_ID" \
--name "aws-secret-access-key" --value "$AWS_SECRET_ACCESS_KEY" | jq -r '.id')

echo "$AWS_KEY_ID_SECRET / $AWS_SECRET_SECRET"

Step 3 — Create a SigV4-signed S3 tool​

auth.type: aws_sigv4 requires region, service, access_key_id and secret_access_key; optional session_token is sent as X-Amz-Security-Token. Replace my-bucket with a bucket your key can read. See Tools — Computed credentials.

S3_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "get-s3-object" \
--type http \
--description "Reads an object from the reports bucket" \
--parameters '{"type":"object","properties":{"key":{"type":"string","description":"Object key inside the bucket"}},"required":["key"]}' \
--execute '{
"url": "https://my-bucket.s3.us-east-1.amazonaws.com/{key}",
"method": "GET",
"auth": {
"type": "aws_sigv4",
"region": "us-east-1",
"service": "s3",
"access_key_id": "{{secret:'"$AWS_KEY_ID_SECRET"'}}",
"secret_access_key": "{{secret:'"$AWS_SECRET_SECRET"'}}"
}
}' | jq -r '.id')

soat call-tool --tool-id "$S3_TOOL_ID" --input '{"key":"reports/2026-08.txt"}'

SOAT sends Authorization: AWS4-HMAC-SHA256 …, X-Amz-Date and X-Amz-Content-Sha256 where applicable, signing last over the final request. Signed headers: Tools — Computed credentials.


Step 4 — Let an agent call it​

Attach it like any other HTTP tool — Agents — Tool Bindings.

AI_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Local Ollama" \
--provider "ollama" \
--default-model "qwen2.5:0.5b" | jq -r '.id')

AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Reports Analyst" \
--instructions "You read report files from object storage. Use get-s3-object when asked about a report." \
--tool-bindings '[{"tool_id":"'"$S3_TOOL_ID"'"}]' | jq -r '.id')

soat create-agent-generation --wait true --agent-id "$AGENT_ID" \
--messages '[{"role":"user","content":"Summarize reports/2026-08.txt in one sentence."}]' \
| jq '{status}'

The model sees parameters only; signing happens server-side after the model has chosen its arguments.


Step 5 — One combination is refused on write​

aws_sigv4 cannot be combined with body_mode: "multipart": fetch generates the multipart body and boundary, so the bytes SigV4 must hash are unknown at signing time. SOAT rejects the combination on create and update with 400 VALIDATION_FAILED (Tools).

# → expect-fail
soat create-tool --project-id "$PROJECT_ID" --name "bad-upload" --type http --description "Rejected on write" --parameters '{"type":"object","properties":{"file":{"type":"string"}}}' --execute '{"url":"https://my-bucket.s3.us-east-1.amazonaws.com/upload","method":"POST","body_mode":"multipart","auth":{"type":"aws_sigv4","region":"us-east-1","service":"s3","access_key_id":"{{secret:'"$AWS_KEY_ID_SECRET"'}}","secret_access_key":"{{secret:'"$AWS_SECRET_SECRET"'}}"}}'

Every auth field is validated on write (a missing region, an unknown type) and during validate-formation, before a formation apply starts.


Step 6 — Store the GCP service account key​

The whole key file JSON is one secret value; jq -Rs slurps the file into a single JSON string for --value.

GCP_SECRET_ID=$(soat create-secret --project-id "$PROJECT_ID" \
--name "gcp-service-account" \
--value "$(cat ./service-account.json)" | jq -r '.id')

echo "GCP_SECRET_ID: $GCP_SECRET_ID"

Step 7 — Create a BigQuery tool​

auth.type: gcp_service_account requires credentials (the key file JSON as a string) and scopes. SOAT signs the assertion, exchanges it for an access token, and sends Authorization: Bearer <access token> (Tools).

BQ_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "create-bigquery-job" \
--type http \
--description "Submits a BigQuery query job" \
--parameters '{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}' \
--execute '{
"url": "https://bigquery.googleapis.com/bigquery/v2/projects/my-gcp-project/jobs",
"method": "POST",
"auth": {
"type": "gcp_service_account",
"credentials": "{{secret:'"$GCP_SECRET_ID"'}}",
"scopes": ["https://www.googleapis.com/auth/bigquery"]
}
}' | jq -r '.id')

soat call-tool --tool-id "$BQ_TOOL_ID" \
--input '{"configuration":{"query":{"query":"SELECT 1","useLegacySql":false}}}'

Tokens are cached per service account, token endpoint and scope set, and refreshed shortly before expiry.

note

input becomes the request body verbatim; use the target API's casing (useLegacySql, not use_legacy_sql). See Tools — Request body encoding.


Step 8 — Read the failure modes​

  • 502 TOOL_AUTH_FAILED — the credential could not be produced; the request never reached the target.
  • 502 TOOL_HTTP_ERROR — the target rejected the call.
  • 400 VALIDATION_FAILED — malformed auth config caught on write.

Full semantics, including signature and path-encoding gotchas: Tools — Computed credentials.


What's next​

Tools — Computed credentials, Secrets, Gate a Tool with Guardrails.