@sema-agent/server 1.294.0 → 1.296.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/dist/config.d.ts CHANGED
@@ -162,6 +162,8 @@ export interface ServiceConfig {
162
162
  keyPrefix?: string;
163
163
  presignTtlSec?: number;
164
164
  };
165
+ snapshotBlobSqlMaxBytes?: number;
166
+ snapshotBlobAllowSql: boolean;
165
167
  sendUserFile?: {
166
168
  endpoint: string;
167
169
  publicEndpoint?: string;
package/dist/config.js CHANGED
@@ -536,6 +536,8 @@ export function loadConfig() {
536
536
  ...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
537
537
  }
538
538
  : undefined,
539
+ snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
540
+ snapshotBlobAllowSql: process.env.SNAPSHOT_BLOB_ALLOW_SQL_BYTES === "true",
539
541
  sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
540
542
  ? {
541
543
  endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
@@ -63,6 +63,7 @@ export interface ServiceDeps {
63
63
  approvalExemptionStore?: ApprovalExemptionStore;
64
64
  sessionPolicyStore?: ServiceSessionPolicyStore;
65
65
  fileSnapshotStore?: ServiceFileSnapshotStore;
66
+ snapshotBlobSqlCapBytes?: number;
66
67
  backend?: StoreBackend;
67
68
  sessionMirrorRuling?: (principal: string | undefined) => Promise<SessionMirrorRuling | undefined>;
68
69
  approvalStore?: ApprovalStore;
@@ -668,6 +668,9 @@ export function createHttpServer(deps) {
668
668
  approvals: Boolean(deps.checkpointStore),
669
669
  sessions: Boolean(deps.sessionAudit),
670
670
  sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
671
+ fleet: deps.fleetBus
672
+ ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory" }
673
+ : false,
671
674
  leader: Boolean(deps.leaderEndpoint),
672
675
  memory: false,
673
676
  memoryWrite: false,
@@ -4062,6 +4065,13 @@ export function createHttpServer(deps) {
4062
4065
  return;
4063
4066
  }
4064
4067
  const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
4068
+ if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
4069
+ sendJson(res, 413, {
4070
+ code: "blob_too_large_for_sql",
4071
+ error: `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes; mysql-protocol packet limit) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs`,
4072
+ });
4073
+ return;
4074
+ }
4065
4075
  if (createHash("sha256").update(body).digest("hex") !== hash) {
4066
4076
  sendJson(res, 400, { error: "blob hash mismatch" });
4067
4077
  return;
package/dist/main.js CHANGED
@@ -28,7 +28,7 @@ import { loadConfig, logConfigDiagnostics } from "./config.js";
28
28
  import { resourceSuspendOptIn } from "./resource-suspend.js";
29
29
  import { createSessionStore, ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
30
30
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
31
- import { createStoreBackend } from "./plugins/store-backend.js";
31
+ import { createStoreBackend, assertCloudSnapshotBlobPosture } from "./plugins/store-backend.js";
32
32
  import { e2bExecutionEnvFactory } from "./plugins/remote-env-e2b.js";
33
33
  import { k8sExecutionEnvFactory } from "./plugins/remote-env-k8s.js";
34
34
  import { sshExecutionEnvFactory } from "./plugins/remote-env-ssh.js";
@@ -80,7 +80,7 @@ import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "./config
80
80
  import { TiDBRosterStore, PgRosterStore, ensureTiDBRosterSchema, ensurePgRosterSchema } from "./plugins/roster-store-sql.js";
81
81
  import { TiDBTaskAttachmentStore, PgTaskAttachmentStore, ensureTiDBTaskAttachmentSchema, ensurePgTaskAttachmentSchema, bindAttachmentsForTask, materializeAttachmentsInto } from "./plugins/task-attachment-store.js";
82
82
  import { LocalTaskAttachmentStore } from "./plugins/local-task-attachment-store.js";
83
- import { MinioBlobBackend } from "./plugins/blob-backend.js";
83
+ import { MinioBlobBackend, SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
84
84
  import { TiDBBackgroundAgentStore, PgBackgroundAgentStore, ensureTiDBBackgroundAgentSchema, ensurePgBackgroundAgentSchema } from "./plugins/background-agent-store-sql.js";
85
85
  import { TiDBMailboxStore, PgMailboxStore, ensureTiDBMailboxSchema, ensurePgMailboxSchema } from "./plugins/mailbox-store-sql.js";
86
86
  import { createPrincipalCapsClient, gateExecutionLane, scopedTokenNeedsWorker, applyObserverEnvOptIn } from "./runtime-caps-resolver.js";
@@ -634,10 +634,14 @@ async function main() {
634
634
  "the in-memory store cannot enforce session ownership.");
635
635
  }
636
636
  if (config.requirePrincipal && backend?.kind === "local") {
637
- throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local in-memory backend (DB_BACKEND=local): its owner map is " +
638
- "process-local and lost on restart, so it cannot durably enforce multi-tenant session ownership. Use a DB " +
639
- "backend (DB_BACKEND=tidb|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false.");
637
+ throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
638
+ "durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 durable " +
639
+ "owners = P1), so it cannot durably enforce multi-tenant session ownership. Use a SQL backend " +
640
+ "(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
641
+ "still inject x-agent-principal per request for memory scoping and audit attribution).");
640
642
  }
643
+ if (backend)
644
+ assertCloudSnapshotBlobPosture(backend.kind, config);
641
645
  if (!config.requirePrincipal && sessionStore.ownerOf) {
642
646
  logger.warn("principal_optional", {
643
647
  note: "owner-aware session store with REQUIRE_PRINCIPAL=false — owned sessions are protected, but headerless callers can create/share anonymous sessions; set REQUIRE_PRINCIPAL=true for multi-tenant",
@@ -1046,6 +1050,9 @@ async function main() {
1046
1050
  ...(sessionPolicyStore ? { sessionPolicyStore } : {}),
1047
1051
  ...(runtimeCapsResolver ? { runtimeCapsResolver } : {}),
1048
1052
  ...(fileSnapshotStore ? { fileSnapshotStore } : {}),
1053
+ ...(backend && backend.kind !== "local" && !config.snapshotBlobStore
1054
+ ? ((cap) => (cap !== undefined ? { snapshotBlobSqlCapBytes: cap } : {}))(config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
1055
+ : {}),
1049
1056
  ...(executionEnvFactory ? { executionEnvFactory } : {}),
1050
1057
  ...(lspManager ? { lspManager } : {}),
1051
1058
  onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
@@ -1,5 +1,10 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
+ export declare const SQL_BLOB_DEFAULT_MAX_BYTES = 6291456;
4
+ export declare class BlobTooLargeError extends Error {
5
+ readonly code: "blob_too_large_for_sql";
6
+ constructor(byteLen: number, capBytes: number);
7
+ }
3
8
  export interface BlobBackend {
4
9
  putBlob(hash: string, bytes: Uint8Array): Promise<void>;
5
10
  getBlob(hash: string): Promise<Uint8Array | undefined>;
@@ -9,7 +14,8 @@ export interface BlobBackend {
9
14
  export declare class SqlBlobBackend implements BlobBackend {
10
15
  private readonly dialect;
11
16
  private readonly pool;
12
- constructor(dialect: "tidb" | "pg", pool: MysqlPool | PgPool);
17
+ private readonly capBytes;
18
+ constructor(dialect: "tidb" | "pg", pool: MysqlPool | PgPool, maxBytes?: number);
13
19
  putBlob(hash: string, bytes: Uint8Array): Promise<void>;
14
20
  getBlob(hash: string): Promise<Uint8Array | undefined>;
15
21
  hasBlobs(hashes: string[]): Promise<Set<string>>;
@@ -1,6 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { presignS3Url } from "./s3-presign.js";
3
3
  const BLOB_GC_GRACE_MS = 3_600_000;
4
+ export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
5
+ export class BlobTooLargeError extends Error {
6
+ code = "blob_too_large_for_sql";
7
+ constructor(byteLen, capBytes) {
8
+ super(`blob of ${byteLen} bytes exceeds the SQL snapshot-blob cap (${capBytes} bytes): a single-row INSERT this large ` +
9
+ `hits TiDB's txn-entry-size-limit (default 6MiB) or the mysql-protocol max_allowed_packet (~2x text-protocol ` +
10
+ `inflation). Configure object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot ` +
11
+ `blobs, or override the cap with SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment raised those limits.`);
12
+ this.name = "BlobTooLargeError";
13
+ }
14
+ }
4
15
  const MINIO_OP_CONCURRENCY = 16;
5
16
  async function mapBounded(items, limit, fn) {
6
17
  let i = 0;
@@ -15,11 +26,15 @@ async function mapBounded(items, limit, fn) {
15
26
  export class SqlBlobBackend {
16
27
  dialect;
17
28
  pool;
18
- constructor(dialect, pool) {
29
+ capBytes;
30
+ constructor(dialect, pool, maxBytes) {
19
31
  this.dialect = dialect;
20
32
  this.pool = pool;
33
+ this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
21
34
  }
22
35
  async putBlob(hash, bytes) {
36
+ if (bytes.byteLength > this.capBytes)
37
+ throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
23
38
  if (this.dialect === "tidb") {
24
39
  await this.pool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)", [hash, bytes.byteLength, Buffer.from(bytes), new Date()]);
25
40
  }
@@ -93,6 +93,7 @@ export interface StoreBackend {
93
93
  pgPool(): PgPool | undefined;
94
94
  dbNowMs?(): Promise<number>;
95
95
  }
96
+ export declare function assertCloudSnapshotBlobPosture(kind: "mysql" | "pg" | "local", config: Pick<ServiceConfig, "snapshotBlobStore" | "snapshotBlobAllowSql">): void;
96
97
  export declare function snapshotBoundsFromConfig(config: ServiceConfig): FileSnapshotBounds;
97
98
  export declare function createStoreBackend(config: ServiceConfig): StoreBackend | undefined;
98
99
  //# sourceMappingURL=store-backend.d.ts.map
@@ -21,7 +21,7 @@ import { TiDBWorkflowJournalStore } from "./tidb-workflow-journal-store.js";
21
21
  import { PgWorkflowJournalStore } from "./pg-workflow-journal-store.js";
22
22
  import { TiDBFileSnapshotStore } from "./tidb-file-snapshot-store.js";
23
23
  import { PgFileSnapshotStore } from "./pg-file-snapshot-store.js";
24
- import { MinioBlobBackend } from "./blob-backend.js";
24
+ import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
25
25
  import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
26
26
  import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
27
27
  import { TiDBRunStore } from "./tidb-run-store.js";
@@ -47,7 +47,7 @@ import { PgSessionStore } from "./pg-session-storage.js";
47
47
  function snapshotBlobBackend(config, dialect, pool) {
48
48
  const s = config.snapshotBlobStore;
49
49
  if (!s)
50
- return undefined;
50
+ return new SqlBlobBackend(dialect, pool, config.snapshotBlobSqlMaxBytes);
51
51
  return new MinioBlobBackend({
52
52
  endpoint: s.endpoint,
53
53
  bucket: s.bucket,
@@ -58,6 +58,15 @@ function snapshotBlobBackend(config, dialect, pool) {
58
58
  ...(s.presignTtlSec ? { presignTtlSec: s.presignTtlSec } : {}),
59
59
  }, { dialect, pool });
60
60
  }
61
+ export function assertCloudSnapshotBlobPosture(kind, config) {
62
+ if (kind === "local" || config.snapshotBlobStore || config.snapshotBlobAllowSql)
63
+ return;
64
+ throw new Error("cloud deployments (DB_BACKEND=mysql|pg) must configure object storage for snapshot blobs " +
65
+ "(MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY): single-row SQL blob writes hit TiDB's " +
66
+ "txn-entry-size-limit (default 6MiB) / the mysql-protocol packet limit (bytes belong in object storage — " +
67
+ "same ruling as the D-1 attachment store). Single-box/test rigs may explicitly opt out with " +
68
+ "SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true (a per-blob cap then applies; oversized blobs are rejected 413).");
69
+ }
61
70
  export function snapshotBoundsFromConfig(config) {
62
71
  const mb = config.rewindSnapshotMaxMb;
63
72
  return mb !== undefined ? { ...DEFAULT_SNAPSHOT_BOUNDS, maxBytes: Math.round(mb * 1024 * 1024) } : DEFAULT_SNAPSHOT_BOUNDS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.294.0",
3
+ "version": "1.296.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",