| service | sagemakerruntime | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sdk_module | aws-sdk-go-v2/service/sagemakerruntime@v1.49.3 | ||||||||||||||||||||||||||||||||||||
| last_audit_commit | ab7ac08a7 | ||||||||||||||||||||||||||||||||||||
| last_audit_date | 2026-10-10 | ||||||||||||||||||||||||||||||||||||
| overall | A | ||||||||||||||||||||||||||||||||||||
| ops |
|
||||||||||||||||||||||||||||||||||||
| families |
|
||||||||||||||||||||||||||||||||||||
| gaps | |||||||||||||||||||||||||||||||||||||
| items_still_open |
|
||||||||||||||||||||||||||||||||||||
| structural_gaps |
|
||||||||||||||||||||||||||||||||||||
| deferred | |||||||||||||||||||||||||||||||||||||
| leaks |
|
InvokeEndpoint, InvokeEndpointWithResponseStream and InvokeEndpointAsync forward to the model container run by the services/sagemaker engine (ContainerInvoker, wired per region by Provider.Init). The earlier fixed bodies (mock response from Gopherstack, mock streaming response from Gopherstack) and the s3://sagemaker-runtime-mock/... async locations are gone; with the engine off the ops return 503 ServiceUnavailable. Streaming emits one PayloadPart event per chunk the container writes. Tests use a stub ContainerInvoker (fake_invoker_test.go) and invoke_errors_test.go proves the no-fabrication and ModelError shapes through the SDK; the container path itself is covered by services/sagemaker/engine_*_test.go and the docker test in internal/app.
2026-10-10 (realism pass): the EndpointName/InService check never ran in a real server -- Provider.Init looked up the SageMaker handler before internal/app/cli.go stored it, so the lookup stayed nil and unknown endpoints returned 200. The lookup is now resolved per call (lazyEndpointLookup). Also new: unknown TargetVariant gives ValidationError; header constraints from the service model (CustomAttributes <=1024, InferenceId <=64, InputLocation must be an s3/https URI, RequestTTLSeconds 60..21600, InvocationTimeoutSeconds 1..3600, SessionId and TargetVariant patterns) give ValidationError. Not emulated: ModelError (no model container).
Protocol: restjson1. Three ops, disambiguated purely by path suffix (no
X-Amz-Target header): /endpoints/{EndpointName}/invocations,
/endpoints/{EndpointName}/async-invocations,
/endpoints/{EndpointName}/invocations-response-stream. EndpointName is a
path segment (extractEndpointName cuts on the first / after the
/endpoints/ prefix), never a query param or header.
Bugs found and fixed this pass (2026-07-24; see git diff for exact lines):
-
EndpointName existence/InService was never validated. Real AWS returns
ValidationError("Endpoint of account not found.") for both an unknown EndpointName and one that has not yet reached InService (confirmed against real-world AWS error reports: an endpoint stillCreatingis reported the same way, since the runtime's routing table only serves InService endpoints). The previous audit correctly identified this as a genuine gap but deferred it as requiring cross-service backend wiring "out of scope for a same-service-only pass." This pass wired it:endpoint_lookup.godefines a minimalEndpointLookupinterface (DescribeEndpoint(ctx, name) (*sagemaker.Endpoint, error)), already satisfied by*sagemaker.InMemoryBackendwith zero changes toservices/sagemaker(itsDescribeEndpointwas already an exported, lock-safe read accessor).provider.go'swireEndpointLookupconnects the two atProvider.Inittime via a privatesagemakerHandlerProviderinterface type-asserted againstctx.Config, following the exact precedent ofservices/cloudwatchlogs/provider.go'ss3HandlerProvider/wireExportSink(NOT the much largerservices/cloudformation-styleBackendsProvider, which would have been overkill for a single dependency). When unwired (every pre-existing test in this package constructs a bareNewInMemoryBackend), validation is a no-op, preserving prior behaviour for standalone use. -
NewSessionId'sExpires=attribute used the wrong wire format.handler.goformatted it as an RFC 1123 HTTP-date ("Mon, 02 Jan 2006 15:04:05 GMT"), but the SDK model'sNewSessionResponseHeadershape declares the pattern^[a-zA-Z0-9](-*[a-zA-Z0-9])*;\sExpires=[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$(confirmed against botocore'ssagemaker-runtimeservice-2.json) -- i.e. an RFC 3339 timestamp with no fractional seconds. A client validating or parsing the header against that pattern would have rejected gopherstack's previous output. Fixed tosession.ExpiresAt.UTC().Format("2006-01-02T15:04:05Z"); seeTestNewSessionHeaderFormat_MatchesSDKPattern. -
ClosedSessionIdwas never emitted; sessionExpiresAtwas tracked but never enforced.TouchSessionnow returns aSessionTouchOutcome: if the touched session has passed itsExpiresAt, it is evicted andClosedSessionIDis set, whichInvokeEndpointsurfaces as theX-Amzn-SageMaker-Closed-Session-Idresponse header (matchingInvokeEndpointOutput.ClosedSessionId).InvokeEndpointWithResponseStreamcallsTouchSessiontoo but discards the outcome, sinceInvokeEndpointWithResponseStreamOutputhas noClosedSessionIdmember in the SDK model -- onlyInvokeEndpointOutputdoes. A test-onlyExpireSessionForTesthelper (export_test.go, followingservices/sagemaker/export_test.go's precedent) forces a session'sExpiresAtinto the past deterministically, since AWS provides no real API to trigger session closure (it's entirely model/container-driven).
Traps for the next auditor (looks-wrong-but-correct):
handleInvokeEndpointAsyncaccepts a client-suppliedX-Amzn-Sagemaker-Outputlocationrequest header and uses it verbatim. This header does not exist in the realInvokeEndpointAsyncInputwire shape (checkedserializers.go'sawsRestjson1_serializeOpHttpBindingsInvokeEndpointAsyncInput: the only headers bound are Accept/Content-Type/CustomAttributes/Filename/ InferenceId/InputLocation/InvocationTimeoutSeconds/RequestTTLSeconds/ S3OutputPathExtension). It's a harmless gopherstack-only testing backdoor: real SDK callers never send it, so they always take the "generate a fake S3 path" branch. Not a parity bug, just don't mistake it for a real SDK field.X-Amzn-Sagemaker-*header constants use lowercase "maker" in the Go source (e.g.headerCustomAttributes = "X-Amzn-Sagemaker-Custom-Attributes") instead of the docs'X-Amzn-SageMaker-*casing. This is not a bug: Go'snet/httpcanonicalizes every header name viatextproto.CanonicalMIMEHeaderKeyon bothSetandGet, which title-cases each hyphen-separated segment (SageMaker->Sagemaker) regardless of the literal casing used in source -- so the wire bytes and the client'sHeader. Getlookups are unaffected either way. Confirmed by cross-referencingdeserializers.go's literalresponse.Header.Values("X-Amzn-SageMaker-...")calls, which resolve to the same canonical key. This also holds for botocore'sservice-2.json, which even declaresInvokeEndpointOutput. InvokedProductionVariant'slocationNameas"x-Amzn-Invoked-Production- Variant"(lowercase leadingx) -- same reasoning, same non-issue.InvokeEndpointWithResponseStream'sSessionIdheader is only ever passed toTouchSession, neverStartSession-- this is correct: the SDK doc forInvokeEndpointWithResponseStreamInput.SessionIdexplicitly states "You can't create a stateful session by using the InvokeEndpointWithResponseStream action."pathToOperation's default"Unknown"branch (unmatched path suffix under/endpoints/) returns a non-SDK"UnknownOperationException"__type. Left as-is: unreachable by any real SDK call (the SDK only ever generates the three known suffixes), so it's defensive-only code, not a wire-parity concern.TargetModel/TargetContainerHostname/EnableExplanations/InferenceComponentNamerequest headers (confirmed againstservice-2.json:X-Amzn-SageMaker-Target-Model,X-Amzn-SageMaker-Target-Container-Hostname,X-Amzn-SageMaker-Enable-Explanations,X-Amzn-SageMaker-Inference-Component) are intentionally accepted but not consumed by the handler: none of them appear in any operation's output shape, so there is nothing wire-visible to get wrong by ignoring them -- they only affect real model-container routing/behaviour, which this emulator does not simulate (deterministic synthetic responses are the documented parity bar for this service).InvokeEndpointWithResponseStreamInput.ContentTypeis bound to the plainContent-Typeheader (same as the sync op), while its siblingAcceptis bound toX-Amzn-SageMaker-Accept-- an intentional asymmetry in the real SDK model, not a copy-paste error to "fix": the output side is asymmetric too (InvokeEndpointWithResponseStreamOutput.ContentTypebinds toX-Amzn-SageMaker-Content-Type, unlike the sync op's plainContent-Typeoutput). Confirmed againstservice-2.json;handler.go'sheaderAsyncAccept/headerStreamContentTypeconstants already matched this correctly before this pass.ModelError/ModelNotReadyException/ServiceUnavailable/InternalDependencyException/ModelStreamError/InternalStreamFailureerror paths have no request-side trigger (there is no real model to fail).InternalStreamFailure/ModelStreamErroradditionally have nohttpStatusCodeinservice-2.json'serrortrait at all (unlike the other six, which map to 530/500/424/429/503/400 respectively) -- confirmed they are delivered as in-band event-stream exception events, not top-level HTTP error responses, which is why they're absent from the status-code table. Chaos-injection (ChaosServiceName/ChaosOperations/ ChaosRegions) is wired at the framework level (pkgs/chaos) and can inject arbitrary faults independent of handler code, so this is not a gap in the handler itself -- just no organic way to hit these paths without chaos rules configured.
Verified correct without changes: InvokeEndpoint request/response header
binding (Accept/ContentType negotiation, CustomAttributes echo,
X-Amzn-Invoked-Production-Variant default "AllTraffic" / target-variant
override), the event-stream binary frame format for
InvokeEndpointWithResponseStream (prelude length/headers-length/CRC32,
:message-type/:event-type/:content-type headers, payload-only
PayloadPart.Bytes), InvokeEndpointAsyncOutput's wire shape
(InferenceId is a plain JSON body field with no location binding in the
SDK model -- unlike OutputLocation/FailureLocation, which are
header-bound -- and gopherstack's JSON-body-plus-headers response shape
matches exactly), FIFO eviction bounds for sessions/async-invocations/
invocation-history, and Handler.Snapshot/Restore delegation to the backend
(all exercised by persistence_test.go).
Re-derived every wire shape from scratch against the pinned
aws-sdk-go-v2/service/sagemakerruntime@v1.43.4 source
(serializers.go/deserializers.go/api_op_*.go/types/errors.go),
independent of this file's prior claims. Zero bugs found. This service
is structurally almost immune to the wrapper-key/nested-shape bug class the
rest of this campaign hunts: all three ops are pure HTTP header/URI binding
plus an opaque []byte payload -- InvokeEndpointInput/Output,
InvokeEndpointAsyncInput/Output, and
InvokeEndpointWithResponseStreamInput/Output have no nested structs, no
lists, no maps, and (ModelError's three extra scalar fields aside) no
non-flat error shapes to mis-nest a key under.
Verified line-for-line against the SDK source, all matching gopherstack's existing implementation exactly (see file:line citations already in this document from the 2026-07-24 pass, re-confirmed this pass):
- Every request header binding, all three ops
(
serializers.go:82-149,215-273,343-402), including theInvokeEndpointWithResponseStream/InvokeEndpointasymmetries (ContentTypeon plainContent-TypevsX-Amzn-Sagemaker-AcceptforAccept;TargetModel/EnableExplanationspresent onInvokeEndpointonly, absent from the stream op's input). - Every response header binding, all three ops
(
deserializers.go:141-166,317-333,483-497). - Each op's own error switch is a distinct set, not shared across ops:
InvokeEndpoint= InternalDependencyException/InternalFailure/ ModelError/ModelNotReadyException/ServiceUnavailable/ValidationError (deserializers.go:113-131);InvokeEndpointAsync= InternalFailure/ ServiceUnavailable/ValidationError only, no ModelError (deserializers.go:296-306);InvokeEndpointWithResponseStream= InternalFailure/InternalStreamFailure/ModelError/ModelStreamError/ ServiceUnavailable/ValidationError, no InternalDependencyException, no ModelNotReadyException (deserializers.go:451-471). gopherstack never hand-codes these switches (mock-only synchronous success path), so there is nothing to mismatch, but the sets themselves are worth recording so a future auditor doesn't assume they're identical. ModelError's extra members (OriginalStatusCode *int32,OriginalMessage *string,LogStreamArn *string) andModelStreamError's (ErrorCode_ *string) confirmed againsttypes/errors.go-- unreachable organically (no real model container to fail), same as the 2026-07-24 finding.X-Amzn-ErrorTypeis never set byhandler.go'serrorResponsepath (checked: only a JSON body with__type/messageis written). Verified this is not a bug here (unlike the mediastoredata precedent this campaign flagged): the SDK'sawsRestjson1_deserializeOpErrorInvoke*functions callrestjson.GetErrorInfo, which falls back to the body's__typefield whenever the header is absent (aws-sdk-go-v2@v1.43.4/aws/protocol/restjson/decoder_util.go:15-46), anderrorResponse's{"__type": code, "message": msg}shape is exactly what that fallback expects -- confirmed a client still gets the correct typed exception (e.g.types.ValidationError) without the header.InvokeEndpointAsyncInput'sBody []bytemember (the mutual-exclusivity gap flagged 2026-08-11) re-confirmed present and unenforced; not fixed this pass either -- it's an unenforced-validation gap, not one of the five wrapper-key/nesting bug shapes this sweep targets, and was already correctly disclosed.
New this pass: added TestSDKEventStreamFraming_RealReader
(invoke_endpoint_stream_test.go) -- drives
InvokeEndpointWithResponseStream through the real SDK client and reads the
response with the SDK's own GetStream().Events() reader (not hand-parsed
bytes), asserting every event decodes to
*types.ResponseStreamMemberPayloadPart (never UnknownUnionMember) with
the expected Bytes content. The pre-existing TestSDKResponseBindings
only asserted GetStream().Close() didn't error, which doesn't prove frame
correctness (a truncated/malformed frame can still close cleanly without
being read). Proved this test is not vacuous by hand-corrupting
eventStreamHeaderValueTypeString from 7 to 3 (via the scratchpad
cp-based hand-revert method): the new test fails with "unexpected EOF"
while the pre-existing stream tests still pass, confirming only the new
test actually exercises frame-level correctness.
Provenance: the prior stamp (last_audit_commit: 95ab0584,
last_audit_date: 2026-07-24) checked out: git show -s --format=%ad 95ab0584 -> Mon Jul 13 10:54:25 2026 -0500, an 11-day gap before the
claimed audit date, which per this campaign's provenance heuristic
("sha predates date" is the tell) is suspicious -- though note that commit
is unrelated to this service (a dlm fix), consistent with this campaign's
observation that commit-doesn't-touch-the-directory is not itself
disqualifying. Separately, the 2026-08-11 SDK pin-correction pass
(d39bf33e4, bumped v1.39.3 -> v1.43.4 and added the Body-field gap
entry) touched this file's content but did not advance
last_audit_commit/last_audit_date -- the stamp had gone stale relative
to real work already done on the file. Both are refreshed by this pass to
current HEAD (914e8b59, 2026-08-20).
Gates: go build, go vet, go fix -diff (empty), gofmt -l (empty),
go test -race (all green, including the new round-trip test),
golangci-lint run (0 issues). git status --short outside this service
directory: clean.
cmd/reqfielddiff reported 6 tier-1 findings, all httpHeader-bound fields it
structurally can't see being read (it doesn't inspect header consumption,
only decode-target structs/params). TargetVariant (both InvokeEndpoint and
InvokeEndpointWithResponseStream, handler.go:297/:250 via
setVariantResponseHeader) and InvokeEndpointAsync.InferenceId
(handler.go:212) were already read -- false positives, no change. Fixed
one real drop: InvokeEndpointAsync.Filename was never read; RecordAsyncInvocation
always hardcoded the generated OutputLocation's final path segment to
"output", ignoring the SDK doc's "generates a filename based on the
inference ID" default. Now reads X-Amzn-Sagemaker-Filename
(async_invocations.go) and uses it as that segment when the caller
supplies one, proved via TestSDKInvokeEndpointAsync_FilenameShapesOutputLocation
(real SDK client, asserts OutputLocation suffix). InvocationTimeoutSeconds/
RequestTTLSeconds recorded in items_still_open: request-only headers
with no output member and no async queue to time out against -- structural.
Tier-1: 6 before -> 6 after (unfixable-by-tool-design false positives plus
now-disclosed structural items remain; the one real gap is fixed).
Diffed 914e8b59..HEAD for this directory rather than re-deriving from
scratch: only test-only churn (an unrelated services/sagemaker
CreateEndpointFSM signature update in endpoint_validation_test.go, the
repo-wide mechanical iam_enforcement_test.go IAM-enforcement harness, and a
regenerated README.md), plus a PARITY.md stamp bump that lands the
2026-08-20 wrapper-key-sweep content -- no production wire-shape code
changed since the last real audit. Confirmed 914e8b59 itself is not an
ancestor of this branch's HEAD (squash-merge history rewrite, per this
repo's own documented Closes-trailer-loss behaviour) but its tree content
matches what this file already describes, so nothing was missed by treating
it as the baseline.
Fixed the one disclosed-but-unfixed gap: InvokeEndpointAsyncInput.Body's
doc comment (api_op_InvokeEndpointAsync.go) states "Body and InputLocation
are mutually exclusive. Provide exactly one of them," and
validateOpInvokeEndpointAsyncInput (validators.go:84-97) confirms this is
not a client-side check -- only EndpointName is required there -- so a
real client can construct and send a request with neither or both, meaning
this must be a server-side validation in real AWS. handleInvokeEndpointAsync
(handler.go) previously read the raw HTTP body unconditionally and never
read the X-Amzn-Sagemaker-Inputlocation header at all (confirmed via
serializers.go's awsRestjson1_serializeOpHttpBindingsInvokeEndpointAsyncInput,
which binds InputLocation to that exact header) -- so a request with
neither field, or with both, was silently accepted with 202 Accepted. Fixed:
handleInvokeEndpointAsync now compares len(body) > 0 against the new
X-Amzn-Sagemaker-Inputlocation header's presence and returns ValidationError
(400) when they're equal (both true or both false). Added
headerInputLocation header constant. Regression test
TestAsyncInvocation_BodyInputLocationMutualExclusion
(invoke_endpoint_async_test.go) covers all four combinations; proved
non-vacuous by neutering the guard (if (len(body) > 0) == hasInputLocation
-> if false && ...), confirming exactly the two reject cases fail (202
instead of 400) while the two accept cases still pass, then restoring from a
backup copy. Updated TestHandler_EndpointValidation's async-invocations
subtests and TestAsyncInvocationInferenceIDPreserved (both previously sent
neither field, relying on the bug) to send a body instead, since their intent
is EndpointName/InferenceId behaviour, not this constraint.
Five dimensions:
- AWS behavior compliance: fixed the Body/InputLocation gap above; everything else re-confirmed unchanged against the pinned SDK (no other wire-shape code changed since 914e8b59).
- LocalStack parity: NOT CHECKED this pass (no LocalStack instance available in this environment; prior passes found no LocalStack-specific divergence to compare against either).
- Cross-service integration: re-confirmed clean --
validateEndpointcorrectly rejects an EndpointName the wiredservices/sagemakerregistry doesn't know about, or hasn't reachedInService(TestHandler_EndpointValidation,TestProvider_WiresSageMakerEndpointLookup). The new IAM-enforcement test (iam_enforcement_test.go, added by an unrelated repo-wide campaign since the last audit) was read in full and correctly mapsPOST /endpoints/{name}/invocations-> denied withoutsagemaker-runtime:InvokeEndpoint-- not specific to this service, so not re-derived from the SDK independently. - Performance: NOT CHECKED beyond what the 2026-08-20 pass already recorded
(no hot-path code changed this pass); no new
sort.Sliceor O(n) under-lock scans introduced or found (grep -n 'sort\.\(Slice\|SliceStable\)'across the package: zero hits). - Resource leaks: re-confirmed clean --
grep -n 'go func'across the package: zero hits (no goroutines in this service at all); FIFO caps on sessions/asyncInvocations/invocations unchanged.
Baseline (before this pass's fix): golangci-lint run 0 issues,
go test -race -count=1 all green -- i.e. the Body/InputLocation gap was a
genuine silent-accept bug, not something already caught by an existing test.
Gates after the fix: go build, go vet, gofmt -l (empty),
go test -race -count=1 (all green, including the new regression test),
golangci-lint run (0 issues, after --fix reordered the new test table's
struct fields for fieldalignment).
sagemakerruntime is region-isolated: async invocations and sessions live per region and each sibling validates endpoints through the SageMaker backend of the request region. Per-region sibling handlers via pkgs/regionpeers; snapshots gain an additive regions key only when a sibling exists (no version bump; older snapshots restore). NewHandler alone stays single-region. Proof: TestHandler_MultiRegionIsolation, TestHandler_MultiRegionPersistence. Limitation: the dashboard shows the home region only.
InvokeEndpoint.TargetVariant is a tool false positive (read from the X-Amzn-Sagemaker-Target-Variant header and echoed as X-Amzn-Invoked-Production-Variant). InvokeEndpointAsync.InvocationTimeoutSeconds/RequestTTLSeconds stay recorded: the pinned SDK documents only their defaults (900 s / 21600 s), and no async queue exists to expire requests against.
Inapplicable (value semantics). Invoke* operations are data-plane with no Create/Describe/Update surface and no SDK-documented defaults beyond pass-through headers.