Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/extension/src/amicode_service/fleet_staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ export function stageFleetDataPlane(opts: StageFleetDataPlaneOptions = {}): Flee
return { staged: true, receipt };
}

/**
* #1524: the honest staging receipt for the OBSERVATION-ONLY base peer-studio
* path. The base observation authority (index.ts `baseStudioActivates`) is
* entitlement-FREE — it mounts the read/observation routes for a verified
* serving peer without ever consulting the premium staging gate. So on the
* observation-only path we do NOT call `stageFleetDataPlane` (AC5: no premium
* plane stages even if an entitlement is resolvable); instead this returns a
* receipt that honestly reports `entitlement: "absent", staged: false` — the
* SAME semantics the #1478 base-activation surface reports, never a forged
* present/staged flag.
*/
export function observationOnlyStagingReceipt(now: () => string = () => new Date().toISOString()): FleetStagingReceipt {
return emptyReceipt("absent", false, now());
}

/** Convenience for logging/boot lines: a one-line staging summary. */
export function fleetStagingSummary(result: FleetStagingResult): string {
if (result.receipt.entitlement === "absent") return "fleet staging: entitlement absent — zero fleet surfaces";
Expand Down
45 changes: 38 additions & 7 deletions packages/extension/src/amicode_service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { buildMergedProjection, buildFleetProjection, type UpstreamMode, type Me
import { FleetPostureDetector, type FleetPostureTuning } from "./fleet_posture";
import { handleFleetWrite, type FleetWriteDeps } from "./fleet_writes";
import { inspectTunnelConfigFile, TUNNEL_GENERATION_HEADER } from "./fleet_tunnel";
import { stageFleetDataPlane, type FleetStagingReceipt } from "./fleet_staging";
import { stageFleetDataPlane, observationOnlyStagingReceipt, type FleetStagingReceipt } from "./fleet_staging";
import { resolveFleetProgram, type FleetProgramReceipt } from "./fleet_program";
import { createProject, listProjects } from "./project";
import { rehydratePeerRelationships, type RehydrationResult } from "./fleet_headless_rehydration";
Expand Down Expand Up @@ -670,6 +670,16 @@ export function createAmicodeService(
* engine). Suppresses the standalone→engine mode flip and switches the
* no-upstream 503 to the client's own honest hub-down state. */
client?: boolean;
/** #1524 (base peer-observation decoupled from hub-activation): mount the
* base peer-studio OBSERVATION routes (baseStudioActivates) WITHOUT the
* premium data plane. When true, createAmicodeService BYPASSES
* stageFleetDataPlane / the premium plane entirely (AC5 — no premium plane
* stages even if an entitlement is resolvable) and consults ONLY
* baseStudioActivates(opts.fleet) to decide whether to mount the routes.
* The wiring sets this for an UNARMED machine carrying a fleet-peer
* provider (no hub config). Undefined/false → today's exact staging path
* (byte-identical for entitled/armed machines, H3). */
observationOnly?: boolean;
/** The data-driven routing mode; default "fleet" (a staged plane with
* no getter runs fleet). */
getMode?: () => UpstreamMode;
Expand Down Expand Up @@ -755,12 +765,22 @@ export function createAmicodeService(
// entitlement → this block never arms anything → zero fleet surfaces,
// byte-identical.
if (opts.fleet !== undefined) {
const staging = stageFleetDataPlane({
entitlements: opts.fleet.entitlements,
entitlementConfigDir: opts.fleet.entitlementConfigDir,
overlaySource: opts.fleet.overlaySource,
});
if (staging.staged) {
// #1524: OBSERVATION-ONLY base peer-studio decouples base peer-observation
// route mounting from hub-activation/entitlement. When set, BYPASS the
// premium staging gate entirely — the base observation authority
// (baseStudioActivates) is entitlement-free (AC5: no premium plane stages
// even if an entitlement is resolvable) — and ride an honest absent/
// not-staged receipt (no forgery). Undefined/false → today's EXACT staging
// path, so an entitled/armed machine is byte-identical (H3).
const observationOnly = opts.fleet.observationOnly === true;
const staging = observationOnly
? { staged: false as const, receipt: observationOnlyStagingReceipt() }
: stageFleetDataPlane({
entitlements: opts.fleet.entitlements,
entitlementConfigDir: opts.fleet.entitlementConfigDir,
overlaySource: opts.fleet.overlaySource,
});
if (!observationOnly && staging.staged) {
fleetMultiplexerArmed = true;
// #1131: the staged fleet program (amicissimo#418) — resolved through
// the same entitlement gate inputs; its receipt rides the fleet status
Expand Down Expand Up @@ -984,6 +1004,17 @@ export function createAmicodeService(
// through the N-peer projection. AC3 (service/engine accept-set parity)
// is owned by #1485.
//
// #1524: this same branch is now the OBSERVATION-ONLY path (observationOnly
// true, hub-activation absent). The projection reads roster + peer tokens
// LATE per request, so a peer that changes state AFTER boot is reflected on
// the next request — PROVIDED the routes were mounted at boot.
// #1524 follow-up: the route-MOUNT decision (baseStudioActivates) is
// boot-time — a machine that boots with ZERO serving peers stays 404 until
// restart even if a peer comes online later (issue AC4). Moving the mount
// decision to per-request would need the routes always-mounted-but-
// conditionally-404, which would risk the AC2/H3 byte-identity guard; left
// as a deliberate follow-up rather than forced here.
//
// #1487 (AC1): HEADLESS PEER REHYDRATION — on base-activation, run
// rehydration to restore persisted peer relationships into named recovery
// states. The result is a read-only snapshot of the rehydration outcome,
Expand Down
43 changes: 34 additions & 9 deletions packages/extension/src/amicode_service_wiring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export interface AmicodeServiceWiringOptions {
* N-peer projection (index.ts:474). Never assigned when localMachineId is
* absent — the legacy 2-source projection stays byte-identical. */
fleetPeers?: FleetPeerProvider;
/** #1524: mount the base peer-studio OBSERVATION routes without the premium
* data plane, decoupled from hub-activation. Set by the assembly below for
* an UNARMED machine (no hub config) that carries a fleet-peer provider —
* createAmicodeService then consults ONLY baseStudioActivates to mount the
* read/observation surface (no FleetPlane, hub proxy, or multiplex). */
observationOnly?: boolean;
};
/** #398 (slice 4e): the fleet activation — config/env-driven (see
* fleet_activation.ts). A resolved snapshot OR a late-bound resolver
Expand Down Expand Up @@ -302,6 +308,23 @@ export async function startAmicodeService(
},
} : {}),
};
} else if (opts.localMachineId !== undefined && opts.localMachineId.trim() !== "") {
// #1524: OBSERVATION-ONLY base peer-studio. Activation is NOT armed (no
// hub config), yet this machine may hold a roster of serving peers +
// reader tokens (the live bug: /amicode/fleet/sessions 404'd on a VALID
// roster because opts.fleet was built ONLY inside the armed block above).
// Decouple base peer-observation route mounting from hub-activation: build
// a MINIMAL observation-only fleet — the fleet-peer provider (#1446), a
// NULL hub (no upstream), observationOnly:true — and pass it.
// createAmicodeService then BYPASSES the premium plane and consults ONLY
// baseStudioActivates: ≥1 serving peer → the observation routes mount;
// ZERO serving peers → nothing mounts → byte-identical (H3). NO FleetPlane
// / hub proxy / multiplex is attached on this path.
fleet = {
fleetPeers: buildFleetPeerProvider({ localMachineId: opts.localMachineId }),
hub: { getUrl: () => undefined },
observationOnly: true,
};
}
const service = createAmicodeService({
engine: opts.engine,
Expand Down Expand Up @@ -344,15 +367,17 @@ export async function startAmicodeService(
// input is a NAMED outcome (which reason), never a silent no-op.
const fleetInput = fleet ?? opts.fleet;
const fleetNote =
fleetInput !== undefined
? `; ${fleetStagingSummary(
stageFleetDataPlane({
entitlements: fleetInput.entitlements,
entitlementConfigDir: fleetInput.entitlementConfigDir,
overlaySource: fleetInput.overlaySource,
}),
)}`
: "";
fleetInput === undefined
? ""
: fleetInput.observationOnly === true
? "; fleet observation-only (base peer-studio; hub-activation absent — no premium plane)"
: `; ${fleetStagingSummary(
stageFleetDataPlane({
entitlements: fleetInput.entitlements,
entitlementConfigDir: fleetInput.entitlementConfigDir,
overlaySource: fleetInput.overlaySource,
}),
)}`;
log.appendLine(
`[amicode-service] listening on ${url.toString()} (${service.routeCount} routes; auth: ${authNote})${engineNote}${shelfNote}${activationNote}${transportNote}${fleetNote}`,
);
Expand Down
Loading
Loading