import mongoose, { Document, Model, Schema, Types } from 'mongoose'; import { declaredRuntimeIdentity } from '../utils/runtimeIdentity'; export type RegistryType = 'commonly-official' | 'commonly-community' | 'private'; export type RegistryStatus = 'active' | 'deprecated' | 'unpublished' | 'pending-review'; export type ManifestRuntimeType = 'standalone' | 'commonly-hosted' | 'hybrid'; export type ManifestConnectionType = 'mcp' | 'rest' | 'websocket'; export type AgentInstallationStatus = 'active' | 'paused' | 'uninstalled' | 'error' | 'stale'; export interface IManifestCapability { name?: string; description?: string; } export interface IManifest { name: string; version: string; description?: string; author?: string; license?: string; homepage?: string; repository?: string; capabilities?: IManifestCapability[]; context?: { required?: string[]; optional?: string[] }; integrations?: { supported?: string[]; required?: string[] }; models?: { supported?: string[]; recommended?: string }; runtime?: { type?: ManifestRuntimeType; connection?: ManifestConnectionType; minMemory?: string; ports?: Map; }; configSchema?: unknown; hooks?: { postInstall?: string; preUpdate?: string; postUpdate?: string }; } export interface IAgentRegistry extends Document { agentName: string; displayName: string; description: string; readme?: string; manifest: IManifest; versions: Array<{ version?: string; manifest?: IManifest; publishedAt?: Date; deprecated?: boolean; deprecationReason?: string; }>; latestVersion?: string; registry: RegistryType; verified: boolean; publisher: { userId?: Types.ObjectId; organizationId?: Types.ObjectId; name?: string; }; categories: string[]; tags: string[]; stats: { installs: number; weeklyInstalls: number; rating: number; ratingCount: number; }; status: RegistryStatus; iconUrl?: string; ephemeral?: boolean; createdAt: Date; updatedAt: Date; } export interface IAgentRegistryModel extends Model { search(query: string, options?: { limit?: number; offset?: number; category?: string | null; registry?: string | null; verified?: boolean | null }): mongoose.Query; getByName(agentName: string): mongoose.Query; incrementInstalls(agentName: string): Promise; } const ManifestCapabilitySchema = new Schema({ name: String, description: String }, { _id: false }); const ManifestContextSchema = new Schema({ required: [String], optional: [String] }, { _id: false }); const ManifestRuntimeSchema = new Schema( { type: { type: String, enum: ['standalone', 'commonly-hosted', 'hybrid'], default: 'standalone' }, connection: { type: String, enum: ['mcp', 'rest', 'websocket'], default: 'mcp' }, // Driver identity — a different axis from `type`'s deployment shape. This is // the field `routes/registry/install.ts` reads to fill a runtimeType the // caller omitted, and it has to be DECLARED here for that read to ever work: // the subdocument is `strict: true`, so a manifest writing // `runtime.runtimeType` had the field silently dropped on every persist // (measured 2026-09-18 — `new AgentRegistry({manifest:{runtime:{type:'native', // runtimeType:'native'}}})` comes back as `{type:'native',connection:'mcp'}`), // which left native first-party installs with runtimeType null while a mocked // fixture kept the fallback's test green (TASK-043). runtimeType: { type: String }, minMemory: String, ports: { type: Map, of: Number }, }, { _id: false }, ); const ManifestSchema = new Schema( { name: { type: String, required: true }, version: { type: String, required: true }, description: String, author: String, license: String, homepage: String, repository: String, capabilities: [ManifestCapabilitySchema], context: ManifestContextSchema, integrations: { supported: [String], required: [String] }, models: { supported: [String], recommended: String }, runtime: ManifestRuntimeSchema, configSchema: Schema.Types.Mixed, hooks: { postInstall: String, preUpdate: String, postUpdate: String }, }, { _id: false }, ); const AgentRegistrySchema = new Schema( { agentName: { type: String, required: true, unique: true, lowercase: true, match: /^(@[a-z0-9-]+\/)?[a-z0-9-]+$/ }, displayName: { type: String, required: true }, description: { type: String, required: true }, readme: String, manifest: { type: ManifestSchema, required: true }, versions: [ { version: String, manifest: ManifestSchema, publishedAt: Date, deprecated: Boolean, deprecationReason: String, }, ], latestVersion: String, registry: { type: String, enum: ['commonly-official', 'commonly-community', 'private'], default: 'commonly-community' }, verified: { type: Boolean, default: false }, publisher: { userId: { type: Schema.Types.ObjectId, ref: 'User' }, organizationId: Schema.Types.ObjectId, name: String, }, categories: [String], tags: [String], stats: { installs: { type: Number, default: 0 }, weeklyInstalls: { type: Number, default: 0 }, rating: { type: Number, default: 0 }, ratingCount: { type: Number, default: 0 }, }, status: { type: String, enum: ['active', 'deprecated', 'unpublished', 'pending-review'], default: 'active' }, iconUrl: String, // ADR-006: self-serve webhook installs synthesize an ephemeral registry // row owned by the installing user. Excluded from the marketplace catalog // (search() filters by default), still resolvable by name for install/ // uninstall flows. GC of orphan ephemerals is deferred (ADR-006 OQ #1). ephemeral: { type: Boolean, default: false }, }, { timestamps: true }, ); AgentRegistrySchema.index({ categories: 1 }); AgentRegistrySchema.index({ tags: 1 }); AgentRegistrySchema.index({ 'stats.installs': -1 }); AgentRegistrySchema.index({ registry: 1, status: 1 }); AgentRegistrySchema.index({ 'publisher.userId': 1 }); AgentRegistrySchema.index({ agentName: 'text', displayName: 'text', description: 'text', tags: 'text' }); AgentRegistrySchema.statics.search = function ( query: string, options: { limit?: number; offset?: number; category?: string | null; registry?: string | null; verified?: boolean | null } = {}, ) { const { limit = 20, offset = 0, category = null, registry = null, verified = null } = options; // Self-serve / ephemeral rows are private to their installer — never appear // in marketplace browse. Direct getByName() lookup still resolves them. const filter: Record = { status: 'active', ephemeral: { $ne: true } }; if (query) filter.$text = { $search: query }; if (category) filter.categories = category; if (registry) filter.registry = registry; if (verified !== null) filter.verified = verified; return this.find(filter) .sort(query ? { score: { $meta: 'textScore' } } : { 'stats.installs': -1 }) .skip(offset) .limit(limit) .lean(); }; AgentRegistrySchema.statics.getByName = function (agentName: string) { return this.findOne({ agentName: agentName.toLowerCase() }); }; AgentRegistrySchema.statics.incrementInstalls = async function (agentName: string) { return this.updateOne( { agentName: agentName.toLowerCase() }, { $inc: { 'stats.installs': 1, 'stats.weeklyInstalls': 1 } }, ); }; // --- AgentInstallation (per-pod registry agent installations) --- export interface IAgentInstallationRegistry extends Document { agentName: string; podId: Types.ObjectId; instanceId: string; displayName?: string; version: string; config?: Map; scopes: string[]; status: AgentInstallationStatus; errorMessage?: string; staleSince?: Date; /** Moves only when this install's runtime identity is written or changes. */ runtimeIdentityAt?: Date; lastFoundWorkAt?: Date | null; installedBy: Types.ObjectId; skillLoad?: { at: Date; mounted: string[]; conflicted: string[]; status?: 'available' | 'unavailable' }; usage: { lastUsedAt?: Date; totalCalls: number; totalTokens: number; }; runtimeTokens: Array<{ tokenHash?: string; label?: string; createdAt: Date; lastUsedAt?: Date; }>; // Server-granted because GitHub issue writes use the instance credential. // Never map this to the client-editable `scopes` array. githubIssueWrite: boolean; createdAt: Date; updatedAt: Date; recordUsage(tokens?: number): Promise; } export interface IAgentInstallationRegistryModel extends Model { getInstalledAgents(podId: Types.ObjectId): mongoose.Query; isInstalled(agentName: string, podId: Types.ObjectId, instanceId?: string): Promise; install(agentName: string, podId: Types.ObjectId, options: { version: string; config?: Map; scopes?: string[]; githubIssueWrite?: boolean; installedBy: Types.ObjectId; instanceId?: string; displayName?: string }): Promise; upsert(agentName: string, podId: Types.ObjectId, options: { version: string; config?: Map; scopes?: string[]; githubIssueWrite?: boolean; installedBy: Types.ObjectId; instanceId?: string; displayName?: string }): Promise; uninstall(agentName: string, podId: Types.ObjectId, instanceId?: string): Promise; } const AgentInstallationSchema = new Schema( { agentName: { type: String, required: true, lowercase: true }, podId: { type: Schema.Types.ObjectId, ref: 'Pod', required: true }, instanceId: { type: String, default: 'default' }, displayName: { type: String }, version: { type: String, required: true }, config: { type: Map, of: Schema.Types.Mixed }, scopes: [String], status: { type: String, enum: ['active', 'paused', 'uninstalled', 'error', 'stale'], default: 'active' }, errorMessage: String, staleSince: { type: Date }, // The clock a seat's runtime chip is read by (TASK-569). updatedAt moves on // every write, including the skill-load report, so it cannot say which // install was attached last; this one moves only with the identity. runtimeIdentityAt: { type: Date }, // CAS throttle for the kernel's unclaimed-work nudge, per seat and pod. lastFoundWorkAt: { type: Date, default: null }, installedBy: { type: Schema.Types.ObjectId, ref: 'User', required: true }, skillLoad: { type: new Schema({ at: Date, mounted: [String], conflicted: [String], status: { type: String, enum: ['available', 'unavailable'] } }, { _id: false }), default: undefined, }, usage: { lastUsedAt: Date, totalCalls: { type: Number, default: 0 }, totalTokens: { type: Number, default: 0 }, }, runtimeTokens: [ { tokenHash: String, label: String, createdAt: { type: Date, default: Date.now }, lastUsedAt: Date, }, ], githubIssueWrite: { type: Boolean, default: false }, }, { timestamps: true }, ); const identitySignature = (config: unknown): string => JSON.stringify(declaredRuntimeIdentity(config)); AgentInstallationSchema.post('init', function (this: any) { this.$locals.runtimeIdentitySignature = identitySignature(this.config); }); AgentInstallationSchema.pre('save', function (this: any) { const signature = identitySignature(this.config); const changed = this.isNew ? signature !== 'null' : this.isModified('config') && signature !== 'null' && signature !== this.$locals.runtimeIdentitySignature; // A reactivated install is a re-attach even when its identity is unchanged. const reattached = !this.isNew && this.isModified('status') && this.status === 'active' && signature !== 'null'; if (changed || reattached) this.runtimeIdentityAt = new Date(); this.$locals.runtimeIdentitySignature = signature; }); AgentInstallationSchema.index({ agentName: 1, podId: 1, instanceId: 1 }, { unique: true }); AgentInstallationSchema.index({ podId: 1, status: 1 }); // First-run connection status polls active installs by their human owner. // Keep that ownership lookup indexed so the 3s UI poll never scans the full // installation collection as the marketplace grows. AgentInstallationSchema.index({ installedBy: 1, status: 1 }); // The grant-broker seam (TASK-175) judges a calling seat by reading every ACTIVE // installation and matching the seat's identity key in JS. It has to do that in // JS: `seatEnvironmentKey` normalises BOTH identity parts, because the schema // lowercases `agentName` but NOT `instanceId` (services/seatEnvironmentProjection.ts), // so a query that filtered on identity would be a NARROWER read than the key // match — i.e. fail-open on a refusal. What is left is an identity-free read of // the whole active set, on the broker hot path (`callTool` and `tools/list`), // selecting only these four fields — so give it a COVERED index rather than a // collection scan. Measured on the live shape in a real mongod (502 rows, 288 // active, an opaque `config` map on each): without it COLLSCAN // `totalDocsExamined=502`; with it PROJECTION_COVERED>IXSCAN // `totalDocsExamined=0, totalKeysExamined=288`. `_id` is a key here because an // inclusion projection still returns it, and without `_id` the same index is // chosen but gains a FETCH stage (288 documents pulled to read a field the // index already holds). AgentInstallationSchema.index({ status: 1, agentName: 1, instanceId: 1, installedBy: 1, _id: 1 }); AgentInstallationSchema.statics.getInstalledAgents = function (podId: Types.ObjectId) { return this.find({ podId, status: 'active' }).lean(); }; AgentInstallationSchema.statics.isInstalled = async function (agentName: string, podId: Types.ObjectId, instanceId = 'default') { const installation = await this.findOne({ agentName: agentName.toLowerCase(), podId, instanceId, status: 'active' }); return !!installation; }; AgentInstallationSchema.statics.install = async function (agentName: string, podId: Types.ObjectId, options: { version: string; config?: Map; scopes?: string[]; githubIssueWrite?: boolean; installedBy: Types.ObjectId; instanceId?: string; displayName?: string; }) { const { version, config, scopes, githubIssueWrite, installedBy, instanceId = 'default', displayName, } = options; const existing = await this.findOne({ agentName: agentName.toLowerCase(), podId, instanceId }); if (existing) { if (existing.status === 'active') throw new Error('Agent already installed'); existing.status = 'active'; existing.version = version; existing.config = config; existing.scopes = scopes; if (githubIssueWrite !== undefined) existing.githubIssueWrite = githubIssueWrite === true; return existing.save(); } return this.create({ agentName: agentName.toLowerCase(), podId, instanceId, displayName, version, config, scopes, installedBy, githubIssueWrite: githubIssueWrite === true, }); }; /** * Idempotent variant of `install`. Re-fires (e.g. mention-driven autoJoin * on a row that already exists) MUST NOT throw or create duplicates. The * existing `install` throws when an active row exists; that's the right * behavior for an admin-driven first install but wrong for a runtime path * that may fire many times. Use this whenever the call site doesn't want * to care if it's the first attempt. * * Atomically created via findOneAndUpdate with upsert+setOnInsert so * concurrent autoJoin attempts can't race a duplicate row past the * unique index on (agentName, podId, instanceId). */ AgentInstallationSchema.statics.upsert = async function (agentName: string, podId: Types.ObjectId, options: { version: string; config?: Map; scopes?: string[]; githubIssueWrite?: boolean; installedBy: Types.ObjectId; instanceId?: string; displayName?: string; }) { const { version, config, scopes, githubIssueWrite, installedBy, instanceId = 'default', displayName, } = options; const filter = { agentName: agentName.toLowerCase(), podId, instanceId }; const update = { $setOnInsert: { agentName: agentName.toLowerCase(), podId, instanceId, installedBy, version, displayName }, $set: { // An upsert that declares an identity is an attach, onto a new row or an // existing one, so it moves the clock either way. findOneAndUpdate skips // the pre('save') stamp, so it is set here. ...(declaredRuntimeIdentity(config) ? { runtimeIdentityAt: new Date() } : {}), ...(config ? { config } : {}), ...(scopes ? { scopes } : {}), ...(githubIssueWrite !== undefined ? { githubIssueWrite: githubIssueWrite === true } : {}), status: 'active', }, }; return this.findOneAndUpdate(filter, update, { new: true, upsert: true, setDefaultsOnInsert: true }); }; AgentInstallationSchema.statics.uninstall = async function (agentName: string, podId: Types.ObjectId, instanceId = 'default') { return this.updateOne({ agentName: agentName.toLowerCase(), podId, instanceId }, { status: 'uninstalled' }); }; AgentInstallationSchema.methods.recordUsage = async function (tokens = 0) { this.usage.lastUsedAt = new Date(); this.usage.totalCalls += 1; this.usage.totalTokens += tokens; return this.save(); }; export const AgentRegistry = mongoose.model('AgentRegistry', AgentRegistrySchema); export const AgentInstallation = mongoose.model('AgentInstallation', AgentInstallationSchema);