The Command framework lets you declare what arguments a command accepts once, as a class attribute. That declaration drives CLI tab-completion, web UI input forms, and server-side payload validation — with no per-command client code required.
Overview
#A Command is a fluent builder that produces a structured argument schema. Declare Command instances as class attributes on any plugin class and the framework auto-discovers them at class-creation time via __init_subclass__.
| Plugin type | How schemas are declared | Auto-discovery |
|---|
| Agent package (built-in commands) | Command class attributes on AgentPackageBase subclass | Yes — __init_subclass__ |
| Agent module (Python wheel package) | Command class attributes on AgentModuleBase subclass | Yes — __init_subclass__ |
| Agent module (YAML manifest) | options_schema block — auto-converted | N/A (manifest parsed at load) |
| Tool plugin (agentless module) | Command class attributes on AgentlessModuleBase subclass | Yes — __init_subclass__ |
Regardless of source, all schemas end up in the same wire format served from the capabilities API endpoint.
The Command Builder
#Both Command and its backward-compatible alias CommandSchema live in tantoc2.server.command_schema.
1
| from tantoc2.server.command_schema import Command, AgentFilter, ModuleFilter
|
Constructor
#1
| Command(name, description="")
|
Fluent argument methods
#1
2
| .arg(name, *, type="str", desc="", required=_SENTINEL, default=_SENTINEL,
choices=None, choices_from=None, positional=True)
|
Add a primitive argument. Smart defaults:
- If
default is provided (even None), required defaults to False. - If no
default is given and required is not explicit, required defaults to True. positional=True by default; arguments are auto-numbered starting at 0.
1
| .path(name, *, desc="", required=_SENTINEL, default=_SENTINEL)
|
Shorthand for .arg(name, type="path", ...) — a remote filesystem path.
1
| .file(name, *, desc="", required=_SENTINEL, default=_SENTINEL)
|
Shorthand for .arg(name, type="file", ...) — a file to upload. Defaults to required=True.
Entity methods
#Entity arguments let clients look up live server data for completion and form rendering.
1
2
3
4
| .agent(name, *, desc="", required=_SENTINEL, filter=None)
.listener(name, *, desc="", required=_SENTINEL, filter=None)
.module(name, *, desc="", required=_SENTINEL, filter=None)
.credential(name, *, desc="", required=_SENTINEL, filter=None)
|
Entity methods default to required=True. The filter parameter takes a list of filter enum values.
Filter enums
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from tantoc2.server.command_schema import AgentFilter, ListenerFilter, ModuleFilter, CredentialFilter
class AgentFilter(StrEnum):
ACTIVE = "active"
RELAY_CAPABLE = "relay"
SESSION_MODE = "session"
BEACON_MODE = "beacon"
class ListenerFilter(StrEnum):
RUNNING = "running"
STOPPED = "stopped"
EXTERNAL = "external"
NON_EXTERNAL = "non_external"
class ModuleFilter(StrEnum):
COMPATIBLE = "compatible"
LOADED = "loaded"
|
Argument Types
#Primitive types
#| Type | Description |
|---|
"str" | Free-form string (default) |
"int" | Integer — coerced from string before validation |
"float" | Floating-point number |
"bool" | Boolean — accepts "true", "1", "yes", "on" / "false", "0", "no", "off" |
Path types
#| Type | Description |
|---|
"path" | Filesystem path on the agent (remote) |
"local_path" | Filesystem path on the operator machine — CLI offers tab-completion |
"file" | A file to be read and uploaded — web UI renders a file picker |
Entity types
#Entity types cause clients to fetch live data for completion and validation. Server-side entity existence is checked when the task is dispatched, not during payload validation.
| Type | Description |
|---|
"agent" | An agent ID from the current engagement |
"listener" | A listener ID from the current engagement |
"module" | An agent module name from the registry |
"credential" | A credential ID from the current engagement |
For Agent Package Developers
#Class-attribute pattern (recommended)
#Declare Command instances as class attributes on your AgentPackageBase subclass. __init_subclass__ scans for them automatically — no need to override command_schemas() or built_in_commands().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| from tantoc2.server.agent_package import AgentPackageBase
from tantoc2.server.command_schema import Command
class MyAgentPackage(AgentPackageBase):
# No-argument commands
kill = Command("kill", "Terminate the agent process")
whoami = Command("whoami", "Print current user identity")
pwd = Command("pwd", "Print working directory")
ps = Command("ps", "List running processes")
# Positional path argument, optional with default
ls = Command("ls", "List directory contents") \
.path("path", desc="Directory path to list", default=".")
# Required positional path
cat = Command("cat", "Display file contents") \
.path("path", desc="File path to read")
# Named (flag-style) optional arguments
beacon_config = (
Command("beacon_config", "Update beacon interval and jitter")
.arg("interval", type="int", desc="Beacon interval in seconds",
required=False, positional=False)
.arg("jitter", type="int", desc="Beacon jitter percentage (0-100)",
required=False, positional=False)
)
# String with static choices
output_format = Command("output_format", "Set output format") \
.arg("format", type="str", desc="Output format", default="text",
choices=["text", "json", "csv"])
|
After declaring these, built_in_commands() and command_schemas() return the auto-discovered results. Do not redeclare management commands (upload, download, load, unload) — the teamserver merges them automatically.
Reference implementation: Dev agent package
#Source: dev_agent/src/tantoc2_dev_agent/package.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| class DevAgentPackage(AgentPackageBase):
kill = Command("kill", "Terminate the agent process")
survey = Command("survey", "Collect system survey information")
pwd = Command("pwd", "Print current working directory")
whoami = Command("whoami", "Print current user identity")
ps = Command("ps", "List running processes")
netstat = Command("netstat", "List network connections")
ls = Command("ls", "List directory contents").path(
"path", desc="Directory path to list", default="."
)
cat = Command("cat", "Display file contents").path(
"path", desc="File path to read"
)
cd = Command("cd", "Change working directory").path(
"path", desc="Directory to change to"
)
env = Command("env", "List or get environment variables").arg(
"name", type="str",
desc="Variable name (optional, lists all if omitted)",
default=None,
)
beacon_config = (
Command("beacon_config", "Update beacon interval and jitter")
.arg("interval", type="int", desc="Beacon interval in seconds",
required=False, positional=False)
.arg("jitter", type="int", desc="Beacon jitter percentage (0-100)",
required=False, positional=False)
)
# command_schemas() is auto-discovered — no override needed.
|
Entity-type arguments
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Agent selector — only show active relay-capable agents
connect = Command("connect", "Relay through an agent") \
.agent("relay_agent", desc="Relay agent to use",
filter=[AgentFilter.ACTIVE, AgentFilter.RELAY_CAPABLE])
# Listener selector — only running listeners
bind = Command("bind", "Bind to a listener") \
.listener("listener_id", desc="Listener to bind to",
filter=[ListenerFilter.RUNNING])
# Module selector — only compatible modules
load = Command("load", "Load a module") \
.module("module_name", desc="Module to load",
filter=[ModuleFilter.COMPATIBLE])
# Credential selector
auth = Command("auth", "Authenticate using credential") \
.credential("cred", desc="Credential to use")
|
Dynamic choices via choices_from
#The choices_from field names a live data source resolved client-side:
1
2
3
4
5
6
| Command("relay", "Use relay agent").arg(
"relay_agent",
type="agent",
desc="Agent to relay through",
choices_from="agents",
)
|
Positional vs flag arguments
#positional | CLI behaviour | Web UI behaviour |
|---|
True (default) | Positional by auto-incremented index | Ordered field without label prefix |
False | --name value flag | Labelled text/checkbox field |
Positional arguments are ordered by position (0-based, auto-assigned).
For Agent Module Developers (Python wheel packages)
#Class-attribute pattern
#Use the same Command class attributes on your AgentModuleBase subclass:
1
2
3
4
5
6
7
8
9
10
11
| from tantoc2.server.agent_module_base import AgentModuleBase
from tantoc2.server.command import Command
class SysinfoModule(AgentModuleBase):
name = "sysinfo"
description = "Collect detailed system information"
format = "py"
payload_file = "sysinfo.py"
sysinfo = Command("sysinfo", "Collect system info") \
.arg("verbose", type="bool", desc="Include extended details", default=False)
|
__init_subclass__ auto-discovers the sysinfo command. command_schema() and all_command_schemas() return the results.
YAML manifest options_schema (legacy)
#For YAML-manifest modules, the options_schema block is automatically converted to a Command when the manifest is loaded. This path does not support positional or entity-type filters; use the Python class approach for those features.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| name: port_scan
description: TCP port scanner
options_schema:
target:
type: str
description: Target IP or CIDR range
required: true
ports:
type: str
description: "Port range (e.g., 1-1024 or 80,443)"
required: false
default: "1-1024"
timeout_ms:
type: int
description: Connection timeout in milliseconds
required: false
default: 500
|
Supported fields per option: type, description, required, default, choices, choices_from.
For Tool Plugin Developers
#Class-attribute pattern (recommended)
#Declare Command instances as class attributes and implement handle_<operation> methods. The base class auto-discovers commands and auto-dispatches execute() to the correct handler.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| from tantoc2.server.agentless_base import AgentlessModuleBase, AgentlessResult, AgentlessTarget
from tantoc2.server.command_schema import Command
class WinRMExecModule(AgentlessModuleBase):
name = "winrm"
description = "Execute commands on Windows hosts via WinRM"
author = "Your Name"
protocol = "winrm"
mitre_attack = ["T1021.006"]
# Command declarations — one per operation
exec = (
Command("exec", "Execute command via WinRM")
.arg("command", type="str", desc="Command to execute")
.arg("use_ssl", type="bool", desc="Use HTTPS (port 5986)", default=False,
positional=False)
)
ps = (
Command("ps", "Execute PowerShell via WinRM")
.arg("command", type="str", desc="PowerShell command")
)
# Handlers — auto-dispatched by execute()
def handle_exec(self, targets, options, *, credentials=None, proxy=None):
...
def handle_ps(self, targets, options, *, credentials=None, proxy=None):
...
|
No metadata() or execute() override needed. The base class builds metadata() from class attributes and dispatches execute() to handle_<operation>.
Reference implementation: SSH tool
#Source: tools/ssh/src/tantoc2_tool_ssh/ssh_command.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class SSHCommandModule(AgentlessModuleBase):
name = "ssh"
description = "Execute commands on remote hosts via SSH"
protocol = "ssh"
supports_shell = True
exec = (
Command("exec", "Execute remote command")
.arg("command", type="str", desc="Command to execute", required=True)
)
upload = (
Command("upload", "Upload file to remote host")
.file("local_path", desc="Local file to upload")
.path("remote_path", desc="Remote destination path", required=True)
)
download = (
Command("download", "Download file from remote host")
.path("remote_path", desc="Remote file path", required=True)
.path("local_path", desc="Local save path", required=False, default=".")
)
def handle_exec(self, targets, options, *, credentials=None, proxy=None):
...
def handle_upload(self, targets, options, *, credentials=None, proxy=None):
...
def handle_download(self, targets, options, *, credentials=None, proxy=None):
...
|
API Reference
#Capabilities endpoint
#1
| GET /api/v1/agents/<id>/capabilities
|
The command_schemas key is a flat dict mapping command name to schema dict. It merges schemas from three sources:
- Agent package
command_schemas() (built-in commands — auto-discovered from class attributes) - Management commands (
upload, download, load, unload) — always included - Currently loaded agent modules (their schema merged in while running)
Example response:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| {
"module_formats": ["py"],
"built_in_commands": ["kill", "ls", "cat", "whoami", "ps", "netstat",
"upload", "download", "load_module", "unload_module"],
"supports_daemonize": true,
"supports_relay": true,
"supported_relay_protocols": ["http"],
"command_schemas": {
"kill": {
"command_name": "kill",
"description": "Terminate the agent process",
"arguments": {},
"source": "built_in"
},
"ls": {
"command_name": "ls",
"description": "List directory contents",
"arguments": {
"path": {
"name": "path",
"type": "path",
"description": "Directory path to list",
"required": false,
"default": ".",
"positional": true,
"position": 0
}
},
"source": "built_in"
},
"upload": {
"command_name": "upload",
"description": "Upload a file to the agent",
"arguments": {
"local_path": {
"name": "local_path",
"type": "local_path",
"description": "Local file path to upload",
"required": true,
"positional": true,
"position": 0
},
"remote_path": {
"name": "remote_path",
"type": "path",
"description": "Destination path on the agent",
"required": true,
"positional": true,
"position": 1
}
},
"source": "management"
},
"port_check": {
"command_name": "port_check",
"description": "Check if a TCP port is open on a target host",
"arguments": {
"host": {
"name": "host",
"type": "str",
"description": "Target host to check",
"required": true
},
"port": {
"name": "port",
"type": "int",
"description": "TCP port to check",
"required": true
},
"timeout": {
"name": "timeout",
"type": "int",
"description": "Connection timeout in seconds",
"required": false,
"default": 3
}
},
"source": "agent_module"
}
}
}
|
Agent module detail endpoint
#1
| GET /api/v1/agent-modules/<name>
|
Includes a command_schema key (singular) with the module’s schema.
Agentless module list and detail endpoints
#1
2
| GET /api/v1/agentless/modules/
GET /api/v1/agentless/modules/<name>
|
Include a command_schemas key (plural) with one schema per operation, keyed as "{module_name}:{operation}".
Payload Validation
#The teamserver validates task payloads against the schema before dispatching. Call validate_payload() directly in test code:
1
2
3
4
5
6
7
8
9
10
11
| from tantoc2.server.command_schema import validate_payload, Command
schema = Command("port_check") \
.arg("host", type="str") \
.arg("port", type="int")
errors = validate_payload(schema, {"host": "10.0.0.1", "port": "8080"})
# => [] (int is coerced from str)
errors = validate_payload(schema, {"host": "10.0.0.1"})
# => ["Missing required argument: port"]
|
Validation rules:
- Required arguments missing from the payload produce an error.
- Primitive type values (
int, float, bool) are coerced from strings. choices violations produce an error.- Entity types (
agent, listener, module, credential) are not validated here — entity existence is checked when the task is dispatched. - Path types (
path, local_path, file) are treated as strings; no filesystem check is performed server-side.
Checklist
#When adding commands to an agent package:
When adding a YAML manifest options_schema: