@jini-ai/daemon 0.5.3 → 0.6.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 (77) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/agent-executor/contracts.d.ts +45 -14
  3. package/dist/agent-executor/contracts.js +0 -9
  4. package/dist/agent-executor/event-translation.d.ts +2 -2
  5. package/dist/agent-executor/event-translation.js +0 -2
  6. package/dist/agent-executor/events.d.ts +26 -6
  7. package/dist/agent-executor/events.js +198 -11
  8. package/dist/agent-executor/handoff.d.ts +1 -1
  9. package/dist/agent-executor/handoff.js +0 -1
  10. package/dist/agent-executor/index.d.ts +1 -1
  11. package/dist/agent-executor/launch.d.ts +1 -1
  12. package/dist/agent-executor/launch.js +47 -3
  13. package/dist/agent-executor/mcp-bridge.d.ts +2 -2
  14. package/dist/agent-executor/mcp-bridge.js +19 -8
  15. package/dist/agent-executor/model-receipts.d.ts +6 -0
  16. package/dist/agent-executor/model-receipts.js +26 -0
  17. package/dist/agent-executor.d.ts +7 -1665
  18. package/dist/agent-executor.js +7 -3007
  19. package/dist/core/read-only-tools.d.ts +130 -0
  20. package/dist/core/read-only-tools.js +115 -0
  21. package/dist/core/tool-audit.d.ts +131 -0
  22. package/dist/core/tool-audit.js +113 -0
  23. package/dist/http/active-context.d.ts +2 -2
  24. package/dist/http/agents.d.ts +8 -2
  25. package/dist/http/agents.js +6 -2
  26. package/dist/http/component-catalog.d.ts +2 -2
  27. package/dist/http/connectors.d.ts +17 -17
  28. package/dist/http/daemon-status.d.ts +2 -2
  29. package/dist/http/db-ops.d.ts +3 -3
  30. package/dist/http/delegated-tools.d.ts +1 -1
  31. package/dist/http/delegated-tools.js +1 -1
  32. package/dist/http/frontend-sessions.d.ts +1 -1
  33. package/dist/http/health.d.ts +6 -6
  34. package/dist/http/host-tools.d.ts +2 -2
  35. package/dist/http/index.d.ts +2 -2
  36. package/dist/http/index.js +1 -1
  37. package/dist/http/media.d.ts +4 -4
  38. package/dist/http/memory.d.ts +15 -15
  39. package/dist/http/remote-run-events.d.ts +2 -2
  40. package/dist/http/research.d.ts +1 -1
  41. package/dist/http/route-manifest.js +2 -2
  42. package/dist/http/routines.d.ts +7 -7
  43. package/dist/http/run-credentials.d.ts +2 -2
  44. package/dist/http/run-credentials.js +2 -2
  45. package/dist/http/runs.d.ts +31 -6
  46. package/dist/http/runs.js +36 -1
  47. package/dist/http/terminals.d.ts +6 -6
  48. package/dist/http/tool-catalog.d.ts +2 -2
  49. package/dist/http/xai.d.ts +6 -6
  50. package/dist/index.d.ts +75 -571
  51. package/dist/index.js +75 -571
  52. package/dist/read-only-tools.d.ts +1 -129
  53. package/dist/read-only-tools.js +2 -114
  54. package/dist/run-credentials.d.ts +47 -23
  55. package/dist/run-credentials.js +24 -8
  56. package/dist/scheduler.d.ts +5 -0
  57. package/dist/scheduler.js +13 -1
  58. package/dist/session-coordination.d.ts +58 -24
  59. package/dist/session-coordination.js +40 -12
  60. package/dist/store/event-log/sqlite.js +3 -3
  61. package/dist/surface-exchanges.d.ts +207 -61
  62. package/dist/surface-exchanges.js +167 -20
  63. package/dist/tool-audit.d.ts +1 -113
  64. package/dist/tool-audit.js +2 -94
  65. package/dist/tool-recovery/diagnostics.d.ts +132 -0
  66. package/dist/tool-recovery/diagnostics.js +110 -0
  67. package/dist/tool-recovery/federated-refusal.d.ts +92 -0
  68. package/dist/tool-recovery/federated-refusal.js +152 -0
  69. package/dist/tool-recovery/index.d.ts +6 -0
  70. package/dist/tool-recovery/index.js +6 -0
  71. package/dist/tool-recovery/recovery.d.ts +49 -0
  72. package/dist/tool-recovery/recovery.js +419 -0
  73. package/dist/tool-recovery/redaction.d.ts +166 -0
  74. package/dist/tool-recovery/redaction.js +192 -0
  75. package/dist/tool-recovery/stack.d.ts +69 -0
  76. package/dist/tool-recovery/stack.js +62 -0
  77. package/package.json +35 -12
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file The general tool-failure contract (2026-09-01): what ANY tool attaches to a failed outcome
3
+ * so a caller — human, agent, or an automated retry loop — has something to reason about instead of a
4
+ * bare status code with nothing behind it.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * Hand-writing a bespoke recovery path per failure mode does not scale — there will always be more
9
+ * flaws, gaps, and provider quirks than anyone can anticipate and special-case individually. The
10
+ * leverage is not a smarter model reading a smarter error message; it is a SHARED SHAPE every tool's
11
+ * failure can be poured into, so one generic loop — read the failure, ask the human through a
12
+ * surface, apply the answer, retry once — can handle a failure mode nobody wrote code for, using the
13
+ * identical mechanism that already handles one somebody did.
14
+ *
15
+ * ## The four things a diagnostic carries
16
+ *
17
+ * 1. **What failed.** Deliberately NOT modeled in this shared contract — it is always domain-specific,
18
+ * and forcing every failure mode into one generic "what failed" string or object would either
19
+ * flatten real structure that needs to stay queryable (a concrete diagnostic's own fact fields —
20
+ * see {@link AuthFailureDiagnostic}'s `schemeSent`/`usernameStored` in `features/custom-credentials/
21
+ * credentialed-request.ts` for the worked example) or force a restructuring this file has no
22
+ * business demanding of every domain that wants to adopt it. A concrete diagnostic type carries
23
+ * its own facts as its own additional fields, declared alongside the two this contract does own.
24
+ * 2. **The best hypothesis why** — folded into {@link ToolFailureDiagnostic.hint}.
25
+ * 3. **What input or state would change the outcome** — also folded into `hint`, not a separate field;
26
+ * see that property's own doc for why the two live together.
27
+ * 4. **Whether a registered tool can supply it** — {@link ToolFailureDiagnostic.remedyToolId}.
28
+ *
29
+ * ## Built from one worked example, not designed from zero
30
+ *
31
+ * `AuthFailureDiagnostic` is this contract's first instance, extracted AFTER it shipped and proved
32
+ * itself against a real incident — not designed up front from a guess at what failures might someday
33
+ * look like. See that type's own file header, "Authentication-failure diagnostics", for the incident
34
+ * and for the exact honesty contract every one of its fields is held to. This file generalizes its
35
+ * SHAPE, not its content: the fields that type adds (`schemeSent`, `usernameStored`) and the exact
36
+ * wording of its `hint` are unchanged by this extraction.
37
+ *
38
+ * ## The honesty contract every field is held to
39
+ *
40
+ * - **Silence is the default, not an afterthought.** Both fields below are optional, and absent
41
+ * together far more often than present: a diagnostic type should fill them in for the ONE narrow
42
+ * shape it can honestly speak to, and stay silent for every other shape — including shapes that
43
+ * look superficially similar. Repeating advice that has already been tried and failed, or guessing
44
+ * at a cause this diagnostic cannot actually tell apart from some other one, is a false lead, not
45
+ * merely an unhelpful one.
46
+ * - **Never a claim.** `hint`, when present, is hedged ("may"/"might"/"could") — a hypothesis for a
47
+ * human or agent to try, never an assertion of the real cause.
48
+ * - **Additive, never a replacement.** A diagnostic rides ALONGSIDE the tool's own real failure output
49
+ * (a provider's response body, a thrown error's message) — it never suppresses or reinterprets it.
50
+ * - **`hint` and `remedyToolId` travel as a pair, never `remedyToolId` alone.** A tool id with nothing
51
+ * explaining why it might help is a suggestion with no argument behind it, not a diagnostic. The
52
+ * reverse is fine: `hint` with no `remedyToolId` simply means no registered tool can supply the fix
53
+ * yet — the hypothesis is still worth surfacing to a human who can act on it by hand.
54
+ *
55
+ * ## The one-cycle guard
56
+ *
57
+ * `remedyToolId` is a POINTER — a tool id, nothing else. It carries no callback, no arguments, and no
58
+ * instruction to invoke it automatically. That is deliberate: the consuming loop (ask the human → apply
59
+ * the answer through the named tool → retry ONCE) owns the decision of whether and how to act on it,
60
+ * and owns the guard against a second cycle. A diagnostic that carried more than an id — an executable
61
+ * action, say — would let that guard slip into this file instead of staying where the actual retry
62
+ * happens, turning a bug in this contract into a genuine loop instead of a single bounded retry.
63
+ *
64
+ * ## Architectural role
65
+ *
66
+ * `contracts/core`, domain-agnostic — it names no post, page, credential, or other domain concept, so
67
+ * a failure mode in any feature can adopt it without this file growing a dependency on that feature.
68
+ */
69
+ /**
70
+ * Every diagnostic a tool has issued, by identity. Membership is the proof that a `{hint,
71
+ * remedyToolId}` object came from tool code reporting a failure, not from data the tool merely
72
+ * returned: stored content (a post's `bodyJson`, a widget's props) is plain JSON and can carry the
73
+ * same two fields, and the recovery loop turns them into a dialog that runs `remedyToolId`. A parsed
74
+ * or stored object can never be a member. Weak, so an issued diagnostic is collected with its result.
75
+ */
76
+ const issuedDiagnostics = new WeakSet();
77
+ /**
78
+ * Marks `diagnostic` as issued by the calling tool for a real failure, so the recovery loop
79
+ * (`assistant/tool-failure-recovery.ts`) may act on it. Returns the same object. Call it where the
80
+ * diagnostic is built, and return that object itself: a copy (spread, JSON round trip) is not issued.
81
+ *
82
+ * @complexity O(1).
83
+ */
84
+ export function issueToolFailureDiagnostic({ diagnostic }, _optional = {}) {
85
+ issuedDiagnostics.add(diagnostic);
86
+ return diagnostic;
87
+ }
88
+ /**
89
+ * True only for an object {@link issueToolFailureDiagnostic} marked.
90
+ *
91
+ * @complexity O(1).
92
+ */
93
+ export function isIssuedToolFailureDiagnostic({ value }, _optional = {}) {
94
+ return typeof value === "object" && value !== null && issuedDiagnostics.has(value);
95
+ }
96
+ /** Only card metadata travels through recovery; secret values belong to the parked card. */
97
+ export function issueCredentialSetup(required, _optional = {}) {
98
+ for (const [key, value] of Object.entries(required.prefill)) {
99
+ if (/token|secret|password|authorization|env|headers|body|key/i.test(key))
100
+ throw new Error("Credential setup accepts only non-secret metadata.");
101
+ if (typeof value === "string") {
102
+ required.assertCredentialFreeField({ value, field: key }, {});
103
+ if (required.containsSecret({ text: value }))
104
+ throw new Error("Credential setup accepts only non-secret metadata.");
105
+ }
106
+ }
107
+ return issueToolFailureDiagnostic({ diagnostic: { setupToolId: required.setupToolId, prefill: required.prefill, remedyToolId: required.setupToolId,
108
+ hint: "A missing or rejected credential may be fixed by saving it through the secure card." } }, {});
109
+ }
110
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1,92 @@
1
+ import type { ToolExecutor } from "../tool-executor.js";
2
+ export interface FederatedRefusalDiagnosisDeps {
3
+ readonly inner: ToolExecutor;
4
+ readonly isFederatedTool: (required: {
5
+ toolId: string;
6
+ }) => boolean;
7
+ readonly parseConnectionId: (required: {
8
+ toolId: string;
9
+ }) => string | null;
10
+ readonly findRefusal: (required: {
11
+ toolId: string;
12
+ }) => {
13
+ remoteName: string;
14
+ connectionId: string;
15
+ explanation: string;
16
+ } | null;
17
+ readonly newExecutionId: () => string;
18
+ readonly redactSecretShapes: (required: {
19
+ text: string;
20
+ }) => {
21
+ text: string;
22
+ };
23
+ readonly getBootStatus?: () => FederationBootStatus;
24
+ }
25
+ /**
26
+ * The federation boot pass's own live state, for the "still connecting" case below — deliberately
27
+ * NOT `FederationRuntime` itself (`external-mcp-federation-runtime.ts`): that type carries a whole
28
+ * runtime's worth of methods (`start`, `reload`, …) this decorator has no business calling, and
29
+ * importing it here would pull a composition-layer type into what is otherwise a pure reduction
30
+ * over data, same posture as `refusal-notice.ts`'s own `FederationAdmissionSnapshotEntry`. A caller
31
+ * builds this from its own `FederationRuntime` with `{ settled: federation.started, connectFailures:
32
+ * federation.connectFailures() }` — see `byok-tool-surface.ts`'s call site.
33
+ */
34
+ export interface FederationBootStatus {
35
+ /** Whether the boot pass (every configured connection, attempted once, in order) has finished.
36
+ * `false` for the window between "the surface started federation" and "every connection has
37
+ * either admitted or failed" — the exact gap `npx` startup (6-13s) can outlast this decorator's
38
+ * caller's own bounded wait (`assistant-byok.ts`'s `FEDERATION_TURN_WAIT_MS`, 5s). */
39
+ readonly settled: boolean;
40
+ /** `AttachFederatedToolsResult.connectFailures`, verbatim — a connectionId that reached `attach`
41
+ * but never admission at all, with its human-readable reason. */
42
+ readonly connectFailures: readonly {
43
+ readonly connectionId: string;
44
+ readonly reason: string;
45
+ }[];
46
+ /**
47
+ * The boot roster's connection ids (2026-09-25), known as soon as the runtime's own
48
+ * `resolveConnections()` call resolves — see `external-mcp-federation-runtime.ts`'s
49
+ * `configuredConnectionIds()`. `undefined` means the roster itself is not known yet (the earliest
50
+ * sliver of boot, before that call has even resolved); an omitted/`undefined` value is treated as
51
+ * "cannot rule anything out yet", never as "nothing is configured" — those are different claims,
52
+ * and only a present array can positively prove a connectionId was never configured at all. Without
53
+ * this field, `!status.settled` alone told a caller naming ANY federated-shaped id, including one
54
+ * that was never in the roster, "still connecting" for as long as boot took.
55
+ */
56
+ readonly configuredConnectionIds?: readonly string[];
57
+ }
58
+ /**
59
+ * Wraps `inner` so a call naming a federated tool id THIS BOOT REFUSED fails with a message naming
60
+ * the tool, the server, and the fix, instead of the bare `unknown tool` throw escaping all the way to
61
+ * `@jini-ai/http-kit`'s SEC-005 redaction.
62
+ *
63
+ * @param getSnapshot - Reads the CURRENT admission snapshot lazily, not a value closed over once —
64
+ * this decorator is composed in `agent-daemon-server.ts` before `attachFederatedMcpTools` resolves
65
+ * (mirroring that file's own `federationRefusalPrefix` `let` binding for the identical reason), so
66
+ * a plain array parameter captured at construction time would forever see the empty pre-boot
67
+ * snapshot. Pass `() => federationAdmissionReports` (a closure over the module-scope binding), not
68
+ * `() => someArrayCapturedNow`.
69
+ * @param getBootStatus - Optional (2026-09-24). When given, an unknown-tool throw for a federated id
70
+ * this boot's snapshot does NOT (yet) refuse is diagnosed one step further before falling back to
71
+ * the original throw: while `!settled`, the connection is presumed still connecting (`npx`
72
+ * startup, 6-13s, can outlast a caller's own bounded wait — see `assistant-byok.ts`'s
73
+ * `FEDERATION_TURN_WAIT_MS`) and the call gets a clear "still connecting" message instead of the
74
+ * raw `ToolExecutor: unknown tool` throw a model cannot act on. Once `settled`, a connectionId
75
+ * present in `connectFailures` gets that failure's own reason instead. Omitted (the daemon's own
76
+ * call site, where a turn never runs until `federation.start()` has fully resolved — see
77
+ * `agent-daemon-server.ts`'s `start()`), this decorator's behavior is byte-identical to before this
78
+ * parameter existed: every one of this file's own pre-2026-09-24 tests passes an unchanged 2-arg
79
+ * call and must keep throwing for a federated-shaped id this boot never refused.
80
+ * @returns A drop-in `ToolExecutor`. Every call that is not BOTH (a) an unknown-tool throw AND (b) a
81
+ * federated id this boot's snapshot actually refused passes through with byte-identical behavior —
82
+ * completed results, every other status, and the throw itself for a genuinely unknown or
83
+ * native-typo id — so `resumeConfirmation`/`cancel`/`getAuditRecord` delegate straight through with
84
+ * no wrapping at all.
85
+ * @complexity One extra `instanceof`/regex test and one string-prefix test per call that reaches
86
+ * `catch` — already the unhappy path; {@link findFederatedToolRefusal}'s own O(connections ×
87
+ * refusals) cost only when both tests pass, and `getBootStatus`'s own O(connectFailures) lookup
88
+ * only when they pass AND no refusal was found.
89
+ * @overallScore 100
90
+ */
91
+ export declare function withFederatedRefusalDiagnosis({ inner, ...deps }: FederatedRefusalDiagnosisDeps, _optional?: {}): ToolExecutor;
92
+ //# sourceMappingURL=federated-refusal.d.ts.map
@@ -0,0 +1,152 @@
1
+ // Local federation forks moved to @jini-ai/mcp/federation (+ /stdio, /approvals); see development/DELETED-CODE.md.
2
+ /**
3
+ * `ToolExecutor.execute`'s own contract for an unregistered id (`@jini-ai/daemon`'s
4
+ * `tool-executor.ts`): a thrown `Error` reading `unknown tool "<id>"`, with no code or subclass to
5
+ * match on instead. Duplicated from `tool-executor-audit.ts`'s identical `isUnknownToolError` rather
6
+ * than imported: that file is a sibling one layer up with no dependency on `mcp-federation`, and
7
+ * importing across that line for an 18-character regex is not worth the coupling. Both copies carry
8
+ * the same MEDIUM message-text-coupling note that file's own doc records — if `@jini-ai/daemon`
9
+ * rewords this message, this decorator silently stops matching and every call it would have
10
+ * diagnosed falls back to today's opaque redaction, never a worse or incorrect outcome.
11
+ *
12
+ * @complexity O(n) in the message length.
13
+ * @overallScore 90
14
+ */
15
+ function isUnknownToolError(error) {
16
+ return error instanceof Error && /unknown tool/i.test(error.message);
17
+ }
18
+ /**
19
+ * The "still connecting" / "failed to connect" diagnosis (2026-09-24) — reached only for a federated
20
+ * id that {@link findFederatedToolRefusal} could NOT explain, i.e. one whose connection has no
21
+ * admission report at all yet (never refused because it was never even admitted).
22
+ *
23
+ * Two distinct outcomes, both reached only via `status.connectFailures`/`!status.settled` and never
24
+ * both — `attachFederatedMcpTools`'s per-connection loop is sequential and produces at most one of
25
+ * "still running" or "failed" for a given connectionId at any moment this decorator could observe it:
26
+ *
27
+ * - `!status.settled`: the boot pass has not finished walking every configured connection, so THIS
28
+ * connectionId is presumed still connecting (npx startup, 6-13s, routinely outlasts a caller's own
29
+ * bounded wait) — UNLESS `status.configuredConnectionIds` is already known and does not contain it,
30
+ * in which case it was never configured at all and the original throw stands (2026-09-25; before
31
+ * this check, a hallucinated/stale id got "still connecting" for as long as boot took, same as a
32
+ * real pending one). A model naming an id that IS in the roster has, by doing so, already decided it
33
+ * wants it — telling it to retry is strictly better than the opaque `unknown tool` throw it would
34
+ * get instead, even in the rare case this particular connection actually already failed while a
35
+ * LATER one in the loop is still connecting; the very next call after boot settles gets the precise
36
+ * failure reason below instead.
37
+ * - `status.settled` and this connectionId is in `connectFailures`: the boot pass finished and this
38
+ * connection genuinely never admitted — its own human-readable reason (`bootstrap.ts`'s
39
+ * `messageOf(error)`, already proven secret-free by that field's own doc) is relayed verbatim.
40
+ *
41
+ * `status` is `undefined` for a caller that never passed `getBootStatus` at all (the daemon's own
42
+ * call site) or for a toolId that is not federated-shaped — both return `null` immediately, letting
43
+ * the original `unknown tool` throw stand exactly as it did before this diagnosis existed.
44
+ *
45
+ * @complexity O(1) for the connecting case; O(connectFailures) for the settled case.
46
+ * @overallScore 100
47
+ */
48
+ function diagnoseStillConnectingOrFailed(toolId, status, deps) {
49
+ if (!status)
50
+ return null;
51
+ const connectionId = deps.parseConnectionId({ toolId });
52
+ if (connectionId === null)
53
+ return null;
54
+ if (!status.settled) {
55
+ // Only a positively-known roster can rule `connectionId` OUT; `undefined` (roster not resolved
56
+ // yet) keeps the pre-2026-09-25 behavior of presuming still-connecting rather than guessing wrong
57
+ // in either direction.
58
+ if (status.configuredConnectionIds && !status.configuredConnectionIds.includes(connectionId))
59
+ return null;
60
+ return failedResult(`External MCP server '${connectionId}' is still connecting — try again in a moment.`, deps);
61
+ }
62
+ const failure = status.connectFailures.find((entry) => entry.connectionId === connectionId);
63
+ if (!failure)
64
+ return null;
65
+ // `reason` is already proven secret-free by `AttachFederatedToolsResult.connectFailures`'s own
66
+ // doc, but redacted anyway for the same defense-in-depth reason the refusal branch below redacts
67
+ // an operator-authored/fixed-prose string that "never matches a redaction rule" today — a
68
+ // guarantee this file has no way to keep verifying stays true upstream.
69
+ return failedResult(deps.redactSecretShapes({ text: `External MCP server '${connectionId}' failed to connect: ${failure.reason}` }).text, deps);
70
+ }
71
+ /** One `status: 'failed'`, `errorKind: 'validation'` result — the shape both diagnosis branches
72
+ * above return, factored out purely to keep {@link diagnoseStillConnectingOrFailed} and the main
73
+ * refused-tool branch from hand-building the same four-field object twice. See this file's header
74
+ * ("Why the result is `status: 'failed', errorKind: 'validation'`") for why this exact shape is what
75
+ * reaches the model un-redacted-by-the-stack rather than a thrown error. */
76
+ function failedResult(message, deps) {
77
+ return { executionId: deps.newExecutionId(), status: "failed", errorKind: "validation", error: message };
78
+ }
79
+ /**
80
+ * Wraps `inner` so a call naming a federated tool id THIS BOOT REFUSED fails with a message naming
81
+ * the tool, the server, and the fix, instead of the bare `unknown tool` throw escaping all the way to
82
+ * `@jini-ai/http-kit`'s SEC-005 redaction.
83
+ *
84
+ * @param getSnapshot - Reads the CURRENT admission snapshot lazily, not a value closed over once —
85
+ * this decorator is composed in `agent-daemon-server.ts` before `attachFederatedMcpTools` resolves
86
+ * (mirroring that file's own `federationRefusalPrefix` `let` binding for the identical reason), so
87
+ * a plain array parameter captured at construction time would forever see the empty pre-boot
88
+ * snapshot. Pass `() => federationAdmissionReports` (a closure over the module-scope binding), not
89
+ * `() => someArrayCapturedNow`.
90
+ * @param getBootStatus - Optional (2026-09-24). When given, an unknown-tool throw for a federated id
91
+ * this boot's snapshot does NOT (yet) refuse is diagnosed one step further before falling back to
92
+ * the original throw: while `!settled`, the connection is presumed still connecting (`npx`
93
+ * startup, 6-13s, can outlast a caller's own bounded wait — see `assistant-byok.ts`'s
94
+ * `FEDERATION_TURN_WAIT_MS`) and the call gets a clear "still connecting" message instead of the
95
+ * raw `ToolExecutor: unknown tool` throw a model cannot act on. Once `settled`, a connectionId
96
+ * present in `connectFailures` gets that failure's own reason instead. Omitted (the daemon's own
97
+ * call site, where a turn never runs until `federation.start()` has fully resolved — see
98
+ * `agent-daemon-server.ts`'s `start()`), this decorator's behavior is byte-identical to before this
99
+ * parameter existed: every one of this file's own pre-2026-09-24 tests passes an unchanged 2-arg
100
+ * call and must keep throwing for a federated-shaped id this boot never refused.
101
+ * @returns A drop-in `ToolExecutor`. Every call that is not BOTH (a) an unknown-tool throw AND (b) a
102
+ * federated id this boot's snapshot actually refused passes through with byte-identical behavior —
103
+ * completed results, every other status, and the throw itself for a genuinely unknown or
104
+ * native-typo id — so `resumeConfirmation`/`cancel`/`getAuditRecord` delegate straight through with
105
+ * no wrapping at all.
106
+ * @complexity One extra `instanceof`/regex test and one string-prefix test per call that reaches
107
+ * `catch` — already the unhappy path; {@link findFederatedToolRefusal}'s own O(connections ×
108
+ * refusals) cost only when both tests pass, and `getBootStatus`'s own O(connectFailures) lookup
109
+ * only when they pass AND no refusal was found.
110
+ * @overallScore 100
111
+ */
112
+ export function withFederatedRefusalDiagnosis({ inner, ...deps }, _optional = {}) {
113
+ return {
114
+ execute: async ({ principal, run, toolId, input }, { signal, emitSurface } = {}) => {
115
+ try {
116
+ return await inner.execute({ principal, run, toolId, input }, { ...(signal === undefined ? {} : { signal }), ...(emitSurface === undefined ? {} : { emitSurface }) });
117
+ }
118
+ catch (error) {
119
+ if (!isUnknownToolError(error) || !deps.isFederatedTool({ toolId }))
120
+ throw error;
121
+ const refusal = deps.findRefusal({ toolId });
122
+ if (refusal === null) {
123
+ const stillConnecting = diagnoseStillConnectingOrFailed(toolId, deps.getBootStatus?.(), { inner, ...deps });
124
+ if (stillConnecting !== null)
125
+ return stillConnecting;
126
+ throw error;
127
+ }
128
+ return {
129
+ executionId: deps.newExecutionId(),
130
+ status: "failed",
131
+ errorKind: "validation",
132
+ // Redacted like every other `errorKind: 'validation'` failure (`tool-failure-redaction.ts`'s
133
+ // own validation branch) even though THIS layer sits outside `withRedactedToolFailures` in
134
+ // `agent-daemon-server.ts`'s composition and so would otherwise never pass back through it —
135
+ // see this file's header for why it is composed outermost. `remoteName` is the one
136
+ // upstream-influenced piece of this string: it is the external server's OWN advertised tool
137
+ // name, sanitized by `safeRemoteName` only against injection characters, never against
138
+ // secret shapes, so a remote naming a tool e.g. `sk-ant-api03-...` would otherwise leak it
139
+ // verbatim. `connectionId` and `explanation` are operator-authored / fixed prose and never
140
+ // match a redaction rule, so this is a no-op for them.
141
+ error: deps.redactSecretShapes({
142
+ text: `tool "${refusal.remoteName}" on external server "${refusal.connectionId}" was refused: ${refusal.explanation}`,
143
+ }).text,
144
+ };
145
+ }
146
+ },
147
+ resumeConfirmation: (args) => inner.resumeConfirmation(args),
148
+ cancel: (executionId) => inner.cancel(executionId),
149
+ getAuditRecord: (executionId) => inner.getAuditRecord(executionId),
150
+ };
151
+ }
152
+ //# sourceMappingURL=federated-refusal.js.map
@@ -0,0 +1,6 @@
1
+ export * from "./diagnostics.js";
2
+ export * from "./recovery.js";
3
+ export * from "./redaction.js";
4
+ export * from "./stack.js";
5
+ export * from "./federated-refusal.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from "./diagnostics.js";
2
+ export * from "./recovery.js";
3
+ export * from "./redaction.js";
4
+ export * from "./stack.js";
5
+ export * from "./federated-refusal.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,49 @@
1
+ import type { ToolRegistry } from "@jini-ai/core";
2
+ import type { ToolExecutor } from "../tool-executor.js";
3
+ import { type SurfaceExchangeStore } from "../surface-exchanges.js";
4
+ import { type ReadOnlyToolMessages } from "../core/read-only-tools.js";
5
+ export interface ToolFailureRecoveryDeps {
6
+ /** The SAME instance `agent-daemon-server.ts` mounts `registerMcpUiToolCallsRoute` with — a
7
+ * different one would leave this loop's own recovery surface unreachable, exactly the failure mode
8
+ * `@jini-ai/daemon/surface-exchanges`'s own `AssistantSurfaceDeps` doc warns about for every other
9
+ * consumer of this store. */
10
+ readonly surfaceExchanges: SurfaceExchangeStore;
11
+ /** Read-only: this loop only ever calls {@link ToolRegistry.list} to look up a remedy tool's own
12
+ * published `inputSchema` — never a handler, and never anything requiring authorization to read. */
13
+ readonly registry: Pick<ToolRegistry, "list">;
14
+ readonly recoveryToolId: string;
15
+ readonly containsSecret: (required: {
16
+ text: string;
17
+ }) => boolean;
18
+ readonly assertCredentialFreeField: (required: {
19
+ value: string;
20
+ field: string;
21
+ }, optional: Record<string, never>) => void;
22
+ readonly readOnlyMessages: ReadOnlyToolMessages;
23
+ readonly formatReadOnlyRemedyRefusal: (required: {
24
+ refusal: string;
25
+ }) => string;
26
+ /** Rendering is host-owned: daemon cannot depend on browser/UI packages. */
27
+ readonly buildRecoveryForm: (required: {
28
+ exchangeId: string;
29
+ hint: string;
30
+ remedyToolId: string;
31
+ askFor?: {
32
+ key: string;
33
+ prompt: string;
34
+ };
35
+ }, optional?: Record<string, never>) => unknown;
36
+ }
37
+ /**
38
+ * Wraps `inner` with the generic ask -> apply -> retry-once recovery loop described in this file's
39
+ * header. A drop-in `ToolExecutor` — every method other than `execute` delegates straight through,
40
+ * matching `tool-executor-audit.ts`'s own `withToolAttemptAudit` shape.
41
+ *
42
+ * @complexity The no-hint path (the overwhelming majority) is one `inner.execute` call plus one bounded
43
+ * output scan — O(1) beyond `inner`'s own cost. The recovery path adds at most two further
44
+ * `inner.execute` calls (the remedy, the retry) and one surface round trip.
45
+ */
46
+ export declare function withToolFailureRecovery({ inner, ...deps }: ToolFailureRecoveryDeps & {
47
+ inner: ToolExecutor;
48
+ }, _optional?: {}): ToolExecutor;
49
+ //# sourceMappingURL=recovery.d.ts.map