@retinue/agentkit 0.1.0 → 0.3.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 (246) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/embeddings/openai.d.ts +45 -0
  6. package/dist/adapters/embeddings/openai.js +109 -0
  7. package/dist/adapters/memory/connections.d.ts +10 -0
  8. package/dist/adapters/memory/connections.js +87 -0
  9. package/dist/adapters/memory/graph.d.ts +17 -0
  10. package/dist/adapters/memory/graph.js +267 -0
  11. package/dist/adapters/memory/index.d.ts +3 -0
  12. package/dist/adapters/memory/index.js +3 -0
  13. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  14. package/dist/adapters/memory/rate-limit.js +28 -0
  15. package/dist/adapters/memory/runtime.js +0 -0
  16. package/dist/adapters/postgres/connections.d.ts +15 -0
  17. package/dist/adapters/postgres/connections.js +134 -0
  18. package/dist/adapters/postgres/graph.d.ts +24 -0
  19. package/dist/adapters/postgres/graph.js +325 -0
  20. package/dist/adapters/postgres/index.d.ts +2 -0
  21. package/dist/adapters/postgres/index.js +2 -0
  22. package/dist/adapters/postgres/migrations.js +198 -0
  23. package/dist/adapters/postgres/run-store.js +26 -2
  24. package/dist/adapters/postgres/schema.d.ts +61 -2
  25. package/dist/adapters/postgres/schema.js +95 -10
  26. package/dist/adapters/redis/index.d.ts +1 -0
  27. package/dist/adapters/redis/index.js +1 -0
  28. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  29. package/dist/adapters/redis/rate-limit.js +55 -0
  30. package/dist/adapters/supabase/index.d.ts +15 -0
  31. package/dist/adapters/supabase/index.js +15 -0
  32. package/dist/adapters/supabase/rls.js +33 -0
  33. package/dist/agents/agent.d.ts +99 -1
  34. package/dist/agents/agent.js +215 -16
  35. package/dist/agents/define.js +24 -4
  36. package/dist/agents/engine.d.ts +48 -0
  37. package/dist/agents/engine.js +370 -13
  38. package/dist/audio/index.d.ts +151 -0
  39. package/dist/audio/index.js +183 -0
  40. package/dist/capabilities/index.d.ts +5 -1
  41. package/dist/capabilities/index.js +23 -0
  42. package/dist/capabilities/runtime.d.ts +8 -0
  43. package/dist/connections/cipher.d.ts +103 -0
  44. package/dist/connections/cipher.js +141 -0
  45. package/dist/connections/index.d.ts +156 -0
  46. package/dist/connections/index.js +72 -0
  47. package/dist/connections/oauth/client.d.ts +84 -0
  48. package/dist/connections/oauth/client.js +141 -0
  49. package/dist/connections/oauth/index.d.ts +166 -0
  50. package/dist/connections/oauth/index.js +226 -0
  51. package/dist/connections/oauth/service.d.ts +90 -0
  52. package/dist/connections/oauth/service.js +132 -0
  53. package/dist/connections/pause.d.ts +68 -0
  54. package/dist/connections/pause.js +97 -0
  55. package/dist/connections/resolver.d.ts +59 -0
  56. package/dist/connections/resolver.js +138 -0
  57. package/dist/connections/resume.d.ts +64 -0
  58. package/dist/connections/resume.js +99 -0
  59. package/dist/connections/vault-cipher.d.ts +95 -0
  60. package/dist/connections/vault-cipher.js +206 -0
  61. package/dist/core/budget.d.ts +55 -0
  62. package/dist/core/budget.js +56 -0
  63. package/dist/core/content-parts.d.ts +26 -2
  64. package/dist/core/content-parts.js +1 -0
  65. package/dist/core/context.d.ts +25 -0
  66. package/dist/core/errors.d.ts +1 -1
  67. package/dist/core/errors.js +15 -0
  68. package/dist/core/events.d.ts +110 -2
  69. package/dist/core/events.js +33 -2
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.js +1 -0
  72. package/dist/core/validation.js +19 -0
  73. package/dist/documents/index.d.ts +14 -0
  74. package/dist/documents/parsers/text.d.ts +16 -0
  75. package/dist/documents/parsers/text.js +54 -2
  76. package/dist/entries/connections.d.ts +10 -0
  77. package/dist/entries/connections.js +10 -0
  78. package/dist/entries/guardrails.d.ts +14 -0
  79. package/dist/entries/guardrails.js +14 -0
  80. package/dist/entries/knowledge.d.ts +17 -0
  81. package/dist/entries/knowledge.js +16 -0
  82. package/dist/entries/mcp-server.d.ts +9 -0
  83. package/dist/entries/mcp-server.js +9 -0
  84. package/dist/entries/runtime.d.ts +10 -0
  85. package/dist/entries/runtime.js +8 -0
  86. package/dist/entries/testing.d.ts +28 -0
  87. package/dist/entries/testing.js +28 -0
  88. package/dist/graphql/resolvers.d.ts +7 -1
  89. package/dist/graphql/resolvers.js +20 -0
  90. package/dist/graphql/schema.d.ts +1 -1
  91. package/dist/graphql/schema.js +44 -0
  92. package/dist/guardrails/index.d.ts +115 -0
  93. package/dist/guardrails/index.js +108 -0
  94. package/dist/guardrails/moderation.d.ts +53 -0
  95. package/dist/guardrails/moderation.js +75 -0
  96. package/dist/guardrails/pii.d.ts +75 -0
  97. package/dist/guardrails/pii.js +193 -0
  98. package/dist/knowledge/communities.d.ts +166 -0
  99. package/dist/knowledge/communities.js +377 -0
  100. package/dist/knowledge/graph-global.d.ts +153 -0
  101. package/dist/knowledge/graph-global.js +246 -0
  102. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  103. package/dist/knowledge/graph-retrieval.js +303 -0
  104. package/dist/knowledge/graph.d.ts +223 -0
  105. package/dist/knowledge/graph.js +369 -0
  106. package/dist/knowledge/index.d.ts +25 -0
  107. package/dist/knowledge/index.js +37 -1
  108. package/dist/knowledge/navigate.d.ts +89 -0
  109. package/dist/knowledge/navigate.js +107 -0
  110. package/dist/knowledge/retrieval.d.ts +98 -5
  111. package/dist/knowledge/retrieval.js +158 -28
  112. package/dist/mcp-server/index.d.ts +168 -0
  113. package/dist/mcp-server/index.js +175 -0
  114. package/dist/models/extraction.d.ts +73 -0
  115. package/dist/models/extraction.js +118 -0
  116. package/dist/models/index.d.ts +40 -1
  117. package/dist/models/index.js +22 -1
  118. package/dist/models/pricing.d.ts +2 -0
  119. package/dist/models/pricing.js +17 -1
  120. package/dist/models/provider-factory.js +9 -7
  121. package/dist/models/streaming.d.ts +111 -1
  122. package/dist/models/streaming.js +184 -3
  123. package/dist/persistence/index.d.ts +230 -0
  124. package/dist/persistence/index.js +2 -0
  125. package/dist/runtime/index.d.ts +1 -1
  126. package/dist/runtime/index.js +12 -0
  127. package/dist/runtime/worker.d.ts +36 -1
  128. package/dist/runtime/worker.js +37 -4
  129. package/dist/security/checklist.js +55 -0
  130. package/dist/security/findings.js +18 -9
  131. package/dist/server/bin.d.ts +19 -0
  132. package/dist/server/bin.js +226 -0
  133. package/dist/server/boot.d.ts +20 -2
  134. package/dist/server/boot.js +29 -3
  135. package/dist/server/cli.d.ts +13 -0
  136. package/dist/server/cli.js +7 -4
  137. package/dist/server/doctor.d.ts +74 -0
  138. package/dist/server/doctor.js +280 -0
  139. package/dist/skills/catalogue.d.ts +49 -0
  140. package/dist/skills/catalogue.js +61 -0
  141. package/dist/skills/context.d.ts +61 -0
  142. package/dist/skills/context.js +107 -0
  143. package/dist/skills/index.d.ts +2 -0
  144. package/dist/skills/index.js +2 -0
  145. package/dist/telemetry/spans.js +17 -0
  146. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  147. package/dist/testing/conformance/artifact-exports.js +172 -0
  148. package/dist/testing/conformance/artifacts.d.ts +11 -0
  149. package/dist/testing/conformance/artifacts.js +316 -0
  150. package/dist/testing/conformance/audio.d.ts +38 -0
  151. package/dist/testing/conformance/audio.js +145 -0
  152. package/dist/testing/conformance/capability.d.ts +23 -0
  153. package/dist/testing/conformance/capability.js +27 -0
  154. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  155. package/dist/testing/conformance/checkpoint-store.js +72 -0
  156. package/dist/testing/conformance/connections.d.ts +14 -0
  157. package/dist/testing/conformance/connections.js +171 -0
  158. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  159. package/dist/testing/conformance/conversation-store.js +60 -0
  160. package/dist/testing/conformance/evaluation.d.ts +10 -0
  161. package/dist/testing/conformance/evaluation.js +295 -0
  162. package/dist/testing/conformance/files.d.ts +19 -0
  163. package/dist/testing/conformance/files.js +454 -0
  164. package/dist/testing/conformance/flows.d.ts +16 -0
  165. package/dist/testing/conformance/flows.js +193 -0
  166. package/dist/testing/conformance/graph.d.ts +22 -0
  167. package/dist/testing/conformance/graph.js +500 -0
  168. package/dist/testing/conformance/hitl.d.ts +25 -0
  169. package/dist/testing/conformance/hitl.js +523 -0
  170. package/dist/testing/conformance/index.d.ts +163 -0
  171. package/dist/testing/conformance/index.js +321 -0
  172. package/dist/testing/conformance/invariants.d.ts +23 -0
  173. package/dist/testing/conformance/invariants.js +80 -0
  174. package/dist/testing/conformance/knowledge.d.ts +41 -0
  175. package/dist/testing/conformance/knowledge.js +592 -0
  176. package/dist/testing/conformance/parents.d.ts +50 -0
  177. package/dist/testing/conformance/parents.js +39 -0
  178. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  179. package/dist/testing/conformance/rate-limit.js +71 -0
  180. package/dist/testing/conformance/records.d.ts +36 -0
  181. package/dist/testing/conformance/records.js +400 -0
  182. package/dist/testing/conformance/rollups.d.ts +30 -0
  183. package/dist/testing/conformance/rollups.js +425 -0
  184. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  185. package/dist/testing/conformance/run-coordinator.js +150 -0
  186. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  187. package/dist/testing/conformance/run-event-log.js +138 -0
  188. package/dist/testing/conformance/run-store.d.ts +12 -0
  189. package/dist/testing/conformance/run-store.js +294 -0
  190. package/dist/testing/conformance/session-state.d.ts +38 -0
  191. package/dist/testing/conformance/session-state.js +198 -0
  192. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  193. package/dist/testing/conformance/usage-limits.js +309 -0
  194. package/dist/testing/conformance.d.ts +8 -0
  195. package/dist/testing/conformance.js +8 -0
  196. package/dist/testing/memory-backend.d.ts +39 -0
  197. package/dist/testing/memory-backend.js +41 -0
  198. package/dist/testing/pglite.d.ts +55 -0
  199. package/dist/testing/pglite.js +78 -0
  200. package/dist/testing/stub-model.d.ts +58 -0
  201. package/dist/testing/stub-model.js +71 -0
  202. package/dist/testing/supabase-storage-double.d.ts +35 -0
  203. package/dist/testing/supabase-storage-double.js +101 -0
  204. package/dist/toolkit/files.d.ts +125 -0
  205. package/dist/toolkit/files.js +320 -0
  206. package/dist/toolkit/http.d.ts +27 -0
  207. package/dist/toolkit/http.js +48 -0
  208. package/dist/toolkit/index.d.ts +31 -1
  209. package/dist/toolkit/index.js +26 -1
  210. package/dist/toolkit/mime.d.ts +97 -0
  211. package/dist/toolkit/mime.js +233 -0
  212. package/dist/toolkit/sandbox.d.ts +119 -0
  213. package/dist/toolkit/sandbox.js +239 -0
  214. package/dist/toolkit/ssrf.d.ts +139 -0
  215. package/dist/toolkit/ssrf.js +349 -0
  216. package/dist/toolkit/vendor.d.ts +91 -0
  217. package/dist/toolkit/vendor.js +153 -0
  218. package/dist/toolkit/web.d.ts +13 -0
  219. package/dist/toolkit/web.js +7 -1
  220. package/dist/tools/budget.d.ts +28 -0
  221. package/dist/tools/budget.js +35 -0
  222. package/dist/tools/credentials.d.ts +316 -0
  223. package/dist/tools/credentials.js +369 -0
  224. package/dist/tools/define.d.ts +45 -0
  225. package/dist/tools/define.js +26 -0
  226. package/dist/tools/find.d.ts +109 -0
  227. package/dist/tools/find.js +210 -0
  228. package/dist/tools/index.d.ts +35 -2
  229. package/dist/tools/index.js +11 -0
  230. package/dist/tools/library/audio.d.ts +63 -0
  231. package/dist/tools/library/audio.js +148 -0
  232. package/dist/tools/library/fs.d.ts +24 -0
  233. package/dist/tools/library/fs.js +102 -0
  234. package/dist/tools/library/index.d.ts +37 -2
  235. package/dist/tools/library/index.js +47 -0
  236. package/dist/tools/library/shell.d.ts +45 -0
  237. package/dist/tools/library/shell.js +70 -0
  238. package/dist/tools/meta-tools.js +8 -0
  239. package/dist/tools/registry.d.ts +146 -0
  240. package/dist/tools/registry.js +250 -4
  241. package/dist/usage/index.d.ts +21 -0
  242. package/dist/usage/index.js +1 -0
  243. package/dist/usage/rate-limit.d.ts +122 -0
  244. package/dist/usage/rate-limit.js +131 -0
  245. package/dist/usage/recorder.js +3 -0
  246. package/package.json +33 -2
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Guardrails — REQ-046 (#205), task #211.
3
+ *
4
+ * A seam for checks a deployment needs and this runtime does not ship: PII redaction, moderation, a topic
5
+ * restriction, an output schema. Without it, a deployment that needs any of those has to edit the engine.
6
+ *
7
+ * The injection half of "guardrails" is already built and lives elsewhere (`security/prompt-safety.ts`), and it
8
+ * is deliberately *not* a guardrail in this sense: containment is structural — untrusted content is wrapped in a
9
+ * nonce-delimited envelope whether or not anything recognises an attack — whereas everything here is
10
+ * *inspection*, which can only act on what it detects. Conflating the two would invite someone to switch off
11
+ * containment because a detector is present.
12
+ *
13
+ * ## Three decisions that make this worth having
14
+ *
15
+ * **Tool calls are outputs.** A guardrail that inspects only the final message can be walked straight past by
16
+ * putting the data in a tool argument. Checking prose and not arguments is checking the boring half, so
17
+ * `GuardrailOutput` is a discriminated union of a message *and* a tool call, and the tool-call case is enforced
18
+ * at the one choke point every call goes through.
19
+ *
20
+ * **Fail closed.** A guardrail that throws refuses the turn, attributed to the guardrail that threw. The
21
+ * opposite default is how a guardrail silently stops guarding the day its dependency times out — and the run
22
+ * looks entirely normal afterwards, which is the property that makes it dangerous.
23
+ *
24
+ * **Every verdict is recorded, and never the value.** A redaction that leaves no trace is indistinguishable from
25
+ * the model never having been told, which makes an incident unreconstructable. So a record names *what* was
26
+ * redacted — the field, the entity type — and never what it contained, or the audit trail becomes the leak.
27
+ */
28
+ import type { ExecutionContext } from "../core/context.js";
29
+ /** What a guardrail may conclude. */
30
+ export declare const GUARDRAIL_OUTCOMES: readonly ["pass", "redacted", "refused"];
31
+ export type GuardrailOutcome = (typeof GUARDRAIL_OUTCOMES)[number];
32
+ /** The turn's input, before the model sees it. */
33
+ export type GuardrailInput = {
34
+ readonly text: string;
35
+ /** Identifiers only. A guardrail that needs contents reads them through the file service, under its own budget. */
36
+ readonly attachmentIds?: readonly string[];
37
+ };
38
+ /**
39
+ * Every boundary crossing that is not the turn's own input.
40
+ *
41
+ * Three kinds, one hook, deliberately. A separate hook per kind lets an author implement two of three and have a
42
+ * gap they did not choose — and the gap would be in whichever kind was added last, which is the one nobody
43
+ * remembers. The union forces the `switch` to be written.
44
+ *
45
+ * `tool-result` is here even though a tool produced it rather than the model: it is content *entering* the
46
+ * model's context from outside the tenant, and it is the likeliest source of personal data in a whole run — a
47
+ * document read by a tool contains what the document contains. Inspecting the arguments and not the results
48
+ * would check the direction data leaves and ignore the direction it arrives.
49
+ */
50
+ export type GuardrailOutput = {
51
+ readonly kind: "message";
52
+ readonly text: string;
53
+ } | {
54
+ readonly kind: "tool-call";
55
+ readonly toolName: string;
56
+ readonly input: unknown;
57
+ } | {
58
+ readonly kind: "tool-result";
59
+ readonly toolName: string;
60
+ readonly output: unknown;
61
+ };
62
+ export type GuardrailVerdict<T> = {
63
+ readonly kind: "pass";
64
+ }
65
+ /** `what` names fields or entity types — never values. */
66
+ | {
67
+ readonly kind: "redacted";
68
+ readonly value: T;
69
+ readonly what: readonly string[];
70
+ } | {
71
+ readonly kind: "refused";
72
+ readonly code: string;
73
+ readonly message: string;
74
+ };
75
+ export interface Guardrail {
76
+ /** Stable, and used in records and refusals: "which check stopped this" must be answerable. */
77
+ readonly name: string;
78
+ inspectInput?(input: GuardrailInput, context: ExecutionContext): Promise<GuardrailVerdict<GuardrailInput>> | GuardrailVerdict<GuardrailInput>;
79
+ inspectOutput?(output: GuardrailOutput, context: ExecutionContext): Promise<GuardrailVerdict<GuardrailOutput>> | GuardrailVerdict<GuardrailOutput>;
80
+ }
81
+ /** One line of the audit trail. Carries no inspected value, by construction. */
82
+ export type GuardrailRecord = {
83
+ readonly guardrail: string;
84
+ readonly subject: "input" | "message" | "tool-call" | "tool-result";
85
+ readonly outcome: GuardrailOutcome;
86
+ /** For a redaction: the fields or entity types touched. Never their contents. */
87
+ readonly what?: readonly string[];
88
+ /** For a refusal. */
89
+ readonly code?: string;
90
+ /** True when the guardrail threw and was therefore treated as a refusal. */
91
+ readonly threw?: boolean;
92
+ };
93
+ export type GuardrailDecision<T> = {
94
+ readonly outcome: "allowed";
95
+ readonly value: T;
96
+ readonly records: readonly GuardrailRecord[];
97
+ } | {
98
+ readonly outcome: "refused";
99
+ readonly by: string;
100
+ readonly code: string;
101
+ readonly message: string;
102
+ readonly records: readonly GuardrailRecord[];
103
+ };
104
+ /** Before the model sees the turn. */
105
+ export declare const applyInputGuardrails: (guardrails: readonly Guardrail[], input: GuardrailInput, context: ExecutionContext) => Promise<GuardrailDecision<GuardrailInput>>;
106
+ /** Before anything leaves the model — a message *or* a tool call. */
107
+ export declare const applyOutputGuardrails: (guardrails: readonly Guardrail[], output: GuardrailOutput, context: ExecutionContext) => Promise<GuardrailDecision<GuardrailOutput>>;
108
+ /**
109
+ * Whether a record could carry an inspected value — used by the test that asserts it never does.
110
+ *
111
+ * Here rather than in the test file because it states the invariant next to the type it constrains: a record has
112
+ * a fixed shape, and adding a field that holds content is the change this is meant to make somebody notice.
113
+ */
114
+ export declare const recordCarriesOnlyMetadata: (record: GuardrailRecord) => boolean;
115
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Guardrails — REQ-046 (#205), task #211.
3
+ *
4
+ * A seam for checks a deployment needs and this runtime does not ship: PII redaction, moderation, a topic
5
+ * restriction, an output schema. Without it, a deployment that needs any of those has to edit the engine.
6
+ *
7
+ * The injection half of "guardrails" is already built and lives elsewhere (`security/prompt-safety.ts`), and it
8
+ * is deliberately *not* a guardrail in this sense: containment is structural — untrusted content is wrapped in a
9
+ * nonce-delimited envelope whether or not anything recognises an attack — whereas everything here is
10
+ * *inspection*, which can only act on what it detects. Conflating the two would invite someone to switch off
11
+ * containment because a detector is present.
12
+ *
13
+ * ## Three decisions that make this worth having
14
+ *
15
+ * **Tool calls are outputs.** A guardrail that inspects only the final message can be walked straight past by
16
+ * putting the data in a tool argument. Checking prose and not arguments is checking the boring half, so
17
+ * `GuardrailOutput` is a discriminated union of a message *and* a tool call, and the tool-call case is enforced
18
+ * at the one choke point every call goes through.
19
+ *
20
+ * **Fail closed.** A guardrail that throws refuses the turn, attributed to the guardrail that threw. The
21
+ * opposite default is how a guardrail silently stops guarding the day its dependency times out — and the run
22
+ * looks entirely normal afterwards, which is the property that makes it dangerous.
23
+ *
24
+ * **Every verdict is recorded, and never the value.** A redaction that leaves no trace is indistinguishable from
25
+ * the model never having been told, which makes an incident unreconstructable. So a record names *what* was
26
+ * redacted — the field, the entity type — and never what it contained, or the audit trail becomes the leak.
27
+ */
28
+ /** What a guardrail may conclude. */
29
+ export const GUARDRAIL_OUTCOMES = ["pass", "redacted", "refused"];
30
+ const REFUSED_BY_THROW = "guardrail_failed";
31
+ const subjectOf = (value) => {
32
+ if (typeof value === "object" && value !== null && "kind" in value) {
33
+ const kind = value.kind;
34
+ return kind === "tool-call" || kind === "tool-result" ? kind : "message";
35
+ }
36
+ return "input";
37
+ };
38
+ /**
39
+ * Run a list in declared order, threading the value through.
40
+ *
41
+ * Threading is what makes two redacting guardrails compose: the second inspects what the first produced, so one
42
+ * cannot undo the other by inspecting the original and returning its own edit of it. Order is the caller's
43
+ * declaration and is never sorted here — a guardrail set whose order depends on object key iteration is a set
44
+ * whose behaviour changes when someone reformats the config.
45
+ *
46
+ * A refusal short-circuits: the remaining guardrails are not consulted, because the turn is over and running
47
+ * them would spend money to annotate a decision already taken.
48
+ *
49
+ * Two exported entry points over one core rather than a `hook: "inspectInput" | "inspectOutput"` parameter. The
50
+ * parameterised version does not typecheck — indexing a union of two method signatures gives a function callable
51
+ * with neither argument type — and the `never` cast that silences it would have erased exactly the distinction
52
+ * the two subjects exist to keep.
53
+ */
54
+ const applyEach = async (guardrails, select, value, context) => {
55
+ const records = [];
56
+ let current = value;
57
+ for (const guardrail of guardrails) {
58
+ const inspect = select(guardrail);
59
+ if (inspect === undefined)
60
+ continue;
61
+ let verdict;
62
+ try {
63
+ verdict = await inspect(current, context);
64
+ }
65
+ catch (error) {
66
+ // Fail closed, and say which one. A guardrail whose dependency timed out must not become a guardrail that
67
+ // passed everything: the whole point is that its absence is visible.
68
+ records.push({
69
+ guardrail: guardrail.name,
70
+ subject: subjectOf(current),
71
+ outcome: "refused",
72
+ code: REFUSED_BY_THROW,
73
+ threw: true,
74
+ });
75
+ return {
76
+ outcome: "refused",
77
+ by: guardrail.name,
78
+ code: REFUSED_BY_THROW,
79
+ message: `guardrail ${guardrail.name} could not complete: ${error instanceof Error ? error.message : String(error)}`,
80
+ records,
81
+ };
82
+ }
83
+ if (verdict.kind === "pass") {
84
+ records.push({ guardrail: guardrail.name, subject: subjectOf(current), outcome: "pass" });
85
+ continue;
86
+ }
87
+ if (verdict.kind === "redacted") {
88
+ records.push({ guardrail: guardrail.name, subject: subjectOf(current), outcome: "redacted", what: verdict.what });
89
+ current = verdict.value;
90
+ continue;
91
+ }
92
+ records.push({ guardrail: guardrail.name, subject: subjectOf(current), outcome: "refused", code: verdict.code });
93
+ return { outcome: "refused", by: guardrail.name, code: verdict.code, message: verdict.message, records };
94
+ }
95
+ return { outcome: "allowed", value: current, records };
96
+ };
97
+ /** Before the model sees the turn. */
98
+ export const applyInputGuardrails = (guardrails, input, context) => applyEach(guardrails, (g) => (g.inspectInput ? (v, c) => g.inspectInput(v, c) : undefined), input, context);
99
+ /** Before anything leaves the model — a message *or* a tool call. */
100
+ export const applyOutputGuardrails = (guardrails, output, context) => applyEach(guardrails, (g) => (g.inspectOutput ? (v, c) => g.inspectOutput(v, c) : undefined), output, context);
101
+ /**
102
+ * Whether a record could carry an inspected value — used by the test that asserts it never does.
103
+ *
104
+ * Here rather than in the test file because it states the invariant next to the type it constrains: a record has
105
+ * a fixed shape, and adding a field that holds content is the change this is meant to make somebody notice.
106
+ */
107
+ export const recordCarriesOnlyMetadata = (record) => Object.keys(record).every((key) => ["guardrail", "subject", "outcome", "what", "code", "threw"].includes(key));
108
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Moderation, as an adapter — REQ-046 (#205), task #212, AC-4.
3
+ *
4
+ * **Off unless declared, and it takes the classifier rather than choosing one.** That is the whole design
5
+ * decision, and it is a cost decision rather than a technical one: a model call on every turn doubles the
6
+ * latency floor and adds a per-turn charge, and whether that is worth it depends on what a deployment is for.
7
+ * A runtime that imposed it would be spending somebody else's money on a policy they did not choose.
8
+ *
9
+ * So `classify` is supplied by the host. OpenAI's moderation endpoint is one implementation; a local classifier,
10
+ * a keyword list, or a shared service are others. None of them is a dependency of this package.
11
+ *
12
+ * ## The cost, stated
13
+ *
14
+ * One classifier call per inspected subject. With `subjects: ["input", "message"]` — the default — that is two
15
+ * calls per turn, in series with the model rather than parallel to it, because a turn that has already been
16
+ * answered cannot be un-answered. Add roughly the classifier's own latency twice to every turn.
17
+ *
18
+ * Reducing that is a real option and is why `subjects` is configurable: inspecting only `input` halves the cost
19
+ * and leaves generated content unchecked, which is the right trade for an internal tool and the wrong one for
20
+ * anything public.
21
+ *
22
+ * ## Failure is handled by the port, not here
23
+ *
24
+ * A classifier that times out throws, and `applyInputGuardrails` turns a throw into a refusal attributed to this
25
+ * guardrail. That is deliberate and it is the expensive-looking choice: a moderation outage stops turns. The
26
+ * alternative is a moderation outage that silently stops moderating, which is the one nobody notices.
27
+ */
28
+ import type { Guardrail } from "./index.js";
29
+ export type ModerationResult = {
30
+ readonly flagged: boolean;
31
+ /** Category names from the classifier. Used in the refusal message and the record; never the content. */
32
+ readonly categories?: readonly string[];
33
+ };
34
+ /** What the host supplies. Throwing is a refusal — see the module comment. */
35
+ export type ModerationClassifier = (text: string) => Promise<ModerationResult> | ModerationResult;
36
+ /** Which subjects to spend a classifier call on. */
37
+ export declare const MODERATION_SUBJECTS: readonly ["input", "message", "tool-result"];
38
+ export type ModerationSubject = (typeof MODERATION_SUBJECTS)[number];
39
+ export type ModerationOptions = {
40
+ readonly classify: ModerationClassifier;
41
+ /**
42
+ * Defaults to `["input", "message"]` — two calls per turn.
43
+ *
44
+ * `tool-result` is off by default because a tool result is usually structured data rather than prose, and
45
+ * classifying JSON produces confident nonsense. Turn it on when tools return free text somebody will read.
46
+ */
47
+ readonly subjects?: readonly ModerationSubject[];
48
+ /** Minimum text length worth a call. Defaults to 1: a classifier call on an empty string is pure cost. */
49
+ readonly minLength?: number;
50
+ readonly name?: string;
51
+ };
52
+ export declare const createModerationGuardrail: (options: ModerationOptions) => Guardrail;
53
+ //# sourceMappingURL=moderation.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Moderation, as an adapter — REQ-046 (#205), task #212, AC-4.
3
+ *
4
+ * **Off unless declared, and it takes the classifier rather than choosing one.** That is the whole design
5
+ * decision, and it is a cost decision rather than a technical one: a model call on every turn doubles the
6
+ * latency floor and adds a per-turn charge, and whether that is worth it depends on what a deployment is for.
7
+ * A runtime that imposed it would be spending somebody else's money on a policy they did not choose.
8
+ *
9
+ * So `classify` is supplied by the host. OpenAI's moderation endpoint is one implementation; a local classifier,
10
+ * a keyword list, or a shared service are others. None of them is a dependency of this package.
11
+ *
12
+ * ## The cost, stated
13
+ *
14
+ * One classifier call per inspected subject. With `subjects: ["input", "message"]` — the default — that is two
15
+ * calls per turn, in series with the model rather than parallel to it, because a turn that has already been
16
+ * answered cannot be un-answered. Add roughly the classifier's own latency twice to every turn.
17
+ *
18
+ * Reducing that is a real option and is why `subjects` is configurable: inspecting only `input` halves the cost
19
+ * and leaves generated content unchecked, which is the right trade for an internal tool and the wrong one for
20
+ * anything public.
21
+ *
22
+ * ## Failure is handled by the port, not here
23
+ *
24
+ * A classifier that times out throws, and `applyInputGuardrails` turns a throw into a refusal attributed to this
25
+ * guardrail. That is deliberate and it is the expensive-looking choice: a moderation outage stops turns. The
26
+ * alternative is a moderation outage that silently stops moderating, which is the one nobody notices.
27
+ */
28
+ /** Which subjects to spend a classifier call on. */
29
+ export const MODERATION_SUBJECTS = ["input", "message", "tool-result"];
30
+ const textOf = (output) => {
31
+ if (output.kind === "message")
32
+ return output.text;
33
+ if (output.kind === "tool-result")
34
+ return typeof output.output === "string" ? output.output : null;
35
+ // A tool call's arguments are not prose. Classifying a JSON object is a call spent on a question the
36
+ // classifier was not trained for — the PII guardrail is the one that reads arguments.
37
+ return null;
38
+ };
39
+ export const createModerationGuardrail = (options) => {
40
+ const subjects = options.subjects ?? ["input", "message"];
41
+ const minLength = options.minLength ?? 1;
42
+ const judge = async (text) => {
43
+ if (text.trim().length < minLength)
44
+ return { kind: "pass" };
45
+ const result = await options.classify(text);
46
+ if (!result.flagged)
47
+ return { kind: "pass" };
48
+ const categories = result.categories ?? [];
49
+ return {
50
+ kind: "refused",
51
+ code: "moderation",
52
+ // Categories, never the content. The message is shown to a person and stored in an event.
53
+ message: categories.length > 0
54
+ ? `That content was flagged as ${categories.join(", ")}.`
55
+ : "That content was flagged by moderation.",
56
+ };
57
+ };
58
+ return {
59
+ name: options.name ?? "moderation",
60
+ ...(subjects.includes("input")
61
+ ? { inspectInput: (input) => judge(input.text) }
62
+ : {}),
63
+ ...(subjects.some((s) => s === "message" || s === "tool-result")
64
+ ? {
65
+ inspectOutput: async (output) => {
66
+ if (!subjects.includes(output.kind))
67
+ return { kind: "pass" };
68
+ const text = textOf(output);
69
+ return text === null ? { kind: "pass" } : judge(text);
70
+ },
71
+ }
72
+ : {}),
73
+ };
74
+ };
75
+ //# sourceMappingURL=moderation.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Personal data, detected and redacted — REQ-046 (#205), task #212.
3
+ *
4
+ * Deterministic and **offline**. No network call, no model call: a guardrail that costs a round trip per turn is
5
+ * one a deployment switches off under load, and the moment it is off is the moment it was needed. Everything
6
+ * here is a pattern plus, where one exists, a checksum.
7
+ *
8
+ * ## Why a checksum matters more than a pattern
9
+ *
10
+ * A sixteen-digit order number matches every "card number" regex ever written. Flagging it teaches people that
11
+ * this guardrail cries wolf, and a guardrail people have learned to ignore is worse than none — they route
12
+ * around it. So card numbers are Luhn-checked and IBANs mod-97-checked, and a candidate that fails its checksum
13
+ * is *not* personal data, it is a number.
14
+ *
15
+ * ## Referential consistency, without state
16
+ *
17
+ * A placeholder is derived from a hash of the value, so the same value always yields the same placeholder —
18
+ * `[email:7a3f19]`. That is not a cosmetic choice. A model asked to compare two records, handed two *different*
19
+ * placeholders for one email, concludes the records differ and reasons wrongly about data it was never allowed
20
+ * to see. Deriving from the value keeps equality and nothing else.
21
+ *
22
+ * The alternative — a counter per turn — would need state keyed by run and would break the moment a run
23
+ * resumed in a different process, which is the normal case for a durable runtime.
24
+ *
25
+ * A hash placeholder does permit equality testing, and that is the intended trade: equality is exactly the
26
+ * property being preserved. It leaks no plaintext and is not reversible.
27
+ */
28
+ import type { Guardrail } from "./index.js";
29
+ /** What to look for. Named so a record can say `email` without saying which email. */
30
+ export declare const PII_ENTITIES: readonly ["email", "phone", "card_number", "iban", "ssn", "ip_address"];
31
+ export type PiiEntity = (typeof PII_ENTITIES)[number];
32
+ /**
33
+ * What to do when something is found.
34
+ *
35
+ * `redact` by default, and the reasoning is worth stating because the other choice is defensible: refusing is
36
+ * safer and ruder. A support agent pasting a customer's email into a chat has done something ordinary, and a
37
+ * runtime that refuses the turn teaches them to paste it somewhere with no guardrail at all. Redaction keeps the
38
+ * conversation working while the value never reaches the model.
39
+ *
40
+ * `refuse` is right where the data must not have been sent at all — a card number, under most policies — so the
41
+ * default is per-entity rather than global.
42
+ */
43
+ export type PiiAction = "redact" | "refuse";
44
+ export type PiiOptions = {
45
+ /** Which entities to look for. Defaults to all of them. */
46
+ readonly entities?: readonly PiiEntity[];
47
+ /** Per-entity action. Anything unlisted uses `defaultAction`. */
48
+ readonly actions?: Partial<Record<PiiEntity, PiiAction>>;
49
+ /** Defaults to `redact` — see `PiiAction`. */
50
+ readonly defaultAction?: PiiAction;
51
+ };
52
+ /** Six hex characters of a salted-by-entity hash: enough to distinguish values, short enough to read. */
53
+ export declare const placeholderFor: (entity: PiiEntity, value: string) => string;
54
+ export type PiiFinding = {
55
+ readonly entity: PiiEntity;
56
+ readonly value: string;
57
+ };
58
+ /**
59
+ * Every entity in a string, longest match first so a redaction cannot corrupt an overlapping one.
60
+ *
61
+ * Exported because it is the whole detector and it deserves to be tested directly rather than through a
62
+ * guardrail's verdict.
63
+ */
64
+ export declare const findPii: (text: string, entities: readonly PiiEntity[]) => readonly PiiFinding[];
65
+ /** Replace every finding with its derived placeholder. Same value ⇒ same placeholder, always. */
66
+ export declare const redactText: (text: string, findings: readonly PiiFinding[]) => string;
67
+ /**
68
+ * A guardrail that finds personal data in the turn, in tool arguments and in tool results.
69
+ *
70
+ * All three, because each is a real path: the person types it, the model puts it in an argument, or a tool reads
71
+ * a document that contains it. A guardrail covering one of the three is a guardrail whose coverage nobody can
72
+ * state.
73
+ */
74
+ export declare const createPiiGuardrail: (options?: PiiOptions) => Guardrail;
75
+ //# sourceMappingURL=pii.d.ts.map
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Personal data, detected and redacted — REQ-046 (#205), task #212.
3
+ *
4
+ * Deterministic and **offline**. No network call, no model call: a guardrail that costs a round trip per turn is
5
+ * one a deployment switches off under load, and the moment it is off is the moment it was needed. Everything
6
+ * here is a pattern plus, where one exists, a checksum.
7
+ *
8
+ * ## Why a checksum matters more than a pattern
9
+ *
10
+ * A sixteen-digit order number matches every "card number" regex ever written. Flagging it teaches people that
11
+ * this guardrail cries wolf, and a guardrail people have learned to ignore is worse than none — they route
12
+ * around it. So card numbers are Luhn-checked and IBANs mod-97-checked, and a candidate that fails its checksum
13
+ * is *not* personal data, it is a number.
14
+ *
15
+ * ## Referential consistency, without state
16
+ *
17
+ * A placeholder is derived from a hash of the value, so the same value always yields the same placeholder —
18
+ * `[email:7a3f19]`. That is not a cosmetic choice. A model asked to compare two records, handed two *different*
19
+ * placeholders for one email, concludes the records differ and reasons wrongly about data it was never allowed
20
+ * to see. Deriving from the value keeps equality and nothing else.
21
+ *
22
+ * The alternative — a counter per turn — would need state keyed by run and would break the moment a run
23
+ * resumed in a different process, which is the normal case for a durable runtime.
24
+ *
25
+ * A hash placeholder does permit equality testing, and that is the intended trade: equality is exactly the
26
+ * property being preserved. It leaks no plaintext and is not reversible.
27
+ */
28
+ import { createHash } from "node:crypto";
29
+ /** What to look for. Named so a record can say `email` without saying which email. */
30
+ export const PII_ENTITIES = ["email", "phone", "card_number", "iban", "ssn", "ip_address"];
31
+ const LUHN_OK = (digits) => {
32
+ let sum = 0;
33
+ let double = false;
34
+ for (let i = digits.length - 1; i >= 0; i -= 1) {
35
+ let d = Number(digits[i]);
36
+ if (Number.isNaN(d))
37
+ return false;
38
+ if (double)
39
+ d = d * 2 > 9 ? d * 2 - 9 : d * 2;
40
+ sum += d;
41
+ double = !double;
42
+ }
43
+ return digits.length >= 13 && sum % 10 === 0;
44
+ };
45
+ /** ISO 7064 mod-97: move the first four characters to the end, letters to digits, remainder must be 1. */
46
+ const IBAN_OK = (value) => {
47
+ const normalized = value.replace(/[\s-]/g, "").toUpperCase();
48
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(normalized))
49
+ return false;
50
+ const rearranged = normalized.slice(4) + normalized.slice(0, 4);
51
+ const expanded = [...rearranged].map((c) => (/[A-Z]/.test(c) ? String(c.charCodeAt(0) - 55) : c)).join("");
52
+ let remainder = 0;
53
+ for (const digit of expanded)
54
+ remainder = (remainder * 10 + Number(digit)) % 97;
55
+ return remainder === 1;
56
+ };
57
+ /**
58
+ * A phone number, as distinct from any other run of digits.
59
+ *
60
+ * The corpus caught this: the first pattern matched an order number, an epoch timestamp and an invoice
61
+ * reference, giving 78.6% precision. A guardrail that fires on invoice numbers is one somebody switches off, and
62
+ * for this kind of check **precision matters more than recall** — a false positive is visible on every turn,
63
+ * a false negative is invisible until it matters, and only one of the two gets the guardrail disabled.
64
+ *
65
+ * Two conditions, both needed:
66
+ *
67
+ * - **9 to 15 digits.** Below nine and it is a date, an error code or a quantity; above fifteen and it is longer
68
+ * than E.164 permits, which usually means a card the Luhn check already rejected.
69
+ * - **A leading `+`, or at least two separators.** Humans group phone numbers — `020 7946 0958` — and machines
70
+ * do not group serial numbers. Two separators rather than one, because one is `INV-2026-0043`.
71
+ *
72
+ * A date like `2026-08-27` has two separators and is excluded by the digit count; a bare `1756300000` has the
73
+ * digits and is excluded by the grouping.
74
+ */
75
+ const PHONE_OK = (candidate) => {
76
+ const digits = (candidate.match(/\d/g) ?? []).length;
77
+ if (digits < 9 || digits > 15)
78
+ return false;
79
+ const separators = (candidate.match(/[\s\-()]/g) ?? []).length;
80
+ return candidate.trimStart().startsWith("+") || separators >= 2;
81
+ };
82
+ /**
83
+ * Detectors, each a pattern and an optional validator.
84
+ *
85
+ * Order matters: `card_number` runs before `phone`, because a long digit run matches both and a card number is
86
+ * the more consequential reading. Getting that backwards would redact a card as a phone number and apply the
87
+ * phone policy to it.
88
+ */
89
+ const DETECTORS = [
90
+ { entity: "email", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
91
+ { entity: "iban", pattern: /\b[A-Z]{2}\d{2}[A-Z0-9 ]{10,34}\b/g, valid: IBAN_OK },
92
+ { entity: "card_number", pattern: /\b(?:\d[ -]?){13,19}\b/g, valid: (m) => LUHN_OK(m.replace(/[ -]/g, "")) },
93
+ { entity: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
94
+ { entity: "ip_address", pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g },
95
+ // Last: the loosest pattern, so a card or an IBAN has already claimed its digits. And validated, because
96
+ // "a run of digits" is not a phone number — see PHONE_OK.
97
+ { entity: "phone", pattern: /\+?\d[\d\s\-()]{7,20}\d/g, valid: PHONE_OK },
98
+ ];
99
+ /** Six hex characters of a salted-by-entity hash: enough to distinguish values, short enough to read. */
100
+ export const placeholderFor = (entity, value) => `[${entity}:${createHash("sha256").update(`${entity}:${value}`).digest("hex").slice(0, 6)}]`;
101
+ /**
102
+ * Every entity in a string, longest match first so a redaction cannot corrupt an overlapping one.
103
+ *
104
+ * Exported because it is the whole detector and it deserves to be tested directly rather than through a
105
+ * guardrail's verdict.
106
+ */
107
+ export const findPii = (text, entities) => {
108
+ const found = [];
109
+ const claimed = [];
110
+ for (const detector of DETECTORS) {
111
+ if (!entities.includes(detector.entity))
112
+ continue;
113
+ for (const match of text.matchAll(detector.pattern)) {
114
+ const start = match.index ?? 0;
115
+ const end = start + match[0].length;
116
+ // A span already claimed by an earlier (more specific) detector is not re-read as something looser.
117
+ if (claimed.some((c) => start < c.end && end > c.start))
118
+ continue;
119
+ const value = match[0].trim();
120
+ if (detector.valid && !detector.valid(value))
121
+ continue;
122
+ claimed.push({ start, end });
123
+ found.push({ entity: detector.entity, value });
124
+ }
125
+ }
126
+ return found;
127
+ };
128
+ /** Replace every finding with its derived placeholder. Same value ⇒ same placeholder, always. */
129
+ export const redactText = (text, findings) => {
130
+ let out = text;
131
+ // Longest first: replacing a short value that is a substring of a longer one would leave a fragment behind.
132
+ for (const finding of [...findings].sort((a, b) => b.value.length - a.value.length)) {
133
+ out = out.split(finding.value).join(placeholderFor(finding.entity, finding.value));
134
+ }
135
+ return out;
136
+ };
137
+ /** Walk any JSON-ish value, redacting strings. Tool arguments and results are objects, not prose. */
138
+ const redactDeep = (value, entities, found) => {
139
+ if (typeof value === "string") {
140
+ const hits = findPii(value, entities);
141
+ found.push(...hits);
142
+ return hits.length === 0 ? value : redactText(value, hits);
143
+ }
144
+ if (Array.isArray(value))
145
+ return value.map((v) => redactDeep(v, entities, found));
146
+ if (value !== null && typeof value === "object") {
147
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, entities, found)]));
148
+ }
149
+ return value;
150
+ };
151
+ /**
152
+ * A guardrail that finds personal data in the turn, in tool arguments and in tool results.
153
+ *
154
+ * All three, because each is a real path: the person types it, the model puts it in an argument, or a tool reads
155
+ * a document that contains it. A guardrail covering one of the three is a guardrail whose coverage nobody can
156
+ * state.
157
+ */
158
+ export const createPiiGuardrail = (options = {}) => {
159
+ const entities = options.entities ?? [...PII_ENTITIES];
160
+ const defaultAction = options.defaultAction ?? "redact";
161
+ const actionFor = (entity) => options.actions?.[entity] ?? defaultAction;
162
+ const decide = (findings, redacted) => {
163
+ if (findings.length === 0)
164
+ return { kind: "pass" };
165
+ const refusing = [...new Set(findings.filter((f) => actionFor(f.entity) === "refuse").map((f) => f.entity))];
166
+ if (refusing.length > 0) {
167
+ return {
168
+ kind: "refused",
169
+ code: "pii_present",
170
+ // Names the entity type, never the value — the message is shown to a person and stored in an event.
171
+ message: `That content contains ${refusing.join(", ")}. Please remove it and try again.`,
172
+ };
173
+ }
174
+ return { kind: "redacted", value: redacted(), what: [...new Set(findings.map((f) => f.entity))] };
175
+ };
176
+ return {
177
+ name: "pii",
178
+ inspectInput(input) {
179
+ const findings = findPii(input.text, entities);
180
+ return decide(findings, () => ({ ...input, text: redactText(input.text, findings) }));
181
+ },
182
+ inspectOutput(output, _context) {
183
+ if (output.kind === "message") {
184
+ const findings = findPii(output.text, entities);
185
+ return decide(findings, () => ({ ...output, text: redactText(output.text, findings) }));
186
+ }
187
+ const found = [];
188
+ const payload = redactDeep(output.kind === "tool-call" ? output.input : output.output, entities, found);
189
+ return decide(found, () => output.kind === "tool-call" ? { ...output, input: payload } : { ...output, output: payload });
190
+ },
191
+ };
192
+ };
193
+ //# sourceMappingURL=pii.js.map