spineforge 0.1.0__tar.gz

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.
@@ -0,0 +1,23 @@
1
+ # Runtime data (created by spineforge at runtime)
2
+ .spineforge/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ *.egg
12
+
13
+ # Environment
14
+ .env
15
+ .venv/
16
+ env/
17
+ venv/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Spineforge. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are proprietary and confidential.
4
+ Unauthorized copying, distribution, reproduction, publication, or modification of this file,
5
+ via any medium, is strictly prohibited.
6
+
7
+ This software is for internal company use only, unless explicitly authorized in writing by Spineforge.
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: spineforge
3
+ Version: 0.1.0
4
+ Summary: AI agent identity and observability SDK — Okta + Datadog for the AI agent era
5
+ Project-URL: Homepage, https://spineforge-site.vercel.app/
6
+ Project-URL: Documentation, https://spineforge-site.vercel.app/docs
7
+ Project-URL: Repository, https://github.com/spineforge/spineforge
8
+ Author-email: Kushagra Chavel <kushagrachavel@gmail.com>
9
+ License: Copyright (c) 2026 Spineforge. All rights reserved.
10
+
11
+ This software and associated documentation files (the "Software") are proprietary and confidential.
12
+ Unauthorized copying, distribution, reproduction, publication, or modification of this file,
13
+ via any medium, is strictly prohibited.
14
+
15
+ This software is for internal company use only, unless explicitly authorized in writing by Spineforge.
16
+ License-File: LICENSE
17
+ Classifier: Development Status :: 3 - Alpha
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: Other/Proprietary License
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: System :: Monitoring
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: cryptography>=43.0.0
28
+ Requires-Dist: opentelemetry-api>=1.20.0
29
+ Requires-Dist: opentelemetry-instrumentation-groq>=0.35.0
30
+ Requires-Dist: opentelemetry-instrumentation-langchain>=0.35.0
31
+ Requires-Dist: opentelemetry-sdk>=1.20.0
32
+ Requires-Dist: opentelemetry-semantic-conventions>=0.41b0
33
+ Requires-Dist: pyjwt>=2.9.0
34
+ Provides-Extra: examples
35
+ Requires-Dist: duckduckgo-search; extra == 'examples'
36
+ Requires-Dist: groq; extra == 'examples'
37
+ Requires-Dist: langchain; extra == 'examples'
38
+ Requires-Dist: langchain-community; extra == 'examples'
39
+ Requires-Dist: langchain-core; extra == 'examples'
40
+ Requires-Dist: langchain-groq; extra == 'examples'
41
+ Requires-Dist: pydantic; extra == 'examples'
42
+ Requires-Dist: python-dotenv; extra == 'examples'
43
+ Requires-Dist: wikipedia; extra == 'examples'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Spineforge SDK
47
+
48
+ > AI agent identity and observability — **Okta + Datadog for the AI agent era.**
49
+
50
+ [Homepage](https://spineforge-site.vercel.app/) | [Documentation](https://spineforge-site.vercel.app/docs)
51
+
52
+ Spineforge assigns every AI agent a stable identity (Spine ID), instruments LLM and tool calls via [OpenLLMetry](https://github.com/traceloop/openllmetry) (OpenTelemetry-based auto-instrumentation), and groups events into Runs and Actions that map directly to the hosted Spineforge backend.
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### Install
59
+
60
+ ```bash
61
+ pip install spineforge
62
+
63
+ # For running the example agents
64
+ pip install "spineforge[examples]"
65
+ ```
66
+
67
+ ### Usage (3 lines to integrate)
68
+
69
+ ```python
70
+ import spineforge
71
+
72
+ # 1. Init — resolves/creates a stable Spine ID for this agent
73
+ spine = spineforge.init(agent_name="my-research-agent")
74
+
75
+ # 2. Wrap your agent's top-level invocation in a run
76
+ with spine.run(input=user_query) as run:
77
+ result = run_agent(user_query)
78
+ run.set_output(result)
79
+ ```
80
+
81
+ That's it. Spineforge auto-instruments LLM calls (Groq, LangChain) in the background via OpenTelemetry. No code changes to your agent logic.
82
+
83
+ ### For tool functions not auto-captured
84
+
85
+ ```python
86
+ @spineforge.track_tool
87
+ def call_tool(name: str, query: str) -> str:
88
+ """Only needed for raw SDK agents — LangChain tools are auto-captured."""
89
+ return tools[name].run(query)
90
+ ```
91
+
92
+ ---
93
+
94
+ ## How It Works
95
+
96
+ ### `init(agent_name)` → `Spine`
97
+
98
+ 1. **Resolves identity** — looks up `agent_name` in `.spineforge/registry.json`. If it exists, returns the same Spine ID. If not, generates a new UUID-4 and persists it.
99
+ 2. **Wires instrumentation** — creates an OpenTelemetry `TracerProvider`, registers a custom `SpineforgeSpanProcessor`, and activates OpenLLMetry instrumentors for installed libraries (Groq, LangChain).
100
+ 3. **Returns a `Spine` handle** — use `spine.run()` to group actions into runs.
101
+
102
+ ### `spine.run(input=...)` → context manager
103
+
104
+ The `with spine.run(...)` block:
105
+ - Generates a `run_id` and sets it in a `ContextVar` so all spans created during the block are associated with this run.
106
+ - Emits a `RunEvent(status="running")` at entry.
107
+ - Emits a `RunEvent(status="success")` or `RunEvent(status="error")` at exit.
108
+ - Force-flushes the OTel pipeline to ensure all action spans are written before the run-end event.
109
+
110
+ ### Span → ActionEvent translation
111
+
112
+ The `SpineforgeSpanProcessor` (a custom OTel `SpanProcessor`) receives every completed span and classifies it:
113
+
114
+ | Source | Span attribute | ActionEvent type |
115
+ |--------|---------------|------------------|
116
+ | Groq instrumentor | `gen_ai.system` present | `llm_call` |
117
+ | LangChain instrumentor | `traceloop.span.kind = "llm"` | `llm_call` |
118
+ | LangChain instrumentor | `traceloop.span.kind = "tool"` | `tool_call` |
119
+ | `@track_tool` decorator | `spineforge.tool_call = True` | `tool_call` |
120
+
121
+ Unrecognised spans (HTTP, framework internals) are silently ignored.
122
+
123
+ ---
124
+
125
+ ## Sink Abstraction
126
+
127
+ All events flow through a `Sink` interface:
128
+
129
+ ```python
130
+ class Sink(ABC):
131
+ def emit(self, event: dict) -> None: ...
132
+ def flush(self) -> None: ...
133
+ def shutdown(self) -> None: ...
134
+ ```
135
+
136
+ **Built-in sinks:**
137
+ - `ConsoleSink` — compact, coloured one-liners to stdout
138
+ - `FileSink` — non-blocking JSONL append via background thread + queue
139
+ - `APISink` — batched, non-blocking POSTs to the Spineforge backend with automatic retry and local file fallback
140
+
141
+ **Adding a custom sink (e.g. `SupabaseSink`):**
142
+
143
+ ```python
144
+ from spineforge.sinks import Sink
145
+
146
+ class SupabaseSink(Sink):
147
+ def __init__(self, supabase_url: str, api_key: str):
148
+ self._client = create_supabase_client(supabase_url, api_key)
149
+
150
+ def emit(self, event: dict) -> None:
151
+ table = "runs" if event["event"] == "run" else "actions"
152
+ self._client.table(table).insert(event).execute()
153
+
154
+ def flush(self) -> None:
155
+ pass # or batch-flush pending writes
156
+
157
+ def shutdown(self) -> None:
158
+ self._client.close()
159
+ ```
160
+
161
+ No changes to `instrumentation.py` or `span_processor.py` needed — just add the sink to the list in `__init__.py:init()`.
162
+
163
+ ---
164
+
165
+ ## Adding a New Framework Instrumentor
166
+
167
+ To add support for a new framework (e.g. CrewAI):
168
+
169
+ ### 1. Install the instrumentor package
170
+
171
+ ```bash
172
+ pip install opentelemetry-instrumentation-crewai
173
+ ```
174
+
175
+ ### 2. Add activation in `instrumentation.py`
176
+
177
+ ```python
178
+ # In _instrument_libraries():
179
+ try:
180
+ from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
181
+ CrewAIInstrumentor().instrument(tracer_provider=provider)
182
+ _log_instrumentor_status("CrewAI", True)
183
+ except ImportError:
184
+ _log_instrumentor_status("CrewAI", False, "not installed")
185
+ ```
186
+
187
+ ### 3. Add span classification in `span_processor.py`
188
+
189
+ Add a new condition in `_classify_and_build()` to detect CrewAI-specific span attributes and route them to `_build_llm_event()` or a new tool event builder.
190
+
191
+ ### 4. Add the dependency to `pyproject.toml`
192
+
193
+ ```toml
194
+ "opentelemetry-instrumentation-crewai>=0.35.0",
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Which Example Agents Need `@track_tool`?
200
+
201
+ | Agent | File | `@track_tool` needed? | Why |
202
+ |-------|------|-----------------------|-----|
203
+ | Scratch (raw Groq) | `scratch_agent_demo.py` | **Yes** | The Groq instrumentor only wraps `client.chat.completions.create()` — it has no knowledge of tool execution. `call_tool()` must be decorated. |
204
+ | LangChain | `langchain_agent_demo.py` | **No** | The LangChain instrumentor hooks into LangChain's callback system (`on_tool_start`/`on_tool_end`), which fires automatically when `Tool.run()` is called via `AgentExecutor`. |
205
+
206
+ ---
207
+
208
+ ## Output Formats
209
+
210
+ ### Console (human-readable)
211
+
212
+ ```
213
+ 🦴 Spineforge initialised: agent='research-agent-scratch' spine_id=a1b2c3d4…
214
+ 🦴 [SPINE:a1b2c3d4] RUN started input="What is quantum computing?"
215
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] ⚡ llm_call llama-3.1-8b-instant 342ms ✓ (prompt:128 comp:256)
216
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] 🔧 tool_call search_tool 1204ms ✓
217
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] ⚡ llm_call llama-3.1-8b-instant 567ms ✓ (prompt:384 comp:512)
218
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] RUN completed 4521ms ✓
219
+ ```
220
+
221
+ ### JSONL (`.spineforge/logs/actions.jsonl`)
222
+
223
+ Each line is a JSON object — either a `run` event or an `action` event:
224
+
225
+ ```json
226
+ {"event": "run", "run_id": "...", "spine_id": "...", "agent_name": "...", "status": "running", ...}
227
+ {"event": "action", "action_id": "...", "run_id": "...", "type": "llm_call", "name": "llama-3.1-8b-instant", ...}
228
+ {"event": "action", "action_id": "...", "run_id": "...", "type": "tool_call", "name": "search_tool", ...}
229
+ {"event": "run", "run_id": "...", "status": "success", "ended_at": "...", ...}
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Configuration
235
+
236
+ | Env var | Default | Description |
237
+ |---------|---------|-------------|
238
+ | `SPINEFORGE_DATA_DIR` | CWD | Base directory for `.spineforge/` |
239
+ | `SPINEFORGE_LOG_FILE` | `actions.jsonl` | JSONL log filename |
240
+ | `SPINEFORGE_VERBOSE` | `true` | Enable console output |
241
+ | `SPINEFORGE_TRACE_CONTENT` | `true` | Capture prompt/completion text |
242
+ | `SPINEFORGE_REGISTRY_URL` | `""` (empty) | URL of the Spineforge backend. Empty means offline mode. |
243
+
244
+ All can also be passed as kwargs to `spineforge.init()`.
245
+
246
+ ---
247
+
248
+ ## Project Structure
249
+
250
+ ```
251
+ spineforge-sdk/
252
+ ├── pyproject.toml # Package metadata + dependencies
253
+ ├── README.md # This file
254
+ ├── spineforge/
255
+ │ ├── __init__.py # Public API: init(), track_tool, Spine, Run
256
+ │ ├── config.py # SpineforgeConfig (env-configurable)
257
+ │ ├── models.py # RunEvent, ActionEvent dataclasses
258
+ │ ├── identity.py # Spine ID registry (registry.json)
259
+ │ ├── sinks.py # Sink ABC + ConsoleSink + FileSink
260
+ │ ├── span_processor.py # SpineforgeSpanProcessor (OTel → events)
261
+ │ ├── instrumentation.py # TracerProvider + instrumentor wiring
262
+ │ └── decorators.py # @track_tool
263
+ ├── examples/
264
+ │ ├── scratch_agent_demo.py # scratch.py + spineforge
265
+ │ ├── langchain_agent_demo.py # main.py + spineforge
266
+ │ └── tools.py # Shared tools
267
+ └── .spineforge/ # Created at runtime (gitignored)
268
+ ├── registry.json
269
+ └── logs/
270
+ └── actions.jsonl
271
+ ```
272
+
273
+ ---
274
+
275
+ ## License
276
+
277
+ This software is proprietary and confidential. Unauthorized copying, distribution, reproduction, publication, or modification of this software, via any medium, is strictly prohibited. For internal use only.
@@ -0,0 +1,232 @@
1
+ # Spineforge SDK
2
+
3
+ > AI agent identity and observability — **Okta + Datadog for the AI agent era.**
4
+
5
+ [Homepage](https://spineforge-site.vercel.app/) | [Documentation](https://spineforge-site.vercel.app/docs)
6
+
7
+ Spineforge assigns every AI agent a stable identity (Spine ID), instruments LLM and tool calls via [OpenLLMetry](https://github.com/traceloop/openllmetry) (OpenTelemetry-based auto-instrumentation), and groups events into Runs and Actions that map directly to the hosted Spineforge backend.
8
+
9
+ ---
10
+
11
+ ## Quick Start
12
+
13
+ ### Install
14
+
15
+ ```bash
16
+ pip install spineforge
17
+
18
+ # For running the example agents
19
+ pip install "spineforge[examples]"
20
+ ```
21
+
22
+ ### Usage (3 lines to integrate)
23
+
24
+ ```python
25
+ import spineforge
26
+
27
+ # 1. Init — resolves/creates a stable Spine ID for this agent
28
+ spine = spineforge.init(agent_name="my-research-agent")
29
+
30
+ # 2. Wrap your agent's top-level invocation in a run
31
+ with spine.run(input=user_query) as run:
32
+ result = run_agent(user_query)
33
+ run.set_output(result)
34
+ ```
35
+
36
+ That's it. Spineforge auto-instruments LLM calls (Groq, LangChain) in the background via OpenTelemetry. No code changes to your agent logic.
37
+
38
+ ### For tool functions not auto-captured
39
+
40
+ ```python
41
+ @spineforge.track_tool
42
+ def call_tool(name: str, query: str) -> str:
43
+ """Only needed for raw SDK agents — LangChain tools are auto-captured."""
44
+ return tools[name].run(query)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## How It Works
50
+
51
+ ### `init(agent_name)` → `Spine`
52
+
53
+ 1. **Resolves identity** — looks up `agent_name` in `.spineforge/registry.json`. If it exists, returns the same Spine ID. If not, generates a new UUID-4 and persists it.
54
+ 2. **Wires instrumentation** — creates an OpenTelemetry `TracerProvider`, registers a custom `SpineforgeSpanProcessor`, and activates OpenLLMetry instrumentors for installed libraries (Groq, LangChain).
55
+ 3. **Returns a `Spine` handle** — use `spine.run()` to group actions into runs.
56
+
57
+ ### `spine.run(input=...)` → context manager
58
+
59
+ The `with spine.run(...)` block:
60
+ - Generates a `run_id` and sets it in a `ContextVar` so all spans created during the block are associated with this run.
61
+ - Emits a `RunEvent(status="running")` at entry.
62
+ - Emits a `RunEvent(status="success")` or `RunEvent(status="error")` at exit.
63
+ - Force-flushes the OTel pipeline to ensure all action spans are written before the run-end event.
64
+
65
+ ### Span → ActionEvent translation
66
+
67
+ The `SpineforgeSpanProcessor` (a custom OTel `SpanProcessor`) receives every completed span and classifies it:
68
+
69
+ | Source | Span attribute | ActionEvent type |
70
+ |--------|---------------|------------------|
71
+ | Groq instrumentor | `gen_ai.system` present | `llm_call` |
72
+ | LangChain instrumentor | `traceloop.span.kind = "llm"` | `llm_call` |
73
+ | LangChain instrumentor | `traceloop.span.kind = "tool"` | `tool_call` |
74
+ | `@track_tool` decorator | `spineforge.tool_call = True` | `tool_call` |
75
+
76
+ Unrecognised spans (HTTP, framework internals) are silently ignored.
77
+
78
+ ---
79
+
80
+ ## Sink Abstraction
81
+
82
+ All events flow through a `Sink` interface:
83
+
84
+ ```python
85
+ class Sink(ABC):
86
+ def emit(self, event: dict) -> None: ...
87
+ def flush(self) -> None: ...
88
+ def shutdown(self) -> None: ...
89
+ ```
90
+
91
+ **Built-in sinks:**
92
+ - `ConsoleSink` — compact, coloured one-liners to stdout
93
+ - `FileSink` — non-blocking JSONL append via background thread + queue
94
+ - `APISink` — batched, non-blocking POSTs to the Spineforge backend with automatic retry and local file fallback
95
+
96
+ **Adding a custom sink (e.g. `SupabaseSink`):**
97
+
98
+ ```python
99
+ from spineforge.sinks import Sink
100
+
101
+ class SupabaseSink(Sink):
102
+ def __init__(self, supabase_url: str, api_key: str):
103
+ self._client = create_supabase_client(supabase_url, api_key)
104
+
105
+ def emit(self, event: dict) -> None:
106
+ table = "runs" if event["event"] == "run" else "actions"
107
+ self._client.table(table).insert(event).execute()
108
+
109
+ def flush(self) -> None:
110
+ pass # or batch-flush pending writes
111
+
112
+ def shutdown(self) -> None:
113
+ self._client.close()
114
+ ```
115
+
116
+ No changes to `instrumentation.py` or `span_processor.py` needed — just add the sink to the list in `__init__.py:init()`.
117
+
118
+ ---
119
+
120
+ ## Adding a New Framework Instrumentor
121
+
122
+ To add support for a new framework (e.g. CrewAI):
123
+
124
+ ### 1. Install the instrumentor package
125
+
126
+ ```bash
127
+ pip install opentelemetry-instrumentation-crewai
128
+ ```
129
+
130
+ ### 2. Add activation in `instrumentation.py`
131
+
132
+ ```python
133
+ # In _instrument_libraries():
134
+ try:
135
+ from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
136
+ CrewAIInstrumentor().instrument(tracer_provider=provider)
137
+ _log_instrumentor_status("CrewAI", True)
138
+ except ImportError:
139
+ _log_instrumentor_status("CrewAI", False, "not installed")
140
+ ```
141
+
142
+ ### 3. Add span classification in `span_processor.py`
143
+
144
+ Add a new condition in `_classify_and_build()` to detect CrewAI-specific span attributes and route them to `_build_llm_event()` or a new tool event builder.
145
+
146
+ ### 4. Add the dependency to `pyproject.toml`
147
+
148
+ ```toml
149
+ "opentelemetry-instrumentation-crewai>=0.35.0",
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Which Example Agents Need `@track_tool`?
155
+
156
+ | Agent | File | `@track_tool` needed? | Why |
157
+ |-------|------|-----------------------|-----|
158
+ | Scratch (raw Groq) | `scratch_agent_demo.py` | **Yes** | The Groq instrumentor only wraps `client.chat.completions.create()` — it has no knowledge of tool execution. `call_tool()` must be decorated. |
159
+ | LangChain | `langchain_agent_demo.py` | **No** | The LangChain instrumentor hooks into LangChain's callback system (`on_tool_start`/`on_tool_end`), which fires automatically when `Tool.run()` is called via `AgentExecutor`. |
160
+
161
+ ---
162
+
163
+ ## Output Formats
164
+
165
+ ### Console (human-readable)
166
+
167
+ ```
168
+ 🦴 Spineforge initialised: agent='research-agent-scratch' spine_id=a1b2c3d4…
169
+ 🦴 [SPINE:a1b2c3d4] RUN started input="What is quantum computing?"
170
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] ⚡ llm_call llama-3.1-8b-instant 342ms ✓ (prompt:128 comp:256)
171
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] 🔧 tool_call search_tool 1204ms ✓
172
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] ⚡ llm_call llama-3.1-8b-instant 567ms ✓ (prompt:384 comp:512)
173
+ 🦴 [SPINE:a1b2c3d4] [RUN:e5f6g7h8] RUN completed 4521ms ✓
174
+ ```
175
+
176
+ ### JSONL (`.spineforge/logs/actions.jsonl`)
177
+
178
+ Each line is a JSON object — either a `run` event or an `action` event:
179
+
180
+ ```json
181
+ {"event": "run", "run_id": "...", "spine_id": "...", "agent_name": "...", "status": "running", ...}
182
+ {"event": "action", "action_id": "...", "run_id": "...", "type": "llm_call", "name": "llama-3.1-8b-instant", ...}
183
+ {"event": "action", "action_id": "...", "run_id": "...", "type": "tool_call", "name": "search_tool", ...}
184
+ {"event": "run", "run_id": "...", "status": "success", "ended_at": "...", ...}
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Configuration
190
+
191
+ | Env var | Default | Description |
192
+ |---------|---------|-------------|
193
+ | `SPINEFORGE_DATA_DIR` | CWD | Base directory for `.spineforge/` |
194
+ | `SPINEFORGE_LOG_FILE` | `actions.jsonl` | JSONL log filename |
195
+ | `SPINEFORGE_VERBOSE` | `true` | Enable console output |
196
+ | `SPINEFORGE_TRACE_CONTENT` | `true` | Capture prompt/completion text |
197
+ | `SPINEFORGE_REGISTRY_URL` | `""` (empty) | URL of the Spineforge backend. Empty means offline mode. |
198
+
199
+ All can also be passed as kwargs to `spineforge.init()`.
200
+
201
+ ---
202
+
203
+ ## Project Structure
204
+
205
+ ```
206
+ spineforge-sdk/
207
+ ├── pyproject.toml # Package metadata + dependencies
208
+ ├── README.md # This file
209
+ ├── spineforge/
210
+ │ ├── __init__.py # Public API: init(), track_tool, Spine, Run
211
+ │ ├── config.py # SpineforgeConfig (env-configurable)
212
+ │ ├── models.py # RunEvent, ActionEvent dataclasses
213
+ │ ├── identity.py # Spine ID registry (registry.json)
214
+ │ ├── sinks.py # Sink ABC + ConsoleSink + FileSink
215
+ │ ├── span_processor.py # SpineforgeSpanProcessor (OTel → events)
216
+ │ ├── instrumentation.py # TracerProvider + instrumentor wiring
217
+ │ └── decorators.py # @track_tool
218
+ ├── examples/
219
+ │ ├── scratch_agent_demo.py # scratch.py + spineforge
220
+ │ ├── langchain_agent_demo.py # main.py + spineforge
221
+ │ └── tools.py # Shared tools
222
+ └── .spineforge/ # Created at runtime (gitignored)
223
+ ├── registry.json
224
+ └── logs/
225
+ └── actions.jsonl
226
+ ```
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ This software is proprietary and confidential. Unauthorized copying, distribution, reproduction, publication, or modification of this software, via any medium, is strictly prohibited. For internal use only.
@@ -0,0 +1,124 @@
1
+ # Spineforge — Scope Model Reference
2
+
3
+ ## Overview
4
+
5
+ Spineforge uses **scope-based access control** — direct scope grants per Spine ID.
6
+ There are no roles, no groups, no RBAC layer. Each agent gets a flat set of
7
+ scope strings that determine what it is allowed to do.
8
+
9
+ ## Scope Format
10
+
11
+ Scopes are **free-form strings** matched by **exact equality** — no wildcards,
12
+ no pattern matching, no parsing. The recommended (not enforced) convention is:
13
+
14
+ | Pattern | Use case |
15
+ |---|---|
16
+ | `lease:<resource>` | Credential leasing (e.g. `lease:groq-api-key`) |
17
+ | `call:<target-spine-id>:<action>` | Agent-to-agent calls (e.g. `call:abc123:echo`) |
18
+
19
+ You can use any string you like. The registry stores and compares them as-is.
20
+
21
+ ## Lifecycle
22
+
23
+ ### 1. Declaration (registration time)
24
+
25
+ Scopes are declared by the **developer** — not the agent — at registration time:
26
+
27
+ ```python
28
+ # SDK
29
+ spine = spineforge.init(agent_name="my-agent")
30
+ # Scopes are set via the registry's /agents/register endpoint
31
+ # with allowed_scopes=["lease:groq-api-key", "call:<target>:echo"]
32
+ ```
33
+
34
+ Or via the `/agents/register` HTTP endpoint:
35
+
36
+ ```json
37
+ {
38
+ "agent_name": "my-agent",
39
+ "public_key": "-----BEGIN PUBLIC KEY-----\n...",
40
+ "allowed_scopes": ["lease:groq-api-key", "call:target-agent:echo"]
41
+ }
42
+ ```
43
+
44
+ ### 2. Token request (runtime)
45
+
46
+ At runtime, the agent requests a token with a **subset** of its allowed scopes:
47
+
48
+ ```python
49
+ token = spine.request_token(
50
+ scopes=["lease:groq-api-key"],
51
+ aud="registry", # or a target agent's spine_id
52
+ )
53
+ ```
54
+
55
+ The registry checks: `requested ⊆ allowed`. If not, the entire request is
56
+ rejected with `invalid_scope`. **No scope narrowing** — the token either
57
+ carries exactly what was requested, or it isn't issued at all.
58
+
59
+ ### 3. Enforcement (at the resource)
60
+
61
+ The resource (registry lease endpoint, or a receiving agent) checks the
62
+ token's `scope` claim for the required scope string:
63
+
64
+ - **Lease endpoint**: checks `f"lease:{secret_name}"` is in scope
65
+ - **Receiving agent**: checks the required scope string using `verify_token()`
66
+
67
+ ### 4. Scope updates
68
+
69
+ Changing an agent's allowed scopes in the DB affects **future token issuance
70
+ immediately**. Already-issued tokens remain valid until their own short TTL
71
+ expires (default: 15 minutes). This follows the same principle as credential
72
+ revocation — no instant-kill mechanism.
73
+
74
+ ## Token Structure
75
+
76
+ Every access token carries these claims:
77
+
78
+ | Claim | Type | Description |
79
+ |---|---|---|
80
+ | `sub` | string (UUID) | The agent's Spine ID |
81
+ | `name` | string | The agent's human-readable name |
82
+ | `scope` | string | Space-separated list of granted scopes |
83
+ | `aud` | string | `"registry"` for lease flows, or a target agent's spine_id |
84
+ | `iat` | int | Issued-at timestamp (epoch seconds) |
85
+ | `exp` | int | Expiry timestamp (epoch seconds) |
86
+ | `iss` | string | Always `"spineforge"` |
87
+
88
+ Tokens are signed with the registry's **Ed25519 private key** (EdDSA algorithm).
89
+ The corresponding public key is available at `/.well-known/jwks.json`.
90
+
91
+ ## Offline Verification
92
+
93
+ Receiving agents verify tokens **locally** — no round-trip to the registry:
94
+
95
+ ```python
96
+ from spineforge.verify import verify_token
97
+
98
+ claims = verify_token(
99
+ token,
100
+ expected_aud="my-spine-id",
101
+ required_scope="call:my-spine-id:some_action",
102
+ registry_url="https://registry.spineforge.dev",
103
+ )
104
+ # The public key is fetched once from /.well-known/jwks.json and cached.
105
+ ```
106
+
107
+ Verification checks (in order):
108
+ 1. **Signature** — Ed25519 using the registry's public key
109
+ 2. **Expiry** — `exp` must be in the future
110
+ 3. **Issuer** — must be `"spineforge"`
111
+ 4. **Audience** — must match `expected_aud`
112
+ 5. **Scope** — `required_scope` must be present in the `scope` claim
113
+
114
+ ## Design Principles
115
+
116
+ - **Scopes are developer-declared, not agent-defined.** An agent can never
117
+ create or expand its own scopes, even if compromised at runtime.
118
+ - **No scope narrowing.** If any requested scope isn't allowed, the entire
119
+ token request fails. The SDK never has to guess what it actually got.
120
+ - **External systems stay scope-blind.** Provider APIs (Groq, Stripe, etc.)
121
+ only ever see a working credential — no Spine ID, scope, or Spineforge
122
+ metadata leaks past the vault boundary.
123
+ - **Mutual auth is out of scope.** B verifies A. A does not verify B.
124
+ TLS on B's endpoint is sufficient for this pass.