fix(#1264): preserve portable standalone launch
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
goals
2026-08-16 19:01:26 -05:00
parent 9dc90be7e1
commit 3af594590a
11 changed files with 161 additions and 68 deletions
@@ -27,7 +27,8 @@ The fleet path never falls back to an interactive wizard. It exits nonzero befor
when:
- `MOSAIC_AGENT_NAME` is not an exact roster member;
- an ambient `MOSAIC_AGENT_CLASS` disagrees with that member's canonical class;
- a defined ambient `MOSAIC_AGENT_CLASS` is blank/whitespace or disagrees with that member's
canonical class;
- a missing destination has no safe regular default source;
- a source or existing destination is a symlink (including dangling), directory, unavailable, or over the bounded size;
- the fleet communications helper/roster cannot be validated; or
@@ -56,8 +57,8 @@ The build leg is load-bearing: `dist/` is ignored, so a direct Vitest invocation
absent or stale CLI output. The gate runs the exact-source built CLI in subprocesses with piped stdin,
temporary homes, a canonical fixture roster, fake runtime/broker executables, and no provider call. It
covers no-TTY launch, exact identity,
private modes, no-clobber, missing/symlink defaults, unknown members, class mismatch,
standalone wizard preservation, and concurrent first start.
private modes, no-clobber, missing/symlink defaults, unknown members, blank/mismatched class,
portable standalone composition/wizard preservation, and concurrent first start.
Do not use this fixture as proof that a real provider credential is present or that a package has
been deployed. Those require separate environment-specific evidence.
@@ -16,8 +16,8 @@ The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet command
1. A present, nonblank, whitespace-exact `MOSAIC_AGENT_NAME` selects the fleet path.
2. `resolveFleetIdentity()` must resolve that exact member through the existing roster/helper
boundary, and any ambient `MOSAIC_AGENT_CLASS` must canonicalize to the roster class, before any
identity seed.
boundary, and any defined `MOSAIC_AGENT_CLASS` (including blank/whitespace) must canonicalize to
the roster class, before any identity seed. Only undefined means absent.
3. `lstatSync()` preflights every destination directory entry without following links, so a dangling
link is rejected before its counterpart can be published.
4. Safe bounded snapshots are read from only the missing contracts under `defaults/`.
@@ -26,10 +26,12 @@ The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet command
concurrent seat or operator won; the existing path is preserved and revalidated.
7. Temporary files are removed, and both installed contracts are re-opened through the no-symlink
secure-file reader before launch continues.
8. `composeContract()` independently re-resolves the roster, securely reads `USER.md` through a
descriptor at the point of use, and injects exact member identity and communications data.
8. `composeContract()` independently re-resolves the roster, securely reads fleet `USER.md` through
a Linux descriptor at the point of use, and injects exact member identity and communications data.
A standalone launch with no `MOSAIC_AGENT_NAME` retains the interactive wizard.
A standalone launch with no `MOSAIC_AGENT_NAME` retains the portable tolerant USER read and the
interactive wizard. Fleet-only no-follow enforcement must not make supported standalone macOS
composition depend on Linux `/proc` descriptor traversal.
## Identity and authority
@@ -42,9 +44,9 @@ not the source of a fleet seat's identity. The canonical roster controls:
- tmux socket and helper target; and
- communications generation.
An unknown/padded ambient name or mismatched ambient class fails before any file is seeded. This
avoids replacing the interactive wall with a fleet of indistinguishable or ambiently invented
identities.
An unknown/padded ambient name, mismatched class, or explicitly blank/whitespace class fails before
any file is seeded. This avoids replacing the interactive wall with a fleet of indistinguishable or
ambiently invented identities.
## Concurrency and filesystem properties
@@ -64,8 +66,9 @@ no-TTY subprocess, not a direct wizard test. The package `test:vitest` gate buil
Vitest, while the clean-checkout command builds its workspace dependencies first, so ignored
`dist/cli.js` cannot be absent or stale. A fake lease launcher records whether execution reached the
runtime boundary and captures the composed prompt.
Positive and negative cases prove the check can both proceed and refuse. Composition coverage also
replaces a previously validated `USER.md` with an external symlink and proves point-of-use refusal.
Positive and negative cases prove the check can both proceed and refuse. Fleet composition coverage
replaces a previously validated `USER.md` with an external symlink and proves point-of-use refusal;
a standalone unreadable-optional-USER case proves the portable tolerant branch remains separate.
Real Pi authentication and provider task execution remain environment tests, not claims of this
fixture.
@@ -25,8 +25,8 @@ base. The canonical roster row supplies each seat's exact name, class, peers, so
## Operator behavior
A normal standalone launch without a fleet identity still uses the interactive wizard when
`SOUL.md` is absent:
A normal standalone launch without a fleet identity retains its portable configuration path and still
uses the interactive wizard when `SOUL.md` is absent:
```bash
mosaic pi
@@ -38,8 +38,8 @@ A roster-owned seat may be started without attaching to its pane:
mosaic fleet start <exact-roster-name>
```
Mosaic refuses before runtime execution if the requested member is absent, its ambient class
conflicts with the roster, a required default is missing or unsafe, or an existing identity contract
Mosaic refuses before runtime execution if the requested member is absent, its explicitly supplied
ambient class is blank or conflicts with the roster, a required default is missing or unsafe, or an existing identity contract
is not a safe regular file. Repair the named component and retry the same exact roster member; do not
copy another seat's personalized identity.
@@ -71,6 +71,25 @@ Result: risk `none`, confidence `0.93`, 9 files, no critical/high/medium/low fin
could not run Vitest because Vite attempted to create a temporary config artifact on its read-only
mount (`EROFS`); executor-owned focused and full results are recorded in the QA report.
## Second exact-head review
Daphne reviewed exact head `9dc90be7e13b1cd609f6df97d43d890ef5392ca0` and filed Gitea review
ID 169 as `REQUEST_CHANGES`. Review 169 confirmed all review-168 closures, then found:
1. the new point-of-use reader was Linux-only but had been applied to every standalone USER read,
breaking supported non-fleet macOS composition; and
2. explicit blank/whitespace `MOSAIC_AGENT_CLASS` was treated as absent and could seed before
runtime, while only undefined should mean absent.
Red-first remediation preserves legacy `readOptional()` for standalone composition, keeps descriptor
no-follow consumption fleet-only, moves the replacement-symlink case under a valid fleet identity,
and rejects defined blank/whitespace classes before seeding. Three blank-class CLI cases and one
tolerant standalone composition case failed before the source change and pass after it.
Review-169 remediation code review approved at confidence `0.90` (4 files, no findings). Security
review reported risk `none` at confidence `0.90` (4 files, no findings). The review sandbox retained
its known Vite `EROFS` limitation; executor-owned tests are in the QA report.
## Remaining review gate
Daphne must re-review the next exact pushed head. This report cannot record that future verdict
@@ -25,6 +25,8 @@
- [x] Initial padded-name finding remediated and automated re-review approved.
- [x] Daphne formal review ID 168 completed on exact first head `43fa0477` and requested changes.
- [x] Four formal-review groups reproduced red and remediated; automated remediation code/security
reviews are clean.
- [ ] Daphne exact-remediation-head re-review completed after push (Fred/goals excluded).
- [x] Four review-168 groups reproduced red and remediated; automated reviews are clean.
- [x] Daphne review ID 169 completed on exact head `9dc90be7` and confirmed review-168 closures.
- [x] Review-169 standalone-portability and blank-class blockers reproduced red and remediated;
automated reviews are clean.
- [ ] Daphne exact-second-remediation-head re-review completed after push (Fred/goals excluded).
@@ -1,7 +1,7 @@
# #1264 Unattended Fleet First-Start Verification
> Status: **IN PROGRESS — review remediation complete locally; push/re-review pending** | Executor:
> goals | Date: 2026-08-16 | Target: isolated local fixtures only
> Status: **IN PROGRESS — review-169 remediation complete locally; push/re-review pending** |
> Executor: goals | Date: 2026-08-16 | Target: isolated local fixtures only
## Objective
@@ -44,7 +44,7 @@ executables.
| ---------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------- |
| No-TTY fleet first start avoids wizard and reaches runtime | Exact-source built CLI with piped stdin | CLI GREEN |
| Missing top-level identity files are initialized from shipped defaults | Exact-byte and `0600` assertions | CLI + filesystem GREEN |
| Exact seat identity remains roster-owned | Captured argv; name/class mismatch no-side-effect refusals | CLI GREEN |
| Exact seat identity remains roster-owned | Captured argv; mismatched/blank class no-side-effect refusals | CLI GREEN |
| Existing operator identity is never overwritten | Custom bytes/mode with defaults removed | CLI + filesystem GREEN |
| Concurrent/repeated first start is safe | Four parallel CLIs plus repeated launch | CLI GREEN |
| Missing/unsafe defaults and destinations fail before partial mutation | Missing, target/dangling symlink, oversized, invalid-root cases | Filesystem/CLI GREEN |
@@ -85,7 +85,18 @@ passed:
3. dangling `USER.md` allowed `SOUL.md` to be published before refusal; and
4. replacing a securely validated `USER.md` with an external symlink was followed by composition.
This establishes that all four reviewer findings were observable on the pushed implementation.
This establishes that all four review-168 findings were observable on the pushed implementation.
Daphne's review ID 169 then found two more exact-head failures at `9dc90be7`. Before production
changes, four new assertions failed:
1. standalone composition routed an unreadable optional `USER.md` through the Linux-only descriptor
reader instead of the legacy portable tolerant path; and
2. explicit `MOSAIC_AGENT_CLASS` values `""`, `" "`, and tab were treated as absent, seeded both
identity files, and reached runtime.
The replacement-symlink case was also moved under a valid roster identity so it tests the fleet-only
security boundary rather than standalone behavior.
## Final GREEN
@@ -105,16 +116,17 @@ env -u DATABASE_URL sh -c '
'
```
Exit `0`: `6/6` files, `124/124` tests.
Exit `0`: `6/6` files, `128/128` tests.
- 12 real-CLI/no-TTY tests cover exact roster name/class, byte-equal `0600` seeds, no-clobber,
partial seed, missing/symlink defaults, unknown/padded/blank name, mismatched class, standalone
wizard preservation, and four concurrent starts.
- 15 real-CLI/no-TTY tests cover exact roster name/class, byte-equal `0600` seeds, no-clobber,
partial seed, missing/symlink defaults, unknown/padded/blank name, mismatched or explicitly blank
class, standalone wizard preservation, and four concurrent starts.
- 12 direct filesystem tests cover complete publication, existing operators, idempotence, source
prevalidation, target and dangling destination links, invalid roots, oversized input, and
unexpected link errors.
- Composition coverage deterministically replaces validated `USER.md` with an external symlink and
requires refusal at point of use.
- Composition coverage deterministically replaces a valid fleet seat's validated `USER.md` with an
external symlink and requires refusal at point of use. A separate standalone case proves tolerant
optional composition remains outside the Linux-only fleet reader.
Full package gate (which rebuilds Mosaic itself after the clean-checkout dependency build):
@@ -122,13 +134,13 @@ Full package gate (which rebuilds Mosaic itself after the clean-checkout depende
env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic run test:vitest
```
Exit `0`: `88/88` files, `1,573/1,573` tests.
Exit `0`: `88/88` files, `1,577/1,577` tests.
Focused helper + point-of-use coverage:
```text
2 files, 52/52 tests
Statements 97.97% | Branches 88% | Functions 100% | Lines 97.97%
2 files, 53/53 tests
Statements 97.84% | Branches 91.66% | Functions 100% | Lines 97.84%
Exit 0
```
@@ -164,9 +176,14 @@ because this checkout prohibits the PostgreSQL-dependent gateway isolation path.
- Codex security review of the remediation: risk `none`, confidence `0.93`, 9 files, no findings.
Its sandbox could not execute Vitest because Vite attempted a write on a read-only mount; the
executor-owned results above are the test evidence.
- Daphne formal review ID 168 at exact head `43fa0477`: `REQUEST_CHANGES`, four blocking groups. All
four have red-first regressions and local green remediation. Re-review of the next pushed exact
head is necessarily pending until that head exists.
- Daphne formal review ID 168 at exact head `43fa0477`: `REQUEST_CHANGES`, four blocking groups; all
closed by review 169.
- Daphne formal review ID 169 at exact head `9dc90be7`: `REQUEST_CHANGES`, two blocking groups
(standalone portability and explicit blank class). Both now have red-first regressions and local
green remediation.
- Codex review of review-169 remediation: `approve`, confidence `0.90`, 4 files, no findings.
- Codex security review of review-169 remediation: risk `none`, confidence `0.90`, 4 files, no
findings. Exact-new-head Daphne re-review is pending until that head is pushed.
## CI evidence and external blocker
@@ -183,7 +200,9 @@ FAIL: host provides 'pi' in the system path; missing-binary cases are not measur
That line comes from the inherited `test-start-agent-session.sh` CI-fit guard, not #1264. Fred filed
the correction as PR #1270. Its pipeline 2448 is terminal green and proves the four formerly masked
suites execute, but #1270 is not merged, so `next` still carries the failing chain. A new #1268
pipeline is pending the remediation push. Terminal-green #1268 CI is not claimed.
pipeline 2449 at `9dc90be7` reproduced the same single inherited `FAIL:` after Mosaic passed
`1,573/1,573`. A new pipeline is pending the review-169 remediation push. Terminal-green #1268 CI is
not claimed.
PR #1268's envelope was read back as `user.login=mos-dt-0`; its commit is explicitly authored and
committed by `goals <[email protected]>`. No goals Gitea login exists on this host, and no
+23 -16
View File
@@ -7,13 +7,14 @@
- Branch: `fix/1264-fleet-unattended-first-start`
- Base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449`
- First pushed head: `43fa0477877e0d0f110da8d11c3033b40ddeb191`
- Current remediation worktree: `/var/home/jason.woltje/agent-work/1264-review-remediation`
- Current remediation worktree: `/var/home/jason.woltje/agent-work/1264-review2-remediation`
- Coordinator: Fred; reviewer must be neither Fred nor this implementation seat.
- `docs/TASKS.md` is orchestrator-owned and is not modified by this worker.
The original `/var/home/jason.woltje/agent-work/1264-unattended-first-start` worktree was removed
without force after its pushed head and clean state were verified. The Fred-authorized plain-Git
worktree exception was reused for exact-head review remediation because `/src` remains unavailable.
The original `/var/home/jason.woltje/agent-work/1264-unattended-first-start` and first remediation
worktrees were removed without force after each pushed head and clean state were verified. The
Fred-authorized plain-Git worktree exception was reused for exact-head review remediation because
`/src` remains unavailable.
## Objective
@@ -37,14 +38,16 @@ preserving the standalone interactive wizard.
- PRD IDs: `FCM-REQ-12`, `AC-FCM-10`; `FCM-REQ-11` is reserved by #1256.
- A present fleet name must be nonblank, whitespace-exact, and resolve through the canonical roster.
- Any ambient class must canonicalize to the roster class before mutation.
- Any defined ambient class, including blank/whitespace, must canonicalize to the roster class before
mutation; only undefined means absent.
- Preflight all destination directory entries with no-follow existence semantics so dangling links
fail before counterpart publication.
- Seed only missing top-level files from bounded regular defaults with owner-private, atomic,
no-clobber hard links.
- Generic defaults are behavior, not identity or authority.
- Securely consume `USER.md` through a descriptor at composition time.
- Standalone missing identity retains the wizard.
- Securely consume fleet `USER.md` through a Linux descriptor at composition time.
- Standalone composition retains the portable tolerant USER read and missing identity retains the
wizard.
## Progress
@@ -54,9 +57,11 @@ preserving the standalone interactive wizard.
- [x] Initial automated review finding (padded name before write) remediated.
- [x] Commit `43fa0477` pushed; PR #1268 opened against `next`; original worktree removed cleanly.
- [x] Daphne formal review ID 168 completed on exact first head: `REQUEST_CHANGES` with four groups.
- [x] All four groups reproduced red before remediation and now pass locally.
- [x] Remediation Codex review approved; remediation security review risk `none`.
- [ ] Commit/push remediation with explicit goals author/committer; verify remote object/content.
- [x] All four review-168 groups reproduced red before remediation and passed at `9dc90be7`.
- [x] Daphne review ID 169 completed on `9dc90be7`: review-168 closures confirmed; two new blockers.
- [x] Review-169 portability and blank-class blockers reproduced red and now pass locally.
- [x] Review-169 Codex review approved; security review risk `none`.
- [ ] Commit/push second remediation with explicit goals author/committer; verify remote object/content.
- [ ] Daphne exact-new-head re-review.
- [ ] Terminal #1268 CI. Pipeline 2445's only `FAIL:` was the inherited Pi-PATH CI-fit guard; PR
#1270's pipeline 2448 is green, but #1270 is not merged.
@@ -73,13 +78,15 @@ and never created the fake runtime-boundary capture.
Against exact first-head production code, four new tests failed while 1,568 existing tests passed:
class mismatch mutated before refusal; each dangling destination left its counterpart; and a
replacement `USER.md` symlink was consumed by composition.
replacement `USER.md` symlink was consumed by composition. Review-169 RED then proved standalone
composition hit the Linux-only reader and three explicit blank/whitespace class cases seeded and
launched.
### Final local GREEN
- Exact-source focused gate: `6/6` files, `124/124` tests.
- Full exact-source Mosaic Vitest: `88/88` files, `1,573/1,573` tests.
- Helper + point-of-use coverage: `52/52`; 97.97% statements/lines, 88% branches, 100% functions.
- Exact-source focused gate: `6/6` files, `128/128` tests.
- Full exact-source Mosaic Vitest: `88/88` files, `1,577/1,577` tests.
- Helper + point-of-use coverage: `53/53`; 97.84% statements/lines, 91.66% branches, 100% functions.
- Root preflight passed; typecheck `45/45`, lint `25/25`, build `25/25`.
- Initial targeted shell gates passed: start-agent-session, install migration `21/21`, init-RCE.
- Local aggregate framework shell stops at operator-global Pi `0.84.2` versus measured `0.84.1`.
@@ -90,8 +97,8 @@ The full evidence and command boundaries are in
## Review / delivery notes
- Codex remediation review: approve, confidence `0.88`, no findings.
- Codex remediation security review: risk `none`, confidence `0.93`, no findings.
- Review-168 remediation Codex review: approve `0.88`; security risk `none` `0.93`.
- Review-169 remediation Codex review: approve `0.90`; security risk `none` `0.90`.
- PR envelope reads `mos-dt-0`; the commit reads goals/goals. No goals Gitea principal exists on this
host, so no other principal will be borrowed. Tracked in #1272.
- PR #1270 is pushed, not merged. Do not represent `next` or #1268 CI as green until measured.
@@ -334,7 +334,22 @@ describe('composeContract — overlay composer', () => {
}
});
it('refuses a USER.md replacement symlink at the point of composition', () => {
it('refuses a fleet USER.md replacement symlink at the point of composition', () => {
mkdirSync(join(fixture.home, 'fleet'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: exact-user-seat',
' runtime: pi',
' class: worker',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'exact-user-seat';
process.env['MOSAIC_AGENT_CLASS'] = 'worker';
writeFileSync(join(fixture.home, 'defaults', 'SOUL.md'), '# Generic soul\n');
writeFileSync(join(fixture.home, 'defaults', 'USER.md'), '# Generic user\n');
expect(seedFleetIdentityDefaults(fixture.home)).toEqual(['SOUL.md']);
@@ -351,6 +366,17 @@ describe('composeContract — overlay composer', () => {
expect(readFileSync(external, 'utf8')).toBe('UNSAFE-REPLACEMENT-USER-CONTENT\n');
});
it('preserves tolerant standalone composition when optional USER.md is unreadable', () => {
const userPath = join(fixture.home, 'USER.md');
rmSync(userPath);
mkdirSync(userPath);
const out = composeContract('pi', fixture.home);
expect(out).toContain(AGENTS);
expect(out).not.toContain('# User Profile');
});
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'never injects installed TOOLS.md through a target symlink for %s',
(runtime) => {
@@ -51,12 +51,11 @@ function installedEntryExists(path: string): boolean {
}
}
/** Secure point-of-use read for a top-level identity contract. */
export function readOptionalInstalledIdentityContract(
/** Secure fleet point-of-use read for a top-level identity contract. */
export function readInstalledIdentityContractAtPointOfUse(
mosaicHome: string,
entry: (typeof FLEET_IDENTITY_DEFAULTS)[number],
required: boolean = false,
): Buffer | undefined {
): Buffer {
const configuredPath = join(mosaicHome, entry);
try {
// Preserve the launcher's established support for a symlinked Mosaic home,
@@ -68,7 +67,6 @@ export function readOptionalInstalledIdentityContract(
maxBytes: MAX_IDENTITY_CONTRACT_BYTES,
}).content;
} catch (error: unknown) {
if (!required && isFilesystemError(error, 'ENOENT')) return undefined;
throw unsafeIdentityError('installed', configuredPath, error);
}
}
@@ -317,6 +317,23 @@ describe('fleet unattended first start (#1264)', () => {
expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
});
it.each(['', ' ', '\t'])(
'refuses explicit blank ambient fleet class %j before seeding',
(agentClass: string) => {
const fixture = createGreenfieldFixture();
const result = launchSync(fixture, { agentClass });
const output = outputOf(result);
expect(result.status, output).toBe(1);
expect(output).toContain('Refusing split identity authority');
expect(output).not.toContain('Running setup wizard');
expect(existsSync(fixture.capturePath)).toBe(false);
expect(existsSync(join(fixture.mosaicHome, 'SOUL.md'))).toBe(false);
expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
},
);
it.each([' unattended-seat', 'unattended-seat ', ''])(
'refuses non-exact ambient fleet name %j before seeding',
(agentName: string) => {
+10 -9
View File
@@ -31,7 +31,7 @@ import { readPersonaContractBlock } from '../fleet/persona-contract.js';
import { canonicalizeRoleClass } from './fleet-personas.js';
import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js';
import {
readOptionalInstalledIdentityContract,
readInstalledIdentityContractAtPointOfUse,
seedFleetIdentityDefaults,
} from './fleet-first-start-identity.js';
import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js';
@@ -236,7 +236,7 @@ function checkRuntime(cmd: string): void {
function assertAmbientFleetClassMatches(canonicalName: string, canonicalClass: string): void {
const configuredClass = process.env['MOSAIC_AGENT_CLASS'];
if (!configuredClass?.trim()) return;
if (configuredClass === undefined) return;
const ambientClass = canonicalizeRoleClass(configuredClass).canonicalClass;
if (ambientClass !== canonicalClass) {
@@ -583,20 +583,21 @@ For required push/merge/issue-close/release actions, execute without routine con
parts.push(readFileSync(join(mosaicHome, 'AGENTS.md'), 'utf-8'));
// USER.md (+ USER.local.md operator overlay, appended directly under the
// profile its base owns).
// profile its base owns). Fleet first start is Linux/systemd-owned and uses
// the no-follow reader at point of use. Standalone launches retain the
// portable tolerant path used on macOS and other supported hosts.
const fleetAgentName = process.env['MOSAIC_AGENT_NAME'];
const user =
readOptionalInstalledIdentityContract(
mosaicHome,
'USER.md',
process.env['MOSAIC_AGENT_NAME'] !== undefined,
)?.toString('utf8') ?? '';
fleetAgentName === undefined
? readOptional(join(mosaicHome, 'USER.md'))
: readInstalledIdentityContractAtPointOfUse(mosaicHome, 'USER.md').toString('utf8');
if (user) parts.push('\n\n# User Profile\n\n' + user);
const userLocal = readOptional(join(mosaicHome, 'USER.local.md'));
if (userLocal.trim()) {
parts.push('\n\n## Operator Overlay (USER.local.md)\n\n' + userLocal);
}
const fleetIdentity = resolveFleetIdentity(mosaicHome, process.env['MOSAIC_AGENT_NAME']);
const fleetIdentity = resolveFleetIdentity(mosaicHome, fleetAgentName);
if (!fleetIdentity.ok) {
throw new Error(`Fleet communications contract unavailable: ${fleetIdentity.error}`);
}