ghost-layer 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.
- ghost_layer-0.1.0/LICENSE +21 -0
- ghost_layer-0.1.0/PKG-INFO +196 -0
- ghost_layer-0.1.0/README.md +164 -0
- ghost_layer-0.1.0/ghost/__init__.py +27 -0
- ghost_layer-0.1.0/ghost/cli.py +206 -0
- ghost_layer-0.1.0/ghost/crypto.py +62 -0
- ghost_layer-0.1.0/ghost/proxy.py +109 -0
- ghost_layer-0.1.0/ghost/session.py +307 -0
- ghost_layer-0.1.0/ghost/store.py +148 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/PKG-INFO +196 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/SOURCES.txt +16 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/dependency_links.txt +1 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/entry_points.txt +2 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/requires.txt +7 -0
- ghost_layer-0.1.0/ghost_layer.egg-info/top_level.txt +1 -0
- ghost_layer-0.1.0/pyproject.toml +48 -0
- ghost_layer-0.1.0/setup.cfg +4 -0
- ghost_layer-0.1.0/tests/test_ghost.py +197 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timothy Walton / Script Master Labs LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghost-layer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ephemeral execution layer for autonomous AI agents — scoped credentials, cryptographic residue.
|
|
5
|
+
Author-email: Timothy Walton <ScriptMasterLabs@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://scriptmasterlabs.com/stack/ghost
|
|
8
|
+
Project-URL: Repository, https://github.com/timwal78/ghost-layer
|
|
9
|
+
Project-URL: Documentation, https://github.com/timwal78/ghost-layer/blob/main/docs/ARCHITECTURE.md
|
|
10
|
+
Project-URL: Issues, https://github.com/timwal78/ghost-layer/issues
|
|
11
|
+
Keywords: ai-agents,autonomous,ephemeral,credentials,cryptography,audit,x402
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: click>=8.0
|
|
26
|
+
Requires-Dist: cryptography>=41.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Requires-Dist: twine; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# GHOST
|
|
34
|
+
### The Spectral Execution Layer for Autonomous Agents
|
|
35
|
+
|
|
36
|
+
> *"Most agents die in the light. Ours operate in the dark."*
|
|
37
|
+
|
|
38
|
+
Give your AI agent a **body that vanishes**. GHOST is an ephemeral execution
|
|
39
|
+
layer: an agent declares intent, spawns a short-lived signing key, executes
|
|
40
|
+
**scoped** actions through an intercept that records **cryptographic residue**,
|
|
41
|
+
then evaporates — leaving a tamper-evident audit trail and **zero standing
|
|
42
|
+
credentials**.
|
|
43
|
+
|
|
44
|
+
[](https://github.com/timwal78/ghost-layer)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
[](pyproject.toml)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## The Problem
|
|
51
|
+
|
|
52
|
+
You gave your agent AWS keys. Now you're watching CloudTrail at 3am.
|
|
53
|
+
|
|
54
|
+
Agents are **ephemeral bursts of intent**. Humans are persistent. Yet today's
|
|
55
|
+
agents execute with persistent, human-shaped credentials and unbounded scope.
|
|
56
|
+
One leaked key compromises everything, and there's no signed proof of *why* the
|
|
57
|
+
agent did what it did.
|
|
58
|
+
|
|
59
|
+
## The Inversion
|
|
60
|
+
|
|
61
|
+
| Human model | Agent model (GHOST) |
|
|
62
|
+
|---|---|
|
|
63
|
+
| log in → do stuff → log out | declare intent → **spawn** → execute → **evaporate** → leave **residue** |
|
|
64
|
+
| session persists | session auto-expires (TTL) |
|
|
65
|
+
| broad standing access | scoped to declared tools |
|
|
66
|
+
| audit logs (unsigned) | Ed25519-signed, tamper-evident chain |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## The Ritual (Quickstart)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install ghost-layer
|
|
74
|
+
|
|
75
|
+
ghost spawn --intent "deploy_staging" --ttl 300 --scope aws_ec2
|
|
76
|
+
ghost act --tool aws_ec2 --action RunInstances --session-id gh_9ddb...
|
|
77
|
+
ghost evaporate --session-id gh_9ddb...
|
|
78
|
+
ghost replay --session-id gh_9ddb...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
What just happened: your agent never held a standing credential. The session
|
|
82
|
+
lived under a second. The residue is **Ed25519-signed** and immutable — replay
|
|
83
|
+
it and verify exactly why that instance spawned.
|
|
84
|
+
|
|
85
|
+
Out-of-scope calls are refused before they run:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ghost act --tool stripe --action CreateCharge --session-id gh_9ddb...
|
|
89
|
+
# DENIED (scope): tool 'stripe' not in session scopes ['aws_ec2'] (exit 2)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Five Commands
|
|
95
|
+
|
|
96
|
+
| Command | What it does |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `ghost spawn` | Mint an ephemeral session + fresh Ed25519 keypair, TTL countdown begins |
|
|
99
|
+
| `ghost possess` | Bind an agent to the session via the intercept proxy |
|
|
100
|
+
| `ghost act` | Record a **scoped, signed** action (blocked if out-of-scope or expired) |
|
|
101
|
+
| `ghost evaporate` | Shred the key, sign the whole action chain, finalize the residue |
|
|
102
|
+
| `ghost replay` | Re-verify every signature + the root chain signature |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Use It In Code (SDK-agnostic)
|
|
107
|
+
|
|
108
|
+
The transport is injected, so GHOST wraps **any** HTTP client or agent
|
|
109
|
+
framework — LangChain, the OpenAI SDK, raw `requests`/`httpx`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from ghost import spawn, possess, evaporate, replay
|
|
113
|
+
from ghost.store import ResidueStore
|
|
114
|
+
import requests
|
|
115
|
+
|
|
116
|
+
store = ResidueStore()
|
|
117
|
+
session = spawn(store, intent="enrich_lead", ttl=120, scopes=["httpbin"])
|
|
118
|
+
|
|
119
|
+
def transport(method, url, headers=None, **kw):
|
|
120
|
+
return requests.request(method, url, headers=headers, **kw)
|
|
121
|
+
|
|
122
|
+
proxy = possess(store, session["session_id"], transport, token="ghtok_demo")
|
|
123
|
+
|
|
124
|
+
# Auth headers the agent sets are STRIPPED; the ghost token is injected.
|
|
125
|
+
proxy.request("POST", "https://httpbin.org/post",
|
|
126
|
+
tool="httpbin", action="submit",
|
|
127
|
+
headers={"Authorization": "Bearer WILL_BE_STRIPPED"})
|
|
128
|
+
|
|
129
|
+
evaporate(store, session["session_id"])
|
|
130
|
+
print(replay(store, session["session_id"])["verified"]) # True
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
See [`examples/`](examples/) for a LangChain-style agent and an
|
|
134
|
+
**x402 / XRPL payment agent** that settles a micropayment through a single
|
|
135
|
+
60-second ghost body.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Why It's Tamper-Evident
|
|
140
|
+
|
|
141
|
+
Every action is signed over a canonical hash binding `session_id`, sequence,
|
|
142
|
+
tool, action, and the hashes of params/response. At `evaporate`, a **root
|
|
143
|
+
signature** covers the ordered chain. Change one byte of the residue and
|
|
144
|
+
`ghost replay` reports `verified: false`. The private key is gone by then — it
|
|
145
|
+
**cannot** be re-signed.
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
spawn ─▶ keypair (priv on disk 0600, pub in residue)
|
|
149
|
+
│
|
|
150
|
+
├─ act ─▶ sign(payload) ─▶ residue row
|
|
151
|
+
├─ act ─▶ sign(payload) ─▶ residue row
|
|
152
|
+
│
|
|
153
|
+
evaporate ─▶ sign(chain) = root_sig ─▶ shred priv key
|
|
154
|
+
│
|
|
155
|
+
replay ─▶ verify(each) + verify(root) ✓ tamper-evident
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Where It Fits
|
|
161
|
+
|
|
162
|
+
GHOST is the execution-safety layer beneath the **x402 agentic web**. It lets
|
|
163
|
+
autonomous agents trigger payment rails ([NEXUS-402](https://nexus-402.com),
|
|
164
|
+
402Proof, XAH Portal) and act on [SqueezeOS](https://scriptmasterlabs.com)
|
|
165
|
+
signals **without ever holding a standing key** — every autonomous move leaves
|
|
166
|
+
a signed receipt.
|
|
167
|
+
|
|
168
|
+
Full catalog: **[scriptmasterlabs.com/stack](https://scriptmasterlabs.com/stack)**
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Status & Roadmap
|
|
173
|
+
|
|
174
|
+
- [x] Python CLI — spawn / possess / act / evaporate / replay
|
|
175
|
+
- [x] SQLite residue store, Ed25519 signing, scope + TTL enforcement
|
|
176
|
+
- [x] Intercept proxy (SDK-agnostic), tamper-detection tests (17 passing)
|
|
177
|
+
- [ ] Rust proxy core (performance)
|
|
178
|
+
- [ ] Native LangChain / OpenAI tool wrappers
|
|
179
|
+
- [ ] Pre/post validation hooks, webhook notifications
|
|
180
|
+
- [ ] Optional cloud-hosted proxy
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Install from source
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git clone https://github.com/timwal78/ghost-layer
|
|
188
|
+
cd ghost-layer
|
|
189
|
+
pip install -e ".[dev]"
|
|
190
|
+
pytest -q # 17 passed
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
Built by **Script Master Labs LLC** · Disabled U.S. Army Veteran–Owned (SDVOSB) · Kinston, NC
|
|
196
|
+
Docs: [ARCHITECTURE.md](docs/ARCHITECTURE.md) · [SECURITY.md](docs/SECURITY.md) · MIT License
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# GHOST
|
|
2
|
+
### The Spectral Execution Layer for Autonomous Agents
|
|
3
|
+
|
|
4
|
+
> *"Most agents die in the light. Ours operate in the dark."*
|
|
5
|
+
|
|
6
|
+
Give your AI agent a **body that vanishes**. GHOST is an ephemeral execution
|
|
7
|
+
layer: an agent declares intent, spawns a short-lived signing key, executes
|
|
8
|
+
**scoped** actions through an intercept that records **cryptographic residue**,
|
|
9
|
+
then evaporates — leaving a tamper-evident audit trail and **zero standing
|
|
10
|
+
credentials**.
|
|
11
|
+
|
|
12
|
+
[](https://github.com/timwal78/ghost-layer)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](pyproject.toml)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The Problem
|
|
19
|
+
|
|
20
|
+
You gave your agent AWS keys. Now you're watching CloudTrail at 3am.
|
|
21
|
+
|
|
22
|
+
Agents are **ephemeral bursts of intent**. Humans are persistent. Yet today's
|
|
23
|
+
agents execute with persistent, human-shaped credentials and unbounded scope.
|
|
24
|
+
One leaked key compromises everything, and there's no signed proof of *why* the
|
|
25
|
+
agent did what it did.
|
|
26
|
+
|
|
27
|
+
## The Inversion
|
|
28
|
+
|
|
29
|
+
| Human model | Agent model (GHOST) |
|
|
30
|
+
|---|---|
|
|
31
|
+
| log in → do stuff → log out | declare intent → **spawn** → execute → **evaporate** → leave **residue** |
|
|
32
|
+
| session persists | session auto-expires (TTL) |
|
|
33
|
+
| broad standing access | scoped to declared tools |
|
|
34
|
+
| audit logs (unsigned) | Ed25519-signed, tamper-evident chain |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## The Ritual (Quickstart)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ghost-layer
|
|
42
|
+
|
|
43
|
+
ghost spawn --intent "deploy_staging" --ttl 300 --scope aws_ec2
|
|
44
|
+
ghost act --tool aws_ec2 --action RunInstances --session-id gh_9ddb...
|
|
45
|
+
ghost evaporate --session-id gh_9ddb...
|
|
46
|
+
ghost replay --session-id gh_9ddb...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
What just happened: your agent never held a standing credential. The session
|
|
50
|
+
lived under a second. The residue is **Ed25519-signed** and immutable — replay
|
|
51
|
+
it and verify exactly why that instance spawned.
|
|
52
|
+
|
|
53
|
+
Out-of-scope calls are refused before they run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ghost act --tool stripe --action CreateCharge --session-id gh_9ddb...
|
|
57
|
+
# DENIED (scope): tool 'stripe' not in session scopes ['aws_ec2'] (exit 2)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Five Commands
|
|
63
|
+
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `ghost spawn` | Mint an ephemeral session + fresh Ed25519 keypair, TTL countdown begins |
|
|
67
|
+
| `ghost possess` | Bind an agent to the session via the intercept proxy |
|
|
68
|
+
| `ghost act` | Record a **scoped, signed** action (blocked if out-of-scope or expired) |
|
|
69
|
+
| `ghost evaporate` | Shred the key, sign the whole action chain, finalize the residue |
|
|
70
|
+
| `ghost replay` | Re-verify every signature + the root chain signature |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Use It In Code (SDK-agnostic)
|
|
75
|
+
|
|
76
|
+
The transport is injected, so GHOST wraps **any** HTTP client or agent
|
|
77
|
+
framework — LangChain, the OpenAI SDK, raw `requests`/`httpx`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from ghost import spawn, possess, evaporate, replay
|
|
81
|
+
from ghost.store import ResidueStore
|
|
82
|
+
import requests
|
|
83
|
+
|
|
84
|
+
store = ResidueStore()
|
|
85
|
+
session = spawn(store, intent="enrich_lead", ttl=120, scopes=["httpbin"])
|
|
86
|
+
|
|
87
|
+
def transport(method, url, headers=None, **kw):
|
|
88
|
+
return requests.request(method, url, headers=headers, **kw)
|
|
89
|
+
|
|
90
|
+
proxy = possess(store, session["session_id"], transport, token="ghtok_demo")
|
|
91
|
+
|
|
92
|
+
# Auth headers the agent sets are STRIPPED; the ghost token is injected.
|
|
93
|
+
proxy.request("POST", "https://httpbin.org/post",
|
|
94
|
+
tool="httpbin", action="submit",
|
|
95
|
+
headers={"Authorization": "Bearer WILL_BE_STRIPPED"})
|
|
96
|
+
|
|
97
|
+
evaporate(store, session["session_id"])
|
|
98
|
+
print(replay(store, session["session_id"])["verified"]) # True
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
See [`examples/`](examples/) for a LangChain-style agent and an
|
|
102
|
+
**x402 / XRPL payment agent** that settles a micropayment through a single
|
|
103
|
+
60-second ghost body.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Why It's Tamper-Evident
|
|
108
|
+
|
|
109
|
+
Every action is signed over a canonical hash binding `session_id`, sequence,
|
|
110
|
+
tool, action, and the hashes of params/response. At `evaporate`, a **root
|
|
111
|
+
signature** covers the ordered chain. Change one byte of the residue and
|
|
112
|
+
`ghost replay` reports `verified: false`. The private key is gone by then — it
|
|
113
|
+
**cannot** be re-signed.
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
spawn ─▶ keypair (priv on disk 0600, pub in residue)
|
|
117
|
+
│
|
|
118
|
+
├─ act ─▶ sign(payload) ─▶ residue row
|
|
119
|
+
├─ act ─▶ sign(payload) ─▶ residue row
|
|
120
|
+
│
|
|
121
|
+
evaporate ─▶ sign(chain) = root_sig ─▶ shred priv key
|
|
122
|
+
│
|
|
123
|
+
replay ─▶ verify(each) + verify(root) ✓ tamper-evident
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Where It Fits
|
|
129
|
+
|
|
130
|
+
GHOST is the execution-safety layer beneath the **x402 agentic web**. It lets
|
|
131
|
+
autonomous agents trigger payment rails ([NEXUS-402](https://nexus-402.com),
|
|
132
|
+
402Proof, XAH Portal) and act on [SqueezeOS](https://scriptmasterlabs.com)
|
|
133
|
+
signals **without ever holding a standing key** — every autonomous move leaves
|
|
134
|
+
a signed receipt.
|
|
135
|
+
|
|
136
|
+
Full catalog: **[scriptmasterlabs.com/stack](https://scriptmasterlabs.com/stack)**
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Status & Roadmap
|
|
141
|
+
|
|
142
|
+
- [x] Python CLI — spawn / possess / act / evaporate / replay
|
|
143
|
+
- [x] SQLite residue store, Ed25519 signing, scope + TTL enforcement
|
|
144
|
+
- [x] Intercept proxy (SDK-agnostic), tamper-detection tests (17 passing)
|
|
145
|
+
- [ ] Rust proxy core (performance)
|
|
146
|
+
- [ ] Native LangChain / OpenAI tool wrappers
|
|
147
|
+
- [ ] Pre/post validation hooks, webhook notifications
|
|
148
|
+
- [ ] Optional cloud-hosted proxy
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Install from source
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git clone https://github.com/timwal78/ghost-layer
|
|
156
|
+
cd ghost-layer
|
|
157
|
+
pip install -e ".[dev]"
|
|
158
|
+
pytest -q # 17 passed
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
Built by **Script Master Labs LLC** · Disabled U.S. Army Veteran–Owned (SDVOSB) · Kinston, NC
|
|
164
|
+
Docs: [ARCHITECTURE.md](docs/ARCHITECTURE.md) · [SECURITY.md](docs/SECURITY.md) · MIT License
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""GHOST — The Spectral Execution Layer for Autonomous Agents.
|
|
2
|
+
|
|
3
|
+
Ephemeral, scoped, signed execution for AI agents. Spawn a short-lived session,
|
|
4
|
+
route the agent's actions through an intercept that records cryptographic
|
|
5
|
+
residue, then evaporate — leaving a tamper-evident audit trail.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__author__ = "Timothy Walton (Script Master Labs LLC)"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
|
|
14
|
+
from .session import act, evaporate, replay, spawn # noqa: E402
|
|
15
|
+
from .store import ResidueStore # noqa: E402
|
|
16
|
+
from .proxy import GhostProxy, possess # noqa: E402
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"spawn",
|
|
20
|
+
"act",
|
|
21
|
+
"evaporate",
|
|
22
|
+
"replay",
|
|
23
|
+
"possess",
|
|
24
|
+
"GhostProxy",
|
|
25
|
+
"ResidueStore",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""GHOST command-line interface.
|
|
2
|
+
|
|
3
|
+
Give your AI agent a body that vanishes. Thin Click layer over ghost.session.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from . import session as _session
|
|
16
|
+
from .session import (
|
|
17
|
+
ExpiredSessionError,
|
|
18
|
+
ScopeError,
|
|
19
|
+
SessionError,
|
|
20
|
+
)
|
|
21
|
+
from .store import ResidueStore
|
|
22
|
+
|
|
23
|
+
GREEN = "green"
|
|
24
|
+
GOLD = "yellow"
|
|
25
|
+
PINK = "magenta"
|
|
26
|
+
CYAN = "cyan"
|
|
27
|
+
RED = "red"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _emit(obj: dict, color: str) -> None:
|
|
31
|
+
click.secho(json.dumps(obj, indent=2), fg=color)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group()
|
|
35
|
+
@click.version_option(version=__version__, prog_name="ghost")
|
|
36
|
+
@click.option("--db", type=click.Path(), default=None, help="Override residue DB path.")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def cli(ctx: click.Context, db: Optional[str]) -> None:
|
|
39
|
+
"""GHOST — Ephemeral Execution Layer for Autonomous Agents.
|
|
40
|
+
|
|
41
|
+
Declare intent -> spawn -> execute -> evaporate -> leave signed residue.
|
|
42
|
+
"""
|
|
43
|
+
ctx.ensure_object(dict)
|
|
44
|
+
ctx.obj["store"] = ResidueStore(db)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@cli.command()
|
|
48
|
+
@click.option("--intent", required=True, help="Human-readable intent, e.g. 'deploy_staging'.")
|
|
49
|
+
@click.option("--ttl", default=300, show_default=True, help="Session lifetime in seconds.")
|
|
50
|
+
@click.option("--scope", multiple=True, help="Allowed tool scope (repeatable).")
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def spawn(ctx: click.Context, intent: str, ttl: int, scope: tuple) -> None:
|
|
53
|
+
"""Spawn an ephemeral session with a fresh signing key."""
|
|
54
|
+
store: ResidueStore = ctx.obj["store"]
|
|
55
|
+
result = _session.spawn(store, intent=intent, ttl=ttl, scopes=list(scope))
|
|
56
|
+
_emit(result, GREEN)
|
|
57
|
+
click.secho(
|
|
58
|
+
f"\nEphemeral session spawned: {result['session_id']}", fg=GREEN, bold=True
|
|
59
|
+
)
|
|
60
|
+
click.secho(
|
|
61
|
+
f" TTL {ttl}s | scopes: {', '.join(scope) if scope else '(none)'}", fg=CYAN
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@cli.command()
|
|
66
|
+
@click.option("--agent", required=True, help="Agent identifier, e.g. openai://gpt-4.")
|
|
67
|
+
@click.option("--session-id", required=True, help="Session from 'ghost spawn'.")
|
|
68
|
+
@click.option("--port", default=9999, show_default=True, help="Proxy listen port.")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def possess(ctx: click.Context, agent: str, session_id: str, port: int) -> None:
|
|
71
|
+
"""Bind an agent to the session via the local intercept proxy."""
|
|
72
|
+
store: ResidueStore = ctx.obj["store"]
|
|
73
|
+
row = store.get_session(session_id)
|
|
74
|
+
if row is None:
|
|
75
|
+
click.secho(f"Error: session {session_id} not found", fg=RED)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
if row["evaporated_at"]:
|
|
78
|
+
click.secho(f"Error: session {session_id} already evaporated", fg=RED)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
click.secho(f"Proxy ready on localhost:{port}", fg=CYAN, bold=True)
|
|
81
|
+
click.secho(f" agent : {agent}", fg=CYAN)
|
|
82
|
+
click.secho(f" session : {session_id}", fg=CYAN)
|
|
83
|
+
click.secho(f" scopes : {row['scopes'] or '(none)'}", fg=CYAN)
|
|
84
|
+
click.secho("\n Route agent HTTP through GhostProxy (see examples/). ", fg=GOLD)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@cli.command()
|
|
88
|
+
@click.option("--tool", required=True, help="Tool name, e.g. aws_ec2.")
|
|
89
|
+
@click.option("--action", required=True, help="Action name, e.g. RunInstances.")
|
|
90
|
+
@click.option("--params", type=click.File("r"), default=None, help="JSON params file.")
|
|
91
|
+
@click.option("--session-id", required=True, help="Target session.")
|
|
92
|
+
@click.option("--no-scope", is_flag=True, help="Disable scope enforcement.")
|
|
93
|
+
@click.pass_context
|
|
94
|
+
def act(
|
|
95
|
+
ctx: click.Context,
|
|
96
|
+
tool: str,
|
|
97
|
+
action: str,
|
|
98
|
+
params: Optional[click.File],
|
|
99
|
+
session_id: str,
|
|
100
|
+
no_scope: bool,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Record a scoped, signed action against the session."""
|
|
103
|
+
store: ResidueStore = ctx.obj["store"]
|
|
104
|
+
payload = json.load(params) if params else {}
|
|
105
|
+
try:
|
|
106
|
+
result = _session.act(
|
|
107
|
+
store,
|
|
108
|
+
session_id,
|
|
109
|
+
tool=tool,
|
|
110
|
+
action=action,
|
|
111
|
+
params=payload,
|
|
112
|
+
enforce_scope=not no_scope,
|
|
113
|
+
)
|
|
114
|
+
except ScopeError as e:
|
|
115
|
+
click.secho(f"DENIED (scope): {e}", fg=RED)
|
|
116
|
+
sys.exit(2)
|
|
117
|
+
except ExpiredSessionError as e:
|
|
118
|
+
click.secho(f"DENIED (expired): {e}", fg=RED)
|
|
119
|
+
sys.exit(3)
|
|
120
|
+
except SessionError as e:
|
|
121
|
+
click.secho(f"Error: {e}", fg=RED)
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
_emit(result, GREEN)
|
|
124
|
+
click.secho("\n action signed + logged to residue", fg=GREEN)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@cli.command()
|
|
128
|
+
@click.option("--session-id", required=True, help="Session to evaporate.")
|
|
129
|
+
@click.pass_context
|
|
130
|
+
def evaporate(ctx: click.Context, session_id: str) -> None:
|
|
131
|
+
"""Destroy the key, sign the chain, finalize the residue."""
|
|
132
|
+
store: ResidueStore = ctx.obj["store"]
|
|
133
|
+
try:
|
|
134
|
+
result = _session.evaporate(store, session_id)
|
|
135
|
+
except SessionError as e:
|
|
136
|
+
click.secho(f"Error: {e}", fg=RED)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
_emit(result, GOLD)
|
|
139
|
+
click.secho("\n Session evaporated. Ephemeral key shredded.", fg=PINK, bold=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@cli.command()
|
|
143
|
+
@click.option("--session-id", default=None, help="Session to replay.")
|
|
144
|
+
@click.option("--all", "all_", is_flag=True, help="List all sessions.")
|
|
145
|
+
@click.option(
|
|
146
|
+
"--format", "fmt", type=click.Choice(["json", "csv"]), default="json", show_default=True
|
|
147
|
+
)
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def replay(ctx: click.Context, session_id: Optional[str], all_: bool, fmt: str) -> None:
|
|
150
|
+
"""Retrieve and verify signed execution history."""
|
|
151
|
+
store: ResidueStore = ctx.obj["store"]
|
|
152
|
+
if all_:
|
|
153
|
+
rows = store.list_sessions()
|
|
154
|
+
if fmt == "csv":
|
|
155
|
+
click.echo("session_id,intent,spawned_at,evaporated_at,actions")
|
|
156
|
+
for r in rows:
|
|
157
|
+
n = store.count_actions(r["session_id"])
|
|
158
|
+
click.echo(
|
|
159
|
+
f"{r['session_id']},{r['intent']},{r['spawned_at']},"
|
|
160
|
+
f"{r['evaporated_at'] or ''},{n}"
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
for r in rows:
|
|
164
|
+
_emit(
|
|
165
|
+
{
|
|
166
|
+
"session_id": r["session_id"],
|
|
167
|
+
"intent": r["intent"],
|
|
168
|
+
"spawned_at": r["spawned_at"],
|
|
169
|
+
"evaporated_at": r["evaporated_at"],
|
|
170
|
+
"actions": store.count_actions(r["session_id"]),
|
|
171
|
+
},
|
|
172
|
+
GREEN,
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if not session_id:
|
|
177
|
+
click.secho("Error: provide --session-id or --all", fg=RED)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
try:
|
|
180
|
+
result = _session.replay(store, session_id)
|
|
181
|
+
except SessionError as e:
|
|
182
|
+
click.secho(f"Error: {e}", fg=RED)
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
if fmt == "csv":
|
|
186
|
+
click.echo("seq,tool,action,timestamp,verified")
|
|
187
|
+
for a in result["actions"]:
|
|
188
|
+
click.echo(
|
|
189
|
+
f"{a['seq']},{a['tool']},{a['action']},{a['timestamp']},{a['verified']}"
|
|
190
|
+
)
|
|
191
|
+
click.echo(f"# root_verified={result['root_verified']} verified={result['verified']}")
|
|
192
|
+
else:
|
|
193
|
+
_emit(result, GREEN if result["verified"] else RED)
|
|
194
|
+
if result["verified"]:
|
|
195
|
+
click.secho("\n residue verified", fg=GREEN, bold=True)
|
|
196
|
+
else:
|
|
197
|
+
click.secho("\n RESIDUE VERIFICATION FAILED", fg=RED, bold=True)
|
|
198
|
+
sys.exit(4)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main() -> None:
|
|
202
|
+
cli(obj={})
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Cryptographic primitives for GHOST.
|
|
2
|
+
|
|
3
|
+
Ed25519 signing of residue entries plus SHA-256 hashing helpers. The signing
|
|
4
|
+
key is generated per-session at spawn time and destroyed at evaporate time;
|
|
5
|
+
only the public (verifying) key is persisted so the audit trail can be verified
|
|
6
|
+
after the session is gone.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from cryptography.exceptions import InvalidSignature
|
|
17
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
18
|
+
Ed25519PrivateKey,
|
|
19
|
+
Ed25519PublicKey,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_keypair() -> tuple[bytes, bytes]:
|
|
24
|
+
"""Return (private_seed_32b, public_raw_32b) for a fresh Ed25519 key."""
|
|
25
|
+
sk = Ed25519PrivateKey.generate()
|
|
26
|
+
private_seed = sk.private_bytes_raw()
|
|
27
|
+
public_raw = sk.public_key().public_bytes_raw()
|
|
28
|
+
return private_seed, public_raw
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sign(private_seed: bytes, message: bytes) -> str:
|
|
32
|
+
"""Sign message with the private seed; return base64 signature."""
|
|
33
|
+
sk = Ed25519PrivateKey.from_private_bytes(private_seed)
|
|
34
|
+
sig = sk.sign(message)
|
|
35
|
+
return base64.b64encode(sig).decode("ascii")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def verify(public_raw: bytes, message: bytes, signature_b64: str) -> bool:
|
|
39
|
+
"""Verify a base64 signature against message using the raw public key."""
|
|
40
|
+
try:
|
|
41
|
+
pk = Ed25519PublicKey.from_public_bytes(public_raw)
|
|
42
|
+
pk.verify(base64.b64decode(signature_b64), message)
|
|
43
|
+
return True
|
|
44
|
+
except (InvalidSignature, ValueError):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def b64(data: bytes) -> str:
|
|
49
|
+
return base64.b64encode(data).decode("ascii")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def unb64(text: str) -> bytes:
|
|
53
|
+
return base64.b64decode(text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sha256_hex(value: str) -> str:
|
|
57
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def canonical_hash(obj: Any) -> str:
|
|
61
|
+
"""Deterministic SHA-256 over a JSON-serialisable object."""
|
|
62
|
+
return sha256_hex(json.dumps(obj, sort_keys=True, separators=(",", ":")))
|