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
25 changes: 25 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,31 @@ export default class Serve extends Command {
}
// @objectstack/service-ai not installed — AI features unavailable
}

// 4b. Auto-register AI Studio (AI-driven metadata authoring / "online
// development") when the private @objectstack/service-ai-studio package
// is installed. It is NOT part of the open-source framework: the dynamic
// import below silently skips when absent, so open-source installs get
// the generic AI runtime only. Enterprise installs that ship the package
// get full AI authoring. AIStudioPlugin attaches via the `ai:ready` hook.
const hasAIStudio = plugins.some(
(p: any) => p.name === 'com.objectstack.service-ai-studio'
|| p.constructor?.name === 'AIStudioPlugin'
);
if (!hasAIStudio) {
try {
const studioPkg = '@objectstack/service-ai-studio';
const { AIStudioPlugin } = await import(/* webpackIgnore: true */ studioPkg);
await kernel.use(new AIStudioPlugin());
trackPlugin('AIStudio');
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
console.error('[AI Studio] AIStudioPlugin failed to start:', msg);
}
// @objectstack/service-ai-studio not installed — AI authoring unavailable
}
}
}

// 5. Capability resolver — auto-load service plugins declared in
Expand Down
17 changes: 15 additions & 2 deletions packages/runtime/src/cloud/artifact-kernel-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export interface ArtifactKernelFactoryConfig {
* `process.env.OS_AUTH_SECRET` / `AUTH_SECRET` at construction time.
*/
authBaseSecret?: string;
/**
* Capability tokens force-mounted on every per-environment kernel, merged
* (and de-duped by the loader) with the artifact's own `requires`. Lets a
* host make a capability ubiquitous across tenants — e.g. `['ai','aiStudio']`.
*/
defaultRequires?: string[];
}

/**
Expand All @@ -81,12 +87,14 @@ export class ArtifactKernelFactory implements EnvironmentKernelFactory {
private readonly logger: NonNullable<ArtifactKernelFactoryConfig['logger']>;
private readonly kernelConfig?: ArtifactKernelFactoryConfig['kernelConfig'];
private readonly authBaseSecret: string;
private readonly defaultRequires: string[];

constructor(config: ArtifactKernelFactoryConfig) {
this.client = config.client;
this.envRegistry = config.envRegistry;
this.logger = config.logger ?? console;
this.kernelConfig = config.kernelConfig;
this.defaultRequires = config.defaultRequires ?? [];
this.authBaseSecret = (
config.authBaseSecret
?? readEnvWithDeprecation('OS_AUTH_SECRET', ['AUTH_SECRET', 'BETTER_AUTH_SECRET'])
Expand Down Expand Up @@ -363,8 +371,13 @@ export class ArtifactKernelFactory implements EnvironmentKernelFactory {
(Array.isArray(bundle?.requires) ? bundle.requires : null) ??
(Array.isArray(sys?.requires) ? sys.requires : null) ??
[];
const requires: string[] = (requiresRaw as unknown[])
.filter((x): x is string => typeof x === 'string' && x.length > 0);
// Merge host-forced defaults (e.g. cloud's ['ai','aiStudio']) with the
// artifact's own requires. loadCapabilities de-dupes via a Set, so
// overlap is safe.
const requires: string[] = [
...(requiresRaw as unknown[]),
...this.defaultRequires,
].filter((x): x is string => typeof x === 'string' && x.length > 0);

if (requires.length > 0) {
const installed = await loadCapabilities({
Expand Down
10 changes: 10 additions & 0 deletions packages/runtime/src/cloud/capability-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export const CAPABILITY_PROVIDERS: Record<string, CapabilitySpec> = {
pkg: '@objectstack/service-ai',
export: 'AIServicePlugin',
},
// AI Studio — AI-driven metadata authoring ("online development"). This is
// a commercial capability that ships in the private @objectstack/service-ai-studio
// package (not part of the open-source framework). The dynamic import below
// silently skips when the package isn't installed, so the open-source build
// is unaffected; cloud and enterprise installs that ship the package light it
// up. Pair with `ai` in `requires` (it attaches via the `ai:ready` hook).
aiStudio: {
pkg: '@objectstack/service-ai-studio',
export: 'AIStudioPlugin',
},
analytics: {
pkg: '@objectstack/service-analytics',
export: 'AnalyticsServicePlugin',
Expand Down
10 changes: 10 additions & 0 deletions packages/runtime/src/cloud/objectos-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export interface ObjectOSStackConfig {
* (ADR §5.2 — "framework exposes seams; cloud supplies metadata + policy").
*/
extraPlugins?: Plugin[];
/**
* Capability tokens force-mounted on EVERY per-environment kernel, in
* addition to whatever the app artifact declares in `requires`. Merged and
* de-duped with `bundle.requires` before the capability loader runs. This
* is the host seam for a cloud operator to make a capability ubiquitous
* across all tenants without editing each app — e.g. `['ai','aiStudio']`
* so every cloud environment supports AI-driven online development.
*/
defaultRequires?: string[];
}

export interface ObjectOSStackResult {
Expand Down Expand Up @@ -209,6 +218,7 @@ class ObjectOSEnvironmentPlugin implements Plugin {
client: client as ArtifactApiClient,
envRegistry,
logger: ctx.logger,
defaultRequires: this.config.defaultRequires,
});

const kernelManager = new KernelManager({
Expand Down
11 changes: 11 additions & 0 deletions packages/runtime/src/cloud/runtime-config-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export interface RuntimeConfigPluginConfig {
controlPlaneUrl?: string;
/** Override the `features.installLocal` flag. Default: false. */
installLocal?: boolean;
/**
* Override the `features.aiStudio` flag — whether the SPA should surface
* AI-driven metadata authoring ("online development") affordances. Default:
* true (the actual authoring capability is still gated server-side by the
* presence of the `metadata_assistant` agent / @objectstack/service-ai-studio
* package; set false to force-hide the authoring UI for a tier/deployment).
*/
aiStudio?: boolean;
/**
* Report this runtime as a single-environment deployment (CLI
* `objectstack dev` / `os serve`). Defaults to `false` for
Expand All @@ -63,6 +71,7 @@ export class RuntimeConfigPlugin implements Plugin {

private readonly cloudUrl: string;
private readonly installLocal: boolean;
private readonly aiStudio: boolean;
private readonly singleEnvironment: boolean;
private readonly productName: string;
private readonly productShortName: string;
Expand All @@ -74,6 +83,7 @@ export class RuntimeConfigPlugin implements Plugin {
? ''
: (resolveCloudUrl(config.controlPlaneUrl) ?? '');
this.installLocal = !!config.installLocal;
this.aiStudio = config.aiStudio !== false; // default true (override-to-hide)
this.singleEnvironment = !!config.singleEnvironment;
const envName = (typeof process !== 'undefined' ? process.env?.OS_PRODUCT_NAME : undefined)?.trim();
const envShort = (typeof process !== 'undefined' ? process.env?.OS_PRODUCT_SHORT_NAME : undefined)?.trim();
Expand Down Expand Up @@ -113,6 +123,7 @@ export class RuntimeConfigPlugin implements Plugin {
const features = {
installLocal: this.installLocal,
marketplace: true,
aiStudio: this.aiStudio,
};
let envRegistry: any = null;
try { envRegistry = ctx.getService('env-registry'); } catch { /* not mounted (file/CLI mode) */ }
Expand Down
Loading