@jini-ai/core 0.3.0 → 0.4.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 (96) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +154 -14
  3. package/dist/agent-tools.d.ts +29 -0
  4. package/dist/agent-tools.js +2 -0
  5. package/dist/api-token-auth.d.ts +12 -5
  6. package/dist/api-token-auth.js +7 -7
  7. package/dist/bindings.d.ts +16 -5
  8. package/dist/bindings.js +7 -6
  9. package/dist/composition.d.ts +41 -0
  10. package/dist/composition.js +39 -0
  11. package/dist/contribution-registry.d.ts +16 -0
  12. package/dist/contribution-registry.js +12 -0
  13. package/dist/daemon.d.ts +12 -7
  14. package/dist/daemon.js +21 -9
  15. package/dist/gated-mutations/actor-identity.d.ts +20 -0
  16. package/dist/gated-mutations/actor-identity.js +31 -0
  17. package/dist/gated-mutations/gateway.d.ts +115 -0
  18. package/dist/gated-mutations/gateway.js +123 -0
  19. package/dist/gated-mutations/ports.d.ts +27 -0
  20. package/dist/gated-mutations/ports.js +2 -0
  21. package/dist/gated-mutations/token.d.ts +95 -0
  22. package/dist/gated-mutations/token.js +87 -0
  23. package/dist/gated-mutations.d.ts +5 -0
  24. package/dist/gated-mutations.js +5 -0
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.js +6 -1
  27. package/dist/model-facing-tool-errors.d.ts +37 -0
  28. package/dist/model-facing-tool-errors.js +71 -0
  29. package/dist/naming.d.ts +31 -0
  30. package/dist/naming.js +33 -0
  31. package/dist/origin-validation.d.ts +64 -19
  32. package/dist/origin-validation.js +43 -33
  33. package/dist/pack-lifecycle.d.ts +19 -12
  34. package/dist/pack-lifecycle.js +14 -14
  35. package/dist/pack.d.ts +41 -13
  36. package/dist/pack.js +2 -2
  37. package/dist/primitives/http.d.ts +57 -0
  38. package/dist/primitives/http.js +2 -0
  39. package/dist/primitives/ids.d.ts +8 -0
  40. package/dist/primitives/ids.js +5 -0
  41. package/dist/primitives/index.d.ts +8 -0
  42. package/dist/primitives/index.js +8 -0
  43. package/dist/primitives/json.d.ts +10 -0
  44. package/dist/primitives/json.js +2 -0
  45. package/dist/primitives/logger.d.ts +26 -0
  46. package/dist/primitives/logger.js +17 -0
  47. package/dist/primitives/paths.d.ts +16 -0
  48. package/dist/primitives/paths.js +38 -0
  49. package/dist/primitives/result.d.ts +9 -0
  50. package/dist/primitives/result.js +2 -0
  51. package/dist/primitives/time.d.ts +17 -0
  52. package/dist/primitives/time.js +13 -0
  53. package/dist/redact.d.ts +43 -8
  54. package/dist/redact.js +121 -50
  55. package/dist/registration-kit.d.ts +323 -0
  56. package/dist/registration-kit.js +396 -0
  57. package/dist/text/index.d.ts +3 -0
  58. package/dist/text/index.js +2 -0
  59. package/dist/text/sanitize-untrusted-text.d.ts +22 -0
  60. package/dist/text/sanitize-untrusted-text.js +108 -0
  61. package/dist/token.d.ts +29 -2
  62. package/dist/token.js +22 -2
  63. package/dist/tool-registry.d.ts +60 -14
  64. package/dist/tool-registry.js +24 -9
  65. package/dist/tool-tokens.js +1 -1
  66. package/package.json +51 -7
  67. package/dist/api-token-auth.d.ts.map +0 -1
  68. package/dist/api-token-auth.js.map +0 -1
  69. package/dist/bindings.d.ts.map +0 -1
  70. package/dist/bindings.js.map +0 -1
  71. package/dist/compose.typecheck.d.ts +0 -2
  72. package/dist/compose.typecheck.d.ts.map +0 -1
  73. package/dist/compose.typecheck.js +0 -29
  74. package/dist/compose.typecheck.js.map +0 -1
  75. package/dist/daemon.d.ts.map +0 -1
  76. package/dist/daemon.js.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/index.js.map +0 -1
  79. package/dist/internal.d.ts.map +0 -1
  80. package/dist/internal.js.map +0 -1
  81. package/dist/origin-validation.d.ts.map +0 -1
  82. package/dist/origin-validation.js.map +0 -1
  83. package/dist/pack-lifecycle.d.ts.map +0 -1
  84. package/dist/pack-lifecycle.js.map +0 -1
  85. package/dist/pack.d.ts.map +0 -1
  86. package/dist/pack.js.map +0 -1
  87. package/dist/principal.d.ts.map +0 -1
  88. package/dist/principal.js.map +0 -1
  89. package/dist/redact.d.ts.map +0 -1
  90. package/dist/redact.js.map +0 -1
  91. package/dist/token.d.ts.map +0 -1
  92. package/dist/token.js.map +0 -1
  93. package/dist/tool-registry.d.ts.map +0 -1
  94. package/dist/tool-registry.js.map +0 -1
  95. package/dist/tool-tokens.d.ts.map +0 -1
  96. package/dist/tool-tokens.js.map +0 -1
package/dist/redact.js CHANGED
@@ -1,19 +1,23 @@
1
1
  /**
2
2
  * @module redact
3
3
  *
4
- * Lexical secret / PII scrubber for telemetry payloads. Verbatim port of
5
- * OD's `apps/daemon/src/redact.ts` — no product-identity strings or
6
- * hardcoded env vars in the original, so no genericization was needed.
4
+ * Lexical secret / PII scrubber for telemetry payloads. Originally a verbatim port of
5
+ * OD's daemon redactor — no product-identity strings or hardcoded env vars in the original,
6
+ * so no genericization was needed. The shared kernel now also covers the CLI/provider rule
7
+ * union and accepts explicit host secrets; the original categories and rationale stay here.
7
8
  *
8
9
  * Runs before any prompt or assistant text is sent to an external
9
- * observability sink. The patterns here are intentionally conservative:
10
+ * observability sink. The default policy is intentionally conservative:
10
11
  * each one matches a well-defined token shape with extremely low
11
12
  * false-positive rate (API keys have a fixed prefix, JWTs have the
12
13
  * "header.payload.signature" triple, credit-card matches go through a Luhn
13
14
  * check). What this file does NOT do — and any caller-facing copy must
14
15
  * reflect that — is detect names, addresses, business secrets, or anything
15
16
  * that requires understanding the meaning of the surrounding text. That's
16
- * an ML / LLM problem this module doesn't take on.
17
+ * an ML / LLM problem this module doesn't take on. Callers can explicitly opt
18
+ * into `policy: 'aggressive'` to also mask bare 20+ character opaque runs;
19
+ * that CLI-style heuristic trades identifier/path readability for broader masking
20
+ * and must never be used by diagnostics or secret-scan guards.
17
21
  *
18
22
  * Output format: every match is replaced by `[REDACTED:<kind>]` so a
19
23
  * reviewer reading a trace can see exactly which category fired without
@@ -27,6 +31,27 @@
27
31
  * - AWS access key shape: 'AKIA' + 16 uppercase alphanumerics.
28
32
  * - Luhn algorithm (credit cards): https://en.wikipedia.org/wiki/Luhn_algorithm
29
33
  */
34
+ // Named once and reused (by SECRET_SHAPE_PATTERNS here and by the guard's ALLOWLIST) rather than repeated
35
+ // as a literal in multiple places.
36
+ export const PEM_PATTERN_NAME = "PEM private key block";
37
+ // Share data between static scans and runtime redactors without importing a CLI entry point:
38
+ // top-level argv/path handling in a scanner can crash a daemon that imports it. Keeping the
39
+ // vocabulary dependency-free also avoids making core redaction depend on feature code.
40
+ // Match each vendor's actual alphabet. Fixed-length patterns need lookaround boundaries so a
41
+ // credential-shaped substring of a longer inert value is not mistaken for a complete secret.
42
+ export const SECRET_SHAPE_PATTERNS = [
43
+ { name: "Google API key (AIza)", pattern: /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9_-])/ },
44
+ { name: "Anthropic API key (sk-ant-)", pattern: /(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{90,}/ },
45
+ { name: "generic sk- secret key (OpenAI-shaped)", pattern: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{40,}/ },
46
+ { name: "GitHub personal access token (classic, ghp_)", pattern: /(?<!\w)ghp_\w{36}(?!\w)/ },
47
+ { name: "GitHub OAuth token (gho_)", pattern: /(?<!\w)gho_\w{36}(?!\w)/ },
48
+ { name: "GitHub fine-grained PAT", pattern: /(?<!\w)github_pat_\w{80,}/ },
49
+ { name: "npm access token", pattern: /(?<!\w)npm_\w{36}(?!\w)/ },
50
+ { name: "Slack token", pattern: /(?<![A-Za-z0-9-])xox[baprs]-[A-Za-z0-9-]{12,}/ },
51
+ { name: "AWS access key ID", pattern: /(?<![0-9A-Z])AKIA[0-9A-Z]{16}(?![0-9A-Z])/ },
52
+ { name: "Fastmail app password (fm2_)", pattern: /(?<![A-Za-z0-9+/=])fm2_[A-Za-z0-9+/=]{20,}/ },
53
+ { name: PEM_PATTERN_NAME, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
54
+ ];
30
55
  // Order matters: list specific rules before more general ones. Langfuse
31
56
  // keys (`sk-lf-...`) would otherwise be eaten by the generic `sk-...`
32
57
  // rule and labeled as a generic OpenAI-style key.
@@ -40,7 +65,7 @@ const PATTERNS = [
40
65
  // covers most "sk-..." secrets a user might paste into a prompt.
41
66
  { name: 'sk_key', regex: /\bsk-(?:proj-|live-|test-|ant-)?[A-Za-z0-9_-]{20,}\b/g },
42
67
  // GitHub personal / OAuth / server / user-server / refresh tokens.
43
- { name: 'github_token', regex: /\bgh[opsur]_[A-Za-z0-9]{36,251}\b/g },
68
+ { name: 'github_token', regex: /\bgh[opsur]_[A-Za-z0-9]{36,}\b/g },
44
69
  // GitHub legacy 40-hex personal access tokens that ship with no prefix
45
70
  // are indistinguishable from a sha1 hash, so we don't try to match them
46
71
  // here — false positives would be brutal in commit logs / artifact slugs.
@@ -92,7 +117,7 @@ const API_KEY_HEADER = /(^|[^?&\w-])("?)(x-api-key|api-key|x-goog-api-key)\2(\s*
92
117
  const API_KEY_QUERY = /([?&](?:key|api_key|api-key)=)[^&#\s,;"']+/gi;
93
118
  // The length guard and the per-digit `d < 0 || d > 9` guard from the origin
94
119
  // were both dropped here (a documented deviation from the otherwise-verbatim
95
- // port — see source-map.md): `isLuhnValid` is private with exactly one call
120
+ // port — see archived provenance ledger): `isLuhnValid` is private with exactly one call
96
121
  // site (the `CARD_CANDIDATE.replace` callbacks below), and `CARD_CANDIDATE`'s
97
122
  // own regex (`\b(?:\d[ -]?){12,18}\d\b`) already guarantees any string
98
123
  // reaching here is 13-19 characters, and the preceding `match.replace(/\D/g,
@@ -117,74 +142,120 @@ function redactApiKeyHeaderValue(prefix, quote, name, separator, value) {
117
142
  const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
118
143
  return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
119
144
  }
145
+ // A labeled credential has its value masked even when too short to have a vendor token shape.
146
+ // Bare long opaque runs are opt-in CLI heuristics: UUIDs, hashes, model IDs and path segments
147
+ // share that alphabet, so the default telemetry policy must leave them readable. Whole
148
+ // correlation IDs and the delegated-tool route stay readable even in aggressive mode;
149
+ // appending secret characters defeats that exception. The host's ERR-XXXX-XXXX-XXXX-XXXX
150
+ // correlation ID uses uppercase hex and is meant to reach the model and be quoted to an
151
+ // operator; delegated-tool-calls is the route segment named in delegated tool failures.
152
+ // Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
153
+ export const LABELED_SECRET_RE = /\b(authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie)\b\s*[:=]\s*\S+/gi;
154
+ export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
155
+ export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls)$/;
156
+ // Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
157
+ // its historical lowercase marker contract and uses the unmodified lexical vocabulary.
158
+ // Reject a masked header/query value before matching, so a following field's credential
159
+ // remains available to the global scan rather than being consumed beside the marker.
160
+ const MASK_AWARE_LABELED_SECRET_RE = new RegExp(LABELED_SECRET_RE.source.replace(/\\S\+$/, "(?!Bearer\\s+\\[REDACTED:[^\\]]+\\](?:\\s|$))(?!\\[REDACTED:[^\\]]+\\][;&])(\\S+)"), LABELED_SECRET_RE.flags);
161
+ // Provider failures also echo short bearer values. Exclude our own marker on subsequent passes.
162
+ const SHORT_BEARER_RE = /(?<=Bearer\s+)[A-Za-z0-9_\-.+/=]+/gi;
163
+ const PROVIDER_API_KEY_HEADER = /(x-api-key|api-key|x-goog-api-key)\s*[:=]\s*([^\s,;"']+)/gi;
164
+ const PROVIDER_API_KEY_QUERY = /([?&]key=)([^&\s]+)/gi;
165
+ // URL userinfo and signed query values have explicit credential context; unlike opaque runs,
166
+ // they are safe to recognize in the conservative policy. Keep scheme/host and query names
167
+ // readable for debugging without exposing credentials that authorize access to the resource.
168
+ const URL_CREDENTIALS_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/\s?#@]+)@/gi;
169
+ const SIGNED_QUERY_RE = /([?&](?:signature|sig|x-amz-signature|x-goog-signature|access_token|refresh_token)=)([^&#\s,;"']+)/gi;
170
+ // Only an entire marker is already masked. A secret glued onto marker-like text still needs
171
+ // masking; a substring check would preserve credential suffixes in echoed header/query values.
172
+ function isMaskedValue(value) {
173
+ return /^["']?\[REDACTED:[^\]]+\]["']?$/.test(value);
174
+ }
120
175
  /**
121
176
  * Returns `input` with every recognised secret / PII pattern replaced by a
122
177
  * `[REDACTED:<kind>]` marker. Idempotent — re-running on already redacted
123
- * text only matches new tokens.
178
+ * text only matches new tokens. Defaults to conservative; `{ policy: 'aggressive' }`
179
+ * additionally masks bare 20+ character opaque runs. Exact secrets and extra patterns
180
+ * are honored under either policy.
124
181
  *
125
182
  * Empty / non-string input passes through unchanged so the caller can use
126
183
  * this as a no-op on optional fields.
127
184
  */
128
- export function redactSecrets(input) {
129
- if (!input)
130
- return input;
131
- let out = input;
132
- for (const { name, regex } of PATTERNS) {
133
- out = out.replace(regex, `[REDACTED:${name}]`);
134
- }
135
- out = out
136
- .replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => redactApiKeyHeaderValue(prefix, quote, name, separator, value))
137
- .replace(API_KEY_QUERY, '$1[REDACTED:api_key_query]');
138
- out = out.replace(CARD_CANDIDATE, (match) => {
139
- const digits = match.replace(/\D/g, '');
140
- return isLuhnValid(digits) ? '[REDACTED:credit_card]' : match;
141
- });
142
- return out;
185
+ export function redactSecrets({ input }, options = {}) {
186
+ return redactSecretsWithCounts({ input }, options).redacted;
143
187
  }
144
188
  /**
145
189
  * Same as {@link redactSecrets} but also returns per-category counts so the
146
190
  * caller can attach an audit summary to the trace metadata ('we stripped 2
147
191
  * keys + 1 email before send').
192
+ *
193
+ * Exact secrets run first, longest first: lexical rules or a shorter supplied secret must not
194
+ * split a known credential and leave a suffix behind. Opt-in aggressive opaque rules precede
195
+ * PII rules for the same reason (a numeric token can also contain phone/card candidates).
148
196
  */
149
- export function redactSecretsWithCounts(input) {
197
+ export function redactSecretsWithCounts({ input }, options = {}) {
150
198
  const counts = {};
151
199
  if (!input)
152
200
  return { redacted: input, counts };
153
201
  let out = input;
154
- for (const { name, regex } of PATTERNS) {
155
- let matched = 0;
156
- out = out.replace(regex, () => {
157
- matched += 1;
158
- return `[REDACTED:${name}]`;
159
- });
160
- if (matched > 0)
161
- counts[name] = matched;
202
+ const marker = (name) => {
203
+ counts[name] = (counts[name] ?? 0) + 1;
204
+ return `[REDACTED:${name}]`;
205
+ };
206
+ const applyPattern = ({ name, regex }) => {
207
+ // Clone host/global regexes so lastIndex state never leaks across requests. Named extra
208
+ // patterns use the same global replacement contract as the built-in vocabulary.
209
+ const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
210
+ out = out.replace(new RegExp(regex.source, flags), () => marker(name));
211
+ };
212
+ const secrets = [...new Set((options.exactSecrets ?? []).filter((secret) => typeof secret === 'string' && secret.length > 0))].sort((a, b) => b.length - a.length);
213
+ for (const secret of secrets) {
214
+ const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
215
+ out = out.replace(new RegExp(escaped, 'g'), () => marker('exact_secret'));
216
+ }
217
+ out = out.replace(URL_CREDENTIALS_RE, (match, scheme, userinfo) => isMaskedValue(userinfo) ? match : `${scheme}${marker('url_credentials')}@`);
218
+ out = out.replace(SIGNED_QUERY_RE, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('signed_query')}`);
219
+ const piiNames = new Set(['email', 'ipv4', 'phone']);
220
+ for (const pattern of PATTERNS) {
221
+ if (!piiNames.has(pattern.name))
222
+ applyPattern(pattern);
162
223
  }
163
- let apiKeyHeaderCount = 0;
224
+ for (const shape of SECRET_SHAPE_PATTERNS)
225
+ applyPattern({ name: 'credential', regex: shape.pattern });
226
+ for (const pattern of options.extraPatterns ?? [])
227
+ applyPattern(pattern);
164
228
  out = out.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
165
- apiKeyHeaderCount += 1;
229
+ // Already masked values are not new credentials and should not inflate the audit count.
230
+ if (isMaskedValue(value))
231
+ return `${prefix}${quote}${name}${quote}${separator}${value}`;
232
+ marker('api_key_header');
166
233
  return redactApiKeyHeaderValue(prefix, quote, name, separator, value);
167
234
  });
168
- if (apiKeyHeaderCount > 0)
169
- counts.api_key_header = apiKeyHeaderCount;
170
- let apiKeyQueryCount = 0;
171
- out = out.replace(API_KEY_QUERY, (_match, prefix) => {
172
- apiKeyQueryCount += 1;
173
- return `${prefix}[REDACTED:api_key_query]`;
235
+ // The provider rule includes a query fragment in an echoed key; mask that whole value
236
+ // before the narrower core query rule can split it and leave a credential suffix behind.
237
+ out = out.replace(PROVIDER_API_KEY_QUERY, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('api_key_query')}`);
238
+ out = out.replace(API_KEY_QUERY, (match, prefix) => isMaskedValue(match.slice(prefix.length)) ? match : `${prefix}${marker('api_key_query')}`);
239
+ out = out.replace(SHORT_BEARER_RE, () => marker('bearer_token'));
240
+ out = out.replace(PROVIDER_API_KEY_HEADER, (match, name, value) => isMaskedValue(value) ? match : `${name}: ${marker('api_key_header')}`);
241
+ out = out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, _label, value) => {
242
+ // Replace only the captured value, retaining labels even when no whitespace separates
243
+ // them. Glued credential suffixes fail the complete-marker check and remain maskable.
244
+ if (isMaskedValue(value))
245
+ return match;
246
+ return `${match.slice(0, -value.length)}${marker('labeled_secret')}`;
174
247
  });
175
- if (apiKeyQueryCount > 0)
176
- counts.api_key_query = apiKeyQueryCount;
177
- let cardCount = 0;
248
+ if (options.policy === 'aggressive') {
249
+ out = out.replace(OPAQUE_TOKEN_RE, (match) => NON_SECRET_TOKEN_RE.test(match) ? match : marker('opaque_token'));
250
+ }
251
+ for (const pattern of PATTERNS) {
252
+ if (piiNames.has(pattern.name))
253
+ applyPattern(pattern);
254
+ }
178
255
  out = out.replace(CARD_CANDIDATE, (match) => {
179
256
  const digits = match.replace(/\D/g, '');
180
- if (isLuhnValid(digits)) {
181
- cardCount += 1;
182
- return '[REDACTED:credit_card]';
183
- }
184
- return match;
257
+ return isLuhnValid(digits) ? marker('credit_card') : match;
185
258
  });
186
- if (cardCount > 0)
187
- counts.credit_card = cardCount;
188
259
  return { redacted: out, counts };
189
260
  }
190
261
  //# sourceMappingURL=redact.js.map
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @file The cross-domain half of the agent-tool wiring split: everything a domain's own
3
+ * `tool-registrations.ts` needs that is NOT specific to that domain.
4
+ *
5
+ * Why this file exists at all. Every domain's wiring repeats the same five moves — index its
6
+ * catalog by id, read a handful of fields off `ctx.input`, run the tool's permission check,
7
+ * decorate a shape rejection with the published schema, and then loop the handler map into
8
+ * `ToolRegistration`s behind a build-time risk/schema gate. Those five moves were previously copied
9
+ * per domain inside one shared `assistant/tool-registrations.ts`, which is exactly why that file
10
+ * grew past 2000 lines and why three independent domain slices could not be developed in parallel
11
+ * without colliding on it. Each domain now owns a `tool-registrations.ts` next to its own code and
12
+ * imports the shared moves from here; `assistant/tool-registrations.ts` is reduced to the
13
+ * aggregator that assembles them.
14
+ *
15
+ * The membership test for anything in this file is "would a fourth, not-yet-written domain need
16
+ * it?". Domain-shaped things deliberately stay out: each domain's model-facing `to*View`
17
+ * projections, its dependency-bag builder, its own `DERIVED_RISK_BY_TOOL_ID` slice (a claim about
18
+ * that domain's handlers, so it is maintained where those handlers live), and its unwired-tool set.
19
+ *
20
+ * What is deliberately NOT abstracted here is the authorization DECISION. The host's permission helper
21
+ * performs a check a caller asks for; it never decides that a check is needed. The "one
22
+ * evaluator" rule is a per-tool fact — some domains self-enforce inside their service function and
23
+ * must NOT be double-checked here, others have no service-layer gate at all and must be checked at
24
+ * the handler — and that judgement stays recorded in the domain file next to the handler it
25
+ * describes, where a reviewer reading the handler can see it.
26
+ */
27
+ import { type ToolExecutionOptions, type ToolHandler, type ToolRegistration } from "./tool-registry.js";
28
+ import type { Result } from "./primitives/index.js";
29
+ import type { AgentToolSideEffect, AgentToolDefinition } from "./agent-tools.js";
30
+ /**
31
+ * Catalog entries use the kernel's AgentToolDefinition. The distinct deletion classification
32
+ * and its rationale live beside that contract in agent-tools.ts: every wiring gate compares
33
+ * classifications for equality, so deleting durable state cannot pass as a milder edit.
34
+ *
35
+ * This remains a structural minimum. A domain may tighten its own catalog, for example by
36
+ * requiring inputSchema for every wired entry or declaring authorization.orPermission for
37
+ * real OR gates. Neither requires importing another domain or loosening its own contract.
38
+ *
39
+ * A domain's independent classification of what its own handlers actually do, keyed by tool id.
40
+ *
41
+ * The point of the type is the point of the pattern: a tool's risk must not be self-declared. A
42
+ * catalog entry that quietly downgraded itself to `sideEffects:'none'` would otherwise become
43
+ * "safe" by editing one word in a file that carries no knowledge of which domain function runs.
44
+ * Each domain's `tool-registrations.ts` maintains its slice next to the handlers it describes, and
45
+ * {@link assertToolIsWirable} refuses to build when the two disagree. An id absent from the map
46
+ * cannot be wired at all, so the conservative default is "refuse", not "assume safe".
47
+ */
48
+ export type DerivedRiskByToolId = ReadonlyMap<string, AgentToolSideEffect>;
49
+ /**
50
+ * Audit provenance stamped onto every revision row a tool handler writes.
51
+ *
52
+ * A constant, not `ctx.principal`'s own kind, and that is the point: `ctx.principal.id` is the
53
+ * HUMAN admin's principal id — the host's proxy reads it from the browser session and stamps it
54
+ * into the run's `contextRef`, and the daemon hands it back to the handler. So a content-type change made by the assistant and one the same person made by clicking
55
+ * through the admin UI record an identical `actorId`. What distinguishes them is the path, and
56
+ * reaching a `tool-registrations.ts` handler IS the agent path — those handlers are only ever
57
+ * invoked by `@jini-ai/daemon`'s `ToolExecutor` during a run. `'agent'` is therefore a fact about
58
+ * the call site, not a default.
59
+ */
60
+ export declare const AGENT_TOOL_PRINCIPAL_KIND: "agent";
61
+ /**
62
+ * Actor-class rules that need a human confirmer.
63
+ *
64
+ * `confirmer-must-equal-own-delegatedBy` means "a human must confirm this, and the confirmer must
65
+ * be the principal who delegated". The kit never delivers that through `descriptor.requiresConfirmation`:
66
+ * without a host `ExecutionDelegate`, that flag parks the execution on a promise only
67
+ * `resumeConfirmation` can settle, and nothing would call it (unbounded, too, because
68
+ * `descriptor.timeoutMs`'s timer is armed only AFTER the confirmation await).
69
+ *
70
+ * So a tool carrying this rule is wirable only when its handler was built with
71
+ * {@link humanConfirmedHandler}, which asks the human through the host's own confirmation transport
72
+ * and runs the action only on an explicit yes. Any other handler fails the build.
73
+ */
74
+ export declare const ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT: Set<string>;
75
+ /**
76
+ * The human who confirmed, as {@link humanConfirmedHandler} hands it to `run`: always the run's own
77
+ * delegating principal (`ctx.principal.id`, see {@link AGENT_TOOL_PRINCIPAL_KIND}), always
78
+ * `kind: "user"`. The only thing a `run` step may pass to a confirm step as its confirmer.
79
+ */
80
+ export interface HumanConfirmer {
81
+ readonly id: string;
82
+ readonly kind: "user";
83
+ }
84
+ /** What the host's transport reports back. Only exactly `confirmed: true` counts as a yes. */
85
+ export type HumanConfirmationAnswer = {
86
+ confirmed: true;
87
+ } | {
88
+ confirmed: false;
89
+ result: unknown;
90
+ };
91
+ /**
92
+ * Builds the one kind of handler a `confirmer-must-equal-own-delegatedBy` tool may be wired with
93
+ * (see {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}).
94
+ *
95
+ * The order is fixed here, not left to each domain: `prepare` (validate, pre-authorize, derive what
96
+ * the human will see) → `askHuman` (the host's transport, e.g. an MCP-UI dialog) → `run`, only when
97
+ * the answer is exactly `{ confirmed: true }`. The decision never comes from `ctx.input`, so nothing
98
+ * the model sends can stand in for the human. `run` gets the confirmer as the delegating human.
99
+ *
100
+ * @param steps.prepare - Reads input and derives what the human is asked about. May throw.
101
+ * @param steps.askHuman - Asks the human through the host's transport. Throws when no human can be
102
+ * asked; a no-answer returns `{ confirmed: false, result }` and `result` is the tool's result.
103
+ * @param steps.run - Performs the action on a yes.
104
+ * Each step receives the invoking transport's execution options as its second argument so
105
+ * the confirmation dialog can be emitted before awaiting the human's answer.
106
+ * @returns A handler {@link assertToolIsWirable} accepts for a tool carrying the rule.
107
+ * @complexity O(1) plus the three steps.
108
+ */
109
+ export declare function humanConfirmedHandler<TPrepared>(steps: {
110
+ prepare: (required: {
111
+ ctx: Parameters<ToolHandler>[0];
112
+ }, optional?: ToolExecutionOptions) => Promise<TPrepared>;
113
+ askHuman: (required: {
114
+ ctx: Parameters<ToolHandler>[0];
115
+ prepared: TPrepared;
116
+ }, optional?: ToolExecutionOptions) => Promise<HumanConfirmationAnswer>;
117
+ run: (required: {
118
+ ctx: Parameters<ToolHandler>[0];
119
+ prepared: TPrepared;
120
+ confirmer: HumanConfirmer;
121
+ }, optional?: ToolExecutionOptions) => Promise<unknown>;
122
+ }, _optional?: Record<string, never>): ToolHandler;
123
+ /**
124
+ * Indexes a domain catalog by tool id — the single lookup used for descriptors, risk metadata, and
125
+ * schema-bearing error messages.
126
+ *
127
+ * @param required.catalog - The domain's own `agent-tools.ts` catalog array.
128
+ * @returns The catalog keyed by `name`.
129
+ * @complexity O(n) once at module load, O(1) per subsequent lookup.
130
+ * @overallScore 100
131
+ */
132
+ export declare function indexCatalogById<T extends {
133
+ name: string;
134
+ }>(required: {
135
+ catalog: readonly T[];
136
+ }, _optional?: Record<string, never>): ReadonlyMap<string, T>;
137
+ export declare function isRecord(required: {
138
+ value: unknown;
139
+ }, _optional?: Record<string, never>): required is {
140
+ value: Record<string, unknown>;
141
+ };
142
+ /** Narrows `ctx.input` to a record, refusing anything else. The first line of most handlers. */
143
+ export declare function requireInputRecord(required: {
144
+ input: unknown;
145
+ }, _optional?: Record<string, never>): Record<string, unknown>;
146
+ export declare function requireString(required: {
147
+ input: Record<string, unknown>;
148
+ key: string;
149
+ }, _optional?: Record<string, never>): string;
150
+ export declare function requireNumber(requiredArgs: {
151
+ input: Record<string, unknown>;
152
+ key: string;
153
+ }, optionalArgs?: Record<string, never>): number;
154
+ export declare function requireObject(requiredArgs: {
155
+ input: Record<string, unknown>;
156
+ key: string;
157
+ }, optionalArgs?: Record<string, never>): Record<string, unknown>;
158
+ export declare function optionalString(required: {
159
+ input: Record<string, unknown>;
160
+ key: string;
161
+ }, _optional?: Record<string, never>): string | undefined;
162
+ export declare function optionalNumber(requiredArgs: {
163
+ input: Record<string, unknown>;
164
+ key: string;
165
+ }, optionalArgs?: Record<string, never>): number | undefined;
166
+ export declare function optionalBoolean(requiredArgs: {
167
+ input: Record<string, unknown>;
168
+ key: string;
169
+ }, optionalArgs?: Record<string, never>): boolean | undefined;
170
+ /**
171
+ * The reader for a parameterless tool: `ctx.input` may be omitted entirely or passed as `{}`,
172
+ * nothing else. Refusing a populated object is deliberate — silently ignoring keys would teach a
173
+ * model that a filter it invented was applied.
174
+ */
175
+ export declare function requireNoInput(required: {
176
+ input: unknown;
177
+ }, _optional?: Record<string, never>): void;
178
+ /**
179
+ * Wraps a `Result<T, Error>`-returning domain call as a value: `ToolExecutor` treats a thrown error
180
+ * as a `'failed'` execution, so an `{ok:false}` becomes a throw rather than a silently-swallowed
181
+ * value.
182
+ */
183
+ export declare function fromResult<T>(requiredArgs: {
184
+ fn: () => Promise<Result<T>>;
185
+ }, optionalArgs?: Record<string, never>): Promise<T>;
186
+ /**
187
+ * Re-throws a rejection with the tool's published `inputSchema` appended, so a model can correct
188
+ * its call in a single turn instead of guessing the rest of the shape from one field's complaint.
189
+ *
190
+ * `isShapeRejection` is the domain's own decision and has no default: only errors a DIFFERENT input
191
+ * would fix are decorated. A `ForbiddenError` or a slug conflict is not a shape problem, and
192
+ * appending a schema to one would be noise the model must read past — worse, it would imply the
193
+ * call is retryable when it is not.
194
+ *
195
+ * @param spec.toolId - The tool whose schema is published with the failure.
196
+ * @param spec.catalog - The domain catalog, consulted for that schema.
197
+ * @param spec.isShapeRejection - Predicate selecting the errors worth decorating.
198
+ * @param spec.fn - The domain call.
199
+ * @throws The original error unchanged when it is not a shape rejection, or a decorated `Error`
200
+ * when it is. A tool whose catalog entry publishes no schema still gets the retry-is-futile note.
201
+ * @complexity O(1) beyond the wrapped call, plus the schema's own serialization on the failure path.
202
+ * @overallScore 100
203
+ */
204
+ export declare function withSchemaOnRejection<T>(spec: {
205
+ toolId: string;
206
+ catalog: ReadonlyMap<string, AgentToolDefinition>;
207
+ isShapeRejection: (required: {
208
+ error: unknown;
209
+ }) => boolean;
210
+ fn: () => Promise<T>;
211
+ }, _optional?: Record<string, never>): Promise<T>;
212
+ /**
213
+ * The synchronous half of {@link withSchemaOnRejection}, for a domain that validates BEFORE the
214
+ * call rather than inside it (content-types parses `fields` at a boundary parser, so its rejection
215
+ * never reaches a `try` around a domain call).
216
+ *
217
+ * @returns The `Error` to throw — returned rather than thrown so the caller's own `throw` keeps
218
+ * TypeScript's control-flow narrowing at the call site.
219
+ * @complexity O(s) in the serialized schema size, on the failure path only.
220
+ * @overallScore 100
221
+ */
222
+ export declare function decorateWithSchema(params: {
223
+ toolId: string;
224
+ catalog: ReadonlyMap<string, AgentToolDefinition>;
225
+ message: string;
226
+ }): Error;
227
+ /**
228
+ * Build-time gate on a single tool's risk metadata: refuses to wire a tool whose declared
229
+ * `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
230
+ * whose id that layer has not classified at all, or whose `actorClassRule` needs a human confirmer
231
+ * its handler does not ask for.
232
+ *
233
+ * Throws rather than warning, and at registration time rather than call time: a metadata
234
+ * inconsistency is a developer error that should stop the daemon booting, not a runtime condition
235
+ * to degrade around.
236
+ *
237
+ * @param params.toolId - The wired tool id.
238
+ * @param params.catalogEntry - Its `agent-tools.ts` entry, the declared side of the comparison.
239
+ * @param params.derivedRisk - The wiring layer's independent classification, the derived side.
240
+ * @param params.handler - The handler being wired; checked only for a tool carrying a rule in
241
+ * {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
242
+ * @throws {Error} If the tool is unclassified, misclassified, or needs a human confirmer its handler
243
+ * does not ask for.
244
+ * @complexity O(1) — two map/set lookups.
245
+ * @overallScore 100
246
+ */
247
+ export declare function assertToolIsWirable(params: {
248
+ toolId: string;
249
+ catalogEntry: AgentToolDefinition;
250
+ derivedRisk: DerivedRiskByToolId;
251
+ }, optional?: {
252
+ handler?: ToolHandler;
253
+ }): void;
254
+ /**
255
+ * Turns one domain's handler map into `ToolRegistration`s, applying every build-time gate the
256
+ * wiring layer owes each tool, and tripwiring on any catalog entry that is neither wired nor
257
+ * explicitly declared unwired.
258
+ *
259
+ * The tripwire is the reason this is one shared function rather than a loop each domain copies.
260
+ * "Add a catalog entry, forget to wire it" is the failure this layer is meant to make impossible,
261
+ * and a per-domain copy of the check is a per-domain opportunity to get it subtly wrong — one of
262
+ * the copies this replaced omitted the schema assertion, another omitted the unwired-set escape
263
+ * hatch. Silence is never the outcome: an unwired entry either appears in `unwiredToolIds` with a
264
+ * recorded reason next to it, or the build fails.
265
+ *
266
+ * `policy.authorize` is a pass-through `'allow'` for every registration, and that is by design
267
+ * rather than an omission: each tool's permission is evaluated exactly once — inside its domain
268
+ * function for the self-enforcing domains, or by the handler's own host permission helper
269
+ * call for the domains whose gate lives in the route. A check here would be a SECOND evaluator of
270
+ * the same rule, and a `ToolPolicy`-only check would be bypassable by any future non-tool caller of
271
+ * the same domain function, which is precisely why the chokepoint owns the gate.
272
+ *
273
+ * `descriptor.requiresConfirmation` is deliberately never set — a tool that needs a human confirm
274
+ * asks inside its own {@link humanConfirmedHandler}; see
275
+ * {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
276
+ *
277
+ * @param spec.domain - Human-readable domain name, used only in failure messages.
278
+ * @param spec.catalogModule - Path of the catalog file, named in the drift message so the failure
279
+ * points at the file to edit.
280
+ * @param spec.catalog - That domain's catalog indexed by id (see {@link indexCatalogById}).
281
+ * @param spec.handlers - Tool id to handler. Registration order follows this map's key order.
282
+ * @param spec.derivedRisk - The domain's own risk classification (see {@link DerivedRiskByToolId}).
283
+ * @param optional.unwiredToolIds - Catalog ids deliberately not wired. Omit when the domain wires its
284
+ * entire catalog, which makes ANY unwired entry a build failure.
285
+ * @throws {Error} On catalog drift, an unclassified/misclassified tool, a wired tool publishing no
286
+ * `inputSchema`, or a catalog entry that is neither wired nor declared unwired.
287
+ * @complexity O(h + c) in the handler and catalog counts.
288
+ * @overallScore 100
289
+ */
290
+ export declare function buildDomainRegistrations(spec: {
291
+ domain: string;
292
+ catalogModule: string;
293
+ catalog: ReadonlyMap<string, AgentToolDefinition>;
294
+ handlers: Record<string, ToolHandler>;
295
+ derivedRisk: DerivedRiskByToolId;
296
+ }, optional?: {
297
+ unwiredToolIds?: ReadonlySet<string>;
298
+ }): ToolRegistration[];
299
+ /**
300
+ * Folds every domain's risk slice into the one map the assistant-level
301
+ * `assertRiskMetadataIsWirable(toolId, catalogEntry)` consults, refusing any id two domains both
302
+ * claim.
303
+ *
304
+ * That refusal is the structural answer to the failure this restructure was prompted by: three
305
+ * domain slices were developed in parallel, each checking its new ids only against the ids that
306
+ * existed before it started, so a name two of them both picked would have been discovered at
307
+ * runtime as a silently double-registered tool. A cross-domain id collision now fails the build.
308
+ * The one real collision in the current catalogs — `backup_create_restore_point`, declared by both
309
+ * Database and Recovery — passes because Recovery declares it unwired and therefore contributes no
310
+ * risk entry for it, which is exactly the resolution this check is meant to force.
311
+ *
312
+ * @param slices - Each domain's name (for the failure message) and its own risk map.
313
+ * @throws {Error} If two domains classify the same tool id.
314
+ * @complexity O(t) in the total classified-tool count.
315
+ * @overallScore 100
316
+ */
317
+ export declare function mergeDerivedRiskMaps(requiredArgs: {
318
+ slices: readonly {
319
+ domain: string;
320
+ risk: DerivedRiskByToolId;
321
+ }[];
322
+ }, optionalArgs?: Record<string, never>): DerivedRiskByToolId;
323
+ //# sourceMappingURL=registration-kit.d.ts.map