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.
@@ -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
+ [![status](https://img.shields.io/badge/status-alpha-39FF14)](https://github.com/timwal78/ghost-layer)
45
+ [![license](https://img.shields.io/badge/license-MIT-FFD700)](LICENSE)
46
+ [![python](https://img.shields.io/badge/python-3.9%2B-FF1493)](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
+ [![status](https://img.shields.io/badge/status-alpha-39FF14)](https://github.com/timwal78/ghost-layer)
13
+ [![license](https://img.shields.io/badge/license-MIT-FFD700)](LICENSE)
14
+ [![python](https://img.shields.io/badge/python-3.9%2B-FF1493)](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=(",", ":")))