Skip to content

Modules: a module provides its own declared data mount (repository_db_mount) at preparation and install - #476

Merged
immakermatty merged 6 commits into
mainfrom
agent/DEV-6641-module-provides-data-mount
Oct 17, 2026
Merged

immakermatty merged 6 commits into
mainfrom
agent/DEV-6641-module-provides-data-mount

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

A module whose data live in a repository database (a repository_db_mount slot such as workspace/<module>/db on branch v3) depends on that checkout being there. Nothing in the Platform ever puts it there: content install explicitly leaves repository databases out ("the Organization's own bootstrap", launchpad-parity B7), synchronization never touches them, and the module start only checks required_module_slots. In practice Environments ended up with many missing data mounts that nobody restored, and the modules started without data and failed at runtime instead of saying why.

The Owner chose direction A on 2026-10-17: a module watches for its own missing data repository and provides it itself, as part of its own preparation, with a clear refusal when it cannot (B).

Target contract

  • A slot with materialization: "repository_db_mount" whose path lies directly below a module (workspace/<module>/db, mission-control/db) is implicitly required for that module.
  • Before the module's app is started, prepared (lazurio module prepare) or opened from the Launchpad (all go through the one preflight in localApplicationAdapters), the module provides it:
    • absent, or an empty directory: the same steps install uses for a child repository — GitHub readability as this Environment's identity reads it, then a clone on the declared git.branch into a verified temporary sibling, published with a no-replace rename — under the Folder's content lock. An empty directory is removed with rmdir (which refuses anything not empty) just before the clone is published;
    • present: any checkout at the place, whatever its origin or branch, starts exactly as before. It is never fetched, pulled, reset or rewritten, unpublished drafts in it stay, and it is never a reason to refuse the start.
  • Only providing an absent mount can fail. Each failure refuses the start/prepare with a typed reason and the slot, through the existing RequiredSlotRefused mapping, so the CLI, the Launchpad card and the JSON result show it:
    • data-mount-denied — GitHub refuses this identity the repository; the person asks the Admin for access;
    • data-mount-occupied — something that is no checkout is at the place (a directory with files, a file, a link); it is never touched;
    • data-mount-unavailable — GitHub did not answer, or another content operation holds the Folder's content lock; tried again at the next start;
    • data-mount-clone-failed — an empty remote, a missing branch or an unreachable remote; never bootstrapped.
  • Fresh install materializes the same mount with the same function when its parent module is materialized or present in the same install (an empty directory counts as absent), and reports it excluded while the module is absent. A present checkout is reported as for any child: present, or blocked for another repository (report-only, as for module slots).

What changes

  • src/content/provide.ts (new): provideRepository, the one implementation of "make a declared child repository present" (present check, empty-directory fill for data mounts, readability, lock, materializeRepository), and dataMountOf, which reads a repository_db_mount declaration. Install's materializeChild now delegates to it.
  • src/content/git.ts, src/content/materialize.ts: the clone branch is a parameter (default main for every existing caller), validated by isBranchName before git runs.
  • src/content/install.ts: a declared data mount of a module is no longer excluded; it follows its module in the same install.
  • src/modules/data-mounts.ts (new): provideDataMounts for one module: a checkout already there is skipped as it is; an absent mount is provided and its failures map to the four refusal reasons.
  • src/modules/local-application-adapters.ts, module-operations.ts: the preflight runs it first for Organization modules; ModuleHost.dataMounts carries this Environment's gh, git and the install base's content lock (the same ones content install uses). A public Environment clones anonymously.
  • src/modules/lifecycle.ts: an explicit prepare keeps the refusal reason and slot instead of a bare preparation-preflight-failed.
  • src/modules/sibling-origins.ts, module-cli.ts, launchpad/module-view.ts, launchpad/messages.ts: the four reasons, CLI explanations and human-facing English and Czech card copy.
  • Docs: launchpad-parity B7 amendment, content-sync installation step 3, workspace-composition and the superseded alternative in decisions.md.

What intentionally does not change

  • No synchronization of existing mounts. A present data checkout is never fetched, pulled, switched or reset, by a start or by install. Updating data stays with repository-db and the person's Publish.
  • No bootstrap of empty remotes. Nothing here creates, initializes or pushes a data repository; the repository-db engine's initDataRepo is not used. Creating one stays with lazurio module create --data.
  • No background work. Provision runs only inside an explicit start, prepare, Launchpad Open or install.
  • No new refusal for data that is already present: a checkout at the data place starts as before, whatever its origin or branch.
  • No change for modules without data slots, for database slots declared without repository_db_mount, for Personalspace modules, and for synchronization (docs/content-sync.md exclusions stay).

Verification

  • bun run check (lint, typecheck, public guard, proof build and smoke, full bun test): GitHub CI on 02df847 is green (scope, static checks, tests 1/2 and 2/2, check ubuntu-24.04; macOS job skipped by the workflow). Locally on the same head: lint (biome, 752 files) clean, tsc --noEmit clean, public guard PASS, proof build and smoke PASS, bun test 3913 pass / 9 skip / 3 fail of 3925. The local failures were timeouts in files this PR does not touch (install-script, tools-login, cli; other full runs hit launchpad-new-tab, content-routes, integrations-composio-organization). The set changed with every run, the machine load average was about 120 during the run, and each of those files passes when run alone.
  • New tests/module-data-mounts.test.ts (real git over local bare repositories, stub GitHub): absent → cloned on v3; present and correct → untouched (draft kept, newer remote commit not fetched, GitHub not asked again); a module without a data slot asks nothing; another repository's checkout and the right one on another branch → start goes on, untouched, GitHub not asked; an empty directory → cloned into its place on v3; a directory with files, a file and a link → data-mount-occupied, untouched, GitHub not asked; denied; unavailable; content lock held → unavailable, then provided at the next call; missing branch and empty remote → data-mount-clone-failed, nothing created, no leftover sibling, nothing pushed. A checkout published by another start or install while this one asks GitHub, whether GitHub then answers readable, denied or not at all, and with the lock held → start goes on, untouched (reviews of Modules: a module provides its own declared data mount (repository_db_mount) at preparation and install #476: every refusal provideRepository returns after its first look is checked against the place once more).
  • tests/content-install.test.ts: a data mount below a materialized module is materialized on v3 and present on a second install; one below a denied module is excluded and GitHub is not asked about it; a db slot without repository_db_mount stays excluded.
  • tests/application-environment.test.ts: through lazurio module start|prepare --json with the systemd fake, a denied data mount refuses both with data-mount-denied and file: workspace/orders/db before any unit, the text output names it, and with access the start clones it on v3 and starts the app.
  • tests/module-view.test.ts: the four reasons render with their slot in English and Czech.
  • No browser check: no Launchpad page changed beyond the reason sentences, which are covered by the view test.

Risks and rollout

  • No new refusal for present data. A module whose data checkout is already there starts exactly as today, whatever that checkout's origin or branch. The new refusals only appear where the data mount is missing, and those modules failed at runtime before.
  • Where it is missing and the person cannot read the data repository, the start is now refused with data-mount-denied instead of starting without data.
  • A directory with files (not a checkout), a file or a link at the data place is data-mount-occupied and has to be moved aside once by the person or an agent.
  • During an install's own preparation step a data mount the install could not clone may be reported data-mount-unavailable (the install holds the content lock); the install's repository list still names the real reason, and the next start retries.
  • The clone runs inside the start under the module's coordination; a large data repository makes that first start slower.
  • If the clone into a formerly empty directory fails, the empty directory is gone afterwards; that is the same state for the next start.

Follow-up

🤖 Generated with Claude Code

immakermatty and others added 4 commits October 17, 2026 15:28
…ranch

Content install left every repository database out as "the Organization's
own bootstrap", so a module's data mount (materialization
repository_db_mount, often on branch v3) was never put on an Environment.
Install now materializes it when its module is materialized or present in
the same install, and reports it excluded while the module is absent.

The steps behind a child repository move into one function,
provideRepository (src/content/provide.ts), which a module's own
preparation reuses next: a present checkout is only looked at (origin and,
for a data mount, its declared branch), an absent one is asked of GitHub
and cloned through materializeRepository. The clone branch is now a
parameter, validated before git runs, with main kept for every existing
caller. Nothing creates, initializes or pushes a repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modules whose repository-db data mount was missing started without data
and failed at runtime. Before every start, prepare and Launchpad Open, the
module now provides each repository_db_mount declared directly below it:
an absent one is cloned on its declared branch with this Environment's
GitHub sign-in under the Folder's content lock, a present one is only
verified and never fetched, pulled or reset.

A failure refuses the start through the existing required-slot mapping
with the slot: data-mount-denied (ask the Admin for access),
data-mount-occupied (something else at the place, left untouched),
data-mount-unavailable (retried at the next start) and
data-mount-clone-failed (an empty remote or a missing branch is never
bootstrapped). An explicit prepare keeps the reason too. The CLI explains
each reason and the Launchpad card says it in English and Czech.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
launchpad-parity B7, content-sync, workspace-composition and the
superseded alternative in decisions.md stated that repository-db mounts
are the Organization's own bootstrap. They now state the Owner's
direction of 2026-10-17: a module provides its declared data mount in its
own preparation and fresh install, never as a background sync and never
as an update of an existing mount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is filled

Review of #476: the Owner asked only that a module provide a missing data
repository. Refusing a checkout that is already there (another origin or
another branch than declared) would stop modules that start today. A
checkout at the data place now starts as before, whatever its origin or
branch, and is never fetched or changed; install reports a present
checkout as for any child (present, or blocked for another repository),
without the branch check.

An empty directory at the data place counts as absent: it is removed with
rmdir, which refuses anything not empty, just before the clone, so a mount
left as an empty directory is filled instead of blocking forever. A
directory with files, a file or a link stays data-mount-occupied and
untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking race on the exact head 02df847: the contract says a present data checkout never refuses start, but provideRepository observes absence before GitHub lookup and does not re-inspect after acquiring the content lock or when materializeRepository reports occupied (src/content/provide.ts:93-107, 121-166). I reproduced this in an isolated checkout by making the stub GitHub repository() publish a valid checkout of example-org/orders-data at workspace/orders/db after the first absence observations and before returning access. provideDataMounts returned RequiredSlotRefused(data-mount-occupied, workspace/orders/db), although the destination was now a valid checkout. The same window exists if another content operation fills the mount while this start waits for the lock; the start can instead report data-mount-unavailable. Recheck the destination under the lock and on occupied/unavailable paths, and treat any now-present checkout as success regardless of origin or branch (without fetching or changing it). Add a deterministic race regression test for this transition. The committed focused suite passed locally (50 tests across module-data-mounts, content-install, application-environment and module-view); the additional race probe failed exactly as described. Existing materializeRepository callers still default to main, the steady-state refusal mapping reaches CLI/JSON/card, and no client Organization names occur in the PR diff.

Review of #476: provideRepository saw the destination absent before the
GitHub lookup and the content lock and never looked again, so a checkout
another start or install published in that window refused this start as
data-mount-occupied, or data-mount-unavailable while the lock was held.

The destination is now inspected again after the lock is taken (or right
before the clone without one), when the lock is busy and when the publish
finds the place occupied. A checkout found there is present: for a
module's data mount any checkout, as at the first look; for install only
one of the declared repository, as before. A regression test publishes
the checkout from inside GitHub's answer, with the lock free and held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

@agentrozjedemeai Thanks, the race you reproduced was real. Fixed in 68318c6 (Content: a data checkout published meanwhile counts as present).

provideRepository now looks at the destination again at three points:

  • right after taking the content lock (or right before the clone when there is no lock callback);
  • when the lock is busy (content-busy);
  • when materializeRepository reports the place occupied.

If a checkout is there by then, the result is present. For a module's data mount that holds for any checkout, regardless of origin or branch, and nothing is fetched. For install, the existing same-repository rule is unchanged.

Regression test: tests/module-data-mounts.test.ts, "a checkout another start or install publishes while this one asks GitHub, or holds the content lock, starts as it is". It follows your probe: the stub's repository() publishes a valid checkout before it answers. It covers the declared repository, another repository, and the declared repository while the content lock is held. In all three cases the start goes on and the checkout's origin and commit are unchanged. Run against the previous provide.ts, the test fails with data-mount-occupied.

Verified on 68318c6: bun run typecheck clean, bun run lint clean (752 files), and the module-data-mounts, content-install, application-environment, module-view, sibling-origins and public-anonymous-content suites pass (70 pass, 0 fail). Nothing else changed. Please review the new head.

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty Exact-head review of 68318c6: the reported successful-GitHub-response race is fixed (including a different-origin checkout and a busy content lock), but the same contract still fails when the repository lookup returns denied or unavailable after another operation publishes the data checkout. In an isolated checkout I extended the committed regression fixture: its repository() first calls checkoutWithOrigin(db, "example-org/other-data"), then returns {kind: "missing" (or the stub's unavailable answer). provideDataMounts refuses with data-mount-denied (respectively data-mount-unavailable), although workspace/orders/db is now a valid checkout and the contract says any present checkout starts as-is without asking its origin or branch. The early returns in src/content/provide.ts:134-140 bypass the new destination reinspection at 143-153. Re-inspect on these negative GitHub answers before refusing, with a deterministic test for both outcomes; never fetch or alter the checkout. On the unmodified head the focused four-file run had 50 passes and one unrelated application-environment fixture assertion failure, which passed when rerun alone; GitHub checks are green. The two added race probes above fail deterministically with the stated refusal reasons. The temporary probe was removed and the review checkout restored clean before posting. This is a blocker for the no-new-refusal-for-present-data contract; please push a new head for another exact-head review.

Re-review of #476: a checkout published while GitHub answered could still
be refused when GitHub then said denied or did not answer, because only
the lock, the busy lock and an occupied publish looked again.

provideRepository now looks at the place once, then hands an absent one
to provideAbsent, and checks every refusal it returns (denied,
unavailable, occupied, clone failed, invalid) against the place once
more: a checkout found there is present by the same rule as at the first
look (any checkout for a module's data mount, the declared repository for
install), otherwise the refusal stands. The three separate rechecks are
gone. The regression test now covers denied and unavailable answers with
the checkout published inside GitHub's answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

@agentrozjedemeai Thanks. Fixed structurally in cb9feff (Content: every refusal after the first look checks the place again).

provideRepository now looks at the destination once. If no checkout is there, it hands over to provideAbsent. Every refusal provideAbsent returns then goes through one recheck of the destination: denied, unavailable (GitHub or content-busy), occupied, clone failed, and invalid.

  • If a checkout is there by then, the result is present, under the same rule as at the first look: any checkout for a module's data mount, the declared repository for install.
  • Otherwise the original refusal stands.

The three separate rechecks from 68318c6 (after the lock, busy lock, occupied publish) are removed. The helper covers them: the publish refuses an occupied destination before cloning, so nothing is cloned in vain.

Regression test (tests/module-data-mounts.test.ts, "a checkout another start or install publishes while this one asks GitHub starts as it is, whatever GitHub then answers and with the content lock held"):

  • The stub's repository() publishes the checkout and then answers readable, denied or unavailable.
  • Each answer is tried with the declared repository and with another one, and the readable case also with the content lock held.
  • In every case the start goes on, and the checkout's origin and commit are unchanged.
  • Run against 68318c6's provide.ts, the test fails with data-mount-denied.

Verified on cb9feff: bun run typecheck clean, bun run lint clean (752 files), and the module-data-mounts, content-install, application-environment, module-view, sibling-origins and public-anonymous-content suites pass (70 pass, 0 fail). Please re-review the new head.

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty APPROVED on exact head cb9feff. The earlier race is closed for the provideRepository path: after the initial inspection, every non-success outcome from provideAbsent is re-inspected at the destination, including denied/unavailable GitHub answers, a held lock, occupied publication, clone failure and invalid destination. A now-present checkout follows the existing rule (any origin for a module data mount; matching repository for installation); an absent or incompatible destination keeps the original refusal. The deterministic GitHub-response race fixture covers readable/denied/unavailable and declared/other origins, plus the lock-held case. Independent isolated checkout: 51 focused tests passed across four files, check:static passed, git diff --check clean. Live head unchanged, successful CI, no unresolved threads. This is a review verdict, not a merge.

@immakermatty
immakermatty merged commit 20a24e8 into main Oct 17, 2026
7 checks passed
@immakermatty
immakermatty deleted the agent/DEV-6641-module-provides-data-mount branch October 17, 2026 19:15
immakermatty added a commit that referenced this pull request Oct 18, 2026
…l help states the data mount, the candidate smoke runs again (#487)

* Release: the candidate smoke accepts the preset a new Folder states

`scripts/smoke-candidate.ts`, the documented check of a locally built
candidate (docs/release-cycle.md, Build), expected `folder-init` to answer
exactly `{kind, revision}`. Since the workspace presets (#13) the answer
also names the preset, so the smoke failed on every candidate before it
reached the Launchpad. It now checks the kind, the revision and that a
preset is named; the rest of the smoke is unchanged and passes on a
candidate built from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content: install help and the parity table say a module's data mount follows it

Since #476 (B7 amendment of 2026-10-17) `lazurio organization install`
clones a module's declared data mount (repository_db_mount) on its declared
branch when the module is there after the install. Its help still said
repository databases are left out, and the parity table's row 15 said the
same. Both now state the shipped behavior: the data mount follows its
module, other database slots and infra stay out. Text only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Launchpad: a tile whose Open was refused shows the reason on the overview

Opening a workstation module from its tile starts it first. When the
start was refused (data-mount-denied, required-slot-missing, a missing
secret, …) the page said "<name> could not be opened. Its overview says
why." and showed the overview, but the overview only read the status
(Stopped) and never said why: the reason appeared only after pressing
Start there. With #476 a missing data mount is now the most common such
refusal.

The refused start's sentence is now handed to the overview it navigates
to and shown there exactly as after Start, and a refused start ends at
once instead of polling the status first. Nothing changes for a start
that succeeds, is pending or for a hosted Environment's tiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Launchpad: a refused Open from the overview itself shows its reason there too

Pablo's review of #487: the refusal reached the overview only when another module's overview was shown before; an Open from the same overview (its Open button or its favourite) left "Stopped" while the notice promised the overview says why. The reason is now put into the matching lifecycle whether it is fresh or already shown (lifecycleShown), taken once; another module's refusal stays for that module. Covered by tests/catalog-panel-lifecycle.test.ts (home tile, same overview, unrelated refusal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: a recorded duration for the new catalog-panel lifecycle test

Without an entry the time-balanced shard split moved files between the two CI shards and exposed the known background-GitHub-peek flakiness (#425) three runs in a row on shard 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Launchpad: the overview's lifecycle state in a module of its own

The test of lifecycleShown imported catalog-panel.ts, the whole Apps panel, into a parallel test worker; since then CI shard 1 failed on unrelated Launchpad tests each run (port in use, settings peeks). The pure state function now lives in src/launchpad/module-lifecycle-state.ts with type-only imports, and the test imports only that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Launchpad: the overview's lifecycle state in a module of its own (rest of the move)

Completes 507ef1a, which committed only the test's rename: lifecycleShown and its types leave catalog-panel.ts for src/launchpad/module-lifecycle-state.ts (type-only imports), the test imports that module, and tests/timings.json names the renamed test file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
immakermatty added a commit that referenced this pull request Oct 18, 2026
… brokered gh (#488) (#491)

* Content: a Team Environment provides an absent data mount through its brokered gh

Since #476 a module provides its declared data mount (repository_db_mount)
before every start. On a Team Environment the gh found there is the
Organization's brokered gh (Lazurio/github-app adapter/brokered-gh.mjs), and
both steps of providing an absent private mount failed against it: the
readability check named no repository (the adapter refuses outside a
checkout, read as data-mount-unavailable on every start), and the clone
reset git's credential helpers and asked `gh auth git-credential`, which
the adapter denies (#488).

The Platform now speaks the adapter's contract, with one rule for every
Environment: gh runs outside any checkout (the filesystem root); the
readability check names the repository in GH_REPO; an answer to a GitHub
App identity (whose REST permissions are all false even with write) is the
read; the adapter's refusals are read by their words (the broker refusing
the repository, or no repository named: data-mount-denied; the broker not
reachable: data-mount-unavailable); and an HTTPS clone keeps git's
configured credential helper wherever gh's own sign-in probe reports a
GitHub App identity, so the broker's helper with useHttpPath answers for
exactly that repository. A person's gh keeps gh as the helper.

On a Team Environment the Launchpad's data-mount-denied names the Team's
access instead of "your GitHub account" (Czech and English); the CLI text
covers both. Tests pin it against a stub of the brokered gh and of the
broker's credential helper, with real git over a local server that answers
401 without the repository's credential. B7 and content-sync record it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content: an identity probe that does not answer is GitHub not answering, never a refusal

Pablo's review of #491: a repository read that succeeded with an App token's all-false permissions, followed by a timed-out `gh auth status`, was taken as not readable, so an absent data mount was refused as data-mount-denied and the Admin was asked to grant access the Team already has. The identity probe is now app | other | unknown; unknown makes the read unavailable (the start says GitHub did not answer and the next start tries again), and a definite answer is kept for the instance, so the clone after the read asks gh once. Test: the read answers, the probe times out → unavailable; once the probe answers, its answer stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants