@superatomai/sdk-node 0.0.56-dsp → 0.0.57-dsp
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +298 -1
- package/dist/index.d.ts +298 -1
- package/dist/index.js +574 -61
- package/dist/index.mjs +554 -59
- package/dist/userResponse/scripts/script-bootstrap.js +20 -11
- package/dist/userResponse/scripts/script-bootstrap.mjs +20 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
DEFAULT_AGENT_CONFIG: () => DEFAULT_AGENT_CONFIG,
|
|
37
37
|
LLM: () => LLM,
|
|
38
38
|
MainAgent: () => MainAgent,
|
|
39
|
+
MissingRequestContextError: () => MissingRequestContextError,
|
|
39
40
|
STORAGE_CONFIG: () => STORAGE_CONFIG,
|
|
40
41
|
ScriptMatcher: () => ScriptMatcher,
|
|
41
42
|
ScriptStore: () => ScriptStore,
|
|
@@ -47,36 +48,53 @@ __export(index_exports, {
|
|
|
47
48
|
UserManager: () => UserManager,
|
|
48
49
|
anthropicLLM: () => anthropicLLM,
|
|
49
50
|
assertReadOnlySql: () => assertReadOnlySql,
|
|
51
|
+
bindContext: () => bindContext,
|
|
52
|
+
captureContext: () => captureContext,
|
|
53
|
+
clearContextTokens: () => clearContextTokens,
|
|
54
|
+
contextTokenCount: () => contextTokenCount,
|
|
50
55
|
createCodexSession: () => createCodexSession,
|
|
51
56
|
createCodingAgentSession: () => createCodingAgentSession,
|
|
52
57
|
dashboardConversationHistory: () => dashboardConversationHistory,
|
|
53
58
|
dataFingerprint: () => dataFingerprint,
|
|
59
|
+
describeContext: () => describeContext,
|
|
54
60
|
describeTriggers: () => describeTriggers,
|
|
55
61
|
emitAgentEvent: () => emitAgentEvent,
|
|
56
62
|
extractNamedExport: () => extractNamedExport,
|
|
57
63
|
geminiLLM: () => geminiLLM,
|
|
58
64
|
getCodingAgent: () => getCodingAgent,
|
|
59
65
|
getCodingAgentsConfig: () => getCodingAgentsConfig,
|
|
66
|
+
getRequestContext: () => getRequestContext,
|
|
60
67
|
groqLLM: () => groqLLM,
|
|
61
68
|
hybridRerank: () => hybridRerank,
|
|
69
|
+
isUserContext: () => isUserContext,
|
|
62
70
|
listCodingAgents: () => listCodingAgents,
|
|
63
71
|
llmUsageLogger: () => llmUsageLogger,
|
|
64
72
|
logger: () => logger,
|
|
73
|
+
mintContextToken: () => mintContextToken,
|
|
65
74
|
normalizeScriptBody: () => normalizeScriptBody,
|
|
66
75
|
onAgentRun: () => onAgentRun,
|
|
67
76
|
openaiLLM: () => openaiLLM,
|
|
77
|
+
policiesFromConfig: () => policiesFromConfig,
|
|
68
78
|
queryCache: () => queryCache,
|
|
69
79
|
registerCodingAgentCollection: () => registerCodingAgentCollection,
|
|
70
80
|
registerCodingAgents: () => registerCodingAgents,
|
|
71
81
|
registerDashboardAgentCollection: () => registerDashboardAgentCollection,
|
|
82
|
+
requireRequestContext: () => requireRequestContext,
|
|
72
83
|
rerankChromaResults: () => rerankChromaResults,
|
|
73
84
|
rerankConversationResults: () => rerankConversationResults,
|
|
85
|
+
resolveContextToken: () => resolveContextToken,
|
|
74
86
|
resolveScriptRecipeStore: () => resolveScriptRecipeStore,
|
|
87
|
+
revokeContextToken: () => revokeContextToken,
|
|
88
|
+
runAsSystem: () => runAsSystem,
|
|
89
|
+
runAsUser: () => runAsUser,
|
|
75
90
|
runCodingAgent: () => runCodingAgent,
|
|
76
91
|
runScript: () => runScript,
|
|
92
|
+
runWithContext: () => runWithContext,
|
|
93
|
+
runWithMessageIdentity: () => runWithMessageIdentity,
|
|
77
94
|
startAgentTriggers: () => startAgentTriggers,
|
|
78
95
|
stopAgentTriggers: () => stopAgentTriggers,
|
|
79
96
|
userPromptErrorLogger: () => userPromptErrorLogger,
|
|
97
|
+
withCapturedContext: () => withCapturedContext,
|
|
80
98
|
writeBridgeShims: () => writeBridgeShims
|
|
81
99
|
});
|
|
82
100
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -360,11 +378,17 @@ var UserPromptRequestPayloadSchema = import_zod3.z.object({
|
|
|
360
378
|
}).optional(),
|
|
361
379
|
responseMode: import_zod3.z.enum(["component", "text"]).optional()
|
|
362
380
|
});
|
|
381
|
+
var MessageAuthContextSchema = import_zod3.z.object({
|
|
382
|
+
userId: import_zod3.z.string(),
|
|
383
|
+
orgId: import_zod3.z.string().optional(),
|
|
384
|
+
role: import_zod3.z.string().optional()
|
|
385
|
+
});
|
|
363
386
|
var UserPromptRequestMessageSchema = import_zod3.z.object({
|
|
364
387
|
id: import_zod3.z.string(),
|
|
365
388
|
from: MessageParticipantSchema,
|
|
366
389
|
type: import_zod3.z.literal("USER_PROMPT_REQ"),
|
|
367
|
-
payload: UserPromptRequestPayloadSchema
|
|
390
|
+
payload: UserPromptRequestPayloadSchema,
|
|
391
|
+
authContext: MessageAuthContextSchema.optional()
|
|
368
392
|
});
|
|
369
393
|
var UserPromptAbortRequestPayloadSchema = import_zod3.z.object({
|
|
370
394
|
uiBlockId: import_zod3.z.string(),
|
|
@@ -814,7 +838,8 @@ var DashCompRequestMessageSchema = import_zod3.z.object({
|
|
|
814
838
|
id: import_zod3.z.string(),
|
|
815
839
|
from: MessageParticipantSchema,
|
|
816
840
|
type: import_zod3.z.literal("DASH_COMP_REQ"),
|
|
817
|
-
payload: DashCompRequestPayloadSchema
|
|
841
|
+
payload: DashCompRequestPayloadSchema,
|
|
842
|
+
authContext: MessageAuthContextSchema.optional()
|
|
818
843
|
});
|
|
819
844
|
var DashboardAgentRequestPayloadSchema = import_zod3.z.object({
|
|
820
845
|
prompt: import_zod3.z.string(),
|
|
@@ -887,7 +912,8 @@ var ReportCompRequestMessageSchema = import_zod3.z.object({
|
|
|
887
912
|
id: import_zod3.z.string(),
|
|
888
913
|
from: MessageParticipantSchema,
|
|
889
914
|
type: import_zod3.z.literal("REPORT_COMP_REQ"),
|
|
890
|
-
payload: ReportCompRequestPayloadSchema
|
|
915
|
+
payload: ReportCompRequestPayloadSchema,
|
|
916
|
+
authContext: MessageAuthContextSchema.optional()
|
|
891
917
|
});
|
|
892
918
|
var ReportCompAbortRequestPayloadSchema = import_zod3.z.object({
|
|
893
919
|
reportId: import_zod3.z.string()
|
|
@@ -1681,6 +1707,101 @@ var ThreadManager = class _ThreadManager {
|
|
|
1681
1707
|
|
|
1682
1708
|
// src/utils/query-cache.ts
|
|
1683
1709
|
var import_crypto3 = __toESM(require("crypto"));
|
|
1710
|
+
|
|
1711
|
+
// src/utils/request-context.ts
|
|
1712
|
+
var import_async_hooks = require("async_hooks");
|
|
1713
|
+
var STORAGE_KEY = Symbol.for("superatom.requestContext.v1");
|
|
1714
|
+
function getStorage() {
|
|
1715
|
+
const g = globalThis;
|
|
1716
|
+
let store = g[STORAGE_KEY];
|
|
1717
|
+
if (!store) {
|
|
1718
|
+
store = new import_async_hooks.AsyncLocalStorage();
|
|
1719
|
+
g[STORAGE_KEY] = store;
|
|
1720
|
+
}
|
|
1721
|
+
return store;
|
|
1722
|
+
}
|
|
1723
|
+
function enforcementApplies() {
|
|
1724
|
+
return (process.env.SA_SQL_REWRITE_MODE || "").toLowerCase() === "enforce";
|
|
1725
|
+
}
|
|
1726
|
+
function getRequestContext() {
|
|
1727
|
+
return getStorage().getStore();
|
|
1728
|
+
}
|
|
1729
|
+
function requireRequestContext(what = "this operation") {
|
|
1730
|
+
const ctx = getStorage().getStore();
|
|
1731
|
+
if (!ctx) {
|
|
1732
|
+
throw new MissingRequestContextError(
|
|
1733
|
+
`No request context for ${what}. The caller must establish one with runWithContext() or declare itself trusted with runAsSystem().`
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
return ctx;
|
|
1737
|
+
}
|
|
1738
|
+
var MissingRequestContextError = class extends Error {
|
|
1739
|
+
constructor(message) {
|
|
1740
|
+
super(message);
|
|
1741
|
+
this.name = "MissingRequestContextError";
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
function runWithContext(ctx, fn) {
|
|
1745
|
+
return getStorage().run(ctx, fn);
|
|
1746
|
+
}
|
|
1747
|
+
function runAsUser(user, fn) {
|
|
1748
|
+
const { userId, orgId, role, projectId, requestId, policies } = user;
|
|
1749
|
+
return runWithContext(
|
|
1750
|
+
{ principal: { kind: "user", userId, orgId, role }, projectId, requestId, policies },
|
|
1751
|
+
fn
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
function runAsSystem(reason, fn, opts) {
|
|
1755
|
+
if (!reason || !reason.trim()) {
|
|
1756
|
+
throw new Error("runAsSystem() requires a non-empty reason \u2014 it identifies the path in the audit log.");
|
|
1757
|
+
}
|
|
1758
|
+
return runWithContext(
|
|
1759
|
+
{ principal: { kind: "system", reason: reason.trim() }, projectId: opts?.projectId, requestId: opts?.requestId },
|
|
1760
|
+
fn
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
function captureContext() {
|
|
1764
|
+
return getStorage().getStore();
|
|
1765
|
+
}
|
|
1766
|
+
function withCapturedContext(ctx, fn) {
|
|
1767
|
+
return ctx ? runWithContext(ctx, fn) : fn();
|
|
1768
|
+
}
|
|
1769
|
+
function bindContext(fn) {
|
|
1770
|
+
const captured = captureContext();
|
|
1771
|
+
return (...args) => withCapturedContext(captured, () => fn(...args));
|
|
1772
|
+
}
|
|
1773
|
+
function describeContext(ctx = getRequestContext()) {
|
|
1774
|
+
if (!ctx) return "anonymous";
|
|
1775
|
+
return ctx.principal.kind === "user" ? `user:${ctx.principal.userId}` : `system:${ctx.principal.reason}`;
|
|
1776
|
+
}
|
|
1777
|
+
function isUserContext(ctx = getRequestContext()) {
|
|
1778
|
+
return ctx?.principal.kind === "user";
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/utils/query-cache.ts
|
|
1782
|
+
var fingerprints = /* @__PURE__ */ new WeakMap();
|
|
1783
|
+
function canonicalPolicies(policies) {
|
|
1784
|
+
const norm = policies.map((p) => ({
|
|
1785
|
+
s: p.sourceId ?? "*",
|
|
1786
|
+
t: [...p.readableTables ?? []].sort(),
|
|
1787
|
+
f: (p.rowFilters ?? []).map((f) => ({
|
|
1788
|
+
t: f.table ?? "*",
|
|
1789
|
+
c: f.column,
|
|
1790
|
+
o: f.op ?? "in",
|
|
1791
|
+
v: [...f.values ?? []].map((v) => String(v)).sort()
|
|
1792
|
+
})).sort((a, b) => `${a.t}.${a.c}.${a.o}`.localeCompare(`${b.t}.${b.c}.${b.o}`))
|
|
1793
|
+
}));
|
|
1794
|
+
norm.sort((a, b) => a.s.localeCompare(b.s));
|
|
1795
|
+
return JSON.stringify(norm);
|
|
1796
|
+
}
|
|
1797
|
+
function policyFingerprint(policies) {
|
|
1798
|
+
const memo = fingerprints.get(policies);
|
|
1799
|
+
if (memo !== void 0) return memo;
|
|
1800
|
+
const fp = import_crypto3.default.createHash("sha256").update(canonicalPolicies(policies)).digest("hex").slice(0, 16);
|
|
1801
|
+
fingerprints.set(policies, fp);
|
|
1802
|
+
return fp;
|
|
1803
|
+
}
|
|
1804
|
+
var warnedNoContext = false;
|
|
1684
1805
|
var QueryCache = class {
|
|
1685
1806
|
constructor() {
|
|
1686
1807
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -1712,6 +1833,31 @@ var QueryCache = class {
|
|
|
1712
1833
|
getTTL() {
|
|
1713
1834
|
return this.ttlMs / 60 / 1e3;
|
|
1714
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Which access bucket this call reads and writes.
|
|
1838
|
+
*
|
|
1839
|
+
* `''` means the unrestricted bucket and produces a key identical to the one
|
|
1840
|
+
* used before authorization existed. `null` means do not touch the cache.
|
|
1841
|
+
*/
|
|
1842
|
+
scope() {
|
|
1843
|
+
if (!enforcementApplies()) return "";
|
|
1844
|
+
const ctx = getRequestContext();
|
|
1845
|
+
if (!ctx) {
|
|
1846
|
+
if (!warnedNoContext) {
|
|
1847
|
+
warnedNoContext = true;
|
|
1848
|
+
logger.warn("[QueryCache] bypassed \u2014 no request context under SA_SQL_REWRITE_MODE=enforce");
|
|
1849
|
+
}
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
const policies = ctx.policies ?? [];
|
|
1853
|
+
return policies.length ? policyFingerprint(policies) : "";
|
|
1854
|
+
}
|
|
1855
|
+
/** The stored key: the scope namespace, then the caller's key. NUL cannot occur in SQL. */
|
|
1856
|
+
scopedKey(query) {
|
|
1857
|
+
const s = this.scope();
|
|
1858
|
+
if (s === null) return null;
|
|
1859
|
+
return s ? `${s}\0${query}` : query;
|
|
1860
|
+
}
|
|
1715
1861
|
/**
|
|
1716
1862
|
* Store query result in data cache.
|
|
1717
1863
|
* If the key already exists, it's removed first so the re-insert places it
|
|
@@ -1719,12 +1865,14 @@ var QueryCache = class {
|
|
|
1719
1865
|
* a genuinely new key past the size limit.
|
|
1720
1866
|
*/
|
|
1721
1867
|
set(query, data) {
|
|
1722
|
-
this.
|
|
1868
|
+
const key = this.scopedKey(query);
|
|
1869
|
+
if (key === null) return;
|
|
1870
|
+
this.cache.delete(key);
|
|
1723
1871
|
if (this.cache.size >= this.maxCacheSize) {
|
|
1724
1872
|
const oldestKey = this.cache.keys().next().value;
|
|
1725
1873
|
if (oldestKey) this.cache.delete(oldestKey);
|
|
1726
1874
|
}
|
|
1727
|
-
this.cache.set(
|
|
1875
|
+
this.cache.set(key, {
|
|
1728
1876
|
query,
|
|
1729
1877
|
data,
|
|
1730
1878
|
timestamp: Date.now()
|
|
@@ -1737,14 +1885,16 @@ var QueryCache = class {
|
|
|
1737
1885
|
* iteration order — turning FIFO eviction into true LRU.
|
|
1738
1886
|
*/
|
|
1739
1887
|
get(query) {
|
|
1740
|
-
const
|
|
1888
|
+
const key = this.scopedKey(query);
|
|
1889
|
+
if (key === null) return null;
|
|
1890
|
+
const entry = this.cache.get(key);
|
|
1741
1891
|
if (!entry) return null;
|
|
1742
1892
|
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
1743
|
-
this.cache.delete(
|
|
1893
|
+
this.cache.delete(key);
|
|
1744
1894
|
return null;
|
|
1745
1895
|
}
|
|
1746
|
-
this.cache.delete(
|
|
1747
|
-
this.cache.set(
|
|
1896
|
+
this.cache.delete(key);
|
|
1897
|
+
this.cache.set(key, entry);
|
|
1748
1898
|
logger.info(`[QueryCache] Cache HIT for query (${query.substring(0, 50)}...)`);
|
|
1749
1899
|
return entry.data;
|
|
1750
1900
|
}
|
|
@@ -1758,7 +1908,8 @@ var QueryCache = class {
|
|
|
1758
1908
|
* Remove a specific query from cache
|
|
1759
1909
|
*/
|
|
1760
1910
|
delete(query) {
|
|
1761
|
-
this.
|
|
1911
|
+
const key = this.scopedKey(query);
|
|
1912
|
+
if (key !== null) this.cache.delete(key);
|
|
1762
1913
|
}
|
|
1763
1914
|
/**
|
|
1764
1915
|
* Clear all cached entries
|
|
@@ -2261,6 +2412,96 @@ function buildDirectToolCacheKey(toolId, parameters) {
|
|
|
2261
2412
|
return `et-direct:${toolId}:${stableStringify(parameters || {})}`;
|
|
2262
2413
|
}
|
|
2263
2414
|
|
|
2415
|
+
// src/handlers/request-identity.ts
|
|
2416
|
+
function policiesFromConfig(config2) {
|
|
2417
|
+
const entries = Array.isArray(config2) ? config2 : [config2];
|
|
2418
|
+
const out = [];
|
|
2419
|
+
for (const entry of entries) {
|
|
2420
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2421
|
+
const e = entry;
|
|
2422
|
+
const rawFilters = Array.isArray(e.rowFilters) ? e.rowFilters : [];
|
|
2423
|
+
const rowFilters = [];
|
|
2424
|
+
for (const f of rawFilters) {
|
|
2425
|
+
if (!f || typeof f !== "object") continue;
|
|
2426
|
+
const rf = f;
|
|
2427
|
+
if (typeof rf.column !== "string" || !rf.column) continue;
|
|
2428
|
+
if (!Array.isArray(rf.values) || rf.values.length === 0) continue;
|
|
2429
|
+
const op = typeof rf.op === "string" ? rf.op.toLowerCase() : "in";
|
|
2430
|
+
if (op !== "in" && op !== "not_in" && op !== "eq" && op !== "ne") continue;
|
|
2431
|
+
rowFilters.push({
|
|
2432
|
+
table: typeof rf.table === "string" && rf.table ? rf.table : void 0,
|
|
2433
|
+
column: rf.column,
|
|
2434
|
+
op,
|
|
2435
|
+
values: rf.values,
|
|
2436
|
+
label: typeof rf.label === "string" && rf.label.trim() ? rf.label.trim() : void 0
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
const readableTables = Array.isArray(e.readableTables) ? e.readableTables.filter((t) => typeof t === "string" && !!t) : [];
|
|
2440
|
+
if (rowFilters.length === 0 && readableTables.length === 0) continue;
|
|
2441
|
+
const label = typeof e.label === "string" && e.label.trim() ? e.label.trim() : void 0;
|
|
2442
|
+
if (!label && rowFilters.some((f) => !f.label) && !warnedUnlabelled) {
|
|
2443
|
+
warnedUnlabelled = true;
|
|
2444
|
+
logger.warn(
|
|
2445
|
+
'[RequestIdentity] An access policy has no "label" \u2014 the agent will describe it to the user as a raw SQL predicate. Add a short noun phrase, e.g. "label": "Hyderabad branch".'
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
out.push({
|
|
2449
|
+
sourceId: typeof e.sourceId === "string" && e.sourceId ? e.sourceId : null,
|
|
2450
|
+
label,
|
|
2451
|
+
rowFilters,
|
|
2452
|
+
readableTables
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
return out;
|
|
2456
|
+
}
|
|
2457
|
+
var warnedUntrusted = false;
|
|
2458
|
+
var warnedAnonymous = false;
|
|
2459
|
+
var warnedUnlabelled = false;
|
|
2460
|
+
var announcedVerified = false;
|
|
2461
|
+
var reportedIdMismatches = /* @__PURE__ */ new Set();
|
|
2462
|
+
function runWithMessageIdentity(message, fn) {
|
|
2463
|
+
const verified = message?.authContext;
|
|
2464
|
+
const requestId = message?.id;
|
|
2465
|
+
const policies = policiesFromConfig(message?.payload?.config);
|
|
2466
|
+
if (verified?.userId) {
|
|
2467
|
+
const claimedId = message?.payload?.userId;
|
|
2468
|
+
if (!announcedVerified) {
|
|
2469
|
+
announcedVerified = true;
|
|
2470
|
+
logger.info(
|
|
2471
|
+
`[RequestIdentity] Using the relay-verified authContext as the principal (userId ${verified.userId})${claimedId ? `; client claimed ${claimedId === verified.userId ? "the same id" : claimedId}` : ""}.`
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
if (claimedId && claimedId !== verified.userId) {
|
|
2475
|
+
const pair = `${verified.userId}<-${claimedId}`;
|
|
2476
|
+
if (!reportedIdMismatches.has(pair) && reportedIdMismatches.size < 100) {
|
|
2477
|
+
reportedIdMismatches.add(pair);
|
|
2478
|
+
logger.warn(
|
|
2479
|
+
`[RequestIdentity] IDENTITY MISMATCH: the verified session says userId=${verified.userId} but the client sent payload.userId=${claimedId}. The verified id wins. Anything keyed on the principal (conversation history, threads, bookmarks) will look EMPTY for this user until the two agree.`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
return runAsUser(
|
|
2484
|
+
{ userId: verified.userId, orgId: verified.orgId, role: verified.role, requestId, policies },
|
|
2485
|
+
fn
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
const claimed = message?.payload?.userId;
|
|
2489
|
+
if (typeof claimed === "string" && claimed) {
|
|
2490
|
+
if (!warnedUntrusted) {
|
|
2491
|
+
warnedUntrusted = true;
|
|
2492
|
+
logger.warn(
|
|
2493
|
+
"[RequestIdentity] Using the client-supplied payload.userId as the principal \u2014 the relay is not stamping a verified authContext yet, so this identity is NOT trustworthy. Fine for plumbing; do not enable policy enforcement until the relay change lands."
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
return runAsUser({ userId: claimed, requestId, policies }, fn);
|
|
2497
|
+
}
|
|
2498
|
+
if (!warnedAnonymous) {
|
|
2499
|
+
warnedAnonymous = true;
|
|
2500
|
+
logger.info("[RequestIdentity] No user identity on this message \u2014 running without a request context.");
|
|
2501
|
+
}
|
|
2502
|
+
return fn();
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2264
2505
|
// src/handlers/data-request.ts
|
|
2265
2506
|
function getQueryCacheKey(query) {
|
|
2266
2507
|
if (typeof query === "string") {
|
|
@@ -2300,6 +2541,9 @@ function getCacheKey(collection, op, params, tools) {
|
|
|
2300
2541
|
return "";
|
|
2301
2542
|
}
|
|
2302
2543
|
async function handleDataRequest(data, collections, sendMessage, tools) {
|
|
2544
|
+
return runWithMessageIdentity(data, () => handleDataRequestInner(data, collections, sendMessage, tools));
|
|
2545
|
+
}
|
|
2546
|
+
async function handleDataRequestInner(data, collections, sendMessage, tools) {
|
|
2303
2547
|
let requestId;
|
|
2304
2548
|
let collection;
|
|
2305
2549
|
let op;
|
|
@@ -7157,7 +7401,6 @@ Rules: state ONLY filters you actually applied (do not claim a filter you didn't
|
|
|
7157
7401
|
### Knowledge Base Context
|
|
7158
7402
|
{{KNOWLEDGE_BASE_CONTEXT}}
|
|
7159
7403
|
|
|
7160
|
-
### User Access Scope
|
|
7161
7404
|
{{USER_ACCESS_CONTEXT}}
|
|
7162
7405
|
|
|
7163
7406
|
### Conversation History
|
|
@@ -7187,7 +7430,6 @@ Your job is to fetch the requested data using the available tool.
|
|
|
7187
7430
|
|
|
7188
7431
|
### Critical Rules
|
|
7189
7432
|
- **CRITICAL: Always apply the filtering rules from the Global Knowledge Base and Knowledge Base Context sections below.** These define how business entities should be filtered in queries. If a knowledge base entry specifies column conditions for an entity, ALWAYS include those conditions as WHERE clauses.
|
|
7190
|
-
- **CRITICAL: Always apply the restrictions in the User Access Scope section below \u2014 unlike other defaults in this prompt, this is NOT overridden by an explicit user request.** If it names a value for a dimension (e.g. a location/branch id) and this table has a column representing that dimension, ALWAYS filter on it, inferring the matching column from the schema. If the user explicitly asks for a different value on that dimension, do NOT fetch it \u2014 say they don't have access to it.
|
|
7191
7433
|
- **CRITICAL: Follow ALL the database-specific SQL rules above.** These prevent syntax errors specific to this database engine. Violating them will cause query failures.
|
|
7192
7434
|
- **CRITICAL ROW LIMIT: Every query MUST limit results to {{MAX_ROWS}} rows maximum.** {{ROW_LIMIT_SYNTAX}}. Queries without a row limit will be rejected. For UNION queries, apply the limit to the outermost query or to each branch.
|
|
7193
7435
|
|
|
@@ -7231,7 +7473,6 @@ Your job is to fetch the requested data using the available tool.
|
|
|
7231
7473
|
### Knowledge Base Context
|
|
7232
7474
|
{{KNOWLEDGE_BASE_CONTEXT}}
|
|
7233
7475
|
|
|
7234
|
-
### User Access Scope
|
|
7235
7476
|
{{USER_ACCESS_CONTEXT}}`,
|
|
7236
7477
|
user: `Current Date and Time: {{CURRENT_DATETIME}}
|
|
7237
7478
|
|
|
@@ -9212,9 +9453,9 @@ var PromptLoader = class {
|
|
|
9212
9453
|
const rulesPath = import_path3.default.join(this.promptsDir, "database-rules", `${this.databaseType}.md`);
|
|
9213
9454
|
try {
|
|
9214
9455
|
if (import_fs4.default.existsSync(rulesPath)) {
|
|
9215
|
-
const
|
|
9216
|
-
this.databaseRulesCache.set(this.databaseType,
|
|
9217
|
-
return
|
|
9456
|
+
const rules2 = import_fs4.default.readFileSync(rulesPath, "utf-8");
|
|
9457
|
+
this.databaseRulesCache.set(this.databaseType, rules2);
|
|
9458
|
+
return rules2;
|
|
9218
9459
|
}
|
|
9219
9460
|
} catch (error) {
|
|
9220
9461
|
logger.warn(`Could not load database rules for '${this.databaseType}' from file system: ${error}`);
|
|
@@ -9255,9 +9496,9 @@ var PromptLoader = class {
|
|
|
9255
9496
|
const rulesPath = import_path3.default.join(this.promptsDir, "database-rules", `${dbType}.md`);
|
|
9256
9497
|
try {
|
|
9257
9498
|
if (import_fs4.default.existsSync(rulesPath)) {
|
|
9258
|
-
const
|
|
9259
|
-
this.databaseRulesCache.set(dbType,
|
|
9260
|
-
return
|
|
9499
|
+
const rules2 = import_fs4.default.readFileSync(rulesPath, "utf-8");
|
|
9500
|
+
this.databaseRulesCache.set(dbType, rules2);
|
|
9501
|
+
return rules2;
|
|
9261
9502
|
}
|
|
9262
9503
|
} catch (error) {
|
|
9263
9504
|
logger.warn(`Could not load database rules for '${dbType}' from file system: ${error}`);
|
|
@@ -9955,6 +10196,57 @@ function validateAndFixSqlQuery(query, dbType) {
|
|
|
9955
10196
|
};
|
|
9956
10197
|
}
|
|
9957
10198
|
|
|
10199
|
+
// src/userResponse/agents/access-section.ts
|
|
10200
|
+
var HEADING = "### User Access Scope";
|
|
10201
|
+
var enforced = enforcementApplies;
|
|
10202
|
+
var EMPTY_RESULT_RULE = `**An EMPTY result is not evidence of absence.** Zero rows under a restriction usually means the records are outside the user's access, not that they do not exist. Never report it as "no data found", "there are none", or "no records exist" \u2014 say the data is outside their access.`;
|
|
10203
|
+
function mainRule() {
|
|
10204
|
+
return enforced() ? "**These restrictions are applied automatically to every query before it runs.** Do NOT write them into your SQL, into an INTENT you dispatch to a source, or into a saved script \u2014 a scope you add is redundant, and it goes stale the moment this user's access changes. Dispatch the question as the user asked it; the scope is enforced for you.\n**If the user names a value outside this scope** (by name or via a resolved id), do not query it \u2014 say they lack access. For a question mixing in-scope and out-of-scope items (a comparison, say), answer the part you can and state plainly which part you cannot.\n" + EMPTY_RESULT_RULE : "**CRITICAL: Apply this on every question and every dispatch \u2014 it overrides an explicit user request AND a resolved entity id.** If the user names a value outside this scope (by name or via a resolved id), do not query it \u2014 say they lack access. For a question mixing in-scope and out-of-scope items (a comparison, say), answer the part you can and state plainly which part you cannot.\n" + EMPTY_RESULT_RULE;
|
|
10205
|
+
}
|
|
10206
|
+
function sourceRule() {
|
|
10207
|
+
return (enforced() ? "**These restrictions are already applied to every query before it runs.** Do NOT add them to your SQL \u2014 a filter you write is redundant at best, and stale the moment this user's access changes. Write the query as if unrestricted; the scope is enforced for you.\n" : "**CRITICAL: Apply these restrictions on every query \u2014 they override both an explicit user request AND a RESOLVED ENTITIES id.** If a restriction names a value for a dimension (e.g. a location/branch id) and the table you are querying has a column representing that dimension, ALWAYS filter on it, inferring the matching column from the schema. Never use the user's or a resolved id's value where it conflicts. On conflict, refuse and say the user lacks access \u2014 do not fetch.\n") + EMPTY_RESULT_RULE;
|
|
10208
|
+
}
|
|
10209
|
+
function editRule() {
|
|
10210
|
+
return enforced() ? "**CRITICAL: this overrides the edit instruction.** The scope below is applied automatically on every run. If the script hardcodes any of it \u2014 as a SQL predicate or in its text \u2014 REMOVE it: a baked-in scope returns zero rows the moment this user's access changes. Never add one." : '**CRITICAL: this overrides the edit instruction.** A filter that conflicts with the scope below is not "correct as-is" \u2014 fix it regardless of what the edit instruction named.';
|
|
10211
|
+
}
|
|
10212
|
+
var rules = () => ({
|
|
10213
|
+
main: mainRule(),
|
|
10214
|
+
source: sourceRule(),
|
|
10215
|
+
edit: editRule()
|
|
10216
|
+
});
|
|
10217
|
+
function buildAccessSection(renderedConfig, variant) {
|
|
10218
|
+
const body = (renderedConfig || "").trim();
|
|
10219
|
+
if (!body) return "";
|
|
10220
|
+
return `${HEADING}
|
|
10221
|
+
${rules()[variant]}
|
|
10222
|
+
${body}`;
|
|
10223
|
+
}
|
|
10224
|
+
function buildAccessBodyContract(renderedConfig) {
|
|
10225
|
+
if (!(renderedConfig || "").trim()) return "";
|
|
10226
|
+
return `
|
|
10227
|
+
|
|
10228
|
+
### Access policy \u2014 never write it into the script
|
|
10229
|
+
|
|
10230
|
+
A query result may report that an authorization policy restricted it. That restriction is enforced
|
|
10231
|
+
automatically on every run, including replays. So:
|
|
10232
|
+
|
|
10233
|
+
- **Never put a policy predicate in your SQL** (e.g. a branch/tenant/region id you were told was
|
|
10234
|
+
applied). It is redundant, and it goes stale the moment that user's access changes \u2014 the recipe
|
|
10235
|
+
would keep filtering on the old scope while the live policy applies the new one, giving zero rows.
|
|
10236
|
+
- **Never hardcode the scope in text.** \`getAnalysis\` and \`getComponents\` receive the restrictions
|
|
10237
|
+
that actually applied to THIS run as their 6th argument, \`scope: string[]\`. Use it:
|
|
10238
|
+
|
|
10239
|
+
\`\`\`js
|
|
10240
|
+
export function getAnalysis(data, params, fmt, datasets, entities, scope) {
|
|
10241
|
+
const limitedTo = scope.length ? \` (\${scope.join('; ')})\` : '';
|
|
10242
|
+
return { analysis: \`**\${fmt.num(data[0].Total)} records**\${limitedTo}.\` };
|
|
10243
|
+
}
|
|
10244
|
+
\`\`\`
|
|
10245
|
+
|
|
10246
|
+
\`scope\` is empty when no policy applied, so the same body is correct for restricted and
|
|
10247
|
+
unrestricted users alike.`;
|
|
10248
|
+
}
|
|
10249
|
+
|
|
9958
10250
|
// src/userResponse/agents/source-agent.ts
|
|
9959
10251
|
var SourceAgent = class {
|
|
9960
10252
|
constructor(tool, config2, streamBuffer, options) {
|
|
@@ -10141,6 +10433,21 @@ Analyze the error and try again with a corrected query.`;
|
|
|
10141
10433
|
name: this.tool.name,
|
|
10142
10434
|
params: cappedInput,
|
|
10143
10435
|
result: {
|
|
10436
|
+
// The query was restricted before it ran. The agent cannot see
|
|
10437
|
+
// this from the SQL it wrote — it wrote no such restriction — so
|
|
10438
|
+
// without being told it labels the answer as if unrestricted.
|
|
10439
|
+
// Confirmed live: a branch-scoped count reported as covering
|
|
10440
|
+
// every branch, plus a wasted turn trying to remove a
|
|
10441
|
+
// restriction that is re-applied to every query.
|
|
10442
|
+
//
|
|
10443
|
+
// The WORDING is a fixed, generic frame; the restrictions
|
|
10444
|
+
// themselves are whatever the deployment's policy produced.
|
|
10445
|
+
// Every client shares this code, so nothing here may assume a
|
|
10446
|
+
// policy TYPE (row filter, masked column, restricted table) or
|
|
10447
|
+
// any client's vocabulary.
|
|
10448
|
+
...result.metadata?.appliedPolicies?.length ? {
|
|
10449
|
+
_ACCESS_POLICY_APPLIED: `An authorization policy restricted this query before it ran: ${result.metadata.appliedPolicies.join("; ")}. These results are ONLY what this user is permitted to see. State the restriction in titles, analysis and method lines; never present the figures as complete, overall, or covering everything. The policy is re-applied to every query, so rewriting without the restriction returns the same result \u2014 do not retry to widen it.`
|
|
10450
|
+
} : {},
|
|
10144
10451
|
...hitFetchCap ? {
|
|
10145
10452
|
_WARNING_TRUNCATED: `This result FILLED the ${this.config.maxRowsFetched}-row fetch cap \u2014 it is a PARTIAL head slice, NOT the full result. There are almost certainly more rows, and (if ordered) often all from just the first few groups (e.g. the first districts alphabetically). Treat the set of districts/categories/entities and any count/total shown here as INCOMPLETE \u2014 never as the full set.`
|
|
10146
10453
|
} : {},
|
|
@@ -10166,7 +10473,9 @@ Analyze the error and try again with a corrected query.`;
|
|
|
10166
10473
|
const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
|
|
10167
10474
|
const followUpNote = successfulQueries < 2 ? "STOP now and return this data \u2014 it satisfies the intent in the common case. Make a second query ONLY if a column or row the intent explicitly requires is verifiably MISSING from these results (not merely to widen, re-sort, or double-check). A redundant follow-up wastes a full round-trip." : "Maximum queries reached. Use the data you have.";
|
|
10168
10475
|
const capWarning = hitFetchCap ? ` \u26A0\uFE0F This filled the ${this.config.maxRowsFetched}-row fetch cap \u2014 a partial head slice; the groups/values shown are NOT the complete set.` : "";
|
|
10169
|
-
|
|
10476
|
+
const emptyUnderPolicy = resultData.length === 0;
|
|
10477
|
+
const policyNote = result.metadata?.appliedPolicies?.length ? ` \u{1F512} An authorization policy restricted this query before it ran: ${result.metadata.appliedPolicies.join("; ")}. These rows are ONLY what this user may see. ` + (emptyUnderPolicy ? `It returned ZERO rows UNDER THAT RESTRICTION \u2014 that is NOT evidence the records do not exist; they are most likely outside this user's access. Say that; never report it as "no data found" or "there are none". ` : "") + `Work that restriction into your wording naturally (e.g. "\u2026 for <restriction>"); never paste it in as a raw filter expression, and never call the figures complete or overall. The policy re-applies to every query, so re-running without it returns the same rows. Do NOT copy the restriction into your SQL or into a saved script: it is enforced automatically, and a policy written into a script goes stale the moment this user's access changes.` : "";
|
|
10478
|
+
return `\u2705 Query executed successfully. ${resultData.length} rows returned.${capWarning}${policyNote} ${followUpNote}
|
|
10170
10479
|
|
|
10171
10480
|
${formatted}`;
|
|
10172
10481
|
} catch (execError) {
|
|
@@ -10385,7 +10694,7 @@ Even if a table appears in the detailed schema above, search_schema returns samp
|
|
|
10385
10694
|
logger.warn(`[SourceAgent:${this.tool.name}] Trace capture failed for schemaSearchInstructionsText (non-blocking): ${traceErr}`);
|
|
10386
10695
|
}
|
|
10387
10696
|
const displaySourceType = sourceType === "excel" || sourceType === "csv" ? `${sourceType} \u2014 uses DuckDB SQL engine` : sourceType;
|
|
10388
|
-
const userAccessContextText = this.config.userAccessContext
|
|
10697
|
+
const userAccessContextText = buildAccessSection(this.config.userAccessContext ?? "", "source");
|
|
10389
10698
|
try {
|
|
10390
10699
|
if (this.traceSchemaEntry) this.traceSchemaEntry.userAccessContextText = userAccessContextText;
|
|
10391
10700
|
} catch (traceErr) {
|
|
@@ -10574,6 +10883,55 @@ var LineSplitter = class {
|
|
|
10574
10883
|
}
|
|
10575
10884
|
};
|
|
10576
10885
|
|
|
10886
|
+
// src/utils/context-token.ts
|
|
10887
|
+
var import_crypto5 = require("crypto");
|
|
10888
|
+
var DEFAULT_TTL_MS = 30 * 6e4;
|
|
10889
|
+
var KEY = Symbol.for("superatom.contextTokens.v1");
|
|
10890
|
+
function registry() {
|
|
10891
|
+
const g = globalThis;
|
|
10892
|
+
let m = g[KEY];
|
|
10893
|
+
if (!m) {
|
|
10894
|
+
m = /* @__PURE__ */ new Map();
|
|
10895
|
+
g[KEY] = m;
|
|
10896
|
+
}
|
|
10897
|
+
return m;
|
|
10898
|
+
}
|
|
10899
|
+
function sweep(now = Date.now()) {
|
|
10900
|
+
const reg = registry();
|
|
10901
|
+
for (const [token, entry] of reg) {
|
|
10902
|
+
if (entry.expiresAt <= now) reg.delete(token);
|
|
10903
|
+
}
|
|
10904
|
+
}
|
|
10905
|
+
function mintContextToken(ttlMs = DEFAULT_TTL_MS) {
|
|
10906
|
+
const ctx = captureContext();
|
|
10907
|
+
if (!ctx) return void 0;
|
|
10908
|
+
sweep();
|
|
10909
|
+
const token = (0, import_crypto5.randomBytes)(24).toString("base64url");
|
|
10910
|
+
registry().set(token, { ctx, expiresAt: Date.now() + ttlMs });
|
|
10911
|
+
return token;
|
|
10912
|
+
}
|
|
10913
|
+
function resolveContextToken(token) {
|
|
10914
|
+
if (!token) return void 0;
|
|
10915
|
+
const entry = registry().get(token);
|
|
10916
|
+
if (!entry) return void 0;
|
|
10917
|
+
if (entry.expiresAt <= Date.now()) {
|
|
10918
|
+
registry().delete(token);
|
|
10919
|
+
logger.warn("[ContextToken] Rejected an expired token \u2014 the script outlived its context grant.");
|
|
10920
|
+
return void 0;
|
|
10921
|
+
}
|
|
10922
|
+
return entry.ctx;
|
|
10923
|
+
}
|
|
10924
|
+
function revokeContextToken(token) {
|
|
10925
|
+
if (token) registry().delete(token);
|
|
10926
|
+
}
|
|
10927
|
+
function contextTokenCount() {
|
|
10928
|
+
sweep();
|
|
10929
|
+
return registry().size;
|
|
10930
|
+
}
|
|
10931
|
+
function clearContextTokens() {
|
|
10932
|
+
registry().clear();
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10577
10935
|
// src/userResponse/scripts/script-runner.ts
|
|
10578
10936
|
var fmtConfig;
|
|
10579
10937
|
function setFmtConfig(cfg) {
|
|
@@ -10699,7 +11057,8 @@ FIX: pass every required parameter to execute_script, or give it a default in wr
|
|
|
10699
11057
|
// analysisData: getData produces it
|
|
10700
11058
|
void 0,
|
|
10701
11059
|
// analysisDatasets: ctx.emit produces them
|
|
10702
|
-
options.entities
|
|
11060
|
+
options.entities,
|
|
11061
|
+
options.appliedPolicies
|
|
10703
11062
|
);
|
|
10704
11063
|
const totalMs = Date.now() - startedAt;
|
|
10705
11064
|
if (result.kind === "ok") {
|
|
@@ -10747,10 +11106,11 @@ FIX: pass every required parameter to execute_script, or give it a default in wr
|
|
|
10747
11106
|
scriptSemaphore.release();
|
|
10748
11107
|
}
|
|
10749
11108
|
}
|
|
10750
|
-
function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs, signal, mode, analysisData, analysisDatasets, entities) {
|
|
11109
|
+
function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs, signal, mode, analysisData, analysisDatasets, entities, appliedPolicies) {
|
|
10751
11110
|
return new Promise((resolve2) => {
|
|
10752
11111
|
const tsxBin = resolveTsxBinary();
|
|
10753
11112
|
const bootstrap = resolveBootstrapPath();
|
|
11113
|
+
const contextToken = mintContextToken();
|
|
10754
11114
|
const child = (0, import_child_process.spawn)(
|
|
10755
11115
|
process.execPath,
|
|
10756
11116
|
// node — tsx runs via its own cli, we invoke it with node
|
|
@@ -10769,7 +11129,12 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
10769
11129
|
env: {
|
|
10770
11130
|
...process.env,
|
|
10771
11131
|
// Keep the child quiet about dotenv etc.
|
|
10772
|
-
NODE_NO_WARNINGS: "1"
|
|
11132
|
+
NODE_NO_WARNINGS: "1",
|
|
11133
|
+
// Identity for the bridge calls the script will make. The async
|
|
11134
|
+
// context cannot cross a process boundary, so it travels as a
|
|
11135
|
+
// token instead. Absent when the run has no context — the bridge
|
|
11136
|
+
// then treats the queries as anonymous, which is the truth.
|
|
11137
|
+
...contextToken ? { SA_CONTEXT_TOKEN: contextToken } : {}
|
|
10773
11138
|
}
|
|
10774
11139
|
}
|
|
10775
11140
|
);
|
|
@@ -10777,6 +11142,7 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
10777
11142
|
let executedQueries = [];
|
|
10778
11143
|
let stderrBuffer = "";
|
|
10779
11144
|
const cleanup = () => {
|
|
11145
|
+
revokeContextToken(contextToken);
|
|
10780
11146
|
if (child.killed) return;
|
|
10781
11147
|
const pid = child.pid;
|
|
10782
11148
|
if (pid && process.platform !== "win32") {
|
|
@@ -10881,7 +11247,8 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
|
|
|
10881
11247
|
...analysisData ? { data: analysisData } : {},
|
|
10882
11248
|
...analysisDatasets && Object.keys(analysisDatasets).length ? { datasets: analysisDatasets } : {},
|
|
10883
11249
|
...fmtConfig ? { fmt: fmtConfig } : {},
|
|
10884
|
-
...entities && entities.length ? { entities } : {}
|
|
11250
|
+
...entities && entities.length ? { entities } : {},
|
|
11251
|
+
...appliedPolicies && appliedPolicies.length ? { appliedPolicies } : {}
|
|
10885
11252
|
});
|
|
10886
11253
|
child.stdin.write(init);
|
|
10887
11254
|
function handleChildMessage(msg) {
|
|
@@ -10966,7 +11333,12 @@ function enrichQueries(queries, externalTools) {
|
|
|
10966
11333
|
count: q.count,
|
|
10967
11334
|
totalCount: q.totalCount,
|
|
10968
11335
|
executionTimeMs: q.executionTimeMs,
|
|
10969
|
-
virtual: q.virtual
|
|
11336
|
+
virtual: q.virtual,
|
|
11337
|
+
// Carried through explicitly: this rebuilds each query field by field,
|
|
11338
|
+
// so anything not listed is silently dropped. Losing it here left
|
|
11339
|
+
// getAnalysis's `scope` empty while the query WAS restricted — the
|
|
11340
|
+
// analysis then described a branch-scoped count as system-wide.
|
|
11341
|
+
appliedPolicies: q.appliedPolicies
|
|
10970
11342
|
};
|
|
10971
11343
|
});
|
|
10972
11344
|
}
|
|
@@ -11058,7 +11430,8 @@ async function runComponentsOnly(recipe, scriptPath, params, data, options, data
|
|
|
11058
11430
|
"components",
|
|
11059
11431
|
data,
|
|
11060
11432
|
datasets,
|
|
11061
|
-
options.entities
|
|
11433
|
+
options.entities,
|
|
11434
|
+
options.appliedPolicies
|
|
11062
11435
|
);
|
|
11063
11436
|
if (result.kind === "ok") {
|
|
11064
11437
|
return { components: result.components, componentsError: result.componentsError };
|
|
@@ -11091,7 +11464,8 @@ async function runAnalysisOnly(recipe, scriptPath, params, data, options, datase
|
|
|
11091
11464
|
"analysis",
|
|
11092
11465
|
data,
|
|
11093
11466
|
datasets,
|
|
11094
|
-
options.entities
|
|
11467
|
+
options.entities,
|
|
11468
|
+
options.appliedPolicies
|
|
11095
11469
|
);
|
|
11096
11470
|
if (result.kind === "ok") {
|
|
11097
11471
|
return { analysis: result.analysis, analysisError: result.analysisError };
|
|
@@ -11288,7 +11662,7 @@ function resolveScriptRecipeStore(collections) {
|
|
|
11288
11662
|
// src/userResponse/scripts/script-store.ts
|
|
11289
11663
|
var fs6 = __toESM(require("fs"));
|
|
11290
11664
|
var path6 = __toESM(require("path"));
|
|
11291
|
-
var
|
|
11665
|
+
var import_crypto6 = require("crypto");
|
|
11292
11666
|
var DEFAULT_STORE_DIR = "scripts-store";
|
|
11293
11667
|
function stripAnalysisExport(body) {
|
|
11294
11668
|
return stripNamedExport(body, "getAnalysis");
|
|
@@ -11315,7 +11689,7 @@ function stripNamedExport(body, fnName) {
|
|
|
11315
11689
|
function dataFingerprint(scriptBody, parameters) {
|
|
11316
11690
|
const data = extractNamedExport(scriptBody, "getData") ?? scriptBody;
|
|
11317
11691
|
const params = (parameters ?? []).map((p) => `${p.name}:${p.type}:${JSON.stringify(p.default ?? null)}`).sort().join("|");
|
|
11318
|
-
return (0,
|
|
11692
|
+
return (0, import_crypto6.createHash)("sha256").update(`${data.trim()}\0${params}`).digest("hex");
|
|
11319
11693
|
}
|
|
11320
11694
|
function extractNamedExport(body, fnName) {
|
|
11321
11695
|
const start = body.search(new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`));
|
|
@@ -11500,7 +11874,7 @@ var ScriptStore = class {
|
|
|
11500
11874
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11501
11875
|
const existing = input.recipeId ? await this.store.getById(input.recipeId) : null;
|
|
11502
11876
|
const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
|
|
11503
|
-
const id = reuse ? existing.id : `script_${Date.now()}_${(0,
|
|
11877
|
+
const id = reuse ? existing.id : `script_${Date.now()}_${(0, import_crypto6.randomBytes)(3).toString("hex")}`;
|
|
11504
11878
|
const fileBase = reuse ? existing.fileBase : input.parentId ? `${this.toSlug(input.name)}-${id.slice(-6)}` : await this.computeFileBase(input.name, id);
|
|
11505
11879
|
let normalized = normalizeScriptBody(input.scriptBody);
|
|
11506
11880
|
if (reuse) {
|
|
@@ -11863,7 +12237,7 @@ ${carried.trim()}
|
|
|
11863
12237
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed-script";
|
|
11864
12238
|
}
|
|
11865
12239
|
hash(body) {
|
|
11866
|
-
return (0,
|
|
12240
|
+
return (0, import_crypto6.createHash)("sha256").update(body).digest("hex");
|
|
11867
12241
|
}
|
|
11868
12242
|
/**
|
|
11869
12243
|
* Attach getComponents to the recipe body. The stored `components` column is
|
|
@@ -11871,6 +12245,32 @@ ${carried.trim()}
|
|
|
11871
12245
|
* specs, a stale spec array beside a live program is how the wrong one gets
|
|
11872
12246
|
* read later. See backend/docs/COMPONENTS-AS-PROGRAM-DESIGN.md § 4.4.
|
|
11873
12247
|
*/
|
|
12248
|
+
/**
|
|
12249
|
+
* Body text as it stands now, for reverting an attach that turns out to be
|
|
12250
|
+
* invalid.
|
|
12251
|
+
*
|
|
12252
|
+
* getComponents can only be VALIDATED by running it, and it can only be run
|
|
12253
|
+
* once it is on disk — so attachComponents necessarily writes before anyone
|
|
12254
|
+
* knows whether the body is good. Without a revert, a rejected body stays
|
|
12255
|
+
* attached: promote() then sees the recipe "exports getComponents", clears
|
|
12256
|
+
* the specs the fallback generator produced, and the recipe is verified with
|
|
12257
|
+
* component code that was already rejected. Confirmed live on
|
|
12258
|
+
* "Total Bookings by Branch", which shipped a sourceRef of 'computed:_final'
|
|
12259
|
+
* that validateSpecs had refused.
|
|
12260
|
+
*/
|
|
12261
|
+
async snapshotBody(recipeId) {
|
|
12262
|
+
const recipe = await this.get(recipeId);
|
|
12263
|
+
if (!recipe?.fileBase) return null;
|
|
12264
|
+
return this.readBody(recipe.fileBase) || null;
|
|
12265
|
+
}
|
|
12266
|
+
/** Put back a body captured by {@link snapshotBody}. */
|
|
12267
|
+
async restoreBody(recipeId, body) {
|
|
12268
|
+
const recipe = await this.get(recipeId);
|
|
12269
|
+
if (!recipe?.fileBase) return false;
|
|
12270
|
+
this.writeBody(recipe.fileBase, body);
|
|
12271
|
+
logger.info(`[ScriptStore] Reverted body of "${recipe.name}" (${recipeId}) after a rejected attach`);
|
|
12272
|
+
return true;
|
|
12273
|
+
}
|
|
11874
12274
|
async attachComponents(recipeId, componentsBody) {
|
|
11875
12275
|
const recipe = await this.get(recipeId);
|
|
11876
12276
|
if (!recipe) {
|
|
@@ -11949,7 +12349,7 @@ ${analysisBody.trim()}
|
|
|
11949
12349
|
writeArchive(fileBase, version, body) {
|
|
11950
12350
|
if (!fs6.existsSync(this.archiveDir)) fs6.mkdirSync(this.archiveDir, { recursive: true });
|
|
11951
12351
|
const finalPath = path6.join(this.archiveDir, `${fileBase}.v${version}.ts`);
|
|
11952
|
-
const tmpPath = `${finalPath}.tmp-${(0,
|
|
12352
|
+
const tmpPath = `${finalPath}.tmp-${(0, import_crypto6.randomBytes)(4).toString("hex")}`;
|
|
11953
12353
|
fs6.writeFileSync(tmpPath, body);
|
|
11954
12354
|
fs6.renameSync(tmpPath, finalPath);
|
|
11955
12355
|
}
|
|
@@ -11976,7 +12376,7 @@ ${analysisBody.trim()}
|
|
|
11976
12376
|
writeBody(fileBase, normalizedBody) {
|
|
11977
12377
|
if (!fs6.existsSync(this.storeDir)) fs6.mkdirSync(this.storeDir, { recursive: true });
|
|
11978
12378
|
const finalPath = this.bodyPath(fileBase);
|
|
11979
|
-
const tmpPath = `${finalPath}.tmp-${(0,
|
|
12379
|
+
const tmpPath = `${finalPath}.tmp-${(0, import_crypto6.randomBytes)(4).toString("hex")}`;
|
|
11980
12380
|
fs6.writeFileSync(tmpPath, normalizedBody);
|
|
11981
12381
|
fs6.renameSync(tmpPath, finalPath);
|
|
11982
12382
|
}
|
|
@@ -12801,7 +13201,7 @@ ${catalog}`,
|
|
|
12801
13201
|
return "write_components requires data: run write_script then execute_script successfully first.";
|
|
12802
13202
|
}
|
|
12803
13203
|
if (this.renderComponentAttempts >= MAX_RENDER_COMPONENT_ATTEMPTS) {
|
|
12804
|
-
return `Reached the max of ${MAX_RENDER_COMPONENT_ATTEMPTS} write_components attempts. Stop calling it
|
|
13204
|
+
return `Reached the max of ${MAX_RENDER_COMPONENT_ATTEMPTS} write_components attempts. Stop calling it \u2014 the system will build the dashboard. This limit is write_components only; if you have not called write_analysis, call it now.`;
|
|
12805
13205
|
}
|
|
12806
13206
|
this.renderComponentAttempts++;
|
|
12807
13207
|
const hardcodedAggregates = findHardcodedAggregates(body);
|
|
@@ -12829,6 +13229,10 @@ STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reaso
|
|
|
12829
13229
|
const recipeId = this.scriptState.recipeId;
|
|
12830
13230
|
const draft = recipeId ? await this.scriptStore.get(recipeId) : null;
|
|
12831
13231
|
if (!recipeId || !draft) return "Draft was lost from the store. Call write_script again.";
|
|
13232
|
+
const snapshot = await this.scriptStore.snapshotBody(recipeId);
|
|
13233
|
+
const revert = async () => {
|
|
13234
|
+
if (snapshot) await this.scriptStore.restoreBody(recipeId, snapshot);
|
|
13235
|
+
};
|
|
12832
13236
|
const attached = await this.scriptStore.attachComponents(recipeId, body);
|
|
12833
13237
|
if (!attached) return "Could not attach getComponents to the draft. Call render-free: write your answer and the system will build the dashboard.";
|
|
12834
13238
|
const datasets = {};
|
|
@@ -12842,12 +13246,13 @@ STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reaso
|
|
|
12842
13246
|
this.scriptStore.getScriptPath(draft),
|
|
12843
13247
|
{},
|
|
12844
13248
|
result.data,
|
|
12845
|
-
{ externalTools: this.externalTools, signal: this.abortSignal, entities: this.resolvedEntities },
|
|
13249
|
+
{ externalTools: this.externalTools, signal: this.abortSignal, entities: this.resolvedEntities, appliedPolicies: this.observedPolicies() },
|
|
12846
13250
|
datasets
|
|
12847
13251
|
);
|
|
12848
13252
|
if (componentsError || !components?.length) {
|
|
12849
13253
|
const why = componentsError || "it returned no components";
|
|
12850
13254
|
logger.warn(`[MainAgent] write_components failed: ${why}`);
|
|
13255
|
+
await revert();
|
|
12851
13256
|
return `getComponents failed: ${why}
|
|
12852
13257
|
|
|
12853
13258
|
Fix the body and call write_components again. It receives (data, params, fmt, datasets) and must RETURN an array of component specs.`;
|
|
@@ -12856,6 +13261,14 @@ Fix the body and call write_components again. It receives (data, params, fmt, da
|
|
|
12856
13261
|
if (findings.length > 0) {
|
|
12857
13262
|
logger.warn(`[MainAgent] write_components rejected (attempt ${this.renderComponentAttempts}): ${findings.length} finding(s)`);
|
|
12858
13263
|
findings.forEach((f) => logger.warn(`[MainAgent] ${f.code} ${f.componentType}: ${f.message}`));
|
|
13264
|
+
await revert();
|
|
13265
|
+
if (this.renderComponentAttempts >= MAX_RENDER_COMPONENT_ATTEMPTS && accepted.length > 0) {
|
|
13266
|
+
this.componentSpecs = accepted;
|
|
13267
|
+
this.componentLayout = { title: input?.layoutTitle || "Dashboard", description: input?.layoutDescription };
|
|
13268
|
+
const dropped = findings.map((f) => `${f.componentType} (${f.code})`).join(", ");
|
|
13269
|
+
logger.warn(`[MainAgent] write_components out of attempts \u2014 keeping ${accepted.length} valid spec(s), dropping ${findings.length}: ${dropped}`);
|
|
13270
|
+
return `Out of write_components attempts. Kept ${accepted.length} valid component(s); dropped ${findings.length} (${dropped}). Stop calling write_components and write your answer.`;
|
|
13271
|
+
}
|
|
12859
13272
|
return formatFindingsForAgent(findings);
|
|
12860
13273
|
}
|
|
12861
13274
|
this.componentSpecs = accepted;
|
|
@@ -12871,6 +13284,20 @@ Now write your answer for the user. Do not restate the component list.`;
|
|
|
12871
13284
|
// ============================================
|
|
12872
13285
|
// Script-authoring tool handlers
|
|
12873
13286
|
// ============================================
|
|
13287
|
+
/**
|
|
13288
|
+
* Restrictions the seam reported while this run's queries executed.
|
|
13289
|
+
*
|
|
13290
|
+
* getData does NOT re-run for write_components/write_analysis, so the child
|
|
13291
|
+
* cannot observe these itself — the parent hands them across, exactly as it
|
|
13292
|
+
* does for `entities`.
|
|
13293
|
+
*/
|
|
13294
|
+
observedPolicies() {
|
|
13295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13296
|
+
for (const q of this.scriptState.lastSuccessfulResult?.executedQueries ?? []) {
|
|
13297
|
+
for (const p of q?.appliedPolicies ?? []) seen.add(p);
|
|
13298
|
+
}
|
|
13299
|
+
return [...seen];
|
|
13300
|
+
}
|
|
12874
13301
|
/**
|
|
12875
13302
|
* Author the answer as a program. Runs after execute_script, so the body is
|
|
12876
13303
|
* written with the result already known and can react to what it found.
|
|
@@ -12904,14 +13331,16 @@ Now write your answer for the user. Do not restate the component list.`;
|
|
|
12904
13331
|
const lintError = formatLintError(violations);
|
|
12905
13332
|
if (lintError) {
|
|
12906
13333
|
const named = violations.map((v) => `"${v.text}"`).join(", ");
|
|
13334
|
+
const budget = `
|
|
13335
|
+
Attempt ${this.scriptState.analysisAttempts} of ${MAX_ANALYSIS_ATTEMPTS}. Fix the value(s) and call write_analysis again \u2014 do not switch to prose while attempts remain.`;
|
|
12907
13336
|
if (this.scriptState.analysisAttempts < MAX_ANALYSIS_ATTEMPTS) {
|
|
12908
13337
|
logger.warn(`[MainAgent] write_analysis rejected \u2014 ${violations.length} hardcoded value(s): ${named}`);
|
|
12909
13338
|
if (repeatedTexts.length > 0) {
|
|
12910
13339
|
const repeatedList = repeatedTexts.map((t) => `"${t}"`).join(", ");
|
|
12911
13340
|
return `${lintError}
|
|
12912
|
-
STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reason won't help a second time \u2014 this is almost certainly a parameter or an already-resolved value, not something to compute fresh: if it's an id, use \`params.<name>\` directly; if it's a name, read it from \`data\`/\`datasets\` (e.g. \`datasets.summary?.[0]?.<field>\`). Replace the literal itself, not the wording around it
|
|
13341
|
+
STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reason won't help a second time \u2014 this is almost certainly a parameter or an already-resolved value, not something to compute fresh: if it's an id, use \`params.<name>\` directly; if it's a name, read it from \`data\`/\`datasets\` (e.g. \`datasets.summary?.[0]?.<field>\`). Replace the literal itself, not the wording around it.` + budget;
|
|
12913
13342
|
}
|
|
12914
|
-
return lintError;
|
|
13343
|
+
return lintError + budget;
|
|
12915
13344
|
}
|
|
12916
13345
|
logger.warn(`[MainAgent] write_analysis accepted on the final attempt DESPITE ${violations.length} hardcoded value(s): ${named} \u2014 a computed answer with a frozen figure beats LLM prose. Those value(s) will not track the data on replay.`);
|
|
12917
13346
|
}
|
|
@@ -12931,7 +13360,7 @@ STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reaso
|
|
|
12931
13360
|
this.scriptStore.getScriptPath(draft),
|
|
12932
13361
|
{},
|
|
12933
13362
|
verified.data,
|
|
12934
|
-
{ externalTools: this.externalTools, signal: this.abortSignal, entities: this.resolvedEntities },
|
|
13363
|
+
{ externalTools: this.externalTools, signal: this.abortSignal, entities: this.resolvedEntities, appliedPolicies: this.observedPolicies() },
|
|
12935
13364
|
datasets
|
|
12936
13365
|
);
|
|
12937
13366
|
if (analysisError || !analysis?.analysis) {
|
|
@@ -13232,11 +13661,26 @@ The verified script is intact. You can still call write_analysis and write_compo
|
|
|
13232
13661
|
*/
|
|
13233
13662
|
buildResolvedEntitiesBlock() {
|
|
13234
13663
|
if (!this.resolvedEntities.length) return "";
|
|
13235
|
-
const
|
|
13664
|
+
const render = (e) => {
|
|
13236
13665
|
const scopeSql = e.attrs?.scopeSql;
|
|
13237
13666
|
const scopePart = typeof scopeSql === "string" && scopeSql.trim() ? ` \u2014 READY-MADE SCOPE (use this directly, do not re-derive it): ${scopeSql}` : "";
|
|
13238
13667
|
return `- "${e.mention}" \u2192 ${e.entityType} id ${e.instanceId} (name in DB: ${e.displayName})${scopePart}`;
|
|
13239
|
-
}
|
|
13668
|
+
};
|
|
13669
|
+
const byMention = /* @__PURE__ */ new Map();
|
|
13670
|
+
for (const e of this.resolvedEntities) {
|
|
13671
|
+
const group = byMention.get(e.mention) ?? [];
|
|
13672
|
+
group.push(e);
|
|
13673
|
+
byMention.set(e.mention, group);
|
|
13674
|
+
}
|
|
13675
|
+
const lines = [];
|
|
13676
|
+
for (const [mention, group] of byMention) {
|
|
13677
|
+
if (group.length === 1) {
|
|
13678
|
+
lines.push(render(group[0]));
|
|
13679
|
+
continue;
|
|
13680
|
+
}
|
|
13681
|
+
lines.push(`- "${mention}" matched ${group.length} entity types at the same confidence \u2014 CHOOSE the one the question means and filter on that id ALONE, never on more than one:`);
|
|
13682
|
+
for (const e of group) lines.push(` ${render(e).slice(2)}`);
|
|
13683
|
+
}
|
|
13240
13684
|
return `
|
|
13241
13685
|
|
|
13242
13686
|
RESOLVED ENTITIES \u2014 these were looked up already. Filter on the ids; do NOT search for them by name. Where a name must appear in output or a comparison, use the "name in DB" spelling EXACTLY as given. Where a READY-MADE SCOPE is given, paste it into the query as-is instead of writing your own join/city-match/lookup for the same thing:
|
|
@@ -13556,9 +14000,25 @@ ${r.body}`).join("\n\n");
|
|
|
13556
14000
|
logger.info(
|
|
13557
14001
|
`[MainAgent] resolve_entities: ${res?.resolved?.length ?? 0} resolved, ${res?.unresolved?.length ?? 0} unresolved, ${Object.keys(res?.entityMap || {}).length} concept(s)`
|
|
13558
14002
|
);
|
|
14003
|
+
const TIE_EPSILON = 0.05;
|
|
14004
|
+
const key = (e) => `${e.entityType}::${e.instanceId}`;
|
|
14005
|
+
const add = (mention, c) => {
|
|
14006
|
+
if (!c?.instanceId) return;
|
|
14007
|
+
if (this.resolvedEntities.some((e) => key(e) === key(c))) return;
|
|
14008
|
+
this.resolvedEntities.push({
|
|
14009
|
+
mention: String(mention ?? ""),
|
|
14010
|
+
entityType: String(c.entityType ?? ""),
|
|
14011
|
+
instanceId: String(c.instanceId),
|
|
14012
|
+
displayName: String(c.displayName ?? mention ?? ""),
|
|
14013
|
+
attrs: c.attrs && typeof c.attrs === "object" ? c.attrs : void 0
|
|
14014
|
+
});
|
|
14015
|
+
};
|
|
13559
14016
|
for (const r of Array.isArray(res?.resolved) ? res.resolved : []) {
|
|
13560
14017
|
if (!r?.instanceId) continue;
|
|
13561
|
-
const
|
|
14018
|
+
for (const a of Array.isArray(r.alternatives) ? r.alternatives : []) {
|
|
14019
|
+
const tied = typeof r.score !== "number" || typeof a?.score !== "number" ? false : r.score - a.score <= TIE_EPSILON;
|
|
14020
|
+
if (tied) add(r.mention, a);
|
|
14021
|
+
}
|
|
13562
14022
|
if (this.resolvedEntities.some((e) => key(e) === key(r))) continue;
|
|
13563
14023
|
this.resolvedEntities.push({
|
|
13564
14024
|
mention: String(r.mention ?? ""),
|
|
@@ -13817,7 +14277,7 @@ ${propLines}` : ""
|
|
|
13817
14277
|
const scriptBodyContract = await promptLoader.loadScriptBodyContract({
|
|
13818
14278
|
MAX_ROWS_RENDERED: String(MAX_ROWS_RENDERED),
|
|
13819
14279
|
MAX_ROWS_FETCHED: String(MAX_ROWS_FETCHED)
|
|
13820
|
-
});
|
|
14280
|
+
}) + buildAccessBodyContract(this.config.userAccessContext ?? "");
|
|
13821
14281
|
try {
|
|
13822
14282
|
if (this.config.trace) this.config.trace.scriptBodyContractText = scriptBodyContract;
|
|
13823
14283
|
} catch (traceErr) {
|
|
@@ -13835,7 +14295,9 @@ ${propLines}` : ""
|
|
|
13835
14295
|
GLOBAL_KNOWLEDGE_BASE: this.config.globalKnowledgeBase || "No global knowledge base available.",
|
|
13836
14296
|
CURRENT_DATETIME: getCurrentDateTimeForPrompt(),
|
|
13837
14297
|
KNOWLEDGE_BASE_CONTEXT: this.config.knowledgeBaseContext || "No additional knowledge base context available.",
|
|
13838
|
-
|
|
14298
|
+
// '' when the user has no policy — the whole section disappears rather
|
|
14299
|
+
// than rendering a heading over "no restrictions". See access-section.ts.
|
|
14300
|
+
USER_ACCESS_CONTEXT: buildAccessSection(this.config.userAccessContext ?? "", this.editMode ? "edit" : "main"),
|
|
13839
14301
|
ENTITY_CATALOG: entityCatalogText || "No entity catalog available \u2014 resolve_entities may still be called with mentions.",
|
|
13840
14302
|
CONCEPT_CATALOG: conceptCatalogText || "No concept catalog available \u2014 derive definitions from the schema and say that you did.",
|
|
13841
14303
|
// Bodies, not names: an invariant is injected precisely because it
|
|
@@ -14040,8 +14502,20 @@ Execution time: ${metadata.executionTimeMs}ms
|
|
|
14040
14502
|
rows: t?.result?._mainAgentRows ?? t?.result?._sampleData ?? [],
|
|
14041
14503
|
summary: t?.result?._summary,
|
|
14042
14504
|
total: t?.result?._totalRecords,
|
|
14043
|
-
shown: t?.result?._recordsShown
|
|
14505
|
+
shown: t?.result?._recordsShown,
|
|
14506
|
+
policy: t?.result?._ACCESS_POLICY_APPLIED
|
|
14044
14507
|
})).filter((q) => Boolean(q.sql));
|
|
14508
|
+
const policyNotes = [...new Set(queries.map((q) => q.policy).filter(Boolean))];
|
|
14509
|
+
if (policyNotes.length) {
|
|
14510
|
+
output += `
|
|
14511
|
+
\u{1F512} ACCESS POLICY \u2014 applies to EVERY figure below:
|
|
14512
|
+
${policyNotes.map((n) => `- ${n}`).join("\n")}
|
|
14513
|
+
Reflect this in the answer: name the restriction in component titles, the analysis, and the method line \u2014 phrased naturally as part of the sentence, never pasted in as a raw filter expression. Do not describe these numbers as complete, overall, or covering everything.
|
|
14514
|
+
If a query came back EMPTY, that is NOT evidence the records do not exist \u2014 under a restriction it usually means they are outside this user's access. Say so; never write "no data found" or "there are none".
|
|
14515
|
+
Do NOT copy the restriction into the script's SQL: it is enforced automatically on every run, and a value written into a recipe goes stale the moment this user's access changes. getAnalysis and getComponents receive what was actually applied as their 6th argument, \`scope: string[]\` \u2014 use that to name it, in BOTH functions.
|
|
14516
|
+
|
|
14517
|
+
`;
|
|
14518
|
+
}
|
|
14045
14519
|
if (queries.length === 0) {
|
|
14046
14520
|
output += "No data returned.";
|
|
14047
14521
|
return output;
|
|
@@ -17480,16 +17954,37 @@ function storeScriptDatasetQuery(q, ctx) {
|
|
|
17480
17954
|
return queryCache.storeQuery(q.sql, { data, count });
|
|
17481
17955
|
}
|
|
17482
17956
|
function buildUserAccessContext(userConfig) {
|
|
17483
|
-
|
|
17484
|
-
const
|
|
17485
|
-
const
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17957
|
+
const entries = Array.isArray(userConfig) ? userConfig : [userConfig];
|
|
17958
|
+
const restrictions = [];
|
|
17959
|
+
const prompts = [];
|
|
17960
|
+
let anyRestriction = false;
|
|
17961
|
+
for (const e of entries) {
|
|
17962
|
+
if (!e || typeof e !== "object") continue;
|
|
17963
|
+
const rowFilters = Array.isArray(e.rowFilters) ? e.rowFilters : [];
|
|
17964
|
+
const readableTables = Array.isArray(e.readableTables) ? e.readableTables.filter((t) => typeof t === "string" && !!t) : [];
|
|
17965
|
+
const realFilters = rowFilters.filter(
|
|
17966
|
+
(f) => f && typeof f === "object" && f.column && Array.isArray(f.values) && f.values.length > 0
|
|
17967
|
+
);
|
|
17968
|
+
if (realFilters.length || readableTables.length) {
|
|
17969
|
+
anyRestriction = true;
|
|
17970
|
+
if (enforcementApplies()) {
|
|
17971
|
+
const clean = (v) => typeof v === "string" ? v.replace(/\s+/g, " ").trim() : "";
|
|
17972
|
+
const labels = [
|
|
17973
|
+
.../* @__PURE__ */ new Set([
|
|
17974
|
+
...realFilters.map((f) => clean(f.label)).filter(Boolean),
|
|
17975
|
+
...clean(e.label) ? [clean(e.label)] : []
|
|
17976
|
+
])
|
|
17977
|
+
];
|
|
17978
|
+
restrictions.push(labels.length ? labels.join("; ") : "This user's data access is restricted.");
|
|
17979
|
+
} else {
|
|
17980
|
+
const { prompt: _p, ...rest } = e;
|
|
17981
|
+
restrictions.push(JSON.stringify(rest));
|
|
17982
|
+
}
|
|
17983
|
+
}
|
|
17984
|
+
if (typeof e.prompt === "string" && e.prompt.trim()) prompts.push(e.prompt.trim());
|
|
17491
17985
|
}
|
|
17492
|
-
return
|
|
17986
|
+
if (!anyRestriction && prompts.length === 0) return "";
|
|
17987
|
+
return [...restrictions, ...prompts].join("\n\n");
|
|
17493
17988
|
}
|
|
17494
17989
|
var get_agent_user_response = async (prompt, components, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, conversationHistory, streamCallback, collections, externalTools, userId, mainAgentModel, sourceAgentModel, workflows, signal, activeBindings, userConfig) => {
|
|
17495
17990
|
const startTime = Date.now();
|
|
@@ -19252,7 +19747,7 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
19252
19747
|
};
|
|
19253
19748
|
};
|
|
19254
19749
|
async function handleUserPromptRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows) {
|
|
19255
|
-
const response = await get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows);
|
|
19750
|
+
const response = await runWithMessageIdentity(data, () => get_user_request(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, externalTools, mainAgentModel, sourceAgentModel, workflows));
|
|
19256
19751
|
if (response.data?.component?.props?.config?.components) {
|
|
19257
19752
|
response.data.component.props.config.components = response.data.component.props.config.components.map((comp) => ({
|
|
19258
19753
|
...comp,
|
|
@@ -22353,9 +22848,9 @@ async function loadDatabaseRulesForTools(tools) {
|
|
|
22353
22848
|
}
|
|
22354
22849
|
const sections = await Promise.all(
|
|
22355
22850
|
dbTypes.map(async (dbType) => {
|
|
22356
|
-
const
|
|
22851
|
+
const rules2 = await promptLoader.loadDatabaseRulesForType(dbType);
|
|
22357
22852
|
return `## ${dbType.toUpperCase()} Rules (apply ONLY to ${dbType} tools \u2014 check each tool's ID prefix)
|
|
22358
|
-
${
|
|
22853
|
+
${rules2}`;
|
|
22359
22854
|
})
|
|
22360
22855
|
);
|
|
22361
22856
|
return sections.join("\n\n---\n\n");
|
|
@@ -23406,7 +23901,7 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
|
|
|
23406
23901
|
};
|
|
23407
23902
|
};
|
|
23408
23903
|
async function handleDashCompRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, dashCompModels) {
|
|
23409
|
-
const response = await processDashCompRequest(
|
|
23904
|
+
const response = await runWithMessageIdentity(data, () => processDashCompRequest(
|
|
23410
23905
|
data,
|
|
23411
23906
|
components,
|
|
23412
23907
|
sendMessage,
|
|
@@ -23418,7 +23913,7 @@ async function handleDashCompRequest(data, components, sendMessage, anthropicApi
|
|
|
23418
23913
|
collections,
|
|
23419
23914
|
tools,
|
|
23420
23915
|
dashCompModels
|
|
23421
|
-
);
|
|
23916
|
+
));
|
|
23422
23917
|
sendDashCompResponse(
|
|
23423
23918
|
response.id || data.id,
|
|
23424
23919
|
{
|
|
@@ -25400,7 +25895,7 @@ var processReportCompRequest = async (data, components, _sendMessage, anthropicA
|
|
|
25400
25895
|
};
|
|
25401
25896
|
};
|
|
25402
25897
|
async function handleReportCompRequest(data, components, sendMessage, anthropicApiKey, groqApiKey, geminiApiKey, openaiApiKey, llmProviders, collections, tools, modelConfig) {
|
|
25403
|
-
const response = await processReportCompRequest(
|
|
25898
|
+
const response = await runWithMessageIdentity(data, () => processReportCompRequest(
|
|
25404
25899
|
data,
|
|
25405
25900
|
components,
|
|
25406
25901
|
sendMessage,
|
|
@@ -25412,7 +25907,7 @@ async function handleReportCompRequest(data, components, sendMessage, anthropicA
|
|
|
25412
25907
|
collections,
|
|
25413
25908
|
tools,
|
|
25414
25909
|
modelConfig
|
|
25415
|
-
);
|
|
25910
|
+
));
|
|
25416
25911
|
sendReportCompResponse(
|
|
25417
25912
|
response.id || data.id,
|
|
25418
25913
|
{
|
|
@@ -28065,6 +28560,7 @@ var SuperatomSDK = class {
|
|
|
28065
28560
|
DEFAULT_AGENT_CONFIG,
|
|
28066
28561
|
LLM,
|
|
28067
28562
|
MainAgent,
|
|
28563
|
+
MissingRequestContextError,
|
|
28068
28564
|
STORAGE_CONFIG,
|
|
28069
28565
|
ScriptMatcher,
|
|
28070
28566
|
ScriptStore,
|
|
@@ -28076,36 +28572,53 @@ var SuperatomSDK = class {
|
|
|
28076
28572
|
UserManager,
|
|
28077
28573
|
anthropicLLM,
|
|
28078
28574
|
assertReadOnlySql,
|
|
28575
|
+
bindContext,
|
|
28576
|
+
captureContext,
|
|
28577
|
+
clearContextTokens,
|
|
28578
|
+
contextTokenCount,
|
|
28079
28579
|
createCodexSession,
|
|
28080
28580
|
createCodingAgentSession,
|
|
28081
28581
|
dashboardConversationHistory,
|
|
28082
28582
|
dataFingerprint,
|
|
28583
|
+
describeContext,
|
|
28083
28584
|
describeTriggers,
|
|
28084
28585
|
emitAgentEvent,
|
|
28085
28586
|
extractNamedExport,
|
|
28086
28587
|
geminiLLM,
|
|
28087
28588
|
getCodingAgent,
|
|
28088
28589
|
getCodingAgentsConfig,
|
|
28590
|
+
getRequestContext,
|
|
28089
28591
|
groqLLM,
|
|
28090
28592
|
hybridRerank,
|
|
28593
|
+
isUserContext,
|
|
28091
28594
|
listCodingAgents,
|
|
28092
28595
|
llmUsageLogger,
|
|
28093
28596
|
logger,
|
|
28597
|
+
mintContextToken,
|
|
28094
28598
|
normalizeScriptBody,
|
|
28095
28599
|
onAgentRun,
|
|
28096
28600
|
openaiLLM,
|
|
28601
|
+
policiesFromConfig,
|
|
28097
28602
|
queryCache,
|
|
28098
28603
|
registerCodingAgentCollection,
|
|
28099
28604
|
registerCodingAgents,
|
|
28100
28605
|
registerDashboardAgentCollection,
|
|
28606
|
+
requireRequestContext,
|
|
28101
28607
|
rerankChromaResults,
|
|
28102
28608
|
rerankConversationResults,
|
|
28609
|
+
resolveContextToken,
|
|
28103
28610
|
resolveScriptRecipeStore,
|
|
28611
|
+
revokeContextToken,
|
|
28612
|
+
runAsSystem,
|
|
28613
|
+
runAsUser,
|
|
28104
28614
|
runCodingAgent,
|
|
28105
28615
|
runScript,
|
|
28616
|
+
runWithContext,
|
|
28617
|
+
runWithMessageIdentity,
|
|
28106
28618
|
startAgentTriggers,
|
|
28107
28619
|
stopAgentTriggers,
|
|
28108
28620
|
userPromptErrorLogger,
|
|
28621
|
+
withCapturedContext,
|
|
28109
28622
|
writeBridgeShims
|
|
28110
28623
|
});
|
|
28111
28624
|
//# sourceMappingURL=index.js.map
|