godcode-engine 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- godcode/__init__.py +86 -0
- godcode/__main__.py +7 -0
- godcode/agentics.py +235 -0
- godcode/ast.py +227 -0
- godcode/chain.py +210 -0
- godcode/cli.py +698 -0
- godcode/environment.py +74 -0
- godcode/errors.py +69 -0
- godcode/interpreter.py +1104 -0
- godcode/ledger.py +88 -0
- godcode/lexer.py +218 -0
- godcode/lsp.py +677 -0
- godcode/parser.py +525 -0
- godcode/plugins.py +255 -0
- godcode/registry.py +515 -0
- godcode/sandbox.py +385 -0
- godcode/scrolls/covenant.god +10 -0
- godcode/scrolls/covenant.toml +6 -0
- godcode/scrolls/lists.god +53 -0
- godcode/scrolls/lists.toml +6 -0
- godcode/scrolls/math.god +64 -0
- godcode/scrolls/math.toml +6 -0
- godcode/scrolls/prophecy.god +14 -0
- godcode/scrolls/prophecy.toml +6 -0
- godcode/scrolls/strings.god +32 -0
- godcode/scrolls/strings.toml +6 -0
- godcode/scrolls/time.god +10 -0
- godcode/scrolls/time.toml +6 -0
- godcode/spirit.py +180 -0
- godcode/tokens.py +91 -0
- godcode/tools.py +378 -0
- godcode/values.py +56 -0
- godcode_engine-4.0.0.dist-info/METADATA +212 -0
- godcode_engine-4.0.0.dist-info/RECORD +38 -0
- godcode_engine-4.0.0.dist-info/WHEEL +5 -0
- godcode_engine-4.0.0.dist-info/entry_points.txt +2 -0
- godcode_engine-4.0.0.dist-info/licenses/LICENSE +201 -0
- godcode_engine-4.0.0.dist-info/top_level.txt +1 -0
godcode/spirit.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""SpiritEngine -- discerns the spiritual intent behind words. 🕊
|
|
2
|
+
|
|
3
|
+
Loads the God Code training dataset, builds a keyword index over each row's
|
|
4
|
+
primary_action + spiritual_intent + logic_flows, and classifies new text by
|
|
5
|
+
keyword overlap. When nothing resonates, it falls back to silent contemplation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import csv
|
|
11
|
+
import re
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_WORD_RE = re.compile(r"[a-z0-9]+")
|
|
16
|
+
|
|
17
|
+
STOPWORDS = frozenset(
|
|
18
|
+
"""
|
|
19
|
+
a an the of and to in for on with by or is are was were be been being
|
|
20
|
+
this that these those it its as at from into over under between through
|
|
21
|
+
during before after above below up down out off again further then once
|
|
22
|
+
here there when where why how all any both each few more most other some
|
|
23
|
+
such no nor not only own same so than too very can will just don should
|
|
24
|
+
now your you we they them their our us my me him her his she he i
|
|
25
|
+
do does did have has had shall may might must would could ought
|
|
26
|
+
""".split()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
SILENT_CONTEMPLATION = {
|
|
30
|
+
"intent": "Silent contemplation",
|
|
31
|
+
"confidence": 0.0,
|
|
32
|
+
"spiritual_intent": "The Spirit is quiet on this matter.",
|
|
33
|
+
"suggestion": "BREATHE and try again.",
|
|
34
|
+
"keywords": [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _keywords(text: str) -> set[str]:
|
|
39
|
+
"""Lowercase word tokens with stopwords removed."""
|
|
40
|
+
return {w for w in _WORD_RE.findall((text or "").lower()) if w not in STOPWORDS}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SpiritEngine:
|
|
44
|
+
"""Keyword-overlap classifier over the God Code training dataset."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, dataset_path: str | Path | None = None) -> None:
|
|
47
|
+
if dataset_path is None:
|
|
48
|
+
# Default: god_code_training_dataset.csv at the repo root.
|
|
49
|
+
dataset_path = Path(__file__).resolve().parent.parent / "god_code_training_dataset.csv"
|
|
50
|
+
self.dataset_path = Path(dataset_path)
|
|
51
|
+
self.rows: list[dict] = self._load()
|
|
52
|
+
# v4.0 -- declared intents: rite name -> natural-language intent text,
|
|
53
|
+
# registered by DECLARE INTENT and consulted by resolve_intent().
|
|
54
|
+
self.declared_intents: dict[str, str] = {}
|
|
55
|
+
|
|
56
|
+
def _load(self) -> list[dict]:
|
|
57
|
+
try:
|
|
58
|
+
with open(self.dataset_path, newline="", encoding="utf-8") as f:
|
|
59
|
+
records = list(csv.reader(f))
|
|
60
|
+
except OSError:
|
|
61
|
+
# No dataset beside the engine (e.g. an installed copy):
|
|
62
|
+
# the Spirit is quiet, and classify() falls back gracefully.
|
|
63
|
+
return []
|
|
64
|
+
if not records:
|
|
65
|
+
return []
|
|
66
|
+
header, records = records[0], records[1:]
|
|
67
|
+
rows: list[dict] = []
|
|
68
|
+
buf: list[str] = []
|
|
69
|
+
for rec in records:
|
|
70
|
+
if len(rec) == len(header):
|
|
71
|
+
# A row ends here: the code field may have spanned earlier
|
|
72
|
+
# physical lines (unquoted), accumulated in buf.
|
|
73
|
+
code = "\n".join(buf + [rec[0]])
|
|
74
|
+
row = dict(zip(header, [code] + rec[1:]))
|
|
75
|
+
row["keywords"] = _keywords(
|
|
76
|
+
" ".join(row[c] for c in ("primary_action", "spiritual_intent", "logic_flows"))
|
|
77
|
+
)
|
|
78
|
+
rows.append(row)
|
|
79
|
+
buf = []
|
|
80
|
+
else:
|
|
81
|
+
# Continuation line of the multi-line code field.
|
|
82
|
+
buf.extend(rec)
|
|
83
|
+
return rows
|
|
84
|
+
|
|
85
|
+
def classify(self, text: str) -> dict:
|
|
86
|
+
"""Classify text -> {intent, confidence, spiritual_intent, suggestion, keywords}."""
|
|
87
|
+
words = _keywords(text)
|
|
88
|
+
best: dict | None = None
|
|
89
|
+
best_overlap = 0
|
|
90
|
+
best_confidence = 0.0
|
|
91
|
+
for row in self.rows:
|
|
92
|
+
overlap = len(words & row["keywords"])
|
|
93
|
+
if overlap == 0:
|
|
94
|
+
continue
|
|
95
|
+
confidence = overlap / max(1, len(row["keywords"]))
|
|
96
|
+
# Most shared keywords wins; ties go to the row whose
|
|
97
|
+
# vocabulary best explains the input (highest confidence),
|
|
98
|
+
# so an incidental word cannot outshout the true theme.
|
|
99
|
+
if overlap > best_overlap or (
|
|
100
|
+
overlap == best_overlap and confidence > best_confidence
|
|
101
|
+
):
|
|
102
|
+
best, best_overlap, best_confidence = row, overlap, confidence
|
|
103
|
+
if best is None:
|
|
104
|
+
return dict(SILENT_CONTEMPLATION)
|
|
105
|
+
confidence = min(1.0, best_overlap / max(1, len(best["keywords"])))
|
|
106
|
+
return {
|
|
107
|
+
"intent": best["primary_action"],
|
|
108
|
+
"confidence": round(confidence, 4),
|
|
109
|
+
"spiritual_intent": best["spiritual_intent"],
|
|
110
|
+
"suggestion": best["next_suggestions"],
|
|
111
|
+
"keywords": sorted(words & best["keywords"]),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def prophesy(self, program_text: str) -> str:
|
|
115
|
+
"""Compose a 2-3 sentence divine forecast over a program's lines."""
|
|
116
|
+
lines = [ln.strip() for ln in (program_text or "").splitlines() if ln.strip()]
|
|
117
|
+
results = [self.classify(ln) for ln in lines] or [dict(SILENT_CONTEMPLATION)]
|
|
118
|
+
counts = Counter(r["intent"] for r in results)
|
|
119
|
+
top = max(counts.values())
|
|
120
|
+
# Dominant intent = mode; ties resolve to the earliest line's intent.
|
|
121
|
+
dominant = next(r for r in results if counts[r["intent"]] == top)
|
|
122
|
+
avg_conf = sum(r["confidence"] for r in results) / len(results)
|
|
123
|
+
pct = round(avg_conf * 100)
|
|
124
|
+
return (
|
|
125
|
+
f"Thus the Spirit speaks over this creation: the prevailing wind is "
|
|
126
|
+
f"'{dominant['intent']}', discerned with {pct}% certainty. "
|
|
127
|
+
f"{dominant['spiritual_intent']} "
|
|
128
|
+
f"Therefore the counsel of heaven is this: {dominant['suggestion']}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------- v4.0: intent layer
|
|
132
|
+
|
|
133
|
+
def declare_intent(self, rite_name: str, text: str) -> None:
|
|
134
|
+
"""Register a natural-language intent on a named rite."""
|
|
135
|
+
self.declared_intents[str(rite_name)] = str(text)
|
|
136
|
+
|
|
137
|
+
def intents_aligned(self, declared: str, discerned: dict) -> bool:
|
|
138
|
+
"""True when the declared intent shares a keyword with the discerned one.
|
|
139
|
+
|
|
140
|
+
*declared* is the natural-language text from DECLARE INTENT;
|
|
141
|
+
*discerned* is a classify() result for the rite's actual words.
|
|
142
|
+
The Spirit counsels on divergence; it never condemns.
|
|
143
|
+
"""
|
|
144
|
+
declared_words = _keywords(declared)
|
|
145
|
+
discerned_words = (
|
|
146
|
+
_keywords(discerned.get("intent") or "")
|
|
147
|
+
| _keywords(discerned.get("spiritual_intent") or "")
|
|
148
|
+
| {str(k).lower() for k in (discerned.get("keywords") or [])}
|
|
149
|
+
)
|
|
150
|
+
return bool(declared_words & discerned_words)
|
|
151
|
+
|
|
152
|
+
def resolve_intent(self, text: str) -> dict:
|
|
153
|
+
"""Classify *text* and report which declared intents it aligns with.
|
|
154
|
+
|
|
155
|
+
Returns the classify() payload plus ``aligned_with``: a list of
|
|
156
|
+
``{rite, declared, shared_keywords}`` for every declared intent
|
|
157
|
+
sharing at least one keyword with the text.
|
|
158
|
+
"""
|
|
159
|
+
result = self.classify(text)
|
|
160
|
+
words = _keywords(text)
|
|
161
|
+
aligned_with = []
|
|
162
|
+
for rite, declared in self.declared_intents.items():
|
|
163
|
+
shared = sorted(words & _keywords(declared))
|
|
164
|
+
if shared:
|
|
165
|
+
aligned_with.append(
|
|
166
|
+
{"rite": rite, "declared": declared,
|
|
167
|
+
"shared_keywords": shared}
|
|
168
|
+
)
|
|
169
|
+
result["aligned_with"] = aligned_with
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
def counsel(self, question: str) -> str:
|
|
173
|
+
"""Speak 2-3 sentences of counsel over a question, in prophesy's voice."""
|
|
174
|
+
r = self.classify(question)
|
|
175
|
+
pct = round(r["confidence"] * 100)
|
|
176
|
+
return (
|
|
177
|
+
f"Concerning '{question}', the Spirit discerns '{r['intent']}' "
|
|
178
|
+
f"with {pct}% certainty. {r['spiritual_intent']} "
|
|
179
|
+
f"Therefore the counsel of heaven is this: {r['suggestion']}"
|
|
180
|
+
)
|
godcode/tokens.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Token types and the Token dataclass for God Code v2.0.
|
|
2
|
+
|
|
3
|
+
Keyword tokens carry their canonical UPPER name as ``value``;
|
|
4
|
+
identifiers preserve the author's casing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenType(Enum):
|
|
14
|
+
# --- keywords -----------------------------------------------------------
|
|
15
|
+
BEGIN = auto()
|
|
16
|
+
CREATION = auto()
|
|
17
|
+
END = auto()
|
|
18
|
+
DECLARE = auto()
|
|
19
|
+
AS = auto()
|
|
20
|
+
BREATHE = auto()
|
|
21
|
+
LIFE = auto()
|
|
22
|
+
INTO = auto()
|
|
23
|
+
REVEAL = auto()
|
|
24
|
+
PROPHESY = auto()
|
|
25
|
+
ASCEND = auto()
|
|
26
|
+
IF = auto()
|
|
27
|
+
THEN = auto()
|
|
28
|
+
ELSE = auto()
|
|
29
|
+
ENDIF = auto()
|
|
30
|
+
FOR = auto()
|
|
31
|
+
IN = auto()
|
|
32
|
+
ENDFOR = auto()
|
|
33
|
+
WHILE = auto()
|
|
34
|
+
DO = auto()
|
|
35
|
+
ENDWHILE = auto()
|
|
36
|
+
DEFINE = auto()
|
|
37
|
+
RITE = auto()
|
|
38
|
+
INVOKE = auto()
|
|
39
|
+
RETURN = auto()
|
|
40
|
+
IMPORT = auto()
|
|
41
|
+
IS = auto()
|
|
42
|
+
NOT = auto()
|
|
43
|
+
AND = auto()
|
|
44
|
+
OR = auto()
|
|
45
|
+
# §4 additions (missed in §1's list)
|
|
46
|
+
REFLECT = auto()
|
|
47
|
+
BLESS = auto()
|
|
48
|
+
ANOINT = auto()
|
|
49
|
+
SEAL = auto()
|
|
50
|
+
TESTIFY = auto()
|
|
51
|
+
# boolean / void literals (added for fmt <-> parse round-trip fidelity)
|
|
52
|
+
TRUE = auto()
|
|
53
|
+
FALSE = auto()
|
|
54
|
+
VOID = auto()
|
|
55
|
+
# --- literals -----------------------------------------------------------
|
|
56
|
+
NUMBER = auto()
|
|
57
|
+
STRING = auto()
|
|
58
|
+
IDENT = auto()
|
|
59
|
+
# --- operators ----------------------------------------------------------
|
|
60
|
+
PLUS = auto() # +
|
|
61
|
+
MINUS = auto() # -
|
|
62
|
+
STAR = auto() # *
|
|
63
|
+
SLASH = auto() # /
|
|
64
|
+
PERCENT = auto() # %
|
|
65
|
+
EQ = auto() # = or ==
|
|
66
|
+
NEQ = auto() # !=
|
|
67
|
+
LT = auto() # <
|
|
68
|
+
GT = auto() # >
|
|
69
|
+
LTE = auto() # <=
|
|
70
|
+
GTE = auto() # >=
|
|
71
|
+
# --- punctuation --------------------------------------------------------
|
|
72
|
+
LPAREN = auto()
|
|
73
|
+
RPAREN = auto()
|
|
74
|
+
LBRACKET = auto()
|
|
75
|
+
RBRACKET = auto()
|
|
76
|
+
COMMA = auto()
|
|
77
|
+
# --- structural ---------------------------------------------------------
|
|
78
|
+
NEWLINE = auto()
|
|
79
|
+
EOF = auto()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Token:
|
|
84
|
+
type: TokenType
|
|
85
|
+
value: object # NUMBER -> int|float, STRING -> str (unescaped),
|
|
86
|
+
# IDENT/keywords -> str, operators/punct -> the lexeme
|
|
87
|
+
line: int # 1-based
|
|
88
|
+
col: int # 1-based
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str: # compact, readable in test failures
|
|
91
|
+
return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.col})"
|
godcode/tools.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Agent tool bridge for God Code v4.0 -- "Intent & Chain".
|
|
2
|
+
|
|
3
|
+
MCP-compatible tool schemas plus a stdio JSON-RPC 2.0 bridge, with no
|
|
4
|
+
third-party dependencies. ``godcode tools`` prints the schemas;
|
|
5
|
+
``godcode bridge`` speaks the protocol so any MCP-compatible agent host
|
|
6
|
+
can check, run, consult, resolve intent, and verify chains.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
TOOL_SCHEMAS: list[dict] = [
|
|
14
|
+
{
|
|
15
|
+
"name": "check",
|
|
16
|
+
"description": (
|
|
17
|
+
"Lex and parse a God Code scroll without running it. "
|
|
18
|
+
"Returns ok plus diagnostics with line, column, error code, "
|
|
19
|
+
"and a fix hint."
|
|
20
|
+
),
|
|
21
|
+
"inputSchema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"file": {"type": "string",
|
|
25
|
+
"description": "Path to the .god scroll"},
|
|
26
|
+
},
|
|
27
|
+
"required": ["file"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "run",
|
|
32
|
+
"description": (
|
|
33
|
+
"Run a God Code scroll and capture its output. "
|
|
34
|
+
"Runs inside the deny-by-default sandbox unless sandbox is false."
|
|
35
|
+
),
|
|
36
|
+
"inputSchema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"file": {"type": "string",
|
|
40
|
+
"description": "Path to the .god scroll"},
|
|
41
|
+
"sandbox": {"type": "boolean",
|
|
42
|
+
"description": "Use the sandbox (default true)"},
|
|
43
|
+
},
|
|
44
|
+
"required": ["file"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "consult",
|
|
49
|
+
"description": (
|
|
50
|
+
"Ask the Spirit Engine -- the local oracle -- a question. "
|
|
51
|
+
"Returns two to three sentences of counsel. No external calls."
|
|
52
|
+
),
|
|
53
|
+
"inputSchema": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {
|
|
56
|
+
"question": {"type": "string",
|
|
57
|
+
"description": "The question to lay before the Spirit"},
|
|
58
|
+
},
|
|
59
|
+
"required": ["question"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "intent",
|
|
64
|
+
"description": (
|
|
65
|
+
"Resolve the spiritual intent behind words with the Spirit "
|
|
66
|
+
"Engine: intent, confidence, spiritual intent, suggestion, "
|
|
67
|
+
"keywords, and declared intents the words align with."
|
|
68
|
+
),
|
|
69
|
+
"inputSchema": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"text": {"type": "string",
|
|
73
|
+
"description": "Words to resolve the intent of"},
|
|
74
|
+
},
|
|
75
|
+
"required": ["text"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "anchor_verify",
|
|
80
|
+
"description": (
|
|
81
|
+
"Verify a blockchain anchor receipt (as returned by ANCHOR) "
|
|
82
|
+
"against its anchor chain."
|
|
83
|
+
),
|
|
84
|
+
"inputSchema": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"receipt": {"type": "object",
|
|
88
|
+
"description": "The anchor receipt map"},
|
|
89
|
+
},
|
|
90
|
+
"required": ["receipt"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "ledger_verify",
|
|
95
|
+
"description": (
|
|
96
|
+
"Verify the covenant chain and the anchor chain for tampering."
|
|
97
|
+
),
|
|
98
|
+
"inputSchema": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
"file": {"type": "string",
|
|
102
|
+
"description": "Covenant chain file (default covenant.chain)"},
|
|
103
|
+
"anchor_file": {"type": "string",
|
|
104
|
+
"description": "Anchor chain file (default anchors.chain)"},
|
|
105
|
+
},
|
|
106
|
+
"required": [],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
_TOOLS_BY_NAME = {t["name"]: t for t in TOOL_SCHEMAS}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# tool implementations (each returns a plain-JSON result dict)
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
def tool_check(arguments: dict) -> dict:
|
|
118
|
+
from godcode import agentics
|
|
119
|
+
from godcode.errors import GodCodeError
|
|
120
|
+
from godcode.lexer import Lexer
|
|
121
|
+
from godcode.parser import Parser
|
|
122
|
+
|
|
123
|
+
file = arguments.get("file")
|
|
124
|
+
if not isinstance(file, str):
|
|
125
|
+
return {"ok": False, "error": "check needs 'file' as a string path"}
|
|
126
|
+
try:
|
|
127
|
+
source = Path(file).read_text(encoding="utf-8")
|
|
128
|
+
except OSError as exc:
|
|
129
|
+
return {"ok": False, "error": f"cannot read '{file}': {exc.strerror or exc}"}
|
|
130
|
+
try:
|
|
131
|
+
Parser(Lexer(source).lex()).parse()
|
|
132
|
+
except GodCodeError as err:
|
|
133
|
+
return {"ok": False, "diagnostics": [agentics.diagnostic(err)]}
|
|
134
|
+
return {"ok": True, "diagnostics": []}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def tool_run(arguments: dict) -> dict:
|
|
138
|
+
from godcode.errors import GodCodeError
|
|
139
|
+
|
|
140
|
+
file = arguments.get("file")
|
|
141
|
+
if not isinstance(file, str):
|
|
142
|
+
return {"ok": False, "error": "run needs 'file' as a string path"}
|
|
143
|
+
sandbox = arguments.get("sandbox", True)
|
|
144
|
+
try:
|
|
145
|
+
source = Path(file).read_text(encoding="utf-8")
|
|
146
|
+
except OSError as exc:
|
|
147
|
+
return {"ok": False, "error": f"cannot read '{file}': {exc.strerror or exc}"}
|
|
148
|
+
if sandbox:
|
|
149
|
+
from godcode.sandbox import run_sandboxed
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
output = run_sandboxed(source, source_name=file)
|
|
153
|
+
except GodCodeError as err:
|
|
154
|
+
return {"ok": False, "output": [], "error": str(err)}
|
|
155
|
+
return {"ok": True, "output": output, "error": None}
|
|
156
|
+
from godcode.cli import _make_interpreter
|
|
157
|
+
import contextlib
|
|
158
|
+
import io
|
|
159
|
+
|
|
160
|
+
interp = _make_interpreter(None)
|
|
161
|
+
buf = io.StringIO()
|
|
162
|
+
try:
|
|
163
|
+
with contextlib.redirect_stdout(buf):
|
|
164
|
+
interp.run_source(source, source_name=file)
|
|
165
|
+
except GodCodeError as err:
|
|
166
|
+
return {"ok": False,
|
|
167
|
+
"output": buf.getvalue().splitlines(),
|
|
168
|
+
"error": str(err)}
|
|
169
|
+
return {"ok": True, "output": buf.getvalue().splitlines(), "error": None}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def tool_consult(arguments: dict) -> dict:
|
|
173
|
+
from godcode.spirit import SpiritEngine
|
|
174
|
+
|
|
175
|
+
question = arguments.get("question")
|
|
176
|
+
if not isinstance(question, str):
|
|
177
|
+
return {"ok": False, "error": "consult needs 'question' as a string"}
|
|
178
|
+
try:
|
|
179
|
+
counsel = SpiritEngine().counsel(question)
|
|
180
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
181
|
+
return {"ok": False, "error": f"the oracle faltered: {exc}"}
|
|
182
|
+
return {"ok": True, "counsel": counsel}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def tool_intent(arguments: dict) -> dict:
|
|
186
|
+
from godcode.spirit import SpiritEngine
|
|
187
|
+
|
|
188
|
+
text = arguments.get("text")
|
|
189
|
+
if not isinstance(text, str):
|
|
190
|
+
return {"ok": False, "error": "intent needs 'text' as a string"}
|
|
191
|
+
try:
|
|
192
|
+
result = SpiritEngine().resolve_intent(text)
|
|
193
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
194
|
+
return {"ok": False, "error": f"the Spirit faltered: {exc}"}
|
|
195
|
+
return {"ok": True, "result": result}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def tool_anchor_verify(arguments: dict) -> dict:
|
|
199
|
+
from godcode.chain import default_adapters
|
|
200
|
+
|
|
201
|
+
receipt = arguments.get("receipt")
|
|
202
|
+
if not isinstance(receipt, dict):
|
|
203
|
+
return {"ok": False, "error": "anchor_verify needs 'receipt' as an object"}
|
|
204
|
+
chain_name = receipt.get("chain", "simulated")
|
|
205
|
+
adapter = default_adapters().get(chain_name)
|
|
206
|
+
if adapter is None:
|
|
207
|
+
return {"ok": False,
|
|
208
|
+
"error": f"unknown chain '{chain_name}'"}
|
|
209
|
+
valid = adapter.verify(receipt)
|
|
210
|
+
return {
|
|
211
|
+
"ok": True,
|
|
212
|
+
"valid": valid,
|
|
213
|
+
"message": ("the anchor stands" if valid
|
|
214
|
+
else "the anchor does not verify"),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def tool_ledger_verify(arguments: dict) -> dict:
|
|
219
|
+
from godcode.chain import SimulatedChainAdapter
|
|
220
|
+
from godcode.ledger import CovenantLedger
|
|
221
|
+
|
|
222
|
+
file = arguments.get("file", "covenant.chain")
|
|
223
|
+
anchor_file = arguments.get("anchor_file", "anchors.chain")
|
|
224
|
+
cov_ok, cov_msg = CovenantLedger(file).verify()
|
|
225
|
+
anc_ok, anc_msg = SimulatedChainAdapter(anchor_file).verify_chain()
|
|
226
|
+
return {
|
|
227
|
+
"ok": True,
|
|
228
|
+
"covenants": {"ok": cov_ok, "message": cov_msg},
|
|
229
|
+
"anchors": {"ok": anc_ok, "message": anc_msg},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_TOOL_FUNCS = {
|
|
234
|
+
"check": tool_check,
|
|
235
|
+
"run": tool_run,
|
|
236
|
+
"consult": tool_consult,
|
|
237
|
+
"intent": tool_intent,
|
|
238
|
+
"anchor_verify": tool_anchor_verify,
|
|
239
|
+
"ledger_verify": tool_ledger_verify,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def call_tool(name: str, arguments: dict) -> dict:
|
|
244
|
+
"""Run one tool by name; always returns a JSON-safe result dict."""
|
|
245
|
+
func = _TOOL_FUNCS.get(name)
|
|
246
|
+
if func is None:
|
|
247
|
+
return {"ok": False,
|
|
248
|
+
"error": f"unknown tool '{name}'",
|
|
249
|
+
"known_tools": sorted(_TOOL_FUNCS)}
|
|
250
|
+
if not isinstance(arguments, dict):
|
|
251
|
+
return {"ok": False, "error": "arguments must be an object"}
|
|
252
|
+
try:
|
|
253
|
+
return func(arguments)
|
|
254
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
255
|
+
return {"ok": False, "error": f"the tool faltered: {exc!r}"}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# `godcode tools`
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
def cmd_tools(args) -> int:
|
|
262
|
+
"""Print the MCP-compatible tool schemas (--json for machines)."""
|
|
263
|
+
if getattr(args, "json", False):
|
|
264
|
+
print(json.dumps({"tools": TOOL_SCHEMAS}, ensure_ascii=False, indent=2))
|
|
265
|
+
return 0
|
|
266
|
+
print("God Code tools (MCP-compatible):")
|
|
267
|
+
for tool in TOOL_SCHEMAS:
|
|
268
|
+
required = ", ".join(tool["inputSchema"].get("required", [])) or "none"
|
|
269
|
+
print(f" {tool['name']}({required}): {tool['description']}")
|
|
270
|
+
print("Speak them through `godcode bridge`, the stdio JSON-RPC loop.")
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# `godcode bridge` -- stdio JSON-RPC 2.0
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
_PARSE_ERROR = -32700
|
|
278
|
+
_INVALID_REQUEST = -32600
|
|
279
|
+
_METHOD_NOT_FOUND = -32601
|
|
280
|
+
_INVALID_PARAMS = -32602
|
|
281
|
+
_INTERNAL_ERROR = -32603
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _response_ok(req_id, result: dict) -> dict:
|
|
285
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _response_err(req_id, code: int, message: str) -> dict:
|
|
289
|
+
return {"jsonrpc": "2.0", "id": req_id,
|
|
290
|
+
"error": {"code": code, "message": message}}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _handle_initialize(params: dict) -> dict:
|
|
294
|
+
from godcode import __version__
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"protocolVersion": "2024-11-05",
|
|
298
|
+
"capabilities": {"tools": {}},
|
|
299
|
+
"serverInfo": {"name": "godcode-bridge", "version": __version__},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _handle_tools_call(params: dict) -> dict:
|
|
304
|
+
name = params.get("name")
|
|
305
|
+
arguments = params.get("arguments", {})
|
|
306
|
+
if not isinstance(name, str):
|
|
307
|
+
raise _BridgeError(_INVALID_PARAMS, "tools/call needs 'name' as a string")
|
|
308
|
+
result = call_tool(name, arguments if isinstance(arguments, dict) else {})
|
|
309
|
+
content = [{"type": "text",
|
|
310
|
+
"text": json.dumps(result, ensure_ascii=False)}]
|
|
311
|
+
payload = {"content": content}
|
|
312
|
+
if not result.get("ok", True):
|
|
313
|
+
payload["isError"] = True
|
|
314
|
+
return payload
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _BridgeError(Exception):
|
|
318
|
+
def __init__(self, code: int, message: str) -> None:
|
|
319
|
+
super().__init__(message)
|
|
320
|
+
self.code = code
|
|
321
|
+
self.message = message
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _dispatch(method: str, params: dict):
|
|
325
|
+
if method == "initialize":
|
|
326
|
+
return _handle_initialize(params)
|
|
327
|
+
if method == "ping":
|
|
328
|
+
return {}
|
|
329
|
+
if method == "tools/list":
|
|
330
|
+
return {"tools": TOOL_SCHEMAS}
|
|
331
|
+
if method == "tools/call":
|
|
332
|
+
return _handle_tools_call(params)
|
|
333
|
+
raise _BridgeError(_METHOD_NOT_FOUND, f"no such method '{method}'")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_bridge(args) -> int: # noqa: ARG001
|
|
337
|
+
"""Serve the tool bridge: JSON-RPC 2.0 over stdio, one request per line."""
|
|
338
|
+
import sys
|
|
339
|
+
|
|
340
|
+
stdin, stdout = sys.stdin, sys.stdout
|
|
341
|
+
for line in stdin:
|
|
342
|
+
line = line.strip()
|
|
343
|
+
if not line:
|
|
344
|
+
continue
|
|
345
|
+
try:
|
|
346
|
+
request = json.loads(line)
|
|
347
|
+
except json.JSONDecodeError:
|
|
348
|
+
stdout.write(json.dumps(_response_err(None, _PARSE_ERROR,
|
|
349
|
+
"the request was not JSON"))
|
|
350
|
+
+ "\n")
|
|
351
|
+
stdout.flush()
|
|
352
|
+
continue
|
|
353
|
+
req_id = request.get("id") if isinstance(request, dict) else None
|
|
354
|
+
# Notifications carry no id and get no answer.
|
|
355
|
+
is_notification = isinstance(request, dict) and "id" not in request
|
|
356
|
+
try:
|
|
357
|
+
if not isinstance(request, dict) or "method" not in request:
|
|
358
|
+
raise _BridgeError(_INVALID_REQUEST,
|
|
359
|
+
"a request needs a 'method'")
|
|
360
|
+
params = request.get("params", {})
|
|
361
|
+
if not isinstance(params, dict):
|
|
362
|
+
raise _BridgeError(_INVALID_PARAMS,
|
|
363
|
+
"'params' must be an object")
|
|
364
|
+
result = _dispatch(request["method"], params)
|
|
365
|
+
except _BridgeError as err:
|
|
366
|
+
response = _response_err(req_id, err.code, err.message)
|
|
367
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
368
|
+
response = _response_err(req_id, _INTERNAL_ERROR,
|
|
369
|
+
f"the bridge faltered: {exc!r}")
|
|
370
|
+
else:
|
|
371
|
+
if is_notification:
|
|
372
|
+
continue
|
|
373
|
+
response = _response_ok(req_id, result)
|
|
374
|
+
if is_notification:
|
|
375
|
+
continue
|
|
376
|
+
stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
377
|
+
stdout.flush()
|
|
378
|
+
return 0
|