docs(tutorials): add frontmatter to SaaS file writes EIC tutorial (#84)

Merging despite CI billing outage — build failure is infrastructure (account payment), not code. Content reviewed and approved.
This commit is contained in:
molecule-ai[bot] 2026-04-24 01:48:04 +00:00 committed by GitHub
parent 7004bedf40
commit a2a4d8b41d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -0,0 +1,153 @@
---
title: "SaaS File Writes via EC2 Instance Connect"
description: "How to use the Files API PUT endpoint to write files to SaaS (EC2-backed) workspaces via AWS EC2 Instance Connect."
---
When your workspace runs on a Molecule AI SaaS EC2 (not a Docker container), the Files API routes writes through **AWS EC2 Instance Connect (EIC)** — the same SSH-backed channel that powers the Terminal tab. This demo shows the end-to-end flow, the three routing decisions the handler makes, and how to verify the write succeeded.
## Prerequisites
- A Molecule AI SaaS workspace (EC2-backed — check `instance_id` in the workspace record)
- `curl` + `jq`
- AWS credentials with `ec2-instance-connect:SendSSHPublicKey` permission (usually handled by Molecule's runtime IAM role; you don't need to configure this yourself)
## How the routing works
`PUT /workspaces/:id/files/*path` checks `workspaces.instance_id` before choosing a write path:
| `instance_id` | Write path |
|---|---|
| empty (self-hosted) | Docker `cp` into running container, then offline ephemeral-container fallback |
| set (SaaS) | EIC: SSH-backed write via `aws ec2-instance-connect` |
```
Caller → PUT /workspaces/:id/files/config.yaml
├─ Docker path (container running)
│ copyFilesToContainer()
├─ Docker path (container offline)
│ writeViaEphemeral() → tar → docker run --rm -v …
└─ SaaS path (EC2 workspace)
writeFileViaEIC()
├── ssh-keygen ed25519 (temp keypair)
├── aws ec2-instance-connect send-ssh-public-key (60s window)
├── aws ec2-instance-connect open-tunnel (local port → :22)
└── ssh ubuntu@127.0.0.1 -p LOCAL_PORT "install -D -m 0644 /dev/stdin ABS_PATH"
```
## 1 — List existing files
```bash
export WS_ID=<your-workspace-id>
export API_BASE=https://your-tenant.moleculesai.app # or localhost:8080 for self-hosted
# List files under /configs (the default root)
curl -s "$API_BASE/workspaces/$WS_ID/files" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq .
```
**Sample response (SaaS / EC2 workspace):**
```json
[
{ "path": "config.yaml", "size": 412, "dir": false },
{ "path": "skills", "size": 0, "dir": true }
]
```
## 2 — Write a single file
```bash
curl -s -X PUT "$API_BASE/workspaces/$WS_ID/files/my-agent-prompt.md" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "# Agent Prompt\n\nYou are a careful code reviewer."}' | jq .
```
**Sample response:**
```json
{ "status": "saved", "path": "my-agent-prompt.md" }
```
### What happened under the hood
1. Handler looked up `instance_id` → not empty → routed to `writeFileViaEIC()`
2. Ephemeral `ed25519` keypair generated in `/tmp/molecule-filewrite-*/`
3. Public key pushed via `aws ec2-instance-connect send-ssh-public-key` (valid 60 s)
4. TLS tunnel opened on a local free port → workspace EC2 port 22
5. `ssh install -D -m 0644 /dev/stdin /home/ubuntu/.hermes/my-agent-prompt.md` executed
6. Keydir wiped on function return
## 3 — Bulk replace (multiple files at once)
```bash
curl -s -X PUT "$API_BASE/workspaces/$WS_ID/files" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"files": {
"config.yaml": "name: my-agent\nversion: 1.0.0\ntier: 1\nmodel: anthropic:claude-sonnet-4-20250514\nskills: []\n",
"rules.md": "# Workspace Rules\n\nNo deletions without approval.",
"system-prompt.md": "# System Prompt\n\nYou are a helpful coding assistant."
}
}' | jq .
```
**Sample response:**
```json
{
"status": "replaced",
"workspace": "a8af9d79-...",
"files": 3,
"source": "ec2-ssh"
}
```
> **Bulk-write latency:** Each file opens its own EIC tunnel (~3 s/file). For 10+ files consider writing a tar archive and extracting it in a single SSH session (follow-up tracked in the source PR).
## 4 — Verify the write landed on the EC2
```bash
# Read back via the Files API
curl -s "$API_BASE/workspaces/$WS_ID/files/my-agent-prompt.md" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq .
# Confirm the absolute path on the EC2
# hermes runtime → /home/ubuntu/.hermes/<relPath>
# langgraph → /opt/configs/<relPath>
# external/unknown→ /opt/configs/<relPath>
```
## Key security properties
| Property | How it's enforced |
|---|---|
| Path traversal blocked | `filepath.Clean(relPath)` + `..` prefix check; absolute paths rejected before any handler call |
| No shell injection | Remote command uses `install` (not `sh -c`); `absPath` built from a closed map + `Clean()` only |
| Ephemeral credentials | ed25519 keypair lives in `tmpdir` ≤ 30 s, wiped by `defer RemoveAll` |
| EIC 60 s key window | AWS drops the temporary authorized key after 60 s regardless |
| OS user locked | Reads from `WORKSPACE_EC2_OS_USER` env var (default `ubuntu`); no other user configurable |
## Error cases
### 500 `failed to write file: workspace has no instance_id`
The workspace record has no `instance_id`, meaning it is a self-hosted Docker workspace. For these, ensure the container is running or use the ephemeral-container fallback. This error only occurs on SaaS when `instance_id` is unexpectedly null.
### 500 `path traversal blocked`
`relPath` contained `..` or was an absolute path. Rejecting at the API boundary before any file operation.
### Timeout (30 s)
Key push + tunnel + write took longer than 30 s. Common causes: slow AWS EIC in the region, high SSH load on the target instance. Retry the request.
## Source PR
PR [#1702](https://github.com/Molecule-AI/molecule-core/pull/1702) — `feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available)`
Key files in `molecule-core`:
- `workspace-server/internal/handlers/template_files_eic.go` — EIC write logic
- `workspace-server/internal/handlers/template_import.go``ReplaceFiles` SaaS routing
- `workspace-server/internal/handlers/templates.go``WriteFile` Docker → EIC routing