Skip to content

Commit 05dacf2

Browse files
feat(service-ai): extract AI metadata authoring into cloud-only AI Studio (#1571)
1 parent ac1fc4c commit 05dacf2

39 files changed

Lines changed: 112 additions & 4867 deletions

packages/cli/src/commands/serve.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,31 @@ export default class Serve extends Command {
13651365
}
13661366
// @objectstack/service-ai not installed — AI features unavailable
13671367
}
1368+
1369+
// 4b. Auto-register AI Studio (AI-driven metadata authoring / "online
1370+
// development") when the private @objectstack/service-ai-studio package
1371+
// is installed. It is NOT part of the open-source framework: the dynamic
1372+
// import below silently skips when absent, so open-source installs get
1373+
// the generic AI runtime only. Enterprise installs that ship the package
1374+
// get full AI authoring. AIStudioPlugin attaches via the `ai:ready` hook.
1375+
const hasAIStudio = plugins.some(
1376+
(p: any) => p.name === 'com.objectstack.service-ai-studio'
1377+
|| p.constructor?.name === 'AIStudioPlugin'
1378+
);
1379+
if (!hasAIStudio) {
1380+
try {
1381+
const studioPkg = '@objectstack/service-ai-studio';
1382+
const { AIStudioPlugin } = await import(/* webpackIgnore: true */ studioPkg);
1383+
await kernel.use(new AIStudioPlugin());
1384+
trackPlugin('AIStudio');
1385+
} catch (err: unknown) {
1386+
const msg = err instanceof Error ? err.message : String(err);
1387+
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
1388+
console.error('[AI Studio] AIStudioPlugin failed to start:', msg);
1389+
}
1390+
// @objectstack/service-ai-studio not installed — AI authoring unavailable
1391+
}
1392+
}
13681393
}
13691394

13701395
// 5. Capability resolver — auto-load service plugins declared in

packages/runtime/src/cloud/artifact-kernel-factory.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface ArtifactKernelFactoryConfig {
6161
* `process.env.OS_AUTH_SECRET` / `AUTH_SECRET` at construction time.
6262
*/
6363
authBaseSecret?: string;
64+
/**
65+
* Capability tokens force-mounted on every per-environment kernel, merged
66+
* (and de-duped by the loader) with the artifact's own `requires`. Lets a
67+
* host make a capability ubiquitous across tenants — e.g. `['ai','aiStudio']`.
68+
*/
69+
defaultRequires?: string[];
6470
}
6571

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

8592
constructor(config: ArtifactKernelFactoryConfig) {
8693
this.client = config.client;
8794
this.envRegistry = config.envRegistry;
8895
this.logger = config.logger ?? console;
8996
this.kernelConfig = config.kernelConfig;
97+
this.defaultRequires = config.defaultRequires ?? [];
9098
this.authBaseSecret = (
9199
config.authBaseSecret
92100
?? readEnvWithDeprecation('OS_AUTH_SECRET', ['AUTH_SECRET', 'BETTER_AUTH_SECRET'])
@@ -363,8 +371,13 @@ export class ArtifactKernelFactory implements EnvironmentKernelFactory {
363371
(Array.isArray(bundle?.requires) ? bundle.requires : null) ??
364372
(Array.isArray(sys?.requires) ? sys.requires : null) ??
365373
[];
366-
const requires: string[] = (requiresRaw as unknown[])
367-
.filter((x): x is string => typeof x === 'string' && x.length > 0);
374+
// Merge host-forced defaults (e.g. cloud's ['ai','aiStudio']) with the
375+
// artifact's own requires. loadCapabilities de-dupes via a Set, so
376+
// overlap is safe.
377+
const requires: string[] = [
378+
...(requiresRaw as unknown[]),
379+
...this.defaultRequires,
380+
].filter((x): x is string => typeof x === 'string' && x.length > 0);
368381

369382
if (requires.length > 0) {
370383
const installed = await loadCapabilities({

packages/runtime/src/cloud/capability-loader.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export const CAPABILITY_PROVIDERS: Record<string, CapabilitySpec> = {
5656
pkg: '@objectstack/service-ai',
5757
export: 'AIServicePlugin',
5858
},
59+
// AI Studio — AI-driven metadata authoring ("online development"). This is
60+
// a commercial capability that ships in the private @objectstack/service-ai-studio
61+
// package (not part of the open-source framework). The dynamic import below
62+
// silently skips when the package isn't installed, so the open-source build
63+
// is unaffected; cloud and enterprise installs that ship the package light it
64+
// up. Pair with `ai` in `requires` (it attaches via the `ai:ready` hook).
65+
aiStudio: {
66+
pkg: '@objectstack/service-ai-studio',
67+
export: 'AIStudioPlugin',
68+
},
5969
analytics: {
6070
pkg: '@objectstack/service-analytics',
6171
export: 'AnalyticsServicePlugin',

packages/runtime/src/cloud/objectos-stack.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ export interface ObjectOSStackConfig {
7979
* (ADR §5.2 — "framework exposes seams; cloud supplies metadata + policy").
8080
*/
8181
extraPlugins?: Plugin[];
82+
/**
83+
* Capability tokens force-mounted on EVERY per-environment kernel, in
84+
* addition to whatever the app artifact declares in `requires`. Merged and
85+
* de-duped with `bundle.requires` before the capability loader runs. This
86+
* is the host seam for a cloud operator to make a capability ubiquitous
87+
* across all tenants without editing each app — e.g. `['ai','aiStudio']`
88+
* so every cloud environment supports AI-driven online development.
89+
*/
90+
defaultRequires?: string[];
8291
}
8392

8493
export interface ObjectOSStackResult {
@@ -209,6 +218,7 @@ class ObjectOSEnvironmentPlugin implements Plugin {
209218
client: client as ArtifactApiClient,
210219
envRegistry,
211220
logger: ctx.logger,
221+
defaultRequires: this.config.defaultRequires,
212222
});
213223

214224
const kernelManager = new KernelManager({

packages/runtime/src/cloud/runtime-config-plugin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export interface RuntimeConfigPluginConfig {
4040
controlPlaneUrl?: string;
4141
/** Override the `features.installLocal` flag. Default: false. */
4242
installLocal?: boolean;
43+
/**
44+
* Override the `features.aiStudio` flag — whether the SPA should surface
45+
* AI-driven metadata authoring ("online development") affordances. Default:
46+
* true (the actual authoring capability is still gated server-side by the
47+
* presence of the `metadata_assistant` agent / @objectstack/service-ai-studio
48+
* package; set false to force-hide the authoring UI for a tier/deployment).
49+
*/
50+
aiStudio?: boolean;
4351
/**
4452
* Report this runtime as a single-environment deployment (CLI
4553
* `objectstack dev` / `os serve`). Defaults to `false` for
@@ -63,6 +71,7 @@ export class RuntimeConfigPlugin implements Plugin {
6371

6472
private readonly cloudUrl: string;
6573
private readonly installLocal: boolean;
74+
private readonly aiStudio: boolean;
6675
private readonly singleEnvironment: boolean;
6776
private readonly productName: string;
6877
private readonly productShortName: string;
@@ -74,6 +83,7 @@ export class RuntimeConfigPlugin implements Plugin {
7483
? ''
7584
: (resolveCloudUrl(config.controlPlaneUrl) ?? '');
7685
this.installLocal = !!config.installLocal;
86+
this.aiStudio = config.aiStudio !== false; // default true (override-to-hide)
7787
this.singleEnvironment = !!config.singleEnvironment;
7888
const envName = (typeof process !== 'undefined' ? process.env?.OS_PRODUCT_NAME : undefined)?.trim();
7989
const envShort = (typeof process !== 'undefined' ? process.env?.OS_PRODUCT_SHORT_NAME : undefined)?.trim();
@@ -113,6 +123,7 @@ export class RuntimeConfigPlugin implements Plugin {
113123
const features = {
114124
installLocal: this.installLocal,
115125
marketplace: true,
126+
aiStudio: this.aiStudio,
116127
};
117128
let envRegistry: any = null;
118129
try { envRegistry = ctx.getService('env-registry'); } catch { /* not mounted (file/CLI mode) */ }

0 commit comments

Comments
 (0)