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
16 changes: 14 additions & 2 deletions packages/ocap-kernel/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export class Kernel {
this.#kernelServiceManager.registerKernelServiceObject(
'kernelFacet',
kernelFacet,
{ systemOnly: true },
);
return kernelFacet;
}
Expand Down Expand Up @@ -367,10 +368,21 @@ export class Kernel {
*
* @param name - The name of the service.
* @param object - The service object to register.
* @param options - Registration options.
* @param options.systemOnly - Whether the service is only available to system
* subclusters. Defaults to `false`.
* @returns The registration details including the kref.
*/
registerKernelServiceObject(name: string, object: object): KernelService {
return this.#kernelServiceManager.registerKernelServiceObject(name, object);
registerKernelServiceObject(
name: string,
object: object,
options?: { systemOnly?: boolean },
): KernelService {
return this.#kernelServiceManager.registerKernelServiceObject(
name,
object,
options,
);
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/ocap-kernel/src/KernelServiceManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ describe('KernelServiceManager', () => {
expect(registered.name).toBe('testService');
expect(registered.kref).toMatch(/^ko\d+$/u);
expect(registered.service).toBe(testService);
expect(registered.systemOnly).toBe(false);
});

it('defaults systemOnly to false when no options provided', () => {
const testService = { testMethod: () => 'test' };

const registered = serviceManager.registerKernelServiceObject(
'testService',
testService,
);

expect(registered.systemOnly).toBe(false);
});

it('sets systemOnly to true when specified', () => {
const testService = { testMethod: () => 'test' };

const registered = serviceManager.registerKernelServiceObject(
'testService',
testService,
{ systemOnly: true },
);

expect(registered.systemOnly).toBe(true);
});

it('pins the service object in kernel store', () => {
Expand Down Expand Up @@ -123,6 +147,18 @@ describe('KernelServiceManager', () => {
expect(retrieved).toStrictEqual(registered);
});

it('returns the systemOnly flag on retrieved service', () => {
const testService = { testMethod: () => 'test' };

serviceManager.registerKernelServiceObject('sysOnly', testService, {
systemOnly: true,
});
serviceManager.registerKernelServiceObject('open', testService);

expect(serviceManager.getKernelService('sysOnly')?.systemOnly).toBe(true);
expect(serviceManager.getKernelService('open')?.systemOnly).toBe(false);
});

it('returns undefined for non-existent service', () => {
const retrieved = serviceManager.getKernelService('nonExistent');
expect(retrieved).toBeUndefined();
Expand Down
12 changes: 10 additions & 2 deletions packages/ocap-kernel/src/KernelServiceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type KernelService = {
name: string;
kref: string;
service: object;
systemOnly: boolean;
};

type KernelServiceManagerConstructorProps = {
Expand Down Expand Up @@ -60,9 +61,16 @@ export class KernelServiceManager {
*
* @param name - The name of the service.
* @param service - The service object.
* @param options - Registration options.
* @param options.systemOnly - Whether the service is only available to system
* subclusters. Defaults to `false`.
* @returns The registered kernel service with its kref.
*/
registerKernelServiceObject(name: string, service: object): KernelService {
registerKernelServiceObject(
name: string,
service: object,
{ systemOnly = false }: { systemOnly?: boolean } = {},
): KernelService {
if (this.#kernelServicesByName.has(name)) {
throw new Error(`Kernel service "${name}" is already registered`);
}
Expand All @@ -73,7 +81,7 @@ export class KernelServiceManager {
this.#kernelStore.kv.set(serviceKey, kref);
this.#kernelStore.pinObject(kref);
}
const kernelService = { name, kref, service };
const kernelService = { name, kref, service, systemOnly };
this.#kernelServicesByName.set(name, kernelService);
this.#kernelServicesByObject.set(kref, kernelService);
return kernelService;
Expand Down
49 changes: 46 additions & 3 deletions packages/ocap-kernel/src/vats/SubclusterManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ describe('SubclusterManager', () => {
let mockKernelStore: Mocked<KernelStore>;
let mockKernelQueue: Mocked<KernelQueue>;
let mockVatManager: Mocked<VatManager>;
let mockGetKernelService: (name: string) => { kref: string } | undefined;
let mockGetKernelService: (
name: string,
) => { kref: string; systemOnly: boolean } | undefined;
let mockQueueMessage: (
target: KRef,
method: string,
Expand Down Expand Up @@ -77,7 +79,7 @@ describe('SubclusterManager', () => {

mockGetKernelService = vi.fn().mockReturnValue(undefined) as unknown as (
name: string,
) => { kref: string } | undefined;
) => { kref: string; systemOnly: boolean } | undefined;
mockQueueMessage = vi
.fn()
.mockResolvedValue({ body: '{"result":"ok"}', slots: [] }) as unknown as (
Expand Down Expand Up @@ -151,7 +153,7 @@ describe('SubclusterManager', () => {
);
});

it('includes kernel services when specified', async () => {
it('includes unrestricted kernel services when specified', async () => {
const config: ClusterConfig = {
bootstrap: 'testVat',
vats: {
Expand All @@ -161,6 +163,7 @@ describe('SubclusterManager', () => {
};
(mockGetKernelService as ReturnType<typeof vi.fn>).mockReturnValue({
kref: 'ko-service',
systemOnly: false,
});

await subclusterManager.launchSubcluster(config);
Expand All @@ -172,6 +175,46 @@ describe('SubclusterManager', () => {
]);
});

it('throws when user subcluster requests a restricted service', async () => {
const config: ClusterConfig = {
bootstrap: 'testVat',
vats: {
testVat: { sourceSpec: 'test.js' },
},
services: ['kernelFacet'],
};
(mockGetKernelService as ReturnType<typeof vi.fn>).mockReturnValue({
kref: 'ko-service',
systemOnly: true,
});

await expect(subclusterManager.launchSubcluster(config)).rejects.toThrow(
"kernel service 'kernelFacet' is restricted to system subclusters",
);
});

it('allows system subcluster to access restricted services', async () => {
const config: ClusterConfig = {
bootstrap: 'testVat',
vats: {
testVat: { sourceSpec: 'test.js' },
},
services: ['kernelFacet'],
};
(mockGetKernelService as ReturnType<typeof vi.fn>).mockReturnValue({
kref: 'ko-service',
systemOnly: true,
});

await subclusterManager.launchSubcluster(config, { isSystem: true });

expect(mockGetKernelService).toHaveBeenCalledWith('kernelFacet');
expect(mockQueueMessage).toHaveBeenCalledWith('ko1', 'bootstrap', [
expect.anything(),
{ kernelFacet: expect.anything() },
]);
});

it('throws for invalid cluster config', async () => {
const invalidConfig = {} as ClusterConfig;

Expand Down
40 changes: 37 additions & 3 deletions packages/ocap-kernel/src/vats/SubclusterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ type SubclusterManagerOptions = {
kernelStore: KernelStore;
kernelQueue: KernelQueue;
vatManager: VatManager;
getKernelService: (name: string) => { kref: string } | undefined;
getKernelService: (
name: string,
) => { kref: string; systemOnly: boolean } | undefined;
queueMessage: (
target: KRef,
method: string,
Expand All @@ -45,7 +47,9 @@ export class SubclusterManager {
readonly #vatManager: VatManager;

/** Function to get kernel services */
readonly #getKernelService: (name: string) => { kref: string } | undefined;
readonly #getKernelService: (
name: string,
) => { kref: string; systemOnly: boolean } | undefined;

/** Function to queue messages */
readonly #queueMessage: (
Expand Down Expand Up @@ -92,17 +96,22 @@ export class SubclusterManager {
* Launches a sub-cluster of vats.
*
* @param config - Configuration object for sub-cluster.
* @param options - Launch options.
* @param options.isSystem - Whether this is a system subcluster. System
* subclusters may access restricted kernel services. Defaults to `false`.
* @returns A promise for the subcluster ID, bootstrap root kref, and
* bootstrap result.
*/
async launchSubcluster(
config: ClusterConfig,
{ isSystem = false }: { isSystem?: boolean } = {},
): Promise<SubclusterLaunchResult> {
await this.#kernelQueue.waitForCrank();
isClusterConfig(config) || Fail`invalid cluster config`;
if (!config.vats[config.bootstrap]) {
Fail`invalid bootstrap vat name ${config.bootstrap}`;
}
this.#validateServices(config, isSystem);
const subclusterId = this.#kernelStore.addSubcluster(config);
const { rootKref, bootstrapResult } = await this.#launchVatsForSubcluster(
subclusterId,
Expand Down Expand Up @@ -204,6 +213,31 @@ export class SubclusterManager {
this.#kernelStore.deleteSubcluster(subclusterId);
}

/**
* Validates that all requested services exist and are accessible.
*
* @param config - The cluster configuration to validate.
* @param isSystem - Whether this is a system subcluster.
* @throws If a requested service does not exist or is system-only and the
* subcluster is not a system subcluster.
*/
#validateServices(config: ClusterConfig, isSystem: boolean): void {
if (!config.services) {
return;
}
for (const name of config.services) {
const service = this.#getKernelService(name);
if (!service) {
throw Error(`no registered kernel service '${name}'`);
}
if (service.systemOnly && !isSystem) {
throw Error(
`kernel service '${name}' is restricted to system subclusters`,
);
}
Comment on lines +233 to +237
Copy link
Contributor

Choose a reason for hiding this comment

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

I think in this case I'd throw the "no registered kernel service" error, rather than leaking information about system internals and that there's a thing there that's none of your business.

}
}

/**
* Launches all vats for a subcluster and sets up their bootstrap connections.
*
Expand Down Expand Up @@ -295,7 +329,7 @@ export class SubclusterManager {
}

for (const { name, config } of newConfigs) {
const result = await this.launchSubcluster(config);
const result = await this.launchSubcluster(config, { isSystem: true });
this.#systemSubclusterRoots.set(name, result.rootKref);

// Persist the mapping
Expand Down
Loading