second first commit
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user