Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# Node.js
node_modules/
dist/
dist-ssr/
*.local

# npm/yarn logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Rust
backend/target/
backend/cargo_out.log
backend/cargo-check.log
backend/check.err
backend/test.log
backend/test_output.log

# System
.DS_Store
Thumbs.db

# Secrets
.env
.env.*
!.env.example

# Editor
.vscode/
.idea/
*.swp
*.swo
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,26 @@ In practical terms:
| Can the plugin shape prompt injection locally? | Yes | Yes |
| Is there a supported local fallback memory engine? | Historically yes | No |

## 4. Request Flow
## 4. Admin Plane

Chronicle Engine includes a bundled **Admin Plane** for operators to manage memories, monitor distillation, and trace recall logic.

- **URL**: Accessible at `/admin` on the backend host.
- **Auth**: Protected by a dedicated `auth.admin.token` (bearer auth).
- **Features**:
- **Dashboard**: Overview of active principals and their activity stats.
- **Memories**: Browse and manage memory rows for any principal.
- **Behavioral Guidance**: Inspect active behavioral rules.
- **Recall Lab**: Side-effect-free recall simulation with full debug traces.
- **Distill Jobs**: Monitor background knowledge distillation status and artifacts.
- **Transcripts**: View session transcripts for context analysis.
- **Governance**: Review and promote candidate memories derived from distillation.
- **Audit Log**: Track admin-plane mutations and configuration changes.
- **Settings**: Read-only (MVP) view of the active runtime configuration.

The Admin UI is a React SPA bundled into the backend binary and served directly by the Rust service.

## 5. Request Flow

### Generic recall

Expand All @@ -99,6 +118,17 @@ User prompt
-> <relevant-memories> injected into prompt
```

### Admin Access

```text
Operator browser
-> GET /admin
-> backend serves SPA shell + assets
-> SPA requests /admin/api/* with admin bearer
-> backend validates admin token + rate limits
-> Operator manages memories / traces recall
```

### Cadence-driven distill flow

```text
Expand Down
15 changes: 15 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
toml = "1.1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
uuid = { version = "1", features = ["v4"] }
tower-http = { version = "0.6.8", features = ["fs"] }

[dev-dependencies]
tempfile = "3"
Expand Down
97 changes: 97 additions & 0 deletions backend/src/admin/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use crate::error::{AppError, AppResult};
use axum::{
body::Body,
extract::State,
http::{header, HeaderMap, Request},
middleware::Next,
response::Response,
};

use super::rate_limit::AdminRateLimiter;

/// Extracted admin auth context attached to each authenticated admin request.
#[derive(Clone, Debug)]
pub struct AdminAuthContext {
/// SHA-256 fingerprint (hex, first 16 chars) of the admin token.
pub token_fingerprint: String,
}

/// Admin auth middleware: validates `Authorization: Bearer <admin_token>`,
/// rejects runtime tokens, and enforces admin rate-limiting.
pub async fn admin_auth_middleware(
State((admin_token, runtime_token, rate_limiter)): State<(
String,
String,
AdminRateLimiter,
)>,
request: Request<Body>,
next: Next,
) -> AppResult<Response> {
let token = admin_bearer_token(request.headers())?;

// Reject runtime token on admin routes.
if token == runtime_token {
return Err(AppError::forbidden(
"runtime bearer token is not accepted on admin API routes",
));
}

if token != admin_token {
return Err(AppError::unauthorized("invalid admin bearer token"));
}

// Rate-limit check.
let remote_ip = extract_remote_ip(&request);
let token_fingerprint = token_fingerprint(&token);
rate_limiter.check_rate_limit(&remote_ip, &token_fingerprint)?;

let auth_ctx = AdminAuthContext { token_fingerprint };
let mut request = request;
request.extensions_mut().insert(auth_ctx);

Ok(next.run(request).await)
}

fn admin_bearer_token(headers: &HeaderMap) -> AppResult<String> {
let value = headers
.get(header::AUTHORIZATION)
.ok_or_else(|| AppError::unauthorized("missing Authorization header on admin route"))?
.to_str()
.map_err(|_| AppError::unauthorized("invalid Authorization header encoding"))?;
let prefix = "Bearer ";
if !value.starts_with(prefix) {
return Err(AppError::unauthorized(
"Authorization header must use Bearer scheme",
));
}
let token = value[prefix.len()..].trim();
if token.is_empty() {
return Err(AppError::unauthorized("Bearer token cannot be empty"));
}
Ok(token.to_string())
}

fn extract_remote_ip(request: &Request<Body>) -> String {
// Try X-Forwarded-For first, then fall back to peer address.
if let Some(xff) = request.headers().get("x-forwarded-for") {
if let Ok(val) = xff.to_str() {
if let Some(first) = val.split(',').next() {
let ip = first.trim();
if !ip.is_empty() {
return ip.to_string();
}
}
}
}
// Axum's ConnectInfo is not always available in test/oneshot mode;
// fall back to "unknown".
"unknown".to_string()
}

fn token_fingerprint(token: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
token.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
Loading
Loading