@opengeni/core 0.4.6 → 0.4.8

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.
@@ -1,10 +1,6 @@
1
1
  import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
2
- import type { AccessGrant, WorkspaceEnvironment } from "@opengeni/contracts";
3
- import {
4
- getWorkspaceEnvironment,
5
- recordAuditEvent,
6
- type Database,
7
- } from "@opengeni/db";
2
+ import type { AccessGrant, VariableSet } from "@opengeni/contracts";
3
+ import { getVariableSet, recordAuditEvent, type Database } from "@opengeni/db";
8
4
  import { HTTPException } from "hono/http-exception";
9
5
  import { requirePermission } from "../access";
10
6
 
@@ -13,8 +9,8 @@ export const MAX_VARIABLES_PER_ENVIRONMENT = 100;
13
9
 
14
10
  // Names the platform itself injects into sandboxes (sandboxEnvironmentForRun,
15
11
  // collectGitIdentityEnvironment) plus loader/startup-injection vectors. These
16
- // can never be set as workspace environment variables, so the run-scoped
17
- // GitHub auth block and git identity always win without silent collisions.
12
+ // can never be set as variable set variables, so the run-scoped
13
+ // git auth block and git identity always win without silent collisions.
18
14
  const reservedExactNames = new Set([
19
15
  "HOME",
20
16
  "PATH",
@@ -32,6 +28,8 @@ const reservedExactNames = new Set([
32
28
  "PERL5LIB",
33
29
  "GH_TOKEN",
34
30
  "GITHUB_TOKEN",
31
+ "GITLAB_TOKEN",
32
+ "AZURE_DEVOPS_EXT_PAT",
35
33
  "GIT_ASKPASS",
36
34
  "GIT_TERMINAL_PROMPT",
37
35
  ]);
@@ -45,70 +43,92 @@ const reservedPrefixes = [
45
43
  "DYLD_",
46
44
  ];
47
45
 
48
- export function assertAllowedEnvironmentVariableName(name: string): void {
46
+ export function assertAllowedVariableSetVariableName(name: string): void {
49
47
  if (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
50
- throw new HTTPException(422, { message: `reserved environment variable name: ${name}` });
48
+ throw new HTTPException(422, {
49
+ message: `reserved variable set variable name / reserved environment variable name: ${name}`,
50
+ });
51
51
  }
52
52
  }
53
53
 
54
- export function requireEnvironmentEncryption(settings: Settings): Uint8Array {
54
+ /** @deprecated use assertAllowedVariableSetVariableName */
55
+ export const assertAllowedEnvironmentVariableName = assertAllowedVariableSetVariableName;
56
+
57
+ export function requireVariableSetEncryption(settings: Settings): Uint8Array {
55
58
  const key = environmentsEncryptionKeyBytes(settings);
56
59
  if (!key) {
57
- throw new HTTPException(503, { message: "workspace environments require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY" });
60
+ throw new HTTPException(503, {
61
+ message: "variable sets require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
62
+ });
58
63
  }
59
64
  return key;
60
65
  }
61
66
 
62
- export async function requireEnvironmentForApi(db: Database, workspaceId: string, environmentId: string): Promise<WorkspaceEnvironment> {
63
- const environment = await getWorkspaceEnvironment(db, workspaceId, environmentId);
64
- if (!environment) {
65
- throw new HTTPException(404, { message: "environment not found" });
67
+ /** @deprecated use requireVariableSetEncryption */
68
+ export const requireEnvironmentEncryption = requireVariableSetEncryption;
69
+
70
+ export async function requireVariableSetForApi(
71
+ db: Database,
72
+ workspaceId: string,
73
+ variableSetId: string,
74
+ ): Promise<VariableSet> {
75
+ const variableSet = await getVariableSet(db, workspaceId, variableSetId);
76
+ if (!variableSet) {
77
+ throw new HTTPException(404, { message: "variableSet not found" });
66
78
  }
67
- return environment;
79
+ return variableSet;
68
80
  }
69
81
 
70
82
  /**
71
- * Validates an environment attachment supplied in a request payload (session
83
+ * Validates an variableSet attachment supplied in a request payload (session
72
84
  * create, scheduled task create/update, pack enable). Requires the
73
- * `environments:use` permission unless the attachment was already authorized
85
+ * `variable-sets:use` permission unless the attachment was already authorized
74
86
  * (pack-installation-inherited attachments), and maps a missing or
75
- * cross-workspace environment to 422 because the id is payload, not the route
87
+ * cross-variable set to 422 because the id is payload, not the route
76
88
  * target. RLS plus the workspace_id clause make cross-workspace ids
77
89
  * indistinguishable from missing ones.
78
90
  */
79
- export async function validateEnvironmentAttachment(
91
+ export async function validateVariableSetAttachment(
80
92
  deps: { settings: Settings; db: Database },
81
93
  grant: AccessGrant,
82
94
  workspaceId: string,
83
- environmentId: string,
95
+ variableSetId: string,
84
96
  options: { preauthorized?: boolean } = {},
85
- ): Promise<WorkspaceEnvironment> {
86
- requireEnvironmentEncryption(deps.settings);
97
+ ): Promise<VariableSet> {
98
+ requireVariableSetEncryption(deps.settings);
87
99
  if (!options.preauthorized) {
88
- requirePermission(grant, "environments:use");
100
+ requirePermission(grant, "variable-sets:use");
89
101
  }
90
- const environment = await getWorkspaceEnvironment(deps.db, workspaceId, environmentId);
91
- if (!environment) {
92
- throw new HTTPException(422, { message: "unknown environmentId" });
102
+ const variableSet = await getVariableSet(deps.db, workspaceId, variableSetId);
103
+ if (!variableSet) {
104
+ throw new HTTPException(422, { message: "unknown variableSetId" });
93
105
  }
94
- return environment;
106
+ return variableSet;
95
107
  }
96
108
 
97
- export async function recordEnvironmentAuditEvent(db: Database, input: {
98
- grant: AccessGrant;
99
- action: "environment.created" | "environment.updated" | "environment.deleted" | "environment.variable.set" | "environment.variable.deleted";
100
- environmentId: string;
101
- variableName?: string;
102
- }): Promise<void> {
109
+ export async function recordVariableSetAuditEvent(
110
+ db: Database,
111
+ input: {
112
+ grant: AccessGrant;
113
+ action:
114
+ | "variable_set.created"
115
+ | "variable_set.updated"
116
+ | "variable_set.deleted"
117
+ | "variable_set.variable.set"
118
+ | "variable_set.variable.deleted";
119
+ variableSetId: string;
120
+ variableName?: string;
121
+ },
122
+ ): Promise<void> {
103
123
  await recordAuditEvent(db, {
104
124
  accountId: input.grant.accountId,
105
125
  workspaceId: input.grant.workspaceId,
106
126
  subjectId: input.grant.subjectId,
107
127
  action: input.action,
108
- targetType: "workspace_environment",
109
- targetId: input.environmentId,
128
+ targetType: "workspace_variable_set",
129
+ targetId: input.variableSetId,
110
130
  metadata: {
111
- environmentId: input.environmentId,
131
+ variableSetId: input.variableSetId,
112
132
  ...(input.variableName ? { name: input.variableName } : {}),
113
133
  },
114
134
  });
@@ -3,7 +3,12 @@ import {
3
3
  type ScheduledTaskAgentConfig,
4
4
  type SocialConnection,
5
5
  } from "@opengeni/contracts";
6
- import { getWorkspacePack, listPackInstallations, listWorkspacePacks, type Database } from "@opengeni/db";
6
+ import {
7
+ getWorkspacePack,
8
+ listPackInstallations,
9
+ listWorkspacePacks,
10
+ type Database,
11
+ } from "@opengeni/db";
7
12
  import { HTTPException } from "hono/http-exception";
8
13
 
9
14
  export const MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
@@ -11,7 +16,8 @@ export const MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
11
16
  const marketingSocialPack: CapabilityPack = {
12
17
  id: MARKETING_SOCIAL_PACK_ID,
13
18
  name: "Marketing social daily analysis",
14
- description: "Connect social accounts, attach marketing knowledge, and schedule agents to produce daily media performance analysis.",
19
+ description:
20
+ "Connect social accounts, attach marketing knowledge, and schedule agents to produce daily media performance analysis.",
15
21
  role: "marketing",
16
22
  category: "social-media",
17
23
  version: "0.1.0",
@@ -54,7 +60,12 @@ const marketingSocialPack: CapabilityPack = {
54
60
  category: "social-media",
55
61
  authModel: "oauth2_authorization_code",
56
62
  providers: ["instagram", "facebook"],
57
- scopes: ["instagram_basic", "instagram_manage_insights", "pages_read_engagement", "pages_show_list"],
63
+ scopes: [
64
+ "instagram_basic",
65
+ "instagram_manage_insights",
66
+ "pages_read_engagement",
67
+ "pages_show_list",
68
+ ],
58
69
  required: false,
59
70
  metadata: {
60
71
  docs: "https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/",
@@ -78,7 +89,10 @@ const marketingSocialPack: CapabilityPack = {
78
89
  category: "social-media",
79
90
  authModel: "oauth2_authorization_code",
80
91
  providers: ["youtube"],
81
- scopes: ["https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/yt-analytics.readonly"],
92
+ scopes: [
93
+ "https://www.googleapis.com/auth/youtube.readonly",
94
+ "https://www.googleapis.com/auth/yt-analytics.readonly",
95
+ ],
82
96
  required: false,
83
97
  metadata: {
84
98
  docs: "https://developers.google.com/youtube/v3",
@@ -90,7 +104,8 @@ const marketingSocialPack: CapabilityPack = {
90
104
  type: "document_base",
91
105
  id: "marketing-playbook",
92
106
  name: "Marketing playbook",
93
- description: "Optional workspace document base with brand voice, campaign calendars, audience research, and reporting rules.",
107
+ description:
108
+ "Optional workspace document base with brand voice, campaign calendars, audience research, and reporting rules.",
94
109
  required: false,
95
110
  },
96
111
  ],
@@ -139,7 +154,10 @@ export function isBuiltInCapabilityPack(packId: string): boolean {
139
154
  * (for example after a contract tightening) are skipped instead of breaking
140
155
  * the whole catalog.
141
156
  */
142
- export async function listWorkspaceCapabilityPacks(db: Database, workspaceId: string): Promise<CapabilityPack[]> {
157
+ export async function listWorkspaceCapabilityPacks(
158
+ db: Database,
159
+ workspaceId: string,
160
+ ): Promise<CapabilityPack[]> {
143
161
  const registered = await listWorkspacePacks(db, workspaceId);
144
162
  const builtInIds = new Set(packs.map((pack) => pack.id));
145
163
  const registeredPacks = registered
@@ -151,7 +169,11 @@ export async function listWorkspaceCapabilityPacks(db: Database, workspaceId: st
151
169
  return [...packs, ...registeredPacks];
152
170
  }
153
171
 
154
- export async function resolveCapabilityPack(db: Database, workspaceId: string, packId: string): Promise<CapabilityPack | null> {
172
+ export async function resolveCapabilityPack(
173
+ db: Database,
174
+ workspaceId: string,
175
+ packId: string,
176
+ ): Promise<CapabilityPack | null> {
155
177
  const builtIn = getCapabilityPack(packId);
156
178
  if (builtIn) {
157
179
  return builtIn;
@@ -171,7 +193,11 @@ export async function resolveCapabilityPack(db: Database, workspaceId: string, p
171
193
  * generic capability enable path) and re-checked at session start by the
172
194
  * worker, which also covers manifests re-registered after enablement.
173
195
  */
174
- export async function assertPackSandboxImageCompatible(db: Database, workspaceId: string, pack: CapabilityPack): Promise<void> {
196
+ export async function assertPackSandboxImageCompatible(
197
+ db: Database,
198
+ workspaceId: string,
199
+ pack: CapabilityPack,
200
+ ): Promise<void> {
175
201
  if (!pack.sandboxImage) {
176
202
  return;
177
203
  }
@@ -218,13 +244,18 @@ function marketingDailyAnalysisPrompt(input: {
218
244
  documentBaseIds: string[];
219
245
  promptInstructions?: string;
220
246
  }): string {
221
- const connectionLines = input.connections.map((connection) => {
222
- return `- ${connection.provider}: ${connection.accountHandle} (${connection.id})`;
223
- }).join("\n");
224
- const knowledgeLine = input.documentBaseIds.length > 0
225
- ? `Use these document base IDs for brand/campaign knowledge through the docs MCP: ${input.documentBaseIds.join(", ")}.`
226
- : "No document base IDs were selected; rely only on social context returned by tools.";
227
- const extra = input.promptInstructions ? `\nAdditional operator instructions:\n${input.promptInstructions.trim()}\n` : "";
247
+ const connectionLines = input.connections
248
+ .map((connection) => {
249
+ return `- ${connection.provider}: ${connection.accountHandle} (${connection.id})`;
250
+ })
251
+ .join("\n");
252
+ const knowledgeLine =
253
+ input.documentBaseIds.length > 0
254
+ ? `Use these document base IDs for brand/campaign knowledge through the docs MCP: ${input.documentBaseIds.join(", ")}.`
255
+ : "No document base IDs were selected; rely only on social context returned by tools.";
256
+ const extra = input.promptInstructions
257
+ ? `\nAdditional operator instructions:\n${input.promptInstructions.trim()}\n`
258
+ : "";
228
259
 
229
260
  return [
230
261
  "Run the daily social media analysis for the selected accounts.",
@@ -237,5 +268,7 @@ function marketingDailyAnalysisPrompt(input: {
237
268
  extra,
238
269
  "Produce a concise report with these sections: executive summary, notable account changes, winning posts, underperforming posts, audience and content signals, recommended actions for the next 24 hours, and data gaps.",
239
270
  "Use only metrics and posts returned by tools or document search. Do not invent metrics, posts, or account capabilities.",
240
- ].filter(Boolean).join("\n");
271
+ ]
272
+ .filter(Boolean)
273
+ .join("\n");
241
274
  }
@@ -8,11 +8,7 @@ import {
8
8
  type ResourceRef,
9
9
  type ToolRef,
10
10
  } from "@opengeni/contracts";
11
- import {
12
- listGitHubInstallationIdsForWorkspace,
13
- requireFile,
14
- type Database,
15
- } from "@opengeni/db";
11
+ import { listGitHubInstallationIdsForWorkspace, requireFile, type Database } from "@opengeni/db";
16
12
  import { HTTPException } from "hono/http-exception";
17
13
 
18
14
  export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[] {
@@ -20,7 +16,9 @@ export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[
20
16
  const out: ToolRef[] = [];
21
17
  for (const tool of tools) {
22
18
  if (tool.kind !== "mcp") {
23
- throw new HTTPException(422, { message: `unsupported tool kind: ${(tool as { kind?: string }).kind}` });
19
+ throw new HTTPException(422, {
20
+ message: `unsupported tool kind: ${(tool as { kind?: string }).kind}`,
21
+ });
24
22
  }
25
23
  const optional = tool.optional === true;
26
24
  if (!mcpServerIds.has(tool.id)) {
@@ -37,25 +35,36 @@ export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[
37
35
  // - optional:true + unknown id is skipped above: the client explicitly
38
36
  // opted into graceful degradation for MCPs (for example docs servers
39
37
  // like context7) that only some deployments configure.
40
- out.push(optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id });
38
+ out.push(
39
+ optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id },
40
+ );
41
41
  }
42
42
  return mergeToolRefs([], out);
43
43
  }
44
44
 
45
45
  type McpSettings = Pick<Settings, "mcpServers">;
46
46
 
47
- export function enabledCapabilityMcpToolRefs(settings: McpSettings, runtimeSettings: McpSettings): ToolRef[] {
47
+ export function enabledCapabilityMcpToolRefs(
48
+ settings: McpSettings,
49
+ runtimeSettings: McpSettings,
50
+ ): ToolRef[] {
48
51
  const configuredIds = new Set(settings.mcpServers.map((server) => server.id));
49
- return runtimeSettings.mcpServers
50
- .filter((server) => !configuredIds.has(server.id))
51
- // AUTO-ATTACHED (workspace-default) capability servers are marked optional:
52
- // one of them having a broken/expired credential must SKIP that server, not
53
- // fail the whole turn before the model runs. The caller only reaches here
54
- // when the request omitted `tools`; an explicit list is never defaulted.
55
- .map((server) => ({ kind: "mcp", id: server.id, optional: true }));
52
+ return (
53
+ runtimeSettings.mcpServers
54
+ .filter((server) => !configuredIds.has(server.id))
55
+ // AUTO-ATTACHED (workspace-default) capability servers are marked optional:
56
+ // one of them having a broken/expired credential must SKIP that server, not
57
+ // fail the whole turn before the model runs. The caller only reaches here
58
+ // when the request omitted `tools`; an explicit list is never defaulted.
59
+ .map((server) => ({ kind: "mcp", id: server.id, optional: true }))
60
+ );
56
61
  }
57
62
 
58
- export function withDefaultEnabledCapabilityMcpTools(tools: ToolRef[], settings: McpSettings, runtimeSettings: McpSettings): ToolRef[] {
63
+ export function withDefaultEnabledCapabilityMcpTools(
64
+ tools: ToolRef[],
65
+ settings: McpSettings,
66
+ runtimeSettings: McpSettings,
67
+ ): ToolRef[] {
59
68
  return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
60
69
  }
61
70
 
@@ -91,14 +100,25 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
91
100
  ref: resource.ref.trim(),
92
101
  mountPath,
93
102
  ...(resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {}),
94
- ...(resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {}),
103
+ ...(resource.provider ? { provider: resource.provider } : {}),
104
+ ...(resource.repositoryId !== undefined ? { repositoryId: resource.repositoryId } : {}),
105
+ ...(resource.installationId !== undefined
106
+ ? { installationId: resource.installationId }
107
+ : {}),
108
+ ...(resource.projectId !== undefined ? { projectId: resource.projectId } : {}),
109
+ ...(resource.connectionId ? { connectionId: resource.connectionId } : {}),
110
+ ...(resource.githubInstallationId
111
+ ? { githubInstallationId: resource.githubInstallationId }
112
+ : {}),
95
113
  ...(resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}),
96
114
  };
97
115
  }
98
116
  const key = stableJson(normalized);
99
117
  const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : undefined;
100
118
  if (mounted && mounted !== key) {
101
- throw new HTTPException(422, { message: `duplicate resource mount path: ${normalized.mountPath}` });
119
+ throw new HTTPException(422, {
120
+ message: `duplicate resource mount path: ${normalized.mountPath}`,
121
+ });
102
122
  }
103
123
  if (normalized.mountPath) {
104
124
  mountPaths.set(normalized.mountPath, key);
@@ -106,7 +126,9 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
106
126
  const identity = resourceIdentityKey(normalized);
107
127
  const seenIdentity = identities.get(identity);
108
128
  if (seenIdentity && seenIdentity !== key) {
109
- throw new HTTPException(422, { message: `duplicate resource with different settings: ${identity}` });
129
+ throw new HTTPException(422, {
130
+ message: `duplicate resource with different settings: ${identity}`,
131
+ });
110
132
  }
111
133
  identities.set(identity, key);
112
134
  if (!seenResources.has(key)) {
@@ -117,7 +139,10 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
117
139
  return out;
118
140
  }
119
141
 
120
- export function mergeResourceRefs(existing: ResourceRef[], additions: ResourceRef[]): ResourceRef[] {
142
+ export function mergeResourceRefs(
143
+ existing: ResourceRef[],
144
+ additions: ResourceRef[],
145
+ ): ResourceRef[] {
121
146
  try {
122
147
  return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
123
148
  } catch (error) {
@@ -133,8 +158,12 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
133
158
  if (resource.kind !== "repository") {
134
159
  return [];
135
160
  }
136
- const installationRaw = resource.githubInstallationId;
137
- const repositoryRaw = resource.githubRepositoryId;
161
+ const installationRaw =
162
+ resource.githubInstallationId ??
163
+ (resource.provider === "github" ? resource.installationId : undefined);
164
+ const repositoryRaw =
165
+ resource.githubRepositoryId ??
166
+ (resource.provider === "github" ? resource.repositoryId : undefined);
138
167
  if (installationRaw === null && repositoryRaw === null) {
139
168
  return [];
140
169
  }
@@ -145,7 +174,8 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
145
174
  const repositoryId = positiveInteger(repositoryRaw);
146
175
  if (!installationId || !repositoryId) {
147
176
  throw new HTTPException(422, {
148
- message: "GitHub App repository resources require positive github_installation_id and github_repository_id",
177
+ message:
178
+ "GitHub App repository resources require positive github_installation_id and github_repository_id",
149
179
  });
150
180
  }
151
181
  return [{ installationId, repositoryId }];
@@ -162,20 +192,31 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
162
192
  return installationId;
163
193
  }
164
194
 
165
- export async function validateGitHubRepositorySelection(db: Database, workspaceId: string, resources: ResourceRef[]): Promise<void> {
195
+ export async function validateGitHubRepositorySelection(
196
+ db: Database,
197
+ workspaceId: string,
198
+ resources: ResourceRef[],
199
+ ): Promise<void> {
166
200
  const installationId = validateGitHubRepositorySelectionShape(resources);
167
201
  if (installationId === null) {
168
202
  return;
169
203
  }
170
- const linkedInstallationIds = new Set(await listGitHubInstallationIdsForWorkspace(db, workspaceId));
204
+ const linkedInstallationIds = new Set(
205
+ await listGitHubInstallationIdsForWorkspace(db, workspaceId),
206
+ );
171
207
  if (!linkedInstallationIds.has(installationId)) {
172
208
  throw new HTTPException(422, {
173
- message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace",
209
+ message:
210
+ "GitHub App repository resources must belong to a GitHub App installation linked to this workspace",
174
211
  });
175
212
  }
176
213
  }
177
214
 
178
- export async function validateFileResources(db: Database, workspaceId: string, resources: ResourceRef[]): Promise<void> {
215
+ export async function validateFileResources(
216
+ db: Database,
217
+ workspaceId: string,
218
+ resources: ResourceRef[],
219
+ ): Promise<void> {
179
220
  const fileIds = new Set<string>();
180
221
  for (const resource of resources) {
181
222
  if (resource.kind !== "file") {
@@ -190,7 +231,9 @@ export async function validateFileResources(db: Database, workspaceId: string, r
190
231
  throw new HTTPException(422, { message: `unknown file resource: ${resource.fileId}` });
191
232
  }
192
233
  if (file.status !== "ready") {
193
- throw new HTTPException(422, { message: `file resource ${resource.fileId} is ${file.status}` });
234
+ throw new HTTPException(422, {
235
+ message: `file resource ${resource.fileId} is ${file.status}`,
236
+ });
194
237
  }
195
238
  }
196
239
  }