From d2776ef28a2f1c01e802315337bfb1a6bb1400f9 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Fri, 6 Nov 2026 14:48:30 +0000 Subject: [PATCH] . --- developer/authored/Man_In_Grey.copy/Guard.py | 2 +- .../authored/Man_In_Grey.copy/Interpreter.py | 2 +- .../authored/Man_In_Grey.copy/Man_In_Grey.py | 16 +- .../{FileLanguage.py => SmokeLanguage.py} | 17 +- .../{guard_file.py => guard_smoke.py} | 16 +- ...terpreter_file.py => interpreter_smoke.py} | 16 +- .../Man_In_Grey_file_gate.CLI.c | 9 - .../Man_In_Grey_smoke_gate.CLI.c | 9 + document/Man_In_Grey.html | 233 ++++++++---------- .../composer/DNS/unbound.conf.py | 0 .../composer/obsolete.py | 0 .../composer/unbound_conf.py | 0 .../composer/web/site_conf.py | 0 .../composer_argument/wrong_mode.py | 0 .../composer_create/new.py | 0 .../composer_refused/allowed_and_not.py | 0 .../expected/run_1/out/net/unbound.conf | 0 .../expected/run_1/out/unbound_conf | 0 .../expected/run_1/out/web/nginx.conf | 0 .../expected/run_2/out/net/unbound.conf | 0 .../expected/run_2/out/unbound_conf | 0 .../run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ | 0 .../expected/run_2/out/web/nginx.conf | 0 .../run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ | 0 tester/authored/{write_file => smoke}/run | 26 +- 25 files changed, 158 insertions(+), 188 deletions(-) rename developer/authored/Man_In_Grey.copy/{FileLanguage.py => SmokeLanguage.py} (95%) rename developer/authored/Man_In_Grey.copy/{guard_file.py => guard_smoke.py} (91%) rename developer/authored/Man_In_Grey.copy/{interpreter_file.py => interpreter_smoke.py} (84%) delete mode 100644 developer/authored/Man_In_Grey_gate.make/Man_In_Grey_file_gate.CLI.c create mode 100644 developer/authored/Man_In_Grey_gate.make/Man_In_Grey_smoke_gate.CLI.c rename tester/authored/{write_file => smoke}/composer/DNS/unbound.conf.py (100%) rename tester/authored/{write_file => smoke}/composer/obsolete.py (100%) rename tester/authored/{write_file => smoke}/composer/unbound_conf.py (100%) rename tester/authored/{write_file => smoke}/composer/web/site_conf.py (100%) rename tester/authored/{write_file => smoke}/composer_argument/wrong_mode.py (100%) rename tester/authored/{write_file => smoke}/composer_create/new.py (100%) rename tester/authored/{write_file => smoke}/composer_refused/allowed_and_not.py (100%) rename tester/authored/{write_file => smoke}/expected/run_1/out/net/unbound.conf (100%) rename tester/authored/{write_file => smoke}/expected/run_1/out/unbound_conf (100%) rename tester/authored/{write_file => smoke}/expected/run_1/out/web/nginx.conf (100%) rename tester/authored/{write_file => smoke}/expected/run_2/out/net/unbound.conf (100%) rename tester/authored/{write_file => smoke}/expected/run_2/out/unbound_conf (100%) rename tester/authored/{write_file => smoke}/expected/run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ (100%) rename tester/authored/{write_file => smoke}/expected/run_2/out/web/nginx.conf (100%) rename tester/authored/{write_file => smoke}/expected/run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ (100%) rename tester/authored/{write_file => smoke}/run (93%) diff --git a/developer/authored/Man_In_Grey.copy/Guard.py b/developer/authored/Man_In_Grey.copy/Guard.py index ec2d5bc..13db77b 100644 --- a/developer/authored/Man_In_Grey.copy/Guard.py +++ b/developer/authored/Man_In_Grey.copy/Guard.py @@ -9,7 +9,7 @@ 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_file.py and guard_emplace_bio.py. +and writes the verdict; see guard_emplace_bio.py. """ import Journal diff --git a/developer/authored/Man_In_Grey.copy/Interpreter.py b/developer/authored/Man_In_Grey.copy/Interpreter.py index e1c95b0..18368e4 100644 --- a/developer/authored/Man_In_Grey.copy/Interpreter.py +++ b/developer/authored/Man_In_Grey.copy/Interpreter.py @@ -8,7 +8,7 @@ the journal is well formed, 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_file.py and interpreter_emplace_bio.py. +interpret(); see interpreter_emplace_bio.py. """ import Journal 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 e612eb6..2186782 100755 --- a/developer/authored/Man_In_Grey.copy/Man_In_Grey.py +++ b/developer/authored/Man_In_Grey.copy/Man_In_Grey.py @@ -11,18 +11,18 @@ guard passes the journal, the interpreter, which executes 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: the file language from -FileLanguage.py, emplace_bio from EmplaceBioLanguage.py. +gate's flags, and loads the module named for it: emplace_bio from +EmplaceBioLanguage.py. -Usage: Man_In_Grey [option ...] +Usage: Man_In_Grey gate [option ...] help | -h print this message (also printed when given no arguments) orchestrate 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 + gate the gate to invoke, which also sets the instruction + language the composers write in; required Options: - gate the gate to invoke; by default - ../Man_In_Grey_gate/Man_In_Grey_file_gate print print the journal after composing compose_only stop after composing; nothing reaches the gate check_only have the gate run the guard and not the interpreter @@ -47,7 +47,6 @@ import Journal # 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/. HERE_DPATH = Path(__file__).resolve().parent -DEFAULT_GATE = HERE_DPATH.parent / "Man_In_Grey_gate" / "Man_In_Grey_file_gate" FLAG = ("print" ,"compose_only" ,"check_only") VALUE = ("gate" ,) @@ -204,7 +203,10 @@ def compose_all(composer_root ,prov_list ,language): def orchestrate(path ,gate ,flag): path = Path(path).absolute() - gate_path = Path(gate).absolute() if gate else DEFAULT_GATE + if not gate: + print("error: orchestrate is given the gate to invoke, as gate " ,file=sys.stderr) + return 2 + gate_path = Path(gate).absolute() try: language = language_module(gate_flag(gate_path).get("language")) prov_list = composer_list(path) diff --git a/developer/authored/Man_In_Grey.copy/FileLanguage.py b/developer/authored/Man_In_Grey.copy/SmokeLanguage.py similarity index 95% rename from developer/authored/Man_In_Grey.copy/FileLanguage.py rename to developer/authored/Man_In_Grey.copy/SmokeLanguage.py index 14d9344..bc82634 100644 --- a/developer/authored/Man_In_Grey.copy/FileLanguage.py +++ b/developer/authored/Man_In_Grey.copy/SmokeLanguage.py @@ -1,7 +1,12 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -FileLanguage - a small instruction language for writing files, Man_In_Grey's -test language. It is a constrained form of the POSIX file interface. +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 @@ -24,7 +29,7 @@ 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_file.py: is_allowed_account, is_allowed_open and is_allowed_unlink. +guard_smoke.py: is_allowed_account, is_allowed_open and is_allowed_unlink. """ import os ,pwd ,stat ,time @@ -32,7 +37,7 @@ from pathlib import Path from Journal import JournalError ,Symbol ,has_control ,clean -NAME = "file" +NAME = "smoke" MODE = ("create" ,"replace" ,"replace_keep") @@ -83,7 +88,7 @@ def permission_value(value): class JournalInterface: """ - The journal interface of the file language. A composer is given one, and + 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. """ @@ -254,7 +259,7 @@ def execute(data ,log ,constraint): 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 file language needs nothing from its constraint module to execute. + an error. The smoke language needs nothing from its guard's constraints to execute. """ opened = {} # symbol index -> {dirfd ,fd ,tmp ,argument} try: diff --git a/developer/authored/Man_In_Grey.copy/guard_file.py b/developer/authored/Man_In_Grey.copy/guard_smoke.py similarity index 91% rename from developer/authored/Man_In_Grey.copy/guard_file.py rename to developer/authored/Man_In_Grey.copy/guard_smoke.py index 641c7cf..7790532 100644 --- a/developer/authored/Man_In_Grey.copy/guard_file.py +++ b/developer/authored/Man_In_Grey.copy/guard_smoke.py @@ -1,16 +1,16 @@ #!/usr/bin/env -S python3 -B # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -guard_file - the guard of the file instruction language. +guard_smoke - the guard of the smoke instruction language. -Started by the gate Man_In_Grey_file_gate, with the gate's real and +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 file check passed + guard smoke check passed and exits 0. That line and that status together are the only verdict the gate admits a journal on. @@ -22,7 +22,7 @@ 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_file +Usage: guard_smoke help | -h print this message (also printed when given no arguments) check check the journal on standard input @@ -65,11 +65,11 @@ sys.dont_write_bytecode = True import pwd ,stat from pathlib import Path -LANGUAGE = "file" +LANGUAGE = "smoke" VERSION = "3" HERE_DPATH = Path(__file__).resolve().parent -MODULE = ("Journal.py" ,"Guard.py" ,"Constraint.py" ,"FileLanguage.py" ,"guard_file.py") +MODULE = ("Journal.py" ,"Guard.py" ,"Constraint.py" ,"SmokeLanguage.py" ,"guard_smoke.py") def trusted_owner_problem(path): @@ -101,7 +101,7 @@ try: import Journal import Guard import Constraint - import FileLanguage as Language + import SmokeLanguage as Language except ImportError as error: say("cannot_start" ,str(error)) sys.exit(2) @@ -130,7 +130,7 @@ 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_file with no arguments for its usage") + say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run guard_smoke with no arguments for its usage") return 2 return check() diff --git a/developer/authored/Man_In_Grey.copy/interpreter_file.py b/developer/authored/Man_In_Grey.copy/interpreter_smoke.py similarity index 84% rename from developer/authored/Man_In_Grey.copy/interpreter_file.py rename to developer/authored/Man_In_Grey.copy/interpreter_smoke.py index 078ac8c..9cff4f4 100644 --- a/developer/authored/Man_In_Grey.copy/interpreter_file.py +++ b/developer/authored/Man_In_Grey.copy/interpreter_smoke.py @@ -1,9 +1,9 @@ #!/usr/bin/env -S python3 -B # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -interpreter_file - the interpreter of the file instruction language. +interpreter_smoke - the interpreter of the smoke instruction language. -Started by the gate Man_In_Grey_file_gate, with the gate's real and +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 output, as : , a line for each lasting change it @@ -13,13 +13,13 @@ The gate logs the journal's course itself. 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_file.py, for what execution needs from the constraints at its top. +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_file +Usage: interpreter_smoke help | -h print this message (also printed when given no arguments) execute execute the journal on standard input @@ -34,7 +34,7 @@ import stat from pathlib import Path HERE_DPATH = Path(__file__).resolve().parent -MODULE = ("Journal.py" ,"Interpreter.py" ,"Constraint.py" ,"FileLanguage.py" ,"guard_file.py" ,"interpreter_file.py") +MODULE = ("Journal.py" ,"Interpreter.py" ,"Constraint.py" ,"SmokeLanguage.py" ,"guard_smoke.py" ,"interpreter_smoke.py") def trusted_owner_problem(path): @@ -61,8 +61,8 @@ for each in [HERE_DPATH] + [HERE_DPATH / m for m in MODULE]: try: import Journal import Interpreter - import FileLanguage as Language - import guard_file as Guard_program + import SmokeLanguage as Language + import guard_smoke as Guard_program except ImportError as error: say("cannot_start" ,str(error)) sys.exit(2) @@ -86,7 +86,7 @@ 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_file with no arguments for its usage") + say("usage" ,f"unknown command {Journal.clean(repr(' '.join(argv)))}; run interpreter_smoke with no arguments for its usage") return 2 return execute() diff --git a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_file_gate.CLI.c b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_file_gate.CLI.c deleted file mode 100644 index 1659b7c..0000000 --- a/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_file_gate.CLI.c +++ /dev/null @@ -1,9 +0,0 @@ -/* - Man_In_Grey_file_gate - the gate of the file 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("file" ,"../Man_In_Grey/guard_file.py" ,"../Man_In_Grey/interpreter_file.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 new file mode 100644 index 0000000..1e01b77 --- /dev/null +++ b/developer/authored/Man_In_Grey_gate.make/Man_In_Grey_smoke_gate.CLI.c @@ -0,0 +1,9 @@ +/* + 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 accd582..e7ad8dc 100644 --- a/document/Man_In_Grey.html +++ b/document/Man_In_Grey.html @@ -46,6 +46,10 @@

This document is both the design of Man_In_Grey and the user manual, as Man_In_Grey is a programmer's tool.

+

+ All programs in Man_In_Grey are written with lead-in 'CLI' functions whose only job is to parse arguments and to format return values. Each CLI will call one or more functions to do the actual work. In this manner programs can be included as modules so as to reuse their helper functions, or to gain access to the main work functions. +

+ @@ -118,15 +122,14 @@ Symbol

- A composer writes instructions, but it does not run them. When an instruction would produce a result when run, as for example the file open instruction, which returns a file handle, 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. + 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.

- file = journal_iface.open("/etc/unbound" ,"unbound-US.conf" - ,"replace_keep" ,"root" ,0o644) - journal_iface.write(file ,"server:\n verbosity: 1\n") + account = journal_iface.make_account("Thomas·www") + journal_iface.add_to_group(account ,"http")

- In this example file is a result symbol. The value of file will be 0, i.e. the index into the journal of the open instruction. + 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.

@@ -175,23 +178,24 @@ 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.

- 2026-11-06 07:55:01 Z [:] Thomas·developer@IdealX - §/home/Thomas·developer/project/Man_In_Grey/consumer§ - > ./Man_In_Grey_gate/Man_In_Grey_file_gate flags - real.uid=2005 - real.user_name=Thomas·developer - real.gid=2005 - real.group_name=Thomas·developer - effective.uid=2005 - effective.user_name=Thomas·developer - effective.gid=2005 - effective.group_name=Thomas·developer - uid_differ=0 - gid_differ=0 - language=file - guard.path=/home/Thomas·developer/project/Man_In_Grey/consumer/Man_In_Grey/guard_file.py - interpreter.path=/home/Thomas·developer/project/Man_In_Grey/consumer/Man_In_Grey/interpreter_file.py + > /usr/local/lib/Man_In_Grey/Man_In_Grey_gate/Man_In_Grey_emplace_bio_gate flags + real.uid=2006 + real.user_name=Thomas + real.gid=2006 + real.group_name=Thomas + effective.uid=996 + effective.user_name=http + effective.gid=995 + effective.group_name=http + 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 +

+ 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. +

Th admit command takes the journal on standard input, with the option check_only. When the gate is asked to admit a journal, it: @@ -223,24 +227,24 @@ One gate per instruction language

- Man_In_Grey's general gate is the library module Gate.lib.c, whose function Man_In_Grey_gate·Gate·run takes the name of the instruction language, the guard's path and the interpreter's path, and the command line. An instruction language's gate is a *.CLI.c of three lines that calls it with the three fixed. The gate of the file language: + Man_In_Grey's general gate is the library module Gate.lib.c, whose function Man_In_Grey_gate·Gate·run takes the name of the instruction language, the guard's path and the interpreter's path, and the command line. An instruction language's gate is a *.CLI.c of three lines that calls it with the three fixed. The gate of the example language:

#include "Gate.lib.c" int main(int argc ,char **argv){ - return Man_In_Grey_gate·Gate·run("file" ,"../Man_In_Grey/guard_file.py" - ,"../Man_In_Grey/interpreter_file.py" ,argc ,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); }

- The general gate is curried, with its first three arguments supplied when the gate is compiled. Whoever invokes a gate can choose neither program it runs; if he could, he could give it a guard that approves everything. The language name holds no space and no control character, and the gate refuses to run when it does. Man_In_Grey has two gates: the file language's, above, and Man_In_Grey_emplace_bio_gate, the gate of the example language in . + The general gate is curried, with its first three arguments supplied when the gate is compiled. Whoever invokes a gate can choose neither program it runs; if he could, he could give it a guard that approves everything. The language name holds no space and no control character, and the gate refuses to run when it does.

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; 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 wellformed check refuses a directory, file name or owner 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; 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.

@@ -249,7 +253,7 @@ A setuid gate owned by root gives the interpreter every one of root's rights. Root's rights are a set of about forty separate capabilities, among them CAP_DAC_OVERRIDE, to ignore file permissions; CAP_CHOWN, to change owners; CAP_SYS_ADMIN, to mount; and CAP_SETUID, to become another uid. The design is to give each such gate only the capabilities its instruction language needs.

- 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 the file language 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. + 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.

@@ -272,7 +276,7 @@ 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's real uid is account A's, and its real gid and supplementary groups are those it inherited from the invoking program, since the setuid and setgid bits change only the effective ids. All three pass to the guard, which reads them from the kernel, so account A cannot misstate 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 file language's four checks are in . + The other checks a guard makes belong to its instruction language. The example language's four checks are in .

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. @@ -311,116 +315,73 @@ Interpreter

- 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. The interpreter does not check the constraints; that is the guard's work. It imports the guard program only for what execution needs from its constraints, as the example language's interpreter calls the guard's page_directory() for the directory its pages go in. It decodes the journal, checks once more that the journal is well formed, and then executes the instructions in order. + 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 interpreter writes to standard output, as <tag>: <message>, the highlights of what it does, as its developer chooses them, and the gate copies each line to the person and to the system log by the output rule. A line for each instruction would be impractical: a language that builds a file from many writes would log each, and a line that echoed an instruction's arguments would quote content, which can be long and does not belong in a log. The interpreters of Man_In_Grey write a line for each lasting change, such as a file put in place or removed, and none for an instruction as such. The gate logs the journal's course itself, so the log shows each journal received, approved and interpreted whatever an interpreter writes. The interpreter's exit status is 0 when it finished the journal, 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's exit status is 0 when it finished the journal, 1 when it stopped partway, and 2 when the journal could not be read or is not well formed. The interpreter writes to standard output, as <tag>: <message>, the highlights of what it does, as its developer chooses them.

- Execution stops at the first error. What the interpreter did before then stays done, except what its instruction language undoes; for the file language, see . Playing a journal as a transaction is a possible future feature; see . + Execution stops at the first error. What the interpreter did before then stays done, except what its instruction language undoes; for the example language, see . Playing a journal as a transaction is a possible future feature; see .

Instruction language

- An instruction language is defined once and used from both sides. Its composer half is its journal interface: one function per instruction, which checks the call's arguments, raises an error on a wrong one, and places the instruction. Its other half is the checks the guard makes, the constraint check among them, and the code the interpreter runs for each instruction. Keeping both halves in one module keeps the two checks in step. + 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.

- 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. + 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.

- 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 file language can name any file, so its constraints list the files allowed. The example language, , can name no file at all: what it writes, and where, follows from the account that gives the journal. + 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. +

+

+ Narrowness matters most for content. Constraints on where a file goes, and with what owner and permission, do not make the file safe when account A writes every byte of it. A configuration file is read by a program, often one started by root, and what the file says is what that program does; unbound.conf, for one, can name a Python script for the daemon to load. A language that lets account A write such a file freely gives account A much of the power of the program that reads it, however tightly its constraints fix the destination. The safe design is a language narrow enough that every journal it can express is safe: its interpreter writes the file, and account A supplies only values the language has a place for, as in the example.

- - - The file language -

- Man_In_Grey's own instruction language is a constrained form of the POSIX file interface: -

- - - Produces a symbol for the file. A relative directory is made absolute from the composer's working directory. permission is an integer such as 0o644, or three or four octal digits such as "0644". - - - Appends content, text or bytes, to the file an open produced. Text is encoded as UTF-8. - - - Removes a file. A file that is absent is not an error. - - - - - The file must not exist. - - - An existing file is discarded. - - - An existing file is kept as <name>.YYYYMMDDTHHMMSSZ, the time in UTC, with .1, .2 added when that name is taken. - - -

- There is no close. Each opened file is written to a temporary file in its own directory, and after the journal's last instruction every opened file is given its owner and permission and put in place, each in one step: a rename for replace, a hard link that refuses an existing name for create, and for replace_keep a hard link of the old file to its backup name followed by the rename, so the file's name is never absent. Every directory is opened without following a symbolic link, and names are worked on inside the open directory. -

-

- Execution stops at the first error, and every temporary file not yet put in place is removed. An unlink earlier in the journal has already happened by then. The interpreter writes a line for each file it puts in place or removes, such as replaced: /etc/unbound/unbound-US.conf (kept as unbound-US.conf.20261105T083807Z), and none for an open or a write. -

-

- The guard checks a journal in four steps, each over every instruction, and stops at the first step that finds a problem. It reports each problem as <step>_broken and each step that passes as check_passed: -

- - - Known instructions with exactly their arguments, of the right kinds; absolute directories in normal form; bare file names; no control character in a directory, file name or owner; every write naming the symbol of an open. - - - is_allowed_account allows account A; is_allowed_open allows each file opened, in its mode, with its owner and permission; is_allowed_unlink allows each file removed. - - - Every owner names an account. - - - Every directory exists, opens, and passes through no symbolic link; no file is named by two instructions; a file opened in create mode does not exist; an existing file to be replaced or unlinked is a regular file. - - -

- The constraints at the top of the file language's guard, guard_file.py, allowing Thomas to keep one unbound configuration file and to remove another: -

- - def is_allowed_account(user_name): - return user_name == "Thomas" - - def is_allowed_open(directory ,name ,mode ,owner ,permission): - return (directory ,name ,mode ,owner ,permission) == ("/etc/unbound" ,"unbound-US.conf" ,"replace_keep" ,"root" ,0o644) - - def is_allowed_unlink(directory ,name): - return (directory ,name) == ("/etc/unbound" ,"unbound-old.conf") - -

- The permission is given to is_allowed_open as an integer, so 0o644 and "0644" in a composer both arrive as 0o644. -

-
Journal

- A journal is a CBOR map: + A journal is a CBOR map of four entries:

- 3. + The version of the journal's form, now 3. A guard or interpreter refuses a journal of another version. - The name of the instruction language, such as file. + 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. - When, where and by whom it was composed, and the composers that ran. + When, on which host and by which account the journal was composed, the composer directory, and the composers that ran, in order. - The instructions, each a map of name, argument, and composer. An argument that is a symbol is the map {"symbol": <index>}. + The instructions, in the order the composers 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 name of the composer function that placed it. The first four instructions of the journal in , decoded, and written in JSON notation: +

+ + {"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"} + +

+ 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. +

+

+ 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: +

+ + {"name": "make_account", "argument": {"name": "Thomas·www"}, "composer": "www.py"} + {"name": "add_to_group", "argument": {"account": {"symbol": 0}, "group": "http"}, "composer": "www.py"} + +

+ The 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. +

@@ -436,7 +397,7 @@ Building

- 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 and the gates of the file language and the example 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: + 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 @@ -446,14 +407,12 @@ 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/FileLanguage.py the file language: interface, checks, execution - consumer/Man_In_Grey/guard_file.py the file language's guard, its constraints at its top - consumer/Man_In_Grey/interpreter_file.py the file language's interpreter - consumer/Man_In_Grey/EmplaceBioLanguage.py the example language - consumer/Man_In_Grey/guard_emplace_bio.py its guard + 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_file_gate the file language's gate 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

@@ -464,7 +423,7 @@ 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; its open and unlink instructions say where its files go. + 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 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. @@ -473,12 +432,16 @@ 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.

- # composer/unbound-US.conf.py + # composer/bio.py def compose(prov ,journal_iface): - file = journal_iface.open("/etc/unbound" ,prov.implied_fname ,"replace_keep" ,"root" ,0o644) - journal_iface.write(file ,"server:\n verbosity: 1\n") + journal_iface.display_name("Thomas") + journal_iface.paragraph() + journal_iface.text("I write software." ,[]) +

+ This composer writes in the example language; shows a whole one. compose is given prov, which says where the composer is: +

The composer's own file name less .py, for a composer named for the file it writes. @@ -491,7 +454,7 @@

- 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, FileLanguage.py for file and EmplaceBioLanguage.py for emplace_bio. + 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.

@@ -501,16 +464,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 print compose_only - Man_In_Grey orchestrate composer check_only - Man_In_Grey orchestrate composer + 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> Run the composer in a composer file, or every composer in a composer directory, and invoke the gate with the journal. - The gate to invoke, which also sets the instruction language the composers write in. Default: ../Man_In_Grey_gate/Man_In_Grey_file_gate from Man_In_Grey.py. + The gate to invoke, which also sets the instruction language the composers write in. Required: the orchestrator has no gate of its own. Print the journal after composing. @@ -523,7 +486,7 @@

- The command and its path come first, 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. To undo a replace_keep, rename the backup back. + 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.

@@ -546,19 +509,18 @@ Testing

- The tester has two tests. Each 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 a gate that is 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. The test tester/authored/write_file runs the file language's gate, and checks twenty-five things. Two runs of composers using every mode and unlink are compared with expected/run_1 and expected/run_2, with backup times replaced by the letters YYYYMMDDTHHMMSSZ. A file opened in create mode is written once and refused the second time. A journal with one allowed file and one the constraints do not list is refused whole. A composer giving a wrong mode fails before any journal reaches the gate. A composer directory holding a composer other accounts can write is refused by the orchestrator. A check_only run is approved by the guard and changes nothing. A run under a file size limit the gate cannot raise is refused before the guard starts. Each refusal leaves every file as it was. Since the gate checks ownership on every run, every directory from / down to the promoted interpreter is to be writable by no account but its owner. In the tester role, from its directory tester/: + 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.

- - authored/write_file/run test - authored/write_file/run clean -

- The test tester/authored/emplace_bio runs the example language's gate, and checks twenty-six 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: + It checks twenty-six 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:

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. +

@@ -794,14 +756,11 @@ The old compile script built the gate with -z relro, -z now and -fstack-protector-strong. Harmony's build has no flags for one namespace, so the gate is built without them. - - Execution stops at the first error and removes the temporary files of files not yet put in place, but an unlink that ran earlier in the journal stays done. - - The filesystem check and execution are separate, so a file can appear or change between them. Execution then fails on that instruction rather than acting on the change: a create whose file has appeared is refused by the hard link. + The guard's filesystem check and the interpreter's execution are separate moments, so a file can appear or change between them. An interpreter is to be written so that it fails on such a change rather than acting on it: the example language's interpreter opens its directory without following a symbolic link, and a rename into place replaces a symbolic link put there since rather than following it. - A process whose real or effective uid is account A's can send signals to the gate, the guard and the interpreter, whose real uid is account A's, and so can kill the interpreter partway through a journal. The file language puts its files in place only after the last instruction, which keeps the window small, but an unlink already done, or a file already put in place, stays as it is. + A process whose real or effective uid is account A's can send signals to the gate, the guard and the interpreter, whose real uid is account A's, and so can kill the interpreter partway through a journal. The example language writes its files to temporary names and renames each into place, so a page is never half written, but a file already put in place stays as it is. The gate checks the guard's verdict, not the guard's work: a guard that writes its approval line without reading the journal approves everything. The guard is installed code the application developer writes, and is to be tested as such. @@ -856,10 +815,10 @@ A Btrfs subvolume is a directory tree that can be snapshotted. Making a snapshot takes about as long as making a directory, whatever the size of the tree, because the snapshot shares every block with its source; a block is copied only when one side writes it. Before playing a journal, the interpreter would snapshot each subvolume the journal writes in. When the play succeeds, it deletes the snapshots. When the play fails, it puts back each file it changed from the snapshot, then deletes the snapshots. It knows which files those are, since it touched them.

- Putting back files one at a time is the general case. A running system cannot replace the subvolume it runs from, such as the one holding /etc, so there the rollback is a restore of each changed file, each in one step, by the same rename the file language uses to put a file in place. Where the destination is a subvolume of its own that is not mounted separately and that no program depends on keeping open, such as a web server's page directory, the interpreter could instead play the journal into a snapshot and then exchange the snapshot and the subvolume in one call, renameat2 with RENAME_EXCHANGE, so the whole change appears at once or not at all. Whether Btrfs allows that exchange between subvolumes is to be confirmed before the design relies on it. + Putting back files one at a time is the general case. A running system cannot replace the subvolume it runs from, such as the one holding /etc, so there the rollback is a restore of each changed file, each in one step, by the same rename the example language uses to put a file in place. Where the destination is a subvolume of its own that is not mounted separately and that no program depends on keeping open, such as a web server's page directory, the interpreter could instead play the journal into a snapshot and then exchange the snapshot and the subvolume in one call, renameat2 with RENAME_EXCHANGE, so the whole change appears at once or not at all. Whether Btrfs allows that exchange between subvolumes is to be confirmed before the design relies on it.

- The cost is privilege. Making a snapshot needs ownership of the subvolume, or root, and write access to the directory the snapshot goes in. Deleting one needs CAP_SYS_ADMIN, unless the file system is mounted with user_subvol_rm_allowed. A gate whose account B is not root would need the second, and a gate holding only the capabilities its language needs would need the first, which is nearly root by itself. On other file systems the play stays as it is, and a language undoes its own work where it can, as the file language does by putting its files in place only after its last instruction. + The cost is privilege. Making a snapshot needs ownership of the subvolume, or root, and write access to the directory the snapshot goes in. Deleting one needs CAP_SYS_ADMIN, unless the file system is mounted with user_subvol_rm_allowed. A gate whose account B is not root would need the second, and a gate holding only the capabilities its language needs would need the first, which is nearly root by itself. On other file systems the play stays as it is, and a language undoes its own work where it can, as the example language does by writing its files to temporary names and renaming each into place.

diff --git a/tester/authored/write_file/composer/DNS/unbound.conf.py b/tester/authored/smoke/composer/DNS/unbound.conf.py similarity index 100% rename from tester/authored/write_file/composer/DNS/unbound.conf.py rename to tester/authored/smoke/composer/DNS/unbound.conf.py diff --git a/tester/authored/write_file/composer/obsolete.py b/tester/authored/smoke/composer/obsolete.py similarity index 100% rename from tester/authored/write_file/composer/obsolete.py rename to tester/authored/smoke/composer/obsolete.py diff --git a/tester/authored/write_file/composer/unbound_conf.py b/tester/authored/smoke/composer/unbound_conf.py similarity index 100% rename from tester/authored/write_file/composer/unbound_conf.py rename to tester/authored/smoke/composer/unbound_conf.py diff --git a/tester/authored/write_file/composer/web/site_conf.py b/tester/authored/smoke/composer/web/site_conf.py similarity index 100% rename from tester/authored/write_file/composer/web/site_conf.py rename to tester/authored/smoke/composer/web/site_conf.py diff --git a/tester/authored/write_file/composer_argument/wrong_mode.py b/tester/authored/smoke/composer_argument/wrong_mode.py similarity index 100% rename from tester/authored/write_file/composer_argument/wrong_mode.py rename to tester/authored/smoke/composer_argument/wrong_mode.py diff --git a/tester/authored/write_file/composer_create/new.py b/tester/authored/smoke/composer_create/new.py similarity index 100% rename from tester/authored/write_file/composer_create/new.py rename to tester/authored/smoke/composer_create/new.py diff --git a/tester/authored/write_file/composer_refused/allowed_and_not.py b/tester/authored/smoke/composer_refused/allowed_and_not.py similarity index 100% rename from tester/authored/write_file/composer_refused/allowed_and_not.py rename to tester/authored/smoke/composer_refused/allowed_and_not.py diff --git a/tester/authored/write_file/expected/run_1/out/net/unbound.conf b/tester/authored/smoke/expected/run_1/out/net/unbound.conf similarity index 100% rename from tester/authored/write_file/expected/run_1/out/net/unbound.conf rename to tester/authored/smoke/expected/run_1/out/net/unbound.conf diff --git a/tester/authored/write_file/expected/run_1/out/unbound_conf b/tester/authored/smoke/expected/run_1/out/unbound_conf similarity index 100% rename from tester/authored/write_file/expected/run_1/out/unbound_conf rename to tester/authored/smoke/expected/run_1/out/unbound_conf diff --git a/tester/authored/write_file/expected/run_1/out/web/nginx.conf b/tester/authored/smoke/expected/run_1/out/web/nginx.conf similarity index 100% rename from tester/authored/write_file/expected/run_1/out/web/nginx.conf rename to tester/authored/smoke/expected/run_1/out/web/nginx.conf diff --git a/tester/authored/write_file/expected/run_2/out/net/unbound.conf b/tester/authored/smoke/expected/run_2/out/net/unbound.conf similarity index 100% rename from tester/authored/write_file/expected/run_2/out/net/unbound.conf rename to tester/authored/smoke/expected/run_2/out/net/unbound.conf diff --git a/tester/authored/write_file/expected/run_2/out/unbound_conf b/tester/authored/smoke/expected/run_2/out/unbound_conf similarity index 100% rename from tester/authored/write_file/expected/run_2/out/unbound_conf rename to tester/authored/smoke/expected/run_2/out/unbound_conf diff --git a/tester/authored/write_file/expected/run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ b/tester/authored/smoke/expected/run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ similarity index 100% rename from tester/authored/write_file/expected/run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ rename to tester/authored/smoke/expected/run_2/out/unbound_conf.YYYYMMDDTHHMMSSZ diff --git a/tester/authored/write_file/expected/run_2/out/web/nginx.conf b/tester/authored/smoke/expected/run_2/out/web/nginx.conf similarity index 100% rename from tester/authored/write_file/expected/run_2/out/web/nginx.conf rename to tester/authored/smoke/expected/run_2/out/web/nginx.conf diff --git a/tester/authored/write_file/expected/run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ b/tester/authored/smoke/expected/run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ similarity index 100% rename from tester/authored/write_file/expected/run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ rename to tester/authored/smoke/expected/run_2/out/web/nginx.conf.YYYYMMDDTHHMMSSZ diff --git a/tester/authored/write_file/run b/tester/authored/smoke/run similarity index 93% rename from tester/authored/write_file/run rename to tester/authored/smoke/run index 27d006a..349864d 100755 --- a/tester/authored/write_file/run +++ b/tester/authored/smoke/run @@ -3,11 +3,13 @@ """ run -The write_file test of Man_In_Grey's file instruction language. It copies +The smoke test of Man_In_Grey, run through the smoke 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_file.py, allowing the files it expects, and runs that copy through -the file language's gate, not setuid. It checks: +guard_smoke.py, allowing the files it expects, and runs that copy through +the smoke language's gate, not setuid. The smoke language is not for use +on a system; it is here to exercise the gate, the guard and the +interpreter. It checks: run_1 orchestrate composer/: each composer's file written, and out/obsolete.conf, made by the test beforehand, unlinked; the @@ -33,7 +35,7 @@ of the run, so names are compared with the time replaced by the letters YYYYMMDDTHHMMSSZ. Content, mode and owner are compared too. The test reads consumer/ and writes only in its scratchpad directory, -$REPO_HOME/tester/scratchpad/write_file, where it runs its copy of +$REPO_HOME/tester/scratchpad/smoke, where it runs its copy of Man_In_Grey from tree/. Usage: run @@ -45,7 +47,8 @@ Usage: run import sys ,os ,re ,stat ,json ,shutil ,subprocess ,getpass ,resource from pathlib import Path -TEST_NAME = "write_file" +TEST_NAME = "smoke" +GATE_NAME = "Man_In_Grey_smoke_gate" TEST_DPATH = Path(__file__).resolve().parent OUTPUT_DIR = ("out" ,"out/net" ,"out/web") @@ -157,7 +160,8 @@ def limit_file_size(): resource.setrlimit(resource.RLIMIT_FSIZE ,(1 << 20 ,1 << 20)) def run_Man_In_Grey(program ,path ,work_dpath ,*option ,preexec=None): - command = [str(program) ,"orchestrate" ,str(path) ,*option] + gate = program.parent.parent / "Man_In_Grey_gate" / GATE_NAME + command = [str(program) ,"orchestrate" ,str(path) ,"gate" ,str(gate) ,*option] result = subprocess.run(command ,cwd=work_dpath ,capture_output=True ,text=True ,preexec_fn=preexec) return result.returncode ,(result.stdout + result.stderr).strip() @@ -199,11 +203,11 @@ def put_constraint(guard_fpath ,constraint): os.chmod(guard_fpath ,0o644) def write_constraint(tree ,work_dpath): - "the test's constraints in its copy of guard_file.py, allowing the files in ALLOWED to the person running the test" + "the test's constraints in its copy of guard_smoke.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 = ( - "# written by the write_file test\n\n" + "# written by the smoke test\n\n" f"PERSON = {person!r}\n" f"ALLOWED = {allowed!r}\n\n" "def is_allowed_account(user_name):\n" @@ -215,7 +219,7 @@ 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_file.py" ,text) + put_constraint(tree / "Man_In_Grey" / "guard_smoke.py" ,text) def test(repo_home): consumer = repo_home / "consumer" @@ -229,7 +233,7 @@ def test(repo_home): promoted = os.access(consumer / "Man_In_Grey" / "Man_In_Grey" ,os.X_OK) if not log.check(promoted ,"Man_In_Grey is promoted" - ,None if promoted else "the developer promotes Man_In_Grey and Man_In_Grey_file_gate first"): + ,None if promoted else f"the developer promotes Man_In_Grey and {GATE_NAME} first"): return log.summary() if work_dpath.exists(): @@ -286,7 +290,7 @@ def test(repo_home): # check_only before = snapshot(out) status ,output = run_Man_In_Grey(program ,TEST_DPATH / "composer" ,work_dpath ,"check_only") - check_status(log ,"check_only is approved by the guard" ,status ,0 ,output ,"approved: guard file check passed") + 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 -- 2.20.1