@junghanacs/entwurf 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +299 -0
- package/DELIVERY.md +4 -3
- package/README.md +99 -67
- package/VERIFY.md +13 -7
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +221 -16
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +14 -4
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +1041 -174
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +700 -23
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-fresh-cut-gate.sh +12 -4
- package/scripts/check-gate-qualification.ts +8 -2
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +104 -71
- package/scripts/copilot-bridge-install.sh +180 -20
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/dev-bin.sh +50 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +96 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -0
- package/scripts/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- package/scripts/smoke-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +2 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read omp's resolved agent-dir config.yml for the MCP tool-surface.
|
|
3
|
+
|
|
4
|
+
stdlib only. This is a RUNTIME read of ``tools.xdev`` / ``tools.xdevInlineDevices``
|
|
5
|
+
(Hard Rule 13 — never an ownership claim). Absent file or absent key means the
|
|
6
|
+
vendor default applies: ``tools.xdev: true`` and an empty inline allowlist
|
|
7
|
+
(oh-my-pi ``settings-schema.ts`` ``tools.xdev`` default true,
|
|
8
|
+
``tools.xdevInlineDevices`` default ``[]``, measured omp 18.0.0).
|
|
9
|
+
|
|
10
|
+
The doctor, not this leaf, decides RED vs note. This leaf reports the effective
|
|
11
|
+
state and whether any inline glob covers our dialect send-tool name.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import fnmatch
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# Vendor load order: config.yml then config.yaml (utils/src/dirs.ts MAIN_CONFIG_FILENAMES).
|
|
21
|
+
MAIN_CONFIG_FILENAMES = ("config.yml", "config.yaml")
|
|
22
|
+
|
|
23
|
+
# omp mints mcp__<server>_<tool> after lowercasing and replacing [^a-z_]+ with _.
|
|
24
|
+
# The send tool `entwurf_v2` therefore surfaces as mcp__entwurf_bridge_entwurf_v.
|
|
25
|
+
# A glob "covers our server key" when it matches that dialect name.
|
|
26
|
+
COVER_NAME = "mcp__entwurf_bridge_entwurf_v"
|
|
27
|
+
|
|
28
|
+
TRUE_WORDS = {"true", "yes", "on", "y"}
|
|
29
|
+
FALSE_WORDS = {"false", "no", "off", "n"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def die(code: int, message: str) -> None:
|
|
33
|
+
sys.stderr.write(message.rstrip("\n") + "\n")
|
|
34
|
+
raise SystemExit(code)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def strip_comment(raw: str) -> str:
|
|
38
|
+
in_single = False
|
|
39
|
+
in_double = False
|
|
40
|
+
escaped = False
|
|
41
|
+
for index, char in enumerate(raw):
|
|
42
|
+
if escaped:
|
|
43
|
+
escaped = False
|
|
44
|
+
continue
|
|
45
|
+
if char == "\\" and in_double:
|
|
46
|
+
escaped = True
|
|
47
|
+
continue
|
|
48
|
+
if char == "'" and not in_double:
|
|
49
|
+
in_single = not in_single
|
|
50
|
+
continue
|
|
51
|
+
if char == '"' and not in_single:
|
|
52
|
+
in_double = not in_double
|
|
53
|
+
continue
|
|
54
|
+
if char == "#" and not in_single and not in_double:
|
|
55
|
+
return raw[:index].rstrip()
|
|
56
|
+
return raw.rstrip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_flow_seq(text: str) -> list[object] | None:
|
|
60
|
+
body = text.strip()
|
|
61
|
+
if not (body.startswith("[") and body.endswith("]")):
|
|
62
|
+
return None
|
|
63
|
+
inner = body[1:-1].strip()
|
|
64
|
+
if inner == "":
|
|
65
|
+
return []
|
|
66
|
+
items: list[object] = []
|
|
67
|
+
buf: list[str] = []
|
|
68
|
+
in_single = False
|
|
69
|
+
in_double = False
|
|
70
|
+
depth = 0
|
|
71
|
+
for char in inner:
|
|
72
|
+
if char == "'" and not in_double:
|
|
73
|
+
in_single = not in_single
|
|
74
|
+
buf.append(char)
|
|
75
|
+
continue
|
|
76
|
+
if char == '"' and not in_single:
|
|
77
|
+
in_double = not in_double
|
|
78
|
+
buf.append(char)
|
|
79
|
+
continue
|
|
80
|
+
if not in_single and not in_double:
|
|
81
|
+
if char in "[{":
|
|
82
|
+
depth += 1
|
|
83
|
+
elif char in "]}":
|
|
84
|
+
depth -= 1
|
|
85
|
+
elif char == "," and depth == 0:
|
|
86
|
+
items.append(parse_scalar("".join(buf).strip()))
|
|
87
|
+
buf = []
|
|
88
|
+
continue
|
|
89
|
+
buf.append(char)
|
|
90
|
+
if in_single or in_double or depth != 0:
|
|
91
|
+
return None
|
|
92
|
+
items.append(parse_scalar("".join(buf).strip()))
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_scalar(raw: str) -> object:
|
|
97
|
+
text = raw.strip()
|
|
98
|
+
if text == "" or text in ("~", "null", "Null", "NULL"):
|
|
99
|
+
return None
|
|
100
|
+
if (len(text) >= 2) and ((text[0] == text[-1] == '"') or (text[0] == text[-1] == "'")):
|
|
101
|
+
return text[1:-1]
|
|
102
|
+
if text.startswith("[") and text.endswith("]"):
|
|
103
|
+
parsed = parse_flow_seq(text)
|
|
104
|
+
return parsed if parsed is not None else text
|
|
105
|
+
folded = text.casefold()
|
|
106
|
+
if folded in TRUE_WORDS:
|
|
107
|
+
return True
|
|
108
|
+
if folded in FALSE_WORDS:
|
|
109
|
+
return False
|
|
110
|
+
return text
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def split_key_value(stripped: str) -> tuple[str, str] | None:
|
|
114
|
+
in_single = False
|
|
115
|
+
in_double = False
|
|
116
|
+
for index, char in enumerate(stripped):
|
|
117
|
+
if char == "'" and not in_double:
|
|
118
|
+
in_single = not in_single
|
|
119
|
+
continue
|
|
120
|
+
if char == '"' and not in_single:
|
|
121
|
+
in_double = not in_double
|
|
122
|
+
continue
|
|
123
|
+
if char == ":" and not in_single and not in_double:
|
|
124
|
+
key = stripped[:index].strip()
|
|
125
|
+
if not key:
|
|
126
|
+
return None
|
|
127
|
+
if (len(key) >= 2) and ((key[0] == key[-1] == '"') or (key[0] == key[-1] == "'")):
|
|
128
|
+
key = key[1:-1]
|
|
129
|
+
return key, stripped[index + 1 :].strip()
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_block(lines: list[tuple[int, str]], start: int, indent: int) -> tuple[object, int]:
|
|
134
|
+
"""Parse a mapping or list whose items are indented strictly deeper than ``indent``."""
|
|
135
|
+
if start >= len(lines):
|
|
136
|
+
return {}, start
|
|
137
|
+
first_indent, first_text = lines[start]
|
|
138
|
+
if first_indent <= indent:
|
|
139
|
+
return {}, start
|
|
140
|
+
if first_text.startswith("- "):
|
|
141
|
+
items: list[object] = []
|
|
142
|
+
index = start
|
|
143
|
+
while index < len(lines):
|
|
144
|
+
item_indent, item_text = lines[index]
|
|
145
|
+
if item_indent < first_indent:
|
|
146
|
+
break
|
|
147
|
+
if item_indent > first_indent:
|
|
148
|
+
return None, start
|
|
149
|
+
if not item_text.startswith("- "):
|
|
150
|
+
return None, start
|
|
151
|
+
rest = item_text[2:].strip()
|
|
152
|
+
index += 1
|
|
153
|
+
if rest == "" or split_key_value(rest) is not None:
|
|
154
|
+
nested, index = parse_block(lines, index, item_indent)
|
|
155
|
+
if nested is None:
|
|
156
|
+
return None, start
|
|
157
|
+
if rest == "":
|
|
158
|
+
items.append(nested)
|
|
159
|
+
else:
|
|
160
|
+
pair = split_key_value(rest)
|
|
161
|
+
if pair is None:
|
|
162
|
+
return None, start
|
|
163
|
+
key, raw = pair
|
|
164
|
+
node: dict[str, object] = dict(nested) if isinstance(nested, dict) else {}
|
|
165
|
+
if raw == "":
|
|
166
|
+
child, index = parse_block(lines, index, item_indent)
|
|
167
|
+
if child is None:
|
|
168
|
+
return None, start
|
|
169
|
+
node[key] = child
|
|
170
|
+
else:
|
|
171
|
+
node[key] = parse_scalar(raw)
|
|
172
|
+
items.append(node)
|
|
173
|
+
else:
|
|
174
|
+
items.append(parse_scalar(rest))
|
|
175
|
+
return items, index
|
|
176
|
+
|
|
177
|
+
mapping: dict[str, object] = {}
|
|
178
|
+
index = start
|
|
179
|
+
while index < len(lines):
|
|
180
|
+
item_indent, item_text = lines[index]
|
|
181
|
+
if item_indent < first_indent:
|
|
182
|
+
break
|
|
183
|
+
if item_indent > first_indent:
|
|
184
|
+
return None, start
|
|
185
|
+
pair = split_key_value(item_text)
|
|
186
|
+
if pair is None:
|
|
187
|
+
return None, start
|
|
188
|
+
key, raw = pair
|
|
189
|
+
index += 1
|
|
190
|
+
if raw == "":
|
|
191
|
+
child, index = parse_block(lines, index, item_indent)
|
|
192
|
+
if child is None:
|
|
193
|
+
return None, start
|
|
194
|
+
mapping[key] = child
|
|
195
|
+
else:
|
|
196
|
+
mapping[key] = parse_scalar(raw)
|
|
197
|
+
return mapping, index
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_top_mapping(text: str) -> dict[str, object] | None:
|
|
201
|
+
lines: list[tuple[int, str]] = []
|
|
202
|
+
for raw in text.splitlines():
|
|
203
|
+
if "\t" in raw.split("#", 1)[0]:
|
|
204
|
+
return None
|
|
205
|
+
stripped_nl = raw.rstrip("\n")
|
|
206
|
+
if stripped_nl.strip() == "":
|
|
207
|
+
continue
|
|
208
|
+
body = strip_comment(stripped_nl)
|
|
209
|
+
if body.strip() == "":
|
|
210
|
+
continue
|
|
211
|
+
if body.strip() in ("---", "...") or body.strip().startswith("%"):
|
|
212
|
+
continue
|
|
213
|
+
indent = len(body) - len(body.lstrip(" "))
|
|
214
|
+
lines.append((indent, body.lstrip(" ")))
|
|
215
|
+
if not lines:
|
|
216
|
+
return {}
|
|
217
|
+
mapping, index = parse_block(lines, 0, -1)
|
|
218
|
+
if mapping is None or index != len(lines) or not isinstance(mapping, dict):
|
|
219
|
+
return None
|
|
220
|
+
return mapping
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def covers(globs: list[str]) -> bool:
|
|
224
|
+
for pattern in globs:
|
|
225
|
+
if not pattern:
|
|
226
|
+
continue
|
|
227
|
+
if fnmatch.fnmatchcase(COVER_NAME, pattern):
|
|
228
|
+
return True
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def emit(fields: list[tuple[str, str]]) -> None:
|
|
233
|
+
for key, value in fields:
|
|
234
|
+
sys.stdout.write(f"{key} {value}\n")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def report_unreadable(path: str) -> None:
|
|
238
|
+
emit([("file", path), ("verdict", "unreadable")])
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def classify(agent_dir: str) -> None:
|
|
242
|
+
chosen = None
|
|
243
|
+
for name in MAIN_CONFIG_FILENAMES:
|
|
244
|
+
candidate = os.path.join(agent_dir, name)
|
|
245
|
+
if os.path.lexists(candidate):
|
|
246
|
+
chosen = candidate
|
|
247
|
+
break
|
|
248
|
+
if chosen is None:
|
|
249
|
+
emit(
|
|
250
|
+
[
|
|
251
|
+
("file", "absent"),
|
|
252
|
+
("xdev-key", "absent"),
|
|
253
|
+
("xdev", "default-true"),
|
|
254
|
+
("inline-key", "absent"),
|
|
255
|
+
("covers", "no"),
|
|
256
|
+
("verdict", "xdev-on-uncovered"),
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
path = os.path.abspath(chosen)
|
|
262
|
+
if os.path.isdir(path):
|
|
263
|
+
report_unreadable(path)
|
|
264
|
+
return
|
|
265
|
+
try:
|
|
266
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
267
|
+
text = handle.read()
|
|
268
|
+
except OSError:
|
|
269
|
+
report_unreadable(path)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
mapping = load_top_mapping(text)
|
|
273
|
+
if mapping is None:
|
|
274
|
+
report_unreadable(path)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
tools = mapping.get("tools", None)
|
|
278
|
+
if tools is None:
|
|
279
|
+
emit(
|
|
280
|
+
[
|
|
281
|
+
("file", path),
|
|
282
|
+
("xdev-key", "absent"),
|
|
283
|
+
("xdev", "default-true"),
|
|
284
|
+
("inline-key", "absent"),
|
|
285
|
+
("covers", "no"),
|
|
286
|
+
("verdict", "xdev-on-uncovered"),
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
return
|
|
290
|
+
if not isinstance(tools, dict):
|
|
291
|
+
report_unreadable(path)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
xdev_present = "xdev" in tools
|
|
295
|
+
xdev_value = tools.get("xdev") if xdev_present else None
|
|
296
|
+
inline_present = "xdevInlineDevices" in tools
|
|
297
|
+
inline_value = tools.get("xdevInlineDevices") if inline_present else None
|
|
298
|
+
|
|
299
|
+
globs: list[str] = []
|
|
300
|
+
if inline_present:
|
|
301
|
+
# Vendor compileInlineGlobs drops a non-array (a scalar is an empty allowlist).
|
|
302
|
+
if isinstance(inline_value, list):
|
|
303
|
+
globs = [item for item in inline_value if isinstance(item, str)]
|
|
304
|
+
elif inline_value is None:
|
|
305
|
+
globs = []
|
|
306
|
+
else:
|
|
307
|
+
globs = []
|
|
308
|
+
|
|
309
|
+
covered = covers(globs)
|
|
310
|
+
|
|
311
|
+
if not xdev_present or xdev_value is None:
|
|
312
|
+
verdict = "xdev-on-uncovered"
|
|
313
|
+
xdev_field = "default-true"
|
|
314
|
+
xdev_key = "absent"
|
|
315
|
+
elif xdev_value is False:
|
|
316
|
+
verdict = "xdev-off"
|
|
317
|
+
xdev_field = "false"
|
|
318
|
+
xdev_key = "present"
|
|
319
|
+
elif xdev_value is True:
|
|
320
|
+
xdev_field = "true"
|
|
321
|
+
xdev_key = "present"
|
|
322
|
+
verdict = "xdev-on-covered" if covered else "xdev-on-uncovered"
|
|
323
|
+
else:
|
|
324
|
+
report_unreadable(path)
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
emit(
|
|
328
|
+
[
|
|
329
|
+
("file", path),
|
|
330
|
+
("xdev-key", xdev_key),
|
|
331
|
+
("xdev", xdev_field),
|
|
332
|
+
("inline-key", "present" if inline_present else "absent"),
|
|
333
|
+
("covers", "yes" if covered else "no"),
|
|
334
|
+
("verdict", verdict),
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main(argv: list[str]) -> None:
|
|
340
|
+
if len(argv) != 2:
|
|
341
|
+
die(5, "usage: omp-tool-surface.py <absolute-agent-dir>")
|
|
342
|
+
agent_dir = argv[1]
|
|
343
|
+
if not os.path.isabs(agent_dir):
|
|
344
|
+
die(5, "omp-tool-surface: agent dir must be absolute")
|
|
345
|
+
classify(agent_dir)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
main(sys.argv)
|
|
@@ -63,3 +63,25 @@ def unchanged(before: dict, after: dict) -> bool:
|
|
|
63
63
|
writers mutate in place, so they parse the raw text twice rather than aliasing it.
|
|
64
64
|
"""
|
|
65
65
|
return before == after
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def classify_installer_root(state: dict) -> "tuple[str, str | None]":
|
|
69
|
+
"""ONE typed verdict for a user-scope install-state's `installerRoot` (#86 C2).
|
|
70
|
+
|
|
71
|
+
Shared by register-pi-provider install/remove and the doctor-pi-package ownership
|
|
72
|
+
coupling, because two per-site checks (`isinstance(str)` in one arm, `is None` in
|
|
73
|
+
another) let every OTHER type — a number, an empty string, a bool, an object, an
|
|
74
|
+
array — fall between them and proceed unattributed. A state MORE unattributed than
|
|
75
|
+
legacy must never be treated MORE leniently than legacy.
|
|
76
|
+
|
|
77
|
+
("legacy", None) key absent, or an explicit null — the pre-#86 shape; each
|
|
78
|
+
caller names its own adoption/refusal contract for it.
|
|
79
|
+
("owner", root) a non-empty string — the recorded owner root.
|
|
80
|
+
("corrupt", None) everything else — fail-closed before any write.
|
|
81
|
+
"""
|
|
82
|
+
if "installerRoot" not in state or state["installerRoot"] is None:
|
|
83
|
+
return "legacy", None
|
|
84
|
+
root = state["installerRoot"]
|
|
85
|
+
if isinstance(root, str) and root:
|
|
86
|
+
return "owner", root
|
|
87
|
+
return "corrupt", None
|