Skip to Content
SimulatorsS3 changelog

Simulators

S3 changelog

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.

Last updated on