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
47 changes: 47 additions & 0 deletions packages/runtime/src/cloud/objectos-stack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Tests for the `extraPlugins` host seam on createObjectOSStack (ADR §5.2).
* A host (e.g. ObjectStack Cloud) supplies product/policy plugins via the
* official seam instead of mutating the returned plugins array by hand.
*/

import { describe, it, expect } from 'vitest';
import { createObjectOSStack } from './objectos-stack.js';
import type { Plugin } from '@objectstack/core';

function fakePlugin(name: string): Plugin {
return {
name,
version: '0.0.0',
async init() { /* no-op */ },
async start() { /* no-op */ },
} as Plugin;
}

describe('createObjectOSStack — extraPlugins seam', () => {
it('appends host extraPlugins after the framework defaults', async () => {
const a = fakePlugin('com.host.alpha');
const b = fakePlugin('com.host.beta');
const stack = await createObjectOSStack({
controlPlaneUrl: 'http://localhost:0',
extraPlugins: [a, b],
});
const names = stack.plugins.map((p: any) => p.name);
// Both host plugins are present...
expect(names).toContain('com.host.alpha');
expect(names).toContain('com.host.beta');
// ...and appended LAST, after the framework marketplace proxy.
const proxyIdx = names.indexOf('com.objectstack.runtime.marketplace-proxy');
expect(proxyIdx).toBeGreaterThanOrEqual(0);
expect(names.indexOf('com.host.alpha')).toBeGreaterThan(proxyIdx);
// ...preserving the given order.
expect(names.indexOf('com.host.beta')).toBeGreaterThan(names.indexOf('com.host.alpha'));
});

it('is a no-op when extraPlugins is omitted (default stack unchanged)', async () => {
const withSeam = await createObjectOSStack({ controlPlaneUrl: 'http://localhost:0', extraPlugins: [] });
const without = await createObjectOSStack({ controlPlaneUrl: 'http://localhost:0' });
expect(withSeam.plugins.length).toBe(without.plugins.length);
});
});
27 changes: 26 additions & 1 deletion packages/runtime/src/cloud/objectos-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ export interface ObjectOSStackConfig {
artifactCacheTtlMs?: number;
/** API prefix (carried for parity with cloud-stack). Default: /api/v1. */
apiPrefix?: string;
/**
* Host-supplied runtime plugins appended to the stack's default plugin
* list. This is the official seam for a host (e.g. the ObjectStack Cloud
* repo) to add **product/policy** plugins — marketplace install, cloud-
* account binding, set-initial-password — to the otherwise-neutral
* framework runtime, WITHOUT a framework release and without reaching into
* the returned array by hand.
*
* They are appended last, so they mount their routes after the framework
* plugins and can override/augment behaviour (e.g. supply a credentialled
* install path that the browse-only MarketplaceProxyPlugin deliberately
* does not). See docs/design/cloud-account-binding-marketplace-install.md
* (ADR §5.2 — "framework exposes seams; cloud supplies metadata + policy").
*/
extraPlugins?: Plugin[];
}

export interface ObjectOSStackResult {
Expand Down Expand Up @@ -252,7 +267,17 @@ export async function createObjectOSStack(config: ObjectOSStackConfig): Promise<
const enginePlugins = await createHostEnginePlugins();

return {
plugins: [...enginePlugins, new ObjectOSEnvironmentPlugin(merged), new AuthProxyPlugin(), new MarketplaceProxyPlugin({ controlPlaneUrl: merged.controlPlaneUrl === 'file' ? undefined : merged.controlPlaneUrl }), new RuntimeConfigPlugin({ controlPlaneUrl: merged.controlPlaneUrl === 'file' ? undefined : merged.controlPlaneUrl, installLocal: false })],
plugins: [
...enginePlugins,
new ObjectOSEnvironmentPlugin(merged),
new AuthProxyPlugin(),
new MarketplaceProxyPlugin({ controlPlaneUrl: merged.controlPlaneUrl === 'file' ? undefined : merged.controlPlaneUrl }),
new RuntimeConfigPlugin({ controlPlaneUrl: merged.controlPlaneUrl === 'file' ? undefined : merged.controlPlaneUrl, installLocal: false }),
// Host-supplied product/policy plugins (the official seam — see
// ObjectOSStackConfig.extraPlugins). Appended last so they mount
// after the framework defaults.
...(config.extraPlugins ?? []),
],
api: {
enableProjectScoping: true,
projectResolution: 'auto',
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineConfig({
'@objectstack/spec/kernel': path.resolve(__dirname, '../spec/src/kernel/index.ts'),
'@objectstack/spec/shared': path.resolve(__dirname, '../spec/src/shared/index.ts'),
'@objectstack/spec/system': path.resolve(__dirname, '../spec/src/system/index.ts'),
'@objectstack/spec/ui': path.resolve(__dirname, '../spec/src/ui/index.ts'),
'@objectstack/spec': path.resolve(__dirname, '../spec/src/index.ts'),
'@objectstack/types': path.resolve(__dirname, '../types/src/index.ts'),
},
Expand Down