Skip to content

Commit 78106db

Browse files
authored
Merge pull request #7 from Jahanzeb9999/feat/agents-integration
merged Feat/agents integration
2 parents cf63407 + c0986a9 commit 78106db

17 files changed

Lines changed: 1285 additions & 24 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ Run `make help` to see all available targets. Key ones:
310310
| [Dataset Guide](docs/DATASET-GUIDE.md) | Preparing and uploading datasets |
311311
| [Deployment](docs/DEPLOYMENT.md) | Production deployment guide |
312312
| [LLM Integration](docs/LLM-INTEGRATION.md) | LLM assistant architecture and extending |
313+
| [API for agents & automation](docs/API-FOR-AGENTS.md) | Same REST API for OpenClaw, CLI, and any coding agent; API key auth |
314+
| [OpenClaw Integration](docs/OPENCLAW-INTEGRATION.md) | Drive OpenModelStudio from OpenClaw AI agents (Telegram, Discord, etc.) |
315+
| [OpenClaw & Claude step-by-step](docs/OPENCLAW-AND-CLAUDE.md) | Follow-along guide: OpenClaw and Claude Code setup and usage |
316+
| [OpenClaw Quickstart & Testing](docs/OPENCLAW-QUICKSTART.md) | Full flow, config, API key, and test checklist |
313317
| [Research Models](docs/RESEARCH_MODELS.md) | Research architectures: HARPA, Genie, JEPA |
314318
| [Contributing](docs/CONTRIBUTING.md) | How to contribute |
315319

api/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ base64 = "0.22"
3030
futures = "0.3"
3131
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
3232
password-hash = "0.5"
33+
sha2 = "0.10"
34+
hex = "0.4"
3335
rustls = { version = "0.23", default-features = false, features = ["ring"] }
3436

3537
[dev-dependencies]

api/src/auth.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use argon2::{
55
use chrono::{Duration, Utc};
66
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
77
use serde::{Deserialize, Serialize};
8+
use sha2::{Digest, Sha256};
89
use uuid::Uuid;
910

1011
use crate::models::user::UserRole;
@@ -110,3 +111,21 @@ pub fn validate_token(
110111
)?;
111112
Ok(data.claims)
112113
}
114+
115+
/// SHA-256 hash of the API key for secure storage and lookup (recommended).
116+
/// New keys use this; lookup tries this first.
117+
pub fn compute_api_key_hash(raw: &str) -> String {
118+
let mut hasher = Sha256::new();
119+
hasher.update(raw.as_bytes());
120+
hex::encode(hasher.finalize())
121+
}
122+
123+
/// Legacy FNV-1a hash; only for backwards-compat lookup of keys created before SHA-256 was introduced.
124+
pub fn compute_api_key_hash_legacy(raw: &str) -> String {
125+
let mut h: u64 = 0xcbf29ce484222325;
126+
for b in raw.as_bytes() {
127+
h ^= *b as u64;
128+
h = h.wrapping_mul(0x100000001b3);
129+
}
130+
format!("{:016x}", h)
131+
}

api/src/middleware/auth.rs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
use axum::extract::FromRequestParts;
22
use axum::http::request::Parts;
3+
use chrono::Utc;
4+
use sqlx::FromRow;
5+
use uuid::Uuid;
36

4-
use crate::auth::{validate_token, Claims};
7+
use crate::auth::{compute_api_key_hash, compute_api_key_hash_legacy, validate_token, Claims};
58
use crate::error::AppError;
69
use crate::models::user::UserRole;
710
use crate::AppState;
811

9-
/// Extracts and validates JWT from Authorization header
12+
/// Extracts and validates JWT or API key from Authorization header.
13+
/// Accepts: Bearer <jwt> or Bearer oms_<uuid> (API key from Settings → API Keys).
1014
#[derive(Debug, Clone)]
1115
pub struct AuthUser(pub Claims);
1216

17+
#[derive(FromRow)]
18+
struct ApiKeyUserRow {
19+
user_id: Uuid,
20+
email: String,
21+
role: UserRole,
22+
}
23+
1324
impl FromRequestParts<AppState> for AuthUser {
1425
type Rejection = AppError;
1526

@@ -27,12 +38,50 @@ impl FromRequestParts<AppState> for AuthUser {
2738
.strip_prefix("Bearer ")
2839
.ok_or_else(|| AppError::Unauthorized("Invalid authorization format".into()))?;
2940

30-
let claims = validate_token(token, &state.config.jwt_secret)
31-
.map_err(|e| AppError::Unauthorized(format!("Invalid token: {e}")))?;
41+
let claims = if token.starts_with("oms_") {
42+
// API key: look up by hash (SHA-256 first, then legacy FNV for old keys)
43+
let key_hash = compute_api_key_hash(token);
44+
let key_hash_legacy = compute_api_key_hash_legacy(token);
45+
let row: Option<ApiKeyUserRow> = sqlx::query_as(
46+
"SELECT u.id AS user_id, u.email, u.role
47+
FROM api_keys ak
48+
JOIN users u ON u.id = ak.user_id
49+
WHERE ak.key_hash = $1 OR ak.key_hash = $2",
50+
)
51+
.bind(&key_hash)
52+
.bind(&key_hash_legacy)
53+
.fetch_optional(&state.db)
54+
.await
55+
.map_err(|e| AppError::Unauthorized(format!("API key lookup failed: {e}")))?;
56+
57+
let row = row.ok_or_else(|| AppError::Unauthorized("Invalid or revoked API key".into()))?;
58+
59+
// Update last_used_at (best-effort; match whichever hash was stored)
60+
let _ = sqlx::query("UPDATE api_keys SET last_used_at = $1 WHERE key_hash = $2 OR key_hash = $3")
61+
.bind(Utc::now())
62+
.bind(&key_hash)
63+
.bind(&key_hash_legacy)
64+
.execute(&state.db)
65+
.await;
3266

33-
if claims.token_type != "access" {
34-
return Err(AppError::Unauthorized("Not an access token".into()));
35-
}
67+
let now = Utc::now().timestamp();
68+
Claims {
69+
sub: row.user_id,
70+
email: row.email,
71+
role: row.role,
72+
iat: now,
73+
exp: now + 365 * 24 * 3600, // API keys don't expire from our side
74+
token_type: "access".into(),
75+
}
76+
} else {
77+
// JWT
78+
let claims = validate_token(token, &state.config.jwt_secret)
79+
.map_err(|e| AppError::Unauthorized(format!("Invalid token: {e}")))?;
80+
if claims.token_type != "access" {
81+
return Err(AppError::Unauthorized("Not an access token".into()));
82+
}
83+
claims
84+
};
3685

3786
Ok(AuthUser(claims))
3887
}

api/src/routes/api_keys.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use axum::{
44
};
55
use uuid::Uuid;
66

7+
use crate::auth::compute_api_key_hash;
78
use crate::error::AppResult;
89
use crate::middleware::auth::AuthUser;
910
use crate::models::extra::{ApiKey, ApiKeyPublic};
@@ -55,15 +56,7 @@ pub async fn create(
5556
let id = Uuid::new_v4();
5657
let raw_key = format!("oms_{}", Uuid::new_v4().to_string().replace('-', ""));
5758
let prefix = format!("{}...", &raw_key[..12]);
58-
// Simple hash for storage — not cryptographic, but sufficient for API key lookup
59-
let key_hash = format!("{:016x}", {
60-
let mut h: u64 = 0xcbf29ce484222325;
61-
for b in raw_key.as_bytes() {
62-
h ^= *b as u64;
63-
h = h.wrapping_mul(0x100000001b3);
64-
}
65-
h
66-
});
59+
let key_hash = compute_api_key_hash(&raw_key);
6760

6861
sqlx::query(
6962
"INSERT INTO api_keys (id, user_id, name, key_hash, prefix, created_at)

docs/API-FOR-AGENTS.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# API for agents and automation
2+
3+
OpenModelStudio is **API-first**: the UI, OpenClaw, a future CLI, and any coding agent all use the **same REST API**. Whatever works for OpenClaw works for every other client.
4+
5+
---
6+
7+
## One interface, many clients
8+
9+
| Client | How it uses the API |
10+
|--------|----------------------|
11+
| **Web UI** | Browser → same API (with user session / JWT) |
12+
| **OpenClaw plugin** | Agent tools → HTTP requests with API key |
13+
| **Future CLI** | Commands → HTTP requests with API key |
14+
| **Cursor / any coding agent** | Code or tools → HTTP requests with API key |
15+
| **Python SDK** | Wraps the same API with an API key or JWT |
16+
17+
There is no special “OpenClaw-only” path. The OpenClaw plugin is just one client that calls the REST API with a base URL and an API key.
18+
19+
---
20+
21+
## Contract for any coding agent or CLI
22+
23+
To control OpenModelStudio from **any** agent (OpenClaw, Cursor, a script, or a CLI):
24+
25+
1. **Base URL** — OpenModelStudio API root, e.g. `http://localhost:31001` or `https://api.your-oms.com`.
26+
2. **Auth** — API key (recommended) or JWT in the `Authorization` header:
27+
- `Authorization: Bearer oms_xxxxxxxx...` (API key from Settings → API Keys)
28+
- or `Authorization: Bearer <jwt>` (from `POST /auth/login`).
29+
3. **Endpoints** — Same REST routes the UI and OpenClaw use: projects, models, datasets, training, experiments, workspaces, search, etc.
30+
31+
Example (curl):
32+
33+
```bash
34+
export OMS_BASE_URL="http://localhost:31001"
35+
export OMS_API_KEY="oms_xxxxxxxx..."
36+
37+
curl -s -H "Authorization: Bearer $OMS_API_KEY" "$OMS_BASE_URL/projects"
38+
curl -s -X POST -H "Authorization: Bearer $OMS_API_KEY" \
39+
-H "Content-Type: application/json" \
40+
-d '{"name":"My Project"}' "$OMS_BASE_URL/projects"
41+
```
42+
43+
Any coding agent that can send HTTP requests and store a base URL + API key can do the same (e.g. in Python with `requests`, in Node with `fetch`, or via a small CLI that wraps these calls).
44+
45+
---
46+
47+
## API surface (overview)
48+
49+
The API already covers the system. Main areas:
50+
51+
- **Auth**`/auth/login`, `/auth/refresh`, `/auth/me`; API keys via `/api-keys`.
52+
- **Projects**`/projects` (list, create, get, update, delete, collaborators, activity).
53+
- **Datasets**`/datasets`, `/projects/{id}/datasets` (list, create, get, delete, upload).
54+
- **Data sources**`/data-sources` (list, create, delete, test).
55+
- **Models**`/models`, `/projects/{id}/models` (list, create, get, update, delete, code, run, versions).
56+
- **Training**`/training/start`, `/training/jobs`, `/training/{id}`, metrics, logs, cancel.
57+
- **Inference**`/inference/run`, `/inference/{id}`, output.
58+
- **Experiments**`/experiments` (list, create, get, delete, runs, add run, compare).
59+
- **Artifacts**`/jobs/{id}/artifacts`, `/artifacts` (create, get, delete, download).
60+
- **Workspaces**`/workspaces`, `/workspaces/launch`, get, stop.
61+
- **Environments**`/environments` (list, create, get, update, delete).
62+
- **Features**`/features`, `/projects/{id}/features`, groups.
63+
- **Search**`/search?q=...`.
64+
- **LLM**`/llm/chat`, `/llm/conversations`.
65+
- **SDK-style**`/sdk/*` (register-model, datasets, features, hyperparameters, start-training, start-inference, jobs, pipelines, sweeps).
66+
- **Admin**`/admin/users`, `/admin/stats` (admin role).
67+
68+
So the system is already **API-enabled**; a CLI would be another client on top of this surface.
69+
70+
---
71+
72+
## OpenClaw vs other agents
73+
74+
- **OpenClaw:** Plugin registers tools (e.g. `oms_list_projects`, `oms_create_project`); each tool runs an HTTP request to the API with the configured `baseUrl` and `accessToken` (API key or JWT). No magic—just REST.
75+
- **Other agents (e.g. Cursor, custom scripts):** Use the same `baseUrl` + API key and the same endpoints. You can:
76+
- Give the agent the base URL and API key (via env or a config file) and instructions to call the REST API, or
77+
- Build a small CLI (e.g. `oms projects list`, `oms training start ...`) that calls the API and let the agent run CLI commands, or
78+
- Use the Python SDK with an API key.
79+
80+
So: **yes, whatever you do for OpenClaw works for all coding agents**—same API, same auth. Making the system “API-enabled” is already done; adding a CLI is another client that reuses this same interface.
81+
82+
---
83+
84+
## See also
85+
86+
- [OpenClaw Integration](OPENCLAW-INTEGRATION.md) — Plugin setup and tools.
87+
- [OpenClaw Quickstart](OPENCLAW-QUICKSTART.md) — Full flow and config (API key, base URL).
88+
- [Python SDK](../sdk/python/README.md) — Programmatic access from Python (e.g. notebooks, scripts).

docs/OPENCLAW-AND-CLAUDE.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# OpenClaw and Claude: Step-by-Step Guide
2+
3+
This guide walks you through controlling OpenModelStudio with **OpenClaw** (chat bots) and **Claude Code** (terminal) using the same API and API key.
4+
5+
---
6+
7+
## Prerequisites
8+
9+
- OpenModelStudio running (API reachable).
10+
- An **API key** from OpenModelStudio: log in to the UI → **Settings → API Keys** → Create (e.g. name: `OpenClaw`). Copy the key once (`oms_...`).
11+
12+
---
13+
14+
## Part 1: OpenClaw (Telegram / Discord / Control UI)
15+
16+
OpenClaw is an AI agent that runs 24/7. You talk to it in natural language; it uses tools to call the OpenModelStudio API.
17+
18+
### Step 1 — Start OpenModelStudio
19+
20+
```bash
21+
cd /path/to/OpenModelStudio
22+
make k8s-deploy
23+
```
24+
25+
- API: **http://localhost:31001**
26+
- UI: http://localhost:31000 (login: `test@openmodel.studio` / `Test1234`)
27+
28+
Or API only: `make dev-api` → API at **http://localhost:8080**.
29+
30+
### Step 2 — Install the OpenClaw plugin
31+
32+
```bash
33+
openclaw plugins install -l ./integrations/openclaw
34+
cd ~/.openclaw/extensions/openmodelstudio && npm install
35+
```
36+
37+
### Step 3 — Configure the plugin
38+
39+
Edit **`~/.openclaw/openclaw.json`**. Under `plugins.entries["openclaw-plugin"].config` set:
40+
41+
```json
42+
"openclaw-plugin": {
43+
"enabled": true,
44+
"config": {
45+
"baseUrl": "http://localhost:31001",
46+
"accessToken": "oms_your_api_key_here"
47+
}
48+
}
49+
```
50+
51+
- Use `http://localhost:8080` if you used `make dev-api`.
52+
- Allow the plugin for your agent, e.g. `agents.list[].tools.allow: ["openclaw-plugin"]`.
53+
54+
### Step 4 — Start OpenClaw and add LLM auth
55+
56+
```bash
57+
openclaw gateway
58+
```
59+
60+
- Control UI: http://127.0.0.1:18789
61+
- Add your Anthropic (or other) API key for the agent: `openclaw agents add main` or via the UI.
62+
63+
### Step 5 — Use it
64+
65+
In Telegram, Discord, or the Control UI, say things like:
66+
67+
- *"List my OpenModelStudio projects."*
68+
- *"Create a project called Sales and add a dataset named sales_data."*
69+
- *"Start training for model X with dataset Y."*
70+
71+
The agent uses the `oms_*` tools and reports back. No user needs to paste a token.
72+
73+
---
74+
75+
## Part 2: Claude Code (terminal)
76+
77+
Claude Code is the Claude CLI in your terminal. You give it the same base URL and API key; it runs `curl` or scripts to call the OpenModelStudio API.
78+
79+
### Step 1 — Start OpenModelStudio
80+
81+
Same as Part 1 — e.g. `make k8s-deploy` (API at http://localhost:31001) or `make dev-api` (http://localhost:8080).
82+
83+
### Step 2 — Set environment variables
84+
85+
In the **same terminal** where you will run `claude`:
86+
87+
```bash
88+
export OMS_BASE_URL="http://localhost:31001"
89+
export OMS_API_KEY="oms_your_api_key_here"
90+
```
91+
92+
Use `http://localhost:8080` if you used `make dev-api`.
93+
94+
### Step 3 — Start Claude Code
95+
96+
```bash
97+
claude
98+
```
99+
100+
Tell Claude: *"Use the env vars OMS_BASE_URL and OMS_API_KEY for OpenModelStudio API calls."*
101+
102+
### Step 4 — Ask Claude to control OpenModelStudio
103+
104+
Examples:
105+
106+
- *"List my OpenModelStudio projects."*
107+
- *"Create an OpenModelStudio project named Test."*
108+
- *"Create a dataset in project &lt;project_id&gt; named mydata."*
109+
- *"Create dummy CSV data and upload it to OpenModelStudio dataset &lt;dataset_id&gt;."*
110+
- *"Create a PyTorch model in project &lt;project_id&gt; named MyModel."*
111+
- *"Start OpenModelStudio training for model &lt;model_id&gt; with dataset &lt;dataset_id&gt;."*
112+
- *"Get status of OpenModelStudio training job &lt;job_id&gt;."*
113+
- *"Get metrics for OpenModelStudio training job &lt;job_id&gt;."*
114+
- *"Launch an OpenModelStudio workspace for project &lt;project_id&gt;."*
115+
116+
Claude will call the API (e.g. with `curl` or the Python SDK) using your base URL and API key.
117+
118+
---
119+
120+
## Summary
121+
122+
| | OpenClaw | Claude Code |
123+
|---|----------|-------------|
124+
| **Where** | Telegram, Discord, Control UI | Terminal (`claude`) |
125+
| **Config** | `~/.openclaw/openclaw.json``baseUrl` + `accessToken` | `OMS_BASE_URL` + `OMS_API_KEY` in shell |
126+
| **Auth** | API key in config (no user-provided token) | API key in env |
127+
| **Same API** | Yes | Yes |
128+
129+
Both use the **same REST API** and **API key**. Production: use HTTPS and good secret handling; see [OpenClaw Integration](OPENCLAW-INTEGRATION.md) and [API for agents](API-FOR-AGENTS.md).

0 commit comments

Comments
 (0)