diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e027af..fefebd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,14 @@ _primitives/_rust/target/release/kei-changelog \ - Marker file relocated from `~/.claude/keisei-attached.toml` to `~/.keisei/attached.toml`. `~/.claude/` is Claude-Code-specific territory and should not host cross-adapter keisei state. `config::read()` performs a one-shot migration the first time it runs under v0.21: if the legacy file exists and the new location is empty, the marker moves over (new file written, legacy file deleted) and a stderr notice is emitted. - `Scope` enum (`user` / `project`) on the `ClientAdapter` trait. Adapters declare `supported_scopes()`; `config_path(scope)`, `attach(brain, scope)`, `detach(brain_name, scope)` are scope-aware. Claude Code and Cursor support both scopes; Continue and Zed are user-only. `keisei attach` gains `--scope=` (default `user`); `keisei mount` stays host-wide (`Scope::User` fan-out by design). - Marker schema v3: each `[[attachments]]` entry carries `scope = "user" | "project"`. Pre-v0.21 markers without the field default to `Scope::User` silently. New error variant `Error::ScopeUnsupported { client, scope, supported }` fires when a caller asks for a scope the adapter doesn't advertise. +- **primitives (v0.21 — `kei-store` real S3 backend):** + - `S3CloudStore` — functional S3 / R2 / MinIO / Wasabi backend via `aws-sdk-s3` v1. GetObject / PutObject / ListObjectsV2 (paginated) / DeleteObject wired behind the existing `MemoryStore` trait (sync-over-async via a single-thread tokio runtime). Enables `keisei attach s3://my-bucket/brain/` as a real cloud-mount path, not just a local stub. + - Opt-in feature flag `s3` on the `kei-store` crate — off by default so users who don't need cloud pay zero binary weight. Enabling adds tokio + hyper + rustls + aws-sdk-s3 (~5 MB release binary growth). + - AWS default credential chain honoured (env vars → `~/.aws/credentials` → IMDS). No new credential format; RULE 0.8 secrets-single-source unchanged. + - Endpoint override for non-AWS S3-compat providers via `KEI_STORE_S3_ENDPOINT` env var (runtime) or `s3.endpoint` in `store-config.toml` (persistent). Path-style addressing auto-enabled when a custom endpoint is set (MinIO / some R2 configs). + - "Branch" semantics: S3 has no native branching, so a branch is modelled as a key prefix (`/`). `branch()` sets the active prefix in-memory; default `main`. + - Factory auto-routes: `backend = "s3"` + feature `s3` + `s3.bucket` set → real cloud; otherwise falls back to the v0.14 local-manifest stub (still behind `KEI_STORE_ALLOW_S3_STUB=1`). + - Path-traversal guard parity with `FilesystemStore`: absolute and `..`-component paths rejected before keys are spliced. - **primitives (v0.20 — brain schema v2 + per-client hint):** - Brain schema v2 with per-platform `mcp_server` dispatch — a single brain directory can now host binaries for darwin-arm64/darwin-x64/linux-x64/linux-arm64/windows-x64 and `keisei attach` picks the right one automatically. Schema v1 (single string) still accepted for backward-compat. - `ClientAdapter::post_attach_hint()` — per-client reload instruction, no more hardcoded Claude-Code string in the orchestrator. diff --git a/README.md b/README.md index 81932f4..d39a7f4 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Use cases: - **Laptop travel.** Brain lives on USB / iCloud Drive. Plug in at home → `keisei mount /Volumes/MyBrain` attaches to Claude Code + Cursor simultaneously. Unplug → `keisei detach` clears everything. - **Team shared persona library.** Commit a brain repo to your private Forgejo/GitHub. Every developer clones it, runs `keisei mount ./team-brain`, same 30-agent persona library active in their Claude Code. -- **Cloud brain.** Point `keisei attach s3://my-bucket/brain/` at an S3-backed brain synced via `kei-store` (v0.20 — S3 backend currently MVP stub). Memory follows you to any machine with network. +- **Cloud brain.** Point `keisei attach s3://my-bucket/brain/` at an S3-backed brain synced via `kei-store` (v0.21 — real S3 / R2 / MinIO backend behind the `s3` feature flag). Memory follows you to any machine with network. - **Experimental personas in isolation.** Spin up a test brain via `cp -r ~/production-brain ~/experimental-brain`, `keisei attach ~/experimental-brain`. Iterate without touching production state. Security hardening (v0.19.0): @@ -395,7 +395,7 @@ Two output modes, chosen once in `/sleep-setup` Phase 3b: | Forgejo self-hosted | production | Same wire protocol as GitHub | | Gitea self-hosted | production | Same wire protocol | | Filesystem only | production | Local `.git`; no push; fastest | -| S3 / R2 / MinIO | stub — local only until v0.15 | Manifest-based local cache ONLY; no upload to S3/R2/MinIO yet. Requires `KEI_STORE_ALLOW_S3_STUB=1` (explicit opt-in so you don't accidentally believe your data is in the cloud). `aws-sdk-s3` integration planned for v0.15. | +| S3 / R2 / MinIO | production (v0.21, behind `s3` feature) | Real GetObject / PutObject / ListObjectsV2 via `aws-sdk-s3`. Build with `cargo build -p kei-store --features s3` and set `[s3] bucket = "..."` in `store-config.toml`. AWS default credential chain (env vars → `~/.aws/credentials` → IMDS). Custom endpoint for R2 / MinIO / Wasabi via `KEI_STORE_S3_ENDPOINT` env or `s3.endpoint` TOML field. Binary grows ~5 MB when the feature is on. Omit the feature OR omit `s3.bucket` to fall back to the v0.14 local-manifest stub (still gated by `KEI_STORE_ALLOW_S3_STUB=1`). | Requires the new `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, and `kei-store` primitives (shipped in the `dev` and `full` profiles). Governed by the Phase C extension of RULE 0.15 in `~/.claude/rules/sleep-layer.md`. @@ -417,7 +417,7 @@ Requires the new `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, | `kei-conflict-scan` | v0.13.0 — deep-sleep conflict scanner across rules/hooks/blocks/orphans/CP violations | | `kei-refactor-engine` | v0.13.0 — consumes `kei-conflict-scan` JSON; emits plan markdown + auto-resolve review markdown (NOT a unified diff; v0.14.1 retraction) | | `kei-graph-check` | v0.13.0 — post-refactor wikilink + handoff + block-ref resolver gate | -| `kei-store` | v0.13.0 — memory-repo backend abstraction (GitHub / Forgejo / Gitea / Filesystem / S3) | +| `kei-store` | v0.21.0 — memory-repo backend abstraction (GitHub / Forgejo / Gitea / Filesystem / S3). S3 backend is a real `aws-sdk-s3` client when built with `--features s3` (supports AWS / R2 / MinIO / Wasabi); otherwise falls back to the v0.14 local-manifest stub. | | `keisei` | v0.19.0 — exobrain multi-client CLI — `attach` / `mount` / `detach` / `status` / `list-adapters`. Mounts a portable brain into every detected AI client in one shot. Supported clients: Claude Code, Cursor, Continue, Zed. `mount` fan-outs to all detected adapters; `detach` round-trips cleanly and preserves the user's other MCP/context-server entries. Marker SSoT is `~/.claude/keisei-attached.toml` (schema v2 — list of attachments; v1 auto-migrated). | ## Primitives (shell) diff --git a/_primitives/MANIFEST.toml b/_primitives/MANIFEST.toml index a38c21b..0666e96 100644 --- a/_primitives/MANIFEST.toml +++ b/_primitives/MANIFEST.toml @@ -181,8 +181,8 @@ desc = "Post-refactor graph-integrity gate — wikilinks + handoffs + block refs [primitive.kei-store] kind = "rust" crate = "kei-store" -deps = ["git2 (vendored libgit2)"] -desc = "Memory-repo backend abstraction — GitHub / Forgejo / Gitea / Filesystem / S3 (S3 = MVP stub)" +deps = ["git2 (vendored libgit2)", "aws-sdk-s3 + tokio + rustls (optional, behind `s3` feature)"] +desc = "Memory-repo backend abstraction — GitHub / Forgejo / Gitea / Filesystem / S3 (real S3/R2/MinIO via aws-sdk-s3 when built with `--features s3`; local-manifest stub otherwise)" # --- v0.14 LBM port (10) --------------------------------------------------- diff --git a/_primitives/_rust/Cargo.lock b/_primitives/_rust/Cargo.lock index e7b40f8..ce2b5d8 100644 --- a/_primitives/_rust/Cargo.lock +++ b/_primitives/_rust/Cargo.lock @@ -109,12 +109,442 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.63.6", + "aws-smithy-json 0.62.5", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.4.0", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.6", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.119.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.10.9", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.63.6", + "aws-smithy-json 0.62.5", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.6", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2 0.11.0", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" +dependencies = [ + "aws-smithy-http 0.62.6", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2 0.10.9", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.38", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.63.6", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "base64" version = "0.21.7" @@ -127,6 +557,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -151,6 +591,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -181,6 +630,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "cc" version = "1.2.60" @@ -251,6 +710,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "colorchoice" version = "1.0.5" @@ -272,6 +746,22 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -287,6 +777,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -302,6 +801,19 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc-fast" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +dependencies = [ + "crc", + "digest 0.10.7", + "rand 0.9.4", + "regex", + "rustversion", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -336,29 +848,68 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -376,6 +927,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -488,6 +1045,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -503,6 +1066,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -633,6 +1202,44 @@ dependencies = [ "url", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -649,6 +1256,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -685,7 +1294,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -694,7 +1303,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -706,6 +1324,181 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper 1.9.0", + "hyper-util", + "rustls 0.23.38", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -864,6 +1657,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -906,7 +1705,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -918,12 +1717,12 @@ dependencies = [ "base64 0.22.1", "chrono", "clap", - "hmac", - "rand", + "hmac 0.12.1", + "rand 0.8.6", "rusqlite", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -975,7 +1774,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -1050,7 +1849,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "sha2", + "sha2 0.10.9", "sqlx", "tempfile", "tokio", @@ -1123,11 +1922,15 @@ name = "kei-store" version = "0.1.0" dependencies = [ "anyhow", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", "clap", "git2", "serde", "serde_json", "tempfile", + "tokio", "toml", ] @@ -1259,6 +2062,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1266,7 +2078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1308,7 +2120,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -1343,11 +2155,17 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-integer" version = "0.1.46" @@ -1390,6 +2208,18 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -1446,6 +2276,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -1501,6 +2337,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1563,8 +2405,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1574,7 +2426,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1586,6 +2448,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1627,6 +2498,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.10" @@ -1653,14 +2530,14 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1681,6 +2558,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1700,11 +2586,38 @@ version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ + "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -1714,6 +2627,15 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -1724,6 +2646,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1745,6 +2679,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1761,6 +2704,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1851,8 +2817,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -1862,8 +2828,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -1878,8 +2855,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -1909,6 +2886,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1988,11 +2975,11 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.21.12", "rustls-pemfile", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlformat", "thiserror 1.0.69", @@ -2031,7 +3018,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -2054,7 +3041,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2064,18 +3051,18 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.6", "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -2103,17 +3090,17 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -2259,6 +3246,36 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2303,7 +3320,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -2319,6 +3336,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.38", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2330,6 +3367,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -2371,6 +3421,28 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -2403,6 +3475,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.0" @@ -2472,6 +3550,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2515,6 +3599,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -2525,6 +3615,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2975,6 +4074,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.8.2" diff --git a/_primitives/_rust/kei-store/Cargo.toml b/_primitives/_rust/kei-store/Cargo.toml index d28f914..a32b2e4 100644 --- a/_primitives/_rust/kei-store/Cargo.toml +++ b/_primitives/_rust/kei-store/Cargo.toml @@ -3,7 +3,7 @@ name = "kei-store" version = "0.1.0" edition.workspace = true rust-version.workspace = true -description = "Memory-repo backend abstraction — GitHub/Forgejo/Gitea/Filesystem/S3 (v0.13.0)" +description = "Memory-repo backend abstraction — GitHub/Forgejo/Gitea/Filesystem/S3 (v0.21.0)" [[bin]] name = "kei-store" @@ -12,6 +12,13 @@ path = "src/main.rs" [lib] path = "src/lib.rs" +[features] +# Default: no cloud deps. S3 backend behaves as the v0.14 local-manifest stub +# (gated by KEI_STORE_ALLOW_S3_STUB=1). Users who actually need real S3 / R2 / +# MinIO push opt into the heavier AWS SDK stack by enabling this feature. +default = [] +s3 = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-credential-types", "dep:tokio"] + [dependencies] clap = { workspace = true } serde = { workspace = true } @@ -20,5 +27,11 @@ anyhow = "1" toml = "0.8" git2 = { version = "0.19", default-features = false } +# v0.21 — optional cloud stack behind `s3` feature. +aws-config = { version = "1", default-features = false, features = ["behavior-version-latest", "rustls", "rt-tokio"], optional = true } +aws-sdk-s3 = { version = "1", default-features = false, features = ["behavior-version-latest", "rustls", "rt-tokio"], optional = true } +aws-credential-types = { version = "1", optional = true } +tokio = { version = "1", features = ["rt", "macros"], optional = true } + [dev-dependencies] tempfile = "3" diff --git a/_primitives/_rust/kei-store/src/factory.rs b/_primitives/_rust/kei-store/src/factory.rs index 2f4f047..c369d75 100644 --- a/_primitives/_rust/kei-store/src/factory.rs +++ b/_primitives/_rust/kei-store/src/factory.rs @@ -1,9 +1,15 @@ //! Factory — construct a `Box` from a Config. //! //! v0.14.1: the S3 backend is gated behind `KEI_STORE_ALLOW_S3_STUB=1` -//! because it does NOT push to S3 yet — it's a local-manifest stub. -//! Previous behaviour silently stored data locally, confusing users who -//! thought their traces were uploaded. +//! because the default build has no real S3 push — it's a local-manifest +//! stub. Previous behaviour silently stored data locally, confusing users +//! who thought their traces were uploaded. +//! +//! v0.21.0: when the crate is built with `--features s3` AND +//! `s3.bucket` is configured, the real `S3CloudStore` is used (no +//! KEI_STORE_ALLOW_S3_STUB gate needed — data really is uploaded). +//! The stub path remains available for users who don't want the AWS SDK +//! in their binary: omit `s3.bucket` and set the stub opt-in env. use crate::config::{expand_tilde, Config}; use crate::{filesystem::FilesystemStore, forgejo::ForgejoStore, gitea::GiteaStore, @@ -29,12 +35,30 @@ pub fn build_store(cfg: &Config) -> Result> { } } +#[cfg(feature = "s3")] fn build_s3(cfg: &Config) -> Result> { + // Cloud path: real S3 round-trips when bucket is configured. + if cfg.s3.bucket.is_some() { + return Ok(Box::new( + crate::s3_cloud::S3CloudStore::new(cfg.s3.clone())?, + )); + } + // Fallback: local stub (legacy behaviour, requires opt-in). + build_s3_stub(cfg) +} + +#[cfg(not(feature = "s3"))] +fn build_s3(cfg: &Config) -> Result> { + build_s3_stub(cfg) +} + +fn build_s3_stub(cfg: &Config) -> Result> { if std::env::var("KEI_STORE_ALLOW_S3_STUB").is_err() { bail!( "S3 backend is a local-only MVP stub (no upload to S3/R2/MinIO yet). \ Set KEI_STORE_ALLOW_S3_STUB=1 to proceed; data will be stored in the \ - configured cache_path only. Production S3 support is planned for v0.15." + configured cache_path only. For real S3 push, build with \ + `--features s3` AND set s3.bucket in config." ); } eprintln!( diff --git a/_primitives/_rust/kei-store/src/lib.rs b/_primitives/_rust/kei-store/src/lib.rs index 9f9ffa4..bd1cc35 100644 --- a/_primitives/_rust/kei-store/src/lib.rs +++ b/_primitives/_rust/kei-store/src/lib.rs @@ -3,7 +3,9 @@ //! Trait `MemoryStore` + 5 implementations: //! - `GitHubStore`, `ForgejoStore`, `GiteaStore` — git-over-SSH/HTTPS //! - `FilesystemStore` — local `.git` only; never pushes -//! - `S3Store` — object-storage with manifest.json (MVP stub) +//! - `S3Store` — object-storage with manifest.json (MVP local stub) +//! - `S3CloudStore` — real S3 / R2 / MinIO via `aws-sdk-s3` +//! (behind `s3` feature; v0.21+) //! //! Config loaded from `~/.claude/agents/_primitives/store-config.toml` //! by default; overridable via `--config`. @@ -18,6 +20,8 @@ pub mod forgejo; pub mod gitea; pub mod github; pub mod s3; +#[cfg(feature = "s3")] +pub mod s3_cloud; pub mod store_trait; pub use config::Config; diff --git a/_primitives/_rust/kei-store/src/s3_cloud/client.rs b/_primitives/_rust/kei-store/src/s3_cloud/client.rs new file mode 100644 index 0000000..982f8fc --- /dev/null +++ b/_primitives/_rust/kei-store/src/s3_cloud/client.rs @@ -0,0 +1,81 @@ +//! aws-sdk-s3 client builder for the S3 cloud backend. +//! +//! Wraps `aws_config::defaults()` + optional endpoint override for +//! R2 / MinIO / Wasabi / any S3-compat provider. Credential chain is the +//! AWS default — env vars, `~/.aws/credentials`, IMDS — we do NOT invent +//! a new credential format (RULE 0.8 secrets-single-source honoured). + +use crate::config::S3Cfg; +use anyhow::Result; +use aws_sdk_s3::Client; + +/// Resolve the effective endpoint URL: +/// 1. `KEI_STORE_S3_ENDPOINT` env var (runtime override for tests / R2) +/// 2. `cfg.endpoint` (TOML config) +/// 3. None → aws-sdk-s3 default (real AWS) +pub fn effective_endpoint(cfg: &S3Cfg) -> Option { + if let Ok(url) = std::env::var("KEI_STORE_S3_ENDPOINT") { + if !url.is_empty() { + return Some(url); + } + } + cfg.endpoint.clone() +} + +/// Build the aws-sdk-s3 client with optional endpoint + region overrides. +pub async fn build_client(cfg: &S3Cfg) -> Result { + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + if let Some(region) = cfg.region.clone() { + loader = loader.region(aws_config::Region::new(region)); + } + let shared = loader.load().await; + let mut s3_builder = aws_sdk_s3::config::Builder::from(&shared); + if let Some(endpoint) = effective_endpoint(cfg) { + // Path-style is the safest default for non-AWS endpoints (MinIO, + // some R2 configs). AWS itself also accepts path-style. + s3_builder = s3_builder.endpoint_url(endpoint).force_path_style(true); + } + Ok(Client::from_conf(s3_builder.build())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn cfg_with_endpoint(endpoint: &str) -> S3Cfg { + S3Cfg { + endpoint: Some(endpoint.to_string()), + bucket: Some("test-bucket".to_string()), + region: Some("us-east-1".to_string()), + access_key_env: None, + secret_key_env: None, + cache_path: None, + } + } + + #[test] + fn effective_endpoint_env_overrides_cfg() { + std::env::set_var("KEI_STORE_S3_ENDPOINT", "http://127.0.0.1:9000"); + let cfg = cfg_with_endpoint("http://other:8080"); + let got = effective_endpoint(&cfg); + std::env::remove_var("KEI_STORE_S3_ENDPOINT"); + assert_eq!(got.as_deref(), Some("http://127.0.0.1:9000")); + } + + #[test] + fn effective_endpoint_cfg_when_no_env() { + std::env::remove_var("KEI_STORE_S3_ENDPOINT"); + let cfg = cfg_with_endpoint("http://127.0.0.1:9999"); + assert_eq!( + effective_endpoint(&cfg).as_deref(), + Some("http://127.0.0.1:9999") + ); + } + + #[test] + fn effective_endpoint_none_when_no_env_no_cfg() { + std::env::remove_var("KEI_STORE_S3_ENDPOINT"); + let cfg = S3Cfg::default(); + assert_eq!(effective_endpoint(&cfg), None); + } +} diff --git a/_primitives/_rust/kei-store/src/s3_cloud/keys.rs b/_primitives/_rust/kei-store/src/s3_cloud/keys.rs new file mode 100644 index 0000000..2d7b3a4 --- /dev/null +++ b/_primitives/_rust/kei-store/src/s3_cloud/keys.rs @@ -0,0 +1,60 @@ +//! Key-path helpers for the S3 cloud backend. +//! +//! `validate_rel` is the CVE-class guard: rejects absolute paths and +//! `..` components before they are spliced into an S3 key. Same pattern as +//! `filesystem::safe_join` — keeps the per-branch prefix unescapable. + +use anyhow::{bail, Result}; + +/// Reject absolute paths and any `..` component in the caller's rel path. +pub fn validate_rel(rel: &str) -> Result<()> { + if rel.starts_with('/') { + bail!("path traversal rejected: absolute path {:?}", rel); + } + for part in rel.split('/') { + if part == ".." { + bail!("path traversal rejected: parent-dir component in {:?}", rel); + } + } + Ok(()) +} + +/// Tiny DJB2 — deterministic, avoids pulling sha2 just for manifest naming. +pub fn short_hash(s: &str) -> String { + let mut h: u64 = 5381; + for b in s.bytes() { + h = h.wrapping_mul(33).wrapping_add(b as u64); + } + format!("{:x}", h) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_rel_rejects_absolute() { + let err = validate_rel("/etc/passwd").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("absolute"), "unexpected err: {msg}"); + } + + #[test] + fn validate_rel_rejects_parent() { + let err = validate_rel("a/../b").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("parent-dir"), "unexpected err: {msg}"); + } + + #[test] + fn validate_rel_accepts_normal() { + validate_rel("traces/session.jsonl").unwrap(); + validate_rel("a/b/c.txt").unwrap(); + } + + #[test] + fn short_hash_deterministic() { + assert_eq!(short_hash("abc"), short_hash("abc")); + assert_ne!(short_hash("abc"), short_hash("abd")); + } +} diff --git a/_primitives/_rust/kei-store/src/s3_cloud/mod.rs b/_primitives/_rust/kei-store/src/s3_cloud/mod.rs new file mode 100644 index 0000000..8a0b07b --- /dev/null +++ b/_primitives/_rust/kei-store/src/s3_cloud/mod.rs @@ -0,0 +1,190 @@ +//! S3CloudStore — real object-storage backend via `aws-sdk-s3`. +//! +//! Behind `#[cfg(feature = "s3")]`; ~5 MB release binary growth. +//! Credentials via AWS default chain (env / ~/.aws / IMDS). +//! Endpoint override for R2 / MinIO / Wasabi: `KEI_STORE_S3_ENDPOINT` +//! env var OR `s3.endpoint` in TOML. +//! "Branch" = key prefix (`/`); default `main`. +//! Sync `MemoryStore` trait bridged over async SDK via a single +//! current-thread tokio runtime (`block_on` per call). + +mod client; +mod keys; + +use crate::config::S3Cfg; +use crate::store_trait::MemoryStore; +use anyhow::{anyhow, Context, Result}; +use aws_sdk_s3::primitives::ByteStream; +use aws_sdk_s3::Client; +use std::sync::Mutex; +use tokio::runtime::{Builder, Runtime}; + +const DEFAULT_BRANCH: &str = "main"; + +pub struct S3CloudStore { + client: Client, + bucket: String, + rt: Runtime, + branch: Mutex, +} + +impl S3CloudStore { + /// Build a cloud-S3 backend. `bucket` MUST be configured. + pub fn new(cfg: S3Cfg) -> Result { + let bucket = cfg + .bucket + .clone() + .ok_or_else(|| anyhow!("s3 backend requires s3.bucket in config"))?; + let rt = Builder::new_current_thread() + .enable_all() + .build() + .context("init tokio runtime")?; + let client = rt.block_on(client::build_client(&cfg))?; + Ok(Self { + client, + bucket, + rt, + branch: Mutex::new(DEFAULT_BRANCH.to_string()), + }) + } + + fn current_branch(&self) -> String { + self.branch + .lock() + .map(|g| g.clone()) + .unwrap_or_else(|poison| poison.into_inner().clone()) + } + + fn key(&self, rel: &str) -> Result { + keys::validate_rel(rel)?; + Ok(format!("{}/{}", self.current_branch(), rel)) + } + + async fn get(&self, key: &str) -> Result> { + let resp = self + .client + .get_object() + .bucket(&self.bucket) + .key(key) + .send() + .await + .with_context(|| format!("s3 get_object {key}"))?; + let body = resp + .body + .collect() + .await + .with_context(|| format!("s3 read body {key}"))?; + Ok(body.into_bytes().to_vec()) + } + + async fn put(&self, key: &str, bytes: Vec) -> Result<()> { + self.client + .put_object() + .bucket(&self.bucket) + .key(key) + .body(ByteStream::from(bytes)) + .send() + .await + .with_context(|| format!("s3 put_object {key}"))?; + Ok(()) + } + + async fn list_prefix(&self, prefix: &str) -> Result> { + let mut out = Vec::new(); + let mut token: Option = None; + loop { + let mut req = self + .client + .list_objects_v2() + .bucket(&self.bucket) + .prefix(prefix) + .delimiter("/"); + if let Some(t) = token.as_ref() { + req = req.continuation_token(t); + } + let resp = req + .send() + .await + .with_context(|| format!("s3 list {prefix}"))?; + for obj in resp.contents() { + if let Some(k) = obj.key() { + if let Some(name) = k.strip_prefix(prefix) { + if !name.is_empty() { + out.push(name.to_string()); + } + } + } + } + if resp.is_truncated().unwrap_or(false) { + token = resp.next_continuation_token().map(|s| s.to_string()); + } else { + break; + } + } + out.sort(); + Ok(out) + } +} + +impl MemoryStore for S3CloudStore { + fn read(&self, path: &str) -> Result> { + let key = self.key(path)?; + self.rt.block_on(self.get(&key)) + } + + fn write(&self, path: &str, bytes: &[u8]) -> Result<()> { + let key = self.key(path)?; + self.rt.block_on(self.put(&key, bytes.to_vec())) + } + + fn list(&self, dir: &str) -> Result> { + let raw = self.key(dir)?; + let prefix = if raw.ends_with('/') { + raw + } else { + format!("{raw}/") + }; + self.rt.block_on(self.list_prefix(&prefix)) + } + + fn branch(&self, name: &str) -> Result<()> { + keys::validate_rel(name)?; + let mut g = self + .branch + .lock() + .map_err(|_| anyhow!("branch lock poisoned"))?; + *g = name.to_string(); + Ok(()) + } + + fn commit(&self, message: &str) -> Result { + // Object stores have no native commit — write a manifest blob naming + // every key under the current branch and return a cheap hash. + let entries = self.list("")?; + let manifest = serde_json::json!({ + "message": message, + "branch": self.current_branch(), + "entries": entries, + }) + .to_string(); + let hash = keys::short_hash(&manifest); + self.write(&format!("manifest-{hash}.json"), manifest.as_bytes())?; + Ok(hash) + } + + fn push(&self, _branch: &str) -> Result<()> { + // Every write() already persists to S3 — no staging to flush. + Ok(()) + } + + fn pull(&self, _branch: &str) -> Result<()> { + Ok(()) + } + + fn backend_name(&self) -> &'static str { + "s3-cloud" + } +} + +#[cfg(test)] +mod tests; diff --git a/_primitives/_rust/kei-store/src/s3_cloud/tests.rs b/_primitives/_rust/kei-store/src/s3_cloud/tests.rs new file mode 100644 index 0000000..309f3a2 --- /dev/null +++ b/_primitives/_rust/kei-store/src/s3_cloud/tests.rs @@ -0,0 +1,63 @@ +//! Unit tests for S3CloudStore — no network, mock-endpoint only. +//! +//! These tests verify builder correctness + path-safety guards. They do +//! NOT exercise real S3 round-trips (that would require live AWS/MinIO +//! and would fail in CI without credentials). See `tests/s3_smoke.rs` +//! for the cross-crate smoke integration. + +use super::*; + +fn cfg(endpoint: &str) -> S3Cfg { + S3Cfg { + endpoint: Some(endpoint.to_string()), + bucket: Some("test-bucket".to_string()), + region: Some("us-east-1".to_string()), + access_key_env: None, + secret_key_env: None, + cache_path: None, + } +} + +#[test] +fn new_rejects_missing_bucket() { + let c = S3Cfg { + endpoint: Some("http://127.0.0.1:9999".to_string()), + region: Some("us-east-1".to_string()), + ..Default::default() + }; + let err = S3CloudStore::new(c) + .err() + .expect("missing bucket should error"); + assert!(format!("{err:#}").contains("bucket")); +} + +#[test] +fn new_builds_with_mock_endpoint() { + let store = S3CloudStore::new(cfg("http://127.0.0.1:9999")).unwrap(); + assert_eq!(store.backend_name(), "s3-cloud"); + assert_eq!(store.current_branch(), "main"); +} + +#[test] +fn branch_updates_prefix() { + let store = S3CloudStore::new(cfg("http://127.0.0.1:9999")).unwrap(); + store.branch("feat/foo").unwrap(); + assert_eq!( + store.key("traces/a.jsonl").unwrap(), + "feat/foo/traces/a.jsonl" + ); +} + +#[test] +fn branch_rejects_parent() { + let store = S3CloudStore::new(cfg("http://127.0.0.1:9999")).unwrap(); + let err = store.branch("../escape").unwrap_err(); + assert!(format!("{err:#}").contains("parent-dir")); +} + +#[test] +fn key_rejects_absolute() { + let store = S3CloudStore::new(cfg("http://127.0.0.1:9999")).unwrap(); + let err = store.key("/etc/passwd").unwrap_err(); + assert!(format!("{err:#}").contains("absolute")); +} diff --git a/_primitives/_rust/kei-store/tests/s3_smoke.rs b/_primitives/_rust/kei-store/tests/s3_smoke.rs new file mode 100644 index 0000000..3d75fa9 --- /dev/null +++ b/_primitives/_rust/kei-store/tests/s3_smoke.rs @@ -0,0 +1,81 @@ +//! Smoke tests for the S3 cloud backend (behind `s3` feature). +//! +//! These tests never hit real AWS. They verify: +//! - the `S3CloudStore` builder accepts a mock endpoint without panic +//! - the library re-exports `s3_cloud` when the feature is enabled +//! - path-safety guards reject traversal attempts +//! +//! Run with: `cargo test -p kei-store --features s3 --test s3_smoke`. +//! Without the feature, this file compiles to an empty crate — harmless. + +#![cfg(feature = "s3")] + +use kei_store::config::S3Cfg; +use kei_store::s3_cloud::S3CloudStore; +use kei_store::MemoryStore; + +fn mock_cfg(endpoint: &str) -> S3Cfg { + S3Cfg { + endpoint: Some(endpoint.to_string()), + bucket: Some("test-bucket".to_string()), + region: Some("us-east-1".to_string()), + access_key_env: None, + secret_key_env: None, + cache_path: None, + } +} + +#[test] +fn builder_accepts_mock_endpoint() { + let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9999")) + .expect("builder must not require network"); + assert_eq!(store.backend_name(), "s3-cloud"); +} + +#[test] +fn builder_rejects_missing_bucket() { + let cfg = S3Cfg { + endpoint: Some("http://127.0.0.1:9999".to_string()), + region: Some("us-east-1".to_string()), + ..Default::default() + }; + let err = S3CloudStore::new(cfg) + .err() + .expect("missing bucket should error"); + assert!(format!("{err:#}").contains("bucket")); +} + +#[test] +fn branch_switches_prefix() { + let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9999")).unwrap(); + store.branch("agent/foo").unwrap(); + // No network IO — just verify branch() does not error and backend_name + // stays stable. + assert_eq!(store.backend_name(), "s3-cloud"); +} + +#[test] +fn write_fails_gracefully_on_unreachable_endpoint() { + // Point at a closed port — real put_object must error, NOT panic. + let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9")).unwrap(); + let err = store.write("traces/x.jsonl", b"hello").unwrap_err(); + let msg = format!("{err:#}"); + // We only assert that an error propagates — the exact wording depends + // on the aws-smithy layer. + assert!(!msg.is_empty()); +} + +#[test] +fn endpoint_env_var_is_honoured() { + std::env::set_var("KEI_STORE_S3_ENDPOINT", "http://127.0.0.1:9999"); + // cfg endpoint differs — env should win. Builder still succeeds. + let cfg = S3Cfg { + endpoint: Some("http://unused:1".to_string()), + bucket: Some("b".to_string()), + region: Some("us-east-1".to_string()), + ..Default::default() + }; + let s = S3CloudStore::new(cfg); + std::env::remove_var("KEI_STORE_S3_ENDPOINT"); + assert!(s.is_ok()); +}