Back to Nodes

@cognexuslabs/n8n-nodes-artzain

v0.1.8
Last updated Nov 3, 2026

n8n nodes for the ArtzAIn Decision API and envelope proxy. Fail closed on HTTP 503. review does not wait inside n8n.

296 Weekly Downloads
640 Monthly Downloads

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 | ![PyPI version](https://pypi.org/project/artzain/) ![PyPI downloads](https://pypi.org/project/artzain/) | Python SDK — local guards, decide(), CLI (login, quickstart, init, audit, policy, registry) |
| @cognexuslabs/artzain | npm i @cognexuslabs/artzain | ![npm version](https://www.npmjs.com/package/@cognexuslabs/artzain) ![npm downloads](https://www.npmjs.com/package/@cognexuslabs/artzain) | TypeScript SDK — remote-only Node client (decide, events, identity) |
| @cognexuslabs/openclaw-artzain | npm i @cognexuslabs/openclaw-artzain | ![npm version](https://www.npmjs.com/package/@cognexuslabs/openclaw-artzain) | OpenClaw beforetoolcall plugin (deny / review / errors block; not /approve) |
| @cognexuslabs/n8n-nodes-artzain | npm i @cognexuslabs/n8n-nodes-artzain | ![npm version](https://www.npmjs.com/package/@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 | ![npm version](https://www.npmjs.com/package/@cognexuslabs/grokbot-artzain) | Grok Bot cooperative Decision skill, opt-in announce, and pull enroll (no host intercept) |

All five are Apache-2.0 ![License](https://pypi.org/project/artzain/). 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 artzain

d = 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": , "arguments": {…}} |
| 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 artzain

msg = 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 artzain

resp = 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/decisions with Allow / Review / Deny outputs. HTTP 503 fails closed. review is a third output, not an n8n Wait node.
  • ArtzAIn Envelope — POST /api/v1/envelope/v1/chat/completions with 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-.sh 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: push tag 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.
  • TypeScript: push tag 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 plugin: push tag openclaw-v (must match openclaw/package.json).
  • n8n nodes: push tag n8n-v (must match n8n/package.json).
  • Grok Bot skill: push tag grokbot-v (must match grokbot/package.json).
  • OpenShell sidecar image: push tag 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.
  • All four npm packages publish through the same 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.

From our network Other tools and sites we build