diff --git a/.changeset/readme-story-permissions.md b/.changeset/readme-story-permissions.md
new file mode 100644
index 0000000..095f736
--- /dev/null
+++ b/.changeset/readme-story-permissions.md
@@ -0,0 +1,20 @@
+---
+"@smooai/smooth-operator-core": patch
+---
+
+docs: rewrite the root + per-language package READMEs as registry landing pages that tell a story
+
+Every README (root and the Rust / TypeScript / Python / Go / .NET package pages)
+now opens with a hook and a narrative arc — problem → one engine in five
+languages → observe→think→act → the permission gate + deny-policy that makes an
+agent safe to point at production → build → get started. Each package page leads
+with a tight agent-plus-tool quickstart in its own idiom (the mock scripted to
+call the tool, then answer) and a permissions/deny-policy example using that
+language's real API (`with_deny_policy` in Rust, `denyPolicy`/`permissionMode`
+options in TS/Py, `WithDenyPolicy` in Go/C#).
+
+Adds the headline permission system + deny-policy (AutoMode ask / accept-edits /
+deny-unmatched / bypass, circuit-breakers, declarative TOML rules + semantic
+predicates) to the feature surface, refreshes the polyglot table
+(language → package → registry), and fixes stale test-count claims. Docs only —
+no code changes.
diff --git a/README.md b/README.md
index 886c709..9b28599 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
@@ -19,13 +19,15 @@
---
-> The agent runtime behind the [smooth-operator](https://github.com/SmooAI/smooth-operator) service and [lom.smoo.ai](https://lom.smoo.ai). Agents, workflows, tools, checkpointing, memory, human-in-the-loop, and per-model cost budgets — as a single embeddable Rust crate. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — because you decide what it must never do.
+>
+> One observe→think→act engine — typed tools, streaming, checkpointing, memory, cost budgets, and a permission gate with hard lines the model can't cross — native in **Rust, TypeScript, Python, Go, and C#**.
-`smooai-smooth-operator-core` is the agent runtime that powers the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service and [**lom.smoo.ai**](https://lom.smoo.ai). It gives you the moving parts of a serious agent framework — an observe→think→act loop, a typed tool system, a graph workflow engine, pluggable checkpoint stores, memory, RAG, human-in-the-loop gates, and per-model cost budgets — as a single, embeddable Rust crate.
+Most agent frameworks hand the model a pile of tools and hope for the best. `smooth-operator-core` gives you the whole loop **and the brakes**: a typed tool system with pre/post hooks, human-in-the-loop gates, per-model cost budgets — and a **deny-policy** that lets you draw lines the model can never cross, not even in bypass mode. *No prod AWS profile. No writes to the DB writer. No `rm -rf /`.* Declared once, enforced on every tool call.
-Inspired by LangGraph, CrewAI, and Agno, with one hard difference: **it's the engine, not a notebook demo.** Every surface is covered by **337 fast, offline unit tests** built on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded.
+It's the runtime that powers the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service and [**lom.smoo.ai**](https://lom.smoo.ai) — not a notebook demo. Inspired by LangGraph, CrewAI, and Agno, with one hard difference: every surface is covered by **hundreds of fast, offline unit tests** built on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded. And it's the **same engine in five languages** — write your agent where your stack already lives.
-> The Rust implementation is the source of truth. TypeScript, Go, C#/.NET, and Python bindings mirror its surface (protocol-first; see [Repository layout](#repository-layout)).
+> The Rust implementation is the source of truth. The TypeScript, Python, Go, and C#/.NET ports mirror its surface at parity (protocol-first; see [Repository layout](#repository-layout)).
---
@@ -166,6 +168,7 @@ let agent = Agent::new(config, registry).with_checkpoint_store(checkpoints);
| --- | --- |
| An agent loop you can **trust** | observe→think→act with iteration caps, parallel tool calls, and a typed `AgentEvent` stream |
| **Typed tools** with guardrails | `Tool` trait + `ToolRegistry`, with pre/post hooks for surveillance, secret detection, prompt-injection guards |
+| **Deny what must never run** | `PermissionHook` gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) + hard circuit-breakers + a consumer `DenyPolicy` (declarative TOML rules + semantic predicates) |
| **Stateful graphs** (a LangGraph analog) | `Workflow` / `WorkflowBuilder` with conditional edges and typed state |
| **Resume after a crash** | `CheckpointStore`: in-memory, file, SQLite, or Postgres |
| **RAG + memory** | `KnowledgeBase` / `Memory` traits (with in-memory impls) as clean seams |
@@ -178,6 +181,44 @@ It's the runtime the smooth-operator service actually ships on — not a referen
---
+## Permissions & deny-policy — draw lines the agent can't cross
+
+Here's the thing that makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt, jailbreak, or model mistake can talk it out of that.
+
+Every tool call passes through a gate before it runs. `AutoMode` sets the baseline posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in *every* mode, `Bypass` included. On top of that you attach a **`DenyPolicy`**: declarative TOML rules for the lines you can name, plus semantic predicates for the ones you can't.
+
+```rust
+use std::sync::Arc;
+use smooth_operator_core::{Agent, AutoMode, DenyPolicy, DenyPredicate, DenyReason, ToolCall};
+
+// Predicate: the checks strings can't express — is this AWS call the *prod account*?
+// Is this DB connection the *writer* endpoint? Return Some(reason) to deny.
+struct DenyDbWriter;
+impl DenyPredicate for DenyDbWriter {
+ fn evaluate(&self, call: &ToolCall) -> Option {
+ (call.name == "db_query" && call.arguments.to_string().contains("writer"))
+ .then(|| DenyReason::new("DB writer is off-limits — reads go to the replica"))
+ }
+}
+
+// Declarative rules: never the prod AWS profile, never a prod host.
+let policy = DenyPolicy::from_toml(r#"
+ schema_version = 1
+ [bash]
+ deny_patterns = ["aws * --profile prod"]
+ [network]
+ deny_hosts = ["*.prod.internal"]
+"#)?.with_predicate(Arc::new(DenyDbWriter));
+
+let agent = Agent::new(config, registry)
+ .with_permission_mode(AutoMode::Ask)
+ .with_deny_policy(Arc::new(policy));
+```
+
+A deny-policy match is a **hard deny of circuit-breaker tier** — no stored grant waives it, no mode downgrades it. That's the difference between "we asked the model nicely" and "it structurally cannot." And it's identical across all five languages.
+
+---
+
## Architecture
### The agent loop
@@ -237,7 +278,7 @@ The service is thin: it terminates the WebSocket protocol and hands turns to the
## Test-driven by default — verified, not vibe-coded
-This is the part we care about most. The engine ships **408 unit tests** that run in **seconds, fully offline**, because every LLM call goes through an `LlmProvider` seam that tests satisfy with `MockLlmClient`:
+This is the part we care about most. The engine ships **hundreds of unit tests** that run in **seconds, fully offline**, because every LLM call goes through an `LlmProvider` seam that tests satisfy with `MockLlmClient`:
```rust
use smooth_operator_core::llm_provider::{LlmProvider, MockLlmClient};
@@ -272,7 +313,7 @@ flowchart TD
J["LLM-as-judge evals — multi-turn quality, 0–5"]
E["Live E2E — real gateway + WS, streamed answer"]
C["Conformance — SQLite + Postgres stores, testcontainers"]
- U["337 unit tests — MockLlmClient, offline, fast"]
+ U["hundreds of unit tests — MockLlmClient, offline, fast"]
J --> E --> C --> U
@@ -282,7 +323,7 @@ flowchart TD
class U teal
```
-- **Unit (408):** the bulk. Loop control, tool dispatch, workflow edges, compaction, cost enforcement, HITL gating, checkpoint round-trips — all against `MockLlmClient`.
+- **Unit (the bulk):** loop control, tool dispatch, workflow edges, compaction, cost enforcement, permission-gate + deny-policy verdicts, HITL gating, checkpoint round-trips — all against `MockLlmClient`.
- **Conformance:** the `sqlite` and `postgres` checkpoint stores run the same suite against real engines (testcontainers), so "resume" means the same thing everywhere.
- **Live E2E:** the smooth-operator service + [chat-widget](https://github.com/SmooAI/chat-widget) drive a real streamed, knowledge-grounded answer through a live gateway.
- **LLM-as-judge:** multi-turn conversation quality is scored 0–5 by a judge model. This caught a real multi-turn context defect: a regression scored **1/5**, the fix landed, and it went back to **5/5** — a class of bug no assertion-based test would have flagged.
@@ -309,17 +350,17 @@ cargo clippy --all-targets -- -D warnings
## Repository layout
-This is a multi-language SmooAI package. The Rust crate is the reference; other languages mirror its surface. For install commands and a hello-agent example in every language, see [**docs/Polyglot-Engines.md**](./docs/Polyglot-Engines.md).
+This is a multi-language SmooAI package. The Rust crate is the reference; the other four are **native ports at parity** — the same engine, idiomatic in each language, held to a shared eval suite. Each ships to its language's registry with its own README landing page. For install commands and a hello-agent example in every language, see [**docs/Polyglot-Engines.md**](./docs/Polyglot-Engines.md).
-| Directory | Language | Status |
-| --- | --- | --- |
-| [`rust/`](./rust) | Rust (reference) | Active — crate `smooai-smooth-operator-core` (lib `smooth_operator_core`) |
-| [`typescript/`](./typescript) | TypeScript | Planned |
-| [`go/`](./go) | Go | Active — module `github.com/SmooAI/smooth-operator-core/go` |
-| [`dotnet/`](./dotnet) | C# / .NET | Planned (first-class target) |
-| [`python/`](./python) | Python | Planned |
+| Language | Directory | Package | Registry |
+| --- | --- | --- | --- |
+| Rust (reference) | [`rust/`](./rust/smooth-operator-core) | `smooai-smooth-operator-core` (lib `smooth_operator_core`) | [crates.io](https://crates.io/crates/smooai-smooth-operator-core) |
+| TypeScript | [`typescript/`](./typescript/core) | `@smooai/smooth-operator-core` | [npm](https://www.npmjs.com/package/@smooai/smooth-operator-core) |
+| Python | [`python/`](./python/core) | `smooai-smooth-operator-core` | [PyPI](https://pypi.org/project/smooai-smooth-operator-core/) |
+| Go | [`go/`](./go/core) | `github.com/SmooAI/smooth-operator-core/go/core` | [pkg.go.dev](https://pkg.go.dev/github.com/SmooAI/smooth-operator-core/go/core) |
+| C# / .NET | [`dotnet/`](./dotnet/core) | `SmooAI.SmoothOperator.Core` | [nuget.org](https://www.nuget.org/packages/SmooAI.SmoothOperator.Core) |
-Bindings follow a **protocol-first** strategy (a stable wire spec each language implements natively), with in-process FFI (napi-rs, PyO3/uniffi) layered on where embedding the engine pays off.
+The ports follow a **protocol-first** strategy: a stable wire spec each language implements natively, so the loop, tool system, permission gate, checkpointing, and cost accounting behave the same everywhere.
---
diff --git a/dotnet/core/README.md b/dotnet/core/README.md
index c53758c..33e8a6f 100644
--- a/dotnet/core/README.md
+++ b/dotnet/core/README.md
@@ -15,11 +15,13 @@
---
-> The C#/.NET sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core). Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable NuGet package. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — right in your .NET process.
+>
+> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.
-`SmooAI.SmoothOperator.Core` is the **native C# implementation** of the Smoo AI agent engine — the in-process observe→think→act loop that powers [**lom.smoo.ai**](https://lom.smoo.ai). It's a sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) and one of the [polyglot set](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite. Its API follows Microsoft.Extensions.AI naming.
+`SmooAI.SmoothOperator.Core` is the agent engine itself, in-process — an observe→think→act loop over any `IChatClient`, with typed tools (authored from ordinary C# methods via `AIFunctionFactory`), streaming, checkpointing, cost budgets, and a permission gate you control. Its API follows Microsoft.Extensions.AI naming, so it drops into an existing .NET AI stack. Not a client to a remote server: the agent *is* your process.
-It's a library, not a client to a remote server: it *is* the agent, running in your .NET process. Every surface is covered by **fast, offline tests** built on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
+It's the native C# port of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) — one of five siblings (Rust, TypeScript, Python, Go, C#/.NET) that share one wire spec and one eval suite. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline tests on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
## Install
@@ -31,17 +33,30 @@ dotnet add package SmooAI.SmoothOperator.Core
A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:
+A complete agent with one tool — the mock is scripted to call the tool, then answer. Author tools from ordinary C# methods with `AIFunctionFactory.Create`:
+
```csharp
+using Microsoft.Extensions.AI;
using SmooAI.SmoothOperator.Core;
-var provider = new MockLlmProvider().PushText("the answer is 42");
-var agent = new SmoothAgent(provider, new AgentOptions { Instructions = "You are a helpful assistant" });
+var getWeather = AIFunctionFactory.Create(
+ (string city) => $"Weather in {city}: 72F, sunny",
+ "get_weather",
+ "Get the current weather for a city");
+
+var provider = new MockLlmProvider()
+ .PushToolCall("call_1", "get_weather", new Dictionary { ["city"] = "Tokyo" })
+ .PushText("It's 72F and sunny in Tokyo.");
+
+var options = new AgentOptions { Instructions = "You are a helpful assistant" };
+options.Tools.Add(getWeather);
+var agent = new SmoothAgent(provider, options);
-var response = await agent.RunAsync("what is the answer?");
+var response = await agent.RunAsync("what's the weather in Tokyo?");
Console.WriteLine(response.Text);
```
-`new SmoothAgent(chatClient, options)` takes an `IChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions`. `await agent.RunAsync(...)` returns an `AgentRunResponse`; `response.Text` is the final assistant message.
+`new SmoothAgent(chatClient, options)` takes an `IChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions`; tools are `AITool`s added to `options.Tools`. `await agent.RunAsync(...)` returns an `AgentRunResponse`; `response.Text` is the final assistant message.
## Features
@@ -57,6 +72,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — rerank retrieved hits before injection (lexical reranker built in).
- **Sub-agents / delegation** — spawn child agents for sub-tasks.
- **Cast + clearance** — roles with per-role tool-access policy.
+- **Permissions + deny-policy** — a tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — require approval before designated tool calls run.
- **Conversation thread** — carry a conversation across multiple `RunAsync` calls.
- **`LlmProvider` seam + `MockLlmProvider`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
@@ -66,6 +82,39 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — stream incremental text, tool calls, and tool results as the turn runs.
+## Permissions & deny-policy — lines the agent can't cross
+
+This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `Bypass` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.
+
+```csharp
+using Microsoft.Extensions.AI;
+using SmooAI.SmoothOperator.Core;
+
+// Declarative rules (TOML): never the prod AWS profile, never a prod host.
+var policy = DenyPolicy.FromToml(@"
+ schema_version = 1
+ [bash]
+ deny_patterns = [""aws * --profile prod""]
+ [network]
+ deny_hosts = [""*.prod.internal""]
+").WithPredicate(new DenyDbWriter());
+
+var options = new AgentOptions { Instructions = "You are a careful assistant" }
+ .WithPermissionMode(AutoMode.Ask) // read allow · mutate ask · dangerous deny
+ .WithDenyPolicy(policy);
+options.Tools.Add(getWeather);
+var agent = new SmoothAgent(provider, options);
+
+// A predicate for what strings can't express — return a DenyReason to deny, null to allow.
+sealed class DenyDbWriter : IDenyPredicate
+{
+ public DenyReason? Evaluate(FunctionCallContent call) =>
+ call.Name == "db_query" && call.Arguments?.Values.Any(v => $"{v}".Contains("writer")) == true
+ ? new DenyReason("DB writer endpoint is off-limits — reads go to the replica")
+ : null;
+}
+```
+
## Streaming
`RunStreamingAsync` is the streaming variant of `RunAsync`: it yields incremental updates — text deltas as the model produces them, each tool call before dispatch, each tool result after it finishes, and a terminal update carrying the same response `RunAsync` would have returned.
diff --git a/go/core/README.md b/go/core/README.md
index fce9be9..71731c6 100644
--- a/go/core/README.md
+++ b/go/core/README.md
@@ -15,11 +15,13 @@
---
-> The Go sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core). Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable package. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — right in your Go process.
+>
+> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.
-`github.com/SmooAI/smooth-operator-core/go/core` is the **native Go implementation** of the Smoo AI agent engine — the in-process observe→think→act loop that powers [**lom.smoo.ai**](https://lom.smoo.ai). It's a sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) and one of the [polyglot set](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite.
+`github.com/SmooAI/smooth-operator-core/go/core` is the agent engine itself, in-process — an observe→think→act loop over any OpenAI-compatible client, with typed tools, streaming, checkpointing, cost budgets, and a permission gate you control. Not a client to a remote server: the agent *is* your process.
-It's a library, not a client to a remote server: it *is* the agent, running in your Go process. Every surface is covered by **fast, offline tests** built on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
+It's the native Go port of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) — one of five siblings (Rust, TypeScript, Python, Go, C#/.NET) that share one wire spec and one eval suite. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline tests on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
## Install
@@ -33,6 +35,8 @@ The engine is the `core` package; idiomatic alias `core`.
A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:
+A complete agent with one tool — the mock is scripted to call the tool, then answer:
+
```go
package main
@@ -44,10 +48,29 @@ import (
)
func main() {
- provider := core.NewMockLlmProvider().PushText("the answer is 42")
- agent := core.NewSmoothAgent(provider, core.AgentOptions{Instructions: "You are a helpful assistant"})
+ weather := core.FuncTool{
+ ToolName: "get_weather",
+ Desc: "Get the current weather for a city",
+ Params: map[string]any{
+ "type": "object",
+ "properties": map[string]any{"city": map[string]any{"type": "string"}},
+ "required": []string{"city"},
+ },
+ Fn: func(ctx context.Context, args map[string]any) (string, error) {
+ return fmt.Sprintf("Weather in %v: 72F, sunny", args["city"]), nil
+ },
+ }
+
+ provider := core.NewMockLlmProvider().
+ PushToolCall("call_1", "get_weather", `{"city":"Tokyo"}`).
+ PushText("It's 72F and sunny in Tokyo.")
+
+ agent := core.NewSmoothAgent(provider, core.AgentOptions{
+ Instructions: "You are a helpful assistant",
+ Tools: []core.Tool{weather},
+ })
- res, err := agent.Run(context.Background(), "what is the answer?", nil)
+ res, err := agent.Run(context.Background(), "what's the weather in Tokyo?", nil)
if err != nil {
panic(err)
}
@@ -55,7 +78,7 @@ func main() {
}
```
-`NewSmoothAgent(client, options)` takes a `ChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions` struct. `Run(ctx, message, history)` — pass `nil` history for a fresh turn — returns `(AgentRunResponse, error)`; `res.Text` is the final answer.
+`NewSmoothAgent(client, options)` takes a `ChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions` struct. `FuncTool` wraps a function as a `Tool`. `Run(ctx, message, history)` — pass `nil` history for a fresh turn — returns `(AgentRunResponse, error)`; `res.Text` is the final answer.
## Features
@@ -71,6 +94,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — rerank retrieved hits before injection (lexical reranker built in).
- **Sub-agents / delegation** — spawn child agents for sub-tasks.
- **Cast + clearance** — roles with per-role tool-access policy.
+- **Permissions + deny-policy** — a tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — require approval before designated tool calls run.
- **Conversation thread** — carry a conversation across multiple `Run` calls.
- **`LlmProvider` seam + `MockLlmProvider`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
@@ -80,6 +104,43 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — stream incremental text, tool calls, and tool results as the turn runs.
+## Permissions & deny-policy — lines the agent can't cross
+
+This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `AutoModeBypass` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.
+
+```go
+// A DenyPredicate for what strings can't express — return (reason, true) to deny.
+type denyDbWriter struct{}
+
+func (denyDbWriter) Evaluate(name string, args map[string]any) (core.DenyReason, bool) {
+ if name == "db_query" && strings.Contains(fmt.Sprint(args), "writer") {
+ return core.NewDenyReason("DB writer endpoint is off-limits — reads go to the replica"), true
+ }
+ return core.DenyReason{}, false
+}
+
+// Declarative rules (TOML): never the prod AWS profile, never a prod host.
+policy, err := core.DenyPolicyFromTOML(`
+ schema_version = 1
+ [bash]
+ deny_patterns = ["aws * --profile prod"]
+ [network]
+ deny_hosts = ["*.prod.internal"]
+`)
+if err != nil {
+ panic(err)
+}
+policy = policy.WithPredicate(denyDbWriter{})
+
+mode := core.AutoModeAsk
+agent := core.NewSmoothAgent(provider, core.AgentOptions{
+ Instructions: "You are a careful assistant",
+ Tools: []core.Tool{weather},
+ PermissionMode: &mode, // read allow · mutate ask · dangerous deny
+ DenyPolicy: policy,
+})
+```
+
## Streaming
`RunStream` is the streaming variant of `Run`: it yields incremental events — `text` deltas as the model produces them, each tool call before dispatch, each tool result after it finishes, and a terminal `done` event carrying the same response `Run` would have returned.
diff --git a/python/core/README.md b/python/core/README.md
index d27a72f..0fbaf7a 100644
--- a/python/core/README.md
+++ b/python/core/README.md
@@ -15,11 +15,13 @@
---
-> The Python sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core). Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable package. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — right in your Python process.
+>
+> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.
-`smooai-smooth-operator-core` is the **native Python implementation** of the Smoo AI agent engine — the in-process observe→think→act loop that powers [**lom.smoo.ai**](https://lom.smoo.ai). It's a sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) and one of the [polyglot set](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite.
+`smooai-smooth-operator-core` is the agent engine itself, in-process — an observe→think→act loop over any OpenAI-compatible client, with typed tools, streaming, checkpointing, cost budgets, and a permission gate you control. Not a client to a remote server: the agent *is* your process.
-It's a library, not a client to a remote server: it *is* the agent, running in your Python process. Every surface is covered by **fast, offline tests** built on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
+It's the native Python port of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) — one of five siblings (Rust, TypeScript, Python, Go, C#/.NET) that share one wire spec and one eval suite. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline tests on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
## Install
@@ -33,22 +35,36 @@ Import as `smooth_operator_core`.
A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:
+A complete agent with one tool — the mock is scripted to call the tool, then answer:
+
```python
import asyncio
-from smooth_operator_core import SmoothAgent, AgentOptions, MockLlmProvider
+import json
+from smooth_operator_core import SmoothAgent, AgentOptions, FunctionTool, MockLlmProvider
+
+async def get_weather(args):
+ return f"Weather in {args['city']}: 72F, sunny"
async def main():
+ weather = FunctionTool(
+ name="get_weather",
+ description="Get the current weather for a city",
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
+ func=get_weather,
+ )
+
provider = MockLlmProvider()
- provider.push_text("the answer is 42")
+ provider.push_tool_call("call_1", "get_weather", json.dumps({"city": "Tokyo"}))
+ provider.push_text("It's 72F and sunny in Tokyo.")
- agent = SmoothAgent(provider, AgentOptions(instructions="You are a helpful assistant"))
- result = await agent.run("what is the answer?")
+ agent = SmoothAgent(provider, AgentOptions(instructions="You are a helpful assistant", tools=[weather]))
+ result = await agent.run("what's the weather in Tokyo?")
print(result.text)
asyncio.run(main())
```
-`SmoothAgent(chat_client, options)` takes the provider (the `MockLlmProvider` — swap in any OpenAI-compatible client) and an `AgentOptions` dataclass (all fields default, so `AgentOptions()` is valid). `await agent.run(...)` returns an `AgentRunResponse`; `result.text` is the final answer.
+`SmoothAgent(chat_client, options)` takes the provider (the `MockLlmProvider` — swap in any OpenAI-compatible client) and an `AgentOptions` dataclass (all fields default, so `AgentOptions()` is valid). `FunctionTool` wraps an async function as a tool. `await agent.run(...)` returns an `AgentRunResponse`; `result.text` is the final answer.
## Features
@@ -64,6 +80,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — rerank retrieved hits before injection (lexical reranker built in).
- **Sub-agents / delegation** — spawn child agents for sub-tasks.
- **Cast + clearance** — roles with per-role tool-access policy.
+- **Permissions + deny-policy** — a tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — require approval before designated tool calls run.
- **Conversation thread** — `SmoothAgentThread` carries a conversation across multiple `run` calls.
- **`LlmProvider` seam + `MockLlmProvider`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
@@ -73,6 +90,44 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — stream incremental text, tool calls, and tool results as the turn runs.
+## Permissions & deny-policy — lines the agent can't cross
+
+This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `BYPASS` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.
+
+```python
+from smooth_operator_core import (
+ SmoothAgent, AgentOptions, AutoMode, DenyPolicy, DenyPredicate, DenyReason,
+)
+
+# Declarative rules (TOML): never the prod AWS profile, never a prod host.
+policy = DenyPolicy.from_toml(
+ """
+ schema_version = 1
+ [bash]
+ deny_patterns = ["aws * --profile prod"]
+ [network]
+ deny_hosts = ["*.prod.internal"]
+ """
+)
+
+# Predicate for what strings can't express — return a DenyReason to deny, None to allow.
+class DenyDbWriter(DenyPredicate):
+ def evaluate(self, call):
+ if call.name == "db_query" and "writer" in str(call.arguments):
+ return DenyReason.new("DB writer endpoint is off-limits — reads go to the replica")
+ return None
+
+agent = SmoothAgent(
+ provider,
+ AgentOptions(
+ instructions="You are a careful assistant",
+ tools=[weather],
+ permission_mode=AutoMode.ASK, # read allow · mutate ask · dangerous deny
+ deny_policy=policy.with_predicate(DenyDbWriter()),
+ ),
+)
+```
+
## Streaming
`run_stream` is the async streaming variant of `run`: it yields incremental events — `text` deltas as the model produces them, each tool call before dispatch, each tool result after it finishes, and a terminal `done` event carrying the same response `run` would have returned.
diff --git a/rust/smooth-operator-core/README.md b/rust/smooth-operator-core/README.md
index 29ff1cf..3f2f3fb 100644
--- a/rust/smooth-operator-core/README.md
+++ b/rust/smooth-operator-core/README.md
@@ -15,11 +15,13 @@
---
-> The agent runtime behind the [smooth-operator](https://github.com/SmooAI/smooth-operator) service and [lom.smoo.ai](https://lom.smoo.ai). Agents, workflows, tools, checkpointing, memory, human-in-the-loop, and per-model cost budgets — as a single embeddable Rust crate. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — a single embeddable Rust crate.
+>
+> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.
-`smooai-smooth-operator-core` is the **reference implementation** of the Smoo AI agent engine — the observe→think→act loop that powers the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service and [**lom.smoo.ai**](https://lom.smoo.ai). It gives you the moving parts of a serious agent framework — a typed tool system, a graph workflow engine, pluggable checkpoint stores, memory, RAG, human-in-the-loop gates, and per-model cost budgets — as one embeddable crate.
+`smooai-smooth-operator-core` is the agent engine itself — an observe→think→act loop over any OpenAI-compatible client, with a typed tool system, pre/post hooks, pluggable checkpoint stores, memory, RAG, human-in-the-loop gates, per-model cost budgets, and a permission gate you control. One crate; runs in a Lambda, a container, any host process. It's the runtime the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service actually ships on.
-This Rust crate is the source of truth: the [TypeScript, Python, Go, and C#/.NET engines](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) mirror its behavior. Every surface is covered by **fast, offline unit tests** built on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded.
+This crate is the **reference implementation** — the source of truth the [TypeScript, Python, Go, and C#/.NET ports](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) mirror at parity. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline unit tests on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded.
## Install
@@ -33,23 +35,48 @@ The crate is `smooai-smooth-operator-core` (library `smooth_operator_core`), v0.
## Quickstart
-A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:
+A complete agent with one tool — no credentials needed — using the deterministic mock provider the engine's own tests run on. The mock is scripted to call the tool, then answer:
```rust
use std::sync::Arc;
-use smooth_operator_core::{Agent, AgentConfig, LlmConfig, ToolRegistry};
+use async_trait::async_trait;
+use smooth_operator_core::{Agent, AgentConfig, LlmConfig, Tool, ToolRegistry, ToolSchema};
use smooth_operator_core::llm_provider::MockLlmClient;
+struct GetWeather;
+
+#[async_trait]
+impl Tool for GetWeather {
+ fn schema(&self) -> ToolSchema {
+ ToolSchema {
+ name: "get_weather".into(),
+ description: "Get the current weather for a city".into(),
+ parameters: serde_json::json!({
+ "type": "object",
+ "properties": { "city": { "type": "string" } },
+ "required": ["city"]
+ }),
+ }
+ }
+
+ async fn execute(&self, args: serde_json::Value) -> anyhow::Result {
+ Ok(format!("Weather in {}: 72F, sunny", args["city"].as_str().unwrap_or("?")))
+ }
+}
+
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mock = MockLlmClient::new();
- mock.push_text("the answer is 42");
+ mock.push_tool_call("call_1", "get_weather", serde_json::json!({ "city": "Tokyo" }));
+ mock.push_text("It's 72F and sunny in Tokyo.");
+
+ let mut registry = ToolRegistry::new();
+ registry.register(GetWeather);
let config = AgentConfig::new("agent", "You are a helpful assistant", LlmConfig::openrouter("fake-key"));
- let agent = Agent::new(config, ToolRegistry::new())
- .with_llm_provider(Arc::new(mock.clone()));
+ let agent = Agent::new(config, registry).with_llm_provider(Arc::new(mock.clone()));
- let conversation = agent.run("what is the answer?").await?;
+ let conversation = agent.run("what's the weather in Tokyo?").await?;
println!("{}", conversation.last_assistant_content().unwrap_or(""));
Ok(())
}
@@ -71,6 +98,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — rerank retrieved hits before injection (lexical reranker built in).
- **Sub-agents / delegation** — spawn child agents for sub-tasks.
- **Cast + clearance** — roles with per-role tool-access policy.
+- **Permissions + deny-policy** — a `PermissionHook` tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — `ConfirmationHook` requires approval before designated tool calls run.
- **Conversation thread** — carry a conversation across multiple `run` calls.
- **`LlmProvider` seam + `MockLlmClient`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
@@ -80,6 +108,42 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — incremental text, tool calls, and tool results as the turn runs.
+## Permissions & deny-policy — lines the agent can't cross
+
+This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `Bypass` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.
+
+```rust
+use std::sync::Arc;
+use smooth_operator_core::{Agent, AutoMode, DenyPolicy, DenyPredicate, DenyReason, ToolCall};
+
+// A predicate for what strings can't express — return Some(reason) to deny.
+struct DenyDbWriter;
+impl DenyPredicate for DenyDbWriter {
+ fn evaluate(&self, call: &ToolCall) -> Option {
+ if call.name == "db_query" && call.arguments.to_string().contains("writer") {
+ return Some(DenyReason::new("DB writer endpoint is off-limits — reads go to the replica"));
+ }
+ None
+ }
+}
+
+// Declarative rules (TOML): never the prod AWS profile, never a prod host.
+let policy = DenyPolicy::from_toml(r#"
+ schema_version = 1
+ [bash]
+ deny_patterns = ["aws * --profile prod"]
+ [network]
+ deny_hosts = ["*.prod.internal"]
+"#)?.with_predicate(Arc::new(DenyDbWriter));
+
+let agent = Agent::new(config, registry)
+ .with_permission_mode(AutoMode::Ask) // read allow · mutate ask · dangerous deny
+ .with_deny_policy(Arc::new(policy))
+ .with_extension_host(host); // the gate is installed here; set mode/policy first
+```
+
+The gate is installed by `with_extension_host` (the SEP extension host), so call `with_permission_mode` / `with_deny_policy` **before** it.
+
## Streaming
For live token deltas, tool-call, and tool-result events, drive the agent with `run_with_channel(msg, tx)` and consume the `AgentEvent` stream off the receiver — instead of `run`, which returns a single completed `Conversation`.
diff --git a/typescript/core/README.md b/typescript/core/README.md
index b896e4a..21c8900 100644
--- a/typescript/core/README.md
+++ b/typescript/core/README.md
@@ -15,11 +15,13 @@
---
-> The TypeScript sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core). Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable npm package. It's the engine, not a notebook demo.
+> ### The agent brain you can point at production — right in your Node process.
+>
+> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.
-`@smooai/smooth-operator-core` is the **native TypeScript implementation** of the Smoo AI agent engine — the in-process observe→think→act loop that powers [**lom.smoo.ai**](https://lom.smoo.ai). It's a sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) and one of the [polyglot set](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite.
+`@smooai/smooth-operator-core` is the agent engine itself, in-process — an observe→think→act loop over any OpenAI-compatible client, with typed tools, streaming, checkpointing, cost budgets, and a permission gate you control. Not a client to a remote server: the agent *is* your process.
-It's a library, not a client to a remote server: it *is* the agent, running in your Node process. Every surface is covered by **fast, offline tests** built on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
+It's the native TypeScript port of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) — one of five siblings (Rust, TypeScript, Python, Go, C#/.NET) that share one wire spec and one eval suite. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline tests on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
## Install
@@ -29,19 +31,34 @@ npm install @smooai/smooth-operator-core
## Quickstart
-A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:
+A complete agent with one tool — no credentials needed — using the deterministic mock provider the engine's own tests run on. The mock is scripted to call the tool, then answer:
```ts
-import { SmoothAgent, MockLlmProvider } from '@smooai/smooth-operator-core';
-
-const provider = new MockLlmProvider().pushText('the answer is 42');
-const agent = new SmoothAgent(provider, { instructions: 'You are a helpful assistant' });
-
-const response = await agent.run('what is the answer?');
+import { SmoothAgent, MockLlmProvider, type Tool } from '@smooai/smooth-operator-core';
+
+const getWeather: Tool = {
+ name: 'get_weather',
+ description: 'Get the current weather for a city',
+ parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
+ async execute(args) {
+ return `Weather in ${args.city}: 72F, sunny`;
+ },
+};
+
+const provider = new MockLlmProvider()
+ .pushToolCall('call_1', 'get_weather', JSON.stringify({ city: 'Tokyo' }))
+ .pushText("It's 72F and sunny in Tokyo.");
+
+const agent = new SmoothAgent(provider, {
+ instructions: 'You are a helpful assistant',
+ tools: [getWeather],
+});
+
+const response = await agent.run("what's the weather in Tokyo?");
console.log(response.text);
```
-`SmoothAgent`'s constructor takes a `ChatClientLike` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions` object. `run` returns an `AgentRunResponse` whose `text` is the final answer.
+`SmoothAgent`'s constructor takes a `ChatClientLike` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions` object. A `Tool` is a `{ name, description, parameters, execute }` object. `run` returns an `AgentRunResponse` whose `text` is the final answer.
## Features
@@ -57,6 +74,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — `LexicalReranker` reranks retrieved hits before injection.
- **Sub-agents / delegation** — `delegateTool` spawns child agents for sub-tasks.
- **Cast + clearance** — `Cast`, `Clearance`, `makeRole` for per-role tool-access policy.
+- **Permissions + deny-policy** — a tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — `HumanGate` requires approval before designated tool calls run.
- **Conversation thread** — `SmoothAgentThread` carries a conversation across multiple `run` calls.
- **`LlmProvider` seam + `MockLlmProvider`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
@@ -66,6 +84,36 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — stream incremental text, tool calls, and tool results as the turn runs.
+## Permissions & deny-policy — lines the agent can't cross
+
+This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `Bypass` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.
+
+```ts
+import { SmoothAgent, AutoMode, DenyPolicy, type DenyPredicate } from '@smooai/smooth-operator-core';
+
+// Declarative rules (TOML): never the prod AWS profile, never a prod host.
+const policy = DenyPolicy.fromToml(`
+ schema_version = 1
+ [bash]
+ deny_patterns = ["aws * --profile prod"]
+ [network]
+ deny_hosts = ["*.prod.internal"]
+`);
+
+// Predicate for what strings can't express — return a reason to deny, or undefined to allow.
+const denyDbWriter: DenyPredicate = (call) =>
+ call.name === 'db_query' && /writer/.test(JSON.stringify(call.arguments))
+ ? 'DB writer endpoint is off-limits — reads go to the replica'
+ : undefined;
+
+const agent = new SmoothAgent(provider, {
+ instructions: 'You are a careful assistant',
+ tools: [getWeather],
+ permissionMode: AutoMode.Ask, // read allow · mutate ask · dangerous deny
+ denyPolicy: policy.withPredicate(denyDbWriter),
+});
+```
+
## Streaming
`runStream` is an async generator over a `StreamEvent` tagged union (discriminated on `type`): `text` deltas as the model produces them, each `tool_call` before dispatch, each `tool_result` after it finishes, and a terminal `done` event carrying the same response `run` would have returned.