@jini-ai/daemon 0.5.3 → 0.5.4
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.
- package/CHANGELOG.md +4 -0
- package/dist/agent-executor/contracts.d.ts +12 -10
- package/dist/agent-executor/contracts.js +0 -9
- package/dist/agent-executor/event-translation.d.ts +2 -2
- package/dist/agent-executor/event-translation.js +0 -2
- package/dist/agent-executor/events.d.ts +9 -4
- package/dist/agent-executor/events.js +21 -5
- package/dist/agent-executor/handoff.d.ts +1 -1
- package/dist/agent-executor/handoff.js +0 -1
- package/dist/agent-executor/launch.d.ts +1 -1
- package/dist/agent-executor/launch.js +22 -1
- package/dist/agent-executor/mcp-bridge.d.ts +2 -2
- package/dist/agent-executor/mcp-bridge.js +0 -2
- package/dist/agent-executor/model-receipts.d.ts +6 -0
- package/dist/agent-executor/model-receipts.js +26 -0
- package/dist/agent-executor.d.ts +7 -1665
- package/dist/agent-executor.js +7 -3007
- package/dist/core/read-only-tools.d.ts +130 -0
- package/dist/core/read-only-tools.js +115 -0
- package/dist/core/tool-audit.d.ts +131 -0
- package/dist/core/tool-audit.js +113 -0
- package/dist/http/active-context.d.ts +2 -2
- package/dist/http/agents.d.ts +8 -2
- package/dist/http/agents.js +6 -2
- package/dist/http/component-catalog.d.ts +2 -2
- package/dist/http/connectors.d.ts +17 -17
- package/dist/http/daemon-status.d.ts +2 -2
- package/dist/http/db-ops.d.ts +3 -3
- package/dist/http/delegated-tools.d.ts +1 -1
- package/dist/http/delegated-tools.js +1 -1
- package/dist/http/frontend-sessions.d.ts +1 -1
- package/dist/http/health.d.ts +6 -6
- package/dist/http/host-tools.d.ts +2 -2
- package/dist/http/media.d.ts +4 -4
- package/dist/http/memory.d.ts +15 -15
- package/dist/http/remote-run-events.d.ts +2 -2
- package/dist/http/research.d.ts +1 -1
- package/dist/http/routines.d.ts +7 -7
- package/dist/http/run-credentials.d.ts +2 -2
- package/dist/http/run-credentials.js +2 -2
- package/dist/http/runs.d.ts +4 -4
- package/dist/http/terminals.d.ts +6 -6
- package/dist/http/tool-catalog.d.ts +2 -2
- package/dist/http/xai.d.ts +6 -6
- package/dist/index.d.ts +75 -571
- package/dist/index.js +75 -571
- package/dist/read-only-tools.d.ts +1 -129
- package/dist/read-only-tools.js +2 -114
- package/dist/run-credentials.d.ts +47 -23
- package/dist/run-credentials.js +24 -8
- package/dist/scheduler.d.ts +5 -0
- package/dist/scheduler.js +13 -1
- package/dist/session-coordination.d.ts +58 -24
- package/dist/session-coordination.js +40 -12
- package/dist/store/event-log/sqlite.js +3 -3
- package/dist/surface-exchanges.d.ts +207 -61
- package/dist/surface-exchanges.js +167 -20
- package/dist/tool-audit.d.ts +1 -113
- package/dist/tool-audit.js +2 -94
- package/dist/tool-recovery/diagnostics.d.ts +132 -0
- package/dist/tool-recovery/diagnostics.js +110 -0
- package/dist/tool-recovery/federated-refusal.d.ts +92 -0
- package/dist/tool-recovery/federated-refusal.js +152 -0
- package/dist/tool-recovery/index.d.ts +6 -0
- package/dist/tool-recovery/index.js +6 -0
- package/dist/tool-recovery/recovery.d.ts +49 -0
- package/dist/tool-recovery/recovery.js +419 -0
- package/dist/tool-recovery/redaction.d.ts +166 -0
- package/dist/tool-recovery/redaction.js +192 -0
- package/dist/tool-recovery/stack.d.ts +69 -0
- package/dist/tool-recovery/stack.js +62 -0
- 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,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
|