@jini-ai/core 0.4.1 → 0.4.3

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 (131) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +19 -1
  3. package/dist/agent-tools.d.ts +1 -0
  4. package/dist/approval.d.ts +134 -0
  5. package/dist/approval.js +141 -0
  6. package/dist/fetch/cache.d.ts +129 -0
  7. package/dist/fetch/cache.js +243 -0
  8. package/dist/fetch/index.d.ts +5 -0
  9. package/dist/fetch/index.js +3 -0
  10. package/dist/fetch/types.d.ts +93 -0
  11. package/dist/fetch/types.js +2 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +6 -1
  14. package/dist/model-facing-tool-errors.d.ts +152 -0
  15. package/dist/model-facing-tool-errors.js +124 -0
  16. package/dist/native-regexp.d.ts +10 -0
  17. package/dist/native-regexp.js +2 -0
  18. package/dist/primitives/index.d.ts +2 -0
  19. package/dist/primitives/index.js +1 -0
  20. package/dist/primitives/sse.d.ts +60 -0
  21. package/dist/primitives/sse.js +122 -0
  22. package/dist/redact.js +4 -3
  23. package/dist/registration-kit.d.ts +13 -0
  24. package/dist/registration-kit.js +12 -0
  25. package/dist/settings/agent-tools.d.ts +10 -0
  26. package/dist/settings/agent-tools.js +204 -0
  27. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  28. package/dist/settings/agent-value-write-tools.js +157 -0
  29. package/dist/settings/agent-writable-preferences.d.ts +67 -0
  30. package/dist/settings/agent-writable-preferences.js +132 -0
  31. package/dist/settings/agent-write-denylist.d.ts +19 -0
  32. package/dist/settings/agent-write-denylist.js +15 -0
  33. package/dist/settings/change-feed.d.ts +83 -0
  34. package/dist/settings/change-feed.js +61 -0
  35. package/dist/settings/definitions-dispatch.d.ts +56 -0
  36. package/dist/settings/definitions-dispatch.js +86 -0
  37. package/dist/settings/dictionaries/index.d.ts +48 -0
  38. package/dist/settings/dictionaries/index.js +69 -0
  39. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
  40. package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
  41. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
  42. package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
  43. package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
  44. package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
  45. package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
  46. package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
  47. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
  48. package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
  49. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
  50. package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
  51. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
  52. package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
  53. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
  54. package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
  55. package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
  56. package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
  57. package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
  58. package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
  59. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
  60. package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
  61. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
  62. package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
  63. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
  64. package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
  65. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
  66. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
  67. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
  68. package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
  69. package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
  70. package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
  71. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
  72. package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
  73. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
  74. package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
  75. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
  76. package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
  77. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
  78. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
  79. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
  80. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
  81. package/dist/settings/ensure-definitions.d.ts +101 -0
  82. package/dist/settings/ensure-definitions.js +101 -0
  83. package/dist/settings/errors.d.ts +42 -0
  84. package/dist/settings/errors.js +46 -0
  85. package/dist/settings/express/cms-adapter.d.ts +20 -0
  86. package/dist/settings/express/cms-adapter.js +124 -0
  87. package/dist/settings/express/contracts.d.ts +135 -0
  88. package/dist/settings/express/contracts.js +2 -0
  89. package/dist/settings/express/http.d.ts +10 -0
  90. package/dist/settings/express/http.js +211 -0
  91. package/dist/settings/express/index.d.ts +5 -0
  92. package/dist/settings/express/index.js +4 -0
  93. package/dist/settings/express/shared.d.ts +68 -0
  94. package/dist/settings/express/shared.js +54 -0
  95. package/dist/settings/express/stream.d.ts +24 -0
  96. package/dist/settings/express/stream.js +147 -0
  97. package/dist/settings/index.d.ts +40 -0
  98. package/dist/settings/index.js +37 -0
  99. package/dist/settings/ports.d.ts +118 -0
  100. package/dist/settings/ports.js +2 -0
  101. package/dist/settings/principal-lookup.d.ts +9 -0
  102. package/dist/settings/principal-lookup.js +2 -0
  103. package/dist/settings/purge-service.d.ts +54 -0
  104. package/dist/settings/purge-service.js +106 -0
  105. package/dist/settings/repo.memory.d.ts +119 -0
  106. package/dist/settings/repo.memory.js +199 -0
  107. package/dist/settings/settings.d.ts +139 -0
  108. package/dist/settings/settings.js +259 -0
  109. package/dist/settings/tool-registrations.d.ts +54 -0
  110. package/dist/settings/tool-registrations.js +250 -0
  111. package/dist/settings/types.d.ts +125 -0
  112. package/dist/settings/types.js +3 -0
  113. package/dist/settings/ui-tab-definitions.d.ts +41 -0
  114. package/dist/settings/ui-tab-definitions.js +148 -0
  115. package/dist/settings/write-service.d.ts +320 -0
  116. package/dist/settings/write-service.js +683 -0
  117. package/dist/surface-exchanges.d.ts +209 -0
  118. package/dist/surface-exchanges.js +55 -0
  119. package/dist/text/escape.d.ts +20 -0
  120. package/dist/text/escape.js +23 -0
  121. package/dist/text/index.d.ts +1 -0
  122. package/dist/text/index.js +1 -0
  123. package/dist/tool-metadata.d.ts +71 -0
  124. package/dist/tool-metadata.js +56 -0
  125. package/dist/tool-permissions.d.ts +76 -0
  126. package/dist/tool-permissions.js +57 -0
  127. package/dist/tool-registry.d.ts +19 -1
  128. package/dist/tool-registry.js +44 -0
  129. package/package.json +79 -4
  130. package/plugin-directory.d.ts +32 -0
  131. package/plugin-directory.js +140 -0
@@ -7,12 +7,57 @@
7
7
  // Domains explicitly own which classes/messages are safe to expose, including anonymous callers:
8
8
  // stacks, internal paths, SQL, credentials, and another tenant's data must remain unlisted/redacted.
9
9
  import { ToolInputError } from "./tool-registry.js";
10
+ /**
11
+ * The code every domain publishes an authorization refusal under, parameterized by domain prefix.
12
+ *
13
+ * Exists because `ForbiddenError` is the ONE class in this ladder that no domain declares: the kit's
14
+ * `requireToolPermission` throws it on every domain's behalf. Deriving the rule here rather than
15
+ * retyping `{ error: ForbiddenError, code: "X_FORBIDDEN" }` in six files means a later decision to
16
+ * reconcile this code with the HTTP arm's own `FORBIDDEN` is a one-line change here, not a sweep.
17
+ *
18
+ * NOTE (2026-09-16): the model-facing and HTTP arms disagree on this code today — the HTTP mappers
19
+ * return `FORBIDDEN` while the model-facing arm returns `<DOMAIN>_FORBIDDEN`. That drift predates
20
+ * this file (see `features/widgets/tool-registrations.ts`'s `toModelFacingWidgetsError` doc, which
21
+ * documented it for Widgets) and is owned elsewhere. This helper deliberately matches the SHIPPED
22
+ * model-facing convention rather than inventing a third spelling.
23
+ *
24
+ * The refusal message `requireToolPermission` builds names the principal, the permission, and the
25
+ * `authorize()` reason — no data, no internals. Surfacing it tells a model whether to stop asking
26
+ * or to ask a human for access; redacting it tells it only that something broke.
27
+ *
28
+ * @param required.domainPrefix - The domain's SCREAMING_SNAKE prefix, e.g. `MEMBERS`.
29
+ * @param required.error - The consumer's permission-error class; core has no CMS dependency.
30
+ * @returns The rule to place in that domain's allowlist.
31
+ * @complexity O(1).
32
+ */
10
33
  export function forbiddenRule(required) {
11
34
  return { error: required.error, code: `${required.domainPrefix}_FORBIDDEN` };
12
35
  }
13
36
  // First match wins: list subclasses before superclasses. Unmatched values must retain identity so
14
37
  // the transport's redaction remains the default; already-classified ToolInputError also stays intact
15
38
  // to avoid doubling a prefix attached by schema/recovery wrapping.
39
+ /**
40
+ * Re-classifies one rejection against a domain's allowlist, so a listed domain error reaches the
41
+ * model as a `ToolInputError` carrying its real message instead of a redacted `INTERNAL_ERROR`.
42
+ *
43
+ * Rules are evaluated IN ORDER and the first `instanceof` match wins, so a domain listing both a
44
+ * subclass and its superclass must place the subclass first — the same ordering discipline any
45
+ * hand-written `instanceof` ladder carries, made explicit here because an array hides it less than
46
+ * a chain of `if`s does.
47
+ *
48
+ * A matched rule publishes the error's own message, or {@link ModelFacingErrorRule.message} instead
49
+ * when the rule sets one — see that field for which classes need it and why.
50
+ *
51
+ * An already-`ToolInputError` rejection is returned untouched: it is already correctly classified,
52
+ * and re-wrapping it would double a code prefix that a `withSchemaOnRejection` wrap may have
53
+ * already attached.
54
+ *
55
+ * @param required.err - The rejection, of unknown type — anything a handler can throw.
56
+ * @param required.rules - The domain's allowlist. Anything unmatched is returned UNCHANGED and stays
57
+ * redacted; see this file's header for why that default is the security-relevant half.
58
+ * @returns The value to re-throw: a `ToolInputError` for a matched rule, otherwise `err` itself.
59
+ * @complexity O(r) in the rule count, on the failure path only.
60
+ */
16
61
  export function reclassifyToolError(required) {
17
62
  const { err, rules } = required;
18
63
  if (err instanceof ToolInputError)
@@ -28,6 +73,24 @@ export function reclassifyToolError(required) {
28
73
  // Wrap the whole map once: a correct per-handler converter previously reached only one handler
29
74
  // while sibling operations still leaked into the internal-error redactor. Central wrapping has no
30
75
  // omitted call-site arm, and successful results pass through untouched.
76
+ /**
77
+ * Wraps EVERY handler in a domain's map with {@link reclassifyToolError}, returning a new map.
78
+ *
79
+ * Wrapping the whole map once — rather than reclassifying at each call site — is the point. This
80
+ * codebase's dominant defect is a correct primitive with an unwired call site, and a per-call-site
81
+ * reshape is exactly that defect waiting to happen: `features/post/tool-registrations.ts` had a
82
+ * correct `toModelFacingUpdateError` wired into ONE of its handlers while its siblings kept
83
+ * throwing the same class straight through to the redactor. A map-level wrap cannot have a missed
84
+ * arm, because it has no arms.
85
+ *
86
+ * The wrap is transparent on the success path: the handler's own return value is passed through
87
+ * untouched, and only the `catch` does work.
88
+ *
89
+ * @param required.handlers - The domain's handler map, keyed by tool id.
90
+ * @param rules - The domain's allowlist, applied identically to every handler.
91
+ * @returns A new map with the same keys; the input map is not mutated.
92
+ * @complexity O(h) handlers wrapped once at build time; O(r) per failed call, none per successful one.
93
+ */
31
94
  export function withModelFacingErrors(required) {
32
95
  const { handlers, rules } = required;
33
96
  return Object.fromEntries(Object.entries(handlers).map(([toolId, handler]) => [
@@ -44,6 +107,24 @@ export function withModelFacingErrors(required) {
44
107
  }
45
108
  // Registration-array builders need the same blanket coverage as handler maps. Preserve descriptor,
46
109
  // policy, ordering, and successful results; only the rejection path is reclassified.
110
+ /**
111
+ * Wraps EVERY registration's handler in a domain's `ToolRegistration[]` with
112
+ * {@link reclassifyToolError}, returning a new array. The `ToolRegistration[]`-shaped sibling of
113
+ * {@link withModelFacingErrors} — for the registry-converted domains whose `build*Registrations`
114
+ * returns an array of `{descriptor, handler, policy}` (see `assistant/tool-contribution-registry.ts`'s
115
+ * `ToolContributor.build`) rather than a `Record<toolId, handler>`. `features/entries`,
116
+ * `features/content-types`, `features/navigation` and `features/workspace` are re-exported Jini
117
+ * builders of exactly this shape — see each one's `contribute*Tools`.
118
+ *
119
+ * Same transparency guarantee as {@link withModelFacingErrors}: only the `catch` arm does work, and
120
+ * every other field of each registration (`descriptor`, `policy`) passes through unchanged.
121
+ *
122
+ * @param required.registrations - The domain's registrations, in whatever order `build*Registrations`
123
+ * returned them.
124
+ * @param rules - The domain's allowlist, applied identically to every registration.
125
+ * @returns A new array with the same length and order; the input array is not mutated.
126
+ * @complexity O(n) registrations wrapped once at build time; O(r) per failed call, none per successful one.
127
+ */
47
128
  export function withModelFacingRegistrationErrors(required) {
48
129
  const { registrations, rules } = required;
49
130
  return registrations.map((registration) => ({
@@ -60,6 +141,20 @@ export function withModelFacingRegistrationErrors(required) {
60
141
  }
61
142
  // Unknown classes, raw driver errors, and non-Error throws get fixed fallback text, never raw error
62
143
  // content. The fallback itself must not be derived from err; first matching rule still wins.
144
+ /**
145
+ * The message a RETURNED failure result may carry for `err`: a listed class's message (or its fixed
146
+ * replacement), otherwise `fallback`.
147
+ *
148
+ * Unknown means redacted. An error class added tomorrow, a raw driver error, or a non-`Error` throw
149
+ * all get `fallback`, never their own text. Rules are evaluated in order; the first `instanceof`
150
+ * match wins.
151
+ *
152
+ * @param required.err - The caught value, of unknown type.
153
+ * @param required.rules - The allowlist.
154
+ * @param required.fallback - Fixed text for everything unlisted. Must not be built from `err`.
155
+ * @returns The message to publish.
156
+ * @complexity O(r) in the rule count.
157
+ */
63
158
  export function callerSafeErrorMessage(required) {
64
159
  const { err, rules, fallback } = required;
65
160
  for (const rule of rules) {
@@ -68,4 +163,33 @@ export function callerSafeErrorMessage(required) {
68
163
  }
69
164
  return fallback;
70
165
  }
166
+ /** A driver/system error code worth logging (`SQLITE_READONLY`, `ECONNREFUSED`, `ERR_INVALID_CHAR`) —
167
+ * a short constant token, never free text that could carry a value. */
168
+ const LOGGABLE_ERROR_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
169
+ /** `ClassName` or `ClassName(CODE)` for one thrown value; `typeof` for a non-`Error` throw. */
170
+ function describeOneErrorForLog(value) {
171
+ if (!(value instanceof Error))
172
+ return typeof value;
173
+ const className = value.constructor.name || "Error";
174
+ const code = value.code;
175
+ return typeof code === "string" && LOGGABLE_ERROR_CODE.test(code) ? `${className}(${code})` : className;
176
+ }
177
+ /**
178
+ * What a server-side log line may say about an error whose message is NOT known to be safe: its class
179
+ * name, a constant-token `code`, and the same for its direct `cause`. Never the message, which can
180
+ * quote a secret — `JSON.parse` quotes its input, and a sealer is handed plaintext.
181
+ *
182
+ * Use it wherever the raw message could carry a credential. Where a class's message is designed for
183
+ * logs (`EgressRefusedError.message`), log that instead.
184
+ *
185
+ * @param required.err - The caught value.
186
+ * @returns e.g. `SqliteError(SQLITE_BUSY)`, or `TypeError cause=Error(ECONNREFUSED)`.
187
+ * @complexity O(1).
188
+ */
189
+ export function describeErrorForLog(required, _optional = {}) {
190
+ const { err } = required;
191
+ const described = describeOneErrorForLog(err);
192
+ const cause = err instanceof Error ? err.cause : undefined;
193
+ return cause === undefined ? described : `${described} cause=${describeOneErrorForLog(cause)}`;
194
+ }
71
195
  //# sourceMappingURL=model-facing-tool-errors.js.map
@@ -0,0 +1,10 @@
1
+ /** Native RegExp.escape is available in Node 24 and modern browsers, but the supported
2
+ * TypeScript 5.8 libraries do not declare it yet. Declare the native contract without a
3
+ * polyfill or a second escaping implementation; hosts must supply the native capability. */
4
+ declare global {
5
+ interface RegExpConstructor {
6
+ escape(value: string): string;
7
+ }
8
+ }
9
+ export {};
10
+ //# sourceMappingURL=native-regexp.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=native-regexp.js.map
@@ -2,6 +2,8 @@ export * from './time.js';
2
2
  export * from './ids.js';
3
3
  export * from './json.js';
4
4
  export * from './http.js';
5
+ export { decodeSseFrames, decodeSseStream, parseSseRecord } from './sse.js';
6
+ export type { DecodedSseEvent, DecodedSseFrame, SseFramingOptions } from './sse.js';
5
7
  export * from './logger.js';
6
8
  export * from './result.js';
7
9
  export * from './paths.js';
@@ -2,6 +2,7 @@ export * from './time.js';
2
2
  export * from './ids.js';
3
3
  export * from './json.js';
4
4
  export * from './http.js';
5
+ export { decodeSseFrames, decodeSseStream, parseSseRecord } from './sse.js';
5
6
  export * from './logger.js';
6
7
  export * from './result.js';
7
8
  export * from './paths.js';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @module primitives/sse
3
+ *
4
+ * Minimal, tolerant Server-Sent-Events *frame decoder* for an INBOUND
5
+ * provider stream — the response body a call to the Anthropic Messages API
6
+ * or the OpenAI Chat Completions API sends back when `stream: true` is
7
+ * requested. This is deliberately not `@jini-ai/http-kit`'s `sse.ts`: that module
8
+ * is the OUTBOUND channel a route uses to push events to a browser client
9
+ * (bounded queue, backpressure, `Last-Event-ID` replay). This module has
10
+ * none of that — it only turns a raw byte/text stream into `{event, data}`
11
+ * frames per the SSE wire format (`text/event-stream`, RFC-ish: fields
12
+ * separated by `\n`, records separated by a blank line), so a provider-
13
+ * specific turn-runner (`anthropic-messages.ts`, `openai-chat.ts`) can
14
+ * `JSON.parse` each frame's `data` field without re-deriving line/record
15
+ * framing itself.
16
+ *
17
+ * Field parsing and framing are shared by providers and durable run consumers.
18
+ * The public leaf has no Node imports; callers keep their own JSON, EOF and
19
+ * reader-ownership policies rather than re-deriving line/record framing.
20
+ */
21
+ /** Provider-facing record. Metadata remains absent from this compatibility contract. */
22
+ export interface DecodedSseEvent {
23
+ /** Null without event: (OpenAI Chat Completions implicitly sends plain messages). */
24
+ readonly event: string | null;
25
+ /** Multiple data: values joined with newlines, per the SSE spec. */
26
+ readonly data: string;
27
+ }
28
+ /** Raw field values, including explicit (possibly empty) IDs; IDs do not carry across records. */
29
+ export interface DecodedSseFrame extends DecodedSseEvent {
30
+ readonly id?: string;
31
+ readonly dataLines: readonly string[];
32
+ }
33
+ export interface SseFramingOptions {
34
+ /** Providers flush EOF tails; durable run readers require a terminating blank line. */
35
+ readonly flushFinalFrame?: boolean;
36
+ /** Bounds all pending record text, including comments and ignored fields, in UTF-16 units. */
37
+ readonly maxBufferChars?: number;
38
+ }
39
+ /** Parse one delimiter-free record without JSON interpretation. O(text) time and space. */
40
+ export declare function parseSseRecord({ rawFrame }: {
41
+ rawFrame: string;
42
+ }, _options?: Record<string, never>): DecodedSseFrame;
43
+ /**
44
+ * Browser-safe inbound framing for byte/text iterables; accepts LF and CRLF, including split
45
+ * UTF-8 and CRLF chunks. Reader ownership stays with the source iterator. Optional bounds throw
46
+ * RangeError before buffered text exceeds the limit. Early return closes the source iterator.
47
+ * @complexity O(text + repeated copying of an unfinished line); O(pending record + chunk) space.
48
+ */
49
+ export declare function decodeSseFrames({ source }: {
50
+ source: AsyncIterable<Uint8Array | string>;
51
+ }, options?: SseFramingOptions): AsyncGenerator<DecodedSseFrame>;
52
+ /**
53
+ * Provider compatibility entry: drops reconnect metadata (meaningless for a single proxied
54
+ * request/response pair) and flushes unterminated EOF records. Keeps the published frame shape.
55
+ * @complexity Inherits decodeSseFrames' streaming cost and buffer size.
56
+ */
57
+ export declare function decodeSseStream({ source }: {
58
+ source: AsyncIterable<Uint8Array | string>;
59
+ }, _options?: Record<string, never>): AsyncGenerator<DecodedSseEvent>;
60
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module primitives/sse
3
+ *
4
+ * Minimal, tolerant Server-Sent-Events *frame decoder* for an INBOUND
5
+ * provider stream — the response body a call to the Anthropic Messages API
6
+ * or the OpenAI Chat Completions API sends back when `stream: true` is
7
+ * requested. This is deliberately not `@jini-ai/http-kit`'s `sse.ts`: that module
8
+ * is the OUTBOUND channel a route uses to push events to a browser client
9
+ * (bounded queue, backpressure, `Last-Event-ID` replay). This module has
10
+ * none of that — it only turns a raw byte/text stream into `{event, data}`
11
+ * frames per the SSE wire format (`text/event-stream`, RFC-ish: fields
12
+ * separated by `\n`, records separated by a blank line), so a provider-
13
+ * specific turn-runner (`anthropic-messages.ts`, `openai-chat.ts`) can
14
+ * `JSON.parse` each frame's `data` field without re-deriving line/record
15
+ * framing itself.
16
+ *
17
+ * Field parsing and framing are shared by providers and durable run consumers.
18
+ * The public leaf has no Node imports; callers keep their own JSON, EOF and
19
+ * reader-ownership policies rather than re-deriving line/record framing.
20
+ */
21
+ /** Owns field parsing for both raw records and incremental streams; callers own interpretation. */
22
+ function createRecordDecoder() {
23
+ let event = null;
24
+ let id;
25
+ let dataLines = [];
26
+ return {
27
+ line(raw) {
28
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
29
+ if (line.startsWith(':'))
30
+ return; // comment line, per spec
31
+ const colon = line.indexOf(':');
32
+ const field = colon === -1 ? line : line.slice(0, colon);
33
+ let value = colon === -1 ? '' : line.slice(colon + 1);
34
+ if (value.startsWith(' '))
35
+ value = value.slice(1);
36
+ if (field === 'event')
37
+ event = value;
38
+ else if (field === 'data')
39
+ dataLines.push(value);
40
+ else if (field === 'id')
41
+ id = value;
42
+ // retry/unknown fields do not belong to per-record interpretation.
43
+ },
44
+ take() {
45
+ const frame = { event, data: dataLines.join('\n'), dataLines, ...(id !== undefined ? { id } : {}) };
46
+ event = null;
47
+ id = undefined;
48
+ dataLines = [];
49
+ return frame;
50
+ },
51
+ };
52
+ }
53
+ /** Parse one delimiter-free record without JSON interpretation. O(text) time and space. */
54
+ export function parseSseRecord({ rawFrame }, _options = {}) {
55
+ const record = createRecordDecoder();
56
+ for (const line of rawFrame.split('\n'))
57
+ record.line(line);
58
+ return record.take();
59
+ }
60
+ /**
61
+ * Browser-safe inbound framing for byte/text iterables; accepts LF and CRLF, including split
62
+ * UTF-8 and CRLF chunks. Reader ownership stays with the source iterator. Optional bounds throw
63
+ * RangeError before buffered text exceeds the limit. Early return closes the source iterator.
64
+ * @complexity O(text + repeated copying of an unfinished line); O(pending record + chunk) space.
65
+ */
66
+ export async function* decodeSseFrames({ source }, options = {}) {
67
+ const decoder = new TextDecoder();
68
+ const record = createRecordDecoder();
69
+ let buffer = '';
70
+ let scanFrom = 0;
71
+ let recordChars = 0;
72
+ function* drainCompleteLines() {
73
+ if (options.maxBufferChars !== undefined && recordChars + buffer.length > options.maxBufferChars) {
74
+ throw new RangeError(`SSE stream exceeded the ${options.maxBufferChars}-character unterminated-frame buffer limit`);
75
+ }
76
+ let newlineIndex;
77
+ while ((newlineIndex = buffer.indexOf('\n', scanFrom)) !== -1) {
78
+ const line = buffer.slice(0, newlineIndex);
79
+ buffer = buffer.slice(newlineIndex + 1);
80
+ scanFrom = 0;
81
+ if (line === '' || line === '\r') {
82
+ const completed = record.take();
83
+ recordChars = 0;
84
+ if (completed.event !== null || completed.dataLines.length > 0 || completed.id !== undefined)
85
+ yield completed;
86
+ }
87
+ else {
88
+ recordChars += newlineIndex + 1;
89
+ record.line(line);
90
+ }
91
+ }
92
+ scanFrom = buffer.length;
93
+ }
94
+ for await (const chunk of source) {
95
+ buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
96
+ yield* drainCompleteLines();
97
+ }
98
+ buffer += decoder.decode();
99
+ yield* drainCompleteLines();
100
+ if (options.flushFinalFrame === false)
101
+ return;
102
+ // Flush whatever is left: either a trailing unterminated line (no final
103
+ // `\n` at all), or a fully-parsed-but-not-blank-line-terminated record.
104
+ // A provider closing immediately after its last byte must not silently lose it.
105
+ if (buffer.length > 0)
106
+ record.line(buffer);
107
+ const completed = record.take();
108
+ if (completed.event !== null || completed.dataLines.length > 0 || completed.id !== undefined)
109
+ yield completed;
110
+ }
111
+ /**
112
+ * Provider compatibility entry: drops reconnect metadata (meaningless for a single proxied
113
+ * request/response pair) and flushes unterminated EOF records. Keeps the published frame shape.
114
+ * @complexity Inherits decodeSseFrames' streaming cost and buffer size.
115
+ */
116
+ export async function* decodeSseStream({ source }, _options = {}) {
117
+ for await (const frame of decodeSseFrames({ source })) {
118
+ if (frame.event !== null || frame.dataLines.length > 0)
119
+ yield { event: frame.event, data: frame.data };
120
+ }
121
+ }
122
+ //# sourceMappingURL=sse.js.map
package/dist/redact.js CHANGED
@@ -102,11 +102,12 @@ const PATTERNS = [
102
102
  // Phone numbers. Tight US-leaning shape; a global PII detector would
103
103
  // need a real lib. We keep this so US-based test prompts ('call me at
104
104
  // (415) 555-...') don't ship. Note: no leading \b — '(' isn't a word
105
- // char, so a starting boundary would refuse to match `(415)`. We
106
- // require a non-digit (or start of string) before the run instead.
105
+ // char, so a starting boundary would refuse to match `(415)`. Reject
106
+ // adjacent word characters or hyphens so digit segments of UUIDs,
107
+ // hashes and identifiers stay readable; separators within phones still match.
107
108
  {
108
109
  name: 'phone',
109
- regex: /(?<!\d)(?:\+?\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}(?!\d)/g,
110
+ regex: /(?<![\w-])(?:\+?\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}(?![\w-])/g,
110
111
  },
111
112
  ];
112
113
  // Credit card sweep is special: a naive 13-19 digit run matches a lot of
@@ -139,6 +139,17 @@ export declare function isRecord(required: {
139
139
  }, _optional?: Record<string, never>): required is {
140
140
  value: Record<string, unknown>;
141
141
  };
142
+ /**
143
+ * Accepts non-null, non-array objects, including class instances and null-prototype records.
144
+ * This is a shape check, not plain-object validation or sanitization. Keep `isRecord`'s
145
+ * broader contract: existing tool readers deliberately continue to accept arrays.
146
+ * @param required.value - The untrusted value whose object shape is checked.
147
+ */
148
+ export declare function isNonArrayRecord(required: {
149
+ value: unknown;
150
+ }, _optional?: Record<string, never>): required is {
151
+ value: Record<string, unknown>;
152
+ };
142
153
  /** Narrows `ctx.input` to a record, refusing anything else. The first line of most handlers. */
143
154
  export declare function requireInputRecord(required: {
144
155
  input: unknown;
@@ -327,6 +338,8 @@ export declare function buildDomainRegistrations(spec: {
327
338
  catalog: ReadonlyMap<string, AgentToolDefinition>;
328
339
  handlers: Record<string, ToolHandler>;
329
340
  derivedRisk: DerivedRiskByToolId;
341
+ /** Host vocabulary and reviewed action policy; risk remains independently checked above. */
342
+ metadata?: import('./tool-metadata.js').ToolMetadataById;
330
343
  }, optional?: {
331
344
  unwiredToolIds?: ReadonlySet<string>;
332
345
  }): ToolRegistration[];
@@ -25,6 +25,7 @@
25
25
  * describes, where a reviewer reading the handler can see it.
26
26
  */
27
27
  import { ToolInputError } from "./tool-registry.js";
28
+ import { toolMetadataFor } from "./tool-metadata.js";
28
29
  /**
29
30
  * Audit provenance stamped onto every revision row a tool handler writes.
30
31
  *
@@ -103,6 +104,15 @@ export function isRecord(required, _optional = {}) {
103
104
  const { value } = required;
104
105
  return typeof value === "object" && value !== null;
105
106
  }
107
+ /**
108
+ * Accepts non-null, non-array objects, including class instances and null-prototype records.
109
+ * This is a shape check, not plain-object validation or sanitization. Keep `isRecord`'s
110
+ * broader contract: existing tool readers deliberately continue to accept arrays.
111
+ * @param required.value - The untrusted value whose object shape is checked.
112
+ */
113
+ export function isNonArrayRecord(required, _optional = {}) {
114
+ return isRecord(required) && !Array.isArray(required.value);
115
+ }
106
116
  /** Narrows `ctx.input` to a record, refusing anything else. The first line of most handlers. */
107
117
  export function requireInputRecord(required, _optional = {}) {
108
118
  const { input } = required;
@@ -371,11 +381,13 @@ export function buildDomainRegistrations(spec, optional = {}) {
371
381
  if (!catalogEntry.inputSchema) {
372
382
  throw new Error(`tool-registrations: wired tool '${id}' publishes no inputSchema — add one to its entry in ${spec.catalogModule} so the model gets a contract, or leave the tool unwired`);
373
383
  }
384
+ const metadata = catalogEntry.metadata ?? toolMetadataFor({ toolId: id, metadata: spec.metadata });
374
385
  registrations.push({
375
386
  descriptor: {
376
387
  id,
377
388
  description: catalogEntry.description,
378
389
  inputSchema: catalogEntry.inputSchema,
390
+ ...(metadata ? { metadata } : {}),
379
391
  // The ONE place a domain's declared risk becomes the descriptor flag every read-only gate
380
392
  // reads (`@jini-ai/core`'s `isReadOnlyTool`). Placed here rather than in each domain's
381
393
  // `tool-registrations.ts` for the same reason the drift tripwire is: twelve copies of this
@@ -0,0 +1,10 @@
1
+ import type { AgentToolDefinition } from "../agent-tools.js";
2
+ /**
3
+ * The Settings domain's fixed agent-tool catalog — three reads, three bounded writes, and two bulk/schema writes documented but never
4
+ * wired. See file header.
5
+ *
6
+ * @complexity O(1) — a fixed, statically-defined list.
7
+ * @overallScore 100
8
+ */
9
+ export declare function getSettingsAgentToolCatalog(): AgentToolDefinition[];
10
+ //# sourceMappingURL=agent-tools.d.ts.map