looks to be in a fully working state core_developer_branch
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 7 Nov 2026 09:18:35 +0000 (09:18 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 7 Nov 2026 09:18:35 +0000 (09:18 +0000)
developer/authored/Man_In_Grey.copy/interpreter_emplace_bio.py
developer/authored/Man_In_Grey.copy/interpreter_smoke.py
developer/authored/Man_In_Grey_gate.make/Gate.lib.c
document/Man_In_Grey.html
tester/authored/smoke/run

index 6979b1a..c53735c 100644 (file)
@@ -6,10 +6,17 @@ interpreter_emplace_bio - the interpreter of the emplace_bio instruction languag
 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 output, as <tag>: <message>, a line for each lasting change it
+standard error, as <tag>: <message>, 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,
@@ -70,7 +77,7 @@ except ImportError as error:
 
 
 def log(tag ,message):
-  print(f"{tag}: {Journal.clean(message)}" ,flush=True)
+  print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
 
 def execute():
   try:
@@ -78,7 +85,10 @@ def execute():
   except RuntimeError as error:
     log("cannot_execute" ,str(error))
     return 2
-  return Interpreter.interpret(sys.stdin.buffer.read() ,Language ,log ,Guard_program ,gate_invoking_account)
+  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
 
 
 # --------------------------------------------------------------------------------
index 4d22080..54dd827 100644 (file)
@@ -6,10 +6,17 @@ 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 output, as <tag>: <message>, a line for each lasting change it
+standard error, as <tag>: <message>, 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,
@@ -70,7 +77,7 @@ except ImportError as error:
 
 
 def log(tag ,message):
-  print(f"{tag}: {Journal.clean(message)}" ,flush=True)
+  print(f"{tag}: {Journal.clean(message)}" ,file=sys.stderr ,flush=True)
 
 def execute():
   try:
@@ -78,7 +85,10 @@ def execute():
   except RuntimeError as error:
     log("cannot_execute" ,str(error))
     return 2
-  return Interpreter.interpret(sys.stdin.buffer.read() ,Language ,log ,Guard_program ,gate_invoking_account)
+  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
 
 
 # --------------------------------------------------------------------------------
index 3e003cf..649106a 100644 (file)
@@ -34,7 +34,9 @@
     5. starts the guard with the journal on its standard input, and admits
        the journal only when the guard exits 0 having written exactly one
        line to standard output: guard <language> check passed;
-    6. starts the interpreter with the same journal.
+    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 <language> journal played.
 
   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.
@@ -313,7 +315,7 @@ int Man_In_Grey_gate·Gate·run(
 
   typedef struct{
     const char *source;      // "guard" or "interpreter"
-    bool stdout_is_verdict;  // the guard's standard output is kept, not shown
+    bool stdout_is_verdict;  // standard output is kept as the verdict, not shown
     char verdict[VERDICT_MAX];
     size_t verdict_length;
     size_t line_count;
@@ -631,7 +633,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 = false};
+    Child interpreter = {.source = "interpreter" ,.stdout_is_verdict = true};
     say("interpreting" ,"%s" ,interpreter_path);
     status = run_child(&interpreter ,interpreter_argument ,journal ,journal_length);
     if( interpreter.broken ){
@@ -642,11 +644,21 @@ int Man_In_Grey_gate·Gate·run(
       say("failed" ,"the interpreter was stopped by signal %d" ,WIFSIGNALED(status) ? WTERMSIG(status) : 0);
       return 2;
     }
-    switch( WEXITSTATUS(status) ){
-      case 0: say("interpreted" ,"the interpreter finished the journal"); return 0;
-      case 1: say("failed" ,"the interpreter stopped partway; what it had done stays done"); return 1;
-      default: say("failed" ,"the interpreter could not run (exit status %d)" ,WEXITSTATUS(status)); return 2;
+    if( WEXITSTATUS(status) == 1 ){
+      say("failed" ,"the interpreter stopped partway; what it had done stays done");
+      return 1;
+    }
+    if( WEXITSTATUS(status) != 0 ){
+      say("failed" ,"the interpreter could not run (exit status %d)" ,WEXITSTATUS(status));
+      return 2;
+    }
+    snprintf(expected ,sizeof expected ,"interpreter %s journal played\n" ,language);
+    if( strcmp(interpreter.verdict ,expected) ){
+      say("failed" ,"the interpreter exited 0 without writing the one line 'interpreter %s journal played'; what it did stays done" ,language);
+      return 2;
     }
+    say("interpreted" ,"interpreter %s journal played" ,language);
+    return 0;
   }
 
 #endif // Man_In_Grey_gate·Gate
index f19e94e..145a10a 100644 (file)
             <li>reads the journal from its standard input, up to 16 MiB;</li>
             <li>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 <a href="#sec-isolation"><RT·Counter·read snapshot="sec-isolation" key="counter count"></RT·Counter·read></a>;</li>
             <li>starts <RT·code>/usr/bin/python3 -E -s -B</RT·code> on the guard, with the journal on its standard input, and goes on only when the guard approves the journal;</li>
-            <li>starts the interpreter the same way, with the same journal.</li>
+            <li>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.</li>
           </ol>
           <p>
             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.
             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.
           </p>
           <p>
-            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 <RT·code>&lt;tag&gt;: &lt;message&gt;</RT·code>, the highlights of what it does, as its developer chooses them.
+            The gate logs when a journal is received, approved, and interpreted. The interpreter writes to standard error, as <RT·code>&lt;tag&gt;: &lt;message&gt;</RT·code>, 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, <RT·code>interpreter &lt;language&gt; journal played</RT·code>, 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.
+          </p>
+          <p>
+            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.
           </p>
           <p>
             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 <a href="#sec-example"><RT·Counter·read snapshot="sec-example" key="counter count"></RT·Counter·read></a>. Playing a journal as a transaction is a possible future feature; see <a href="#sec-transaction"><RT·Counter·read snapshot="sec-transaction" key="counter count"></RT·Counter·read></a>.
             gate: interpreting: /usr/local/lib/Man_In_Grey/Man_In_Grey/interpreter_emplace_bio.py
             interpreter: emplaced: /srv/http/bio/Thomas.jpg
             interpreter: emplaced: /srv/http/bio/Thomas.html
-            gate: interpreted: the interpreter finished the journal
+            gate: interpreted: interpreter emplace_bio journal played
           </RT·code>
           <p>
             An account the constraints do not list is refused before anything is written:
       </RT·section>
 
       <RT·section id="sec-problem">
-        <RT·name>Known problem</RT·name>
-        <RT·dictionary key="Problem" definition="What happens">
+        <RT·name>Known issue</RT·name>
+        <RT·dictionary key="Issue" definition="What happens">
           <RT·entry key="A guard can pass without reading">
             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.
           </RT·entry>
-          <RT·entry key="An interpreter that breaks the rules has already acted">
-            The gate stops an interpreter whose output breaks its rules, but what the interpreter did before then stays done.
+          <RT·entry key="An interpreter that fails partway leaves its work partly done">
+            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.
+          </RT·entry>
+          <RT·entry key="Man_In_Grey cannot tell whether the interpreter did the right thing">
+            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.
           </RT·entry>
         </RT·dictionary>
       </RT·section>
         <ol>
           <li>Gates holding only the capabilities their instruction language needs, in place of setuid root.</li>
           <li>A language for installing programs, so that installing a gate, guard and interpreter is itself done through Man_In_Grey.</li>
+          <li>Transactional play: a journal takes effect wholly or not at all; see <a href="#sec-transaction"><RT·Counter·read snapshot="sec-transaction" key="counter count"></RT·Counter·read></a>.</li>
           <li>Later, interpreters in a compiled language in place of Python.</li>
         </ol>
       </RT·section>
index 349864d..b803728 100755 (executable)
@@ -29,6 +29,8 @@ interpreter. It checks:
              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
+             writing its verdict line: the gate reports the run failed
 
 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
@@ -299,6 +301,19 @@ def test(repo_home):
   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"
+  text = interpreter.read_text(encoding="utf-8")
+  verdict_line = '    sys.stdout.write("interpreter smoke journal played\\n")\n'
+  interpreter.unlink()
+  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)
+  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)
+
   return log.summary()
 
 def clean(repo_home):