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/sandbox.py ADDED
@@ -0,0 +1,385 @@
1
+ """In-process sandbox for God Code v3.0 — Pillar 1 ("Strong Foundations").
2
+
3
+ Runs a God Code creation under a deny-by-default :class:`SandboxPolicy`:
4
+ every side-effecting power (scroll imports outside the consecrated paths,
5
+ the ASK rite that speaks with the outer world) is withheld unless the
6
+ policy explicitly grants it. File-writing rites keep their meaning but
7
+ not their reach: ANCHOR is answered with an ephemeral in-memory chain,
8
+ so nothing is ever written to disk. A step budget and a wall-clock grant
9
+ bound runaway creations.
10
+
11
+ Entry points
12
+ ------------
13
+ ``run_sandboxed(source, policy, source_name)``
14
+ Lex, parse, and run ``source`` under ``policy``; returns the list of
15
+ REVEAL lines captured during the run.
16
+
17
+ ``apply_policy(interpreter, policy)``
18
+ Install the sandbox hooks on an existing ``Interpreter``; returns the
19
+ ``Sandbox`` guard.
20
+
21
+ ``SandboxPolicy.strict(...)``
22
+ The policy the CLI uses: no writes, no network, no subprocesses, no
23
+ stdin, scroll imports limited to the stdlib scrolls and (optionally)
24
+ the creation's own directory, a 5-second time grant, and a 100,000
25
+ step budget.
26
+
27
+ Honest limits (see docs/sandbox.md): this is an *in-process* sandbox —
28
+ a cooperative audit of the tree-walker, not OS-level isolation. It
29
+ cannot contain a hostile program that escapes the interpreter, and it
30
+ cannot cap memory.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import contextlib
36
+ import os
37
+ import signal
38
+ import time
39
+ from dataclasses import dataclass, field
40
+ from pathlib import Path
41
+ from typing import Any, Callable
42
+
43
+ from godcode.errors import GodCodeError, SandboxViolation
44
+
45
+ __all__ = [
46
+ "SandboxPolicy",
47
+ "Sandbox",
48
+ "SandboxViolation",
49
+ "apply_policy",
50
+ "run_sandboxed",
51
+ "run_sandboxed_with_interpreter",
52
+ ]
53
+
54
+
55
+ def _stdlib_scrolls_dir() -> str:
56
+ return str(Path(__file__).parent / "scrolls")
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Policy
61
+ # ---------------------------------------------------------------------------
62
+ @dataclass
63
+ class SandboxPolicy:
64
+ """What a sandboxed creation is permitted to do. Deny-by-default.
65
+
66
+ ``allow_read_paths``
67
+ Approved directories for filesystem *reads*. ``None`` (the
68
+ default) denies all reads; a tuple of directories grants reads
69
+ under those trees. (No file-reading rites exist in v2.0; this is
70
+ enforced should any be added.)
71
+ ``allow_write`` / ``allow_network`` / ``allow_subprocess``
72
+ Filesystem writes, network access, subprocess spawning. All deny
73
+ by default. The ANCHOR rite is the one file-writing rite: under a
74
+ policy that denies writes it is answered with an ephemeral
75
+ in-memory chain instead of being refused, so creations keep
76
+ their meaning without touching the disk.
77
+ ``allow_stdin``
78
+ Whether the ASK rite may speak with the outer world (``input()``).
79
+ Denied by default.
80
+ ``allowed_import_paths``
81
+ Approved directories for IMPORT of scrolls. Empty (the default)
82
+ denies every import. Each entry is a directory; a scroll is
83
+ allowed if its real path lies under one of them.
84
+ ``timeout_seconds``
85
+ Wall-clock grant for the whole run. ``None`` or ``<= 0``
86
+ disables the grant (not recommended).
87
+ ``max_steps``
88
+ Step budget: every statement dispatch and every expression
89
+ evaluation counts one step, bounding both runaway loops and
90
+ unbounded rite recursion.
91
+ """
92
+
93
+ allow_read_paths: tuple[str, ...] | None = None
94
+ allow_write: bool = False
95
+ allow_network: bool = False
96
+ allow_subprocess: bool = False
97
+ allow_stdin: bool = False
98
+ allowed_import_paths: tuple[str, ...] = ()
99
+ timeout_seconds: float | None = 5.0
100
+ max_steps: int = 100_000
101
+
102
+ @classmethod
103
+ def strict(
104
+ cls,
105
+ *,
106
+ source_dir: str | None = None,
107
+ timeout_seconds: float | None = 5.0,
108
+ ) -> "SandboxPolicy":
109
+ """The CLI's strict policy: deny everything, bless the stdlib.
110
+
111
+ Scroll imports are allowed from the bundled stdlib scrolls and,
112
+ when given, the creation's own directory — nothing else.
113
+ """
114
+ paths = [_stdlib_scrolls_dir()]
115
+ if source_dir:
116
+ paths.append(str(source_dir))
117
+ return cls(
118
+ allowed_import_paths=tuple(paths),
119
+ timeout_seconds=timeout_seconds,
120
+ )
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # The guard
125
+ # ---------------------------------------------------------------------------
126
+ # Builtin rite name -> SandboxPolicy attribute that must be truthy for the
127
+ # rite to run. ANCHOR is deliberately absent: under a policy that denies
128
+ # writes it is answered with an ephemeral in-memory chain (see
129
+ # run_sandboxed_with_interpreter), so the rite itself never reaches the
130
+ # filesystem and needs no grant.
131
+ _SIDE_EFFECT_RITES: dict[str, str] = {
132
+ "ASK": "allow_stdin",
133
+ }
134
+
135
+ _HOOKS = ("_exec_stmt", "_eval_expr", "_call", "_exec_import")
136
+
137
+
138
+ class Sandbox:
139
+ """Enforces a :class:`SandboxPolicy` on a live Interpreter.
140
+
141
+ Installed hooks (see :meth:`install`):
142
+
143
+ * ``_exec_stmt`` / ``_eval_expr`` — count one step per statement
144
+ dispatch and expression evaluation; check the step budget and the
145
+ wall-clock grant; record the current line for divine errors.
146
+ * ``_call`` — refuse side-effecting rites the policy does not grant
147
+ (ASK today; the registry above grows with the language).
148
+ * ``_exec_import`` — resolve the scroll, then refuse any path
149
+ outside ``allowed_import_paths``.
150
+ """
151
+
152
+ def __init__(self, interpreter: Any, policy: SandboxPolicy):
153
+ self.interpreter = interpreter
154
+ self.policy = policy
155
+ self.steps = 0
156
+ self.started_at: float | None = None
157
+ self.current_line: int | None = None
158
+ self._originals: dict[str, Callable] = {}
159
+ self._allowed_import_dirs = [
160
+ os.path.realpath(p) for p in policy.allowed_import_paths
161
+ ]
162
+
163
+ # -- installation ---------------------------------------------------
164
+
165
+ def install(self) -> "Sandbox":
166
+ """Wrap the interpreter's dispatch points. Idempotent."""
167
+ interp = self.interpreter
168
+ if self._originals:
169
+ return self
170
+ for name in _HOOKS:
171
+ original = getattr(interp, name)
172
+ self._originals[name] = original
173
+ setattr(interp, name, self._wrap(name, original))
174
+ return self
175
+
176
+ def uninstall(self) -> None:
177
+ """Restore the interpreter's original dispatch points."""
178
+ for name, original in self._originals.items():
179
+ setattr(self.interpreter, name, original)
180
+ self._originals.clear()
181
+
182
+ def _wrap(self, name: str, original: Callable) -> Callable:
183
+ if name == "_exec_stmt":
184
+ def exec_stmt(stmt, env):
185
+ self.tick(stmt)
186
+ return original(stmt, env)
187
+ return exec_stmt
188
+ if name == "_eval_expr":
189
+ def eval_expr(expr, env):
190
+ self.tick(expr)
191
+ return original(expr, env)
192
+ return eval_expr
193
+ if name == "_call":
194
+ def call(rite_name, args, env, line):
195
+ self.check_rite(rite_name, line)
196
+ return original(rite_name, args, env, line)
197
+ return call
198
+ if name == "_exec_import":
199
+ def exec_import(stmt, env):
200
+ line = getattr(stmt, "line", None)
201
+ path = self.interpreter._resolve_import(stmt.path, line)
202
+ self.check_import_path(path, stmt.path, line)
203
+ return original(stmt, env)
204
+ return exec_import
205
+ raise AssertionError(f"unknown hook: {name}") # pragma: no cover
206
+
207
+ # -- enforcement -----------------------------------------------------
208
+
209
+ def tick(self, node: Any) -> None:
210
+ """Count one interpreter step; enforce budget and time grant."""
211
+ self.steps += 1
212
+ line = getattr(node, "line", None)
213
+ if line is not None:
214
+ self.current_line = line
215
+ if self.steps > self.policy.max_steps:
216
+ raise SandboxViolation(
217
+ "The sandbox withholds this power: the creation has taken "
218
+ f"more than {self.policy.max_steps:,} steps — "
219
+ "the step budget is spent, and the cycle is released.",
220
+ self.current_line,
221
+ )
222
+ if self.started_at is not None and self.policy.timeout_seconds:
223
+ elapsed = time.monotonic() - self.started_at
224
+ if elapsed > self.policy.timeout_seconds:
225
+ raise SandboxViolation(
226
+ "The sandbox withholds this power: the appointed time "
227
+ f"({self.policy.timeout_seconds:g}s) is spent — "
228
+ "the creation is released in peace.",
229
+ self.current_line,
230
+ )
231
+
232
+ def check_rite(self, name: str, line: int | None) -> None:
233
+ """Refuse side-effecting rites the policy does not grant."""
234
+ need = _SIDE_EFFECT_RITES.get(str(name).upper())
235
+ if need is not None and not getattr(self.policy, need, False):
236
+ divine = {
237
+ "ASK": "the rite ASK would speak with the outer world",
238
+ }.get(str(name).upper(), f"the rite {name}")
239
+ raise SandboxViolation(
240
+ f"The sandbox withholds this power: {divine} — "
241
+ "it is not granted.",
242
+ line,
243
+ )
244
+
245
+ def check_import_path(
246
+ self, path: Path, requested: str, line: int | None
247
+ ) -> None:
248
+ """Refuse scrolls outside the consecrated import paths."""
249
+ resolved = os.path.realpath(path)
250
+ for allowed in self._allowed_import_dirs:
251
+ if resolved == allowed or resolved.startswith(allowed + os.sep):
252
+ return
253
+ raise SandboxViolation(
254
+ "The sandbox withholds this power: the scroll "
255
+ f"'{requested}' lies outside the consecrated paths — "
256
+ "it may not be breathed in.",
257
+ line,
258
+ )
259
+
260
+
261
+ def apply_policy(interpreter: Any, policy: SandboxPolicy) -> Sandbox:
262
+ """Install sandbox hooks on ``interpreter``; return the guard.
263
+
264
+ One guard per interpreter; the hooks stay installed until
265
+ ``guard.uninstall()`` is called.
266
+ """
267
+ return Sandbox(interpreter, policy).install()
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Timeout: POSIX signal alarm with a wall-clock fallback
272
+ # ---------------------------------------------------------------------------
273
+ @contextlib.contextmanager
274
+ def _time_limit(seconds: float | None, guard: Sandbox):
275
+ """Raise SandboxViolation from a POSIX alarm when the grant expires.
276
+
277
+ The wall-clock check in :meth:`Sandbox.tick` is the fallback (and the
278
+ only enforcement on platforms without ``setitimer``, or outside the
279
+ main thread, where signals cannot be armed).
280
+ """
281
+ armed = False
282
+ old_handler = None
283
+
284
+ def _handler(signum, frame): # noqa: ARG001
285
+ raise SandboxViolation(
286
+ "The sandbox withholds this power: the appointed time "
287
+ f"({seconds:g}s) is spent — the creation is released in peace.",
288
+ guard.current_line,
289
+ )
290
+
291
+ try:
292
+ if seconds and hasattr(signal, "setitimer"):
293
+ old_handler = signal.signal(signal.SIGALRM, _handler)
294
+ signal.setitimer(signal.ITIMER_REAL, seconds)
295
+ armed = True
296
+ except (ValueError, OSError, RuntimeError):
297
+ armed = False # not the main thread, or signals unavailable
298
+ try:
299
+ yield
300
+ finally:
301
+ if armed:
302
+ try:
303
+ signal.setitimer(signal.ITIMER_REAL, 0)
304
+ signal.signal(signal.SIGALRM, old_handler)
305
+ except (ValueError, OSError, RuntimeError):
306
+ pass
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # High-level entry point
311
+ # ---------------------------------------------------------------------------
312
+ def run_sandboxed_with_interpreter(
313
+ source: str,
314
+ policy: SandboxPolicy | None = None,
315
+ source_name: str = "<sandbox>",
316
+ ) -> tuple[list[str], Any]:
317
+ """Run God Code ``source`` under ``policy``; return (REVEAL lines, interpreter).
318
+
319
+ The interpreter is returned so callers can read machine state the run
320
+ gathered (e.g. ``intent_checks`` for the v4.0 intent layer).
321
+ """
322
+ from godcode import plugins
323
+ from godcode.chain import MemoryChainAdapter
324
+ from godcode.interpreter import Interpreter
325
+ from godcode.spirit import SpiritEngine
326
+
327
+ policy = policy or SandboxPolicy.strict()
328
+ env_var = plugins.DISABLE_ENV_VAR
329
+ previous = os.environ.get(env_var)
330
+ os.environ[env_var] = "1"
331
+ try:
332
+ interpreter = Interpreter(log_path=None, interactive=False)
333
+ finally:
334
+ if previous is None:
335
+ os.environ.pop(env_var, None)
336
+ else:
337
+ os.environ[env_var] = previous
338
+ # v4.0: the covenant ledger and the audit log stay unbound (they write
339
+ # to the host world, which the sandbox does not permit). The Spirit
340
+ # only reads its dataset, so it is bound: CONSULT and the DECLARE
341
+ # INTENT discernment need it. ANCHOR is answered with an ephemeral
342
+ # in-memory chain, so no file is ever written.
343
+ try:
344
+ interpreter.spirit = SpiritEngine()
345
+ except Exception:
346
+ pass
347
+ if not policy.allow_write:
348
+ interpreter.chain_adapters = {"simulated": MemoryChainAdapter()}
349
+ guard = apply_policy(interpreter, policy)
350
+ guard.started_at = time.monotonic()
351
+ try:
352
+ with _time_limit(policy.timeout_seconds, guard):
353
+ interpreter.run_source(source, source_name=source_name)
354
+ except RecursionError:
355
+ raise SandboxViolation(
356
+ "The sandbox withholds this power: the rites called upon "
357
+ "themselves past the deep places — the recursion budget is "
358
+ "spent, and the cycle is released.",
359
+ guard.current_line,
360
+ ) from None
361
+ finally:
362
+ guard.started_at = None
363
+ return list(interpreter.output), interpreter
364
+
365
+
366
+ def run_sandboxed(
367
+ source: str,
368
+ policy: SandboxPolicy | None = None,
369
+ source_name: str = "<sandbox>",
370
+ ) -> list[str]:
371
+ """Run God Code ``source`` under ``policy``; return captured REVEAL lines.
372
+
373
+ Uses the strict policy when none is given. The covenant ledger and
374
+ the audit log stay unbound -- they write to the host world, which
375
+ the sandbox does not permit. The Spirit is bound (it only reads its
376
+ dataset), and ANCHOR answers with an ephemeral in-memory chain, so
377
+ CONSULT and DECLARE INTENT work while no file is ever written. Host
378
+ plugin auto-loading is disabled for the run (via the plugin system's
379
+ own opt-out): plugins are trusted host code that runs outside any
380
+ policy, so a deny-by-default sandbox must not breathe them in unasked.
381
+ Raises :class:`SandboxViolation` (a GodCodeError, line-numbered)
382
+ when the creation reaches beyond its grant.
383
+ """
384
+ output, _ = run_sandboxed_with_interpreter(source, policy, source_name)
385
+ return output
@@ -0,0 +1,10 @@
1
+ # The Scroll of the Covenant -- making and sealing binding promises.
2
+ # Rites: NEW_COVENANT, SEAL_COVENANT.
3
+
4
+ DEFINE RITE NEW_COVENANT(name)
5
+ RETURN contract(name)
6
+ END RITE
7
+
8
+ DEFINE RITE SEAL_COVENANT(c)
9
+ SEAL c
10
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "covenant"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of the Covenant: making and sealing binding promises. Rites: NEW_COVENANT, SEAL_COVENANT."
5
+ entry = "covenant.god"
6
+ godcode = ">=2.0"
@@ -0,0 +1,53 @@
1
+ # The Scroll of Multitudes -- reckonings over gathered lists.
2
+ # Rites: SUM, AVG, CONTAINS, SECOND, TAIL, COUNT.
3
+
4
+ DEFINE RITE SUM(xs)
5
+ DECLARE total AS 0
6
+ DECLARE i AS 0
7
+ WHILE i < LEN(xs) DO
8
+ DECLARE total AS total + xs[i]
9
+ DECLARE i AS i + 1
10
+ ENDWHILE
11
+ RETURN total
12
+ END RITE
13
+
14
+ DEFINE RITE AVG(xs)
15
+ RETURN SUM(xs) / LEN(xs)
16
+ END RITE
17
+
18
+ DEFINE RITE CONTAINS(xs, x)
19
+ DECLARE i AS 0
20
+ WHILE i < LEN(xs) DO
21
+ IF xs[i] IS x THEN
22
+ RETURN 1
23
+ ENDIF
24
+ DECLARE i AS i + 1
25
+ ENDWHILE
26
+ RETURN 0
27
+ END RITE
28
+
29
+ DEFINE RITE SECOND(xs)
30
+ RETURN xs[1]
31
+ END RITE
32
+
33
+ DEFINE RITE TAIL(xs)
34
+ DECLARE tail AS []
35
+ DECLARE i AS 1
36
+ WHILE i < LEN(xs) DO
37
+ DECLARE tail AS PUSH(tail, xs[i])
38
+ DECLARE i AS i + 1
39
+ ENDWHILE
40
+ RETURN tail
41
+ END RITE
42
+
43
+ DEFINE RITE COUNT(xs, x)
44
+ DECLARE n AS 0
45
+ DECLARE i AS 0
46
+ WHILE i < LEN(xs) DO
47
+ IF xs[i] IS x THEN
48
+ DECLARE n AS n + 1
49
+ ENDIF
50
+ DECLARE i AS i + 1
51
+ ENDWHILE
52
+ RETURN n
53
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "lists"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of Multitudes: reckonings over gathered lists. Rites: SUM, AVG, CONTAINS, SECOND, TAIL, COUNT."
5
+ entry = "lists.god"
6
+ godcode = ">=2.0"
@@ -0,0 +1,64 @@
1
+ # The Scroll of Numbers -- divine arithmetic for the children of creation.
2
+ # Rites: SQRT, POW, ABS, MIN, MAX, FACTORIAL, IS_EVEN.
3
+
4
+ DEFINE RITE SQRT(x)
5
+ IF x IS 0 THEN
6
+ RETURN 0
7
+ ENDIF
8
+ IF x < 0 THEN
9
+ RETURN 0
10
+ ENDIF
11
+ DECLARE guess AS x
12
+ WHILE ABS(guess * guess - x) > 0.000000001 DO
13
+ DECLARE guess AS guess - (guess * guess - x) / (2 * guess)
14
+ ENDWHILE
15
+ RETURN guess
16
+ END RITE
17
+
18
+ DEFINE RITE POW(b, e)
19
+ DECLARE result AS 1
20
+ DECLARE i AS 0
21
+ WHILE i < e DO
22
+ DECLARE result AS result * b
23
+ DECLARE i AS i + 1
24
+ ENDWHILE
25
+ RETURN result
26
+ END RITE
27
+
28
+ DEFINE RITE ABS(x)
29
+ IF x < 0 THEN
30
+ RETURN 0 - x
31
+ ENDIF
32
+ RETURN x
33
+ END RITE
34
+
35
+ DEFINE RITE MIN(a, b)
36
+ IF a < b THEN
37
+ RETURN a
38
+ ENDIF
39
+ RETURN b
40
+ END RITE
41
+
42
+ DEFINE RITE MAX(a, b)
43
+ IF a > b THEN
44
+ RETURN a
45
+ ENDIF
46
+ RETURN b
47
+ END RITE
48
+
49
+ DEFINE RITE FACTORIAL(n)
50
+ DECLARE result AS 1
51
+ DECLARE i AS 1
52
+ WHILE i <= n DO
53
+ DECLARE result AS result * i
54
+ DECLARE i AS i + 1
55
+ ENDWHILE
56
+ RETURN result
57
+ END RITE
58
+
59
+ DEFINE RITE IS_EVEN(n)
60
+ IF n % 2 IS 0 THEN
61
+ RETURN 1
62
+ ENDIF
63
+ RETURN 0
64
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "math"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of Numbers: divine arithmetic. Rites: SQRT, POW, ABS, MIN, MAX, FACTORIAL, IS_EVEN."
5
+ entry = "math.god"
6
+ godcode = ">=2.0"
@@ -0,0 +1,14 @@
1
+ # The Scroll of Lots -- discerning choice by sacred chance.
2
+ # Rites: PROPHESY_NUMBER, CAST_LOTS, CHOOSE.
3
+
4
+ DEFINE RITE PROPHESY_NUMBER(n)
5
+ RETURN RANDOM(n)
6
+ END RITE
7
+
8
+ DEFINE RITE CAST_LOTS()
9
+ RETURN RANDOM(2)
10
+ END RITE
11
+
12
+ DEFINE RITE CHOOSE(xs)
13
+ RETURN xs[RANDOM(LEN(xs))]
14
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "prophecy"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of Lots: discerning choice by sacred chance. Rites: PROPHESY_NUMBER, CAST_LOTS, CHOOSE."
5
+ entry = "prophecy.god"
6
+ godcode = ">=2.0"
@@ -0,0 +1,32 @@
1
+ # The Scroll of Tongues -- words shaped, lifted up, and proclaimed.
2
+ # Rites: SHOUT, WHISPER, WORDS, CHARS, FIRST, LAST.
3
+
4
+ DEFINE RITE SHOUT(s)
5
+ RETURN UPPER(s) + "!"
6
+ END RITE
7
+
8
+ DEFINE RITE WHISPER(s)
9
+ RETURN LOWER(s)
10
+ END RITE
11
+
12
+ DEFINE RITE WORDS(s)
13
+ RETURN SPLIT(s, " ")
14
+ END RITE
15
+
16
+ DEFINE RITE CHARS(s)
17
+ DECLARE chars AS []
18
+ DECLARE i AS 0
19
+ WHILE i < LEN(s) DO
20
+ DECLARE chars AS PUSH(chars, s[i])
21
+ DECLARE i AS i + 1
22
+ ENDWHILE
23
+ RETURN chars
24
+ END RITE
25
+
26
+ DEFINE RITE FIRST(s)
27
+ RETURN s[0]
28
+ END RITE
29
+
30
+ DEFINE RITE LAST(s)
31
+ RETURN s[LEN(s) - 1]
32
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "strings"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of Tongues: words shaped, lifted up, and proclaimed. Rites: SHOUT, WHISPER, WORDS, CHARS, FIRST, LAST."
5
+ entry = "strings.god"
6
+ godcode = ">=2.0"
@@ -0,0 +1,10 @@
1
+ # The Scroll of Appointed Times -- beholding the hour and the day.
2
+ # Rites: NOW, TODAY.
3
+
4
+ DEFINE RITE NOW()
5
+ RETURN BEHOLD()
6
+ END RITE
7
+
8
+ DEFINE RITE TODAY()
9
+ RETURN SPLIT(BEHOLD(), "T")[0]
10
+ END RITE
@@ -0,0 +1,6 @@
1
+ name = "time"
2
+ version = "1.0.0"
3
+ author = "God Code contributors"
4
+ description = "The Scroll of Appointed Times: beholding the hour and the day. Rites: NOW, TODAY."
5
+ entry = "time.god"
6
+ godcode = ">=2.0"