Skip to content

Design: Amazon Bedrock AgentCore emulation

Status: implemented and actively expanded. Tracked by the AgentCore epic and follow-up coverage work on this fork.

Goal

Emulate Amazon Bedrock AgentCore well enough that AWS SDK/CLI clients can create, read, update, delete, list, and invoke agent runtimes against Floci on port 4566. No real agent execution: the control plane maintains a stateful registry, the data plane returns a canned response.

Designed clean-room from the AWS public API reference only.

Two AWS services are involved:

Service API version SigV4 signing name Protocol Role
bedrock-agentcore-control 2023-06-05 bedrock-agentcore REST JSON (restJson1) stateful CRUD registry
bedrock-agentcore 2024-02-28 bedrock-agentcore REST JSON (binary payload) InvokeAgentRuntime canned-response stub

Implemented scope

  • Agent Runtime CRUD and versioning (bedrock-agentcore-control)
  • Agent Runtime Endpoints
  • InvokeAgentRuntime data-plane canned-response stub (bedrock-agentcore)
  • Tagging + Workload Identity
  • Gateway + Gateway Target + Gateway Rule primitives
  • Memory primitives
  • Browser + Browser Profile primitives
  • Code Interpreter primitives
  • API key + OAuth2 credential providers
  • Resource policies

Still out of scope: real inference, streaming invoke semantics, policy engine, registry, payment, dataset, evaluator, harness, the broader token-vault data plane, and IAM enforcement. These remain candidates for future coverage expansion.

Floci integration conventions

Follows AGENTS.md + CONTRIBUTING.md. Verified against the current codebase.

  1. Packages — services/bedrockagentcorecontrol/ and services/bedrockagentcore/: *Controller.java (thin JAX-RS), *Service.java (@ApplicationScoped), model/ POJOs.
  2. Controller — class @Path("/"), @Produces/@Consumes(APPLICATION_JSON); full path per method. Inject Service, RegionResolver, ObjectMapper. Take the raw body as a String and parse with ObjectMapper.readTree. First line: regionResolver.resolveRegion(headers).
  3. Service registration — one descriptor(...) line in core/common/ResolvedServiceCatalog.java (+ import the controller). Template: the bedrockruntime/pipes entries. For a REST-JSON service: defaultProtocol/supportedProtocols = ServiceProtocol.REST_JSON; targetPrefixes = Set.of(); credentialScopes = Set.of("bedrock-agentcore") (SigV4 signing name — verify against the SDK model); resourceClasses = Set.of(<Controller>.class) (required for enable/disable); storageKey = "bedrockagentcore" (control plane) / null (stateless data plane).
  4. Config — config/EmulatorConfig.java: add a ServicesConfig accessor + nested interface (@WithDefault("true") boolean enabled();). Env: FLOCI_SERVICES_BEDROCK_AGENT_CORE_CONTROL_ENABLED and FLOCI_SERVICES_BEDROCK_AGENT_CORE_ENABLED.
  5. Storage — StorageFactory.create("bedrockagentcore", "<file>.json", new TypeReference<Map<String,V>>(){}). Account partitioning is automatic (AccountAwareStorageBackend). Two-constructor DI (public @Inject builds the store; package-private takes the store for unit tests). Keys: "<entity>:<region>:<id>". Not-found → store.get(k).orElseThrow(() -> new AwsException("ResourceNotFoundException", msg, 404)).
  6. ARNs — only via regionResolver.buildArn("bedrock-agentcore", region, resource); account via regionResolver.getAccountId().
  7. Errors — throw AwsException(code, message, status). The global AwsExceptionMapper builds the JSON body but does NOT set X-Amzn-Errortype, which SDK v2 restJson1 needs. Follow the services/rdsdata/RdsDataController / services/batch/BatchController pattern: return Response.status(s).header("X-Amzn-Errortype", code).entity(new AwsErrorResponse(code, message)).build(). Codes: ValidationException (400), ResourceNotFoundException (404), ConflictException (409).
  8. Reflection — @io.quarkus.runtime.annotations.RegisterForReflection on every model/DTO.
  9. Pagination — copy the services/acm/AcmService cursor pattern (sort by ARN, Base64(JSON) nextToken carrying the last ARN, skip/limit; bad token → ValidationException).
  10. clientToken — accept-and-ignore (scheduler pattern); key uniqueness yields ConflictException.
  11. Tests — unit *ServiceTest.java; integration *IntegrationTest.java as @QuarkusTest + RestAssured hitting routes directly (NOT an SDK client — that goes in compatibility-tests/). @TestMethodOrder(OrderAnnotation.class) for lifecycles. No Authorization header → region us-east-1, account 000000000000.
  12. Docs — docs/services/bedrock-agentcore.md, register in tools/docs/services.yaml, run make docs-sync (CI runs make docs-check). Add to mkdocs.yml nav.

Routing

No existing service claims /runtimes (verified). Control-plane and data-plane controllers both live under /runtimes on distinct method paths/verbs — safe under Floci's path-based JAX-RS dispatch.

Pinned wire contracts

All verified against the AWS API reference. Note verbs and trailing slashes.

Agent Runtime (control plane)

Operation Method + Path Success Notes
CreateAgentRuntime PUT /runtimes/ 202 required: agentRuntimeArtifact, agentRuntimeName ([a-zA-Z][a-zA-Z0-9_]{0,47}), networkConfiguration, roleArn
GetAgentRuntime GET /runtimes/{agentRuntimeId}/?version={v} 200 full runtime shape
ListAgentRuntimes POST /runtimes/?maxResults={n}&nextToken={t} 200 POST, no body; agentRuntimes[] + nextToken
UpdateAgentRuntime PUT /runtimes/{agentRuntimeId}/ 202 required: agentRuntimeArtifact, networkConfiguration, roleArn; bumps version
ListAgentRuntimeVersions POST /runtimes/{agentRuntimeId}/versions/?maxResults=&nextToken= 200 POST, no body
DeleteAgentRuntime DELETE /runtimes/{agentRuntimeId}/?clientToken={t} 202 body {agentRuntimeId, status: DELETING}

Identity: agentRuntimeId = <name>-<10 [a-zA-Z0-9]> ([a-zA-Z][a-zA-Z0-9_]{0,99}-[a-zA-Z0-9]{10}). ARN embeds a fresh UUID + version: arn:aws:bedrock-agentcore:<region>:<account>:agent/<uuid>:<version>. agentRuntimeVersion is a string int starting "1", incremented on Update. status = READY.

Agent Runtime Endpoint (control plane)

Operation Method + Path Success
CreateAgentRuntimeEndpoint PUT /runtimes/{agentRuntimeId}/runtime-endpoints/ 202
GetAgentRuntimeEndpoint GET /runtimes/{agentRuntimeId}/runtime-endpoints/{endpointName}/ 200
UpdateAgentRuntimeEndpoint PUT /runtimes/{agentRuntimeId}/runtime-endpoints/{endpointName}/ 202
DeleteAgentRuntimeEndpoint DELETE /runtimes/{agentRuntimeId}/runtime-endpoints/{endpointName}/?clientToken={t} 202
ListAgentRuntimeEndpoints POST /runtimes/{agentRuntimeId}/runtime-endpoints/?maxResults=&nextToken= 200

Endpoint ARN arn:aws:bedrock-agentcore:<region>:<account>:agentEndpoint/<uuid> (no version). Create body: name (required), agentRuntimeVersion/description/clientToken/tags (optional). Fields: liveVersion (serving) vs targetVersion (requested). A DEFAULT endpoint is auto-created on runtime create so an unqualified invoke resolves.

InvokeAgentRuntime (data plane)

POST /runtimes/{agentRuntimeArn}/invocations?accountId={id}&qualifier={q} → 200. Payload is opaque binary (≤100 MB) — take byte[], do not parse (mirror BedrockRuntimeController.invokeModel, @Consumes(WILDCARD)). Echo X-Amzn-Bedrock-AgentCore-Runtime-Session-Id; return canned JSON, default {"output":"yes"}. The ARN in the path is URL-encoded — use @Path("/runtimes/{agentRuntimeArn:.+}/invocations") and test with an SDK-encoded ARN. Streaming is out of scope; the stub returns a single non-streaming 200.

Tagging (control plane) — matches Floci's shared /tags/ route

Operation Method + Path Success
TagResource POST /tags/{resourceArn} 204
UntagResource DELETE /tags/{resourceArn}?tagKeys={k1}&tagKeys={k2} 204
ListTagsForResource GET /tags/{resourceArn} 200

Implement a TagHandler (not a controller): serviceKey()="bedrock-agentcore", tagsBodyKey()="tags", tagsBodyIsList()=false, tagKeysQueryName()="tagKeys". Body {"tags":{"k":"v"}}. Template: services/scheduler/SchedulerTagHandler.java. Taggable resources — dispatched by the ARN's resource segment: agent/ (runtimes), gateway/, and memory/; other AgentCore ARNs get ValidationException.

Workload Identity (control plane) — RPC-in-path

POST /identities/<Operation> with all input in the body: CreateWorkloadIdentity (201), GetWorkloadIdentity (200), UpdateWorkloadIdentity (200), DeleteWorkloadIdentity (204), ListWorkloadIdentities (200, maxResults 1–20). Required body: name ([A-Za-z0-9_.-]+, 3–255). workloadIdentityArn has no published regex (≤1024) — synthesize arn:aws:bedrock-agentcore:<region>:<account>:workload-identity-directory/default/workload-identity/<name>-<suffix>. CreateAgentRuntime auto-creates one and sets the runtime's workloadIdentityArn.

Gateway + Gateway Target (control plane)

Standard REST, trailing slashes; identifier is gatewayId (not ARN): POST /gateways/ (202), GET|PUT|DELETE /gateways/{gatewayIdentifier}/, GET /gateways/?maxResults=&nextToken=. Targets nested: POST /gateways/{gatewayIdentifier}/targets/, GET|PUT|DELETE /gateways/{gatewayIdentifier}/targets/{targetId}/, GET /gateways/{gatewayIdentifier}/targets/?.... Gateway ARN arn:aws...:bedrock-agentcore:<region>:<account>:gateway/([0-9a-z][-]?){1,48}-[a-z0-9]{10}; gatewayId ([0-9a-z][-]?){1,100}-[0-9a-z]{10}; targetId [0-9a-zA-Z]{10} (targets have no own ARN — return the parent gatewayArn). Mutations return 202. protocolType: gateway MCP, target MCP|HTTP.

Memory (control plane) — action-suffix REST

POST /memories/create (202, required name + eventExpiryDuration int 3–365; optional description, encryptionKeyArn, memoryExecutionRoleArn, tags map), GET /memories/{memoryId}/details?view={view} (200), PUT /memories/{memoryId}/update (202, accepts description, eventExpiryDuration 3–365, memoryExecutionRoleArn — not encryptionKeyArn), DELETE /memories/{memoryId}/delete?clientToken={t} (202), POST /memories/?maxResults=&nextToken= (200 list). memoryId [a-zA-Z][a-zA-Z0-9-_]{0,99}-[a-zA-Z0-9]{10}; ARN unpublished — synthesize arn:aws:bedrock-agentcore:<region>:<account>:memory/<memoryId>. status CREATING|ACTIVE|FAILED|DELETING|UPDATING → return ACTIVE. clientToken is a body field on Create/Update but a query param on Delete. The Memory response shape has no tags field — tags surface only through ListTagsForResource.

Upgrade path: extending coverage

The AgentCore control plane is much broader than the currently implemented subset. Additional operations can be added incrementally without rework because they follow the same Floci recipe.

Three wire-protocol styles coexist in this one service

Classify each new operation before writing the controller:

Style Shape Examples JAX-RS handling
A. Standard REST resource in path, verb = HTTP method Runtimes, Endpoints, Gateways, Targets @GET/@PUT/@POST/@DELETE on the path. Several list ops are POST, not GET.
B. RPC-in-path operation name is a path segment; input in body Workload Identity (/identities/<Op>), Credential Providers, Token Vault one @POST per /identities/<Op>
C. Action-suffix REST resource path + literal action suffix Memory (/memories/create, /{id}/details, /update, /delete) REST verb + exact suffix in @Path

Always confirm the exact Method + Path on each operation's API_<Op>.html page — never assume plain REST.

Repeatable recipe (every future op)

  1. Fetch the API page; record method, path (trailing slashes), success code, required fields, response shape, ID/ARN patterns.
  2. Add the handler method to the matching controller (or a new services/<family>/ package), classified A/B/C.
  3. New resource → add a StorageFactory store, a @RegisterForReflection model, and (if taggable) extend the bedrock-agentcore TagHandler. A new operation on an existing service needs no new descriptor(...) — only a new service does.
  4. Add unit + RestAssured integration tests; run make docs-sync.

Full operation catalog & status

"Implemented" means the operation family has working Floci coverage. "Future" identifies remaining families that can use the same protocol-first implementation recipe.

Resource family Operations Style Status
Agent Runtime Create/Get/Update/Delete/List + ListVersions A Implemented
Agent Runtime Endpoint Create/Get/Update/Delete/List A Implemented
InvokeAgentRuntime (data plane) Invoke A (binary) Implemented (canned-response stub)
Tagging Tag/Untag/ListTagsForResource A (/tags/{arn}) Implemented
Workload Identity Create/Get/Update/Delete/List B Implemented
Gateway + Gateway Target Create/Get/Update/Delete/List (x2) A Implemented
Memory Create/Get/Update/Delete/List C Implemented
Gateway Rule Create/Get/Update/Delete/List A Implemented
API Key + OAuth2 Credential Providers Create/Get/Update/Delete/List B Implemented
Browser / Browser Profile / Code Interpreter Create/Get/Delete/List A Implemented
Resource Policy Get/Put/Delete A Implemented
Payment Credential Providers Create/Get/Update/Delete/List B Future
Policy / Policy Engine / Policy Generation Create/Get/Update/Delete/List + Start/Summary mixed Future
Registry / Registry Record Create/Get/Update/Delete/List + Submit mixed Future
Dataset / Dataset Version / Examples Create/Get/Update/Delete/List + Add/Delete examples mixed Future
Evaluator / Online Evaluation Config Create/Get/Update/Delete/List A Future
Harness / Harness Endpoint Create/Get/Update/Delete/List A Future
Configuration Bundle Create/Get/Update/Delete/List (+ versions) A Future
Payment Manager / Connector Create/Get/Update/Delete/List mixed Future
Broader Token Vault operations Additional token/secret management operations mixed Future

Future rows are candidates for follow-up coverage work using the same investigate, implement, validate, review, and commit cycle.