@opengeni/core 2.6.4 → 2.7.2-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ import { type ConfiguredModel, type Settings } from "@opengeni/config";
2
+ import { type ModelAvailabilityV1, type ModelCredentialReadinessV1, type WorkspaceModelPolicyContract } from "@opengeni/contracts";
3
+ import { type Database } from "@opengeni/db";
4
+ export type ResolvedCatalogSettings = {
5
+ settings: Settings;
6
+ source: "code" | "database";
7
+ version: number | null;
8
+ modelNotes: Record<string, string>;
9
+ };
10
+ /**
11
+ * Curated workspace Gateway products and workspace-owned custom slugs share
12
+ * one public prefix. Only the latter have a mutable catalog row whose active
13
+ * generation must be rechecked at a fresh acceptance commit boundary.
14
+ */
15
+ export declare function isWorkspaceGatewayCustomModelId(settings: Settings, modelId: string): boolean;
16
+ export declare function isWorkspaceOpenRouterCustomModelId(settings: Settings, modelId: string): boolean;
17
+ export type WorkspaceCustomModelReference = {
18
+ scope: "workspace" | "organization";
19
+ providerKind: "vercel_gateway" | "openrouter";
20
+ upstreamModelId: string;
21
+ };
22
+ export declare function workspaceCustomModelReference(settings: Settings, modelId: string): WorkspaceCustomModelReference | null;
23
+ export declare function isWorkspaceCustomModelId(settings: Settings, modelId: string): boolean;
24
+ export declare function lockActiveCustomModelForAdmission(db: Database, input: {
25
+ accountId: string;
26
+ workspaceId: string;
27
+ reference: WorkspaceCustomModelReference;
28
+ }): Promise<boolean>;
29
+ /**
30
+ * Resolve the deployment catalog without making synchronous env settings read
31
+ * Postgres. Database mode fails closed when the singleton is absent or invalid;
32
+ * code mode preserves the already-validated env catalog.
33
+ */
34
+ export declare function resolveCatalogSettings(db: Database, envSettings: Settings): Promise<ResolvedCatalogSettings>;
35
+ /**
36
+ * Resolve the deployment catalog and add only the custom Gateway slugs owned by
37
+ * one workspace. Use this at model-bearing workspace boundaries; public config
38
+ * and deployment-operator surfaces must continue to use `resolveCatalogSettings`.
39
+ */
40
+ export declare function resolveWorkspaceCatalogSettings(db: Database, envSettings: Settings, input: {
41
+ accountId: string;
42
+ workspaceId: string;
43
+ retainedProductModelId?: string | null;
44
+ retainedProductModelIds?: readonly (string | null | undefined)[];
45
+ }): Promise<ResolvedCatalogSettings>;
46
+ export type ModelAvailabilityObservation = {
47
+ status: "available" | "degraded" | "unavailable";
48
+ reason: "not_entitled" | "provider_unhealthy" | null;
49
+ checkedAt: string;
50
+ };
51
+ export type ModelCredentialReadinessObservation = {
52
+ status: "ready";
53
+ checkedAt: string;
54
+ } | {
55
+ status: "not_ready";
56
+ reason: "prerequisites_missing" | "needs_reauth";
57
+ checkedAt: string;
58
+ } | {
59
+ status: "error";
60
+ reason: "resolver_error";
61
+ checkedAt: string;
62
+ };
63
+ export declare const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS: number;
64
+ export type WorkspaceModelSelectionInput = {
65
+ settings: Settings;
66
+ policy: WorkspaceModelPolicyContract | null;
67
+ codexSubscriptionActive: boolean;
68
+ xaiSubscriptionActive?: boolean;
69
+ workspaceGatewayConnectionActive?: boolean;
70
+ workspaceOpenRouterConnectionActive?: boolean;
71
+ organizationGatewayConnectionActive?: boolean;
72
+ organizationOpenRouterConnectionActive?: boolean;
73
+ workspaceGatewayCustomModels?: readonly {
74
+ upstreamModelId: string;
75
+ label?: string | null;
76
+ }[];
77
+ workspaceOpenRouterCustomModels?: readonly {
78
+ upstreamModelId: string;
79
+ label?: string | null;
80
+ }[];
81
+ organizationGatewayCustomModels?: readonly {
82
+ upstreamModelId: string;
83
+ label?: string | null;
84
+ }[];
85
+ organizationOpenRouterCustomModels?: readonly {
86
+ upstreamModelId: string;
87
+ label?: string | null;
88
+ }[];
89
+ credentialReadinessObservations?: Readonly<Record<string, ModelCredentialReadinessObservation>> | undefined;
90
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
91
+ now?: Date | undefined;
92
+ credentialReadinessMaxAgeMs?: number | undefined;
93
+ };
94
+ export type WorkspaceModelSelection = {
95
+ model: ConfiguredModel;
96
+ credentialReadiness: ModelCredentialReadinessV1;
97
+ policyAllowed: boolean;
98
+ availability: ModelAvailabilityV1;
99
+ };
100
+ /**
101
+ * One shared picker/tool decision. Catalog membership, credential readiness,
102
+ * workspace policy, and optional provider-health observations are evaluated in
103
+ * configured catalog order so every consumer exposes the same selectable set.
104
+ */
105
+ export declare function resolveWorkspaceModelSelection(input: WorkspaceModelSelectionInput): WorkspaceModelSelection[];
@@ -128,6 +128,24 @@ export type FleetSwapResult = {
128
128
  * means no compute is attached.
129
129
  */
130
130
  export declare function listFleet(services: FleetServices, ctx: FleetContext): Promise<FleetListResult>;
131
+ type FleetResourceContext = Pick<FleetContext, "accountId" | "workspaceId" | "subjectId">;
132
+ export type CreateTimeSandboxTargetPreflight = {
133
+ ok: true;
134
+ targetSandboxId: string;
135
+ workingDir: string | null;
136
+ } | {
137
+ ok: false;
138
+ reason: string;
139
+ code: BackendUnresolvableCode | "invalid_working_directory";
140
+ };
141
+ /**
142
+ * Validate a named create-time machine target before any session row exists.
143
+ * The caller still commits the pointer with `setActiveSandbox` inside the
144
+ * session-create transaction, which rechecks durable ownership/enrollment
145
+ * authority and closes the remove/revoke race without holding database locks
146
+ * across the liveness probe.
147
+ */
148
+ export declare function preflightCreateTimeSandboxTarget(services: FleetServices, ctx: FleetResourceContext, target: string, workingDir: string | null): Promise<CreateTimeSandboxTargetPreflight>;
131
149
  /**
132
150
  * THE SWAP (and attach — identical mechanic). Validate the target's ownership +
133
151
  * liveness, then repoint the session via the epoch-fenced CAS `setActiveSandbox`:
@@ -238,3 +256,4 @@ export declare function provisionSandbox(services: FleetServices, ctx: FleetCont
238
256
  kind: "selfhosted" | "modal";
239
257
  name?: string;
240
258
  }): Promise<ProvisionResult>;
259
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.6.4",
3
+ "version": "2.7.2-canary.0",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,15 +54,15 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/codex": "^0.2.20",
58
- "@opengeni/config": "^0.22.5",
59
- "@opengeni/contracts": "^2.9.2",
60
- "@opengeni/db": "^3.7.4",
61
- "@opengeni/documents": "^0.8.14",
62
- "@opengeni/events": "^0.4.12",
63
- "@opengeni/observability": "^0.8.14",
64
- "@opengeni/runtime": "^2.0.1",
65
- "@opengeni/storage": "^0.2.115",
57
+ "@opengeni/codex": "^0.2.20-canary.2",
58
+ "@opengeni/config": "^0.23.2-canary.0",
59
+ "@opengeni/contracts": "^2.11.1-canary.0",
60
+ "@opengeni/db": "^3.8.2-canary.0",
61
+ "@opengeni/documents": "^0.8.17-canary.0",
62
+ "@opengeni/events": "^0.4.15-canary.0",
63
+ "@opengeni/observability": "^0.8.17-canary.0",
64
+ "@opengeni/runtime": "^2.1.2-canary.0",
65
+ "@opengeni/storage": "^0.2.118-canary.0",
66
66
  "hono": "^4.12.18",
67
67
  "zod": "^4.2.1"
68
68
  },
@@ -1,4 +1,8 @@
1
- import { configuredStaticUsageLimits, resolveModelProvider } from "@opengeni/config";
1
+ import {
2
+ configuredStaticUsageLimits,
3
+ resolveModelProviderForTurn,
4
+ type Settings,
5
+ } from "@opengeni/config";
2
6
  import type {
3
7
  LimitAction,
4
8
  LimitDecision,
@@ -26,14 +30,35 @@ export type LimitCheckInput = {
26
30
  workspaceId?: string;
27
31
  action: LimitAction;
28
32
  quantity?: number;
29
- // The turn's model id, when the action represents an agent turn. Externally
30
- // billed models consume ZERO OpenGeni credits, so the credit-balance +
31
- // model-cost + token gates are skipped. Connected subscriptions still require
32
- // live workspace readiness; an explicitly anonymous deployment route is
33
- // statically ready by definition. Non-model infra actions leave this undefined.
33
+ // The turn's model id, when the action represents an agent turn. The model's
34
+ // deployment cost controls credit/cost gates; upstream metering independently
35
+ // controls the token cap. Connected subscriptions still require live workspace
36
+ // readiness. Non-model infra actions leave this undefined.
34
37
  model?: string | null;
35
38
  };
36
39
 
40
+ export function modelFundingForAdmission(
41
+ settings: Settings,
42
+ model: string | null | undefined,
43
+ codexBilled: boolean,
44
+ ): { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean } {
45
+ const resolvedModel = model ? resolveModelProviderForTurn(settings, model)?.model : null;
46
+ const codexSubscriptionModel =
47
+ resolvedModel?.credentialSource.kind === "connected_subscription" &&
48
+ resolvedModel.credentialSource.provider === "codex";
49
+ return {
50
+ // A Codex namespace/definition never bypasses credits by itself: the live
51
+ // workspace credential predicate above remains authoritative. SuperGrok's
52
+ // static overlay is likewise secret-free; its worker/provider admission
53
+ // owns live account selection before any upstream request can occur.
54
+ fundedWithoutCredits:
55
+ codexBilled ||
56
+ (resolvedModel != null && !codexSubscriptionModel && resolvedModel.cost !== "credits"),
57
+ countsTowardTokenCap:
58
+ !codexBilled && resolvedModel != null && resolvedModel.billing.upstreamPayer === "deployment",
59
+ };
60
+ }
61
+
37
62
  export async function requireLimit(deps: LimitDependencies, input: LimitCheckInput): Promise<void> {
38
63
  const decision = await checkLimit(deps, input);
39
64
  if (decision.allowed) {
@@ -59,25 +84,22 @@ export async function checkLimit(
59
84
  model: input.model,
60
85
  })
61
86
  : false;
62
- const credentialFreeExternal = input.model
63
- ? (() => {
64
- const resolved = resolveModelProvider(deps.settings, input.model);
65
- return (
66
- resolved?.model.billing.metering === "external" &&
67
- resolved.model.credentialSource.kind === "deployment" &&
68
- resolved.model.credentialSource.mechanism === "none"
69
- );
70
- })()
71
- : false;
72
- const externallyBilled = codexBilled || credentialFreeExternal;
73
- const creditDecision = await checkCreditBalance(deps, input, externallyBilled);
87
+ const { fundedWithoutCredits, countsTowardTokenCap } = modelFundingForAdmission(
88
+ deps.settings,
89
+ input.model,
90
+ codexBilled,
91
+ );
92
+ const creditDecision = await checkCreditBalance(deps, input, fundedWithoutCredits);
74
93
  if (!creditDecision.allowed) {
75
94
  return creditDecision;
76
95
  }
77
96
  if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
78
97
  return { allowed: true };
79
98
  }
80
- return await checkStaticCaps(deps, input, externallyBilled);
99
+ return await checkStaticCaps(deps, input, {
100
+ fundedWithoutCredits,
101
+ countsTowardTokenCap,
102
+ });
81
103
  }
82
104
 
83
105
  async function checkCreditBalance(
@@ -101,10 +123,14 @@ async function checkCreditBalance(
101
123
  async function checkStaticCaps(
102
124
  deps: LimitDependencies,
103
125
  input: LimitCheckInput,
104
- externallyBilled: boolean,
126
+ funding: { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean },
105
127
  ): Promise<LimitDecision> {
106
128
  const limits = configuredStaticUsageLimits(deps.settings);
107
- if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !externallyBilled) {
129
+ if (
130
+ limits.maxMonthlyCostMicrosPerAccount &&
131
+ isCostlyAction(input.action) &&
132
+ !funding.fundedWithoutCredits
133
+ ) {
108
134
  const used = await sumUsageQuantity(deps.db, {
109
135
  accountId: input.accountId,
110
136
  eventType: "model.cost",
@@ -185,7 +211,11 @@ async function checkStaticCaps(
185
211
  );
186
212
  }
187
213
  case "tokens:consume": {
188
- if (externallyBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
214
+ if (
215
+ !funding.countsTowardTokenCap ||
216
+ !limits.maxMonthlyTokensPerWorkspace ||
217
+ !input.workspaceId
218
+ ) {
189
219
  return { allowed: true };
190
220
  }
191
221
  const used = await sumUsageQuantity(deps.db, {
@@ -19,6 +19,7 @@ import type { ManagedAuthSessionAdapter } from "./managed-auth-session-sets";
19
19
  import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
20
20
  import type { TranscriptionSegmenter, TranscriptionService } from "./transcription";
21
21
  import type { EditableArtifactApplicationPort } from "./editable-artifact-live";
22
+ import type { ResolvedCatalogSettings } from "./model-catalog";
22
23
  import type {
23
24
  EditableArtifactAgentApplication,
24
25
  EditableArtifactDurableExportService,
@@ -129,6 +130,13 @@ export type ManagedEmailTransport = {
129
130
 
130
131
  export type AppDependencies = {
131
132
  settings: Settings;
133
+ /**
134
+ * Original deployment settings when `settings` is already overlaid with a
135
+ * deployment/workspace catalog snapshot. Model-bearing request adapters set
136
+ * this marker so core admission never feeds a synthetic reviewed provider
137
+ * back through deployment validation.
138
+ */
139
+ catalogSourceSettings?: Settings;
132
140
  db: Database;
133
141
  /**
134
142
  * Host-composed editable artifact engine. Standalone startup binds the same
@@ -186,6 +194,8 @@ export type AppDependencies = {
186
194
  codexFetch?: typeof fetch;
187
195
  /** Injectable GitHub transport for deterministic personal-OAuth tests. */
188
196
  githubPersonalFetch?: typeof fetch;
197
+ /** Injectable credential-free GitHub transport for public repository verification tests. */
198
+ githubAnonymousFetch?: typeof fetch;
189
199
  /** Injectable xAI OAuth/subscription transport for deterministic API/provider tests. */
190
200
  xaiFetch?: typeof fetch;
191
201
  /** Injectable Slack Web API transport for deterministic bot-connection tests. */
@@ -225,6 +235,7 @@ export type AppDependencies = {
225
235
  export type ObjectStorageDependency = ReturnType<typeof createObjectStorage>;
226
236
 
227
237
  export type ApiRouteDeps = AppDependencies & {
238
+ resolveCatalogSettings: () => Promise<ResolvedCatalogSettings>;
228
239
  managedEmailTransport: ManagedEmailTransport;
229
240
  objectStorage: ObjectStorageDependency;
230
241
  githubStateSecret: string;
@@ -244,7 +255,12 @@ export type ApiRouteDeps = AppDependencies & {
244
255
  */
245
256
  export type AcceptSessionUserMessageDependencies = Pick<
246
257
  AppDependencies,
247
- "settings" | "db" | "bus" | "sessionAuthorization" | "schedulePromptPostCommit"
258
+ | "settings"
259
+ | "catalogSourceSettings"
260
+ | "db"
261
+ | "bus"
262
+ | "sessionAuthorization"
263
+ | "schedulePromptPostCommit"
248
264
  > & {
249
265
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
250
266
  objectStorage: ObjectStorageDependency;
@@ -57,6 +57,7 @@ import { hasPermission } from "../access";
57
57
  import { isFikenConnection, preferredFikenConnection } from "./fiken";
58
58
  import { listSkillLibraryEntries, type SkillLibraryEntry } from "@opengeni/runtime/skill-library";
59
59
  import { listCapabilityPacks, listWorkspaceCapabilityPacks } from "./packs";
60
+ import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
60
61
 
61
62
  const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
62
63
  const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
@@ -403,7 +404,7 @@ function normalizedMcpCredentialHeaders(
403
404
  }
404
405
 
405
406
  async function validateMcpCapabilityConnectionRef(
406
- input: { db: Database; grant: AccessGrant; workspaceId: string },
407
+ input: { db: Database; grant: AccessGrant; workspaceId: string; settings: Settings },
407
408
  item: CapabilityCatalogItem,
408
409
  ref: McpServerConnectionRef,
409
410
  ): Promise<McpServerConnectionRef> {
@@ -426,6 +427,7 @@ async function validateMcpCapabilityConnectionRef(
426
427
  providerDomain: ref.providerDomain.trim(),
427
428
  subjectScope,
428
429
  ...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
430
+ ...(ref.authoritySource === "host" ? { authoritySource: "host" as const } : {}),
429
431
  ...(ref.provider ? { provider: ref.provider.trim() } : {}),
430
432
  ...(ref.kind ? { kind: ref.kind } : {}),
431
433
  ...(ref.scopes ? { scopes: uniqueStrings(ref.scopes) } : {}),
@@ -449,6 +451,10 @@ async function validateMcpCapabilityConnectionRef(
449
451
  "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef",
450
452
  });
451
453
  }
454
+ if (normalized.authoritySource === "host") {
455
+ assertHostMcpAuthoritySourceAdmissionEnabled(input.settings, normalized);
456
+ return normalized;
457
+ }
452
458
 
453
459
  let connection = normalized.connectionId
454
460
  ? await getConnectionMetadata(
@@ -1680,12 +1686,22 @@ function installationConnectionRef(
1680
1686
  if (!ref || typeof ref !== "object") {
1681
1687
  return null;
1682
1688
  }
1683
- const { connectionId, providerDomain, kind, subjectScope } = ref as Record<string, unknown>;
1689
+ const { authoritySource, connectionId, providerDomain, kind, subjectScope } = ref as Record<
1690
+ string,
1691
+ unknown
1692
+ >;
1684
1693
  if (typeof providerDomain !== "string" || typeof kind !== "string") {
1685
1694
  return null;
1686
1695
  }
1696
+ if (authoritySource === "host") {
1697
+ // The internal installation/runtime ref retains the exact host binding.
1698
+ // Public capability catalogs use the existing null representation for an
1699
+ // enabled capability without a native OpenGeni connection, so indefinitely
1700
+ // open old browser bundles cannot treat a host UUID as native OAuth state.
1701
+ return null;
1702
+ }
1687
1703
  if (subjectScope === "subject") {
1688
- // Never project a personal connection UUID through workspace-visible
1704
+ // Never project a native personal connection UUID through workspace-visible
1689
1705
  // capability configuration, including legacy rows that still contain one.
1690
1706
  return { providerDomain, kind, subjectScope: "subject" };
1691
1707
  }
@@ -0,0 +1,25 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { McpServerConnectionRef } from "@opengeni/contracts";
3
+ import { HTTPException } from "hono/http-exception";
4
+
5
+ /**
6
+ * Explicit host-owned MCP refs change which credential authority executes an
7
+ * opaque connection id. Admit new external refs only after the operator has
8
+ * completed the two-phase fleet rollout. Readers and inheritance remain
9
+ * tolerant regardless of this switch, and markerless legacy refs retain their
10
+ * bounded non-UUID compatibility path.
11
+ */
12
+ export function assertHostMcpAuthoritySourceAdmissionEnabled(
13
+ settings: Pick<Settings, "hostMcpAuthoritySourceAdmissionEnabled">,
14
+ connectionRef: McpServerConnectionRef | null | undefined,
15
+ ): void {
16
+ if (
17
+ connectionRef?.authoritySource === "host" &&
18
+ !settings.hostMcpAuthoritySourceAdmissionEnabled
19
+ ) {
20
+ throw new HTTPException(422, {
21
+ message:
22
+ "new host-owned MCP connection refs are not admitted; upgrade the complete API/worker/web fleet, then set OPENGENI_HOST_MCP_AUTHORITY_SOURCE_ADMISSION_ENABLED=true",
23
+ });
24
+ }
25
+ }
@@ -305,6 +305,8 @@ export async function getWorkspaceInsights(
305
305
  creditUsd: microsToUsd(row.pricedCostMicros),
306
306
  estimatedProviderUsd: microsToUsd(row.estimatedProviderCostMicros),
307
307
  estimatedProviderCostKnownCalls: row.estimatedProviderCostKnownCalls,
308
+ equivalentCreditUsd: microsToUsd(row.equivalentCreditCostMicros),
309
+ equivalentCreditCostKnownCalls: row.equivalentCreditCostKnownCalls,
308
310
  }))
309
311
  .sort((a, b) => b.totalTokens - a.totalTokens);
310
312
 
@@ -326,6 +328,22 @@ export async function getWorkspaceInsights(
326
328
  (sum, row) => sum + row.estimatedProviderCostKnownCalls,
327
329
  0,
328
330
  );
331
+ const equivalentCreditCostMicros = modelRows.reduce(
332
+ (sum, row) => sum + row.equivalentCreditCostMicros,
333
+ 0,
334
+ );
335
+ const priorEquivalentCreditCostMicros = priorModelRows.reduce(
336
+ (sum, row) => sum + row.equivalentCreditCostMicros,
337
+ 0,
338
+ );
339
+ const equivalentCreditCostKnownCalls = modelRows.reduce(
340
+ (sum, row) => sum + row.equivalentCreditCostKnownCalls,
341
+ 0,
342
+ );
343
+ const priorEquivalentCreditCostKnownCalls = priorModelRows.reduce(
344
+ (sum, row) => sum + row.equivalentCreditCostKnownCalls,
345
+ 0,
346
+ );
329
347
  const modelCalls = modelRows.reduce((sum, row) => sum + row.calls, 0);
330
348
  const priorInputTokens = priorModelRows.reduce((sum, row) => sum + row.inputTokens, 0);
331
349
  const priorTotalTokens = priorModelRows.reduce((sum, row) => sum + row.totalTokens, 0);
@@ -342,6 +360,8 @@ export async function getWorkspaceInsights(
342
360
  costMicros: 0,
343
361
  estimatedProviderCostMicros: 0,
344
362
  estimatedProviderCostKnownCalls: 0,
363
+ equivalentCreditCostMicros: 0,
364
+ equivalentCreditCostKnownCalls: 0,
345
365
  inputTokens: 0,
346
366
  outputTokens: 0,
347
367
  cachedTokens: 0,
@@ -361,6 +381,8 @@ export async function getWorkspaceInsights(
361
381
  modelCostUsd: microsToUsd(modelCostMicros),
362
382
  estimatedProviderUsd: microsToUsd(facts.estimatedProviderCostMicros),
363
383
  estimatedProviderCostKnownCalls: facts.estimatedProviderCostKnownCalls,
384
+ equivalentCreditUsd: microsToUsd(facts.equivalentCreditCostMicros),
385
+ equivalentCreditCostKnownCalls: facts.equivalentCreditCostKnownCalls,
364
386
  warmSeconds: usageBuckets.get(bucket)?.warmSeconds ?? 0,
365
387
  inputTokens: facts.inputTokens,
366
388
  outputTokens: facts.outputTokens,
@@ -393,6 +415,8 @@ export async function getWorkspaceInsights(
393
415
  creditUsd,
394
416
  estimatedProviderUsd: microsToUsd(row.estimatedProviderCostMicros),
395
417
  estimatedProviderCostKnownCalls: row.estimatedProviderCostKnownCalls,
418
+ equivalentCreditUsd: microsToUsd(row.equivalentCreditCostMicros),
419
+ equivalentCreditCostKnownCalls: row.equivalentCreditCostKnownCalls,
396
420
  tokens: row.totalTokens,
397
421
  cacheHitPct: cacheHitPct(row.cachedTokens, row.cacheInputTokens),
398
422
  pctOfCreditUsd:
@@ -414,6 +438,8 @@ export async function getWorkspaceInsights(
414
438
  creditUsd: fact ? microsToUsd(fact.pricedCostMicros) : null,
415
439
  estimatedProviderUsd: fact ? microsToUsd(fact.estimatedProviderCostMicros) : null,
416
440
  estimatedProviderCostKnownCalls: fact ? fact.estimatedProviderCostKnownCalls : null,
441
+ equivalentCreditUsd: fact ? microsToUsd(fact.equivalentCreditCostMicros) : null,
442
+ equivalentCreditCostKnownCalls: fact ? fact.equivalentCreditCostKnownCalls : null,
417
443
  tokens: fact ? fact.totalTokens : null,
418
444
  cacheHitPct: fact ? cacheHitPct(fact.cachedTokens, fact.cacheInputTokens) : null,
419
445
  billing: fact ? billingPathOf(fact.billingPath) : null,
@@ -489,6 +515,8 @@ export async function getWorkspaceInsights(
489
515
  row.estimatedProviderCostMicros == null
490
516
  ? null
491
517
  : microsToUsd(row.estimatedProviderCostMicros),
518
+ equivalentCreditUsd:
519
+ row.equivalentCreditCostMicros == null ? null : microsToUsd(row.equivalentCreditCostMicros),
492
520
  pricingSource: pricingSourceOf(row.pricingSource),
493
521
  })),
494
522
  promptContributions,
@@ -522,6 +550,10 @@ export async function getWorkspaceInsights(
522
550
  priorEstimatedProviderUsd: microsToUsd(priorEstimatedProviderCostMicros),
523
551
  estimatedProviderCostKnownCalls,
524
552
  priorEstimatedProviderCostKnownCalls,
553
+ equivalentCreditUsd: microsToUsd(equivalentCreditCostMicros),
554
+ priorEquivalentCreditUsd: microsToUsd(priorEquivalentCreditCostMicros),
555
+ equivalentCreditCostKnownCalls,
556
+ priorEquivalentCreditCostKnownCalls,
525
557
  modelCalls,
526
558
  priorInputTokens,
527
559
  priorTotalTokens,
@@ -31,6 +31,7 @@ import {
31
31
  PR_REVIEW_AUTOMATION_ADAPTER_ID,
32
32
  PR_REVIEW_AUTOMATION_TEMPLATE_ID,
33
33
  } from "./pr-review";
34
+ import { OPENGENI_PRODUCT_INTEGRATION_PACK } from "./product-integration-pack";
34
35
 
35
36
  export const MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
36
37
 
@@ -277,7 +278,11 @@ const openGeniPrReviewPack: CapabilityPack = {
277
278
  },
278
279
  };
279
280
 
280
- const packs = [marketingSocialPack, openGeniPrReviewPack] satisfies CapabilityPack[];
281
+ const packs = [
282
+ marketingSocialPack,
283
+ openGeniPrReviewPack,
284
+ OPENGENI_PRODUCT_INTEGRATION_PACK,
285
+ ] satisfies CapabilityPack[];
281
286
 
282
287
  export function listCapabilityPacks(): CapabilityPack[] {
283
288
  return packs;
@@ -355,6 +360,7 @@ export type InlinePackSkillInstall = {
355
360
  sourcePath: string;
356
361
  name: string;
357
362
  description: string;
363
+ activationMode: "workspace_managed" | "session_selected";
358
364
  contentSha256: string;
359
365
  totalBytes: number;
360
366
  files: Array<{ path: string; content: string; byteSize: number; contentSha256: string }>;
@@ -372,19 +378,22 @@ export function inlinePackSkillInstall(
372
378
  });
373
379
  }
374
380
  const normalizedName = skill.name.toLowerCase();
381
+ const activationMode = skill.activationMode ?? "workspace_managed";
382
+ const activationIdentity = activationMode === "session_selected" ? "session-selected/" : "";
375
383
  const encodedSkill = encodeURIComponent(normalizedName);
376
- const sourceUrl = `https://opengeni.invalid/pack-inline-skills/${encodedSkill}/${artifact.contentSha256}`;
377
- const capabilityId = `skill:pack-inline/${normalizedName}@${artifact.contentSha256}`;
384
+ const sourceUrl = `https://opengeni.invalid/pack-inline-skills/${activationIdentity}${encodedSkill}/${artifact.contentSha256}`;
385
+ const capabilityId = `skill:pack-inline/${activationIdentity}${normalizedName}@${artifact.contentSha256}`;
378
386
  return {
379
387
  componentKey: `inline-skill/${normalizedName}`,
380
388
  capabilityId,
381
- pluginKey: `pack-skill/${normalizedName}/${artifact.contentSha256}`,
389
+ pluginKey: `pack-skill/${activationIdentity}${normalizedName}/${artifact.contentSha256}`,
382
390
  sourceUrl,
383
391
  repositoryUrl: "https://opengeni.invalid/pack-inline-skills",
384
392
  sourceCommit: artifact.contentSha256,
385
393
  sourcePath: normalizedName,
386
394
  name: artifact.name,
387
395
  description: artifact.description,
396
+ activationMode,
388
397
  contentSha256: artifact.contentSha256,
389
398
  totalBytes: artifact.totalBytes,
390
399
  files: artifact.files.map((file) => ({
@@ -405,21 +414,42 @@ export async function previewCapabilityPackInstallation(
405
414
  const { workspaceId } = access;
406
415
  const installation = await getPackInstallation(db, workspaceId, pack.id);
407
416
  const inlineInstalls = pack.skills.map((skill) => inlinePackSkillInstall(pack, skill));
408
- const [referencedComponents, inlineComponents] = await Promise.all([
409
- resolvePackComponentReferences(db, workspaceId, pack.components),
410
- resolvePackInlineSkillReferences(
411
- db,
412
- workspaceId,
413
- inlineInstalls.map((inline) => ({
414
- key: inline.componentKey,
415
- capabilityId: inline.capabilityId,
416
- name: inline.name,
417
- contentSha256: inline.contentSha256,
418
- })),
419
- installation?.id,
420
- ),
421
- ]);
422
417
  const manifestDigest = capabilityPackManifestDigest(pack);
418
+ const inlineRequirements = inlineInstalls.map((inline) => ({
419
+ key: inline.componentKey,
420
+ capabilityId: inline.capabilityId,
421
+ name: inline.name,
422
+ activationMode: inline.activationMode,
423
+ contentSha256: inline.contentSha256,
424
+ }));
425
+ const [referencedComponents, plannedInlineComponents, installedSessionSelectedComponents] =
426
+ await Promise.all([
427
+ resolvePackComponentReferences(db, workspaceId, pack.components),
428
+ resolvePackInlineSkillReferences(db, workspaceId, inlineRequirements, installation?.id),
429
+ installation?.status === "active" && installation.manifestDigest === manifestDigest
430
+ ? resolvePackInlineSkillReferences(
431
+ db,
432
+ workspaceId,
433
+ inlineRequirements.filter(
434
+ (requirement) => requirement.activationMode === "session_selected",
435
+ ),
436
+ )
437
+ : Promise.resolve([]),
438
+ ]);
439
+ const installedSessionSelectedByKey = new Map(
440
+ installedSessionSelectedComponents.map((component) => [component.key, component]),
441
+ );
442
+ // Installation planning excludes the Pack's own current ownership so an
443
+ // update can replace old inline content. For launch affordances, preserve an
444
+ // independently resolved active facet-installation id on an exact installed
445
+ // manifest. A missing facet retains the future capability id and therefore
446
+ // cannot be mistaken for something a new session can select right now.
447
+ const inlineComponents = plannedInlineComponents.map((component) => {
448
+ const installed = installedSessionSelectedByKey.get(component.key);
449
+ return installed?.status === "ready" && installed.resolvedId
450
+ ? { ...component, resolvedId: installed.resolvedId }
451
+ : component;
452
+ });
423
453
  const components: PackComponentResolution[] = [...referencedComponents, ...inlineComponents];
424
454
  const blockers = components
425
455
  .filter((component) => component.required && component.status !== "ready")