CMLabs · Psyclone AIOS

# 13. PsySpec XML Reference

Every PsySpec element and attribute, with an honest status badge for each: what ships, what is parsed but inert, and what is still roadmap.

## How to read this chapter

This is the lookup chapter, extracted from the current parser code. Each element carries a badge:

- Shipped — parsed and functionally wired.
- Stub — parsed into internal structures but not (or only partially) acted on.
- Roadmap — documented/reserved syntax; no real parsing or behaviour yet.

## Status summary

Element | Status | Notes
--- | --- | ---
`<psyspec>` | Shipped | Root tag; new SSL policy attributes
`<include>` | Shipped | XML file inclusion
`<variable>` | Shipped | `%var%` substitution + new cmdline override
`<node>` | Shipped | Multi-node topology; per-node `<library>`
`<library>` | Shipped | DLL/.so registration
`<space>` | Shipped | Only `type="external"` handled at top level
`<interface>` | Shipped | Auth/encryption children stubbed; new SSL attrs
`<service>` | Stub | Partial: custom services have a NULL receiver
`<psyprobe>` | Shipped | Port / location / alias / post
`<module>` | Shipped | Per-module verbose/debug/logfile now wired
`<whiteboard>` | Shipped | `root=` persistence is parsed-only
`<catalog>` | Shipped | File/Data/Replay + CCM/TimeSeries families
`<stream>` | Stub | Component created, backing function empty
`<cli>` | Shipped | Recipe bake step: runs a command line, `retryable` opt-in
`<interactive>` | Stub | Grammar accepted & validated; keeps the bake alive, but streaming terminal I/O ships in 2.7
`<feed>` | Roadmap | Log-only branch, never becomes a component
`<supervisor>` | Roadmap | Log-only branch
`<monitoring>` / `<performance>` | Roadmap | “Not yet implemented” log
`<group>` | Roadmap | Log-only branch
`<trigger>` | Shipped | Filters, interval, context triggers
`<filter>` | Shipped | 7 comparison types + maxage
`<triggergroup>` | Shipped | Declarative multi-trigger join (all/N-of-M, time windows, snapshot, etc.)
`<triggers>` | Stub | Recognised, silently does nothing
`<retrieve>` | Shipped | Whiteboard/catalog retrieval spec
`<query>` | Shipped | Incl. remote host/port queries
`<retrieves>` / `<posts>` / `<signals>` | Stub | Empty branches
`<crank>` | Shipped | Function / script / inline / external
`<post>` | Shipped |
`<signal>` | Shipped | Minimal: name + type only
`<context>` (in module) | Shipped | Groups triggers under a named context
`<parameter>` | Shipped | Typed values incl. ranges/lists/random
`<setup>` / `<key>` / `<customview>` | Shipped |
`<recording>` / `<playback>` | Stub | Parameters stored, not acted on
`<executable>` | Stub | Cmdline stored, never launched
`<alias>` (in psyprobe) | Shipped |
`<authentication>` (in interface) | Stub | Parser body empty
`<phase>` | Roadmap | No parsing code at all
`system=` trigger attribute | Roadmap | Use `context=` instead

## <psyspec> (root) Shipped

The root element of every spec. Besides `port=` (main system port, see System Guide), it now carries the global SSL client-verification policy:

Attribute | Meaning | Default
--- | --- | ---
`allowselfsigned` | New. `yes`/`true`/`1` loosens verification (accepts self-signed certificates) for all SSL client connections. Per-interface value overrides either way. | secure — verify peer
`cafile` | New. Path to a CA PEM file used for SSL verification instead of the OS trust store. | OS trust store
`capath` | New. Directory of CA certificates, same mechanism as `cafile`. | OS trust store

Note. Top-level `verbose`/`debug`/`logfile` on the root element are Roadmap — only per-component logging attributes are wired. System-wide levels are set on the command line.

Children: `<include>`, `<variable>`, `<node>`, `<library>`, `<space>`, `<interface>`, `<service>`, `<psyprobe>`, `<whiteboard>`, `<catalog>`, `<module>`, `<stream>`, plus the roadmap tags listed above.

## <include> Shipped

`<include file="system.xml" />` splices the referenced file's contents into the spec (like a C include) and reparses; errors in the expanded spec report line/column. `file` is required. Includes and variables are processed before anything else.

## <variable> Shipped

`<variable name="SystemID" value="2" />` defines a literal `%SystemID%` text substitution applied over the whole spec before XML parsing — usable in names, cranks, triggers, anywhere, as long as the result stays valid XML. Both `name` and `value` are required.

Tip. New: a `name=value` argument on the Psyclone command line overrides the spec default — `Psyclone spec=robot.xml SystemID=3`. Reserved parameters (`spec=`, `port=`, `html=`, `verbose=`, `debug=`, per-topic variants) are respected and not treated as variables.

## <node> Shipped

Attribute | Meaning | Default
--- | --- | ---
`name` | Node name; `Main` (or no/local address) reconfigures the local node | —
`addr` / `address` | IP or hostname (`addr` preferred, `address` accepted) | local
`type` | Node type string (`adhoc`/`java` types not implemented Roadmap) | —
`port` | Port; for the local node a differing port adds Main/PsyProbe/Console interfaces on it | 0
`timeout` | Connect timeout (ms) | 0

Child: `<library name library path>` — libraries loaded only on that node.

## <library> Shipped

`<library name="MyExamples" library="Examples" path="../libs" />`. `name` is how cranks refer to it (`MyExamples::Ping`); `library` is the file name, resolved as `Examples.dll` (Windows) / `libExamples.so` (UNIX) — debug builds first try the `...Debug` variants; `path` is an optional directory; `node` restricts to a node.

## <space> Shipped

`<space name="GUISpace" type="external" />`. At the top level only `type="external"` is handled (creates a process-map entry waiting for an external program to connect). Regular spaces are created implicitly by the `space=` attribute on components (default `Root`). `node` restricts to a node. See Distributed Systems.

## <interface> Shipped

    <interface name="MyWebInterface" node="Node0" port="1234" protocol="HTTP"
               service="MyWebService" timeout="3000" default="yes" />

Attribute | Meaning | Default
--- | --- | ---
`name` | Interface name (required; parse fails without) | —
`service` | Target service name (required) | —
`node` | Node the interface lives on | any/local
`port` | TCP/UDP port (required) | —
`protocol` | `Message` | `HTTP` | `Telnet` | `RAW` (anything else = parse failure) | unset
`ip` | `UDP` selects UDP; otherwise TCP | TCP
`timeout` | Timeout ms | unset
`default` | `yes` = default interface on a shared port | no
`allowselfsigned` | New. Per-interface SSL verification override; wins over the global setting either way | inherit global
`cafile` / `capath` | New. Per-interface CA file/directory override | inherit global

Child `<authentication>`: Stub — parsed by an empty function body; interface encryption attributes are likewise stubbed.

## <service> Stub (partial)

Attributes: `name`, `rootdir`, `subdir`, `timeout`, `node`. The service is registered and a component is created for its subscription, so `<trigger>`/`<post>`/`<crank>` children parse — but custom services register a NULL receiver, so inbound requests are discarded. Built-in PsyProbe/Console services work. See Services & Interfaces.

## <psyprobe> Shipped

Attribute | Meaning | Default
--- | --- | ---
`defaultport` | Any value other than `no` removes PsyProbe from the main port | shares main port
`port` | Adds a dedicated PsyProbe HTTP interface on this port | —
`location` | Web root directory for PsyProbe | built-in

Children: `<alias name location default>` (URL alias to a local directory with optional default file) and `<post>` (same attributes as a module post plus XML content, registered as a clickable manual post in PsyProbe).

## Components: <module> / <whiteboard> / <catalog> / <stream>

All component tags share one parser; the tag name selects the type. `<module type="external">` declares an external process component (connects via a space). `<module count=N>` tag-instancing is Roadmap. Common attributes:

Attribute | Meaning | Default
--- | --- | ---
`name` | Component name (required) | —
`tag` | Appends `_tag` to the name; `*` uses the component's own name as tag | none
`node` | Node assignment; modules also accept `*` = every node | config node
`space` | Process space; Python-crank modules auto-get their own space | Root
`selftrigger` | `allow` | `warn` | anything-else = none | warn
`logfile` | New — now wired. Per-module log file | node log
`verbose` / `debug` | New — now wired. Per-module verbosity/debug levels (0–9) | node levels
`migration` | `yes` flags the component movable; runtime migration itself is parsed-only Stub | no
`priority` | uint8 stored; scheduling effect parsed-only Stub | 0

Extra attributes for whiteboard/catalog/stream/service:

Attribute | Meaning | Default
--- | --- | ---
`function` / `type` | Backing crank `Lib::Func`; bare names get the `PsySystem::` prefix. Defaults: whiteboard→`PsySystem::Whiteboard`, stream→`PsySystem::Stream`; a catalog/service with neither is an error. Catalog `type=` selects the family: File/Data/Replay/RequestStore and the CCM & TimeSeries families are Shipped; the `Media` and `Google` catalog types are Stub — do not rely on them. | see left
`root` | Storage root directory (whiteboard `root=` persistence is parsed-only Stub) | —
`subdir` / `ext` | Subdirectory / file extension | —
`operation` | Default operation | —
`key` + `keytype` | Adds an index key (repeatable via `<key>` children) | time index
`timeout` / `interval` | ms | 0
`maxcount` / `maxsize` | Max entries / max bytes | 0
`rotate` | `yes`/`no` rotation (replay looping) | no

### Component children

Child | Status | Meaning
--- | --- | ---
`<recording root filesize overwrite>` | Stub | Stores recording parameters; not acted on per-module
`<playback root interval>` | Stub | Stores playback parameters; not acted on
`<executable consoleoutput autorestart>cmdline</executable>` | Stub | Command line stored but never launched (roadmap: auto-start external processes)
`<customview name template>` | Shipped | Registers a PsyProbe custom view; also accepts an inline XML/text body instead of a `template=` file
`<setup>` | Shipped | Free-form XML/text blob, read via `getParameterString("componentsetup")`; `file=` variant supported
`<key name type>` | Shipped | Extra index keys
`<parameter>` | Shipped | See below
`<context name>` | Shipped | See below

## <parameter> Shipped

`<parameter name=".." type="String|Integer|Float" value=".." interval=".." />` (type defaults to String). Rich value syntax:

Form | Example | Meaning
--- | --- | ---
Plain value | `value="10"` | Fixed initial value
Range | `value="[0:110]"` | Bounded range, initial = lowest
Stepped range | `value="[0:110:2]"` | Step 2; step can also come from `interval="2"`
Range + initial | `value="[0:110]=55"` | Initial value 55
Random initial | `value="[0:110]=*"` | Random initial within the range
Lists | `value="[-2.5;1.5;1.8]"`, `value="['a';'b';'c']"` | Enumerated numeric or string choices

All parameters are queryable, settable and tweakable at runtime (`getParameterString/Int`, `setParameter`, `tweakParameter`, `resetParameter`) — see Modules.

## <context> (in a component) Shipped

`<context name="SoB.Alive.Awake">...</context>` groups child trigger/post/retrieve/query/crank/signal elements under a named context instead of the default `Psyclone.Ready`. See Contexts.

## <trigger> Shipped

Subscribes the component to messages; a match dispatches its cranks. Valid if it has a `name` plus at least one of `type`, `from`, `to`; duplicate names are an error.

Attribute | Meaning | Default
--- | --- | ---
`name` | Trigger name (required, unique) | —
`type` | Message type to match (wildcards allowed: `input.audio.*`) | —
`from` / `to` | Only messages from this component / addressed to this component | any
`interval` | ms — makes it a timer trigger (needs one real message first to start) | 0
`delay` / `after` | ms delay before firing (`after` is an alias) | 0
`context` | Fire on context change to this context (forces type `CTRL_CONTEXT_CHANGE`) | —
`maxage` | ms — becomes a MAXAGE filter | ∞
`tag` | Match tag; `*` = component's own name | any

Trigger `priority`/`history` and the `system=` attribute are Roadmap — use `context=` to react to context changes.

### <filter> (child of trigger) Shipped

    <filter type="haskey" key="SomeKey" />                      <!-- entry exists (any type) -->
    <filter type="equals" key="camera" value="front-left" />    <!-- string ==, exact, case-sensitive -->
    <filter type="notequals" key="state" value="disabled" />    <!-- string != -->
    <filter type="equalsnumeric" key="mode" value="3" />        <!-- float == -->
    <filter type="notequalsnumeric" key="channel" value="0" />  <!-- float != -->
    <filter type="greaterthan" key="temp" value="80" />         <!-- float > (≥ at the epsilon boundary) -->
    <filter type="lessthan" key="pressure" value="2.5" />       <!-- float < (≤ at the epsilon boundary) -->

`type` is one of `equals` (exact, case-sensitive, no wildcards), `notequals`, `equalsnumeric`, `notequalsnumeric`, `greaterthan`, `lessthan` (all take key+value; numeric values are parsed as float64 with an epsilon compare, so exactly-equal values also pass greaterthan/lessthan) and `haskey` (key only, matches any entry type). The eighth filter operation, `maxage`, is written as a trigger attribute (milliseconds) rather than a `<filter>` child. Unrecognised types are silently ignored.

## <retrieve> Shipped

On-demand fetch from a whiteboard/catalog when the owning trigger fires (see Whiteboards).

Attribute | Meaning | Default
--- | --- | ---
`name` | Required, unique | —
`source` | Component to retrieve from (required) | —
`type` / `from` / `to` | Type / original sender / addressee filters | any
`maxcount` | Max messages (0 = all) | 0
`maxage` | Age window, stored ×1000 (exact wall-clock unit under review — see Whiteboards) | ∞
`tag` | Tag filter; `*` = own name | any
`key` / `keytype` | Index key; keytype `string`|`integer`|`float`|`time` (invalid drops the spec) | `time`
`start` / `end` | Range bounds, interpreted per keytype | open

## <query> Shipped

Free-form query to a catalog/component; catalogs dispatch on the Query/Operation fields. Attributes: `name` (required, unique), `source` (required), `type` (string), `subdir`, `ext`, `binary` (`yes`), `maxcount`, `maxage` (stored ×1000; unit under review), `key`, `value`, `operation`, and for remote queries `host` (DNS-resolved at parse time) + `port`.

## <crank> Shipped

The processing function attached to triggers. Four forms:

    <crank name="c1" function="lib::function" />            <!-- C/C++ library crank -->
    <crank name="c2" language="python" script="file.py" />  <!-- external script file -->
    <crank name="c3" language="python">inline code</crank>  <!-- inline script -->
    <crank name="c4" />                                     <!-- external crank slot -->

A function without `lib::` is an internal (built-in) crank. Duplicate crank names across components are rejected. Modules with no crank get the built-in passthrough (`Internal::Simple`) default crank. Java cranks are Roadmap.

## <post> Shipped

Message posted when the owning trigger fires. Valid if it has a `name` and (`type` or `context`).

Attribute | Meaning | Default
--- | --- | ---
`name` | Required, unique | —
`type` | Type of the posted message | —
`context` | Post a context change to this context | —
`to` | Address to a specific component (also still delivered to subscribers) | broadcast
`ttl` | Seconds (stored ×1000 ms); 0 = no expiry | 0
`guaranteed` | `no` marks the message non-guaranteed | guaranteed
`tag` | `*` = own name | none

Message content comes from child XML elements; the older `contentkey`/`content` attribute form is no longer parsed. `time="trigger"` is Roadmap.

## <signal> Shipped (minimal)

`<signal name="Output" type="My.Signal.1" />` — both attributes required; no others are parsed. See Messaging & Signals.

## <triggergroup> declarative join Shipped

Joins several triggers into one crank activation: member messages sharing the same tag are buffered on the node until the group completes, then delivered together as one set (wakeups ≈ joins, not one per member). Untagged messages group under the literal empty-string tag `""` and never mix with real tags. This is the primitive for multi-modal fusion (audio + text + vision joined before a decision crank runs).

    <module name="Simple" node="Node0">
      <triggergroup name="ts" count="2" maxage="2000">
        <trigger name="t1" type="bla1" />
        <trigger name="t2" type="bla2" />
        <trigger name="t3" type="bla3" optional="true" maxage="200" />
      </triggergroup>
      <crank name="c1" function="lib::function" />
      <post name="p1" type="bla4" to="WB/Stream/Cat" />
    </module>

Attribute | Meaning | Default
--- | --- | ---
`name` | Required, unique within the module. A duplicate group name discards the extra group with a level-1 warning; a group missing `name` or `maxage` is skipped with a warning. | —
`maxage` | ms — required, no default. A partial set living longer than this is evicted. | —
`count` | Fire when any N members are present (`"all"` = every named member). For N-of-M, any N complete the set; members arriving after the set closes are discarded. | `all`
`min` | Fire at N members, then re-fire as more of the same tag-set arrive (growing set). | —
`within` | ms — all required members present and their post times (send time) within the window. | —
`time` | ms — as `within` but on content times (created time). `time="0"` = identical content time. | —
`mode` | `snapshot` — fire on the primary trigger (first child, or `primary="true"`), attaching the latest value of each other member: no wait, no tag match. | —
`slide` | Sliding-window correlation (with `within=`): re-emit a fresh joined set each time a member updates while the window still holds. | off
`debounce` | ms — coalesce fast member streams: fire at most once per N ms with the latest set. | —
`dup` | On a duplicate member before completion: `latest` (latest-wins) or `keep` (queue all). | `latest`
`order` | `strict` — only fire if members arrived in declared order (pipeline-stage joins). | off
`timeout` / `ontimeout` | On `timeout="<ms>"` (or `maxage`) expiry with an incomplete set: `ontimeout="fire"` partial-fires (with a level-1 warning) or `"discard"` drops the set. | `fire`

Child `<trigger>` elements accept all their normal attributes (they still route individually) plus:

Attribute | Meaning
--- | ---
`optional="true"` | Member is not required for completion; included in the delivered map if present.
`maxage` (per member) | A member older than its own freshness bound does not count toward completion.
`primary="true"` | Marks the primary trigger for `mode="snapshot"` (defaults to the first child).

SDK. A completed (or partial-fired) set arrives as one grouped activation. The crank calls `waitForNewMessageGroup(ms, groupName)`, which returns a `std::map<std::string, DataMessage*>` keyed by trigger name — optional/absent members are simply not in the map (check with `.count("t3")`). `getCurrentTriggerName()` returns the group name. Messages are owned by the API for the activation and freed on the next `waitForNewMessageGroup()`/`waitForNewMessage()` call. Existing single-message cranks are untouched.

## Stub grouping elements Stub

`<triggers>`, `<retrieves>`, `<posts>` and `<signals>` are recognised by the parser but their handling bodies are empty — they silently do nothing.

## Roadmap-only elements Roadmap

`<feed>`, `<supervisor>`, `<monitoring>`, `<performance>` and `<group>` are logged and ignored; `<phase>` has no parsing code at all. None of these produce components or behaviour in the current build — do not rely on them.

## Cross-cutting rules

- Config-node scoping: elements without `node=` run on the config node; with `node=` only on the named node.
- Expansion order: variables and includes are processed before all other parsing; the spec is reparsed after each expansion.
- Code escaping: inside the spec, message content and setup blobs must remain valid XML.

## Recipes: `<cli>` and `<interactive>` Shipped

A recipe is this same PsySpec grammar, but submitted to a running node's Builder to be executed step-by-step at runtime instead of only at startup. It goes through the identical parse path, so `<include>` expansion and `%variable%` substitution behave exactly as documented above. Two elements exist only for recipes; everything else is the component grammar you already know.

The procedural step tags — the elements a bake actually executes, in document order — are `<feed>`, `<whiteboard>`, `<stream>`, `<catalog>`, `<service>`, `<module>` and `<cli>`.

Optional recipe header. On the root element, `version="major[.minor]"` (default `1.0`) and `capabilities="flagA,flagB"`. Unknown elements, attributes and capability flags are accepted and ignored, so an older Psyclone still bakes what it understands from a newer recipe. The one fatal case is a declared major version newer than the node supports — that fails the parse cleanly rather than half-baking.

### <cli> Shipped

Runs a command line as a bake step. Live stdout/stderr is streamed back to subscribers as interim progress (see below), and the step's return code is recorded in the bake's step log.

    <cli name="Fetch" retryable="yes">git pull</cli>

Attribute | Meaning | Default
--- | --- | ---
`name` | Step name, used in progress messages and the step log | —
`retryable` | `"yes"`/`"true"` declares the step safe to re-execute. Opt-in per step: a RETRY control verb aimed at a step that has not opted in is refused with a typed step-log entry. This deliberately protects side-effecting commands from non-idempotent re-runs. | `no`

Note. CLI steps are authorised before execution; a command that fails authorisation never runs. Per-step live output is kept in a ring of the most recent 32 lines and is never carried across a step boundary.

### <interactive> Stub

Makes the bake stay alive after its last procedural step, awaiting data from the sender, instead of finishing. The bake ends when the sender releases it (a TERMINATE verb) or when the idle timeout expires.

Status. The grammar below is accepted and validated today (placement, count and `idletimeout` rules are all enforced, and the keep-alive/timeout/subscriber-gate behaviour is real), but bidirectional terminal I/O — pushing stdin to the session and receiving ordered command output — ships in phase 2.7. Until then an interactive section keeps the bake alive without a data channel.

    <psyspec name="Session">
      <whiteboard name="WB" />
      <cli name="Prep">./prepare.sh</cli>
      <interactive idletimeout="120000" />
    </psyspec>

Rule | Behaviour
--- | ---
How many | At most one, and it must be a direct child of the recipe root. A second one fails the parse with a typed error.
Where | It must be the trailing section — no procedural step may follow it. A misplaced one fails the parse.
`idletimeout` | Milliseconds, must be a positive integer. Default 600000 (10 minutes). `idletimeout=""` is treated as malformed, not as “use the default”.
Absent | Bake is non-interactive and finishes normally — every pre-existing recipe behaves exactly as before.

Rejected at submit, not mid-bake. An interactive recipe submitted when no subscriber is listening to `Psyclone.BakeProgress` is refused immediately. Only a sender can release such a bake, so with nobody attached it would sit idle until the timeout — failing now is the whole point of declaring the section.

### Watching and steering a bake

Three message types carry the conversation, all correlated by bake-ID. The bake-ID is in the message `reference` field and written as a `bakeid` user entry, because `<filter>` matches named user entries rather than header fields:

Type | Direction | Carries
--- | --- | ---
`Psyclone.BakeProgress` | bake → subscriber | `kind` = `start` / `step` / `interim` / `final`
`Psyclone.BakeControl` | sender → bake | the control verbs below
`Psyclone.BakeDataRequest` | bake → sender | a request for data by key

    <trigger name="BP" type="Psyclone.BakeProgress">
      <filter type="haskey" key="bakeid" />
    </trigger>

Control verbs: `start`/`resume`, `pause`, `terminate`, `skip`, `retry`. `PAUSE` parks the loop with a safety cap of 300000 ms, after which the configured expiry action applies — TERMINATE (default) or RESUME. `TERMINATE` takes effect immediately, including killing an in-flight `<cli>` child rather than waiting for the step to end. `SKIP` and `RETRY` act on the live step cursor.

Progress under load. Each bake holds a bounded send queue (high-water 64). Framing messages (`start`/`step`/`final`) are protected and never dropped; `interim` %-progress is coalescable, so under pressure the newest interim replaces the previous trailing one and you always keep the latest percentage. If the queue stays full for 5 seconds the subscriber is judged gone: a non-interactive bake terminates, an interactive one pauses so a human can resume.
Ordering caveat. Progress is delivered through shared memory and drained by a subscriber crank polling on its own schedule, so the order messages are recorded is not guaranteed to match the order they were published. The guarantee is that the latest interim is not discarded — not that it arrives last. Do not write consumers (or tests) that assume arrival order.
PreviousPsyProbe NextTutorials