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/plugins.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Pillar 3 — plugins and the SUMMON foreign-function interface.
|
|
2
|
+
|
|
3
|
+
A plugin is a plain Python module living in a plugins directory. On load it
|
|
4
|
+
exposes::
|
|
5
|
+
|
|
6
|
+
PLUGIN_API_VERSION = 1
|
|
7
|
+
|
|
8
|
+
def register(interpreter):
|
|
9
|
+
interpreter.register_plugin_verb("myns.myverb", myverb, plugin="myns")
|
|
10
|
+
|
|
11
|
+
The interpreter calls ``register(interpreter)`` at startup, so the plugin can
|
|
12
|
+
add new built-in verbs through the same ``_builtins`` mechanism the v2 core
|
|
13
|
+
verbs use. God Code calls those verbs through the ``SUMMON`` FFI verb::
|
|
14
|
+
|
|
15
|
+
SUMMON("myns.myverb", arg1, arg2)
|
|
16
|
+
|
|
17
|
+
Plugins are **trusted host code**: they run with the full power of Python,
|
|
18
|
+
and SUMMON calls into them bypass any sandbox policy *by design*. Every
|
|
19
|
+
plugin verb is recorded with ``trusted=True`` on the interpreter
|
|
20
|
+
(``interpreter.plugin_verb_info``) so a future sandbox pillar can consult the
|
|
21
|
+
marker when deciding what a scroll may touch.
|
|
22
|
+
|
|
23
|
+
Set ``GODCODE_NO_PLUGINS=1`` in the environment to skip plugin auto-loading
|
|
24
|
+
entirely.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import importlib.metadata
|
|
30
|
+
import importlib.util
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
import types
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
from godcode.values import Contract, RiteFunction, Symbol # noqa: F401 (re-exported for plugin authors)
|
|
39
|
+
|
|
40
|
+
PLUGIN_API_VERSION = 1
|
|
41
|
+
"""Version of the plugin contract this engine speaks."""
|
|
42
|
+
|
|
43
|
+
ENTRY_POINT_GROUP = "godcode_plugins"
|
|
44
|
+
"""importlib.metadata entry-point group for installed plugins (best-effort)."""
|
|
45
|
+
|
|
46
|
+
DISABLE_ENV_VAR = "GODCODE_NO_PLUGINS"
|
|
47
|
+
"""Set to "1" to skip plugin auto-loading entirely."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# discovery
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def default_plugin_dirs() -> list[Path]:
|
|
55
|
+
"""Where plugins are looked for: ``./plugins`` then ``~/.godcode/plugins``."""
|
|
56
|
+
dirs = [Path.cwd() / "plugins"]
|
|
57
|
+
try:
|
|
58
|
+
dirs.append(Path.home() / ".godcode" / "plugins")
|
|
59
|
+
except Exception: # pragma: no cover - exotic platforms without a home
|
|
60
|
+
pass
|
|
61
|
+
return dirs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _warn(message: str) -> None:
|
|
65
|
+
print(f"godcode: plugin warning: {message}", file=sys.stderr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _iter_plugin_files(dirs: list[Path]):
|
|
69
|
+
"""Yield ``(stem, path)`` for each candidate plugin file in *dirs*."""
|
|
70
|
+
for directory in dirs:
|
|
71
|
+
try:
|
|
72
|
+
if not directory.is_dir():
|
|
73
|
+
continue
|
|
74
|
+
entries = sorted(directory.iterdir())
|
|
75
|
+
except OSError as exc:
|
|
76
|
+
_warn(f"cannot scan {directory}: {exc}")
|
|
77
|
+
continue
|
|
78
|
+
for path in entries:
|
|
79
|
+
if not path.is_file() or path.suffix != ".py":
|
|
80
|
+
continue
|
|
81
|
+
stem = path.stem
|
|
82
|
+
if stem == "__init__" or stem.startswith(("_", ".")):
|
|
83
|
+
continue
|
|
84
|
+
yield stem, path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _module_name_for(stem: str) -> str:
|
|
88
|
+
safe = re.sub(r"[^0-9a-zA-Z_]", "_", stem)
|
|
89
|
+
if not safe or safe[0].isdigit():
|
|
90
|
+
safe = "_" + safe
|
|
91
|
+
return f"godcode_plugin_{safe}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _import_from_path(stem: str, path: Path) -> types.ModuleType:
|
|
95
|
+
"""Import the module at *path* without touching ``sys.path``."""
|
|
96
|
+
name = _module_name_for(stem)
|
|
97
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
98
|
+
if spec is None or spec.loader is None: # pragma: no cover - defensive
|
|
99
|
+
raise ImportError(f"cannot build a module spec for {path}")
|
|
100
|
+
module = importlib.util.module_from_spec(spec)
|
|
101
|
+
# NOTE: deliberately *not* cached in sys.modules — each interpreter gets a
|
|
102
|
+
# freshly executed plugin module, so plugin state never leaks between runs.
|
|
103
|
+
spec.loader.exec_module(module)
|
|
104
|
+
return module
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _entry_point_plugins() -> dict[str, Any]:
|
|
108
|
+
"""Best-effort load of the ``godcode_plugins`` entry-point group."""
|
|
109
|
+
found: dict[str, Any] = {}
|
|
110
|
+
try:
|
|
111
|
+
entry_points = importlib.metadata.entry_points()
|
|
112
|
+
except Exception as exc: # pragma: no cover - metadata backend missing
|
|
113
|
+
_warn(f"could not read entry points: {exc}")
|
|
114
|
+
return found
|
|
115
|
+
try:
|
|
116
|
+
if hasattr(entry_points, "select"): # Python 3.10+ EntryPoints
|
|
117
|
+
selected = entry_points.select(group=ENTRY_POINT_GROUP)
|
|
118
|
+
else: # pragma: no cover - legacy dict-style interface
|
|
119
|
+
selected = entry_points.get(ENTRY_POINT_GROUP, ())
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
_warn(f"could not select {ENTRY_POINT_GROUP!r} entry points: {exc}")
|
|
122
|
+
return found
|
|
123
|
+
for ep in selected:
|
|
124
|
+
try:
|
|
125
|
+
found[ep.name] = ep.load()
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
_warn(f"entry-point plugin {ep.name!r} failed to load: {exc}")
|
|
128
|
+
return found
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def discover_plugins(dirs: list[Path] | None = None) -> dict[str, Any]:
|
|
132
|
+
"""Import every plugin module found and return ``{name: module}``.
|
|
133
|
+
|
|
134
|
+
*dirs* defaults to :func:`default_plugin_dirs`, with the
|
|
135
|
+
``godcode_plugins`` entry-point group consulted afterwards (later sources
|
|
136
|
+
shadow earlier ones on name clashes).
|
|
137
|
+
|
|
138
|
+
A module that fails to import is skipped with a warning on stderr — a
|
|
139
|
+
broken plugin never crashes the host run.
|
|
140
|
+
"""
|
|
141
|
+
if dirs is None:
|
|
142
|
+
dirs = default_plugin_dirs()
|
|
143
|
+
discovered: dict[str, Any] = {}
|
|
144
|
+
for stem, path in _iter_plugin_files(dirs):
|
|
145
|
+
try:
|
|
146
|
+
discovered[stem] = _import_from_path(stem, path)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
_warn(f"plugin {path} failed to import ({exc}) — skipped")
|
|
149
|
+
for name, module in _entry_point_plugins().items():
|
|
150
|
+
discovered[name] = module
|
|
151
|
+
return discovered
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def load_plugins(interpreter, dirs: list[Path] | None = None) -> list[str]:
|
|
155
|
+
"""Discover plugins and call ``register(interpreter)`` on each.
|
|
156
|
+
|
|
157
|
+
Returns the names of the plugins whose ``register()`` ran cleanly. A
|
|
158
|
+
missing ``register()``, a ``PLUGIN_API_VERSION`` mismatch, or an
|
|
159
|
+
exception inside ``register()`` only produces a stderr warning — never
|
|
160
|
+
an exception, so startup always survives a bad plugin.
|
|
161
|
+
"""
|
|
162
|
+
loaded: list[str] = []
|
|
163
|
+
for name, module in discover_plugins(dirs).items():
|
|
164
|
+
api = getattr(module, "PLUGIN_API_VERSION", 1)
|
|
165
|
+
if api != PLUGIN_API_VERSION:
|
|
166
|
+
_warn(
|
|
167
|
+
f"plugin {name!r} speaks API version {api}, "
|
|
168
|
+
f"this engine speaks {PLUGIN_API_VERSION} — skipped"
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
register = getattr(module, "register", None)
|
|
172
|
+
if not callable(register):
|
|
173
|
+
_warn(f"plugin {name!r} exposes no register(interpreter) — skipped")
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
register(interpreter)
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
_warn(f"plugin {name!r} register() failed ({exc}) — skipped")
|
|
179
|
+
continue
|
|
180
|
+
loaded.append(name)
|
|
181
|
+
return loaded
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# value conversion: God Code <-> Python
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
#
|
|
188
|
+
# God Code values already *are* Python objects inside the interpreter, so most
|
|
189
|
+
# conversions are the identity. The table below is the contract plugin
|
|
190
|
+
# authors can rely on:
|
|
191
|
+
#
|
|
192
|
+
# God Code number <-> Python int / float (identity)
|
|
193
|
+
# God Code string <-> Python str (identity)
|
|
194
|
+
# God Code boolean <-> Python bool (identity)
|
|
195
|
+
# God Code void <-> Python None (identity)
|
|
196
|
+
# God Code list <-> Python list (recursive)
|
|
197
|
+
# God Code symbol <-> Symbol, a str subclass (identity)
|
|
198
|
+
# God Code contract <-> Contract (opaque, identity)
|
|
199
|
+
# God Code rite <-> RiteFunction (opaque, identity)
|
|
200
|
+
#
|
|
201
|
+
# Anything else a plugin returns (dict, set, tuple, custom objects, ...)
|
|
202
|
+
# passes through opaquely, except tuples which become lists recursively.
|
|
203
|
+
# REVEAL renders opaque values with str().
|
|
204
|
+
|
|
205
|
+
def to_python(value: Any) -> Any:
|
|
206
|
+
"""Convert a God Code value to the Python value a plugin receives.
|
|
207
|
+
|
|
208
|
+
Identity for every documented God Code value; see the table above.
|
|
209
|
+
"""
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def to_godcode(value: Any) -> Any:
|
|
214
|
+
"""Convert a plugin's Python return value back into a God Code value.
|
|
215
|
+
|
|
216
|
+
Tuples become lists (recursively); every other value passes through
|
|
217
|
+
as-is. Values with no God Code counterpart (dicts, sets, custom
|
|
218
|
+
objects) travel opaquely and are rendered with ``str()`` by REVEAL.
|
|
219
|
+
"""
|
|
220
|
+
if isinstance(value, tuple):
|
|
221
|
+
return [to_godcode(item) for item in value]
|
|
222
|
+
if isinstance(value, list):
|
|
223
|
+
return [to_godcode(item) for item in value]
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# interpreter mixin helpers (kept here so interpreter.py stays small)
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def make_verb_adapter(name: str, func: Callable[..., Any]):
|
|
232
|
+
"""Wrap a plain ``func(*args)`` into the ``(args, line)`` builtin shape.
|
|
233
|
+
|
|
234
|
+
Arguments are converted God Code -> Python, the return value Python ->
|
|
235
|
+
God Code. A plugin exception becomes a line-numbered GodRuntimeError;
|
|
236
|
+
divine errors pass through untouched.
|
|
237
|
+
"""
|
|
238
|
+
from godcode.errors import GodCodeError, GodRuntimeError
|
|
239
|
+
|
|
240
|
+
def _adapter(args: list, line):
|
|
241
|
+
py_args = [to_python(arg) for arg in args]
|
|
242
|
+
try:
|
|
243
|
+
result = func(*py_args)
|
|
244
|
+
except GodCodeError:
|
|
245
|
+
raise
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
raise GodRuntimeError(
|
|
248
|
+
f"The '{name}' verb faltered in the outer world: {exc}",
|
|
249
|
+
line,
|
|
250
|
+
) from exc
|
|
251
|
+
return to_godcode(result)
|
|
252
|
+
|
|
253
|
+
_adapter.__name__ = f"plugin:{name}"
|
|
254
|
+
_adapter.__doc__ = getattr(func, "__doc__", None)
|
|
255
|
+
return _adapter
|