# MuMu × AWS (S3) Plugin
A lightweight extension that adds **Amazon S3** capabilities to the MuMu runtime: credentials sessions, bucket creation, and object fetching (to file or as a streaming iterator). Built-in SigV4 signing, friendly error surfaces, and examples for both real S3 and S3‑compatible endpoints (e.g., LocalStack).
Language:**Rust**Runtime:**MuMu**Auth:**SigV4**Transport:**reqwest (blocking)**
**Table of contents**
1. [Highlights](#highlights)
2. [Quick start](#quick-start)
3. [Usage](#usage)
4. [API reference](#api-reference)
5. [How it works](#how-it-works)
6. [Repository layout](#repo-layout)
7. [LocalStack tips](#localstack)
8. [Troubleshooting](#troubleshooting)
9. [Security](#security)
10. [License](#license)
11. [Acknowledgements](#ack)
## Highlights
- 🔐 **Session‑based credentials** via aws:credentials(…) (*no* global state; returns a short session id)
- ✍️ **SigV4 signing** in pure Rust (src/functions/sigv4.rs)
- 🪣 **Create bucket**: aws:s3:create_bucket(…)
- ⬇️ **Fetch object**: save to file (path/folder) *or* stream as a MuMu **InkIterator**
- 🧯 **Robust errors**: dynamic calls catch panics and return [ok: false, error: "…"]
## Quick start
### 1) Build the plugin
```bash
# from the repository root
cargo build --release
# point MuMu to the build directory (adjust for your OS/shell)
export MUMU_PLUGIN_PATH="$(pwd)/target/release"
```
### 2) Run MuMu (REPL or script)
```bash
# REPL (verbose is handy while developing)
mumu --verbose
# or (depending on install)
lava-mumu --verbose
# Run a .mu script
mumu examples/s3/fetch.mu
```
### 3) Load the plugin and configure credentials
```mu
extend("aws")
extend("process") // to read environment variables
credentials = aws:credentials([
accessKeyId: process:env("AWS_ACCESS_KEY_ID"),
secretAccessKey: process:env("AWS_SECRET_ACCESS_KEY"),
region: process:env("AWS_REGION"),
// Optional: S3-compatible endpoint (e.g., LocalStack)
// endpoint: "http://localhost:4566"
])
```
*Return value:* a short session id string — pass this to other AWS calls.
## Usage
### Create a bucket
```mu
extend("aws")
extend("process")
extend("test") // just for test:diceware()
credentials = aws:credentials([
accessKeyId: process:env("AWS_ACCESS_KEY_ID"),
secretAccessKey: process:env("AWS_SECRET_ACCESS_KEY"),
region: process:env("AWS_REGION"),
endpoint: "http://localhost:4566" // optional
])
result = aws:s3:create_bucket([
credentials: credentials,
bucket: test:diceware()
])
slog(result) // → [ok: true, bucket: "..."] or [ok: false, error: "..."]
```
### Fetch an object — save to disk
```mu
extend("aws")
extend("process")
credentials = aws:credentials([
accessKeyId: process:env("AWS_ACCESS_KEY_ID"),
secretAccessKey: process:env("AWS_SECRET_ACCESS_KEY"),
region: process:env("AWS_REGION")
])
result = aws:s3:fetch([
credentials: credentials,
bucket: "my-bucket",
key: "path/to/object.png",
// use either:
// path: "/tmp/object.png",
folder: "/tmp/s3-downloads" // filename inferred from key
])
slog(result) // → [ok: true, path: "/tmp/s3-downloads/object.png"]
```
### Fetch an object — stream chunks
Omit path/folder to get an **InkIterator** yielding chunks: { chunk: IntArray }.
```mu
extend("aws")
extend("file")
extend("process")
credentials = aws:credentials([
accessKeyId: process:env("AWS_ACCESS_KEY_ID"),
secretAccessKey: process:env("AWS_SECRET_ACCESS_KEY"),
region: process:env("AWS_REGION")
])
stream = aws:s3:fetch([
credentials: credentials,
bucket: "my-bucket",
key: "large-object.bin"
])
file:write("/tmp/large-object.bin", stream)
```
Chunks are read in up to 8192‑byte blocks and delivered as *signed* ints (0–255) in an IntArray. The iterator finishes naturally when the stream ends.
> **More streaming examples:** examples/s3/fetch-stream.mu, examples/s3/fetch-stream-map.mu, examples/s3/fetch-multi.mu.
## API reference
### `aws:credentials(opts)` → *session_id:string*
| `accessKeyId` | string | Yes | AWS access key id |
| `secretAccessKey` | string | Yes | AWS secret |
| `region` | string | Yes | e.g., `us-east-1` |
| `endpoint` | string | No | S3‑compatible endpoint (LocalStack, MinIO w/ S3 API, etc.) |
Validates inputs and stores an in‑memory session. Returns a short session id. On failure, the dynamic wrapper surfaces an error object:
```mu
[ok: false, error: "aws:credentials: ..."]
```
### `aws:s3:create_bucket(opts)` → *{ ok, bucket? , error? }*
| `credentials` | string | Yes | Session id from `aws:credentials` |
| `bucket` | string | Yes | Bucket name |
On success returns [ok: true, bucket: "…"]; on error returns [ok: false, error: "…"].
### `aws:s3:fetch(opts)` → *InkIterator* **or** *{ ok, path? , error? }*
| `credentials` | string | Yes | Session id |
| `bucket` | string | Yes | Bucket name |
| `key` | string | Yes | Object key |
| `path` | string | No | Full destination path (direct download) |
| `folder` | string | No | Destination directory (filename inferred) |
- If `path` *or* `folder` is provided → downloads the object and returns [ok: true, path: "..."].
- If neither is provided → returns a streaming **InkIterator** of chunks `{ chunk: IntArray }`.
## How it works
- **Session store:** SESSIONS (lazy static HashMap<String, AwsSession>) keyed by session id. (src/register/credentials.rs)
- **SigV4:** canonical request + hex SHA‑256 + HMAC chain; returns ready‑to‑use headers (including Authorization). (src/functions/sigv4.rs)
- **Networking:** reqwest::blocking client. Direct download writes to a file; streaming spawns a reader thread that sends 8 KiB blocks over an MPSC channel to a MuMu plugin iterator. (src/register/s3_fetch.rs)
- **Error model:** all dynamic functions are guarded with catch_unwind; failures are returned as keyed arrays, never raw panics.
## Repository layout
```
LOCAL/
src/
functions/
sigv4.rs
register/
credentials.rs
s3_create_bucket.rs
s3_fetch.rs
hello.rs
examples/
s3/
create-bucket.mu
fetch.mu
fetch-multi.mu
fetch-stream.mu
fetch-stream-map.mu
REMOTE/ (MuMu engine; vendored)
src/
parser/... # lexer, parser, interpreter, eval helpers, etc.
modules/... # extend, compose/pipe, slog/sput, ink, include, type
```
The vendored MuMu engine includes improvements for λ by‑reference semantics, composition, numeric coercions, REPL input, autocompletion, and statement write‑through to reference cells.
## LocalStack tips
1. Run LocalStack with S3 enabled (e.g., via Docker).
2. Use test credentials and set
```
endpoint
```
in
```
aws:credentials
```
:
```mu
credentials = aws:credentials([
accessKeyId: "test",
secretAccessKey: "test",
region: "us-east-1",
endpoint: "http://localhost:4566"
])
```
3. Use the same APIs as you would for AWS.
## Troubleshooting
- **“no session with id …”** — The session id is missing or stale. Call aws:credentials again and reuse its return value.
- **“AWS error status …”** — Check bucket permissions, region, and the exact bucket/key. For S3‑compatible endpoints ensure the endpoint is set in aws:credentials.
- Plugin not found
— Ensure
MUMU_PLUGIN_PATH
includes the directory with the compiled library. The loader also searches near the MuMu binary and common system locations:
- Current exe dir and ../lib
- CWD: ./libmumuaws.* or ./aws/libmumuaws.*
- Linux: /usr/local/lib, /usr/lib64, /usr/lib
- macOS: /usr/local/lib, /opt/homebrew/lib
- Windows: %ProgramFiles%\lava-mumu\lib
## Security
- Sessions are in‑memory only; no persistence.
- Prefer process:env("…") for secrets; avoid hard‑coding.
- Use S3‑compatible stacks (e.g., LocalStack) for development and tests.
## License
MuMu engine files reference the MIT license in headers. If this plugin has a separate license, consult the repository’s LICENSE. Example files are provided for educational use.
## Acknowledgements
Thanks to the MuMu contributors for the runtime, REPL, and libraries. This plugin follows MuMu’s dynamic function conventions (extend("aws")) and uses **InkIterator** for streaming downloads.
------
© 2025 MuMu × AWS Plugin