sigrix-runtime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigrix_runtime/__init__.py +18 -0
- sigrix_runtime/configuration.py +110 -0
- sigrix_runtime/execution.py +465 -0
- sigrix_runtime/loader.py +336 -0
- sigrix_runtime/postern/__init__.py +61 -0
- sigrix_runtime/postern/__main__.py +460 -0
- sigrix_runtime/postern/_worker.py +116 -0
- sigrix_runtime/postern/describe.py +559 -0
- sigrix_runtime/postern/engine.py +468 -0
- sigrix_runtime/postern/entitlement.py +898 -0
- sigrix_runtime/postern/errors.py +226 -0
- sigrix_runtime/postern/mcp.py +561 -0
- sigrix_runtime/postern/pull.py +624 -0
- sigrix_runtime/postern/server.py +1154 -0
- sigrix_runtime/postern/transport.py +209 -0
- sigrix_runtime/postern/version_check.py +126 -0
- sigrix_runtime/py.typed +0 -0
- sigrix_runtime/quiet.py +52 -0
- sigrix_runtime/runner_env.py +36 -0
- sigrix_runtime/sandbox.py +95 -0
- sigrix_runtime/workforce.py +442 -0
- sigrix_runtime-0.1.0.dist-info/METADATA +173 -0
- sigrix_runtime-0.1.0.dist-info/RECORD +27 -0
- sigrix_runtime-0.1.0.dist-info/WHEEL +4 -0
- sigrix_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- sigrix_runtime-0.1.0.dist-info/licenses/LICENSE +201 -0
- sigrix_runtime-0.1.0.dist-info/licenses/NOTICE +10 -0
sigrix_runtime/loader.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""YAML loader + validator for Sigrix listing bundles.
|
|
2
|
+
|
|
3
|
+
Parses ``agents.yaml`` and ``tasks.yaml`` into structured data, validates
|
|
4
|
+
references and tool support, and (when ``build_crew`` is invoked)
|
|
5
|
+
constructs CrewAI ``Agent`` / ``Task`` / ``Crew`` objects.
|
|
6
|
+
|
|
7
|
+
The pure parsing/validation half is testable without crewai installed.
|
|
8
|
+
``build_crew`` imports crewai lazily.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
SUPPORTED_TOOLS = frozenset({"file_read", "file_write", "web_search", "serper_search"})
|
|
20
|
+
SUPPORTED_PROCESSES = frozenset({"sequential", "hierarchical", "parallel"})
|
|
21
|
+
|
|
22
|
+
PROCESS_KEY = "_process"
|
|
23
|
+
# Hierarchical crews name their manager agent here (top-level tasks.yaml key,
|
|
24
|
+
# next to ``_process``). CrewAI requires a hierarchical Crew to carry a
|
|
25
|
+
# manager and requires that manager NOT to be listed among its agents, so the
|
|
26
|
+
# loader needs to know which agent to lift out.
|
|
27
|
+
MANAGER_KEY = "_manager"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BundleConfigError(ValueError):
|
|
31
|
+
"""Raised when agents.yaml or tasks.yaml fails validation."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class AgentSpec:
|
|
36
|
+
name: str
|
|
37
|
+
role: str
|
|
38
|
+
goal: str
|
|
39
|
+
backstory: str
|
|
40
|
+
tools: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TaskSpec:
|
|
45
|
+
name: str
|
|
46
|
+
description: str
|
|
47
|
+
expected_output: str
|
|
48
|
+
# Empty means unassigned: allowed only under ``_process: hierarchical``,
|
|
49
|
+
# where the manager delegates the task to the member it chooses.
|
|
50
|
+
agent: str = ""
|
|
51
|
+
context: list[str] = field(default_factory=list)
|
|
52
|
+
# Fan-out: an async task is kicked off without being awaited;
|
|
53
|
+
# a later task lists it in ``context`` to join on its result. This is how
|
|
54
|
+
# ``_process: parallel`` panels run members concurrently.
|
|
55
|
+
async_execution: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class BundleConfig:
|
|
60
|
+
agents: list[AgentSpec]
|
|
61
|
+
tasks: list[TaskSpec]
|
|
62
|
+
process: str = "sequential"
|
|
63
|
+
manager: str = "" # agent name from ``_manager``; set iff hierarchical
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def tool_refs(self) -> list[str]:
|
|
67
|
+
seen: list[str] = []
|
|
68
|
+
for agent in self.agents:
|
|
69
|
+
for tool_ref in agent.tools:
|
|
70
|
+
if tool_ref not in seen:
|
|
71
|
+
seen.append(tool_ref)
|
|
72
|
+
return seen
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_config(config_dir: Path) -> BundleConfig:
|
|
76
|
+
agents_path = config_dir / "agents.yaml"
|
|
77
|
+
tasks_path = config_dir / "tasks.yaml"
|
|
78
|
+
|
|
79
|
+
if not agents_path.exists():
|
|
80
|
+
raise BundleConfigError(f"agents.yaml not found at {agents_path}")
|
|
81
|
+
if not tasks_path.exists():
|
|
82
|
+
raise BundleConfigError(f"tasks.yaml not found at {tasks_path}")
|
|
83
|
+
|
|
84
|
+
raw_agents = yaml.safe_load(agents_path.read_text(encoding="utf-8")) or {}
|
|
85
|
+
raw_tasks = yaml.safe_load(tasks_path.read_text(encoding="utf-8")) or {}
|
|
86
|
+
|
|
87
|
+
if not isinstance(raw_agents, dict):
|
|
88
|
+
raise BundleConfigError("agents.yaml must be a mapping of agent_name -> spec")
|
|
89
|
+
if not isinstance(raw_tasks, dict):
|
|
90
|
+
raise BundleConfigError("tasks.yaml must be a mapping of task_name -> spec")
|
|
91
|
+
|
|
92
|
+
process = "sequential"
|
|
93
|
+
if PROCESS_KEY in raw_tasks:
|
|
94
|
+
candidate = raw_tasks.pop(PROCESS_KEY)
|
|
95
|
+
if not isinstance(candidate, str):
|
|
96
|
+
raise BundleConfigError(f"{PROCESS_KEY} must be a string")
|
|
97
|
+
process = candidate
|
|
98
|
+
|
|
99
|
+
manager = ""
|
|
100
|
+
if MANAGER_KEY in raw_tasks:
|
|
101
|
+
candidate = raw_tasks.pop(MANAGER_KEY)
|
|
102
|
+
if not isinstance(candidate, str):
|
|
103
|
+
raise BundleConfigError(f"{MANAGER_KEY} must be a string")
|
|
104
|
+
manager = candidate
|
|
105
|
+
|
|
106
|
+
agents = [_parse_agent(name, spec) for name, spec in raw_agents.items()]
|
|
107
|
+
tasks = [_parse_task(name, spec) for name, spec in raw_tasks.items()]
|
|
108
|
+
|
|
109
|
+
config = BundleConfig(agents=agents, tasks=tasks, process=process, manager=manager)
|
|
110
|
+
_validate(config)
|
|
111
|
+
return config
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _parse_agent(name: str, spec: Any) -> AgentSpec:
|
|
115
|
+
if not isinstance(spec, dict):
|
|
116
|
+
raise BundleConfigError(f"agent {name!r}: spec must be a mapping")
|
|
117
|
+
required = {"role", "goal", "backstory"}
|
|
118
|
+
missing = required - spec.keys()
|
|
119
|
+
if missing:
|
|
120
|
+
raise BundleConfigError(f"agent {name!r}: missing required fields: {sorted(missing)}")
|
|
121
|
+
tools = spec.get("tools") or []
|
|
122
|
+
if not isinstance(tools, list):
|
|
123
|
+
raise BundleConfigError(f"agent {name!r}: tools must be a list")
|
|
124
|
+
return AgentSpec(
|
|
125
|
+
name=name,
|
|
126
|
+
role=str(spec["role"]),
|
|
127
|
+
goal=str(spec["goal"]),
|
|
128
|
+
backstory=str(spec["backstory"]),
|
|
129
|
+
tools=[str(t) for t in tools],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_task(name: str, spec: Any) -> TaskSpec:
|
|
134
|
+
if not isinstance(spec, dict):
|
|
135
|
+
raise BundleConfigError(f"task {name!r}: spec must be a mapping")
|
|
136
|
+
# ``agent`` is validated later: it may be omitted only under
|
|
137
|
+
# ``_process: hierarchical`` (the manager delegates the task).
|
|
138
|
+
required = {"description", "expected_output"}
|
|
139
|
+
missing = required - spec.keys()
|
|
140
|
+
if missing:
|
|
141
|
+
raise BundleConfigError(f"task {name!r}: missing required fields: {sorted(missing)}")
|
|
142
|
+
context = spec.get("context") or []
|
|
143
|
+
if not isinstance(context, list):
|
|
144
|
+
raise BundleConfigError(f"task {name!r}: context must be a list")
|
|
145
|
+
async_execution = spec.get("async_execution", False)
|
|
146
|
+
if not isinstance(async_execution, bool):
|
|
147
|
+
raise BundleConfigError(f"task {name!r}: async_execution must be a boolean")
|
|
148
|
+
return TaskSpec(
|
|
149
|
+
name=name,
|
|
150
|
+
description=str(spec["description"]),
|
|
151
|
+
expected_output=str(spec["expected_output"]),
|
|
152
|
+
agent=str(spec.get("agent") or ""),
|
|
153
|
+
context=[str(c) for c in context],
|
|
154
|
+
async_execution=async_execution,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _validate(config: BundleConfig) -> None:
|
|
159
|
+
if not config.agents:
|
|
160
|
+
raise BundleConfigError("agents.yaml must define at least one agent")
|
|
161
|
+
|
|
162
|
+
agent_names = [a.name for a in config.agents]
|
|
163
|
+
if len(set(agent_names)) != len(agent_names):
|
|
164
|
+
raise BundleConfigError("agents.yaml has duplicate agent names")
|
|
165
|
+
|
|
166
|
+
for agent in config.agents:
|
|
167
|
+
for tool_ref in agent.tools:
|
|
168
|
+
if tool_ref not in SUPPORTED_TOOLS:
|
|
169
|
+
raise BundleConfigError(
|
|
170
|
+
f"agent {agent.name!r}: unsupported tool {tool_ref!r}. Supported tools: {sorted(SUPPORTED_TOOLS)}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not config.tasks:
|
|
174
|
+
raise BundleConfigError("tasks.yaml must define at least one task")
|
|
175
|
+
|
|
176
|
+
task_names = [t.name for t in config.tasks]
|
|
177
|
+
if len(set(task_names)) != len(task_names):
|
|
178
|
+
raise BundleConfigError("tasks.yaml has duplicate task names")
|
|
179
|
+
|
|
180
|
+
if config.process not in SUPPORTED_PROCESSES:
|
|
181
|
+
raise BundleConfigError(f"unsupported process {config.process!r}. Supported: {sorted(SUPPORTED_PROCESSES)}")
|
|
182
|
+
|
|
183
|
+
hierarchical = config.process == "hierarchical"
|
|
184
|
+
agent_name_set = set(agent_names)
|
|
185
|
+
|
|
186
|
+
# CrewAI requires a hierarchical Crew to carry a manager, and requires
|
|
187
|
+
# that manager NOT to be listed among its agents — so a hierarchical
|
|
188
|
+
# bundle must name one via ``_manager``, and the loader lifts it out at
|
|
189
|
+
# build time. Reject at load what would crash at runtime.
|
|
190
|
+
if hierarchical and not config.manager:
|
|
191
|
+
raise BundleConfigError(f"{PROCESS_KEY}: hierarchical requires {MANAGER_KEY} to name the manager agent")
|
|
192
|
+
if config.manager and not hierarchical:
|
|
193
|
+
raise BundleConfigError(f"{MANAGER_KEY} is only valid with {PROCESS_KEY}: hierarchical")
|
|
194
|
+
if config.manager:
|
|
195
|
+
if config.manager not in agent_name_set:
|
|
196
|
+
raise BundleConfigError(f"{MANAGER_KEY}: references undefined agent {config.manager!r}")
|
|
197
|
+
if len(agent_name_set) < 2:
|
|
198
|
+
raise BundleConfigError(
|
|
199
|
+
f"{MANAGER_KEY}: a hierarchical crew needs at least one member agent besides the manager"
|
|
200
|
+
)
|
|
201
|
+
manager_spec = next(a for a in config.agents if a.name == config.manager)
|
|
202
|
+
if manager_spec.tools:
|
|
203
|
+
# CrewAI rejects a manager agent that carries tools.
|
|
204
|
+
raise BundleConfigError(f"{MANAGER_KEY}: manager agent {config.manager!r} must not have tools")
|
|
205
|
+
|
|
206
|
+
task_name_set = set(task_names)
|
|
207
|
+
for task in config.tasks:
|
|
208
|
+
if not task.agent:
|
|
209
|
+
if not hierarchical:
|
|
210
|
+
raise BundleConfigError(
|
|
211
|
+
f"task {task.name!r}: 'agent' is required (only {PROCESS_KEY}: hierarchical tasks may omit it)"
|
|
212
|
+
)
|
|
213
|
+
elif task.agent not in agent_name_set:
|
|
214
|
+
raise BundleConfigError(f"task {task.name!r}: references undefined agent {task.agent!r}")
|
|
215
|
+
elif task.agent == config.manager:
|
|
216
|
+
raise BundleConfigError(
|
|
217
|
+
f"task {task.name!r}: must not be assigned to the manager agent {config.manager!r} "
|
|
218
|
+
"(the manager delegates; leave the task unassigned instead)"
|
|
219
|
+
)
|
|
220
|
+
for ctx in task.context:
|
|
221
|
+
if ctx not in task_name_set:
|
|
222
|
+
raise BundleConfigError(f"task {task.name!r}: context references undefined task {ctx!r}")
|
|
223
|
+
|
|
224
|
+
# Fan-out rules. An async task is launched without being
|
|
225
|
+
# awaited, so a trailing one would finish after the crew returns (its
|
|
226
|
+
# output silently lost), and one that declares ``context`` would have to
|
|
227
|
+
# block on other tasks — defeating the point. Reject at load what would
|
|
228
|
+
# misbehave at runtime.
|
|
229
|
+
if config.tasks[-1].async_execution:
|
|
230
|
+
raise BundleConfigError(
|
|
231
|
+
f"task {config.tasks[-1].name!r}: the final task must not set async_execution "
|
|
232
|
+
"(nothing would wait for its result)"
|
|
233
|
+
)
|
|
234
|
+
for task in config.tasks:
|
|
235
|
+
if task.async_execution and task.context:
|
|
236
|
+
raise BundleConfigError(
|
|
237
|
+
f"task {task.name!r}: an async_execution task must not declare context "
|
|
238
|
+
"(async tasks fan out independently; a later synchronous task joins them)"
|
|
239
|
+
)
|
|
240
|
+
if config.process == "parallel" and not any(t.async_execution for t in config.tasks):
|
|
241
|
+
raise BundleConfigError(
|
|
242
|
+
f"{PROCESS_KEY}: parallel requires at least one async_execution task "
|
|
243
|
+
"(without one it would run as a plain sequential chain)"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
_check_acyclic(config.tasks)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_acyclic(tasks: list[TaskSpec]) -> None:
|
|
250
|
+
deps = {t.name: set(t.context) for t in tasks}
|
|
251
|
+
resolved: set[str] = set()
|
|
252
|
+
while len(resolved) < len(deps):
|
|
253
|
+
progress = False
|
|
254
|
+
for name, requires in deps.items():
|
|
255
|
+
if name in resolved:
|
|
256
|
+
continue
|
|
257
|
+
if requires <= resolved:
|
|
258
|
+
resolved.add(name)
|
|
259
|
+
progress = True
|
|
260
|
+
if not progress:
|
|
261
|
+
unresolved = sorted(set(deps) - resolved)
|
|
262
|
+
raise BundleConfigError(f"task DAG has a cycle involving: {unresolved}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def build_crew(config: BundleConfig, tools: list[Any]) -> Any:
|
|
266
|
+
"""Construct a CrewAI ``Crew`` object from the parsed config.
|
|
267
|
+
|
|
268
|
+
The ``tools`` list is in the same order as ``config.tool_refs`` (this
|
|
269
|
+
is the contract ``sandbox.build_tools`` honours).
|
|
270
|
+
|
|
271
|
+
A hierarchical config (``_process: hierarchical`` + ``_manager``) builds
|
|
272
|
+
the manager as CrewAI's ``manager_agent`` — excluded from ``agents=``
|
|
273
|
+
(CrewAI rejects a manager listed there) and free to delegate an
|
|
274
|
+
unassigned task to whichever member it chooses.
|
|
275
|
+
|
|
276
|
+
Each task carries its own ``name=``: CrewAI's ``TaskOutput`` —
|
|
277
|
+
what a ``task_callback`` receives — mirrors it verbatim, and
|
|
278
|
+
``execution.py``'s progress reporting reads that name for a Postern
|
|
279
|
+
``usage.steps[*].name``. Leave it unset and CrewAI's own ``TaskOutput``
|
|
280
|
+
reports none either, so the step falls back to the task's *description* —
|
|
281
|
+
the seller's prompt text, truncated mid-word at 120 characters.
|
|
282
|
+
"""
|
|
283
|
+
# Every crew-construction path (buyer main.py, the server-side example
|
|
284
|
+
# runner, tests) inherits the quiet posture; setdefault semantics keep
|
|
285
|
+
# caller overrides intact.
|
|
286
|
+
from sigrix_runtime.quiet import apply_quiet_env_defaults
|
|
287
|
+
|
|
288
|
+
apply_quiet_env_defaults()
|
|
289
|
+
|
|
290
|
+
from crewai import Agent, Crew, Process, Task
|
|
291
|
+
|
|
292
|
+
tools_by_ref: dict[str, Any] = dict(zip(config.tool_refs, tools, strict=True))
|
|
293
|
+
|
|
294
|
+
crewai_agents: dict[str, Any] = {
|
|
295
|
+
spec.name: Agent(
|
|
296
|
+
role=spec.role,
|
|
297
|
+
goal=spec.goal,
|
|
298
|
+
backstory=spec.backstory,
|
|
299
|
+
tools=[tools_by_ref[t] for t in spec.tools],
|
|
300
|
+
allow_delegation=(spec.name == config.manager),
|
|
301
|
+
verbose=False,
|
|
302
|
+
)
|
|
303
|
+
for spec in config.agents
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
crewai_tasks: dict[str, Any] = {}
|
|
307
|
+
for spec in config.tasks:
|
|
308
|
+
crewai_tasks[spec.name] = Task(
|
|
309
|
+
name=spec.name,
|
|
310
|
+
description=spec.description,
|
|
311
|
+
expected_output=spec.expected_output,
|
|
312
|
+
agent=crewai_agents[spec.agent] if spec.agent else None,
|
|
313
|
+
context=[crewai_tasks[c] for c in spec.context],
|
|
314
|
+
async_execution=spec.async_execution,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
process_map = {
|
|
318
|
+
"sequential": Process.sequential,
|
|
319
|
+
"hierarchical": Process.hierarchical,
|
|
320
|
+
# CrewAI's public Process enum has no parallel mode; a parallel
|
|
321
|
+
# bundle runs as a sequential pipeline whose ``async_execution``
|
|
322
|
+
# member tasks fan out concurrently and whose final synchronous
|
|
323
|
+
# task (``context`` = the members) joins them — real fan-out, not
|
|
324
|
+
# a silent chain.
|
|
325
|
+
"parallel": Process.sequential,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
manager_agent = crewai_agents.pop(config.manager) if config.manager else None
|
|
329
|
+
|
|
330
|
+
return Crew(
|
|
331
|
+
agents=list(crewai_agents.values()),
|
|
332
|
+
tasks=list(crewai_tasks.values()),
|
|
333
|
+
process=process_map[config.process],
|
|
334
|
+
manager_agent=manager_agent,
|
|
335
|
+
verbose=False,
|
|
336
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Postern v0 for a Sigrix listing bundle — the reference implementation.
|
|
2
|
+
|
|
3
|
+
Postern (``sigrix-io/postern``) is the open execution and entitlement
|
|
4
|
+
protocol for packaged AI agents: four verbs over local HTTP —
|
|
5
|
+
``describe`` / ``run`` / ``stream`` / ``status`` — and one entitlement flow.
|
|
6
|
+
This package serves them for the bundle it is started inside::
|
|
7
|
+
|
|
8
|
+
python -m sigrix_runtime.postern
|
|
9
|
+
|
|
10
|
+
It is also what the ``sigrix/runner`` image runs as its entrypoint. Those
|
|
11
|
+
are two deployment shapes of one codebase rather than two implementations;
|
|
12
|
+
if they ever diverge, that is the bug.
|
|
13
|
+
|
|
14
|
+
**Nothing here is a second execution path.** ``run`` and ``stream`` reach
|
|
15
|
+
the agent through ``sigrix_runtime.execution``, which is the same function
|
|
16
|
+
``python main.py "…"`` calls. A crew's kickoff inputs are a contract
|
|
17
|
+
spanning several files, and a server that built its own ``crew.kickoff``
|
|
18
|
+
would be one more place for it to drift.
|
|
19
|
+
|
|
20
|
+
**Zero dependencies beyond the standard library.** The bundle's
|
|
21
|
+
``requirements.txt`` is something a buyer installs on their own machine,
|
|
22
|
+
and four routes on loopback do not justify adding a web framework and its
|
|
23
|
+
tree to it. ``http.server`` serves them; ``http.client`` makes the one
|
|
24
|
+
outbound call this protocol defines.
|
|
25
|
+
|
|
26
|
+
Layout:
|
|
27
|
+
|
|
28
|
+
``errors`` the specification's error envelope, as exceptions
|
|
29
|
+
``describe`` the agent's contract: shipped by the distributor, or derived
|
|
30
|
+
``transport`` the one outbound connection, and SPEC 7's rule on it
|
|
31
|
+
``entitlement`` the distributor client and the state machine of SPEC 5.7
|
|
32
|
+
``pull`` bundle retrieval (SPEC 5.6), which is the image's boot step
|
|
33
|
+
``execution`` the run engine — a subprocess per run, so a timeout is real
|
|
34
|
+
``server`` the HTTP surface: routing, CORS, and the SSE framing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
# The specification version every payload carries. The ``/postern/v0/``
|
|
40
|
+
# path prefix is deliberately coarser than this (SPEC 5.3, VERSIONING.md):
|
|
41
|
+
# the prefix moves only on a breaking revision, and this field is the only
|
|
42
|
+
# sanctioned way to know exactly what you are talking to.
|
|
43
|
+
POSTERN_VERSION = "0.1"
|
|
44
|
+
|
|
45
|
+
# SPEC 3. This runner implements all four verbs, so it reports Level 3.
|
|
46
|
+
# ``stream`` emits no ``delta`` — see ``server`` for why that is conformant
|
|
47
|
+
# rather than a shortfall.
|
|
48
|
+
CONFORMANCE_LEVEL = 3
|
|
49
|
+
|
|
50
|
+
PATH_PREFIX = "/postern/v0"
|
|
51
|
+
|
|
52
|
+
DEFAULT_PORT = 8787
|
|
53
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"CONFORMANCE_LEVEL",
|
|
57
|
+
"DEFAULT_HOST",
|
|
58
|
+
"DEFAULT_PORT",
|
|
59
|
+
"PATH_PREFIX",
|
|
60
|
+
"POSTERN_VERSION",
|
|
61
|
+
]
|