@opengeni/core 0.17.1 → 0.18.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.17.1",
3
+ "version": "0.18.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": {
@@ -35,14 +35,14 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "@opengeni/codex": "^0.2.9",
38
- "@opengeni/config": "^0.9.3",
39
- "@opengeni/contracts": "^0.30.0",
40
- "@opengeni/db": "^0.21.0",
41
- "@opengeni/documents": "^0.2.66",
42
- "@opengeni/events": "^0.3.57",
43
- "@opengeni/observability": "^0.4.2",
44
- "@opengeni/runtime": "^0.16.2",
45
- "@opengeni/storage": "^0.2.52",
38
+ "@opengeni/config": "^0.10.0",
39
+ "@opengeni/contracts": "^0.31.0",
40
+ "@opengeni/db": "^0.22.1",
41
+ "@opengeni/documents": "^0.2.69",
42
+ "@opengeni/events": "^0.3.59",
43
+ "@opengeni/observability": "^0.4.3",
44
+ "@opengeni/runtime": "^0.17.0",
45
+ "@opengeni/storage": "^0.2.53",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -898,6 +898,19 @@ function packCatalogItem(
898
898
  pack: ReturnType<typeof listCapabilityPacks>[number],
899
899
  source: "built_in" | "manual",
900
900
  ): CapabilityCatalogItem {
901
+ const customMetadata = { ...pack.metadata };
902
+ for (const key of [
903
+ "packId",
904
+ "version",
905
+ "connectors",
906
+ "knowledge",
907
+ "scheduledTaskTemplates",
908
+ "sandboxImage",
909
+ "sandboxProviderImages",
910
+ "skills",
911
+ ]) {
912
+ delete customMetadata[key];
913
+ }
901
914
  return CapabilityCatalogItem.parse({
902
915
  id: `pack:${pack.id}`,
903
916
  kind: "pack",
@@ -912,6 +925,7 @@ function packCatalogItem(
912
925
  notes: "Enables role-scoped tools, connectors, knowledge, and scheduled-task templates.",
913
926
  },
914
927
  metadata: {
928
+ ...customMetadata,
915
929
  packId: pack.id,
916
930
  version: pack.version,
917
931
  connectors: pack.connectors,
@@ -919,8 +933,8 @@ function packCatalogItem(
919
933
  scheduledTaskTemplates: pack.scheduledTaskTemplates,
920
934
  // Runtime composition surface only: skill names, never file content.
921
935
  ...(pack.sandboxImage ? { sandboxImage: pack.sandboxImage } : {}),
936
+ ...(pack.sandboxProviderImages ? { sandboxProviderImages: pack.sandboxProviderImages } : {}),
922
937
  ...(pack.skills.length > 0 ? { skills: pack.skills.map((skill) => skill.name) } : {}),
923
- ...pack.metadata,
924
938
  },
925
939
  });
926
940
  }
@@ -0,0 +1,448 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ redactSensitiveText,
4
+ stableJson,
5
+ type KnowledgeMemoryKind,
6
+ type KnowledgeMemoryStatus,
7
+ } from "@opengeni/contracts";
8
+
9
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
10
+ const SELECTOR_SEGMENT_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
11
+ const encoder = new TextEncoder();
12
+ const decoder = new TextDecoder();
13
+
14
+ export const MEMORY_SLACK_PROJECTION_VERSION = 1;
15
+ export const MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES = 512;
16
+ export const MEMORY_SLACK_NAMESPACE_MAX_UTF8_BYTES = 128;
17
+ export const MEMORY_SLACK_LABEL_MAX_UTF8_BYTES = 64;
18
+ export const MEMORY_SLACK_LABEL_MAX_COUNT = 8;
19
+ export const MEMORY_SLACK_OWNER_MAX_UTF8_BYTES = 96;
20
+ export const MEMORY_SLACK_PROJECTION_MAX_UTF8_BYTES = 4096;
21
+
22
+ export type MemorySlackImportance = "major" | "normal" | "minor";
23
+ export type MemorySlackRequestedMode = "auto" | "review" | "never";
24
+ export type MemorySlackDeliveryMode = Exclude<MemorySlackRequestedMode, "never">;
25
+ export type MemorySlackChangeKind = "created" | "corrected" | "superseded";
26
+ export type MemorySlackOrigin = "native" | "slack_derived";
27
+ export type MemorySlackAudience = "workspace" | "restricted";
28
+
29
+ export type MemorySlackPublicationPolicy = {
30
+ enabled: boolean;
31
+ autoImportances: readonly MemorySlackImportance[];
32
+ reviewImportances: readonly MemorySlackImportance[];
33
+ };
34
+
35
+ /**
36
+ * Nothing publishes without an explicit workspace-admin enablement. When
37
+ * enabled without tuning, major changes may publish automatically, normal
38
+ * changes require review, and minor changes stay quiet.
39
+ */
40
+ export const DEFAULT_MEMORY_SLACK_PUBLICATION_POLICY = {
41
+ enabled: false,
42
+ autoImportances: ["major"],
43
+ reviewImportances: ["normal"],
44
+ } as const satisfies MemorySlackPublicationPolicy;
45
+
46
+ export type MemorySlackPublicationInput = {
47
+ context: {
48
+ accountId: string;
49
+ workspaceId: string;
50
+ now: string;
51
+ };
52
+ policy?: MemorySlackPublicationPolicy | undefined;
53
+ memory: {
54
+ sourceType: "workspace_memory";
55
+ accountId: string;
56
+ workspaceId: string;
57
+ id: string;
58
+ version: number;
59
+ status: KnowledgeMemoryStatus;
60
+ kind: KnowledgeMemoryKind;
61
+ scopeType: "workspace" | "user" | "role" | "session" | "ephemeral" | "legacy";
62
+ namespace: string;
63
+ labels: readonly string[];
64
+ validFrom: string;
65
+ validUntil: string | null;
66
+ supersedesId: string | null;
67
+ supersededById: string | null;
68
+ };
69
+ change: {
70
+ kind: MemorySlackChangeKind;
71
+ relatedMemoryId: string | null;
72
+ occurredAt: string;
73
+ origin: MemorySlackOrigin;
74
+ ownerLabel?: string | null | undefined;
75
+ };
76
+ distribution: {
77
+ importance: MemorySlackImportance;
78
+ audience: MemorySlackAudience;
79
+ slackMode: MemorySlackRequestedMode;
80
+ shareSummary: string;
81
+ };
82
+ };
83
+
84
+ export type MemorySlackPublicationDenialReason =
85
+ | "disabled"
86
+ | "invalid_input"
87
+ | "unsupported_source"
88
+ | "tenant_mismatch"
89
+ | "not_decision"
90
+ | "status_ineligible"
91
+ | "restricted_scope"
92
+ | "restricted_audience"
93
+ | "not_yet_valid"
94
+ | "expired"
95
+ | "slack_origin_loop"
96
+ | "mode_never"
97
+ | "below_noise_policy"
98
+ | "missing_summary"
99
+ | "invalid_change_lineage";
100
+
101
+ export type MemorySlackPublicationProjection = {
102
+ version: typeof MEMORY_SLACK_PROJECTION_VERSION;
103
+ workspaceId: string;
104
+ memoryId: string;
105
+ memoryVersion: number;
106
+ changeKind: MemorySlackChangeKind;
107
+ relatedMemoryId: string | null;
108
+ occurredAt: string;
109
+ importance: MemorySlackImportance;
110
+ deliveryMode: MemorySlackDeliveryMode;
111
+ summary: string;
112
+ summaryRedacted: boolean;
113
+ summaryTruncated: boolean;
114
+ namespace: string;
115
+ labels: string[];
116
+ labelsTruncated: boolean;
117
+ ownerLabel: string | null;
118
+ ownerLabelRedacted: boolean;
119
+ ownerLabelTruncated: boolean;
120
+ authoritativeRecord: {
121
+ workspaceId: string;
122
+ memoryId: string;
123
+ };
124
+ };
125
+
126
+ export type MemorySlackPublicationDecision =
127
+ | {
128
+ eligible: false;
129
+ reason: MemorySlackPublicationDenialReason;
130
+ }
131
+ | {
132
+ eligible: true;
133
+ deliveryMode: MemorySlackDeliveryMode;
134
+ idempotencyKey: string;
135
+ projection: MemorySlackPublicationProjection;
136
+ };
137
+
138
+ const IMPORTANCES = new Set<MemorySlackImportance>(["major", "normal", "minor"]);
139
+ const REQUESTED_MODES = new Set<MemorySlackRequestedMode>(["auto", "review", "never"]);
140
+ const CHANGE_KINDS = new Set<MemorySlackChangeKind>(["created", "corrected", "superseded"]);
141
+ const ORIGINS = new Set<MemorySlackOrigin>(["native", "slack_derived"]);
142
+ const AUDIENCES = new Set<MemorySlackAudience>(["workspace", "restricted"]);
143
+ const SCOPE_TYPES = new Set<MemorySlackPublicationInput["memory"]["scopeType"]>([
144
+ "workspace",
145
+ "user",
146
+ "role",
147
+ "session",
148
+ "ephemeral",
149
+ "legacy",
150
+ ]);
151
+ const MEMORY_STATUSES = new Set<KnowledgeMemoryStatus>([
152
+ "proposed",
153
+ "approved",
154
+ "rejected",
155
+ "active",
156
+ "superseded",
157
+ "archived",
158
+ ]);
159
+ const MEMORY_KINDS = new Set<KnowledgeMemoryKind>([
160
+ "semantic",
161
+ "episodic",
162
+ "procedural",
163
+ "decision",
164
+ "preference",
165
+ ]);
166
+ const VISIBLE_STATUSES = new Set<KnowledgeMemoryStatus>(["active", "approved"]);
167
+
168
+ /**
169
+ * Produce the only data shape a later delivery slice may persist or
170
+ * render. This is a deterministic policy/security boundary, not a persistence
171
+ * hook: callers must durably bind the returned projection and key in a later
172
+ * transaction/outbox slice.
173
+ */
174
+ export function evaluateMemorySlackPublication(
175
+ input: MemorySlackPublicationInput,
176
+ ): MemorySlackPublicationDecision {
177
+ const policy = input.policy ?? DEFAULT_MEMORY_SLACK_PUBLICATION_POLICY;
178
+ if (!validPolicy(policy)) return denied("invalid_input");
179
+ if (!policy.enabled) return denied("disabled");
180
+
181
+ if (input.memory.sourceType !== "workspace_memory") return denied("unsupported_source");
182
+ if (!validIdentity(input)) return denied("invalid_input");
183
+ if (
184
+ input.memory.accountId !== input.context.accountId ||
185
+ input.memory.workspaceId !== input.context.workspaceId
186
+ ) {
187
+ return denied("tenant_mismatch");
188
+ }
189
+ if (input.memory.kind !== "decision") return denied("not_decision");
190
+ if (input.memory.scopeType !== "workspace") return denied("restricted_scope");
191
+ if (input.distribution.audience !== "workspace") return denied("restricted_audience");
192
+ if (input.change.origin === "slack_derived") return denied("slack_origin_loop");
193
+ if (input.distribution.slackMode === "never") return denied("mode_never");
194
+
195
+ const timeDecision = evaluateValidity(input);
196
+ if (timeDecision) return denied(timeDecision);
197
+ if (!statusEligibleForChange(input)) return denied("status_ineligible");
198
+ if (!validChangeLineage(input)) return denied("invalid_change_lineage");
199
+
200
+ const deliveryMode = effectiveDeliveryMode(policy, input.distribution);
201
+ if (!deliveryMode) return denied("below_noise_policy");
202
+
203
+ const collapsedSummary = collapseText(input.distribution.shareSummary);
204
+ const redactedSummary = redactSensitiveText(collapsedSummary);
205
+ if (!redactedSummary) return denied("missing_summary");
206
+ const summary = truncateUtf8(redactedSummary, MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES);
207
+
208
+ const namespace = normalizeNamespace(input.memory.namespace);
209
+ const labels = normalizeLabels(input.memory.labels);
210
+ if (!namespace || !labels) return denied("invalid_input");
211
+
212
+ const owner = boundedOptionalText(input.change.ownerLabel, MEMORY_SLACK_OWNER_MAX_UTF8_BYTES);
213
+ const projection: MemorySlackPublicationProjection = {
214
+ version: MEMORY_SLACK_PROJECTION_VERSION,
215
+ workspaceId: input.context.workspaceId,
216
+ memoryId: input.memory.id,
217
+ memoryVersion: input.memory.version,
218
+ changeKind: input.change.kind,
219
+ relatedMemoryId: input.change.relatedMemoryId,
220
+ occurredAt: new Date(input.change.occurredAt).toISOString(),
221
+ importance: input.distribution.importance,
222
+ deliveryMode,
223
+ summary: summary.value,
224
+ summaryRedacted: redactedSummary !== collapsedSummary,
225
+ summaryTruncated: summary.truncated,
226
+ namespace,
227
+ labels: labels.values,
228
+ labelsTruncated: labels.truncated,
229
+ ownerLabel: owner.value,
230
+ ownerLabelRedacted: owner.redacted,
231
+ ownerLabelTruncated: owner.truncated,
232
+ authoritativeRecord: {
233
+ workspaceId: input.context.workspaceId,
234
+ memoryId: input.memory.id,
235
+ },
236
+ };
237
+
238
+ if (utf8Bytes(stableJson(projection)) > MEMORY_SLACK_PROJECTION_MAX_UTF8_BYTES) {
239
+ return denied("invalid_input");
240
+ }
241
+
242
+ const digest = createHash("sha256").update(stableJson(projection), "utf8").digest("hex");
243
+ return {
244
+ eligible: true,
245
+ deliveryMode,
246
+ idempotencyKey: `memory-slack:v${MEMORY_SLACK_PROJECTION_VERSION}:${digest}`,
247
+ projection,
248
+ };
249
+ }
250
+
251
+ function validPolicy(policy: MemorySlackPublicationPolicy): boolean {
252
+ return (
253
+ typeof policy.enabled === "boolean" &&
254
+ Array.isArray(policy.autoImportances) &&
255
+ Array.isArray(policy.reviewImportances) &&
256
+ policy.autoImportances.every((importance) => IMPORTANCES.has(importance)) &&
257
+ policy.reviewImportances.every((importance) => IMPORTANCES.has(importance))
258
+ );
259
+ }
260
+
261
+ function validIdentity(input: MemorySlackPublicationInput): boolean {
262
+ const uuids = [
263
+ input.context.accountId,
264
+ input.context.workspaceId,
265
+ input.memory.accountId,
266
+ input.memory.workspaceId,
267
+ input.memory.id,
268
+ input.memory.supersedesId,
269
+ input.memory.supersededById,
270
+ input.change.relatedMemoryId,
271
+ ].filter((value): value is string => value !== null);
272
+ return (
273
+ uuids.every((value) => UUID_PATTERN.test(value)) &&
274
+ Number.isSafeInteger(input.memory.version) &&
275
+ input.memory.version > 0 &&
276
+ typeof input.memory.namespace === "string" &&
277
+ Array.isArray(input.memory.labels) &&
278
+ input.memory.labels.every((label) => typeof label === "string") &&
279
+ (input.change.ownerLabel === undefined ||
280
+ input.change.ownerLabel === null ||
281
+ typeof input.change.ownerLabel === "string") &&
282
+ typeof input.distribution.shareSummary === "string" &&
283
+ IMPORTANCES.has(input.distribution.importance) &&
284
+ REQUESTED_MODES.has(input.distribution.slackMode) &&
285
+ CHANGE_KINDS.has(input.change.kind) &&
286
+ ORIGINS.has(input.change.origin) &&
287
+ AUDIENCES.has(input.distribution.audience) &&
288
+ SCOPE_TYPES.has(input.memory.scopeType) &&
289
+ MEMORY_STATUSES.has(input.memory.status) &&
290
+ MEMORY_KINDS.has(input.memory.kind)
291
+ );
292
+ }
293
+
294
+ function evaluateValidity(
295
+ input: MemorySlackPublicationInput,
296
+ ): "invalid_input" | "not_yet_valid" | "expired" | null {
297
+ const now = Date.parse(input.context.now);
298
+ const occurredAt = Date.parse(input.change.occurredAt);
299
+ const validFrom = Date.parse(input.memory.validFrom);
300
+ const validUntil = input.memory.validUntil === null ? null : Date.parse(input.memory.validUntil);
301
+ if (
302
+ !Number.isFinite(now) ||
303
+ !Number.isFinite(occurredAt) ||
304
+ !Number.isFinite(validFrom) ||
305
+ (validUntil !== null && !Number.isFinite(validUntil)) ||
306
+ (validUntil !== null && validUntil <= validFrom)
307
+ ) {
308
+ return "invalid_input";
309
+ }
310
+ if (now < validFrom) return "not_yet_valid";
311
+ if (validUntil !== null && now >= validUntil) return "expired";
312
+ return null;
313
+ }
314
+
315
+ function validChangeLineage(input: MemorySlackPublicationInput): boolean {
316
+ if (
317
+ input.memory.supersedesId === input.memory.id ||
318
+ input.memory.supersededById === input.memory.id ||
319
+ input.change.relatedMemoryId === input.memory.id
320
+ ) {
321
+ return false;
322
+ }
323
+
324
+ switch (input.change.kind) {
325
+ case "created":
326
+ return (
327
+ input.change.relatedMemoryId === null &&
328
+ input.memory.supersedesId === null &&
329
+ input.memory.supersededById === null
330
+ );
331
+ case "corrected":
332
+ return (
333
+ input.change.relatedMemoryId !== null &&
334
+ input.change.relatedMemoryId !== input.memory.id &&
335
+ input.memory.supersedesId === input.change.relatedMemoryId &&
336
+ input.memory.supersededById === null
337
+ );
338
+ case "superseded":
339
+ return (
340
+ input.change.relatedMemoryId !== null &&
341
+ input.change.relatedMemoryId !== input.memory.id &&
342
+ input.memory.supersededById === input.change.relatedMemoryId
343
+ );
344
+ }
345
+ }
346
+
347
+ function statusEligibleForChange(input: MemorySlackPublicationInput): boolean {
348
+ return input.change.kind === "superseded"
349
+ ? input.memory.status === "superseded"
350
+ : VISIBLE_STATUSES.has(input.memory.status);
351
+ }
352
+
353
+ function effectiveDeliveryMode(
354
+ policy: MemorySlackPublicationPolicy,
355
+ distribution: MemorySlackPublicationInput["distribution"],
356
+ ): MemorySlackDeliveryMode | null {
357
+ const auto = new Set(policy.autoImportances);
358
+ const review = new Set(policy.reviewImportances);
359
+ if (distribution.slackMode === "review") {
360
+ return auto.has(distribution.importance) || review.has(distribution.importance)
361
+ ? "review"
362
+ : null;
363
+ }
364
+ if (auto.has(distribution.importance)) return "auto";
365
+ if (review.has(distribution.importance)) return "review";
366
+ return null;
367
+ }
368
+
369
+ function normalizeNamespace(value: string): string | null {
370
+ const trimmed = value.trim();
371
+ if (redactSensitiveText(trimmed) !== trimmed) return null;
372
+ const namespace = trimmed.toLowerCase();
373
+ if (!namespace || utf8Bytes(namespace) > MEMORY_SLACK_NAMESPACE_MAX_UTF8_BYTES) return null;
374
+ if (redactSensitiveText(namespace) !== namespace) return null;
375
+ const segments = namespace.split("/");
376
+ if (segments.some((segment) => !SELECTOR_SEGMENT_PATTERN.test(segment))) return null;
377
+ return segments.join("/");
378
+ }
379
+
380
+ function normalizeLabels(
381
+ values: readonly string[],
382
+ ): { values: string[]; truncated: boolean } | null {
383
+ if (!Array.isArray(values)) return null;
384
+ const labels = new Set<string>();
385
+ for (const value of values) {
386
+ if (typeof value !== "string") return null;
387
+ const trimmed = value.trim();
388
+ if (redactSensitiveText(trimmed) !== trimmed) return null;
389
+ const label = trimmed.toLowerCase();
390
+ if (
391
+ redactSensitiveText(label) !== label ||
392
+ !SELECTOR_SEGMENT_PATTERN.test(label) ||
393
+ utf8Bytes(label) > MEMORY_SLACK_LABEL_MAX_UTF8_BYTES
394
+ ) {
395
+ return null;
396
+ }
397
+ labels.add(label);
398
+ }
399
+ const sorted = [...labels].sort();
400
+ return {
401
+ values: sorted.slice(0, MEMORY_SLACK_LABEL_MAX_COUNT),
402
+ truncated: sorted.length > MEMORY_SLACK_LABEL_MAX_COUNT,
403
+ };
404
+ }
405
+
406
+ function boundedOptionalText(
407
+ value: string | null | undefined,
408
+ maxBytes: number,
409
+ ): { value: string | null; redacted: boolean; truncated: boolean } {
410
+ const collapsed = collapseText(value ?? "");
411
+ if (!collapsed) return { value: null, redacted: false, truncated: false };
412
+ const redacted = redactSensitiveText(collapsed);
413
+ const bounded = truncateUtf8(redacted, maxBytes);
414
+ return {
415
+ value: bounded.value || null,
416
+ redacted: redacted !== collapsed,
417
+ truncated: bounded.truncated,
418
+ };
419
+ }
420
+
421
+ function collapseText(value: string): string {
422
+ // eslint-disable-next-line no-control-regex
423
+ return value
424
+ .replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
425
+ .replace(/\s+/g, " ")
426
+ .trim();
427
+ }
428
+
429
+ function truncateUtf8(value: string, maxBytes: number): { value: string; truncated: boolean } {
430
+ const bytes = encoder.encode(value);
431
+ if (bytes.byteLength <= maxBytes) return { value, truncated: false };
432
+ const marker = "…";
433
+ const markerBytes = encoder.encode(marker);
434
+ let end = Math.max(0, maxBytes - markerBytes.byteLength);
435
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
436
+ return {
437
+ value: `${decoder.decode(bytes.subarray(0, end)).trimEnd()}${marker}`,
438
+ truncated: true,
439
+ };
440
+ }
441
+
442
+ function utf8Bytes(value: string): number {
443
+ return encoder.encode(value).byteLength;
444
+ }
445
+
446
+ function denied(reason: MemorySlackPublicationDenialReason): MemorySlackPublicationDecision {
447
+ return { eligible: false, reason };
448
+ }
package/src/index.ts CHANGED
@@ -63,6 +63,7 @@ export * from "./domain/session-tool-policy";
63
63
  export * from "./domain/scheduled-tasks";
64
64
  export * from "./domain/sessions";
65
65
  export * from "./domain/insights";
66
+ export * from "./domain/memory-slack-publication";
66
67
  export * from "./domain/slack-bot";
67
68
  export * from "./domain/workspace-members";
68
69
  export * from "./application/new-session-drafts";