You have right, do {code} and do {code} while expr are mutually exclusive.

I thought more something like this do {{code}} (no spaces between curly braces) or, as you suggested, a new keyword like WRAP {...}, of course, BLOCK {...} and SCOPE {...} are also acceptable.

By the way, we already have an alternative for argumentless DO
do {code} while 0
It looks ugly and confusing and
- does not preseve $T
- does not establich a new stack level
but one can get over both easily respective do a work-around.

Oh, I see, at the very first use of a (new) private global,  the storage is decremented (on my  machine) by 768 bytes. I think, that bytes will hold some management data for each provate global (name).

Instance: cindy:ICINDY
Version : IRIS for UNIX (Ubuntu Server LTS for x86-64) 2021.2 (Build 649U) Thu Jan 20 2022 08:49:51 EST

Username: kav
Password: ******
USER>s x=$storage

USER>set x=$storage, ^myKav=123 write x-$storage
768
USER>set x=$storage, ^myKav2=123 write x-$storage
768
USER>set x=$storage, ^myKav3=123 write x-$storage
768
USER>set x=$storage, ^myKav3=123 write x-$storage
0
USER>set x=$storage, ^myKav2=123 write x-$storage
0
USER>set x=$storage, ^myKav=123 write x-$storage
0
USER>

In general, $extract() and $zstrip() are your friends.
If you want to strip ONLY the LAST character, then use this

set data="abc,,"
set $extract(data,*)=""
write data --> abc,

If you want to strip ALL (same) trailing characters, use this

set remove=","
set data1="abc,"
set data2="abc,,,"
set data3="abc,,-,,"
set data1=$zstrip(data1,">",remove)
set data2=$zstrip(data2,">",remove)
set data3=$zstrip(data3,">",remove)

write data1 --> abc
write data2 --> abc
write data3 --> abc,,-

Maybe you didn't read it carefully enough...

/// the documentation say clearly:
///
/// %request.Data(itemName, itemIndex)
///
set vProfile1 = $Get(%request.Data("profile",1)) // the first value
set vProfile2 = $Get(%request.Data("profile",2)) // the second value
/// etc.

/// or you use a loop
///
kill value
ser value=0
set idx=$order(%request.Data("profile",idx))
while idx]"" {
    set value($increment(value))=%request.Data("profile",idx)
    set idx=$order(%request.Data("profile",idx)
}

/// value = item count
/// value(i) = i-th value

If you know which record is locked (i.e. ^My.Global(123) ) then you can identify the locking process (and therefore the user) in a simple method

Class DC.Lock Extends %RegisteredObject
{
/// For a given (global) reference
/// return the (exclusive) locking processID and username
/// 
/// ref: a global reference, for example: $name(^My.Global(1,2,3))
/// 
/// For other lock types (shared, remote)
/// use the infos obtained by info_types OWNER, MODE, FLAGS and COUNTS, see
/// https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_slock
/// 
ClassMethod Who(ref)
{
    if ^$LOCK(ref,"MODE")="X" {
        set pid=^$LOCK(ref,"OWNER")
        if pid {
            set job=##class(%SYS.ProcessQuery).%OpenId(pid)
            quit {"pid":(pid), "usr":($s(job:job.UserName,1:""))}
        }
        
    } else  { quit {} }
}
}

For example:

set ref=$name(^My.Global(123))
lock +@ref:1
if '$test {
    // in case, the node is locked,
    // check up, by who is the node locked
    set who=##class(DC.Lock).Who(ref)
    write who.%ToJSON() --> {"pid":"2396","usr":"kav"}
}

A few lines of code and you have your own object cloner

Class DC.ObjCloner [ Abstract ]
{
/// obj: Cache/IRIS or a Dynamic(JSON) Object
/// 
/// For simplicity, JSON-Types null, true, false, etc. are ignored
ClassMethod Clone(obj)
{
    if $isobject(obj) {
        if obj.%IsA("%DynamicAbstractObject") {
            if obj.%IsA("%DynamicObject") { set new={},arr=0 } else { set new=[], arr=1 }
            set iter=obj.%GetIterator()
            while iter.%GetNext(.key,.val) {
                set:$isobject(val) val=..Clone(val)
                do $case(arr, 1:new.%Push(val), :new.%Set(key,val))
            }
            quit new
            
        } else { quit obj.%ConstructClone(1) }
    } else { quit "" }
}
}
Class DC.Import Extends (%Persistent, %JSON.Adaptor)
{

Property thingone As %String;
Property thingtwo As %String;
ClassMethod Test()
{
	// create a test stream
	set myStream=##class(%Stream.TmpCharacter).%New()
	do myStream.Write("[{""thingone"":""Red"",""thingtwo"":""Green""},{""thingone"":""Blue"",""thingtwo"":""Yellow""}]")
	
	// convert input (JSON) stream into a JSON object
	set data={}.%FromJSON(myStream)
	
	// loop over the JSON-Array and import each array element into your database
	for i=0:1:data.%Size()-1 {
		set obj=..%New()
		set sts=obj.%JSONImport(data.%Get(i))
		if sts set sts=obj.%Save()
		if sts continue
		write "Error: i=",i,", reason=",$system.Status.GetOneErrorText(sts),!
	}
}

Of course, instead of "myStream" you should use your REST-input stream.

do ##class(DC.Import).Test()

zwrite ^DC.ImportD
^DC.ImportD=2
^DC.ImportD(1)=$lb("","Red","Green")
^DC.ImportD(2)=$lb("","Blue","Yellow")

Shouldn't be a problem, $ZTZ isn't the only thing you can change.

Whatever value or setting you change, $namespace, $zeof, $horolog, etc. you have to consider a simple rule, if you change things (which can be used by subsequent routines, methods, etc.) for your own use, then after using them, you have to restore them to their original value. That's all.

YouRESTorWHATEVERmethod()
{
  set oldZTZ=$ztz
  set $ztz=<your preferred value>
  ...
  set $ztz=oldZTZ
  return
}

You can change the Timezone system variable

//  for example EST (Eastern Standard Time, 5 hours behind UTC)
    set $ztz = 300
//  you can even consider daylight saving
//  for example CET (Central European Time, 1 hour ahead of UTC)
#define DSTOFFSET	-60
#; -60=CET, 300=EST
#define LOCALZONE   -60
    set $ztz = $$$LOCALZONE+$s($system.Util.IsDST():$$$DSTOFFSET,1:0)
    

Setting $ZTZ affects the current job only (the job, which sets $ztz), so there is no danger for other processes.

If you want, for whatever reason, to compute the CRC of a Stream then write an short method like this (or use this):

/// Input : str - a sting or a stream
///         typ - CRC-Type (0..7, See ISC documentation)
///         
/// Output: crc-value
/// 
ClassMethod CRC(str, typ = 7)
{
    i $isobject(str),str.%IsA("%Stream.Object") {
        d str.Rewind() s crc=0
        while 'str.AtEnd { s crc=$zcrc(str.Read(32000),typ,crc) }
        ret crc
        
    } else { ret $zcrc(str,typ) }
}

Where is the problem?

I already have a database (named APPLIB) where such classes, routines and (some) data are stored and mapped to %ALL. By the way, I use %APP since it was introduced.
Nevertheless, mapping the above mentioned class (Py.Utility) to APPLIB has the same effect: it works, if I'm in %SYS but does not work for in other (for example USER) namespace.
I thought, it's simpler to explain the problem if I use a percent-class (%Zpy.Utility).
 

USER>

USER>w ##class(Py.Utility).Info()

 set val = ..internal(arg)
 ^
<OBJECT DISPATCH>zInfo+1^Py.Utility.1 *python object not found
USER 2e1>q

USER>

USER>w ##class(%SYS.Namespace).GetPackageDest(,"Py")
^/opt/isc/icindy/db/applib/
USER>

USER>zn "%SYS"

%SYS>w ##class(Py.Utility).Info()
def
%SYS>

the above approach ist the right way. And I do not see any problem there:

First, in the very first line (of the question) it's stated: "I need to develop a tool ... what data is being consumed by a certain process, ... to build an automated test scenario.", which means, this will be used during a development and/or test phase to gather informations about the touched globals (for automated tests). So the performance is not an issue.
 
Second, the suggestion of Paul Waterman can always run, assuming the process runs with the required right and flag. One can always provide the required conditions.