@tangle-network/agent-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +59 -0
  2. package/dist/billing/index.d.ts +108 -0
  3. package/dist/billing/index.js +7 -0
  4. package/dist/billing/index.js.map +1 -0
  5. package/dist/chunk-45MYQ3GD.js +62 -0
  6. package/dist/chunk-45MYQ3GD.js.map +1 -0
  7. package/dist/chunk-4NXVI7PW.js +32 -0
  8. package/dist/chunk-4NXVI7PW.js.map +1 -0
  9. package/dist/chunk-7P6VIHI4.js +33 -0
  10. package/dist/chunk-7P6VIHI4.js.map +1 -0
  11. package/dist/chunk-C5CREGT2.js +45 -0
  12. package/dist/chunk-C5CREGT2.js.map +1 -0
  13. package/dist/chunk-CN75FIPT.js +61 -0
  14. package/dist/chunk-CN75FIPT.js.map +1 -0
  15. package/dist/chunk-EDIQ6F55.js +274 -0
  16. package/dist/chunk-EDIQ6F55.js.map +1 -0
  17. package/dist/chunk-FS5OUVRB.js +208 -0
  18. package/dist/chunk-FS5OUVRB.js.map +1 -0
  19. package/dist/chunk-GMFPCCQZ.js +245 -0
  20. package/dist/chunk-GMFPCCQZ.js.map +1 -0
  21. package/dist/chunk-L2TG5DBW.js +74 -0
  22. package/dist/chunk-L2TG5DBW.js.map +1 -0
  23. package/dist/chunk-SIDR6BH3.js +57 -0
  24. package/dist/chunk-SIDR6BH3.js.map +1 -0
  25. package/dist/chunk-YGUNTIT5.js +48 -0
  26. package/dist/chunk-YGUNTIT5.js.map +1 -0
  27. package/dist/crypto/index.d.ts +27 -0
  28. package/dist/crypto/index.js +13 -0
  29. package/dist/crypto/index.js.map +1 -0
  30. package/dist/delegation/index.d.ts +50 -0
  31. package/dist/delegation/index.js +11 -0
  32. package/dist/delegation/index.js.map +1 -0
  33. package/dist/eval/index.d.ts +50 -0
  34. package/dist/eval/index.js +17 -0
  35. package/dist/eval/index.js.map +1 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +149 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integrations/index.d.ts +84 -0
  40. package/dist/integrations/index.js +11 -0
  41. package/dist/integrations/index.js.map +1 -0
  42. package/dist/redact/index.d.ts +22 -0
  43. package/dist/redact/index.js +7 -0
  44. package/dist/redact/index.js.map +1 -0
  45. package/dist/runtime/index.d.ts +219 -0
  46. package/dist/runtime/index.js +17 -0
  47. package/dist/runtime/index.js.map +1 -0
  48. package/dist/stream/index.d.ts +39 -0
  49. package/dist/stream/index.js +35 -0
  50. package/dist/stream/index.js.map +1 -0
  51. package/dist/tangle/index.d.ts +93 -0
  52. package/dist/tangle/index.js +9 -0
  53. package/dist/tangle/index.js.map +1 -0
  54. package/dist/tools/index.d.ts +213 -0
  55. package/dist/tools/index.js +37 -0
  56. package/dist/tools/index.js.map +1 -0
  57. package/dist/types-CeWor4bQ.d.ts +120 -0
  58. package/dist/web/index.d.ts +51 -0
  59. package/dist/web/index.js +15 -0
  60. package/dist/web/index.js.map +1 -0
  61. package/package.json +101 -0
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @tangle-network/agent-app
2
+
3
+ Shared **application-shell framework** for Tangle agent products (insurance, tax, legal, creative, gtm, agent-builder). The substrate packages (`@tangle-network/{sandbox, agent-runtime, agent-eval, agent-integrations, agent-knowledge, tcloud}`) are the *engine*; this package is the *shell* — the opinionated, reusable application layer those products currently fork-duplicate.
4
+
5
+ The goal: a product should `pnpm add @tangle-network/agent-app`, supply its domain seams (schema, prompt, taxonomy, persistence), and get the whole shell — instead of copy-forking another agent app and inheriting its bugs (the way insurance forked legal and inherited legal's IRS/FinCEN filing scripts).
6
+
7
+ Everything here is **domain-seamed**: the generic mechanism lives in the package; each product supplies callbacks/config for the domain-specific bits. The package imports no product code.
8
+
9
+ ## Modules
10
+
11
+ | Subpath | Status | What it is |
12
+ |---|---|---|
13
+ | `@tangle-network/agent-app/tools` | ✅ **shipped + tested** | The structured agent→app tool side channel — `submit_proposal` (approval-gated), `schedule_followup`, `render_ui`, `add_citation`. OpenAI tool defs, MCP-server builder, HTTP route handler, agent-runtime executor, capability auth. Replaces brittle fenced `:::` blocks with validated tool calls. Seam: `AppToolHandlers` + `AppToolTaxonomy`. |
14
+ | `@tangle-network/agent-app/delegation` | ✅ **shipped + tested** | The agent-runtime "driven loop" MCP (`delegate_research` / `delegate_code` / `delegation_status` …) for multi-step work that runs to completion in its own agent-driver sandbox. Optional; opt in by spreading into the profile `mcp` map. |
15
+ | `@tangle-network/agent-app/tangle` | ✅ **shipped + tested** | Tangle login (SSO) + the developer self-service **app-registration → broker-token** flow: `buildConsentUrl` (one-time user consent) + `createBrokerTokenProvider` (caches/auto-refreshes the `sk-tan-broker-` token per durable grant, shares in-flight mints). Structural (depends on the minter contract; pass the concrete `TangleAppsClient` from `@tangle-network/agent-integrations`). |
16
+ | `@tangle-network/agent-app/runtime` | ✅ **shipped + tested** | `runAppToolLoop` — the bounded multi-turn tool loop every app's chat runtime hand-rolls: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free via a `streamTurn` seam (wrap any backend / `runAgentTaskStream`) + an `executeToolCall` seam (route to integration + app-tool executors). |
17
+ | `@tangle-network/agent-app/eval` | ✅ **shipped + tested** | The inline completion gate: `producedFromToolEvents` (bridge `/tools` produced events), `verifyCompletion` (per-requirement `satisfiedBy` gate), `tokenRecallChecker` (deterministic content check), `weightedScore`. For full campaigns/traces/LLM-judge use `@tangle-network/agent-eval`; this composes with it. |
18
+
19
+ ✅ = built, typechecked, unit-tested, builds. All five modules done — 39 tests.
20
+
21
+ ## `/tools` usage (the shipped module)
22
+
23
+ A product supplies its taxonomy + handlers (its real DB/vault ops), then wires the three surfaces:
24
+
25
+ ```ts
26
+ import {
27
+ buildAppToolOpenAITools, createAppToolRuntimeExecutor, handleAppToolRequest,
28
+ buildAppToolMcpServer, type AppToolHandlers, type AppToolTaxonomy,
29
+ } from '@tangle-network/agent-app/tools'
30
+
31
+ const taxonomy: AppToolTaxonomy = { proposalTypes: [...], regulatedTypes: [...] }
32
+ const handlers: AppToolHandlers = { submitProposal, scheduleFollowup, renderUi, addCitation } // your DB ops
33
+
34
+ // 1. Sandbox MCP path — one route file per tool:
35
+ export const action = ({ request }) =>
36
+ handleAppToolRequest(request, { tool: 'submit_proposal', handlers, taxonomy, verifyToken })
37
+
38
+ // 2. Per-turn MCP servers (spread into the agent profile's mcp map):
39
+ const mcp = { submit_proposal: buildAppToolMcpServer({ tool: 'submit_proposal', baseUrl, token, ctx, description }) /* … */ }
40
+
41
+ // 3. agent-runtime chat path (eval / non-sandbox) — advertise tools + execute:
42
+ runChatThroughRuntime({ /* … */ backend: makeBackend({ tools: buildAppToolOpenAITools(taxonomy) }),
43
+ appToolExecutor: createAppToolRuntimeExecutor({ handlers, taxonomy, ctx, onProduced }) })
44
+ ```
45
+
46
+ `insurance-agent` is the reference consumer; its `src/lib/.server/tools/*` is being refactored to delegate here.
47
+
48
+ ## Why this exists
49
+
50
+ Each agent app re-implements the same plumbing (chat pipeline, approval queue, the structured side channel, vault, auth/RBAC, eval scaffold). That fork-duplication is why a single change — e.g. migrating the human-in-the-loop gate from fenced `:::proposal` blocks to validated tool calls — has to be redone in five apps. Lifting the shell here makes it a one-place change, propagated by a version bump.
51
+
52
+ ## Develop
53
+
54
+ ```bash
55
+ pnpm install
56
+ pnpm typecheck && pnpm test && pnpm build
57
+ ```
58
+
59
+ Build: tsup (ESM + d.ts). Tests: vitest. No upward deps on any product.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.
3
+ *
4
+ * Each workspace (the paying entity) runs the agent on its OWN child API key
5
+ * minted from the platform parent key. The child carries a hard USD budget the
6
+ * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,
7
+ * zero app-side accounting. The app charges its own subscription (e.g. 5× the
8
+ * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the
9
+ * platform, so a new budget = a fresh key + revoke the prior (rotate).
10
+ *
11
+ * The mint / rotate / rollover / usage LOGIC is generic and lives here.
12
+ * Persistence (which D1 table), secret encryption, and key provisioning are
13
+ * SEAMS each product supplies — so this module imports no DB and no key-mgmt
14
+ * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`
15
+ * SDK is the provisioner a product passes in; it is not a dependency here.
16
+ */
17
+ /** The key-provisioning operations this needs — the `@tangle-network/tcloud`
18
+ * SDK's `TCloudClient` satisfies it structurally; pass it in. */
19
+ interface KeyProvisioner {
20
+ createKey(input: {
21
+ name: string;
22
+ product: string;
23
+ budgetUsd: number;
24
+ expiresAt: string;
25
+ }): Promise<{
26
+ id?: string;
27
+ key?: string;
28
+ }>;
29
+ revokeKey(keyId: string): Promise<unknown>;
30
+ getKey(keyId: string): Promise<{
31
+ budgetUsd?: number;
32
+ budgetSpent?: number;
33
+ expiresAt?: string | null;
34
+ }>;
35
+ }
36
+ /** A stored child-key record (the app's row, shape-normalized). */
37
+ interface WorkspaceKeyRecord {
38
+ /** App row id (opaque). */
39
+ id: string;
40
+ keyId: string;
41
+ /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */
42
+ keyEncrypted: string;
43
+ budgetUsd: number;
44
+ expiresAt: Date | null;
45
+ }
46
+ /** Persistence seam — the product implements this against its own D1 table. */
47
+ interface WorkspaceKeyStore {
48
+ /** Most-recent active key for the workspace, or null. */
49
+ getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>;
50
+ /** All active keys (to revoke priors on rotate). */
51
+ listActive(workspaceId: string): Promise<Array<{
52
+ id: string;
53
+ keyId: string;
54
+ }>>;
55
+ /** Persist a freshly minted active key. */
56
+ insert(record: {
57
+ workspaceId: string;
58
+ keyId: string;
59
+ keyEncrypted: string;
60
+ budgetUsd: number;
61
+ expiresAt: Date;
62
+ }): Promise<void>;
63
+ /** Mark a prior row revoked. */
64
+ markRevoked(id: string, now: Date): Promise<void>;
65
+ }
66
+ /** Secret encryption seam (the app's at-rest crypto). */
67
+ interface KeyCrypto {
68
+ encrypt(secret: string): Promise<string>;
69
+ decrypt(encrypted: string): Promise<string>;
70
+ }
71
+ interface WorkspaceKeyManagerOptions {
72
+ provisioner: KeyProvisioner;
73
+ store: WorkspaceKeyStore;
74
+ crypto: KeyCrypto;
75
+ /** Default monthly allowance (USD) when a call doesn't specify one. */
76
+ defaultBudgetUsd: number;
77
+ /** Injectable clock. Default `() => new Date()`. */
78
+ now?: () => Date;
79
+ /** tcloud product the key is scoped to. Default `'router'`. */
80
+ product?: string;
81
+ }
82
+ interface WorkspaceModelKeyUsage {
83
+ keyId: string;
84
+ budgetUsd: number;
85
+ budgetSpent: number;
86
+ budgetRemaining: number;
87
+ expiresAt: string | null;
88
+ exhausted: boolean;
89
+ }
90
+ interface WorkspaceKeyManager {
91
+ /** The workspace's active child-key secret, provisioning one if absent/expired. */
92
+ ensureKey(workspaceId: string, opts?: {
93
+ budgetUsd?: number;
94
+ }): Promise<string>;
95
+ /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`
96
+ * carries the prior key's unused budget into the new one, bounded by
97
+ * `rolloverCapUsd`. Returns the new secret. */
98
+ rotateKey(workspaceId: string, opts?: {
99
+ budgetUsd?: number;
100
+ rollover?: boolean;
101
+ rolloverCapUsd?: number;
102
+ }): Promise<string>;
103
+ /** Live budget usage for the active key (drives the "$X of $Y used" panel). */
104
+ getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>;
105
+ }
106
+ declare function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager;
107
+
108
+ export { type KeyCrypto, type KeyProvisioner, type WorkspaceKeyManager, type WorkspaceKeyManagerOptions, type WorkspaceKeyRecord, type WorkspaceKeyStore, type WorkspaceModelKeyUsage, createWorkspaceKeyManager };
@@ -0,0 +1,7 @@
1
+ import {
2
+ createWorkspaceKeyManager
3
+ } from "../chunk-45MYQ3GD.js";
4
+ export {
5
+ createWorkspaceKeyManager
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,62 @@
1
+ // src/billing/index.ts
2
+ function nextPeriodEnd(now) {
3
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0));
4
+ }
5
+ function createWorkspaceKeyManager(opts) {
6
+ const clock = opts.now ?? (() => /* @__PURE__ */ new Date());
7
+ const product = opts.product ?? "router";
8
+ const getUsage = async (workspaceId) => {
9
+ const active = await opts.store.getActive(workspaceId);
10
+ if (!active) return null;
11
+ const info = await opts.provisioner.getKey(active.keyId);
12
+ const budgetUsd = info.budgetUsd ?? active.budgetUsd;
13
+ const budgetSpent = info.budgetSpent ?? 0;
14
+ const budgetRemaining = Math.max(0, budgetUsd - budgetSpent);
15
+ return {
16
+ keyId: active.keyId,
17
+ budgetUsd,
18
+ budgetSpent,
19
+ budgetRemaining,
20
+ expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),
21
+ exhausted: budgetRemaining <= 0
22
+ };
23
+ };
24
+ const rotateKey = async (workspaceId, ropts) => {
25
+ const now = clock();
26
+ const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd;
27
+ let budgetUsd = allowance;
28
+ if (ropts?.rollover) {
29
+ const prior = await getUsage(workspaceId).catch(() => null);
30
+ budgetUsd = allowance + (prior?.budgetRemaining ?? 0);
31
+ if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd);
32
+ }
33
+ const expiresAt = nextPeriodEnd(now);
34
+ const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() });
35
+ if (!created.key || !created.id) throw new Error("tcloud createKey returned no key");
36
+ const keyEncrypted = await opts.crypto.encrypt(created.key);
37
+ const priors = await opts.store.listActive(workspaceId);
38
+ await opts.store.insert({ workspaceId, keyId: created.id, keyEncrypted, budgetUsd, expiresAt });
39
+ for (const p of priors) {
40
+ await opts.store.markRevoked(p.id, now);
41
+ try {
42
+ await opts.provisioner.revokeKey(p.keyId);
43
+ } catch {
44
+ }
45
+ }
46
+ return created.key;
47
+ };
48
+ const ensureKey = async (workspaceId, eopts) => {
49
+ const now = clock();
50
+ const active = await opts.store.getActive(workspaceId);
51
+ if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {
52
+ return opts.crypto.decrypt(active.keyEncrypted);
53
+ }
54
+ return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd });
55
+ };
56
+ return { ensureKey, rotateKey, getUsage };
57
+ }
58
+
59
+ export {
60
+ createWorkspaceKeyManager
61
+ };
62
+ //# sourceMappingURL=chunk-45MYQ3GD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/billing/index.ts"],"sourcesContent":["/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations this needs — the `@tangle-network/tcloud`\n * SDK's `TCloudClient` satisfies it structurally; pass it in. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const keyEncrypted = await opts.crypto.encrypt(created.key)\n\n const priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: created.id, keyEncrypted, budgetUsd, expiresAt })\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n"],"mappings":";AAwFA,SAAS,cAAc,KAAiB;AACtC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AACtF;AAEO,SAAS,0BAA0B,MAAuD;AAC/F,QAAM,QAAQ,KAAK,QAAQ,MAAM,oBAAI,KAAK;AAC1C,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,WAA4C,OAAO,gBAAgB;AACvE,UAAM,SAAS,MAAM,KAAK,MAAM,UAAU,WAAW;AACrD,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,MAAM,KAAK,YAAY,OAAO,OAAO,KAAK;AACvD,UAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,UAAM,cAAc,KAAK,eAAe;AACxC,UAAM,kBAAkB,KAAK,IAAI,GAAG,YAAY,WAAW;AAC3D,WAAO;AAAA,MACL,OAAO,OAAO;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,cAAc,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,MAClF,WAAW,mBAAmB;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,YAA8C,OAAO,aAAa,UAAU;AAChF,UAAM,MAAM,MAAM;AAClB,UAAM,YAAY,OAAO,aAAa,KAAK;AAE3C,QAAI,YAAY;AAChB,QAAI,OAAO,UAAU;AACnB,YAAM,QAAQ,MAAM,SAAS,WAAW,EAAE,MAAM,MAAM,IAAI;AAC1D,kBAAY,aAAa,OAAO,mBAAmB;AACnD,UAAI,MAAM,kBAAkB,KAAM,aAAY,KAAK,IAAI,WAAW,MAAM,cAAc;AAAA,IACxF;AAEA,UAAM,YAAY,cAAc,GAAG;AACnC,UAAM,UAAU,MAAM,KAAK,YAAY,UAAU,EAAE,MAAM,MAAM,WAAW,IAAI,SAAS,WAAW,WAAW,UAAU,YAAY,EAAE,CAAC;AACtI,QAAI,CAAC,QAAQ,OAAO,CAAC,QAAQ,GAAI,OAAM,IAAI,MAAM,kCAAkC;AACnF,UAAM,eAAe,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAE1D,UAAM,SAAS,MAAM,KAAK,MAAM,WAAW,WAAW;AACtD,UAAM,KAAK,MAAM,OAAO,EAAE,aAAa,OAAO,QAAQ,IAAI,cAAc,WAAW,UAAU,CAAC;AAC9F,eAAW,KAAK,QAAQ;AACtB,YAAM,KAAK,MAAM,YAAY,EAAE,IAAI,GAAG;AAGtC,UAAI;AACF,cAAM,KAAK,YAAY,UAAU,EAAE,KAAK;AAAA,MAC1C,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,QAAQ;AAAA,EACjB;AAEA,QAAM,YAA8C,OAAO,aAAa,UAAU;AAChF,UAAM,MAAM,MAAM;AAClB,UAAM,SAAS,MAAM,KAAK,MAAM,UAAU,WAAW;AACrD,QAAI,WAAW,CAAC,OAAO,aAAa,OAAO,UAAU,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAC/E,aAAO,KAAK,OAAO,QAAQ,OAAO,YAAY;AAAA,IAChD;AACA,WAAO,UAAU,aAAa,EAAE,WAAW,OAAO,UAAU,CAAC;AAAA,EAC/D;AAEA,SAAO,EAAE,WAAW,WAAW,SAAS;AAC1C;","names":[]}
@@ -0,0 +1,32 @@
1
+ // src/eval/index.ts
2
+ import { verifyCompletion, extractProducedState, weightedComposite, createLlmCorrectnessChecker } from "@tangle-network/agent-eval";
3
+ function producedFromToolEvents(events) {
4
+ return events.map(
5
+ (e) => e.type === "proposal_created" ? { type: "proposal_created", proposalId: e.proposalId, title: e.title, status: e.status } : { type: "artifact", artifactId: `vault:${e.path}`, name: e.path, uri: `vault://${e.path}`, mimeType: "text/markdown", content: e.content }
6
+ );
7
+ }
8
+ var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "to", "of", "in", "on", "with", "review", "update", "new", "proposed"]);
9
+ function createTokenRecallChecker(opts = {}) {
10
+ const minRecall = opts.minRecall ?? 0.5;
11
+ const minLen = opts.minContentLength ?? 120;
12
+ return async (requirement, content) => {
13
+ const body = content.trim();
14
+ if (body.length < minLen) return { correct: false, reason: `content too thin (${body.length} chars) to be the deliverable` };
15
+ const tokens = requirement.title.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
16
+ if (tokens.length === 0) return { correct: true, reason: "requirement title has no significant tokens \u2014 structural match accepted" };
17
+ const lower = body.toLowerCase();
18
+ const hits = tokens.filter((t) => lower.includes(t)).length;
19
+ const recall = hits / tokens.length;
20
+ return recall >= minRecall ? { correct: true, reason: `content recalls ${hits}/${tokens.length} requirement tokens` } : { correct: false, reason: `content recalls only ${hits}/${tokens.length} requirement tokens` };
21
+ };
22
+ }
23
+
24
+ export {
25
+ producedFromToolEvents,
26
+ createTokenRecallChecker,
27
+ verifyCompletion,
28
+ extractProducedState,
29
+ weightedComposite,
30
+ createLlmCorrectnessChecker
31
+ };
32
+ //# sourceMappingURL=chunk-4NXVI7PW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/eval/index.ts"],"sourcesContent":["/**\n * Eval — the app-shell BRIDGE to `@tangle-network/agent-eval`, not a reimpl.\n *\n * The completion/scoring ENGINE lives in agent-eval (a peer dependency):\n * `verifyCompletion`, `extractProducedState`, `weightedComposite`,\n * `createLlmCorrectnessChecker`, and the `CompletionRequirement` / `TaskGold` /\n * `ProducedState` types — all re-exported here so a consumer has one import\n * root. This module adds only what agent-eval doesn't have and what is\n * app-shell-specific:\n *\n * 1. {@link producedFromToolEvents} — the bridge: turn the structured app-tool\n * side channel's `AppToolProducedEvent`s (from a tool runtime executor's\n * `onProduced`) into the `RuntimeEventLike`s agent-eval's\n * `extractProducedState` consumes. This is the one piece that knows about\n * the app-tool channel, so it belongs here, not in the engine.\n * 2. {@link createTokenRecallChecker} — a deterministic, no-LLM\n * `CorrectnessChecker` (agent-eval ships only the LLM one). For apps/tests\n * that gate completion without a judge call.\n *\n * Full campaigns (persona simulation, traces, scorecards, held-out gates) are\n * agent-eval's `runEvalCampaign` / `AgentDriver` / `BenchmarkRunner` — use them\n * directly; this module composes with them.\n */\nimport type { RuntimeEventLike, CompletionRequirement } from '@tangle-network/agent-eval'\nimport type { AppToolProducedEvent } from '../tools/types'\n\n// Re-export the engine so consumers import completion + scoring from one place.\nexport { verifyCompletion, extractProducedState, weightedComposite, createLlmCorrectnessChecker } from '@tangle-network/agent-eval'\nexport type {\n CompletionRequirement,\n TaskGold,\n ProducedState,\n SatisfiedBy,\n CompletionVerdict,\n CorrectnessChecker,\n RuntimeEventLike,\n} from '@tangle-network/agent-eval'\n\n/**\n * Bridge the app-tool side channel's produced events into the runtime-event\n * shape agent-eval's `extractProducedState` reads. Pipe it:\n * `verifyCompletion(taskGold, extractProducedState(producedFromToolEvents(events)), checker)`\n */\nexport function producedFromToolEvents(events: readonly AppToolProducedEvent[]): RuntimeEventLike[] {\n return events.map((e) =>\n e.type === 'proposal_created'\n ? { type: 'proposal_created', proposalId: e.proposalId, title: e.title, status: e.status }\n : { type: 'artifact', artifactId: `vault:${e.path}`, name: e.path, uri: `vault://${e.path}`, mimeType: 'text/markdown', content: e.content },\n )\n}\n\nconst STOPWORDS = new Set(['the', 'a', 'an', 'and', 'or', 'for', 'to', 'of', 'in', 'on', 'with', 'review', 'update', 'new', 'proposed'])\n\n/**\n * A deterministic `CorrectnessChecker` (agent-eval exports only\n * `createLlmCorrectnessChecker`). A produced item fulfils a requirement when\n * its content is substantive and recalls ≥ `minRecall` of the requirement\n * title's significant tokens. No network — the default gate for apps/tests\n * without an LLM judge. Pass to `verifyCompletion` as the checker.\n */\nexport function createTokenRecallChecker(opts: { minRecall?: number; minContentLength?: number } = {}): (\n requirement: CompletionRequirement,\n content: string,\n) => Promise<{ correct: boolean; reason: string }> {\n const minRecall = opts.minRecall ?? 0.5\n const minLen = opts.minContentLength ?? 120\n return async (requirement, content) => {\n const body = content.trim()\n if (body.length < minLen) return { correct: false, reason: `content too thin (${body.length} chars) to be the deliverable` }\n const tokens = requirement.title.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t))\n if (tokens.length === 0) return { correct: true, reason: 'requirement title has no significant tokens — structural match accepted' }\n const lower = body.toLowerCase()\n const hits = tokens.filter((t) => lower.includes(t)).length\n const recall = hits / tokens.length\n return recall >= minRecall\n ? { correct: true, reason: `content recalls ${hits}/${tokens.length} requirement tokens` }\n : { correct: false, reason: `content recalls only ${hits}/${tokens.length} requirement tokens` }\n }\n}\n"],"mappings":";AA2BA,SAAS,kBAAkB,sBAAsB,mBAAmB,mCAAmC;AAgBhG,SAAS,uBAAuB,QAA6D;AAClG,SAAO,OAAO;AAAA,IAAI,CAAC,MACjB,EAAE,SAAS,qBACP,EAAE,MAAM,oBAAoB,YAAY,EAAE,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE,OAAO,IACvF,EAAE,MAAM,YAAY,YAAY,SAAS,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,KAAK,WAAW,EAAE,IAAI,IAAI,UAAU,iBAAiB,SAAS,EAAE,QAAQ;AAAA,EAC/I;AACF;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,OAAO,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,QAAQ,UAAU,UAAU,OAAO,UAAU,CAAC;AAShI,SAAS,yBAAyB,OAA0D,CAAC,GAGjD;AACjD,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,SAAS,KAAK,oBAAoB;AACxC,SAAO,OAAO,aAAa,YAAY;AACrC,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,KAAK,SAAS,OAAQ,QAAO,EAAE,SAAS,OAAO,QAAQ,qBAAqB,KAAK,MAAM,gCAAgC;AAC3H,UAAM,SAAS,YAAY,MAAM,YAAY,EAAE,MAAM,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;AAClH,QAAI,OAAO,WAAW,EAAG,QAAO,EAAE,SAAS,MAAM,QAAQ,+EAA0E;AACnI,UAAM,QAAQ,KAAK,YAAY;AAC/B,UAAM,OAAO,OAAO,OAAO,CAAC,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE;AACrD,UAAM,SAAS,OAAO,OAAO;AAC7B,WAAO,UAAU,YACb,EAAE,SAAS,MAAM,QAAQ,mBAAmB,IAAI,IAAI,OAAO,MAAM,sBAAsB,IACvF,EAAE,SAAS,OAAO,QAAQ,wBAAwB,IAAI,IAAI,OAAO,MAAM,sBAAsB;AAAA,EACnG;AACF;","names":[]}
@@ -0,0 +1,33 @@
1
+ // src/delegation/index.ts
2
+ var DELEGATION_MCP_SERVER_KEY = "agent-runtime-delegation";
3
+ var DELEGATION_TOOLS = [
4
+ "delegate_code",
5
+ "delegate_research",
6
+ "delegate_feedback",
7
+ "delegation_status",
8
+ "delegation_history"
9
+ ];
10
+ function buildDelegationMcpServer(opts) {
11
+ if (!opts.apiKey) return void 0;
12
+ const env = { TANGLE_API_KEY: opts.apiKey };
13
+ const forward = opts.forwardEnv ?? {};
14
+ for (const key of ["SANDBOX_BASE_URL", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "TRACE_ID", "PARENT_SPAN_ID"]) {
15
+ const value = forward[key];
16
+ if (value) env[key] = value;
17
+ }
18
+ return {
19
+ transport: "stdio",
20
+ command: "npx",
21
+ args: ["-y", opts.packageSpec ?? "@tangle-network/agent-runtime", "mcp"],
22
+ env,
23
+ enabled: true,
24
+ metadata: { surface: "delegation:dispatch", tools: DELEGATION_TOOLS }
25
+ };
26
+ }
27
+
28
+ export {
29
+ DELEGATION_MCP_SERVER_KEY,
30
+ DELEGATION_TOOLS,
31
+ buildDelegationMcpServer
32
+ };
33
+ //# sourceMappingURL=chunk-7P6VIHI4.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,45 @@
1
+ // src/redact/index.ts
2
+ var SSN_PATTERN = /\d{3}-\d{2}-\d{4}/;
3
+ var EIN_PATTERN = /\d{2}-\d{7}/;
4
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
5
+ "ssn",
6
+ "ein",
7
+ "password",
8
+ "apikey",
9
+ "token",
10
+ "secret",
11
+ "authorization",
12
+ "email",
13
+ "phone"
14
+ ]);
15
+ function redactString(value) {
16
+ if (SSN_PATTERN.test(value)) return "[REDACTED:ssn]";
17
+ if (EIN_PATTERN.test(value)) return "[REDACTED:ein]";
18
+ return value;
19
+ }
20
+ function isPlainObject(value) {
21
+ if (value === null || typeof value !== "object") return false;
22
+ const proto = Object.getPrototypeOf(value);
23
+ return proto === Object.prototype || proto === null;
24
+ }
25
+ function redactForIngestion(value) {
26
+ if (typeof value === "string") return redactString(value);
27
+ if (Array.isArray(value)) return value.map(redactForIngestion);
28
+ if (isPlainObject(value)) {
29
+ const out = {};
30
+ for (const [k, v] of Object.entries(value)) {
31
+ if (SENSITIVE_KEYS.has(k.toLowerCase())) {
32
+ out[k] = "[REDACTED:field]";
33
+ continue;
34
+ }
35
+ out[k] = redactForIngestion(v);
36
+ }
37
+ return out;
38
+ }
39
+ return value;
40
+ }
41
+
42
+ export {
43
+ redactForIngestion
44
+ };
45
+ //# sourceMappingURL=chunk-C5CREGT2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redact/index.ts"],"sourcesContent":["/**\n * PII redaction for production trace payloads.\n *\n * The chat-trace emission sites pass tool args + results — and at one\n * point the LLM span carried the system prompt + user message verbatim\n * — through the wire. Anything that lands in the ingestion store is\n * also fair game for the analyst-loop's LLM prompts, so personal\n * identifiers MUST be stripped before they leave the request path.\n *\n * Discipline:\n * - Match cheap, deterministic patterns at the string level (SSN, EIN).\n * - Match well-known sensitive object keys (case-insensitive) and\n * replace the value, never the key, so the shape of the object\n * remains debuggable.\n * - Recurse arrays + plain objects only; pass through everything else\n * unchanged (numbers, booleans, null, undefined, functions, etc).\n * - NEVER throw — a redaction failure must not crash the chat handler.\n * Unrecognized inputs round-trip as-is.\n */\n\nconst SSN_PATTERN = /\\d{3}-\\d{2}-\\d{4}/\nconst EIN_PATTERN = /\\d{2}-\\d{7}/\n\nconst SENSITIVE_KEYS = new Set([\n 'ssn',\n 'ein',\n 'password',\n 'apikey',\n 'token',\n 'secret',\n 'authorization',\n 'email',\n 'phone',\n])\n\nfunction redactString(value: string): string {\n if (SSN_PATTERN.test(value)) return '[REDACTED:ssn]'\n if (EIN_PATTERN.test(value)) return '[REDACTED:ein]'\n return value\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\nexport function redactForIngestion(value: unknown): unknown {\n if (typeof value === 'string') return redactString(value)\n if (Array.isArray(value)) return value.map(redactForIngestion)\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n if (SENSITIVE_KEYS.has(k.toLowerCase())) {\n out[k] = '[REDACTED:field]'\n continue\n }\n out[k] = redactForIngestion(v)\n }\n return out\n }\n return value\n}\n"],"mappings":";AAoBA,IAAM,cAAc;AACpB,IAAM,cAAc;AAEpB,IAAM,iBAAiB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,aAAa,OAAuB;AAC3C,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,cAAc,OAAkD;AACvE,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,SAAO,UAAU,OAAO,aAAa,UAAU;AACjD;AAEO,SAAS,mBAAmB,OAAyB;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO,aAAa,KAAK;AACxD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,kBAAkB;AAC7D,MAAI,cAAc,KAAK,GAAG;AACxB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,UAAI,eAAe,IAAI,EAAE,YAAY,CAAC,GAAG;AACvC,YAAI,CAAC,IAAI;AACT;AAAA,MACF;AACA,UAAI,CAAC,IAAI,mBAAmB,CAAC;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,61 @@
1
+ // src/web/index.ts
2
+ async function parseJsonObjectBody(request) {
3
+ let raw;
4
+ try {
5
+ raw = await request.json();
6
+ } catch {
7
+ return [null, Response.json({ error: "Invalid JSON body" }, { status: 400 })];
8
+ }
9
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
10
+ return [null, Response.json({ error: "Body must be a JSON object" }, { status: 400 })];
11
+ }
12
+ return [raw, null];
13
+ }
14
+ function requireString(body, field) {
15
+ const v = body[field];
16
+ if (typeof v !== "string" || v.length === 0) {
17
+ return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 });
18
+ }
19
+ return v;
20
+ }
21
+ function extractRequestContext(request) {
22
+ const ipAddress = request.headers.get("CF-Connecting-IP") ?? request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim() ?? "0.0.0.0";
23
+ return {
24
+ ipAddress,
25
+ userAgent: request.headers.get("User-Agent") ?? "",
26
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27
+ requestId: crypto.randomUUID()
28
+ };
29
+ }
30
+ async function checkRateLimit(kv, key, limit, windowSeconds) {
31
+ const now = Math.floor(Date.now() / 1e3);
32
+ const windowStart = now - windowSeconds;
33
+ const kvKey = `rl:${key}`;
34
+ const raw = await kv.get(kvKey);
35
+ const timestamps = raw ? JSON.parse(raw) : [];
36
+ const valid = timestamps.filter((t) => t > windowStart);
37
+ if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds };
38
+ valid.push(now);
39
+ await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 });
40
+ return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds };
41
+ }
42
+ function addSecurityHeaders(response, opts = {}) {
43
+ response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
44
+ response.headers.set("X-Content-Type-Options", "nosniff");
45
+ response.headers.set("X-Frame-Options", "SAMEORIGIN");
46
+ response.headers.set("Referrer-Policy", "same-origin");
47
+ response.headers.set("X-XSS-Protection", "1; mode=block");
48
+ if (opts.disclaimer) response.headers.set("X-AI-Disclaimer", opts.disclaimer);
49
+ if (opts.retention) response.headers.set("X-Data-Retention", opts.retention);
50
+ for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v);
51
+ return response;
52
+ }
53
+
54
+ export {
55
+ parseJsonObjectBody,
56
+ requireString,
57
+ extractRequestContext,
58
+ checkRateLimit,
59
+ addSecurityHeaders
60
+ };
61
+ //# sourceMappingURL=chunk-CN75FIPT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/index.ts"],"sourcesContent":["/**\n * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing\n * + narrowing, request-context extraction (real client IP behind Cloudflare),\n * a KV-backed sliding-window rate limiter, and security response headers. Pure\n * mechanism — no DB, no domain. The KV is a structural interface so this needs\n * no `@cloudflare/workers-types` dependency.\n */\n\nexport type JsonObject = Record<string, unknown>\n\n/** Parse + object-narrow a Request body. `[body, null]` on success, `[null,\n * errorResponse]` on a non-object body (callers `if (err) return err`). */\nexport async function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]> {\n let raw: unknown\n try {\n raw = await request.json()\n } catch {\n return [null, Response.json({ error: 'Invalid JSON body' }, { status: 400 })]\n }\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n return [null, Response.json({ error: 'Body must be a JSON object' }, { status: 400 })]\n }\n return [raw as JsonObject, null]\n}\n\n/** Narrow one required string field, 400 if missing/empty. */\nexport function requireString(body: JsonObject, field: string): string | Response {\n const v = body[field]\n if (typeof v !== 'string' || v.length === 0) {\n return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 })\n }\n return v\n}\n\nexport interface RequestContext {\n ipAddress: string\n userAgent: string\n timestamp: string\n requestId: string\n}\n\n/** Extract request context for audit trails. Uses `CF-Connecting-IP` for the\n * real client IP behind Cloudflare. */\nexport function extractRequestContext(request: Request): RequestContext {\n const ipAddress =\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ??\n '0.0.0.0'\n return {\n ipAddress,\n userAgent: request.headers.get('User-Agent') ?? '',\n timestamp: new Date().toISOString(),\n requestId: crypto.randomUUID(),\n }\n}\n\n/** Minimal KV contract (Cloudflare `KVNamespace` satisfies it structurally). */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n}\n\n/** KV-backed sliding-window rate limit. Stores recent timestamps per key,\n * prunes the window, allows until `limit` is hit. */\nexport async function checkRateLimit(kv: KvLike, key: string, limit: number, windowSeconds: number): Promise<RateLimitResult> {\n const now = Math.floor(Date.now() / 1000)\n const windowStart = now - windowSeconds\n const kvKey = `rl:${key}`\n const raw = await kv.get(kvKey)\n const timestamps: number[] = raw ? JSON.parse(raw) : []\n const valid = timestamps.filter((t) => t > windowStart)\n if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds }\n valid.push(now)\n await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 })\n return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds }\n}\n\nexport interface SecurityHeaderOptions {\n /** Product disclaimer (e.g. \"AI-powered tool. Not legal advice.\"). Omitted if absent. */\n disclaimer?: string\n /** Data-retention label (e.g. \"7-years\"). Omitted if absent. */\n retention?: string\n /** Extra headers to set. */\n extra?: Record<string, string>\n}\n\n/** Set standard security headers on a response (HSTS, nosniff, frame-options,\n * referrer-policy, XSS) + optional product disclaimer/retention. The security\n * set is generic; the disclaimer/retention are the product's. */\nexport function addSecurityHeaders(response: Response, opts: SecurityHeaderOptions = {}): Response {\n response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')\n response.headers.set('X-Content-Type-Options', 'nosniff')\n response.headers.set('X-Frame-Options', 'SAMEORIGIN')\n response.headers.set('Referrer-Policy', 'same-origin')\n response.headers.set('X-XSS-Protection', '1; mode=block')\n if (opts.disclaimer) response.headers.set('X-AI-Disclaimer', opts.disclaimer)\n if (opts.retention) response.headers.set('X-Data-Retention', opts.retention)\n for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v)\n return response\n}\n"],"mappings":";AAYA,eAAsB,oBAAoB,SAAkE;AAC1G,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,CAAC,MAAM,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EAC9E;AACA,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjE,WAAO,CAAC,MAAM,SAAS,KAAK,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EACvF;AACA,SAAO,CAAC,KAAmB,IAAI;AACjC;AAGO,SAAS,cAAc,MAAkB,OAAkC;AAChF,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,OAAO,MAAM,YAAY,EAAE,WAAW,GAAG;AAC3C,WAAO,SAAS,KAAK,EAAE,OAAO,gCAAgC,KAAK,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AACA,SAAO;AACT;AAWO,SAAS,sBAAsB,SAAkC;AACtE,QAAM,YACJ,QAAQ,QAAQ,IAAI,kBAAkB,KACtC,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAC5D;AACF,SAAO;AAAA,IACL;AAAA,IACA,WAAW,QAAQ,QAAQ,IAAI,YAAY,KAAK;AAAA,IAChD,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,WAAW,OAAO,WAAW;AAAA,EAC/B;AACF;AAgBA,eAAsB,eAAe,IAAY,KAAa,OAAe,eAAiD;AAC5H,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,cAAc,MAAM;AAC1B,QAAM,QAAQ,MAAM,GAAG;AACvB,QAAM,MAAM,MAAM,GAAG,IAAI,KAAK;AAC9B,QAAM,aAAuB,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AACtD,QAAM,QAAQ,WAAW,OAAO,CAAC,MAAM,IAAI,WAAW;AACtD,MAAI,MAAM,UAAU,MAAO,QAAO,EAAE,SAAS,OAAO,WAAW,GAAG,UAAU,MAAM,CAAC,KAAK,OAAO,cAAc;AAC7G,QAAM,KAAK,GAAG;AACd,QAAM,GAAG,IAAI,OAAO,KAAK,UAAU,KAAK,GAAG,EAAE,eAAe,gBAAgB,EAAE,CAAC;AAC/E,SAAO,EAAE,SAAS,MAAM,WAAW,QAAQ,MAAM,QAAQ,SAAS,MAAM,cAAc;AACxF;AAcO,SAAS,mBAAmB,UAAoB,OAA8B,CAAC,GAAa;AACjG,WAAS,QAAQ,IAAI,6BAA6B,8CAA8C;AAChG,WAAS,QAAQ,IAAI,0BAA0B,SAAS;AACxD,WAAS,QAAQ,IAAI,mBAAmB,YAAY;AACpD,WAAS,QAAQ,IAAI,mBAAmB,aAAa;AACrD,WAAS,QAAQ,IAAI,oBAAoB,eAAe;AACxD,MAAI,KAAK,WAAY,UAAS,QAAQ,IAAI,mBAAmB,KAAK,UAAU;AAC5E,MAAI,KAAK,UAAW,UAAS,QAAQ,IAAI,oBAAoB,KAAK,SAAS;AAC3E,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,EAAG,UAAS,QAAQ,IAAI,GAAG,CAAC;AAChF,SAAO;AACT;","names":[]}