From: Thomas Walker Lynch
Date: Tue, 10 Nov 2026 08:15:18 +0000 (+0000)
Subject: aligns to RT code practices, adds a return value after the journal plays
X-Git-Url: https://git.reasoningtechnology.com/?a=commitdiff_plain;h=d13e9c1ac02fe8dd5b27a2f6efcb5fc7a6c80351;p=Man-In-Grey
aligns to RT code practices, adds a return value after the journal plays
---
diff --git a/developer/authored/Man_In_Grey.copy/Constraint.py b/developer/authored/Man_In_Grey.copy/Constraint.py
deleted file mode 100644
index 9219286..0000000
--- a/developer/authored/Man_In_Grey.copy/Constraint.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-Constraint - predicates that recur among guards' constraints.
-
-A guard's constraints are written at the top of the guard itself, as
-predicates such as is_allowed_account(user_name). This library holds the
-predicates many guards would otherwise each write: membership of a group,
-a path within a directory, a permission within a limit. A guard imports it
-after its ownership check, as it imports its other modules, and its
-constraints call it by name, as Constraint.is_group_member(user_name ,"bio").
-"""
-
-import grp ,os ,pwd
-
-
-def is_group_member(user_name ,group_name):
- """
- True when the account is a member of the group by the group database:
- listed in /etc/group as a member, or having the group as its primary group.
- """
- try:
- group = grp.getgrnam(group_name)
- except KeyError:
- return False
- if user_name in group.gr_mem:
- return True
- try:
- return pwd.getpwnam(user_name).pw_gid == group.gr_gid
- except KeyError:
- return False
-
-def has_real_group(group_name):
- """
- True when the gate's real gid or supplementary groups include the group.
- These are what the program that invoked the gate held, which the kernel
- passes on: a session started before the account joined the group does not
- hold it, though is_group_member already says yes.
- """
- try:
- gid = grp.getgrnam(group_name).gr_gid
- except KeyError:
- return False
- return gid == os.getgid() or gid in os.getgroups()
-
-def is_path_under(path ,directory):
- """
- True when path is directory or lies within it. Both are to be absolute and
- in normal form, as the guard's wellformed check makes a journal's paths.
- """
- if not (isinstance(path ,str) and isinstance(directory ,str)):
- return False
- if not (path.startswith("/") and directory.startswith("/")):
- return False
- if os.path.normpath(path) != path or os.path.normpath(directory) != directory:
- return False
- return path == directory or path.startswith(directory.rstrip("/") + "/")
-
-def has_permission_within(permission ,limit):
- """
- True when permission sets no bit that limit does not: 0o644 and 0o600 are
- within 0o644, and 0o4755, setuid, is within neither 0o644 nor 0o755.
- """
- return isinstance(permission ,int) and not isinstance(permission ,bool) and permission & ~limit == 0
diff --git a/developer/authored/Man_In_Grey.copy/EmplaceBioLanguage.py b/developer/authored/Man_In_Grey.copy/EmplaceBioLanguage.py
deleted file mode 100644
index 61dda8d..0000000
--- a/developer/authored/Man_In_Grey.copy/EmplaceBioLanguage.py
+++ /dev/null
@@ -1,361 +0,0 @@
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-EmplaceBioLanguage - the emplace_bio instruction language: a bio page.
-
-A host publishes one bio page for each permitted account, as
-.html in a directory its web server serves, with the picture
-beside it as .png or .jpg. The web server's account
-owns the directory, so a person cannot write there. He gives his page's
-display name, text and picture as instructions, and the emplace_bio gate,
-owned by the web server's account, places the page.
-
-A journal names no file. The file names come from the user name of
-account A, which the gate gives the interpreter, and the directory comes
-from page_directory() at the top of the guard. The interpreter writes the
-HTML itself, from a fixed form, and escapes every text it is given, so
-nothing a person gives becomes markup. Decoration travels out of band, as a
-list of styles beside each run of text, and each style becomes one fixed
-tag with no attribute.
-
- display_name(text) the page's title and heading; required, once
- paragraph() starts a paragraph
- text(content ,style) a run of text in the current paragraph, with a list
- of styles drawn from STYLE; an empty list is plain text
- picture(content) a PNG or JPEG image; at most once
-
-The journal describes the whole page: a run that gives no picture removes
-the picture an earlier run placed.
-
-The module has two halves. The composer half is JournalInterface, which
-checks every argument when the call is made and raises JournalError on a
-wrong one. The interpreter half holds the checks the guard makes of a
-journal received as data, and the execution the interpreter carries out.
-
-The constraints are predicates written at the top of the guard,
-guard_emplace_bio.py: is_allowed_account, is_allowed_text_size and
-is_allowed_picture_size, and page_directory(), where the pages go.
-"""
-
-import html ,os ,stat ,urllib.parse
-
-from Journal import JournalError ,has_control ,clean
-
-NAME = "emplace_bio"
-
-# each style and its tag, in the order the tags nest, outermost first
-STYLE = {
- "strong": "strong"
- ,"emphasis": "em"
- ,"underline": "u"
- ,"strikethrough": "s"
-}
-
-ARGUMENT = {
- "display_name": ("text" ,)
- ,"paragraph": ()
- ,"text": ("content" ,"style")
- ,"picture": ("content" ,)
-}
-
-DISPLAY_NAME_MAX = 200 # characters
-
-SIGNATURE = (
- (b"\x89PNG\r\n\x1a\n" ,".png")
- ,(b"\xff\xd8\xff" ,".jpg")
-)
-
-PAGE_PERMISSION = 0o644
-
-
-# --------------------------------------------------------------------------------
-# argument checks, shared by both halves
-
-def display_name_problem(value):
- if not isinstance(value ,str) or not value.strip():
- return "the display name is to be text that is not empty"
- if has_control(value):
- return f"the display name {clean(value)} holds a control character"
- if len(value) > DISPLAY_NAME_MAX:
- return f"the display name is longer than {DISPLAY_NAME_MAX} characters"
- return None
-
-def content_problem(value):
- if not isinstance(value ,str) or not value:
- return "content is to be text that is not empty"
- if has_control(value):
- return f"content {clean(value)} holds a control character; a new paragraph is the instruction paragraph()"
- return None
-
-def style_problem(value):
- if not isinstance(value ,list) or not all(isinstance(s ,str) for s in value):
- return "style is to be a list of style names"
- for s in value:
- if s not in STYLE:
- return f"style {clean(repr(s))} is not one of {', '.join(STYLE)}"
- if len(set(value)) != len(value):
- return "style names a style twice"
- return None
-
-def picture_extension(content):
- "the extension for the picture's format, or None when it is neither PNG nor JPEG"
- for signature ,extension in SIGNATURE:
- if content.startswith(signature):
- return extension
- return None
-
-
-# --------------------------------------------------------------------------------
-# composer half
-
-class JournalInterface:
- """
- The journal interface of the emplace_bio language. A composer is given
- one, and places instructions with its four functions.
- """
-
- def __init__(self ,journal ,working_dpath):
- self._journal = journal
- self._in_paragraph = False
-
- def display_name(self ,text):
- problem = display_name_problem(text)
- if problem: raise JournalError(f"display_name: {problem}")
- self._journal.place("display_name" ,{"text": text})
-
- def paragraph(self):
- self._journal.place("paragraph" ,{})
- self._in_paragraph = True
-
- def text(self ,content ,style=()):
- if not self._in_paragraph:
- raise JournalError("text: no paragraph has been started; place paragraph() first")
- problem = content_problem(content)
- if problem: raise JournalError(f"text: {problem}")
- if isinstance(style ,str) or not hasattr(style ,"__iter__"):
- raise JournalError("text: style is to be a list of style names")
- style = list(style)
- problem = style_problem(style)
- if problem: raise JournalError(f"text: {problem}")
- style = [s for s in STYLE if s in style]
- self._journal.place("text" ,{"content": content ,"style": style})
-
- def picture(self ,content):
- if not isinstance(content ,(bytes ,bytearray)):
- raise JournalError("picture: content is to be the bytes of a PNG or JPEG file")
- content = bytes(content)
- if picture_extension(content) is None:
- raise JournalError("picture: content is neither a PNG nor a JPEG file")
- self._journal.place("picture" ,{"content": content})
-
-
-# --------------------------------------------------------------------------------
-# interpreter half: the guard's checks
-
-def check_wellformed(data):
- problem_list = []
- count = {name: 0 for name in ARGUMENT}
- in_paragraph = False
- run_count = 0
- for index ,instruction in enumerate(data["instruction_list"]):
- name ,argument = instruction["name"] ,instruction["argument"]
- if name not in ARGUMENT:
- problem_list.append(f"@{index}: unknown instruction {clean(repr(name))}"); continue
- # the order of the instructions, checked whatever their arguments
- count[name] += 1
- if name == "paragraph":
- if in_paragraph and run_count == 0:
- problem_list.append(f"@{index} paragraph: the paragraph before it holds no text")
- in_paragraph ,run_count = True ,0
- elif name == "text":
- if not in_paragraph:
- problem_list.append(f"@{index} text: no paragraph has been started")
- run_count += 1
- if set(argument) != set(ARGUMENT[name]):
- problem_list.append(f"@{index} {name}: arguments {clean(sorted(argument))}, expected {sorted(ARGUMENT[name])}"); continue
- if name == "display_name":
- problem = display_name_problem(argument["text"])
- if problem: problem_list.append(f"@{index} display_name: {problem}")
- elif name == "text":
- for problem in (content_problem(argument["content"]) ,style_problem(argument["style"])):
- if problem: problem_list.append(f"@{index} text: {problem}")
- elif name == "picture":
- if not isinstance(argument["content"] ,(bytes ,bytearray)) or not argument["content"]:
- problem_list.append(f"@{index} picture: content is not bytes")
- if count["display_name"] != 1:
- problem_list.append(f"the journal gives the display name {count['display_name']} times; it is given once")
- if count["paragraph"] == 0:
- problem_list.append("the journal has no paragraph")
- elif run_count == 0:
- problem_list.append("the last paragraph holds no text")
- if count["picture"] > 1:
- problem_list.append(f"the journal gives {count['picture']} pictures; it gives at most one")
- return problem_list
-
-def page_directory_problem(directory):
- if not isinstance(directory ,str) or not directory.startswith("/") or os.path.normpath(directory) != directory:
- return f"page_directory() gives {clean(repr(directory))}, not an absolute directory in normal form"
- return None
-
-def check_constraint(data ,constraint ,gate_invoking_account):
- "constraint is the guard, which holds the predicates; a predicate allows only by answering True"
- if constraint.is_allowed_account(gate_invoking_account) is not True:
- return [f"gate invoking account {clean(repr(gate_invoking_account))} is not allowed to give journals to this gate"]
- problem_list = []
- problem = page_directory_problem(constraint.page_directory())
- if problem: problem_list.append(problem)
- text_bytes = 0
- for instruction in data["instruction_list"]:
- name ,argument = instruction["name"] ,instruction["argument"]
- if name == "display_name":
- text_bytes += len(argument["text"].encode("utf-8"))
- elif name == "text":
- text_bytes += len(argument["content"].encode("utf-8"))
- elif name == "picture" and constraint.is_allowed_picture_size(len(argument["content"])) is not True:
- problem_list.append(f"the picture is {len(argument['content'])} bytes, which is not allowed")
- if constraint.is_allowed_text_size(text_bytes) is not True:
- problem_list.append(f"the text is {text_bytes} bytes, which is not allowed")
- return problem_list
-
-def check_content(data):
- problem_list = []
- for index ,instruction in enumerate(data["instruction_list"]):
- if instruction["name"] == "picture" and picture_extension(instruction["argument"]["content"]) is None:
- problem_list.append(f"@{index} picture: the content is neither a PNG nor a JPEG file")
- return problem_list
-
-def page_name_list(user_name):
- "the files a page of this account can occupy"
- return [user_name + ".html"] + [user_name + extension for _ ,extension in SIGNATURE]
-
-def check_filesystem(data ,constraint ,gate_invoking_account):
- problem_list = []
- if has_control(gate_invoking_account) or gate_invoking_account.startswith("."):
- return [f"the account name {clean(repr(gate_invoking_account))} cannot name a page"]
- directory = constraint.page_directory()
- if not os.path.isdir(directory):
- return [f"{directory} is not a directory"]
- if os.path.realpath(directory) != directory:
- return [f"{directory} passes through a symbolic link"]
- # access() alone answers for the real uid; effective_ids asks for the effective uid, which writes
- if not os.access(directory ,os.W_OK | os.X_OK ,effective_ids=os.access in os.supports_effective_ids):
- problem_list.append(f"{directory} cannot be written by the effective uid's account")
- for name in page_name_list(gate_invoking_account):
- try:
- st = os.lstat(os.path.join(directory ,name))
- except FileNotFoundError:
- continue
- if not stat.S_ISREG(st.st_mode):
- problem_list.append(f"{directory}/{name} exists and is not a regular file")
- return problem_list
-
-CHECK = (
- ("wellformed" ,lambda data ,constraint ,gate_invoking_account: check_wellformed(data))
- ,("constraint" ,check_constraint)
- ,("content" ,lambda data ,constraint ,gate_invoking_account: check_content(data))
- ,("filesystem" ,check_filesystem)
-)
-
-
-# --------------------------------------------------------------------------------
-# interpreter half: the page
-
-def page(display_name ,paragraph_list ,picture_name):
- """
- The HTML of a page. Every text given is escaped; every tag comes from this
- function or from STYLE.
- """
- name = html.escape(display_name ,quote=True)
- part = [
- "\n"
- ,"\n\n"
- ,'\n'
- ,'\n'
- ,'\n'
- ,"" ,name ,"\n"
- ,"\n\n"
- ,"" ,name ,"
\n"
- ]
- if picture_name:
- part += ['
\n']
- for run_list in paragraph_list:
- part.append("")
- for content ,style in run_list:
- tag_list = [STYLE[s] for s in STYLE if s in style]
- part += [f"<{tag}>" for tag in tag_list]
- part.append(html.escape(content ,quote=False))
- part += [f"{tag}>" for tag in reversed(tag_list)]
- part.append("
\n")
- part.append("\n\n")
- return "".join(part)
-
-def _write_temporary(dirfd ,target ,content):
- tmp = f".{target}.Man_In_Grey.{os.getpid()}"
- fd = os.open(tmp ,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW ,0o600 ,dir_fd=dirfd)
- try:
- view = memoryview(content)
- while view:
- view = view[os.write(fd ,view):]
- os.fchmod(fd ,PAGE_PERMISSION)
- os.fsync(fd)
- finally:
- os.close(fd)
- return tmp
-
-def execute(data ,log ,constraint ,gate_invoking_account):
- """
- Write the picture and the page to temporary files in the directory, then
- rename each into place, and remove a picture the page no longer shows.
- log is given a tag and a message for each thing done. Returns 0, or 1 on
- an error, removing every temporary file not yet put in place.
- """
- user_name = gate_invoking_account
- directory = constraint.page_directory()
- display_name ,paragraph_list ,picture = None ,[] ,None
- for instruction in data["instruction_list"]:
- name ,argument = instruction["name"] ,instruction["argument"]
- if name == "display_name":
- display_name = argument["text"]
- elif name == "paragraph":
- paragraph_list.append([])
- elif name == "text":
- paragraph_list[-1].append((argument["content"] ,argument["style"]))
- elif name == "picture":
- picture = argument["content"]
- picture_name = user_name + picture_extension(picture) if picture is not None else None
- page_name = user_name + ".html"
-
- dirfd = None
- pending = [] # (temporary ,target)
- try:
- dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
- if picture_name:
- pending.append((_write_temporary(dirfd ,picture_name ,picture) ,picture_name))
- pending.append((_write_temporary(dirfd ,page_name ,page(display_name ,paragraph_list ,picture_name).encode("utf-8")) ,page_name))
- while pending:
- tmp ,target = pending[0]
- os.rename(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd)
- pending.pop(0)
- log("emplaced" ,f"{directory}/{target}")
- for name in page_name_list(user_name)[1:]:
- if name == picture_name:
- continue
- try:
- st = os.lstat(name ,dir_fd=dirfd)
- except FileNotFoundError:
- continue
- if stat.S_ISREG(st.st_mode):
- os.unlink(name ,dir_fd=dirfd)
- log("removed" ,f"{directory}/{name}")
- os.fsync(dirfd)
- return 0
- except OSError as error:
- log("execution_failed" ,str(error))
- for tmp ,_ in pending:
- try:
- os.unlink(tmp ,dir_fd=dirfd)
- except OSError:
- pass
- return 1
- finally:
- if dirfd is not None:
- os.close(dirfd)
diff --git "a/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Guard.py" "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Guard.py"
new file mode 100644
index 0000000..c6dca17
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Guard.py"
@@ -0,0 +1,161 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+EmplaceBio·Guard - the guard of the emplace_bio instruction language.
+
+Started by the gate Man_In_Grey_gate·emplace_bio, with the gate's real and
+effective ids, and with the journal on standard input. It checks the
+journal: first with the well-formed guard, against the language definition
+in EmplaceBio·Language.py, then with the language's own checks in
+EmplaceBio·GuardCheck.py, the constraints among them. It says what it found on
+standard error, one line per finding, as : ; the first two
+lines are name: and version:.
+
+When every check passes, it writes one line to standard output,
+
+ guard emplace_bio check passed
+
+and exits 0. That line and that status together are the only verdict the
+gate admits a journal on.
+
+Its constraints are the predicates at the top of this file. The
+interpreter imports this file for what execution needs from them.
+
+Before it imports anything, it checks that its modules and their directory
+are owned by root or by the effective uid's account, and writable by no
+other account. A module runs code when it is imported.
+
+Usage: EmplaceBio·Guard
+ help | -h print this message (also printed when given no arguments)
+ check check the journal on standard input
+
+Exit status: 0 when every check passed, 1 when a check refused the journal,
+2 when the journal could not be checked.
+"""
+
+# ------------------------------------------------------------------------------
+# The constraints: the predicates this guard checks a journal with. They are
+# written by whoever installs the gate, between the two marks below, and
+# install_gate can put them in from a file and keeps them when it installs
+# the guard again. A predicate allows only by answering True. As promoted
+# with Man_In_Grey, is_allowed_account refuses every account.
+#
+# They are called only after the modules below are checked and imported, so
+# they can call Guard·LibraryConstraint, the library of predicates that recur,
+# as Guard·LibraryConstraint.is_group_member(user_name ,"bio").
+
+# constraints begin
+
+def is_allowed_account(user_name):
+ "True when the account, the gate's real uid, can have a page"
+ return False
+
+def is_allowed_text_size(byte_count):
+ "True when the display name and the text together can be byte_count bytes of UTF-8"
+ return byte_count <= 8192
+
+def is_allowed_picture_size(byte_count):
+ "True when the picture can be byte_count bytes"
+ return byte_count <= 1 << 20
+
+def page_directory():
+ "the absolute directory the pages are placed in, owned by the gate owner account"
+ return "/srv/http/bio"
+
+# constraints end
+# ------------------------------------------------------------------------------
+
+
+import sys ,os
+sys.dont_write_bytecode = True
+
+import stat
+from pathlib import Path
+
+LANGUAGE = "emplace_bio"
+VERSION = "4"
+
+HERE_DPATH = Path(__file__).resolve().parent
+MODULE = ("Language.py" ,"Journal.py" ,"Guard·WellFormed.py" ,"Guard.py" ,"Guard·LibraryConstraint.py" ,"EmplaceBio·Language.py" ,"EmplaceBio·GuardCheck.py" ,"EmplaceBio·Guard.py")
+
+
+def trusted_owner_problem(path):
+ "None when path is owned by root or the effective uid's account and writable by no other account"
+ try:
+ st = os.lstat(path)
+ except OSError as error:
+ return f"{path}: {error.strerror}"
+ if st.st_uid not in (0 ,os.geteuid()):
+ return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
+ if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
+ return f"{path} is writable by group or other accounts"
+ return None
+
+def say(tag ,message):
+ print(f"{tag}: {message}" ,file=sys.stderr)
+
+if __name__ == "__main__" and sys.argv[1:] == ["check"]:
+ say("name" ,LANGUAGE)
+ say("version" ,VERSION)
+
+for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
+ problem = trusted_owner_problem(each)
+ if problem:
+ say("refused_to_start" ,problem)
+ sys.exit(2)
+
+try:
+ import Language
+ import Journal
+ import Guard
+ import Guard·LibraryConstraint # for the constraints at the top, which call it by name
+ import EmplaceBio·Language as language_module
+ import EmplaceBio·GuardCheck as guard_check
+except ImportError as error:
+ say("cannot_start" ,str(error))
+ sys.exit(2)
+
+
+def report(tag ,message):
+ say(tag ,Journal.clean(message))
+
+def check():
+ report("constraints" ,str(Path(__file__).resolve()))
+ try:
+ gate_invoking_account = Guard.gate_invoking_account()
+ except RuntimeError as error:
+ report("cannot_check" ,str(error))
+ return 2
+ try:
+ language_definition = Language.Definition(language_module.NAME ,language_module.DEFINITION)
+ except Language.DefinitionError as error:
+ report("cannot_check" ,str(error))
+ return 2
+ status = Guard.check(sys.stdin.buffer.read() ,language_definition ,guard_check ,sys.modules[__name__] ,gate_invoking_account ,report)
+ if status == 0:
+ sys.stdout.write(f"guard {LANGUAGE} check passed\n")
+ return status
+
+
+# --------------------------------------------------------------------------------
+# command line
+
+def print_help():
+ print(__doc__.strip())
+ return 0
+
+def CLI(argv=None):
+ argv = sys.argv[1:] if argv is None else argv
+ if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
+ if argv != ["check"]:
+ say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run EmplaceBio·Guard with no arguments for its usage")
+ return 2
+ return check()
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(CLI())
+ except Exception as error:
+ say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
+ sys.exit(2)
diff --git "a/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267GuardCheck.py" "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267GuardCheck.py"
new file mode 100644
index 0000000..6368936
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267GuardCheck.py"
@@ -0,0 +1,203 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+EmplaceBio·GuardCheck - the guard's checks of the emplace_bio instruction language: a bio page.
+
+A host publishes one bio page for each permitted account, as
+.html in a directory its web server serves, with the picture
+beside it as .png or .jpg. The web server's account
+owns the directory, so a person cannot write there. He gives his page's
+display name, text and picture as instructions, and the emplace_bio gate,
+owned by the web server's account, places the page.
+
+A journal names no file. The file names come from the user name of
+account A, which the gate gives the interpreter, and the directory comes
+from page_directory() at the top of the guard. The interpreter writes the
+HTML itself, from a fixed form, and escapes every text it is given, so
+nothing a person gives becomes markup. Decoration travels out of band, as a
+list of styles beside each run of text, and each style becomes one fixed
+tag with no attribute.
+
+The instructions and their types are in EmplaceBio·Language.py. The
+journal describes the whole page: a run that gives no picture removes the
+picture an earlier run placed.
+
+This module holds the guard's checks of the language, made after the
+well-formed guard has checked the journal against the definition. They are
+LIST_CHECK:
+
+ language what the definition cannot say: the display name is given
+ once, text comes only within a paragraph and no paragraph is
+ empty, at most one picture, every style one of STYLE and none
+ twice, and no control character in any text
+ constraint the predicates at the top of the guard, EmplaceBio·Guard.py:
+ is_allowed_account, is_allowed_text_size and
+ is_allowed_picture_size, and page_directory(), where the
+ pages go
+ content the picture is a PNG or a JPEG
+ filesystem the page directory, and the files the page can occupy
+
+The implementation of the instructions is in EmplaceBio·LibraryInstruction.py.
+"""
+
+
+import os ,stat
+
+from Journal import has_control ,clean
+
+# each style and its tag, in the order the tags nest, outermost first
+STYLE = {
+ "strong": "strong"
+ ,"emphasis": "em"
+ ,"underline": "u"
+ ,"strikethrough": "s"
+}
+
+
+DISPLAY_NAME_MAX = 200 # characters
+
+SIGNATURE = (
+ (b"\x89PNG\r\n\x1a\n" ,".png")
+ ,(b"\xff\xd8\xff" ,".jpg")
+)
+
+
+# --------------------------------------------------------------------------------
+# value checks
+
+def display_name_problem(value):
+ if not isinstance(value ,str) or not value.strip():
+ return "the display name is to be text that is not empty"
+ if has_control(value):
+ return f"the display name {clean(value)} holds a control character"
+ if len(value) > DISPLAY_NAME_MAX:
+ return f"the display name is longer than {DISPLAY_NAME_MAX} characters"
+ return None
+
+def content_problem(value):
+ if not isinstance(value ,str) or not value:
+ return "content is to be text that is not empty"
+ if has_control(value):
+ return f"content {clean(value)} holds a control character; a new paragraph is the instruction paragraph()"
+ return None
+
+def style_problem(value):
+ if not isinstance(value ,list) or not all(isinstance(s ,str) for s in value):
+ return "style is to be a list of style names"
+ for s in value:
+ if s not in STYLE:
+ return f"style {clean(repr(s))} is not one of {', '.join(STYLE)}"
+ if len(set(value)) != len(value):
+ return "style names a style twice"
+ return None
+
+def picture_extension(content):
+ "the extension for the picture's format, or None when it is neither PNG nor JPEG"
+ for signature ,extension in SIGNATURE:
+ if content.startswith(signature):
+ return extension
+ return None
+
+
+# --------------------------------------------------------------------------------
+# the guard's checks, each given the instructions as Journal.InstructionView
+
+def check_language(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ """
+ What the definition cannot say. No instruction of this language writes a
+ variable, so the well-formed guard has already ensured every argument is
+ a literal.
+ """
+ list_problem = []
+ count = {"display_name": 0 ,"paragraph": 0 ,"text": 0 ,"picture": 0}
+ in_paragraph = False
+ run_count = 0
+ for view in list_instruction_view:
+ index ,name = view.index ,view.operation
+ value = [argument.value for argument in view.list_argument]
+ count[name] += 1
+ if name == "paragraph":
+ if in_paragraph and run_count == 0:
+ list_problem.append(f"@{index} paragraph: the paragraph before it holds no text")
+ in_paragraph ,run_count = True ,0
+ elif name == "text":
+ if not in_paragraph:
+ list_problem.append(f"@{index} text: no paragraph has been started")
+ run_count += 1
+ for problem in (content_problem(value[0]) ,style_problem(value[1])):
+ if problem: list_problem.append(f"@{index} text: {problem}")
+ elif name == "display_name":
+ problem = display_name_problem(value[0])
+ if problem: list_problem.append(f"@{index} display_name: {problem}")
+ elif name == "picture" and not value[0]:
+ list_problem.append(f"@{index} picture: the picture is empty")
+ if count["display_name"] != 1:
+ list_problem.append(f"the journal gives the display name {count['display_name']} times; it is given once")
+ if count["paragraph"] == 0:
+ list_problem.append("the journal has no paragraph")
+ elif run_count == 0:
+ list_problem.append("the last paragraph holds no text")
+ if count["picture"] > 1:
+ list_problem.append(f"the journal gives {count['picture']} pictures; it gives at most one")
+ return list_problem
+
+def page_directory_problem(directory):
+ if not isinstance(directory ,str) or not directory.startswith("/") or os.path.normpath(directory) != directory:
+ return f"page_directory() gives {clean(repr(directory))}, not an absolute directory in normal form"
+ return None
+
+def check_constraint(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ "constraint is the guard, which holds the predicates; a predicate allows only by answering True"
+ if constraint.is_allowed_account(gate_invoking_account) is not True:
+ return [f"gate invoking account {clean(repr(gate_invoking_account))} is not allowed to give journals to this gate"]
+ list_problem = []
+ problem = page_directory_problem(constraint.page_directory())
+ if problem: list_problem.append(problem)
+ text_bytes = 0
+ for view in list_instruction_view:
+ value = view.list_argument[0].value if view.list_argument else None
+ if view.operation in ("display_name" ,"text"):
+ text_bytes += len(value.encode("utf-8"))
+ elif view.operation == "picture" and constraint.is_allowed_picture_size(len(value)) is not True:
+ list_problem.append(f"the picture is {len(value)} bytes, which is not allowed")
+ if constraint.is_allowed_text_size(text_bytes) is not True:
+ list_problem.append(f"the text is {text_bytes} bytes, which is not allowed")
+ return list_problem
+
+def check_content(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ list_problem = []
+ for view in list_instruction_view:
+ if view.operation == "picture" and picture_extension(view.list_argument[0].value) is None:
+ list_problem.append(f"@{view.index} picture: the content is neither a PNG nor a JPEG file")
+ return list_problem
+
+def list_page_name(user_name):
+ "the files a page of this account can occupy"
+ return [user_name + ".html"] + [user_name + extension for _ ,extension in SIGNATURE]
+
+def check_filesystem(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ list_problem = []
+ if has_control(gate_invoking_account) or gate_invoking_account.startswith("."):
+ return [f"the account name {clean(repr(gate_invoking_account))} cannot name a page"]
+ directory = constraint.page_directory()
+ if not os.path.isdir(directory):
+ return [f"{directory} is not a directory"]
+ if os.path.realpath(directory) != directory:
+ return [f"{directory} passes through a symbolic link"]
+ # access() alone answers for the real uid; effective_ids asks for the effective uid, which writes
+ if not os.access(directory ,os.W_OK | os.X_OK ,effective_ids=os.access in os.supports_effective_ids):
+ list_problem.append(f"{directory} cannot be written by the effective uid's account")
+ for name in list_page_name(gate_invoking_account):
+ try:
+ st = os.lstat(os.path.join(directory ,name))
+ except FileNotFoundError:
+ continue
+ if not stat.S_ISREG(st.st_mode):
+ list_problem.append(f"{directory}/{name} exists and is not a regular file")
+ return list_problem
+
+LIST_CHECK = (
+ ("language" ,check_language)
+ ,("constraint" ,check_constraint)
+ ,("content" ,check_content)
+ ,("filesystem" ,check_filesystem)
+)
diff --git "a/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Interpreter.py" "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Interpreter.py"
new file mode 100644
index 0000000..1980e7a
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Interpreter.py"
@@ -0,0 +1,141 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+EmplaceBio·Interpreter - the interpreter of the emplace_bio instruction language.
+
+Started by the gate Man_In_Grey_gate·emplace_bio, with the gate's real and
+effective ids, and with the journal on standard input, once the guard has
+passed the journal. It executes the journal's instructions, and writes to
+standard error, as : , a line for each lasting change it
+makes, such as a file put in place, and none for an instruction as such.
+The gate logs the journal's course itself.
+
+When it has played the whole journal, it writes the journal's result, when
+the journal declares one, to descriptor 3, which the gate gives it: a CBOR
+map of the result's type name and value. Then it writes one line to
+standard output,
+
+ interpreter emplace_bio journal played
+
+and exits 0. The gate takes the journal as played on that line and that
+status together, and on nothing else.
+
+It checks again that the journal is well formed before executing it. It
+does not check the constraints; that is the guard's work, and the gate
+starts the interpreter only on the guard's verdict. It imports the guard,
+EmplaceBio·Guard.py, for what execution needs from the constraints at its top.
+
+Before it imports anything, it checks that its modules and their directory
+are owned by root or by the effective uid's account, and writable by no
+other account.
+
+Usage: EmplaceBio·Interpreter
+ help | -h print this message (also printed when given no arguments)
+ execute execute the journal on standard input
+
+Exit status: 0 when the journal was executed, 1 when execution failed,
+2 when the journal could not be read or is not well formed.
+"""
+
+import sys ,os
+sys.dont_write_bytecode = True
+
+import stat
+from pathlib import Path
+
+HERE_DPATH = Path(__file__).resolve().parent
+MODULE = ("Language.py" ,"Journal.py" ,"Guard·WellFormed.py" ,"Guard.py" ,"Interpreter.py" ,"Guard·LibraryConstraint.py" ,"EmplaceBio·Language.py" ,"EmplaceBio·GuardCheck.py" ,"EmplaceBio·LibraryInstruction.py" ,"EmplaceBio·Guard.py" ,"EmplaceBio·Interpreter.py")
+
+
+def trusted_owner_problem(path):
+ "None when path is owned by root or the effective uid's account and writable by no other account"
+ try:
+ st = os.lstat(path)
+ except OSError as error:
+ return f"{path}: {error.strerror}"
+ if st.st_uid not in (0 ,os.geteuid()):
+ return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
+ if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
+ return f"{path} is writable by group or other accounts"
+ return None
+
+def say(tag ,message):
+ print(f"{tag}: {message}" ,file=sys.stderr)
+
+for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
+ problem = trusted_owner_problem(each)
+ if problem:
+ say("refused_to_start" ,problem)
+ sys.exit(2)
+
+try:
+ import Language
+ import Journal
+ import Guard
+ import Interpreter
+ import EmplaceBio·Language as language_module
+ import EmplaceBio·LibraryInstruction as library_instruction
+ import EmplaceBio·Guard as guard_program
+except ImportError as error:
+ say("cannot_start" ,str(error))
+ sys.exit(2)
+
+
+RESULT_FD = 3
+
+def write_result(result):
+ "the encoded result, all of it, on descriptor 3, which is then closed"
+ view = memoryview(result)
+ while view:
+ view = view[os.write(RESULT_FD ,view):]
+ os.close(RESULT_FD)
+
+def log(tag ,message):
+ print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
+
+def execute():
+ try:
+ gate_invoking_account = Guard.gate_invoking_account()
+ except RuntimeError as error:
+ log("cannot_execute" ,str(error))
+ return 2
+ try:
+ language_definition = Language.Definition(language_module.NAME ,language_module.DEFINITION)
+ except Language.DefinitionError as error:
+ log("cannot_execute" ,str(error))
+ return 2
+ status ,result = Interpreter.interpret(sys.stdin.buffer.read() ,language_definition ,library_instruction ,log ,guard_program ,gate_invoking_account)
+ if status != 0:
+ return status
+ if result is not None:
+ try:
+ write_result(result)
+ except OSError as error:
+ log("cannot_return_result" ,f"descriptor {RESULT_FD}: {error.strerror}")
+ return 2
+ sys.stdout.write("interpreter emplace_bio journal played\n")
+ return 0
+
+
+# --------------------------------------------------------------------------------
+# command line
+
+def print_help():
+ print(__doc__.strip())
+ return 0
+
+def CLI(argv=None):
+ argv = sys.argv[1:] if argv is None else argv
+ if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
+ if argv != ["execute"]:
+ say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run EmplaceBio·Interpreter with no arguments for its usage")
+ return 2
+ return execute()
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(CLI())
+ except Exception as error:
+ say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
+ sys.exit(2)
diff --git "a/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Language.py" "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Language.py"
new file mode 100644
index 0000000..c2aa97e
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267Language.py"
@@ -0,0 +1,40 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+EmplaceBio·Language - the language definition of emplace_bio, a bio page.
+
+Data only; see Language.py for its form. The composer's journal interface,
+the guard and the interpreter all read it, and a journal carries its hash.
+
+ display_name(name_text) the page's title and heading; once
+ paragraph() starts a paragraph
+ text(run_text ,list_style) a run of text in the current paragraph,
+ with a list of style names
+ picture(image) a PNG or JPEG image; at most once
+
+No instruction produces a value, so every argument is a literal, and a
+journal of this language has no result.
+"""
+
+NAME = "emplace_bio"
+
+DEFINITION = {
+ "list_type": [
+ ["name_text" ,"text"]
+ ,["run_text" ,"text"]
+ ,["list_style" ,["list" ,"text"]]
+ ,["image" ,"bytes"]
+ ]
+ ,"list_format": [
+ ["format_name" ,["in" ,"name_text"]]
+ ,["format_empty"]
+ ,["format_run" ,["in" ,"run_text"] ,["in" ,"list_style"]]
+ ,["format_image" ,["in" ,"image"]]
+ ]
+ ,"list_result": []
+ ,"map_instruction": {
+ "display_name": "format_name"
+ ,"paragraph": "format_empty"
+ ,"text": "format_run"
+ ,"picture": "format_image"
+ }
+}
diff --git "a/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267LibraryInstruction.py" "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267LibraryInstruction.py"
new file mode 100644
index 0000000..55b9783
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/EmplaceBio\302\267LibraryInstruction.py"
@@ -0,0 +1,131 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+EmplaceBio·LibraryInstruction - the implementation of the emplace_bio instructions.
+
+The interpreter makes one LibraryInstruction for each journal and calls its
+functions, one for each operation specifier of the definition,
+EmplaceBio·Language.py, given each instruction's arguments by
+position. They build the page up; finish() writes it from a fixed form, and
+escapes every text it is given, so nothing a person gives becomes markup.
+The page's file names come from the user name of account A, and its
+directory from page_directory() among the guard's constraints.
+"""
+
+import html ,os ,stat ,urllib.parse
+
+from EmplaceBio·GuardCheck import STYLE ,picture_extension ,list_page_name
+
+PAGE_PERMISSION = 0o644
+
+
+# --------------------------------------------------------------------------------
+# the interpreter's side: the page
+
+def page(display_name ,list_paragraph ,picture_name):
+ """
+ The HTML of a page. Every text given is escaped; every tag comes from this
+ function or from STYLE.
+ """
+ name = html.escape(display_name ,quote=True)
+ part = [
+ "\n"
+ ,"\n\n"
+ ,'\n'
+ ,'\n'
+ ,'\n'
+ ,"" ,name ,"\n"
+ ,"\n\n"
+ ,"" ,name ,"
\n"
+ ]
+ if picture_name:
+ part += ['
\n']
+ for list_run in list_paragraph:
+ part.append("")
+ for content ,style in list_run:
+ list_tag = [STYLE[s] for s in STYLE if s in style]
+ part += [f"<{tag}>" for tag in list_tag]
+ part.append(html.escape(content ,quote=False))
+ part += [f"{tag}>" for tag in reversed(list_tag)]
+ part.append("
\n")
+ part.append("\n\n")
+ return "".join(part)
+
+def _write_temporary(dirfd ,target ,content):
+ tmp = f".{target}.Man_In_Grey.{os.getpid()}"
+ fd = os.open(tmp ,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW ,0o600 ,dir_fd=dirfd)
+ try:
+ view = memoryview(content)
+ while view:
+ view = view[os.write(fd ,view):]
+ os.fchmod(fd ,PAGE_PERMISSION)
+ os.fsync(fd)
+ finally:
+ os.close(fd)
+ return tmp
+
+class LibraryInstruction:
+ """
+ The instructions of one journal: each adds to the page, and finish()
+ writes the picture and the page to temporary files in the directory, then
+ renames each into place, and removes a picture the page no longer shows.
+ On an error, every temporary file not yet put in place is removed.
+ """
+
+ def __init__(self ,constraint ,gate_invoking_account ,log):
+ self._constraint = constraint
+ self._user_name = gate_invoking_account
+ self._log = log
+ self._display_name = None
+ self._list_paragraph = []
+ self._picture = None
+
+ def display_name(self ,name_text):
+ self._display_name = name_text
+
+ def paragraph(self):
+ self._list_paragraph.append([])
+
+ def text(self ,run_text ,list_style):
+ self._list_paragraph[-1].append((run_text ,list_style))
+
+ def picture(self ,image):
+ self._picture = image
+
+ def finish(self):
+ log ,user_name = self._log ,self._user_name
+ directory = self._constraint.page_directory()
+ picture = self._picture
+ picture_name = user_name + picture_extension(picture) if picture is not None else None
+ page_name = user_name + ".html"
+
+ dirfd = None
+ pending = [] # (temporary ,target)
+ try:
+ dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ if picture_name:
+ pending.append((_write_temporary(dirfd ,picture_name ,picture) ,picture_name))
+ pending.append((_write_temporary(dirfd ,page_name ,page(self._display_name ,self._list_paragraph ,picture_name).encode("utf-8")) ,page_name))
+ while pending:
+ tmp ,target = pending[0]
+ os.rename(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd)
+ pending.pop(0)
+ log("emplaced" ,f"{directory}/{target}")
+ for name in list_page_name(user_name)[1:]:
+ if name == picture_name:
+ continue
+ try:
+ st = os.lstat(name ,dir_fd=dirfd)
+ except FileNotFoundError:
+ continue
+ if stat.S_ISREG(st.st_mode):
+ os.unlink(name ,dir_fd=dirfd)
+ log("removed" ,f"{directory}/{name}")
+ os.fsync(dirfd)
+ finally:
+ for tmp ,_ in pending:
+ try:
+ os.unlink(tmp ,dir_fd=dirfd)
+ except OSError:
+ pass
+ if dirfd is not None:
+ os.close(dirfd)
diff --git a/developer/authored/Man_In_Grey.copy/Guard.py b/developer/authored/Man_In_Grey.copy/Guard.py
index cd2116c..876fb21 100644
--- a/developer/authored/Man_In_Grey.copy/Guard.py
+++ b/developer/authored/Man_In_Grey.copy/Guard.py
@@ -2,19 +2,28 @@
"""
Guard - the general guard of Man_In_Grey.
-The guard decides whether a journal can be executed. An instruction language
-supplies its checks, in order, as CHECK; check() decodes the journal, runs
-the checks, and stops at the first that finds problems. Every finding is
-given to report as a tag and a message: _broken for each problem,
-check_passed for each check that found none.
+The guard decides whether a journal can be executed. check() decodes the
+journal and makes the checks in order, stopping at the first that finds
+problems:
+
+ wellformed the well-formed guard, Man_In_Grey's own: the journal against
+ the language definition, by Guard·WellFormed.is_well_formed_journal
+ ... the language's own checks, in the order its LIST_CHECK gives them:
+ what a definition cannot say, such as an order the
+ instructions are to come in, the values a literal can take,
+ and the constraints
+
+Every finding is given to report as a tag and a message: _broken for
+each problem, check_passed for each check that found none.
This module is a library. An instruction language's guard program calls
-check() and writes the verdict; see guard_emplace_bio.py.
+check() and writes the verdict; see EmplaceBio·Guard.py.
"""
import os ,pwd
import Journal
+import Guard·WellFormed
def gate_invoking_account():
@@ -42,27 +51,35 @@ def gate_invoking_account():
return user_name
-def check(journal_bytes ,language ,constraint ,gate_invoking_account ,report):
+def check(journal_bytes ,language_definition ,guard_check ,constraint ,gate_invoking_account ,report):
"""
- Given the encoded journal, the instruction language module, its constraint
- module of predicates, the gate invoking account's name, and report(tag
- ,message). Returns 0 when every check passed, 1 when a check found problems, 2 when the journal
- could not be read.
+ Given the encoded journal, the language definition, the language's guard
+ check module, the constraint module of predicates, the gate invoking
+ account's name, and report(tag ,message). A language check is given the
+ instructions as a list of Guard·WellFormed.InstructionView, the decoded
+ journal, the constraint module and the gate invoking account, and returns
+ a list of problems. Returns 0 when every check passed, 1 when a check found
+ problems, 2 when the journal could not be read.
"""
try:
data = Journal.decode(journal_bytes)
except Journal.JournalError as error:
report("journal_unreadable" ,str(error))
return 2
- if data["instruction_language"] != language.NAME:
- report("wrong_language" ,f"the journal is in {data['instruction_language']!r}; this guard checks {language.NAME!r}")
+ if data["instruction_language"] != language_definition.name:
+ report("wrong_language" ,f"the journal is in {data['instruction_language']!r}; this guard checks {language_definition.name!r}")
+ return 1
+ report("journal" ,f"{len(data['list_variable'])} variables, {len(data['list_instruction'])} instructions"
+ + (f", result {data['result']}" if data["result"] is not None else ""))
+ if not Guard·WellFormed.is_well_formed_journal(data ,language_definition ,report):
return 1
- report("journal" ,f"{len(data['instruction_list'])} instructions")
- for name ,function in language.CHECK:
- problem_list = function(data ,constraint ,gate_invoking_account)
- for problem in problem_list:
+ report("check_passed" ,"wellformed")
+ list_instruction_view = Guard·WellFormed.make_list_instruction_view(data ,language_definition)
+ for name ,function in guard_check.LIST_CHECK:
+ list_problem = function(list_instruction_view ,data ,constraint ,gate_invoking_account)
+ for problem in list_problem:
report(f"{name}_broken" ,problem)
- if problem_list:
+ if list_problem:
return 1
report("check_passed" ,name)
return 0
diff --git "a/developer/authored/Man_In_Grey.copy/Guard\302\267LibraryConstraint.py" "b/developer/authored/Man_In_Grey.copy/Guard\302\267LibraryConstraint.py"
new file mode 100644
index 0000000..e75f001
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Guard\302\267LibraryConstraint.py"
@@ -0,0 +1,64 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Guard·LibraryConstraint - predicates that recur among guards' constraints.
+
+A guard's constraints are written at the top of the guard itself, as
+predicates such as is_allowed_account(user_name). This library holds the
+predicates many guards would otherwise each write: membership of a group,
+a path within a directory, a permission within a limit. A guard imports it
+after its ownership check, as it imports its other modules, and its
+constraints call it by name, as
+Guard·LibraryConstraint.is_group_member(user_name ,"bio").
+"""
+
+import grp ,os ,pwd
+
+
+def is_group_member(user_name ,group_name):
+ """
+ True when the account is a member of the group by the group database:
+ listed in /etc/group as a member, or having the group as its primary group.
+ """
+ try:
+ group = grp.getgrnam(group_name)
+ except KeyError:
+ return False
+ if user_name in group.gr_mem:
+ return True
+ try:
+ return pwd.getpwnam(user_name).pw_gid == group.gr_gid
+ except KeyError:
+ return False
+
+def has_real_group(group_name):
+ """
+ True when the gate's real gid or supplementary groups include the group.
+ These are what the program that invoked the gate held, which the kernel
+ passes on: a session started before the account joined the group does not
+ hold it, though is_group_member already says yes.
+ """
+ try:
+ gid = grp.getgrnam(group_name).gr_gid
+ except KeyError:
+ return False
+ return gid == os.getgid() or gid in os.getgroups()
+
+def is_path_under(path ,directory):
+ """
+ True when path is directory or lies within it. Both are to be absolute and
+ in normal form, as the guard's wellformed check makes a journal's paths.
+ """
+ if not (isinstance(path ,str) and isinstance(directory ,str)):
+ return False
+ if not (path.startswith("/") and directory.startswith("/")):
+ return False
+ if os.path.normpath(path) != path or os.path.normpath(directory) != directory:
+ return False
+ return path == directory or path.startswith(directory.rstrip("/") + "/")
+
+def has_permission_within(permission ,limit):
+ """
+ True when permission sets no bit that limit does not: 0o644 and 0o600 are
+ within 0o644, and 0o4755, setuid, is within neither 0o644 nor 0o755.
+ """
+ return isinstance(permission ,int) and not isinstance(permission ,bool) and permission & ~limit == 0
diff --git "a/developer/authored/Man_In_Grey.copy/Guard\302\267WellFormed.py" "b/developer/authored/Man_In_Grey.copy/Guard\302\267WellFormed.py"
new file mode 100644
index 0000000..1ae8f4e
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Guard\302\267WellFormed.py"
@@ -0,0 +1,185 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Guard·WellFormed - the well-formed guard: a journal checked against its language definition.
+
+A journal is straight-line: no branch, no loop. So one pass from first
+instruction to last settles, for every argument, whether it is well formed:
+every variable is declared, its type is the type the format names, it is
+written before it is read, and the result is written by the end. Scan makes
+that pass. The guard and the interpreter make it on the journal they
+receive, through is_well_formed_journal; the journal interface makes it as
+each instruction is placed, so a composer's mistake is an error when the
+composer makes it.
+
+The pass also resolves each instruction for the language's own checks, as
+InstructionView: the values of its literals, and for each variable the
+instruction that last wrote it.
+
+This module is a library.
+"""
+
+from Journal import clean ,LIST_KIND
+from Language import is_name
+
+
+class Argument:
+ """
+ One argument of an instruction, as the language's checks see it. A literal
+ has its value; a variable has its name, its direction, and source, the
+ index of the instruction that last wrote it before this one, or None when
+ this instruction is the first to write it.
+ """
+ __slots__ = ("kind" ,"value" ,"name" ,"direction" ,"source")
+
+ def __init__(self ,kind ,value=None ,name=None ,direction="in" ,source=None):
+ self.kind = kind
+ self.value = value
+ self.name = name
+ self.direction = direction
+ self.source = source
+
+ def is_literal(self):
+ return self.kind == "literal"
+
+
+class InstructionView:
+ """An instruction as the language's checks see it: its index, operation specifier and arguments."""
+ __slots__ = ("index" ,"operation" ,"list_argument")
+
+ def __init__(self ,index ,operation ,list_argument):
+ self.index = index
+ self.operation = operation
+ self.list_argument = list_argument
+
+
+class Scan:
+ """
+ The state of one pass over a journal: the variables declared, and for each
+ the instruction that last wrote it. Given the language definition. declare,
+ instruction and result each return a list of problems, empty when they
+ accept, and record what they accept.
+ """
+
+ def __init__(self ,language_definition):
+ self.language_definition = language_definition
+ self.map_declared = {} # variable name -> type name
+ self.map_writer = {} # variable name -> index of the instruction that last wrote it
+ self.list_instruction_view = []
+
+ def declare(self ,name ,type_name):
+ if not is_name(name):
+ return [f"variable {clean(repr(name))} is not a name"]
+ if name in self.map_declared:
+ return [f"variable {clean(name)} is declared twice"]
+ if not self.language_definition.is_type_name(type_name):
+ return [f"variable {clean(name)}: {clean(repr(type_name))} is not a type of the language {self.language_definition.name}"]
+ self.map_declared[name] = type_name
+ return []
+
+ def instruction(self ,index ,operation ,list_pair):
+ definition = self.language_definition
+ where = f"@{index} {clean(operation)}"
+ format_ = definition.format_of(operation)
+ if format_ is None:
+ return [f"@{index}: {clean(repr(operation))} is not an operation of the language {definition.name}"]
+ if type(list_pair) is not list or len(list_pair) != len(format_):
+ count = len(list_pair) if type(list_pair) is list else "no list of"
+ return [f"{where}: {count} arguments; the format takes {len(format_)}"]
+ list_problem = []
+ list_argument = []
+ list_written_here = []
+ for position ,(pair ,(direction ,type_name)) in enumerate(zip(list_pair ,format_)):
+ at = f"{where} argument {position}"
+ if type(pair) is not list or len(pair) != 2 or pair[0] not in LIST_KIND:
+ list_problem.append(f"{at}: an argument is [\"literal\" ,] or [\"variable\" ,]"); continue
+ kind ,payload = pair
+ if kind == "literal":
+ if direction != "in":
+ list_problem.append(f"{at}: a literal is given where the format writes ({direction})"); continue
+ if definition.is_handle_holder(type_name):
+ list_problem.append(f"{at}: the type {type_name} holds a handle, which only an instruction produces, never a literal"); continue
+ problem = definition.value_problem(payload ,type_name ,"the literal")
+ if problem:
+ list_problem.append(f"{at}: {clean(problem)}, as the type {type_name} requires"); continue
+ list_argument.append(Argument("literal" ,value=payload))
+ continue
+ name = payload
+ if type(name) is not str or name not in self.map_declared:
+ list_problem.append(f"{at}: the variable {clean(repr(name))} is not declared"); continue
+ if self.map_declared[name] != type_name:
+ list_problem.append(f"{at}: the variable {clean(name)} is of the type {self.map_declared[name]}; the format takes {type_name}"); continue
+ if direction in ("in" ,"in-out") and name not in self.map_writer:
+ list_problem.append(f"{at}: the variable {clean(name)} is read before any instruction writes it"); continue
+ if direction in ("out" ,"in-out"):
+ if name in list_written_here or any(a.kind == "variable" and a.name == name for a in list_argument):
+ list_problem.append(f"{at}: the variable {clean(name)} is written here and given again in the same instruction"); continue
+ list_written_here.append(name)
+ elif name in list_written_here:
+ list_problem.append(f"{at}: the variable {clean(name)} is written here and given again in the same instruction"); continue
+ list_argument.append(Argument("variable" ,name=name ,direction=direction ,source=self.map_writer.get(name)))
+ if list_problem:
+ return list_problem
+ for name in list_written_here:
+ self.map_writer[name] = index
+ self.list_instruction_view.append(InstructionView(index ,operation ,list_argument))
+ return []
+
+ def result(self ,name):
+ if name is None:
+ return []
+ if type(name) is not str or name not in self.map_declared:
+ return [f"the result {clean(repr(name))} is not a declared variable"]
+ type_name = self.map_declared[name]
+ if type_name not in self.language_definition.list_result_type:
+ return [f"the result {clean(name)} is of the type {type_name}, which is not among the result types {self.language_definition.list_result_type}"]
+ if name not in self.map_writer:
+ return [f"the result {clean(name)} is never written"]
+ return []
+
+
+def scan_journal(data ,language_definition):
+ """
+ Make the pass over a decoded journal. Returns (list_problem ,scan); the
+ scan's list_instruction_view is complete only when there is no problem.
+ """
+ scan = Scan(language_definition)
+ if data["instruction_language"] != language_definition.name:
+ return [f"the journal is in {clean(repr(data['instruction_language']))}; this is {language_definition.name}"] ,scan
+ if data["definition_hash"] != language_definition.hash:
+ return [f"the journal was composed against another definition of {language_definition.name}"
+ f" (hash {clean(str(data['definition_hash'])[:16])}..., not {language_definition.hash[:16]}...);"
+ " the composer's copy of the definition and the guard's differ"] ,scan
+ list_problem = []
+ for index ,entry in enumerate(data["list_variable"]):
+ if type(entry) is not list or len(entry) != 2:
+ list_problem.append(f"variable @{index}: a declaration is [ ,]"); continue
+ list_problem += scan.declare(*entry)
+ if list_problem:
+ return list_problem ,scan
+ for index ,instruction in enumerate(data["list_instruction"]):
+ list_problem += scan.instruction(index ,instruction["operation"] ,instruction["list_argument"])
+ if list_problem:
+ return list_problem ,scan
+ return scan.result(data["result"]) ,scan
+
+def is_well_formed_journal(data ,language_definition ,report):
+ """
+ True when the decoded journal is well formed against the language
+ definition. Each problem found is given to report(tag ,message), as
+ wellformed_broken.
+ """
+ list_problem ,_ = scan_journal(data ,language_definition)
+ for problem in list_problem:
+ report("wellformed_broken" ,problem)
+ return not list_problem
+
+def make_list_instruction_view(data ,language_definition):
+ """
+ The instructions of a well-formed journal as InstructionView, for the
+ language's checks and the interpreter. Raises ValueError on a journal that
+ is not well formed; call is_well_formed_journal first.
+ """
+ list_problem ,scan = scan_journal(data ,language_definition)
+ if list_problem:
+ raise ValueError(f"the journal is not well formed: {list_problem[0]}")
+ return scan.list_instruction_view
diff --git a/developer/authored/Man_In_Grey.copy/Interpreter.py b/developer/authored/Man_In_Grey.copy/Interpreter.py
index 8730dc8..32b180d 100644
--- a/developer/authored/Man_In_Grey.copy/Interpreter.py
+++ b/developer/authored/Man_In_Grey.copy/Interpreter.py
@@ -2,37 +2,124 @@
"""
Interpreter - the general interpreter of Man_In_Grey.
-The interpreter executes a journal the guard has passed. It carries out
-instructions and does not judge them, with one exception: it checks again that
-the journal is well formed, since it decodes the journal anyway, so that a
+The interpreter plays a journal the guard has passed. It carries out the
+instructions and does not judge them, with one exception: it makes the
+well-formed check again, since it decodes the journal anyway, so that a
malformed journal never reaches execution.
-This module is a library. An instruction language's interpreter program calls
-interpret(); see interpreter_emplace_bio.py.
+A language gives the implementation of its instructions as a class,
+LibraryInstruction, in its instruction library module, such as
+EmplaceBio·LibraryInstruction.py. An instance is made once for each journal, and
+holds whatever the instructions build up between them. For each operation
+specifier in the definition the class has a function of that name, given the
+instruction's arguments by position:
+
+ in a copy of the value, so the function cannot change a variable
+ through it
+ out an empty list, a box, to which the function appends the value
+ in-out a box holding a copy of the variable's value, which the function
+ replaces
+
+After each call, every box is to hold exactly one value of the format's
+type; the interpreter takes the value out and keeps it as the variable's.
+An empty box is evidence that the function wrote nothing, which no stand-in
+value could be.
+
+The function reports failure by raising an exception. LibraryInstruction can
+also have finish(), called after the last instruction, and release(), called
+at the end whatever happened. The functions are found by the operation
+specifiers the definition lists, never by a name taken from the journal.
+
+This module is a library. An instruction language's interpreter program
+calls interpret(), writes the result to descriptor 3 and then its verdict;
+see EmplaceBio·Interpreter.py.
"""
+import copy
+
import Journal
+import Guard·WellFormed
-def interpret(journal_bytes ,language ,log ,constraint ,gate_invoking_account):
+def map_operation_function(language_definition ,library_instruction):
+ """
+ The function for each operation specifier of the definition, from the
+ LibraryInstruction instance. Raises RuntimeError when one is missing.
"""
- Given the encoded journal, the instruction language module, log(tag
- ,message), the guard program, whose constraints the language reads for
- what execution needs, and the user name of account A. Returns 0 when the journal was executed, 1 when
- execution failed, 2 when the journal could not be read or is not well
- formed.
+ map_function = {}
+ for operation in language_definition.list_operation:
+ function = getattr(library_instruction ,operation ,None)
+ if not callable(function):
+ raise RuntimeError(f"the language's LibraryInstruction has no function {operation}, which its definition lists")
+ map_function[operation] = function
+ return map_function
+
+def interpret(journal_bytes ,language_definition ,library_instruction_module ,log ,constraint ,gate_invoking_account):
+ """
+ Given the encoded journal, the language definition, the language's
+ instruction library module, log(tag ,message), the guard program, whose
+ constraints the language reads for what execution needs, and the user name
+ of account A. Returns (status
+ ,result), where result is the encoded result record, or None when the
+ journal declares no result. The status is 0 when the journal was played, 1
+ when execution failed, 2 when the journal could not be read or is not well
+ formed, or the language cannot execute it.
"""
try:
data = Journal.decode(journal_bytes)
except Journal.JournalError as error:
log("journal_unreadable" ,str(error))
- return 2
- if data["instruction_language"] != language.NAME:
- log("wrong_language" ,f"the journal is in {data['instruction_language']!r}; this interpreter executes {language.NAME!r}")
- return 2
- problem_list = language.check_wellformed(data)
- for problem in problem_list:
- log("wellformed_broken" ,problem)
- if problem_list:
- return 2
- return language.execute(data ,log ,constraint ,gate_invoking_account)
+ return 2 ,None
+ if not Guard·WellFormed.is_well_formed_journal(data ,language_definition ,log):
+ return 2 ,None
+ list_instruction_view = Guard·WellFormed.make_list_instruction_view(data ,language_definition)
+
+ library_instruction = library_instruction_module.LibraryInstruction(constraint ,gate_invoking_account ,log)
+ try:
+ map_function = map_operation_function(language_definition ,library_instruction)
+ except RuntimeError as error:
+ log("cannot_execute" ,str(error))
+ return 2 ,None
+
+ map_value = {} # variable name -> value
+ where = "before the first instruction"
+ try:
+ for view in list_instruction_view:
+ where = f"@{view.index} {view.operation}"
+ list_value ,list_box = [] ,[]
+ for argument ,(direction ,type_name) in zip(view.list_argument ,language_definition.format_of(view.operation)):
+ if argument.is_literal():
+ list_value.append(copy.deepcopy(argument.value))
+ elif direction == "in":
+ list_value.append(copy.deepcopy(map_value[argument.name]))
+ else:
+ box = [] if direction == "out" else [copy.deepcopy(map_value[argument.name])]
+ list_value.append(box)
+ list_box.append((argument.name ,type_name ,box))
+ map_function[view.operation](*list_value)
+ for name ,type_name ,box in list_box:
+ if type(box) is not list or len(box) != 1:
+ raise RuntimeError(f"the box for {name} holds {len(box) if type(box) is list else 'no list of'} values; it is to hold one")
+ problem = language_definition.value_problem(box[0] ,type_name ,f"the value written to {name}")
+ if problem:
+ raise RuntimeError(problem)
+ map_value[name] = copy.deepcopy(box[0])
+ where = "after the last instruction"
+ finish = getattr(library_instruction ,"finish" ,None)
+ if callable(finish):
+ finish()
+ except Exception as error:
+ log("execution_failed" ,f"{where}: {error}")
+ return 1 ,None
+ finally:
+ release = getattr(library_instruction ,"release" ,None)
+ if callable(release):
+ try:
+ release()
+ except Exception as error:
+ log("release_failed" ,str(error))
+
+ if data["result"] is None:
+ return 0 ,None
+ result_type = dict(map(tuple ,data["list_variable"]))[data["result"]]
+ return 0 ,Journal.encode_result(result_type ,map_value[data["result"]])
diff --git a/developer/authored/Man_In_Grey.copy/Journal.py b/developer/authored/Man_In_Grey.copy/Journal.py
index fd93f7f..023058d 100644
--- a/developer/authored/Man_In_Grey.copy/Journal.py
+++ b/developer/authored/Man_In_Grey.copy/Journal.py
@@ -1,29 +1,50 @@
# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
"""
-Journal - the journal of Man_In_Grey, independent of any instruction language.
-
-A composer places instructions into a journal. An instruction is data: the name of
-the instruction, its arguments, and the composer that placed it. An instruction whose
-execution produces a value, such as an open file, is given a symbol when it
-is placed. A later instruction names that symbol where it needs the value. The
-value does not exist until the journal interpreter executes the instruction, so
-the composer holds the symbol and nothing more.
-
-The journal travels to the journal interpreter encoded in CBOR. CBOR carries
-strings and byte strings with their lengths, so content is never scanned for
-marks, and decoding it makes maps, lists, strings and bytes, never code.
-
-An instruction language is defined elsewhere, as a module with a composer half,
-its journal interface, and an interpreter half, its guard and executor.
+Journal - the data form of a Man_In_Grey journal, independent of any instruction language.
+
+A journal is a CBOR map of seven entries:
+
+ version the version of the journal's form, now 4
+ instruction_language the name of the instruction language
+ definition_hash the hash of the language definition it was composed
+ against; see Language.py
+ meta when, where and by which account it was composed, and
+ the composer that ran; a note, never evidence
+ list_variable the variables, each [ ,], the type
+ one the language definition declares
+ result the name of the variable whose value is the journal's
+ result, or null for a journal with no result
+ list_instruction the instructions, each {"operation": ,"list_argument": [ ...]}
+
+An argument is a pair whose first element says how to read the second:
+
+ ["literal" ,] the value itself, whatever it is, a map included
+ ["variable" ,] a variable, which the instruction reads, writes,
+ or both, as its format's direction says
+
+The kind of an argument is never inferred from its value, so no literal can
+be mistaken for a variable.
+
+This module decodes a journal into its data, checking only the shape every
+journal has; prints a journal; and encodes and decodes the result record.
+The well-formed guard, Guard·WellFormed.py, checks a journal against its
+language definition; Journal·Interface.py is the composer's side.
+
+CBOR carries strings and byte strings with their lengths, so content is
+never scanned for marks, and decoding it makes maps, lists, strings and
+bytes, never code.
"""
import cbor2
-JOURNAL_VERSION = 3
+JOURNAL_VERSION = 4
+LIST_ENTRY = ("version" ,"instruction_language" ,"definition_hash" ,"meta" ,"list_variable" ,"result" ,"list_instruction")
+LIST_KIND = ("literal" ,"variable")
def is_control(character):
- "C0 controls, DEL and C1 controls: the characters a gate refuses in what the guard and interpreter write"
+ "True for C0 controls, DEL and C1 controls: the characters a gate refuses in what the guard and interpreter write"
code = ord(character)
return code < 0x20 or code == 0x7F or 0x80 <= code <= 0x9F
@@ -39,80 +60,8 @@ class JournalError(Exception):
"""A journal interface call was given a wrong argument, or a journal is malformed."""
-class Symbol:
- """
- The value an instruction will produce when it is executed. A symbol is the
- index of the instruction that produces it, counted from 0. It is encoded in the
- journal as {"symbol": index}.
- """
- __slots__ = ("index" ,)
-
- def __init__(self ,index):
- self.index = index
-
- def as_data(self):
- return {"symbol": self.index}
-
- def __repr__(self):
- return f"Symbol({self.index})"
-
- @staticmethod
- def is_data(value):
- return isinstance(value ,dict) and set(value) == {"symbol"} and isinstance(value["symbol"] ,int)
-
-
-class Instruction:
- __slots__ = ("name" ,"argument" ,"composer")
-
- def __init__(self ,name ,argument ,composer):
- self.name = name
- self.argument = argument
- self.composer = composer
-
- def as_data(self):
- return {
- "name": self.name
- ,"argument": {k: (v.as_data() if isinstance(v ,Symbol) else v) for k ,v in self.argument.items()}
- ,"composer": self.composer
- }
-
-
-class Journal:
- """
- The instructions of one run, in order, for one instruction language.
- """
-
- def __init__(self ,instruction_language):
- self.instruction_language = instruction_language
- self.meta = {}
- self.instruction_list = []
- self.composer = None
-
- def place(self ,name ,argument):
- """Place an instruction; returns its symbol."""
- self.instruction_list.append(Instruction(name ,dict(argument) ,self.composer))
- return Symbol(len(self.instruction_list) - 1)
-
- def as_data(self):
- data = {
- "version": JOURNAL_VERSION
- ,"instruction_language": self.instruction_language
- ,"meta": dict(self.meta)
- ,"instruction_list": [c.as_data() for c in self.instruction_list]
- }
- return data
-
- def encode(self):
- return cbor2.dumps(self.as_data() ,canonical=True)
-
- def print(self ,file=None):
- import sys
- file = file or sys.stdout
- if not self.instruction_list:
- print("(the journal is empty)" ,file=file)
- for index ,instruction in enumerate(self.instruction_list):
- print(instruction_line(index ,instruction.as_data()) ,file=file)
-
+# --------------------------------------------------------------------------------
+# decoding and printing
def decode(journal_bytes):
"""
@@ -123,37 +72,66 @@ def decode(journal_bytes):
data = cbor2.loads(journal_bytes)
except Exception as error:
raise JournalError(f"the journal is not valid CBOR: {error}")
- if not isinstance(data ,dict):
+ if type(data) is not dict:
raise JournalError("the journal is not a map")
if data.get("version") != JOURNAL_VERSION:
- raise JournalError(f"journal version {data.get('version')!r}; this Man_In_Grey reads version {JOURNAL_VERSION}")
- if not isinstance(data.get("instruction_language") ,str):
- raise JournalError("the journal names no instruction language")
- instruction_list = data.get("instruction_list")
- if not isinstance(instruction_list ,list):
- raise JournalError("the journal has no instruction list")
- for index ,instruction in enumerate(instruction_list):
- if not (isinstance(instruction ,dict) and isinstance(instruction.get("name") ,str) and isinstance(instruction.get("argument") ,dict)):
- raise JournalError(f"instruction {index} is not an instruction")
- for name ,value in instruction["argument"].items():
- if isinstance(value ,dict) and not Symbol.is_data(value):
- raise JournalError(f"instruction {index}: argument {name} is neither a value nor a symbol")
- if Symbol.is_data(value) and not (0 <= value["symbol"] < index):
- raise JournalError(f"instruction {index}: argument {name} names symbol {value['symbol']}, which no earlier instruction produces")
+ raise JournalError(f"journal version {clean(repr(data.get('version')))}; this Man_In_Grey reads version {JOURNAL_VERSION}")
+ if set(data) != set(LIST_ENTRY):
+ raise JournalError(f"the journal has the entries {clean(sorted(map(str ,data)))}; it is to have exactly {list(LIST_ENTRY)}")
+ if type(data["instruction_language"]) is not str or type(data["definition_hash"]) is not str:
+ raise JournalError("the journal names no instruction language or no definition hash")
+ if type(data["meta"]) is not dict or type(data["list_variable"]) is not list or type(data["list_instruction"]) is not list:
+ raise JournalError("the journal's meta is not a map, or its variables or instructions are not a list")
+ if data["result"] is not None and type(data["result"]) is not str:
+ raise JournalError("the journal's result is neither a variable name nor null")
+ for index ,instruction in enumerate(data["list_instruction"]):
+ if type(instruction) is not dict or set(instruction) != {"operation" ,"list_argument"} \
+ or type(instruction["operation"]) is not str or type(instruction["list_argument"]) is not list:
+ raise JournalError(f"instruction {index} is not {{\"operation\": ,\"list_argument\": [...]}}")
return data
+MAP_DIRECTION_MARK = {"in": "<" ,"out": ">" ,"in-out": "<>"}
+
+def argument_text(pair ,direction):
+ kind ,payload = pair
+ if kind == "variable":
+ return f"{MAP_DIRECTION_MARK.get(direction ,'?')}{payload}"
+ if type(payload) is bytes:
+ return f"<{len(payload)} bytes>"
+ return clean(repr(payload))
-def instruction_line(index ,instruction):
- """One line describing an instruction, for printing a journal."""
- part = []
- for name ,value in instruction["argument"].items():
- if Symbol.is_data(value):
- part.append(f"{name}=@{value['symbol']}")
- elif isinstance(value ,(bytes ,bytearray)):
- part.append(f"{name}=<{len(value)} bytes>")
- elif name == "permission" and isinstance(value ,int):
- part.append(f"{name}={value:04o}")
- else:
- part.append(f"{name}={value!r}")
- composer = instruction.get("composer")
- return f"@{index:<3} {instruction['name']:<12} {' '.join(part)}" + (f" ({composer})" if composer else "")
+def print_data(data ,language_definition ,file):
+ """
+ A journal, one line for each declaration and instruction. A variable is
+ shown as name when it writes it, and
+ <>name when it does both.
+ """
+ for name ,type_name in data["list_variable"]:
+ print(f"variable {clean(name)} {clean(type_name)}" ,file=file)
+ if data["result"] is not None:
+ print(f"result {clean(data['result'])}" ,file=file)
+ if not data["list_instruction"]:
+ print("(the journal has no instructions)" ,file=file)
+ for index ,instruction in enumerate(data["list_instruction"]):
+ format_ = language_definition.format_of(instruction["operation"]) or []
+ list_direction = [d for d ,_ in format_] + ["?"] * len(instruction["list_argument"])
+ list_part = [argument_text(p ,d) for p ,d in zip(instruction["list_argument"] ,list_direction)]
+ print(f"@{index:<3} {clean(instruction['operation']):<12} {' '.join(list_part)}" ,file=file)
+
+
+# --------------------------------------------------------------------------------
+# the result
+
+def encode_result(type_name ,value):
+ "the record an interpreter writes on descriptor 3: the result's type name and its value"
+ return cbor2.dumps({"type": type_name ,"value": value} ,canonical=True)
+
+def decode_result(result_bytes):
+ "the (type name ,value) of a result record. Raises JournalError."
+ try:
+ data = cbor2.loads(result_bytes)
+ except Exception as error:
+ raise JournalError(f"the result is not valid CBOR: {error}")
+ if type(data) is not dict or set(data) != {"type" ,"value"} or type(data["type"]) is not str:
+ raise JournalError("the result is not {\"type\": ,\"value\": }")
+ return data["type"] ,data["value"]
diff --git "a/developer/authored/Man_In_Grey.copy/Journal\302\267Interface.py" "b/developer/authored/Man_In_Grey.copy/Journal\302\267Interface.py"
new file mode 100644
index 0000000..430e24e
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Journal\302\267Interface.py"
@@ -0,0 +1,118 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Journal·Interface - the composer's side of a journal.
+
+A composer is given a JournalInterface. It has one function for each
+operation of the language, named by its operation specifier, given the
+instruction's arguments in the order of its format: a Variable where the
+format reads or writes a variable, any other value as a literal. Besides
+them, declare(name ,type_name) returns a new Variable, and result(variable)
+makes a variable the journal's result.
+
+Every declaration and instruction is checked by the well-formed guard's pass
+as it is placed, and refused with JournalError when it is not well formed,
+so a composer's mistake is an error in the composer, and no journal reaches
+the gate.
+
+An application that is its own orchestrator makes a JournalComposed from
+the language definition, and a JournalInterface on it, and encodes the
+JournalComposed when the composing is done.
+
+This module is a library.
+"""
+
+import cbor2
+
+import Journal
+from Journal import JournalError
+from Guard·WellFormed import Scan
+
+
+class Variable:
+ """A variable of the journal, as a composer holds it: its name and type name."""
+ __slots__ = ("name" ,"type_name")
+
+ def __init__(self ,name ,type_name):
+ self.name = name
+ self.type_name = type_name
+
+ def __repr__(self):
+ return f"Variable({self.name!r} ,{self.type_name!r})"
+
+
+class JournalComposed:
+ """
+ The journal being composed, for one instruction language. Given the
+ language definition.
+ """
+
+ def __init__(self ,language_definition):
+ self.language_definition = language_definition
+ self.meta = {}
+ self.list_variable = []
+ self.result_name = None
+ self.list_instruction = []
+ self._scan = Scan(language_definition)
+
+ def declare(self ,name ,type_name):
+ list_problem = self._scan.declare(name ,type_name)
+ if list_problem: raise JournalError(list_problem[0])
+ self.list_variable.append([name ,type_name])
+ return Variable(name ,type_name)
+
+ def set_result(self ,variable):
+ if not isinstance(variable ,Variable) or self._scan.map_declared.get(variable.name) != variable.type_name:
+ raise JournalError("result: give a variable declare returned")
+ if variable.type_name not in self.language_definition.list_result_type:
+ raise JournalError(f"result: the type {variable.type_name} is not among the result types {self.language_definition.list_result_type}")
+ self.result_name = variable.name
+
+ def place(self ,operation ,list_value):
+ list_pair = [["variable" ,v.name] if isinstance(v ,Variable) else ["literal" ,v] for v in list_value]
+ list_problem = self._scan.instruction(len(self.list_instruction) ,operation ,list_pair)
+ if list_problem: raise JournalError(list_problem[0])
+ self.list_instruction.append({"operation": operation ,"list_argument": list_pair})
+
+ def as_data(self):
+ list_problem = self._scan.result(self.result_name)
+ if list_problem: raise JournalError(list_problem[0])
+ return {
+ "version": Journal.JOURNAL_VERSION
+ ,"instruction_language": self.language_definition.name
+ ,"definition_hash": self.language_definition.hash
+ ,"meta": dict(self.meta)
+ ,"list_variable": [list(v) for v in self.list_variable]
+ ,"result": self.result_name
+ ,"list_instruction": list(self.list_instruction)
+ }
+
+ def encode(self):
+ return cbor2.dumps(self.as_data() ,canonical=True)
+
+ def print(self ,file=None):
+ import sys
+ Journal.print_data(self.as_data() ,self.language_definition ,file or sys.stdout)
+
+
+class JournalInterface:
+ """
+ The journal interface a composer is given, on a JournalComposed. See the
+ module's description.
+ """
+
+ def __init__(self ,journal_composed):
+ self._journal_composed = journal_composed
+ for operation in journal_composed.language_definition.list_operation:
+ setattr(self ,operation ,self._placer(operation))
+
+ def _placer(self ,operation):
+ def place(*list_value):
+ self._journal_composed.place(operation ,list(list_value))
+ place.__name__ = operation
+ return place
+
+ def declare(self ,name ,type_name):
+ return self._journal_composed.declare(name ,type_name)
+
+ def result(self ,variable):
+ self._journal_composed.set_result(variable)
diff --git a/developer/authored/Man_In_Grey.copy/Language.py b/developer/authored/Man_In_Grey.copy/Language.py
new file mode 100644
index 0000000..cf1aa78
--- /dev/null
+++ b/developer/authored/Man_In_Grey.copy/Language.py
@@ -0,0 +1,251 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Language - what a Man_In_Grey instruction language is, and how its definition is checked.
+
+This module defines no language. An instruction language is defined by its
+application developer as data: a map of four sections, written as a literal
+value in a module of its own, such as EmplaceBio·Language.py. Account A
+cannot change it. The guard, the interpreter and the journal interface all
+read the same definition, and a journal carries the hash of the definition
+it was composed against.
+
+ list_type a list of type declarations, each [ ,].
+ A type expression is a primitive, the name of a type declared
+ earlier in the list, or a compound:
+ ["dictionary" ,[ ,] ...]
+ ["list" ,]
+ list_format a list of instruction formats, each
+ [ ,[ ,] ...], where the
+ direction is in, out or in-out, and the type name is a
+ primitive or a declared type. Arguments are given by position.
+ list_result the list of type names a journal's result can have.
+ map_instruction
+ a map from each operation specifier to its format name. The
+ operation specifier is also the name of the function that
+ carries out the instruction.
+
+The primitives are text, integer, bytes, boolean and handle. A handle is a
+value an instruction produces for later instructions, such as an open file:
+no literal can be a handle, and no result can hold one.
+
+Types are compared by name. A variable of the type short_name is not a
+variable of the type text, even when short_name is declared as text, so a
+reference passes only where the format names its type exactly. A literal is
+compared with the type's structure.
+
+This module is a library. is_well_formed_language(data ,report) answers
+whether a definition is well formed, reporting each problem;
+Definition(name ,data) is a definition that has passed, as the guard, the
+interpreter and the journal interface each use it.
+"""
+
+import hashlib ,keyword
+
+import cbor2
+
+LIST_PRIMITIVE = ("text" ,"integer" ,"bytes" ,"boolean" ,"handle")
+LIST_COMPOUND = ("dictionary" ,"list")
+LIST_DIRECTION = ("in" ,"out" ,"in-out")
+LIST_SECTION = ("list_type" ,"list_format" ,"list_result" ,"map_instruction")
+
+# operation specifiers the journal interface and the interpreter keep for themselves
+LIST_OPERATION_RESERVED = ("declare" ,"result" ,"finish" ,"release")
+
+
+class DefinitionError(Exception):
+ """A language definition is not well formed."""
+
+
+def is_control(character):
+ code = ord(character)
+ return code < 0x20 or code == 0x7F or 0x80 <= code <= 0x9F
+
+def is_name(value):
+ "True for a name: text that is not empty and holds no space and no control character"
+ return type(value) is str and value != "" and not any(c.isspace() or is_control(c) for c in value)
+
+def hash_text(data):
+ "the SHA-256 of a definition's canonical CBOR encoding, in hexadecimal"
+ return hashlib.sha256(cbor2.dumps(data ,canonical=True)).hexdigest()
+
+
+# --------------------------------------------------------------------------------
+# the well-formed check of a definition
+
+def _type_expression_problem(expression ,map_type ,where):
+ "None when expression is a well-formed type expression over the primitives and the types in map_type"
+ if type(expression) is str:
+ if expression in LIST_PRIMITIVE or expression in map_type:
+ return None
+ return f"{where}: {expression!r} is neither a primitive nor a type declared before it"
+ if type(expression) is not list or not expression:
+ return f"{where}: a type expression is a type name or a list beginning with dictionary or list"
+ head = expression[0]
+ if head == "list":
+ if len(expression) != 2:
+ return f"{where}: a list type is [\"list\" ,]"
+ return _type_expression_problem(expression[1] ,map_type ,f"{where} list element")
+ if head == "dictionary":
+ set_key = set()
+ for entry in expression[1:]:
+ if type(entry) is not list or len(entry) != 2 or not is_name(entry[0]):
+ return f"{where}: a dictionary entry is [ ,]"
+ if entry[0] in set_key:
+ return f"{where}: the key {entry[0]!r} is given twice"
+ set_key.add(entry[0])
+ problem = _type_expression_problem(entry[1] ,map_type ,f"{where} key {entry[0]}")
+ if problem: return problem
+ return None
+ return f"{where}: {head!r} is not a compound type; the compounds are {', '.join(LIST_COMPOUND)}"
+
+def _is_handle_holder(expression ,map_type):
+ "True when the type expression is, or holds, a handle"
+ if type(expression) is str:
+ if expression == "handle": return True
+ if expression in map_type: return _is_handle_holder(map_type[expression] ,map_type)
+ return False
+ if expression[0] == "list":
+ return _is_handle_holder(expression[1] ,map_type)
+ return any(_is_handle_holder(entry[1] ,map_type) for entry in expression[1:])
+
+def list_problem_language(data):
+ "every problem with a language definition, as a list of messages; empty when it is well formed"
+ if type(data) is not dict:
+ return ["the definition is not a map"]
+ if set(data) != set(LIST_SECTION):
+ return [f"the definition has the sections {sorted(data)}; it is to have exactly {list(LIST_SECTION)}"]
+ list_problem = []
+
+ map_type = {}
+ if type(data["list_type"]) is not list:
+ list_problem.append("list_type: is not a list")
+ else:
+ for index ,entry in enumerate(data["list_type"]):
+ where = f"list_type @{index}"
+ if type(entry) is not list or len(entry) != 2:
+ list_problem.append(f"{where}: a type declaration is [ ,]"); continue
+ name ,expression = entry
+ if not is_name(name):
+ list_problem.append(f"{where}: {name!r} is not a name"); continue
+ if name in LIST_PRIMITIVE or name in LIST_COMPOUND or name in map_type:
+ list_problem.append(f"{where}: the type name {name!r} is a primitive, a compound or declared before"); continue
+ problem = _type_expression_problem(expression ,map_type ,f"{where} {name}")
+ if problem: list_problem.append(problem); continue
+ map_type[name] = expression
+
+ set_format_name = set()
+ if type(data["list_format"]) is not list:
+ list_problem.append("list_format: is not a list")
+ else:
+ for index ,entry in enumerate(data["list_format"]):
+ where = f"list_format @{index}"
+ if type(entry) is not list or not entry or not is_name(entry[0]):
+ list_problem.append(f"{where}: an instruction format is [ ,[ ,] ...]"); continue
+ name = entry[0]
+ if name in set_format_name:
+ list_problem.append(f"{where}: the format {name!r} is declared twice"); continue
+ set_format_name.add(name)
+ for position ,argument in enumerate(entry[1:]):
+ if type(argument) is not list or len(argument) != 2:
+ list_problem.append(f"{where} {name} argument {position}: an argument is [ ,]"); continue
+ direction ,type_name = argument
+ if direction not in LIST_DIRECTION:
+ list_problem.append(f"{where} {name} argument {position}: {direction!r} is not one of {', '.join(LIST_DIRECTION)}")
+ if type(type_name) is not str or not (type_name in LIST_PRIMITIVE or type_name in map_type):
+ list_problem.append(f"{where} {name} argument {position}: {type_name!r} is neither a primitive nor a declared type")
+
+ if type(data["list_result"]) is not list:
+ list_problem.append("list_result: is not a list")
+ else:
+ for type_name in data["list_result"]:
+ if type(type_name) is not str or not (type_name in LIST_PRIMITIVE or type_name in map_type):
+ list_problem.append(f"list_result: {type_name!r} is neither a primitive nor a declared type"); continue
+ if _is_handle_holder(type_name ,map_type):
+ list_problem.append(f"list_result: {type_name!r} holds a handle, which has no meaning outside the interpreter")
+ if len(set(map(repr ,data["list_result"]))) != len(data["list_result"]):
+ list_problem.append("list_result: a type is given twice")
+
+ if type(data["map_instruction"]) is not dict or not data["map_instruction"]:
+ list_problem.append("map_instruction: is not a map of at least one operation specifier to its format name")
+ else:
+ for operation ,format_name in data["map_instruction"].items():
+ if type(operation) is not str or not operation.isidentifier() or keyword.iskeyword(operation) or operation.startswith("_"):
+ list_problem.append(f"map_instruction: {operation!r} is not an operation specifier; it is a Python function name not beginning with _"); continue
+ if operation in LIST_OPERATION_RESERVED:
+ list_problem.append(f"map_instruction: {operation!r} is kept by Man_In_Grey; the kept names are {', '.join(LIST_OPERATION_RESERVED)}")
+ if format_name not in set_format_name:
+ list_problem.append(f"map_instruction: {operation} has the format {format_name!r}, which is not declared")
+ return list_problem
+
+def is_well_formed_language(data ,report):
+ """
+ True when the language definition data is well formed. Each problem found
+ is given to report(tag ,message), as definition_broken.
+ """
+ list_problem = list_problem_language(data)
+ for problem in list_problem:
+ report("definition_broken" ,problem)
+ return not list_problem
+
+
+# --------------------------------------------------------------------------------
+# a checked definition
+
+class Definition:
+ """
+ A language definition that has passed its well-formed check. Given the
+ language's name and the definition data. Raises DefinitionError.
+ """
+
+ def __init__(self ,name ,data):
+ list_problem = list_problem_language(data)
+ if list_problem:
+ raise DefinitionError(f"the definition of {name} is not well formed: " + "; ".join(list_problem))
+ self.name = name
+ self.data = data
+ self.hash = hash_text(data)
+ self._map_type = {entry[0]: entry[1] for entry in data["list_type"]}
+ self._map_format = {entry[0]: [tuple(argument) for argument in entry[1:]] for entry in data["list_format"]}
+ self.list_result_type = list(data["list_result"])
+ self.list_operation = list(data["map_instruction"])
+
+ def is_type_name(self ,name):
+ return type(name) is str and (name in LIST_PRIMITIVE or name in self._map_type)
+
+ def format_of(self ,operation):
+ "the arguments of the operation's format, as (direction ,type name) in order; None for an unknown operation"
+ format_name = self.data["map_instruction"].get(operation) if type(operation) is str else None
+ return None if format_name is None else self._map_format[format_name]
+
+ def is_handle_holder(self ,type_name):
+ return _is_handle_holder(type_name ,self._map_type)
+
+ def value_problem(self ,value ,expression ,where="the value"):
+ """
+ None when value has the structure of the type expression; otherwise what
+ is wrong. Checks type() exactly, so that a boolean is never taken for an
+ integer, as isinstance(True ,int) would.
+ """
+ if type(expression) is str:
+ if expression in self._map_type:
+ return self.value_problem(value ,self._map_type[expression] ,where)
+ want = {"text": str ,"integer": int ,"bytes": bytes ,"boolean": bool ,"handle": int}[expression]
+ if type(value) is not want:
+ return f"{where} is {type(value).__name__}, not {expression}"
+ return None
+ if expression[0] == "list":
+ if type(value) is not list:
+ return f"{where} is {type(value).__name__}, not a list"
+ for i ,element in enumerate(value):
+ problem = self.value_problem(element ,expression[1] ,f"{where}[{i}]")
+ if problem: return problem
+ return None
+ map_key_type = {entry[0]: entry[1] for entry in expression[1:]}
+ if type(value) is not dict:
+ return f"{where} is {type(value).__name__}, not a dictionary"
+ if set(value) != set(map_key_type):
+ return f"{where} has the keys {sorted(map(str ,value))}, not {sorted(map_key_type)}"
+ for key ,element_type in map_key_type.items():
+ problem = self.value_problem(value[key] ,element_type ,f"{where}[{key!r}]")
+ if problem: return problem
+ return None
diff --git a/developer/authored/Man_In_Grey.copy/Man_In_Grey.py b/developer/authored/Man_In_Grey.copy/Man_In_Grey.py
index 2bd4653..7e95d0f 100755
--- a/developer/authored/Man_In_Grey.copy/Man_In_Grey.py
+++ b/developer/authored/Man_In_Grey.copy/Man_In_Grey.py
@@ -3,25 +3,26 @@
"""
Man_In_Grey
-The orchestrator. It runs composers, which place instructions into one journal,
-and invokes a gate with the journal on its standard input. The gate starts
-the guard, which checks the journal against its constraints, and, when the
-guard passes the journal, the interpreter, which executes it.
+The orchestrator. It runs a composer, which declares variables and places
+instructions into a journal, and invokes a gate with the journal on its
+standard input. The gate starts the guard, which checks the journal against
+the language definition and the constraints, and, when the guard passes the
+journal, the interpreter, which plays it. When the journal declares a
+result, the gate hands it back on descriptor 3, and the orchestrator prints
+it.
A composer is a Python file defining compose(prov ,journal_iface). prov
-describes where the composer is; journal_iface places instructions of the
-gate's instruction language. The orchestrator learns the language from the
-gate's flags, and loads the module named for it: emplace_bio from
-EmplaceBioLanguage.py.
+describes where the composer is; journal_iface has a function for each
+instruction of the gate's instruction language, and declare and result. The
+orchestrator learns the language from the gate's flags, and loads the
+definition named for it: emplace_bio from EmplaceBio·Language.py.
Usage: Man_In_Grey ...
help | -h print this message (also printed when given no arguments)
- orchestrate gate [option ...]
- is a composer file, or a composer directory; every
- file in a composer directory is a composer, and they run
- depth first, in order of name at each level; is the
- gate to invoke, which also sets the instruction language
- the composers write in
+ orchestrate gate [option ...]
+ is a composer file; is the gate to
+ invoke, which also sets the instruction language the
+ composer writes in
install owner [group ] into [constraints ]
install a gate setuid and setgid, by running install_gate
as root; when not run by root, su asks for root's password;
@@ -32,13 +33,15 @@ Options of orchestrate:
compose_only stop after composing; nothing reaches the gate
check_only have the gate run the guard and not the interpreter
-Every composer, and every directory from / down to it, is to be owned by the
-gate invoking account or by root, and writable by no other account. Symbolic
-links in a composer directory are refused.
+The composer, and every directory from / down to it, is to be owned by the
+gate invoking account or by root, and writable by no other account. A
+composer that is a symbolic link is refused.
+
+A result is printed as one line, result: .
Exit status: 0 when the run did what it was asked; 1 when the guard refused
the journal or an instruction failed to execute; 2 when the run could not start,
-a composer failed, or the gate, guard or interpreter could not run.
+the composer failed, or the gate, guard or interpreter could not run.
"""
import sys ,os
@@ -47,7 +50,9 @@ sys.dont_write_bytecode = True
import datetime ,importlib ,pwd ,re ,runpy ,shlex ,stat ,subprocess ,traceback
from pathlib import Path
+import Language
import Journal
+import Journal·Interface
# The promoted layout: this file in Man_In_Grey/, beside Man_In_Grey_gate/.
# The same layout is staged in developer/scratchpad/made/ and promoted to consumer/.
@@ -70,19 +75,15 @@ class Provenance:
Where a composer is. Given to compose().
"""
__slots__ = (
- "composer_root_dpath"
- ,"composer_abs_fpath"
- ,"composer_rel_fpath"
+ "composer_abs_fpath"
,"composer_dir_dpath"
,"implied_fname"
,"process_user"
,"cwd_dpath"
)
- def __init__(self ,composer_root ,composer_path):
- self.composer_root_dpath = composer_root
+ def __init__(self ,composer_path):
self.composer_abs_fpath = composer_path
- self.composer_rel_fpath = composer_path.relative_to(composer_root)
self.composer_dir_dpath = composer_path.parent
name = composer_path.name
if name.endswith(".py"):
@@ -115,38 +116,22 @@ def chain_problem(path):
return problem
return None
-def composer_list(path):
+def composer_provenance(path):
"""
- Given a composer file or a composer directory, absolute. Returns the
- composers in the order they run, each as a Provenance. Raises RuntimeError
- on a symbolic link, a file that is not regular, or a failed ownership check.
+ Given a composer file, absolute. Returns its Provenance. Raises
+ RuntimeError on a symbolic link, a file that is not regular, or a failed
+ ownership check.
"""
problem = chain_problem(path)
if problem:
- raise RuntimeError(f"refusing to run composers: {problem}")
+ raise RuntimeError(f"refusing to run the composer: {problem}")
if path.is_symlink():
raise RuntimeError(f"{path} is a symbolic link")
- if stat.S_ISREG(path.lstat().st_mode):
- return [Provenance(path.parent ,path)]
- if not path.is_dir():
- raise RuntimeError(f"{path} is neither a composer file nor a composer directory")
- found = []
- def walk(directory):
- for entry in sorted(os.scandir(directory) ,key=lambda e: e.name):
- each = Path(entry.path)
- if entry.is_symlink():
- raise RuntimeError(f"{each} is a symbolic link; a composer directory holds composers and directories")
- problem = trusted_owner_problem(each)
- if problem:
- raise RuntimeError(f"refusing to run composers: {problem}")
- if entry.is_dir(follow_symlinks=False):
- walk(each)
- elif entry.is_file(follow_symlinks=False):
- found.append(Provenance(path ,each))
- else:
- raise RuntimeError(f"{each} is neither a file nor a directory")
- walk(path)
- return found
+ if path.is_dir():
+ raise RuntimeError(f"{path} is a directory; this version of Man_In_Grey runs one composer, given as a file")
+ if not stat.S_ISREG(path.lstat().st_mode):
+ raise RuntimeError(f"{path} is not a composer file")
+ return Provenance(path)
# --------------------------------------------------------------------------------
@@ -161,51 +146,103 @@ def gate_flag(gate_path):
raise RuntimeError(f"the gate {gate_path} did not report its flags")
return dict(line.split("=" ,1) for line in result.stdout.splitlines() if "=" in line)
-def language_module(name):
- """The module of an instruction language: emplace_bio is EmplaceBioLanguage.py. Raises RuntimeError."""
+def language_definition(name):
+ """
+ The definition of an instruction language: emplace_bio's is in
+ EmplaceBio·Language.py, beside this file. Raises RuntimeError.
+ """
if not re.fullmatch(r"[a-z][a-z0-9_]*" ,name or ""):
- raise RuntimeError(f"the gate names the language {name!r}, which names no module")
- module = "".join(part.capitalize() for part in name.split("_")) + "Language"
+ raise RuntimeError(f"the gate names the language {name!r}, which names no definition")
+ module = "".join(part.capitalize() for part in name.split("_")) + "·Language"
if not (HERE_DPATH / f"{module}.py").is_file():
raise RuntimeError(f"the gate's language is {name}, and {module}.py is not beside {Path(__file__).name}")
- language = importlib.import_module(module)
- if getattr(language ,"NAME" ,None) != name:
- raise RuntimeError(f"{module}.py is not the module of the language {name}")
- return language
+ definition_module = importlib.import_module(module)
+ if getattr(definition_module ,"NAME" ,None) != name:
+ raise RuntimeError(f"{module}.py is not the definition of the language {name}")
+ try:
+ return Language.Definition(name ,definition_module.DEFINITION)
+ except Language.DefinitionError as error:
+ raise RuntimeError(str(error))
# --------------------------------------------------------------------------------
# composing, and invoking the gate
-def compose_all(composer_root ,prov_list ,language):
- """Run each composer into one journal. Raises RuntimeError naming the composer that failed."""
- journal = Journal.Journal(language.NAME)
- journal_iface = language.JournalInterface(journal ,Path.cwd())
- for prov in prov_list:
- rel = prov.composer_rel_fpath.as_posix()
- journal.composer = rel
- try:
- env = runpy.run_path(str(prov.composer_abs_fpath))
- compose = env.get("compose")
- if not callable(compose):
- raise RuntimeError("defines no compose(prov ,journal_iface)")
- compose(prov ,journal_iface)
- except Journal.JournalError as error:
- raise RuntimeError(f"{rel}: {error}")
- except RuntimeError as error:
- raise RuntimeError(f"{rel}: {error}")
- except Exception:
- raise RuntimeError(f"{rel}: the composer raised an error\n{traceback.format_exc()}")
- journal.composer = None
+def compose(prov ,language_definition):
+ """Run the composer into a journal. Raises RuntimeError naming the composer when it fails."""
+ journal = Journal·Interface.JournalComposed(language_definition)
+ journal_iface = Journal·Interface.JournalInterface(journal)
+ name = prov.composer_abs_fpath.name
+ try:
+ env = runpy.run_path(str(prov.composer_abs_fpath))
+ function = env.get("compose")
+ if not callable(function):
+ raise RuntimeError("defines no compose(prov ,journal_iface)")
+ function(prov ,journal_iface)
+ journal.as_data() # a result declared and never written is refused here
+ except Journal.JournalError as error:
+ raise RuntimeError(f"{name}: {error}")
+ except RuntimeError as error:
+ raise RuntimeError(f"{name}: {error}")
+ except Exception:
+ raise RuntimeError(f"{name}: the composer raised an error\n{traceback.format_exc()}")
journal.meta = {
"composed_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
,"process_user": gate_invoking_account_name()
,"host": os.uname().nodename
- ,"composer_root": str(composer_root)
- ,"composer_list": [p.composer_rel_fpath.as_posix() for p in prov_list]
+ ,"composer": str(prov.composer_abs_fpath)
}
return journal
+RESULT_FD = 3
+
+def admit(gate_path ,journal_bytes ,check_only=False):
+ """
+ Invoke the gate with the journal on its standard input, and descriptor 3
+ open to receive the result. The gate's standard output and standard error
+ are this process's. Returns (status ,result), where result is the bytes
+ the gate wrote on descriptor 3, empty when there was none.
+
+ An application that is its own orchestrator calls this function with the
+ journal it made.
+ """
+ command = [str(gate_path) ,"admit"] + (["check_only"] if check_only else [])
+ read_fd ,write_fd = os.pipe()
+ try:
+ gate = subprocess.Popen(command ,stdin=subprocess.PIPE ,close_fds=False
+ ,preexec_fn=lambda: os.dup2(write_fd ,RESULT_FD))
+ except BaseException:
+ os.close(read_fd); os.close(write_fd)
+ raise
+ os.close(write_fd)
+ try:
+ gate.stdin.write(journal_bytes)
+ except BrokenPipeError:
+ pass
+ finally:
+ try:
+ gate.stdin.close()
+ except BrokenPipeError:
+ pass
+ list_part = []
+ with os.fdopen(read_fd ,"rb") as result:
+ while True:
+ part = result.read(1 << 16)
+ if not part: break
+ list_part.append(part)
+ return gate.wait() ,b"".join(list_part)
+
+def print_result(result_bytes ,file=None):
+ "one line, result: , with bytes shown by their count"
+ try:
+ type_name ,value = Journal.decode_result(result_bytes)
+ except Journal.JournalError as error:
+ print(f"error: {error}" ,file=sys.stderr)
+ return False
+ shown = f"<{len(value)} bytes>" if type(value) is bytes else repr(value)
+ print(f"result: {type_name} {shown}" ,file=file or sys.stdout)
+ return True
+
def orchestrate(path ,gate ,flag):
path = Path(path).absolute()
if not gate:
@@ -213,10 +250,7 @@ def orchestrate(path ,gate ,flag):
return 2
gate_path = Path(gate).absolute()
try:
- language = language_module(gate_flag(gate_path).get("language"))
- prov_list = composer_list(path)
- composer_root = path if path.is_dir() else path.parent
- journal = compose_all(composer_root ,prov_list ,language)
+ journal = compose(composer_provenance(path) ,language_definition(gate_flag(gate_path).get("language")))
except (OSError ,RuntimeError) as error:
print(f"error: {error}" ,file=sys.stderr)
return 2
@@ -224,11 +258,11 @@ def orchestrate(path ,gate ,flag):
journal.print()
if "compose_only" in flag:
return 0
-
- command = [str(gate_path) ,"admit"]
- if "check_only" in flag:
- command.append("check_only")
- return subprocess.run(command ,input=journal.encode()).returncode
+ sys.stdout.flush()
+ status ,result = admit(gate_path ,journal.encode() ,"check_only" in flag)
+ if result and not print_result(result):
+ return 2
+ return status
# --------------------------------------------------------------------------------
@@ -241,18 +275,18 @@ def print_help():
# --------------------------------------------------------------------------------
# installing a gate
-def install(argument_list):
+def install(list_argument):
"""
Run install_gate install with the arguments given, as root. Run by root, it
runs install_gate itself; run by anyone else, it runs it through su, which
asks for root's password. A constraints file is made absolute first, as
root's shell starts elsewhere. Returns install_gate's exit status.
"""
- argument_list = list(argument_list)
- for i ,word in enumerate(argument_list[:-1]):
+ list_argument = list(list_argument)
+ for i ,word in enumerate(list_argument[:-1]):
if word == "constraints":
- argument_list[i + 1] = str(Path(argument_list[i + 1]).absolute())
- command = [str(HERE_DPATH / "install_gate") ,"install" ,*argument_list]
+ list_argument[i + 1] = str(Path(list_argument[i + 1]).absolute())
+ command = [str(HERE_DPATH / "install_gate") ,"install" ,*list_argument]
if os.geteuid() != 0:
print("install_gate is run by root; su asks for root's password" ,file=sys.stderr)
command = ["su" ,"root" ,"-c" ,shlex.join(command)]
@@ -270,7 +304,7 @@ def CLI(argv=None):
print_help()
return 2
if not rest:
- print(f"Error: {command} is given a composer file or a composer directory." ,file=sys.stderr)
+ print(f"Error: {command} is given a composer file." ,file=sys.stderr)
return 2
path ,rest = rest[0] ,rest[1:]
value ,flag = {} ,set()
diff --git a/developer/authored/Man_In_Grey.copy/SmokeLanguage.py b/developer/authored/Man_In_Grey.copy/SmokeLanguage.py
deleted file mode 100644
index 2022119..0000000
--- a/developer/authored/Man_In_Grey.copy/SmokeLanguage.py
+++ /dev/null
@@ -1,331 +0,0 @@
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-SmokeLanguage - the instruction language of the tester's smoke test.
-
-It exists to exercise the gate, the guard library and the interpreter
-library, and is not for use on a system: its constraints can say where a
-file goes, and with what mode, owner and permission, but not what the file
-says, and a configuration file's content is what its reader does. It is a
-constrained form of the POSIX file interface.
-
- open(directory ,name ,mode ,owner ,permission) produces a symbol for the file
- write(file ,content) appends content to the file
- unlink(directory ,name) removes a file
-
-The modes of open:
-
- create the file must not exist
- replace an existing file is discarded
- replace_keep an existing file is kept as .YYYYMMDDTHHMMSSZ
-
-There is no close. Each opened file is written to a temporary file in its
-directory, and after the journal's last instruction every opened file is put in
-place, each in one step.
-
-The module has two halves. The composer half is JournalInterface, through
-which a composer places instructions; it checks every argument when the call is
-made, and raises JournalError on a wrong one. The interpreter half holds the
-checks the guard makes of a journal received as data, and the execution the
-interpreter carries out.
-
-The constraints are predicates written at the top of the guard,
-guard_smoke.py: is_allowed_account, is_allowed_open and is_allowed_unlink.
-"""
-
-import os ,pwd ,stat ,time
-from pathlib import Path
-
-from Journal import JournalError ,Symbol ,has_control ,clean
-
-NAME = "smoke"
-
-MODE = ("create" ,"replace" ,"replace_keep")
-
-ARGUMENT = {
- "open": ("directory" ,"name" ,"mode" ,"owner" ,"permission")
- ,"write": ("file" ,"content")
- ,"unlink": ("directory" ,"name")
-}
-
-
-# --------------------------------------------------------------------------------
-# argument checks, shared by both halves
-
-def directory_problem(value):
- if not isinstance(value ,str) or not value.startswith("/"):
- return "directory is to be an absolute path"
- if has_control(value):
- return f"directory {clean(value)} holds a control character"
- if os.path.normpath(value) != value:
- return f"directory {value!r} is not in normal form; give {os.path.normpath(value)!r}"
- return None
-
-def name_problem(value):
- if not isinstance(value ,str) or not value or "/" in value or value in ("." ,".."):
- return f"name {clean(repr(value))} is not a bare file name"
- if has_control(value):
- return f"name {clean(value)} holds a control character"
- return None
-
-def owner_problem(value):
- if not isinstance(value ,str) or not value or has_control(value):
- return "owner is to be an account name"
- return None
-
-def permission_value(value):
- "an int 0..0o7777, or a string of 3 or 4 octal digits; None when neither"
- if isinstance(value ,bool):
- return None
- if isinstance(value ,int):
- return value if 0 <= value <= 0o7777 else None
- if isinstance(value ,str) and len(value) in (3 ,4) and all(c in "01234567" for c in value):
- return int(value ,8)
- return None
-
-
-# --------------------------------------------------------------------------------
-# composer half
-
-class JournalInterface:
- """
- The journal interface of the smoke language. A composer is given one, and
- places instructions with its three functions. open returns a Symbol: the file
- the open instruction will produce when the journal is executed.
- """
-
- def __init__(self ,journal ,working_dpath):
- self._journal = journal
- self._working_dpath = Path(working_dpath)
-
- def _directory(self ,directory):
- if not isinstance(directory ,(str ,Path)) or str(directory) == "":
- raise JournalError("directory is to be a path")
- path = Path(directory)
- if not path.is_absolute():
- path = self._working_dpath / path
- value = os.path.normpath(str(path))
- problem = directory_problem(value)
- if problem: raise JournalError(problem)
- return value
-
- def open(self ,directory ,name ,mode ,owner ,permission):
- directory = self._directory(directory)
- problem = name_problem(name)
- if problem: raise JournalError(f"open: {problem}")
- if mode not in MODE:
- raise JournalError(f"open: mode {mode!r} is not one of {', '.join(MODE)}")
- problem = owner_problem(owner)
- if problem: raise JournalError(f"open: {problem}")
- value = permission_value(permission)
- if value is None:
- raise JournalError(f"open: permission {permission!r} is not an integer 0 to 0o7777 or three or four octal digits")
- return self._journal.place("open" ,{
- "directory": directory ,"name": name ,"mode": mode ,"owner": owner ,"permission": value
- })
-
- def write(self ,file ,content):
- if not isinstance(file ,Symbol) or self._journal.instruction_list[file.index].name != "open":
- raise JournalError("write: file is to be the symbol an open returned")
- if isinstance(content ,str):
- content = content.encode("utf-8")
- if not isinstance(content ,(bytes ,bytearray)):
- raise JournalError("write: content is to be text or bytes")
- self._journal.place("write" ,{"file": file ,"content": bytes(content)})
-
- def unlink(self ,directory ,name):
- directory = self._directory(directory)
- problem = name_problem(name)
- if problem: raise JournalError(f"unlink: {problem}")
- self._journal.place("unlink" ,{"directory": directory ,"name": name})
-
-
-# --------------------------------------------------------------------------------
-# interpreter half: checks, each returning a list of problems
-
-def check_wellformed(data):
- problem_list = []
- for index ,instruction in enumerate(data["instruction_list"]):
- name ,argument = instruction["name"] ,instruction["argument"]
- if name not in ARGUMENT:
- problem_list.append(f"@{index}: unknown instruction {clean(repr(name))}"); continue
- if set(argument) != set(ARGUMENT[name]):
- problem_list.append(f"@{index} {name}: arguments {clean(sorted(argument))}, expected {sorted(ARGUMENT[name])}"); continue
- if name in ("open" ,"unlink"):
- for problem in (directory_problem(argument["directory"]) ,name_problem(argument["name"])):
- if problem: problem_list.append(f"@{index} {name}: {problem}")
- if name == "open":
- if argument["mode"] not in MODE:
- problem_list.append(f"@{index} open: unknown mode {clean(repr(argument['mode']))}")
- if owner_problem(argument["owner"]):
- problem_list.append(f"@{index} open: owner is not an account name")
- if permission_value(argument["permission"]) is None or not isinstance(argument["permission"] ,int):
- problem_list.append(f"@{index} open: permission {clean(repr(argument['permission']))} is not valid")
- if name == "write":
- file = argument["file"]
- if not Symbol.is_data(file) or data["instruction_list"][file["symbol"]]["name"] != "open":
- problem_list.append(f"@{index} write: file is not the symbol of an open")
- if not isinstance(argument["content"] ,(bytes ,bytearray)):
- problem_list.append(f"@{index} write: content is not bytes")
- return problem_list
-
-def check_constraint(data ,constraint ,gate_invoking_account):
- "constraint is the guard, which holds the predicates; a predicate allows only by answering True"
- if constraint.is_allowed_account(gate_invoking_account) is not True:
- return [f"gate invoking account {clean(repr(gate_invoking_account))} is not allowed to give journals to this gate"]
- problem_list = []
- for index ,instruction in enumerate(data["instruction_list"]):
- name ,argument = instruction["name"] ,instruction["argument"]
- where = f"{argument.get('directory')}/{argument.get('name')}"
- if name == "open":
- if constraint.is_allowed_open(argument["directory"] ,argument["name"] ,argument["mode"] ,argument["owner"] ,argument["permission"]) is not True:
- problem_list.append(f"@{index} open: {where} in mode {argument['mode']}, owner {argument['owner']}, permission {argument['permission']:04o} is not allowed")
- elif name == "unlink":
- if constraint.is_allowed_unlink(argument["directory"] ,argument["name"]) is not True:
- problem_list.append(f"@{index} unlink: {where} is not allowed to be removed")
- return problem_list
-
-def check_validity(data):
- problem_list = []
- for index ,instruction in enumerate(data["instruction_list"]):
- if instruction["name"] == "open":
- owner = instruction["argument"]["owner"]
- try:
- pwd.getpwnam(owner)
- except KeyError:
- problem_list.append(f"@{index} open: no account is named {clean(owner)}")
- return problem_list
-
-def _regular_or_absent(dirfd ,name):
- "True when absent; False when a regular file; raises OSError otherwise"
- try:
- st = os.lstat(name ,dir_fd=dirfd)
- except FileNotFoundError:
- return True
- if not stat.S_ISREG(st.st_mode):
- raise OSError("it exists and is not a regular file")
- return False
-
-def check_filesystem(data):
- problem_list = []
- seen = set()
- for index ,instruction in enumerate(data["instruction_list"]):
- name ,argument = instruction["name"] ,instruction["argument"]
- if name not in ("open" ,"unlink"):
- continue
- directory ,file_name = argument["directory"] ,argument["name"]
- where = f"{directory}/{file_name}"
- if (directory ,file_name) in seen:
- problem_list.append(f"@{index} {name}: {where} is named by more than one instruction"); continue
- seen.add((directory ,file_name))
- if os.path.realpath(directory) != directory:
- problem_list.append(f"@{index} {name}: {directory} passes through a symbolic link"); continue
- try:
- dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
- except OSError as error:
- problem_list.append(f"@{index} {name}: cannot open directory {directory} ({error.strerror})"); continue
- try:
- absent = _regular_or_absent(dirfd ,file_name)
- if name == "open" and argument["mode"] == "create" and not absent:
- problem_list.append(f"@{index} open: {where} exists, and the mode is create")
- except OSError as error:
- problem_list.append(f"@{index} {name}: {where}: {error}")
- finally:
- os.close(dirfd)
- return problem_list
-
-CHECK = (
- ("wellformed" ,lambda data ,constraint ,gate_invoking_account: check_wellformed(data))
- ,("constraint" ,check_constraint)
- ,("validity" ,lambda data ,constraint ,gate_invoking_account: check_validity(data))
- ,("filesystem" ,lambda data ,constraint ,gate_invoking_account: check_filesystem(data))
-)
-
-
-# --------------------------------------------------------------------------------
-# interpreter half: execution
-
-def _backup_name(dirfd ,name):
- stamp = time.strftime("%Y%m%dT%H%M%SZ" ,time.gmtime())
- candidate ,n = f"{name}.{stamp}" ,1
- while True:
- try:
- os.lstat(candidate ,dir_fd=dirfd)
- except FileNotFoundError:
- return candidate
- candidate ,n = f"{name}.{stamp}.{n}" ,n + 1
-
-def execute(data ,log ,constraint ,gate_invoking_account):
- """
- Execute the instructions in order, then put each opened file in place. Stops
- at the first error, removing every temporary file not yet put in place.
- log is given a tag and a message for each thing done. Returns 0, or 1 on
- an error. The smoke language needs nothing from its guard's constraints to execute.
- """
- opened = {} # symbol index -> {dirfd ,fd ,tmp ,argument}
- try:
- for index ,instruction in enumerate(data["instruction_list"]):
- name ,argument = instruction["name"] ,instruction["argument"]
- if name == "open":
- dirfd = os.open(argument["directory"] ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
- tmp = f".{argument['name']}.Man_In_Grey.{os.getpid()}.{index}"
- fd = os.open(tmp ,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW ,0o600 ,dir_fd=dirfd)
- opened[index] = {"dirfd": dirfd ,"fd": fd ,"tmp": tmp ,"argument": argument}
- elif name == "write":
- fd ,view = opened[argument["file"]["symbol"]]["fd"] ,memoryview(argument["content"])
- while view:
- view = view[os.write(fd ,view):]
- elif name == "unlink":
- dirfd = os.open(argument["directory"] ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
- try:
- if not _regular_or_absent(dirfd ,argument["name"]):
- os.unlink(argument["name"] ,dir_fd=dirfd)
- os.fsync(dirfd)
- log("unlinked" ,f"{argument['directory']}/{argument['name']}")
- finally:
- os.close(dirfd)
-
- for index in sorted(opened):
- record = opened[index]
- argument ,dirfd ,fd ,tmp = record["argument"] ,record["dirfd"] ,record["fd"] ,record["tmp"]
- target = argument["name"]
- where = f"{argument['directory']}/{target}"
- account = pwd.getpwnam(argument["owner"])
- os.fsync(fd)
- # Only root can give a file away. Any other interpreter account makes
- # files it already owns, so the owner is changed only when it differs.
- st = os.fstat(fd)
- if (st.st_uid ,st.st_gid) != (account.pw_uid ,account.pw_gid):
- try:
- os.fchown(fd ,account.pw_uid ,account.pw_gid)
- except OSError as error:
- raise OSError(error.errno ,f"{where}: cannot give it the owner {argument['owner']} and that account's group: {error.strerror}")
- os.fchmod(fd ,argument["permission"])
- os.fsync(fd)
- os.close(fd); record["fd"] = None
- if argument["mode"] == "create":
- os.link(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd ,follow_symlinks=False)
- os.unlink(tmp ,dir_fd=dirfd)
- log("created" ,where)
- else:
- kept = None
- if argument["mode"] == "replace_keep" and not _regular_or_absent(dirfd ,target):
- kept = _backup_name(dirfd ,target)
- os.link(target ,kept ,src_dir_fd=dirfd ,dst_dir_fd=dirfd ,follow_symlinks=False)
- os.rename(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd)
- log("replaced" ,where + (f" (kept as {kept})" if kept else ""))
- record["tmp"] = None
- os.fsync(dirfd)
- return 0
- except OSError as error:
- log("execution_failed" ,str(error))
- return 1
- finally:
- for record in opened.values():
- if record.get("fd") is not None:
- os.close(record["fd"])
- if record.get("tmp") is not None:
- try:
- os.unlink(record["tmp"] ,dir_fd=record["dirfd"])
- except OSError:
- pass
- os.close(record["dirfd"])
diff --git "a/developer/authored/Man_In_Grey.copy/Smoke\302\267Guard.py" "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Guard.py"
new file mode 100644
index 0000000..2b9551d
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Guard.py"
@@ -0,0 +1,157 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Smoke·Guard - the guard of the smoke instruction language.
+
+Started by the gate Man_In_Grey_gate·smoke, with the gate's real and
+effective ids, and with the journal on standard input. It checks the
+journal: first with the well-formed guard, against the language definition
+in Smoke·Language.py, then with the language's own checks in
+Smoke·GuardCheck.py, the constraints among them. It says what it found on
+standard error, one line per finding, as : ; the first two
+lines are name: and version:.
+
+When every check passes, it writes one line to standard output,
+
+ guard smoke check passed
+
+and exits 0. That line and that status together are the only verdict the
+gate admits a journal on.
+
+Its constraints are the predicates at the top of this file. The
+interpreter imports this file for what execution needs from them.
+
+Before it imports anything, it checks that its modules and their directory
+are owned by root or by the effective uid's account, and writable by no
+other account. A module runs code when it is imported.
+
+Usage: Smoke·Guard
+ help | -h print this message (also printed when given no arguments)
+ check check the journal on standard input
+
+Exit status: 0 when every check passed, 1 when a check refused the journal,
+2 when the journal could not be checked.
+"""
+
+# ------------------------------------------------------------------------------
+# The constraints: the predicates this guard checks a journal with. They are
+# written by whoever installs the gate, between the two marks below, and
+# install_gate can put them in from a file and keeps them when it installs
+# the guard again. A predicate allows only by answering True. As promoted
+# with Man_In_Grey, is_allowed_account refuses every account.
+#
+# They are called only after the modules below are checked and imported, so
+# they can call Guard·LibraryConstraint, the library of predicates that recur,
+# as Guard·LibraryConstraint.is_group_member(user_name ,"bio").
+
+# constraints begin
+
+def is_allowed_account(user_name):
+ "True when the account, the gate's real uid, can give journals to the gate"
+ return False
+
+def is_allowed_open(directory ,name ,mode ,owner ,permission):
+ "True when the file can be opened in this mode, to be given this owner and permission"
+ return False
+
+def is_allowed_unlink(directory ,name):
+ "True when the file can be removed"
+ return False
+
+# constraints end
+# ------------------------------------------------------------------------------
+
+
+import sys ,os
+sys.dont_write_bytecode = True
+
+import stat
+from pathlib import Path
+
+LANGUAGE = "smoke"
+VERSION = "4"
+
+HERE_DPATH = Path(__file__).resolve().parent
+MODULE = ("Language.py" ,"Journal.py" ,"Guard·WellFormed.py" ,"Guard.py" ,"Guard·LibraryConstraint.py" ,"Smoke·Language.py" ,"Smoke·GuardCheck.py" ,"Smoke·Guard.py")
+
+
+def trusted_owner_problem(path):
+ "None when path is owned by root or the effective uid's account and writable by no other account"
+ try:
+ st = os.lstat(path)
+ except OSError as error:
+ return f"{path}: {error.strerror}"
+ if st.st_uid not in (0 ,os.geteuid()):
+ return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
+ if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
+ return f"{path} is writable by group or other accounts"
+ return None
+
+def say(tag ,message):
+ print(f"{tag}: {message}" ,file=sys.stderr)
+
+if __name__ == "__main__" and sys.argv[1:] == ["check"]:
+ say("name" ,LANGUAGE)
+ say("version" ,VERSION)
+
+for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
+ problem = trusted_owner_problem(each)
+ if problem:
+ say("refused_to_start" ,problem)
+ sys.exit(2)
+
+try:
+ import Language
+ import Journal
+ import Guard
+ import Guard·LibraryConstraint # for the constraints at the top, which call it by name
+ import Smoke·Language as language_module
+ import Smoke·GuardCheck as guard_check
+except ImportError as error:
+ say("cannot_start" ,str(error))
+ sys.exit(2)
+
+
+def report(tag ,message):
+ say(tag ,Journal.clean(message))
+
+def check():
+ report("constraints" ,str(Path(__file__).resolve()))
+ try:
+ gate_invoking_account = Guard.gate_invoking_account()
+ except RuntimeError as error:
+ report("cannot_check" ,str(error))
+ return 2
+ try:
+ language_definition = Language.Definition(language_module.NAME ,language_module.DEFINITION)
+ except Language.DefinitionError as error:
+ report("cannot_check" ,str(error))
+ return 2
+ status = Guard.check(sys.stdin.buffer.read() ,language_definition ,guard_check ,sys.modules[__name__] ,gate_invoking_account ,report)
+ if status == 0:
+ sys.stdout.write(f"guard {LANGUAGE} check passed\n")
+ return status
+
+
+# --------------------------------------------------------------------------------
+# command line
+
+def print_help():
+ print(__doc__.strip())
+ return 0
+
+def CLI(argv=None):
+ argv = sys.argv[1:] if argv is None else argv
+ if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
+ if argv != ["check"]:
+ say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run Smoke·Guard with no arguments for its usage")
+ return 2
+ return check()
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(CLI())
+ except Exception as error:
+ say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
+ sys.exit(2)
diff --git "a/developer/authored/Man_In_Grey.copy/Smoke\302\267GuardCheck.py" "b/developer/authored/Man_In_Grey.copy/Smoke\302\267GuardCheck.py"
new file mode 100644
index 0000000..48a2cb4
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Smoke\302\267GuardCheck.py"
@@ -0,0 +1,168 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Smoke·GuardCheck - the guard's checks of the smoke instruction language, the tester's smoke test.
+
+It exists to exercise the gate, the guard library and the interpreter
+library, and is not for use on a system: its constraints can say where a
+file goes, and with what mode, owner and permission, but not what the file
+says, and a configuration file's content is what its reader does. It is a
+constrained form of the POSIX file interface. The instructions and their
+types are in Smoke·Language.py.
+
+The modes of open:
+
+ create the file must not exist
+ replace an existing file is discarded
+ replace_keep an existing file is kept as .YYYYMMDDTHHMMSSZ
+
+There is no close. Each opened file is written to a temporary file in its
+directory, and after the journal's last instruction every opened file is
+put in place, each in one step.
+
+This module holds the guard's checks of the language, made after the
+well-formed guard has checked the journal against the definition. They are
+LIST_CHECK:
+
+ language the values of the literals: absolute directories in normal
+ form, bare file names, a known mode, an account name, a
+ permission from 0 to 0o7777
+ constraint the predicates at the top of the guard, Smoke·Guard.py:
+ is_allowed_account, is_allowed_open and is_allowed_unlink
+ validity every owner is an account
+ filesystem each directory, reached through no symbolic link, and each
+ file, named by one instruction only, regular or absent
+
+A file variable can only hold what an open wrote there, since a handle is
+never a literal; so the well-formed guard has already ensured every write
+writes a file some open opened.
+
+The implementation of the instructions is in Smoke·LibraryInstruction.py.
+"""
+
+import os ,pwd ,stat
+
+from Journal import has_control ,clean
+
+MODE = ("create" ,"replace" ,"replace_keep")
+
+
+# --------------------------------------------------------------------------------
+# value checks
+
+def directory_problem(value):
+ if not value.startswith("/"):
+ return f"directory {clean(repr(value))} is not an absolute path"
+ if has_control(value):
+ return f"directory {clean(value)} holds a control character"
+ if os.path.normpath(value) != value:
+ return f"directory {value!r} is not in normal form; give {os.path.normpath(value)!r}"
+ return None
+
+def file_name_problem(value):
+ if not value or "/" in value or value in ("." ,".."):
+ return f"file name {clean(repr(value))} is not a bare file name"
+ if has_control(value):
+ return f"file name {clean(value)} holds a control character"
+ return None
+
+def account_name_problem(value):
+ if not value or has_control(value):
+ return f"owner {clean(repr(value))} is not an account name"
+ return None
+
+
+# --------------------------------------------------------------------------------
+# the guard's checks, each given the instructions as Journal.InstructionView
+
+def literal(view):
+ "the values of an instruction's arguments, None for a variable"
+ return [argument.value for argument in view.list_argument]
+
+def check_language(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ list_problem = []
+ for view in list_instruction_view:
+ name ,value = view.operation ,literal(view)
+ if name in ("open" ,"unlink"):
+ for problem in (directory_problem(value[0]) ,file_name_problem(value[1])):
+ if problem: list_problem.append(f"@{view.index} {name}: {problem}")
+ if name == "open":
+ if value[2] not in MODE:
+ list_problem.append(f"@{view.index} open: mode {clean(repr(value[2]))} is not one of {', '.join(MODE)}")
+ problem = account_name_problem(value[3])
+ if problem: list_problem.append(f"@{view.index} open: {problem}")
+ if not 0 <= value[4] <= 0o7777:
+ list_problem.append(f"@{view.index} open: permission {value[4]} is not from 0 to 0o7777")
+ return list_problem
+
+def check_constraint(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ "constraint is the guard, which holds the predicates; a predicate allows only by answering True"
+ if constraint.is_allowed_account(gate_invoking_account) is not True:
+ return [f"gate invoking account {clean(repr(gate_invoking_account))} is not allowed to give journals to this gate"]
+ list_problem = []
+ for view in list_instruction_view:
+ name ,value = view.operation ,literal(view)
+ if name == "open":
+ directory ,file_name ,mode ,owner ,permission = value[:5]
+ if constraint.is_allowed_open(directory ,file_name ,mode ,owner ,permission) is not True:
+ list_problem.append(f"@{view.index} open: {directory}/{file_name} in mode {mode}, owner {owner}, permission {permission:04o} is not allowed")
+ elif name == "unlink":
+ directory ,file_name = value
+ if constraint.is_allowed_unlink(directory ,file_name) is not True:
+ list_problem.append(f"@{view.index} unlink: {directory}/{file_name} is not allowed to be removed")
+ return list_problem
+
+def check_validity(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ list_problem = []
+ for view in list_instruction_view:
+ if view.operation == "open":
+ owner = literal(view)[3]
+ try:
+ pwd.getpwnam(owner)
+ except KeyError:
+ list_problem.append(f"@{view.index} open: no account is named {clean(owner)}")
+ return list_problem
+
+def is_absent(dirfd ,name):
+ "True when absent; False when a regular file; raises OSError otherwise"
+ try:
+ st = os.lstat(name ,dir_fd=dirfd)
+ except FileNotFoundError:
+ return True
+ if not stat.S_ISREG(st.st_mode):
+ raise OSError("it exists and is not a regular file")
+ return False
+
+def check_filesystem(list_instruction_view ,data ,constraint ,gate_invoking_account):
+ list_problem = []
+ seen = set()
+ for view in list_instruction_view:
+ name ,value = view.operation ,literal(view)
+ if name not in ("open" ,"unlink"):
+ continue
+ directory ,file_name = value[0] ,value[1]
+ where = f"{directory}/{file_name}"
+ if (directory ,file_name) in seen:
+ list_problem.append(f"@{view.index} {name}: {where} is named by more than one instruction"); continue
+ seen.add((directory ,file_name))
+ if os.path.realpath(directory) != directory:
+ list_problem.append(f"@{view.index} {name}: {directory} passes through a symbolic link"); continue
+ try:
+ dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ except OSError as error:
+ list_problem.append(f"@{view.index} {name}: cannot open directory {directory} ({error.strerror})"); continue
+ try:
+ absent = is_absent(dirfd ,file_name)
+ if name == "open" and value[2] == "create" and not absent:
+ list_problem.append(f"@{view.index} open: {where} exists, and the mode is create")
+ except OSError as error:
+ list_problem.append(f"@{view.index} {name}: {where}: {error}")
+ finally:
+ os.close(dirfd)
+ return list_problem
+
+LIST_CHECK = (
+ ("language" ,check_language)
+ ,("constraint" ,check_constraint)
+ ,("validity" ,check_validity)
+ ,("filesystem" ,check_filesystem)
+)
diff --git "a/developer/authored/Man_In_Grey.copy/Smoke\302\267Interpreter.py" "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Interpreter.py"
new file mode 100644
index 0000000..b1ef4b9
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Interpreter.py"
@@ -0,0 +1,141 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Smoke·Interpreter - the interpreter of the smoke instruction language.
+
+Started by the gate Man_In_Grey_gate·smoke, with the gate's real and
+effective ids, and with the journal on standard input, once the guard has
+passed the journal. It executes the journal's instructions, and writes to
+standard error, as : , a line for each lasting change it
+makes, such as a file put in place, and none for an instruction as such.
+The gate logs the journal's course itself.
+
+When it has played the whole journal, it writes the journal's result, when
+the journal declares one, to descriptor 3, which the gate gives it: a CBOR
+map of the result's type name and value. Then it writes one line to
+standard output,
+
+ interpreter smoke journal played
+
+and exits 0. The gate takes the journal as played on that line and that
+status together, and on nothing else.
+
+It checks again that the journal is well formed before executing it. It
+does not check the constraints; that is the guard's work, and the gate
+starts the interpreter only on the guard's verdict. It imports the guard,
+Smoke·Guard.py, for what execution needs from the constraints at its top.
+
+Before it imports anything, it checks that its modules and their directory
+are owned by root or by the effective uid's account, and writable by no
+other account.
+
+Usage: Smoke·Interpreter
+ help | -h print this message (also printed when given no arguments)
+ execute execute the journal on standard input
+
+Exit status: 0 when the journal was executed, 1 when execution failed,
+2 when the journal could not be read or is not well formed.
+"""
+
+import sys ,os
+sys.dont_write_bytecode = True
+
+import stat
+from pathlib import Path
+
+HERE_DPATH = Path(__file__).resolve().parent
+MODULE = ("Language.py" ,"Journal.py" ,"Guard·WellFormed.py" ,"Guard.py" ,"Interpreter.py" ,"Guard·LibraryConstraint.py" ,"Smoke·Language.py" ,"Smoke·GuardCheck.py" ,"Smoke·LibraryInstruction.py" ,"Smoke·Guard.py" ,"Smoke·Interpreter.py")
+
+
+def trusted_owner_problem(path):
+ "None when path is owned by root or the effective uid's account and writable by no other account"
+ try:
+ st = os.lstat(path)
+ except OSError as error:
+ return f"{path}: {error.strerror}"
+ if st.st_uid not in (0 ,os.geteuid()):
+ return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
+ if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
+ return f"{path} is writable by group or other accounts"
+ return None
+
+def say(tag ,message):
+ print(f"{tag}: {message}" ,file=sys.stderr)
+
+for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
+ problem = trusted_owner_problem(each)
+ if problem:
+ say("refused_to_start" ,problem)
+ sys.exit(2)
+
+try:
+ import Language
+ import Journal
+ import Guard
+ import Interpreter
+ import Smoke·Language as language_module
+ import Smoke·LibraryInstruction as library_instruction
+ import Smoke·Guard as guard_program
+except ImportError as error:
+ say("cannot_start" ,str(error))
+ sys.exit(2)
+
+
+RESULT_FD = 3
+
+def write_result(result):
+ "the encoded result, all of it, on descriptor 3, which is then closed"
+ view = memoryview(result)
+ while view:
+ view = view[os.write(RESULT_FD ,view):]
+ os.close(RESULT_FD)
+
+def log(tag ,message):
+ print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
+
+def execute():
+ try:
+ gate_invoking_account = Guard.gate_invoking_account()
+ except RuntimeError as error:
+ log("cannot_execute" ,str(error))
+ return 2
+ try:
+ language_definition = Language.Definition(language_module.NAME ,language_module.DEFINITION)
+ except Language.DefinitionError as error:
+ log("cannot_execute" ,str(error))
+ return 2
+ status ,result = Interpreter.interpret(sys.stdin.buffer.read() ,language_definition ,library_instruction ,log ,guard_program ,gate_invoking_account)
+ if status != 0:
+ return status
+ if result is not None:
+ try:
+ write_result(result)
+ except OSError as error:
+ log("cannot_return_result" ,f"descriptor {RESULT_FD}: {error.strerror}")
+ return 2
+ sys.stdout.write("interpreter smoke journal played\n")
+ return 0
+
+
+# --------------------------------------------------------------------------------
+# command line
+
+def print_help():
+ print(__doc__.strip())
+ return 0
+
+def CLI(argv=None):
+ argv = sys.argv[1:] if argv is None else argv
+ if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
+ if argv != ["execute"]:
+ say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run Smoke·Interpreter with no arguments for its usage")
+ return 2
+ return execute()
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(CLI())
+ except Exception as error:
+ say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
+ sys.exit(2)
diff --git "a/developer/authored/Man_In_Grey.copy/Smoke\302\267Language.py" "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Language.py"
new file mode 100644
index 0000000..b8be754
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Smoke\302\267Language.py"
@@ -0,0 +1,47 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Smoke·Language - the language definition of smoke, the tester's smoke test.
+
+Data only; see Language.py for its form. A constrained form of the POSIX
+file interface, made to exercise every part of the journal's form: a handle
+an instruction produces, an argument both read and written, and a result.
+
+ open(directory ,file_name ,mode ,account_name ,permission ,>file)
+ opens a file to be put in place after the last
+ instruction, and writes its handle to the variable file
+ write(<>file ,content)
+ appends content to the file; the file is read and
+ written, as the instruction changes it
+ unlink(directory ,file_name)
+ removes a file
+ summary(>summary) writes the counts of the files opened and removed so
+ far, which a journal can make its result
+"""
+
+NAME = "smoke"
+
+DEFINITION = {
+ "list_type": [
+ ["directory" ,"text"]
+ ,["file_name" ,"text"]
+ ,["mode" ,"text"]
+ ,["account_name" ,"text"]
+ ,["permission" ,"integer"]
+ ,["file" ,"handle"]
+ ,["content" ,"bytes"]
+ ,["summary" ,["dictionary" ,["open_count" ,"integer"] ,["unlink_count" ,"integer"]]]
+ ]
+ ,"list_format": [
+ ["format_open" ,["in" ,"directory"] ,["in" ,"file_name"] ,["in" ,"mode"] ,["in" ,"account_name"] ,["in" ,"permission"] ,["out" ,"file"]]
+ ,["format_write" ,["in-out" ,"file"] ,["in" ,"content"]]
+ ,["format_unlink" ,["in" ,"directory"] ,["in" ,"file_name"]]
+ ,["format_summary" ,["out" ,"summary"]]
+ ]
+ ,"list_result": ["summary"]
+ ,"map_instruction": {
+ "open": "format_open"
+ ,"write": "format_write"
+ ,"unlink": "format_unlink"
+ ,"summary": "format_summary"
+ }
+}
diff --git "a/developer/authored/Man_In_Grey.copy/Smoke\302\267LibraryInstruction.py" "b/developer/authored/Man_In_Grey.copy/Smoke\302\267LibraryInstruction.py"
new file mode 100644
index 0000000..d5014e5
--- /dev/null
+++ "b/developer/authored/Man_In_Grey.copy/Smoke\302\267LibraryInstruction.py"
@@ -0,0 +1,115 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+Smoke·LibraryInstruction - the implementation of the smoke instructions.
+
+The interpreter makes one LibraryInstruction for each journal and calls its
+functions, one for each operation specifier of the definition,
+Smoke·Language.py, given each instruction's arguments by position.
+Each opened file is written to a temporary file in its directory, and
+finish() puts every opened file in place, each in one step.
+"""
+
+import os ,pwd ,time
+
+from Smoke·GuardCheck import is_absent
+
+
+# --------------------------------------------------------------------------------
+# the interpreter's side
+
+def _backup_name(dirfd ,name):
+ stamp = time.strftime("%Y%m%dT%H%M%SZ" ,time.gmtime())
+ candidate ,n = f"{name}.{stamp}" ,1
+ while True:
+ try:
+ os.lstat(candidate ,dir_fd=dirfd)
+ except FileNotFoundError:
+ return candidate
+ candidate ,n = f"{name}.{stamp}.{n}" ,n + 1
+
+class LibraryInstruction:
+ """
+ The instructions of one journal. open makes a temporary file in its directory and
+ writes the file's handle, a number standing for the open file, to its box;
+ write appends to the file the handle stands for; finish() puts every opened
+ file in place, each in one step; release() closes what is open and removes
+ every temporary file not put in place. The smoke language needs nothing
+ from its guard's constraints to execute.
+ """
+
+ def __init__(self ,constraint ,gate_invoking_account ,log):
+ self._log = log
+ self._opened = [] # by handle: {dirfd ,fd ,tmp ,directory ,name ,mode ,owner ,permission}
+ self._unlink_count = 0
+
+ def open(self ,directory ,file_name ,mode ,account_name ,permission ,file):
+ handle = len(self._opened)
+ dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ record = {"dirfd": dirfd ,"fd": None ,"tmp": None ,"directory": directory ,"name": file_name
+ ,"mode": mode ,"owner": account_name ,"permission": permission}
+ self._opened.append(record)
+ tmp = f".{file_name}.Man_In_Grey.{os.getpid()}.{handle}"
+ record["fd"] = os.open(tmp ,os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW ,0o600 ,dir_fd=dirfd)
+ record["tmp"] = tmp
+ file.append(handle)
+
+ def write(self ,file ,content):
+ fd ,view = self._opened[file[0]]["fd"] ,memoryview(content)
+ while view:
+ view = view[os.write(fd ,view):]
+
+ def unlink(self ,directory ,file_name):
+ dirfd = os.open(directory ,os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ try:
+ if not is_absent(dirfd ,file_name):
+ os.unlink(file_name ,dir_fd=dirfd)
+ os.fsync(dirfd)
+ self._log("unlinked" ,f"{directory}/{file_name}")
+ finally:
+ os.close(dirfd)
+ self._unlink_count += 1
+
+ def summary(self ,summary):
+ summary.append({"open_count": len(self._opened) ,"unlink_count": self._unlink_count})
+
+ def finish(self):
+ for record in self._opened:
+ dirfd ,fd ,tmp ,target = record["dirfd"] ,record["fd"] ,record["tmp"] ,record["name"]
+ where = f"{record['directory']}/{target}"
+ account = pwd.getpwnam(record["owner"])
+ os.fsync(fd)
+ # Only root can give a file away. Any other interpreter account makes
+ # files it already owns, so the owner is changed only when it differs.
+ st = os.fstat(fd)
+ if (st.st_uid ,st.st_gid) != (account.pw_uid ,account.pw_gid):
+ try:
+ os.fchown(fd ,account.pw_uid ,account.pw_gid)
+ except OSError as error:
+ raise OSError(error.errno ,f"{where}: cannot give it the owner {record['owner']} and that account's group: {error.strerror}")
+ os.fchmod(fd ,record["permission"])
+ os.fsync(fd)
+ os.close(fd); record["fd"] = None
+ if record["mode"] == "create":
+ os.link(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd ,follow_symlinks=False)
+ os.unlink(tmp ,dir_fd=dirfd)
+ self._log("created" ,where)
+ else:
+ kept = None
+ if record["mode"] == "replace_keep" and not is_absent(dirfd ,target):
+ kept = _backup_name(dirfd ,target)
+ os.link(target ,kept ,src_dir_fd=dirfd ,dst_dir_fd=dirfd ,follow_symlinks=False)
+ os.rename(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd)
+ self._log("replaced" ,where + (f" (kept as {kept})" if kept else ""))
+ record["tmp"] = None
+ os.fsync(dirfd)
+
+ def release(self):
+ for record in self._opened:
+ if record["fd"] is not None:
+ os.close(record["fd"])
+ if record["tmp"] is not None:
+ try:
+ os.unlink(record["tmp"] ,dir_fd=record["dirfd"])
+ except OSError:
+ pass
+ os.close(record["dirfd"])
diff --git a/developer/authored/Man_In_Grey.copy/guard_emplace_bio.py b/developer/authored/Man_In_Grey.copy/guard_emplace_bio.py
deleted file mode 100644
index 6cf6671..0000000
--- a/developer/authored/Man_In_Grey.copy/guard_emplace_bio.py
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/usr/bin/env -S python3 -B
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-guard_emplace_bio - the guard of the emplace_bio instruction language.
-
-Started by the gate Man_In_Grey_emplace_bio_gate, with the gate's real and
-effective ids, and with the journal on standard input. It checks the
-journal, and says what it found on standard error, one line per finding,
-as : ; the first two lines are name: and version:.
-
-When every check passes, it writes one line to standard output,
-
- guard emplace_bio check passed
-
-and exits 0. That line and that status together are the only verdict the
-gate admits a journal on.
-
-Its constraints are the predicates at the top of this file. The
-interpreter imports this file for what execution needs from them.
-
-Before it imports anything, it checks that its modules and their directory
-are owned by root or by the effective uid's account, and writable by no
-other account. A module runs code when it is imported.
-
-Usage: guard_emplace_bio
- help | -h print this message (also printed when given no arguments)
- check check the journal on standard input
-
-Exit status: 0 when every check passed, 1 when a check refused the journal,
-2 when the journal could not be checked.
-"""
-
-# ------------------------------------------------------------------------------
-# The constraints: the predicates this guard checks a journal with. They are
-# written by whoever installs the gate, between the two marks below, and
-# install_gate can put them in from a file and keeps them when it installs
-# the guard again. A predicate allows only by answering True. As promoted
-# with Man_In_Grey, is_allowed_account refuses every account.
-#
-# They are called only after the modules below are checked and imported, so
-# they can call Constraint, the library of predicates that recur, as
-# Constraint.is_group_member(user_name ,"bio").
-
-# constraints begin
-
-def is_allowed_account(user_name):
- "True when the account, the gate's real uid, can have a page"
- return False
-
-def is_allowed_text_size(byte_count):
- "True when the display name and the text together can be byte_count bytes of UTF-8"
- return byte_count <= 8192
-
-def is_allowed_picture_size(byte_count):
- "True when the picture can be byte_count bytes"
- return byte_count <= 1 << 20
-
-def page_directory():
- "the absolute directory the pages are placed in, owned by the gate owner account"
- return "/srv/http/bio"
-
-# constraints end
-# ------------------------------------------------------------------------------
-
-
-import sys ,os
-sys.dont_write_bytecode = True
-
-import stat
-from pathlib import Path
-
-LANGUAGE = "emplace_bio"
-VERSION = "3"
-
-HERE_DPATH = Path(__file__).resolve().parent
-MODULE = ("Journal.py" ,"Guard.py" ,"Constraint.py" ,"EmplaceBioLanguage.py" ,"guard_emplace_bio.py")
-
-
-def trusted_owner_problem(path):
- "None when path is owned by root or the effective uid's account and writable by no other account"
- try:
- st = os.lstat(path)
- except OSError as error:
- return f"{path}: {error.strerror}"
- if st.st_uid not in (0 ,os.geteuid()):
- return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
- if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
- return f"{path} is writable by group or other accounts"
- return None
-
-def say(tag ,message):
- print(f"{tag}: {message}" ,file=sys.stderr)
-
-if __name__ == "__main__" and sys.argv[1:] == ["check"]:
- say("name" ,LANGUAGE)
- say("version" ,VERSION)
-
-for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
- problem = trusted_owner_problem(each)
- if problem:
- say("refused_to_start" ,problem)
- sys.exit(2)
-
-try:
- import Journal
- import Guard
- import Constraint
- import EmplaceBioLanguage as Language
-except ImportError as error:
- say("cannot_start" ,str(error))
- sys.exit(2)
-
-
-def report(tag ,message):
- say(tag ,Journal.clean(message))
-
-def check():
- report("constraints" ,str(Path(__file__).resolve()))
- try:
- gate_invoking_account = Guard.gate_invoking_account()
- except RuntimeError as error:
- report("cannot_check" ,str(error))
- return 2
- status = Guard.check(sys.stdin.buffer.read() ,Language ,sys.modules[__name__] ,gate_invoking_account ,report)
- if status == 0:
- sys.stdout.write(f"guard {LANGUAGE} check passed\n")
- return status
-
-
-# --------------------------------------------------------------------------------
-# command line
-
-def print_help():
- print(__doc__.strip())
- return 0
-
-def CLI(argv=None):
- argv = sys.argv[1:] if argv is None else argv
- if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
- if argv != ["check"]:
- say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run guard_emplace_bio with no arguments for its usage")
- return 2
- return check()
-
-
-if __name__ == "__main__":
- try:
- sys.exit(CLI())
- except Exception as error:
- say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
- sys.exit(2)
diff --git a/developer/authored/Man_In_Grey.copy/guard_smoke.py b/developer/authored/Man_In_Grey.copy/guard_smoke.py
deleted file mode 100644
index 092ab59..0000000
--- a/developer/authored/Man_In_Grey.copy/guard_smoke.py
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env -S python3 -B
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-guard_smoke - the guard of the smoke instruction language.
-
-Started by the gate Man_In_Grey_smoke_gate, with the gate's real and
-effective ids, and with the journal on standard input. It checks the
-journal, and says what it found on standard error, one line per finding,
-as : ; the first two lines are name: and version:.
-
-When every check passes, it writes one line to standard output,
-
- guard smoke check passed
-
-and exits 0. That line and that status together are the only verdict the
-gate admits a journal on.
-
-Its constraints are the predicates at the top of this file. The
-interpreter imports this file for what execution needs from them.
-
-Before it imports anything, it checks that its modules and their directory
-are owned by root or by the effective uid's account, and writable by no
-other account. A module runs code when it is imported.
-
-Usage: guard_smoke
- help | -h print this message (also printed when given no arguments)
- check check the journal on standard input
-
-Exit status: 0 when every check passed, 1 when a check refused the journal,
-2 when the journal could not be checked.
-"""
-
-# ------------------------------------------------------------------------------
-# The constraints: the predicates this guard checks a journal with. They are
-# written by whoever installs the gate, between the two marks below, and
-# install_gate can put them in from a file and keeps them when it installs
-# the guard again. A predicate allows only by answering True. As promoted
-# with Man_In_Grey, is_allowed_account refuses every account.
-#
-# They are called only after the modules below are checked and imported, so
-# they can call Constraint, the library of predicates that recur, as
-# Constraint.is_group_member(user_name ,"bio").
-
-# constraints begin
-
-def is_allowed_account(user_name):
- "True when the account, the gate's real uid, can give journals to the gate"
- return False
-
-def is_allowed_open(directory ,name ,mode ,owner ,permission):
- "True when the file can be opened in this mode, to be given this owner and permission"
- return False
-
-def is_allowed_unlink(directory ,name):
- "True when the file can be removed"
- return False
-
-# constraints end
-# ------------------------------------------------------------------------------
-
-
-import sys ,os
-sys.dont_write_bytecode = True
-
-import stat
-from pathlib import Path
-
-LANGUAGE = "smoke"
-VERSION = "3"
-
-HERE_DPATH = Path(__file__).resolve().parent
-MODULE = ("Journal.py" ,"Guard.py" ,"Constraint.py" ,"SmokeLanguage.py" ,"guard_smoke.py")
-
-
-def trusted_owner_problem(path):
- "None when path is owned by root or the effective uid's account and writable by no other account"
- try:
- st = os.lstat(path)
- except OSError as error:
- return f"{path}: {error.strerror}"
- if st.st_uid not in (0 ,os.geteuid()):
- return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
- if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
- return f"{path} is writable by group or other accounts"
- return None
-
-def say(tag ,message):
- print(f"{tag}: {message}" ,file=sys.stderr)
-
-if __name__ == "__main__" and sys.argv[1:] == ["check"]:
- say("name" ,LANGUAGE)
- say("version" ,VERSION)
-
-for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
- problem = trusted_owner_problem(each)
- if problem:
- say("refused_to_start" ,problem)
- sys.exit(2)
-
-try:
- import Journal
- import Guard
- import Constraint
- import SmokeLanguage as Language
-except ImportError as error:
- say("cannot_start" ,str(error))
- sys.exit(2)
-
-
-def report(tag ,message):
- say(tag ,Journal.clean(message))
-
-def check():
- report("constraints" ,str(Path(__file__).resolve()))
- try:
- gate_invoking_account = Guard.gate_invoking_account()
- except RuntimeError as error:
- report("cannot_check" ,str(error))
- return 2
- status = Guard.check(sys.stdin.buffer.read() ,Language ,sys.modules[__name__] ,gate_invoking_account ,report)
- if status == 0:
- sys.stdout.write(f"guard {LANGUAGE} check passed\n")
- return status
-
-
-# --------------------------------------------------------------------------------
-# command line
-
-def print_help():
- print(__doc__.strip())
- return 0
-
-def CLI(argv=None):
- argv = sys.argv[1:] if argv is None else argv
- if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
- if argv != ["check"]:
- say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run guard_smoke with no arguments for its usage")
- return 2
- return check()
-
-
-if __name__ == "__main__":
- try:
- sys.exit(CLI())
- except Exception as error:
- say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
- sys.exit(2)
diff --git a/developer/authored/Man_In_Grey.copy/install_gate b/developer/authored/Man_In_Grey.copy/install_gate
index 2a946e3..de1d37a 100755
--- a/developer/authored/Man_In_Grey.copy/install_gate
+++ b/developer/authored/Man_In_Grey.copy/install_gate
@@ -33,11 +33,11 @@ Usage: install_gate ...
help | -h print this message (also printed when given no arguments)
install owner [group ] into [constraints ]
is a gate's file name in Man_In_Grey_gate/, such as
- Man_In_Grey_emplace_bio_gate; is a Python file holding
+ Man_In_Grey_gate·emplace_bio; is a Python file holding
the guard's predicates
Example, run by root from consumer/Man_In_Grey/:
- ./install_gate install Man_In_Grey_emplace_bio_gate owner http into /usr/local/lib/Man_In_Grey constraints bio_constraint.py
+ ./install_gate install Man_In_Grey_gate·emplace_bio owner http into /usr/local/lib/Man_In_Grey constraints bio_constraint.py
Exit status: 0 when the gate was installed, 1 when the installation was
refused, 2 when it was given wrong arguments.
@@ -106,20 +106,20 @@ BEGIN ,END = "# constraints begin" ,"# constraints end"
def section_bound(text ,path):
"the line numbers of the two constraint marks in a guard's text; raises Refused"
- line_list = text.splitlines()
- if line_list.count(BEGIN) != 1 or line_list.count(END) != 1 or line_list.index(BEGIN) > line_list.index(END):
+ list_line = text.splitlines()
+ if list_line.count(BEGIN) != 1 or list_line.count(END) != 1 or list_line.index(BEGIN) > list_line.index(END):
raise Refused(f"{path} does not hold the marks '{BEGIN}' and '{END}', once each and in that order")
- return line_list ,line_list.index(BEGIN) ,line_list.index(END)
+ return list_line ,list_line.index(BEGIN) ,list_line.index(END)
def constraint_of(text ,path):
"the constraints between the marks"
- line_list ,begin ,end = section_bound(text ,path)
- return "\n".join(line_list[begin + 1:end]).strip("\n") + "\n"
+ list_line ,begin ,end = section_bound(text ,path)
+ return "\n".join(list_line[begin + 1:end]).strip("\n") + "\n"
def with_constraint(text ,path ,constraint):
"the guard's text with constraint between the marks"
- line_list ,begin ,end = section_bound(text ,path)
- return "\n".join(line_list[:begin + 1] + ["" ,constraint.strip("\n") ,""] + line_list[end:]) + "\n"
+ list_line ,begin ,end = section_bound(text ,path)
+ return "\n".join(list_line[:begin + 1] + ["" ,constraint.strip("\n") ,""] + list_line[end:]) + "\n"
def install_text(text ,target ,mode):
"write text to target through a temporary file in target's directory, owned by root"
@@ -191,7 +191,7 @@ def install(gate ,owner ,directory ,constraints=None ,group=None):
# the constraints of the guards installed already, to keep
kept = {}
- for installed in sorted(program_dpath.glob("guard_*.py")):
+ for installed in sorted(program_dpath.glob("*·Guard.py")):
if installed.is_file() and not installed.is_symlink():
try:
kept[installed.name] = constraint_of(installed.read_text(encoding="utf-8") ,installed)
diff --git a/developer/authored/Man_In_Grey.copy/interpreter_emplace_bio.py b/developer/authored/Man_In_Grey.copy/interpreter_emplace_bio.py
deleted file mode 100644
index c53735c..0000000
--- a/developer/authored/Man_In_Grey.copy/interpreter_emplace_bio.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env -S python3 -B
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-interpreter_emplace_bio - the interpreter of the emplace_bio instruction language.
-
-Started by the gate Man_In_Grey_emplace_bio_gate, with the gate's real and
-effective ids, and with the journal on standard input, once the guard has
-passed the journal. It executes the journal's instructions, and writes to
-standard error, as : , a line for each lasting change it
-makes, such as a file put in place, and none for an instruction as such.
-The gate logs the journal's course itself.
-
-When it has played the whole journal, it writes one line to standard output,
-
- interpreter emplace_bio journal played
-
-and exits 0. The gate takes the journal as played on that line and that
-status together, and on nothing else.
-
-It checks again that the journal is well formed before executing it. It
-does not check the constraints; that is the guard's work, and the gate
-starts the interpreter only on the guard's verdict. It imports the guard,
-guard_emplace_bio.py, for what execution needs from the constraints at its top.
-
-Before it imports anything, it checks that its modules and their directory
-are owned by root or by the effective uid's account, and writable by no
-other account.
-
-Usage: interpreter_emplace_bio
- help | -h print this message (also printed when given no arguments)
- execute execute the journal on standard input
-
-Exit status: 0 when the journal was executed, 1 when execution failed,
-2 when the journal could not be read or is not well formed.
-"""
-
-import sys ,os
-sys.dont_write_bytecode = True
-
-import stat
-from pathlib import Path
-
-HERE_DPATH = Path(__file__).resolve().parent
-MODULE = ("Journal.py" ,"Guard.py" ,"Interpreter.py" ,"Constraint.py" ,"EmplaceBioLanguage.py" ,"guard_emplace_bio.py" ,"interpreter_emplace_bio.py")
-
-
-def trusted_owner_problem(path):
- "None when path is owned by root or the effective uid's account and writable by no other account"
- try:
- st = os.lstat(path)
- except OSError as error:
- return f"{path}: {error.strerror}"
- if st.st_uid not in (0 ,os.geteuid()):
- return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
- if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
- return f"{path} is writable by group or other accounts"
- return None
-
-def say(tag ,message):
- print(f"{tag}: {message}" ,file=sys.stderr)
-
-for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
- problem = trusted_owner_problem(each)
- if problem:
- say("refused_to_start" ,problem)
- sys.exit(2)
-
-try:
- import Journal
- import Guard
- import Interpreter
- import EmplaceBioLanguage as Language
- import guard_emplace_bio as Guard_program
-except ImportError as error:
- say("cannot_start" ,str(error))
- sys.exit(2)
-
-
-def log(tag ,message):
- print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
-
-def execute():
- try:
- gate_invoking_account = Guard.gate_invoking_account()
- except RuntimeError as error:
- log("cannot_execute" ,str(error))
- return 2
- status = Interpreter.interpret(sys.stdin.buffer.read() ,Language ,log ,Guard_program ,gate_invoking_account)
- if status == 0:
- sys.stdout.write("interpreter emplace_bio journal played\n")
- return status
-
-
-# --------------------------------------------------------------------------------
-# command line
-
-def print_help():
- print(__doc__.strip())
- return 0
-
-def CLI(argv=None):
- argv = sys.argv[1:] if argv is None else argv
- if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
- if argv != ["execute"]:
- say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run interpreter_emplace_bio with no arguments for its usage")
- return 2
- return execute()
-
-
-if __name__ == "__main__":
- try:
- sys.exit(CLI())
- except Exception as error:
- say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
- sys.exit(2)
diff --git a/developer/authored/Man_In_Grey.copy/interpreter_smoke.py b/developer/authored/Man_In_Grey.copy/interpreter_smoke.py
deleted file mode 100644
index 54dd827..0000000
--- a/developer/authored/Man_In_Grey.copy/interpreter_smoke.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env -S python3 -B
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-interpreter_smoke - the interpreter of the smoke instruction language.
-
-Started by the gate Man_In_Grey_smoke_gate, with the gate's real and
-effective ids, and with the journal on standard input, once the guard has
-passed the journal. It executes the journal's instructions, and writes to
-standard error, as : , a line for each lasting change it
-makes, such as a file put in place, and none for an instruction as such.
-The gate logs the journal's course itself.
-
-When it has played the whole journal, it writes one line to standard output,
-
- interpreter smoke journal played
-
-and exits 0. The gate takes the journal as played on that line and that
-status together, and on nothing else.
-
-It checks again that the journal is well formed before executing it. It
-does not check the constraints; that is the guard's work, and the gate
-starts the interpreter only on the guard's verdict. It imports the guard,
-guard_smoke.py, for what execution needs from the constraints at its top.
-
-Before it imports anything, it checks that its modules and their directory
-are owned by root or by the effective uid's account, and writable by no
-other account.
-
-Usage: interpreter_smoke
- help | -h print this message (also printed when given no arguments)
- execute execute the journal on standard input
-
-Exit status: 0 when the journal was executed, 1 when execution failed,
-2 when the journal could not be read or is not well formed.
-"""
-
-import sys ,os
-sys.dont_write_bytecode = True
-
-import stat
-from pathlib import Path
-
-HERE_DPATH = Path(__file__).resolve().parent
-MODULE = ("Journal.py" ,"Guard.py" ,"Interpreter.py" ,"Constraint.py" ,"SmokeLanguage.py" ,"guard_smoke.py" ,"interpreter_smoke.py")
-
-
-def trusted_owner_problem(path):
- "None when path is owned by root or the effective uid's account and writable by no other account"
- try:
- st = os.lstat(path)
- except OSError as error:
- return f"{path}: {error.strerror}"
- if st.st_uid not in (0 ,os.geteuid()):
- return f"{path} is owned by uid {st.st_uid}, neither root nor the effective uid's account"
- if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
- return f"{path} is writable by group or other accounts"
- return None
-
-def say(tag ,message):
- print(f"{tag}: {message}" ,file=sys.stderr)
-
-for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]:
- problem = trusted_owner_problem(each)
- if problem:
- say("refused_to_start" ,problem)
- sys.exit(2)
-
-try:
- import Journal
- import Guard
- import Interpreter
- import SmokeLanguage as Language
- import guard_smoke as Guard_program
-except ImportError as error:
- say("cannot_start" ,str(error))
- sys.exit(2)
-
-
-def log(tag ,message):
- print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
-
-def execute():
- try:
- gate_invoking_account = Guard.gate_invoking_account()
- except RuntimeError as error:
- log("cannot_execute" ,str(error))
- return 2
- status = Interpreter.interpret(sys.stdin.buffer.read() ,Language ,log ,Guard_program ,gate_invoking_account)
- if status == 0:
- sys.stdout.write("interpreter smoke journal played\n")
- return status
-
-
-# --------------------------------------------------------------------------------
-# command line
-
-def print_help():
- print(__doc__.strip())
- return 0
-
-def CLI(argv=None):
- argv = sys.argv[1:] if argv is None else argv
- if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
- if argv != ["execute"]:
- say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run interpreter_smoke with no arguments for its usage")
- return 2
- return execute()
-
-
-if __name__ == "__main__":
- try:
- sys.exit(CLI())
- except Exception as error:
- say("failed" ,Journal.clean(f"{type(error).__name__}: {error}"))
- sys.exit(2)
diff --git a/developer/authored/Man_In_Grey_gate.make/Gate.lib.c b/developer/authored/Man_In_Grey_gate.make/Gate.lib.c
index 649106a..cd5629a 100644
--- a/developer/authored/Man_In_Grey_gate.make/Gate.lib.c
+++ b/developer/authored/Man_In_Grey_gate.make/Gate.lib.c
@@ -17,8 +17,10 @@
0. settles what the invoking program handed it: puts every signal's
disposition back to its default, cancels the interval timers and
clears the blocked mask, so a signal already pending is delivered now;
- opens /dev/null on any of descriptors 0, 1 and 2 left closed, and
- closes every descriptor above 2; and raises each resource limit that
+ opens /dev/null on any of descriptors 0, 1 and 2 left closed, keeps
+ descriptor 3, the result descriptor, when the invoking program left it
+ open and opens /dev/null there when it did not, and closes every
+ descriptor above 3; and raises each resource limit that
could stop the guard or the interpreter partway, refusing when one
stays too low;
1. refuses unless the guard and the interpreter, and every directory
@@ -36,7 +38,14 @@
line to standard output: guard check passed;
6. starts the interpreter with the same journal, and takes the journal
as played only when the interpreter exits 0 having written exactly one
- line to standard output: interpreter journal played.
+ line to standard output: interpreter journal played;
+ 7. forwards the interpreter's result, what it wrote on its descriptor 3,
+ to the gate's own descriptor 3, and only once the journal is taken as
+ played.
+
+ The guard is given no descriptor 3. The interpreter's descriptor 3 is a
+ pipe to the gate, which the gate reads as it reads the interpreter's other
+ output, up to RESULT_MAX bytes, and never examines.
The guard and the interpreter inherit the gate's uids and its ignored
signals, and learn account A from MIG_REAL_UID and MIG_REAL_USER_NAME.
@@ -97,9 +106,12 @@ int Man_In_Grey_gate·Gate·run(
#define OUTPUT_LINE_MAX 4096
#define OUTPUT_LINE_COUNT_MAX 2000
#define VERDICT_MAX 512
+ #define RESULT_MAX ((size_t)16 << 20)
+ #define RESULT_FD 3
Local const char *Language = "";
Local bool Logging = false;
+ Local bool Result_discarded = false; // descriptor 3 was not open, so a result goes to /dev/null
// ------------------------------------------------------------------------------
// saying things: to the person on standard error, and to the system log
@@ -174,13 +186,18 @@ int Man_In_Grey_gate·Gate·run(
}
/* Descriptors 0, 1 and 2 open, on /dev/null when the invoking program left
- one closed, so a pipe the gate makes cannot land on one of them; every
- other descriptor closed, so nothing the invoking program opened reaches
- the guard or the interpreter. */
+ one closed, so a pipe the gate makes cannot land on one of them.
+ Descriptor 3 is the result descriptor: the invoking program opens it to
+ receive the interpreter's result. When it is open the gate keeps it,
+ marked close-on-exec so that neither the guard nor the interpreter
+ inherits it; when it is not, the gate opens /dev/null there, and a
+ result is discarded. Every other descriptor is closed, so nothing the
+ invoking program opened reaches the guard or the interpreter. */
Local void settle_descriptors(void){
- for(int fd = 0; fd <= 2; ++fd){
+ for(int fd = 0; fd <= RESULT_FD; ++fd){
if( fcntl(fd ,F_GETFD) >= 0 ) continue;
- int null = open("/dev/null" ,O_RDWR);
+ if( fd == RESULT_FD ) Result_discarded = true;
+ int null = open("/dev/null" ,fd == RESULT_FD ? O_WRONLY : O_RDWR);
if( null < 0 ) die("cannot open /dev/null: %s" ,strerror(errno));
if( null != fd ){
if( dup2(null ,fd) < 0 ) die("cannot open descriptor %d: %s" ,fd ,strerror(errno));
@@ -197,7 +214,7 @@ int Man_In_Grey_gate·Gate·run(
while( (entry = readdir(dir)) ){
char *end;
long fd = strtol(entry->d_name ,&end ,10);
- if( *end || end == entry->d_name || fd <= 2 || fd == own ) continue;
+ if( *end || end == entry->d_name || fd <= RESULT_FD || fd == own ) continue;
if( count == size ){
size *= 2;
int *larger = realloc(list ,size * sizeof *list);
@@ -209,6 +226,9 @@ int Man_In_Grey_gate·Gate·run(
closedir(dir);
for(size_t i = 0; i < count; ++i) close(list[i]);
free(list);
+ int flags = fcntl(RESULT_FD ,F_GETFD);
+ if( flags < 0 || fcntl(RESULT_FD ,F_SETFD ,flags | FD_CLOEXEC) < 0 )
+ die("cannot set close-on-exec on descriptor %d: %s" ,RESULT_FD ,strerror(errno));
}
typedef struct{
@@ -316,8 +336,12 @@ int Man_In_Grey_gate·Gate·run(
typedef struct{
const char *source; // "guard" or "interpreter"
bool stdout_is_verdict; // standard output is kept as the verdict, not shown
+ bool has_result; // given a descriptor 3, whose bytes are kept as its result
char verdict[VERDICT_MAX];
size_t verdict_length;
+ unsigned char *result;
+ size_t result_length;
+ size_t result_size;
size_t line_count;
const char *broken; // the rule broken, or NULL
} Child;
@@ -354,6 +378,20 @@ int Man_In_Grey_gate·Gate·run(
child->verdict[child->verdict_length] = '\0';
}
+ Local void take_result(Child *child ,const char *bytes ,size_t n){
+ if( child->result_length + n > RESULT_MAX ){ child->broken = "a result larger than the gate accepts"; return; }
+ if( child->result_length + n > child->result_size ){
+ size_t size = child->result_size ? child->result_size : 1 << 12;
+ while( size < child->result_length + n ) size *= 2;
+ unsigned char *larger = realloc(child->result ,size);
+ if( !larger ) die("out of memory");
+ child->result = larger;
+ child->result_size = size;
+ }
+ memcpy(child->result + child->result_length ,bytes ,n);
+ child->result_length += n;
+ }
+
Local void close_on_exec(int fd){
int flags = fcntl(fd ,F_GETFD);
if( flags < 0 || fcntl(fd ,F_SETFD ,flags | FD_CLOEXEC) < 0 ) die("cannot set close-on-exec: %s" ,strerror(errno));
@@ -363,31 +401,39 @@ int Man_In_Grey_gate·Gate·run(
and read its standard output and standard error by the gate's rules.
Returns the wait status. */
Local int run_child(Child *child ,char *const argument[] ,const unsigned char *input ,size_t input_length){
- int in[2] ,out[2] ,err[2];
+ int in[2] ,out[2] ,err[2] ,res[2] = {-1 ,-1};
if( pipe(in) || pipe(out) || pipe(err) ) die("cannot make a pipe: %s" ,strerror(errno));
- for(int k = 0; k < 2; ++k){ close_on_exec(in[k]); close_on_exec(out[k]); close_on_exec(err[k]); }
+ if( child->has_result && pipe(res) ) die("cannot make a pipe: %s" ,strerror(errno));
+ for(int k = 0; k < 2; ++k){
+ close_on_exec(in[k]); close_on_exec(out[k]); close_on_exec(err[k]);
+ if( child->has_result ) close_on_exec(res[k]);
+ }
pid_t pid = fork();
if( pid < 0 ) die("cannot start the %s: %s" ,child->source ,strerror(errno));
if( pid == 0 ){
if( dup2(in[0] ,0) < 0 || dup2(out[1] ,1) < 0 || dup2(err[1] ,2) < 0 ) _exit(127);
+ if( child->has_result && dup2(res[1] ,RESULT_FD) < 0 ) _exit(127);
execv(PYTHON ,argument);
dprintf(2 ,"cannot_start: %s: %s\n" ,PYTHON ,strerror(errno));
_exit(127);
}
close(in[0]); close(out[1]); close(err[1]);
+ if( child->has_result ) close(res[1]);
fcntl(in[1] ,F_SETFL ,fcntl(in[1] ,F_GETFL) | O_NONBLOCK);
Stream out_stream = {.fd = out[0] ,.echo = stdout ,.stream = "stdout"};
Stream err_stream = {.fd = err[0] ,.echo = stderr ,.stream = "stderr"};
int in_fd = in[1];
+ int res_fd = child->has_result ? res[0] : -1;
size_t written = 0;
if( input_length == 0 ){ close(in_fd); in_fd = -1; }
- while( (out_stream.fd >= 0 || err_stream.fd >= 0) && !child->broken ){
- struct pollfd poll_list[3];
- int count = 0 ,in_at = -1 ,out_at = -1 ,err_at = -1;
+ while( (out_stream.fd >= 0 || err_stream.fd >= 0 || res_fd >= 0) && !child->broken ){
+ struct pollfd poll_list[4];
+ int count = 0 ,in_at = -1 ,out_at = -1 ,err_at = -1 ,res_at = -1;
if( in_fd >= 0 ){ poll_list[count] = (struct pollfd){.fd = in_fd ,.events = POLLOUT}; in_at = count++; }
+ if( res_fd >= 0 ){ poll_list[count] = (struct pollfd){.fd = res_fd ,.events = POLLIN}; res_at = count++; }
if( out_stream.fd >= 0 ){ poll_list[count] = (struct pollfd){.fd = out_stream.fd ,.events = POLLIN}; out_at = count++; }
if( err_stream.fd >= 0 ){ poll_list[count] = (struct pollfd){.fd = err_stream.fd ,.events = POLLIN}; err_at = count++; }
if( poll(poll_list ,count ,-1) < 0 ){
@@ -399,6 +445,12 @@ int Man_In_Grey_gate·Gate·run(
if( n > 0 ) written += (size_t)n;
if( (n < 0 && errno != EAGAIN && errno != EINTR) || written == input_length ){ close(in_fd); in_fd = -1; }
}
+ if( res_at >= 0 && poll_list[res_at].revents ){
+ char buffer[4096];
+ ssize_t n = read(res_fd ,buffer ,sizeof buffer);
+ if( n > 0 ) take_result(child ,buffer ,(size_t)n);
+ else if( n == 0 || (errno != EAGAIN && errno != EINTR) ){ close(res_fd); res_fd = -1; }
+ }
Stream *stream_list[2] = {&out_stream ,&err_stream};
int at_list[2] = {out_at ,err_at};
for(int k = 0; k < 2; ++k){
@@ -419,6 +471,7 @@ int Man_In_Grey_gate·Gate·run(
if( child->broken ) kill(pid ,SIGKILL);
if( in_fd >= 0 ) close(in_fd);
+ if( res_fd >= 0 ) close(res_fd);
if( out_stream.fd >= 0 ) close(out_stream.fd);
if( err_stream.fd >= 0 ) close(err_stream.fd);
int status = 0;
@@ -441,6 +494,8 @@ int Man_In_Grey_gate·Gate·run(
" guard passes the journal, run the interpreter\n"
"Option of admit:\n"
" check_only run the guard and not the interpreter\n"
+ "A journal that declares a result has it written to descriptor 3 once the\n"
+ "journal is played; when descriptor 3 is not open, the result is discarded.\n"
,program
);
}
@@ -503,6 +558,24 @@ int Man_In_Grey_gate·Gate·run(
return true;
}
+ /* Write the result to descriptor 3, all of it, waiting while the
+ invoking program's end of it is full. Returns false when it cannot. */
+ Local bool forward_result(const unsigned char *result ,size_t length){
+ size_t written = 0;
+ while( written < length ){
+ ssize_t n = write(RESULT_FD ,result + written ,length - written);
+ if( n > 0 ){ written += (size_t)n; continue; }
+ if( n < 0 && errno == EINTR ) continue;
+ if( n < 0 && errno == EAGAIN ){
+ struct pollfd p = {.fd = RESULT_FD ,.events = POLLOUT};
+ poll(&p ,1 ,-1);
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
Local unsigned char *read_journal(size_t *length){
size_t size = 1 << 16;
unsigned char *journal = malloc(size);
@@ -633,7 +706,7 @@ int Man_In_Grey_gate·Gate·run(
char *interpreter_argument[] = {
(char *)PYTHON ,(char *)"-E" ,(char *)"-s" ,(char *)"-B" ,interpreter_path ,(char *)"execute" ,NULL
};
- Child interpreter = {.source = "interpreter" ,.stdout_is_verdict = true};
+ Child interpreter = {.source = "interpreter" ,.stdout_is_verdict = true ,.has_result = true};
say("interpreting" ,"%s" ,interpreter_path);
status = run_child(&interpreter ,interpreter_argument ,journal ,journal_length);
if( interpreter.broken ){
@@ -658,6 +731,14 @@ int Man_In_Grey_gate·Gate·run(
return 2;
}
say("interpreted" ,"interpreter %s journal played" ,language);
+ if( interpreter.result_length > 0 ){
+ if( Result_discarded )
+ say("result_discarded" ,"%zu bytes; descriptor 3 was not open" ,interpreter.result_length);
+ else if( forward_result(interpreter.result ,interpreter.result_length) )
+ say("result_forwarded" ,"%zu bytes on descriptor 3" ,interpreter.result_length);
+ else
+ say("result_lost" ,"%zu bytes; descriptor 3 cannot be written: %s" ,interpreter.result_length ,strerror(errno));
+ }
return 0;
}
diff --git a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_emplace_bio_gate.CLI.c b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_emplace_bio_gate.CLI.c
deleted file mode 100644
index cdf0fcd..0000000
--- a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_emplace_bio_gate.CLI.c
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- Man_In_Grey_emplace_bio_gate - the gate of the emplace_bio instruction
- language: the general gate with the language name and the guard's and
- interpreter's paths fixed.
-*/
-#include "Gate.lib.c"
-
-int main(int argc ,char **argv){
- return Man_In_Grey_gate·Gate·run("emplace_bio" ,"../Man_In_Grey/guard_emplace_bio.py"
- ,"../Man_In_Grey/interpreter_emplace_bio.py" ,argc ,argv);
-}
diff --git "a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267emplace_bio.CLI.c" "b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267emplace_bio.CLI.c"
new file mode 100644
index 0000000..1c00664
--- /dev/null
+++ "b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267emplace_bio.CLI.c"
@@ -0,0 +1,11 @@
+/*
+ Man_In_Grey_gate·emplace_bio - the gate of the emplace_bio instruction
+ language: the general gate with the language name and the guard's and
+ interpreter's paths fixed.
+*/
+#include "Gate.lib.c"
+
+int main(int argc ,char **argv){
+ return Man_In_Grey_gate·Gate·run("emplace_bio" ,"../Man_In_Grey/EmplaceBio·Guard.py"
+ ,"../Man_In_Grey/EmplaceBio·Interpreter.py" ,argc ,argv);
+}
diff --git "a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267smoke.CLI.c" "b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267smoke.CLI.c"
new file mode 100644
index 0000000..af06b03
--- /dev/null
+++ "b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_gate\302\267smoke.CLI.c"
@@ -0,0 +1,9 @@
+/*
+ Man_In_Grey_gate·smoke - the gate of the smoke test's instruction language: the general
+ gate with the language name and the guard's and interpreter's paths fixed.
+*/
+#include "Gate.lib.c"
+
+int main(int argc ,char **argv){
+ return Man_In_Grey_gate·Gate·run("smoke" ,"../Man_In_Grey/Smoke·Guard.py" ,"../Man_In_Grey/Smoke·Interpreter.py" ,argc ,argv);
+}
diff --git a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_smoke_gate.CLI.c b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_smoke_gate.CLI.c
deleted file mode 100644
index 1e01b77..0000000
--- a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_smoke_gate.CLI.c
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- Man_In_Grey_smoke_gate - the gate of the smoke test's instruction language: the general
- gate with the language name and the guard's and interpreter's paths fixed.
-*/
-#include "Gate.lib.c"
-
-int main(int argc ,char **argv){
- return Man_In_Grey_gate·Gate·run("smoke" ,"../Man_In_Grey/guard_smoke.py" ,"../Man_In_Grey/interpreter_smoke.py" ,argc ,argv);
-}
diff --git a/document/Man_In_Grey.html b/document/Man_In_Grey.html
index 145a10a..f8d1469 100644
--- a/document/Man_In_Grey.html
+++ b/document/Man_In_Grey.html
@@ -26,7 +26,7 @@
@@ -59,25 +59,40 @@
An , as referred to in this manual, is any program that uses the Man_In_Grey tool.
-
- A directory containing composers.
+ A Python file defining compose(prov ,journal_iface), a function the orchestrator calls to declare variables and place instructions into the journal. In this version a journal is made by one composer.
- The program Man_In_Grey orchestrate. It is given a composer file or a composer directory, runs the composers, all of them placing instructions into one journal, and invokes a gate with that journal on its standard input. An application can act as its own orchestrator, placing instructions through the journal interface and invoking the gate itself.
+ The program Man_In_Grey orchestrate. It is given a composer file, runs the composer, invokes a gate with the journal on its standard input, and prints the result the gate hands back. An application can act as its own orchestrator, placing instructions through the journal interface and invoking the gate itself.
- The ordered instructions the composers place, encoded as binary data in CBOR.
+ The variables a composer declares and the instructions it places, in order, encoded as binary data in CBOR. See .
- A journal entry. It consists of an instruction operation specifier followed by arguments.
+ A journal entry. It consists of an operation specifier followed by arguments, given by position.
+
+
+ A named place in a journal, of a type the language definition declares, which instructions write and read. A composer holds a variable where it needs a value that does not exist until the interpreter plays the instruction that writes it. See .
-
- In the sense of this manual, a symbol is a placeholder for an instruction result. Currently it is merely an index into the journal's instruction list.
+
+ The value of one variable of a journal, which the gate hands back to whoever invoked it, on descriptor 3, once the journal is played. See .
- A grammar that describes the available instruction formats. Each instruction currently has the format of an instruction operation specifier followed by arguments.
+ The instructions an application's gate accepts, with their argument types, as its language definition gives them.
+
+
+ Data, written by the application developer, that defines an instruction language: its types, its instruction formats, the types a result can have, and its operation specifiers. Account A cannot change it. See .
+
+
+ The direction and type of each argument of an instruction, by position. Several operations can share one format.
+
+
+ A language's module of the checks its guard makes after the well-formed guard, such as EmplaceBio·GuardCheck.py.
+
+
+ A language's module holding LibraryInstruction, the implementation of its instructions, such as EmplaceBio·LibraryInstruction.py.
+
+
+ Man_In_Grey's own check of a journal against the language definition, the first check every guard makes. See .
A compiled program that is invoked with a journal on its standard input, runs the guard on it, and when the guard approves the journal, runs the interpreter on it.
@@ -92,7 +107,7 @@
A function written at the top of a guard, between the lines # constraints begin and # constraints end, that answers whether a journal's request is allowed, such as is_allowed_account(user_name). See .
- The interpreter is invoked by the gate after, and only if, the guard has approved the journal. The interperter walks the journal while executing the instructions in order.
+ The interpreter is invoked by the gate after, and only if, the guard has approved the journal. The interpreter walks the journal while executing the instructions in order.
The two user ids a process carries. The effective uid determines what the process is permitted to do, and the real uid identifies the ultimate invoking process's real uid. See .
@@ -145,74 +160,199 @@
Instruction language
- An instruction language is defined by the application developer and is built into the application. On the A side of the account divide, the composer writes the language by calling functions on the journal interface, one interface function per instruction. On the B account side of the divide, the guard checks the journal for correct syntax and that other constraints are met.
+ An instruction language is defined by the application developer and is built into the application. He writes it as data, its language definition, , and both sides of the account divide read the same definition. On the A side, the composer writes in the language by calling functions on the journal interface, one function per instruction, made from the definition. On the B side, the guard checks the journal first against the definition, then against what a definition cannot say, the constraints among it.
- The instruction language is not specific to Man_In_Grey. An application that mounts home directories has a language of mount and unmount; one that manages accounts has a language for making and deleting them. Each gate starts the guard and the interpreter of one instruction language, and they refuse a journal written in another language. These coherency checks are due to the journal carrying the name of its language, and due to syntax checking of the constituent instructions passing.
+ The instruction language is not specific to Man_In_Grey. An application that mounts home directories has a language of mount and unmount; one that manages accounts has a language for making and deleting them. Each gate starts the guard and the interpreter of one instruction language, and they refuse a journal written in another language. These coherency checks are due to the journal carrying the name of its language and the hash of its definition, and due to syntax checking of the constituent instructions passing.
A language can be made as narrow as its application. The narrower it is, the less a journal can say, and the less the guard has to rule out. The example language, , can name no file at all: what it writes, and where, is partially compiled in, and partially derived from the name of the invoking user account.
+
+ Narrow is not the same as primitive. A journal can be guarded only at the level of the work it asks for. An instruction that names the work, such as making a child account, can be checked against who asked and what he is allowed. A language of primitive instructions, such as setting a uid or mounting a path, would leave the guard to work out what request they add up to, and to check every value the composer chose against records it would have to consult again. The instructions of a language are therefore requests, specific to the work to be done.
+
- Data form
+ Journal form
+
+ Every journal has the same form, whatever its instruction language: declarations, then the instruction list. The form is Man_In_Grey's own, a generic language; an instruction language is a set of instruction formats written within it, as an instruction set is defined within an assembler's syntax.
+
+
+
+ Language definition
+
+ A language definition is a map of four sections, written by the application developer as a literal value in a module of its own, such as EmplaceBio·Language.py, beside the module's NAME. It is data, not code. The guard, the interpreter and the journal interface all read the same definition, and the guard's copy is installed with the guard, owned by root, so account A cannot change it.
+
+
+
+ A list of type declarations, each [<name> ,<type expression>]. A type expression is a primitive, the name of a type declared earlier in the list, or a compound: ["dictionary" ,[<key> ,<type expression>] ...] or ["list" ,<type expression>]. A type can therefore not refer to itself.
+
+
+ A list of instruction formats, each [<format name> ,[<direction> ,<type name>] ...]. The direction is in, out or in-out; the type name is a primitive or a declared type. Arguments are given by position, so the format fixes how many an instruction takes.
+
+
+ The list of type names a journal's result can have.
+
+
+ A map from each operation specifier to its format name. Its keys are the explicit list of the operations a journal can use. The operation specifier is also the name of the Python function that carries the instruction out.
+
+
+
+ The primitives are text, integer, bytes, boolean and handle. A handle is a value an instruction produces for later instructions, such as an open file. No literal can be a handle, so a journal can name an open file only through the variable the instruction that opened it wrote; no result can hold a handle, since it means nothing outside the interpreter.
+
+
+ In a declaration, a position decides what an element is: the first element of a list is always a type or a name, never data, so no element has to be inspected to find out what it is. A dictionary entry is [<key> ,<type expression>], and an empty dictionary is ["dictionary"].
+
+
+ Types are compared by name. A variable of the type short_name is not a variable of the type text, even when short_name is declared as text, so a variable passes only where the format names its type exactly, and account A cannot make a type of his own. A literal is compared with the structure of its type. The check uses Python's type(), not isinstance, since isinstance(True ,int) is true and a boolean would otherwise pass as an integer.
+
+
+ The smoke test's language uses every part of the form:
+
+
+ DEFINITION = {
+ "list_type": [
+ ["directory" ,"text"]
+ ,["file_name" ,"text"]
+ ,["mode" ,"text"]
+ ,["account_name" ,"text"]
+ ,["permission" ,"integer"]
+ ,["file" ,"handle"]
+ ,["content" ,"bytes"]
+ ,["summary" ,["dictionary" ,["open_count" ,"integer"] ,["unlink_count" ,"integer"]]]
+ ]
+ ,"list_format": [
+ ["format_open" ,["in" ,"directory"] ,["in" ,"file_name"] ,["in" ,"mode"] ,["in" ,"account_name"] ,["in" ,"permission"] ,["out" ,"file"]]
+ ,["format_write" ,["in-out" ,"file"] ,["in" ,"content"]]
+ ,["format_unlink" ,["in" ,"directory"] ,["in" ,"file_name"]]
+ ,["format_summary" ,["out" ,"summary"]]
+ ]
+ ,"list_result": ["summary"]
+ ,"map_instruction": {
+ "open": "format_open"
+ ,"write": "format_write"
+ ,"unlink": "format_unlink"
+ ,"summary": "format_summary"
+ }
+ }
+
+
+ open writes the handle of the file it opens to its last argument; write reads and writes its file, as it changes the file; summary writes counts, which a journal can make its result.
+
+
+ A definition has a well-formed check of its own, is_well_formed_language, in Language.py, made when the guard and the interpreter start and when the journal interface is made: every head a known type, every name declared once, every type used declared before, every format named in the operation map declared, and every operation specifier a Python function name not beginning with _. The names declare, result, finish and release are kept by Man_In_Grey and refused as operation specifiers. The definition's hash is the SHA-256 of its canonical CBOR encoding.
+
+
Journal
- A journal is a CBOR map of four entries:
+ A journal is a CBOR map of seven entries:
- The version of the journal's form, now 3. A guard or interpreter refuses a journal of another version.
+ The version of the journal's form, now 4. A guard or interpreter refuses a journal of another version.
The name of the instruction language the journal is written in, such as emplace_bio. A guard and an interpreter refuse a journal in a language other than their own.
+
+ The hash of the language definition the journal was composed against. A guard refuses a journal whose hash differs from its own definition's, which tells a person whose copy of the definition is out of date why, in place of a puzzling refusal of some instruction.
+
- When, on which host and by which account the journal was composed, the composer directory, and the composers that ran, in order.
+ When, on which host and by which account the journal was composed, and the composer that ran.
+
+
+ The variables, each [<name> ,<type name>], the type one the language definition declares.
+
+
+ The name of the variable whose value is the journal's result, or null for a journal with no result.
-
- The instructions, in the order the composers placed them.
+
+ The instructions, in the order the composer placed them.
- Each instruction is a map of three entries: name, which instruction it is; argument, a map holding all of the instruction's arguments, each keyed by argument name; and composer, the composer that placed it. Man_In_Grey's orchestrator records there the path of the composer's file, relative to the composer directory; an application that places instructions itself records what it chooses. The first four instructions of the journal in , decoded, and written in JSON notation:
+ Each instruction is a map of two entries: operation, its operation specifier, and list_argument, the list of its arguments, in the order of its format. Each argument is a pair whose first element says how to read the second:
+
+
+
+ The value itself, whatever it is, a map included. It is never examined to find out what it is.
+
+
+ A variable, which the instruction reads, writes, or both, as its format's direction says.
+
+
+
+ The kind of an argument is carried beside it, never inferred from its value, so no literal can be taken for a variable. The first four instructions of the journal in , decoded, and written in JSON notation:
+
+
+ {"operation": "display_name", "list_argument": [["literal", "Thomas"]]}
+ {"operation": "paragraph", "list_argument": []}
+ {"operation": "text", "list_argument": [["literal", "I write "], ["literal", []]]}
+ {"operation": "text", "list_argument": [["literal", "software"], ["literal", ["emphasis"]]]}
+
+
+ Since arguments are given by position, the well-formed guard requires exactly the number an instruction's format takes: a text instruction carrying a third argument is refused, whatever that argument says. A journal of the smoke language, in which one instruction writes a variable that later instructions read, with a byte string written as b"...", as JSON has none:
- {"name": "display_name", "argument": {"text": "Thomas"}, "composer": "bio.py"}
- {"name": "paragraph", "argument": {}, "composer": "bio.py"}
- {"name": "text", "argument": {"content": "I write ", "style": []}, "composer": "bio.py"}
- {"name": "text", "argument": {"content": "software", "style": ["emphasis"]}, "composer": "bio.py"}
+ "list_variable": [["unbound_conf", "file"], ["summary", "summary"]]
+ "result": "summary"
+ {"operation": "open", "list_argument": [["literal", "/etc/unbound"], ["literal", "unbound.conf"], ["literal", "replace_keep"],
+ ["literal", "Thomas"], ["literal", 292], ["variable", "unbound_conf"]]}
+ {"operation": "write", "list_argument": [["variable", "unbound_conf"], ["literal", b"server:\n"]]}
+ {"operation": "summary", "list_argument": [["variable", "summary"]]}
- An instruction that takes no arguments, such as paragraph, has an empty argument map. Arguments are given by name rather than by position, so the guard's wellformed check can require exactly the arguments an instruction takes: a text instruction carrying a third argument is refused, whatever that argument says.
+ The meta entry is a note, not evidence: account A writes the whole journal, and could write anything there, so the guard and the interpreter never base a decision on it.
+
+
+
+ Variable
- An argument that is a symbol is the map {"symbol": <index>}, where the index locates the instruction that produces the value. In the account language of , the second instruction names the result of the first:
+ A composer writes instructions, but it does not run them. When an instruction produces a value, as for example an instruction that makes an account and produces its uid, the value does not exist yet at the time the composer runs. The composer therefore declares a variable to hold it, and gives the variable to the instruction that writes it and to each instruction that reads it:
- {"name": "make_account", "argument": {"user_name": "Thomas·www"}, "composer": "www.py"}
- {"name": "add_to_group", "argument": {"account": {"symbol": 0}, "group": "http"}, "composer": "www.py"}
+ account = journal_iface.declare("www" ,"account")
+ journal_iface.make_account("Thomas·www" ,account)
+ journal_iface.add_to_group(account ,"http")
- Man_In_Grey's orchestrator runs the composers one after another into a single journal. Before it calls each composer's compose, it records that composer's path, relative to the composer directory, and every instruction placed during the call carries it. A journal composed from a directory of composers therefore shows which composer placed each instruction, and print shows it at the end of each line, which helps in finding the composer that placed a wrong one. The composer entry, like meta, is a note, not evidence: account A writes the whole journal, and could write anything there, so the guard and the interpreter never base a decision on either.
+ Here make_account's format writes its second argument, and add_to_group's reads its first. The two instructions are of a language an application that manages accounts would define. An instruction can write as many variables as its format has out and in-out arguments, and a variable of a dictionary type holds several values under its keys. A printed journal shows a variable as <name where an instruction reads it, >name where it writes it, and <>name where it does both.
+
+
+
+
+ Well-formed
+
+ A journal is straight-line: no branch, no loop. So one pass from the first instruction to the last settles, for every argument, whether it is well formed, before anything runs. The well-formed guard makes that pass, is_well_formed_journal in Guard·WellFormed.py, and refuses the journal when:
+
+
+ - a variable is declared twice, is not a name, or has a type the definition does not declare;
+ - an instruction's operation specifier is not in the definition, or it has a number of arguments other than its format's;
+ - an argument is not a pair of a known kind;
+ - a literal is given where the format writes, has a type that holds a handle, or lacks the structure of its type;
+ - a variable is not declared, or its type is not the one the format names;
+ - a variable is read, as in or in-out, before any instruction has written it;
+ - a variable an instruction writes is given to that instruction a second time;
+ - the result is not a declared variable, has a type not among the definition's result types, or is never written.
+
+
+ A variable can be written more than once; what the pass tracks is whether it is defined, and its type, at each point. The journal interface makes the same pass as each declaration and instruction is placed, so a composer's mistake is an error when the composer makes it, and the guard and the interpreter make it again on the journal they receive.
+
+
+
+
+ Result
+
+ Man_In_Grey is a function interface: account A gives a journal and can be given a value back. A journal that names a result has the value of that variable handed back when it is played. The interpreter writes the result to its descriptor 3 as a CBOR map, {"type": <type name> ,"value": <value>}, then writes its verdict line. The gate reads descriptor 3 as it reads the interpreter's other output, up to 16 MiB, never examines it, and only once it takes the journal as played forwards it unchanged to its own descriptor 3. A journal that is refused, or whose play fails, gives no result.
+
+
+ The invoking program opens descriptor 3 to receive the result. When it leaves descriptor 3 closed, the gate opens /dev/null there, and logs that it discarded the result. Whoever receives a result decodes it and checks it, as the gate cannot: the type name says which of the definition's result types it is.
-
- Symbol
-
- A composer writes instructions, but it does not run them. When an instruction would produce a result when run, as for example an instruction that makes an account and returns its uid, the result does not exist yet at the time the composer runs. Hence, when an instruction is entered into the journal via the journal interface, the interface returns a symbol placeholder. The symbol is then used as an argument by the composer when it enters instructions that are given said result as an argument. In the current implementation a symbol is simply the index into the journal that locates an instruction. Thus, with the current implementation each instruction can have at most one result.
-
-
- account = journal_iface.make_account("Thomas·www")
- journal_iface.add_to_group(account ,"http")
-
-
- In this example account is a result symbol. The value of account will be 0, i.e. the index into the journal of the make_account instruction. The two instructions are of a language an application that manages accounts would define; the example language in has no instruction that produces a result.
-
-
@@ -222,15 +362,14 @@
Orchestrator
- The orchestrator is a utility program provided with Man_In_Grey. The orchestrator calls one or more composer programs, then sends the resulting journal to a gate program. Like all processes, when the orchestrator program runs, it has a real and effective uid. When the orchestrator is run from a shell, these are both inherited from the shell, and are typically pointing to the login user. In any case, the composers that it invokes will then run with the same effective and real uids as the orchestrator. When the gate executable file has its setuid bit set, when the gate is invoke by the orchestrator, the gate will inherit its real uid from the orchestrator, and will take its effective uid from the gate's executable file owner.
+ The orchestrator is a utility program provided with Man_In_Grey. The orchestrator calls a composer program, then sends the resulting journal to a gate program, and prints the result the gate hands back. Like all processes, when the orchestrator program runs, it has a real and effective uid. When the orchestrator is run from a shell, these are both inherited from the shell, and are typically pointing to the login user. In any case, the composer that it invokes will then run with the same effective and real uids as the orchestrator. When the gate executable file has its setuid bit set, when the gate is invoke by the orchestrator, the gate will inherit its real uid from the orchestrator, and will take its effective uid from the gate's executable file owner.
- The orchestrator finds the composers by file. Given a composer file, or a composer directory, it runs each file, and in each it calls one function, compose(prov ,journal_iface). To the orchestrator a composer is therefore a file: each composer is to be given a file of its own, and the file is to define exactly one function of that form. The file can define other functions as helpers of it, but the orchestrator calls none of them, and a second function meant as a composer is never run. Several composers are several files, and the order of the files in the composer directory, as describes, is the order they run in.
-
+ The orchestrator is given a composer file. It runs the file, and calls one function in it, compose(prov ,journal_iface). The file can define other functions as helpers of it, but the orchestrator calls none of them. Running several composers into one journal would need a way for each to keep variables of its own and to share others, which this version does not have; see .
- An application making use of Man_In_Grey can bypass the orchestrator and call the composers and gate on its own. When this happens the gate's real ID will be that of the application. For a setuid gate, its effective uid still comes from its file owner. Such an application calls its composer functions however it likes, as many to a module as it likes, since it does no search for them.
+ An application making use of Man_In_Grey can bypass the orchestrator and call the composers and gate on its own. When this happens the gate's real ID will be that of the application. For a setuid gate, its effective uid still comes from its file owner. Such an application makes a Journal·Interface.JournalComposed and its Journal·Interface.JournalInterface from the definition, places instructions however it likes, and calls Man_In_Grey.admit(gate ,journal_bytes), the orchestrator's work function, which invokes the gate with descriptor 3 open and returns the gate's exit status and the result.
@@ -244,7 +383,7 @@
The gate is compiled because Linux ignores the setuid bit on a script started with #!. Given no arguments, the gate prints its usage. Its command flags reports the gate's real and effective uid and gid with their names, whether they differ, the name of the gate's instruction language, and the paths of the guard and the interpreter.
- > /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate flags
+ > /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_gate·emplace_bio flags
real.uid=2006
real.user_name=Thomas
real.gid=2006
@@ -256,8 +395,8 @@
uid_differ=1
gid_differ=1
language=emplace_bio
- guard.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/guard_emplace_bio.py
- interpreter.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/interpreter_emplace_bio.py
+ guard.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Guard.py
+ interpreter.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Interpreter.py
Here Thomas asks the example language's gate, , installed setuid to the web server's account http: the real ids are his, and the effective ids are http's.
@@ -269,7 +408,7 @@
- puts every signal's disposition back to its default, turns off the interval timers, and clears the blocked mask, so no signal state the invoking program left can act later;
- - opens /dev/null on any of descriptors 0, 1 and 2 the invoking program left closed, and closes every descriptor above 2, so nothing the invoking program opened reaches the guard or the interpreter;
+ - opens /dev/null on any of descriptors 0, 1 and 2 the invoking program left closed; keeps descriptor 3, the result descriptor, when the invoking program left it open, marked so that neither the guard nor the interpreter inherits it, and opens /dev/null there when it did not; and closes every descriptor above 3, so nothing the invoking program opened reaches the guard or the interpreter;
- raises each resource limit that could stop the guard or the interpreter partway, file size and CPU time to unlimited, address space and data size to 1 GiB, stack size to 8 MiB, and open files to 64, as far as the hard limit allows, or past it when account B is root; and refuses when one stays too low;
- finds the guard and the interpreter at paths fixed when the gate is compiled, relative to the gate itself, so account A cannot name others;
- refuses unless the guard and the interpreter, and every directory above them, are owned by root or by account B, and writable by no other account;
@@ -278,7 +417,8 @@
- reads the journal from its standard input, up to 16 MiB;
- when accounts A and B differ, makes its real uid account B's, so the kernel refuses account A's signals, and ignores the signals a terminal sends; see ;
- starts /usr/bin/python3 -E -s -B on the guard, with the journal on its standard input, and goes on only when the guard approves the journal;
- - starts the interpreter the same way, with the same journal, and takes the journal as played only when the interpreter writes its verdict line and exits 0.
+ - starts the interpreter the same way, with the same journal and a pipe to the gate on its descriptor 3, and takes the journal as played only when the interpreter writes its verdict line and exits 0;
+ - forwards what the interpreter wrote on its descriptor 3, the result, to the gate's own descriptor 3; see .
The guard and the interpreter, before they import anything, each check that every module they load, the guard program among them, is owned by root or by account B, and writable by no other account. A module runs code when it is imported, so the check comes first.
@@ -295,8 +435,8 @@
#include "Gate.lib.c"
int main(int argc ,char **argv){
- return Man_In_Grey_gate·Gate·run("emplace_bio" ,"../Man_In_Grey/guard_emplace_bio.py"
- ,"../Man_In_Grey/interpreter_emplace_bio.py" ,argc ,argv);
+ return Man_In_Grey_gate·Gate·run("emplace_bio" ,"../Man_In_Grey/EmplaceBio·Guard.py"
+ ,"../Man_In_Grey/EmplaceBio·Interpreter.py" ,argc ,argv);
}
@@ -306,7 +446,7 @@
Output rule
- Every line the guard and the interpreter write is copied to the person, marked guard: or interpreter:, and to the system log under the name Man_In_Grey_gate, beside the gate's own lines, marked gate:. The gate logs each journal's course itself, whatever the guard and the interpreter write: received, with account A, account B and the journal's size; isolated, when the gate gives up account A; then approved or refused; then interpreting; then interpreted or failed. A line breaks the gate's rules when it is not UTF-8, or holds a control character (C0, DEL or C1), or is longer than 4096 bytes, or is one line more than 2000; the gate then stops the program and refuses the journal. Unicode is otherwise welcome. The guard writes any control character it quotes as \u{XXXX}, and the example language's wellformed check refuses any text that holds one.
+ Every line the guard and the interpreter write is copied to the person, marked guard: or interpreter:, and to the system log under the name Man_In_Grey_gate, beside the gate's own lines, marked gate:. The gate logs each journal's course itself, whatever the guard and the interpreter write: received, with account A, account B and the journal's size; isolated, when the gate gives up account A; then approved or refused; then interpreting; then interpreted or failed; and, for a journal with a result, result_forwarded, result_discarded when descriptor 3 was not open, or result_lost when it could not be written. A line breaks the gate's rules when it is not UTF-8, or holds a control character (C0, DEL or C1), or is longer than 4096 bytes, or is one line more than 2000; the gate then stops the program and refuses the journal. Unicode is otherwise welcome. The guard writes any control character it quotes as \u{XXXX}, and the example language's language check refuses any text that holds one.
@@ -326,10 +466,10 @@
The kernel tells the gate which account invoked it, as the real uid, and the gate passes it on, so the guard knows it also. The kernel does not say which program invoked the gate. As the guard invocation is compiled into the gate, the guard can know the provenance of the journal from the point it was given to the gate, but not before that.
- Two checks are needed by nearly every guard, whatever its instruction language. The first is that the journal is well formed: that it decodes, names only instructions of its language, and gives each instruction the arguments it takes, of the right kinds. This check carries knowledge of the instruction syntax, so it belongs to the instruction language. The second is who account A is. The gate had account A's uid from the kernel, as its real uid, and records it for the guard before giving it up, ; the real gid and supplementary groups the gate inherited from the invoking program stay account A's, and the guard reads them from the kernel. Account A cannot misstate any of them. A guard can therefore allow a journal from the accounts it names, or from the members of a group, as its constraints decide.
+ Two checks are needed by nearly every guard, whatever its instruction language. The first is that the journal is well formed: that it decodes, and agrees with the language definition in every declaration and argument. Man_In_Grey makes this check itself, the well-formed guard, , from the definition, so a language writes no code for it. The second is who account A is. The gate had account A's uid from the kernel, as its real uid, and records it for the guard before giving it up, ; the real gid and supplementary groups the gate inherited from the invoking program stay account A's, and the guard reads them from the kernel. Account A cannot misstate any of them. A guard can therefore allow a journal from the accounts it names, or from the members of a group, as its constraints decide.
- The other checks a guard makes belong to its instruction language. The example language's four checks are in .
+ The other checks a guard makes belong to its instruction language: what a definition cannot say, such as an order the instructions are to come in or the values a literal can take, and the constraints. A language gives them as LIST_CHECK, in its guard check module, such as EmplaceBio·GuardCheck.py, a list of named checks in the order they are made, each given the instructions as the well-formed guard has resolved them, its literals' values and, for each variable, the instruction that last wrote it. The example language's four checks are in .
Constraint predicate
@@ -337,7 +477,7 @@
A guard's constraints are code. The guard is a Python program, written for one application, so its constraints are Python functions written at the top of the guard itself, where they go with it, and no language for expressing constraints is needed. Each is a constraint predicate, named in the manner of is_allowed_account(user_name) or is_allowed_open(directory ,name ,mode ,owner ,permission), and the guard refuses a journal when one does not answer True. A predicate can compare with a list, ask whether an account is in a group, or anything else Python can say. The predicates sit between the lines # constraints begin and # constraints end, so that install_gate can put in an administrator's and keep them when the guard is installed again. As promoted with Man_In_Grey, they refuse every account.
- Some constraints recur from one guard to the next, and the library module Constraint.py holds them, for a guard's predicates to call:
+ Some constraints recur from one guard to the next, and the library module Guard·LibraryConstraint.py holds them, for a guard's predicates to call:
@@ -354,7 +494,7 @@
- The guard checks and imports its modules, Constraint.py among them, only after its predicates are defined, and calls the predicates only after that, so a predicate can call Constraint.is_group_member by name, though the import comes later in the file. Code at the top level of the constraints runs before the ownership check, so the constraints hold only definitions, and imports of the standard library at most. The interpreter imports the guard program, for what execution needs from its constraints.
+ The guard checks and imports its modules, Guard·LibraryConstraint.py among them, only after its predicates are defined, and calls the predicates only after that, so a predicate can call Guard·LibraryConstraint.is_group_member by name, though the import comes later in the file. Code at the top level of the constraints runs before the ownership check, so the constraints hold only definitions, and imports of the standard library at most. The interpreter imports the guard program, for what execution needs from its constraints.
@@ -374,7 +514,24 @@
The gate starts the interpreter only after the guard has approved the journal, with the effective uid of account B and the same journal on its standard input. Generally the interpreter does not check the constraints as that is the guard's work, but some checks will naturally occur as part of interpretation. Where useful the guard program module can be included so as to pull in the helper functions.
- The gate logs when a journal is received, approved, and interpreted. The interpreter writes to standard error, as <tag>: <message>, the highlights of what it does, as its developer chooses them. Its standard output is for its verdict alone, as the guard's is: when it has played the whole journal, it writes exactly one line there, interpreter <language> journal played, and exits 0. The gate takes the journal as played on that line and that status together; an interpreter that exits 0 without it, having returned early or not being the program it should be, is logged as failed. Otherwise its exit status is 1 when it stopped partway, and 2 when the journal could not be read or is not well formed.
+ The gate logs when a journal is received, approved, and interpreted. The interpreter writes to standard error, as <tag>: <message>, the highlights of what it does, as its developer chooses them. Its standard output is for its verdict alone, as the guard's is: when it has played the whole journal, it writes the result to descriptor 3, when the journal names one, then exactly one line on standard output, interpreter <language> journal played, and exits 0. The gate takes the journal as played on that line and that status together; an interpreter that exits 0 without it, having returned early or not being the program it should be, is logged as failed. Otherwise its exit status is 1 when it stopped partway, and 2 when the journal could not be read or is not well formed.
+
+
+ A language gives the implementation of its instructions as a class, LibraryInstruction, in its instruction library, such as EmplaceBio·LibraryInstruction.py. An instance is made once for each journal, and holds what the instructions build up between them. For each operation specifier in the definition it has a function of that name. The interpreter finds the functions by the names the definition lists, never by a name taken from the journal, so no other function of the class, nor anything the module imports, can be reached from a journal. It gives each function the instruction's arguments by position:
+
+
+
+ A copy of the value, so that the function cannot change a variable through it.
+
+
+ An empty list, a box, to which the function appends the value.
+
+
+ A box holding a copy of the variable's value, which the function replaces.
+
+
+
+ After each call, every box is to hold exactly one value, of its format's type; the interpreter takes the value out and keeps it as the variable's. An empty box is evidence that the function wrote nothing, which no stand-in value could be. A function reports failure by raising an exception. LibraryInstruction can also have finish(), called after the last instruction, and release(), called at the end whatever happened.
The verdict line says only that the interpreter reached its end. It cannot undo what an interpreter did before failing, and it is the interpreter's own claim, not evidence that what it did was right.
@@ -487,7 +644,7 @@
Once the gate has given up account A, the kernel no longer says who account A is: the guard and the interpreter find account B as their real uid. The gate records account A before giving it up, in MIG_REAL_UID, MIG_REAL_GID and MIG_REAL_USER_NAME, having first cleared the environment, so the variables say what the gate says and nothing account A put there. The guard and the interpreter read account A from them, and refuse to work when the uid and the user name do not name the same account. Knowledge of account A therefore rests on the gate, which had it from the kernel.
- The gate gives up only the uid. Its real gid and supplementary groups stay account A's, so Constraint.has_real_group still answers for account A's groups.
+ The gate gives up only the uid. Its real gid and supplementary groups stay account A's, so Guard·LibraryConstraint.has_real_group still answers for account A's groups.
@@ -511,21 +668,29 @@
Man_In_Grey is a Harmony project with two namespaces: Man_In_Grey.copy, the Python programs, and Man_In_Grey_gate.make, the general gate, the example language's gate, and the gate of the smoke test's language. In the developer role, from its directory developer/, build "*" and promote write place them in the following layout, which is how each part finds the others:
- consumer/Man_In_Grey/Man_In_Grey the command a person runs
- consumer/Man_In_Grey/Man_In_Grey.py the orchestrator
- consumer/Man_In_Grey/install_gate installs a gate setuid, run by root
- consumer/Man_In_Grey/Journal.py symbols, instructions, the journal, CBOR
- consumer/Man_In_Grey/Guard.py the general guard
- consumer/Man_In_Grey/Constraint.py predicates that recur among guards' constraints
- consumer/Man_In_Grey/Interpreter.py the general interpreter
- consumer/Man_In_Grey/EmplaceBioLanguage.py the example language: interface, checks, execution
- consumer/Man_In_Grey/guard_emplace_bio.py its guard, with its constraints at its top
- consumer/Man_In_Grey/interpreter_emplace_bio.py its interpreter
- consumer/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate the example language's gate
- consumer/Man_In_Grey_gate/Man_In_Grey_smoke_gate the smoke test's gate, with SmokeLanguage.py,
- guard_smoke.py and interpreter_smoke.py
- consumer/Man_In_Grey_gate/libMan_In_Grey.a the general gate, for other gates
+ consumer/Man_In_Grey/Man_In_Grey the command a person runs
+ consumer/Man_In_Grey/Man_In_Grey.py the orchestrator
+ consumer/Man_In_Grey/install_gate installs a gate setuid, run by root
+ consumer/Man_In_Grey/Guard.py the general guard
+ consumer/Man_In_Grey/Guard·LibraryConstraint.py predicates that recur among guards' constraints
+ consumer/Man_In_Grey/Guard·WellFormed.py the well-formed guard: is_well_formed_journal
+ consumer/Man_In_Grey/Interpreter.py the general interpreter
+ consumer/Man_In_Grey/Journal.py the journal's data form: decode, print, the result record
+ consumer/Man_In_Grey/Journal·Interface.py the composer's side: variables and the journal interface
+ consumer/Man_In_Grey/Language.py the language definition: is_well_formed_language, its hash
+ consumer/Man_In_Grey/EmplaceBio·Guard.py the example language's guard, with its constraints at its top
+ consumer/Man_In_Grey/EmplaceBio·GuardCheck.py its checks for the guard
+ consumer/Man_In_Grey/EmplaceBio·Interpreter.py its interpreter
+ consumer/Man_In_Grey/EmplaceBio·Language.py its definition
+ consumer/Man_In_Grey/EmplaceBio·LibraryInstruction.py the implementation of its instructions
+ consumer/Man_In_Grey/Smoke·... the same five files for the smoke test's language
+ consumer/Man_In_Grey_gate/Man_In_Grey_gate·emplace_bio the example language's gate
+ consumer/Man_In_Grey_gate/Man_In_Grey_gate·smoke the smoke test's gate
+ consumer/Man_In_Grey_gate/libMan_In_Grey.a the general gate, for other gates
+
+ A module is a namespace, and the names follow from that. A file named for a namespace alone, such as Guard.py, is the namespace's main module; a file specific to it carries the namespace before a middle dot, as Guard·WellFormed.py does, and the middle dot stands for the namespace a language without nested namespaces lacks. An application's language is a namespace too, and its files parallel Man_In_Grey's own inside it: Guard.py is the general guard and EmplaceBio·Guard.py the example language's guard; Interpreter.py and EmplaceBio·Interpreter.py likewise. Within a name the broadest category comes first, so that a listing puts siblings together, and a name for a collection begins with the kind of collection, as LibraryInstruction, a library of instructions, does. A gate belongs to Man_In_Grey's gates, so its name is Man_In_Grey_gate·emplace_bio.
+
The same layout is staged in developer/scratchpad/made/, so an experiment in the developer role of the Man_In_Grey project runs from there. Both sides need the Python package cbor2, each in its own Python. Man_In_Grey, and any program using the journal interface, runs on the first python3 on PATH, which in a Harmony role is the project's linked Python when there is one; install the package into it with python3 -m pip install cbor2. The gate always starts /usr/bin/python3 -E -s, so the guard and the interpreter use neither a person's Python nor a copy installed with pip install --user into ~/.local. The system Python needs the package from the distribution, installed by root: python-cbor2 on Arch, python3-cbor2 on Debian, and on openSUSE, which names packages for the Python they are built for, python313-cbor2 for Python 3.13. To see what the interpreter will see: /usr/bin/python3 -E -s -c 'import cbor2; print(cbor2.__file__)'.
@@ -534,13 +699,13 @@
Writing a composer
- Composers are kept in a composer directory, named composer in the examples here. Every file in it is a composer, a Python file defining compose(prov ,journal_iface); notes such as a README belong a directory up. Its subdirectories are namespaces for the author. They set the order the composers run in: depth first, in order of name at each level, files and subdirectories together, by Unicode code point, so capital letters come before small ones. A composer's place says nothing else; what a composer does is said by the instructions it places.
+ A composer is a Python file defining compose(prov ,journal_iface), kept in a directory of its author's choosing, named composer in the examples here. What a composer does is said by the instructions it places.
- A composer runs with the orchestrator's uids, which are normally account A's, and so with account A's rights. If another account could change a composer, or add one to a composer directory, that account's code would run with account A's rights, and its instructions would reach the gate in account A's name. Every composer, and every directory from / down to it, is therefore to be owned by account A or by root, and writable by no other account.
+ A composer runs with the orchestrator's uids, which are normally account A's, and so with account A's rights. If another account could change a composer, that account's code would run with account A's rights, and its instructions would reach the gate in account A's name. The composer, and every directory from / down to it, is therefore to be owned by account A or by root, and writable by no other account.
- This ownership protects account A, not account B, and the gate cannot enforce it. The gate is given a journal, never the program that made it, and anything it is told about the composers comes from account A. To the gate and the guard, every journal is account A's, whatever made it. Composer ownership is therefore a convention, kept by whoever installs an application's composers. Man_In_Grey's orchestrator keeps it for its own runs: before running any composer it makes the check, and it refuses a symbolic link in a composer directory. An application that acts as its own orchestrator keeps the convention itself. To run a composer someone else wrote, review it and copy it into your own composer directory.
+ This ownership protects account A, not account B, and the gate cannot enforce it. The gate is given a journal, never the program that made it, and anything it is told about the composers comes from account A. To the gate and the guard, every journal is account A's, whatever made it. Composer ownership is therefore a convention, kept by whoever installs an application's composers. Man_In_Grey's orchestrator keeps it for its own runs: before running the composer it makes the check, and it refuses a composer that is a symbolic link. An application that acts as its own orchestrator keeps the convention itself. To run a composer someone else wrote, review it and copy it into a directory of your own.
# composer/bio.py
@@ -560,13 +725,36 @@
The name of account A.
-
- The composer directory, the composer's path absolute and relative to it, the directory holding the composer, and the working directory of the run.
+
+ The composer's absolute path, the directory holding it, and the working directory of the run.
- Man_In_Grey orchestrate <path> runs the one composer in a composer file, or every composer in a composer directory. No option given to Man_In_Grey reaches compose; a composer's settings are written in the composer itself. The journal_iface a composer is given is that of the gate's instruction language: the orchestrator asks the gate for its flags, and loads the module named for its language, EmplaceBioLanguage.py for emplace_bio.
+ Man_In_Grey orchestrate <composer> runs the composer. No option given to Man_In_Grey reaches compose; a composer's settings are written in the composer itself. The journal_iface a composer is given is that of the gate's instruction language: the orchestrator asks the gate for its flags, loads the definition named for its language, EmplaceBio·Language.py for emplace_bio, and makes the journal interface from it. It has one function for each operation specifier, given the instruction's arguments in the order of its format, and two more:
+
+
+ Place an instruction. An argument that is a variable declare returned is given as that variable; any other value is a literal.
+
+
+ Declare a variable, and return it.
+
+
+ Make the variable the journal's result.
+
+
+
+ Each call is checked as it is made, by the pass the well-formed guard makes, so a mistake raises JournalError in the composer, naming the instruction and argument, and no journal reaches the gate. A composer of the smoke language, which opens a file, writes it, and makes a summary its result:
+
+
+ def compose(prov ,journal_iface):
+ file = journal_iface.declare("unbound_conf" ,"file")
+ journal_iface.open("/etc/unbound" ,"unbound.conf" ,"replace_keep" ,prov.process_user ,0o444 ,file)
+ journal_iface.write(file ,b"server:\n do-ip6: no\n")
+ summary = journal_iface.declare("summary" ,"summary")
+ journal_iface.summary(summary)
+ journal_iface.result(summary)
+
@@ -575,16 +763,16 @@
A run that changes files is best preceded by two that do not. The first shows the journal while nothing has left the person's account; the second has the guard check the journal, and the gate stop before the interpreter:
- Man_In_Grey orchestrate composer gate <gate> print compose_only
- Man_In_Grey orchestrate composer gate <gate> check_only
- Man_In_Grey orchestrate composer gate <gate>
+ Man_In_Grey orchestrate composer/bio.py gate <gate> print compose_only
+ Man_In_Grey orchestrate composer/bio.py gate <gate> check_only
+ Man_In_Grey orchestrate composer/bio.py gate <gate>
-
- Run the composer in a composer file, or every composer in a composer directory, and invoke the gate with the journal.
+
+ Run the composer, a file, and invoke the gate with the journal.
- The gate to invoke, which also sets the instruction language the composers write in. Required: the orchestrator has no gate of its own.
+ The gate to invoke, which also sets the instruction language the composer writes in. Required: the orchestrator has no gate of its own.
Print the journal after composing.
@@ -597,7 +785,7 @@
- The command and its path come first, then the gate, and the options follow, as plain words; given no arguments, Man_In_Grey prints its help, and so does the gate. The exit status is 0 when the run did what it was asked; 1 when the guard refused the journal or an instruction failed to execute; and 2 when the run could not start, a composer failed or was refused, or the gate, guard or interpreter could not run or broke the gate's rules. The guard's and interpreter's lines are also in the system log; on a system with journald, journalctl -t Man_In_Grey_gate shows them.
+ The command and its path come first, then the gate, and the options follow, as plain words; given no arguments, Man_In_Grey prints its help, and so does the gate. A journal with a result has it printed as one line, result: <type name> <value>. The exit status is 0 when the run did what it was asked; 1 when the guard refused the journal or an instruction failed to execute; and 2 when the run could not start, the composer failed or was refused, or the gate, guard or interpreter could not run or broke the gate's rules. The guard's and interpreter's lines are also in the system log; on a system with journald, journalctl -t Man_In_Grey_gate shows them.
@@ -629,14 +817,14 @@
The tester's test tester/authored/emplace_bio copies the promoted Man_In_Grey into its scratchpad directory, puts constraints of its own at the top of the copy's guard, and runs the copy through the example language's gate, not setuid, so accounts A and B are the same. The gate checks ownership on every run, so the test makes its copy writable by its owner alone, and every directory from / down to the scratchpad directory is to be writable by no account but its owner too.
- It checks thirty-two things. A page with every style and with text that looks like markup is compared with expected/page.html, and its picture with the one given. A later run with a JPEG places it and removes the PNG, and a run with no picture removes the JPEG. A check_only run changes nothing. Seven journals are then made without the journal interface and given to the gate directly, as a program other than the orchestrator could: one whose text instruction carries a file name, one with a style the language does not have, one with text before any paragraph, one with an SVG picture, one with more text than its constraints allow, one from an account they do not allow, and one for a page that has been made a symbolic link. Each is refused by the check that is to refuse it, and changes nothing. Last, the gate is started three times from a process that leaves signal state behind, as describes:
+ It checks thirty-two things. A page with every style and with text that looks like markup is compared with expected/page.html, and its picture with the one given. A later run with a JPEG places it and removes the PNG, and a run with no picture removes the JPEG. A check_only run changes nothing. Seven journals are then made without the journal interface and given to the gate directly, as a program other than the orchestrator could: one whose text instruction carries a file name as a third argument, one with a style the language does not have, one with text before any paragraph, one with an SVG picture, one with more text than its constraints allow, one from an account they do not allow, and one for a page that has been made a symbolic link. Each is refused by the check that is to refuse it, and changes nothing. Last, the gate is started three times from a process that leaves signal state behind, as describes:
authored/emplace_bio/run test
authored/emplace_bio/run clean
- The tester's smoke test, authored/smoke/run test, exercises the gate, the guard and the interpreter through a language of its own that is not for use on a system.
+ The tester's smoke test, authored/smoke/run test, exercises the gate, the guard and the interpreter through a language of its own that is not for use on a system, the language whose definition shows. It checks forty-three things: files opened, written in parts and put in place, kept as backups and removed; the result a run hands back on descriptor 3; a file in create mode refused the second time; a journal the constraints refuse in part, refused whole; a literal of the wrong type refused in the composer, and a mode the language does not have refused by the guard; a composer directory, a composer other accounts can write, a resource limit the gate cannot raise, and an interpreter that exits 0 without its verdict line. Seven journals are then given to the gate directly: a file variable read before it is written, a literal given where the format writes, a surplus argument, a variable not declared, a journal composed against another definition, and one of version 3, each refused; and one whose result the gate discards, since the program invoking it left descriptor 3 closed.
@@ -644,7 +832,7 @@
Example: emplace_bio
- A host publishes a bio page for each of the accounts it allows, at /srv/http/bio/<user_name>.html, with the account's picture beside it. The directory belongs to the web server's account, http on Arch and www-data on Debian, so a person cannot write his page there himself. He writes a composer giving his display name, his text and his picture, and runs Man_In_Grey with the gate Man_In_Grey_emplace_bio_gate, which the administrator has installed setuid to the web server's account. The interpreter then runs with the effective uid of http, and builds the page.
+ A host publishes a bio page for each of the accounts it allows, at /srv/http/bio/<user_name>.html, with the account's picture beside it. The directory belongs to the web server's account, http on Arch and www-data on Debian, so a person cannot write his page there himself. He writes a composer giving his display name, his text and his picture, and runs Man_In_Grey with the gate Man_In_Grey_gate·emplace_bio, which the administrator has installed setuid to the web server's account. The interpreter then runs with the effective uid of http, and builds the page.
Here account B is not root, and the gate stands between two ordinary accounts. The example is made so that a reader can see each step is safe: a person can change only his own page, and only to a page made of his text and picture.
@@ -653,19 +841,48 @@
The instruction language
-
+
The page's title and heading. Given once.
Starts a paragraph.
-
- A run of text in the current paragraph. style is a list drawn from strong, emphasis, underline and strikethrough; an empty list is plain text.
+
+ A run of text in the current paragraph. list_style is a list drawn from strong, emphasis, underline and strikethrough; an empty list is plain text.
-
+
The bytes of a PNG or JPEG image. At most once.
+
+ Its definition, in EmplaceBio·Language.py:
+
+
+ DEFINITION = {
+ "list_type": [
+ ["name_text" ,"text"]
+ ,["run_text" ,"text"]
+ ,["list_style" ,["list" ,"text"]]
+ ,["image" ,"bytes"]
+ ]
+ ,"list_format": [
+ ["format_name" ,["in" ,"name_text"]]
+ ,["format_empty"]
+ ,["format_run" ,["in" ,"run_text"] ,["in" ,"list_style"]]
+ ,["format_image" ,["in" ,"image"]]
+ ]
+ ,"list_result": []
+ ,"map_instruction": {
+ "display_name": "format_name"
+ ,"paragraph": "format_empty"
+ ,"text": "format_run"
+ ,"picture": "format_image"
+ }
+ }
+
+
+ No instruction writes a variable, so every argument of a journal in this language is a literal, and the journal has no result.
+
No instruction names a file. The page's file names come from the user name of the real uid, which the kernel gives the guard and the interpreter, so a person can write only his own page. The directory comes from page_directory() among the guard's constraints, which the administrator writes. A journal describes the whole page: a run that gives no picture removes the picture an earlier run placed.
@@ -677,7 +894,7 @@
The composer
- The composer is Thomas's own program, running with his uid. It holds the text in code and reads the picture from beside the composer directory:
+ The composer is Thomas's own program, running with his uid. It holds the text in code and reads the picture from beside the directory holding the composer:
# composer/bio.py
@@ -697,7 +914,7 @@
journal_iface.paragraph()
for content ,style in paragraph:
journal_iface.text(content ,style)
- journal_iface.picture((Path(prov.composer_root_dpath).parent / "picture.jpg").read_bytes())
+ journal_iface.picture((Path(prov.composer_dir_dpath).parent / "picture.jpg").read_bytes())
A composer could as well read a file in a notation its author likes, such as *emphasis*, and turn it into runs. That conversion runs with the person's own uid, so a mistake in it stays on his side; the gate only ever receives runs and style names.
@@ -707,7 +924,7 @@
The constraints
- The administrator writes the guard's constraints in a file, here bio_constraint.py, and install_gate puts them at the top of the installed guard_emplace_bio.py:
+ The administrator writes the guard's constraints in a file, here bio_constraint.py, and install_gate puts them at the top of the installed EmplaceBio·Guard.py:
def is_allowed_account(user_name):
@@ -727,7 +944,7 @@
def is_allowed_account(user_name):
- return Constraint.is_group_member(user_name ,"bio")
+ return Guard·LibraryConstraint.is_group_member(user_name ,"bio")
@@ -735,7 +952,10 @@
The guard
- Only the four instructions, with exactly their arguments; one display name; at most one picture; text only within a paragraph, and no paragraph without text; every style one of the four, none twice; no control character in any text.
+ The well-formed guard: only the four instructions, each with the number and types of arguments its format gives.
+
+
+ One display name; at most one picture; text only within a paragraph, and no paragraph without text; every style one of the four, none twice; no control character in any text.
is_allowed_account allows account A; is_allowed_text_size and is_allowed_picture_size allow the text and the picture; page_directory() gives an absolute directory.
@@ -755,7 +975,7 @@
The interpreter
- The interpreter writes the picture and the page to temporary files in the directory, gives each permission 0644, and renames each into place, then removes a picture the page no longer shows. The files are owned by http, the effective uid that made them. The page Thomas's composer gives:
+ The language's LibraryInstruction builds the page up, one instruction at a time, and its finish() writes the picture and the page to temporary files in the directory, gives each permission 0644, and renames each into place, then removes a picture the page no longer shows. The files are owned by http, the effective uid that made them. The page Thomas's composer gives:
<!DOCTYPE html>
@@ -791,14 +1011,14 @@
and installs the gate with the constraints above, from his own account, here ubuntu; su asks for root's password:
- consumer/Man_In_Grey/Man_In_Grey install Man_In_Grey_emplace_bio_gate owner http into /usr/local/lib/Man_In_Grey constraints bio_constraint.py
+ consumer/Man_In_Grey/Man_In_Grey install Man_In_Grey_gate·emplace_bio owner http into /usr/local/lib/Man_In_Grey constraints bio_constraint.py
install_gate is run by root; su asks for root's password
Password:
install_gate: installed /usr/local/lib/Man_In_Grey/Man_In_Grey/
- install_gate: put the constraints of /home/ubuntu/bio_constraint.py in /usr/local/lib/Man_In_Grey/Man_In_Grey/guard_emplace_bio.py
- install_gate: installed /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate, owner http:http, mode 6755
+ install_gate: put the constraints of /home/ubuntu/bio_constraint.py in /usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Guard.py
+ install_gate: installed /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_gate·emplace_bio, owner http:http, mode 6755
install_gate: its flags, as seen by root:
real.uid=0
real.user_name=root
@@ -811,9 +1031,9 @@
uid_differ=1
gid_differ=1
language=emplace_bio
- guard.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/guard_emplace_bio.py
- interpreter.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/interpreter_emplace_bio.py
- install_gate: run it as: /usr/local/lib/Man_In_Grey/Man_In_Grey/Man_In_Grey orchestrate <composer> gate /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate
+ guard.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Guard.py
+ interpreter.path=/usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Interpreter.py
+ install_gate: run it as: /usr/local/lib/Man_In_Grey/Man_In_Grey/Man_In_Grey orchestrate <composer> gate /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_gate·emplace_bio
The web server is to serve /srv/http/bio as static files, with no server-side processing, which is how it serves an .html file unless configured otherwise.
@@ -823,24 +1043,25 @@
Running
- Thomas runs the installed Man_In_Grey from his bio directory, which holds composer/ and picture.jpg:
+ Thomas runs the installed Man_In_Grey from his bio directory, which holds composer/bio.py and picture.jpg:
- /usr/local/lib/Man_In_Grey/Man_In_Grey/Man_In_Grey orchestrate composer gate /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate
+ /usr/local/lib/Man_In_Grey/Man_In_Grey/Man_In_Grey orchestrate composer/bio.py gate /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_gate·emplace_bio
gate: received: real uid 2006 (Thomas), effective uid 996 (http), journal of 1857 bytes
gate: isolated: real uid now 996 (http); account A can no longer signal the gate
guard: name: emplace_bio
- guard: version: 3
- guard: constraints: /usr/local/lib/Man_In_Grey/Man_In_Grey/guard_emplace_bio.py
- guard: journal: 10 instructions
+ guard: version: 4
+ guard: constraints: /usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Guard.py
+ guard: journal: 0 variables, 10 instructions
guard: check_passed: wellformed
+ guard: check_passed: language
guard: check_passed: constraint
guard: check_passed: content
guard: check_passed: filesystem
gate: approved: guard emplace_bio check passed
- gate: interpreting: /usr/local/lib/Man_In_Grey/Man_In_Grey/interpreter_emplace_bio.py
+ gate: interpreting: /usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Interpreter.py
interpreter: emplaced: /srv/http/bio/Thomas.jpg
interpreter: emplaced: /srv/http/bio/Thomas.html
gate: interpreted: interpreter emplace_bio journal played
@@ -852,10 +1073,11 @@
gate: received: real uid 2007 (alice), effective uid 996 (http), journal of 1855 bytes
gate: isolated: real uid now 996 (http); account A can no longer signal the gate
guard: name: emplace_bio
- guard: version: 3
- guard: constraints: /usr/local/lib/Man_In_Grey/Man_In_Grey/guard_emplace_bio.py
- guard: journal: 10 instructions
+ guard: version: 4
+ guard: constraints: /usr/local/lib/Man_In_Grey/Man_In_Grey/EmplaceBio·Guard.py
+ guard: journal: 0 variables, 10 instructions
guard: check_passed: wellformed
+ guard: check_passed: language
guard: constraint_broken: gate invoking account 'alice' is not allowed to give journals to this gate
gate: refused: by the guard
@@ -867,7 +1089,7 @@
Why it is safe
- - A journal names no file, and the guard refuses an instruction with any argument the language does not have, so a journal cannot point the interpreter anywhere. The page's name comes from account A's user name, which the gate had from the kernel as its real uid, and its directory from the administrator's constraints.
+ - A journal names no file, and the well-formed guard refuses an instruction with any argument its format does not have, so a journal cannot point the interpreter anywhere. The page's name comes from account A's user name, which the gate had from the kernel as its real uid, and its directory from the administrator's constraints.
- Once it has the journal, the gate gives up account A, so Thomas cannot stop the guard or the interpreter partway, and a page is never left half placed by him.
- The interpreter runs with the effective uid of http, not root, so even a fault in it reaches only what the web server's account can write.
- The page's text cannot become markup, since the interpreter escapes it and writes every tag itself. The page cannot become server code either: its name always ends in .html, never .php or .htaccess.
@@ -886,6 +1108,9 @@
When an interpreter crashes, fails, or breaks the gate's output rules partway through a journal, what it did before then stays done, and the gate can only log the failure. Playing a journal as a transaction is on the road map.
+
+ The gate writes the result to descriptor 3 after the interpreter has finished, and waits while the invoking program's end of it is full. A program that opens descriptor 3 and never reads it leaves the gate waiting, and once the gate has isolated itself, account A cannot end it; root can. The journal has been played by then, so the wait delays only the gate's exit. A deadline, , would bound it.
+
The gate learns only how the interpreter ended, and what it wrote. Its verdict line says it reached its end; whether the files it made are what the journal meant is the interpreter's own claim, so the interpreter, like the guard, is installed code the application developer writes, and is to be tested as such.
@@ -913,38 +1138,64 @@
setcap stores chosen capabilities in a program file's extended attributes. A process running that file holds them while its real and effective uid stay account A's. Capabilities are normally lost when a program execs one whose file holds none, as the gate does when it starts Python; the gate keeps them by raising them as ambient capabilities first, which survive the exec. A gate for a language that places files owned by other accounts would then be able to write files and change owners, and nothing else. The gain is uneven: CAP_SYS_ADMIN, needed for mounting, is nearly root by itself.
-
- Namespace in place of index symbol
+
+ Deadline
- A symbol is now the index of the instruction that produces its value, so each instruction produces at most one value, and a value can be named only by a composer that holds its symbol. An alternative is to have instructions write their values into namespaces held by the journal interpreter, and later instructions read them by name.
+ Once the gate has isolated itself, account A cannot signal it, which is the point of isolation. A guard or interpreter that hangs therefore holds the gate, and the request with it, until root ends it. Waiting for a lock is not the case to fear, since a language's code can bound its own waits; SQLite's busy_timeout, for instance, turns a long wait for a database lock into an error. The case is a call that blocks in the kernel, such as a mount on a device that does not answer, or a loop. The resource limit on CPU time does not catch it, since a process blocked in the kernel uses no CPU.
- A namespace is a dictionary. An instruction that produces values is given the namespace and the keys to write them under, and a later instruction names a namespace and a key where it needs a value. A key can name another namespace, so namespaces form a tree with one root namespace; a value is then named by its path, such as ("unbound" ,"file"), as a file is named by its path. The namespace is the scope, so the journal needs no scoping model of its own beyond the tree.
+ The gate already waits on its children's pipes, so a deadline is a timeout on that wait: when it passes, the gate kills the child and reports the journal failed, as it does for a child that breaks its rules. The deadline is to be compiled into the gate, as the guard's path is, or set by the constraints, never by account A: a short deadline chosen by account A would let it stop an interpreter partway, which isolation exists to prevent. An interpreter stopped by the deadline leaves its work partly done, as one that crashes does, which is what transactional play, , would remedy.
+
+
+
+ Field of a variable
- What the approach gives:
+ An instruction can write a variable of a dictionary type, holding several values under its keys, but a later instruction can read only a whole variable. An argument that reads one field, ["variable" ,<name> ,<key> ...], the keys a path into the value, would let an instruction take, say, the uid of an account another instruction made. The path is a list, never a dotted string, since a dotted string would have to be scanned for its dots, and names hold dots. The well-formed guard would check the path against the variable's type as it checks the variable now.
-
- - Several outputs from one instruction. An instruction can write each of its results under its own key, as a destructuring assignment does. An instruction that makes an account, for example, could give its uid, gid and home under three keys.
- - Values shared between composers. Composers run into one journal share no program variables, so one composer cannot hold another's symbol. A value written under a name one composer chooses can be read by another that knows the name.
- - A journal that reads by names. A printed journal would show what each value is, not only which instruction made it.
-
+
+
+
+ Variables shared between composers
- What the approach costs: the interpreter would hold state that instructions change, and the guard would have to know what a key holds at each point in the journal before anything runs. That stays as simple as it is with indices only under two rules: every key is written once, and every key is written before it is read. With those rules a path of keys can make its namespaces as it is first written, so no instruction for making namespaces is needed. Declarations, if they were wanted, would be instructions every instruction language carries.
+ A journal is made by one composer in this version. The first version ran a directory of composers into one journal, each composer standing for one configuration file; with variables, several composers into one journal would each need variables of their own, and some variables shared with others. That is a scoping layer, declarations describing which variables pass between which composers. Its simplest form keeps every composer's variables private to it, scoped by the composer's path, with no sharing, and allows a result only in a journal of one composer.
+
+
+
+ Interface between arguments and functions
- A smaller step gives the first advantage alone: a symbol that names a field of its instruction's value, as {"symbol": 3 ,"field": "uid"}, keeps index symbols and lets one instruction produce several values.
+ The interpreter calls a language's function with copies of its inputs and boxes for its outputs, and checks what the boxes hold after the call. That is the whole of the interface between a journal's arguments and the code that carries an instruction out. A function interface type, declared with the formats, could give that interface a form of its own: how each argument is passed, what the function returns, how it reports failure. The questions it is to answer are not yet settled.
+
+
+
+
+ Session over a pair of gates
+
+ A gate is a function call: the invoking program waits, and the kernel ties the result to the call, since it arrives on the descriptor of the gate the program started. Some work finishes after the request, or is between parties that do not share a process lifetime, such as a request against storage not yet attached, or a request to an authority on another machine. For that, two gates, one in each direction, can carry a dialog: the request carries a transaction identifier, and the response carries it back to be recognized.
+
+
+ What the two gates lack is a session, the state common to the request and its response. A synchronous call keeps it in the variables still in scope when the call returns; a dialog has to keep it in the requesting party's own records, as an outstanding request under its identifier. Sending the state with the request, to be sent back with the response, is not a sound trust model, since the responder could alter it. Built on the function call as its base:
+
+
+ - the requesting party records each outstanding request, and a response is checked against that record, so the responder can complete only the requests that were made, in the shape that was expected;
+ - the identifier is random, so the responder cannot answer a request it never received, and one-shot, deleted at the first response, so a replayed response finds nothing to answer;
+ - a response that never comes needs a time-out, which needs a clock: deadlines checked only when a journal arrives fire only while there is traffic;
+ - a gate into each person's account would be a setuid program for each account; the cheaper forms are delivery by root, or a responder's outbox the requesting account reads.
+
+
+ No identifier makes a responder more trustworthy than it is. Swapping two responses is a lie about which request each answers, and the defense against a lie is checking the answer against the request and against facts the responder cannot forge.
Group and setgid
- The gate gives up account A's uid when it isolates itself, , but not account A's real gid or supplementary groups. Signals are permitted by uid alone, so the groups do not let account A interrupt it, and keeping them lets a constraint ask about account A's groups with Constraint.has_real_group. Three pieces of work remain:
+ The gate gives up account A's uid when it isolates itself, , but not account A's real gid or supplementary groups. Signals are permitted by uid alone, so the groups do not let account A interrupt it, and keeping them lets a constraint ask about account A's groups with Guard·LibraryConstraint.has_real_group. Three pieces of work remain:
- Recording account A's groups. If the gate gave up account A's gids too, with setregid and setgroups, so that nothing of account A's identity remained in the process, it would record the groups first, as it records the uid, and has_real_group would read them from the record.
- - A constraint by group. Constraint.is_group_member and has_real_group let a guard admit the members of a group, but the example's guard admits accounts by name; a group, such as bio, is the more natural grant on a real host, and the example could show it.
+ - A constraint by group. Guard·LibraryConstraint.is_group_member and has_real_group let a guard admit the members of a group, but the example's guard admits accounts by name; a group, such as bio, is the more natural grant on a real host, and the example could show it.
- Which group the interpreter's files take. A setgid gate makes its files in the group install_gate gives it. A language that places files for several groups would need the interpreter to set each file's group, which an account B other than root can do only to the groups account B belongs to.
@@ -990,6 +1241,9 @@
The first version had configuration programs calling a planner, a plan, a fixed set of operations, copy, displace and delete, an inner executor, and one gate, man_in_grey_apply, that accepted --inner <file> from its caller. That option let any account in the group sudo run a program of its choosing with root's rights, without a password. The present design fixes the interpreter's path in its gate, moves the decision about which accounts can give journals into the constraints, raises an error in the composer on a wrong argument where the first version dropped the operation silently, replaces a scope fence that the gate's change of directory made ineffective with constraints naming each file, removes temporary files after a failure, and gives a second backup within one second a name of its own.
+
+ Journal version 4 replaces version 3. In version 3 an instruction's arguments were a map by name, a value produced by an instruction was a symbol, the map {"symbol": <index>}, recognized by its shape, so that no argument could be a map, and each language wrote its own well-formed check. In version 4 a language is defined as data, a journal declares typed variables, an argument is a pair saying whether it is a literal or a variable, given by position, one well-formed guard serves every language, an instruction can write several variables, and a journal can have a result, handed back on descriptor 3. The composer directory is set aside until variables can be shared between composers.
+
diff --git a/tester/authored/emplace_bio/composer/bio.py b/tester/authored/emplace_bio/composer/bio.py
index ca8e37c..3ac6c0a 100644
--- a/tester/authored/emplace_bio/composer/bio.py
+++ b/tester/authored/emplace_bio/composer/bio.py
@@ -16,4 +16,4 @@ def compose(prov ,journal_iface):
journal_iface.paragraph()
for content ,style in paragraph:
journal_iface.text(content ,style)
- journal_iface.picture((Path(prov.composer_root_dpath).parent / "picture.png").read_bytes())
+ journal_iface.picture((Path(prov.composer_dir_dpath).parent / "picture.png").read_bytes())
diff --git a/tester/authored/emplace_bio/composer_jpeg/bio.py b/tester/authored/emplace_bio/composer_jpeg/bio.py
index e35fd2c..26f5ce1 100644
--- a/tester/authored/emplace_bio/composer_jpeg/bio.py
+++ b/tester/authored/emplace_bio/composer_jpeg/bio.py
@@ -7,4 +7,4 @@ def compose(prov ,journal_iface):
journal_iface.display_name("A. Tester")
journal_iface.paragraph()
journal_iface.text("Now with a JPEG." ,[])
- journal_iface.picture((Path(prov.composer_root_dpath).parent / "picture.jpg").read_bytes())
+ journal_iface.picture((Path(prov.composer_dir_dpath).parent / "picture.jpg").read_bytes())
diff --git a/tester/authored/emplace_bio/run b/tester/authored/emplace_bio/run
index f71343e..05356ed 100755
--- a/tester/authored/emplace_bio/run
+++ b/tester/authored/emplace_bio/run
@@ -6,25 +6,26 @@ run
The emplace_bio test of Man_In_Grey's example language. It copies the
promoted Man_In_Grey, from $REPO_HOME/consumer, into its scratchpad
directory, puts constraints of its own at the top of the copy's
-guard_emplace_bio.py, placing pages in its scratchpad, and runs that copy
+EmplaceBio·Guard.py, placing pages in its scratchpad, and runs that copy
through the emplace_bio gate, not setuid. It checks:
- page orchestrate composer/: .html is compared with
+ page orchestrate composer/bio.py: .html is compared with
expected/page.html, every text in it escaped, every style one tag,
and .png is the given picture
- jpeg orchestrate composer_jpeg/: the JPEG is placed as .jpg, and
+ jpeg orchestrate composer_jpeg/bio.py: the JPEG is placed as .jpg, and
the PNG an earlier run placed is removed
- none orchestrate composer_no_picture/: the page shows no picture, and the
+ none orchestrate composer_no_picture/bio.py: the page shows no picture, and the
JPEG is removed
- check_only orchestrate composer/ with check_only: approved, and nothing changes
+ check_only orchestrate composer/bio.py with check_only: approved, and nothing changes
and, with journals made without the journal interface and given to the gate
directly, as a program other than the orchestrator could:
- name a text instruction with a file name among its arguments: refused by
- the wellformed check, since the language has no way to name a file
- style a style the language does not have: refused by the wellformed check
- order text before any paragraph: refused by the wellformed check
+ name a text instruction with a file name as a third argument: refused by
+ the well-formed guard, since its format takes two, and the
+ language has no way to name a file
+ style a style the language does not have: refused by the language check
+ order text before any paragraph: refused by the language check
svg an SVG picture: refused by the content check
long more text than is_allowed_text_size allows: refused by the constraint check
account an account is_allowed_account does not allow: refused by the constraint check
@@ -56,9 +57,11 @@ import sys ,os ,stat ,json ,shutil ,subprocess ,getpass ,urllib.parse ,signal
sys.dont_write_bytecode = True
from pathlib import Path
+import cbor2
+
TEST_NAME = "emplace_bio"
TEST_DPATH = Path(__file__).resolve().parent
-GATE_NAME = "Man_In_Grey_emplace_bio_gate"
+GATE_NAME = "Man_In_Grey_gate·emplace_bio"
# --------------------------------------------------------------------------------
@@ -129,15 +132,15 @@ def make_tree(consumer ,tree):
def put_constraint(guard_fpath ,constraint):
"replace the predicates between the guard's constraint marks with constraint"
- line_list = guard_fpath.read_text(encoding="utf-8").splitlines()
- begin ,end = line_list.index("# constraints begin") ,line_list.index("# constraints end")
- text = "\n".join(line_list[:begin + 1] + ["" ,constraint.strip("\n") ,""] + line_list[end:]) + "\n"
+ list_line = guard_fpath.read_text(encoding="utf-8").splitlines()
+ begin ,end = list_line.index("# constraints begin") ,list_line.index("# constraints end")
+ text = "\n".join(list_line[:begin + 1] + ["" ,constraint.strip("\n") ,""] + list_line[end:]) + "\n"
guard_fpath.unlink()
guard_fpath.write_text(text ,encoding="utf-8")
os.chmod(guard_fpath ,0o644)
def write_constraint(tree ,work_dpath ,account ,text_max_bytes=8192):
- "the test's constraints in its copy of guard_emplace_bio.py, giving account a page in work_dpath/bio"
+ "the test's constraints in its copy of EmplaceBio·Guard.py, giving account a page in work_dpath/bio"
text = (
"# written by the emplace_bio test\n\n"
"def is_allowed_account(user_name):\n"
@@ -149,7 +152,7 @@ def write_constraint(tree ,work_dpath ,account ,text_max_bytes=8192):
"def page_directory():\n"
f" return {str(work_dpath / 'bio')!r}\n"
)
- put_constraint(tree / "Man_In_Grey" / "guard_emplace_bio.py" ,text)
+ put_constraint(tree / "Man_In_Grey" / "EmplaceBio·Guard.py" ,text)
def run_Man_In_Grey(program ,gate ,path ,work_dpath ,*option):
command = [str(program) ,"orchestrate" ,str(path) ,"gate" ,str(gate) ,*option]
@@ -178,15 +181,24 @@ def leave_sigterm_pending():
signal.pthread_sigmask(signal.SIG_BLOCK ,{signal.SIGTERM})
os.kill(os.getpid() ,signal.SIGTERM)
-def hand_made(Journal ,instruction_list):
- "a journal of the emplace_bio language holding the instructions given as (name ,argument)"
- journal = Journal.Journal("emplace_bio")
- for name ,argument in instruction_list:
- journal.place(name ,argument)
- return journal.encode()
+def hand_made(definition_hash ,instruction_list):
+ """
+ A journal of the emplace_bio language holding the instructions given as
+ (operation ,[value ...]), every value a literal, made as data and encoded,
+ with no journal interface to check it.
+ """
+ return cbor2.dumps({
+ "version": 4
+ ,"instruction_language": "emplace_bio"
+ ,"definition_hash": definition_hash
+ ,"meta": {}
+ ,"list_variable": []
+ ,"result": None
+ ,"list_instruction": [{"operation": o ,"list_argument": [["literal" ,v] for v in value]} for o ,value in instruction_list]
+ } ,canonical=True)
def good_page(*extra):
- return [("display_name" ,{"text": "A. Tester"}) ,("paragraph" ,{}) ,("text" ,{"content": "Hello." ,"style": []}) ,*extra]
+ return [("display_name" ,["A. Tester"]) ,("paragraph" ,[]) ,("text" ,["Hello." ,[]]) ,*extra]
def test(repo_home):
consumer = repo_home / "consumer"
@@ -209,16 +221,18 @@ def test(repo_home):
shutil.rmtree(work_dpath)
(work_dpath / "bio").mkdir(parents=True)
make_tree(consumer ,tree)
- # the journals the test makes by hand use the copy's Journal module, so it is imported once the copy exists
+ # the journals the test makes by hand carry the hash of the copy's definition, so it is imported once the copy exists
sys.path.insert(0 ,str(tree / "Man_In_Grey"))
- import Journal
+ import importlib ,Language
+ language_module = importlib.import_module("EmplaceBio·Language")
+ definition_hash = Language.Definition(language_module.NAME ,language_module.DEFINITION).hash
person = getpass.getuser()
write_constraint(tree ,work_dpath ,person)
bio = work_dpath / "bio"
page_fpath ,png ,jpg = bio / f"{person}.html" ,bio / f"{person}.png" ,bio / f"{person}.jpg"
# page
- status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer" / "bio.py" ,work_dpath)
if not check_status(log ,"page Man_In_Grey exits 0" ,status ,0 ,output):
return log.summary()
expected = (TEST_DPATH / "expected" / "page.html").read_text(encoding="utf-8").replace("USER_NAME" ,urllib.parse.quote(person))
@@ -231,35 +245,35 @@ def test(repo_home):
log.check(modes == [0o644 ,0o644] ,"page files have permission 0644" ,None if modes == [0o644 ,0o644] else f"{[oct(m) for m in modes]}")
# jpeg, then no picture
- status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer_jpeg" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer_jpeg" / "bio.py" ,work_dpath)
check_status(log ,"jpeg Man_In_Grey exits 0" ,status ,0 ,output)
log.check(jpg.is_file() and not png.exists() and f'src="{urllib.parse.quote(person)}.jpg"' in page_fpath.read_text()
,"jpeg places the JPEG and removes the PNG")
- status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer_no_picture" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,gate ,TEST_DPATH / "composer_no_picture" / "bio.py" ,work_dpath)
check_status(log ,"none Man_In_Grey exits 0" ,status ,0 ,output)
log.check(not jpg.exists() and not png.exists() and "
.YYYYMMDDTHHMMSSZ, the replace file overwritten with no
backup; compared with expected/run_2
create orchestrate composer_create/new.py: a file opened in create mode is written
once, and a second run is refused by the guard's filesystem check
- refused orchestrate composer_refused/: a journal opening one allowed file and one the
- constraints do not allow is refused whole by the guard's constraint check
- argument orchestrate composer_argument/: a composer giving a wrong mode fails when it
- places the instruction, and no journal reaches the gate
- writable orchestrate a composer directory holding a composer other accounts can write:
- the orchestrator refuses to run it
- check_only orchestrate composer/ with check_only: the guard approves the journal, and the
- interpreter is not run
- limit orchestrate composer/ with a file size limit the gate cannot raise: the gate
- refuses before starting the guard
- silent orchestrate composer/ through an interpreter that exits 0 without
+ refused orchestrate composer_refused/allowed_and_not.py: a journal opening one allowed
+ file and one the constraints do not allow is refused whole by the guard's
+ constraint check
+ type orchestrate composer_type/wrong_type.py: a literal of the wrong type fails when
+ the composer places the instruction, and no journal reaches the gate
+ mode orchestrate composer_mode/wrong_mode.py: a mode the language does not have is
+ refused by the guard's language check
+ directory orchestrate composer/, a directory: refused, as this version runs one
+ composer, given as a file
+ writable orchestrate a composer other accounts can write: the orchestrator refuses
+ to run it
+ check_only orchestrate composer/config.py with check_only: the guard approves the journal,
+ and the interpreter is not run
+ limit orchestrate composer/config.py with a file size limit the gate cannot raise: the
+ gate refuses before starting the guard
+ silent orchestrate composer/config.py through an interpreter that exits 0 without
writing its verdict line: the gate reports the run failed
+and, with journals made without the journal interface and given to the gate
+directly, as a program other than the orchestrator could:
+
+ unwritten a write of a file variable no open wrote: refused by the well-formed
+ guard, as the variable is read before it is written
+ literal a write given a literal where the format reads and writes its file:
+ refused by the well-formed guard, as a literal cannot be written
+ surplus an unlink with a third argument: refused by the well-formed guard
+ undeclared a write of a variable the journal does not declare: refused by the
+ well-formed guard
+ hash a journal composed against another definition: refused by the
+ well-formed guard
+ version a journal of version 3: the guard cannot read it
+ discarded a journal whose result the gate cannot hand back, since the program
+ invoking it left descriptor 3 closed: played, and the result discarded
+
A refused run is to leave every file as it was. Backup names carry the time
of the run, so names are compared with the time replaced by the letters
YYYYMMDDTHHMMSSZ. Content, mode and owner are compared too.
@@ -47,10 +70,13 @@ Usage: run
"""
import sys ,os ,re ,stat ,json ,shutil ,subprocess ,getpass ,resource
+sys.dont_write_bytecode = True
from pathlib import Path
+import cbor2
+
TEST_NAME = "smoke"
-GATE_NAME = "Man_In_Grey_smoke_gate"
+GATE_NAME = "Man_In_Grey_gate·smoke"
TEST_DPATH = Path(__file__).resolve().parent
OUTPUT_DIR = ("out" ,"out/net" ,"out/web")
@@ -167,6 +193,23 @@ def run_Man_In_Grey(program ,path ,work_dpath ,*option ,preexec=None):
result = subprocess.run(command ,cwd=work_dpath ,capture_output=True ,text=True ,preexec_fn=preexec)
return result.returncode ,(result.stdout + result.stderr).strip()
+def run_gate(gate ,journal_bytes):
+ "a journal given to the gate directly, with no orchestrator and descriptor 3 closed"
+ result = subprocess.run([str(gate) ,"admit"] ,input=journal_bytes ,capture_output=True)
+ return result.returncode ,(result.stdout + result.stderr).decode("utf-8" ,"replace").strip()
+
+def hand_made(definition_hash ,variable ,instruction_list ,result=None ,version=4):
+ "a journal of the smoke language, made as data and encoded, with no journal interface to check it"
+ return cbor2.dumps({
+ "version": version
+ ,"instruction_language": "smoke"
+ ,"definition_hash": definition_hash
+ ,"meta": {}
+ ,"list_variable": variable
+ ,"result": result
+ ,"list_instruction": [{"operation": o ,"list_argument": a} for o ,a in instruction_list]
+ } ,canonical=True)
+
def check_status(log ,what ,status ,want ,output ,want_text=None):
passed = status == want and (want_text is None or want_text in output)
detail = None
@@ -197,15 +240,15 @@ def make_tree(consumer ,tree):
def put_constraint(guard_fpath ,constraint):
"replace the predicates between the guard's constraint marks with constraint"
- line_list = guard_fpath.read_text(encoding="utf-8").splitlines()
- begin ,end = line_list.index("# constraints begin") ,line_list.index("# constraints end")
- text = "\n".join(line_list[:begin + 1] + ["" ,constraint.strip("\n") ,""] + line_list[end:]) + "\n"
+ list_line = guard_fpath.read_text(encoding="utf-8").splitlines()
+ begin ,end = list_line.index("# constraints begin") ,list_line.index("# constraints end")
+ text = "\n".join(list_line[:begin + 1] + ["" ,constraint.strip("\n") ,""] + list_line[end:]) + "\n"
guard_fpath.unlink()
guard_fpath.write_text(text ,encoding="utf-8")
os.chmod(guard_fpath ,0o644)
def write_constraint(tree ,work_dpath):
- "the test's constraints in its copy of guard_smoke.py, allowing the files in ALLOWED to the person running the test"
+ "the test's constraints in its copy of Smoke·Guard.py, allowing the files in ALLOWED to the person running the test"
person = getpass.getuser()
allowed = {(str(work_dpath / d) ,n): (m ,int(p ,8)) for d ,n ,m ,p in ALLOWED}
text = (
@@ -221,13 +264,15 @@ def write_constraint(tree ,work_dpath):
" entry = ALLOWED.get((directory ,name))\n"
" return entry is not None and \"unlink\" in entry[0]\n"
)
- put_constraint(tree / "Man_In_Grey" / "guard_smoke.py" ,text)
+ put_constraint(tree / "Man_In_Grey" / "Smoke·Guard.py" ,text)
def test(repo_home):
consumer = repo_home / "consumer"
work_dpath = (repo_home / "tester" / "scratchpad" / TEST_NAME).resolve()
tree = work_dpath / "tree"
program = tree / "Man_In_Grey" / "Man_In_Grey"
+ gate = tree / "Man_In_Grey_gate" / GATE_NAME
+ config = TEST_DPATH / "composer" / "config.py"
print(f"run test in {TEST_DPATH}")
print(f" program {program}")
print(f" writes {work_dpath}")
@@ -247,11 +292,20 @@ def test(repo_home):
(work_dpath / "out" / "obsolete.conf").write_text("to be unlinked\n")
out = work_dpath / "out"
+ # the hand-made journals use the copy's definition, so it is imported once the copy exists
+ sys.path.insert(0 ,str(tree / "Man_In_Grey"))
+ import importlib ,Language
+ language_module = importlib.import_module("Smoke·Language")
+ definition_hash = Language.Definition(language_module.NAME ,language_module.DEFINITION).hash
+
# run_1 and run_2
for run_name in ("run_1" ,"run_2"):
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,config ,work_dpath)
if check_status(log ,f"{run_name} Man_In_Grey exits 0" ,status ,0 ,output):
compare(log ,run_name ,work_dpath)
+ if run_name == "run_1":
+ want = "result: summary {'open_count': 3, 'unlink_count': 1}"
+ log.check(want in output ,"run_1 result is the summary" ,None if want in output else f"the output does not say {want!r}")
else:
log.note(f"{run_name}: the files were not compared, since Man_In_Grey failed")
return log.summary()
@@ -269,51 +323,89 @@ def test(repo_home):
# a journal the constraints refuse
before = snapshot(out)
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer_refused" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer_refused" / "allowed_and_not.py" ,work_dpath)
check_status(log ,"refused is refused by the constraint check" ,status ,1 ,output ,"constraint_broken:")
log.check(snapshot(out) == before ,"refused changes nothing, the allowed file included")
- # a composer with a wrong argument
+ # a literal of the wrong type, refused in the composer
+ before = snapshot(out)
+ status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer_type" / "wrong_type.py" ,work_dpath)
+ check_status(log ,"type fails in the composer" ,status ,2 ,output ,"wrong_type.py: @0 open argument 4: the literal is str, not integer")
+ log.check(snapshot(out) == before ,"type changes nothing")
+
+ # a mode the language does not have, refused by the guard
before = snapshot(out)
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer_argument" ,work_dpath)
- check_status(log ,"argument fails in the composer" ,status ,2 ,output ,"wrong_mode.py: open: mode 'overwrite'")
- log.check(snapshot(out) == before ,"argument changes nothing")
+ status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer_mode" / "wrong_mode.py" ,work_dpath)
+ check_status(log ,"mode is refused by the language check" ,status ,1 ,output ,"language_broken: @0 open: mode 'overwrite'")
+ log.check(snapshot(out) == before ,"mode changes nothing")
+
+ # a composer directory
+ status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath)
+ check_status(log ,"directory is refused by the orchestrator" ,status ,2 ,output ,"runs one composer, given as a file")
# a composer other accounts can write
writable = work_dpath / "composer_writable"
writable.mkdir()
- shutil.copy(TEST_DPATH / "composer" / "unbound_conf.py" ,writable / "unbound_conf.py")
- os.chmod(writable / "unbound_conf.py" ,0o664)
+ shutil.copy(config ,writable / "config.py")
+ os.chmod(writable / "config.py" ,0o664)
before = snapshot(out)
- status ,output = run_Man_In_Grey(program ,writable ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,writable / "config.py" ,work_dpath)
check_status(log ,"writable composer is refused by the orchestrator" ,status ,2 ,output ,"writable by group or other accounts")
log.check(snapshot(out) == before ,"writable changes nothing")
# check_only
before = snapshot(out)
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath ,"check_only")
+ status ,output = run_Man_In_Grey(program ,config ,work_dpath ,"check_only")
check_status(log ,"check_only is approved by the guard" ,status ,0 ,output ,"approved: guard smoke check passed")
log.check(snapshot(out) == before ,"check_only changes nothing")
# a resource limit the gate cannot raise
before = snapshot(out)
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath ,preexec=limit_file_size)
+ status ,output = run_Man_In_Grey(program ,config ,work_dpath ,preexec=limit_file_size)
check_status(log ,"limit is refused by the gate" ,status ,2 ,output ,"the file size (bytes) limit is 1048576")
log.check(snapshot(out) == before ,"limit changes nothing")
# an interpreter that exits 0 without its verdict line
- interpreter = tree / "Man_In_Grey" / "interpreter_smoke.py"
+ interpreter = tree / "Man_In_Grey" / "Smoke·Interpreter.py"
text = interpreter.read_text(encoding="utf-8")
- verdict_line = ' sys.stdout.write("interpreter smoke journal played\\n")\n'
+ verdict_line = ' sys.stdout.write("interpreter smoke journal played\\n")\n'
+ if verdict_line not in text:
+ log.check(False ,"silent finds the verdict line to remove" ,"Smoke·Interpreter.py has changed")
interpreter.unlink()
- interpreter.write_text(text.replace(verdict_line ," pass\n") ,encoding="utf-8")
+ interpreter.write_text(text.replace(verdict_line ," pass\n") ,encoding="utf-8")
os.chmod(interpreter ,0o644)
- status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath)
+ status ,output = run_Man_In_Grey(program ,config ,work_dpath)
check_status(log ,"silent is reported failed by the gate" ,status ,2 ,output ,"without writing the one line 'interpreter smoke journal played'")
interpreter.unlink()
interpreter.write_text(text ,encoding="utf-8")
os.chmod(interpreter ,0o644)
+ # journals given to the gate directly
+ out_text = str(out)
+ person = getpass.getuser()
+ open_web = ("open" ,[["literal" ,out_text + "/web"] ,["literal" ,"nginx.conf"] ,["literal" ,"replace_keep"]
+ ,["literal" ,person] ,["literal" ,0o644] ,["variable" ,"f"]])
+ list_case = (
+ ("unwritten" ,hand_made(definition_hash ,[["f" ,"file"]] ,[("write" ,[["variable" ,"f"] ,["literal" ,b"x"]])])
+ ,1 ,"read before any instruction writes it")
+ ,("literal" ,hand_made(definition_hash ,[] ,[("write" ,[["literal" ,0] ,["literal" ,b"x"]])])
+ ,1 ,"a literal is given where the format writes (in-out)")
+ ,("surplus" ,hand_made(definition_hash ,[] ,[("unlink" ,[["literal" ,out_text] ,["literal" ,"obsolete.conf"] ,["literal" ,"x"]])])
+ ,1 ,"3 arguments; the format takes 2")
+ ,("undeclared" ,hand_made(definition_hash ,[["f" ,"file"]] ,[open_web ,("write" ,[["variable" ,"g"] ,["literal" ,b"x"]])])
+ ,1 ,"the variable 'g' is not declared")
+ ,("hash" ,hand_made("0" * 64 ,[] ,[("unlink" ,[["literal" ,out_text] ,["literal" ,"obsolete.conf"]])])
+ ,1 ,"composed against another definition")
+ ,("version" ,hand_made(definition_hash ,[] ,[] ,version=3) ,2 ,"journal_unreadable: journal version 3")
+ )
+ for name ,journal_bytes ,want_status ,want_text in list_case:
+ before = snapshot(out)
+ status ,output = run_gate(gate ,journal_bytes)
+ check_status(log ,f"{name} is refused" ,status ,want_status ,output ,want_text)
+ log.check(snapshot(out) == before ,f"{name} changes nothing")
+ status ,output = run_gate(gate ,hand_made(definition_hash ,[["s" ,"summary"]] ,[("summary" ,[["variable" ,"s"]])] ,result="s"))
+ check_status(log ,"discarded is played, its result discarded" ,status ,0 ,output ,"result_discarded:")
+
return log.summary()
def clean(repo_home):