From c912c791fa1ef3611ec6e222577705b4fa4ae335 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Tue, 14 Apr 2026 18:23:11 +0200 Subject: [PATCH] Added coding docs from shsf to shsf-cli --- OpenclawSkill.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 275 insertions(+), 1 deletion(-) diff --git a/OpenclawSkill.md b/OpenclawSkill.md index 4bd5fe5..873e1bc 100644 --- a/OpenclawSkill.md +++ b/OpenclawSkill.md @@ -112,4 +112,278 @@ and then replace the old one with the new one using `mv`. ## Missing a feature? Features might be not documented yet, as this skill document rarely updates. -Use the shsf -h command to see options and features. \ No newline at end of file +Use the shsf -h command to see options and features. + +CODING DOC: +# SHSF Platform Reference (Agent-Optimized) + +## Critical Rules (check before writing any code) + +- Python files use `.py`, Go files use `.go` — never mix runtimes +- Go package must be `main`; entry-point must be `main_user()`, never `main()` +- Python entry-point must be `def main(args):` +- **Always** `import json` and call `json.loads(args.get("body", "{}"))` before accessing body fields in Python +- Forbidden filenames: `_runner.py`, `_runner.js`, `init.sh` +- Filenames must never contain `/` or `\` — no subdirectories +- Never hard-code secrets — use environment variables via `os.getenv()` +- Never invent SHSF APIs not documented here +- Never write partial files or placeholder comments +- Only create `requirements.txt` / `go.mod` if dependencies are actually needed + +--- + +## Entry Points + +### Python +```python +def main(args): + return {"hello": "world"} # plain dict → 200 JSON +``` + +### Go +```go +package main + +func main_user(args interface{}) (interface{}, error) { + return map[string]string{"hello": "world"}, nil +} +``` + +--- + +## The `args` Object + +| Field | Type | Notes | +|------------|------------|-----------------------------------------------------------------------| +| `body` | string | Raw JSON string — **must** be parsed with `json.loads()` before use | +| `queries` | dict / map | URL query parameters | +| `route` | string | Sub-path segment after function URL. Default: `"default"` | +| `headers` | dict / map | Lowercased HTTP request headers | +| `raw_body` | bytes | Raw request body (file uploads, binary data) | +| `method` | string | HTTP method (GET, POST, …) | + +Always use `.get()` / nil-checks — never assume a field is present. + +### Python args example +```python +import json + +def main(args): + body = json.loads(args.get("body", "{}")) # always parse first + queries = args.get("queries", {}) + route = args.get("route", "default") + name = body.get("name", "stranger") + page = queries.get("page", "1") + return {"greeting": f"Hello {name}", "page": page, "route": route} +``` + +--- + +## Response Formats + +### Simple 200 JSON (plain dict) +```python +return {"key": "value"} +``` + +### v2 Envelope (control status, headers, body) +```python +return { + "_shsf": "v2", + "_code": 201, # HTTP status code + "_headers": {"Content-Type": "application/json"}, # optional + "_res": {"created": True, "id": 42} # response body +} +``` + +### Error response +```python +return {"_shsf": "v2", "_code": 400, "_res": {"error": "missing field 'name'"}} +``` + +### Redirect (301 / 302) +```python +return {"_shsf": "v2", "_code": 302, "_location": "https://example.com/target"} +``` + +### HTML response +```python +def main(args): + with open("index.html", "r") as f: + html = f.read() + return {"_shsf": "v2", "_code": 200, "_headers": {"Content-Type": "text/html"}, "_res": html} +``` + +> **Static HTML shortcut**: if the only file is a single `.html` set as the startup file, SHSF serves it directly without spinning up a runtime. + +--- + +## Routing + +`args["route"]` holds the single URL segment after the function base URL (no leading slash, default `"default"`). Only **one** segment is supported. + +```python +def main(args): + route = args.get("route", "default") + if route == "register": return handle_register(args) + elif route == "login": return handle_login(args) + elif route == "status": return {"status": "ok"} + else: return {"_shsf": "v2", "_code": 404, "_res": {"error": "route not found"}} +``` + +--- + +## Environment Variables + +Never hard-code secrets. Define them in the SHSF dashboard. + +```python +import os + +def main(args): + api_key = os.getenv("MY_API_KEY", "") + if not api_key: + return {"_shsf": "v2", "_code": 500, "_res": {"error": "MY_API_KEY not set"}} + return {"ok": True} +``` + +Go: `apiKey := os.Getenv("MY_API_KEY")` + +--- + +## Persistent Storage + +| Path | Persistence | Use for | +|---------|--------------------------|---------------------------------| +| `/app/` | Persists across calls | Cache, state files | +| `/tmp/` | Wiped on container restart | Truly temporary scratch work | + +**WARNING**: SHSF may restart or update containers, which recreates all of it, even the `/app/` directory. For critical data, use `_db_com` instead. + +```python +import json, os + +CACHE = "/app/cache.json" + +def main(args): + data = json.load(open(CACHE)) if os.path.exists(CACHE) else {} + data["hits"] = data.get("hits", 0) + 1 + json.dump(data, open(CACHE, "w")) + return {"hits": data["hits"]} +``` + +### Redis (shared key-value, fast) +```python +import redis + +r = redis.Redis(host="localhost", port=6379, db=0) + +def main(args): + r.incr("counter") + return {"counter": int(r.get("counter"))} +``` + +--- + +## Database (`_db_com`) — Python + +`_db_com.py` is auto-provisioned. Add `requests` to `requirements.txt`. + +```python +from _db_com import database +from datetime import datetime, timedelta + +db = database() + +def main(args): + db.create_storage("my_app", purpose="application data") # idempotent, safe every call + + db.set("my_app", "username", "alice") # write + + expires = (datetime.utcnow() + timedelta(hours=1)).isoformat() + db.set("my_app", "session", "tok_abc", expires_at=expires) # write with TTL + + username = db.get("my_app", "username") # read (None if missing) + exists = db.exists("my_app", "username") # existence check + items = db.list_items("my_app") # list all keys + db.delete_item("my_app", "username") # delete + + return {"username": username, "items": items} +``` + +### Go `dbcom` +```go +package main + +import "myfunction/dbcom" + +func main_user(args interface{}) (interface{}, error) { + db := dbcom.New() + if _, err := db.Set("my-storage", "key", "value", nil); err != nil { + return nil, err + } + value, err := db.Get("my-storage", "key") + if err != nil { + return nil, err + } + return map[string]interface{}{"value": value}, nil +} +``` + +--- + +## File Uploads / Raw Body + +```python +def main(args): + raw = args.get("raw_body") + if raw is None: + return {"_shsf": "v2", "_code": 400, "_res": {"error": "no body provided"}} + if isinstance(raw, str): + raw = raw.encode("latin-1") + with open("/app/upload.bin", "wb") as f: + f.write(raw) + return {"_shsf": "v2", "_code": 200, "_res": {"saved": True}} +``` + +--- + +## Secure Header (`x-secure-header`) + +When the secure-header feature is enabled, SHSF validates the token **before** invoking your function — no need to re-validate. Read it only for logging: + +```python +def main(args): + token = args.get("headers", {}).get("x-secure-header", "") + return {"authenticated": True, "token_preview": token[:4] + "…"} +``` + +--- + +## Dependency Files + +| Runtime | File | Notes | +|---------|-------------------|------------------------------------------------| +| Python | `requirements.txt`| pip-installed before first run | +| Go | `go.mod`+`go.sum` | Module deps, auto-downloaded | + +Only create these files if you have actual dependencies. + +### Python `requirements.txt` +``` +requests==2.31.0 +beautifulsoup4==4.12.2 +``` + +### Go `go.mod` +``` +module myfunction + +go 1.23 + +require ( + github.com/google/uuid v1.3.0 +) +``` + +Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23` \ No newline at end of file