create-egregore 0.21.4 → 0.21.6

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.
Files changed (64) hide show
  1. package/assets/egregore-launcher.js +88 -14
  2. package/bin/cli.js +6 -3
  3. package/lib/api.js +26 -22
  4. package/lib/connect-runtime.js +13 -8
  5. package/lib/hosted-api.js +70 -0
  6. package/lib/local.js +3 -17
  7. package/lib/setup.js +6 -1
  8. package/package.json +2 -2
  9. package/runtime/claude/.claude/context/telemetry.md +8 -18
  10. package/runtime/claude/.claude/skills/emissary/SKILL.md +1 -1
  11. package/runtime/claude/.claude/skills/telemetry/SKILL.md +3 -3
  12. package/runtime/claude/.claude/skills/update/SKILL.md +46 -32
  13. package/runtime/claude/bin/handoff-run.sh +1 -1
  14. package/runtime/claude/bin/hosting-ops.sh +5 -243
  15. package/runtime/claude/bin/lib/framework-update.sh +3 -1
  16. package/runtime/claude/bin/lib/git-sync.sh +2 -2
  17. package/runtime/claude/bin/notify.sh +1 -1
  18. package/runtime/claude/bin/post-update.sh +13 -0
  19. package/runtime/claude/bin/publish-artifact.sh +1 -1
  20. package/runtime/claude/bin/telemetry.sh +7 -10
  21. package/runtime/claude/egregore_runtime/contracts.py +38 -0
  22. package/runtime/claude/egregore_runtime/member_authority.py +405 -0
  23. package/runtime/claude/egregore_runtime/upgrade.py +41 -2
  24. package/runtime/claude/manifest.json +1 -1
  25. package/runtime/codex/bin/handoff-run.sh +1 -1
  26. package/runtime/codex/bin/hosting-ops.sh +5 -243
  27. package/runtime/codex/bin/lib/framework-update.sh +3 -1
  28. package/runtime/codex/bin/lib/git-sync.sh +2 -2
  29. package/runtime/codex/bin/notify.sh +1 -1
  30. package/runtime/codex/bin/post-update.sh +13 -0
  31. package/runtime/codex/bin/publish-artifact.sh +1 -1
  32. package/runtime/codex/bin/telemetry.sh +7 -10
  33. package/runtime/codex/egregore_runtime/contracts.py +38 -0
  34. package/runtime/codex/egregore_runtime/member_authority.py +405 -0
  35. package/runtime/codex/egregore_runtime/upgrade.py +41 -2
  36. package/runtime/codex/manifest.json +1 -1
  37. package/runtime/pi/bin/handoff-run.sh +1 -1
  38. package/runtime/pi/bin/hosting-ops.sh +5 -243
  39. package/runtime/pi/bin/lib/framework-update.sh +3 -1
  40. package/runtime/pi/bin/lib/git-sync.sh +2 -2
  41. package/runtime/pi/bin/notify.sh +1 -1
  42. package/runtime/pi/bin/post-update.sh +13 -0
  43. package/runtime/pi/bin/publish-artifact.sh +1 -1
  44. package/runtime/pi/bin/telemetry.sh +7 -10
  45. package/runtime/pi/egregore_runtime/contracts.py +38 -0
  46. package/runtime/pi/egregore_runtime/member_authority.py +405 -0
  47. package/runtime/pi/egregore_runtime/upgrade.py +41 -2
  48. package/runtime/pi/manifest.json +1 -1
  49. package/runtime/prime/bin/handoff-run.sh +1 -1
  50. package/runtime/prime/bin/hosting-ops.sh +5 -243
  51. package/runtime/prime/bin/lib/framework-update.sh +3 -1
  52. package/runtime/prime/bin/lib/git-sync.sh +2 -2
  53. package/runtime/prime/bin/notify.sh +1 -1
  54. package/runtime/prime/bin/post-update.sh +13 -0
  55. package/runtime/prime/bin/publish-artifact.sh +1 -1
  56. package/runtime/prime/bin/telemetry.sh +7 -10
  57. package/runtime/prime/egregore_runtime/contracts.py +38 -0
  58. package/runtime/prime/egregore_runtime/member_authority.py +405 -0
  59. package/runtime/prime/egregore_runtime/upgrade.py +41 -2
  60. package/runtime/prime/manifest.json +1 -1
  61. package/runtime/claude/bin/build-sealed-dataroom.mjs +0 -722
  62. package/runtime/codex/bin/build-sealed-dataroom.mjs +0 -722
  63. package/runtime/pi/bin/build-sealed-dataroom.mjs +0 -722
  64. package/runtime/prime/bin/build-sealed-dataroom.mjs +0 -722
@@ -0,0 +1,405 @@
1
+ """Request-local member authority above an injected trusted introspector.
2
+
3
+ This module implements parsing, identity resolution and policy intersection.
4
+ It does not issue credentials, call HTTP, read operator identity or provision
5
+ members. The host supplies an authenticated introspector; every resolution and
6
+ policy decision calls it again. Ownership, funding and actor parentage confer
7
+ no permissions here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable, Mapping, Sequence
13
+ from dataclasses import dataclass, fields, replace
14
+ from datetime import UTC, datetime, timedelta
15
+ import re
16
+ from typing import Any, Protocol
17
+ from uuid import UUID
18
+
19
+ from .contracts import (
20
+ AccountIdentity,
21
+ ActionAuthorizer,
22
+ ActorContext,
23
+ ActorIdentity,
24
+ ActorKind,
25
+ Capability,
26
+ EgregoreProfile,
27
+ Entitlements,
28
+ ExecutionAuthority,
29
+ OrgMembership,
30
+ Permission,
31
+ PolicyDecision,
32
+ RetrievalRequest,
33
+ )
34
+ from .errors import AuthorizationDenied
35
+
36
+
37
+ SCHEMA = "egregore.member-authority/v1"
38
+ _PERMISSIONS = frozenset({Permission.DISCOVER, Permission.READ, Permission.WRITE})
39
+ _IDENTIFIER = re.compile(r"^[A-Za-z0-9._:-]{1,160}$")
40
+ _PATH_PART = re.compile(r"^[A-Za-z0-9_.-]+$")
41
+
42
+
43
+ class MemberAuthorityError(AuthorizationDenied):
44
+ """A bounded refusal that contains neither payload nor transport details."""
45
+
46
+ def __init__(self):
47
+ super().__init__("Authenticated member authority could not be verified.")
48
+
49
+
50
+ def _require(value: bool) -> None:
51
+ if not value:
52
+ raise MemberAuthorityError()
53
+
54
+
55
+ def _identifier(value: Any) -> str:
56
+ _require(isinstance(value, str) and _IDENTIFIER.fullmatch(value) is not None)
57
+ return value
58
+
59
+
60
+ def _uuid(value: Any) -> str:
61
+ _require(isinstance(value, str))
62
+ try:
63
+ parsed = UUID(value)
64
+ except (ValueError, AttributeError):
65
+ raise MemberAuthorityError() from None
66
+ _require(parsed.int != 0 and str(parsed) == value)
67
+ return value
68
+
69
+
70
+ def _text(value: Any, limit: int = 160) -> str:
71
+ _require(isinstance(value, str) and 0 < len(value) <= limit
72
+ and value == value.strip() and all(ord(c) >= 32 and ord(c) != 127 for c in value))
73
+ return value
74
+
75
+
76
+ def _mapping(value: Any) -> Mapping:
77
+ _require(isinstance(value, Mapping))
78
+ return value
79
+
80
+
81
+ def _strings(value: Any, *, limit: int = 32) -> tuple[str, ...]:
82
+ _require(isinstance(value, list) and len(value) <= limit)
83
+ result = tuple(_identifier(item) for item in value)
84
+ _require(len(set(result)) == len(result))
85
+ return result
86
+
87
+
88
+ def _prefix(value: Any) -> str:
89
+ _require(isinstance(value, str) and len(value) <= 1024)
90
+ parts = value.split("/")
91
+ _require(parts[0] == "memory" and all(
92
+ part not in {"", ".", ".."} and _PATH_PART.fullmatch(part) for part in parts
93
+ ))
94
+ return value
95
+
96
+
97
+ def _prefixes(value: Any) -> tuple[str, ...]:
98
+ _require(isinstance(value, list) and len(value) <= 64)
99
+ result = tuple(_prefix(item) for item in value)
100
+ _require(len(set(result)) == len(result))
101
+ return result
102
+
103
+
104
+ def _utc(value: Any) -> datetime:
105
+ _require(isinstance(value, datetime) and value.tzinfo is not None
106
+ and value.utcoffset() == timedelta(0))
107
+ return value
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class MemberAuthorityBinding:
112
+ member_account_id: str
113
+ member_actor_id: str
114
+ membership_id: str
115
+ org_id: str
116
+ executor_actor_id: str
117
+ worker_id: str
118
+ binding_id: str
119
+ audience: str
120
+ grant_id: str
121
+
122
+ def __post_init__(self) -> None:
123
+ for field in fields(self):
124
+ value = getattr(self, field.name)
125
+ # Connected instances may retain their immutable Local organization
126
+ # reference (migration 046). It must still match the host-selected
127
+ # reference exactly; the database UUID is not an interchangeable alias.
128
+ if field.name == "org_id" and isinstance(value, str) and re.fullmatch(r"org_[0-9a-f]{32}", value):
129
+ continue
130
+ (_identifier if field.name in {"worker_id", "audience"} else _uuid)(value)
131
+ _require(self.executor_actor_id != self.member_actor_id)
132
+
133
+
134
+ class MemberIntrospector(Protocol):
135
+ def __call__(
136
+ self, *, expected: MemberAuthorityBinding, operation: str,
137
+ ) -> Mapping[str, Any]: ...
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class VerifiedMemberAuthority:
142
+ expected: MemberAuthorityBinding
143
+ executor_kind: ActorKind
144
+ revision: str
145
+ expires_at: datetime
146
+ permissions: frozenset[Permission]
147
+ read_prefixes: tuple[str, ...]
148
+ write_prefixes: tuple[str, ...]
149
+ account_name: str
150
+ member_name: str
151
+ roles: tuple[str, ...]
152
+ teams: tuple[str, ...]
153
+ aliases: tuple[tuple[str, str], ...]
154
+ org_slug: str
155
+ org_name: str
156
+ org_revision: str
157
+ default_policy: str
158
+ default_context_budget: int
159
+ entitlement_plan: str
160
+ entitlement_capabilities: frozenset[Capability]
161
+ entitlement_source: str
162
+ entitlement_revision: str
163
+
164
+ @property
165
+ def execution(self) -> ExecutionAuthority:
166
+ return ExecutionAuthority(
167
+ **{field.name: getattr(self.expected, field.name) for field in fields(self.expected)},
168
+ executor_kind=self.executor_kind, revision=self.revision, expires_at=self.expires_at,
169
+ )
170
+
171
+ def actor_context(self, *, session_id: str, harness: str) -> ActorContext:
172
+ _text(session_id, 240)
173
+ _identifier(harness)
174
+ binding = self.expected
175
+ return ActorContext(
176
+ account=AccountIdentity(binding.member_account_id, self.account_name),
177
+ actor=ActorIdentity(
178
+ actor_id=binding.member_actor_id, kind=ActorKind.PERSON,
179
+ display_name=self.member_name, account_id=binding.member_account_id,
180
+ aliases=dict(self.aliases),
181
+ ),
182
+ profile=EgregoreProfile(
183
+ schema_version="egregore-profile/v1", org_id=binding.org_id,
184
+ slug=self.org_slug, name=self.org_name, revision=self.org_revision,
185
+ default_policy=self.default_policy,
186
+ default_context_budget=self.default_context_budget,
187
+ ),
188
+ membership=OrgMembership(
189
+ binding.membership_id, binding.org_id, binding.member_actor_id,
190
+ roles=self.roles, teams=self.teams, status="active",
191
+ ),
192
+ entitlements=Entitlements(
193
+ self.entitlement_plan, self.entitlement_capabilities,
194
+ self.entitlement_source, self.entitlement_revision,
195
+ ),
196
+ session_id=session_id, harness=harness, execution=self.execution,
197
+ )
198
+
199
+
200
+ def parse_member_authority(
201
+ payload: Mapping[str, Any], *, expected: MemberAuthorityBinding, now: datetime,
202
+ ) -> VerifiedMemberAuthority:
203
+ """Validate a fresh authenticated response against a host-selected binding."""
204
+ try:
205
+ _require(isinstance(expected, MemberAuthorityBinding))
206
+ _utc(now)
207
+ payload = _mapping(payload)
208
+ _require(payload.get("schema") == SCHEMA and payload.get("active") is True)
209
+ actual = MemberAuthorityBinding(**{
210
+ field.name: payload[field.name] for field in fields(expected)
211
+ })
212
+ _require(actual == expected)
213
+ kind = ActorKind(payload["executor_kind"])
214
+ _require(kind in {ActorKind.AGENT, ActorKind.SERVICE})
215
+ expires = _text(payload["expires_at"], 40)
216
+ expires_at = _utc(datetime.fromisoformat(expires.replace("Z", "+00:00")))
217
+ _require(expires_at > now)
218
+ operations = _strings(payload["operations"], limit=3)
219
+ permissions = frozenset(Permission(item) for item in operations)
220
+ _require(bool(permissions) and permissions <= _PERMISSIONS)
221
+ reads, writes = _prefixes(payload["read_prefixes"]), _prefixes(payload["write_prefixes"])
222
+ member, account = _mapping(payload["member"]), _mapping(payload["account"])
223
+ aliases = _mapping(member["aliases"])
224
+ _require(len(aliases) <= 16)
225
+ org, entitlement = _mapping(payload["organization"]), _mapping(payload["entitlements"])
226
+ budget = org["default_context_budget"]
227
+ _require(type(budget) is int and 1 <= budget <= 1_000_000)
228
+ return VerifiedMemberAuthority(
229
+ expected=actual, executor_kind=kind, revision=_identifier(payload["revision"]),
230
+ expires_at=expires_at, permissions=permissions,
231
+ read_prefixes=reads, write_prefixes=writes,
232
+ account_name=_text(account["display_name"]), member_name=_text(member["display_name"]),
233
+ roles=_strings(member["roles"]), teams=_strings(member["teams"]),
234
+ aliases=tuple(sorted((_identifier(key), _text(value)) for key, value in aliases.items())),
235
+ org_slug=_identifier(org["slug"]), org_name=_text(org["name"]),
236
+ org_revision=_text(org["revision"]), default_policy=_identifier(org["default_policy"]),
237
+ default_context_budget=budget, entitlement_plan=_identifier(entitlement["plan"]),
238
+ entitlement_capabilities=frozenset(Capability(item) for item in
239
+ _strings(entitlement["capabilities"])),
240
+ entitlement_source=_text(entitlement["source"]),
241
+ entitlement_revision=_text(entitlement["revision"]),
242
+ )
243
+ except MemberAuthorityError:
244
+ raise
245
+ except Exception:
246
+ raise MemberAuthorityError() from None
247
+
248
+
249
+ def _now() -> datetime:
250
+ return datetime.now(UTC)
251
+
252
+
253
+ @dataclass(frozen=True, slots=True)
254
+ class AuthenticatedMemberResolver:
255
+ expected: MemberAuthorityBinding
256
+ introspect: MemberIntrospector
257
+ clock: Callable[[], datetime] = _now
258
+
259
+ def __post_init__(self) -> None:
260
+ _require(isinstance(self.expected, MemberAuthorityBinding)
261
+ and callable(self.introspect) and callable(self.clock))
262
+
263
+ def _fresh(self, operation: str) -> VerifiedMemberAuthority:
264
+ try:
265
+ _require(operation in {"resolve", "discover", "read", "write"})
266
+ payload = self.introspect(expected=self.expected, operation=operation)
267
+ # Validate time after introspection, not before a potentially slow call.
268
+ return parse_member_authority(payload, expected=self.expected, now=self.clock())
269
+ except MemberAuthorityError:
270
+ raise
271
+ except Exception:
272
+ raise MemberAuthorityError() from None
273
+
274
+ def resolve(self, *, session_id: str, harness: str) -> ActorContext:
275
+ return self._fresh("resolve").actor_context(session_id=session_id, harness=harness)
276
+
277
+ def verify(self, actor: ActorContext, operation: str) -> VerifiedMemberAuthority:
278
+ """Verify current authority without trusting mutable presentation fields."""
279
+ try:
280
+ binding = self.expected
281
+ _require(isinstance(actor, ActorContext) and actor.account is not None
282
+ and actor.membership is not None and isinstance(actor.execution, ExecutionAuthority))
283
+ _require(actor.account.account_id == actor.actor.account_id == binding.member_account_id
284
+ and actor.actor.actor_id == actor.membership.actor_id == binding.member_actor_id
285
+ and actor.profile.org_id == actor.membership.org_id == binding.org_id
286
+ and actor.membership.membership_id == binding.membership_id
287
+ and actor.membership.status == "active" and actor.actor.kind == ActorKind.PERSON)
288
+ _require(all(getattr(actor.execution, field.name) == getattr(binding, field.name)
289
+ for field in fields(binding)))
290
+ verified = self._fresh(operation)
291
+ _require(actor.execution.executor_kind == verified.executor_kind)
292
+ return verified
293
+ except MemberAuthorityError:
294
+ raise
295
+ except Exception:
296
+ raise MemberAuthorityError() from None
297
+
298
+
299
+ def _intersect(left: Sequence[str], right: Sequence[str]) -> tuple[str, ...]:
300
+ """Intersect segment prefixes; never expand an append grant into a write."""
301
+ result = set()
302
+ for first in left:
303
+ _prefix(first)
304
+ for second in right:
305
+ _prefix(second)
306
+ # Both sides are canonical memory prefixes, validated above.
307
+ # This is lexical segment containment, not filesystem traversal;
308
+ # keeping it here lets the hosted parser run without Local policy.
309
+ if first == second or first.startswith(second + "/"):
310
+ result.add(first)
311
+ elif second.startswith(first + "/"):
312
+ result.add(second)
313
+ return tuple(sorted(result))
314
+
315
+
316
+ @dataclass(frozen=True, slots=True)
317
+ class MemberAuthorityPolicy:
318
+ resolver: AuthenticatedMemberResolver
319
+ base_policy_factory: Callable[[ActorContext], ActionAuthorizer]
320
+
321
+ def _denied(self, permission: Permission, reason: str, base=None) -> PolicyDecision:
322
+ if base is not None:
323
+ return replace(base, allowed=False, reasons=(reason,), scopes=(), resource_ids=(),
324
+ read_prefixes=(), write_prefixes=(), append_prefixes=())
325
+ expected = self.resolver.expected
326
+ return PolicyDecision(False, expected.member_actor_id, expected.org_id, permission,
327
+ "member-authority/v1", reasons=(reason,))
328
+
329
+ def _intersect_decision(self, base, verified, permission) -> PolicyDecision:
330
+ expected = self.resolver.expected
331
+ _require(isinstance(base, PolicyDecision) and base.actor_id == expected.member_actor_id
332
+ and base.org_id == expected.org_id and base.permission == permission)
333
+ if not base.allowed:
334
+ return self._denied(permission, "Local policy denied this operation.", base)
335
+ # policy.check consumes prefix fields, not decision.permission. Return
336
+ # only the operation the base policy actually authorized, so a read
337
+ # decision cannot become a write or a discover-only grant expose bytes.
338
+ reads = verified.read_prefixes if (
339
+ permission in {Permission.DISCOVER, Permission.READ}
340
+ and Permission.READ in verified.permissions
341
+ ) else ()
342
+ writes = verified.write_prefixes if (
343
+ permission == Permission.WRITE and Permission.WRITE in verified.permissions
344
+ ) else ()
345
+ read = _intersect(base.read_prefixes, reads)
346
+ write = _intersect(base.write_prefixes, writes)
347
+ append = _intersect(base.append_prefixes, writes)
348
+ selected = read if permission in {Permission.READ, Permission.DISCOVER} else (*write, *append)
349
+ if not selected:
350
+ return self._denied(permission, "The member grant and local policy have no common scope.", base)
351
+ return replace(base, read_prefixes=read, write_prefixes=write, append_prefixes=append,
352
+ scopes=_intersect(base.scopes, reads if permission != Permission.WRITE else writes))
353
+
354
+ def authorize_observe(self, actor: ActorContext, request: RetrievalRequest) -> PolicyDecision:
355
+ try:
356
+ verified = self.resolver.verify(actor, "read")
357
+ if not {Permission.DISCOVER, Permission.READ} <= verified.permissions:
358
+ return self._denied(Permission.READ, "The member grant does not permit discovery and reading.")
359
+ expected = self.resolver.expected
360
+ _require(request.org_id == expected.org_id and request.actor_id == expected.member_actor_id)
361
+ current = verified.actor_context(session_id=actor.session_id, harness=actor.harness)
362
+ base = self.base_policy_factory(current).authorize_observe(current, request)
363
+ return self._intersect_decision(base, verified, Permission.READ)
364
+ except Exception:
365
+ return self._denied(Permission.READ, "Authenticated member authority is unavailable.")
366
+
367
+ def authorize_action(
368
+ self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = (),
369
+ ) -> PolicyDecision:
370
+ try:
371
+ _require(isinstance(permission, Permission) and permission in _PERMISSIONS)
372
+ verified = self.resolver.verify(actor, permission.value)
373
+ if permission not in verified.permissions or (
374
+ permission == Permission.DISCOVER and Permission.READ not in verified.permissions
375
+ ):
376
+ return self._denied(permission, "The member grant does not permit this operation.")
377
+ current = verified.actor_context(session_id=actor.session_id, harness=actor.harness)
378
+ authorizer = self.base_policy_factory(current)
379
+ base = authorizer.authorize_action(current, permission, resource_ids)
380
+ if permission == Permission.DISCOVER:
381
+ # Discovery exposes readable source metadata. Require the
382
+ # underlying policy's read decision as well as its discover
383
+ # decision, and retain the read decision's hold restrictions.
384
+ readable = authorizer.authorize_action(current, Permission.READ, resource_ids)
385
+ expected = self.resolver.expected
386
+ _require(isinstance(base, PolicyDecision) and isinstance(readable, PolicyDecision)
387
+ and base.permission == Permission.DISCOVER and readable.permission == Permission.READ
388
+ and base.actor_id == readable.actor_id == expected.member_actor_id
389
+ and base.org_id == readable.org_id == expected.org_id)
390
+ if not base.allowed or not readable.allowed:
391
+ return self._denied(permission, "Local policy denied discovery or reading.")
392
+ _require(base.policy_epoch == readable.policy_epoch and base.scope == readable.scope
393
+ and base.memory_revision == readable.memory_revision)
394
+ resources = base.resource_ids or readable.resource_ids
395
+ if base.resource_ids and readable.resource_ids:
396
+ resources = tuple(item for item in base.resource_ids if item in readable.resource_ids)
397
+ if not resources:
398
+ return self._denied(permission, "Local discovery and reading scopes do not overlap.")
399
+ base = replace(readable, permission=Permission.DISCOVER,
400
+ resource_ids=resources,
401
+ read_prefixes=_intersect(base.read_prefixes, readable.read_prefixes),
402
+ scopes=_intersect(base.scopes, readable.scopes))
403
+ return self._intersect_decision(base, verified, permission)
404
+ except Exception:
405
+ return self._denied(permission, "Authenticated member authority is unavailable.")
@@ -928,11 +928,27 @@ class UpgradeEngine:
928
928
  return all(isinstance(digest, str) and re.fullmatch(r"[a-f0-9]{64}", digest)
929
929
  for digest in value["files"].values())
930
930
 
931
+ @staticmethod
932
+ def _committed_unchanged(instance: Path, relative: str) -> bool:
933
+ """Git tracks the path and nothing under it differs from HEAD."""
934
+ environment = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
935
+ tracked = subprocess.run(["git", "-C", str(instance), "ls-files", "-z", "--", relative],
936
+ capture_output=True, text=True, timeout=5, env=environment, check=False)
937
+ status = subprocess.run(["git", "-C", str(instance), "status", "--porcelain", "-z",
938
+ "--untracked-files=all", "--", relative],
939
+ capture_output=True, text=True, timeout=5, env=environment, check=False)
940
+ if tracked.returncode != 0 or status.returncode != 0 or not tracked.stdout.strip("\0"):
941
+ return False
942
+ return all(entry[3:].endswith(".pyc") and "/__pycache__/" in f"/{entry[3:]}"
943
+ for entry in status.stdout.split("\0") if entry)
944
+
931
945
  def _legacy_template_baseline(self, instance: Path, *, previously_restored: bool) -> dict:
932
946
  """Recognize an unversioned template without claiming installed Runtime.
933
947
 
934
948
  Git identifies provenance only. Dirty bytes are captured by the normal
935
949
  rollback archive, never reconstructed by checking out a Git revision.
950
+ Runtime files a framework update committed are template content; an
951
+ untracked or edited Runtime file is unreceipted installation evidence.
936
952
  """
937
953
  try:
938
954
  for receipt in _runtime_install_receipt_paths(instance):
@@ -940,10 +956,13 @@ class UpgradeEngine:
940
956
  raise ValueError("installer evidence needs repair")
941
957
  for relative in ("bin/observe-context.sh", "bin/runtime-identity.sh", "bin/runtime-upgrade.sh"):
942
958
  marker = instance / relative
943
- if marker.exists() or marker.is_symlink():
959
+ if marker.is_symlink() or (marker.exists() and not self._committed_unchanged(instance, relative)):
944
960
  raise ValueError("Runtime adapter evidence needs repair")
945
961
  core = instance / "egregore_runtime"
946
- if core.exists() or core.is_symlink():
962
+ delivered = (not core.is_symlink() and core.is_dir()
963
+ and not any(path.is_symlink() for path in core.rglob("*"))
964
+ and self._committed_unchanged(instance, "egregore_runtime"))
965
+ if (core.exists() or core.is_symlink()) and not delivered:
947
966
  # A completed legacy rollback removes added files, not their
948
967
  # parent directories or Python's generated caches. Only a
949
968
  # validated prior baseline permits these harmless remnants.
@@ -1522,6 +1541,26 @@ class UpgradeEngine:
1522
1541
  for relative in (CODEX_SETTINGS_ADAPTER, *NATIVE_SETTINGS_ADAPTERS)
1523
1542
  if relative in selected
1524
1543
  }
1544
+ # Git cleanliness does not make a linked destination safe. Check every
1545
+ # file and parent before opening the journal or backing up any bytes;
1546
+ # copy2 follows destination links, including links inside a tracked tree.
1547
+ # Preserved org-owned paths are not selected and remain untouched.
1548
+ for relative in selected:
1549
+ destination = instance / relative
1550
+ # copy2 treats a directory destination as a container and appends
1551
+ # the source basename, which could select an unchecked child link.
1552
+ if destination.exists() and not destination.is_file():
1553
+ raise UpgradeError(
1554
+ f"Runtime destination is not a regular file: {relative}; "
1555
+ "repair the destination before upgrading"
1556
+ )
1557
+ while destination != instance:
1558
+ if destination.is_symlink():
1559
+ raise UpgradeError(
1560
+ f"Runtime destination contains a symbolic link: {destination.relative_to(instance)}; "
1561
+ "replace it with an instance-local file or directory before upgrading"
1562
+ )
1563
+ destination = destination.parent
1525
1564
  # Journal opens the transaction BEFORE the first byte moves. From here
1526
1565
  # until the caller clears it, a crash is recoverable from the backup.
1527
1566
  journal = {
@@ -144,7 +144,6 @@
144
144
  "bin/base-branch.sh",
145
145
  "bin/boundary.sh",
146
146
  "bin/branch-guard.cjs",
147
- "bin/build-sealed-dataroom.mjs",
148
147
  "bin/capture-reconcile.sh",
149
148
  "bin/capture-run.sh",
150
149
  "bin/codex-install-skills.sh",
@@ -318,6 +317,7 @@
318
317
  "egregore_runtime/knowledge_cli.py",
319
318
  "egregore_runtime/lifecycle.py",
320
319
  "egregore_runtime/lifecycle_cli.py",
320
+ "egregore_runtime/member_authority.py",
321
321
  "egregore_runtime/notification_cli.py",
322
322
  "egregore_runtime/notifications.py",
323
323
  "egregore_runtime/observe.py",
@@ -402,7 +402,7 @@ PID_MEMORY=$!
402
402
  if [ -f "$EMISSARY_CONFIG" ]; then
403
403
  EMISSARY_TOKEN=$(jq -r '.auth_token // empty' "$EMISSARY_CONFIG" 2>/dev/null)
404
404
  fi
405
- EMISSARY_API_URL="${EMISSARY_API_URL:-https://egregore-production-55f2.up.railway.app/api/v1/emissary}"
405
+ EMISSARY_API_URL="${EMISSARY_API_URL:-https://api.egregore.xyz/api/v1/emissary}"
406
406
 
407
407
  # /handoff is an internal team primitive, not an implicit public-emissary
408
408
  # composer. Use the emissary route only for an explicit recipient, and make