From: Thomas Walker Lynch
Date: Wed, 4 Nov 2026 05:47:19 +0000 (+0000)
Subject: updates setup
X-Git-Url: https://git.reasoningtechnology.com/USER_NAME.png?a=commitdiff_plain;h=7243851216ae096e1547c515e7d5f010dfbd9f0e;p=Man_In_Grey
updates setup
---
diff --git a/document/design_Man_In_Grey.html b/document/design_Man_In_Grey.html
index e787fb4..e58187e 100644
--- a/document/design_Man_In_Grey.html
+++ b/document/design_Man_In_Grey.html
@@ -26,7 +26,7 @@
@@ -41,7 +41,7 @@
With the Man_In_Grey approach, each set of one or more configuration files is the work product of an unprivileged configuration program. When the composer program runs, it places commands into a . Later, a running in privileged mode reads the journal, approves it, and then executes its commands. The set of journal commands is purposely limited to make such verification possible.
- Nothing in this arrangement is particular to configuration files. Man_In_Grey is a general means for an unprivileged program to compose changes that a privileged program checks and then makes. The command language a journal is written in is chosen by the application, not by Man_In_Grey. Configuration files are the first use, and the server of AccountTree is the first application.
+ Nothing in this arrangement is particular to configuration files. Man_In_Grey is a general means for a program running as one account to compose changes that a program running as another account checks and then makes. Typically the first account is unprivileged and the second is root. The command language a journal is written in is chosen by the application, not by Man_In_Grey. Configuration files are the first use, and the server of AccountTree is the first application.
@@ -49,7 +49,7 @@
- The , , through which a composer makes a journal.
- - A structure for gating, checking and executing journals: a compiled that gives privilege, a that checks every command of a journal against , and a general interpreter that executes the journal once the guard has found nothing wrong.
+ - A structure for gating, checking and executing journals: a compiled that changes the account the work runs as, a that checks every command of a journal against , and a general interpreter that executes the journal once the guard has found nothing wrong.
An application chooses its command language and its constraints. Man_In_Grey comes with one command language, a small file language used for testing and for configuration files.
@@ -60,7 +60,10 @@
- Out-of-band signalling
+ Out-of-band signaling
- The general method of filling in templates mixes data and control together, i.e. the content to be written, and marks that say where to substitute. Thus this method is a form of in-band signalling. With in-band signaling there will potentially that a content string will be identical to a control string, so data and control can alias. There is no tight way to design the aliasing out. The typical workaround is to disallow control strings in the data and to replace missing characters with 'escape sequences', where the escape then is control. Each level of nesting multiplies the escapes: an f-string that explains how to write f-strings escapes its braces, and a document about that f-string escapes them again.
+ The general method of filling in templates mixes data and control together: the content to be written, and the marks that say where to substitute. This method is therefore a form of in-band signaling. With in-band signaling, a content string can be identical to a control string, so data and control can alias, and there is no tight way to design the aliasing out. The typical workaround is to disallow control strings in the data, and to write each disallowed character as an escape sequence, where the escape is then itself control. Each level of nesting multiplies the escapes: an f-string that explains how to write f-strings escapes its braces, and a document about that f-string escapes them again.
Man_In_Grey signals out of band. A composer is a program, so the logic that forms the output is code, with loops, conditions and lookups, rather than marks embedded in the output. A command in the journal is a structure, and file content is carried in it as a byte string with its length. The journal interpreter never scans content for meaning, so content can hold anything, including text that looks like a journal, with no escaping. This is the same principle as an extent string, where the extent of the content travels beside it rather than being marked within it.
@@ -115,14 +121,14 @@
journal_iface.write(file ,"server:\n verbosity: 1\n")
- Here file is a symbol. In the journal it is the index of the open command, written {"symbol": 0}. A journal has no variables beyond this: a symbol is set once, by the command that produces it, and only later commands name it, which the journal interpreter checks before executing anything. The composer being a program does not let it use the value, since the value comes into being only when the journal is executed.
+ Here file is a symbol. In the journal it is the index of the open command, written {"symbol": 0}. A journal has no variables beyond this: a symbol is set once, by the command that produces it, and only later commands name it, which the journal interpreter checks before executing anything.
Constraints and the guard
- Privilege exists to limit what accounts can do. Man_In_Grey keeps that limit while letting an unprivileged program ask for privileged work: the bespoke journal interpreter has the privilege, and its guard decides, by its constraints, which of the requested work is allowed. Because the guard runs before execution, it filters rather than audits after the fact.
+ Accounts exist to limit what programs can do. Man_In_Grey keeps those limits while letting a program running as one account ask for work done as another: the bespoke journal interpreter runs as the interpreter account, and its guard decides, by its constraints, which of the requested work is allowed. Because the guard runs before execution, it filters rather than audits after the fact.
The command language declares what can be done; the constraints declare where; the composer chooses among what the constraints allow. In the file language the composer names the file it opens, which keeps the composer readable, and the guard refuses any file the constraints do not list. Every later command about the file names its symbol rather than a path, so the constraints are checked once, at open.
@@ -133,15 +139,15 @@
- Check the whole journal, then execute. Every command is checked before the first is executed, so a journal that breaks a constraint anywhere changes nothing.
- Keep the command language small. Each command is a narrow operation whose effect can be stated, so the guard can decide about it. A command such as "run this shell line" could not be checked, and belongs in no command language.
- - Check twice; the second check is the one that counts. The journal interface checks each call's arguments when the composer makes it, and raises an error at once, naming the composer. The journal interpreter checks again, because a journal is data from an unprivileged program and can have been made by anything.
+ - Check twice; the second check is the one that counts. The journal interface checks each call's arguments when the composer makes it, and raises an error at once, naming the composer. The journal interpreter checks again, because a journal is data given by the orchestrator, and can have been made by anything.
- The guard can check what a command does from the command and the system: whether a file is one the constraints list, whether an account exists, whether a directory passes through a symbolic link. It cannot check who asked except through the caller; see .
+ The guard can check what a command does from the command and the system: whether a file is one the constraints list, whether an account exists, whether a directory passes through a symbolic link. Of who asked, it knows for certain only the orchestrator account; see .
- Gates and privilege
+ Gates
A process has a real uid, naming who started it, and an effective uid, the one the kernel checks for permission. Usually they are the same. When a person runs a program file marked setuid, the kernel sets the effective uid to that of the file's owner and leaves the real uid as the person's. A setuid program owned by root therefore runs with root's rights while still knowing who ran it. That is how passwd lets a person change his own password in /etc/shadow.
@@ -149,20 +155,23 @@
The gate is compiled because Linux ignores the setuid bit on a script started with #!: between the kernel opening the script and the interpreter reading it, the script could be swapped. The gate is the first few lines of its bespoke journal interpreter. It:
- - finds the interpreter at a path fixed when the gate is compiled, relative to the gate itself, so a caller cannot name another;
- - when running privileged, refuses unless the interpreter, and every directory above it, is owned by root and writable by no other account;
+ - finds the interpreter at a path fixed when the gate is compiled, relative to the gate itself, so an orchestrator cannot name another;
+ - refuses unless the interpreter, and every directory above it, is owned by root or by the interpreter account, and writable by no other account;
- clears the environment, setting only PATH and LANG, sets the file mode mask to 077, marks the process not dumpable, and changes directory to /;
- - records the caller in MIG_CALLER_UID, MIG_CALLER_GID and MIG_CALLER_NAME;
+ - records the orchestrator account in MIG_ORCHESTRATOR_UID, MIG_ORCHESTRATOR_GID and MIG_ORCHESTRATOR_NAME;
- starts /usr/bin/python3 -E -s -B on the interpreter, passing the journal on standard input.
- The Python part of the bespoke interpreter, before it imports anything, checks that every module it will load and its constraints file are owned by root and writable by no one else. A module runs code when it is imported, so the check comes first.
+ The Python part of the bespoke interpreter, before it imports anything, checks that every module it will load and its constraints file are owned by root or by the interpreter account, and writable by no other account. A module runs code when it is imported, so the check comes first.
+
+
+ These checks are made on every run, through a trivial gate as well as a blessed one, and whoever the orchestrator account is. A check that can be got around by other means still catches mistakes, such as a group-writable directory or an interpreter run from the wrong tree. Through a trivial gate the interpreter account is the person's own, so his own files pass.
Who is allowed to hand a journal to the interpreter is a constraint, not a rule of the gate's. The gate decides only whether the interpreter it would start can be trusted.
- A trivial gate, one not blessed, changes no uid. Everything then runs with the caller's rights, and the guard still applies. The interpreter then accepts constraints given by the caller, with --constraints, since nothing runs with more than the caller's own rights. Running privileged it refuses them, and reads only its own root-owned constraints file. The trivial gate is the convenient way to use Man_In_Grey on files the person owns, and the way to prototype a command language before giving it privilege.
+ A trivial gate, one not blessed, changes no uid, so the interpreter account is the orchestrator account, and the guard still applies. The interpreter then accepts constraints given with --constraints, checked as its own files are, since nothing runs with more than the orchestrator account's own rights. When the gate changes the account, or the interpreter account is root, the interpreter refuses them and reads only its own constraints file. The trivial gate is the convenient way to use Man_In_Grey on files the person owns, and the way to prototype a command language before blessing a gate for it.
Least privilege, planned
@@ -176,7 +185,7 @@
Installation
- A privileged gate and everything its interpreter loads are to be owned by root, in directories only root can write, and the gate checks this. A Harmony consumer/ directory is owned by the person who promoted into it, so a gate blessed there refuses to run. Installing a bespoke journal interpreter is a step of its own, done by root.
+ A blessed gate and everything its interpreter loads are to be owned by root or by the interpreter account, in directories no other account can write, and the gate and the interpreter check this. A Harmony consumer/ directory is owned by the person who promoted into it, so a gate blessed there refuses to run, and so does root running the gate there. Installing a bespoke journal interpreter is a step of its own, done by root, or, when the interpreter account is not root, by root or by that account.
@@ -231,7 +240,7 @@
Known commands with exactly their arguments, of the right kinds; absolute directories in normal form; bare file names; every write naming the symbol of an open.
- The guard: the caller is listed; a principal other than the caller is named only by a caller allowed to; every file opened or unlinked is listed, in that mode, with that owner and permission.
+ The guard: the orchestrator account is listed; a requesting account is named only by an orchestrator account listed as a relay; every file opened or unlinked is listed, in that mode, with that owner and permission.
Every owner names an account.
@@ -245,8 +254,8 @@
{
- "caller": ["Thomas"],
- "principal_caller": [],
+ "orchestrator_account": ["Thomas"],
+ "relay_account": [],
"file": [
{"directory": "/etc/unbound", "name": "unbound-US.conf",
"mode": ["replace_keep"], "owner": "root", "permission": "0644"},
@@ -256,19 +265,71 @@
}
- mode lists the open modes allowed for the file, and unlink when it can be removed. The constraints file installed with the interpreter lists no caller and no file, so it refuses everything until root writes its own.
+ mode lists the open modes allowed for the file, and unlink when it can be removed. orchestrator_account lists the accounts allowed to give journals, and relay_account those of them allowed to name a requesting account. The constraints file installed with the interpreter lists no account and no file, so it refuses everything until root writes its own.
-
- Callers and principals
+
+ Accounts
- The interpreter knows the caller from the kernel, by real uid, and the caller cannot misstate it. When a person runs Man_In_Grey for himself, the caller is also the principal.
+ Three programs take part in a run, and each runs as an account:
+
+
+ The orchestrator runs as it. The gate learns it from the kernel, as its real uid, and the orchestrator cannot misstate it. It is the one account the guard knows for certain.
+
+
+ The composers run as it. Man_In_Grey runs composers in its own process, so its composer account is its orchestrator account.
+
+
+ The guard and the general interpreter run as it. A trivial gate leaves it the orchestrator account; a blessed gate makes it the gate's owner.
+
+
- When a server composes journals on behalf of the accounts it serves, the caller is always the server's account, and the principal is carried in the journal. The interpreter cannot verify that claim; it can verify only that the claim came from a caller entitled to make it. So a journal naming a principal other than its caller is accepted only from a caller the constraints list as principal_caller. The server, for its part, learns the principal from the kernel, as the AccountTree server does through SO_PEERCRED, so the chain from the account that asked to the guard that decides rests on the kernel at both ends and on the listed server in between.
+ The point of Man_In_Grey is that the work is done as the interpreter account while the composers run as another, with only the journal passing between them, and the guard deciding what of it is done. Code that runs as the composer account has no rights it did not already have. The common case has an unprivileged composer account and root as the interpreter account, but Man_In_Grey is not particular to privilege: any two accounts serve. A gate blessed to an ordinary account lets one person's composers change, within the constraints, files only that account can write.
+
+ The five arrangements
+
+ Each of the three accounts can be the same as another or different. The arrangements are the ways of grouping three things, of which there are five. (The 3! = 6 orderings of three things is a different count.)
+
+
+
+ The orchestrator runs the composers in its own process and invokes a trivial gate. The guard applies, and constraints can be given with --constraints. This is how a person uses Man_In_Grey on files he owns, how a command language is prototyped, and how the tester's test runs. When the one account is root, the composers have root's rights, and nothing stops one from changing files directly rather than through the journal; the guard checks only the journal.
+
+
+ The orchestrator runs the composers in its own process and invokes a blessed gate. The case Man_In_Grey is made for. The AccountTree server works this way, with its own account composing and root interpreting.
+
+
+ The orchestrator runs composers it does not trust as another account, and has the journal executed with its own rights through a trivial gate. The composers can do no harm while they run.
+
+
+ The composers have the rights the journal is executed with, so they could make the changes themselves; the guard protects nothing against them. Of little use.
+
+
+ The orchestrator runs composers as one account and has the journal executed as another, through a blessed gate.
+
+
+
+ Man_In_Grey as it stands gives arrangements 1 and 2, since its orchestrator runs composers in its own process. Arrangements 3 and 5 need an orchestrator that starts composers as another account, which takes root's rights or a gate of its own.
+
+
+ Whether an account is privileged is a separate matter from which accounts are the same. The guard needs only that the interpreter account can do what the orchestrator account asks of it, and the constraints say how much of that is allowed.
+
+
+
+ The requesting account
+
+ When a person runs Man_In_Grey for himself, he asked for the work, and he is the orchestrator account. In AccountTree, an account asks the server for work, such as a child account, and the server composes the journal and gives it to the interpreter. The server's account is then the orchestrator account, and the account that asked is the requesting account, named in the journal.
+
+
+ The guard cannot verify a requesting account named in the journal; it can verify only that the orchestrator account is entitled to name one. So a journal naming a requesting account is accepted only from an orchestrator account the constraints list in relay_account.
+
+
+ How the requests reach the orchestrator, and how it learns who made them, belongs to the application. The requests are not a Man_In_Grey command language: they go to the orchestrator, and only the journal it composes goes to the gate. The AccountTree server learns who asked from the kernel through SO_PEERCRED, so the chain from the requesting account to the guard rests on the kernel at both ends, and on the account listed in relay_account between them.
+
+
@@ -283,8 +344,8 @@
The name of the command language, such as file.
-
- Present when the journal was composed on behalf of an account other than the caller.
+
+ Present when the work was asked for by an account other than the orchestrator account.
When, where and by whom it was composed, and the composers that ran.
@@ -298,7 +359,7 @@
Transports
- A journal reaches its interpreter by starting the interpreter's gate with the journal on standard input. Since a journal is data, it can travel other ways: over a Unix domain socket to a running interpreter, which learns its caller through SO_PEERCRED, or over a network, where the caller is known only through authentication. Such an interpreter serves many callers, and chooses constraints, and perhaps the command language, by caller. Man_In_Grey stays at the lower level, as the mechanism servers are built with; a server that receives journals is an application of it.
+ A journal reaches its interpreter by starting the interpreter's gate with the journal on standard input. Since a journal is data, it could travel other ways, such as over a Unix domain socket to an interpreter already running, which would learn the orchestrator account through SO_PEERCRED. Man_In_Grey does not do this. It is the mechanism a program that receives requests is built with, not such a program itself.
@@ -321,7 +382,7 @@
consumer/Man_In_Grey_file_gate/Man_In_Grey_file_gate the gate
- The same layout is staged in developer/scratchpad/made/, so a developer's experiment 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, so the interpreter never uses a person's Python, and the system Python needs the package from the distribution; on openSUSE, python3-cbor2.
+ The same layout is staged in developer/scratchpad/made/, so a developer's experiment 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 interpreter uses 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. openSUSE names the package for the Python it is built for, so for Python 3.13 it is python313-cbor2. To see what the interpreter will see: /usr/bin/python3 -E -s -c 'import cbor2; print(cbor2.__file__)'.
@@ -377,10 +438,10 @@
The gate to hand the journal to. Default: ../Man_In_Grey_file_gate/Man_In_Grey_file_gate from Man_In_Grey.py.
- Constraints for an unprivileged run; refused by a privileged interpreter.
+ Constraints for a run through a trivial gate; refused when the gate changes the account or the interpreter account is root.
- Stop after checking the composer directory and the filter.
+ Stop after checking the path and the filter.
Print the journal after composing; stop after composing.
@@ -398,9 +459,9 @@
- Installing a privileged interpreter
+ Installing a blessed interpreter
- As root, copy consumer/Man_In_Grey/ and consumer/Man_In_Grey_file_gate/ side by side into a directory owned by root and writable by no other account, make every file owned by root and not group- or other-writable, write the constraints into constraints_file.json, and bless the gate:
+ As root, copy consumer/Man_In_Grey/ and consumer/Man_In_Grey_file_gate/ side by side into a directory owned by root and writable by no other account, make every file owned by root and not group- or other-writable, write the constraints into constraints_file.json, and bless the gate to the interpreter account. For root:
chown root:root Man_In_Grey_file_gate/Man_In_Grey_file_gate
@@ -408,14 +469,14 @@
Man_In_Grey_file_gate/Man_In_Grey_file_gate --print-flags
- 4755 is the mode 755 with the setuid bit, 4000, added. --print-flags reports the caller, the effective uid, and whether the gate is running privileged. Promotion replaces files and does not bless, so the installation is redone after each promotion that changes the interpreter. The installation step is a candidate for Man_In_Grey itself, once there is a language for installing programs.
+ 4755 is the mode 755 with the setuid bit, 4000, added. For an interpreter account other than root, the gate is owned by that account and its group, and given the setgid bit, 2000, as well, as chmod 6755. Only root can give a file away, so an interpreter running as another account can make files only that account owns, and its group has to be that account's group too. --print-flags reports the real and effective uid, and whether the gate changes the account. Promotion replaces files and does not bless, so the installation is redone after each promotion that changes the interpreter. The installation step is a candidate for Man_In_Grey itself, once there is a language for installing programs.
Testing
- The tester's test tester/authored/write_file runs the promoted Man_In_Grey through an unblessed gate, with constraints it writes into its scratchpad directory, and checks nineteen 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. Each refusal leaves every file as it was. In the tester role:
+ The tester's test tester/authored/write_file runs the promoted Man_In_Grey through an unblessed gate, with constraints it writes into its scratchpad directory, and checks nineteen 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. 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:
authored/write_file/run test
@@ -436,8 +497,11 @@
The filesystem check and execution are separate, so a file can appear or change between them. Execution then fails on that command rather than acting on the change: a create whose file has appeared is refused by the hard link.
-
- The journal and the guard carry a principal, but Man_In_Grey has no option to name one. A server using the journal interface as a library sets it directly.
+
+ The journal and the guard carry a requesting account, but Man_In_Grey has no option to name one, and no test uses one. A program using the journal interface as a library sets it when it makes the journal.
+
+
+ When the interpreter fails to start, for example because the system Python lacks cbor2, Python exits with status 1, the status of a refused journal. It is to exit 2, with one line naming what is missing.
@@ -448,7 +512,6 @@
The journal interface used as a library by the AccountTree server, and AccountTree's command languages and their bespoke interpreters.
Gates holding only the capabilities their command language needs, in place of setuid root.
A language for installing programs, so that installing a bespoke interpreter is itself done through Man_In_Grey.
- Transports other than starting the gate.
Later, interpreters in a compiled language in place of Python.
@@ -457,6 +520,7 @@
Open questions
- Whether composers are given settings chosen when Man_In_Grey is run, such as a settings file every composer receives.
+ - Whether the orchestrator runs composers as an account of their own, which arrangements 3 and 5 of need.
- Whether constraints name files only exactly, as now, or also by pattern.
- Where installed bespoke interpreters live, and how Man_In_Grey is told which gate a journal goes to.
@@ -465,7 +529,7 @@
Changes from the first version
- 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 callers into the constraints, raises an error in the composer on a wrong argument where the first version dropped the command 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.
+ 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 orchestrator accounts into the constraints, raises an error in the composer on a wrong argument where the first version dropped the command 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.
diff --git a/setup b/setup
index 1992fea..d2a452a 100755
--- a/setup
+++ b/setup
@@ -3,11 +3,24 @@
import os
import sys
+import json
import argparse
import tempfile
PROJECT_ROLES = ["administrator", "consumer", "developer", "tester"]
+# Role shells stack: setup run inside a role shell starts a shell in the new
+# role, and exiting it returns to the shell it was started from. A loop
+# through setup, such as one started from ~/.bashrc, is stopped at this depth.
+DEPTH_MAX = 8
+
+# The environment setup was first run from, before any role, recorded so that
+# every role shell starts from it rather than from the role shell around it.
+BASE_KEY = "RT_SETUP_BASE"
+
+# The outer role's values, for handing an Emacs shell buffer back on exit.
+OUTER_KEY = ("REPO_HOME", "PROJECT", "ROLE", "PATH")
+
def main():
parser = argparse.ArgumentParser(
description="Enter a project role environment by spawning an isolated subshell."
@@ -15,19 +28,35 @@ def main():
parser.add_argument("role", choices=PROJECT_ROLES, help="The project role to assume.")
args = parser.parse_args()
- env = os.environ.copy()
+ outer = os.environ.copy()
- # A role shell started inside another role shell inherits the outer
- # project's PATH, REPO_HOME and functions, and a loop through setup would
- # start shells without end. RT_SETUP_DEPTH counts the role shells this
- # process is inside; any nesting is refused.
- depth = env.get("RT_SETUP_DEPTH", "0")
+ # A role shell started inside another would otherwise inherit the outer
+ # role's environment: its tool directory on PATH, its variables, and the
+ # shell functions the shared setup exports. So every role shell starts
+ # from the base environment, the one setup was first run from, however
+ # deeply role shells are stacked. RT_SETUP_DEPTH counts the role shells
+ # this process is inside.
+ depth = outer.get("RT_SETUP_DEPTH", "0")
depth = int(depth) if depth.isdigit() else 0
- if depth > 0:
- print(f"setup: already inside a role shell ({env.get('PROJECT', '?')} {env.get('ROLE', '?')}).")
- print("setup: type 'exit' to leave it, then run setup again.")
+ if depth >= DEPTH_MAX:
+ print(f"setup: already {depth} role shells deep; refusing, to stop a loop through setup.")
sys.exit(1)
+
+ base = None
+ if depth > 0 and BASE_KEY in outer:
+ try:
+ base = json.loads(outer[BASE_KEY])
+ except ValueError:
+ base = None
+ if not isinstance(base, dict):
+ base = {k: v for k, v in outer.items() if not k.startswith("RT_SETUP_")}
+
+ env = dict(base)
+ env[BASE_KEY] = json.dumps(base)
env["RT_SETUP_DEPTH"] = str(depth + 1)
+ if depth > 0:
+ for key in OUTER_KEY:
+ env[f"RT_SETUP_OUTER_{key}"] = outer.get(key, "")
# Python-domain logic: Variables and Paths
# The project is the directory holding this file, whatever REPO_HOME the
@@ -67,6 +96,11 @@ def main():
'fi',
'if [[ -n "${INSIDE_EMACS}" ]] && command -v env_to_emacs >/dev/null 2>&1; then',
' env_to_emacs',
+ ' # inside another role shell: on exit, hand the Emacs buffer back to the outer role',
+ ' if [[ -n "${RT_SETUP_OUTER_ROLE:-}" ]]; then',
+ ' rt_setup_env_to_emacs="$(command -v env_to_emacs)"',
+ ' trap \'REPO_HOME="$RT_SETUP_OUTER_REPO_HOME" PROJECT="$RT_SETUP_OUTER_PROJECT" ROLE="$RT_SETUP_OUTER_ROLE" PATH="$RT_SETUP_OUTER_PATH" "$rt_setup_env_to_emacs"\' EXIT',
+ ' fi',
'fi',
'rm -f "${BASH_SOURCE[0]}"'
]