@akasecurity/ai-tc-claude-code 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,7 +50,7 @@ var require_ignore = __commonJS({
50
50
  var REGEX_SPLITALL_CRLF = /\r?\n/g;
51
51
  var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
52
52
  var REGEX_TEST_TRAILING_SLASH = /\/$/;
53
- var SLASH = "/";
53
+ var SLASH2 = "/";
54
54
  var TMP_KEY_IGNORE = "node-ignore";
55
55
  if (typeof Symbol !== "undefined") {
56
56
  TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
@@ -422,11 +422,11 @@ var require_ignore = __commonJS({
422
422
  if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
423
423
  return this.test(path);
424
424
  }
425
- const slices = path.split(SLASH).filter(Boolean);
425
+ const slices = path.split(SLASH2).filter(Boolean);
426
426
  slices.pop();
427
427
  if (slices.length) {
428
428
  const parent = this._t(
429
- slices.join(SLASH) + SLASH,
429
+ slices.join(SLASH2) + SLASH2,
430
430
  this._testCache,
431
431
  true,
432
432
  slices
@@ -442,14 +442,14 @@ var require_ignore = __commonJS({
442
442
  return cache[path];
443
443
  }
444
444
  if (!slices) {
445
- slices = path.split(SLASH).filter(Boolean);
445
+ slices = path.split(SLASH2).filter(Boolean);
446
446
  }
447
447
  slices.pop();
448
448
  if (!slices.length) {
449
449
  return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
450
450
  }
451
451
  const parent = this._t(
452
- slices.join(SLASH) + SLASH,
452
+ slices.join(SLASH2) + SLASH2,
453
453
  cache,
454
454
  checkUnignored,
455
455
  slices
@@ -491,10 +491,9 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
- // ../../packages/persistence/src/database.ts
495
- import { randomUUID as randomUUID10 } from "crypto";
496
- import { join as join2, sep } from "path";
497
- import { DatabaseSync } from "node:sqlite";
494
+ // ../../packages/persistence/src/control-plane-credential.ts
495
+ import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync2, statSync } from "fs";
496
+ import { join } from "path";
498
497
 
499
498
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
500
499
  var SQLITE_MIGRATIONS = [
@@ -16198,6 +16197,125 @@ var ConfigScanRecord = external_exports.object({
16198
16197
  findings: external_exports.array(ConfigPostureFindingInput).optional()
16199
16198
  });
16200
16199
 
16200
+ // ../../packages/schema/src/zod/control-plane.ts
16201
+ var ATTACHED_CREDENTIAL_FILENAME = "control-plane-credential.json";
16202
+ var ATTACHED_CREDENTIAL_SPEC_VERSION = 1;
16203
+ var AttachedCredential = external_exports.object({
16204
+ specVersion: external_exports.literal(ATTACHED_CREDENTIAL_SPEC_VERSION),
16205
+ // The control-plane endpoint this credential was minted against.
16206
+ endpoint: external_exports.string().min(1),
16207
+ // The bearer credential itself. Never logged, never rendered — status
16208
+ // surfaces show `keyPrefix` and nothing else.
16209
+ apiKey: external_exports.string().min(1),
16210
+ // First few characters of the key, safe to display so a user can match the
16211
+ // credential against their organization's key list.
16212
+ keyPrefix: external_exports.string().min(1).max(16).optional(),
16213
+ mintedAt: external_exports.iso.datetime().optional()
16214
+ });
16215
+ var MAX_DATE_MS = 253402300799999;
16216
+ var MAX_INT4 = 2147483647;
16217
+ var StorePosturePack = external_exports.object({
16218
+ packId: external_exports.string().min(1),
16219
+ // 'namespace/packId'
16220
+ version: external_exports.string().min(1),
16221
+ enabled: external_exports.boolean(),
16222
+ // Stringified pass-through of the local store's `installed_packs.updated_at`
16223
+ // — the column format is store-version-dependent (epoch millis vs ISO), so
16224
+ // the wire shape assumes neither.
16225
+ updatedAt: external_exports.string().nullable()
16226
+ }).meta({ id: "StorePosturePack" });
16227
+ var StorePosturePolicyCounts = external_exports.object({
16228
+ total: external_exports.number().int().min(0),
16229
+ disabled: external_exports.number().int().min(0),
16230
+ // Exhaustive per-action map; the builder pre-fills every action with 0.
16231
+ //
16232
+ // Spelled out member-by-member rather than `z.record(ActionTaken, …)`. Zod
16233
+ // enforces exhaustiveness either way, but z.record emits `propertyNames` +
16234
+ // `additionalProperties` into a generated schema document, and a type
16235
+ // generator renders THAT with every key optional — a sender built against
16236
+ // the generated type would typecheck and still be rejected at runtime. An
16237
+ // explicit object emits `properties` + `required`, so generated types
16238
+ // demand all five.
16239
+ //
16240
+ // `satisfies Record<ActionTaken, …>` keeps the link to the enum: adding an
16241
+ // ActionTaken member is a COMPILE error here instead of silent drift.
16242
+ // `.strict()` is load-bearing — it rejects an unknown action key, which a
16243
+ // bare object would silently STRIP, accepting a miscounted map as valid.
16244
+ byAction: external_exports.object({
16245
+ warn: external_exports.number().int().min(0),
16246
+ redact: external_exports.number().int().min(0),
16247
+ block: external_exports.number().int().min(0),
16248
+ allow: external_exports.number().int().min(0),
16249
+ log: external_exports.number().int().min(0)
16250
+ }).strict()
16251
+ }).meta({ id: "StorePosturePolicyCounts" });
16252
+ var StorePosturePlugin = external_exports.object({
16253
+ /** Package name of the reporting plugin. */
16254
+ package: external_exports.string().min(1).max(200),
16255
+ version: external_exports.string().min(1).max(64),
16256
+ /** Version of the bundled core, when the build records one separately. */
16257
+ ossVersion: external_exports.string().max(64).nullable(),
16258
+ /**
16259
+ * `version` of the policy bundle this machine last fetched. Bounded at 200
16260
+ * rather than the 64 a bare sha256 hex digest needs today, so a later
16261
+ * format with an algorithm prefix does not start rejecting the channel.
16262
+ */
16263
+ policyBundleVersion: external_exports.string().max(200).nullable(),
16264
+ /** Epoch millis, on the CLIENT clock, of that fetch. */
16265
+ policyFetchedAt: external_exports.number().int().min(0).max(MAX_DATE_MS).nullable()
16266
+ }).meta({ id: "StorePosturePlugin" });
16267
+ var StorePostureSnapshot = external_exports.object({
16268
+ deviceId: external_exports.guid(),
16269
+ hostname: external_exports.string().min(1).max(253),
16270
+ // Epoch millis on the CLIENT clock. Bounded by what a receiving store
16271
+ // accepts (see MAX_DATE_MS), not by what a JavaScript Date can hold.
16272
+ capturedAt: external_exports.number().int().min(0).max(MAX_DATE_MS),
16273
+ // False is a measurement, not an error state: "no local store exists on
16274
+ // this machine".
16275
+ storePresent: external_exports.boolean(),
16276
+ schemaVersion: external_exports.number().int().min(0).max(MAX_INT4).nullable(),
16277
+ // PRAGMA user_version
16278
+ findingsTotal: external_exports.number().int().min(0).max(MAX_INT4),
16279
+ // Epoch millis, bounded like `capturedAt` — see MAX_DATE_MS on what that
16280
+ // bound does and does not do. Worth stating for these two specifically:
16281
+ // they are read from the local store's own ROWS rather than from this
16282
+ // machine's clock, so a damaged or hand-edited store is enough to produce
16283
+ // an out-of-range value with no clock skew involved.
16284
+ findingsFirstAt: external_exports.number().int().min(0).max(MAX_DATE_MS).nullable(),
16285
+ findingsLastAt: external_exports.number().int().min(0).max(MAX_DATE_MS).nullable(),
16286
+ packs: external_exports.array(StorePosturePack).max(500),
16287
+ policyCounts: StorePosturePolicyCounts,
16288
+ // OPTIONAL, not nullable: a reporter that predates this member keeps
16289
+ // getting its 200 without a payload change.
16290
+ plugin: StorePosturePlugin.optional()
16291
+ }).meta({ id: "StorePostureSnapshot" });
16292
+ var CAPTURE_VERSION_PREFIX = "capture/";
16293
+ var RecordAuditEventRequest = AuditEventInput.extend({
16294
+ inspections: external_exports.array(ToolCallInspection).default([])
16295
+ }).refine((v) => v.inspections.every((i) => !i.ruleVersion.startsWith(CAPTURE_VERSION_PREFIX)), {
16296
+ message: `inspections[].ruleVersion must not start with \`${CAPTURE_VERSION_PREFIX}\` \u2014 that namespace is reserved for capture definitions the control plane mints itself`,
16297
+ path: ["inspections"]
16298
+ }).meta({ id: "RecordAuditEventRequest" });
16299
+ var IngestAck = external_exports.object({
16300
+ accepted: external_exports.number().int().nonnegative(),
16301
+ duplicates: external_exports.number().int().nonnegative()
16302
+ });
16303
+ var PRINTABLE = /^[^\p{Cc}\p{Cf}]*$/u;
16304
+ var printable = (max) => external_exports.string().max(max).regex(PRINTABLE, "must not contain control characters");
16305
+ var PluginWhoami = external_exports.object({
16306
+ tenantName: printable(200),
16307
+ userEmail: printable(320),
16308
+ role: printable(64),
16309
+ keyKind: printable(64),
16310
+ serverTime: printable(64)
16311
+ });
16312
+ var ControlPlaneErrorBody = external_exports.object({
16313
+ error: external_exports.object({
16314
+ code: external_exports.string().optional(),
16315
+ message: external_exports.string().optional()
16316
+ }).optional()
16317
+ });
16318
+
16201
16319
  // ../../packages/schema/src/zod/registry.ts
16202
16320
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
16203
16321
  var PackId = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -16500,15 +16618,15 @@ function summaryToDetectionListItem(s) {
16500
16618
  }
16501
16619
  function rowToDetectionDetail(row, findingsLast30d, update) {
16502
16620
  const rules = row.rules.flatMap((r) => {
16503
- const parsed = Matcher.safeParse(r.matcher);
16504
- if (!parsed.success) return [];
16621
+ const parsed2 = Matcher.safeParse(r.matcher);
16622
+ if (!parsed2.success) return [];
16505
16623
  return [
16506
16624
  {
16507
16625
  id: r.id,
16508
16626
  name: r.name,
16509
16627
  category: r.category,
16510
16628
  severity: r.severity,
16511
- matcher: parsed.data
16629
+ matcher: parsed2.data
16512
16630
  }
16513
16631
  ];
16514
16632
  });
@@ -17154,8 +17272,8 @@ function toApiAction(dbVal) {
17154
17272
  }
17155
17273
  function toApiCategory(dbVal) {
17156
17274
  if (dbVal === "code_context") return "source_code";
17157
- const parsed = FindingCategory.safeParse(dbVal);
17158
- return parsed.success ? parsed.data : "custom";
17275
+ const parsed2 = FindingCategory.safeParse(dbVal);
17276
+ return parsed2.success ? parsed2.data : "custom";
17159
17277
  }
17160
17278
  function toApiProvider(sourceTool) {
17161
17279
  return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
@@ -17782,6 +17900,9 @@ var WorkspaceSettings = external_exports.object({
17782
17900
  function defaultWorkspaceSettings() {
17783
17901
  return WorkspaceSettings.parse({});
17784
17902
  }
17903
+ function isAttached(settings) {
17904
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
17905
+ }
17785
17906
  function toInventoryRow(input, id, now) {
17786
17907
  return {
17787
17908
  id,
@@ -18049,8 +18170,8 @@ function builtinPolicyIsReversible(id) {
18049
18170
  return BUILTIN_POLICY_SPECS[id].reversible;
18050
18171
  }
18051
18172
  function policyIdIsReversible(policyId) {
18052
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
18053
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
18173
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
18174
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
18054
18175
  return builtinPolicyIsReversible(id);
18055
18176
  }
18056
18177
  var DEFAULT_ACTIONS = Object.fromEntries(
@@ -18061,8 +18182,8 @@ var BUILTIN_POLICIES = Object.fromEntries(
18061
18182
  );
18062
18183
  var DEFAULT_PACK_POLICY_ID = "monitor";
18063
18184
  function policyIdToAction(policyId) {
18064
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
18065
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
18185
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
18186
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
18066
18187
  return BUILTIN_POLICIES[id].action;
18067
18188
  }
18068
18189
  var UsedByItem = external_exports.object({
@@ -18505,53 +18626,6 @@ function reviewSeverityRank(reasons) {
18505
18626
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
18506
18627
  }
18507
18628
 
18508
- // ../../packages/persistence/src/ids.ts
18509
- import { createHash } from "crypto";
18510
- function sha256Hex(input) {
18511
- return createHash("sha256").update(input).digest("hex");
18512
- }
18513
- function inventoryId(objectType, identityKey) {
18514
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18515
- }
18516
- function sourceProjectId(url2) {
18517
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18518
- }
18519
- function classifiedDataId(cls) {
18520
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18521
- }
18522
- function inspectionDefinitionId(ruleId, version2) {
18523
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18524
- }
18525
- function llmCallId(sessionId, messageId) {
18526
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18527
- }
18528
- function toolCallId(sessionId, toolUseId) {
18529
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18530
- }
18531
- function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
18532
- return sha256Hex(
18533
- canonicalIdentity([
18534
- "inspection_finding",
18535
- auditEventId,
18536
- ruleId,
18537
- String(spanStart),
18538
- String(spanEnd)
18539
- ])
18540
- );
18541
- }
18542
- var NO_SESSION = "no_session";
18543
- var NO_PATH = "no_path";
18544
- function captureId(sessionId, contentHash, filePath = null) {
18545
- return sha256Hex(
18546
- canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
18547
- );
18548
- }
18549
-
18550
- // ../../packages/persistence/src/internal/snapshot.ts
18551
- import { randomUUID } from "crypto";
18552
- import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18553
- import { basename, dirname, join } from "path";
18554
-
18555
18629
  // ../../packages/persistence/src/paths.ts
18556
18630
  import {
18557
18631
  chmodSync,
@@ -18599,8 +18673,181 @@ function tightenFile(file2) {
18599
18673
  function tightenPerms(file2) {
18600
18674
  for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18601
18675
  }
18676
+ function writeExclusiveOwnerOnlySync(file2, data) {
18677
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
18678
+ }
18679
+ function writeOwnerOnlyFileSync(file2, data) {
18680
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
18681
+ try {
18682
+ rmSync(tmp, { force: true });
18683
+ } catch {
18684
+ }
18685
+ try {
18686
+ writeExclusiveOwnerOnlySync(tmp, data);
18687
+ renameSync(tmp, file2);
18688
+ } finally {
18689
+ try {
18690
+ rmSync(tmp, { force: true });
18691
+ } catch {
18692
+ }
18693
+ }
18694
+ tightenFile(file2);
18695
+ }
18696
+ function createOwnerOnlyFileSync(file2, data) {
18697
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
18698
+ try {
18699
+ rmSync(tmp, { force: true });
18700
+ } catch {
18701
+ }
18702
+ let created;
18703
+ try {
18704
+ writeExclusiveOwnerOnlySync(tmp, data);
18705
+ created = publishByLink(tmp, file2, data);
18706
+ } finally {
18707
+ try {
18708
+ rmSync(tmp, { force: true });
18709
+ } catch {
18710
+ }
18711
+ }
18712
+ if (created) tightenFile(file2);
18713
+ return created;
18714
+ }
18715
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
18716
+ function publishByLink(tmp, file2, data) {
18717
+ try {
18718
+ linkSync(tmp, file2);
18719
+ return true;
18720
+ } catch (err) {
18721
+ const code = err.code;
18722
+ if (code === "EEXIST") return false;
18723
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
18724
+ }
18725
+ try {
18726
+ writeExclusiveOwnerOnlySync(file2, data);
18727
+ return true;
18728
+ } catch (err) {
18729
+ if (err.code === "EEXIST") return false;
18730
+ throw err;
18731
+ }
18732
+ }
18733
+
18734
+ // ../../packages/persistence/src/control-plane-credential.ts
18735
+ function controlPlaneCredentialPath(settingsDir2) {
18736
+ return join(settingsDir2, ATTACHED_CREDENTIAL_FILENAME);
18737
+ }
18738
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
18739
+ function isSafeEndpoint(endpoint) {
18740
+ let parsed2;
18741
+ try {
18742
+ parsed2 = new URL(endpoint);
18743
+ } catch {
18744
+ return false;
18745
+ }
18746
+ if (parsed2.protocol === "https:") return true;
18747
+ return parsed2.protocol === "http:" && LOOPBACK_HOSTS.has(parsed2.hostname);
18748
+ }
18749
+ function repairOrRefuseMode(file2) {
18750
+ const link = lstatSync2(file2, { throwIfNoEntry: false });
18751
+ if (link === void 0) return "absent";
18752
+ if (link.isSymbolicLink()) return "untrusted";
18753
+ const stat = statSync(file2, { throwIfNoEntry: false });
18754
+ if (stat === void 0) return "absent";
18755
+ const uid = process.getuid?.();
18756
+ if (uid !== void 0 && stat.uid !== uid) return "untrusted";
18757
+ if (process.platform !== "win32" && (stat.mode & 511) !== DATA_FILE_MODE) {
18758
+ try {
18759
+ chmodSync2(file2, DATA_FILE_MODE);
18760
+ } catch {
18761
+ return "untrusted";
18762
+ }
18763
+ }
18764
+ return "ok";
18765
+ }
18766
+ function readControlPlaneCredentialState(settingsDir2, connection) {
18767
+ const file2 = controlPlaneCredentialPath(settingsDir2);
18768
+ let raw;
18769
+ const gate = repairOrRefuseMode(file2);
18770
+ if (gate === "absent") return { usable: false, reason: "absent" };
18771
+ if (gate === "untrusted") return { usable: false, reason: "untrusted-file" };
18772
+ try {
18773
+ raw = readFileSync(file2, "utf8");
18774
+ } catch (err) {
18775
+ const code = err.code;
18776
+ return { usable: false, reason: code === "ENOENT" ? "absent" : "unreadable" };
18777
+ }
18778
+ let parsed2;
18779
+ try {
18780
+ parsed2 = JSON.parse(raw);
18781
+ } catch {
18782
+ return { usable: false, reason: "malformed" };
18783
+ }
18784
+ const result = AttachedCredential.safeParse(parsed2);
18785
+ if (!result.success) return { usable: false, reason: "malformed" };
18786
+ if (!isSafeEndpoint(result.data.endpoint)) {
18787
+ return { usable: false, reason: "unsafe-endpoint" };
18788
+ }
18789
+ if (connection !== void 0 && connection.endpoint !== result.data.endpoint) {
18790
+ return {
18791
+ usable: false,
18792
+ reason: "endpoint-mismatch",
18793
+ credentialEndpoint: result.data.endpoint,
18794
+ settingsEndpoint: connection.endpoint
18795
+ };
18796
+ }
18797
+ return { usable: true, credential: result.data };
18798
+ }
18799
+
18800
+ // ../../packages/persistence/src/database.ts
18801
+ import { randomUUID as randomUUID10 } from "crypto";
18802
+ import { join as join3, sep } from "path";
18803
+ import { DatabaseSync } from "node:sqlite";
18804
+
18805
+ // ../../packages/persistence/src/ids.ts
18806
+ import { createHash } from "crypto";
18807
+ function sha256Hex(input) {
18808
+ return createHash("sha256").update(input).digest("hex");
18809
+ }
18810
+ function inventoryId(objectType, identityKey) {
18811
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18812
+ }
18813
+ function sourceProjectId(url2) {
18814
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
18815
+ }
18816
+ function classifiedDataId(cls) {
18817
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
18818
+ }
18819
+ function inspectionDefinitionId(ruleId, version2) {
18820
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18821
+ }
18822
+ function llmCallId(sessionId, messageId) {
18823
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18824
+ }
18825
+ function toolCallId(sessionId, toolUseId) {
18826
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18827
+ }
18828
+ function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
18829
+ return sha256Hex(
18830
+ canonicalIdentity([
18831
+ "inspection_finding",
18832
+ auditEventId,
18833
+ ruleId,
18834
+ String(spanStart),
18835
+ String(spanEnd)
18836
+ ])
18837
+ );
18838
+ }
18839
+ var NO_SESSION = "no_session";
18840
+ var NO_PATH = "no_path";
18841
+ function captureId(sessionId, contentHash, filePath = null) {
18842
+ return sha256Hex(
18843
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
18844
+ );
18845
+ }
18602
18846
 
18603
18847
  // ../../packages/persistence/src/internal/snapshot.ts
18848
+ import { randomUUID } from "crypto";
18849
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2 } from "fs";
18850
+ import { basename, dirname, join as join2 } from "path";
18604
18851
  function backupPath(file2, tag) {
18605
18852
  return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18606
18853
  }
@@ -18610,15 +18857,15 @@ var STAGED_NAME_SUFFIX = `.bak${SNAPSHOT_STAGING_SUFFIX}`;
18610
18857
  var SNAPSHOT_STAGING_COPY = "copy";
18611
18858
  function createSnapshotStaging(backup) {
18612
18859
  const stage = `${backup}${SNAPSHOT_STAGING_SUFFIX}`;
18613
- rmSync2(stage, { recursive: true, force: true });
18860
+ rmSync3(stage, { recursive: true, force: true });
18614
18861
  mkdirOwnerOnlySync(stage);
18615
18862
  tightenDir(stage);
18616
- return { stage, copy: join(stage, SNAPSHOT_STAGING_COPY) };
18863
+ return { stage, copy: join2(stage, SNAPSHOT_STAGING_COPY) };
18617
18864
  }
18618
18865
  function idleMs(entry) {
18619
- for (const candidate of [join(entry, SNAPSHOT_STAGING_COPY), entry]) {
18866
+ for (const candidate of [join2(entry, SNAPSHOT_STAGING_COPY), entry]) {
18620
18867
  try {
18621
- return Date.now() - statSync(candidate).mtimeMs;
18868
+ return Date.now() - statSync2(candidate).mtimeMs;
18622
18869
  } catch {
18623
18870
  }
18624
18871
  }
@@ -18635,11 +18882,11 @@ function reapStalePartials(file2) {
18635
18882
  }
18636
18883
  for (const name of entries) {
18637
18884
  if (!name.startsWith(prefix) || !name.endsWith(STAGED_NAME_SUFFIX)) continue;
18638
- const staging = join(dir, name);
18885
+ const staging = join2(dir, name);
18639
18886
  try {
18640
18887
  const idle = idleMs(staging);
18641
18888
  if (idle !== null && idle > STALE_PARTIAL_MS) {
18642
- rmSync2(staging, { recursive: true, force: true });
18889
+ rmSync3(staging, { recursive: true, force: true });
18643
18890
  }
18644
18891
  } catch {
18645
18892
  }
@@ -18653,13 +18900,13 @@ function snapshotStore(db, backup) {
18653
18900
  renameSync2(copy, backup);
18654
18901
  } catch (error51) {
18655
18902
  try {
18656
- rmSync2(stage, { recursive: true, force: true });
18903
+ rmSync3(stage, { recursive: true, force: true });
18657
18904
  } catch {
18658
18905
  }
18659
18906
  throw error51;
18660
18907
  }
18661
18908
  try {
18662
- rmSync2(stage, { recursive: true, force: true });
18909
+ rmSync3(stage, { recursive: true, force: true });
18663
18910
  } catch {
18664
18911
  }
18665
18912
  }
@@ -18674,7 +18921,7 @@ function moveStoreAside(file2, backup) {
18674
18921
  renameSync2(sidecar, moved);
18675
18922
  undo.push([moved, sidecar]);
18676
18923
  } catch {
18677
- rmSync2(sidecar, { force: true });
18924
+ rmSync3(sidecar, { force: true });
18678
18925
  }
18679
18926
  }
18680
18927
  } catch (error51) {
@@ -18690,14 +18937,14 @@ function moveStoreAside(file2, backup) {
18690
18937
  }
18691
18938
  function discardStore(file2, backup) {
18692
18939
  try {
18693
- rmSync2(file2, { force: true });
18940
+ rmSync3(file2, { force: true });
18694
18941
  for (const sidecar of dbSidecars(file2)) {
18695
- rmSync2(sidecar, { force: true });
18942
+ rmSync3(sidecar, { force: true });
18696
18943
  }
18697
18944
  } catch (error51) {
18698
18945
  if (existsSync(file2)) {
18699
18946
  try {
18700
- rmSync2(backup, { force: true });
18947
+ rmSync3(backup, { force: true });
18701
18948
  } catch {
18702
18949
  }
18703
18950
  }
@@ -18929,10 +19176,31 @@ function applyMigrations(db, file2) {
18929
19176
  if (drained) applyLegacyDropMigration(db, file2);
18930
19177
  }
18931
19178
  }
19179
+ function readLegacyTables(db) {
19180
+ let holdsRows = false;
19181
+ const marks = [];
19182
+ for (const table2 of ["events", "findings"]) {
19183
+ try {
19184
+ const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
19185
+ if (row === void 0) {
19186
+ holdsRows = true;
19187
+ marks.push(`${table2}:unreadable`);
19188
+ continue;
19189
+ }
19190
+ if (row.n > 0) holdsRows = true;
19191
+ marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
19192
+ } catch {
19193
+ holdsRows = true;
19194
+ marks.push(`${table2}:unreadable`);
19195
+ }
19196
+ }
19197
+ return { holdsRows, mark: marks.join("|") };
19198
+ }
18932
19199
  function applyLegacyDropMigration(db, file2) {
18933
19200
  const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
18934
19201
  if (!migration) return;
18935
- if (file2) {
19202
+ const before = file2 === void 0 ? void 0 : readLegacyTables(db);
19203
+ if (file2 !== void 0 && before?.holdsRows === true) {
18936
19204
  try {
18937
19205
  backupBeforeLegacyDrop(db, file2);
18938
19206
  } catch (error51) {
@@ -18946,6 +19214,12 @@ function applyLegacyDropMigration(db, file2) {
18946
19214
  () => {
18947
19215
  const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
18948
19216
  if (alreadyDropped) return;
19217
+ if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
19218
+ akaWarn(
19219
+ "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
19220
+ );
19221
+ return;
19222
+ }
18949
19223
  for (const statement of splitStatements(migration.sql)) {
18950
19224
  db.exec(statement);
18951
19225
  }
@@ -19300,8 +19574,8 @@ function safeJson(s, fallback) {
19300
19574
  function parseJsonObject(s) {
19301
19575
  if (s == null) return void 0;
19302
19576
  try {
19303
- const parsed = JSON.parse(s);
19304
- if (typeof parsed === "object" && parsed !== null) return parsed;
19577
+ const parsed2 = JSON.parse(s);
19578
+ if (typeof parsed2 === "object" && parsed2 !== null) return parsed2;
19305
19579
  } catch {
19306
19580
  }
19307
19581
  return void 0;
@@ -19312,16 +19586,16 @@ function encodeKeysetCursor(payload) {
19312
19586
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
19313
19587
  }
19314
19588
  function decodeKeysetCursor(cursor) {
19315
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19316
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19589
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19590
+ if (parsed2 !== void 0 && "startedAtMs" in parsed2 && "id" in parsed2 && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19317
19591
  // resumes from is epoch millis, and a payload carrying ±Infinity or a
19318
19592
  // fraction binds cleanly rather than failing — returning an EMPTY page with
19319
19593
  // a null cursor, which a caller reads as "end of list". That is the one
19320
19594
  // outcome a cursor that does not decode must never produce, since the
19321
19595
  // documented behaviour above is to restart from the top. (`1e999` is valid
19322
19596
  // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19323
- Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19324
- return parsed;
19597
+ Number.isInteger(parsed2.startedAtMs) && typeof parsed2.id === "string") {
19598
+ return parsed2;
19325
19599
  }
19326
19600
  return null;
19327
19601
  }
@@ -19386,18 +19660,18 @@ var DB_EVENT_TYPE_TO_KIND = {
19386
19660
  };
19387
19661
  function safeParseStringArray(raw) {
19388
19662
  if (!raw) return [];
19389
- const parsed = safeJson(raw, null);
19390
- return Array.isArray(parsed) ? parsed : [];
19663
+ const parsed2 = safeJson(raw, null);
19664
+ return Array.isArray(parsed2) ? parsed2 : [];
19391
19665
  }
19392
19666
  var DEFAULT_HARNESS = HARNESS.ClaudeCode;
19393
19667
  function toHarness(raw) {
19394
- const parsed = Harness.safeParse(raw);
19395
- return parsed.success ? parsed.data : DEFAULT_HARNESS;
19668
+ const parsed2 = Harness.safeParse(raw);
19669
+ return parsed2.success ? parsed2.data : DEFAULT_HARNESS;
19396
19670
  }
19397
19671
  function resolveLifecycle(row, lastActivityMs, nowMs) {
19398
19672
  if (row.status) {
19399
- const parsed = SessionStatus.safeParse(row.status);
19400
- if (parsed.success) return { status: parsed.data, endedAtMs: row.ended_at };
19673
+ const parsed2 = SessionStatus.safeParse(row.status);
19674
+ if (parsed2.success) return { status: parsed2.data, endedAtMs: row.ended_at };
19401
19675
  }
19402
19676
  if (row.ended_at !== null) return { status: "completed", endedAtMs: row.ended_at };
19403
19677
  if (lastActivityMs >= nowMs - LIVE_ACTIVITY_WINDOW_MS) {
@@ -20356,9 +20630,9 @@ var SqliteDetectionsRepository = class {
20356
20630
  const ruleIds = /* @__PURE__ */ new Set();
20357
20631
  for (const r of rows) {
20358
20632
  if (intToBool(r.enabled)) active += 1;
20359
- const parsed = parseRules(r.rulesJson);
20360
- rules += parsed.length;
20361
- for (const rule of parsed) {
20633
+ const parsed2 = parseRules(r.rulesJson);
20634
+ rules += parsed2.length;
20635
+ for (const rule of parsed2) {
20362
20636
  if (typeof rule.id === "string") ruleIds.add(rule.id);
20363
20637
  }
20364
20638
  }
@@ -20892,12 +21166,12 @@ function encodeGroupCursor(group) {
20892
21166
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
20893
21167
  }
20894
21168
  function decodeGroupCursor(cursor) {
20895
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20896
- if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
21169
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
21170
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.id === "string") {
20897
21171
  return {
20898
- severity: parsed.sev,
20899
- latestDetectedAt: parsed.t,
20900
- id: parsed.id
21172
+ severity: parsed2.sev,
21173
+ latestDetectedAt: parsed2.t,
21174
+ id: parsed2.id
20901
21175
  };
20902
21176
  }
20903
21177
  return null;
@@ -22031,16 +22305,16 @@ var SqliteInstalledPacksRepository = class {
22031
22305
  continue;
22032
22306
  }
22033
22307
  for (const entry of raw) {
22034
- const parsed = Rule.safeParse(entry);
22035
- if (parsed.success) {
22036
- out.rules.push(parsed.data);
22037
- out.ruleActions.set(parsed.data.id, action);
22038
- out.ruleVersions.set(parsed.data.id, row.version);
22039
- if (reversible) out.reversibleRules.add(parsed.data.id);
22040
- else out.reversibleRules.delete(parsed.data.id);
22308
+ const parsed2 = Rule.safeParse(entry);
22309
+ if (parsed2.success) {
22310
+ out.rules.push(parsed2.data);
22311
+ out.ruleActions.set(parsed2.data.id, action);
22312
+ out.ruleVersions.set(parsed2.data.id, row.version);
22313
+ if (reversible) out.reversibleRules.add(parsed2.data.id);
22314
+ else out.reversibleRules.delete(parsed2.data.id);
22041
22315
  } else {
22042
22316
  out.invalidRules += 1;
22043
- reject(pack, printableRuleId(entry), firstIssueReason(parsed.error));
22317
+ reject(pack, printableRuleId(entry), firstIssueReason(parsed2.error));
22044
22318
  }
22045
22319
  }
22046
22320
  }
@@ -23430,15 +23704,15 @@ function encodeReuseCursor(payload) {
23430
23704
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
23431
23705
  }
23432
23706
  function decodeReuseCursor(cursor) {
23433
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23434
- if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23707
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23708
+ if (parsed2 !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23435
23709
  // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23436
23710
  // null cursor, which the caller reads as "end of list" — the one outcome a
23437
23711
  // malformed cursor must never produce, since restarting from the top is the
23438
23712
  // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23439
23713
  // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23440
- Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23441
- return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23714
+ Number.isInteger(parsed2.occurrences) && typeof parsed2.pointerId === "string") {
23715
+ return { occurrences: parsed2.occurrences, pointerId: parsed2.pointerId };
23442
23716
  }
23443
23717
  return null;
23444
23718
  }
@@ -25167,7 +25441,7 @@ function openAndInitialize(file2) {
25167
25441
  }
25168
25442
  function openLocalDatabase(dir) {
25169
25443
  ensureDataDirSync(dir);
25170
- const file2 = join2(dir, DB_FILENAME);
25444
+ const file2 = join3(dir, DB_FILENAME);
25171
25445
  reapStalePartials(file2);
25172
25446
  const {
25173
25447
  db,
@@ -25403,9 +25677,9 @@ import {
25403
25677
  closeSync,
25404
25678
  existsSync as existsSync2,
25405
25679
  openSync,
25406
- readFileSync,
25407
- rmSync as rmSync3,
25408
- statSync as statSync2,
25680
+ readFileSync as readFileSync2,
25681
+ rmSync as rmSync4,
25682
+ statSync as statSync3,
25409
25683
  writeFileSync as writeFileSync2
25410
25684
  } from "fs";
25411
25685
  import { hostname as hostname3 } from "os";
@@ -25416,20 +25690,20 @@ import { createHash as createHash3 } from "crypto";
25416
25690
 
25417
25691
  // ../../packages/persistence/src/fingerprint.ts
25418
25692
  import { createHmac, randomBytes } from "crypto";
25419
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25420
- import { join as join3 } from "path";
25693
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
25694
+ import { join as join4 } from "path";
25421
25695
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
25422
25696
  var EXCEPTION_KEY_FILENAME = "exception.key";
25423
25697
  var KEY_MATERIAL_BYTES = 32;
25424
25698
  function keyFilePath(dataDir2) {
25425
- return join3(dataDir2, EXCEPTION_KEY_FILENAME);
25699
+ return join4(dataDir2, EXCEPTION_KEY_FILENAME);
25426
25700
  }
25427
25701
  function parseKeyFile(raw) {
25428
- const parsed = JSON.parse(raw);
25429
- if (typeof parsed !== "object" || parsed === null) {
25702
+ const parsed2 = JSON.parse(raw);
25703
+ if (typeof parsed2 !== "object" || parsed2 === null) {
25430
25704
  throw new Error("exception key file is corrupt: not a JSON object");
25431
25705
  }
25432
- const { version: version2, material } = parsed;
25706
+ const { version: version2, material } = parsed2;
25433
25707
  if (typeof version2 !== "number" || !Number.isInteger(version2) || version2 < 1) {
25434
25708
  throw new Error("exception key file is corrupt: bad version");
25435
25709
  }
@@ -25445,7 +25719,7 @@ function parseKeyFile(raw) {
25445
25719
  function readFingerprintKey(dataDir2) {
25446
25720
  let raw;
25447
25721
  try {
25448
- raw = readFileSync2(keyFilePath(dataDir2), "utf8");
25722
+ raw = readFileSync3(keyFilePath(dataDir2), "utf8");
25449
25723
  } catch (err) {
25450
25724
  if (err.code === "ENOENT") return null;
25451
25725
  throw err instanceof Error ? err : new Error(String(err));
@@ -25457,18 +25731,22 @@ function readFingerprintKey(dataDir2) {
25457
25731
  import { renameSync as renameSync3 } from "fs";
25458
25732
  import { mkdir } from "fs/promises";
25459
25733
  import { homedir } from "os";
25460
- import { join as join4 } from "path";
25734
+ import { join as join5 } from "path";
25461
25735
  function defaultDataDir() {
25462
- return join4(homedir(), ".aka");
25736
+ return join5(homedir(), ".aka");
25463
25737
  }
25464
25738
  function settingsDir(base = defaultDataDir()) {
25465
- return join4(base, "settings");
25739
+ return join5(base, "settings");
25466
25740
  }
25467
25741
  function dataDir(base = defaultDataDir()) {
25468
- return join4(base, "data");
25742
+ return join5(base, "data");
25469
25743
  }
25470
25744
  function dbPath(base = defaultDataDir()) {
25471
- return join4(dataDir(base), "aka.db");
25745
+ return join5(dataDir(base), "aka.db");
25746
+ }
25747
+ async function ensureDataDir(dir = defaultDataDir()) {
25748
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
25749
+ tightenDir(dir);
25472
25750
  }
25473
25751
  function ensureLayoutDirSync(dir = defaultDataDir()) {
25474
25752
  ensureDataDirSync(dir);
@@ -25481,8 +25759,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
25481
25759
  for (const { name, dest } of moves) {
25482
25760
  try {
25483
25761
  ensureDataDirSync(dest);
25484
- const moved = join4(dest, name);
25485
- renameSync3(join4(base, name), moved);
25762
+ const moved = join5(dest, name);
25763
+ renameSync3(join5(base, name), moved);
25486
25764
  tightenFile(moved);
25487
25765
  } catch {
25488
25766
  }
@@ -25490,7 +25768,7 @@ function migrateLegacyLayout(base = defaultDataDir()) {
25490
25768
  }
25491
25769
 
25492
25770
  // ../../packages/persistence/src/managed-settings.ts
25493
- import { readFileSync as readFileSync3 } from "fs";
25771
+ import { readFileSync as readFileSync4 } from "fs";
25494
25772
  import { posix, win32 } from "path";
25495
25773
  function managedSettingsPaths(platform2 = process.platform) {
25496
25774
  if (platform2 === "darwin") {
@@ -25508,14 +25786,14 @@ function readManagedSettings(paths = managedSettingsPaths()) {
25508
25786
  for (const path of paths) {
25509
25787
  let text;
25510
25788
  try {
25511
- text = readFileSync3(path, "utf8");
25789
+ text = readFileSync4(path, "utf8");
25512
25790
  } catch {
25513
25791
  continue;
25514
25792
  }
25515
25793
  const record2 = parseJsonObject(text);
25516
25794
  if (!record2) continue;
25517
- const parsed = ManagedSettings.safeParse(record2);
25518
- if (parsed.success) return parsed.data;
25795
+ const parsed2 = ManagedSettings.safeParse(record2);
25796
+ if (parsed2.success) return parsed2.data;
25519
25797
  }
25520
25798
  return null;
25521
25799
  }
@@ -25555,14 +25833,14 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
25555
25833
  }
25556
25834
 
25557
25835
  // ../../packages/persistence/src/settings.ts
25558
- import { readFileSync as readFileSync4 } from "fs";
25559
- import { join as join5 } from "path";
25836
+ import { readFileSync as readFileSync5 } from "fs";
25837
+ import { join as join6 } from "path";
25560
25838
  var SETTINGS_FILENAME = "settings.json";
25561
25839
  function readWorkspaceSettings(base = defaultDataDir()) {
25562
25840
  return overlayManagedSettings(readUserSettings(base), readManagedSettings());
25563
25841
  }
25564
25842
  function readUserSettings(base) {
25565
- const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
25843
+ const record2 = readJson(join6(settingsDir(base), SETTINGS_FILENAME));
25566
25844
  if (!record2) return defaultWorkspaceSettings();
25567
25845
  try {
25568
25846
  return WorkspaceSettings.parse(record2);
@@ -25573,13 +25851,17 @@ function readUserSettings(base) {
25573
25851
  function readJson(file2) {
25574
25852
  let text;
25575
25853
  try {
25576
- text = readFileSync4(file2, "utf8");
25854
+ text = readFileSync5(file2, "utf8");
25577
25855
  } catch {
25578
25856
  return null;
25579
25857
  }
25580
25858
  return parseJsonObject(text) ?? null;
25581
25859
  }
25582
25860
 
25861
+ // ../../packages/persistence/src/store-symlinks.ts
25862
+ import { existsSync as existsSync4, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
25863
+ import { dirname as dirname2, join as join7, resolve } from "path";
25864
+
25583
25865
  // ../../packages/persistence/src/vault/crypto.ts
25584
25866
  import {
25585
25867
  createCipheriv,
@@ -25592,29 +25874,92 @@ import {
25592
25874
  // ../../packages/persistence/src/vault/key-provider.ts
25593
25875
  import { execFileSync } from "child_process";
25594
25876
  import { randomBytes as randomBytes2 } from "crypto";
25595
- import { chmodSync as chmodSync2, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
25596
- import { join as join6 } from "path";
25877
+ import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
25878
+ import { join as join8 } from "path";
25597
25879
 
25598
25880
  // ../../packages/persistence/src/vault/vault.ts
25599
25881
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
25600
25882
 
25601
25883
  // ../../packages/persistence/src/warn-era-cap.ts
25602
- import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
25603
- import { join as join7 } from "path";
25884
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
25885
+ import { join as join9 } from "path";
25604
25886
  var MARKER = "warn-era-capped";
25605
25887
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25606
25888
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25607
- const marker = join7(dataDir2, MARKER);
25608
- if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
25889
+ const marker = join9(dataDir2, MARKER);
25890
+ if (existsSync5(marker)) return { capped: 0, skipped: "already-run" };
25609
25891
  const capped = db.policies.capCategoryActions();
25610
25892
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
25611
25893
  `, { mode: DATA_FILE_MODE });
25612
25894
  return { capped };
25613
25895
  }
25614
25896
 
25897
+ // ../../packages/plugin-runtime/src/attached/failure.ts
25898
+ function statusOf(err) {
25899
+ if (typeof err !== "object" || err === null || !("status" in err)) return null;
25900
+ const { status } = err;
25901
+ if (typeof status !== "number" || !Number.isInteger(status)) return null;
25902
+ return status >= 100 && status <= 599 ? status : null;
25903
+ }
25904
+ function classifyFailure(err) {
25905
+ switch (statusOf(err)) {
25906
+ case 401:
25907
+ return "unauthorized";
25908
+ case 403:
25909
+ return "forbidden";
25910
+ default:
25911
+ return "unreachable";
25912
+ }
25913
+ }
25914
+
25915
+ // ../../packages/plugin-runtime/src/attached/forward-drops.ts
25916
+ import { readFileSync as readFileSync7 } from "fs";
25917
+ import { join as join10 } from "path";
25918
+ var FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
25919
+ function forwardDropsPath(dataDir2) {
25920
+ return join10(dataDir2, FORWARD_DROPS_FILENAME);
25921
+ }
25922
+ function recordForwardDrops(dataDir2, count, nowMs) {
25923
+ if (count <= 0) return;
25924
+ try {
25925
+ ensureDataDirSync(dataDir2);
25926
+ const previous = readForwardDrops(dataDir2);
25927
+ const next = {
25928
+ droppedForwards: (previous?.droppedForwards ?? 0) + count,
25929
+ lastDropAtMs: nowMs
25930
+ };
25931
+ writeOwnerOnlyFileSync(forwardDropsPath(dataDir2), `${JSON.stringify(next)}
25932
+ `);
25933
+ } catch {
25934
+ }
25935
+ }
25936
+ function readForwardDrops(dataDir2) {
25937
+ try {
25938
+ const parsed2 = JSON.parse(readFileSync7(forwardDropsPath(dataDir2), "utf8"));
25939
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
25940
+ const record2 = parsed2;
25941
+ if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
25942
+ return null;
25943
+ }
25944
+ if (record2.droppedForwards <= 0) return null;
25945
+ if (typeof record2.lastDropAtMs !== "number" || !Number.isFinite(record2.lastDropAtMs)) {
25946
+ return null;
25947
+ }
25948
+ return { droppedForwards: record2.droppedForwards, lastDropAtMs: record2.lastDropAtMs };
25949
+ } catch {
25950
+ return null;
25951
+ }
25952
+ }
25953
+
25954
+ // ../../packages/plugin-runtime/src/attached/forward-policy.ts
25955
+ import { randomUUID as randomUUID15 } from "crypto";
25956
+ import { readFileSync as readFileSync12 } from "fs";
25957
+ import { readFile, rename, writeFile } from "fs/promises";
25958
+ import { join as join18 } from "path";
25959
+
25615
25960
  // ../../packages/plugin-sdk/src/config.ts
25616
- import { existsSync as existsSync5 } from "fs";
25617
- import { join as join8 } from "path";
25961
+ import { existsSync as existsSync6 } from "fs";
25962
+ import { join as join11 } from "path";
25618
25963
 
25619
25964
  // ../../packages/plugin-sdk/src/provider-env.ts
25620
25965
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -25650,8 +25995,8 @@ function hostOf(url2) {
25650
25995
  }
25651
25996
  }
25652
25997
  function resolveProvider() {
25653
- const parsed = ProviderEnvSchema.safeParse(process.env);
25654
- const env = parsed.success ? parsed.data : ProviderEnvSchema.parse({});
25998
+ const parsed2 = ProviderEnvSchema.safeParse(process.env);
25999
+ const env = parsed2.success ? parsed2.data : ProviderEnvSchema.parse({});
25655
26000
  if (env.CLAUDE_CODE_USE_BEDROCK === true) return { provider: "bedrock" };
25656
26001
  if (env.CLAUDE_CODE_USE_VERTEX === true) return { provider: "vertex" };
25657
26002
  const baseUrl = env.ANTHROPIC_BASE_URL;
@@ -25668,8 +26013,8 @@ function resolveProvider() {
25668
26013
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
25669
26014
  try {
25670
26015
  ensureLayoutDirSync(base);
25671
- const settingsFile = join8(settingsDir(base), "settings.json");
25672
- if (existsSync5(settingsFile)) tightenFile(settingsFile);
26016
+ const settingsFile = join11(settingsDir(base), "settings.json");
26017
+ if (existsSync6(settingsFile)) tightenFile(settingsFile);
25673
26018
  } catch {
25674
26019
  }
25675
26020
  migrateLegacyLayout(base);
@@ -25692,9 +26037,9 @@ function resolveProviderSafe(resolveProviderFn) {
25692
26037
  }
25693
26038
 
25694
26039
  // ../../packages/plugin-sdk/src/config-inventory.ts
25695
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, realpathSync, statSync as statSync5 } from "fs";
26040
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
25696
26041
  import { homedir as homedir2 } from "os";
25697
- import { basename as basename3, join as join10 } from "path";
26042
+ import { basename as basename3, join as join13 } from "path";
25698
26043
 
25699
26044
  // ../../packages/detections/src/egress/registry.ts
25700
26045
  var EXTRACTOR_VERSION = "1";
@@ -27172,10 +27517,10 @@ var localhost_ref_default = {
27172
27517
  severity: "low",
27173
27518
  matcher: {
27174
27519
  type: "regex",
27175
- pattern: "\\b(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|::1)\\b",
27520
+ pattern: "(?<![A-Za-z0-9_])(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|(?<![A-Za-z0-9_]\\[)::1(?!:|\\.[0-9]))(?![A-Za-z0-9_])",
27176
27521
  flags: "g"
27177
27522
  },
27178
- examples: ["localhost", "127.0.0.1"]
27523
+ examples: ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
27179
27524
  };
27180
27525
 
27181
27526
  // ../../rules/core-code-context/stack-trace.json
@@ -28477,36 +28822,36 @@ function bundledDetections() {
28477
28822
  }
28478
28823
 
28479
28824
  // ../../packages/plugin-sdk/src/repo.ts
28480
- import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
28481
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
28825
+ import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
28826
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join12, sep as sep2 } from "path";
28482
28827
 
28483
28828
  // ../../packages/plugin-sdk/src/events.ts
28484
28829
  import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28485
28830
 
28486
28831
  // ../../packages/plugin-sdk/src/isolated-scan.ts
28487
- import { existsSync as existsSync7 } from "fs";
28832
+ import { existsSync as existsSync8 } from "fs";
28488
28833
  import { fileURLToPath } from "url";
28489
28834
  import { Worker } from "worker_threads";
28490
28835
 
28491
28836
  // ../../packages/plugin-sdk/src/ignore-layers.ts
28492
28837
  var import_ignore = __toESM(require_ignore(), 1);
28493
- import { readFileSync as readFileSync8 } from "fs";
28494
- import { join as join11 } from "path";
28838
+ import { readFileSync as readFileSync10 } from "fs";
28839
+ import { join as join14 } from "path";
28495
28840
 
28496
28841
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
28497
28842
  import { arch, hostname as hostname4, platform, release } from "os";
28498
28843
 
28499
28844
  // ../../packages/plugin-sdk/src/nudge.ts
28500
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
28501
- import { join as join12 } from "path";
28845
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
28846
+ import { join as join15 } from "path";
28502
28847
 
28503
28848
  // ../../packages/plugin-sdk/src/paths.ts
28504
- import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
28505
- import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
28849
+ import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
28850
+ import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
28506
28851
 
28507
28852
  // ../../packages/plugin-sdk/src/project-files.ts
28508
- import { existsSync as existsSync8, readdirSync as readdirSync4 } from "fs";
28509
- import { basename as basename5, join as join13 } from "path";
28853
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "fs";
28854
+ import { basename as basename5, join as join16 } from "path";
28510
28855
 
28511
28856
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
28512
28857
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -28541,41 +28886,1168 @@ import { randomUUID as randomUUID14 } from "crypto";
28541
28886
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28542
28887
 
28543
28888
  // ../../packages/plugin-sdk/src/throttle.ts
28544
- import { mkdirSync as mkdirSync3, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
28545
- import { join as join14 } from "path";
28546
-
28547
- // ../../packages/plugin-runtime/src/standalone-gateway.ts
28548
- import { randomUUID as randomUUID15 } from "crypto";
28549
-
28550
- // ../../packages/plugin-runtime/src/recorder.ts
28551
- var PLUGIN_RECORDER_BINARY = "plugin";
28889
+ import { mkdirSync as mkdirSync3, statSync as statSync8, writeFileSync as writeFileSync6 } from "fs";
28890
+ import { join as join17 } from "path";
28891
+
28892
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
28893
+ var REQUEST_TIMEOUT_MS = 2e3;
28894
+ function withTimeout(promise2, ms) {
28895
+ let timer;
28896
+ const timeout = new Promise((_, reject) => {
28897
+ timer = setTimeout(() => {
28898
+ reject(new Error("attached gateway request timed out"));
28899
+ }, ms);
28900
+ });
28901
+ promise2.catch(() => void 0);
28902
+ return Promise.race([promise2, timeout]).finally(() => {
28903
+ clearTimeout(timer);
28904
+ });
28905
+ }
28552
28906
 
28553
- // ../../packages/plugin-runtime/src/standalone-gateway.ts
28554
- var StandaloneDataGateway = class {
28555
- db;
28556
- // Kept for the fingerprint key lookup (exception.key lives beside the store).
28557
- dataDir;
28558
- // One notice per gateway — see warnRulesetDiscarded.
28559
- warnedRulesetDiscarded = false;
28560
- constructor(dataDir2, detections = [], meta3) {
28561
- this.db = openLocalDatabase(dataDir2);
28562
- this.dataDir = dataDir2;
28563
- this.db.installedPacks.recordInventory(detections, meta3);
28907
+ // ../../packages/plugin-runtime/src/attached/forward-policy.ts
28908
+ function isInvalidRequest(err) {
28909
+ return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
28910
+ }
28911
+ var FORWARD_BUDGET_MS = 1500;
28912
+ var DECISION_PATH_BUDGET_MS = 800;
28913
+ var BREAKER_FAILURE_THRESHOLD = 3;
28914
+ var BREAKER_COOLDOWN_MS = 3e4;
28915
+ var CLOSED = { consecutiveFailures: 0, openedAtMs: null, lastFailure: null };
28916
+ var FAILURES = /* @__PURE__ */ new Set([
28917
+ "unauthorized",
28918
+ "forbidden",
28919
+ "unreachable"
28920
+ ]);
28921
+ var FORWARD_STATE_FILENAME = "attached-state.json";
28922
+ var STATE_FILENAME = FORWARD_STATE_FILENAME;
28923
+ function parseBreakerState(raw, nowMs) {
28924
+ try {
28925
+ const parsed2 = JSON.parse(raw);
28926
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28927
+ const record2 = parsed2;
28928
+ const failures = typeof record2.consecutiveFailures === "number" && record2.consecutiveFailures >= 0 ? record2.consecutiveFailures : 0;
28929
+ const openedAtMs = typeof record2.openedAtMs === "number" && Number.isFinite(record2.openedAtMs) && record2.openedAtMs <= nowMs ? record2.openedAtMs : null;
28930
+ const lastFailure = typeof record2.lastFailure === "string" && FAILURES.has(record2.lastFailure) ? record2.lastFailure : null;
28931
+ return { consecutiveFailures: failures, openedAtMs, lastFailure };
28932
+ } catch {
28933
+ return null;
28564
28934
  }
28565
- recordCapture(record2) {
28566
- this.db.recordCapture(record2.event, record2.findings);
28567
- return Promise.resolve();
28935
+ }
28936
+ function createForwardPolicy(deps) {
28937
+ const now = deps.now ?? (() => Date.now());
28938
+ const file2 = join18(deps.dir, STATE_FILENAME);
28939
+ let state = null;
28940
+ let loading = null;
28941
+ async function readState() {
28942
+ let raw;
28943
+ try {
28944
+ raw = await readFile(file2, "utf8");
28945
+ } catch {
28946
+ return { ...CLOSED };
28947
+ }
28948
+ return parseBreakerState(raw, now()) ?? { ...CLOSED };
28568
28949
  }
28569
- ensureInventory(ctx) {
28570
- return Promise.resolve(this.db.ensureInventory(ctx));
28950
+ async function load() {
28951
+ if (state !== null) return state;
28952
+ loading ??= readState().then((loaded) => {
28953
+ state = loaded;
28954
+ loading = null;
28955
+ return loaded;
28956
+ });
28957
+ return loading;
28571
28958
  }
28572
- recordAuditEvent(event) {
28573
- this.db.auditEvents.insertAuditEvent(event);
28574
- return Promise.resolve();
28959
+ async function persist(next) {
28960
+ state = next;
28961
+ try {
28962
+ await ensureDataDir(deps.dir);
28963
+ const tmp = `${file2}.${randomUUID15()}.tmp`;
28964
+ await writeFile(tmp, JSON.stringify(next), { encoding: "utf8", mode: DATA_FILE_MODE });
28965
+ await rename(tmp, file2);
28966
+ } catch {
28967
+ }
28575
28968
  }
28576
- // The id is minted inside the repository from the natural key — the plugin can't
28577
- // import @akasecurity/persistence to compute it, so the gateway is the boundary that
28578
- // hands the natural key across. UPSERT-take-MAX idempotent re-reads that also
28969
+ return {
28970
+ async run(op, opts) {
28971
+ const budget = opts?.decisionPath === true ? DECISION_PATH_BUDGET_MS : FORWARD_BUDGET_MS;
28972
+ let current;
28973
+ try {
28974
+ current = await load();
28975
+ } catch {
28976
+ current = { ...CLOSED };
28977
+ }
28978
+ const at = now();
28979
+ if (current.openedAtMs !== null) {
28980
+ if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
28981
+ return { ok: false, reason: "breaker-open" };
28982
+ }
28983
+ await persist({
28984
+ consecutiveFailures: current.consecutiveFailures,
28985
+ openedAtMs: at,
28986
+ lastFailure: current.lastFailure
28987
+ });
28988
+ }
28989
+ try {
28990
+ const value = await withTimeout(op(), budget);
28991
+ if (current.openedAtMs !== null || current.consecutiveFailures > 0) {
28992
+ await persist({ ...CLOSED });
28993
+ }
28994
+ return { ok: true, value };
28995
+ } catch (err) {
28996
+ if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
28997
+ const reason = classifyFailure(err);
28998
+ const failures = current.consecutiveFailures + 1;
28999
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
29000
+ await persist({
29001
+ consecutiveFailures: failures,
29002
+ openedAtMs: shouldOpen ? now() : null,
29003
+ lastFailure: reason
29004
+ });
29005
+ return { ok: false, reason };
29006
+ }
29007
+ }
29008
+ };
29009
+ }
29010
+
29011
+ // ../../packages/plugin-runtime/src/attached/gateway.ts
29012
+ var ACTION_STRENGTH = {
29013
+ allow: 0,
29014
+ log: 1,
29015
+ warn: 2,
29016
+ redact: 3,
29017
+ block: 4
29018
+ };
29019
+ function ruleCategoryMap(wireRules, localRules) {
29020
+ const map2 = /* @__PURE__ */ new Map();
29021
+ for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
29022
+ for (const rule of localRules ?? []) map2.set(rule.id, rule.category);
29023
+ for (const pack of bundledDetections()) {
29024
+ for (const rule of pack.rules) map2.set(rule.id, rule.category);
29025
+ }
29026
+ return map2;
29027
+ }
29028
+ function strongerOf(a, b) {
29029
+ if (a === null) return b;
29030
+ if (b === null) return a;
29031
+ return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
29032
+ }
29033
+ function policyKey(policy) {
29034
+ return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
29035
+ }
29036
+ function floorFor(policy, categoryByRuleId) {
29037
+ const category = "category" in policy.target ? policy.target.category : categoryByRuleId.get(policy.target.ruleId);
29038
+ return category === void 0 ? null : DEFAULT_ACTIONS[category];
29039
+ }
29040
+ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
29041
+ const merged = /* @__PURE__ */ new Map();
29042
+ const disabled = [];
29043
+ const remoteCategoryAction = /* @__PURE__ */ new Map();
29044
+ for (const policy of remotePolicies) {
29045
+ if (!policy.enabled) continue;
29046
+ if (!("category" in policy.target)) continue;
29047
+ if (remoteCategoryAction.has(policy.target.category)) continue;
29048
+ const floor = floorFor(policy, categoryByRuleId);
29049
+ remoteCategoryAction.set(
29050
+ policy.target.category,
29051
+ floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
29052
+ );
29053
+ }
29054
+ for (const policy of localPolicies) {
29055
+ if (!policy.enabled) {
29056
+ disabled.push(policy);
29057
+ continue;
29058
+ }
29059
+ const key = policyKey(policy);
29060
+ if (merged.has(key)) continue;
29061
+ let remoteFloor = null;
29062
+ if ("ruleId" in policy.target) {
29063
+ const category = categoryByRuleId.get(policy.target.ruleId);
29064
+ if (category !== void 0) remoteFloor = remoteCategoryAction.get(category) ?? null;
29065
+ }
29066
+ merged.set(
29067
+ key,
29068
+ remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
29069
+ );
29070
+ }
29071
+ const localCategoryAction = /* @__PURE__ */ new Map();
29072
+ for (const policy of merged.values()) {
29073
+ if ("category" in policy.target) localCategoryAction.set(policy.target.category, policy.action);
29074
+ }
29075
+ for (const policy of remotePolicies) {
29076
+ if (!policy.enabled) {
29077
+ disabled.push(policy);
29078
+ continue;
29079
+ }
29080
+ const key = policyKey(policy);
29081
+ const floor = floorFor(policy, categoryByRuleId);
29082
+ let localFloor = null;
29083
+ if ("ruleId" in policy.target) {
29084
+ const category = categoryByRuleId.get(policy.target.ruleId);
29085
+ if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
29086
+ }
29087
+ const effectiveFloor = strongerOf(floor, localFloor);
29088
+ const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
29089
+ const existing = merged.get(key);
29090
+ if (existing === void 0) {
29091
+ merged.set(key, clamped);
29092
+ continue;
29093
+ }
29094
+ if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
29095
+ merged.set(key, clamped);
29096
+ }
29097
+ }
29098
+ return [...merged.values(), ...disabled];
29099
+ }
29100
+ var AttachedDataGateway = class {
29101
+ constructor(deps) {
29102
+ this.deps = deps;
29103
+ }
29104
+ deps;
29105
+ /**
29106
+ * The control plane's OWN resolution of this session's inventory, captured by
29107
+ * ensureInventory. Null until the first successful forward — and it stays
29108
+ * null for the whole session when the control plane is unreachable, which is fine:
29109
+ * reKeyForForward then leaves the event's ids alone and the control plane resolves
29110
+ * what it can from the descriptors it already has.
29111
+ */
29112
+ remoteInventory = null;
29113
+ // ---------------------------------------------------------------------
29114
+ // Writes: local first, then forward.
29115
+ // ---------------------------------------------------------------------
29116
+ async recordCapture(record2) {
29117
+ await this.deps.local.recordCapture(record2);
29118
+ await this.deps.forward.run(
29119
+ () => this.deps.client.ingestEvents({
29120
+ events: [record2.event],
29121
+ ...record2.dedupe ? { dedupe: record2.dedupe } : {}
29122
+ }),
29123
+ { decisionPath: true }
29124
+ );
29125
+ }
29126
+ async ensureInventory(ctx) {
29127
+ const resolved = await this.deps.local.ensureInventory(ctx);
29128
+ const remote = await this.deps.forward.run(() => this.deps.client.ingestInventory(ctx));
29129
+ this.remoteInventory = remote.ok ? remote.value : null;
29130
+ const snapshot = await (async () => {
29131
+ try {
29132
+ return await this.deps.posture?.prepare() ?? null;
29133
+ } catch {
29134
+ return null;
29135
+ }
29136
+ })();
29137
+ if (snapshot) {
29138
+ try {
29139
+ await withTimeout(
29140
+ this.deps.posture?.send(snapshot) ?? Promise.resolve(),
29141
+ REQUEST_TIMEOUT_MS
29142
+ );
29143
+ } catch {
29144
+ }
29145
+ }
29146
+ return resolved;
29147
+ }
29148
+ // The id is minted CLIENT-side and stored verbatim: the control plane does NOT
29149
+ // re-key it. `pgAuditValues` writes `id: event.id` and carries tenancy in
29150
+ // its own scoping columns, so the device and the forwarded copy
29151
+ // share one id space — which is what makes a re-post idempotent at all.
29152
+ //
29153
+ // Re-posts collapse via `onConflictDoUpdate` on the `id` PK, guarded by
29154
+ // `setWhere eventType = 'session'` (NOT onConflictDoNothing). That guard is
29155
+ // what makes an attached retry safe: a capture-stubbed session row can still
29156
+ // be HEALED by the authoritative root, while a duplicate non-session event —
29157
+ // a retried tool_call, exactly this path — can never stomp a populated row.
29158
+ async recordAuditEvent(event) {
29159
+ await this.deps.local.recordAuditEvent(event);
29160
+ await this.deps.forward.run(
29161
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
29162
+ );
29163
+ }
29164
+ // Attached `llm_call` is written locally by the inner gateway, then routed to
29165
+ // the control plane through the existing `recordAuditEvent` ingest (no dedicated
29166
+ // client method yet) by pre-building the audit event from the natural key.
29167
+ // The forward goes DIRECTLY to the client rather than through this.recordAuditEvent,
29168
+ // which would write the event to the local store a second time.
29169
+ async recordLlmCall(input) {
29170
+ await this.deps.local.recordLlmCall(input);
29171
+ await this.deps.forward.run(
29172
+ () => this.deps.client.recordAuditEvent(
29173
+ reKeyForForward(llmAuditEvent(input), this.remoteInventory)
29174
+ )
29175
+ );
29176
+ }
29177
+ /**
29178
+ * Forward one batch, item by item, under ONE aggregate deadline.
29179
+ *
29180
+ * Per-item budgets bound each request and nothing bounded their sum — see
29181
+ * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
29182
+ * rather than sent: the local write has already succeeded, so every caller
29183
+ * has a correct result to return, and a drop is the outcome this path is
29184
+ * built to accept (G8) where a blown hook timeout is not.
29185
+ *
29186
+ * Serial rather than concurrent on purpose. Firing N requests at once would
29187
+ * trade a latency problem for a burst the plane's own per-key rate limiting
29188
+ * would answer with the refusals the breaker then counts.
29189
+ *
29190
+ * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
29191
+ * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
29192
+ * lets status call the forward unhealthy; this path returns BEFORE `run` is
29193
+ * reached, so without the tally in `forward-drops.ts` a slow-but-answering
29194
+ * plane produces no failures, keeps the breaker closed, renders a healthy
29195
+ * block, and discards the tail of every batch indefinitely.
29196
+ */
29197
+ async forwardBatch(inputs, toEvent) {
29198
+ const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
29199
+ for (let i = 0; i < inputs.length; i += 1) {
29200
+ const now = Date.now();
29201
+ if (now >= deadline) {
29202
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
29203
+ return;
29204
+ }
29205
+ const input = inputs[i];
29206
+ await this.deps.forward.run(
29207
+ () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input), this.remoteInventory))
29208
+ );
29209
+ }
29210
+ }
29211
+ // Delegated as a BATCH rather than looped over recordLlmCall: the inner
29212
+ // gateway may write the whole batch in one local transaction, and looping
29213
+ // here would replace that with N separate local writes.
29214
+ async recordLlmCalls(inputs) {
29215
+ await this.deps.local.recordLlmCalls(inputs);
29216
+ await this.forwardBatch(inputs, (input) => llmAuditEvent(input));
29217
+ }
29218
+ // `input.inspections` (secrets detected client-side in the tool's masked
29219
+ // target) ride along on the request's `inspections` field — the control plane
29220
+ // persists each as an inspection_findings row linked to this audit event
29221
+ // (see RecordAuditEventRequest in @akasecurity/schema). The masked
29222
+ // `target` already rides `input.attributes`, so no raw secret leaks either
29223
+ // way — this only stops the FINDING row itself from being dropped.
29224
+ async recordToolCalls(inputs) {
29225
+ await this.deps.local.recordToolCalls(inputs);
29226
+ await this.forwardBatch(inputs, (input) => toolAuditEvent(input));
29227
+ }
29228
+ // Forwarded as a `config_scan` audit event: there is no dedicated
29229
+ // config-scan ingest endpoint, and the audit-event door is the one the
29230
+ // control plane already opens for client-minted, idempotent records.
29231
+ //
29232
+ // ONLY `scanEvent` CROSSES, and unlike `recordCapture` the plane cannot
29233
+ // re-derive the rest. A `ConfigScanRecord` is four things committed together
29234
+ // locally — the inventory `items`, this audit event, and the posture
29235
+ // `definitions`/`findings` that reference it — and three of them stay on the
29236
+ // device. Say that plainly rather than let the asymmetry with `recordCapture`
29237
+ // read as the same argument: there, findings are omitted BECAUSE the plane
29238
+ // re-derives them from `Event.content`; here there is no content to re-derive
29239
+ // from, so what is omitted is simply not sent.
29240
+ //
29241
+ // That is the wire contract as it stands rather than an oversight to patch
29242
+ // here. `items` has no route at all, and `RecordAuditEventRequest.inspections`
29243
+ // is documented as tool-call findings — widening it to carry config-scan
29244
+ // findings is an egress change (a posture finding's `maskedMatch` holds the
29245
+ // matched command) and a decision about what an attached deployment is
29246
+ // entitled to, not a bug fix. An attached machine's config posture therefore
29247
+ // reaches the plane as the event only; the dashboard's own view of it is the
29248
+ // local store.
29249
+ async recordConfigScan(record2) {
29250
+ await this.deps.local.recordConfigScan(record2);
29251
+ await this.deps.forward.run(
29252
+ () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
29253
+ );
29254
+ }
29255
+ async recordBlockedDetection(entry) {
29256
+ return this.deps.local.recordBlockedDetection(entry);
29257
+ }
29258
+ /**
29259
+ * LOCAL-ONLY, deliberately. The shares API is read-plus-decision-override
29260
+ * with no egress ingest endpoint, so there is nothing to forward to; adding a
29261
+ * forward here would be inventing a wire contract that does not exist. The
29262
+ * local write is the whole operation, and its summary is the real one — the
29263
+ * scanner reads a throw as a FAILED WRITE and skips its ledger commit, so
29264
+ * returning the inner gateway's result keeps the retry semantics honest.
29265
+ */
29266
+ async recordProjectEgress(input) {
29267
+ return this.deps.local.recordProjectEgress(input);
29268
+ }
29269
+ // ---------------------------------------------------------------------
29270
+ // Reads and device-local ledgers: pure delegation.
29271
+ // ---------------------------------------------------------------------
29272
+ async configInventoryReport() {
29273
+ return this.deps.local.configInventoryReport();
29274
+ }
29275
+ async readSessionProvider(sessionId) {
29276
+ return this.deps.local.readSessionProvider(sessionId);
29277
+ }
29278
+ async facets() {
29279
+ return this.deps.local.facets();
29280
+ }
29281
+ /**
29282
+ * Delegated UNMODIFIED — including its refusals.
29283
+ *
29284
+ * This is a fail-secure boundary: it decides whether an approved exception
29285
+ * lets a blocked action through. Under local-first the local store owns the
29286
+ * exception ledger, so the honest answer is whatever it says; wrapping this
29287
+ * in a fallback (`catch { return true }`, or defaulting on a timeout) would
29288
+ * turn a store error into a granted bypass. If the inner gateway rejects,
29289
+ * this rejects, and the runtime's own handling decides — which is asserted
29290
+ * end-to-end through runtime.capture rather than here.
29291
+ */
29292
+ async consumeException(id) {
29293
+ return this.deps.local.consumeException(id);
29294
+ }
29295
+ async recentFindings(opts) {
29296
+ return this.deps.local.recentFindings(opts);
29297
+ }
29298
+ async healthSummary() {
29299
+ return this.deps.local.healthSummary();
29300
+ }
29301
+ async activityByDay(days) {
29302
+ return this.deps.local.activityByDay(days);
29303
+ }
29304
+ async tokenReports() {
29305
+ return this.deps.local.tokenReports();
29306
+ }
29307
+ async knownContentHashes() {
29308
+ return this.deps.local.knownContentHashes();
29309
+ }
29310
+ async scanLedger(rulesetHash) {
29311
+ return this.deps.local.scanLedger(rulesetHash);
29312
+ }
29313
+ async recordScanned(entries) {
29314
+ return this.deps.local.recordScanned(entries);
29315
+ }
29316
+ async getRuleProbeVerdict(ruleKey) {
29317
+ return this.deps.local.getRuleProbeVerdict(ruleKey);
29318
+ }
29319
+ async setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
29320
+ return this.deps.local.setRuleProbeVerdict(ruleKey, verdict, worstProbeMs);
29321
+ }
29322
+ async openAtRestKeysForPath(path) {
29323
+ return this.deps.local.openAtRestKeysForPath(path);
29324
+ }
29325
+ async resolvedAtRestKeysForPath(path) {
29326
+ return this.deps.local.resolvedAtRestKeysForPath(path);
29327
+ }
29328
+ async insertResolution(input) {
29329
+ return this.deps.local.insertResolution(input);
29330
+ }
29331
+ async close() {
29332
+ return this.deps.local.close();
29333
+ }
29334
+ // ---------------------------------------------------------------------
29335
+ // Policy
29336
+ // ---------------------------------------------------------------------
29337
+ async getPolicyBundle() {
29338
+ const local = await this.deps.local.getPolicyBundle();
29339
+ const cached2 = await (async () => {
29340
+ try {
29341
+ return await this.deps.readCachedBundle();
29342
+ } catch {
29343
+ return null;
29344
+ }
29345
+ })();
29346
+ if (cached2 === null) return local;
29347
+ const byRuleId = /* @__PURE__ */ new Map();
29348
+ for (const rule of [...local.rules ?? [], ...cached2.rules ?? []]) {
29349
+ if (!byRuleId.has(rule.id)) byRuleId.set(rule.id, rule);
29350
+ }
29351
+ const rules = [...byRuleId.values()];
29352
+ return {
29353
+ ...local,
29354
+ // The remote version identifies the composed bundle for the poller.
29355
+ version: cached2.version,
29356
+ rules,
29357
+ policies: mergeRaiseOnly(
29358
+ local.policies,
29359
+ cached2.policies,
29360
+ ruleCategoryMap(cached2.rules, local.rules)
29361
+ ),
29362
+ customKeywords: [...local.customKeywords, ...cached2.customKeywords]
29363
+ // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
29364
+ // snapshot) and is taken from the LOCAL bundle only — never from the wire
29365
+ // or the on-disk cache. Honoring a cached one would hand the control plane, or
29366
+ // anything able to write policy-cache.json, a kill-switch over the
29367
+ // compiled-in bundled packs: `{ rulesComplete: true, rules: [] }` would
29368
+ // zero local detection. Spread from `local` above, and deliberately not
29369
+ // re-read from `cached` here.
29370
+ //
29371
+ // THREE MORE OF THE CACHED BUNDLE'S FIELDS ARE DROPPED, each on purpose,
29372
+ // and each named here so a reader can tell a decision from an omission:
29373
+ //
29374
+ // `exceptions` — an exception SUPPRESSES a detection, so honoring
29375
+ // one from an unsigned on-disk cache would let
29376
+ // anything able to write that file turn rules off.
29377
+ // Every other field this merge accepts can only
29378
+ // RAISE enforcement; this is the one that cannot,
29379
+ // so it stays local-only until the bundle is
29380
+ // signed. Exceptions remain a device-local ledger.
29381
+ // `reversibleRuleIds` — the Redact & Vault archetype makes a redaction
29382
+ // recoverable, which is a CUSTODY change: it puts
29383
+ // the detected value in the local vault instead of
29384
+ // destroying it. Taking that instruction from the
29385
+ // cache would let a remote party turn one-way
29386
+ // redaction into retention. Dropping it keeps the
29387
+ // one-way behaviour, which the schema itself calls
29388
+ // "the safe direction to default".
29389
+ // `ruleVersions` — remote rules fall back to their own spec version.
29390
+ // Cosmetic rather than protective: it only affects
29391
+ // how a finding is version-attributed, and the two
29392
+ // sides may therefore attribute org rules
29393
+ // differently. Worth carrying once there is a
29394
+ // reader that needs it; nothing reads it today.
29395
+ };
29396
+ }
29397
+ // ---------------------------------------------------------------------
29398
+ // LocalStoreMaintenance — by delegation (D3).
29399
+ //
29400
+ // Implementing these is what actually closes the skipped-local-maintenance
29401
+ // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
29402
+ // any object carrying all five, so the composite qualifies and SessionStart
29403
+ // runs maintenance on the device's real store.
29404
+ //
29405
+ // ⚠ Two of the five are SYNCHRONOUS and must stay that way. `handle-session-start`
29406
+ // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
29407
+ // return value directly; declaring them `async` here would hand those call
29408
+ // sites a Promise and silently break both.
29409
+ // ---------------------------------------------------------------------
29410
+ async sweepTerminalExceptions(retentionMs) {
29411
+ return this.deps.local.sweepTerminalExceptions(retentionMs);
29412
+ }
29413
+ capWarnEraEnforcement(policyMode) {
29414
+ return this.deps.local.capWarnEraEnforcement(policyMode);
29415
+ }
29416
+ async recordProjectFiles(projectId, scan2) {
29417
+ return this.deps.local.recordProjectFiles(projectId, scan2);
29418
+ }
29419
+ async reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
29420
+ return this.deps.local.reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot);
29421
+ }
29422
+ staleBinaryNotice(currentVersion) {
29423
+ return this.deps.local.staleBinaryNotice(currentVersion);
29424
+ }
29425
+ };
29426
+ function reKeyForForward(event, remote) {
29427
+ if (remote === null) {
29428
+ const stripped = { ...event };
29429
+ delete stripped.hostId;
29430
+ delete stripped.harnessId;
29431
+ delete stripped.sourceProjectId;
29432
+ return stripped;
29433
+ }
29434
+ const rekeyed = { ...event };
29435
+ delete rekeyed.hostId;
29436
+ delete rekeyed.harnessId;
29437
+ delete rekeyed.sourceProjectId;
29438
+ if (remote.hostId !== void 0) rekeyed.hostId = remote.hostId;
29439
+ if (remote.harnessId !== void 0) rekeyed.harnessId = remote.harnessId;
29440
+ if (remote.sourceProjectId !== void 0) rekeyed.sourceProjectId = remote.sourceProjectId;
29441
+ return rekeyed;
29442
+ }
29443
+ var BATCH_FORWARD_BUDGET_MS = 3e3;
29444
+ function llmAuditEvent(input) {
29445
+ return {
29446
+ id: llmCallId(input.sessionId, input.messageId),
29447
+ eventType: "llm_call",
29448
+ startedAt: input.startedAt,
29449
+ parentId: input.parentId,
29450
+ rootSessionId: input.rootSessionId,
29451
+ attributes: input.attributes
29452
+ };
29453
+ }
29454
+ function toolAuditEvent(input) {
29455
+ return {
29456
+ id: toolCallId(input.sessionId, input.toolUseId),
29457
+ eventType: "tool_call",
29458
+ startedAt: input.startedAt,
29459
+ parentId: input.parentId,
29460
+ rootSessionId: input.rootSessionId,
29461
+ attributes: input.attributes,
29462
+ inspections: input.inspections
29463
+ };
29464
+ }
29465
+
29466
+ // ../../packages/plugin-runtime/src/attached/policy-store.ts
29467
+ import { randomUUID as randomUUID16 } from "crypto";
29468
+ import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
29469
+ import { join as join19 } from "path";
29470
+
29471
+ // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
29472
+ import { rename as rename2 } from "fs/promises";
29473
+ var RETRYABLE = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
29474
+ var ATTEMPTS = 5;
29475
+ var delay = (ms) => new Promise((resolve2) => {
29476
+ setTimeout(resolve2, ms);
29477
+ });
29478
+ async function publishByRename(tmp, file2, move = rename2) {
29479
+ for (let attempt = 1; ; attempt += 1) {
29480
+ try {
29481
+ await move(tmp, file2);
29482
+ return;
29483
+ } catch (err) {
29484
+ const code = err.code;
29485
+ if (attempt >= ATTEMPTS || code === void 0 || !RETRYABLE.has(code)) throw err;
29486
+ await delay(attempt * 10);
29487
+ }
29488
+ }
29489
+ }
29490
+
29491
+ // ../../packages/plugin-runtime/src/attached/policy-store.ts
29492
+ function createPolicyStore(dir = dataDir()) {
29493
+ const file2 = join19(dir, "policy-cache.json");
29494
+ async function read() {
29495
+ try {
29496
+ const raw = await readFile2(file2, "utf8");
29497
+ const parsed2 = JSON.parse(raw);
29498
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
29499
+ const record2 = parsed2;
29500
+ const bundle = PolicyBundle.parse(record2.bundle);
29501
+ const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
29502
+ const etag = typeof record2.etag === "string" ? record2.etag : void 0;
29503
+ return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
29504
+ } catch {
29505
+ return null;
29506
+ }
29507
+ }
29508
+ async function write(bundle, etag) {
29509
+ await ensureDataDir(dir);
29510
+ const stored = {
29511
+ bundle,
29512
+ fetchedAtMs: Date.now(),
29513
+ ...etag === void 0 ? {} : { etag }
29514
+ };
29515
+ const tmp = `${file2}.${randomUUID16()}.tmp`;
29516
+ try {
29517
+ await writeFile2(tmp, JSON.stringify(stored), {
29518
+ encoding: "utf8",
29519
+ mode: DATA_FILE_MODE,
29520
+ flag: "wx"
29521
+ });
29522
+ await publishByRename(tmp, file2);
29523
+ } catch (err) {
29524
+ await rm(tmp, { force: true }).catch(() => void 0);
29525
+ throw err;
29526
+ }
29527
+ }
29528
+ return { read, write, file: file2 };
29529
+ }
29530
+
29531
+ // ../../packages/remote/src/http.ts
29532
+ import { request as httpRequest } from "http";
29533
+ import { request as httpsRequest } from "https";
29534
+ var DEFAULT_TIMEOUT_MS = 1e4;
29535
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
29536
+ var RemoteRequestError = class extends Error {
29537
+ constructor(status) {
29538
+ super(`control-plane request failed with status ${String(status)}`);
29539
+ this.status = status;
29540
+ this.name = "RemoteRequestError";
29541
+ }
29542
+ status;
29543
+ };
29544
+ var RemoteRequestInvalid = class extends Error {
29545
+ constructor(route, cause) {
29546
+ super(`refusing to send a malformed body to ${route}`);
29547
+ this.cause = cause;
29548
+ this.name = "RemoteRequestInvalid";
29549
+ }
29550
+ cause;
29551
+ };
29552
+ var RemoteResponseInvalid = class extends Error {
29553
+ constructor(route, detail) {
29554
+ super(`control plane answered ${route} with ${detail}`);
29555
+ this.name = "RemoteResponseInvalid";
29556
+ }
29557
+ };
29558
+ var RemoteTransportError = class extends Error {
29559
+ /**
29560
+ * The status the peer sent, when headers arrived and only the BODY was
29561
+ * refused.
29562
+ *
29563
+ * Undefined for the ordinary case this class was written for — no answer at
29564
+ * all. It exists because two paths reject after a status has already been
29565
+ * delivered: an oversized body and an aborted response. Discarding it there
29566
+ * reported a deployment answering 401 with a verbose body as a network
29567
+ * outage, which sends the reader to look at their network instead of their
29568
+ * credential.
29569
+ */
29570
+ constructor(reason, status) {
29571
+ super(`control-plane request did not complete: ${reason}`);
29572
+ this.status = status;
29573
+ this.name = "RemoteTransportError";
29574
+ }
29575
+ status;
29576
+ };
29577
+ async function send(options) {
29578
+ const url2 = new URL(options.url);
29579
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
29580
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
29581
+ const requestOptions = {
29582
+ method: options.method,
29583
+ headers: {
29584
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
29585
+ // last they win, and two of the values below are ones no caller may
29586
+ // replace: `x-api-key` is the credential, and `content-length` is the
29587
+ // byte count that stops a multi-byte body being truncated by the
29588
+ // receiver. `SendOptions.headers` is a free-form record on an exported
29589
+ // function, so "no caller does that today" is not the guarantee to rely
29590
+ // on. The one header any caller actually passes — `if-none-match` on the
29591
+ // conditional GET — is untouched by this order.
29592
+ ...options.headers,
29593
+ // The credential. One header, matching what the deployment authenticates
29594
+ // on; a second copy in an `Authorization` header would be one more place
29595
+ // it can be logged by an intermediary for no gain.
29596
+ "x-api-key": options.apiKey,
29597
+ accept: "application/json",
29598
+ ...options.body === void 0 ? {} : {
29599
+ "content-type": "application/json",
29600
+ // Byte length, not string length: a multi-byte body sent with a
29601
+ // character count is truncated by the receiver.
29602
+ "content-length": String(Buffer.byteLength(options.body))
29603
+ }
29604
+ }
29605
+ };
29606
+ return new Promise((resolve2, reject) => {
29607
+ let settled = false;
29608
+ const fail = (reason, status) => {
29609
+ if (settled) return;
29610
+ settled = true;
29611
+ reject(new RemoteTransportError(reason, status));
29612
+ };
29613
+ const req = send_(url2, requestOptions, (res) => {
29614
+ const chunks = [];
29615
+ let size = 0;
29616
+ res.on("data", (chunk) => {
29617
+ size += chunk.length;
29618
+ if (size > MAX_RESPONSE_BYTES) {
29619
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
29620
+ res.destroy();
29621
+ req.destroy();
29622
+ return;
29623
+ }
29624
+ chunks.push(chunk);
29625
+ });
29626
+ res.on("aborted", () => {
29627
+ fail("the response was aborted", res.statusCode);
29628
+ });
29629
+ res.on("end", () => {
29630
+ if (settled) return;
29631
+ settled = true;
29632
+ resolve2({
29633
+ status: res.statusCode ?? 0,
29634
+ headers: res.headers,
29635
+ body: Buffer.concat(chunks).toString("utf8")
29636
+ });
29637
+ });
29638
+ });
29639
+ const deadline = setTimeout(() => {
29640
+ fail(`no response within ${String(timeoutMs)}ms`);
29641
+ req.destroy();
29642
+ }, timeoutMs);
29643
+ deadline.unref();
29644
+ req.on("upgrade", (_res, socket) => {
29645
+ fail("the deployment answered with a protocol upgrade");
29646
+ socket.destroy();
29647
+ });
29648
+ req.on("close", () => {
29649
+ fail("the connection closed before a response was read");
29650
+ clearTimeout(deadline);
29651
+ });
29652
+ req.on("error", (err) => {
29653
+ fail(err.message);
29654
+ });
29655
+ if (options.body !== void 0) req.write(options.body);
29656
+ req.end();
29657
+ });
29658
+ }
29659
+
29660
+ // ../../packages/remote/src/client.ts
29661
+ var ROUTES = {
29662
+ events: "/v1/events",
29663
+ auditEvents: "/v1/audit-events",
29664
+ inventory: "/v1/inventory",
29665
+ storePosture: "/v1/store-posture",
29666
+ policyBundle: "/v1/policy-bundle",
29667
+ whoami: "/v1/plugin/whoami"
29668
+ };
29669
+ function headerValue(response, name) {
29670
+ const raw = response.headers[name];
29671
+ if (raw === void 0) return void 0;
29672
+ return Array.isArray(raw) ? raw[0] : raw;
29673
+ }
29674
+ function okBody(response) {
29675
+ if (response.status < 200 || response.status >= 300) {
29676
+ throw new RemoteRequestError(response.status);
29677
+ }
29678
+ return response.body;
29679
+ }
29680
+ function parsed(schema, body, route) {
29681
+ let json2;
29682
+ try {
29683
+ json2 = JSON.parse(body);
29684
+ } catch {
29685
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
29686
+ }
29687
+ const result = schema.safeParse(json2);
29688
+ if (!result.success) {
29689
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
29690
+ }
29691
+ return result.data;
29692
+ }
29693
+ function withoutTrailingSlashes(endpoint) {
29694
+ let end = endpoint.length;
29695
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
29696
+ return endpoint.slice(0, end);
29697
+ }
29698
+ var SLASH = "/".charCodeAt(0);
29699
+ function createRemoteClient(options) {
29700
+ const base = withoutTrailingSlashes(options.endpoint);
29701
+ const url2 = (route) => `${base}${route}`;
29702
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
29703
+ return {
29704
+ async ingestEvents(batch) {
29705
+ const response = await send({
29706
+ ...common,
29707
+ method: "POST",
29708
+ url: url2(ROUTES.events),
29709
+ body: JSON.stringify(batch)
29710
+ });
29711
+ return parsed(IngestAck, okBody(response), ROUTES.events);
29712
+ },
29713
+ async ingestInventory(context) {
29714
+ const response = await send({
29715
+ ...common,
29716
+ method: "POST",
29717
+ url: url2(ROUTES.inventory),
29718
+ body: JSON.stringify(context)
29719
+ });
29720
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
29721
+ },
29722
+ async recordAuditEvent(event) {
29723
+ const validated = RecordAuditEventRequest.safeParse(event);
29724
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
29725
+ const submission = validated.data;
29726
+ const response = await send({
29727
+ ...common,
29728
+ method: "POST",
29729
+ url: url2(ROUTES.auditEvents),
29730
+ body: JSON.stringify(submission)
29731
+ });
29732
+ okBody(response);
29733
+ },
29734
+ async reportStorePosture(snapshot) {
29735
+ const response = await send({
29736
+ ...common,
29737
+ method: "POST",
29738
+ url: url2(ROUTES.storePosture),
29739
+ body: JSON.stringify(snapshot)
29740
+ });
29741
+ okBody(response);
29742
+ },
29743
+ async getPolicyBundle(etag) {
29744
+ const response = await send({
29745
+ ...common,
29746
+ method: "GET",
29747
+ url: url2(ROUTES.policyBundle),
29748
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
29749
+ });
29750
+ if (response.status === 304) {
29751
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
29752
+ }
29753
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
29754
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
29755
+ },
29756
+ async whoami() {
29757
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
29758
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
29759
+ }
29760
+ };
29761
+ }
29762
+
29763
+ // ../../packages/plugin-runtime/src/attached/posture-reporter.ts
29764
+ var POSTURE_REPORT_INTERVAL_MS = 60 * 60 * 1e3;
29765
+ function createPostureReporter(deps) {
29766
+ async function prepare() {
29767
+ try {
29768
+ const state = await withTimeout(deps.store.read(), REQUEST_TIMEOUT_MS);
29769
+ if (state === null) return null;
29770
+ const nowMs = deps.now();
29771
+ const elapsed = nowMs - state.lastAttemptedAtMs;
29772
+ if (elapsed >= 0 && elapsed < POSTURE_REPORT_INTERVAL_MS) return null;
29773
+ try {
29774
+ await withTimeout(deps.store.markAttempted(state.deviceId, nowMs), REQUEST_TIMEOUT_MS);
29775
+ } catch {
29776
+ }
29777
+ const { readError, ...measurement } = deps.readStore();
29778
+ if (readError) return null;
29779
+ let plugin;
29780
+ try {
29781
+ plugin = await deps.pluginBlock?.();
29782
+ } catch {
29783
+ plugin = void 0;
29784
+ }
29785
+ return {
29786
+ deviceId: state.deviceId,
29787
+ hostname: deps.hostname(),
29788
+ capturedAt: nowMs,
29789
+ ...measurement,
29790
+ // Omit the key rather than spread an explicit `undefined` —
29791
+ // exactOptionalPropertyTypes distinguishes the two, and the bridge in
29792
+ // factory.ts keys on presence.
29793
+ ...plugin === void 0 ? {} : { plugin }
29794
+ };
29795
+ } catch {
29796
+ return null;
29797
+ }
29798
+ }
29799
+ async function send2(snapshot) {
29800
+ try {
29801
+ await deps.report(snapshot);
29802
+ } catch {
29803
+ }
29804
+ }
29805
+ return { prepare, send: send2 };
29806
+ }
29807
+
29808
+ // ../../packages/plugin-runtime/src/attached/posture-snapshot.ts
29809
+ import { statSync as statSync9 } from "fs";
29810
+ import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
29811
+
29812
+ // ../../packages/plugin-runtime/src/attached/action-counts.ts
29813
+ function emptyActionCounts() {
29814
+ return Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
29815
+ }
29816
+ function isActionTaken(value) {
29817
+ return ACTION_TAKEN_KEYS.includes(value);
29818
+ }
29819
+
29820
+ // ../../packages/plugin-runtime/src/attached/posture-snapshot.ts
29821
+ var CAPTURE_EVENT_TYPES_SQL2 = `('prompt','response','code_change','tool_use')`;
29822
+ function isSchemaAbsent(err) {
29823
+ return err instanceof Error && /no such table/i.test(err.message);
29824
+ }
29825
+ function emptyReadout(readError = false) {
29826
+ const byAction = emptyActionCounts();
29827
+ return {
29828
+ storePresent: false,
29829
+ schemaVersion: null,
29830
+ findingsTotal: 0,
29831
+ findingsFirstAt: null,
29832
+ findingsLastAt: null,
29833
+ packs: [],
29834
+ policyCounts: { total: 0, disabled: 0, byAction },
29835
+ readError
29836
+ };
29837
+ }
29838
+ function readStorePosture(dbPath2) {
29839
+ try {
29840
+ statSync9(dbPath2);
29841
+ } catch (err) {
29842
+ const code = err.code;
29843
+ if (code === "ENOENT" || code === "ENOTDIR") return emptyReadout();
29844
+ return emptyReadout(true);
29845
+ }
29846
+ let db = null;
29847
+ let version2 = null;
29848
+ let packs = [];
29849
+ let policyCounts = {
29850
+ total: 0,
29851
+ disabled: 0,
29852
+ byAction: emptyActionCounts()
29853
+ };
29854
+ let findingsTotal = 0;
29855
+ let findingsFirstAt = null;
29856
+ let findingsLastAt = null;
29857
+ const currentReadout = () => ({
29858
+ storePresent: true,
29859
+ schemaVersion: version2,
29860
+ findingsTotal,
29861
+ findingsFirstAt,
29862
+ findingsLastAt,
29863
+ packs,
29864
+ policyCounts,
29865
+ readError: false
29866
+ });
29867
+ try {
29868
+ db = new DatabaseSync3(dbPath2, { readOnly: true });
29869
+ db.exec("PRAGMA busy_timeout = 2000");
29870
+ version2 = db.prepare("PRAGMA user_version").get().user_version;
29871
+ try {
29872
+ const packRows = db.prepare(
29873
+ `SELECT namespace, pack_id, version, enabled, updated_at FROM installed_packs ORDER BY namespace, pack_id`
29874
+ ).all();
29875
+ packs = packRows.map((r) => ({
29876
+ packId: `${r.namespace}/${r.pack_id}`,
29877
+ version: r.version,
29878
+ enabled: r.enabled !== 0,
29879
+ updatedAt: r.updated_at == null ? null : String(r.updated_at)
29880
+ }));
29881
+ } catch (err) {
29882
+ if (!isSchemaAbsent(err)) throw err;
29883
+ }
29884
+ try {
29885
+ const policyRows = db.prepare(`SELECT action, enabled FROM policies`).all();
29886
+ const byAction = emptyActionCounts();
29887
+ let disabled = 0;
29888
+ for (const row of policyRows) {
29889
+ if (row.enabled === 0) disabled += 1;
29890
+ if (isActionTaken(row.action)) byAction[row.action] += 1;
29891
+ }
29892
+ policyCounts = { total: policyRows.length, disabled, byAction };
29893
+ } catch (err) {
29894
+ if (!isSchemaAbsent(err)) throw err;
29895
+ }
29896
+ try {
29897
+ const agg = db.prepare(
29898
+ `SELECT count(*) AS n, min(f.first_detected_at) AS firstAt, max(f.first_detected_at) AS lastAt
29899
+ FROM inspection_findings f JOIN audit_events e ON e.id = f.audit_event_id
29900
+ WHERE e.event_type IN ${CAPTURE_EVENT_TYPES_SQL2}`
29901
+ ).get();
29902
+ findingsTotal = agg.n;
29903
+ findingsFirstAt = agg.firstAt;
29904
+ findingsLastAt = agg.lastAt;
29905
+ } catch (err) {
29906
+ if (!isSchemaAbsent(err)) throw err;
29907
+ }
29908
+ return currentReadout();
29909
+ } catch {
29910
+ return emptyReadout(true);
29911
+ } finally {
29912
+ try {
29913
+ db?.close();
29914
+ } catch {
29915
+ }
29916
+ }
29917
+ }
29918
+
29919
+ // ../../packages/plugin-runtime/src/attached/posture-store.ts
29920
+ import { randomUUID as randomUUID17 } from "crypto";
29921
+ import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
29922
+ import { join as join20 } from "path";
29923
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
29924
+ function createPostureStore(dir = settingsDir(), legacyDir) {
29925
+ const file2 = join20(dir, "posture-state.json");
29926
+ const legacyFile = legacyDir === void 0 ? null : join20(legacyDir, "posture-state.json");
29927
+ async function persist(state) {
29928
+ await ensureDataDir(dir);
29929
+ const tmp = `${file2}.${randomUUID17()}.tmp`;
29930
+ try {
29931
+ await writeFile3(tmp, JSON.stringify(state), { encoding: "utf8", mode: DATA_FILE_MODE });
29932
+ await publishByRename(tmp, file2);
29933
+ } catch (err) {
29934
+ await rm2(tmp, { force: true }).catch(() => void 0);
29935
+ throw err;
29936
+ }
29937
+ }
29938
+ async function readFrom(path) {
29939
+ let raw;
29940
+ try {
29941
+ raw = await readFile3(path, "utf8");
29942
+ } catch (err) {
29943
+ const code = err.code;
29944
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
29945
+ throw err;
29946
+ }
29947
+ try {
29948
+ const parsed2 = JSON.parse(raw);
29949
+ if (typeof parsed2 === "object" && parsed2 !== null) {
29950
+ const record2 = parsed2;
29951
+ if (typeof record2.deviceId === "string" && UUID_RE.test(record2.deviceId)) {
29952
+ const stamp = typeof record2.lastAttemptedAtMs === "number" ? record2.lastAttemptedAtMs : typeof record2.lastReportedAtMs === "number" ? record2.lastReportedAtMs : 0;
29953
+ return { deviceId: record2.deviceId, lastAttemptedAtMs: stamp };
29954
+ }
29955
+ }
29956
+ } catch {
29957
+ }
29958
+ return null;
29959
+ }
29960
+ async function read() {
29961
+ const current = await readFrom(file2);
29962
+ if (current) return current;
29963
+ const legacy = legacyFile === null || legacyFile === file2 ? null : await readFrom(legacyFile).catch(() => null);
29964
+ if (legacy) {
29965
+ try {
29966
+ await persist(legacy);
29967
+ } catch {
29968
+ }
29969
+ return legacy;
29970
+ }
29971
+ const fresh = { deviceId: randomUUID17(), lastAttemptedAtMs: 0 };
29972
+ try {
29973
+ await ensureDataDir(dir);
29974
+ if (createOwnerOnlyFileSync(file2, JSON.stringify(fresh))) return fresh;
29975
+ } catch {
29976
+ return null;
29977
+ }
29978
+ const winner = await readFrom(file2).catch(() => null);
29979
+ if (winner) return winner;
29980
+ try {
29981
+ await persist(fresh);
29982
+ } catch {
29983
+ return null;
29984
+ }
29985
+ return fresh;
29986
+ }
29987
+ async function markAttempted(deviceId, atMs) {
29988
+ await persist({ deviceId, lastAttemptedAtMs: atMs });
29989
+ }
29990
+ return { read, markAttempted, file: file2 };
29991
+ }
29992
+
29993
+ // ../../packages/plugin-runtime/src/attached/sync-state.ts
29994
+ import { readFileSync as readFileSync13 } from "fs";
29995
+ import { join as join21 } from "path";
29996
+
29997
+ // ../../packages/plugin-runtime/src/attached/status.ts
29998
+ var REFUSAL_LINES = {
29999
+ unauthorized: "KEY REJECTED \u2014 re-attach with a valid plugin key",
30000
+ forbidden: "ACCESS REFUSED \u2014 key is valid but not permitted; ask your org admin"
30001
+ };
30002
+ var OUTCOME_LINES = {
30003
+ ok: "policy synced",
30004
+ "not-modified": "policy up to date",
30005
+ unauthorized: REFUSAL_LINES.unauthorized,
30006
+ forbidden: REFUSAL_LINES.forbidden,
30007
+ unreachable: "control plane unreachable at last attempt",
30008
+ "invalid-bundle": "control plane sent a policy bundle this build cannot read"
30009
+ };
30010
+
30011
+ // ../../packages/plugin-runtime/src/attached/sync-trigger.ts
30012
+ import { spawn } from "child_process";
30013
+ import { fileURLToPath as fileURLToPath2 } from "url";
30014
+ var SYNC_THROTTLE_MS = 15 * 60 * 1e3;
30015
+
30016
+ // ../../packages/plugin-runtime/src/attached/factory.ts
30017
+ import { hostname as hostname5 } from "os";
30018
+
30019
+ // ../../packages/plugin-runtime/src/standalone-gateway.ts
30020
+ import { randomUUID as randomUUID18 } from "crypto";
30021
+
30022
+ // ../../packages/plugin-runtime/src/recorder.ts
30023
+ var PLUGIN_RECORDER_BINARY = "plugin";
30024
+
30025
+ // ../../packages/plugin-runtime/src/standalone-gateway.ts
30026
+ var StandaloneDataGateway = class {
30027
+ db;
30028
+ // Kept for the fingerprint key lookup (exception.key lives beside the store).
30029
+ dataDir;
30030
+ // One notice per gateway — see warnRulesetDiscarded.
30031
+ warnedRulesetDiscarded = false;
30032
+ constructor(dataDir2, detections = [], meta3) {
30033
+ this.db = openLocalDatabase(dataDir2);
30034
+ this.dataDir = dataDir2;
30035
+ this.db.installedPacks.recordInventory(detections, meta3);
30036
+ }
30037
+ recordCapture(record2) {
30038
+ this.db.recordCapture(record2.event, record2.findings);
30039
+ return Promise.resolve();
30040
+ }
30041
+ ensureInventory(ctx) {
30042
+ return Promise.resolve(this.db.ensureInventory(ctx));
30043
+ }
30044
+ recordAuditEvent(event) {
30045
+ this.db.auditEvents.insertAuditEvent(event);
30046
+ return Promise.resolve();
30047
+ }
30048
+ // The id is minted inside the repository from the natural key — the plugin can't
30049
+ // import @akasecurity/persistence to compute it, so the gateway is the boundary that
30050
+ // hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
28579
30051
  // converge a streaming partial/final split (see insertLlmCall).
28580
30052
  recordLlmCall(input) {
28581
30053
  this.db.auditEvents.insertLlmCall(input);
@@ -28586,12 +30058,12 @@ var StandaloneDataGateway = class {
28586
30058
  // reconciler drops the whole pass and recovers it idempotently on the next read.
28587
30059
  recordLlmCalls(inputs) {
28588
30060
  if (inputs.length === 0) return Promise.resolve();
28589
- return new Promise((resolve, reject) => {
30061
+ return new Promise((resolve2, reject) => {
28590
30062
  try {
28591
30063
  this.db.auditEvents.runInTransaction(() => {
28592
30064
  for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
28593
30065
  });
28594
- resolve();
30066
+ resolve2();
28595
30067
  } catch (err) {
28596
30068
  reject(err instanceof Error ? err : new Error(String(err)));
28597
30069
  }
@@ -28603,12 +30075,12 @@ var StandaloneDataGateway = class {
28603
30075
  // drops the whole pass and recovers it idempotently next time.
28604
30076
  recordToolCalls(inputs) {
28605
30077
  if (inputs.length === 0) return Promise.resolve();
28606
- return new Promise((resolve, reject) => {
30078
+ return new Promise((resolve2, reject) => {
28607
30079
  try {
28608
30080
  this.db.auditEvents.runInTransaction(() => {
28609
30081
  for (const input of inputs) this.writeToolCall(input);
28610
30082
  });
28611
- resolve();
30083
+ resolve2();
28612
30084
  } catch (err) {
28613
30085
  reject(err instanceof Error ? err : new Error(String(err)));
28614
30086
  }
@@ -28750,7 +30222,7 @@ var StandaloneDataGateway = class {
28750
30222
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
28751
30223
  const installed = this.installedScanRules();
28752
30224
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
28753
- id: randomUUID15(),
30225
+ id: randomUUID18(),
28754
30226
  scope: "global",
28755
30227
  target: { ruleId },
28756
30228
  action,
@@ -28904,22 +30376,68 @@ var StandaloneDataGateway = class {
28904
30376
  }
28905
30377
  };
28906
30378
 
30379
+ // ../../packages/plugin-runtime/src/attached/factory.ts
30380
+ function resolveGatewayForConfig(config2, meta3) {
30381
+ const local = new StandaloneDataGateway(config2.dataDir, bundledDetections(), meta3);
30382
+ try {
30383
+ if (!isAttached(config2.settings)) return local;
30384
+ const connection = config2.settings.controlPlane;
30385
+ if (connection === void 0) return local;
30386
+ const state = readControlPlaneCredentialState(config2.settingsDir, connection);
30387
+ if (!state.usable) return local;
30388
+ const client = createRemoteClient({
30389
+ endpoint: connection.endpoint,
30390
+ apiKey: state.credential.apiKey
30391
+ });
30392
+ const store = createPolicyStore(config2.dataDir);
30393
+ const postureStore = createPostureStore(config2.settingsDir, config2.dataDir);
30394
+ const forward = createForwardPolicy({ dir: config2.dataDir });
30395
+ return new AttachedDataGateway({
30396
+ local,
30397
+ client,
30398
+ dataDir: config2.dataDir,
30399
+ readCachedBundle: () => store.read().then((cached2) => cached2?.bundle ?? null),
30400
+ forward,
30401
+ posture: createPostureReporter({
30402
+ // THROUGH THE BREAKER, and wrapped HERE rather than around
30403
+ // `PostureReporter.send`. The reporter swallows every error by
30404
+ // contract, so a wrap outside it would hand `forward.run` a resolved
30405
+ // promise for a send that failed — recording a SUCCESS, clearing
30406
+ // `consecutiveFailures` and `lastFailure`, and telling `aka status` the
30407
+ // forward recovered when nothing did. Wrapping the raw client call puts
30408
+ // the breaker above the swallow, where it can see the truth.
30409
+ //
30410
+ // What it buys: once the breaker is open — the plane already confirmed
30411
+ // down by the gateway's own writes — this stops paying a request
30412
+ // timeout per throttle interval to re-learn it.
30413
+ report: (snapshot) => forward.run(() => client.reportStorePosture(snapshot)).then(() => void 0),
30414
+ store: postureStore,
30415
+ readStore: () => readStorePosture(config2.dbPath),
30416
+ hostname: () => hostname5(),
30417
+ now: () => Date.now()
30418
+ })
30419
+ });
30420
+ } catch {
30421
+ return local;
30422
+ }
30423
+ }
30424
+
28907
30425
  // ../../packages/plugin-runtime/src/resolve.ts
28908
- var standaloneGatewayFactory = (config2, meta3) => new StandaloneDataGateway(config2.dataDir, bundledDetections(), meta3);
28909
- var defaultGatewayFactory = standaloneGatewayFactory;
30426
+ var configuredGatewayFactory = (config2, meta3) => resolveGatewayForConfig(config2, meta3);
30427
+ var defaultGatewayFactory = configuredGatewayFactory;
28910
30428
  function resolveDataGateway(config2, meta3, gatewayFactory = defaultGatewayFactory) {
28911
30429
  return gatewayFactory(config2, meta3);
28912
30430
  }
28913
30431
 
28914
30432
  // ../../packages/plugin-runtime/src/handle-session-start.ts
28915
- import { randomUUID as randomUUID16 } from "crypto";
30433
+ import { randomUUID as randomUUID19 } from "crypto";
28916
30434
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
28917
30435
 
28918
30436
  // src/command-registry.ts
28919
30437
  import { readdirSync as readdirSync5 } from "fs";
28920
- import { fileURLToPath as fileURLToPath2 } from "url";
30438
+ import { fileURLToPath as fileURLToPath3 } from "url";
28921
30439
  var COMMAND_NAMESPACE = "aka";
28922
- var COMMANDS_DIR = fileURLToPath2(new URL("../commands", import.meta.url));
30440
+ var COMMANDS_DIR = fileURLToPath3(new URL("../commands", import.meta.url));
28923
30441
  function readRegisteredCommands() {
28924
30442
  return readdirSync5(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
28925
30443
  }
@@ -29029,7 +30547,7 @@ function show(body) {
29029
30547
 
29030
30548
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
29031
30549
  import { writeFileSync as writeFileSync7 } from "fs";
29032
- import { join as join15 } from "path";
30550
+ import { join as join22 } from "path";
29033
30551
 
29034
30552
  // ../../packages/setup-wizard/src/triage/merge.ts
29035
30553
  var RANK = Object.fromEntries(
@@ -29037,9 +30555,9 @@ var RANK = Object.fromEntries(
29037
30555
  );
29038
30556
 
29039
30557
  // ../../packages/setup-wizard/src/triage/plan-file.ts
29040
- import { mkdtempSync, readFileSync as readFileSync10, rmdirSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
30558
+ import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
29041
30559
  import { tmpdir } from "os";
29042
- import { basename as basename6, dirname as dirname4, join as join16 } from "path";
30560
+ import { basename as basename6, dirname as dirname5, join as join23 } from "path";
29043
30561
  var SuppressionEntrySchema = external_exports.object({
29044
30562
  ruleId: external_exports.string(),
29045
30563
  category: DetectionCategory,