@kici-dev/shared 0.6.1 → 0.8.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.
@@ -5,7 +5,7 @@ import { coldDaysToBucket, isLongerColdRetention } from "./bucket.js";
5
5
  import { computeChunkId } from "./chunk-id.js";
6
6
  import { decodeChunk, encodeChunk } from "./chunk-encoder.js";
7
7
  import { parseManifest, serializeManifest } from "./manifest.js";
8
- import { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
8
+ import { ColdStoreCycleResult, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
9
9
  import { resolveTableConfig } from "./config.js";
10
10
  import { sha256 } from "@kici-dev/core";
11
11
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
@@ -154,14 +154,14 @@ var BaseColdStore = class {
154
154
  summary.skipped.disabled += 1;
155
155
  coldStoreArchiveCyclesTotal().add(1, {
156
156
  db: this.db,
157
- result: "disabled"
157
+ result: ColdStoreCycleResult.enum.disabled
158
158
  });
159
159
  return summary;
160
160
  }
161
161
  if (this.adapters.size === 0) {
162
162
  coldStoreArchiveCyclesTotal().add(1, {
163
163
  db: this.db,
164
- result: "no_tables"
164
+ result: ColdStoreCycleResult.enum.no_tables
165
165
  });
166
166
  summary.skipped.no_tables += 1;
167
167
  this.log("info", "cold-store cycle: no adapters registered", {
@@ -178,7 +178,7 @@ var BaseColdStore = class {
178
178
  await this.processAdapter(adapter, summary);
179
179
  summary.tablesProcessed += 1;
180
180
  }
181
- const cycleResult = summary.rowsFailed > 0 ? "failure" : summary.chunksWritten > 0 ? "success" : "no_tables";
181
+ const cycleResult = summary.rowsFailed > 0 ? ColdStoreCycleResult.enum.failure : summary.chunksWritten > 0 ? ColdStoreCycleResult.enum.success : ColdStoreCycleResult.enum.no_tables;
182
182
  coldStoreArchiveCyclesTotal().add(1, {
183
183
  db: this.db,
184
184
  result: cycleResult
@@ -14,7 +14,7 @@ export { computeChunkId } from './chunk-id.js';
14
14
  export { decodeChunk, encodeChunk, type DecodeChunkArgs, type EncodeChunkArgs, type EncodedChunk, } from './chunk-encoder.js';
15
15
  export { parseManifest, serializeManifest } from './manifest.js';
16
16
  export { DEFAULT_TABLE_CONFIG, resolveTableConfig, type ColdStoreConfig, type ColdStoreTableConfig, } from './config.js';
17
- export { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, } from './metrics.js';
17
+ export { ColdStoreCycleResult, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, } from './metrics.js';
18
18
  export { type ChunkCommitMetadata, type EligiblePartition, type TableAdapter, } from './table-adapter.js';
19
19
  export { type ArchiveCycleSummary, type ChunkManifest, type ColdRetention } from './types.js';
20
20
  //# sourceMappingURL=index.d.ts.map
@@ -4,8 +4,8 @@ import { COLD_BUCKET_NAMES, coldDaysToBucket, isLongerColdRetention } from "./bu
4
4
  import { computeChunkId } from "./chunk-id.js";
5
5
  import { decodeChunk, encodeChunk } from "./chunk-encoder.js";
6
6
  import { parseManifest, serializeManifest } from "./manifest.js";
7
- import { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
7
+ import { ColdStoreCycleResult, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
8
8
  import { DEFAULT_TABLE_CONFIG, resolveTableConfig } from "./config.js";
9
9
  import { BaseColdStore } from "./cold-store.js";
10
10
  import { ChunkLru } from "./lru.js";
11
- export { BaseColdStore, COLD_BUCKET_NAMES, ChunkLru, DEFAULT_TABLE_CONFIG, chunkObjectKey, coldDaysToBucket, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, computeChunkId, decodeChunk, encodeChunk, encodeKeySegment, isLongerColdRetention, parseManifest, resolveTableConfig, serializeManifest, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix };
11
+ export { BaseColdStore, COLD_BUCKET_NAMES, ChunkLru, ColdStoreCycleResult, DEFAULT_TABLE_CONFIG, chunkObjectKey, coldDaysToBucket, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, computeChunkId, decodeChunk, encodeChunk, encodeKeySegment, isLongerColdRetention, parseManifest, resolveTableConfig, serializeManifest, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix };
@@ -28,12 +28,28 @@
28
28
  * guarantees we resolve AFTER telemetry is wired up.
29
29
  */
30
30
  import type { Counter, Histogram } from '@opentelemetry/api';
31
+ import { z } from 'zod';
32
+ /**
33
+ * The `result` label of {@link coldStoreArchiveCyclesTotal}: what one
34
+ * `runArchiveCycle()` tick concluded. `disabled` and `no_tables` are the
35
+ * cycle stopping before any adapter runs (config gate off / nothing
36
+ * registered); `success` wrote at least one chunk; `no_tables` is also what a
37
+ * complete pass reports when every adapter ran and none had a row to move;
38
+ * `failure` had a row fail to archive.
39
+ */
40
+ export declare const ColdStoreCycleResult: z.ZodEnum<{
41
+ disabled: "disabled";
42
+ failure: "failure";
43
+ no_tables: "no_tables";
44
+ success: "success";
45
+ }>;
46
+ export type ColdStoreCycleResult = z.infer<typeof ColdStoreCycleResult>;
31
47
  /**
32
48
  * Cycles that `runArchiveCycle()` has completed, labeled by outcome.
33
49
  * Always incremented by 1 on every tick — guaranteed-visible proof
34
50
  * that the cold-store subsystem is registered and running.
35
51
  *
36
- * `result` `no_tables` | `disabled` | `success` | `failure`.
52
+ * `result` is a {@link ColdStoreCycleResult}.
37
53
  */
38
54
  export declare function coldStoreArchiveCyclesTotal(): Counter;
39
55
  /**
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { createMeter } from "../telemetry/metrics.js";
3
+ import { z } from "zod";
3
4
  //#region src/cold-store/metrics.ts
4
5
  let _meter;
5
6
  function meter() {
@@ -20,11 +21,25 @@ let _purgeChunksTotal;
20
21
  let _purgeBytesTotal;
21
22
  let _purgeDurationSeconds;
22
23
  /**
24
+ * The `result` label of {@link coldStoreArchiveCyclesTotal}: what one
25
+ * `runArchiveCycle()` tick concluded. `disabled` and `no_tables` are the
26
+ * cycle stopping before any adapter runs (config gate off / nothing
27
+ * registered); `success` wrote at least one chunk; `no_tables` is also what a
28
+ * complete pass reports when every adapter ran and none had a row to move;
29
+ * `failure` had a row fail to archive.
30
+ */
31
+ const ColdStoreCycleResult = z.enum([
32
+ "disabled",
33
+ "no_tables",
34
+ "success",
35
+ "failure"
36
+ ]);
37
+ /**
23
38
  * Cycles that `runArchiveCycle()` has completed, labeled by outcome.
24
39
  * Always incremented by 1 on every tick — guaranteed-visible proof
25
40
  * that the cold-store subsystem is registered and running.
26
41
  *
27
- * `result` `no_tables` | `disabled` | `success` | `failure`.
42
+ * `result` is a {@link ColdStoreCycleResult}.
28
43
  */
29
44
  function coldStoreArchiveCyclesTotal() {
30
45
  if (!_archiveCyclesTotal) _archiveCyclesTotal = meter().createCounter("cold_store_archive_cycles_total", { description: "Completed archive cycles by db and outcome" });
@@ -174,6 +189,6 @@ function coldStorePurgeDurationSeconds() {
174
189
  return _purgeDurationSeconds;
175
190
  }
176
191
  //#endregion
177
- export { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal };
192
+ export { ColdStoreCycleResult, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal };
178
193
 
179
194
  //# sourceMappingURL=metrics.js.map
@@ -122,7 +122,10 @@ export declare function defineEnv<TShape extends z.ZodRawShape>(opts: DefineEnvO
122
122
  *
123
123
  * Keep this list small and well-justified. Every addition is a typo we can
124
124
  * no longer catch, so only list things that are (a) actually set in the
125
- * wild by our own tooling and (b) could never be a config typo.
125
+ * wild by our own tooling and (b) could never be a config typo. A variable
126
+ * only one service reads belongs in that service's `extraKnown` instead —
127
+ * `KICI_CONFIG` is the reference case, allowlisted at the orchestrator's own
128
+ * call site so the agent and Platform catchers still reject it.
126
129
  */
127
130
  export declare const RESERVED_NON_SCHEMA_KICI_VARS: readonly string[];
128
131
  /**
@@ -176,6 +179,16 @@ export interface ValidateUnknownKiciVarsOptions {
176
179
  warnOnly?: boolean;
177
180
  /** Logger callback for warn-mode (defaults to `console.warn`). */
178
181
  onWarn?: (msg: string) => void;
182
+ /**
183
+ * Advice text for names the caller recognises but does not accept — a
184
+ * documented variable that belongs to some other layer. A rejected name
185
+ * found here is reported with its advice instead of a Levenshtein guess,
186
+ * which finds nothing for exactly these names because they are not near
187
+ * misses of a schema name.
188
+ *
189
+ * This changes the message only. A name listed here is still rejected.
190
+ */
191
+ notStartupVars?: Record<string, string>;
179
192
  }
180
193
  /**
181
194
  * Inspect `env` for KICI_* keys that are not in `known`. Throws (or warns,
@@ -241,7 +241,10 @@ function suggestClosest(name, candidates) {
241
241
  *
242
242
  * Keep this list small and well-justified. Every addition is a typo we can
243
243
  * no longer catch, so only list things that are (a) actually set in the
244
- * wild by our own tooling and (b) could never be a config typo.
244
+ * wild by our own tooling and (b) could never be a config typo. A variable
245
+ * only one service reads belongs in that service's `extraKnown` instead —
246
+ * `KICI_CONFIG` is the reference case, allowlisted at the orchestrator's own
247
+ * call site so the agent and Platform catchers still reject it.
245
248
  */
246
249
  const RESERVED_NON_SCHEMA_KICI_VARS = [
247
250
  "KICI_CACHE",
@@ -305,21 +308,27 @@ function validateUnknownKiciVars(known, options = {}, env = process.env) {
305
308
  ...options.extraKnown ?? [],
306
309
  ...RESERVED_NON_SCHEMA_KICI_VARS
307
310
  ]);
311
+ const advice = options.notStartupVars ?? {};
308
312
  const unknown = [];
309
313
  for (const key of Object.keys(env)) {
310
314
  if (!key.startsWith("KICI_")) continue;
311
315
  if (knownSet.has(key)) continue;
312
316
  if (RESERVED_NON_SCHEMA_KICI_PREFIXES.some((p) => key.startsWith(p))) continue;
313
317
  if (RESERVED_NON_SCHEMA_KICI_SUFFIXES.some((s) => key.endsWith(s) || key.includes(`${s}_`))) continue;
318
+ const note = advice[key];
314
319
  unknown.push({
315
320
  name: key,
316
- suggestion: suggestClosest(key, [...knownSet])
321
+ note,
322
+ suggestion: note ? void 0 : suggestClosest(key, [...knownSet])
317
323
  });
318
324
  }
319
325
  if (unknown.length === 0) return;
320
326
  const header = `Unknown KICI_* env var(s) detected — refusing to start.
321
327
  Set KICI_DEV=true to downgrade this check to a warning.
322
- Unknown vars:\n${unknown.map(({ name, suggestion }) => suggestion ? ` - ${name} (did you mean ${suggestion}?)` : ` - ${name} (no close match in the schema)`).join("\n")}`;
328
+ Unknown vars:\n${unknown.map(({ name, suggestion, note }) => {
329
+ if (note) return ` - ${name} (${note})`;
330
+ return suggestion ? ` - ${name} (did you mean ${suggestion}?)` : ` - ${name} (no close match in the schema)`;
331
+ }).join("\n")}`;
323
332
  if (options.warnOnly ?? env.KICI_DEV === "true") {
324
333
  (options.onWarn ?? console.warn)(header);
325
334
  return;
package/dist/index.d.ts CHANGED
@@ -15,5 +15,5 @@ export { setupGracefulShutdown, type ShutdownStep, type ShutdownLogger, type Shu
15
15
  export { validateRequiredTools, type ToolRequirement } from './tool-check.js';
16
16
  export { kiciTmpBase, kiciMkdtemp } from './tmp-dir.js';
17
17
  export { createS3Client, type CreateS3ClientOptions, type SharedS3Config } from './s3-client.js';
18
- export { BaseColdStore, ChunkLru, COLD_BUCKET_NAMES, DEFAULT_TABLE_CONFIG, chunkObjectKey, coldDaysToBucket, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, computeChunkId, decodeChunk, encodeChunk, encodeKeySegment, isLongerColdRetention, parseManifest, resolveTableConfig, serializeManifest, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix, type ArchiveCycleSummary, type BaseColdStoreDeps, type ChunkCommitMetadata, type ChunkLruOptions, type ChunkManifest, type ColdBucketName, type ColdRetention, type ColdStore, type ColdStoreConfig, type ColdStoreFetchRangeArgs, type ColdStoreReplayChunkArgs, type ColdStoreReplayResult, type ColdStoreReplayRowArgs, type ColdStoreTableConfig, type DbKind, type DecodeChunkArgs, type EligiblePartition, type EncodeChunkArgs, type EncodedChunk, type PurgeableChunk, type PurgeChunkResult, type PurgeExpiredChunksOpts, type PurgeExpiredChunksSummary, type TableAdapter, } from './cold-store/index.js';
18
+ export { BaseColdStore, ChunkLru, COLD_BUCKET_NAMES, DEFAULT_TABLE_CONFIG, chunkObjectKey, coldDaysToBucket, coldStoreArchiveBytesTotal, ColdStoreCycleResult, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, computeChunkId, decodeChunk, encodeChunk, encodeKeySegment, isLongerColdRetention, parseManifest, resolveTableConfig, serializeManifest, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix, type ArchiveCycleSummary, type BaseColdStoreDeps, type ChunkCommitMetadata, type ChunkLruOptions, type ChunkManifest, type ColdBucketName, type ColdRetention, type ColdStore, type ColdStoreConfig, type ColdStoreFetchRangeArgs, type ColdStoreReplayChunkArgs, type ColdStoreReplayResult, type ColdStoreReplayRowArgs, type ColdStoreTableConfig, type DbKind, type DecodeChunkArgs, type EligiblePartition, type EncodeChunkArgs, type EncodedChunk, type PurgeableChunk, type PurgeChunkResult, type PurgeExpiredChunksOpts, type PurgeExpiredChunksSummary, type TableAdapter, } from './cold-store/index.js';
19
19
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -22,10 +22,10 @@ import { COLD_BUCKET_NAMES, coldDaysToBucket, isLongerColdRetention } from "./co
22
22
  import { computeChunkId } from "./cold-store/chunk-id.js";
23
23
  import { decodeChunk, encodeChunk } from "./cold-store/chunk-encoder.js";
24
24
  import { parseManifest, serializeManifest } from "./cold-store/manifest.js";
25
- import { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./cold-store/metrics.js";
25
+ import { ColdStoreCycleResult, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./cold-store/metrics.js";
26
26
  import { DEFAULT_TABLE_CONFIG, resolveTableConfig } from "./cold-store/config.js";
27
27
  import { BaseColdStore } from "./cold-store/cold-store.js";
28
28
  import { ChunkLru } from "./cold-store/lru.js";
29
29
  import "./cold-store/index.js";
30
30
  export * from "@kici-dev/core";
31
- export { AgentDeliveryMode, AgentPlatform, BaseColdStore, BundleChunkAssembler, COLD_BUCKET_NAMES, ChunkLru, ChunkRequestWaiter, DEFAULT_TABLE_CONFIG, FLEET_CHUNK_BYTES, MAX_LOG_BYTES, MIGRATION_HASH_TABLE, REGISTERABLE_TRIGGER_TYPES, RingBuffer, addLogsToArchive, chunkBuffer, chunkObjectKey, clearDispatchQueueDirect, coldDaysToBucket, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, collectRuntimeMetricNames, computeChunkId, computeMigrationsHash, createContextTemplateDirect, createDb, createDbRole, createHealthRoutes, createMeter, createMetricsRoutes, createPool, createReadOnlyDbUser, createS3Client, decodeChunk, decrypt, deleteContextDirect, deriveKey, dropAndCreateDatabase, emitKiciEventDirect, encodeChunk, encodeKeySegment, encrypt, ensureDatabase, generateMasterKey, getPrometheusExporter, getReconnectDelay, initTelemetry, isLongerColdRetention, isPgUniqueViolation, isPoolAcquireTimeout, isSchemaCurrent, kiciMkdtemp, kiciTmpBase, listCheckRunTrackingDirect, listContextsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeContextsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolveTableConfig, seedContextBindingDirect, seedContextDirect, serializeManifest, setContextPolicyDirect, setContextSecretDirect, setupGracefulShutdown, showContextDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, splitAgentPlatform, storeMigrationContentHash, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix, validateRequiredTools };
31
+ export { AgentDeliveryMode, AgentPlatform, BaseColdStore, BundleChunkAssembler, COLD_BUCKET_NAMES, ChunkLru, ChunkRequestWaiter, ColdStoreCycleResult, DEFAULT_TABLE_CONFIG, FLEET_CHUNK_BYTES, MAX_LOG_BYTES, MIGRATION_HASH_TABLE, REGISTERABLE_TRIGGER_TYPES, RingBuffer, addLogsToArchive, chunkBuffer, chunkObjectKey, clearDispatchQueueDirect, coldDaysToBucket, coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal, collectRuntimeMetricNames, computeChunkId, computeMigrationsHash, createContextTemplateDirect, createDb, createDbRole, createHealthRoutes, createMeter, createMetricsRoutes, createPool, createReadOnlyDbUser, createS3Client, decodeChunk, decrypt, deleteContextDirect, deriveKey, dropAndCreateDatabase, emitKiciEventDirect, encodeChunk, encodeKeySegment, encrypt, ensureDatabase, generateMasterKey, getPrometheusExporter, getReconnectDelay, initTelemetry, isLongerColdRetention, isPgUniqueViolation, isPoolAcquireTimeout, isSchemaCurrent, kiciMkdtemp, kiciTmpBase, listCheckRunTrackingDirect, listContextsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeContextsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolveTableConfig, seedContextBindingDirect, seedContextDirect, serializeManifest, setContextPolicyDirect, setContextSecretDirect, setupGracefulShutdown, showContextDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, splitAgentPlatform, storeMigrationContentHash, tablePrefix, tenantDayBucketPrefix, tenantDayPrefix, validateRequiredTools };
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Programmatic nftables rule management for agent network isolation.
3
+ *
4
+ * Manages RFC1918 + cloud metadata blocking rules per-interface (Firecracker/container)
5
+ * or per-UID (bare-metal). All operations use `nft` CLI via child_process.execFile.
6
+ *
7
+ * Table layout:
8
+ * table ip kici {
9
+ * chain forward { type filter hook forward priority 0; policy accept; }
10
+ * chain input { type filter hook input priority 0; policy accept; }
11
+ * chain output { type filter hook output priority 0; policy accept; }
12
+ * }
13
+ *
14
+ * `forward` and `input` answer different questions and both are needed. A
15
+ * packet a sandbox sends to one of the host's OWN addresses — a bridge gateway,
16
+ * the host's LAN address — is delivered on the input hook and never traverses
17
+ * forward, so a forward rule cannot see it. `forward` governs what a sandbox
18
+ * reaches THROUGH the host; `input` governs what it reaches ON the host.
19
+ */
20
+ /**
21
+ * Network policy controlling RFC1918 and internet access for the agents or job
22
+ * containers in one label set.
23
+ *
24
+ * Lives here rather than beside the scaler's own types because both the
25
+ * orchestrator's scaler (agent containers and Firecracker VMs) and the agent's
26
+ * container backend (nested job containers) build rules from it. The
27
+ * orchestrator re-exports it from `scaler/types.ts` so its call sites are
28
+ * unchanged.
29
+ */
30
+ export interface NetworkPolicy {
31
+ /** CIDR ranges allowed as exceptions to the default RFC1918 block */
32
+ allowlist?: string[];
33
+ /** Block all outbound traffic except allowlisted ranges */
34
+ denyAll?: boolean;
35
+ /**
36
+ * What this source class may reach on the HOST itself, in the
37
+ * `<cidr|address|*>[:<port|*>]` vocabulary {@link parseHostAccess} reads.
38
+ * Everything else host-destined is dropped.
39
+ *
40
+ * Distinct from {@link allowlist}, which governs the `forward` hook and so
41
+ * answers what a sandbox reaches *through* the host. Leaving this undefined
42
+ * means the caller supplies its own class default; an empty array means
43
+ * "reach nothing on the host".
44
+ */
45
+ hostAccess?: string[];
46
+ }
47
+ /**
48
+ * One parsed {@link NetworkPolicy.hostAccess} entry.
49
+ *
50
+ * `null` means "unconstrained" on both fields: a `daddr` of `null` matches
51
+ * every host address (nft omits the `ip daddr` clause), and a `port` of
52
+ * `null` matches every port.
53
+ */
54
+ export interface HostAccessRule {
55
+ daddr: string | null;
56
+ port: number | null;
57
+ }
58
+ /**
59
+ * Parse one host-access entry.
60
+ *
61
+ * Grammar: `<cidr|address|*>` optionally followed by `:<port|*>`. A bare number
62
+ * is a port on any host address, which is the common case — an operator naming
63
+ * a host-local registry mirror knows its port, not the host's dynamic
64
+ * addresses.
65
+ *
66
+ * Hostnames are rejected on purpose: nftables matches addresses, so resolving a
67
+ * name at rule-build time produces a rule that goes stale silently the next
68
+ * time the name moves.
69
+ *
70
+ * @throws Error naming the entry and what is wrong with it
71
+ */
72
+ export declare function parseHostAccess(entry: string): HostAccessRule;
73
+ /**
74
+ * Build the host-access rules for one identifier, in final head-to-tail order:
75
+ * the conntrack exception, every accept, then one terminal drop.
76
+ *
77
+ * The conntrack rule leads because the chain is keyed on the sandbox as the
78
+ * SOURCE, so it also sees the reply leg of a connection the HOST opened toward
79
+ * the sandbox — a readiness probe, a metrics scrape. Without the exception
80
+ * those replies fall through to the terminal drop and the host's own connection
81
+ * dies as an opaque timeout. It widens nothing a sandbox can initiate:
82
+ * `established` is reached only by a flow whose first packet was already
83
+ * accepted, so a sandbox connecting to a port with no accept still has its SYN
84
+ * dropped and never reaches that state.
85
+ *
86
+ * A port-scoped entry emits an accept per protocol in
87
+ * {@link HOST_ACCESS_PROTOCOLS}. UDP is not optional: a container whose
88
+ * resolver is the bridge gateway — which is what rootful podman with
89
+ * aardvark-dns gives it — resolves over UDP, so a tcp-only accept on port 53
90
+ * leaves it unable to resolve any name.
91
+ *
92
+ * @returns one token list per rule, in final head-to-tail order
93
+ */
94
+ export declare function buildHostAccessRuleOps(matchClause: string[], hostAccess: string[]): string[][];
95
+ /**
96
+ * Match mode for nftables isolation rules.
97
+ * - 'iifname': Match on input interface name (Firecracker TAP devices)
98
+ * - 'saddr': Match on source IP address (container backends)
99
+ */
100
+ export type NftMatchMode = 'iifname' | 'saddr';
101
+ /** RFC1918 private address ranges. */
102
+ export declare const RFC1918_RANGES: string[];
103
+ /** Cloud metadata service range (AWS/GCP/Azure link-local). */
104
+ export declare const METADATA_RANGE = "169.254.0.0/16";
105
+ /**
106
+ * Subnet of the agent's `kici-jobs` bridge, on which the agent keys ONE drop set
107
+ * covering every nested job container — including one that does not exist yet.
108
+ *
109
+ * It lives here rather than beside the agent's own network constants because
110
+ * the Firecracker host provisioner has to recognise those rules to leave them
111
+ * alone, and the orchestrator carries `@kici-dev/agent` only as a devDependency.
112
+ * The agent module re-exports it, so its own call sites are unchanged.
113
+ */
114
+ export declare const JOB_NETWORK_SUBNET = "172.31.0.0/16";
115
+ /**
116
+ * Options for nft command execution.
117
+ */
118
+ interface NftOptions {
119
+ /**
120
+ * Wrap the `nft` invocation with `sudo -n` so non-root orchestrators (e.g.
121
+ * Pi user-mode systemd) can manage rules. Operators must have a NOPASSWD
122
+ * sudoers entry for /usr/sbin/nft. Default false.
123
+ */
124
+ requireSudo?: boolean;
125
+ /**
126
+ * nftables table these rules live in. Defaults to {@link DEFAULT_NFT_TABLE}.
127
+ *
128
+ * The Firecracker backend exposes this as an operator knob so two
129
+ * coordinators on one host get disjoint tables; the per-VM rules must follow
130
+ * the same knob as the bridge baseline, or coordinator B's rules land in
131
+ * coordinator A's table and A's next provision wipes them.
132
+ */
133
+ table?: string;
134
+ }
135
+ /** Table used when {@link NftOptions.table} is not set. */
136
+ export declare const DEFAULT_NFT_TABLE = "kici";
137
+ /**
138
+ * Validate that nftables is available and the process has NET_ADMIN capability.
139
+ * Attempts `nft list tables` -- if it fails:
140
+ * - ENOENT: nft binary not installed
141
+ * - EPERM: nft binary present but NET_ADMIN capability missing
142
+ * Throws with a clear error message in both cases.
143
+ */
144
+ export declare function validateNftablesAvailability(opts?: NftOptions): Promise<void>;
145
+ /**
146
+ * Ensure the nftables table and the chains this module writes to exist.
147
+ * Idempotent -- safe to call multiple times.
148
+ *
149
+ * Every chain is verified individually. A bare "does the table exist?" check is
150
+ * not enough: the Firecracker host provisioner creates the table before this
151
+ * module ever runs, so a table with no `forward` chain satisfied the old early
152
+ * return — and then every `addIsolationRules` failed with nft's "No such file
153
+ * or directory", leaving every VM on that host with no isolation rules at all.
154
+ *
155
+ * @param opts - `table` selects the table; `requireBaselineChain` additionally
156
+ * ensures the regular {@link BASELINE_CHAIN} exists (Firecracker hosts, whose
157
+ * `forward` chain ends in a jump to it).
158
+ */
159
+ export declare function ensureKiciTable(opts?: NftOptions & {
160
+ requireBaselineChain?: boolean;
161
+ }): Promise<void>;
162
+ /** Chain in the kici table that filters host-destined sandbox traffic. */
163
+ export declare const INPUT_CHAIN = "input";
164
+ /**
165
+ * Ensure the kici table and its `input` chain exist. Idempotent — `nft add` is
166
+ * a create-or-noop for both, so there is no check-then-create window.
167
+ *
168
+ * The chain's policy is `accept` because it is a base chain on the host's own
169
+ * input hook: everything the host itself receives passes through it, and a
170
+ * default-deny there would take the machine off the network. The deny lives in
171
+ * the per-identifier terminal drop {@link buildHostAccessRuleOps} emits.
172
+ */
173
+ export declare function ensureKiciInputChain(opts?: NftOptions): Promise<void>;
174
+ /**
175
+ * Apply one identifier's host-access rules to the `input` chain, replacing
176
+ * whatever it had.
177
+ *
178
+ * The pre-clean is not an optimisation. Bridge networks recycle addresses, so a
179
+ * crash or a `kill -9` leaves the previous holder's accepts behind for the next
180
+ * container on that IP to inherit — which is the boundary this chain exists to
181
+ * hold. Applying without removing first would also stack a second terminal drop
182
+ * above the first run's accepts, shadowing every one of them.
183
+ *
184
+ * Rules are `insert`ed in reverse so the block lands at the chain head in the
185
+ * order {@link buildHostAccessRuleOps} returns: accepts first, terminal drop
186
+ * last. Inserting forwards would put the drop above the accepts and deny
187
+ * everything.
188
+ */
189
+ export declare function addHostIsolationRules(identifier: string, hostAccess: string[], matchMode?: NftMatchMode, opts?: NftOptions): Promise<void>;
190
+ /**
191
+ * A table's `input` chain, as `nft -a list chain` prints it, or `null` when the
192
+ * table or the chain does not exist yet.
193
+ *
194
+ * Returning `null` rather than throwing keeps "the chain is not there" distinct
195
+ * from "the chain is there and holds nothing", which a caller deciding whether
196
+ * a rule set is already installed has to be able to tell apart.
197
+ */
198
+ export declare function readInputChain(opts?: NftOptions): Promise<string | null>;
199
+ /**
200
+ * Remove one identifier's rules from the `input` chain.
201
+ *
202
+ * Best-effort: a missing chain, or a rule another path already deleted, must
203
+ * not abort a teardown.
204
+ */
205
+ export declare function removeHostIsolationRules(identifier: string, opts?: NftOptions): Promise<void>;
206
+ /**
207
+ * Name of the regular (non-hooked) chain holding the host baseline rules.
208
+ *
209
+ * The Firecracker host provisioner puts its six source-scoped baseline rules
210
+ * here and reaches them with a `jump` appended as the `forward` chain's LAST
211
+ * rule. Two properties follow, and both are load-bearing:
212
+ *
213
+ * - **Per-VM rules always win.** They are inserted at the `forward` head, so
214
+ * every one of them is evaluated before the jump. A per-VM `accept`
215
+ * terminates the hook before the baseline can re-drop an allowlisted
216
+ * destination, and a per-VM `denyAll` drop is terminal before the
217
+ * baseline's blanket internet `accept` can let the packet out.
218
+ * - **A self-heal can rebuild the baseline without touching live VMs.** The
219
+ * provisioner flushes and refills only this chain, so it never has to
220
+ * `delete table` — which used to drop every running VM's isolation rules
221
+ * fail-open.
222
+ *
223
+ * A regular chain reached by `jump` rather than a second base chain at a lower
224
+ * priority: in netfilter an `accept` ends only the current base chain, so an
225
+ * allowlist accept in an earlier base chain would still be re-evaluated — and
226
+ * dropped — by the baseline's 10.0.0.0/8 rule in the later one.
227
+ */
228
+ export declare const BASELINE_CHAIN = "baseline";
229
+ /**
230
+ * Build the per-identifier isolation rules in their final head-to-tail order.
231
+ *
232
+ * nftables is first-match-wins within a chain and `accept` is terminal, so the
233
+ * order below is the whole security property:
234
+ *
235
+ * 1. gateway accept
236
+ * 2. allowlisted CIDR accepts — ahead of the drops, so an allowlisted
237
+ * destination inside a dropped range (a 10.x registry endpoint behind the
238
+ * 10.0.0.0/8 drop) is accepted before the drop is evaluated
239
+ * 3. RFC1918 drops
240
+ * 4. cloud-metadata drop
241
+ * 5. `denyAll` drop
242
+ *
243
+ * {@link addIsolationRules} lands them in exactly this order by applying the
244
+ * list in REVERSE with `insert`, which puts the whole block at the chain head —
245
+ * ahead of the tail `jump` to {@link BASELINE_CHAIN}.
246
+ *
247
+ * @returns one token list per rule, in final head-to-tail order
248
+ */
249
+ export declare function buildIsolationRuleOps(matchClause: string[], gatewayIp: string, networkPolicy?: NetworkPolicy): string[][];
250
+ /**
251
+ * Add network isolation rules for one identifier (a TAP interface name, or a
252
+ * container's source IP).
253
+ *
254
+ * Every rule is `insert`ed, applying {@link buildIsolationRuleOps} in reverse,
255
+ * so the block lands at the chain head in its documented order — ahead of any
256
+ * host baseline reached by a tail `jump`.
257
+ *
258
+ * @param identifier - Network interface name or source IP to match (e.g., "veth-abc123" or "172.30.0.5")
259
+ * @param gatewayIp - Gateway IP that must remain accessible (e.g., "10.0.0.1")
260
+ * @param networkPolicy - Optional policy with allowlist and denyAll settings
261
+ * @param matchMode - How to match traffic: 'iifname' for interface name (default), 'saddr' for source IP
262
+ */
263
+ export declare function addIsolationRules(identifier: string, gatewayIp: string, networkPolicy?: NetworkPolicy, matchMode?: NftMatchMode, opts?: NftOptions): Promise<void>;
264
+ /**
265
+ * A table's `forward` chain, as `nft -a list chain` prints it, or `null` when
266
+ * the table or the chain does not exist yet.
267
+ *
268
+ * The text form is what {@link parseRuleHandles} reads, and it is also what a
269
+ * caller asking whether a rule set is ALREADY installed — rather than deleting
270
+ * it — compares against the rule text {@link buildIsolationRuleOps} emits.
271
+ * {@link listForwardRules} cannot answer for a subnet identifier: nft reports a
272
+ * subnet source as a prefix object, which its classifier treats as "not a
273
+ * per-agent rule" on purpose.
274
+ *
275
+ * Returning `null` rather than throwing keeps "the chain is not there" distinct
276
+ * from "the chain is there and holds nothing", exactly as {@link readInputChain}
277
+ * does for the sibling chain. The distinction is load-bearing in the install
278
+ * direction: a caller checking whether a rule set is present must read a missing
279
+ * chain as NOT installed and go install it, and a throw instead aborts the whole
280
+ * ensure path — which for the agent's job network degrades to a container on an
281
+ * unfiltered bridge.
282
+ */
283
+ export declare function readForwardChain(opts?: NftOptions): Promise<string | null>;
284
+ /**
285
+ * Remove every isolation rule this identifier owns, in both the `forward` and
286
+ * the `input` chain. Called during agent cleanup, and as the pre-clean before a
287
+ * re-add.
288
+ *
289
+ * Both chains are swept because they are one boundary: an identifier is a
290
+ * container IP or a TAP name, addresses are recycled, and leaving the input
291
+ * chain's accepts behind hands the next tenant on that address the previous
292
+ * one's host reachability.
293
+ *
294
+ * Best-effort: logs errors but does not throw (cleanup must not block
295
+ * destruction).
296
+ *
297
+ * @param interfaceName - Network interface name or source IP whose rules should be removed
298
+ */
299
+ export declare function removeIsolationRules(interfaceName: string, opts?: NftOptions): Promise<void>;
300
+ /**
301
+ * Re-export NftOptions type for use by callers (Firecracker / container backends)
302
+ * that need to pass the requireSudo flag through to these functions.
303
+ */
304
+ export type { NftOptions };
305
+ /** One rule in the forward chain, as reported by `nft -j -a list chain`. */
306
+ export interface NftForwardRule {
307
+ /** nft rule handle, for `nft delete rule … handle N`. */
308
+ handle: number;
309
+ /**
310
+ * The per-identifier value this rule matches on — a concrete source IP or a
311
+ * concrete interface name — or `null` for a rule that is not per-identifier
312
+ * (a host baseline rule matching a whole subnet or a `kici-*` wildcard, or a
313
+ * `jump`).
314
+ */
315
+ identifier: string | null;
316
+ }
317
+ /**
318
+ * Read every rule in a table's forward chain, with its handle and the per-agent
319
+ * identifier it matches on.
320
+ *
321
+ * This is the enumerate-what-is-there half of rule management; the
322
+ * identifier-known delete path is {@link parseRuleHandles}. Reconciliation
323
+ * needs this one: rules are removed only on the synchronous teardown paths this
324
+ * process drives, so an orchestrator crash, a `kill -9`, or a VM that dies
325
+ * while the orchestrator is down strands `ip saddr <ip> …` rules in the shared
326
+ * chain forever. The allocator then hands that IP to another tenant, who
327
+ * inherits the dead job's allowlist.
328
+ *
329
+ * Returns an empty list rather than throwing when the chain cannot be read —
330
+ * the caller is a best-effort sweep, and a failed read must not be mistaken
331
+ * for "nothing is there".
332
+ */
333
+ export declare function listForwardRules(opts?: NftOptions): Promise<NftForwardRule[]>;
334
+ /**
335
+ * Every per-agent identifier currently present in the forward chain, mapped to
336
+ * the handles of the rules that carry it.
337
+ */
338
+ export declare function listIsolationRules(opts?: NftOptions): Promise<Map<string, number[]>>;
339
+ /**
340
+ * Delete rules from a table's forward chain by handle, highest first so earlier
341
+ * deletions cannot shift the handles still to come.
342
+ *
343
+ * Best-effort per handle: a rule another path already removed must not abort
344
+ * the rest of the sweep.
345
+ */
346
+ export declare function deleteForwardRules(handles: number[], opts?: NftOptions): Promise<number>;
347
+ /**
348
+ * Parse nft rule listing output and extract handles for rules matching a given identifier.
349
+ * Handles lines like: ` iifname "veth-abc" ip daddr 10.0.0.0/8 drop # handle 42`
350
+ *
351
+ * @param nftOutput - Raw output from `nft -a list chain ip kici <chain>`
352
+ * @param identifier - String to search for in each rule line (interface name or UID)
353
+ * @returns Array of numeric rule handles
354
+ */
355
+ export declare function parseRuleHandles(nftOutput: string, identifier: string): number[];
356
+ //# sourceMappingURL=nftables.d.ts.map