@sekuire/sdk 0.1.10 → 0.1.11

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/index.esm.js CHANGED
@@ -78,6 +78,10 @@ function detectDeploymentUrl() {
78
78
  *
79
79
  * Automatically registers the agent with Sekuire and sends periodic heartbeats
80
80
  * to show online status in the Dashboard.
81
+ *
82
+ * Supports two authentication modes:
83
+ * 1. Install Token (recommended): Use an install token from the dashboard
84
+ * 2. API Key: Use an API key for SDK-initiated bootstrap (requires workspace)
81
85
  */
82
86
  // ============================================================================
83
87
  // Beacon Class
@@ -86,16 +90,21 @@ class Beacon {
86
90
  constructor(config) {
87
91
  this.intervalId = null;
88
92
  this.installationId = null;
93
+ this.runtimeToken = null;
94
+ this.refreshToken = null;
89
95
  this.lastHeartbeat = null;
90
96
  this.failedHeartbeats = 0;
91
97
  const resolvedApiKey = config.apiKey || this.getApiKey() || '';
98
+ const resolvedInstallToken = config.installToken || this.getInstallToken();
92
99
  this.config = {
93
100
  ...config,
94
- heartbeatIntervalMs: config.heartbeatIntervalMs ?? 60000, // 60 seconds
101
+ heartbeatIntervalSeconds: config.heartbeatIntervalSeconds ?? 60,
95
102
  apiKey: resolvedApiKey,
103
+ installToken: resolvedInstallToken,
104
+ capabilities: config.capabilities ?? [],
96
105
  };
97
- if (!this.config.apiKey) {
98
- console.warn('[Beacon] No API key provided. Set SEKUIRE_API_KEY environment variable.');
106
+ if (!this.config.installToken && !this.config.apiKey) {
107
+ console.warn('[Beacon] No install token or API key provided. Set SEKUIRE_INSTALL_TOKEN or SEKUIRE_API_KEY.');
99
108
  }
100
109
  }
101
110
  /**
@@ -111,22 +120,27 @@ class Beacon {
111
120
  if (deploymentUrl) {
112
121
  console.log(`[Beacon] Deployment URL: ${deploymentUrl}`);
113
122
  }
114
- // Bootstrap registration
123
+ // Bootstrap registration (exchanges install token for runtime credentials)
115
124
  try {
116
125
  await this.bootstrap(deploymentUrl);
126
+ console.log(`[Beacon] Bootstrap successful, installation ID: ${this.installationId}`);
117
127
  }
118
128
  catch (error) {
119
129
  console.error('[Beacon] Bootstrap failed:', error);
120
- // Continue anyway - heartbeat will retry registration
130
+ this.notifyStatusChange();
131
+ // Cannot continue without bootstrap - runtime token required for heartbeats
132
+ throw error;
121
133
  }
122
134
  // Start heartbeat loop
135
+ const intervalMs = this.config.heartbeatIntervalSeconds * 1000;
123
136
  this.intervalId = setInterval(() => {
124
137
  this.heartbeat().catch((err) => {
125
138
  console.error('[Beacon] Heartbeat failed:', err);
126
139
  });
127
- }, this.config.heartbeatIntervalMs);
140
+ }, intervalMs);
128
141
  // Send first heartbeat immediately
129
142
  await this.heartbeat().catch(() => { });
143
+ this.notifyStatusChange();
130
144
  }
131
145
  /**
132
146
  * Stop the beacon
@@ -145,6 +159,7 @@ class Beacon {
145
159
  return {
146
160
  isRunning: this.intervalId !== null,
147
161
  installationId: this.installationId || undefined,
162
+ runtimeToken: this.runtimeToken ? '***' + this.runtimeToken.slice(-4) : undefined,
148
163
  deploymentUrl: this.config.deploymentUrl || detectDeploymentUrl(),
149
164
  lastHeartbeat: this.lastHeartbeat || undefined,
150
165
  failedHeartbeats: this.failedHeartbeats,
@@ -152,72 +167,116 @@ class Beacon {
152
167
  }
153
168
  /**
154
169
  * Bootstrap registration with Sekuire
170
+ *
171
+ * Exchanges an install token for runtime credentials (installation_id + runtime_token).
172
+ * The install token is obtained from the dashboard or CLI.
155
173
  */
156
174
  async bootstrap(deploymentUrl) {
157
- if (!this.config.apiKey) {
158
- throw new Error('API key required for beacon registration');
175
+ if (!this.config.installToken) {
176
+ throw new Error('Install token required for beacon registration. ' +
177
+ 'Generate one from the dashboard or use: sekuire install token --workspace <id>');
159
178
  }
160
179
  const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/bootstrap`, {
161
180
  method: 'POST',
162
181
  headers: {
163
182
  'Content-Type': 'application/json',
164
- 'X-API-Key': this.config.apiKey,
165
183
  },
166
184
  body: JSON.stringify({
167
- agent_id: this.config.agentId,
185
+ install_token: this.config.installToken,
168
186
  upstream_url: deploymentUrl,
187
+ capabilities: this.config.capabilities ?? [],
188
+ heartbeat_interval_seconds: this.config.heartbeatIntervalSeconds,
169
189
  }),
170
190
  });
171
191
  if (!response.ok) {
172
- throw new Error(`Bootstrap failed: ${response.status} ${response.statusText}`);
192
+ const body = await response.text();
193
+ throw new Error(`Bootstrap failed: ${response.status} ${response.statusText} - ${body}`);
173
194
  }
174
195
  const data = await response.json();
175
196
  this.installationId = data.installation_id;
176
- console.log(`[Beacon] Registered with installation ID: ${this.installationId}`);
197
+ this.runtimeToken = data.runtime_token;
198
+ this.refreshToken = data.refresh_token;
177
199
  }
178
200
  /**
179
- * Send heartbeat to Sekuire
201
+ * Send heartbeat (lease renewal) to Sekuire
202
+ *
203
+ * Uses the installation-based lease endpoint with runtime token authentication.
180
204
  */
181
205
  async heartbeat() {
182
- if (!this.config.apiKey) {
206
+ if (!this.installationId || !this.runtimeToken) {
207
+ console.warn('[Beacon] Cannot send heartbeat - not bootstrapped');
183
208
  return;
184
209
  }
185
210
  const deploymentUrl = this.config.deploymentUrl || detectDeploymentUrl();
186
211
  const prevFailed = this.failedHeartbeats;
187
212
  try {
188
- const payload = this.config.onHeartbeat
189
- ? await this.config.onHeartbeat(deploymentUrl)
190
- : {
191
- agent_id: this.config.agentId,
192
- status: 'running',
193
- public_url: deploymentUrl,
194
- load: 0.0,
195
- };
196
- const response = await fetch(`${this.config.apiBaseUrl}/api/v1/agents/heartbeat`, {
213
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/lease`, {
197
214
  method: 'POST',
198
215
  headers: {
199
216
  'Content-Type': 'application/json',
200
- 'X-API-Key': this.config.apiKey,
217
+ 'Authorization': `Bearer ${this.runtimeToken}`,
201
218
  },
202
- body: JSON.stringify(payload),
219
+ body: JSON.stringify({
220
+ status: 'running',
221
+ heartbeat_interval_seconds: this.config.heartbeatIntervalSeconds,
222
+ upstream_url: deploymentUrl,
223
+ }),
203
224
  });
225
+ if (response.status === 401) {
226
+ // Token expired - try to refresh
227
+ console.warn('[Beacon] Runtime token expired, attempting refresh...');
228
+ await this.refreshRuntimeToken();
229
+ // Retry heartbeat after refresh
230
+ return this.heartbeat();
231
+ }
204
232
  if (!response.ok) {
205
- throw new Error(`Heartbeat failed: ${response.status}`);
233
+ const body = await response.text();
234
+ throw new Error(`Heartbeat failed: ${response.status} - ${body}`);
206
235
  }
207
236
  this.lastHeartbeat = new Date();
208
237
  this.failedHeartbeats = 0;
209
- if (prevFailed > 0 && this.config.onStatusChange) {
210
- this.config.onStatusChange(this.getStatus());
238
+ if (prevFailed > 0) {
239
+ this.notifyStatusChange();
211
240
  }
212
241
  }
213
242
  catch (error) {
214
243
  this.failedHeartbeats++;
215
- if (this.config.onStatusChange) {
216
- this.config.onStatusChange(this.getStatus());
217
- }
244
+ this.notifyStatusChange();
218
245
  throw error;
219
246
  }
220
247
  }
248
+ /**
249
+ * Refresh the runtime token using the refresh token
250
+ */
251
+ async refreshRuntimeToken() {
252
+ if (!this.installationId || !this.refreshToken) {
253
+ throw new Error('Cannot refresh token - missing installation ID or refresh token');
254
+ }
255
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/refresh`, {
256
+ method: 'POST',
257
+ headers: {
258
+ 'Content-Type': 'application/json',
259
+ },
260
+ body: JSON.stringify({
261
+ refresh_token: this.refreshToken,
262
+ }),
263
+ });
264
+ if (!response.ok) {
265
+ const body = await response.text();
266
+ throw new Error(`Token refresh failed: ${response.status} - ${body}`);
267
+ }
268
+ const data = await response.json();
269
+ this.runtimeToken = data.runtime_token;
270
+ console.log('[Beacon] Runtime token refreshed successfully');
271
+ }
272
+ /**
273
+ * Notify status change callback
274
+ */
275
+ notifyStatusChange() {
276
+ if (this.config.onStatusChange) {
277
+ this.config.onStatusChange(this.getStatus());
278
+ }
279
+ }
221
280
  /**
222
281
  * Get API key from environment
223
282
  */
@@ -227,6 +286,15 @@ class Beacon {
227
286
  }
228
287
  return undefined;
229
288
  }
289
+ /**
290
+ * Get install token from environment
291
+ */
292
+ getInstallToken() {
293
+ if (typeof process !== 'undefined' && process.env) {
294
+ return process.env.SEKUIRE_INSTALL_TOKEN;
295
+ }
296
+ return undefined;
297
+ }
230
298
  }
231
299
  // ============================================================================
232
300
  // Convenience Export
@@ -730,17 +798,20 @@ class SekuireSDK {
730
798
  throw new Error("agentId is required");
731
799
  }
732
800
  const apiKey = config.apiKey || process.env.SEKUIRE_API_KEY;
801
+ const installToken = config.installToken || process.env.SEKUIRE_INSTALL_TOKEN;
733
802
  this.config = {
734
803
  privateKey: config.privateKey,
735
804
  apiKey,
805
+ installToken,
736
806
  agentId: config.agentId,
737
807
  agentName: config.agentName ?? "Unknown",
738
808
  apiUrl: (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, ""),
739
809
  workspaceId: config.workspaceId ?? "",
740
810
  environment: config.environment ?? "production",
741
811
  autoHeartbeat: config.autoHeartbeat !== false,
742
- heartbeatInterval: config.heartbeatInterval ?? 60000,
812
+ heartbeatIntervalSeconds: config.heartbeatIntervalSeconds ?? 60,
743
813
  loggingEnabled: config.loggingEnabled !== false,
814
+ capabilities: config.capabilities ?? [],
744
815
  };
745
816
  this.identity = new AgentIdentity(this.config.agentName, this.config.agentId, this.config.privateKey);
746
817
  const loggerConfig = {
@@ -759,22 +830,12 @@ class SekuireSDK {
759
830
  });
760
831
  this.beacon = new Beacon({
761
832
  apiBaseUrl: this.config.apiUrl,
833
+ installToken: this.config.installToken,
762
834
  apiKey: this.config.apiKey,
763
835
  agentId: this.config.agentId,
764
- heartbeatIntervalMs: this.config.heartbeatInterval,
765
- onHeartbeat: this.config.privateKey
766
- ? async (deploymentUrl) => {
767
- const timestamp = Math.floor(Date.now() / 1000);
768
- const message = `${this.config.agentId}|${deploymentUrl ?? ""}|${timestamp}`;
769
- const signature = await this.identity.sign(message);
770
- return {
771
- sekuire_id: this.config.agentId,
772
- url: deploymentUrl ?? null,
773
- timestamp,
774
- signature,
775
- };
776
- }
777
- : undefined,
836
+ workspaceId: this.config.workspaceId || undefined,
837
+ heartbeatIntervalSeconds: this.config.heartbeatIntervalSeconds,
838
+ capabilities: this.config.capabilities,
778
839
  });
779
840
  }
780
841
  /**
@@ -782,10 +843,11 @@ class SekuireSDK {
782
843
  *
783
844
  * Required env vars:
784
845
  * SEKUIRE_AGENT_ID - The agent's sekuire_id
846
+ * SEKUIRE_INSTALL_TOKEN - Install token from dashboard (required for heartbeat)
785
847
  *
786
848
  * Optional env vars:
787
849
  * SEKUIRE_API_KEY - API key for authentication (X-API-Key header)
788
- * SEKUIRE_PRIVATE_KEY - Private key for signing (required for heartbeat)
850
+ * SEKUIRE_PRIVATE_KEY - Private key for signing
789
851
  * SEKUIRE_AGENT_NAME - Human-readable agent name
790
852
  * SEKUIRE_API_URL - API base URL (default: https://api.sekuire.ai)
791
853
  * SEKUIRE_WORKSPACE_ID - Workspace ID for policy enforcement
@@ -799,6 +861,7 @@ class SekuireSDK {
799
861
  return new SekuireSDK({
800
862
  privateKey: process.env.SEKUIRE_PRIVATE_KEY,
801
863
  apiKey: process.env.SEKUIRE_API_KEY,
864
+ installToken: process.env.SEKUIRE_INSTALL_TOKEN,
802
865
  agentId,
803
866
  agentName: process.env.SEKUIRE_AGENT_NAME,
804
867
  apiUrl: process.env.SEKUIRE_API_URL ?? DEFAULT_API_URL,
@@ -811,46 +874,32 @@ class SekuireSDK {
811
874
  /**
812
875
  * Start the SDK.
813
876
  *
814
- * - Sends initial heartbeat
877
+ * - Bootstraps with Sekuire using the install token
815
878
  * - Starts auto-heartbeat loop if enabled
879
+ *
880
+ * Requires SEKUIRE_INSTALL_TOKEN to be set (from dashboard or CLI).
816
881
  */
817
882
  async start() {
818
883
  if (this.isRunning) {
819
884
  return;
820
885
  }
821
886
  this.isRunning = true;
822
- if (this.config.autoHeartbeat && (this.config.privateKey || this.config.apiKey)) {
887
+ if (this.config.autoHeartbeat && this.config.installToken) {
823
888
  await this.beacon.start();
824
889
  }
890
+ else if (this.config.autoHeartbeat && !this.config.installToken) {
891
+ console.warn("[SekuireSDK] Auto-heartbeat enabled but no install token provided. " +
892
+ "Set SEKUIRE_INSTALL_TOKEN or generate one from the dashboard.");
893
+ }
825
894
  }
826
895
  /**
827
- * Send a signed heartbeat to the registry.
828
- *
829
- * The heartbeat proves the agent is running and has access to the private key.
896
+ * Check if the beacon is connected and sending heartbeats.
830
897
  *
831
- * @param url Optional public URL where the agent can be reached
832
- * @returns True if heartbeat was acknowledged, False otherwise
898
+ * @returns True if the beacon has successfully bootstrapped
833
899
  */
834
- async heartbeat(url) {
835
- if (!this.config.privateKey) {
836
- return false;
837
- }
838
- const timestamp = Math.floor(Date.now() / 1000);
839
- const message = `${this.config.agentId}|${url ?? ""}|${timestamp}`;
840
- const signature = await this.identity.sign(message);
841
- const payload = {
842
- sekuire_id: this.config.agentId,
843
- url: url ?? null,
844
- timestamp,
845
- signature,
846
- };
847
- try {
848
- const response = await this.client.post("/api/v1/agents/heartbeat", payload);
849
- return response.status === 200;
850
- }
851
- catch {
852
- return false;
853
- }
900
+ isConnected() {
901
+ const status = this.beacon.getStatus();
902
+ return status.isRunning && !!status.installationId;
854
903
  }
855
904
  /**
856
905
  * Log an event to Sekuire.
@@ -3070,12 +3119,25 @@ const llm = {
3070
3119
  };
3071
3120
 
3072
3121
  class BaseMemoryStorage {
3122
+ constructor() {
3123
+ this._connected = false;
3124
+ }
3125
+ async connect() {
3126
+ this._connected = true;
3127
+ }
3128
+ async disconnect() {
3129
+ this._connected = false;
3130
+ }
3131
+ isConnected() {
3132
+ return this._connected;
3133
+ }
3073
3134
  }
3074
3135
 
3075
3136
  class InMemoryStorage extends BaseMemoryStorage {
3076
3137
  constructor() {
3077
- super(...arguments);
3138
+ super();
3078
3139
  this.storage = new Map();
3140
+ this._connected = true;
3079
3141
  }
3080
3142
  async add(sessionId, message) {
3081
3143
  if (!this.storage.has(sessionId)) {
@@ -3104,32 +3166,49 @@ class InMemoryStorage extends BaseMemoryStorage {
3104
3166
  class PostgresStorage extends BaseMemoryStorage {
3105
3167
  constructor(config) {
3106
3168
  super();
3107
- this.initialized = false;
3169
+ this.connectionPromise = null;
3108
3170
  this.tableName = config.tableName || 'sekuire_memory';
3109
- this.initPool(config);
3171
+ this.config = config;
3172
+ if (!config.lazyConnect) {
3173
+ this.connectionPromise = this.connect();
3174
+ }
3175
+ }
3176
+ async connect() {
3177
+ if (this._connected)
3178
+ return;
3179
+ if (this.connectionPromise)
3180
+ return this.connectionPromise;
3181
+ this.connectionPromise = this.initPool();
3182
+ return this.connectionPromise;
3110
3183
  }
3111
- async initPool(config) {
3184
+ async initPool() {
3112
3185
  try {
3113
3186
  const { Pool } = await import('pg');
3114
- if (config.connectionString) {
3115
- this.pool = new Pool({ connectionString: config.connectionString });
3187
+ if (this.config.connectionString) {
3188
+ this.pool = new Pool({ connectionString: this.config.connectionString });
3116
3189
  }
3117
3190
  else {
3118
3191
  this.pool = new Pool({
3119
- host: config.host || 'localhost',
3120
- port: config.port || 5432,
3121
- database: config.database,
3122
- user: config.user,
3123
- password: config.password,
3192
+ host: this.config.host || 'localhost',
3193
+ port: this.config.port || 5432,
3194
+ database: this.config.database,
3195
+ user: this.config.user,
3196
+ password: this.config.password,
3124
3197
  });
3125
3198
  }
3126
3199
  await this.createTableIfNotExists();
3127
- this.initialized = true;
3200
+ this._connected = true;
3128
3201
  }
3129
3202
  catch (error) {
3203
+ this.connectionPromise = null;
3130
3204
  throw new Error(`Failed to initialize Postgres: ${error}`);
3131
3205
  }
3132
3206
  }
3207
+ async ensureConnected() {
3208
+ if (!this._connected) {
3209
+ await this.connect();
3210
+ }
3211
+ }
3133
3212
  async createTableIfNotExists() {
3134
3213
  const query = `
3135
3214
  CREATE TABLE IF NOT EXISTS ${this.tableName} (
@@ -3146,8 +3225,7 @@ class PostgresStorage extends BaseMemoryStorage {
3146
3225
  await this.pool.query(query);
3147
3226
  }
3148
3227
  async add(sessionId, message) {
3149
- if (!this.initialized)
3150
- throw new Error('Postgres not initialized');
3228
+ await this.ensureConnected();
3151
3229
  const query = `
3152
3230
  INSERT INTO ${this.tableName} (session_id, role, content, timestamp, metadata)
3153
3231
  VALUES ($1, $2, $3, $4, $5)
@@ -3161,8 +3239,7 @@ class PostgresStorage extends BaseMemoryStorage {
3161
3239
  ]);
3162
3240
  }
3163
3241
  async get(sessionId, limit) {
3164
- if (!this.initialized)
3165
- throw new Error('Postgres not initialized');
3242
+ await this.ensureConnected();
3166
3243
  let query = `
3167
3244
  SELECT role, content, timestamp, metadata
3168
3245
  FROM ${this.tableName}
@@ -3181,8 +3258,7 @@ class PostgresStorage extends BaseMemoryStorage {
3181
3258
  }));
3182
3259
  }
3183
3260
  async clear(sessionId) {
3184
- if (!this.initialized)
3185
- throw new Error('Postgres not initialized');
3261
+ await this.ensureConnected();
3186
3262
  const query = `DELETE FROM ${this.tableName} WHERE session_id = $1`;
3187
3263
  await this.pool.query(query, [sessionId]);
3188
3264
  }
@@ -3190,16 +3266,16 @@ class PostgresStorage extends BaseMemoryStorage {
3190
3266
  await this.clear(sessionId);
3191
3267
  }
3192
3268
  async exists(sessionId) {
3193
- if (!this.initialized)
3194
- throw new Error('Postgres not initialized');
3269
+ await this.ensureConnected();
3195
3270
  const query = `SELECT COUNT(*) FROM ${this.tableName} WHERE session_id = $1`;
3196
3271
  const result = await this.pool.query(query, [sessionId]);
3197
3272
  return parseInt(result.rows[0].count) > 0;
3198
3273
  }
3199
3274
  async disconnect() {
3200
- if (this.initialized) {
3275
+ if (this._connected && this.pool) {
3201
3276
  await this.pool.end();
3202
- this.initialized = false;
3277
+ this._connected = false;
3278
+ this.connectionPromise = null;
3203
3279
  }
3204
3280
  }
3205
3281
  }
@@ -3208,56 +3284,70 @@ class RedisStorage extends BaseMemoryStorage {
3208
3284
  constructor(config) {
3209
3285
  super();
3210
3286
  this.client = null;
3211
- this.connected = false;
3287
+ this.connectionPromise = null;
3212
3288
  this.keyPrefix = config.keyPrefix || 'sekuire:memory:';
3213
- this.initClient(config);
3289
+ this.config = config;
3290
+ if (!config.lazyConnect) {
3291
+ this.connectionPromise = this.connect();
3292
+ }
3293
+ }
3294
+ async connect() {
3295
+ if (this._connected)
3296
+ return;
3297
+ if (this.connectionPromise)
3298
+ return this.connectionPromise;
3299
+ this.connectionPromise = this.initClient();
3300
+ return this.connectionPromise;
3214
3301
  }
3215
- async initClient(config) {
3302
+ async initClient() {
3216
3303
  try {
3217
3304
  const { createClient } = await import('redis');
3218
- if (config.url) {
3219
- this.client = createClient({ url: config.url });
3305
+ if (this.config.url) {
3306
+ this.client = createClient({ url: this.config.url });
3220
3307
  }
3221
3308
  else {
3222
3309
  this.client = createClient({
3223
3310
  socket: {
3224
- host: config.host || 'localhost',
3225
- port: config.port || 6379,
3311
+ host: this.config.host || 'localhost',
3312
+ port: this.config.port || 6379,
3226
3313
  },
3227
- password: config.password,
3228
- database: config.db || 0,
3314
+ password: this.config.password,
3315
+ database: this.config.db || 0,
3229
3316
  });
3230
3317
  }
3231
3318
  this.client.on('error', (err) => {
3232
3319
  console.error('Redis client error:', err);
3233
3320
  });
3234
3321
  await this.client.connect();
3235
- this.connected = true;
3322
+ this._connected = true;
3236
3323
  }
3237
3324
  catch (error) {
3325
+ this.connectionPromise = null;
3238
3326
  throw new Error(`Failed to initialize Redis: ${error}`);
3239
3327
  }
3240
3328
  }
3329
+ async ensureConnected() {
3330
+ if (!this._connected) {
3331
+ await this.connect();
3332
+ }
3333
+ }
3241
3334
  getKey(sessionId) {
3242
3335
  return `${this.keyPrefix}${sessionId}`;
3243
3336
  }
3244
3337
  async add(sessionId, message) {
3245
- if (!this.connected)
3246
- throw new Error('Redis not connected');
3338
+ await this.ensureConnected();
3247
3339
  const key = this.getKey(sessionId);
3248
3340
  await this.client.rPush(key, JSON.stringify(message));
3249
3341
  }
3250
3342
  async get(sessionId, limit) {
3251
- if (!this.connected)
3252
- throw new Error('Redis not connected');
3343
+ await this.ensureConnected();
3253
3344
  const key = this.getKey(sessionId);
3254
3345
  const start = limit ? -limit : 0;
3255
3346
  const messages = await this.client.lRange(key, start, -1);
3256
3347
  return messages.map((msg) => JSON.parse(msg));
3257
3348
  }
3258
3349
  async clear(sessionId) {
3259
- if (!this.connected)
3260
- throw new Error('Redis not connected');
3350
+ await this.ensureConnected();
3261
3351
  const key = this.getKey(sessionId);
3262
3352
  await this.client.del(key);
3263
3353
  }
@@ -3265,190 +3355,1080 @@ class RedisStorage extends BaseMemoryStorage {
3265
3355
  await this.clear(sessionId);
3266
3356
  }
3267
3357
  async exists(sessionId) {
3268
- if (!this.connected)
3269
- throw new Error('Redis not connected');
3358
+ await this.ensureConnected();
3270
3359
  const key = this.getKey(sessionId);
3271
3360
  return (await this.client.exists(key)) === 1;
3272
3361
  }
3273
3362
  async disconnect() {
3274
- if (this.connected) {
3363
+ if (this._connected && this.client) {
3275
3364
  await this.client.quit();
3276
- this.connected = false;
3365
+ this._connected = false;
3366
+ this.connectionPromise = null;
3277
3367
  }
3278
3368
  }
3279
3369
  }
3280
3370
 
3281
- function createMemoryStorage(config) {
3282
- // Normalize type (support "buffer" as alias for "in-memory")
3283
- const normalizedType = config.type === 'buffer' ? 'in-memory' : config.type;
3284
- switch (normalizedType) {
3285
- case 'in-memory':
3286
- return new InMemoryStorage();
3287
- case 'redis':
3288
- if (!config.redis) {
3289
- throw new Error('Redis config required for redis memory type');
3290
- }
3291
- return new RedisStorage(config.redis);
3292
- case 'postgres':
3293
- if (!config.postgres) {
3294
- throw new Error('Postgres config required for postgres memory type');
3295
- }
3296
- return new PostgresStorage(config.postgres);
3297
- default:
3298
- throw new Error(`Unknown memory type: ${config.type}`);
3299
- }
3300
- }
3301
-
3302
- const DEFAULT_OVERRIDE_ENV = "SEKUIRE_POLICY_DEV_OVERRIDE";
3303
- class PolicyEnforcer {
3304
- constructor(policy, override, onViolation) {
3305
- this.policy = policy;
3306
- this.onViolation = onViolation;
3307
- const envOverride = process.env[DEFAULT_OVERRIDE_ENV] === "true";
3308
- this.override = override ?? envOverride;
3309
- if (this.override) {
3310
- console.warn("[policy] override enabled; policy enforcement is running in warn-only mode");
3311
- }
3371
+ class SQLiteStorage extends BaseMemoryStorage {
3372
+ constructor(config) {
3373
+ super();
3374
+ this.db = null;
3375
+ this.connectionPromise = null;
3376
+ this.config = config;
3377
+ this.tableName = config.tableName || 'sekuire_memory';
3378
+ this.connectionPromise = this.connect();
3312
3379
  }
3313
- enforceNetwork(domain, protocol) {
3314
- const perms = this.policy.content?.permissions?.network;
3315
- if (!perms) {
3316
- this.warnOnly("network.missing", "Network permissions not configured");
3380
+ async connect() {
3381
+ if (this._connected)
3317
3382
  return;
3383
+ if (this.connectionPromise && !this._connected)
3384
+ return this.connectionPromise;
3385
+ try {
3386
+ const Database = (await import('better-sqlite3')).default;
3387
+ this.db = new Database(this.config.filename, {
3388
+ readonly: this.config.readonly || false,
3389
+ });
3390
+ this.db.pragma('journal_mode = WAL');
3391
+ this.ensureTableExists();
3392
+ this._connected = true;
3318
3393
  }
3319
- if (!perms.enabled)
3320
- this.throw("network.disabled", "Network access is disabled");
3321
- if (perms.require_tls && protocol !== "https") {
3322
- this.throw("network.tls_required", "TLS is required for network requests");
3323
- }
3324
- if (perms.blocked_domains && this.matches(domain, perms.blocked_domains)) {
3325
- this.throw("network.blocked", `Domain ${domain} is blocked by policy`);
3326
- }
3327
- if (perms.allowed_domains && !this.matches(domain, perms.allowed_domains)) {
3328
- this.throw("network.not_allowed", `Domain ${domain} is not in the allowlist`);
3394
+ catch (error) {
3395
+ this.connectionPromise = null;
3396
+ throw new Error(`Failed to initialize SQLite: ${error}`);
3329
3397
  }
3330
3398
  }
3331
- enforceFilesystem(path, operation) {
3332
- const perms = this.policy.content?.permissions?.filesystem;
3333
- if (!perms) {
3334
- this.warnOnly("fs.missing", "Filesystem permissions not configured");
3335
- return;
3336
- }
3337
- if (!perms.enabled)
3338
- this.throw("fs.disabled", "Filesystem access is disabled");
3339
- if (perms.blocked_paths && this.pathMatch(path, perms.blocked_paths)) {
3340
- this.throw("fs.blocked", `Path ${path} is blocked`);
3341
- }
3342
- if (perms.allowed_paths && !this.pathMatch(path, perms.allowed_paths)) {
3343
- this.throw("fs.not_allowed", `Path ${path} is not in the allowlist`);
3344
- }
3345
- if (perms.allowed_extensions) {
3346
- const ext = path.includes(".") ? `.${path.split(".").pop()}` : "";
3347
- if (ext && !perms.allowed_extensions.includes(ext)) {
3348
- this.throw("fs.ext", `Extension ${ext} is not allowed`);
3349
- }
3350
- }
3399
+ ensureTableExists() {
3400
+ this.db.exec(`
3401
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
3402
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3403
+ session_id TEXT NOT NULL,
3404
+ role TEXT NOT NULL,
3405
+ content TEXT NOT NULL,
3406
+ timestamp INTEGER NOT NULL,
3407
+ metadata TEXT,
3408
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
3409
+ )
3410
+ `);
3411
+ this.db.exec(`
3412
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_session_id
3413
+ ON ${this.tableName}(session_id)
3414
+ `);
3351
3415
  }
3352
- enforceTool(toolName) {
3353
- const tools = this.policy.content?.tools;
3354
- // Check blocked tools
3355
- if (tools?.blocked_tools?.includes(toolName)) {
3356
- this.throw("tool.blocked", `Tool ${toolName} is blocked`);
3357
- }
3358
- // If no allowlist, allow all tools
3359
- if (!tools?.allowed_tools || tools.allowed_tools.length === 0) {
3360
- return;
3361
- }
3362
- // Check allowlist with pattern matching
3363
- const isAllowed = tools.allowed_tools.some((t) => {
3364
- const pattern = t.name;
3365
- // Exact match
3366
- if (pattern === toolName) {
3367
- return true;
3368
- }
3369
- // Category patterns: "files:*" => matches "file_read", "file_write", etc
3370
- // Check this BEFORE wildcard matching since it contains both : and *
3371
- if (pattern.includes(":")) {
3372
- const [category, ops] = pattern.split(":", 2);
3373
- const categoryPrefix = this.getCategoryPrefix(category);
3374
- if (ops === "*") {
3375
- // Match all tools in category
3376
- const matches = toolName.startsWith(categoryPrefix + "_");
3377
- return matches;
3378
- }
3379
- if (ops.startsWith("[") && ops.endsWith("]")) {
3380
- // Match specific operations: "files:[read,write]"
3381
- const operations = ops.slice(1, -1).split(",").map(s => s.trim());
3382
- const matches = operations.some(op => toolName === `${categoryPrefix}_${op}`);
3383
- console.log(`[DEBUG] ${matches ? '✓' : '✗'} Category ops: ${operations.join(",")}`);
3384
- return matches;
3385
- }
3386
- }
3387
- // Pattern matching: "file_*", etc (simple wildcards without :)
3388
- if (pattern.includes("*")) {
3389
- const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
3390
- const matches = regex.test(toolName);
3391
- return matches;
3392
- }
3393
- return false;
3394
- });
3395
- if (!isAllowed) {
3396
- this.throw("tool.not_allowed", `Tool ${toolName} is not in the allowlist`);
3416
+ async ensureConnected() {
3417
+ if (!this._connected) {
3418
+ await this.connect();
3397
3419
  }
3398
3420
  }
3399
- getCategoryPrefix(category) {
3400
- const prefixMap = {
3401
- files: "file",
3402
- directories: "dir",
3403
- network: "http",
3404
- data: "json",
3405
- system: "env",
3406
- };
3407
- return prefixMap[category] || category;
3421
+ async add(sessionId, message) {
3422
+ await this.ensureConnected();
3423
+ const stmt = this.db.prepare(`
3424
+ INSERT INTO ${this.tableName} (session_id, role, content, timestamp, metadata)
3425
+ VALUES (?, ?, ?, ?, ?)
3426
+ `);
3427
+ stmt.run(sessionId, message.role, message.content, message.timestamp, message.metadata ? JSON.stringify(message.metadata) : null);
3408
3428
  }
3409
- enforceModel(model) {
3410
- const models = this.policy.content?.agent?.models;
3411
- if (models?.allowed_models && !models.allowed_models.includes(model)) {
3412
- this.throw("model.not_allowed", `Model ${model} is not allowed`);
3413
- }
3414
- if (models?.blocked_models && models.blocked_models.includes(model)) {
3415
- this.throw("model.blocked", `Model ${model} is blocked`);
3429
+ async get(sessionId, limit) {
3430
+ await this.ensureConnected();
3431
+ let sql = `
3432
+ SELECT role, content, timestamp, metadata
3433
+ FROM ${this.tableName}
3434
+ WHERE session_id = ?
3435
+ ORDER BY timestamp ASC
3436
+ `;
3437
+ if (limit) {
3438
+ sql += ` LIMIT ${limit}`;
3416
3439
  }
3440
+ const stmt = this.db.prepare(sql);
3441
+ const rows = stmt.all(sessionId);
3442
+ return rows.map((row) => ({
3443
+ role: row.role,
3444
+ content: row.content,
3445
+ timestamp: row.timestamp,
3446
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
3447
+ }));
3417
3448
  }
3418
- enforceApi(service) {
3419
- const perms = this.policy.content?.permissions?.api;
3420
- if (!perms) {
3421
- this.warnOnly("api.missing", "API permissions not configured");
3422
- return;
3423
- }
3424
- if (!perms.enabled)
3425
- this.throw("api.disabled", "API access is disabled");
3426
- if (perms.allowed_services &&
3427
- !perms.allowed_services.some((s) => s.service_name === service)) {
3428
- this.throw("api.not_allowed", `API service ${service} is not in the allowlist`);
3449
+ async clear(sessionId) {
3450
+ await this.ensureConnected();
3451
+ const stmt = this.db.prepare(`DELETE FROM ${this.tableName} WHERE session_id = ?`);
3452
+ stmt.run(sessionId);
3453
+ }
3454
+ async delete(sessionId) {
3455
+ await this.clear(sessionId);
3456
+ }
3457
+ async exists(sessionId) {
3458
+ await this.ensureConnected();
3459
+ const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM ${this.tableName} WHERE session_id = ?`);
3460
+ const result = stmt.get(sessionId);
3461
+ return result.count > 0;
3462
+ }
3463
+ async disconnect() {
3464
+ if (this._connected && this.db) {
3465
+ this.db.close();
3466
+ this._connected = false;
3467
+ this.connectionPromise = null;
3429
3468
  }
3430
3469
  }
3431
- enforceRateLimit(type, count = 1) {
3432
- const limits = this.policy.content?.rate_limits?.per_agent;
3433
- if (!limits)
3434
- return;
3435
- // Simple local enforcement (note: this does not sync across instances)
3436
- // TODO: Implement proper token bucket or sliding window
3437
- if (type === "request" && limits.requests_per_minute) ;
3470
+ }
3471
+
3472
+ class UpstashStorage extends BaseMemoryStorage {
3473
+ constructor(config) {
3474
+ super();
3475
+ this.client = null;
3476
+ this.connectionPromise = null;
3477
+ this.keyPrefix = config.keyPrefix || 'sekuire:memory:';
3478
+ this.config = config;
3479
+ this.connectionPromise = this.connect();
3438
3480
  }
3439
- throw(rule, reason) {
3440
- if (this.override) {
3441
- this.warnOnly(rule, reason);
3481
+ async connect() {
3482
+ if (this._connected)
3442
3483
  return;
3484
+ if (this.connectionPromise && !this._connected)
3485
+ return this.connectionPromise;
3486
+ try {
3487
+ const { Redis } = await import('@upstash/redis');
3488
+ this.client = new Redis({
3489
+ url: this.config.url,
3490
+ token: this.config.token,
3491
+ });
3492
+ this._connected = true;
3493
+ }
3494
+ catch (error) {
3495
+ this.connectionPromise = null;
3496
+ throw new Error(`Failed to initialize Upstash Redis: ${error}`);
3443
3497
  }
3444
- if (this.onViolation)
3445
- this.onViolation(rule, reason);
3446
- throw new PolicyViolationError(reason, rule);
3447
3498
  }
3448
- warnOnly(rule, reason) {
3449
- if (this.onViolation)
3450
- this.onViolation(rule, reason);
3451
- console.warn(`[policy][${rule}] ${reason}`);
3499
+ async ensureConnected() {
3500
+ if (!this._connected) {
3501
+ await this.connect();
3502
+ }
3503
+ }
3504
+ getKey(sessionId) {
3505
+ return `${this.keyPrefix}${sessionId}`;
3506
+ }
3507
+ async add(sessionId, message) {
3508
+ await this.ensureConnected();
3509
+ const key = this.getKey(sessionId);
3510
+ await this.client.rpush(key, JSON.stringify(message));
3511
+ }
3512
+ async get(sessionId, limit) {
3513
+ await this.ensureConnected();
3514
+ const key = this.getKey(sessionId);
3515
+ const start = limit ? -limit : 0;
3516
+ const messages = await this.client.lrange(key, start, -1);
3517
+ return messages.map((msg) => JSON.parse(msg));
3518
+ }
3519
+ async clear(sessionId) {
3520
+ await this.ensureConnected();
3521
+ const key = this.getKey(sessionId);
3522
+ await this.client.del(key);
3523
+ }
3524
+ async delete(sessionId) {
3525
+ await this.clear(sessionId);
3526
+ }
3527
+ async exists(sessionId) {
3528
+ await this.ensureConnected();
3529
+ const key = this.getKey(sessionId);
3530
+ return (await this.client.exists(key)) === 1;
3531
+ }
3532
+ async disconnect() {
3533
+ this._connected = false;
3534
+ this.client = null;
3535
+ this.connectionPromise = null;
3536
+ }
3537
+ }
3538
+
3539
+ class CloudflareKVStorage extends BaseMemoryStorage {
3540
+ constructor(config) {
3541
+ super();
3542
+ this.binding = null;
3543
+ this.config = config;
3544
+ this.keyPrefix = config.keyPrefix || 'sekuire:memory:';
3545
+ if (config.binding) {
3546
+ this.binding = config.binding;
3547
+ this.useRestApi = false;
3548
+ this._connected = true;
3549
+ }
3550
+ else if (config.accountId && config.namespaceId && config.apiToken) {
3551
+ this.useRestApi = true;
3552
+ this._connected = true;
3553
+ }
3554
+ else {
3555
+ throw new Error('CloudflareKVConfig requires either a binding (in Workers) or accountId, namespaceId, and apiToken (for REST API)');
3556
+ }
3557
+ }
3558
+ getKey(sessionId) {
3559
+ return `${this.keyPrefix}${sessionId}`;
3560
+ }
3561
+ async kvGet(key) {
3562
+ if (!this.useRestApi) {
3563
+ return this.binding.get(key);
3564
+ }
3565
+ const { accountId, namespaceId, apiToken } = this.config;
3566
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`;
3567
+ const response = await fetch(url, {
3568
+ headers: { Authorization: `Bearer ${apiToken}` },
3569
+ });
3570
+ if (response.status === 404)
3571
+ return null;
3572
+ if (!response.ok)
3573
+ throw new Error(`KV GET failed: ${response.statusText}`);
3574
+ return response.text();
3575
+ }
3576
+ async kvPut(key, value) {
3577
+ if (!this.useRestApi) {
3578
+ await this.binding.put(key, value);
3579
+ return;
3580
+ }
3581
+ const { accountId, namespaceId, apiToken } = this.config;
3582
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`;
3583
+ const response = await fetch(url, {
3584
+ method: 'PUT',
3585
+ headers: {
3586
+ Authorization: `Bearer ${apiToken}`,
3587
+ 'Content-Type': 'text/plain',
3588
+ },
3589
+ body: value,
3590
+ });
3591
+ if (!response.ok)
3592
+ throw new Error(`KV PUT failed: ${response.statusText}`);
3593
+ }
3594
+ async kvDelete(key) {
3595
+ if (!this.useRestApi) {
3596
+ await this.binding.delete(key);
3597
+ return;
3598
+ }
3599
+ const { accountId, namespaceId, apiToken } = this.config;
3600
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`;
3601
+ const response = await fetch(url, {
3602
+ method: 'DELETE',
3603
+ headers: { Authorization: `Bearer ${apiToken}` },
3604
+ });
3605
+ if (!response.ok && response.status !== 404) {
3606
+ throw new Error(`KV DELETE failed: ${response.statusText}`);
3607
+ }
3608
+ }
3609
+ async add(sessionId, message) {
3610
+ const key = this.getKey(sessionId);
3611
+ const existing = await this.kvGet(key);
3612
+ const messages = existing ? JSON.parse(existing) : [];
3613
+ messages.push(message);
3614
+ await this.kvPut(key, JSON.stringify(messages));
3615
+ }
3616
+ async get(sessionId, limit) {
3617
+ const key = this.getKey(sessionId);
3618
+ const data = await this.kvGet(key);
3619
+ if (!data)
3620
+ return [];
3621
+ const messages = JSON.parse(data);
3622
+ if (limit) {
3623
+ return messages.slice(-limit);
3624
+ }
3625
+ return messages;
3626
+ }
3627
+ async clear(sessionId) {
3628
+ const key = this.getKey(sessionId);
3629
+ await this.kvDelete(key);
3630
+ }
3631
+ async delete(sessionId) {
3632
+ await this.clear(sessionId);
3633
+ }
3634
+ async exists(sessionId) {
3635
+ const key = this.getKey(sessionId);
3636
+ const data = await this.kvGet(key);
3637
+ return data !== null;
3638
+ }
3639
+ async disconnect() {
3640
+ this._connected = false;
3641
+ }
3642
+ }
3643
+
3644
+ class CloudflareD1Storage extends BaseMemoryStorage {
3645
+ constructor(config) {
3646
+ super();
3647
+ this.binding = null;
3648
+ this.tableInitialized = false;
3649
+ this.config = config;
3650
+ this.tableName = config.tableName || 'sekuire_memory';
3651
+ if (config.binding) {
3652
+ this.binding = config.binding;
3653
+ this.useRestApi = false;
3654
+ this._connected = true;
3655
+ }
3656
+ else if (config.accountId && config.databaseId && config.apiToken) {
3657
+ this.useRestApi = true;
3658
+ this._connected = true;
3659
+ }
3660
+ else {
3661
+ throw new Error('CloudflareD1Config requires either a binding (in Workers) or accountId, databaseId, and apiToken (for REST API)');
3662
+ }
3663
+ }
3664
+ async executeQuery(sql, params = []) {
3665
+ if (!this.useRestApi) {
3666
+ const stmt = this.binding.prepare(sql);
3667
+ if (params.length > 0) {
3668
+ return stmt.bind(...params).all();
3669
+ }
3670
+ return stmt.all();
3671
+ }
3672
+ const { accountId, databaseId, apiToken } = this.config;
3673
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`;
3674
+ const response = await fetch(url, {
3675
+ method: 'POST',
3676
+ headers: {
3677
+ Authorization: `Bearer ${apiToken}`,
3678
+ 'Content-Type': 'application/json',
3679
+ },
3680
+ body: JSON.stringify({ sql, params }),
3681
+ });
3682
+ if (!response.ok) {
3683
+ const errorText = await response.text();
3684
+ throw new Error(`D1 query failed: ${response.statusText} - ${errorText}`);
3685
+ }
3686
+ const result = await response.json();
3687
+ if (!result.success) {
3688
+ throw new Error(`D1 query failed: ${JSON.stringify(result.errors)}`);
3689
+ }
3690
+ return result.result?.[0] || { results: [] };
3691
+ }
3692
+ async ensureTableExists() {
3693
+ if (this.tableInitialized)
3694
+ return;
3695
+ const createTableSQL = `
3696
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
3697
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3698
+ session_id TEXT NOT NULL,
3699
+ role TEXT NOT NULL,
3700
+ content TEXT NOT NULL,
3701
+ timestamp INTEGER NOT NULL,
3702
+ metadata TEXT,
3703
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
3704
+ )
3705
+ `;
3706
+ const createIndexSQL = `
3707
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_session_id ON ${this.tableName}(session_id)
3708
+ `;
3709
+ await this.executeQuery(createTableSQL);
3710
+ await this.executeQuery(createIndexSQL);
3711
+ this.tableInitialized = true;
3712
+ }
3713
+ async add(sessionId, message) {
3714
+ await this.ensureTableExists();
3715
+ const sql = `
3716
+ INSERT INTO ${this.tableName} (session_id, role, content, timestamp, metadata)
3717
+ VALUES (?, ?, ?, ?, ?)
3718
+ `;
3719
+ await this.executeQuery(sql, [
3720
+ sessionId,
3721
+ message.role,
3722
+ message.content,
3723
+ message.timestamp,
3724
+ message.metadata ? JSON.stringify(message.metadata) : null,
3725
+ ]);
3726
+ }
3727
+ async get(sessionId, limit) {
3728
+ await this.ensureTableExists();
3729
+ let sql = `
3730
+ SELECT role, content, timestamp, metadata
3731
+ FROM ${this.tableName}
3732
+ WHERE session_id = ?
3733
+ ORDER BY timestamp ASC
3734
+ `;
3735
+ if (limit) {
3736
+ sql += ` LIMIT ${limit}`;
3737
+ }
3738
+ const result = await this.executeQuery(sql, [sessionId]);
3739
+ return (result.results || []).map((row) => ({
3740
+ role: row.role,
3741
+ content: row.content,
3742
+ timestamp: row.timestamp,
3743
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
3744
+ }));
3745
+ }
3746
+ async clear(sessionId) {
3747
+ await this.ensureTableExists();
3748
+ const sql = `DELETE FROM ${this.tableName} WHERE session_id = ?`;
3749
+ await this.executeQuery(sql, [sessionId]);
3750
+ }
3751
+ async delete(sessionId) {
3752
+ await this.clear(sessionId);
3753
+ }
3754
+ async exists(sessionId) {
3755
+ await this.ensureTableExists();
3756
+ const sql = `SELECT COUNT(*) as count FROM ${this.tableName} WHERE session_id = ?`;
3757
+ const result = await this.executeQuery(sql, [sessionId]);
3758
+ return (result.results?.[0]?.count || 0) > 0;
3759
+ }
3760
+ async disconnect() {
3761
+ this._connected = false;
3762
+ }
3763
+ }
3764
+
3765
+ class DynamoDBStorage extends BaseMemoryStorage {
3766
+ constructor(config) {
3767
+ super();
3768
+ this.client = null;
3769
+ this.docClient = null;
3770
+ this.connectionPromise = null;
3771
+ this.tableReady = false;
3772
+ this.config = config;
3773
+ this.connectionPromise = this.connect();
3774
+ }
3775
+ async connect() {
3776
+ if (this._connected)
3777
+ return;
3778
+ if (this.connectionPromise && !this._connected)
3779
+ return this.connectionPromise;
3780
+ try {
3781
+ const { DynamoDBClient, CreateTableCommand, DescribeTableCommand } = await import('@aws-sdk/client-dynamodb');
3782
+ const { DynamoDBDocumentClient, PutCommand, QueryCommand, DeleteCommand } = await import('@aws-sdk/lib-dynamodb');
3783
+ const clientConfig = {
3784
+ region: this.config.region || process.env.AWS_REGION || 'us-east-1',
3785
+ };
3786
+ if (this.config.endpoint) {
3787
+ clientConfig.endpoint = this.config.endpoint;
3788
+ }
3789
+ if (this.config.credentials) {
3790
+ clientConfig.credentials = this.config.credentials;
3791
+ }
3792
+ this.client = new DynamoDBClient(clientConfig);
3793
+ this.docClient = DynamoDBDocumentClient.from(this.client);
3794
+ if (this.config.createTable) {
3795
+ await this.ensureTableExists();
3796
+ }
3797
+ this._connected = true;
3798
+ }
3799
+ catch (error) {
3800
+ this.connectionPromise = null;
3801
+ throw new Error(`Failed to initialize DynamoDB: ${error}`);
3802
+ }
3803
+ }
3804
+ async ensureTableExists() {
3805
+ if (this.tableReady)
3806
+ return;
3807
+ try {
3808
+ const { DescribeTableCommand } = await import('@aws-sdk/client-dynamodb');
3809
+ await this.client.send(new DescribeTableCommand({ TableName: this.config.tableName }));
3810
+ this.tableReady = true;
3811
+ return;
3812
+ }
3813
+ catch (error) {
3814
+ if (error.name !== 'ResourceNotFoundException') {
3815
+ throw error;
3816
+ }
3817
+ }
3818
+ const { CreateTableCommand } = await import('@aws-sdk/client-dynamodb');
3819
+ await this.client.send(new CreateTableCommand({
3820
+ TableName: this.config.tableName,
3821
+ KeySchema: [
3822
+ { AttributeName: 'sessionId', KeyType: 'HASH' },
3823
+ { AttributeName: 'timestamp', KeyType: 'RANGE' },
3824
+ ],
3825
+ AttributeDefinitions: [
3826
+ { AttributeName: 'sessionId', AttributeType: 'S' },
3827
+ { AttributeName: 'timestamp', AttributeType: 'N' },
3828
+ ],
3829
+ BillingMode: 'PAY_PER_REQUEST',
3830
+ }));
3831
+ const { waitUntilTableExists } = await import('@aws-sdk/client-dynamodb');
3832
+ await waitUntilTableExists({ client: this.client, maxWaitTime: 120 }, { TableName: this.config.tableName });
3833
+ this.tableReady = true;
3834
+ }
3835
+ async ensureConnected() {
3836
+ if (!this._connected) {
3837
+ await this.connect();
3838
+ }
3839
+ }
3840
+ async add(sessionId, message) {
3841
+ await this.ensureConnected();
3842
+ const { PutCommand } = await import('@aws-sdk/lib-dynamodb');
3843
+ await this.docClient.send(new PutCommand({
3844
+ TableName: this.config.tableName,
3845
+ Item: {
3846
+ sessionId,
3847
+ timestamp: message.timestamp,
3848
+ role: message.role,
3849
+ content: message.content,
3850
+ metadata: message.metadata || {},
3851
+ },
3852
+ }));
3853
+ }
3854
+ async get(sessionId, limit) {
3855
+ await this.ensureConnected();
3856
+ const { QueryCommand } = await import('@aws-sdk/lib-dynamodb');
3857
+ const params = {
3858
+ TableName: this.config.tableName,
3859
+ KeyConditionExpression: 'sessionId = :sid',
3860
+ ExpressionAttributeValues: {
3861
+ ':sid': sessionId,
3862
+ },
3863
+ ScanIndexForward: true,
3864
+ };
3865
+ if (limit) {
3866
+ params.Limit = limit;
3867
+ params.ScanIndexForward = false;
3868
+ }
3869
+ const result = await this.docClient.send(new QueryCommand(params));
3870
+ const items = result.Items || [];
3871
+ const messages = items.map((item) => ({
3872
+ role: item.role,
3873
+ content: item.content,
3874
+ timestamp: item.timestamp,
3875
+ metadata: item.metadata && Object.keys(item.metadata).length > 0 ? item.metadata : undefined,
3876
+ }));
3877
+ if (limit) {
3878
+ messages.reverse();
3879
+ }
3880
+ return messages;
3881
+ }
3882
+ async clear(sessionId) {
3883
+ await this.ensureConnected();
3884
+ const messages = await this.get(sessionId);
3885
+ const { DeleteCommand } = await import('@aws-sdk/lib-dynamodb');
3886
+ for (const message of messages) {
3887
+ await this.docClient.send(new DeleteCommand({
3888
+ TableName: this.config.tableName,
3889
+ Key: {
3890
+ sessionId,
3891
+ timestamp: message.timestamp,
3892
+ },
3893
+ }));
3894
+ }
3895
+ }
3896
+ async delete(sessionId) {
3897
+ await this.clear(sessionId);
3898
+ }
3899
+ async exists(sessionId) {
3900
+ await this.ensureConnected();
3901
+ const { QueryCommand } = await import('@aws-sdk/lib-dynamodb');
3902
+ const result = await this.docClient.send(new QueryCommand({
3903
+ TableName: this.config.tableName,
3904
+ KeyConditionExpression: 'sessionId = :sid',
3905
+ ExpressionAttributeValues: {
3906
+ ':sid': sessionId,
3907
+ },
3908
+ Limit: 1,
3909
+ Select: 'COUNT',
3910
+ }));
3911
+ return (result.Count || 0) > 0;
3912
+ }
3913
+ async disconnect() {
3914
+ if (this._connected && this.client) {
3915
+ this.client.destroy();
3916
+ this._connected = false;
3917
+ this.connectionPromise = null;
3918
+ }
3919
+ }
3920
+ }
3921
+
3922
+ class TursoStorage extends BaseMemoryStorage {
3923
+ constructor(config) {
3924
+ super();
3925
+ this.client = null;
3926
+ this.connectionPromise = null;
3927
+ this.tableInitialized = false;
3928
+ this.config = config;
3929
+ this.tableName = config.tableName || 'sekuire_memory';
3930
+ this.connectionPromise = this.connect();
3931
+ }
3932
+ async connect() {
3933
+ if (this._connected)
3934
+ return;
3935
+ if (this.connectionPromise && !this._connected)
3936
+ return this.connectionPromise;
3937
+ try {
3938
+ const { createClient } = await import('@libsql/client');
3939
+ this.client = createClient({
3940
+ url: this.config.url,
3941
+ authToken: this.config.authToken,
3942
+ });
3943
+ await this.ensureTableExists();
3944
+ this._connected = true;
3945
+ }
3946
+ catch (error) {
3947
+ this.connectionPromise = null;
3948
+ throw new Error(`Failed to initialize Turso: ${error}`);
3949
+ }
3950
+ }
3951
+ async ensureTableExists() {
3952
+ if (this.tableInitialized)
3953
+ return;
3954
+ await this.client.execute(`
3955
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
3956
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3957
+ session_id TEXT NOT NULL,
3958
+ role TEXT NOT NULL,
3959
+ content TEXT NOT NULL,
3960
+ timestamp INTEGER NOT NULL,
3961
+ metadata TEXT,
3962
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
3963
+ )
3964
+ `);
3965
+ await this.client.execute(`
3966
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_session_id ON ${this.tableName}(session_id)
3967
+ `);
3968
+ this.tableInitialized = true;
3969
+ }
3970
+ async ensureConnected() {
3971
+ if (!this._connected) {
3972
+ await this.connect();
3973
+ }
3974
+ }
3975
+ async add(sessionId, message) {
3976
+ await this.ensureConnected();
3977
+ await this.client.execute({
3978
+ sql: `
3979
+ INSERT INTO ${this.tableName} (session_id, role, content, timestamp, metadata)
3980
+ VALUES (?, ?, ?, ?, ?)
3981
+ `,
3982
+ args: [
3983
+ sessionId,
3984
+ message.role,
3985
+ message.content,
3986
+ message.timestamp,
3987
+ message.metadata ? JSON.stringify(message.metadata) : null,
3988
+ ],
3989
+ });
3990
+ }
3991
+ async get(sessionId, limit) {
3992
+ await this.ensureConnected();
3993
+ let sql = `
3994
+ SELECT role, content, timestamp, metadata
3995
+ FROM ${this.tableName}
3996
+ WHERE session_id = ?
3997
+ ORDER BY timestamp ASC
3998
+ `;
3999
+ if (limit) {
4000
+ sql += ` LIMIT ${limit}`;
4001
+ }
4002
+ const result = await this.client.execute({
4003
+ sql,
4004
+ args: [sessionId],
4005
+ });
4006
+ return result.rows.map((row) => ({
4007
+ role: row.role,
4008
+ content: row.content,
4009
+ timestamp: Number(row.timestamp),
4010
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
4011
+ }));
4012
+ }
4013
+ async clear(sessionId) {
4014
+ await this.ensureConnected();
4015
+ await this.client.execute({
4016
+ sql: `DELETE FROM ${this.tableName} WHERE session_id = ?`,
4017
+ args: [sessionId],
4018
+ });
4019
+ }
4020
+ async delete(sessionId) {
4021
+ await this.clear(sessionId);
4022
+ }
4023
+ async exists(sessionId) {
4024
+ await this.ensureConnected();
4025
+ const result = await this.client.execute({
4026
+ sql: `SELECT COUNT(*) as count FROM ${this.tableName} WHERE session_id = ?`,
4027
+ args: [sessionId],
4028
+ });
4029
+ return Number(result.rows[0]?.count || 0) > 0;
4030
+ }
4031
+ async disconnect() {
4032
+ if (this._connected && this.client) {
4033
+ this.client.close();
4034
+ this._connected = false;
4035
+ this.connectionPromise = null;
4036
+ }
4037
+ }
4038
+ }
4039
+
4040
+ class ConvexStorage extends BaseMemoryStorage {
4041
+ constructor(config) {
4042
+ super();
4043
+ this.client = null;
4044
+ this.connectionPromise = null;
4045
+ this.config = config;
4046
+ this.connectionPromise = this.connect();
4047
+ }
4048
+ async connect() {
4049
+ if (this._connected)
4050
+ return;
4051
+ if (this.connectionPromise && !this._connected)
4052
+ return this.connectionPromise;
4053
+ try {
4054
+ const { ConvexHttpClient } = await import('convex/browser');
4055
+ this.client = new ConvexHttpClient(this.config.url);
4056
+ this._connected = true;
4057
+ }
4058
+ catch (error) {
4059
+ this.connectionPromise = null;
4060
+ throw new Error(`Failed to initialize Convex: ${error}`);
4061
+ }
4062
+ }
4063
+ async ensureConnected() {
4064
+ if (!this._connected) {
4065
+ await this.connect();
4066
+ }
4067
+ }
4068
+ async add(sessionId, message) {
4069
+ await this.ensureConnected();
4070
+ await this.client.mutation('sekuireMemory:add', {
4071
+ sessionId,
4072
+ role: message.role,
4073
+ content: message.content,
4074
+ timestamp: message.timestamp,
4075
+ metadata: message.metadata || {},
4076
+ });
4077
+ }
4078
+ async get(sessionId, limit) {
4079
+ await this.ensureConnected();
4080
+ const messages = await this.client.query('sekuireMemory:get', {
4081
+ sessionId,
4082
+ limit,
4083
+ });
4084
+ return messages.map((msg) => ({
4085
+ role: msg.role,
4086
+ content: msg.content,
4087
+ timestamp: msg.timestamp,
4088
+ metadata: msg.metadata && Object.keys(msg.metadata).length > 0 ? msg.metadata : undefined,
4089
+ }));
4090
+ }
4091
+ async clear(sessionId) {
4092
+ await this.ensureConnected();
4093
+ await this.client.mutation('sekuireMemory:clear', { sessionId });
4094
+ }
4095
+ async delete(sessionId) {
4096
+ await this.clear(sessionId);
4097
+ }
4098
+ async exists(sessionId) {
4099
+ await this.ensureConnected();
4100
+ return this.client.query('sekuireMemory:exists', { sessionId });
4101
+ }
4102
+ async disconnect() {
4103
+ this._connected = false;
4104
+ this.client = null;
4105
+ this.connectionPromise = null;
4106
+ }
4107
+ }
4108
+ const CONVEX_SCHEMA_TEMPLATE = `
4109
+ // convex/schema.ts
4110
+ import { defineSchema, defineTable } from "convex/server";
4111
+ import { v } from "convex/values";
4112
+
4113
+ export default defineSchema({
4114
+ sekuireMemory: defineTable({
4115
+ sessionId: v.string(),
4116
+ role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")),
4117
+ content: v.string(),
4118
+ timestamp: v.number(),
4119
+ metadata: v.optional(v.any()),
4120
+ }).index("by_session", ["sessionId", "timestamp"]),
4121
+ });
4122
+ `;
4123
+ const CONVEX_FUNCTIONS_TEMPLATE = `
4124
+ // convex/sekuireMemory.ts
4125
+ import { mutation, query } from "./_generated/server";
4126
+ import { v } from "convex/values";
4127
+
4128
+ export const add = mutation({
4129
+ args: {
4130
+ sessionId: v.string(),
4131
+ role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")),
4132
+ content: v.string(),
4133
+ timestamp: v.number(),
4134
+ metadata: v.optional(v.any()),
4135
+ },
4136
+ handler: async (ctx, args) => {
4137
+ await ctx.db.insert("sekuireMemory", args);
4138
+ },
4139
+ });
4140
+
4141
+ export const get = query({
4142
+ args: {
4143
+ sessionId: v.string(),
4144
+ limit: v.optional(v.number()),
4145
+ },
4146
+ handler: async (ctx, args) => {
4147
+ let query = ctx.db
4148
+ .query("sekuireMemory")
4149
+ .withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
4150
+ .order("asc");
4151
+
4152
+ const messages = await query.collect();
4153
+
4154
+ if (args.limit) {
4155
+ return messages.slice(-args.limit);
4156
+ }
4157
+ return messages;
4158
+ },
4159
+ });
4160
+
4161
+ export const clear = mutation({
4162
+ args: {
4163
+ sessionId: v.string(),
4164
+ },
4165
+ handler: async (ctx, args) => {
4166
+ const messages = await ctx.db
4167
+ .query("sekuireMemory")
4168
+ .withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
4169
+ .collect();
4170
+
4171
+ for (const message of messages) {
4172
+ await ctx.db.delete(message._id);
4173
+ }
4174
+ },
4175
+ });
4176
+
4177
+ export const exists = query({
4178
+ args: {
4179
+ sessionId: v.string(),
4180
+ },
4181
+ handler: async (ctx, args) => {
4182
+ const message = await ctx.db
4183
+ .query("sekuireMemory")
4184
+ .withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
4185
+ .first();
4186
+ return message !== null;
4187
+ },
4188
+ });
4189
+ `;
4190
+
4191
+ const storageRegistry = new Map();
4192
+ function registerStorage(type, factory, description) {
4193
+ if (storageRegistry.has(type)) {
4194
+ console.warn(`Storage type '${type}' is already registered. Overwriting.`);
4195
+ }
4196
+ storageRegistry.set(type, { factory, description });
4197
+ }
4198
+ function getStorageFactory(type) {
4199
+ const registration = storageRegistry.get(type);
4200
+ return registration?.factory;
4201
+ }
4202
+ function hasStorage(type) {
4203
+ return storageRegistry.has(type);
4204
+ }
4205
+ function listStorageTypes() {
4206
+ return Array.from(storageRegistry.keys());
4207
+ }
4208
+ function getStorageInfo() {
4209
+ return Array.from(storageRegistry.entries()).map(([type, reg]) => ({
4210
+ type,
4211
+ description: reg.description,
4212
+ }));
4213
+ }
4214
+
4215
+ registerStorage('in-memory', () => new InMemoryStorage(), 'Simple in-memory storage using Map');
4216
+ registerStorage('buffer', () => new InMemoryStorage(), 'Alias for in-memory storage');
4217
+ registerStorage('redis', (config) => {
4218
+ if (!config.redis) {
4219
+ throw new Error('Redis config required for redis memory type');
4220
+ }
4221
+ return new RedisStorage(config.redis);
4222
+ }, 'Self-hosted Redis for persistence and scaling');
4223
+ registerStorage('postgres', (config) => {
4224
+ if (!config.postgres) {
4225
+ throw new Error('Postgres config required for postgres memory type');
4226
+ }
4227
+ return new PostgresStorage(config.postgres);
4228
+ }, 'PostgreSQL with full SQL capabilities');
4229
+ registerStorage('sqlite', (config) => {
4230
+ if (!config.sqlite) {
4231
+ throw new Error('SQLite config required for sqlite memory type');
4232
+ }
4233
+ return new SQLiteStorage(config.sqlite);
4234
+ }, 'Local SQLite database for development and single-node deployments');
4235
+ registerStorage('upstash', (config) => {
4236
+ if (!config.upstash) {
4237
+ throw new Error('Upstash config required for upstash memory type');
4238
+ }
4239
+ return new UpstashStorage(config.upstash);
4240
+ }, 'Upstash Redis - serverless Redis with HTTP API');
4241
+ registerStorage('cloudflare-kv', (config) => {
4242
+ if (!config.cloudflareKV) {
4243
+ throw new Error('CloudflareKV config required for cloudflare-kv memory type');
4244
+ }
4245
+ return new CloudflareKVStorage(config.cloudflareKV);
4246
+ }, 'Cloudflare KV - edge key-value storage');
4247
+ registerStorage('cloudflare-d1', (config) => {
4248
+ if (!config.cloudflareD1) {
4249
+ throw new Error('CloudflareD1 config required for cloudflare-d1 memory type');
4250
+ }
4251
+ return new CloudflareD1Storage(config.cloudflareD1);
4252
+ }, 'Cloudflare D1 - edge SQLite database');
4253
+ registerStorage('dynamodb', (config) => {
4254
+ if (!config.dynamodb) {
4255
+ throw new Error('DynamoDB config required for dynamodb memory type');
4256
+ }
4257
+ return new DynamoDBStorage(config.dynamodb);
4258
+ }, 'AWS DynamoDB - serverless NoSQL database');
4259
+ registerStorage('turso', (config) => {
4260
+ if (!config.turso) {
4261
+ throw new Error('Turso config required for turso memory type');
4262
+ }
4263
+ return new TursoStorage(config.turso);
4264
+ }, 'Turso - edge SQLite with libSQL');
4265
+ registerStorage('convex', (config) => {
4266
+ if (!config.convex) {
4267
+ throw new Error('Convex config required for convex memory type');
4268
+ }
4269
+ return new ConvexStorage(config.convex);
4270
+ }, 'Convex - real-time backend platform');
4271
+ function createMemoryStorage(config) {
4272
+ if (config.instance) {
4273
+ return config.instance;
4274
+ }
4275
+ const factory = getStorageFactory(config.type);
4276
+ if (factory) {
4277
+ return factory(config);
4278
+ }
4279
+ throw new Error(`Unknown memory type: ${config.type}. Available types: ${listStorageTypes().join(', ')}`);
4280
+ }
4281
+
4282
+ const DEFAULT_OVERRIDE_ENV = "SEKUIRE_POLICY_DEV_OVERRIDE";
4283
+ class PolicyEnforcer {
4284
+ constructor(policy, override, onViolation) {
4285
+ this.policy = policy;
4286
+ this.onViolation = onViolation;
4287
+ const envOverride = process.env[DEFAULT_OVERRIDE_ENV] === "true";
4288
+ this.override = override ?? envOverride;
4289
+ if (this.override) {
4290
+ console.warn("[policy] override enabled; policy enforcement is running in warn-only mode");
4291
+ }
4292
+ }
4293
+ enforceNetwork(domain, protocol) {
4294
+ const perms = this.policy.content?.permissions?.network;
4295
+ if (!perms) {
4296
+ this.warnOnly("network.missing", "Network permissions not configured");
4297
+ return;
4298
+ }
4299
+ if (!perms.enabled)
4300
+ this.throw("network.disabled", "Network access is disabled");
4301
+ if (perms.require_tls && protocol !== "https") {
4302
+ this.throw("network.tls_required", "TLS is required for network requests");
4303
+ }
4304
+ if (perms.blocked_domains && this.matches(domain, perms.blocked_domains)) {
4305
+ this.throw("network.blocked", `Domain ${domain} is blocked by policy`);
4306
+ }
4307
+ if (perms.allowed_domains && !this.matches(domain, perms.allowed_domains)) {
4308
+ this.throw("network.not_allowed", `Domain ${domain} is not in the allowlist`);
4309
+ }
4310
+ }
4311
+ enforceFilesystem(path, operation) {
4312
+ const perms = this.policy.content?.permissions?.filesystem;
4313
+ if (!perms) {
4314
+ this.warnOnly("fs.missing", "Filesystem permissions not configured");
4315
+ return;
4316
+ }
4317
+ if (!perms.enabled)
4318
+ this.throw("fs.disabled", "Filesystem access is disabled");
4319
+ if (perms.blocked_paths && this.pathMatch(path, perms.blocked_paths)) {
4320
+ this.throw("fs.blocked", `Path ${path} is blocked`);
4321
+ }
4322
+ if (perms.allowed_paths && !this.pathMatch(path, perms.allowed_paths)) {
4323
+ this.throw("fs.not_allowed", `Path ${path} is not in the allowlist`);
4324
+ }
4325
+ if (perms.allowed_extensions) {
4326
+ const ext = path.includes(".") ? `.${path.split(".").pop()}` : "";
4327
+ if (ext && !perms.allowed_extensions.includes(ext)) {
4328
+ this.throw("fs.ext", `Extension ${ext} is not allowed`);
4329
+ }
4330
+ }
4331
+ }
4332
+ enforceTool(toolName) {
4333
+ const tools = this.policy.content?.tools;
4334
+ // Check blocked tools
4335
+ if (tools?.blocked_tools?.includes(toolName)) {
4336
+ this.throw("tool.blocked", `Tool ${toolName} is blocked`);
4337
+ }
4338
+ // If no allowlist, allow all tools
4339
+ if (!tools?.allowed_tools || tools.allowed_tools.length === 0) {
4340
+ return;
4341
+ }
4342
+ // Check allowlist with pattern matching
4343
+ const isAllowed = tools.allowed_tools.some((t) => {
4344
+ const pattern = t.name;
4345
+ // Exact match
4346
+ if (pattern === toolName) {
4347
+ return true;
4348
+ }
4349
+ // Category patterns: "files:*" => matches "file_read", "file_write", etc
4350
+ // Check this BEFORE wildcard matching since it contains both : and *
4351
+ if (pattern.includes(":")) {
4352
+ const [category, ops] = pattern.split(":", 2);
4353
+ const categoryPrefix = this.getCategoryPrefix(category);
4354
+ if (ops === "*") {
4355
+ // Match all tools in category
4356
+ const matches = toolName.startsWith(categoryPrefix + "_");
4357
+ return matches;
4358
+ }
4359
+ if (ops.startsWith("[") && ops.endsWith("]")) {
4360
+ // Match specific operations: "files:[read,write]"
4361
+ const operations = ops.slice(1, -1).split(",").map(s => s.trim());
4362
+ const matches = operations.some(op => toolName === `${categoryPrefix}_${op}`);
4363
+ console.log(`[DEBUG] ${matches ? '✓' : '✗'} Category ops: ${operations.join(",")}`);
4364
+ return matches;
4365
+ }
4366
+ }
4367
+ // Pattern matching: "file_*", etc (simple wildcards without :)
4368
+ if (pattern.includes("*")) {
4369
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
4370
+ const matches = regex.test(toolName);
4371
+ return matches;
4372
+ }
4373
+ return false;
4374
+ });
4375
+ if (!isAllowed) {
4376
+ this.throw("tool.not_allowed", `Tool ${toolName} is not in the allowlist`);
4377
+ }
4378
+ }
4379
+ getCategoryPrefix(category) {
4380
+ const prefixMap = {
4381
+ files: "file",
4382
+ directories: "dir",
4383
+ network: "http",
4384
+ data: "json",
4385
+ system: "env",
4386
+ };
4387
+ return prefixMap[category] || category;
4388
+ }
4389
+ enforceModel(model) {
4390
+ const models = this.policy.content?.agent?.models;
4391
+ if (models?.allowed_models && !models.allowed_models.includes(model)) {
4392
+ this.throw("model.not_allowed", `Model ${model} is not allowed`);
4393
+ }
4394
+ if (models?.blocked_models && models.blocked_models.includes(model)) {
4395
+ this.throw("model.blocked", `Model ${model} is blocked`);
4396
+ }
4397
+ }
4398
+ enforceApi(service) {
4399
+ const perms = this.policy.content?.permissions?.api;
4400
+ if (!perms) {
4401
+ this.warnOnly("api.missing", "API permissions not configured");
4402
+ return;
4403
+ }
4404
+ if (!perms.enabled)
4405
+ this.throw("api.disabled", "API access is disabled");
4406
+ if (perms.allowed_services &&
4407
+ !perms.allowed_services.some((s) => s.service_name === service)) {
4408
+ this.throw("api.not_allowed", `API service ${service} is not in the allowlist`);
4409
+ }
4410
+ }
4411
+ enforceRateLimit(type, count = 1) {
4412
+ const limits = this.policy.content?.rate_limits?.per_agent;
4413
+ if (!limits)
4414
+ return;
4415
+ // Simple local enforcement (note: this does not sync across instances)
4416
+ // TODO: Implement proper token bucket or sliding window
4417
+ if (type === "request" && limits.requests_per_minute) ;
4418
+ }
4419
+ throw(rule, reason) {
4420
+ if (this.override) {
4421
+ this.warnOnly(rule, reason);
4422
+ return;
4423
+ }
4424
+ if (this.onViolation)
4425
+ this.onViolation(rule, reason);
4426
+ throw new PolicyViolationError(reason, rule);
4427
+ }
4428
+ warnOnly(rule, reason) {
4429
+ if (this.onViolation)
4430
+ this.onViolation(rule, reason);
4431
+ console.warn(`[policy][${rule}] ${reason}`);
3452
4432
  }
3453
4433
  matches(domain, patterns) {
3454
4434
  return patterns.some((pattern) => {
@@ -3907,7 +4887,7 @@ var __spreadArray$5 = (undefined && undefined.__spreadArray) || function (to, fr
3907
4887
  }
3908
4888
  return to.concat(ar || Array.prototype.slice.call(from));
3909
4889
  };
3910
- var API_NAME$3 = 'diag';
4890
+ var API_NAME$4 = 'diag';
3911
4891
  /**
3912
4892
  * Singleton object which represents the entry point to the OpenTelemetry internal
3913
4893
  * diagnostic API
@@ -3962,7 +4942,7 @@ var DiagAPI = /** @class */ (function () {
3962
4942
  };
3963
4943
  self.setLogger = setLogger;
3964
4944
  self.disable = function () {
3965
- unregisterGlobal(API_NAME$3, self);
4945
+ unregisterGlobal(API_NAME$4, self);
3966
4946
  };
3967
4947
  self.createComponentLogger = function (options) {
3968
4948
  return new DiagComponentLogger(options);
@@ -4196,6 +5176,171 @@ var BaseContext = /** @class */ (function () {
4196
5176
  /** The root context is used as the default parent context when there is no active context */
4197
5177
  var ROOT_CONTEXT = new BaseContext();
4198
5178
 
5179
+ /*
5180
+ * Copyright The OpenTelemetry Authors
5181
+ *
5182
+ * Licensed under the Apache License, Version 2.0 (the "License");
5183
+ * you may not use this file except in compliance with the License.
5184
+ * You may obtain a copy of the License at
5185
+ *
5186
+ * https://www.apache.org/licenses/LICENSE-2.0
5187
+ *
5188
+ * Unless required by applicable law or agreed to in writing, software
5189
+ * distributed under the License is distributed on an "AS IS" BASIS,
5190
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5191
+ * See the License for the specific language governing permissions and
5192
+ * limitations under the License.
5193
+ */
5194
+ var __extends$3 = (undefined && undefined.__extends) || (function () {
5195
+ var extendStatics = function (d, b) {
5196
+ extendStatics = Object.setPrototypeOf ||
5197
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5198
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
5199
+ return extendStatics(d, b);
5200
+ };
5201
+ return function (d, b) {
5202
+ if (typeof b !== "function" && b !== null)
5203
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
5204
+ extendStatics(d, b);
5205
+ function __() { this.constructor = d; }
5206
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
5207
+ };
5208
+ })();
5209
+ /**
5210
+ * NoopMeter is a noop implementation of the {@link Meter} interface. It reuses
5211
+ * constant NoopMetrics for all of its methods.
5212
+ */
5213
+ var NoopMeter = /** @class */ (function () {
5214
+ function NoopMeter() {
5215
+ }
5216
+ /**
5217
+ * @see {@link Meter.createGauge}
5218
+ */
5219
+ NoopMeter.prototype.createGauge = function (_name, _options) {
5220
+ return NOOP_GAUGE_METRIC;
5221
+ };
5222
+ /**
5223
+ * @see {@link Meter.createHistogram}
5224
+ */
5225
+ NoopMeter.prototype.createHistogram = function (_name, _options) {
5226
+ return NOOP_HISTOGRAM_METRIC;
5227
+ };
5228
+ /**
5229
+ * @see {@link Meter.createCounter}
5230
+ */
5231
+ NoopMeter.prototype.createCounter = function (_name, _options) {
5232
+ return NOOP_COUNTER_METRIC;
5233
+ };
5234
+ /**
5235
+ * @see {@link Meter.createUpDownCounter}
5236
+ */
5237
+ NoopMeter.prototype.createUpDownCounter = function (_name, _options) {
5238
+ return NOOP_UP_DOWN_COUNTER_METRIC;
5239
+ };
5240
+ /**
5241
+ * @see {@link Meter.createObservableGauge}
5242
+ */
5243
+ NoopMeter.prototype.createObservableGauge = function (_name, _options) {
5244
+ return NOOP_OBSERVABLE_GAUGE_METRIC;
5245
+ };
5246
+ /**
5247
+ * @see {@link Meter.createObservableCounter}
5248
+ */
5249
+ NoopMeter.prototype.createObservableCounter = function (_name, _options) {
5250
+ return NOOP_OBSERVABLE_COUNTER_METRIC;
5251
+ };
5252
+ /**
5253
+ * @see {@link Meter.createObservableUpDownCounter}
5254
+ */
5255
+ NoopMeter.prototype.createObservableUpDownCounter = function (_name, _options) {
5256
+ return NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC;
5257
+ };
5258
+ /**
5259
+ * @see {@link Meter.addBatchObservableCallback}
5260
+ */
5261
+ NoopMeter.prototype.addBatchObservableCallback = function (_callback, _observables) { };
5262
+ /**
5263
+ * @see {@link Meter.removeBatchObservableCallback}
5264
+ */
5265
+ NoopMeter.prototype.removeBatchObservableCallback = function (_callback) { };
5266
+ return NoopMeter;
5267
+ }());
5268
+ var NoopMetric = /** @class */ (function () {
5269
+ function NoopMetric() {
5270
+ }
5271
+ return NoopMetric;
5272
+ }());
5273
+ var NoopCounterMetric = /** @class */ (function (_super) {
5274
+ __extends$3(NoopCounterMetric, _super);
5275
+ function NoopCounterMetric() {
5276
+ return _super !== null && _super.apply(this, arguments) || this;
5277
+ }
5278
+ NoopCounterMetric.prototype.add = function (_value, _attributes) { };
5279
+ return NoopCounterMetric;
5280
+ }(NoopMetric));
5281
+ var NoopUpDownCounterMetric = /** @class */ (function (_super) {
5282
+ __extends$3(NoopUpDownCounterMetric, _super);
5283
+ function NoopUpDownCounterMetric() {
5284
+ return _super !== null && _super.apply(this, arguments) || this;
5285
+ }
5286
+ NoopUpDownCounterMetric.prototype.add = function (_value, _attributes) { };
5287
+ return NoopUpDownCounterMetric;
5288
+ }(NoopMetric));
5289
+ var NoopGaugeMetric = /** @class */ (function (_super) {
5290
+ __extends$3(NoopGaugeMetric, _super);
5291
+ function NoopGaugeMetric() {
5292
+ return _super !== null && _super.apply(this, arguments) || this;
5293
+ }
5294
+ NoopGaugeMetric.prototype.record = function (_value, _attributes) { };
5295
+ return NoopGaugeMetric;
5296
+ }(NoopMetric));
5297
+ var NoopHistogramMetric = /** @class */ (function (_super) {
5298
+ __extends$3(NoopHistogramMetric, _super);
5299
+ function NoopHistogramMetric() {
5300
+ return _super !== null && _super.apply(this, arguments) || this;
5301
+ }
5302
+ NoopHistogramMetric.prototype.record = function (_value, _attributes) { };
5303
+ return NoopHistogramMetric;
5304
+ }(NoopMetric));
5305
+ var NoopObservableMetric = /** @class */ (function () {
5306
+ function NoopObservableMetric() {
5307
+ }
5308
+ NoopObservableMetric.prototype.addCallback = function (_callback) { };
5309
+ NoopObservableMetric.prototype.removeCallback = function (_callback) { };
5310
+ return NoopObservableMetric;
5311
+ }());
5312
+ var NoopObservableCounterMetric = /** @class */ (function (_super) {
5313
+ __extends$3(NoopObservableCounterMetric, _super);
5314
+ function NoopObservableCounterMetric() {
5315
+ return _super !== null && _super.apply(this, arguments) || this;
5316
+ }
5317
+ return NoopObservableCounterMetric;
5318
+ }(NoopObservableMetric));
5319
+ var NoopObservableGaugeMetric = /** @class */ (function (_super) {
5320
+ __extends$3(NoopObservableGaugeMetric, _super);
5321
+ function NoopObservableGaugeMetric() {
5322
+ return _super !== null && _super.apply(this, arguments) || this;
5323
+ }
5324
+ return NoopObservableGaugeMetric;
5325
+ }(NoopObservableMetric));
5326
+ var NoopObservableUpDownCounterMetric = /** @class */ (function (_super) {
5327
+ __extends$3(NoopObservableUpDownCounterMetric, _super);
5328
+ function NoopObservableUpDownCounterMetric() {
5329
+ return _super !== null && _super.apply(this, arguments) || this;
5330
+ }
5331
+ return NoopObservableUpDownCounterMetric;
5332
+ }(NoopObservableMetric));
5333
+ var NOOP_METER = new NoopMeter();
5334
+ // Synchronous instruments
5335
+ var NOOP_COUNTER_METRIC = new NoopCounterMetric();
5336
+ var NOOP_GAUGE_METRIC = new NoopGaugeMetric();
5337
+ var NOOP_HISTOGRAM_METRIC = new NoopHistogramMetric();
5338
+ var NOOP_UP_DOWN_COUNTER_METRIC = new NoopUpDownCounterMetric();
5339
+ // Asynchronous instruments
5340
+ var NOOP_OBSERVABLE_COUNTER_METRIC = new NoopObservableCounterMetric();
5341
+ var NOOP_OBSERVABLE_GAUGE_METRIC = new NoopObservableGaugeMetric();
5342
+ var NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC = new NoopObservableUpDownCounterMetric();
5343
+
4199
5344
  /*
4200
5345
  * Copyright The OpenTelemetry Authors
4201
5346
  *
@@ -4339,7 +5484,7 @@ var __spreadArray$3 = (undefined && undefined.__spreadArray) || function (to, fr
4339
5484
  }
4340
5485
  return to.concat(ar || Array.prototype.slice.call(from));
4341
5486
  };
4342
- var API_NAME$2 = 'context';
5487
+ var API_NAME$3 = 'context';
4343
5488
  var NOOP_CONTEXT_MANAGER = new NoopContextManager();
4344
5489
  /**
4345
5490
  * Singleton object which represents the entry point to the OpenTelemetry Context API
@@ -4361,7 +5506,7 @@ var ContextAPI = /** @class */ (function () {
4361
5506
  * @returns true if the context manager was successfully registered, else false
4362
5507
  */
4363
5508
  ContextAPI.prototype.setGlobalContextManager = function (contextManager) {
4364
- return registerGlobal(API_NAME$2, contextManager, DiagAPI.instance());
5509
+ return registerGlobal(API_NAME$3, contextManager, DiagAPI.instance());
4365
5510
  };
4366
5511
  /**
4367
5512
  * Get the currently active context
@@ -4395,12 +5540,12 @@ var ContextAPI = /** @class */ (function () {
4395
5540
  return this._getContextManager().bind(context, target);
4396
5541
  };
4397
5542
  ContextAPI.prototype._getContextManager = function () {
4398
- return getGlobal(API_NAME$2) || NOOP_CONTEXT_MANAGER;
5543
+ return getGlobal(API_NAME$3) || NOOP_CONTEXT_MANAGER;
4399
5544
  };
4400
5545
  /** Disable and remove the global context manager */
4401
5546
  ContextAPI.prototype.disable = function () {
4402
5547
  this._getContextManager().disable();
4403
- unregisterGlobal(API_NAME$2, DiagAPI.instance());
5548
+ unregisterGlobal(API_NAME$3, DiagAPI.instance());
4404
5549
  };
4405
5550
  return ContextAPI;
4406
5551
  }());
@@ -4977,6 +6122,111 @@ var context = ContextAPI.getInstance();
4977
6122
  */
4978
6123
  var diag = DiagAPI.instance();
4979
6124
 
6125
+ /*
6126
+ * Copyright The OpenTelemetry Authors
6127
+ *
6128
+ * Licensed under the Apache License, Version 2.0 (the "License");
6129
+ * you may not use this file except in compliance with the License.
6130
+ * You may obtain a copy of the License at
6131
+ *
6132
+ * https://www.apache.org/licenses/LICENSE-2.0
6133
+ *
6134
+ * Unless required by applicable law or agreed to in writing, software
6135
+ * distributed under the License is distributed on an "AS IS" BASIS,
6136
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6137
+ * See the License for the specific language governing permissions and
6138
+ * limitations under the License.
6139
+ */
6140
+ /**
6141
+ * An implementation of the {@link MeterProvider} which returns an impotent Meter
6142
+ * for all calls to `getMeter`
6143
+ */
6144
+ var NoopMeterProvider = /** @class */ (function () {
6145
+ function NoopMeterProvider() {
6146
+ }
6147
+ NoopMeterProvider.prototype.getMeter = function (_name, _version, _options) {
6148
+ return NOOP_METER;
6149
+ };
6150
+ return NoopMeterProvider;
6151
+ }());
6152
+ var NOOP_METER_PROVIDER = new NoopMeterProvider();
6153
+
6154
+ /*
6155
+ * Copyright The OpenTelemetry Authors
6156
+ *
6157
+ * Licensed under the Apache License, Version 2.0 (the "License");
6158
+ * you may not use this file except in compliance with the License.
6159
+ * You may obtain a copy of the License at
6160
+ *
6161
+ * https://www.apache.org/licenses/LICENSE-2.0
6162
+ *
6163
+ * Unless required by applicable law or agreed to in writing, software
6164
+ * distributed under the License is distributed on an "AS IS" BASIS,
6165
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6166
+ * See the License for the specific language governing permissions and
6167
+ * limitations under the License.
6168
+ */
6169
+ var API_NAME$2 = 'metrics';
6170
+ /**
6171
+ * Singleton object which represents the entry point to the OpenTelemetry Metrics API
6172
+ */
6173
+ var MetricsAPI = /** @class */ (function () {
6174
+ /** Empty private constructor prevents end users from constructing a new instance of the API */
6175
+ function MetricsAPI() {
6176
+ }
6177
+ /** Get the singleton instance of the Metrics API */
6178
+ MetricsAPI.getInstance = function () {
6179
+ if (!this._instance) {
6180
+ this._instance = new MetricsAPI();
6181
+ }
6182
+ return this._instance;
6183
+ };
6184
+ /**
6185
+ * Set the current global meter provider.
6186
+ * Returns true if the meter provider was successfully registered, else false.
6187
+ */
6188
+ MetricsAPI.prototype.setGlobalMeterProvider = function (provider) {
6189
+ return registerGlobal(API_NAME$2, provider, DiagAPI.instance());
6190
+ };
6191
+ /**
6192
+ * Returns the global meter provider.
6193
+ */
6194
+ MetricsAPI.prototype.getMeterProvider = function () {
6195
+ return getGlobal(API_NAME$2) || NOOP_METER_PROVIDER;
6196
+ };
6197
+ /**
6198
+ * Returns a meter from the global meter provider.
6199
+ */
6200
+ MetricsAPI.prototype.getMeter = function (name, version, options) {
6201
+ return this.getMeterProvider().getMeter(name, version, options);
6202
+ };
6203
+ /** Remove the global meter provider */
6204
+ MetricsAPI.prototype.disable = function () {
6205
+ unregisterGlobal(API_NAME$2, DiagAPI.instance());
6206
+ };
6207
+ return MetricsAPI;
6208
+ }());
6209
+
6210
+ /*
6211
+ * Copyright The OpenTelemetry Authors
6212
+ *
6213
+ * Licensed under the Apache License, Version 2.0 (the "License");
6214
+ * you may not use this file except in compliance with the License.
6215
+ * You may obtain a copy of the License at
6216
+ *
6217
+ * https://www.apache.org/licenses/LICENSE-2.0
6218
+ *
6219
+ * Unless required by applicable law or agreed to in writing, software
6220
+ * distributed under the License is distributed on an "AS IS" BASIS,
6221
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6222
+ * See the License for the specific language governing permissions and
6223
+ * limitations under the License.
6224
+ */
6225
+ // Split module-level variable definition into separate files to allow
6226
+ // tree-shaking on each api instance.
6227
+ /** Entrypoint for metrics API */
6228
+ MetricsAPI.getInstance();
6229
+
4980
6230
  /*
4981
6231
  * Copyright The OpenTelemetry Authors
4982
6232
  *
@@ -15941,4 +17191,4 @@ function createDelegator(config) {
15941
17191
  return new A2ATaskDelegator(config);
15942
17192
  }
15943
17193
 
15944
- export { A2AClient, A2AError, A2AServer, A2ATaskDelegator, SekuireAgent as Agent, AgentIdentity, AnthropicProvider, Beacon, ComplianceError, ComplianceMonitor, ContentPolicyError, CryptoError, DEFAULT_API_URL, FileAccessError, GoogleProvider, InMemoryStorage, NetworkComplianceError, NetworkError, OllamaProvider, OpenAIProvider, PolicyClient, PolicyEnforcer, PolicyViolationError, PostgresStorage, ProtocolError, RedisStorage, SekuireAgent$1 as SekuireAgent, SekuireAgentBuilder, SekuireClient, SekuireCrypto, SekuireError, SekuireLogger, SekuireRegistryClient, SekuireSDK, SekuireServer, SekuireSpanExporter, TaskWorker, ToolPatternParser, ToolRegistry, ToolUsageError, builtInTools, calculateSekuireId, createAgent, createBeacon, createDefaultToolRegistry, createDelegationTool, createDelegator, createDiscoveryTool, createLLMProvider, createMemoryStorage, createRegistryClient, createSekuireClient, createSekuireExpressMiddleware, createSekuireFastifyPlugin, createWorker, detectDeploymentUrl, generateKeyPair, getAgent, getAgentConfig, getAgents, getTools$1 as getLegacyTools, getSystemPrompt, getTools, getTracer, initTelemetry, llm, loadConfig, loadSystemPrompt, loadTools, shutdownTelemetry, tool, tools };
17194
+ export { A2AClient, A2AError, A2AServer, A2ATaskDelegator, SekuireAgent as Agent, AgentIdentity, AnthropicProvider, BaseMemoryStorage, Beacon, CONVEX_FUNCTIONS_TEMPLATE, CONVEX_SCHEMA_TEMPLATE, CloudflareD1Storage, CloudflareKVStorage, ComplianceError, ComplianceMonitor, ContentPolicyError, ConvexStorage, CryptoError, DEFAULT_API_URL, DynamoDBStorage, FileAccessError, GoogleProvider, InMemoryStorage, NetworkComplianceError, NetworkError, OllamaProvider, OpenAIProvider, PolicyClient, PolicyEnforcer, PolicyViolationError, PostgresStorage, ProtocolError, RedisStorage, SQLiteStorage, SekuireAgent$1 as SekuireAgent, SekuireAgentBuilder, SekuireClient, SekuireCrypto, SekuireError, SekuireLogger, SekuireRegistryClient, SekuireSDK, SekuireServer, SekuireSpanExporter, TaskWorker, ToolPatternParser, ToolRegistry, ToolUsageError, TursoStorage, UpstashStorage, builtInTools, calculateSekuireId, createAgent, createBeacon, createDefaultToolRegistry, createDelegationTool, createDelegator, createDiscoveryTool, createLLMProvider, createMemoryStorage, createRegistryClient, createSekuireClient, createSekuireExpressMiddleware, createSekuireFastifyPlugin, createWorker, detectDeploymentUrl, generateKeyPair, getAgent, getAgentConfig, getAgents, getTools$1 as getLegacyTools, getStorageInfo, getSystemPrompt, getTools, getTracer, hasStorage, initTelemetry, listStorageTypes, llm, loadConfig, loadSystemPrompt, loadTools, registerStorage, shutdownTelemetry, tool, tools };