Skip to content

Feature request: AWS / client-auth mode for lib/environments self-hosted worker #366

Description

@bluedoors

Feature request: AWS / client-auth mode for lib/environments self-hosted worker

Summary

The lib/environments self-hosted worker helpers (EnvironmentWorker, WorkPoller) hard-require
an environment key and inject it as a per-request Authorization bearer on every call. This
makes it impossible to run a self-hosted worker against Claude Platform on AWS, where the
documented and supported auth model is AWS IAM (SigV4) or an AWS-Console API key — not an
environment key
.

Please add an explicit opt-in that lets the worker authenticate with the base client's
credentials (SigV4 middleware or x-api-key) and skip the environment-key bearer entirely.

Background / why this matters

Per the self-hosted sandboxes docs:

On Claude Platform on AWS, the worker authenticates with AWS IAM (SigV4) or an API key
generated in the AWS Console, not an environment key
. Attach the
AnthropicSelfHostedEnvironmentAccess managed policy to the IAM principal your worker runs as.
Environment keys generated in the Claude Console don't work with the Claude Platform on AWS
endpoint.

So on AWS the IAM principal (e.g. an EKS pod's IRSA role) is the work-lease credential — the
AWS-native equivalent of the personal platform's environment key. But the Go SDK worker helper
cannot express this: it requires an environment key and overrides the AWS credential on every
request.

The aws/ subpackage already produces a fully AWS-authed client (SigV4 middleware +
anthropic-workspace-id header + regional base URL, exposed as aws.Client.Options). The gap is
purely that the worker helper strips/overrides that auth.

Affected code (verified identical in v1.50.1 and v1.50.2)

1. Environment key is hard-required — guards reject an empty key:

  • lib/environments/poller.go:74-75bearerReqOpts: if environmentKey == "" { return nil, errors.New("environments: environment key is required") }
  • lib/environments/poller.go:249-250NewWorkPoller: case opts.EnvironmentKey == "": p.err = errors.New("environments: WorkPollerOptions.EnvironmentKey is required")
  • lib/environments/worker.go:138-140EnvironmentWorker.Run: requires EnvironmentID and EnvironmentKey
  • lib/environments/worker.go:225-234HandleItem: requires EnvironmentKey

2. The env-key bearer is injected unconditionally on every request
lib/environments/poller.go:73-81:

func bearerReqOpts(environmentKey string) ([]option.RequestOption, error) {
	if environmentKey == "" {
		return nil, errors.New("environments: environment key is required")
	}
	return []option.RequestOption{
		option.WithHeaderDel("X-Api-Key"),     // strips the base client's x-api-key
		option.WithAuthToken(environmentKey),  // sets Authorization: Bearer <env-key>
	}, nil
}

These opts are appended last at every call site, so they win over any base-client auth
(option header-setters are last-writer-wins, option/requestoption.go:231-253):

  • poll / ack / stop — poller.go:285-287
  • heartbeat / force-stop / skills download — worker.go:280-282
  • session tool-runner stream / list / send — worker.go:361-363

The net effect on an AWS-authed base client: WithHeaderDel("X-Api-Key") removes the AWS-Console
API key, and WithAuthToken overwrites the SigV4 Authorization header — so the request reaches
the wire with the (AWS-invalid) environment-key bearer instead of the AWS credential.

3. No escape hatch. None of WorkPollerOptions (poller.go:129-180),
EnvironmentWorkerOptions (worker.go:25-91), or SessionToolRunnerOptions
(betasessiontoolrunner.go:71-129) exposes a SkipAuth / auth-mode / empty-key-allowed field.

4. No caller-side workaround. Caller-supplied RequestOptions are applied before the
env-key opts at every helper call site, so they can't re-add x-api-key or re-assert SigV4 after
the bearer; and the helpers hard-error on an empty key regardless. (SessionToolRunner used
directly is the one exception — it only appends a telemetry header — but that covers
stream/list/send only, not poll/heartbeat/skills, so there is no end-to-end workaround.)

Why the fix is clean

The environment key flows into nothing but the Authorization header — it is never used as a
query param, request body field, or routing header (work-item routing uses the separate
EnvironmentID; the anthropic-beta header is auto-injected by the generated methods). So
removing the bearer injection and letting the base client's auth stand is sufficient at every
call site.

Proposed change

Add an explicit opt-in (preferred over silently treating an empty key as "use base auth", which
would be a footgun on the personal platform — it would let the worker fall back to an org API key
on the host, the exact thing the env-key model exists to prevent):

  1. Add a field to EnvironmentWorkerOptions and WorkPollerOptions, e.g.:

    // UseClientAuth authenticates work and session calls with the base client's
    // own credentials (e.g. AWS SigV4 or an x-api-key) instead of an environment
    // key. Required for Claude Platform on AWS, where the worker authenticates as
    // an IAM principal and environment keys are not accepted. When set,
    // EnvironmentKey is not required and is ignored.
    UseClientAuth bool

    (An AuthMode enum would work equally well if that fits the SDK's conventions better.)

  2. When UseClientAuth is set:

    • Relax the env-key requirement guards (poller.go:249-250, worker.go:138-140,
      worker.go:225-234) to require only EnvironmentID.
    • Make bearerReqOpts return a no-op slice (no WithHeaderDel("X-Api-Key"), no
      WithAuthToken) so the base client's auth flows through unchanged. This single change
      propagates correctly to helperReqOpts and all the call sites listed above (they just append
      an empty auth slice).
  3. SessionToolRunner needs no change — it never required the key and only appends a
    telemetry header; it's fully driven by the RequestOptions it's handed.

This is small, localized, and backward-compatible — the default path (env-key bearer) is untouched.

Intended usage after the change (Go, AWS SigV4)

// AWS-authed base client (SigV4 + workspace header + regional base URL):
awsClient, err := aws.NewClient(ctx, aws.ClientConfig{
    AWSRegion:   region,
    WorkspaceID: workspaceID,
})
// ...
client := anthropic.NewClient(awsClient.Options...)

worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
    EnvironmentID: environmentID,
    UseClientAuth: true,   // no EnvironmentKey on AWS
    Workdir:       "/workspace",
})
err = worker.Run(ctx)

Notes

  • The Python and TypeScript SDK worker helpers likely need the parallel change — their
    self-hosted-sandbox worker examples still pass an environment key. Only the Go SDK was
    source-verified here (it's what the ant CLI uses).
  • Related downstream: the ant CLI's beta:worker command wraps this helper and is blocked on
    this change for AWS support — see feat(auth): add a Claude Platform on AWS credential tier anthropic-cli#61.

Open question for the SDK / Managed Agents team

Every SDK worker example currently passes an environment key. Can you confirm the AWS endpoint
(aws-external-anthropic.{region}.api.aws) already serves the worker routes —
work poll/ack/stop/heartbeat, sessions.events stream/list/send, and skills download —
under IAM/SigV4 auth today (i.e. the doc reflects shipped server behavior)? That determines whether
this SDK change alone unblocks worker-on-AWS or whether server-side work is also pending.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions