@i4ctime/q-ring 0.17.0 → 0.17.6

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.
@@ -1502,10 +1502,23 @@ var secretsPolicySchema = z2.object({
1502
1502
  requireRotationFormatForTags: stringArray.optional(),
1503
1503
  maxTtlSeconds: z2.number().optional()
1504
1504
  }).strict();
1505
+ var rateLimitSchema = z2.object({
1506
+ maxCalls: z2.number().int().positive(),
1507
+ perSeconds: z2.number().positive()
1508
+ }).strict();
1509
+ var wrapPolicySchema = z2.object({
1510
+ allowTools: stringArray.optional(),
1511
+ denyTools: stringArray.optional(),
1512
+ approveTools: stringArray.optional(),
1513
+ rateLimit: rateLimitSchema.optional(),
1514
+ toolRateLimits: z2.record(z2.string(), rateLimitSchema).optional(),
1515
+ redactResults: z2.boolean().optional()
1516
+ }).strict();
1505
1517
  var policySchema = z2.object({
1506
1518
  mcp: mcpPolicySchema.optional(),
1507
1519
  exec: execPolicySchema.optional(),
1508
- secrets: secretsPolicySchema.optional()
1520
+ secrets: secretsPolicySchema.optional(),
1521
+ wrap: wrapPolicySchema.optional()
1509
1522
  }).strict();
1510
1523
  var PolicyConfigError = class extends Error {
1511
1524
  constructor(message) {
@@ -1681,6 +1694,7 @@ function getPolicySummary(projectPath) {
1681
1694
  hasMcpPolicy: !!policy.mcp,
1682
1695
  hasExecPolicy: !!policy.exec,
1683
1696
  hasSecretPolicy: !!policy.secrets,
1697
+ hasWrapPolicy: !!policy.wrap,
1684
1698
  details: policy
1685
1699
  };
1686
1700
  }
@@ -1785,6 +1799,129 @@ function notifyApprovalRequested(key, source) {
1785
1799
  );
1786
1800
  }
1787
1801
 
1802
+ // src/core/canary-webhooks.ts
1803
+ import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
1804
+ import { join as join10 } from "path";
1805
+ import { homedir as homedir7 } from "os";
1806
+ import { randomUUID as randomUUID2 } from "crypto";
1807
+ function getRegistryPath4() {
1808
+ const override = process.env.QRING_CANARY_ALERTS_PATH;
1809
+ if (override) return override;
1810
+ const dir = join10(homedir7(), ".config", "q-ring");
1811
+ if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
1812
+ return join10(dir, "canary-alerts.json");
1813
+ }
1814
+ function loadRegistry4() {
1815
+ return loadJsonRegistry(getRegistryPath4(), { channels: [] });
1816
+ }
1817
+ function listCanaryAlerts() {
1818
+ return loadRegistry4().channels;
1819
+ }
1820
+ function describeAlertUrl(url) {
1821
+ try {
1822
+ const u = new URL(url);
1823
+ const tail = u.pathname.length > 12 ? `${u.pathname.slice(0, 8)}\u2026${u.pathname.slice(-4)}` : u.pathname;
1824
+ return `${u.protocol}//${u.host}${tail}`;
1825
+ } catch {
1826
+ return url;
1827
+ }
1828
+ }
1829
+ function headline(event) {
1830
+ return event.test ? "q-ring canary alert test" : "q-ring: CANARY TRIPPED";
1831
+ }
1832
+ function summary(event) {
1833
+ const who = event.agent ? `${event.source} (${event.agent})` : event.source;
1834
+ const where = event.env ? `${event.scope}/${event.env}` : event.scope;
1835
+ return event.test ? `This is a test from \`qring canary alert test\`. Trips on honeytoken "${event.key}" (${where}) would arrive here.` : `Honeytoken "${event.key}" (${where}) was read by ${who} at ${event.timestamp}. ${event.detail}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`;
1836
+ }
1837
+ function buildAlertPayload(type, event) {
1838
+ const json = { "Content-Type": "application/json", "User-Agent": "q-ring-canary/1.0" };
1839
+ switch (type) {
1840
+ case "discord":
1841
+ return {
1842
+ body: JSON.stringify({ content: `\u{1F6A8} **${headline(event)}**
1843
+ ${summary(event)}` }),
1844
+ headers: json
1845
+ };
1846
+ case "slack":
1847
+ return {
1848
+ body: JSON.stringify({ text: `:rotating_light: *${headline(event)}*
1849
+ ${summary(event)}` }),
1850
+ headers: json
1851
+ };
1852
+ case "ntfy":
1853
+ return {
1854
+ body: summary(event),
1855
+ headers: {
1856
+ "Content-Type": "text/plain",
1857
+ "User-Agent": "q-ring-canary/1.0",
1858
+ Title: headline(event),
1859
+ Priority: event.test ? "3" : "5",
1860
+ Tags: event.test ? "test_tube" : "rotating_light"
1861
+ }
1862
+ };
1863
+ case "generic":
1864
+ return {
1865
+ body: JSON.stringify({
1866
+ event: "canary",
1867
+ test: event.test ?? false,
1868
+ key: event.key,
1869
+ scope: event.scope,
1870
+ env: event.env,
1871
+ source: event.source,
1872
+ agent: event.agent ?? void 0,
1873
+ detail: event.detail,
1874
+ timestamp: event.timestamp
1875
+ }),
1876
+ headers: json
1877
+ };
1878
+ }
1879
+ }
1880
+ async function sendOne(channel, event) {
1881
+ const base = { channelId: channel.id, type: channel.type };
1882
+ const ssrfBlock = await checkSSRF(channel.url);
1883
+ if (ssrfBlock) {
1884
+ logAudit({
1885
+ action: "policy_deny",
1886
+ key: event.key,
1887
+ scope: event.scope,
1888
+ source: "hook",
1889
+ detail: `canary alert SSRF blocked: ${describeAlertUrl(channel.url)}`
1890
+ });
1891
+ return { ...base, success: false, message: ssrfBlock };
1892
+ }
1893
+ try {
1894
+ const { body, headers } = buildAlertPayload(channel.type, event);
1895
+ const res = await httpRequest({
1896
+ url: channel.url,
1897
+ method: "POST",
1898
+ headers,
1899
+ body,
1900
+ timeoutMs: 1e4
1901
+ });
1902
+ return {
1903
+ ...base,
1904
+ success: res.statusCode >= 200 && res.statusCode < 300,
1905
+ message: `HTTP ${res.statusCode}`
1906
+ };
1907
+ } catch (err) {
1908
+ return { ...base, success: false, message: err instanceof Error ? err.message : "HTTP error" };
1909
+ }
1910
+ }
1911
+ async function sendCanaryAlerts(event, onlyId) {
1912
+ const channels = listCanaryAlerts().filter((c) => onlyId ? c.id === onlyId : c.enabled);
1913
+ if (channels.length === 0) return [];
1914
+ const results = await Promise.allSettled(channels.map((c) => sendOne(c, event)));
1915
+ return results.map(
1916
+ (r, i) => r.status === "fulfilled" ? r.value : {
1917
+ channelId: channels[i].id,
1918
+ type: channels[i].type,
1919
+ success: false,
1920
+ message: String(r.reason)
1921
+ }
1922
+ );
1923
+ }
1924
+
1788
1925
  // src/core/canary-alert.ts
1789
1926
  var TRIP_THROTTLE_MS = 30 * 1e3;
1790
1927
  var lastAlerted = /* @__PURE__ */ new Map();
@@ -1798,16 +1935,27 @@ function recordCanaryTrip(trip) {
1798
1935
  source: trip.source,
1799
1936
  detail: `CANARY TRIPPED: ${trip.detail ?? `honeytoken read via ${trip.source}`}`
1800
1937
  });
1801
- if (!notificationsEnabled()) return;
1802
1938
  const now = Date.now();
1803
1939
  const last = lastAlerted.get(trip.key);
1804
1940
  if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
1805
1941
  lastAlerted.set(trip.key, now);
1806
- const who = agent ? `${trip.source} (${agent})` : trip.source;
1807
- notifyUser(
1808
- "q-ring: CANARY TRIPPED",
1809
- `Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
1810
- );
1942
+ if (notificationsEnabled()) {
1943
+ const who = agent ? `${trip.source} (${agent})` : trip.source;
1944
+ notifyUser(
1945
+ "q-ring: CANARY TRIPPED",
1946
+ `Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
1947
+ );
1948
+ }
1949
+ void sendCanaryAlerts({
1950
+ key: trip.key,
1951
+ scope: trip.scope,
1952
+ env: trip.env,
1953
+ source: trip.source,
1954
+ agent,
1955
+ detail: trip.detail ?? `honeytoken read via ${trip.source}`,
1956
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1957
+ }).catch(() => {
1958
+ });
1811
1959
  }
1812
1960
 
1813
1961
  // src/core/provision.ts
@@ -2502,9 +2650,9 @@ function tunnelList() {
2502
2650
  }
2503
2651
 
2504
2652
  // src/core/memory.ts
2505
- import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
2506
- import { join as join10 } from "path";
2507
- import { homedir as homedir7, hostname, userInfo } from "os";
2653
+ import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, chmodSync as chmodSync3 } from "fs";
2654
+ import { join as join11 } from "path";
2655
+ import { homedir as homedir8, hostname, userInfo } from "os";
2508
2656
  import {
2509
2657
  createCipheriv as createCipheriv2,
2510
2658
  createDecipheriv as createDecipheriv2,
@@ -2516,21 +2664,21 @@ var MEMORY_FILE = "agent-memory.enc";
2516
2664
  var KEYRING_SERVICE = "qring-memory-key";
2517
2665
  var KEYRING_ACCOUNT = "encryption-key";
2518
2666
  function getMemoryDir() {
2519
- const dir = join10(homedir7(), ".config", "q-ring");
2520
- if (!existsSync7(dir)) {
2521
- mkdirSync7(dir, { recursive: true, mode: 448 });
2667
+ const dir = join11(homedir8(), ".config", "q-ring");
2668
+ if (!existsSync8(dir)) {
2669
+ mkdirSync8(dir, { recursive: true, mode: 448 });
2522
2670
  }
2523
2671
  return dir;
2524
2672
  }
2525
2673
  function writeMemoryFile(path, data) {
2526
- writeFileSync6(path, data, { mode: 384 });
2674
+ writeFileSync7(path, data, { mode: 384 });
2527
2675
  try {
2528
2676
  chmodSync3(path, 384);
2529
2677
  } catch {
2530
2678
  }
2531
2679
  }
2532
2680
  function getMemoryPath() {
2533
- return join10(getMemoryDir(), MEMORY_FILE);
2681
+ return join11(getMemoryDir(), MEMORY_FILE);
2534
2682
  }
2535
2683
  var PBKDF2_ITERATIONS2 = 21e4;
2536
2684
  var KEY_LENGTH2 = 32;
@@ -2624,7 +2772,7 @@ function decrypt(blob) {
2624
2772
  }
2625
2773
  function loadStore2() {
2626
2774
  const path = getMemoryPath();
2627
- if (!existsSync7(path)) {
2775
+ if (!existsSync8(path)) {
2628
2776
  return { entries: {} };
2629
2777
  }
2630
2778
  try {
@@ -2672,6 +2820,84 @@ function forget(key) {
2672
2820
  return false;
2673
2821
  }
2674
2822
 
2823
+ // src/core/sessions.ts
2824
+ var DEFAULT_MAX_EVENTS = 200;
2825
+ var UNLABELED = "unlabeled";
2826
+ function slug(label) {
2827
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || UNLABELED;
2828
+ }
2829
+ function sessionKeyFor(event) {
2830
+ if (event.action === "wrap" && event.correlationId) return `wrap:${event.correlationId}`;
2831
+ if (event.agent) return `pid:${event.pid}:${slug(event.agent)}`;
2832
+ if (event.source === "mcp" || event.source === "agent") return `pid:${event.pid}:${UNLABELED}`;
2833
+ return null;
2834
+ }
2835
+ function wrapLabelFrom(detail) {
2836
+ const m = detail?.match(/^airlock session started: (.+?)(?: \(env (?:inherited|stripped)\))?$/);
2837
+ return m?.[1];
2838
+ }
2839
+ function buildSessions(events, maxEvents = DEFAULT_MAX_EVENTS) {
2840
+ const byKey = /* @__PURE__ */ new Map();
2841
+ for (const e of events) {
2842
+ const key = sessionKeyFor(e);
2843
+ if (!key) continue;
2844
+ const bucket = byKey.get(key);
2845
+ if (bucket) bucket.push(e);
2846
+ else byKey.set(key, [e]);
2847
+ }
2848
+ const sessions = [];
2849
+ for (const [key, bucket] of byKey) {
2850
+ bucket.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2851
+ const first = bucket[0];
2852
+ const last = bucket[bucket.length - 1];
2853
+ const countsByAction = {};
2854
+ const sources = /* @__PURE__ */ new Map();
2855
+ const keys = /* @__PURE__ */ new Set();
2856
+ let denials = 0;
2857
+ let wrapLabel;
2858
+ for (const e of bucket) {
2859
+ countsByAction[e.action] = (countsByAction[e.action] ?? 0) + 1;
2860
+ sources.set(e.source, (sources.get(e.source) ?? 0) + 1);
2861
+ if (e.key) keys.add(e.key);
2862
+ if (e.action === "policy_deny") denials++;
2863
+ if (!wrapLabel && e.action === "wrap") wrapLabel = wrapLabelFrom(e.detail);
2864
+ }
2865
+ const source = [...sources.entries()].sort((a, b) => b[1] - a[1])[0][0];
2866
+ const agent = bucket.find((e) => e.agent)?.agent ?? UNLABELED;
2867
+ const isWrap = key.startsWith("wrap:");
2868
+ sessions.push({
2869
+ id: isWrap ? key.slice("wrap:".length) : `${first.pid}-${slug(agent)}`,
2870
+ agent,
2871
+ source,
2872
+ pid: first.pid,
2873
+ startedAt: first.timestamp,
2874
+ endedAt: last.timestamp,
2875
+ eventCount: bucket.length,
2876
+ countsByAction,
2877
+ keys: [...keys].sort(),
2878
+ denials,
2879
+ ...isWrap ? { wrapLabel: wrapLabel ?? "(unknown command)" } : {},
2880
+ events: bucket.slice(-maxEvents).reverse()
2881
+ });
2882
+ }
2883
+ sessions.sort((a, b) => new Date(b.endedAt).getTime() - new Date(a.endedAt).getTime());
2884
+ return sessions;
2885
+ }
2886
+ function agentVisibleEvents(events) {
2887
+ return events.filter((e) => e.action !== "canary");
2888
+ }
2889
+ function loadEvents(query) {
2890
+ return queryAudit({ since: query.since, agent: query.agent }).filter((e) => e.action !== "list");
2891
+ }
2892
+ function listAgentSessionsForAgents(query = {}) {
2893
+ const sessions = buildSessions(agentVisibleEvents(loadEvents(query)), query.maxEvents);
2894
+ return query.limit ? sessions.slice(0, query.limit) : sessions;
2895
+ }
2896
+ function summariseSession(session) {
2897
+ const { events: _events, ...summary2 } = session;
2898
+ return summary2;
2899
+ }
2900
+
2675
2901
  export {
2676
2902
  PACKAGE_VERSION,
2677
2903
  checkDecay,
@@ -2714,6 +2940,9 @@ export {
2714
2940
  remember,
2715
2941
  recall,
2716
2942
  listMemory,
2717
- forget
2943
+ forget,
2944
+ buildSessions,
2945
+ listAgentSessionsForAgents,
2946
+ summariseSession
2718
2947
  };
2719
- //# sourceMappingURL=chunk-KFILBHOY.js.map
2948
+ //# sourceMappingURL=chunk-VMDD5QTJ.js.map