tiax-agent: MCP server, persistent engines, trust controls and licensing (0.8)
Claude Desktop / Claude Code / Cursor / VS Code / Windsurf / Cline / Codex / Gemini
│ MCP (stdio, or HTTP 127.0.0.1 + bearer)
▼
tiax-agent.exe (.NET 10 self-contained, official ModelContextProtocol SDK 2.2)
• argument rails → policy → licence gate → approval, in that order, on every call
• cache tools in-process (same catalog as `tiax mcp`), plus agent-only cache_query (Jint)
• script compiler (Roslyn): scripts become net48 DLLs here; the engine only loads finished DLLs
• toolsets / find_tool, MCP annotations, structuredContent, elicitation
• EngineRouter: picks the engine by project extension (.ap19 / .ap21)
│ named pipe, newline JSON, per-user ACL + shared secret
▼
tiax.exe engine / tiax-v21.exe engine (net48, Openness)
• PersistentSession: one STA thread keeps TIA Portal + the project open between calls
• runs the normal tiax verbs in-process → same backup / guard / exit-code rails
│ fallback when no engine can start
▼
tiax.exe <verb> one-shot child (0.3 behaviour)
Model and template tools (0.5, in-process)
template_list, template_show, model_validate, model_generate, model_from_io_list and
template_extract run in the agent's cache lane — no TIA, no engine. They read and write only
the workspace (model/, build/<batch>/, templates/, assumptions.md, the journal). Getting
the generated SCL into a project is a normal build_from_source call (engine, licensed, backup
first). The build_from_model prompt chains the whole flow as plan tasks.
Scripts, extensions and cache queries (0.8, Labs)
script_check and script_run compile C# in the agent with Roslyn against the net48 reference
assemblies and the project's own Openness assemblies (registry → default install folder;
TIATOOL_OPENNESS_REFS / TIATOOL_NET48_REFS override), check the guard rail and cache the DLL by
content hash (%LOCALAPPDATA%\TIATool\scripts, TIATOOL_SCRIPT_CACHE). The DLL runs through the
tiax script verb on the normal live path (engine or one-shot child): read mode inside a transaction
that is never committed or saved; write mode after a person's approval, with backup, commit on success,
save, journal and the post-write pipeline. extension_run does the same for a saved extension (mode from
its manifest); extension_save promotes a script that ran in this session. cache_query runs JavaScript
over the served cache (Jint: 5 s, 64 MB, no .NET access). The legacy net48 tiax mcp answers these
tools with "requires tiax-agent". Guide: self-extension.md.
Policy, approvals and the journal (0.8)
Policy. %ProgramData%\TIATool\policy.json (administrators; /etc/tiatool/policy.json on Linux;
TIATOOL_POLICY only where none is installed) over <workspace>\policy.json. Fields: allow_tools,
deny_tools, read_only, allow_online (default false), allow_scripts (off|read|write),
allowed_project_roots, allowed_output_roots, require_confirmation, max_writes_per_hour,
confirm_mode (elicitation|out_of_band|in_band). Checked in CallToolCore after the argument rails
and before the licence; tools refused whatever their arguments are left out of tools/list; an
invalid file refuses every write, online call and script (reads keep working). tia_status and
workspace_status show the effective values with their source. On the command line:
tiax-agent policy show [--workspace <dir> | --project <file.apXX>] [--json] # every value + the file that set it
tiax-agent policy init [--workspace <dir> | --machine] [--allow-online]
[--confirm-mode elicitation|out_of_band|in_band] [--force]
init --machine needs an elevated prompt and restricts %ProgramData%\TIATool to Administrators and
SYSTEM before writing; an existing file is replaced only with --force (timestamped copy kept).
Examples and every field: security/policy.md. The policy governs MCP calls; the
command lines are not policed by it.
Approvals. plc_download, restore_backup mode=in_place, script_run / extension_run in write
mode, openness_set, openness_invoke and whatever require_confirmation lists need a person's yes:
- elicitation (client declares it): the plan is shown as a form; the model never sees a token. 2026-07-28 clients get an input-required result whose round-trip state is HMAC-bound to the tool and arguments, expires and is single use; older clients get a server-to-client request. No answer in 10 min = no; a client that cannot ask falls back to out of band;
- out of band (otherwise, and always for the legacy
tiax mcp): the result namesAPR-XXXXXXXX; the user runstiax-agent approve <id>(shows the plan, needs a terminal andyes;--deny,--list), then the same call is repeated. Requests bind to the arguments' SHA-256, expire after 30 min, single use (%LOCALAPPDATA%\TIATool\approvals,TIATOOL_APPROVALS); - in band (the pre-0.8
confirm_tokenround trip) only underconfirm_mode: in_band.
Journal. Every entry records os_user, host, mcp_client, the approval (channel, approver) and
prev_hash (SHA-256 chain). Without a configured workspace the first write to a project creates one
under %LOCALAPPDATA%\TIATool\workspaces\<sha1(project path)> (TIATOOL_WORKSPACES), so journal and
post-write verify are on by default. tiax-agent journal verify|export [--csv] (same as tiax journal):
exit 0 intact, 2 problems, 1 no journal.
Persistent session rules
- The project stays open between calls; a call for another project closes the previous one first.
- Before a backup zip is taken the project is released (
SessionCoordinator.ReleaseProject) so the zip never contains a half-written file. - Any exception resets the session (close project, dispose Portal). A timeout kills the Portal processes the engine owns and restarts the STA thread.
- Idle timeout (
--idle-minutes, default 15) closes Portal to free the Openness licence/RAM. - The running-Portal guard ignores Portal processes owned by the engine; a UI instance still blocks.
- If the engine dies after a verb was sent, the agent reports the failure and does not retry (the write may be half-done; the backup zip is the restore point). If the engine can't be reached before the verb is sent, the call transparently runs as a one-shot child.
Engine protocol
→ {"id":1,"op":"hello","secret":"…"} ← {"id":1,"exit":0,"result":{"flavour":"V21",…}}
→ {"id":2,"op":"run","argv":["compile","C:\\p\\x.ap21"]}
← {"id":2,"log":"…"} … ← {"id":2,"exit":0}
ops: hello | ping | status | run | reset | shutdown
HTTP transport
tiax-agent mcp --http [--port 8765] listens on 127.0.0.1 only. Every request needs
Authorization: Bearer <token> (tiax-agent token, stored in %APPDATA%\TIATool\agent.json,
--rotate to change). Browser Origins other than localhost are refused (DNS-rebinding guard).
Licensing
- Token:
TT1.<b64url(claims)>.<b64url(Ed25519 sig)>, claimslic sub plan seats mid iat exp pend vplus, since 0.8, optionalfeatures[]andkid. Signed by the web backend, verified offline (BouncyCastle) against the embedded key setLicenseKeys.Production(kidmissing =k1; an unknownkidsays "update TIATool"). Vectors:tests/vectors/license-v1.json,license-v2.json. Every 0.5–0.7 token keeps working. - Plans:
monthly,yearly,trial,offline,edu,nfr,enterprise. A token withoutfeatures[]grants every paid feature; with it, writes needwrite(LicenseStatus.CanWrite).scriptsis defined forscript_run/extension_savebut not yet checked by them. expis set by the server: never past the paid period end plus the plan's grace (30 days for yearly, enterprise and offline plans, 7 otherwise). A runningmcpsession refreshes in the background (LicenseRefresher: 15-min timer, due after 24 h, back-off 5 min → 6 h; tool calls never wait for the network). Offline > 24 h = grace (still writes, warns); pastexp= every tool refuses. A token is cleared only on an explicit 403/404 from the licence's own server.- Unlicensed / expired / invalid = no tools: every MCP tool and every
tiaxverb exceptdoctor,setup,license,helpandversionrefuses with one "activate or buy" message (LicensePolicy.NoLicenseMessage:tiax-agent license activate <key>, or buy at EUR 99.99/month or EUR 999.99/year).tiax-agent doctor,init,licenseandsetupalways work. - With an active licence, the read/write split decides which feature a call needs: reads (cache and
query tools, honoured previews
dry_run/what_if,restore_backuplist / new folder,project_archive/project_retrieve,plc_compare_online,scl_lint) need no feature; anything that modifies a project needswrite. - There is no trial.
tiax-agent license trialis refused (403 no_trial). Buy a seat, thenlicense activate.license request-offline --key <K>writes atiatool.offline-request/1file for an air-gapped PC;license import <file>installs thetiatool.offline-license/1answer (verified offline, machine-bound, never refreshed).update --check [--channel stable|beta]reports a newer release; it never installs. - Machine id = SHA-256("tiatool-machine-v1:" + Windows MachineGuid)[0..16], hex. One PC per seat
(
MACHINES_PER_SEATon the server, default 1). - Servers, in order:
--server,TIATOOL_LICENSE_SERVER, the server recorded at activation, the website,https://tiatool-web.vercel.app(permanent alias). Failover on network errors, timeouts, 5xx and non-JSON answers; a JSON 4xx is final. The system proxy is used with the user's default credentials (NTLM/Kerberos); Windows credentials are never sent to the licence server. - Dev builds only (
TIATOOL_DEV_OVERRIDES):TIATOOL_LICENSE_FILE,TIATOOL_LICENSE_PUBKEY,TIATOOL_MACHINE_ID; a release ignores them. - Before release: the production public key (
npm run keygenin the web app) isLicenseManager.ProductionPublicKey(k1); further keys go intoLicenseKeys.Productionunder a newkid.
Setup
tiax-agent setup --tiax <cache> [--client <list> | --all] [--toolset <set>] [--http] [--remove] [--dry-run]
merges a tiatool server into Claude Desktop, Cursor, VS Code, Claude Code (~/.claude.json), Windsurf,
Cline, Codex CLI (config.toml) and Gemini CLI — paths and formats in INSTALL.md
(McpClientConfig). It keeps other servers and the user's own keys in our entry, drops the obsolete
env.MCP_TIMEOUT, takes a new timestamped .bak before each change, replaces the file atomically and is
idempotent. Claude Desktop and Codex always get a stdio entry. --all writes only clients installed for
the user. doctor reports per client where TIATool is registered and how long calls can finish there.
tiax setup / tiax doctor exist on the net48 CLI too.
tiax-agent init --demo builds a workspace from samples\ (ExportsSmall + model_sample) under
%LOCALAPPDATA%\TIATool\demo and registers it as tiatool-demo (DemoWorkspace).
Toolsets and find_tool
tiax-agent mcp --toolset explore|generate|commission|admin|extend|all (comma list; also TIATOOL_TOOLSET;
default all) filters only what tools/list shows; every catalog tool stays callable. The map lives in
one place, src/TiaTool.Agent/Toolsets/ToolsetMap.cs (ToolsetTests fails for an unmapped tool):
| Toolset | For | tools/list size (0.8.0, ≈ chars/4) |
|---|---|---|
| explore | read-only users: cache queries, export/scan, audit, diff, docs | 28 tools, ~9.1k tokens |
| generate | templates, model, generators, build/import, compile | 40 tools, ~16.2k tokens |
| commission | compile, download, PLCSIM, online state, verify gate, restore | 30 tools, ~10.8k tokens |
| admin | workspace bookkeeping, backups, languages, clean-up | 16 tools, ~5.8k tokens |
| extend | self-extension: Openness explorer, scripts, extensions, cache queries, learned knowledge | 14 tools, ~7.9k tokens |
| all | everything | 75 tools, ~31.4k tokens |
(Measured by ToolsetTests.CatalogTokenBudget_PerToolset with online tools allowed; the default policy
also hides plc_download, plc_online and plc_compare_online.)
find_tool {query, toolset?, limit?} is always listed (cache lane): it ranks the FULL catalog by
name and description and answers one line per tool — name(required, optional?) — first sentence [status] {toolsets} (not listed in this session).
CI front-end: analyze
tiax-agent analyze audit|diff|verify runs ProjectAudit, ProjectDiff and VerifyGate on .tiax
caches or SIMATIC ML export folders (scanned into a temp cache) — Linux or Windows, no TIA, no licence.
--format json|sarif|junit|md (comma list + --out <folder>), --fail-on error|warning|note|none,
--source-root (SARIF URIs relative to it), audit --baseline (only new findings fail). Exit 0 ok,
1 findings at/above --fail-on, 2 usage/input error (never 1 for a crash). SARIF 2.1.0: one rule per
audit rule / diff kind / verify check, partialFingerprints, block (and network) logical locations, the
exported XML as physical location (project-level results point at the export folder). JUnit: one
testcase per rule/check. Verify: a failed or INCONCLUSIVE check is an error.
Composite action: .github/actions/tiatool-analyze (needs a Linux tiax-agent via agent-path or
agent-url; uploads SARIF with github/codeql-action/upload-sarif); example workflow:
docs/ci/github-actions-example.yml.
Logs, support bundle, privacy
Rolling log tiax-agent.log (+ .1….4, 1 MB each) in %LOCALAPPDATA%\TIATool\logs
($XDG_STATE_HOME/~/.local/state/TIATool/logs elsewhere; TIATOOL_LOG_DIR, TIATOOL_NO_LOG=1):
start line, everything the agent writes to stderr, and each live call's verb, exit code and duration
(no arguments). support-bundle zips doctor, versions, redacted agent/licence config, our entry of each
client config (never other servers'), logs, policy and selftest overlay; Redactor masks licence
keys, TT1 tokens, bearer/tt_ tokens, secret JSON keys, e-mails, and user/machine names (also inside
C:\Users\…, /home/…). privacy prints every endpoint; every ProductEndpoints member must be
classified in PrivacyReport.Purposes (SupportTests).
Built from docs/agent.md in the product repository. Links to files that are not published here are shown as plain text.