# S3 changelog

What changed in each S3 simulator release, newest first.

What changed in each S3 simulator release, newest first.

## v0.0.7 — 2026-11-06

1. **The docs have an Errors and limits section.** It shows the `<Error>` document and its fields, the responses that depart from it, and where the simulator differs from S3: no throttling, no `Content-MD5` check and no cap on a version listing. It lists each listing's page size and next-page marker, and says what a re-sent PUT, DELETE or bucket create does.

## v0.0.6 — 2026-11-06

1. **The docs say what the one account holds.** The limitations describe the account's owner and ACLs without repeating how to run several simulators.

## v0.0.5 — 2026-11-05

1. **Setting the clock never stamps records before the timeline events it fired.** When `POST /_rystic/clock {"time": ...}` fires timeline events due at its target, the next request's records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

2. **A paced clock keeps wall time, and `rystic run --speed 0` stops it.** Under `RYSTIC_PACE`, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. `rystic run` hands the twin only the pace it resolved, so `--speed 0`, a scenario without events, `--tape` and `--live` keep the clock manual even when your shell exports `RYSTIC_PACE`.

3. **Swagger UI to read and call the twin from the browser.** `/_rystic/swagger`, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin.

4. **An unsigned ListBuckets answers 307, as S3 does.** `GET /` with no `Authorization` and no presigned query answers `307 Temporary Redirect` with no body, where it answered `403 AccessDenied`. A request with no credential on a bucket that does not exist answers `404 NoSuchBucket`, as before, and one on a bucket that exists is still served where S3 refuses it, so sign every request.

5. **A state dump seeds back as it was, and the next record gets a fresh id.** `GET /_rystic/inspect` posted to `POST /_rystic/seed` after a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds. `GET /_rystic/inspect?view=sequences` lists every id counter, and a world document's new `sequences` key restores them, so ids the simulator renders from a counter continue where the dump left off.

6. **A slow client no longer holds up the others.** The simulator reads a request's whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients' requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

7. **The call log shows what each call asked for and whether it succeeded.** Each `GET /_rystic/calls` entry now carries `args`, the call's query and body arguments with credentials redacted (long values cut, file contents left out), and `ok`, false for an answer that is not 2xx. A path the simulator does not serve is marked `unknown_route: true`. `?tail=N` returns only the last N calls, and `dropped` counts the calls the log no longer keeps.

8. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md` and `SCENARIOS.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. Multipart uploads and checksums, versioned buckets, presigned URLs, virtual-hosted addressing and Swagger UI each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

9. **The Scenarios table is easier to scan.** Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.

10. **Pro starts with a 14-day trial.** Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

11. **Limitations is shorter to read.** It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind "Show all limitations".

12. **The page has no Plans section.** The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

13. **The docs read more plainly, mark where the simulator differs from S3 and keep to the simulator's own facts.** Sentences are shorter, and a line marked "Differs from S3" sits beside the behaviors that depart from S3, such as the unchecked signature, the 64 MiB object limit and reading an older version. What a misconfigured client sees, conditional requests and the static site's timeline are tables, and Seeding state names the keys of every register. The auth, seeding and reset sections link the shared pages for the generic mechanics.

## v0.0.4 — 2026-10-12

1. **A world can carry its own timeline.** A `--seed` file, or a complete world document posted to `POST /_rystic/seed`, may add `credentials`, binding presented credentials to actors, and `events`: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level `credentials` or `events` key the twin used to ignore now takes effect. `RYSTIC_PACE=<speed>` plays the clock, and the timeline, in real time, and an event's `at` may be `+<duration>`. `/_rystic/inject`, `--inject` and `RYSTIC_INJECT` now need `state_set` instead of `scenarios`.

2. **A timeline call can hand what it minted to later events.** A `call` event may add `bind`, `{"<name>": "<JSON Pointer>"}`, saving fields of its answer, such as an ID the twin minted; a later event uses one as `${name}` in a call's path, headers or body or in a seed event's record keys and values (a register name cannot be bound). A schedule naming a `${name}` no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

3. **`s3_static_site` plays a timeline.** Over the two hours after the pinned clock, the site's deploy bot, bound to its own `AWS4-HMAC-SHA256` authorization header, snapshots `index.html` into `releases/2026-02-01/`, puts a new `assets/app.js`, deletes the `legacy.html` redirect stub, rolls an access log and uploads the release bundle as a one-part multipart upload, all through the S3 API; advance the clock to watch the release land.

4. **A clock jump fires each timeline event at its own time.** When one `POST /_rystic/clock` or paced tick passes several events, each fires with the clock at its `at`, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

5. **A clock jump stamps each timeline event at its exact instant.** When a clock move passes timeline events, the records each one makes carry exactly its `at`, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

6. **A timeline call can carry raw bytes, XML or a JSON array.** A `call` event may send `body_text` or `body_base64` instead of `body`; the twin sends it as written, under the route's own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object's bytes or post an XML document, and `${name}` fills in `body_text`. A `body` may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as `/InitiateMultipartUploadResult/UploadId`.

## v0.0.3 — 2026-10-11

1. **DeleteObjects in quiet mode answers only the keys that failed.** A `<Quiet>true</Quiet>` delete now answers S3's quiet `DeleteResult`: no `Deleted` entries, only an `Error` per key that failed, so a fully successful quiet delete answers an empty result.

2. **GetObjectAttributes answers every attribute you ask for.** `x-amz-object-attributes` naming `Checksum`, `ObjectParts` or `StorageClass` now returns them beside `ETag` and `ObjectSize`: the stored checksum and its type, a completed multipart object's parts, and `STANDARD`. The `ETag` element is unquoted, as S3 answers it.

3. **A multipart upload keeps the checksum algorithm it was created with.** `CreateMultipartUpload` with `x-amz-checksum-algorithm` (CRC32, CRC32C, SHA1, SHA256) now completes with S3's composite checksum for it (`<base64>-<part count>`, type `COMPOSITE`) instead of the default CRC64NVME, and `CompleteMultipartUpload` answers `x-amz-server-side-encryption`. The completed object reads back through `HeadObject` and `GetObject` with that checksum and `x-amz-server-side-encryption`, as a single-PUT object does, and `GetObjectAttributes` lists its parts.

4. **ListObjectVersions lists the objects you write.** In a bucket without versioning every object is listed as a `Version` with `VersionId` `null` and `IsLatest` `true`, so emptying a bucket through the version listing (boto3's `bucket.object_versions.delete()`, Terraform's `force_destroy`) now removes your objects.

5. **DeleteObject in a versioning-enabled bucket hides the object.** A delete marker becomes the key's current version and the object stays listed in `ListObjectVersions` as a noncurrent version, so a later `GetObject` or `HeadObject` answers 404 `NoSuchKey` as S3 does, where the twin used to keep serving the object.

6. **ACL grantee group URIs are declared identifiers.** `http://acs.amazonaws.com/groups/global/AllUsers` and its siblings in a bucket's or an object's ACL are handed out byte for byte and never rebased onto the twin, by declaration (`pipeline_policy.json` `host_roles`), so a seeded ACL naming them passes `RYSTIC_SEED_GATE=reject` and a client comparing a grantee URI against the AWS constant matches.

7. **The scripts in this archive are MIT-licensed.** `check.sh` and the archive's other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts, so you can copy, change and share them. `LICENSE.txt` no longer covers them; it still governs the twin itself.

## v0.0.2 — 2026-10-07

1. **Streaming uploads in the `aws-chunked` coding.** A body sent `aws-chunked` (`Content-Encoding: aws-chunked` or `x-amz-content-sha256: STREAMING-…`), as SDKs stream uploads with a trailing checksum, is decoded: the object stores the payload alone, a trailing `x-amz-checksum-*` is verified, stored and echoed like the header form, and framing that does not parse answers `400 IncompleteBody` without storing anything.

2. **Presigned URLs.** A SigV4 or SigV2 presigned URL, as `aws s3 presign` and boto3's `generate_presigned_url` make, stands in for the `Authorization` header. The twin checks its shape, and its expiry against the real clock, never its signature: a misshapen SigV4 URL answers `400 AuthorizationQueryParametersError`, a misshapen SigV2 one `403 AccessDenied`, an expired one `403 AccessDenied`, and one sent beside a second credential `400 InvalidArgument`, as S3 answers them.

3. **Breaking: a request's SigV4 signature must be well formed and fresh.** The twin still holds no key and never verifies a signature, but it now checks the signing time against your machine's real clock and then the `Authorization` header's shape, in S3's order: a signed request with no parseable `X-Amz-Date` or `Date` answers `403 AccessDenied` ("AWS authentication requires a valid Date or x-amz-date header"), one signed more than 15 minutes from now, either way, answers `403 RequestTimeTooSkewed` whatever its shape, one with a fresh date but without exactly one well-formed `Credential`, `SignedHeaders` and 64-hex `Signature` answers `400 AuthorizationHeaderMalformed`, and one with no credential answers `403 AccessDenied`. A static `Authorization` header, such as the placeholder v0.0.1's `HOWTO.md` and `check.sh` sent, is refused: sign each request instead, as the AWS SDKs and CLI already do, or with `curl --aws-sigv4 'aws:amz:us-east-1:s3' --user <access key>:<secret>` (curl 7.75 or later; any key pair works). `/_rystic/clock` never makes a fresh signature stale.

4. **Virtual-hosted addressing.** A request to `<bucket>.s3.us-east-1.amazonaws.com` or `<bucket>.s3.amazonaws.com` is served as the path-style request with that bucket first in the path, so a client keeping S3's own hostnames and default addressing reaches the twin through `rystic run s3-twin --intercept` and `rystic exec`, when it honors the gateway's proxy and CA settings. Path-style on an endpoint override works as before.

## v0.0.1 — 2026-10-07

1. **A local Amazon S3 for your storage code and the agents that drive it.** The first release of the S3 twin: 32 operations of S3's REST API, path-style on `/`, `/{Bucket}` and `/{Bucket}/{Key+}` — buckets, objects under any key, listings, multipart uploads, copies, tagging, batch deletes, conditional and Range requests and request checksums — served from a model validated against S3 itself. Point boto3 or the AWS CLI at it with an endpoint override, path-style addressing and any access key pair (`HOWTO.md` shows both), and your code runs against it unchanged; nothing it stores reaches AWS.

2. **Objects under any key, read back as S3 answers them.** PutObject, GetObject, HeadObject and DeleteObject on `/{Bucket}/{Key+}`, where a key such as `logs/2026/09/report.csv` is one object and `%2F` in a path is the `/` it stands for; ListObjectsV2, ListObjects and ListObjectVersions list by `prefix` and `delimiter`; GetObjectTagging, PutObjectTagging, DeleteObjectTagging, GetObjectAttributes and GetObjectAcl read and write an object's settings. Bodies run to 64 MiB; a larger one answers `400 EntityTooLarge`. Every routed answer is S3's own XML or headers and carries `x-amz-request-id` and `x-amz-id-2`, every error a routed S3 request gets is S3's `<Error>` document (only a path no route matches answers the twin's own JSON 404), and an operation on a modeled path that the twin does not model (ACL writes, legal hold, retention, restore, select, and every bucket setting but versioning and tags) answers `501 NotImplemented` and changes nothing.

3. **Multipart uploads, copies and batch deletes.** CreateMultipartUpload, UploadPart, ListParts, CompleteMultipartUpload, AbortMultipartUpload and ListMultipartUploads let an SDK's upload manager — boto3's `upload_file`, `aws s3 cp` — start an upload, send its parts and complete it with the multipart ETag S3 gives it, up to twelve parts at S3's 5 MiB minimum; a part whose ETag matches none uploaded answers `InvalidPart`, an unknown upload ID `NoSuchUpload`, and the completed object's `Location` leads back to the twin. CopyObject and UploadPartCopy copy within a bucket or from another by `x-amz-copy-source`, and DeleteObjects deletes every key its `<Delete>` document names.

4. **Request checksums, conditional writes and Range reads.** A PutObject or UploadPart carrying `x-amz-checksum-crc32`, `-crc32c`, `-crc64nvme`, `-sha1` or `-sha256`, as the AWS SDKs send by default, is checked against the body — a mismatch answers `400 BadDigest` and stores nothing — and the stored checksum is echoed under `x-amz-checksum-mode: ENABLED`, so SDK integrity validation passes. `If-None-Match: *` or a stale `If-Match` on PutObject answers `412 PreconditionFailed` and leaves the object alone; `If-None-Match` and `If-Modified-Since` on GetObject and HeadObject answer `304`; `If-Match` naming another ETag answers `412`; and a `Range` answers `206 Partial Content` with its `Content-Range`.

5. **Buckets: create, list, read, configure and delete.** CreateBucket makes a bucket by the name you choose, ListBuckets lists the account's buckets with each one's `CreationDate` (and `BucketRegion` when asked), HeadBucket, GetBucketLocation, GetBucketAcl, GetBucketVersioning and GetBucketTagging read one, PutBucketVersioning turns versioning on or suspends it, PutBucketTagging and DeleteBucketTagging write its tags, and DeleteBucket removes an empty one — a bucket still holding objects answers `BucketNotEmpty`. `x-amz-expected-bucket-owner` passes for the owning account's 12-digit ID (`123456789012` unless a seed says otherwise) and answers `403 AccessDenied` for any other.

6. **Five shipped worlds, and the one your test needs.** `--scenario s3_static_site` boots `acme-demo-site`, an unversioned website bucket with seven keys under real prefixes beside its access-log bucket, and is the world Free boots and `POST /_rystic/reset` returns to. `s3_versioned_archive` holds six version rows with a delete marker among them, `s3_encrypted_vault` four objects pinning different encryption, checksum and storage-class combinations, `s3_bucket_farm` six buckets over five regions and every versioning state, and `s3_fresh_bucket` one empty tagged bucket; `SCENARIOS.md` says what each is for. On Pro and Enterprise, `POST /_rystic/seed` lands buckets, objects, versions and multipart uploads directly, and `POST /_rystic/reset` rewinds the ID sequence and the clock, so two identical runs give byte-identical objects and listings down to `ETag`, `Last-Modified` and `UploadId`.

7. **Measured against S3 itself.** The 2026-10-06 full-suite round matched 117 of 117 probes against `s3.us-east-1.amazonaws.com`, with 0 inconclusive. `HOWTO.md` states what the suite leaves out and the differences this release ships with: a versioning-enabled bucket keeps no versions of what you write, ListObjectVersions lists no `null` version for an object you write, DeleteObjects in quiet mode lists every deleted key, GetObjectAttributes answers `ETag` and `ObjectSize` only, and a multipart upload completes with CRC64NVME whatever algorithm it was created with and reads back without its checksum or encryption headers.

---

Source: https://www.rystic.ai/docs/simulators/s3/changelog · Markdown: https://www.rystic.ai/docs/simulators/s3/changelog.md
