Skip to content

feat(lease): owner namespaces, local owner sessions and private lease records - #66

Merged
guillaume-flambard merged 3 commits into
mainfrom
feat/owner-everything
Oct 23, 2026
Merged

guillaume-flambard merged 3 commits into
mainfrom
feat/owner-everything

Conversation

@guillaume-flambard

Copy link
Copy Markdown
Contributor

Related Issue and spec

Closes #65. Companion: memolabs-dev/memo-bridge#258 for memo-bridge#257 (merges first). Spec: owner decision of 2026-10-22; the scope is in memo-bridge docs/OWNER_SCOPE.md.

STAR receipt

Situation:

  • The ledger accepted memolabs-dev/* only.
  • Private enrollment was granted to the Mac runtime mode only.
  • Each lease commit in this public repository stored repo and issue in clear, so private repositories' names and Issue numbers were published.

Task: let the owner's sessions use every repository he owns without widening anyone else's access, and stop publishing private identities.

Actions:

  • issue_preflight:
    • ORG_REPO covers the owner namespaces memolabs-dev and guillaume-flambard; any other owner is still refused before any GitHub call.
    • The receipt carries repository_private, GitHub's answer.
    • The PUBLIC_ALLOWLIST literal is kept: deployed coordinators still parse it.
  • private_owner_allowlist:
    • Mode stdio (the owner's local Memo Bridge) is accepted next to runtime, with the same darwin, host, uid, 0600, fixed path and owner_sub checks. Any other mode is refused.
    • A second owner file, external-projects.json, enrolls the owner's checkouts outside MEMOLABS_ROOT; it is validated exactly as memo-bridge validates it.
    • Limit 100.
  • issue_lease:
    • A lease on a private repository stores subject (the digest already used as the ref name) in place of repo and issue.
    • The local receipt still names the repository.
    • Old clear records stay readable; a clear private record loses its identity at its next heartbeat.

Results:

  • 128 tests OK (before: 114).
  • On the real ledger, two private-repository leases written by this code carry only subject.

Verification

  • Commit SHA examined: 9ea7763 (feature 37d2366).
  • python3 -m unittest discover -s tests -p 'test_*.py': 128 tests, OK.
    • New: private records over their whole lifecycle, the clear form for public repositories, forged subjects, redaction of a legacy record, the owner account namespace, enrolled private receipts, stdio and other modes, the 100 limit, and the external file (trust, schema, path, owner).
  • memo-bridge suites run against this branch: 1017/1021 pass. The 3 failures fail on memo-bridge main too and are not ledger tests.
  • Live check, nothing written, real GitHub:
    • a non-enrolled private owner repository gives Repository privacy or enrollment mismatch;
    • a wrong mode gives the same;
    • someone-else/x gives Repository not in the owner namespaces allowlist.
  • Live end-to-end through the companion branch: one claim on a private organization repository and three on a private personal one. Every new lease commit has subject, with no repo, issue or name.
  • Known gaps:
    • The digest is unsalted: whoever guesses an exact owner/repo#n can confirm a lease.
    • The release of a legacy clear private record appends a clear commit.
    • Existing clear records stay in history; cleaning them is destructive and is the owner's decision.
    • A separate private ledger is not built.

Safety and delivery

  • No credentials or private data in patch / screenshots / logs
  • Existing branch, release and deployment rules preserved
  • No automatic deployment or approval implied
  • Human review requested when required (T3: independent review still to do)

Merge order: after memo-bridge#258. memo-bridge main at 8794838 fails one coordinator test against this ledger (a personal-namespace case now reaches GitHub instead of repo_not_allowed).

Rollback: revert. Leases written in the new form then read as "Lease identity mismatch" under the old code until released or reclaimed, so prefer rolling forward. Draft, not merged, not deployed.

… records

Owner decision of 2026-10-22 (memolabs-dev/memo-bridge#257): the owner's
sessions reach every repository he owns, public and private.

- issue_preflight: the owner namespaces memolabs-dev and guillaume-flambard;
  any other owner is still refused before any GitHub call. The receipt
  carries repository_private, GitHub's own answer.
- private_owner_allowlist: the owner's local stdio server (mode stdio) is
  accepted next to the Mac runtime, with the same darwin, host, uid, 0600,
  fixed path and owner_sub checks; a second owner file,
  external-projects.json, enrolls the owner's checkouts outside
  MEMOLABS_ROOT; limit 100.
- issue_lease: a lease on a private repository stores `subject` (the ref
  digest) instead of repo and issue, so the public ledger no longer
  publishes private repository names or Issue numbers. Clear records stay
  readable and lose their identity at their next heartbeat.

Closes #65
Review of memo-bridge#257: a non-normalized path or more than 20 rows made
memo-bridge drop the external projects file while the ledger still
enrolled its repositories. Both sides now refuse the same files.

Refs #65
…vate leases

Fix pass of memo-bridge#258.

Finding 3: sha256('<repo>#<n>') is enumerable (Issue numbers are small).
On 2026-10-22, hashing Issues 1 to 2000 of 7 repository names recovered 99
of the 122 lease refs, 27 on private repositories. For an enrolled private
repository the ref name and subject are now HMAC-SHA256 under the owner's
key (private_owner_allowlist.lease_key, a 0600 owner file on the Mac). The
record keeps a keyed body digest and no Issue update time. A private claim
without the key fails closed. Legacy records stay readable where they are;
a live legacy lease keeps working for its holder, and the next lease moves
to the keyed name.

Finding 2: allowed() grants a new private lease only when the call carries
MEMO_BRIDGE_LEDGER_OWNER_CALL=1, which Memo Bridge sets per call for the
owner's principals only. The holder's own verify and heartbeat do not need
it.

Refs memolabs-dev/memo-bridge#257
guillaume-flambard added a commit to memolabs-dev/memo-bridge that referenced this pull request Oct 22, 2026
…cipals only

Fix pass of #258, finding 2. hub:owner, hub:readonly and reader: on the
Mac, and any principal where no private file was set, got the owner
namespaces: tasks_list and task_next searched non-enrolled private
repositories with the owner's gh token and returned titles and bodies.

- withOwnerPrivateRepos attaches the namespaces only for the owner
  (isOwnerPrincipal: oidc:<owner_sub>, or the minted local OS user).
- A repository matched only through the namespaces is searched with
  is:public; exact entries as before. Results are deduplicated.
- task_claim goes through ledger.forOwner() for the owner only, which sets
  MEMO_BRIDGE_LEDGER_OWNER_CALL=1 for that call; the variable is never
  inherited from the server environment. memolabs-dev/.github#66 requires
  it before granting a new private lease.

Refs #257
guillaume-flambard added a commit to memolabs-dev/memo-bridge that referenced this pull request Oct 22, 2026
Fix pass of #258, finding 4. A stage run rewrote the live private-repos.json
and remote.json the running runtime reads; with the old release that makes
the Mac path fail closed until --install.

configure now writes the policy files, a record of the live ones and, when
the owner has none, a new lease-key.json under --out/owner-policy/.
promote (called by --install right before the restart, under the
enrollment lock) moves them into place, keeps a 0600 .bak of each live
file, never replaces a live lease key, and refuses if a live policy changed
since the stage run. The plist sets MEMO_BRIDGE_OWNER_LEASE_KEY_FILE for
the keyed private lease names of memolabs-dev/.github#66 (finding 3).

Refs #257
@guillaume-flambard

Copy link
Copy Markdown
Contributor Author

Fix pass after the security review of memo-bridge#258. Still DRAFT. Head 88fb819.

Finding Status
2: the ledger child in runtime/stdio mode granted a private lease to any caller Fixed: allowed() grants a new private lease only with MEMO_BRIDGE_LEDGER_OWNER_CALL=1. memo-bridge sets it per call, for the owner only. The holder's own verify/heartbeat (holding=True) does not need it.
3: unsalted lease ref names are enumerable Fixed: see below.

How finding 3 is fixed:

  • For an enrolled private repository, the ref name and subject are now HMAC-SHA256(owner key, "star-lease-v1\0owner/repo#n").
  • The key comes from lease_key(): a 0600 owner file, owner-policy/lease-key.json, read only on the owner's Mac.
  • The record keeps a keyed body digest and no issue_updated_at.
  • A private claim without the key fails closed.
  • Legacy records stay readable where they are. A live legacy lease keeps working for its holder; the next lease moves to the keyed name, with the epoch carried over.
  • Re-measured: hashing Issues 1 to 2000 of 7 names recovers 99 of the 122 refs, 27 of them private. On the Mac, the new keyed refs for memo-orbit#126 and jobs#6 are not recovered.
  • Docs corrected: the old text said "confirm that this Issue was leased, and nothing more".

Tests: 14 new or changed tests were red first (2 failures, 12 errors), then the suite went green: 137 OK. Real CLI against GitHub: a claim without the marker is refused with "Repository privacy or enrollment mismatch", and nothing is written.

Owner decision still open: deleting the historical clear and unsalted refs. That is destructive, and their commits may stay fetchable by SHA.

Merge after memo-bridge#258. Deploy both together on the Mac (the lease key is already in place).

@guillaume-flambard
guillaume-flambard marked this pull request as ready for review October 23, 2026 17:19
@mergify

mergify Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

mergify Bot added a commit to memolabs-dev/memo-bridge that referenced this pull request Oct 23, 2026
…nd remote) (#258)

* feat(access): every owner project for the owner's principals (local and remote)

Owner decision of 2026-10-22 (#257): the owner reaches every project he
owns through Memo Bridge, public and private, in his namespaces
memolabs-dev and guillaume-flambard, from his remote connector and from
his local stdio session.

- owner-private-repos: the minted local:<OS user> principal reads the Mac
  owner policy like oidc:<owner_sub>, on mac only; any other remote
  principal is still refused; other local principals get no private
  repository. Owner namespaces, limit 100.
- coordinator pre-filter (allowedRepo): an exact entry first, then any
  repository of the owner's namespaces; the ledger decides public,
  enrolled private or refused. Any other owner never reaches the ledger.
- external projects: a Mac-only 0600 owner file
  (owner-policy/external-projects.json) adds the owner's checkouts outside
  MEMOLABS_ROOT to the catalog, inside his home, outside private folders,
  origin checked. Kept out of the catalog snapshot export and the operator.
- the stdio server gives its ledger child MEMO_BRIDGE_AGENT_MODE=stdio and
  MEMO_BRIDGE_HOST unless set; enrollment refusals map to repo_not_allowed.
- remote.json and enrollment limits 100; Mac installer accepts the owner
  namespaces and enrolls external projects; bridge_info states the live
  write gate and the owner scope; docs/OWNER_SCOPE.md.

Needs memolabs-dev/.github#65 for the stdio mode, the owner account
namespace and private lease records on the ledger side.

Closes #257

* fix(sandbox): a checkout directly in the home closes the checkout, not the home

Found by the local end-to-end check of #257: for an owner external checkout
such as ~/<name>, the folder holding the repository is the home itself.
sandboxPaths denied it whole, which also closed every toolchain under the
home (nvm, pnpm), so both the install and the recipe phase failed with
"Operation not permitted". Close the checkout itself instead (its .env and
the owner's working files); the worktree and the Git data stay reopened
after it, and the private-folder list still covers the rest of the home.

Refs #257

* fix(sandbox): keep the owner's mail export in the home root out of jobs

Review of #257: in the recipe phase the home is open except the private
denylist, which did not name ~/gmail.json; a job could print it to its log.

Refs #257

* fix(sandbox): close the owner's home to recipes, not only the denylist

Fix pass of #258, finding 1 (blocking). The recipe phase read every home
file the denylist did not name: ~/.zshrc with its exported tokens,
~/.config secrets. A test script written through worktree_write_file could
copy one into the worktree, and worktree_read_file would return it.

The recipe profile now closes the home like the install phase: home denied,
then the PATH tool roots and a few read-only toolchain caches, then the
denylist on top, then the worktree, the job folder and the Git data. The
denylist also names the shell startup files and the measured .config
secrets. Seatbelt tests cover a fake home and the real one (byte counts
only).

Refs #257

* fix(access): owner namespaces and private claims for the owner's principals only

Fix pass of #258, finding 2. hub:owner, hub:readonly and reader: on the
Mac, and any principal where no private file was set, got the owner
namespaces: tasks_list and task_next searched non-enrolled private
repositories with the owner's gh token and returned titles and bodies.

- withOwnerPrivateRepos attaches the namespaces only for the owner
  (isOwnerPrincipal: oidc:<owner_sub>, or the minted local OS user).
- A repository matched only through the namespaces is searched with
  is:public; exact entries as before. Results are deduplicated.
- task_claim goes through ledger.forOwner() for the owner only, which sets
  MEMO_BRIDGE_LEDGER_OWNER_CALL=1 for that call; the variable is never
  inherited from the server environment. memolabs-dev/.github#66 requires
  it before granting a new private lease.

Refs #257

* fix(install): stage the owner policy, promote it only with --install

Fix pass of #258, finding 4. A stage run rewrote the live private-repos.json
and remote.json the running runtime reads; with the old release that makes
the Mac path fail closed until --install.

configure now writes the policy files, a record of the live ones and, when
the owner has none, a new lease-key.json under --out/owner-policy/.
promote (called by --install right before the restart, under the
enrollment lock) moves them into place, keeps a 0600 .bak of each live
file, never replaces a live lease key, and refuses if a live policy changed
since the stage run. The plist sets MEMO_BRIDGE_OWNER_LEASE_KEY_FILE for
the keyed private lease names of memolabs-dev/.github#66 (finding 3).

Refs #257

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
@guillaume-flambard
guillaume-flambard merged commit 0d0cda5 into main Oct 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(lease): owner namespaces, local owner sessions and private lease records

1 participant