# Use S3

```yaml
capability_id: s3.use
display_name: Use S3
scope: s3:use
endpoint:
  method: POST
  path: /s3/presign
operation: s3.presign
billable: true
commands:
  - GetObject
  - PutObject
environment_selector: none
```

## Purpose and authorization

`s3:use` authorizes an authenticated, application-scoped request for an AWS
S3 presigned `GetObject` or `PutObject` request. The application must use
[Contract v1](https://docs.getgateman.com/contract-v1/) and have the
`s3:use` scope.

Gateman signs the native AWS SDK operation with the application's configured
AWS credential and returns a URL for the application to call directly. Gateman
does not proxy the object body, receive the subsequent S3 response, or return
the AWS credential.

This is a narrow signing capability, not an object-storage abstraction. It
does not register buckets, enforce prefixes, check ownership or existence, or
authorize individual native S3 fields as separate Gateman privileges.

## Implementation contract

The outer request is authenticated with Contract v1:

```http
POST /s3/presign
Content-Type: application/json
X-GM-App: app_...
X-GM-Ts: 1710000000
X-GM-Sig: <Contract-v1-signature>
X-GM-Idempotency-Key: optional-gateman-key
```

The body is a JSON object:

```json
{
  "operation": "put",
  "bucket": "example-bucket",
  "key": "uploads/photo.jpg",
  "region": "eu-west-1",
  "expires": 900,
  "parameters": {
    "ContentType": "image/jpeg",
    "ContentLength": 73421,
    "Metadata": {
      "source": "web"
    }
  }
}
```

The supported top-level fields are:

| Field | Required | Contract |
| --- | --- | --- |
| `operation` | Yes | `get` or `put`. These select `GetObject` and `PutObject`. |
| `bucket` | Yes | A syntactically valid S3 bucket name. The caller chooses it. |
| `key` | Yes | A nonempty S3 object key of at most 1,024 bytes without control characters. |
| `region` | Conditional | A valid AWS region. Required for customer-managed credentials; managed access uses its recorded region. |
| `expires` | Yes | Integer seconds from 1 through 3,600. |
| `parameters` | No | A JSON object of native SDK input fields for the selected operation. |
| `content_type` | No | Legacy PUT compatibility field. Equivalent to `parameters.ContentType`. |

Unknown top-level fields are rejected. Endpoint, host, scheme, and other
provider-origin overrides are not supported.

### Native `parameters`

`parameters` is validated against the installed AWS SDK operation model. It is
not a static Gateman allowlist. Native parameter names and value types accepted
by the current `GetObject` or `PutObject` model are therefore the source of
truth, subject to the constraints below.

- Top-level `bucket` and `key` map to the SDK's `Bucket` and `Key` inputs.
- `parameters.Bucket`, `parameters.Key`, and `parameters.Body` are rejected.
- `Body` is unavailable because Gateman never receives the object body.
- Unknown native names and SDK-invalid value types are checked locally through
  the SDK model before admission. These validation failures return HTTP `400`
  without consuming an operation. The route controls the HTTP status and
  envelope, but the SDK-generated value-validation text is returned as the
  error string.
- `ChecksumAlgorithm` is rejected because Gateman cannot generate the object
  checksum for a body it never receives. Supply a supported explicit checksum
  value where the SDK model accepts it, and send the corresponding object
  bytes directly to S3.

The SDK model may produce signed request headers or query parameters from a
native field. The response's `headers` must be used exactly for the direct S3
request. The `parameters` object is not an arbitrary JSON bag and does not
grant permissions beyond the AWS credential's IAM policy.

For PUT, `content_type` is accepted for compatibility and is mapped to
`ContentType`. Supplying both forms with different values is rejected. It is
not accepted for GET; use the native GET response-content parameters where
appropriate.

## GET and PUT examples

### Native GET

```json
{
  "operation": "get",
  "bucket": "example-bucket",
  "key": "downloads/report.pdf",
  "region": "eu-west-1",
  "expires": 300,
  "parameters": {
    "ResponseContentType": "application/pdf",
    "Range": "bytes=0-1048575",
    "IfMatch": "\"etag-value\""
  }
}
```

The application sends the returned method, URL, and headers to S3. It reads
the actual AWS response and remains responsible for range, content, and error
handling.

### Native PUT

```json
{
  "operation": "put",
  "bucket": "example-bucket",
  "key": "uploads/photo.jpg",
  "region": "eu-west-1",
  "expires": 900,
  "parameters": {
    "ContentType": "image/jpeg",
    "Metadata": {
      "source": "web"
    },
    "Tagging": "source=web&retention=short",
    "StorageClass": "STANDARD"
  }
}
```

The application sends the object bytes directly to S3 using the returned PUT
URL and every header returned by Gateman. Gateman does not upload the bytes or
confirm that AWS accepted them.

## Response contract and signed headers

A successful response is HTTP `200`:

```json
{
  "ok": true,
  "url": "https://example-bucket.s3.eu-west-1.amazonaws.com/uploads/photo.jpg?...",
  "method": "PUT",
  "expires_at": "2026-10-16T12:15:00+00:00",
  "headers": {
    "Content-Type": "image/jpeg",
    "x-amz-meta-source": "web"
  }
}
```

`url` is the complete presigned URL, `method` is `GET` or `PUT`, and
`expires_at` is the server-calculated UTC expiry. `headers` contains every
non-`Host` header in the completed request returned by the presigner. The
caller must send every header returned in Gateman's `headers` object when
executing the presigned request against AWS S3. The URL, method, and returned
headers must be used together; changing or omitting a returned header can cause
AWS signature or request validation failure. The URL is a bearer-like
capability until it expires, so do not expose it unnecessarily.

For PUT, `ContentLength` is accepted as a native SDK parameter when its value
is valid. The completed request returns `Content-Length`, and the current
presigner includes `content-length` in `X-Amz-SignedHeaders`, so the supplied
value is part of the signature. This is request signing, not a separate
Gateman upload-size policy.

The subsequent direct S3 transfer does not consume another Gateman operation.
The application must interpret the actual AWS status and response body. A
successful Gateman response does not prove that AWS will authorize or complete
the operation.

## Credentials, region, and IAM authority

AWS credentials are stored by Gateman in the encrypted application secret
store. Customer-managed credentials are used as supplied. Gateman-managed
access uses the recorded AWS region; do not send a different region to select
another managed runtime context.

**AWS IAM is authoritative for S3 resource permissions.** The caller chooses
the bucket and full object key. The route does not independently:

- register buckets;
- enforce key prefixes;
- check object ownership or object existence;
- validate effective IAM policy permissions; or
- authorize each native S3 field as a separate Gateman privilege.

Managed provisioning may configure restricted IAM permissions, but AWS
enforces them. See [Configure managed S3 access](https://docs.getgateman.com/capabilities/s3.provisioning/).
The full provisioning and credential lifecycle remains separate from this
runtime capability reference.

## Operation accounting and concurrency

The billable operation is `s3.presign`. One operation is charged for each
admitted invocation. Validation failures rejected before admission do not
consume an operation. Credential, SDK, signing, or other presigning failures
after admission remain billable as failed operations.

The subsequent S3 GET or PUT is not a Gateman operation. The optional
`X-GM-Idempotency-Key` is part of Contract v1 signing but does not create an
S3-specific replay ledger or deduplicate presigning calls. Repeated admitted
calls consume separate operations.

Monthly allowance exhaustion returns HTTP `403` with
`monthly_operation_limit_reached`. Application concurrency rejection returns
HTTP `429` with `Retry-After` when applicable. An unavailable application
concurrency guard returns HTTP `503`. An unavailable operation-accounting
service returns HTTP `503` with `Operation accounting unavailable`.

Do not call this route for every byte, browser render, download retry, or
upload chunk. Request a presigned URL when the application needs one and use
it until it expires or the operation must be reissued.

## Errors

| Condition | HTTP response | Operation consequence |
| --- | ---: | --- |
| Contract v1 authentication failure | `401` | Rejected before capability execution; no operation. |
| Missing `s3:use` scope | `401` | Rejected before capability execution; no operation. |
| Invalid JSON | `400` | Rejected before admission; no operation. |
| Missing required fields or unsupported operation | `400` | Rejected before admission; no operation. |
| Invalid bucket, key, region, or expiry | `400` | Rejected before admission; no operation. |
| Invalid `parameters` shape | `400` | Rejected before admission; no operation. |
| Unknown top-level or native parameter | `400` | Rejected before admission; no operation. |
| Reserved `Bucket`, `Key`, or `Body` | `400` | Rejected before admission; no operation. |
| Conflicting `content_type` and `ContentType` | `400` | Rejected before admission; no operation. |
| Unsupported checksum behavior | `400` | Rejected before admission; no operation. |
| SDK parameter validation error during local model validation | `400` | Rejected before admission; no operation. The route controls the envelope, but the SDK's validation text is returned as the error string. |
| Application concurrency rejection | `429`, with `Retry-After` when applicable | No operation. |
| Application concurrency guard unavailable | `503` | No operation. |
| Monthly operation allowance exhausted | `403` | No new operation is reserved. |
| Operation accounting unavailable | `503` | No confirmed operation is reserved. |
| Missing AWS credential or credential-store failure | `502` | Admitted operation is finalized as failed. |
| SDK serialization or signing failure after admission with an `InvalidArgumentException` | `400` | Admitted operation is finalized as failed; the exception message is returned. |
| Unexpected presigning failure after admission | `502` with `S3 presigning unavailable` | Admitted operation is finalized as failed. |

These are Gateman presigning errors. They are distinct from an AWS status
returned later when the caller uses the URL, such as an IAM denial, expired
signature, missing object, checksum failure, or invalid request header.

## Managed provisioning boundary

Managed setup and teardown are documented separately in [Configure managed S3
access](https://docs.getgateman.com/capabilities/s3.provisioning/). In summary,
managed access creates a dedicated runtime identity, applies the configured
resource restrictions, encrypts the runtime credential, and revokes the
temporary `Gateman-Setup` access key after setup. When managed access is
removed, Gateman verifies and revokes the recorded key before removing local
state; it can revoke access keys belonging to its own runtime IAM user only.
If application deletion cannot verify cleanup, the
application can remain in `pending-deletion`. Gateman does not delete the IAM
user, policy, permissions boundary, buckets, bucket policies, or objects.

## Implementation outcome

An independently correct Use S3 integration:

- authenticates with Contract v1 and the `s3:use` scope;
- selects `get` or `put`, supplies the bucket, full key, region, and expiry;
- represents native SDK inputs in `parameters` and avoids reserved fields;
- uses the returned URL, method, and signed headers for a direct S3 request;
- interprets the actual AWS response rather than fabricating a provider result;
- treats AWS IAM as the authorization boundary;
- protects the presigned URL and refreshes it only when needed; and
- accounts for one `s3.presign` operation per admitted Gateman invocation.

## Implementation instructions for agents

- **MUST** use Contract v1, the `s3:use` scope, and `POST /s3/presign`.
- **MUST** send `operation` as `get` or `put`, with `bucket`, `key`, and an
  integer `expires` from 1 through 3,600.
- **MUST** provide `region` for customer-managed credentials. Managed access
  uses its recorded region.
- **MAY** send native AWS SDK inputs in the `parameters` object. The installed
  SDK operation model, not a hand-written exhaustive list, defines supported
  names and types. Local SDK model validation occurs before admission, but a
  later credentialed serialization or signing failure is billable.
- **MUST NOT** send nested `Bucket`, `Key`, or `Body`; object bytes never pass
  through Gateman. **MUST NOT** use `ChecksumAlgorithm` without a body.
- **MAY** use legacy PUT `content_type`, but **MUST NOT** conflict with
  `parameters.ContentType` or use it for GET.
- **MUST** send every header returned in Gateman's `headers` object, together
  with the returned URL and method, when making the direct S3 request. **MUST**
  interpret AWS's actual response.
- **MUST NOT** implement bucket registration, prefix authorization, or object
  ownership checks inside Gateman. AWS IAM remains authoritative.
- **MUST** treat `X-GM-Idempotency-Key` as Contract v1 input, not an S3 replay
  ledger, and **MUST** account for each admitted `s3.presign` call.
- **SHOULD** avoid unnecessary presign calls and avoid exposing URLs beyond
  the code that needs to perform the transfer.

## Reusable AGENTS.md block

```markdown
<!-- gateman:begin capability=s3.use -->
- Use Contract v1 with the `s3:use` scope and target `POST /s3/presign`.
- Send `operation` as `get` or `put`, plus the caller-selected `bucket`, full
  `key`, integer `expires` from 1 through 3,600, and `region` when using a
  customer-managed AWS credential. Managed access uses its recorded region.
- Put native AWS SDK inputs in the JSON `parameters` object. Gateman derives
  supported names and types from the installed `GetObject` or `PutObject`
  model; it is not an exhaustive static allowlist.
- Do not send nested `Bucket`, `Key`, or `Body`. Object bytes never pass through
  Gateman. `ChecksumAlgorithm` is rejected because no body is available.
- For PUT, `content_type` is legacy compatibility for `ContentType`; do not
  use it for GET or conflict it with `parameters.ContentType`.
- Use the returned `url`, `method`, and every header in Gateman's `headers`
  object unchanged for the direct S3 request. Interpret the actual AWS
  response; do not filter or reconstruct the returned headers.
- AWS IAM is authoritative for bucket, key, prefix, ownership, existence, and
  native-operation permissions. Do not add those checks to Gateman.
- One admitted call consumes one `s3.presign` operation. The direct S3 transfer
  consumes no additional Gateman operation. `X-GM-Idempotency-Key` is not an
  S3 replay ledger; avoid unnecessary presign requests.
- Managed AWS setup and credential lifecycle are separate from runtime signing:
  https://docs.getgateman.com/capabilities/s3.provisioning/
- Full capability reference: https://docs.getgateman.com/capabilities/s3.use/
<!-- gateman:end capability=s3.use -->
```

## Related references

- [Contract v1](https://docs.getgateman.com/contract-v1/)
- [Authentication](https://docs.getgateman.com/authentication/)
- [Operation accounting](https://docs.getgateman.com/operation-accounting/)
- [Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
- [Idempotency](https://docs.getgateman.com/idempotency/)
- [Errors and responses](https://docs.getgateman.com/errors-and-responses/)
- [Configure managed S3 access](https://docs.getgateman.com/capabilities/s3.provisioning/)
- [Official PHP client](https://docs.getgateman.com/canonical-client-php/)
- [Official Node.js client](https://docs.getgateman.com/canonical-client-node/)
- [Official Python client](https://docs.getgateman.com/canonical-client-python/)
- [Official Go client](https://docs.getgateman.com/canonical-client-go/)
- [AWS SDK for PHP S3 `GetObject`](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#getobject)
- [AWS SDK for PHP S3 `PutObject`](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject)
