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
2 changes: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"src/lib/inference/config.ts": 30,
"src/lib/inference/web-search.ts": 21,
"src/lib/messaging/channels/index.ts": 25,
"src/lib/onboard/gateway-binding.ts": 51,
"src/lib/onboard/gateway-binding.ts": 52,
"src/lib/runner.ts": 88,
"src/lib/security/redact.ts": 53,
"src/lib/state/onboard-session.ts": 36,
Expand Down
5 changes: 4 additions & 1 deletion docs/reference/system-readiness.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,10 @@ Explicit CPU-only intent skips the WSL GPU proof.
The gateway phase validates authority again immediately before managed reconciliation or external attachment.

Authoritative rebuild preflight pins read-only probes to the recorded gateway without selecting, starting, or recovering it.
Immediately before source deletion, the rebuild journal requires the complete gateway authority to match that preflight handoff and carries the journaled authority into replacement onboarding.
Immediately before source deletion, the rebuild journal requires the complete gateway authority to match that preflight handoff.
It accepts only one managed lifecycle change: a recorded package-managed service that now resolves as the standalone gateway.
It carries the journaled authority into replacement onboarding.
Refer to [Declare the OpenShell Gateway Lifecycle Authority](../deployment/gateway-lifecycle-authority) for the complete exception contract.
Gateway recovery remains after the canonical readiness gate.

For external supervision, onboarding skips managed gateway selection, reuse refresh, cleanup, start, stop, replace, and standalone fallback paths.
Expand Down
34 changes: 29 additions & 5 deletions src/lib/actions/sandbox/rebuild-recreate-journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,16 +269,17 @@ describe("rebuild replacement journal", () => {
expect(authority?.kind === "selected" && authority.value).toBe(journal.gatewayAuthority);
});

it("refuses an authority change after preflight before writing the journal (#7411)", () => {
it("refuses the standalone to package-managed authority change after preflight (#7411)", () => {
const onAuthorityRefusal = vi.fn();
mocks.resolveGatewayRebuildAuthority.mockReturnValue({
...STANDALONE_GATEWAY_AUTHORITY,
source: "packaged-service",
});

expect(() =>
openRebuildRecreateJournal({
target: NON_DEFAULT_TARGET,
expectedGatewayAuthority: {
...STANDALONE_GATEWAY_AUTHORITY,
source: "packaged-service",
},
expectedGatewayAuthority: STANDALONE_GATEWAY_AUTHORITY,
agentName: "langchain-deepagents-code",
targetIntentFingerprint: fingerprintRebuildRecreateTargetIntent(recreateOptions),
log: vi.fn(),
Expand All @@ -291,6 +292,29 @@ describe("rebuild replacement journal", () => {
expect(session.checkpoint?.sandboxRecreate ?? null).toBeNull();
});

it("adopts the package-managed to standalone migration and journals standalone (#9088)", () => {
const onAuthorityRefusal = vi.fn();

const journal = openRebuildRecreateJournal({
target: NON_DEFAULT_TARGET,
expectedGatewayAuthority: {
...STANDALONE_GATEWAY_AUTHORITY,
source: "packaged-service",
},
agentName: "langchain-deepagents-code",
targetIntentFingerprint: fingerprintRebuildRecreateTargetIntent(recreateOptions),
log: vi.fn(),
onAuthorityRefusal,
});

expect(onAuthorityRefusal).not.toHaveBeenCalled();
expect(journal.gatewayAuthority.source).toBe("standalone");
expect(session.checkpoint?.gatewayAuthority).toMatchObject({
kind: "selected",
value: { source: "standalone" },
});
});

it("starts at deleted when the source sandbox is already absent", () => {
mocks.captureOpenshell.mockReturnValue(absentProbe());

Expand Down
6 changes: 5 additions & 1 deletion src/lib/actions/sandbox/rebuild-recreate-journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { describeGatewayOwnerForError, sameGatewayOwner } from "../../onboard/ga
import {
GatewayAuthorityError,
gatewayAuthorityFailureLines,
isManagedPackagedServiceMigration,
resolveGatewayRebuildAuthority,
} from "../../onboard/gateway-teardown-authority";
import {
Expand Down Expand Up @@ -126,7 +127,10 @@ export function openRebuildRecreateJournal(
gatewayPort: target.gatewayPort,
});
const expectedAuthority = gatewayOwnerFromCheckpoint(input.expectedGatewayAuthority);
if (!sameGatewayOwner(expectedAuthority, authority)) {
if (
!sameGatewayOwner(expectedAuthority, authority) &&
!isManagedPackagedServiceMigration(expectedAuthority, authority)
) {
throw new GatewayAuthorityError(
"Gateway lifecycle authority changed after authoritative rebuild preflight " +
`(${describeGatewayOwnerForError(expectedAuthority)} -> ${describeGatewayOwnerForError(authority)}). ` +
Expand Down
2 changes: 1 addition & 1 deletion src/lib/onboard/gateway-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const BASE_GATEWAY_STATE_DIR_NAME = "openshell-docker-gateway";
/** Docker-driver gateway compatibility container name for the default port. */
export const BASE_GATEWAY_COMPAT_CONTAINER_NAME = "nemoclaw-openshell-gateway";

function isDefaultGatewayPort(port: number): boolean {
export function isDefaultGatewayPort(port: number): boolean {
return port === DEFAULT_GATEWAY_PORT;
}

Expand Down
28 changes: 28 additions & 0 deletions src/lib/onboard/gateway-host-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@ describe("gateway host runtime ownership", () => {
expect(() => runtime.getGatewayOwner()).toThrow(/authority changed during this run/);
});

it("resolves standalone on a non-default gateway port even when the packaged service exists (#9088)", () => {
const runtime = createGatewayHostRuntime(
createDeps({
gatewayName: () => "nemoclaw-9443",
gatewayPort: () => 9443,
hasOpenShellGatewayUserService: () => true,
}),
);

expect(runtime.getGatewayOwner()).toMatchObject({
gatewayName: "nemoclaw-9443",
gatewayPort: 9443,
mode: "nemoclaw-managed",
source: "standalone",
});
});

it("resolves the packaged service on the default gateway port (#9088)", () => {
const runtime = createGatewayHostRuntime(
createDeps({ hasOpenShellGatewayUserService: () => true }),
);

expect(runtime.getGatewayOwner()).toMatchObject({
gatewayPort: 8080,
source: "packaged-service",
});
});

it("adopts only a trusted standalone-to-packaged-service install transition (#7411)", () => {
let hasPackagedService = false;
const runtime = createGatewayHostRuntime(
Expand Down
5 changes: 4 additions & 1 deletion src/lib/onboard/gateway-host-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import path from "node:path";
import { isGatewayHealthy } from "../state/gateway";
import type { GatewayPortListenerRawScan } from "./docker-driver-gateway-port-listener";
import { hasOpenShellGatewayUserService } from "./docker-driver-gateway-service";
import { isDefaultGatewayPort } from "./gateway-binding";
import {
isDockerDriverGatewayHttpReady,
isGatewayHttpReady,
Expand Down Expand Up @@ -137,7 +138,9 @@ export function createGatewayHostRuntime(deps: GatewayHostRuntimeDeps): GatewayH
gatewayName,
gatewayPort,
declaration: loaded.declaration,
hasPackagedService: (deps.hasOpenShellGatewayUserService ?? hasOpenShellGatewayUserService)(),
hasPackagedService:
isDefaultGatewayPort(gatewayPort) &&
(deps.hasOpenShellGatewayUserService ?? hasOpenShellGatewayUserService)(),
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/onboard/gateway-teardown-authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type GatewayTeardownAuthorityResolver = (

type GatewayAuthorityEffect = "credential mutation" | "rebuild" | "teardown";

function isManagedPackagedServiceMigration(
export function isManagedPackagedServiceMigration(
recorded: GatewayOwner,
resolved: GatewayOwner,
): boolean {
Expand Down
Loading