From a702c5fba70630b5ec1df3b42a4757fa8c355f80 Mon Sep 17 00:00:00 2001 From: Brent Rager Date: Fri, 17 Jul 2026 14:38:02 -0400 Subject: [PATCH] docs: rewrite root + per-language READMEs as storytelling registry pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elevate every README (root + Rust/TS/Python/Go/.NET package pages) from a feature list into a narrative: hook → 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 an agent+tool quickstart in its own idiom (mock scripted to call the tool, then answer), verified against the real API. - Adds the headline permission system + deny-policy (AutoMode, circuit-breakers, declarative TOML rules + semantic predicates) to every features list and gives it a dedicated safety-story section with a language-native example (with_deny_policy in Rust, denyPolicy/permissionMode in TS/Py, WithDenyPolicy in Go/C#). - Refreshes the root polyglot table (language → package → registry), fixes the stale "Planned" statuses (TS/Python/.NET are at parity now) and inconsistent test-count claims. Docs only — no code changes. Registry readme-pointers already wired (Cargo readme, pyproject readme, csproj PackageReadmeFile+packed README; npm ships README by default). Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/readme-story-permissions.md | 20 +++++++ README.md | 75 +++++++++++++++++------ dotnet/core/README.md | 63 +++++++++++++++++--- go/core/README.md | 75 ++++++++++++++++++++--- python/core/README.md | 71 +++++++++++++++++++--- rust/smooth-operator-core/README.md | 82 +++++++++++++++++++++++--- typescript/core/README.md | 70 ++++++++++++++++++---- 7 files changed, 397 insertions(+), 59 deletions(-) create mode 100644 .changeset/readme-story-permissions.md 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 @@

Rust reference implementation - 337 tests passing + tests passing

@@ -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.