Skip to content

Commit beeabe2

Browse files
committed
feat: add env_id field for environment scoping in metadata and object schemas
1 parent ad2c0c8 commit beeabe2

7 files changed

Lines changed: 68 additions & 23 deletions

File tree

packages/metadata/src/projection/metadata-projector.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ export class MetadataProjector {
8080
}
8181

8282
try {
83-
// Check if projection already exists
83+
// Check if projection already exists (scoped by env_id for isolation)
84+
const envId = this.scope.environmentId ?? null;
8485
const existing = await this._findOne(targetTable, {
85-
where: { name },
86+
where: { name, env_id: envId },
8687
});
8788

8889
if (existing) {
@@ -112,9 +113,10 @@ export class MetadataProjector {
112113
}
113114

114115
try {
115-
// Find the projection
116+
// Find the projection (scoped by env_id for isolation)
117+
const envId = this.scope.environmentId ?? null;
116118
const existing = await this._findOne(targetTable, {
117-
where: { name },
119+
where: { name, env_id: envId },
118120
});
119121

120122
if (existing) {
@@ -153,6 +155,7 @@ export class MetadataProjector {
153155
private projectObject(name: string, data: any, now: string): Record<string, any> {
154156
return {
155157
name,
158+
env_id: this.scope.environmentId ?? null,
156159
label: data.label || name,
157160
plural_label: data.pluralLabel || data.label || name,
158161
description: data.description || '',
@@ -202,6 +205,7 @@ export class MetadataProjector {
202205
private projectView(name: string, data: any, now: string): Record<string, any> {
203206
return {
204207
name,
208+
env_id: this.scope.environmentId ?? null,
205209
label: data.label || name,
206210
description: data.description || '',
207211
object_name: data.object || '',
@@ -234,6 +238,7 @@ export class MetadataProjector {
234238
private projectAgent(name: string, data: any, now: string): Record<string, any> {
235239
return {
236240
name,
241+
env_id: this.scope.environmentId ?? null,
237242
label: data.label || name,
238243
description: data.description || '',
239244
agent_type: data.type || 'conversational',
@@ -269,6 +274,7 @@ export class MetadataProjector {
269274
private projectTool(name: string, data: any, now: string): Record<string, any> {
270275
return {
271276
name,
277+
env_id: this.scope.environmentId ?? null,
272278
label: data.label || name,
273279
description: data.description || '',
274280
// Parameters and implementation
@@ -293,6 +299,7 @@ export class MetadataProjector {
293299
private projectFlow(name: string, data: any, now: string): Record<string, any> {
294300
return {
295301
name,
302+
env_id: this.scope.environmentId ?? null,
296303
label: data.label || name,
297304
description: data.description || '',
298305
flow_type: data.type || 'autolaunched',

packages/objectos/src/objects/sys-agent.object.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const SysAgent = ObjectSchema.create({
2323
maxLength: 255,
2424
}),
2525

26+
env_id: Field.text({
27+
label: 'Environment ID',
28+
maxLength: 255,
29+
description: 'Project/environment scope — null = control-plane global',
30+
}),
31+
2632
label: Field.text({
2733
label: 'Display Label',
2834
required: true,
@@ -129,7 +135,8 @@ export const SysAgent = ObjectSchema.create({
129135
},
130136

131137
indexes: [
132-
{ fields: ['name'], unique: true },
138+
{ fields: ['name', 'env_id'], unique: true },
139+
{ fields: ['env_id'] },
133140
{ fields: ['agent_type'] },
134141
{ fields: ['namespace'] },
135142
{ fields: ['package_id'] },

packages/objectos/src/objects/sys-flow.object.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const SysFlow = ObjectSchema.create({
2323
maxLength: 255,
2424
}),
2525

26+
env_id: Field.text({
27+
label: 'Environment ID',
28+
maxLength: 255,
29+
description: 'Project/environment scope — null = control-plane global',
30+
}),
31+
2632
label: Field.text({
2733
label: 'Display Label',
2834
required: true,
@@ -113,7 +119,8 @@ export const SysFlow = ObjectSchema.create({
113119
},
114120

115121
indexes: [
116-
{ fields: ['name'], unique: true },
122+
{ fields: ['name', 'env_id'], unique: true },
123+
{ fields: ['env_id'] },
117124
{ fields: ['flow_type'] },
118125
{ fields: ['active'] },
119126
{ fields: ['namespace'] },

packages/objectos/src/objects/sys-object.object.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export const SysObject = ObjectSchema.create({
2525
description: 'Machine name (snake_case)',
2626
}),
2727

28+
env_id: Field.text({
29+
label: 'Environment ID',
30+
maxLength: 255,
31+
description: 'Project/environment scope — null = control-plane global',
32+
}),
33+
2834
label: Field.text({
2935
label: 'Display Label',
3036
required: true,
@@ -206,7 +212,8 @@ export const SysObject = ObjectSchema.create({
206212
},
207213

208214
indexes: [
209-
{ fields: ['name'], unique: true },
215+
{ fields: ['name', 'env_id'], unique: true },
216+
{ fields: ['env_id'] },
210217
{ fields: ['namespace'] },
211218
{ fields: ['package_id'] },
212219
{ fields: ['active'] },

packages/objectos/src/objects/sys-tool.object.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const SysTool = ObjectSchema.create({
2323
maxLength: 255,
2424
}),
2525

26+
env_id: Field.text({
27+
label: 'Environment ID',
28+
maxLength: 255,
29+
description: 'Project/environment scope — null = control-plane global',
30+
}),
31+
2632
label: Field.text({
2733
label: 'Display Label',
2834
required: true,
@@ -75,7 +81,8 @@ export const SysTool = ObjectSchema.create({
7581
},
7682

7783
indexes: [
78-
{ fields: ['name'], unique: true },
84+
{ fields: ['name', 'env_id'], unique: true },
85+
{ fields: ['env_id'] },
7986
{ fields: ['namespace'] },
8087
{ fields: ['package_id'] },
8188
],

packages/objectos/src/objects/sys-view.object.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const SysView = ObjectSchema.create({
2323
maxLength: 255,
2424
}),
2525

26+
env_id: Field.text({
27+
label: 'Environment ID',
28+
maxLength: 255,
29+
description: 'Project/environment scope — null = control-plane global',
30+
}),
31+
2632
label: Field.text({
2733
label: 'Display Label',
2834
required: true,
@@ -123,7 +129,8 @@ export const SysView = ObjectSchema.create({
123129
},
124130

125131
indexes: [
126-
{ fields: ['name'], unique: true },
132+
{ fields: ['name', 'env_id'], unique: true },
133+
{ fields: ['env_id'] },
127134
{ fields: ['object_name'] },
128135
{ fields: ['view_type'] },
129136
{ fields: ['namespace'] },

packages/objectql/src/protocol.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -267,16 +267,17 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
267267
const whereClause: Record<string, unknown> = {
268268
type: request.type,
269269
state: 'active',
270+
// Always filter by env_id: project kernels use their projectId,
271+
// control-plane kernels use NULL (global scope only).
272+
env_id: this.environmentId ?? null,
270273
};
271-
if (this.environmentId !== undefined) whereClause.env_id = this.environmentId;
272274
if (packageId) whereClause._packageId = packageId;
273275
let records = await this.engine.find('sys_metadata', { where: whereClause });
274276
if ((!records || records.length === 0)) {
275277
// Try alternate type name in DB using explicit mapping
276278
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
277279
if (alt) {
278-
const altWhere: Record<string, unknown> = { type: alt, state: 'active' };
279-
if (this.environmentId !== undefined) altWhere.env_id = this.environmentId;
280+
const altWhere: Record<string, unknown> = { type: alt, state: 'active', env_id: this.environmentId ?? null };
280281
if (packageId) altWhere._packageId = packageId;
281282
records = await this.engine.find('sys_metadata', { where: altWhere });
282283
}
@@ -374,9 +375,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
374375
name: request.name,
375376
state: 'active',
376377
};
377-
if (this.environmentId !== undefined) {
378-
scopedWhere.env_id = this.environmentId;
379-
}
378+
// Always filter by env_id: project kernels use their projectId,
379+
// control-plane kernels use NULL (global scope only).
380+
scopedWhere.env_id = this.environmentId ?? null;
380381
const record = await this.engine.findOne('sys_metadata', {
381382
where: scopedWhere,
382383
});
@@ -394,7 +395,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
394395
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
395396
if (alt) {
396397
const altWhere: Record<string, unknown> = { type: alt, name: request.name, state: 'active' };
397-
if (this.environmentId !== undefined) altWhere.env_id = this.environmentId;
398+
altWhere.env_id = this.environmentId ?? null;
398399
const altRecord = await this.engine.findOne('sys_metadata', { where: altWhere });
399400
if (altRecord) {
400401
item = typeof altRecord.metadata === 'string'
@@ -1178,13 +1179,15 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
11781179
let loaded = 0;
11791180
let errors = 0;
11801181
try {
1181-
// Mirror saveMetaItem's scoping: a per-project kernel only
1182-
// hydrates rows stamped with its env_id; a platform kernel
1183-
// (no environmentId) keeps the legacy global query.
1184-
const where: Record<string, unknown> = { state: 'active' };
1185-
if (this.environmentId !== undefined) {
1186-
where.env_id = this.environmentId;
1187-
}
1182+
// Always scope by env_id: project kernels hydrate only their own
1183+
// rows; control-plane kernels (no environmentId) hydrate only
1184+
// rows with env_id = NULL (global scope). Without this filter the
1185+
// control-plane registry would absorb every project's objects and
1186+
// expose them at the control-plane metadata endpoints.
1187+
const where: Record<string, unknown> = {
1188+
state: 'active',
1189+
env_id: this.environmentId ?? null,
1190+
};
11881191
const records = await this.engine.find('sys_metadata', { where });
11891192
for (const record of records) {
11901193
try {

0 commit comments

Comments
 (0)