Description
cognexus-tools
Client SDKs for the ArtzAIn decision engine, made by CogNEXUS Labs.
This is the public home of every ArtzAIn client package. The decision
engine itself lives in a separate private repository and dual-homes copies
for CI and guard-sync until those jobs move here.
| Package | Install | Latest | What it is |
|—|—|—|—|
| artzain | pip install artzain |   | Python SDK — local guards, decide(), CLI (login, quickstart, init, audit, policy, registry) |
| @cognexuslabs/artzain | npm i @cognexuslabs/artzain |   | TypeScript SDK — remote-only Node client (decide, events, identity) |
| @cognexuslabs/openclaw-artzain | npm i @cognexuslabs/openclaw-artzain |  | OpenClaw beforetoolcall plugin (deny / review / errors block; not /approve) |
| @cognexuslabs/n8n-nodes-artzain | npm i @cognexuslabs/n8n-nodes-artzain |  | n8n Decision + Envelope nodes (fail closed on HTTP 503; review does not Wait) |
| @cognexuslabs/grokbot-artzain | npm install -g @cognexuslabs/grokbot-artzain |  | Grok Bot cooperative Decision skill, opt-in announce, and pull enroll (no host intercept) |
All five are Apache-2.0 . artzain also ships the OpenShell sidecar, as
the [openshell] extra, as a signed image on GHCR, and with a one-command
connect script on each release (OpenShell).
Each package is versioned and released on its own. Its release tag publishes
it and creates a GitHub Release carrying that version’s changelog section
(python, typescript,
openclaw, n8n,
grokbot). The Releases box on the repository page
shows only the release marked Latest, which is always artzain‘s; the four
npm packages’ releases are in the same
list.
Putting decide() into an agent’s tool loop (OpenAI or Anthropic)? Start at
Gating tool calls.
python/ # PyPI package artzain (Hatchling src-layout)
typescript/ # npm package @cognexuslabs/artzain
openclaw/ # npm package @cognexuslabs/openclaw-artzain
n8n/ # npm package @cognexuslabs/n8n-nodes-artzain
grokbot/ # npm package @cognexuslabs/grokbot-artzain
images/ # the OpenShell sidecar image (ghcr.io/cognexuslabs/artzain-openshell-sidecar)
tools/ # standalone scripts: the decide latency benchmark
Engine dual-home paths (private repo): pypi-package/, sdk/typescript/,
sdk/openclaw/, sdk/n8n/, sdk/grokbot/. Seed with scripts/seedcognexustools.sh from
the engine tree.
Privacy and telemetry
With no API key, the Python SDK sends nothing. postsdkevent returns
immediately.
With a key, the first event in a process is preceded by one sdk_session
(source pypi_sdk). Its payload is package, version, python, and
platform. Every event is POST /api/events with event_type, source,
payload, level, and title. A payload may also carry tokens_in and
tokensout, and a redacted userprompt when this process has noted one
and the event is not already carrying the screened text as its preview.
Posts are queued on a background thread, at most 1000 waiting. A full queue
drops the new row.
Prompt-defense clean scans are posted whenever a key is set, unless
COGNEXUSPROMPTDEFENSECLOUDPASSES=0. A prompt_defense payload carries
defenserecordversion, request_id, outcome, reason, surface,
componentsource, policy, latencyms, modelid, inputsha256,
preview (96 characters), kind, action, threat, injection_type,
confidence, patterns, patterncount, and userid. The preview masks
SSN, card, IBAN, NINO, and key=value secrets. Other text is previewed as
written. A policyenforcement payload adds violationcount, rule_ids,
and rules_checked, and names the policy ClientPolicy-DocumentDerived.
A promptstaticaudit payload carries outcome, reason, grade,
score, coverage, missing, context, and prompt_hash. Kill-switch
and destructive-guard events (agentkillswitch,
destructiveactionguard) carry defenserecordversion, request_id,
outcome, reason, surface agentaction, componentsource, policy
(Agent-Kill-Switch or Destructive-Action-Guard), input_sha256,
preview, runid, userid, agentid, severity, and ruleids.
The event table and the clean-scan switch are also in the Python package
README (python/README.md here, pypi-package/README.md in the engine
tree), under “Dashboard event logs”.
The TypeScript SDK posts only when the caller invokes postSdkEvent:
event_type, title, level, and payload, with X-Api-Key. Nothing is
sent automatically.
Grok Bot and OpenClaw announce only when announce is exactly true. The
post is names only: the instance, agent ids, and skill slugs, capped at 50
agents, 100 skills, 200 characters a name, and 120 characters for the
instance. Enroll is on by default. It posts the same names to
POST /api/v1/registry/enroll. Turn it off with GROKBOT_ENROLL=false or
--no-enroll, or with enroll: false in OpenClaw.
COGNEXUSSDKBROWSER_HEADERS defaults to "0" since 0.6.11. "1" makes
CLI auth calls and the GUI proxy send browser-like headers. Event posts and
decide calls do not. The switch is scheduled for removal.
Python (artzain)
pip install artzain
optional extras:
pip install "artzain[verify]" # offline Ed25519 signature verification
pip install "artzain[policy]" # policy bundle signing (keygen/sign)export COGNEXUSAPIKEY=cnx_…
artzain login
artzain quickstart
artzain 0.6.32
artzain init --framework openshell
artzain registry list --source openshell
init --framework openshell writes artzainopenshellpolicy.yaml: a
network policy that allows the Decision API and a local inference host
with enforcement on, and skill text that proposes a policy.local rule
and waits. The command does not run that file as Python. registry list
and registry export accept --source openshell. Releases through
0.6.31 do not contain either command.
import artzaind = artzain.decide(
action="send_email",
target="crm:contact:123",
payload=draftemailtext,
kind="model_output",
)
if d["outcome"] == "allow":
actually_send()
Zero mandatory runtime dependencies. Offline decide() runs the same local guards when no API key is set (offline: true, no audit seal).
See python/README.md for guards, CLI, and extras.
Gating tool calls
artzain does not hook your LLM client, and nothing intercepts tool calls
automatically. You place decide() in the gap between the model proposing a
tool call and your code executing it. That gap is the control point.
user input ──► screenuserinput() ──► model ──► tool call ──► decide() ──► execute
| decide() argument | Comes from |
|—|—|
| action | the tool name the model chose |
| target | the resource the call touches, read out of the arguments |
| payload | the whole call as JSON: {"tool": |
| kind | "tool_call": shape and contract checks on the engine, plus the destructive-action guard |
Only allow runs the tool; review means a human decides first. decide()
raises DecisionError on any non-2xx response or when the engine cannot be
reached. Treat that as deny. The destructive-action, injection, policy and
PII screens read the serialized call, and every string in it as the tool
receives them: JSON-decoded, including JSON inside a string such as a
stringified arguments. The destructive-action screen reads each string on
its own; the others read the strings together, so one argument can change the
result for another. The conduct rules count a client word in any value of the
call; an argument or tool name counts only when it holds the profanity as
well. The PII screen runs on the engine only. A line of only --- or three
backticks in an argument comes back review; inline base64 or escape
sequences written out as text can come back deny. Parse arguments with a
strict JSON parser and send the parsed call, as the examples do. The
destructive-action screen also reads a list of strings joined, as an argv list
runs, but a command a tool assembles from separate fields (a cmd beside its
args) is not seen whole: screen that command inside the step as well
(screenagentaction()).
OpenAI-style. Tool calls arrive as a tool_calls array and
function.arguments is a JSON string. Append the assistant message before the
tool results that answer it.
import json
import artzainmsg = client.chat.completions.create(model=..., messages=messages, tools=tools).choices[0].message
messages.append(msg)
for tc in msg.tool_calls or []:
name = tc.function.name
try:
args = json.loads(tc.function.arguments)
except json.JSONDecodeError:
args = None
if not isinstance(args, dict):
d = {"outcome": "deny"} # unparseable arguments: never run them
else:
try:
d = artzain.decide(
action=name,
target=str(args.get("id") or args.get("to") or "unknown")[:300],
payload=json.dumps({"tool": name, "arguments": args}, ensure_ascii=False),
kind="tool_call",
)
except artzain.DecisionError:
d = {"outcome": "deny"} # the engine did not decide: fail closed
result = dispatch(name, args) if d["outcome"] == "allow" else f"Not run ({d['outcome']})."
messages.append({"role": "tool", "toolcallid": tc.id, "content": str(result)})
Anthropic. Tool calls arrive as tool_use content blocks and
block.input is already a dict. Answer every block with a tool_result,
denied ones included: the API requires the pair, and a model told it was
blocked usually re-plans instead of retrying the same call.
import json
import artzainresp = client.messages.create(model=..., messages=messages, tools=tools, max_tokens=1024)
results = []
for block in (b for b in resp.content if b.type == "tool_use"):
try:
d = artzain.decide(
action=block.name,
target=str(block.input.get("id") or block.input.get("to") or "unknown")[:300],
payload=json.dumps({"tool": block.name, "arguments": block.input}, ensure_ascii=False),
kind="tool_call",
)
except artzain.DecisionError:
d = {"outcome": "deny"} # the engine did not decide: fail closed
allowed = d["outcome"] == "allow"
results.append({
"type": "tool_result",
"tooluseid": block.id,
"content": str(dispatch(block.name, block.input)) if allowed else f"Not run ({d['outcome']}).",
"is_error": d["outcome"] == "deny",
})
if resp.content:
messages.append({"role": "assistant", "content": resp.content})
if results:
messages.append({"role": "user", "content": results})
Serialize with ensure_ascii=False, as both examples do. Escaped, each
non-ASCII character takes six characters of the payload limit (twelve above
U+FFFF, as for most emoji). Offline screens before artzain 0.6.16, and engines
without this change, read a run of those escapes as an encoding attack; current
screens still do when the escaped characters are invisible.
Cover every tool, not every call site. Put decide() in the one function
your agent dispatches tools through, so a tool added later is gated by
construction. Then make forgotten tools stop: declare the known ones in your
team’s policy bundle and escalate the finding an undeclared tool draws.
"guard_config": {
"tool_contracts": {
"sendemail": { "requiredargs": ["to"] },
"*": { "denyunknowntools": true }
},
"resolution": { "medium": "review" }
}
Without the resolution line an undeclared tool is only an advisory finding,
and the line escalates every medium vote, not just this one. reasons names
the tool and says the bundle escalated the call; a batch’s other findings are
on the tool-call-contract vote in contributing_agents. Contracts are checked
only for kind="tool_call", on a running engine with the bundle active
(artzain local up is enough). Offline, with no API key, decide() runs the
local guards only: offline: true, nothing sealed, no shape check, no bundle.
A call allowed offline can come back review or deny once you connect.
The full guide covers decide() versus screenagentaction(), where
intent-level gating stops, and choosing what to gate:
cognexuslabs.ai/install#tool-calls.
TypeScript (@cognexuslabs/artzain)
npm install @cognexuslabs/artzain # Node >= 18, zero runtime dependencies
import { configure, decide, DecisionError } from "@cognexuslabs/artzain";configure({ apiKey: process.env.COGNEXUSAPIKEY });
const decision = await decide({
action: "send_email",
target: "crm:contact:123",
payload: draftEmailBody,
});
Remote-only: a missing API key throws. Use the Python SDK where offline guard parity matters.
See typescript/README.md.
OpenClaw (@cognexuslabs/openclaw-artzain)
npm i @cognexuslabs/openclaw-artzain
openclaw plugins install ./openclaw
The plugin registers api.on("beforetoolcall", …) and calls
POST /api/v1/decisions. deny, review, and transport errors set
{ block: true }. It does not map ArtzAIn review onto OpenClaw
/approve. ClawHub listing is dest Trusted Publishing, not a tag from
the engine repo.
See openclaw/README.md.
n8n (@cognexuslabs/n8n-nodes-artzain)
npm i @cognexuslabs/n8n-nodes-artzain
Two nodes:
- ArtzAIn Decision —
POST /api/v1/decisionswith Allow / Review / Deny outputs. HTTP 503 fails closed.reviewis a third output, not an n8n Wait node. - ArtzAIn Envelope —
POST /api/v1/envelope/v1/chat/completionswith an envelope credential (cnxe_…). Not the Decision API.
See n8n/README.md.
Grok Bot (@cognexuslabs/grokbot-artzain)
npm install -g @cognexuslabs/grokbot-artzain
grokbot-artzain --version
Install it with -g, where the Bot runs its commands: the skill tells the
Bot to run the grokbot-artzain command, and only a global install puts
that on the PATH. Then set COGNEXUSAPIKEY (a Decision API key, cnx_…)
and add grokbot/SKILL.md to the Bot as a skill, in
the Grok Bot app.
Cooperative skill (pattern C): Grok Bot has no host beforetoolcall
intercept, so the Bot itself runs grokbot-artzain decide before a
side-effect. deny, review, and transport errors fail closed. The setup
steps, the Windows notes, opt-in announce and default-on pull enroll are in
grokbot/README.md.
OpenShell (artzain[openshell])
artzain carries a sidecar that binds an NVIDIA OpenShell gateway to
ArtzAIn: the gateway calls it before each governed write, and it decides
through the Decision API and fails closed. The [openshell] extra adds what
the gateway’s gRPC interceptor needs (grpcio, protobuf,
cryptography).
Connect a gateway with one command. From 0.6.37, each python-v
release here carries connect- and its .sha256. On the
gateway’s host, as the user the gateway runs as, with the enroll token and
configuration digest ArtzAIn issues once the gateway’s connect is approved:
curl -fsSLO https://github.com/CogNEXUSlabs/cognexus-tools/releases/download/python-v0.6.61/connect-0.6.61.sh &&
echo ' connect-0.6.61.sh' | sha256sum -c - &&
export ARTZAINENROLLTOKEN='' &&
sh connect-0.6.61.sh --config-digest
Keep the &&: a failed download or check then stops the run before the
token is exported or anything runs.
The script checks its own pinned uv by SHA-256, installs artzain and what
it needs into an environment of its own, every wheel checked against the
SHA-256 the script carries, and runs artzain connect openshell up, which installs the sidecar as a systemd user service beside
the gateway that OpenShell’s deb or rpm package runs (or beside the OpenShell
snap’s gateway, as a service of a gateway’s Docker Compose project, or, on a
Mac, as a launchd agent beside Homebrew’s gateway).
artzain connect openshell status, doctor, rotate-key and remove run
from where the script says. Linux on x86_64 or aarch64, or macOS on Apple
silicon (from 0.6.48; check the script with shasum -a 256 -c - there).
Works with NVIDIA OpenShell, release by release: each row is an artzain
release whose sidecar passed conformance against that OpenShell release.
| artzain | OpenShell | Verified |
|—|—|—|
| 0.6.62 | 0.1.6 | 2026-10-30 |
| 0.6.61 | 0.1.6 | 2026-10-30 |
| 0.6.60 | 0.1.6 | 2026-10-29 |
| 0.6.59 | 0.1.6 | 2026-10-28 |
| 0.6.59 | 0.1.5 | 2026-10-27 |
| 0.6.58 | 0.1.5 | 2026-10-25 |
| 0.6.57 | 0.1.5 | 2026-10-23 |
| 0.6.56 | 0.1.5 | 2026-10-22 |
| 0.6.55 | 0.1.5 | 2026-10-21 |
| 0.6.55 | 0.1.4 | 2026-10-20 |
| 0.6.54 | 0.1.4 | 2026-10-17 |
| 0.6.52 | 0.1.4 | 2026-10-14 |
| 0.6.51 | 0.1.4 | 2026-10-14 |
| 0.6.50 | 0.1.3 | 2026-10-13 |
| 0.6.48 | 0.1.3 | 2026-10-12 |
| 0.6.47 | 0.1.3 | 2026-10-11 |
| 0.6.46 | 0.1.3 | 2026-10-11 |
| 0.6.45 | 0.1.3 | 2026-10-10 |
| 0.6.44 | 0.1.3 | 2026-10-10 |
| 0.6.43 | 0.1.3 | 2026-10-09 |
| 0.6.62 | 0.1.2 | 2026-10-30 |
| 0.6.61 | 0.1.2 | 2026-10-30 |
| 0.6.60 | 0.1.2 | 2026-10-29 |
| 0.6.59 | 0.1.2 | 2026-10-27 |
| 0.6.58 | 0.1.2 | 2026-10-25 |
| 0.6.57 | 0.1.2 | 2026-10-23 |
| 0.6.56 | 0.1.2 | 2026-10-22 |
| 0.6.55 | 0.1.2 | 2026-10-20 |
| 0.6.54 | 0.1.2 | 2026-10-17 |
| 0.6.52 | 0.1.2 | 2026-10-14 |
| 0.6.51 | 0.1.2 | 2026-10-14 |
| 0.6.50 | 0.1.2 | 2026-10-13 |
| 0.6.48 | 0.1.2 | 2026-10-12 |
| 0.6.47 | 0.1.2 | 2026-10-11 |
| 0.6.46 | 0.1.2 | 2026-10-11 |
| 0.6.45 | 0.1.2 | 2026-10-10 |
| 0.6.44 | 0.1.2 | 2026-10-10 |
| 0.6.43 | 0.1.2 | 2026-10-09 |
| 0.6.37 | 0.1.2 | 2026-10-04 |
The sidecar as an image, for a gateway that runs in containers:
ghcr.io/cognexuslabs/artzain-openshell-sidecar:, built
here from the published wheel for linux/amd64 and linux/arm64, signed and
SBOM-attested keylessly by sidecar-image.yml at its sidecar-v
tag (see Releases). Run it by digest.
Measure decide latency yourself
status.cognexuslabs.ai, so the
two compare like for like. Standard library only, Python 3.10 or later.
export COGNEXUSAPIKEY=cnx_…
python tools/bench_decide.py # 50 requests on one warm connection
python tools/bench_decide.py --cold --json # and each on a new connection, as JSON
python tools/bench_decide.py --url http://127.0.0.1:8000 # an engine of your own
Every request is a real decision: sealed into your audit chain and counted
against your plan, so a plan with a daily cap spends it. A 429 is waited
out for as long as the engine asks. The key is never printed. A long request,
or one that needs the PII screen, takes the engine longer than this one.
Development
Python (3.10 to 3.14):
cd python
pip install pytest httpx cryptography pyyaml
PYTHONPATH=src pytest tests/ -q
TypeScript (Node ≥ 18):
cd typescript
npm ci
npm run build # tsc → dist/
npm test # vitest
OpenClaw plugin, n8n nodes, and Grok Bot skill:
cd openclaw && npm ci && npm test && npm run build
cd ../n8n && npm ci && npm test && npm run build
cd ../grokbot && npm ci && npm test && npm run build
Releases
python-v (must match version in python/src/artzain/init.py; pyproject.toml reads it from there). Publishes through a PyPI Trusted Publisher — no token, and each artifact carries a PEP 740 attestation. Do not add a password: to the publish step: an unset secret still takes the OIDC path at the pinned action version, so the line sits harmless until someone sets the secret — and then the release silently becomes a token publish with no attestation.sdk-ts-v (must match typescript/package.json). Publishes through npm Trusted Publishing (OIDC) — no token; npm generates the provenance itself, and npm audit signatures verifies it.openclaw-v (must match openclaw/package.json).n8n-v (must match n8n/package.json).grokbot-v (must match grokbot/package.json).sidecar-v (must be the artzain version images/openshell-sidecar/artzain.lock names, which is written after that version is on PyPI). sidecar-image.yml builds it from the published wheel for linux/amd64 and linux/arm64, pushes it to ghcr.io/cognexuslabs/artzain-openshell-sidecar, runs what it pushed, then signs the digest and attests an SBOM, keyless, as that workflow at that tag.publish-npm.yml (npm Trusted Publishing, OIDC, no token) — each package’s npmjs.com binding names that one workflow file. The first publish of a new package is an owner bootstrap, because npm only binds a trusted publisher to a package that already exists; the steps live in the engine repo’s scripts/cognexus-tools-seed/APPLY.md. Do not publish from the engine repo, and never use a bare v* tag here.Nothing publishes from a developer machine (WS-8): the tag is the release, and
the workflows here hold the only credentials involved.
Security
Please see SECURITY.md.
License
Apache License 2.0. Portions of the Python guards are derived from
microsoft/agent-governance-toolkit
(MIT); notices are in LICENSE and the vendored source files.


