API Reference¶
The public surface is exactly what is exported from spine_lite.__all__. Everything else is private and subject to change without notice.
Top-level¶
The package root re-exports the surface so callers don't have to navigate submodules:
from spine_lite import (
Effect,
PRECEDENCE,
most_restrictive,
SpineLiteError,
ManifestError,
ClassificationError,
PostureError,
HookError,
__version__,
)
Effects¶
spine_lite.effects ¶
Closed effects taxonomy.
Six-class enumeration of side-effect categories that a tool call can produce.
The ordering encoded in :data:PRECEDENCE is the spec — DESTRUCTIVE
dominates, READ yields. :func:most_restrictive collapses any non-empty
set of effects to the single highest-precedence class.
This module is pure: no I/O, no clocks, no randomness. The taxonomy is closed by design and mirrors the TypeScript reference at https://github.com/MacFall7/M87-Spine-lite. Extensions require explicit project sign-off.
PRECEDENCE
module-attribute
¶
PRECEDENCE: Final[tuple[Effect, ...]] = (
DESTRUCTIVE,
SPAWN,
EXECUTE,
NETWORK,
WRITE,
READ,
)
Effect classes ordered from most restrictive to least restrictive.
Effect ¶
Bases: StrEnum
Side-effect class produced by a tool call.
Members
READ: pure observation; no state change. WRITE: persistent state change to caller-owned storage. NETWORK: outbound network call. EXECUTE: subprocess invocation, no fork-and-detach. SPAWN: subprocess invocation that may fork-and-detach. DESTRUCTIVE: irreversible state change (delete, drop, force-push).
Source code in src/spine_lite/effects.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
most_restrictive ¶
most_restrictive(effects: Iterable[Effect]) -> Effect
Return the highest-precedence effect from effects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
effects
|
Iterable[Effect]
|
Non-empty iterable of effects. Order is irrelevant. |
required |
Returns:
| Type | Description |
|---|---|
Effect
|
The single effect that dominates all others under :data: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> most_restrictive({Effect.READ, Effect.NETWORK})
<Effect.NETWORK: 'network'>
>>> most_restrictive([Effect.DESTRUCTIVE, Effect.READ])
<Effect.DESTRUCTIVE: 'destructive'>
Source code in src/spine_lite/effects.py
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 | |
Exceptions¶
spine_lite.exceptions ¶
Exception hierarchy.
All errors raised by spine-lite inherit from :class:SpineLiteError. Library
callers can catch the base class for blanket handling or the specific
subclasses for finer control. The hierarchy is closed in spirit — adding new
classes is permitted, but every new class must descend from the base.
Pure module: no I/O, no side effects.
SpineLiteError ¶
Bases: Exception
Base class for every error raised by spine-lite.
Source code in src/spine_lite/exceptions.py
14 15 | |
ManifestError ¶
Bases: SpineLiteError
Raised when a tool manifest is malformed or fails validation.
Source code in src/spine_lite/exceptions.py
18 19 | |
ClassificationError ¶
Bases: SpineLiteError
Raised when a tool call cannot be classified against a manifest.
Source code in src/spine_lite/exceptions.py
22 23 | |
PostureError ¶
Bases: SpineLiteError
Raised on illegal posture transitions or invalid posture state.
Source code in src/spine_lite/exceptions.py
26 27 | |
HookError ¶
Bases: SpineLiteError
Raised when the PreToolUse hook protocol is violated.
Source code in src/spine_lite/exceptions.py
30 31 | |
Manifest¶
spine_lite.manifest ¶
Tool-manifest schema.
Pydantic v2 models for tool definitions, declared effects, and posture constraints. Pure module: validation only, no I/O.
Manifests round-trip authored fixtures byte-for-byte. The two
order-sensitive fields — :attr:ToolDefinition.effects and
:attr:ToolDefinition.permitted_postures — are canonicalised on
construction (deduplicated and sorted by enum-declaration order) so JSON
serialisation is stable across runs and platforms regardless of the
order the author wrote them in.
ToolDefinition ¶
Bases: BaseModel
Declares a single tool's effects and posture constraints.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Tool identifier as the LLM sees it. Must match the key under
which this definition is registered in a :class: |
description |
str | None
|
Optional human-readable description. |
effects |
tuple[Effect, ...]
|
Effect classes this tool's invocations can produce. Must
be non-empty. Stored canonically: deduplicated and sorted by
|
permitted_postures |
tuple[Posture, ...] | None
|
Postures under which this tool may be invoked.
|
require_confirmation |
bool
|
If true, even an otherwise-allowed call must be confirmed by the operator before execution. Phase 3 classifier honours this; Phase 2 just stores it. |
metadata |
dict[str, Any]
|
Free-form additional metadata. Manifest authors may carry arbitrary keys here; spine-lite ignores them but preserves them for round-trip serialisation. |
Examples:
>>> definition = ToolDefinition(
... name="read_file",
... effects=(Effect.READ,),
... )
>>> definition.effects
(<Effect.READ: 'read'>,)
Source code in src/spine_lite/manifest.py
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
Manifest ¶
Bases: BaseModel
A collection of tool definitions keyed by tool name.
A manifest is the policy document for a runtime configuration. Every tool the LLM can call must appear here; calls to undeclared tools fail closed in the classifier.
Attributes:
| Name | Type | Description |
|---|---|---|
tools |
dict[str, ToolDefinition]
|
Mapping from tool name to its :class: |
Examples:
>>> manifest = Manifest(tools={
... "read_file": ToolDefinition(name="read_file", effects=(Effect.READ,)),
... })
>>> manifest.get("read_file").effects
(<Effect.READ: 'read'>,)
Source code in src/spine_lite/manifest.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
get ¶
get(name: str) -> ToolDefinition
Return the definition for name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Tool name to look up. |
required |
Returns:
| Type | Description |
|---|---|
ToolDefinition
|
The matching :class: |
Raises:
| Type | Description |
|---|---|
ManifestError
|
If no tool with that name is declared. |
Source code in src/spine_lite/manifest.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
parse_manifest ¶
parse_manifest(data: Any) -> Manifest
Validate data as a :class:Manifest.
Wraps pydantic's :class:pydantic.ValidationError as
:class:ManifestError so callers can catch a single typed exception
rooted at :class:SpineLiteError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Any
|
A Python mapping (dict), a JSON string, or JSON bytes.
Strings and bytes are parsed via
:meth: |
required |
Returns:
| Type | Description |
|---|---|
Manifest
|
A validated, immutable :class: |
Raises:
| Type | Description |
|---|---|
ManifestError
|
If validation fails for any reason. The original
:class: |
Examples:
>>> parse_manifest({
... "tools": {
... "read_file": {"name": "read_file", "effects": ["read"]},
... },
... }).get("read_file").effects
(<Effect.READ: 'read'>,)
Source code in src/spine_lite/manifest.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
Classifier¶
spine_lite.classifier ¶
Effect classifier.
The :func:classify function maps a :class:ToolCall and a validated
:class:spine_lite.manifest.Manifest to a :class:Decision.
Pure module: deterministic, no I/O, no clocks, no randomness. Identical
inputs produce identical decisions every time. The decision's
rationale is the only string-formatted field, and it is built from
fields in canonical order so two calls with the same inputs produce the
same byte-for-byte rationale.
ToolCall
dataclass
¶
A planned tool invocation to classify.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
str
|
Tool name as declared in the manifest. |
arguments |
dict[str, Any]
|
Free-form key/value arguments. Currently informational only; future phases may use them to refine classification beyond the manifest's declared effects. |
Source code in src/spine_lite/classifier.py
24 25 26 27 28 29 30 31 32 33 34 35 36 | |
Decision
dataclass
¶
The result of classifying a :class:ToolCall.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
str
|
Echoed from the input call. |
effects |
tuple[Effect, ...]
|
The full set of effect classes the call can produce, as a
canonically-ordered tuple (sorted by |
most_restrictive |
Effect
|
The dominant effect under
:data: |
rationale |
str
|
Human-readable explanation of why this effect set was chosen. Format is canonical so byte-stable across runs. |
Source code in src/spine_lite/classifier.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
classify ¶
classify(
tool_call: ToolCall, manifest: Manifest
) -> Decision
Classify tool_call against manifest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool_call
|
ToolCall
|
The planned invocation. |
required |
manifest
|
Manifest
|
A validated :class: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
Decision
|
class: |
Decision
|
and a deterministic rationale. |
Raises:
| Type | Description |
|---|---|
ManifestError
|
If the tool isn't declared in the manifest. |
Examples:
>>> from spine_lite import Effect, Manifest, ToolDefinition
>>> manifest = Manifest(tools={
... "fetch": ToolDefinition(
... name="fetch",
... effects=(Effect.NETWORK, Effect.READ),
... ),
... })
>>> decision = classify(ToolCall(tool="fetch"), manifest)
>>> decision.most_restrictive
<Effect.NETWORK: 'network'>
>>> decision.effects
(<Effect.NETWORK: 'network'>, <Effect.READ: 'read'>)
Source code in src/spine_lite/classifier.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
Posture¶
spine_lite.posture ¶
Posture state machine.
Closed :class:Posture enum, the closed :class:Disposition enum, the
explicit :func:transition rule table, and :func:evaluate — the pure
policy function that maps (posture, definition, decision) to a
disposition.
Pure module: deterministic, no I/O. Same input produces the same output, every time. The transition table and the evaluation rules are encoded inline; both are sensitive to changes and warrant project-level sign-off on extension.
Posture ¶
Bases: StrEnum
Operational posture of the runtime.
Drives how the runtime treats tool calls. Closed enum: extending
requires a project-level decision. The members and their string
values are pinned by docs/concepts/posture-and-hooks.md.
Members
INTERACTIVE: Operator at the keyboard; ambiguous calls escalate.
AUTONOMOUS: No operator in the loop; ambiguous calls fail closed.
DRY_RUN: Classification only; non-READ effects don't fire.
LOCKED: Refuse everything except explicit allow-listed read-only calls.
Source code in src/spine_lite/posture.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
Disposition ¶
Bases: StrEnum
The outcome of evaluating a classified decision under a posture.
Members
ALLOW: Permitted; the tool may run as classified.
DENY: Refused; the tool must not run.
ESCALATE: Permitted only after operator confirmation. Only
returned under :attr:Posture.INTERACTIVE; autonomous
postures fail closed instead of escalating.
Source code in src/spine_lite/posture.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
transition ¶
transition(current: Posture, target: Posture) -> Posture
Transition from current to target.
Identity transitions (target is current) are always permitted and
return current unchanged. Cross-posture transitions must appear
in the allow-set for the source posture.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current
|
Posture
|
The posture being left. |
required |
target
|
Posture
|
The posture being entered. |
required |
Returns:
| Type | Description |
|---|---|
Posture
|
The new posture (always equal to |
Raises:
| Type | Description |
|---|---|
PostureError
|
If the transition is not in the allow-set. |
Examples:
>>> transition(Posture.INTERACTIVE, Posture.AUTONOMOUS)
<Posture.AUTONOMOUS: 'autonomous'>
>>> transition(Posture.LOCKED, Posture.INTERACTIVE)
<Posture.INTERACTIVE: 'interactive'>
Source code in src/spine_lite/posture.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
evaluate ¶
evaluate(
posture: Posture,
definition: ToolDefinition,
decision: Decision,
) -> Disposition
Map a classified call under a posture to a :class:Disposition.
Pure function. Encodes the policy rules:
- Posture allow-list. If the tool's
:attr:
~spine_lite.manifest.ToolDefinition.permitted_posturesis set and the current posture isn't in it, the tool is denied regardless of the rest. - LOCKED. Only
READcalls are permitted. Anything else is denied. - DRY_RUN. Only
READcalls fire. Anything else is denied (the tool would be classified, but DRY_RUN's contract is that no state-changing effect actually executes). require_confirmation. A tool flagged as requiring confirmation escalates underINTERACTIVEand fails closed (denies) underAUTONOMOUS.- Otherwise:
ALLOW.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
posture
|
Posture
|
Current operational posture. |
required |
definition
|
ToolDefinition
|
The tool's manifest definition. |
required |
decision
|
Decision
|
The classifier's output for the tool call. |
required |
Returns:
| Type | Description |
|---|---|
Disposition
|
|
Examples:
>>> from spine_lite import Effect, ToolDefinition, Decision
>>> defn = ToolDefinition(name="t", effects=(Effect.WRITE,))
>>> dec = Decision(
... tool="t", effects=(Effect.WRITE,),
... most_restrictive=Effect.WRITE, rationale="",
... )
>>> evaluate(Posture.LOCKED, defn, dec)
<Disposition.DENY: 'deny'>
>>> evaluate(Posture.INTERACTIVE, defn, dec)
<Disposition.ALLOW: 'allow'>
Source code in src/spine_lite/posture.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
Phase 3 modules¶
Stubs with phase-pinning docstrings. The reference expands as implementations land.
spine_lite.receipt ¶
Decision receipts.
A :class:Receipt is a structured record of a single classified call
under a posture. Receipts are content-addressable: serialising a receipt
to its canonical JSON form produces byte-identical output across runs
and platforms given identical inputs, and :meth:Receipt.content_hash
hashes that canonical form with SHA-256.
Pure module: no clocks, no randomness, no I/O. Wall-clock metadata that needs to live alongside a receipt belongs in the hook layer; the receipt itself stays content-addressable.
Receipt
dataclass
¶
A deterministic record of one classified call under a posture.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
str
|
Name of the tool that was classified. |
arguments |
dict[str, Any]
|
Arguments echoed from the input call. Stored as authored; canonical serialisation sorts keys. |
effects |
tuple[Effect, ...]
|
Canonical effect tuple from the classifier (ordered by
|
most_restrictive |
Effect
|
Dominant effect under |
rationale |
str
|
Byte-stable rationale from the classifier. |
posture |
Posture
|
Operational posture at the time of evaluation. |
disposition |
Disposition
|
|
require_confirmation |
bool
|
Echoed from the tool's manifest definition so receipts stay self-contained for replay. |
Examples:
>>> from spine_lite import Effect, Posture, Disposition
>>> r = Receipt(
... tool="t",
... arguments={"a": 1},
... effects=(Effect.READ,),
... most_restrictive=Effect.READ,
... rationale="...",
... posture=Posture.INTERACTIVE,
... disposition=Disposition.ALLOW,
... require_confirmation=False,
... )
>>> len(r.content_hash())
64
Source code in src/spine_lite/receipt.py
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
to_canonical_dict ¶
to_canonical_dict() -> dict[str, Any]
Return a canonical dict representation.
Keys are sorted; enum values are serialised as their string
forms; the effect tuple is serialised as a list. Suitable as
input to :func:json.dumps with sort_keys=True for byte-
stable output.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A dict whose JSON encoding is byte-stable across runs. |
Source code in src/spine_lite/receipt.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | |
to_canonical_json ¶
to_canonical_json() -> str
Return a byte-stable JSON encoding.
Uses sort_keys=True, ensure_ascii=False (so the rationale
stays human-readable in its original encoding), and a compact
separator pair. Two receipts with identical fields produce
byte-identical output.
Source code in src/spine_lite/receipt.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
content_hash ¶
content_hash() -> str
Return the SHA-256 hex digest of :meth:to_canonical_json.
Identical receipts produce identical hashes. Different receipts — even ones differing only in argument order before canonicalisation — produce identical hashes once round-tripped through this method.
Source code in src/spine_lite/receipt.py
106 107 108 109 110 111 112 113 114 | |
spine_lite.hook ¶
Claude Code PreToolUse hook adapter.
The thin I/O wrapper around the pure pipeline. Reads a JSON payload from stdin, classifies the tool call against the configured manifest, applies the posture state machine, builds a deterministic receipt, and writes the receipt's canonical JSON form to stdout. Exit code signals the disposition.
This is the only module in the package besides :mod:spine_lite.cli
that touches stdin, stdout, or wall-clock time. Everything inside is
pure.
run_hook ¶
run_hook(
manifest: Manifest,
payload: str | bytes,
*,
posture: Posture = Posture.INTERACTIVE,
) -> tuple[Receipt, int]
Execute the pipeline against payload under manifest.
The payload is expected to be a JSON object with at least a tool
field (string) and optionally an arguments field (object). Any
other top-level keys are ignored; the hook contract is intentionally
minimal so it can adapt to multiple host hook formats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifest
|
Manifest
|
A validated manifest declaring every tool the host may invoke. |
required |
payload
|
str | bytes
|
The raw stdin bytes or string from the host. |
required |
posture
|
Posture
|
The current operational posture. Defaults to
|
INTERACTIVE
|
Returns:
| Type | Description |
|---|---|
Receipt
|
A tuple of |
int
|
for writing the receipt to stdout and returning the exit code. |
Raises:
| Type | Description |
|---|---|
HookError
|
If the payload is not valid JSON, is not a JSON object, is missing required fields, or has wrong-typed fields. |
ManifestError
|
If the requested tool is not declared in the manifest. Propagated unchanged from the classifier. |
Source code in src/spine_lite/hook.py
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
main ¶
main(
manifest: Manifest,
*,
stdin: IO[str] = sys.stdin,
stdout: IO[str] = sys.stdout,
stderr: IO[str] = sys.stderr,
posture: Posture = Posture.INTERACTIVE,
) -> int
End-to-end hook invocation.
Reads stdin, runs :func:run_hook, writes the canonical receipt
JSON to stdout, and returns the exit code. On error, writes a
structured JSON error payload to stdout (so the host always parses
a JSON object) and a one-line message to stderr.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifest
|
Manifest
|
A validated manifest. |
required |
stdin
|
IO[str]
|
Input stream. Defaults to :data: |
stdin
|
stdout
|
IO[str]
|
Output stream for the receipt or error JSON. Defaults
to :data: |
stdout
|
stderr
|
IO[str]
|
Output stream for human-readable error messages.
Defaults to :data: |
stderr
|
posture
|
Posture
|
Operational posture. |
INTERACTIVE
|
Returns:
| Type | Description |
|---|---|
int
|
Exit code per the contract: |
int
|
|
int
|
|
int
|
|
int
|
|
int
|
|
Source code in src/spine_lite/hook.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
CLI module¶
spine_lite.cli ¶
Operator command-line interface.
Phase 3 ships the full subcommand surface: version,
validate-manifest, classify, hook. The CLI is one of the two
modules in the package permitted to do I/O (the other is
:mod:spine_lite.hook); everything below it is pure.
app
module-attribute
¶
app = Typer(
name="spine-lite",
help="Deterministic policy and effects runtime for LLM tool calls.",
no_args_is_help=True,
add_completion=False,
)
version ¶
version() -> None
Print the installed spine-lite version and exit.
Source code in src/spine_lite/cli.py
50 51 52 53 | |
See also¶
- How-To / Use the API — practical patterns.
- Reference / CLI — subcommand reference.
- Reference / Exceptions — error catalog.
- Reference / Glossary — vocabulary.