@tangle-network/agent-app 0.1.3 → 0.1.5

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.
@@ -24,10 +24,16 @@ function buildDelegationMcpServer(opts) {
24
24
  metadata: { surface: "delegation:dispatch", tools: DELEGATION_TOOLS }
25
25
  };
26
26
  }
27
+ function delegationMcpForConfig(config, opts) {
28
+ if (!config.delegation?.enabled) return {};
29
+ const server = buildDelegationMcpServer(opts);
30
+ return server ? { [DELEGATION_MCP_SERVER_KEY]: server } : {};
31
+ }
27
32
 
28
33
  export {
29
34
  DELEGATION_MCP_SERVER_KEY,
30
35
  DELEGATION_TOOLS,
31
- buildDelegationMcpServer
36
+ buildDelegationMcpServer,
37
+ delegationMcpForConfig
32
38
  };
33
- //# sourceMappingURL=chunk-7P6VIHI4.js.map
39
+ //# sourceMappingURL=chunk-AQ2BOPOQ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/delegation/index.ts"],"sourcesContent":["/**\n * Delegated looped work — the agent-runtime \"driven loop\" MCP.\n *\n * For multi-step research or document generation, the agent dispatches a loop\n * that runs to completion in its OWN sandbox (via @tangle-network/agent-runtime's\n * stdio MCP, executed in the agent-driver) and returns the artifact. This is\n * how an app's main agent \"programs / delegates\" without doing long mechanical\n * work inline. It is an OPTIONAL module — an app opts in by spreading the\n * server into its profile's `mcp` map.\n *\n * The shape is the portable `AgentProfileMcpServer` the sandbox SDK accepts\n * (transport: 'stdio' → the orchestrator derives `{ type:'local', command }`).\n * Kept structural here so this package needs no sandbox-SDK dependency.\n */\n\nexport const DELEGATION_MCP_SERVER_KEY = 'agent-runtime-delegation'\n\nexport const DELEGATION_TOOLS = [\n 'delegate_code',\n 'delegate_research',\n 'delegate_feedback',\n 'delegation_status',\n 'delegation_history',\n] as const\n\n/** The stdio MCP server entry — structurally an `AgentProfileMcpServer`. */\nexport interface DelegationMcpServer {\n transport: 'stdio'\n command: string\n args: string[]\n env: Record<string, string>\n enabled: true\n metadata: { surface: string; tools: readonly string[] }\n}\n\nexport interface BuildDelegationOptions {\n /** Platform API key the delegated loop authenticates with (required — the\n * loop runs in its own sandbox and bills against this key). Omit/empty →\n * returns undefined (fail-closed: no key, no delegation). */\n apiKey?: string\n /** Extra env to forward into the delegated loop (sandbox base URL, OTel trace\n * propagation, etc.). Only defined values are forwarded. */\n forwardEnv?: Record<string, string | undefined>\n /** npm spec for the runtime MCP. Defaults to the published agent-runtime. */\n packageSpec?: string\n}\n\n/**\n * Build the delegation MCP server entry, keyed under\n * {@link DELEGATION_MCP_SERVER_KEY}, or `undefined` when no platform API key is\n * available. Spread the result into the profile's `mcp` map:\n *\n * const delegation = buildDelegationMcpServer({ apiKey: env.TANGLE_API_KEY, forwardEnv: env })\n * const mcp = { ...(delegation ? { [DELEGATION_MCP_SERVER_KEY]: delegation } : {}) }\n */\nexport function buildDelegationMcpServer(opts: BuildDelegationOptions): DelegationMcpServer | undefined {\n if (!opts.apiKey) return undefined\n const env: Record<string, string> = { TANGLE_API_KEY: opts.apiKey }\n const forward = opts.forwardEnv ?? {}\n for (const key of ['SANDBOX_BASE_URL', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_HEADERS', 'TRACE_ID', 'PARENT_SPAN_ID']) {\n const value = forward[key]\n if (value) env[key] = value\n }\n return {\n transport: 'stdio',\n command: 'npx',\n args: ['-y', opts.packageSpec ?? '@tangle-network/agent-runtime', 'mcp'],\n env,\n enabled: true,\n metadata: { surface: 'delegation:dispatch', tools: DELEGATION_TOOLS },\n }\n}\n"],"mappings":";AAeO,IAAM,4BAA4B;AAElC,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgCO,SAAS,yBAAyB,MAA+D;AACtG,MAAI,CAAC,KAAK,OAAQ,QAAO;AACzB,QAAM,MAA8B,EAAE,gBAAgB,KAAK,OAAO;AAClE,QAAM,UAAU,KAAK,cAAc,CAAC;AACpC,aAAW,OAAO,CAAC,oBAAoB,+BAA+B,8BAA8B,YAAY,gBAAgB,GAAG;AACjI,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,MAAO,KAAI,GAAG,IAAI;AAAA,EACxB;AACA,SAAO;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,MAAM,CAAC,MAAM,KAAK,eAAe,iCAAiC,KAAK;AAAA,IACvE;AAAA,IACA,SAAS;AAAA,IACT,UAAU,EAAE,SAAS,uBAAuB,OAAO,iBAAiB;AAAA,EACtE;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/delegation/index.ts"],"sourcesContent":["/**\n * Delegated looped work — the agent-runtime \"driven loop\" MCP.\n *\n * For multi-step research or document generation, the agent dispatches a loop\n * that runs to completion in its OWN sandbox (via @tangle-network/agent-runtime's\n * stdio MCP, executed in the agent-driver) and returns the artifact. This is\n * how an app's main agent \"programs / delegates\" without doing long mechanical\n * work inline. It is an OPTIONAL module — an app opts in by spreading the\n * server into its profile's `mcp` map.\n *\n * The shape is the portable `AgentProfileMcpServer` the sandbox SDK accepts\n * (transport: 'stdio' → the orchestrator derives `{ type:'local', command }`).\n * Kept structural here so this package needs no sandbox-SDK dependency.\n */\n\nexport const DELEGATION_MCP_SERVER_KEY = 'agent-runtime-delegation'\n\nexport const DELEGATION_TOOLS = [\n 'delegate_code',\n 'delegate_research',\n 'delegate_feedback',\n 'delegation_status',\n 'delegation_history',\n] as const\n\n/** The stdio MCP server entry — structurally an `AgentProfileMcpServer`. */\nexport interface DelegationMcpServer {\n transport: 'stdio'\n command: string\n args: string[]\n env: Record<string, string>\n enabled: true\n metadata: { surface: string; tools: readonly string[] }\n}\n\nexport interface BuildDelegationOptions {\n /** Platform API key the delegated loop authenticates with (required — the\n * loop runs in its own sandbox and bills against this key). Omit/empty →\n * returns undefined (fail-closed: no key, no delegation). */\n apiKey?: string\n /** Extra env to forward into the delegated loop (sandbox base URL, OTel trace\n * propagation, etc.). Only defined values are forwarded. */\n forwardEnv?: Record<string, string | undefined>\n /** npm spec for the runtime MCP. Defaults to the published agent-runtime. */\n packageSpec?: string\n}\n\n/**\n * Build the delegation MCP server entry, keyed under\n * {@link DELEGATION_MCP_SERVER_KEY}, or `undefined` when no platform API key is\n * available. Spread the result into the profile's `mcp` map:\n *\n * const delegation = buildDelegationMcpServer({ apiKey: env.TANGLE_API_KEY, forwardEnv: env })\n * const mcp = { ...(delegation ? { [DELEGATION_MCP_SERVER_KEY]: delegation } : {}) }\n */\nexport function buildDelegationMcpServer(opts: BuildDelegationOptions): DelegationMcpServer | undefined {\n if (!opts.apiKey) return undefined\n const env: Record<string, string> = { TANGLE_API_KEY: opts.apiKey }\n const forward = opts.forwardEnv ?? {}\n for (const key of ['SANDBOX_BASE_URL', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_HEADERS', 'TRACE_ID', 'PARENT_SPAN_ID']) {\n const value = forward[key]\n if (value) env[key] = value\n }\n return {\n transport: 'stdio',\n command: 'npx',\n args: ['-y', opts.packageSpec ?? '@tangle-network/agent-runtime', 'mcp'],\n env,\n enabled: true,\n metadata: { surface: 'delegation:dispatch', tools: DELEGATION_TOOLS },\n }\n}\n\n/**\n * Config-driven wiring: returns the delegation MCP entry keyed under\n * {@link DELEGATION_MCP_SERVER_KEY} when the product's `config.delegation.enabled`\n * is true (and a platform key is available), else an empty object. Spread the\n * result directly into the sandbox profile's `mcp` map — this is the seam\n * `agent.config.delegation` flows through, so a coding agent toggles background\n * agents/loops by flipping one boolean, never by wiring the MCP by hand.\n *\n * const mcp = { ...rest, ...delegationMcpForConfig(config, { apiKey: env.TANGLE_API_KEY, forwardEnv: env }) }\n */\nexport function delegationMcpForConfig(\n config: { delegation?: { enabled?: boolean } },\n opts: BuildDelegationOptions,\n): Record<string, DelegationMcpServer> {\n if (!config.delegation?.enabled) return {}\n const server = buildDelegationMcpServer(opts)\n return server ? { [DELEGATION_MCP_SERVER_KEY]: server } : {}\n}\n"],"mappings":";AAeO,IAAM,4BAA4B;AAElC,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgCO,SAAS,yBAAyB,MAA+D;AACtG,MAAI,CAAC,KAAK,OAAQ,QAAO;AACzB,QAAM,MAA8B,EAAE,gBAAgB,KAAK,OAAO;AAClE,QAAM,UAAU,KAAK,cAAc,CAAC;AACpC,aAAW,OAAO,CAAC,oBAAoB,+BAA+B,8BAA8B,YAAY,gBAAgB,GAAG;AACjI,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,MAAO,KAAI,GAAG,IAAI;AAAA,EACxB;AACA,SAAO;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,MAAM,CAAC,MAAM,KAAK,eAAe,iCAAiC,KAAK;AAAA,IACvE;AAAA,IACA,SAAS;AAAA,IACT,UAAU,EAAE,SAAS,uBAAuB,OAAO,iBAAiB;AAAA,EACtE;AACF;AAYO,SAAS,uBACd,QACA,MACqC;AACrC,MAAI,CAAC,OAAO,YAAY,QAAS,QAAO,CAAC;AACzC,QAAM,SAAS,yBAAyB,IAAI;AAC5C,SAAO,SAAS,EAAE,CAAC,yBAAyB,GAAG,OAAO,IAAI,CAAC;AAC7D;","names":[]}
@@ -0,0 +1,102 @@
1
+ // src/knowledge-loop/index.ts
2
+ import {
3
+ runKnowledgeResearchLoop,
4
+ textSourceAdapter
5
+ } from "@tangle-network/agent-knowledge";
6
+ var DEFAULT_MIN_CONFIDENCE = 0.7;
7
+ var DEFAULT_GOAL = "Acquire and ground the knowledge this product requires.";
8
+ function reviewCandidate(candidate, minConfidence) {
9
+ if (!candidate.proposalText) {
10
+ return {
11
+ accepted: true,
12
+ reason: "no-proposal",
13
+ confidence: candidate.confidence ?? 1,
14
+ minConfidence
15
+ };
16
+ }
17
+ const confidence = candidate.confidence ?? 0;
18
+ if (confidence >= minConfidence) {
19
+ return {
20
+ accepted: true,
21
+ reason: `confidence ${confidence.toFixed(2)} >= minConfidence ${minConfidence.toFixed(2)}`,
22
+ confidence,
23
+ minConfidence
24
+ };
25
+ }
26
+ return {
27
+ accepted: false,
28
+ reason: `confidence ${confidence.toFixed(2)} < minConfidence ${minConfidence.toFixed(2)}`,
29
+ confidence,
30
+ minConfidence
31
+ };
32
+ }
33
+ function createReviewerDecider(propose) {
34
+ return async (input) => {
35
+ const candidate = await propose(input);
36
+ const verdict = reviewCandidate(candidate, input.minConfidence);
37
+ return { candidate, verdict };
38
+ };
39
+ }
40
+ function toResearchDecision(decision) {
41
+ const { candidate, verdict } = decision;
42
+ return {
43
+ notes: candidate.notes,
44
+ sourcePaths: candidate.sourcePaths,
45
+ sourceTexts: candidate.sourceTexts,
46
+ proposalText: verdict.accepted ? candidate.proposalText : void 0,
47
+ done: candidate.done,
48
+ metadata: {
49
+ ...candidate.metadata ?? {},
50
+ gate: verdict
51
+ }
52
+ };
53
+ }
54
+ var noopDecider = (input) => ({
55
+ candidate: { done: true, notes: "no decider supplied; nothing proposed" },
56
+ verdict: {
57
+ accepted: true,
58
+ reason: "no-proposal",
59
+ confidence: 1,
60
+ minConfidence: input.minConfidence
61
+ }
62
+ });
63
+ function createKnowledgeLoop(knowledge, deps) {
64
+ const goal = knowledge.loop?.goal ?? deps.defaultGoal ?? DEFAULT_GOAL;
65
+ const minConfidence = knowledge.loop?.minConfidence ?? deps.defaultMinConfidence ?? DEFAULT_MIN_CONFIDENCE;
66
+ const freshness = knowledge.loop?.freshness;
67
+ const adapters = [...deps.adapters ?? [], textSourceAdapter];
68
+ const decide = deps.decide ?? noopDecider;
69
+ const run = () => runKnowledgeResearchLoop({
70
+ root: deps.root,
71
+ goal,
72
+ maxIterations: deps.maxIterations,
73
+ actor: deps.actor,
74
+ signal: deps.signal,
75
+ onStep: deps.onStep,
76
+ sourceOptions: { adapters },
77
+ async step(context) {
78
+ const decision = await decide({
79
+ context,
80
+ goal,
81
+ minConfidence,
82
+ freshness,
83
+ sources: knowledge.sources,
84
+ driver: deps.driver
85
+ });
86
+ return toResearchDecision(decision);
87
+ }
88
+ });
89
+ return {
90
+ run,
91
+ goal,
92
+ minConfidence,
93
+ adapters
94
+ };
95
+ }
96
+
97
+ export {
98
+ reviewCandidate,
99
+ createReviewerDecider,
100
+ createKnowledgeLoop
101
+ };
102
+ //# sourceMappingURL=chunk-EEPJGZJW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/knowledge-loop/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/knowledge-loop` — wire the declarative\n * `AgentKnowledgeConfig` to a running, source-grounded, eval-gated knowledge\n * acquisition loop.\n *\n * This module does NOT implement a loop. `@tangle-network/agent-knowledge`\n * already ships `runKnowledgeResearchLoop` — the source-grounded,\n * propose-don't-apply primitive (sources become immutable records first; only\n * accepted `---FILE: knowledge/...---` write blocks are applied; lint +\n * validation + optional readiness gate every iteration). It is pluggable on two\n * seams: a `SourceAdapter[]` (how raw bytes/text become curated source records —\n * text by default, audio/video/image adapters added by the consumer) and a\n * `step` decider (the per-iteration policy: an agentic judge, a sandbox run, or\n * a deterministic gate — the loop deliberately bakes none).\n *\n * `createKnowledgeLoop(config.knowledge, deps)` is the thin mapper between the\n * two:\n *\n * - `config.knowledge.sources` → adapter selection. The text adapter is the\n * default; `deps.adapters` is the multimodal seam (prepend an audio/video/\n * image `SourceAdapter` and the loop ingests that media). agent-app bakes no\n * media handler — it's a parameter.\n * - `config.knowledge.loop` → `runKnowledgeResearchLoop` options. `goal` maps\n * to the loop goal, `minConfidence` to the gate threshold, `freshness` is\n * threaded to the decider so a per-source-freshness policy can read it.\n * - `deps.decide` → the pluggable gate. DEFAULT is a reviewer policy\n * (agent-knowledge's propose-don't-apply posture): a candidate carrying\n * confidence below `minConfidence` is gated OUT (its proposal text is\n * dropped — sources are still recorded, because grounding is never the thing\n * we gate); at/above threshold it is accepted and the loop applies the write\n * blocks. Swap in an agentic judge or a sandbox run by passing your own\n * `decide`.\n * - `deps.driver` → the agent-runtime turn driver (this repo's `../runtime`\n * `runAppToolLoop` seam, or any compatible driver) the decider invokes for\n * the loop's agent turns. Optional; a deterministic / sandbox decider needs\n * no model turns and omits it.\n *\n * Layering: agent-knowledge and agent-runtime are PEER dependencies, never\n * bundled. This module imports only TYPES + the loop entry from agent-knowledge\n * and stays substrate-free behind the `decide` / `driver` / `adapters` seams.\n */\n\nimport type {\n AddSourceTextInput,\n KnowledgeResearchLoopContext,\n KnowledgeResearchLoopDecision,\n KnowledgeResearchLoopResult,\n RunKnowledgeResearchLoopOptions,\n SourceAdapter,\n} from '@tangle-network/agent-knowledge'\nimport {\n runKnowledgeResearchLoop,\n textSourceAdapter,\n} from '@tangle-network/agent-knowledge'\nimport type { AgentKnowledgeConfig, KnowledgeSourceSpec } from '../config/index'\n\n/**\n * A research candidate the decider evaluates before the loop applies it. This is\n * the propose-don't-apply unit: notes + the sources discovered this iteration +\n * the proposed write blocks, each carrying a confidence the gate scores against\n * the configured `minConfidence`.\n */\nexport interface KnowledgeCandidate {\n /** Human-readable research transcript for this iteration. */\n notes?: string\n /**\n * Textual source artifacts to register as immutable sources BEFORE any\n * proposal is applied. Recording these is grounding, not the gated step — a\n * rejected candidate still keeps its sources (so the next iteration is better\n * grounded). The decider may not strip these.\n */\n sourceTexts?: AddSourceTextInput[]\n /** Local files to register as immutable sources (same grounding posture). */\n sourcePaths?: string[]\n /**\n * Safe-write-protocol text (`---FILE: knowledge/...---` blocks). This IS the\n * gated step: the default decider drops it when `confidence < minConfidence`.\n */\n proposalText?: string\n /**\n * Aggregate confidence in this candidate's proposal, in [0, 1]. The default\n * reviewer gate compares this to `minConfidence`. A candidate with no\n * `proposalText` needs no confidence (nothing is gated).\n */\n confidence?: number\n /** The researcher's signal that the goal is met; ends the loop. */\n done?: boolean\n metadata?: Record<string, unknown>\n}\n\n/** The verdict a gate returns for one candidate. */\nexport interface KnowledgeGateVerdict {\n /** Whether the candidate's proposal is accepted (applied) or gated out. */\n accepted: boolean\n /** Why — surfaced in the decision metadata for audit. */\n reason: string\n /** The confidence the gate scored (echoed for telemetry). */\n confidence: number\n /** The threshold it was scored against. */\n minConfidence: number\n}\n\n/**\n * The pluggable acquisition policy. Given the loop context (current index, lint,\n * validation, freshness target, the driver), produce a candidate AND a gate\n * verdict on it. This is the seam an agentic judge or a sandbox run plugs into;\n * the default {@link createReviewerDecider} is a confidence gate.\n */\nexport interface KnowledgeDecider {\n (input: KnowledgeDeciderInput): Promise<KnowledgeDecision> | KnowledgeDecision\n}\n\nexport interface KnowledgeDeciderInput {\n /** The agent-knowledge loop context for this iteration. */\n context: KnowledgeResearchLoopContext\n /** The acquisition goal (from `config.loop.goal` or a deps fallback). */\n goal: string\n /** The confidence threshold a proposal must clear to be applied. */\n minConfidence: number\n /** The freshness target (`config.loop.freshness`), if set. */\n freshness?: string\n /** The configured sources, for a decider that fetches/selects among them. */\n sources: KnowledgeSourceSpec[]\n /** The agent-runtime turn driver, if one was supplied to the loop. */\n driver?: KnowledgeLoopDriver\n}\n\nexport interface KnowledgeDecision {\n /** The candidate the policy produced (may be empty to end the loop). */\n candidate: KnowledgeCandidate\n /** The gate's verdict on the candidate's proposal. */\n verdict: KnowledgeGateVerdict\n}\n\n/**\n * The agent-runtime turn driver seam. This is exactly `../runtime`'s\n * `runAppToolLoop` shape (a bounded, awaitable tool-driving turn loop over a\n * model). Typed structurally so a decider can drive the loop's agent turns\n * without this module importing the runtime engine. A deterministic / sandbox\n * decider may omit it.\n */\nexport interface KnowledgeLoopDriver {\n (opts: {\n systemPrompt: string\n userMessage: string\n }): Promise<{ finalText: string }>\n}\n\nexport interface CreateKnowledgeLoopDeps {\n /**\n * The knowledge-base root the loop reads/writes (an agent-knowledge layout).\n * Required — agent-knowledge owns disk; agent-app owns only the wiring.\n */\n root: string\n /**\n * The per-iteration policy. Defaults to {@link createReviewerDecider} keyed on\n * the config's `minConfidence`. Pass your own to use an agentic judge or a\n * sandbox run.\n */\n decide?: KnowledgeDecider\n /**\n * Extra source adapters (audio/video/image/PDF/...). The text adapter is\n * always present as the fallback; these are tried first so a multimodal\n * source is claimed by its adapter. This is the multimodal seam.\n */\n adapters?: SourceAdapter[]\n /** The agent-runtime turn driver for the loop's agent turns. */\n driver?: KnowledgeLoopDriver\n /**\n * Fallback goal when `config.loop.goal` is unset. agent-knowledge requires a\n * goal; this keeps the loop runnable for a config with only requirement specs.\n */\n defaultGoal?: string\n /** Default confidence threshold when `config.loop.minConfidence` is unset. */\n defaultMinConfidence?: number\n /** Max research iterations (forwarded to agent-knowledge). Default 3. */\n maxIterations?: number\n /** Actor stamped on the loop's knowledge events. */\n actor?: string\n /** Abort the loop. */\n signal?: AbortSignal\n /** Per-step hook (forwarded to agent-knowledge's `onStep`). */\n onStep?: RunKnowledgeResearchLoopOptions['onStep']\n}\n\n/** The handle `createKnowledgeLoop` returns. */\nexport interface KnowledgeLoop {\n /** Run the acquisition loop to completion and return the agent-knowledge result. */\n run(): Promise<KnowledgeResearchLoopResult>\n /** The resolved goal the loop pursues. */\n readonly goal: string\n /** The resolved confidence gate threshold. */\n readonly minConfidence: number\n /** The adapters the loop uses (consumer extras first, text last). */\n readonly adapters: SourceAdapter[]\n}\n\nconst DEFAULT_MIN_CONFIDENCE = 0.7\nconst DEFAULT_GOAL = 'Acquire and ground the knowledge this product requires.'\n\n/**\n * The default gate — agent-knowledge's propose-don't-apply reviewer posture as a\n * confidence threshold. A candidate's `proposalText` is applied only when its\n * `confidence` is at/above `minConfidence`; otherwise the proposal is gated out\n * (sources are still recorded). A candidate with no `proposalText` is trivially\n * accepted (nothing to gate). This is the floor policy; swap in an agentic judge\n * or a sandbox-run decider via `deps.decide` for richer review.\n */\nexport function reviewCandidate(\n candidate: KnowledgeCandidate,\n minConfidence: number,\n): KnowledgeGateVerdict {\n if (!candidate.proposalText) {\n return {\n accepted: true,\n reason: 'no-proposal',\n confidence: candidate.confidence ?? 1,\n minConfidence,\n }\n }\n const confidence = candidate.confidence ?? 0\n if (confidence >= minConfidence) {\n return {\n accepted: true,\n reason: `confidence ${confidence.toFixed(2)} >= minConfidence ${minConfidence.toFixed(2)}`,\n confidence,\n minConfidence,\n }\n }\n return {\n accepted: false,\n reason: `confidence ${confidence.toFixed(2)} < minConfidence ${minConfidence.toFixed(2)}`,\n confidence,\n minConfidence,\n }\n}\n\n/**\n * Wrap a candidate-producing policy in the default reviewer gate. The policy\n * decides WHAT to propose (notes, sources, proposalText, confidence); the gate\n * decides whether the proposal is APPLIED. Use this when you have a proposer but\n * want the standard confidence gate; pass a full {@link KnowledgeDecider} to\n * `deps.decide` to own the gate too.\n */\nexport function createReviewerDecider(\n propose: (\n input: KnowledgeDeciderInput,\n ) => Promise<KnowledgeCandidate> | KnowledgeCandidate,\n): KnowledgeDecider {\n return async (input) => {\n const candidate = await propose(input)\n const verdict = reviewCandidate(candidate, input.minConfidence)\n return { candidate, verdict }\n }\n}\n\n/**\n * Apply a gate verdict to a candidate, producing the agent-knowledge decision.\n * Grounding (sources) always passes through; the gated `proposalText` is dropped\n * when the verdict rejects it. The verdict is recorded in `metadata.gate` so the\n * loop's event stream carries the audit trail.\n */\nfunction toResearchDecision(decision: KnowledgeDecision): KnowledgeResearchLoopDecision {\n const { candidate, verdict } = decision\n return {\n notes: candidate.notes,\n sourcePaths: candidate.sourcePaths,\n sourceTexts: candidate.sourceTexts,\n proposalText: verdict.accepted ? candidate.proposalText : undefined,\n done: candidate.done,\n metadata: {\n ...(candidate.metadata ?? {}),\n gate: verdict,\n },\n }\n}\n\n/**\n * The do-nothing default policy: when no `decide` is supplied, the loop runs\n * with a proposer that proposes nothing and ends immediately. A real product\n * supplies a proposer (agentic judge, sandbox run, or deterministic) via\n * `deps.decide`; this keeps `createKnowledgeLoop` total for a config that only\n * declares sources/requirements without a wired researcher yet.\n */\nconst noopDecider: KnowledgeDecider = (input) => ({\n candidate: { done: true, notes: 'no decider supplied; nothing proposed' },\n verdict: {\n accepted: true,\n reason: 'no-proposal',\n confidence: 1,\n minConfidence: input.minConfidence,\n },\n})\n\n/**\n * Build a runnable knowledge-acquisition loop from the product's\n * `AgentKnowledgeConfig` and a small set of seams. Maps config → agent-knowledge\n * `runKnowledgeResearchLoop` options; never reimplements the loop.\n */\nexport function createKnowledgeLoop(\n knowledge: AgentKnowledgeConfig,\n deps: CreateKnowledgeLoopDeps,\n): KnowledgeLoop {\n const goal = knowledge.loop?.goal ?? deps.defaultGoal ?? DEFAULT_GOAL\n const minConfidence =\n knowledge.loop?.minConfidence ?? deps.defaultMinConfidence ?? DEFAULT_MIN_CONFIDENCE\n const freshness = knowledge.loop?.freshness\n\n // Multimodal seam: consumer adapters are tried first; the text adapter is the\n // always-present fallback (it claims text/.md/.txt/.json/.csv). A config\n // source whose media an extra adapter claims is ingested by that adapter.\n const adapters: SourceAdapter[] = [...(deps.adapters ?? []), textSourceAdapter]\n\n const decide = deps.decide ?? noopDecider\n\n const run = (): Promise<KnowledgeResearchLoopResult> =>\n runKnowledgeResearchLoop({\n root: deps.root,\n goal,\n maxIterations: deps.maxIterations,\n actor: deps.actor,\n signal: deps.signal,\n onStep: deps.onStep,\n sourceOptions: { adapters },\n async step(context) {\n const decision = await decide({\n context,\n goal,\n minConfidence,\n freshness,\n sources: knowledge.sources,\n driver: deps.driver,\n })\n return toResearchDecision(decision)\n },\n })\n\n return {\n run,\n goal,\n minConfidence,\n adapters,\n }\n}\n"],"mappings":";AAkDA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAgJP,IAAM,yBAAyB;AAC/B,IAAM,eAAe;AAUd,SAAS,gBACd,WACA,eACsB;AACtB,MAAI,CAAC,UAAU,cAAc;AAC3B,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,YAAY,UAAU,cAAc;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,QAAM,aAAa,UAAU,cAAc;AAC3C,MAAI,cAAc,eAAe;AAC/B,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ,cAAc,WAAW,QAAQ,CAAC,CAAC,qBAAqB,cAAc,QAAQ,CAAC,CAAC;AAAA,MACxF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,QAAQ,cAAc,WAAW,QAAQ,CAAC,CAAC,oBAAoB,cAAc,QAAQ,CAAC,CAAC;AAAA,IACvF;AAAA,IACA;AAAA,EACF;AACF;AASO,SAAS,sBACd,SAGkB;AAClB,SAAO,OAAO,UAAU;AACtB,UAAM,YAAY,MAAM,QAAQ,KAAK;AACrC,UAAM,UAAU,gBAAgB,WAAW,MAAM,aAAa;AAC9D,WAAO,EAAE,WAAW,QAAQ;AAAA,EAC9B;AACF;AAQA,SAAS,mBAAmB,UAA4D;AACtF,QAAM,EAAE,WAAW,QAAQ,IAAI;AAC/B,SAAO;AAAA,IACL,OAAO,UAAU;AAAA,IACjB,aAAa,UAAU;AAAA,IACvB,aAAa,UAAU;AAAA,IACvB,cAAc,QAAQ,WAAW,UAAU,eAAe;AAAA,IAC1D,MAAM,UAAU;AAAA,IAChB,UAAU;AAAA,MACR,GAAI,UAAU,YAAY,CAAC;AAAA,MAC3B,MAAM;AAAA,IACR;AAAA,EACF;AACF;AASA,IAAM,cAAgC,CAAC,WAAW;AAAA,EAChD,WAAW,EAAE,MAAM,MAAM,OAAO,wCAAwC;AAAA,EACxE,SAAS;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,eAAe,MAAM;AAAA,EACvB;AACF;AAOO,SAAS,oBACd,WACA,MACe;AACf,QAAM,OAAO,UAAU,MAAM,QAAQ,KAAK,eAAe;AACzD,QAAM,gBACJ,UAAU,MAAM,iBAAiB,KAAK,wBAAwB;AAChE,QAAM,YAAY,UAAU,MAAM;AAKlC,QAAM,WAA4B,CAAC,GAAI,KAAK,YAAY,CAAC,GAAI,iBAAiB;AAE9E,QAAM,SAAS,KAAK,UAAU;AAE9B,QAAM,MAAM,MACV,yBAAyB;AAAA,IACvB,MAAM,KAAK;AAAA,IACX;AAAA,IACA,eAAe,KAAK;AAAA,IACpB,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,QAAQ,KAAK;AAAA,IACb,eAAe,EAAE,SAAS;AAAA,IAC1B,MAAM,KAAK,SAAS;AAClB,YAAM,WAAW,MAAM,OAAO;AAAA,QAC5B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,QAAQ,KAAK;AAAA,MACf,CAAC;AACD,aAAO,mBAAmB,QAAQ;AAAA,IACpC;AAAA,EACF,CAAC;AAEH,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,318 @@
1
+ import {
2
+ createWorkspaceKeyManager
3
+ } from "./chunk-EAJSWUU5.js";
4
+ import {
5
+ createFieldCrypto
6
+ } from "./chunk-TA5Q4I2K.js";
7
+
8
+ // src/preset-cloudflare/index.ts
9
+ var PRESET_TABLES = {
10
+ proposals: {
11
+ name: "proposals",
12
+ columns: {
13
+ id: "id",
14
+ workspaceId: "workspace_id",
15
+ threadId: "thread_id",
16
+ type: "type",
17
+ title: "title",
18
+ description: "description",
19
+ status: "status",
20
+ createdBy: "created_by",
21
+ createdAt: "created_at"
22
+ }
23
+ },
24
+ threads: {
25
+ name: "threads",
26
+ columns: {
27
+ id: "id",
28
+ workspaceId: "workspace_id",
29
+ title: "title",
30
+ createdAt: "created_at"
31
+ }
32
+ },
33
+ knowledge: {
34
+ name: "knowledge",
35
+ columns: {
36
+ id: "id",
37
+ workspaceId: "workspace_id",
38
+ path: "path",
39
+ kind: "kind",
40
+ label: "label",
41
+ content: "content",
42
+ createdAt: "created_at"
43
+ }
44
+ },
45
+ deadlines: {
46
+ name: "deadlines",
47
+ columns: {
48
+ id: "id",
49
+ workspaceId: "workspace_id",
50
+ threadId: "thread_id",
51
+ title: "title",
52
+ dueDate: "due_date",
53
+ priority: "priority",
54
+ status: "status",
55
+ createdAt: "created_at"
56
+ }
57
+ },
58
+ workspaceKeys: {
59
+ name: "workspace_keys",
60
+ columns: {
61
+ id: "id",
62
+ workspaceId: "workspace_id",
63
+ keyId: "key_id",
64
+ keyEncrypted: "key_encrypted",
65
+ budgetUsd: "budget_usd",
66
+ expiresAt: "expires_at",
67
+ revokedAt: "revoked_at",
68
+ createdAt: "created_at"
69
+ }
70
+ }
71
+ };
72
+ var PRESET_MIGRATION_SQL = [
73
+ `CREATE TABLE IF NOT EXISTS threads (
74
+ id TEXT PRIMARY KEY,
75
+ workspace_id TEXT NOT NULL,
76
+ title TEXT,
77
+ created_at INTEGER NOT NULL
78
+ )`,
79
+ `CREATE TABLE IF NOT EXISTS proposals (
80
+ id TEXT PRIMARY KEY,
81
+ workspace_id TEXT NOT NULL,
82
+ thread_id TEXT,
83
+ type TEXT NOT NULL,
84
+ title TEXT NOT NULL,
85
+ description TEXT,
86
+ status TEXT NOT NULL DEFAULT 'pending',
87
+ created_by TEXT,
88
+ created_at INTEGER NOT NULL
89
+ )`,
90
+ `CREATE TABLE IF NOT EXISTS knowledge (
91
+ id TEXT PRIMARY KEY,
92
+ workspace_id TEXT NOT NULL,
93
+ path TEXT NOT NULL,
94
+ kind TEXT NOT NULL,
95
+ label TEXT,
96
+ content TEXT,
97
+ created_at INTEGER NOT NULL
98
+ )`,
99
+ `CREATE TABLE IF NOT EXISTS deadlines (
100
+ id TEXT PRIMARY KEY,
101
+ workspace_id TEXT NOT NULL,
102
+ thread_id TEXT,
103
+ title TEXT NOT NULL,
104
+ due_date TEXT NOT NULL,
105
+ priority TEXT,
106
+ status TEXT NOT NULL DEFAULT 'scheduled',
107
+ created_at INTEGER NOT NULL
108
+ )`,
109
+ `CREATE TABLE IF NOT EXISTS workspace_keys (
110
+ id TEXT PRIMARY KEY,
111
+ workspace_id TEXT NOT NULL,
112
+ key_id TEXT NOT NULL,
113
+ key_encrypted TEXT NOT NULL,
114
+ budget_usd REAL NOT NULL,
115
+ expires_at INTEGER NOT NULL,
116
+ revoked_at INTEGER,
117
+ created_at INTEGER NOT NULL
118
+ )`,
119
+ `CREATE INDEX IF NOT EXISTS idx_proposals_ws ON proposals (workspace_id, status)`,
120
+ `CREATE INDEX IF NOT EXISTS idx_deadlines_ws ON deadlines (workspace_id, status)`,
121
+ `CREATE INDEX IF NOT EXISTS idx_knowledge_ws ON knowledge (workspace_id)`,
122
+ `CREATE INDEX IF NOT EXISTS idx_workspace_keys_ws ON workspace_keys (workspace_id, revoked_at)`
123
+ ];
124
+ function createPresetDrizzleSchema(d) {
125
+ const { sqliteTable, text, integer, real } = d;
126
+ const C = PRESET_TABLES;
127
+ return {
128
+ threads: sqliteTable(C.threads.name, {
129
+ id: text(C.threads.columns.id).primaryKey(),
130
+ workspaceId: text(C.threads.columns.workspaceId).notNull(),
131
+ title: text(C.threads.columns.title),
132
+ createdAt: integer(C.threads.columns.createdAt).notNull()
133
+ }),
134
+ proposals: sqliteTable(C.proposals.name, {
135
+ id: text(C.proposals.columns.id).primaryKey(),
136
+ workspaceId: text(C.proposals.columns.workspaceId).notNull(),
137
+ threadId: text(C.proposals.columns.threadId),
138
+ type: text(C.proposals.columns.type).notNull(),
139
+ title: text(C.proposals.columns.title).notNull(),
140
+ description: text(C.proposals.columns.description),
141
+ status: text(C.proposals.columns.status).notNull().default("pending"),
142
+ createdBy: text(C.proposals.columns.createdBy),
143
+ createdAt: integer(C.proposals.columns.createdAt).notNull()
144
+ }),
145
+ knowledge: sqliteTable(C.knowledge.name, {
146
+ id: text(C.knowledge.columns.id).primaryKey(),
147
+ workspaceId: text(C.knowledge.columns.workspaceId).notNull(),
148
+ path: text(C.knowledge.columns.path).notNull(),
149
+ kind: text(C.knowledge.columns.kind).notNull(),
150
+ label: text(C.knowledge.columns.label),
151
+ content: text(C.knowledge.columns.content),
152
+ createdAt: integer(C.knowledge.columns.createdAt).notNull()
153
+ }),
154
+ deadlines: sqliteTable(C.deadlines.name, {
155
+ id: text(C.deadlines.columns.id).primaryKey(),
156
+ workspaceId: text(C.deadlines.columns.workspaceId).notNull(),
157
+ threadId: text(C.deadlines.columns.threadId),
158
+ title: text(C.deadlines.columns.title).notNull(),
159
+ dueDate: text(C.deadlines.columns.dueDate).notNull(),
160
+ priority: text(C.deadlines.columns.priority),
161
+ status: text(C.deadlines.columns.status).notNull().default("scheduled"),
162
+ createdAt: integer(C.deadlines.columns.createdAt).notNull()
163
+ }),
164
+ workspaceKeys: sqliteTable(C.workspaceKeys.name, {
165
+ id: text(C.workspaceKeys.columns.id).primaryKey(),
166
+ workspaceId: text(C.workspaceKeys.columns.workspaceId).notNull(),
167
+ keyId: text(C.workspaceKeys.columns.keyId).notNull(),
168
+ keyEncrypted: text(C.workspaceKeys.columns.keyEncrypted).notNull(),
169
+ budgetUsd: real(C.workspaceKeys.columns.budgetUsd).notNull(),
170
+ expiresAt: integer(C.workspaceKeys.columns.expiresAt).notNull(),
171
+ revokedAt: integer(C.workspaceKeys.columns.revokedAt),
172
+ createdAt: integer(C.workspaceKeys.columns.createdAt).notNull()
173
+ })
174
+ };
175
+ }
176
+ function slug(value) {
177
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "item";
178
+ }
179
+ function createPresetToolHandlers(opts) {
180
+ const { db, vault } = opts;
181
+ const newId = opts.newId ?? (() => crypto.randomUUID());
182
+ const now = opts.now ?? (() => Date.now());
183
+ const uiPrefix = opts.uiPathPrefix ?? "ui";
184
+ const citationPrefix = opts.citationPathPrefix ?? "citations";
185
+ const P = PRESET_TABLES.proposals;
186
+ const D = PRESET_TABLES.deadlines;
187
+ const K = PRESET_TABLES.knowledge;
188
+ async function persistArtifact(path, body) {
189
+ await vault.put(path, body);
190
+ }
191
+ async function insertKnowledge(workspaceId, path, kind, label, content) {
192
+ await db.prepare(
193
+ `INSERT INTO ${K.name} (${K.columns.id}, ${K.columns.workspaceId}, ${K.columns.path}, ${K.columns.kind}, ${K.columns.label}, ${K.columns.content}, ${K.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, ?)`
194
+ ).bind(newId(), workspaceId, path, kind, label, content, now()).run();
195
+ }
196
+ return {
197
+ async submitProposal(args, ctx) {
198
+ const existing = await db.prepare(`SELECT ${P.columns.id} AS id FROM ${P.name} WHERE ${P.columns.workspaceId} = ? AND ${P.columns.title} = ? LIMIT 1`).bind(ctx.workspaceId, args.title).first();
199
+ if (existing) return { proposalId: existing.id, deduped: true };
200
+ const id = newId();
201
+ await db.prepare(
202
+ `INSERT INTO ${P.name} (${P.columns.id}, ${P.columns.workspaceId}, ${P.columns.threadId}, ${P.columns.type}, ${P.columns.title}, ${P.columns.description}, ${P.columns.status}, ${P.columns.createdBy}, ${P.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)`
203
+ ).bind(id, ctx.workspaceId, ctx.threadId, args.type, args.title, args.description ?? null, ctx.userId, now()).run();
204
+ return { proposalId: id, deduped: false };
205
+ },
206
+ async scheduleFollowup(args, ctx) {
207
+ const existing = await db.prepare(
208
+ `SELECT ${D.columns.id} AS id, ${D.columns.dueDate} AS dueDate FROM ${D.name} WHERE ${D.columns.workspaceId} = ? AND ${D.columns.title} = ? AND ${D.columns.dueDate} = ? LIMIT 1`
209
+ ).bind(ctx.workspaceId, args.title, args.dueDate).first();
210
+ if (existing) return { id: existing.id, dueDate: existing.dueDate, deduped: true };
211
+ const id = newId();
212
+ await db.prepare(
213
+ `INSERT INTO ${D.name} (${D.columns.id}, ${D.columns.workspaceId}, ${D.columns.threadId}, ${D.columns.title}, ${D.columns.dueDate}, ${D.columns.priority}, ${D.columns.status}, ${D.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'scheduled', ?)`
214
+ ).bind(id, ctx.workspaceId, ctx.threadId, args.title, args.dueDate, args.priority ?? null, now()).run();
215
+ return { id, dueDate: args.dueDate, deduped: false };
216
+ },
217
+ async renderUi(args, ctx) {
218
+ const content = JSON.stringify(args.schema);
219
+ const path = `${uiPrefix}/${ctx.threadId ?? "global"}/${slug(args.title)}.json`;
220
+ await persistArtifact(path, content);
221
+ await insertKnowledge(ctx.workspaceId, path, "ui", args.title, content);
222
+ return { path, content };
223
+ },
224
+ async addCitation(args, ctx) {
225
+ const citationId = newId();
226
+ const path = `${citationPrefix}/${slug(args.label ?? args.path)}-${citationId.slice(0, 8)}.json`;
227
+ const body = JSON.stringify({ sourcePath: args.path, quote: args.quote, label: args.label ?? null });
228
+ await persistArtifact(path, body);
229
+ await insertKnowledge(ctx.workspaceId, path, "citation", args.label ?? null, body);
230
+ return { citationId, path };
231
+ }
232
+ };
233
+ }
234
+ function readDotPath(obj, path) {
235
+ let cur = obj;
236
+ for (const part of path.split(".")) {
237
+ if (cur == null || typeof cur !== "object") return void 0;
238
+ cur = cur[part];
239
+ }
240
+ return cur;
241
+ }
242
+ function createD1KnowledgeStateAccessor(opts) {
243
+ const { db, workspaceId } = opts;
244
+ const defaultWhere = opts.defaultWhereColumn ?? "workspace_id";
245
+ const configFn = typeof opts.config === "function" ? opts.config : (path) => readDotPath(opts.config, path);
246
+ const isIdentifier = (s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
247
+ return {
248
+ config: configFn,
249
+ async count(query) {
250
+ if (!isIdentifier(query.table)) throw new Error(`unsafe table identifier: ${query.table}`);
251
+ const whereCol = query.where ?? defaultWhere;
252
+ if (!isIdentifier(whereCol)) throw new Error(`unsafe where identifier: ${whereCol}`);
253
+ let sql = `SELECT count(*) AS n FROM ${query.table} WHERE ${whereCol} = ?`;
254
+ const binds = [workspaceId];
255
+ if (query.statusIn && query.statusIn.length > 0) {
256
+ sql += ` AND status IN (${query.statusIn.map(() => "?").join(", ")})`;
257
+ binds.push(...query.statusIn);
258
+ }
259
+ const row = await db.prepare(sql).bind(...binds).first();
260
+ return row?.n ?? 0;
261
+ }
262
+ };
263
+ }
264
+ function createPresetFieldCrypto(key) {
265
+ return createFieldCrypto(key);
266
+ }
267
+ function createPresetWorkspaceKeyStore(db) {
268
+ const W = PRESET_TABLES.workspaceKeys;
269
+ return {
270
+ async getActive(workspaceId) {
271
+ const row = await db.prepare(
272
+ `SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId, ${W.columns.keyEncrypted} AS keyEncrypted, ${W.columns.budgetUsd} AS budgetUsd, ${W.columns.expiresAt} AS expiresAt FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL ORDER BY ${W.columns.createdAt} DESC LIMIT 1`
273
+ ).bind(workspaceId).first();
274
+ if (!row) return null;
275
+ return {
276
+ id: row.id,
277
+ keyId: row.keyId,
278
+ keyEncrypted: row.keyEncrypted,
279
+ budgetUsd: row.budgetUsd,
280
+ expiresAt: row.expiresAt == null ? null : new Date(row.expiresAt)
281
+ };
282
+ },
283
+ async listActive(workspaceId) {
284
+ const res = await db.prepare(`SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL`).bind(workspaceId).all();
285
+ return res.results;
286
+ },
287
+ async insert(record) {
288
+ await db.prepare(
289
+ `INSERT INTO ${W.name} (${W.columns.id}, ${W.columns.workspaceId}, ${W.columns.keyId}, ${W.columns.keyEncrypted}, ${W.columns.budgetUsd}, ${W.columns.expiresAt}, ${W.columns.revokedAt}, ${W.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`
290
+ ).bind(crypto.randomUUID(), record.workspaceId, record.keyId, record.keyEncrypted, record.budgetUsd, record.expiresAt.getTime(), Date.now()).run();
291
+ },
292
+ async markRevoked(id, now) {
293
+ await db.prepare(`UPDATE ${W.name} SET ${W.columns.revokedAt} = ? WHERE ${W.columns.id} = ?`).bind(now.getTime(), id).run();
294
+ }
295
+ };
296
+ }
297
+ function createPresetWorkspaceKeyManager(opts) {
298
+ return createWorkspaceKeyManager({
299
+ provisioner: opts.provisioner,
300
+ store: createPresetWorkspaceKeyStore(opts.db),
301
+ crypto: createPresetFieldCrypto(opts.encryptionKey),
302
+ defaultBudgetUsd: opts.defaultBudgetUsd,
303
+ now: opts.now,
304
+ product: opts.product
305
+ });
306
+ }
307
+
308
+ export {
309
+ PRESET_TABLES,
310
+ PRESET_MIGRATION_SQL,
311
+ createPresetDrizzleSchema,
312
+ createPresetToolHandlers,
313
+ createD1KnowledgeStateAccessor,
314
+ createPresetFieldCrypto,
315
+ createPresetWorkspaceKeyStore,
316
+ createPresetWorkspaceKeyManager
317
+ };
318
+ //# sourceMappingURL=chunk-MTJXFHYD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/preset-cloudflare/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/preset-cloudflare` — the batteries-included default\n * stack.\n *\n * Every fleet agent runs the SAME backend: Cloudflare D1 (SQLite) through\n * Drizzle for state, a KV namespace as the artifact vault, AES-GCM field crypto\n * for PII, and per-workspace budget-capped model keys. The other agent-app\n * modules are pure SEAMS — `./tools` needs an `AppToolHandlers`, `./knowledge`\n * needs a `KnowledgeStateAccessor`, `./billing` needs a `WorkspaceKeyStore` +\n * `KeyCrypto`. This module is the ONE implementation of those seams against the\n * house stack, so a consumer that runs D1 + KV stands the whole shell up with\n * config + bindings and ZERO handler code.\n *\n * Layering:\n * - Drizzle is a PEER (the consumer installs `drizzle-orm`, never bundled). The\n * schema is therefore expressed two ways that need no import here: the plain\n * DDL ({@link PRESET_MIGRATION_SQL}) a consumer runs to create the tables, and\n * a {@link createPresetDrizzleSchema} factory that takes the consumer's\n * `drizzle-orm/sqlite-core` builder module and returns the typed tables. The\n * column names in {@link PRESET_TABLES} are the contract the handlers,\n * accessor, and DDL all agree on.\n * - D1 + KV are STRUCTURAL: {@link D1Like} (Cloudflare `D1Database` satisfies it)\n * and `KvLike` from `../web` (Cloudflare `KVNamespace` satisfies it). No\n * `@cloudflare/workers-types` dependency.\n * - Crypto/billing reuse `../crypto` + `../billing` exactly — this only wires\n * them to the D1 key table.\n */\n\nimport { createFieldCrypto } from '../crypto/index'\nimport {\n createWorkspaceKeyManager,\n type KeyCrypto,\n type KeyProvisioner,\n type WorkspaceKeyManager,\n type WorkspaceKeyRecord,\n type WorkspaceKeyStore,\n} from '../billing/index'\nimport type { KnowledgeStateAccessor } from '../knowledge/index'\nimport type {\n AddCitationArgs,\n AddCitationResult,\n AppToolContext,\n AppToolHandlers,\n RenderUiArgs,\n RenderUiResult,\n ScheduleFollowupArgs,\n ScheduleFollowupResult,\n SubmitProposalArgs,\n SubmitProposalResult,\n} from '../tools/index'\nimport type { KvLike } from '../web/index'\n\n// ---------------------------------------------------------------------------\n// D1 structural seam\n//\n// The minimal surface of Cloudflare's `D1Database` the handlers + accessor use.\n// `D1Database` satisfies it structurally, so the consumer passes `env.DB`\n// directly and tests pass an in-memory fake. We keep it to the\n// prepare/bind/first/run/all shape D1 already exposes — no Drizzle here, so the\n// default handlers run on a fresh Worker with only the D1 binding.\n// ---------------------------------------------------------------------------\n\n/** A prepared, bound D1 statement. */\nexport interface D1PreparedLike {\n bind(...values: unknown[]): D1PreparedLike\n first<T = Record<string, unknown>>(colName?: string): Promise<T | null>\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n}\n\n/** The D1 surface the preset needs. Cloudflare `D1Database` satisfies it. */\nexport interface D1Like {\n prepare(query: string): D1PreparedLike\n}\n\n// ---------------------------------------------------------------------------\n// Default schema\n//\n// The four tables the default handlers + accessor read/write. Column names are\n// the single source of truth shared by the DDL, the Drizzle factory, the\n// handlers, and the accessor. Every table is workspace-scoped on `workspace_id`\n// (the accessor's default `where` column) so the knowledge `count` rule and the\n// tool writes agree without per-consumer wiring.\n// ---------------------------------------------------------------------------\n\n/** The preset table + column names — the contract the DDL, Drizzle schema,\n * handlers, and accessor share. Exposed so a consumer can reference a column\n * without a string literal. */\nexport const PRESET_TABLES = {\n proposals: {\n name: 'proposals',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n type: 'type',\n title: 'title',\n description: 'description',\n status: 'status',\n createdBy: 'created_by',\n createdAt: 'created_at',\n },\n },\n threads: {\n name: 'threads',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n title: 'title',\n createdAt: 'created_at',\n },\n },\n knowledge: {\n name: 'knowledge',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n path: 'path',\n kind: 'kind',\n label: 'label',\n content: 'content',\n createdAt: 'created_at',\n },\n },\n deadlines: {\n name: 'deadlines',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n title: 'title',\n dueDate: 'due_date',\n priority: 'priority',\n status: 'status',\n createdAt: 'created_at',\n },\n },\n workspaceKeys: {\n name: 'workspace_keys',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n keyId: 'key_id',\n keyEncrypted: 'key_encrypted',\n budgetUsd: 'budget_usd',\n expiresAt: 'expires_at',\n revokedAt: 'revoked_at',\n createdAt: 'created_at',\n },\n },\n} as const\n\n/**\n * Plain DDL for the preset schema — run by a consumer to create the tables with\n * ZERO drizzle (`for (const sql of PRESET_MIGRATION_SQL) await db.prepare(sql).run()`,\n * or paste into a `.sql` migration). One statement per table so D1's\n * single-statement `prepare` accepts each. Matches {@link PRESET_TABLES} exactly.\n */\nexport const PRESET_MIGRATION_SQL: readonly string[] = [\n `CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n title TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS proposals (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_by TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS knowledge (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n path TEXT NOT NULL,\n kind TEXT NOT NULL,\n label TEXT,\n content TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS deadlines (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n title TEXT NOT NULL,\n due_date TEXT NOT NULL,\n priority TEXT,\n status TEXT NOT NULL DEFAULT 'scheduled',\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS workspace_keys (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n key_id TEXT NOT NULL,\n key_encrypted TEXT NOT NULL,\n budget_usd REAL NOT NULL,\n expires_at INTEGER NOT NULL,\n revoked_at INTEGER,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS idx_proposals_ws ON proposals (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_deadlines_ws ON deadlines (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_knowledge_ws ON knowledge (workspace_id)`,\n `CREATE INDEX IF NOT EXISTS idx_workspace_keys_ws ON workspace_keys (workspace_id, revoked_at)`,\n]\n\n/** A chainable column builder — every modifier returns the builder so calls\n * like `.notNull().default('pending')` typecheck. The concrete drizzle builders\n * satisfy this structurally. */\nexport interface DrizzleColumnLike {\n primaryKey: () => DrizzleColumnLike\n notNull: () => DrizzleColumnLike\n default: (v: unknown) => DrizzleColumnLike\n}\n\n/** The shape of a `drizzle-orm/sqlite-core` module — the few builders the\n * preset schema uses. The consumer passes the real module; agent-app never\n * imports it (it stays a peer). */\nexport interface DrizzleSqliteCoreLike {\n sqliteTable: (name: string, columns: Record<string, DrizzleColumnLike>) => unknown\n text: (name?: string) => DrizzleColumnLike\n integer: (name?: string, config?: unknown) => DrizzleColumnLike\n real: (name?: string) => DrizzleColumnLike\n}\n\n/**\n * Build the typed Drizzle schema for the preset, given the consumer's\n * `drizzle-orm/sqlite-core` module. Returns one table object per\n * {@link PRESET_TABLES} entry — pass to `drizzle(db, { schema })` for typed\n * queries, or to drizzle-kit for migration generation. agent-app never imports\n * drizzle; the builder module is the seam.\n *\n * ```ts\n * import * as d from 'drizzle-orm/sqlite-core'\n * const schema = createPresetDrizzleSchema(d)\n * ```\n */\nexport function createPresetDrizzleSchema(d: DrizzleSqliteCoreLike) {\n const { sqliteTable, text, integer, real } = d\n const C = PRESET_TABLES\n return {\n threads: sqliteTable(C.threads.name, {\n id: text(C.threads.columns.id).primaryKey(),\n workspaceId: text(C.threads.columns.workspaceId).notNull(),\n title: text(C.threads.columns.title),\n createdAt: integer(C.threads.columns.createdAt).notNull(),\n }),\n proposals: sqliteTable(C.proposals.name, {\n id: text(C.proposals.columns.id).primaryKey(),\n workspaceId: text(C.proposals.columns.workspaceId).notNull(),\n threadId: text(C.proposals.columns.threadId),\n type: text(C.proposals.columns.type).notNull(),\n title: text(C.proposals.columns.title).notNull(),\n description: text(C.proposals.columns.description),\n status: text(C.proposals.columns.status).notNull().default('pending'),\n createdBy: text(C.proposals.columns.createdBy),\n createdAt: integer(C.proposals.columns.createdAt).notNull(),\n }),\n knowledge: sqliteTable(C.knowledge.name, {\n id: text(C.knowledge.columns.id).primaryKey(),\n workspaceId: text(C.knowledge.columns.workspaceId).notNull(),\n path: text(C.knowledge.columns.path).notNull(),\n kind: text(C.knowledge.columns.kind).notNull(),\n label: text(C.knowledge.columns.label),\n content: text(C.knowledge.columns.content),\n createdAt: integer(C.knowledge.columns.createdAt).notNull(),\n }),\n deadlines: sqliteTable(C.deadlines.name, {\n id: text(C.deadlines.columns.id).primaryKey(),\n workspaceId: text(C.deadlines.columns.workspaceId).notNull(),\n threadId: text(C.deadlines.columns.threadId),\n title: text(C.deadlines.columns.title).notNull(),\n dueDate: text(C.deadlines.columns.dueDate).notNull(),\n priority: text(C.deadlines.columns.priority),\n status: text(C.deadlines.columns.status).notNull().default('scheduled'),\n createdAt: integer(C.deadlines.columns.createdAt).notNull(),\n }),\n workspaceKeys: sqliteTable(C.workspaceKeys.name, {\n id: text(C.workspaceKeys.columns.id).primaryKey(),\n workspaceId: text(C.workspaceKeys.columns.workspaceId).notNull(),\n keyId: text(C.workspaceKeys.columns.keyId).notNull(),\n keyEncrypted: text(C.workspaceKeys.columns.keyEncrypted).notNull(),\n budgetUsd: real(C.workspaceKeys.columns.budgetUsd).notNull(),\n expiresAt: integer(C.workspaceKeys.columns.expiresAt).notNull(),\n revokedAt: integer(C.workspaceKeys.columns.revokedAt),\n createdAt: integer(C.workspaceKeys.columns.createdAt).notNull(),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Default AppToolHandlers over D1 + KV\n// ---------------------------------------------------------------------------\n\n/** The KV-backed vault. `KvLike` (from `../web`) is the structural KV contract;\n * Cloudflare `KVNamespace` satisfies it. Artifacts are stored under their path. */\nexport type VaultKv = KvLike\n\nexport interface PresetToolHandlerOptions {\n /** The D1 database (Cloudflare `D1Database` satisfies {@link D1Like}). */\n db: D1Like\n /** The KV namespace used as the artifact vault. */\n vault: VaultKv\n /** Id generator. Default `crypto.randomUUID`. Injectable for deterministic tests. */\n newId?: () => string\n /** Clock (epoch ms). Default `Date.now`. Injectable for deterministic tests. */\n now?: () => number\n /** Vault path prefix for `render_ui` artifacts. Default `'ui'`. */\n uiPathPrefix?: string\n /** Vault path prefix for `add_citation` artifacts. Default `'citations'`. */\n citationPathPrefix?: string\n}\n\nfunction slug(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 64) || 'item'\n}\n\n/**\n * The default {@link AppToolHandlers} for the house stack:\n * - `submit_proposal` → insert a `proposals` row (`status='pending'`), deduped\n * on (workspace, title) so a retried turn doesn't double-queue.\n * - `schedule_followup` → insert a `deadlines` row, deduped on (workspace, title, due_date).\n * - `render_ui` → write the schema JSON as a `ui/<thread>/<slug>.json`\n * vault artifact AND a `knowledge` row pointing at it.\n * - `add_citation` → write the quote as a `citations/<slug>.json` artifact AND\n * a `knowledge` row.\n *\n * Returns the EXACT persisted content from `render_ui` (per the seam contract) so\n * a completion oracle sees real bytes. Pure seam wiring: a consumer that runs\n * D1 + KV gets all four tools with no handler code.\n */\nexport function createPresetToolHandlers(opts: PresetToolHandlerOptions): AppToolHandlers {\n const { db, vault } = opts\n const newId = opts.newId ?? (() => crypto.randomUUID())\n const now = opts.now ?? (() => Date.now())\n const uiPrefix = opts.uiPathPrefix ?? 'ui'\n const citationPrefix = opts.citationPathPrefix ?? 'citations'\n const P = PRESET_TABLES.proposals\n const D = PRESET_TABLES.deadlines\n const K = PRESET_TABLES.knowledge\n\n async function persistArtifact(path: string, body: string): Promise<void> {\n await vault.put(path, body)\n }\n\n async function insertKnowledge(workspaceId: string, path: string, kind: string, label: string | null, content: string): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${K.name} (${K.columns.id}, ${K.columns.workspaceId}, ${K.columns.path}, ${K.columns.kind}, ${K.columns.label}, ${K.columns.content}, ${K.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(newId(), workspaceId, path, kind, label, content, now())\n .run()\n }\n\n return {\n async submitProposal(args: SubmitProposalArgs, ctx: AppToolContext): Promise<SubmitProposalResult> {\n const existing = await db\n .prepare(`SELECT ${P.columns.id} AS id FROM ${P.name} WHERE ${P.columns.workspaceId} = ? AND ${P.columns.title} = ? LIMIT 1`)\n .bind(ctx.workspaceId, args.title)\n .first<{ id: string }>()\n if (existing) return { proposalId: existing.id, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${P.name} (${P.columns.id}, ${P.columns.workspaceId}, ${P.columns.threadId}, ${P.columns.type}, ${P.columns.title}, ${P.columns.description}, ${P.columns.status}, ${P.columns.createdBy}, ${P.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.type, args.title, args.description ?? null, ctx.userId, now())\n .run()\n return { proposalId: id, deduped: false }\n },\n\n async scheduleFollowup(args: ScheduleFollowupArgs, ctx: AppToolContext): Promise<ScheduleFollowupResult> {\n const existing = await db\n .prepare(\n `SELECT ${D.columns.id} AS id, ${D.columns.dueDate} AS dueDate FROM ${D.name} WHERE ${D.columns.workspaceId} = ? AND ${D.columns.title} = ? AND ${D.columns.dueDate} = ? LIMIT 1`,\n )\n .bind(ctx.workspaceId, args.title, args.dueDate)\n .first<{ id: string; dueDate: string }>()\n if (existing) return { id: existing.id, dueDate: existing.dueDate, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${D.name} (${D.columns.id}, ${D.columns.workspaceId}, ${D.columns.threadId}, ${D.columns.title}, ${D.columns.dueDate}, ${D.columns.priority}, ${D.columns.status}, ${D.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'scheduled', ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.title, args.dueDate, args.priority ?? null, now())\n .run()\n return { id, dueDate: args.dueDate, deduped: false }\n },\n\n async renderUi(args: RenderUiArgs, ctx: AppToolContext): Promise<RenderUiResult> {\n const content = JSON.stringify(args.schema)\n const path = `${uiPrefix}/${ctx.threadId ?? 'global'}/${slug(args.title)}.json`\n await persistArtifact(path, content)\n await insertKnowledge(ctx.workspaceId, path, 'ui', args.title, content)\n return { path, content }\n },\n\n async addCitation(args: AddCitationArgs, ctx: AppToolContext): Promise<AddCitationResult> {\n const citationId = newId()\n const path = `${citationPrefix}/${slug(args.label ?? args.path)}-${citationId.slice(0, 8)}.json`\n const body = JSON.stringify({ sourcePath: args.path, quote: args.quote, label: args.label ?? null })\n await persistArtifact(path, body)\n await insertKnowledge(ctx.workspaceId, path, 'citation', args.label ?? null, body)\n return { citationId, path }\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// D1-backed KnowledgeStateAccessor\n// ---------------------------------------------------------------------------\n\nexport interface PresetKnowledgeAccessorOptions {\n db: D1Like\n /** The active workspace — every `count` is scoped to it. */\n workspaceId: string\n /** Workspace config the `satisfiedBy: { config }` rules read. A resolved\n * object (dot-path lookup), or a function the accessor calls per path. */\n config: Record<string, unknown> | ((path: string) => unknown)\n /** The default workspace fk column a `count` rule scopes on when its rule\n * omits `where`. Default `'workspace_id'` (the preset schema convention). */\n defaultWhereColumn?: string\n}\n\nfunction readDotPath(obj: Record<string, unknown>, path: string): unknown {\n let cur: unknown = obj\n for (const part of path.split('.')) {\n if (cur == null || typeof cur !== 'object') return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * The {@link KnowledgeStateAccessor} over the preset D1 schema — the seam that\n * lets the declarative `satisfiedBy` rules resolve with ZERO consumer code:\n * - `config(path)` reads the supplied workspace config by dot-path.\n * - `count({ table, where, statusIn })` runs `SELECT count(*)` scoped to the\n * active workspace (the rule's `where` column, default `workspace_id`),\n * optionally filtered to `statusIn` via a parameterized `IN (...)`.\n *\n * Identifiers (table/column) are validated against a safe pattern before\n * interpolation — they originate from the product's own config, never model\n * input, but we fail loud rather than build a malformed/injectable query.\n */\nexport function createD1KnowledgeStateAccessor(opts: PresetKnowledgeAccessorOptions): KnowledgeStateAccessor {\n const { db, workspaceId } = opts\n const defaultWhere = opts.defaultWhereColumn ?? 'workspace_id'\n const configFn = typeof opts.config === 'function' ? opts.config : (path: string) => readDotPath(opts.config as Record<string, unknown>, path)\n\n const isIdentifier = (s: string) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)\n\n return {\n config: configFn,\n async count(query) {\n if (!isIdentifier(query.table)) throw new Error(`unsafe table identifier: ${query.table}`)\n const whereCol = query.where ?? defaultWhere\n if (!isIdentifier(whereCol)) throw new Error(`unsafe where identifier: ${whereCol}`)\n\n let sql = `SELECT count(*) AS n FROM ${query.table} WHERE ${whereCol} = ?`\n const binds: unknown[] = [workspaceId]\n if (query.statusIn && query.statusIn.length > 0) {\n sql += ` AND status IN (${query.statusIn.map(() => '?').join(', ')})`\n binds.push(...query.statusIn)\n }\n const row = await db.prepare(sql).bind(...binds).first<{ n: number }>()\n return row?.n ?? 0\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Crypto + per-workspace key wiring\n// ---------------------------------------------------------------------------\n\n/** Build the {@link KeyCrypto} the billing key store uses — AES-256-GCM field\n * crypto bound to the product's 64-char-hex `ENCRYPTION_KEY` (or a resolver).\n * This is the concrete impl behind the `../billing` `KeyCrypto` seam. */\nexport function createPresetFieldCrypto(key: string | (() => string)): KeyCrypto {\n return createFieldCrypto(key)\n}\n\n/**\n * The {@link WorkspaceKeyStore} over the preset `workspace_keys` table — the\n * persistence seam the per-workspace key manager needs. \"Active\" = a row with a\n * null `revoked_at`. Pure D1 wiring; no key minting (that's the provisioner).\n */\nexport function createPresetWorkspaceKeyStore(db: D1Like): WorkspaceKeyStore {\n const W = PRESET_TABLES.workspaceKeys\n return {\n async getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null> {\n const row = await db\n .prepare(\n `SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId, ${W.columns.keyEncrypted} AS keyEncrypted, ${W.columns.budgetUsd} AS budgetUsd, ${W.columns.expiresAt} AS expiresAt FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL ORDER BY ${W.columns.createdAt} DESC LIMIT 1`,\n )\n .bind(workspaceId)\n .first<{ id: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: number | null }>()\n if (!row) return null\n return {\n id: row.id,\n keyId: row.keyId,\n keyEncrypted: row.keyEncrypted,\n budgetUsd: row.budgetUsd,\n expiresAt: row.expiresAt == null ? null : new Date(row.expiresAt),\n }\n },\n async listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>> {\n const res = await db\n .prepare(`SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL`)\n .bind(workspaceId)\n .all<{ id: string; keyId: string }>()\n return res.results\n },\n async insert(record): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${W.name} (${W.columns.id}, ${W.columns.workspaceId}, ${W.columns.keyId}, ${W.columns.keyEncrypted}, ${W.columns.budgetUsd}, ${W.columns.expiresAt}, ${W.columns.revokedAt}, ${W.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,\n )\n .bind(crypto.randomUUID(), record.workspaceId, record.keyId, record.keyEncrypted, record.budgetUsd, record.expiresAt.getTime(), Date.now())\n .run()\n },\n async markRevoked(id: string, now: Date): Promise<void> {\n await db\n .prepare(`UPDATE ${W.name} SET ${W.columns.revokedAt} = ? WHERE ${W.columns.id} = ?`)\n .bind(now.getTime(), id)\n .run()\n },\n }\n}\n\nexport interface PresetBillingOptions {\n db: D1Like\n /** The key provisioner (`@tangle-network/tcloud`'s client satisfies it structurally). */\n provisioner: KeyProvisioner\n /** Field-crypto key (64-char hex) or resolver — encrypts the minted key at rest. */\n encryptionKey: string | (() => string)\n /** Default monthly USD allowance when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/**\n * Stand up the per-workspace budget-capped {@link WorkspaceKeyManager} on the\n * house stack: the preset `workspace_keys` D1 store + AES-GCM field crypto +\n * the consumer's tcloud provisioner. The mint/rotate/rollover/usage LOGIC lives\n * in `../billing`; this only binds it to the preset table + crypto.\n */\nexport function createPresetWorkspaceKeyManager(opts: PresetBillingOptions): WorkspaceKeyManager {\n return createWorkspaceKeyManager({\n provisioner: opts.provisioner,\n store: createPresetWorkspaceKeyStore(opts.db),\n crypto: createPresetFieldCrypto(opts.encryptionKey),\n defaultBudgetUsd: opts.defaultBudgetUsd,\n now: opts.now,\n product: opts.product,\n })\n}\n"],"mappings":"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","names":[]}