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
InvokeAgentRuntimedata-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.
- Packages —
services/bedrockagentcorecontrol/andservices/bedrockagentcore/:*Controller.java(thin JAX-RS),*Service.java(@ApplicationScoped),model/POJOs. - Controller — class
@Path("/"),@Produces/@Consumes(APPLICATION_JSON); full path per method. InjectService,RegionResolver,ObjectMapper. Take the raw body as aStringand parse withObjectMapper.readTree. First line:regionResolver.resolveRegion(headers). - Service registration — one
descriptor(...)line incore/common/ResolvedServiceCatalog.java(+ import the controller). Template: thebedrockruntime/pipesentries. 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). - Config —
config/EmulatorConfig.java: add aServicesConfigaccessor + nested interface (@WithDefault("true") boolean enabled();). Env:FLOCI_SERVICES_BEDROCK_AGENT_CORE_CONTROL_ENABLEDandFLOCI_SERVICES_BEDROCK_AGENT_CORE_ENABLED. - Storage —
StorageFactory.create("bedrockagentcore", "<file>.json", new TypeReference<Map<String,V>>(){}). Account partitioning is automatic (AccountAwareStorageBackend). Two-constructor DI (public@Injectbuilds 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)). - ARNs — only via
regionResolver.buildArn("bedrock-agentcore", region, resource); account viaregionResolver.getAccountId(). - Errors — throw
AwsException(code, message, status). The globalAwsExceptionMapperbuilds the JSON body but does NOT setX-Amzn-Errortype, which SDK v2 restJson1 needs. Follow theservices/rdsdata/RdsDataController/services/batch/BatchControllerpattern: returnResponse.status(s).header("X-Amzn-Errortype", code).entity(new AwsErrorResponse(code, message)).build(). Codes:ValidationException(400),ResourceNotFoundException(404),ConflictException(409). - Reflection —
@io.quarkus.runtime.annotations.RegisterForReflectionon every model/DTO. - Pagination — copy the
services/acm/AcmServicecursor pattern (sort by ARN, Base64(JSON)nextTokencarrying the last ARN,skip/limit; bad token →ValidationException). - clientToken — accept-and-ignore (scheduler pattern); key uniqueness yields
ConflictException. - Tests — unit
*ServiceTest.java; integration*IntegrationTest.javaas@QuarkusTest+ RestAssured hitting routes directly (NOT an SDK client — that goes incompatibility-tests/).@TestMethodOrder(OrderAnnotation.class)for lifecycles. NoAuthorizationheader → regionus-east-1, account000000000000. - Docs —
docs/services/bedrock-agentcore.md, register intools/docs/services.yaml, runmake docs-sync(CI runsmake docs-check). Add tomkdocs.ymlnav.
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)
- Fetch the API page; record method, path (trailing slashes), success code, required fields, response shape, ID/ARN patterns.
- Add the handler method to the matching controller (or a new
services/<family>/package), classified A/B/C. - New resource → add a
StorageFactorystore, a@RegisterForReflectionmodel, and (if taggable) extend thebedrock-agentcoreTagHandler. A new operation on an existing service needs no newdescriptor(...)— only a new service does. - 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.