diff --git a/.changeset/lockstep-1-7-0.md b/.changeset/lockstep-1-7-0.md
new file mode 100644
index 0000000..cd5c886
--- /dev/null
+++ b/.changeset/lockstep-1-7-0.md
@@ -0,0 +1,10 @@
+---
+"@smooai/smooth-operator-core": minor
+---
+
+First lockstep polyglot release. Changesets now drives publishing for every
+language artifact (npm + crates.io + NuGet + PyPI + Go tag) at a single shared
+version via `scripts/ci-publish.mjs`, with `scripts/sync-versions.mjs`
+propagating the Changeset version to all manifests. This aligns the previously
+divergent per-language versions (npm 0.22, Rust 0.16, .NET 1.6, Python 1.3) onto
+one lockstep line at 1.7.0 — no registry downgrades.
diff --git a/.changeset/polyglot-lockstep-publish.md b/.changeset/polyglot-lockstep-publish.md
new file mode 100644
index 0000000..7fed614
--- /dev/null
+++ b/.changeset/polyglot-lockstep-publish.md
@@ -0,0 +1,5 @@
+---
+'@smooai/smooth-operator-core': patch
+---
+
+Release infra: Changesets now drives lockstep publishing of every polyglot artifact (npm + crates.io + NuGet + PyPI + Go tag) from a single canonical version. Adds `scripts/sync-versions.mjs` (propagates the npm version to Rust/.NET/Python/Go manifests) and `scripts/ci-publish.mjs` (idempotent, skip-if-already-published, DRY_RUN) wired into `release.yml`. The per-language `publish-*.yml` workflows remain as manual fallbacks.
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/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml
index 1aa3caf..7d3998f 100644
--- a/.github/workflows/publish-crate.yml
+++ b/.github/workflows/publish-crate.yml
@@ -1,7 +1,9 @@
# Publish `smooai-smooth-operator-core` to crates.io.
#
# ─────────────────────────────────────────────────────────────────────────────
-# THIS WORKFLOW IS THE ONLY PLACE THIS CRATE IS PUBLISHED.
+# MANUAL FALLBACK. The Changesets release (release.yml → `pnpm ci:publish`) now
+# drives the normal, lockstep publish of this crate. Keep this workflow for
+# out-of-band / recovery publishes; both paths are idempotent (skip-if-exists).
#
# • Publishing to crates.io is IRREVERSIBLE — there is no delete, only
# `cargo yank` (which hides a version but cannot reuse the version number).
diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml
index c569d4b..3988ef6 100644
--- a/.github/workflows/publish-npm.yml
+++ b/.github/workflows/publish-npm.yml
@@ -1,6 +1,10 @@
# Publish the smooth-operator-core TypeScript engine to npm.
#
# ─────────────────────────────────────────────────────────────────────────────
+# MANUAL FALLBACK. The Changesets release (release.yml → `pnpm ci:publish`) now
+# drives the normal, lockstep publish. Keep this workflow for out-of-band /
+# recovery publishes; both paths are idempotent (skip-if-exists).
+#
# Package:
# • @smooai/smooth-operator-core (typescript/core/) — the native, in-process
# TypeScript agent engine. The sibling of the Rust reference engine, the C#
diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml
index 195629d..b487c27 100644
--- a/.github/workflows/publish-nuget.yml
+++ b/.github/workflows/publish-nuget.yml
@@ -1,7 +1,9 @@
# Publish the C# agent-engine core to NuGet.
#
# ─────────────────────────────────────────────────────────────────────────────
-# THIS WORKFLOW IS THE ONLY PLACE THIS PACKAGE IS PUBLISHED.
+# MANUAL FALLBACK. The Changesets release (release.yml → `pnpm ci:publish`) now
+# drives the normal, lockstep publish. Keep this workflow for out-of-band /
+# recovery publishes; both paths are idempotent (--skip-duplicate).
#
# Package: SmooAI.SmoothOperator.Core (dotnet/core/src) — the native C# engine.
#
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 89a6ab2..da92753 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -1,7 +1,9 @@
# Publish the Python agent-engine core to PyPI.
#
# ─────────────────────────────────────────────────────────────────────────────
-# THIS WORKFLOW IS THE ONLY PLACE THIS PACKAGE IS PUBLISHED.
+# MANUAL FALLBACK. The Changesets release (release.yml → `pnpm ci:publish`) now
+# drives the normal, lockstep publish. Keep this workflow for out-of-band /
+# recovery publishes; both paths are idempotent (--check-url).
#
# Package: smooai-smooth-operator-core (python/core) — the native Python engine.
#
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5e78d35..6760bff 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -41,17 +41,47 @@ jobs:
- name: Install dependencies
run: pnpm install
+ # ── Polyglot toolchains ──────────────────────────────────────────
+ # ci:version (version step) syncs every manifest; ci:publish (publish
+ # step) packs + pushes to crates.io / NuGet / PyPI / Go. Both run
+ # inside the changesets/action step below, so every toolchain must be
+ # present before it.
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: '0.7.8'
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: rust
+
- name: Version Update 🦋
id: changesets
uses: changesets/action@v1
with:
+ version: pnpm ci:version
commit: '🦋 New version release'
title: '🦋 New version release'
publish: pnpm ci:publish
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+ # Publish tokens for ci:publish — org-level secrets, same names
+ # the manual publish-*.yml workflows use.
NODE_AUTH_TOKEN: ${{ secrets.SMOOAI_NPM_TOKEN }}
+ NPM_CONFIG_PROVENANCE: 'true'
+ CARGO_REGISTRY_TOKEN: ${{ secrets.SMOOAI_CARGO_REGISTRY_TOKEN }}
+ NUGET_API_KEY: ${{ secrets.SMOOAI_NUGET_API_KEY }}
+ UV_PUBLISH_TOKEN: ${{ secrets.SMOOAI_PYPI_TOKEN }}
- name: Auto-Merge Changeset PR
run: |
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/go/version.go b/go/version.go
index 1018e12..a3b76cb 100644
--- a/go/version.go
+++ b/go/version.go
@@ -5,4 +5,4 @@ package e2e
// language artifacts. The real Go "publish" is a git tag (go/v); this
// constant is the anchor that scripts/sync-versions.mjs keeps in sync with the
// canonical npm version on every changeset release.
-const Version = "1.1.0"
+const Version = "1.6.0"
diff --git a/package.json b/package.json
index 0bb4478..7d3cb39 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,9 @@
"rust:clippy": "cd rust && cargo clippy --workspace --all-targets -- -D warnings",
"rust:test": "cd rust && cargo test --workspace",
"changeset": "changeset",
- "ci:publish": "pnpm --filter @smooai/smooth-operator-core build && changeset publish"
+ "version:sync": "node scripts/sync-versions.mjs",
+ "ci:version": "node scripts/ci-version.mjs",
+ "ci:publish": "node scripts/ci-publish.mjs"
},
"devDependencies": {
"@changesets/cli": "^2.27.10"
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/python/core/pyproject.toml b/python/core/pyproject.toml
index 742ed51..49f9e84 100644
--- a/python/core/pyproject.toml
+++ b/python/core/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "smooai-smooth-operator-core"
-version = "1.3.2"
+version = "1.6.0"
description = "Native Python implementation of the smooth-operator agent engine — an in-process, OpenAI-compatible agentic tool-calling loop with knowledge grounding. The Python sibling of the Rust reference engine and the C# core."
readme = "README.md"
license = { text = "MIT" }
diff --git a/rust/smooth-operator-core/Cargo.toml b/rust/smooth-operator-core/Cargo.toml
index 9b7e60d..0a0a026 100644
--- a/rust/smooth-operator-core/Cargo.toml
+++ b/rust/smooth-operator-core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "smooai-smooth-operator-core"
-version = "0.16.2"
+version = "1.6.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/SmooAI/smooth-operator-core"
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/scripts/ci-publish.mjs b/scripts/ci-publish.mjs
new file mode 100644
index 0000000..8196d46
--- /dev/null
+++ b/scripts/ci-publish.mjs
@@ -0,0 +1,267 @@
+#!/usr/bin/env node
+/**
+ * CI publish orchestrator — runs after the Changeset version PR merges
+ * (wired as the `publish:` input of changesets/action in release.yml).
+ *
+ * Publishes the ONE lockstep version to every polyglot registry:
+ *
+ * • npm — @smooai/smooth-operator-core (typescript/core)
+ * • crates.io— smooai-smooth-operator-core (rust/smooth-operator-core)
+ * • NuGet — SmooAI.SmoothOperator.Core (dotnet/core)
+ * • PyPI — smooai-smooth-operator-core (python/core)
+ * • Go — git tag go/vX.Y.Z (go/ — "publish" == tag)
+ *
+ * ── SAFETY (these are IRREVERSIBLE registries — a NuGet/PyPI/crates version can
+ * never be deleted or reused) ────────────────────────────────────────────────
+ * • IDEMPOTENT: every registry is queried for the target version first and
+ * SKIPPED if already present, so a re-run never double-publishes and never
+ * hard-fails on an existing version. Belt: --skip-duplicate (NuGet) /
+ * --check-url (PyPI) as a second line of defence.
+ * • DRY_RUN (env DRY_RUN=true or `--dry-run`): does the existence checks and
+ * packs/validates, but PUSHES NOTHING and needs NO tokens. This is what you
+ * run to prove the logic locally.
+ * • Isolation: a failure in one language is recorded and the others still run,
+ * but ANY failure makes the whole run exit non-zero — a partial release
+ * surfaces loudly and the idempotent re-run picks up where it left off.
+ *
+ * sync-versions runs FIRST (real runs only) so every manifest carries the
+ * canonical version before we pack. In the normal flow the merged version PR
+ * already synced them; this is belt-and-suspenders.
+ */
+import { execFileSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import https from "node:https";
+import process from "node:process";
+
+const root = process.cwd();
+const DRY_RUN = process.env.DRY_RUN === "true" || process.argv.includes("--dry-run");
+
+const version = JSON.parse(readFileSync(resolve(root, "typescript/core/package.json"), "utf8")).version;
+if (!version) {
+ console.error("Unable to read version from typescript/core/package.json");
+ process.exit(1);
+}
+
+// ── helpers ──────────────────────────────────────────────────────────────────
+
+function run(cmd, args, opts = {}) {
+ console.log(` > ${cmd} ${args.join(" ")}`);
+ execFileSync(cmd, args, { stdio: "inherit", cwd: root, ...opts });
+}
+
+function hasTool(name) {
+ try {
+ execFileSync("sh", ["-c", `command -v ${name}`], { stdio: "ignore" });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function httpGet(url) {
+ return new Promise((res, rej) => {
+ https
+ .get(url, { headers: { "user-agent": "smooth-operator-core-ci-publish" } }, (r) => {
+ let body = "";
+ r.setEncoding("utf8");
+ r.on("data", (c) => (body += c));
+ r.on("end", () => res({ statusCode: r.statusCode, body }));
+ })
+ .on("error", rej);
+ });
+}
+
+// crates.io sparse-index path layout (mirrors smooth/scripts/ci-publish.mjs).
+function sparsePath(crate) {
+ if (crate.length === 1) return `1/${crate}`;
+ if (crate.length === 2) return `2/${crate}`;
+ if (crate.length === 3) return `3/${crate[0]}/${crate}`;
+ return `${crate.slice(0, 2)}/${crate.slice(2, 4)}/${crate}`;
+}
+
+// ── per-registry existence checks (all pure HTTP GET — no auth) ───────────────
+
+async function npmHasVersion(name, ver) {
+ const { statusCode, body } = await httpGet(`https://registry.npmjs.org/${name.replace("/", "%2F")}`);
+ if (statusCode === 404) return false;
+ if (statusCode !== 200) throw new Error(`npm registry returned ${statusCode} for ${name}`);
+ return Boolean(JSON.parse(body).versions?.[ver]);
+}
+
+async function cratesHasVersion(crate, ver) {
+ const { statusCode, body } = await httpGet(`https://index.crates.io/${sparsePath(crate)}`);
+ if (statusCode === 404) return false;
+ if (statusCode !== 200) throw new Error(`crates.io index returned ${statusCode} for ${crate}`);
+ return body
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean)
+ .some((line) => {
+ try {
+ return JSON.parse(line).vers === ver;
+ } catch {
+ return false;
+ }
+ });
+}
+
+async function nugetHasVersion(id, ver) {
+ const { statusCode, body } = await httpGet(`https://api.nuget.org/v3-flatcontainer/${id.toLowerCase()}/index.json`);
+ if (statusCode === 404) return false;
+ if (statusCode !== 200) throw new Error(`NuGet flat-container returned ${statusCode} for ${id}`);
+ return (JSON.parse(body).versions ?? []).includes(ver);
+}
+
+async function pypiHasVersion(name, ver) {
+ const { statusCode, body } = await httpGet(`https://pypi.org/pypi/${name}/json`);
+ if (statusCode === 404) return false;
+ if (statusCode !== 200) throw new Error(`PyPI returned ${statusCode} for ${name}`);
+ return Object.prototype.hasOwnProperty.call(JSON.parse(body).releases ?? {}, ver);
+}
+
+function gitTagExists(tag) {
+ // Local tag OR remote tag counts as "already published".
+ const local = execFileSync("git", ["tag", "-l", tag], { cwd: root }).toString().trim();
+ if (local) return true;
+ const remote = execFileSync("git", ["ls-remote", "--tags", "origin", tag], { cwd: root }).toString().trim();
+ return Boolean(remote);
+}
+
+function requireEnv(name) {
+ if (!process.env[name]) throw new Error(`${name} is not set (required to publish for real)`);
+ return process.env[name];
+}
+
+// ── per-registry publishers ───────────────────────────────────────────────────
+// Each returns a status string. Existence is checked by the caller.
+
+const registries = [
+ {
+ name: "npm",
+ artifact: `@smooai/smooth-operator-core@${version}`,
+ exists: () => npmHasVersion("@smooai/smooth-operator-core", version),
+ tool: "pnpm",
+ publish(dry) {
+ run("pnpm", ["--filter", "@smooai/smooth-operator-core", "build"]);
+ const flags = ["--filter", "@smooai/smooth-operator-core", "publish", "--no-git-checks", "--access", "public"];
+ if (dry) {
+ run("pnpm", [...flags, "--dry-run"]);
+ return;
+ }
+ requireEnv("NODE_AUTH_TOKEN");
+ run("pnpm", flags, { env: { ...process.env, NPM_CONFIG_PROVENANCE: "true" } });
+ },
+ },
+ {
+ name: "crates.io",
+ artifact: `smooai-smooth-operator-core@${version}`,
+ exists: () => cratesHasVersion("smooai-smooth-operator-core", version),
+ tool: "cargo",
+ publish(dry) {
+ if (dry) {
+ // Package the tarball without a full compile — proves the crate
+ // is packable without the multi-minute verify build.
+ run("cargo", ["package", "-p", "smooai-smooth-operator-core", "--no-verify", "--allow-dirty"], { cwd: resolve(root, "rust") });
+ return;
+ }
+ requireEnv("CARGO_REGISTRY_TOKEN");
+ run("cargo", ["publish", "-p", "smooai-smooth-operator-core", "--locked"], { cwd: resolve(root, "rust") });
+ },
+ },
+ {
+ name: "NuGet",
+ artifact: `SmooAI.SmoothOperator.Core@${version}`,
+ exists: () => nugetHasVersion("SmooAI.SmoothOperator.Core", version),
+ tool: "dotnet",
+ publish(dry) {
+ run("dotnet", ["pack", "dotnet/core/src/SmooAI.SmoothOperator.Core.csproj", "-c", "Release", "-o", "dist"]);
+ if (!existsSync(resolve(root, "dist"))) throw new Error("dotnet pack produced no dist/ output");
+ if (dry) return;
+ const apiKey = requireEnv("NUGET_API_KEY");
+ run("dotnet", ["nuget", "push", "dist/*.nupkg", "--api-key", apiKey, "--source", "https://api.nuget.org/v3/index.json", "--skip-duplicate"]);
+ },
+ },
+ {
+ name: "PyPI",
+ artifact: `smooai-smooth-operator-core@${version}`,
+ exists: () => pypiHasVersion("smooai-smooth-operator-core", version),
+ tool: "uv",
+ publish(dry) {
+ const cwd = resolve(root, "python/core");
+ run("uv", ["build"], { cwd });
+ if (dry) return;
+ requireEnv("UV_PUBLISH_TOKEN");
+ // --check-url makes a re-run of an already-published version a no-op.
+ run("uv", ["publish", "--check-url", "https://pypi.org/simple/"], { cwd });
+ },
+ },
+ {
+ name: "Go (git tag)",
+ artifact: `go/v${version}`,
+ exists: async () => gitTagExists(`go/v${version}`),
+ tool: "git",
+ publish(dry) {
+ const tag = `go/v${version}`;
+ if (dry) {
+ console.log(` (dry-run) would create + push tag ${tag} at HEAD`);
+ return;
+ }
+ run("git", ["tag", tag]);
+ run("git", ["push", "origin", tag]);
+ },
+ },
+];
+
+// ── orchestrate ───────────────────────────────────────────────────────────────
+
+(async () => {
+ console.log(`smooth-operator-core release @ ${version}${DRY_RUN ? " (DRY RUN — nothing is pushed)" : ""}\n`);
+
+ if (!DRY_RUN) {
+ console.log("Syncing manifests to canonical version first…");
+ run("node", ["scripts/sync-versions.mjs"]);
+ console.log("");
+ }
+
+ const results = [];
+
+ for (const reg of registries) {
+ console.log(`── ${reg.name}: ${reg.artifact}`);
+ try {
+ if (await reg.exists()) {
+ console.log(` [skip] already published`);
+ results.push({ name: reg.name, status: "skipped" });
+ continue;
+ }
+
+ if (DRY_RUN && !hasTool(reg.tool)) {
+ console.log(` [dry-run] ${reg.tool} not installed — skipping pack (would publish ${reg.artifact})`);
+ results.push({ name: reg.name, status: "would-publish (pack skipped: no toolchain)" });
+ continue;
+ }
+
+ console.log(` ${DRY_RUN ? "[dry-run] pack/validate — would publish" : "[publish]"} ${reg.artifact}`);
+ reg.publish(DRY_RUN);
+ results.push({ name: reg.name, status: DRY_RUN ? "would-publish" : "published" });
+ } catch (err) {
+ // Isolate: record and keep going so one broken language doesn't mask
+ // the state of the others. The non-zero exit below still surfaces it.
+ console.error(` [FAIL] ${reg.name}: ${err.message}`);
+ results.push({ name: reg.name, status: "FAILED", error: err.message });
+ }
+ }
+
+ console.log(`\n── Summary (@ ${version}${DRY_RUN ? ", dry run" : ""}) ──`);
+ for (const r of results) console.log(` ${r.name.padEnd(14)} ${r.status}`);
+
+ const failures = results.filter((r) => r.status === "FAILED");
+ if (failures.length) {
+ console.error(`\n${failures.length} registr${failures.length === 1 ? "y" : "ies"} failed — see above.`);
+ process.exit(1);
+ }
+ console.log(`\nDone.`);
+})().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/scripts/ci-version.mjs b/scripts/ci-version.mjs
new file mode 100644
index 0000000..58fd064
--- /dev/null
+++ b/scripts/ci-version.mjs
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+/**
+ * CI version script — runs during Changeset version-PR creation
+ * (wired as the `version:` input of changesets/action in release.yml).
+ *
+ * 1. `changeset version` — consume changeset files, bump
+ * typescript/core/package.json (the canonical npm version).
+ * 2. `version:sync` — propagate that version to the Rust/.NET/Python/Go
+ * manifests so the SAME version PR carries every polyglot bump.
+ *
+ * Without step 2 in the version step, the sibling manifests would only be
+ * rewritten at publish time — after the version PR already merged with stale
+ * Cargo.toml / csproj / pyproject / version.go. Doing it here keeps the repo
+ * self-consistent at every commit on main.
+ */
+import { execSync } from "node:child_process";
+import process from "node:process";
+
+const root = process.cwd();
+
+function run(cmd) {
+ console.log(`\n> ${cmd}`);
+ execSync(cmd, { stdio: "inherit", cwd: root });
+}
+
+run("pnpm changeset version");
+run("pnpm version:sync");
diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs
new file mode 100644
index 0000000..1465c42
--- /dev/null
+++ b/scripts/sync-versions.mjs
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+/**
+ * Sync the canonical version → every publishable polyglot manifest.
+ *
+ * The single source of truth is the npm package `@smooai/smooth-operator-core`
+ * (typescript/core/package.json) — the one package Changesets version-bumps.
+ * This script propagates that version, in lockstep, to the sibling engines so a
+ * changeset release ships npm + crates.io + NuGet + PyPI + Go all at one number:
+ *
+ * 1. Rust — rust/smooth-operator-core/Cargo.toml [package] version
+ * 2. .NET — dotnet/core/src/SmooAI.SmoothOperator.Core.csproj
+ * 3. Python — python/core/pyproject.toml [project] version
+ * 4. Go — go/version.go const Version (the anchor
+ * scripts read for the `go/vX.Y.Z` publish tag; see ci-publish.mjs)
+ *
+ * NOT touched, on purpose:
+ * • Cargo.lock — gitignored in this repo (see .gitignore), so there is nothing
+ * tracked to rewrite. `cargo publish` regenerates it.
+ * • rust/smooth-operator-temporal — `publish = false`, path-deps core with no
+ * version requirement; it never reaches a registry so its version is inert.
+ *
+ * FAILS LOUDLY: if any expected version anchor is missing we throw rather than
+ * silently ship a mismatched set — a partial sync must abort the release, never
+ * publish half the languages at the new version and half at the old one.
+ */
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import process from "node:process";
+
+const root = process.cwd();
+
+// Canonical version = the npm package Changesets bumps.
+const canonicalPkgPath = resolve(root, "typescript/core/package.json");
+const canonicalPkg = JSON.parse(readFileSync(canonicalPkgPath, "utf8"));
+const version = canonicalPkg.version;
+
+if (!version) {
+ console.error("Unable to read version from typescript/core/package.json");
+ process.exit(1);
+}
+if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) {
+ console.error(`Refusing to sync a non-semver version: "${version}"`);
+ process.exit(1);
+}
+
+/**
+ * Each anchor pins a version inside exactly one manifest. `pattern` MUST have
+ * two capture groups wrapping the version text so we can splice the new value
+ * in without disturbing surrounding formatting.
+ */
+const anchors = [
+ {
+ label: "Rust crate (Cargo.toml [package] version)",
+ path: "rust/smooth-operator-core/Cargo.toml",
+ pattern: /(name = "smooai-smooth-operator-core"\nversion = ")[^"]+(")/,
+ },
+ {
+ label: ".NET package (csproj )",
+ path: "dotnet/core/src/SmooAI.SmoothOperator.Core.csproj",
+ pattern: /()[^<]+(<\/Version>)/,
+ },
+ {
+ label: "Python package (pyproject [project] version)",
+ path: "python/core/pyproject.toml",
+ pattern: /(name = "smooai-smooth-operator-core"\nversion = ")[^"]+(")/,
+ },
+ {
+ label: "Go module (version.go const Version)",
+ path: "go/version.go",
+ pattern: /(const Version = ")[^"]+(")/,
+ },
+];
+
+let touched = 0;
+
+for (const { label, path, pattern } of anchors) {
+ const absolutePath = resolve(root, path);
+ let content;
+ try {
+ content = readFileSync(absolutePath, "utf8");
+ } catch (error) {
+ if (error && error.code === "ENOENT") {
+ throw new Error(`Version anchor file missing: ${path} (${label})`);
+ }
+ throw error;
+ }
+
+ if (!pattern.test(content)) {
+ throw new Error(`Version anchor not found in ${path} (${label}) — refusing partial sync`);
+ }
+
+ const next = content.replace(pattern, `$1${version}$2`);
+ if (next !== content) {
+ writeFileSync(absolutePath, next);
+ touched += 1;
+ console.log(`Updated ${label} → ${version} (${path})`);
+ } else {
+ console.log(`Already ${version}: ${label} (${path})`);
+ }
+}
+
+console.log(`\nSynced version ${version} across ${anchors.length} manifest(s); ${touched} changed.`);
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.
diff --git a/typescript/core/package.json b/typescript/core/package.json
index 68a1d76..f28aa43 100644
--- a/typescript/core/package.json
+++ b/typescript/core/package.json
@@ -1,44 +1,44 @@
{
- "name": "@smooai/smooth-operator-core",
- "version": "0.23.0",
- "description": "Native TypeScript implementation of the smooth-operator agent engine — an in-process, OpenAI-compatible agentic tool-calling loop with knowledge grounding. The TypeScript sibling of the Rust reference engine, the C# core, and the Python core.",
- "type": "module",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/SmooAI/smooth-operator-core.git",
- "directory": "typescript/core"
- },
- "homepage": "https://github.com/SmooAI/smooth-operator-core/tree/main/typescript/core",
- "sideEffects": false,
- "main": "./dist/index.js",
- "types": "./dist/index.d.ts",
- "exports": {
- ".": {
- "types": "./dist/index.d.ts",
- "import": "./dist/index.js"
- },
- "./extension": {
- "types": "./dist/extension/index.d.ts",
- "import": "./dist/extension/index.js"
- }
- },
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "tsc -p tsconfig.build.json",
- "prepack": "pnpm run build",
- "typecheck": "tsc -p tsconfig.json --noEmit",
- "test": "vitest run"
- },
- "dependencies": {
- "openai": "^4.77.0",
- "smol-toml": "^1.7.0"
- },
- "devDependencies": {
- "@types/node": "^25.9.2",
- "typescript": "^5.7.3",
- "vitest": "^2.1.8"
- }
+ "name": "@smooai/smooth-operator-core",
+ "version": "1.6.0",
+ "description": "Native TypeScript implementation of the smooth-operator agent engine — an in-process, OpenAI-compatible agentic tool-calling loop with knowledge grounding. The TypeScript sibling of the Rust reference engine, the C# core, and the Python core.",
+ "type": "module",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/SmooAI/smooth-operator-core.git",
+ "directory": "typescript/core"
+ },
+ "homepage": "https://github.com/SmooAI/smooth-operator-core/tree/main/typescript/core",
+ "sideEffects": false,
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ },
+ "./extension": {
+ "types": "./dist/extension/index.d.ts",
+ "import": "./dist/extension/index.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc -p tsconfig.build.json",
+ "prepack": "pnpm run build",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "openai": "^4.77.0",
+ "smol-toml": "^1.7.0"
+ },
+ "devDependencies": {
+ "@types/node": "^25.9.2",
+ "typescript": "^5.7.3",
+ "vitest": "^2.1.8"
+ }
}